From 5c509e6a145ff665b658ab2250474be624e45b2a Mon Sep 17 00:00:00 2001 From: olegshmuelov Date: Wed, 12 Aug 2026 14:38:32 +0300 Subject: [PATCH 1/2] fix(brains): sync the generated capability catalog and bump to 2.9.1 [BRNS-MCP-052][BRNS-CORE-061] The five published skill artifacts and their digest manifest are generated in ssvlabs/brains and are not editable here. This regenerates them from the catalog and moves both client manifests together. Three rendered skills change. brains-write gains two rules in the action contract: get the user's go-ahead for a write before calling, rather than calling to find out whether it drafts; and a second cap, since direct execution is limited to 10/user/60s across every install and past either cap a direct execution degrades to a draft. brains-automation corrects its dry-run guidance. A verify-mode smoke test suppresses act sends and telegram_push, but board and page writes run live, so a smoke test mutates real rows. The previous wording implied every side effect was muted. brains-build admits a recipe-install row, names create_integration_flow as the integration-authoring entry point, and drops create_share_circle from the sharing row, which sat under "give someone access" and grants access to nothing. Patch rather than minor: every tool these rows name is already published elsewhere in the skill set, so no new capability is advertised. tests/plugin-contract/run.ts pins the action contract verbatim and moves with the rendered bytes. That pin forces a human read of the text before it ships; the digest checks, not the pin, are what verify provenance. --- plugins/brains/.claude-plugin/plugin.json | 2 +- plugins/brains/.codex-plugin/plugin.json | 2 +- plugins/brains/generated/capability-catalog.json | 12 ++++++------ plugins/brains/skills/brains-automation/SKILL.md | 8 ++++---- plugins/brains/skills/brains-build/SKILL.md | 7 ++++--- plugins/brains/skills/brains-write/SKILL.md | 6 ++++-- tests/plugin-contract/run.ts | 6 ++++-- 7 files changed, 24 insertions(+), 19 deletions(-) diff --git a/plugins/brains/.claude-plugin/plugin.json b/plugins/brains/.claude-plugin/plugin.json index 70b13ff..3f0dca1 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.9.0", + "version": "2.9.1", "author": { "name": "brains (ssvlabs)" }, diff --git a/plugins/brains/.codex-plugin/plugin.json b/plugins/brains/.codex-plugin/plugin.json index 52ab452..51b3452 100644 --- a/plugins/brains/.codex-plugin/plugin.json +++ b/plugins/brains/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "brains", - "version": "2.9.0", + "version": "2.9.1", "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 5d698d7..c8a6de4 100644 --- a/plugins/brains/generated/capability-catalog.json +++ b/plugins/brains/generated/capability-catalog.json @@ -7,15 +7,15 @@ "artifacts": [ { "capability_id": "integration-actions", - "catalog_sha256": "29f58ddc5dab294814934fe7af0d808f4b7dc441e3c3d29bb0e67fa705d7add4", + "catalog_sha256": "f93159bc3af5e891ae4b31c82178535242783c5419a07cd76b5aa3598ff299a7", "artifact_path": "plugins/brains/skills/brains-write/SKILL.md", - "artifact_sha256": "21f7cd40bedb7602efbbd43f718c1e5e78e41a19de3d96f3dd599b95fb17732d" + "artifact_sha256": "c289803f96c35dcfd5347e46177c009850dfdc4b42579a770ea6516d0c60e2af" }, { "capability_id": "brains-features", - "catalog_sha256": "a1677c30594d44e516a3aaa8e6524829360e44d90f8304d2ab8291b7e5ed6a3a", + "catalog_sha256": "9976952657698137f1ab4cb0d9eb547555e0c966659632cc4d14e5797d66830b", "artifact_path": "plugins/brains/skills/brains-build/SKILL.md", - "artifact_sha256": "f9196254dd49af3cd8bb1d4d93b1ba36ff10d8efc0bb039fb464901e143e143f" + "artifact_sha256": "95261ee804b3b3cae4f42231264aafa64c96753dce71ab851fad6f97eed37c48" }, { "capability_id": "procedure:board", @@ -27,7 +27,7 @@ "capability_id": "procedure:automation", "catalog_sha256": "ea1e883788e138e39b80627e5a14d6ac35dc7f658b4628e7e32084bb6620fe8b", "artifact_path": "plugins/brains/skills/brains-automation/SKILL.md", - "artifact_sha256": "78c63d6a09ffdf55f80f3b686b1cd0d1000585bc1d5d148323ea03ebda018bc2" + "artifact_sha256": "c91cc266c317a5d2ae479594ecad0e5048970d7431139777ea61f401bb4e9cc6" }, { "capability_id": "procedure:workflow", @@ -36,5 +36,5 @@ "artifact_sha256": "b22814fd630ed78964d54440ec327e3aebcde8c812aaa9985fff9c4d345d3f09" } ], - "source_commit": "e332512644b2f06a6b7068dcc9196eab2d31c05a" + "source_commit": "6fb7b2356592b4e5313176cc55d6d06cb3130e31" } diff --git a/plugins/brains/skills/brains-automation/SKILL.md b/plugins/brains/skills/brains-automation/SKILL.md index 4cb3388..0f22f5f 100644 --- a/plugins/brains/skills/brains-automation/SKILL.md +++ b/plugins/brains/skills/brains-automation/SKILL.md @@ -176,7 +176,7 @@ After one hour a draft moves to the `/inbox` Expired tab but remains approvable. There is no edit-at-confirm step: call `discard_action` before re-drafting corrected input. -Automation tokens cannot self-confirm. A confirmation-required action therefore waits for the human at `/inbox`, but a `requires_confirmation:false` action still executes inline for any caller granted `act_on_integration`. Get explicit agreement before saving an automation that performs an unattended external write. New automations default to `write_policy:'always_draft'`; only `update_automation` can switch to `auto_confirm_safe`, and that policy does not override an action whose recipe already auto-executes. +Automation tokens cannot self-confirm: `confirm_action` is refused for them under either policy, so a confirmation-required action is released by the `write_policy` rule above, not by the sandbox. Get explicit agreement before saving an automation that performs an unattended external write. New automations default to `write_policy:'always_draft'`; switching to `auto_confirm_safe` is an explicit configuration change, and that policy does not override an action whose recipe already auto-executes. **For "notify me" / "ping me" / "DM me" asks**, default to `telegram_push` when the user has Telegram connected — it's the user's primary notification surface. Reach for `act_on_integration` only when the notification belongs in a specific channel (email thread, calendar invite, Drive doc). `telegram_push` bypasses the draft/confirm step and sends immediately, so the user must have opted in by connecting the bot. @@ -248,7 +248,7 @@ Derive from Steps 3 + 4. The user doesn't pick this — you do. Mention it inlin | Un-retire a row (`brains.boards.restore`) | `restore_board_row` | | Pull fresh gmail/calendar/drive data | `fetch_from_integration` | | Read a github/monday adapter (`brains.call("adapter_query", …)`) | `adapter_query` | -| Send | `act_on_integration` (drafts for a confirm-required action; an action declaring `requires_confirmation:false` sends inline) | +| Send | `act_on_integration` (drafts or sends inline per requires_confirmation and write_policy; see the contract above) | | Send a Telegram DM (only when user explicitly asked) | `telegram_push` (bypasses draft/confirm — delivers immediately) | | Write a memory page | `create_page` | | LLM call from sandbox | `automation_complete` | @@ -377,8 +377,8 @@ The new agent lands in `active` state — it will start firing on its cron / pag Before handing off, prove the source compiles and runs end-to-end against a real trigger payload by firing one manual dry run. Call **`run_automation_once`** with: - `automation_id` — from Step 8 -- `dry_run: true` — default; lets the user code execute but suppresses outbound writes (same hook the admin Dry-Run button uses) -- `verify_mode: true` — **REQUIRED for self-verification during this flow.** When true the MCP server suppresses all outbound side-effect tools (telegram_push, email / calendar / drive sends via act_on_integration, fetch_from_integration writes, and non-GET http_fetch) and returns suppressed-shape stubs instead. This lets you confirm the source compiles and the logic path executes end-to-end **without spamming the user's phone / inbox / drive** while you're still iterating. The user code still runs; only the deliveries are muted. +- `dry_run: true` — default; suppresses act sends and `telegram_push`. Board and page writes run live under `dry_run` AND `verify_mode` — a smoke test mutates real rows +- `verify_mode: true` — **REQUIRED for self-verification during this flow.** When true the MCP server suppresses all outbound side-effect tools (telegram_push, email / calendar / drive sends via act_on_integration, fetch_from_integration writes, and non-GET http_fetch) and returns suppressed-shape stubs instead. This lets you confirm the source compiles and the logic path executes end-to-end **without spamming the user's phone / inbox / drive** while you're still iterating. The tool enqueues a `trigger_kind='manual'` run and polls `automation_runs` until it terminates (or `wait_seconds` elapses). Default wait is `max_wall_seconds + 90` to cover the runner's ~30s tick + sandbox spawn. diff --git a/plugins/brains/skills/brains-build/SKILL.md b/plugins/brains/skills/brains-build/SKILL.md index 2ca0059..0ee2438 100644 --- a/plugins/brains/skills/brains-build/SKILL.md +++ b/plugins/brains/skills/brains-build/SKILL.md @@ -23,12 +23,13 @@ Don't restate that artifact here — open it. | **Recurring brief / digest** | Install a prebuilt automation that sends a recurring summary instead of scaffolding one. | `list_recipes` → `install_recipe` | | **Workflow** | A goal container: charter, KPIs, deadlines, roster, an owned board and template automations. | `create_workflow_flow` if your client exposes it (follow the playbook it returns), otherwise `create_workflow` | | **Mini-site** | Static sandboxed HTML on a brain — a deck, a one-pager, a shareable link. | `create_mini_site` | -| **Sharing** | Give someone access — share_board, share_folder (cascades), share_mini_site, create_share_circle. | `share_board` | +| **Sharing** | Give someone access — share_board, share_folder (cascades), share_mini_site. | `share_board` | | **Board source link** | Bind a board to an external resource through an adapter (http_json, ics, github, monday). | `create_board_link` | | **Dataset recipe** | Schedule a board link to materialize into a dataset on a cron — set up on the board's sources page. | the board's Sources page (`/boards//sources`) — no agent tool for this | -| **Integrations** | See what's connected and install, upgrade or remove an integration. | the `brains-integrations` skill if your client loads skills, otherwise `list_my_integrations` → `install_integration` | +| **Integrations** | Connectors: what's connected; install, upgrade, remove. CLI/connector `create_integration_flow` authors one. | the `brains-integrations` skill if your client loads skills, otherwise `list_my_integrations` → `install_integration` | | **Recipe publishing** | Publish a board or other supported build to the catalog — publish_recipe names the kinds it accepts. | `publish_recipe` | -| **Telegram bot** | Reach the brain from a phone, without the laptop. | the Connect Telegram page (`/install/telegram`) — no agent tool for this | +| **Recipe install** | Catalog recipe: install_recipe, uninstall_recipe removes/deletes; list_installed_versions reads 3 of 5. | `list_recipes` | +| **Telegram bot** | Reach the brain from Telegram on a phone, without the laptop. | the Connect Telegram page (`/install/telegram`) — no agent tool for this | Reach for a `*_flow` tool for any create/set-up/scaffold ask: it returns the playbook that produces a structure which actually fits. Use the bare diff --git a/plugins/brains/skills/brains-write/SKILL.md b/plugins/brains/skills/brains-write/SKILL.md index e98d9e7..14483e2 100644 --- a/plugins/brains/skills/brains-write/SKILL.md +++ b/plugins/brains/skills/brains-write/SKILL.md @@ -23,14 +23,16 @@ execution in their brains settings, in which case a short allowlist of low-risk actions also runs inline when called from an eligible client signed in from the user's own computer (CLI clients such as Claude Code and Codex) — every execution is still recorded in /inbox. The mode is decided server-side per call, so treat -any call as potentially executing and never assume a `draft_id`. If +any call as potentially executing and never assume a `draft_id`. Get the user's +go-ahead for the write before you call; do not call to find out whether it +drafts. If `requires_confirmation` is absent, the page predates the field: treat whether it drafts or runs as unknown. `side_effect` says where it writes (`external` = the provider, visible outside brains; `null` or absent = undeclared, treat as external). Inline external writes include `rsvp_event`, `create_draft`, and `add_labels`; do not infer safety from read vs write. -Cap: 30 auto-executions/install/60s. +Cap: 30 auto-executions/install/60s. Direct execution is additionally capped at 10/user/60s across every install; past either cap a direct execution degrades to a draft. Automation `dry_run` suppresses external writes to no-call `[DRY RUN]` drafts. | `kind` | What happened | What you do | diff --git a/tests/plugin-contract/run.ts b/tests/plugin-contract/run.ts index 0b9d9ba..f4d9744 100644 --- a/tests/plugin-contract/run.ts +++ b/tests/plugin-contract/run.ts @@ -1642,14 +1642,16 @@ const WRITE_ACTION_CONTRACT_REGION = [ "actions also runs inline when called from an eligible client signed in from the", "user's own computer (CLI clients such as Claude Code and Codex) — every execution", "is still recorded in /inbox. The mode is decided server-side per call, so treat", - "any call as potentially executing and never assume a `draft_id`. If", + "any call as potentially executing and never assume a `draft_id`. Get the user's", + "go-ahead for the write before you call; do not call to find out whether it", + "drafts. If", "`requires_confirmation` is absent, the page predates the field: treat whether", "it drafts or runs as unknown. `side_effect` says where it writes", "(`external` = the provider, visible outside brains;", "`null` or absent = undeclared, treat as external). Inline external writes", "include `rsvp_event`,", "`create_draft`, and `add_labels`; do not infer safety from read vs write.", - "Cap: 30 auto-executions/install/60s.", + "Cap: 30 auto-executions/install/60s. Direct execution is additionally capped at 10/user/60s across every install; past either cap a direct execution degrades to a draft.", "Automation `dry_run` suppresses external writes to no-call `[DRY RUN]` drafts.", ].join("\n"); // Asserted AFTER the per-rule checks below, deliberately: they run first and name From 5b9cefa4868b6f528072f5e25f34b7ae97bbd876 Mon Sep 17 00:00:00 2001 From: olegshmuelov Date: Wed, 12 Aug 2026 16:03:24 +0300 Subject: [PATCH 2/2] test(plugin-contract): pin the automation draft-vs-execute rules [BRNS-MCP-052] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit brains-automation had three content assertions, all Telegram secret-hygiene rules. Everything else about the artifact was digest equality, which proves the bytes came from the catalog and says nothing about what they say. A regeneration could re-drop any draft-vs-execute rule and this suite would stay green — the same blind spot that let the upstream defect reach production. Three rules now pinned, matching how the brains-write contract is pinned: - The write-policy contract, as a whole region from the confirmation-boundary opener to the notify-me lead-in. Automation tokens cannot self-confirm, and a confirmation-required action is released by write_policy rather than the sandbox. - The smoke-test contract, as a whole region covering the run_automation_once argument list. Board and page writes run live under dry_run AND verify_mode, so a mandated smoke test mutates real rows. - The Send grant row, verbatim, plus a uniqueness check that exactly one row in the grant table states act_on_integration's draft-vs-send disposition. Whole regions rather than substrings because both failure modes are known here: a keyword pin dies to paraphrase, and a lone sentence pin dies at the seam, where the contradiction is written beside the pinned text and every substring check still passes. The Send row is the exception and takes a uniqueness check instead of a region, because the other twenty rows are a grant inventory that moves for unrelated reasons and pinning them would add no coverage of this rule. Each rule was mutation-proved independently: deleted, then paraphrased with surrounding text intact, with the manifest digest realigned each time so the digest check could not be what failed. Six mutations, six failures, each naming its own region. --- tests/plugin-contract/run.ts | 113 +++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/tests/plugin-contract/run.ts b/tests/plugin-contract/run.ts index f4d9744..13e67bd 100644 --- a/tests/plugin-contract/run.ts +++ b/tests/plugin-contract/run.ts @@ -1784,6 +1784,119 @@ assert( "brains-automation lost the handling for a secret the user pasted anyway", ); +// --------------------------------------------------------------------------- +// The automation draft-vs-execute rules, pinned the same way brains-write's +// contract is (see WRITE_ACTION_CONTRACT_REGION above) and for a reason this +// artifact demonstrated: the three checks above are the ONLY content assertions +// brains-automation had. Everything else about this file was digest equality +// (`run.ts` line ~1447), which proves the bytes came from the catalog and says +// nothing about what they say. A regeneration could silently re-drop any rule +// below and CI here would stay green — which is exactly how the upstream defect +// these rules fix reached production in the first place. +// +// WHY WHOLE REGIONS, not substrings. Two failure modes, both already observed in +// this repo. A keyword pin dies to paraphrase: "cannot self-confirm" survives +// "the sandbox may confirm when trusted". A verbatim sentence pin dies at the +// SEAM: leave the pinned sentence intact and write the contradiction NEXT to it +// ("verify_mode also holds back board writes") and every substring check stays +// green while the reader is told the opposite. Only whole-region equality plus a +// composition check refuses both. +// +// COUPLING, same as the block above: these bytes are generated from a different, +// PRIVATE repo, so an author rewording the automation procedure reds a build in a +// public repo they may not know exists. When that happens the fix is to re-derive +// the rule upstream and regenerate — never to trim a region or soften an assert +// so a sync goes green. That is the hole all of this exists to close. +// +// TO UPDATE: read the upstream diff, satisfy yourself the new wording is true of +// what the server does, then replace the constant with the new bytes. + +// Rule 1 — automation tokens cannot self-confirm, and what releases a +// confirmation-required action instead. Region runs from the `You can't confirm` +// opener to the `**For "notify me"` lead-in, EXCLUSIVE: both anchors are +// structural rather than editorial, and the span covers the confirm-boundary +// prose AND the write_policy paragraph, so a contradiction inserted between them +// — the natural place to put one — lands inside the pin. +const AUTOMATION_WRITE_POLICY_REGION = [ + "You can't confirm a draft from this loop. Only the user's out-of-band", + "`/inbox` or Telegram surface has the confirmation secret. So never call `confirm_action` yourself.", + "After one hour a draft moves to the `/inbox` Expired tab but remains", + "approvable. There is no edit-at-confirm step: call", + "`discard_action` before re-drafting corrected input.", + "", + "Automation tokens cannot self-confirm: `confirm_action` is refused for them under either policy, so a confirmation-required action is released by the `write_policy` rule above, not by the sandbox. Get explicit agreement before saving an automation that performs an unattended external write. New automations default to `write_policy:'always_draft'`; switching to `auto_confirm_safe` is an explicit configuration change, and that policy does not override an action whose recipe already auto-executes.", +].join("\n"); +const automationPolicyStart = automationSkill.indexOf("You can't confirm a draft from this loop."); +const automationPolicyEnd = automationSkill.indexOf('**For "notify me"'); +assert(automationPolicyStart > 0, "brains-automation must keep its confirmation-boundary opener"); +assert( + automationPolicyEnd > automationPolicyStart, + "brains-automation's write-policy paragraph must precede the notify-me guidance — the slice below depends on it", +); +assert( + normalizeRegion(automationSkill.slice(automationPolicyStart, automationPolicyEnd)) === + AUTOMATION_WRITE_POLICY_REGION, + "brains-automation's write-policy contract must match the approved copy exactly — a sentence added beside these rules can reverse them while every keyword check passes (regenerate upstream, re-read the change, then update AUTOMATION_WRITE_POLICY_REGION)", +); + +// Rule 2 — the dry_run/verify_mode suppression boundary. This is the one whose +// previous wording was FALSE: it implied a smoke test muted every side effect, +// when suppression fires at four call sites only and board and page writes run +// live. Region is the whole `run_automation_once` argument list, so the +// verify_mode bullet — the adjacent bullet, and the obvious seam for a +// contradicting claim — is inside the pin rather than beside it. +const AUTOMATION_SMOKE_TEST_REGION = [ + "Call **`run_automation_once`** with:", + "", + "- `automation_id` — from Step 8", + "- `dry_run: true` — default; suppresses act sends and `telegram_push`. Board and page writes run live under `dry_run` AND `verify_mode` — a smoke test mutates real rows", + "- `verify_mode: true` — **REQUIRED for self-verification during this flow.** When true the MCP server suppresses all outbound side-effect tools (telegram_push, email / calendar / drive sends via act_on_integration, fetch_from_integration writes, and non-GET http_fetch) and returns suppressed-shape stubs instead. This lets you confirm the source compiles and the logic path executes end-to-end **without spamming the user's phone / inbox / drive** while you're still iterating.", +].join("\n"); +const automationSmokeStart = automationSkill.indexOf("Call **`run_automation_once`** with:"); +const automationSmokeEnd = automationSkill.indexOf("The tool enqueues"); +assert(automationSmokeStart > 0, "brains-automation must keep its smoke-test invocation"); +assert( + automationSmokeEnd > automationSmokeStart, + "brains-automation's smoke-test arguments must precede the polling paragraph — the slice below depends on it", +); +assert( + normalizeRegion(automationSkill.slice(automationSmokeStart, automationSmokeEnd)) === + AUTOMATION_SMOKE_TEST_REGION, + "brains-automation's smoke-test contract must match the approved copy exactly — this is the region that tells an author a mandated dry run still mutates real board and page rows (regenerate upstream, re-read the change, then update AUTOMATION_SMOKE_TEST_REGION)", +); + +// Rule 3 — the grant table's Send row, which states when `act_on_integration` +// drafts versus sends inline. Pinned as a whole row plus a UNIQUENESS check over +// the table, not as a region: the other twenty rows are a grant inventory that +// moves for unrelated reasons, and pinning them would red this build on every +// upstream grant edit while adding no coverage of this rule. The uniqueness check +// is what closes the seam a lone row pin would leave — an added row naming +// `act_on_integration` with a different disposition fails it even though the +// pinned row is untouched. Table sliced between its own header and the +// grant-by-bare-name paragraph; the two other rows in this file that name the +// tool live in a different table and are out of scope by construction. +const AUTOMATION_SEND_GRANT_ROW = + "| Send | `act_on_integration` (drafts or sends inline per requires_confirmation and write_policy; see the contract above) |"; +const grantTableStart = automationSkill.indexOf("| Operation | Grant |"); +const grantTableEnd = automationSkill.indexOf("**Grant by bare MCP tool name.**"); +assert(grantTableStart > 0, "brains-automation must keep its grant-selection table"); +assert( + grantTableEnd > grantTableStart, + "brains-automation's grant table must precede the bare-tool-name rule — the slice below depends on it", +); +const grantTableRows = normalizeRegion(automationSkill.slice(grantTableStart, grantTableEnd)) + .split("\n") + .filter((line) => line.startsWith("|")); +const actGrantRows = grantTableRows.filter((line) => line.includes("act_on_integration")); +assert( + actGrantRows.length === 1, + `brains-automation's grant table must state act_on_integration's draft-vs-send disposition exactly once — ${actGrantRows.length} rows name it, so a reader can be routed by whichever they read first (regenerate upstream, re-read the change, then update AUTOMATION_SEND_GRANT_ROW)`, +); +assert( + actGrantRows[0] === AUTOMATION_SEND_GRANT_ROW, + "brains-automation's Send grant row must match the approved copy exactly — it is where an author learns that this tool may send without drafting (regenerate upstream, re-read the change, then update AUTOMATION_SEND_GRANT_ROW)", +); + // This README is the install instructions for anyone who finds the repo directly rather than the // guided page, so it has to carry the same contract. It documented `export BRAINS_API_TOKEN` as the // way in long after that stopped being able to work, which is exactly the drift these pin.