diff --git a/plugins/brains/.claude-plugin/plugin.json b/plugins/brains/.claude-plugin/plugin.json index a3ef14d..031fcbd 100644 --- a/plugins/brains/.claude-plugin/plugin.json +++ b/plugins/brains/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "brains", "description": "Your memory layer: Gmail, Calendar, Drive, and prior Claude conversations as queryable pages, with reflexive recall, hook-driven turn-by-turn capture and inbox delivery, and boards/automations/workflows on top.", - "version": "2.7.1", + "version": "2.8.0", "author": { "name": "brains (ssvlabs)" }, diff --git a/plugins/brains/.codex-plugin/plugin.json b/plugins/brains/.codex-plugin/plugin.json index fe6dfdf..9096243 100644 --- a/plugins/brains/.codex-plugin/plugin.json +++ b/plugins/brains/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "brains", - "version": "2.7.1", + "version": "2.8.0", "description": "Your personal memory layer for Codex: query Gmail, Calendar, Drive, and prior conversations, then build boards, automations, and workflows.", "author": { "name": "brains (ssvlabs)", diff --git a/plugins/brains/generated/capability-catalog.json b/plugins/brains/generated/capability-catalog.json index fc9f966..4171ad4 100644 --- a/plugins/brains/generated/capability-catalog.json +++ b/plugins/brains/generated/capability-catalog.json @@ -7,16 +7,34 @@ "artifacts": [ { "capability_id": "integration-actions", - "catalog_sha256": "c46b9cf2f88ed3cefe0f5766665c12f06dafaa0e56dba1f5586ab9cd56c39ac2", + "catalog_sha256": "29f58ddc5dab294814934fe7af0d808f4b7dc441e3c3d29bb0e67fa705d7add4", "artifact_path": "plugins/brains/skills/brains-write/SKILL.md", - "artifact_sha256": "f14839e4ed852771fc8dd91b0196f9caeeb63ce56dead747a53d83fd83dd9475" + "artifact_sha256": "b27acd2ffc1ba4914890a88dba00e71f7052bd738369f28f9d86a1566bd2f548" }, { "capability_id": "brains-features", - "catalog_sha256": "03c153311f597a651c9719dc17229e6677b4c2b2823458b0b50c12617146a5bd", + "catalog_sha256": "6086cbe9877b2005b3cea06611ba74ab7b527eaed04af39e5a815c94c8806b37", "artifact_path": "plugins/brains/skills/brains-build/SKILL.md", - "artifact_sha256": "8e377f866efdbf6653cda64fcc44cef6a8038e1d74dfd2311af4a0ded434c237" + "artifact_sha256": "753afa4d8b4833b91622fa8c55d2bc76304e9a9170faed3365c7febd53836ae0" + }, + { + "capability_id": "procedure:board", + "catalog_sha256": "d9242158554683b1b18ac2f26ebfecf6c0d42c00e480524be2e6ee35df1fb879", + "artifact_path": "plugins/brains/skills/brains-board/SKILL.md", + "artifact_sha256": "bfc28da5ef20ec559fe10bd9b21d2d7ca673de5533f012bd9cfaa3947241cf6f" + }, + { + "capability_id": "procedure:automation", + "catalog_sha256": "ea1e883788e138e39b80627e5a14d6ac35dc7f658b4628e7e32084bb6620fe8b", + "artifact_path": "plugins/brains/skills/brains-automation/SKILL.md", + "artifact_sha256": "e71b5648d93e611de49fd81549217db8232427262e282af6d1c1d70c9c2e17c9" + }, + { + "capability_id": "procedure:workflow", + "catalog_sha256": "ec1f5989adae2630f16879299f05cc79c6aabfd9897c968c69618882b35f1278", + "artifact_path": "plugins/brains/skills/brains-workflow/SKILL.md", + "artifact_sha256": "b22814fd630ed78964d54440ec327e3aebcde8c812aaa9985fff9c4d345d3f09" } ], - "source_commit": "9888bc944d0a3e5d2c178eaaf8b23811364457d9" + "source_commit": "864c45fa7e1c4b4d60116b6697abc596a272fbff" } diff --git a/plugins/brains/skills/brains-automation/SKILL.md b/plugins/brains/skills/brains-automation/SKILL.md new file mode 100644 index 0000000..c536dbf --- /dev/null +++ b/plugins/brains/skills/brains-automation/SKILL.md @@ -0,0 +1,623 @@ +--- +name: "brains-automation" +description: "Scaffold a new sandboxed agent — a script on a trigger with scoped grants, caps and a smoke test. Use when the user wants something to happen without them: \"automate this\", \"every morning do…\", \"run this nightly\", \"auto-draft when…\", \"when an email from X arrives…\". Not for editing an agent they already own." +--- + + + +# Create-agent flow — run this with the user + +You're guiding the user through scaffolding a sandboxed agent: a Deno-runtime user-authored script that fires on a trigger, gets a scoped MCP token, and runs in the background. Your job is to translate the user's "I want X to happen automatically" into a clean source + trigger + grants + caps without making them think about runtime details — and to bake in the patterns (idempotency, error handling, page-type guards) that hand-written agents forget. + +## Flow state — one line, every turn + +End EVERY message in this flow with a checkpoint (last line, above any chips), so a later turn +can pick up where you left off: + +`FLOW automation · step 1/13 purpose · done: (none) · facts: (none) · next: the user's answer to the question above` + +Steps in order: `purpose`, `trigger`, `reads`, `writes`, `conflict_check`, `grants`, `egress_check`, `secrets`, `caps`, `confirm`, `save`, `smoke_test`, `handoff`. Write `(none)` in a segment that has nothing yet. +When the flow is finished emit `FLOW automation · done` instead, or +`FLOW automation · cancelled` if the user drops it — without that, +the flow looks live forever. + +`done:` records what you have already CREATED, with the id the call returned — `automation_id=…`. Write it there the moment the call comes back. It is the only record that survives: a confirmation means the user said yes, not that the write happened. + +`facts:` carries the decisions you cannot re-read from the conversation — `trigger`, `dedupe_key`, `hosts_approved`, `idem`. Keep each one short. + +**Picking up a flow already in progress:** + +1. **Before creating anything, check whether you already did.** If the last checkpoint names an + effect for the step you are about to run, that artifact exists — look it up by its id, tell the + user, and continue from the next step. Never create a second one. +2. Checkpoint present and readable → carry on from the step it names. +3. Checkpoint garbled → re-emit a corrected one from what you can see, then carry on. +4. No checkpoint but you still have this procedure → work out where you are from the + conversation, say in one line where you are resuming, and confirm before any write. +5. No checkpoint and no procedure → do not improvise the steps; the gates in here are not + optional. The one exception: if the user asks to pick a flow back up and you no longer have its procedure in front of you, reload it once — then continue from the last checkpoint rather than starting over. Never re-fetch one you can still see. +6. Nothing recoverable → start over, but collect everything in ONE question instead of walking + the whole interview again. + +## Hard rules + +- **Do not re-call `create_automation_flow` in this conversation.** Once you have this playbook in context, refer to the copy you already have — re-fetching it wastes tokens, resets your state in the user's eyes, and produces a worse experience. The flow is one playbook per session, not one per step. The one exception: if the user asks to pick a flow back up and you no longer have its procedure in front of you, reload it once — then continue from the last checkpoint rather than starting over. Never re-fetch one you can still see. +- **One question per turn.** Never ask multiple questions in a single message. +- **Propose, don't interrogate.** After Steps 1–4 you draft the full source from a template (see Skeletons below) and show it for review. Don't ask "what tool grants?" or "how should it dedupe?" — that's your job once you know the trigger and write target. +- **The dedupe-key question is mandatory at Step 4.** If the agent appends to a shared collection (`board.updates`, page timeline, etc.) you MUST ask for or infer the dedupe key (`postId` = page slug for emails, `event_id` for calendar events, etc.) and the skeleton MUST include a "skip if already present" check. Do not draft source without it. +- **`http_fetch` is a high-risk grant; explicit user approval is mandatory at Step 5.5.** Granting `http_fetch` lets the agent send any data the script reads from the brain (emails, board rows, calendar, search results) to any host on the runtime allowlist. The Deno sandbox does NOT block exfiltration — the egress happens from the MCP server in the stage VPC, not from the sandbox, so the host allowlist is the only gate. If `http_fetch` ends up in the proposed grants, you MUST run Step 5.5 verbatim and wait for an explicit "yes" before `save_automation_draft`. Do not soften the wording, do not batch it with other approvals, do not skip it because the hosts "look fine" to you. +- **Never ask the user to paste a secret value into the conversation.** If the source references `{{secret_name}}`, run Step 5.7: names in chat, values in the vault. A missing value never blocks the flow — save + smoke test proceed and fail cleanly until the user stores it. +- **Use the same MCP namespace that returned this playbook.** If you called this as `mcp__brains__create_automation_flow`, every downstream call (`save_automation_draft`, `get_board`, `search`, etc.) uses `mcp__brains__*`. If you called it under a different namespace, use that one throughout. **Don't mix namespaces.** +- **Narrate one short line between steps** so the user knows what just happened and what's next. + +## The sandbox authoring contract — read this BEFORE drafting source + +This is the **complete** surface a sandboxed automation can call. Do NOT guess at it, do NOT probe by trial-and-error, and do NOT assume a capability is missing because you can't see it — it's all listed here. Inside the script, everything hangs off the injected `brains` global (Style A) or `ctx.brains` (Style B). + +**Reads (brain pages & boards):** +- `brains.search({text, type?, limit?})`, `brains.query({text, type?, limit?})`, `brains.discover({text, limit?})` (semantic board/mini-site hits), `brains.discoverRows({text, limit?})` (semantic board-ROW hits), `brains.list_pages({type?, since?, until?, limit?})`, `brains.get_page(slug)` (returns `{...page, markdown, body}`), `brains.list_calendar_events({start, end, limit?})`. **`discover` and `discoverRows` both call the `query` tool — they need the `query` grant, not `search`.** +- Boards: `brains.boards.get(id, {dataset, offset, limit})` (rows are `[]` unless you pass `dataset`), `brains.boards.list()`, `brains.boards.append(id, rows)`, `brains.boards.update_row(id, row_id, patch)` (**patch ONLY the fields you are changing** — see gotchas), `brains.boards.delete(id, row_id)` (SOFT delete — the row keeps its id and is undoable), `brains.boards.restore(id, row_id)`, `brains.boards.list_skills(id)`, `brains.boards.run_skill(id, name, inputs)`. Flat aliases exist (`brains.get_board`, `brains.append_board_rows`, `brains.update_board_row`, …). + +**Integrations — there are TWO distinct paths. Pick by integration class (confirm with `list_integrations`, see below):** + +| Integration class | Reachable HOW from the sandbox | Grant to add | +|---|---|---| +| **gmail / calendar / drive** (and other codex installs with declared actions) | **Writes:** `brains.act({install_id, action_name, input})` — resolve the action first, pass all 3 fields together, and follow the generated action contract below. **Reads/refresh:** `brains.fetch({source, input})` (a.k.a. `fetch_from_integration`). | `act_on_integration` and/or `fetch_from_integration` | +| **adapter integrations — github, monday, …** | `brains.call("adapter_query", {kind, op, ref?, params?})` — ad-hoc mode against the user's stored OAuth token. E.g. `brains.call("adapter_query", {kind:"github", op:"list_open_prs", ref:{owner:"ssvlabs", name:"brains"}})` or `{kind:"monday", op:"list_boards"}`. There is **no** `brains.adapter_query()` helper — go through `brains.call`. These are NOT reachable via `act_on_integration` or `fetch_from_integration`. | `adapter_query` | +| **telegram** | `brains.telegram_push({text})` — delivers immediately, no draft. | `telegram_push` | +| **write a memory page** | `brains.create_page({slug, title, body})` — idempotent on (brain_id, slug); overwrites on re-run. Use type `'derived'` or any custom label (reserved types like `email` are rejected). | `create_page` | + +**Other sandbox capabilities:** +- **Per-automation K/V** (cursors, watermarks, last-seen ids): `brains.kv.get(k)` / `set(k,v)` / `list()` / `delete(k)`. **No grant needed** — it's auto-scoped to this automation. +- **Secrets vault:** reference `{{secret_name}}` inside `http` `url`/`headers`/`query` values — the MCP server substitutes the decrypted value before egress. `brains.call("automation_secret_list")` reads the key names (never values). Grant `automation_secret_list` to enumerate keys; the substitution itself rides on the `http_fetch` grant. Keys are set out-of-band, and the sandbox cannot write secrets, only reference them. During authoring you work with secret NAMES only — **never solicit the value in chat**; run Step 5.7 for the name-confirm / existence-check / missing-value handling. +- **LLM from the sandbox:** `brains.llm({...})` (grant `automation_complete`). **If you feed it bytes fetched from outside brains** (`brains.http`, a third-party API), wrap them first with `brains.wrapUntrusted(text)` / `brains.untrustedClause()` — the prompt-injection guard. Any flow that grants both `http_fetch` and `automation_complete` must do this. +- **Owner identity / timezone:** `brains.whoami()` (grant `whoami`) — the supported way to get the owner's tz, which you need whenever a cron is expressed in local time. `brains.user` / `brains.profile` are injected directly (no grant, no call). +- **Self-diagnosis:** `brains.automation_recent_failures({limit?})` (grant `automation_recent_failures`) — this automation's recent failed runs. +- **Outbound HTTP:** `brains.http({url, method?, headers?, query?, timeout_ms?})` (grant `http_fetch` — see Step 5.5; egress is host-pinned). **Request body — name the field explicitly, never leave it implied:** `body_json` (any JSON value; sends it as JSON and sets Content-Type for you), `body_text` (a string, sent verbatim — set your own Content-Type), or `body` (fetch-style alias: a string goes verbatim, anything else is serialized as JSON). Precedence is `body_json`, then `body_text`, then `body`. **Binary must be base64-encoded into a string first** — a `Uint8Array`/`Blob`/`FormData`/`URLSearchParams` cannot cross to the server and is rejected. `GET`/`DELETE` never send a body. Note `{{secret}}` substitution does **not** apply to the body (url/headers/query only), so a body cannot reference the vault. Max request body 200 KB. +- **Escape hatch:** `brains.call("", {...})` invokes any granted MCP tool by bare name. This is how `adapter_query` (and any tool with no dedicated helper) is reached. It is **still grant-gated** — the bare tool name must be in `tool_grants`. + +**Grant rule (load-bearing):** every tool the source calls must be listed in `tool_grants` by its **bare MCP tool name** (`get_page`, `adapter_query`, `act_on_integration`, …). A missing grant fails at runtime with `tool "X" not in automation grants`. The **only** exception is the `automation_state_*` family (no grant required). Reaching a tool through `brains.call(...)` does **not** bypass the allowlist. + +**Bad grants are refused at SAVE time, not at runtime.** `save_automation_draft` / `update_automation` reject the whole call if a grant is an unknown tool name (typo), is bot-only, requires admin, or requires a human-only account capability — the error names the offending grant. So a typo'd grant costs you a rejected save, not a mysterious failed run. Note `get_insights` is admin-only and `get_perf_insights` requires a human performance entitlement; neither can **ever** be granted to an automation. + +## Step 1 — Purpose + +If a non-empty `purpose_hint` was passed in, skip the cold-ask and confirm it back: *"So you want an agent that — got it."* Then go to Step 2. + +Otherwise ask, verbatim: + +> "What should this agent do? One sentence — e.g. *'log every email from a CRM lead onto their company in the Ethera CRM'*, *'every weekday at 9am summarize my unread emails'*, *'when a candidate row's status changes to offer, draft an offer email'*." + +Wait for the answer. + +## Step 2 — Trigger + +Pick exactly one. Ask, verbatim: + +> "When should this fire? +> - **On every new ** — email arriving, calendar event ingested, doc landing in Drive, a new chat session ending. (`page_ingested`, with a type filter) +> - **On a schedule** — every 15 min / hourly / daily at