From b54a31baf3938f31b6bbcae8d6de440d030beb2c Mon Sep 17 00:00:00 2001 From: stefan-ssv-labs Date: Tue, 28 Jul 2026 15:40:32 +0300 Subject: [PATCH 1/5] feat(core): generate brains-write capability artifact [BRNS-CORE-008] --- .../brains/generated/capability-catalog.json | 11 +++++++ plugins/brains/skills/brains-write/SKILL.md | 7 +++++ tests/plugin-contract/run.ts | 31 ++++++++++++++++--- 3 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 plugins/brains/generated/capability-catalog.json diff --git a/plugins/brains/generated/capability-catalog.json b/plugins/brains/generated/capability-catalog.json new file mode 100644 index 0000000..04dee55 --- /dev/null +++ b/plugins/brains/generated/capability-catalog.json @@ -0,0 +1,11 @@ +{ + "schema_version": 1, + "catalog_schema_version": 1, + "renderer_version": 1, + "capability_id": "integration-actions", + "source_repository": "https://github.com/ssvlabs/brains", + "source_generator": "apps/mcp/scripts/generate-capability-catalog.ts", + "catalog_sha256": "9f9f9fbbfb57db0578f4759759ceb41028f36f98d94415a90a6ef432e8d56efc", + "artifact_path": "plugins/brains/skills/brains-write/SKILL.md", + "artifact_sha256": "b0401fca02774f55d324b902ba64070fd1d753b8679d7e45c40c2ce57ef301d8" +} diff --git a/plugins/brains/skills/brains-write/SKILL.md b/plugins/brains/skills/brains-write/SKILL.md index b85c55f..5b2d02f 100644 --- a/plugins/brains/skills/brains-write/SKILL.md +++ b/plugins/brains/skills/brains-write/SKILL.md @@ -3,6 +3,9 @@ name: brains-write description: How to take ACTIONS on the user's integrations through brains — send an email, create a calendar event, make a Drive doc, comment on a Monday item, run a live Gmail search, post a message. Use whenever the user wants to send/create/reply/forward/mark/schedule something, or "message/ping/DM" someone. Covers the codex action flow and the draft→confirm gate. --- + + # Acting on integrations (writes) Codex-first. Each connected integration declares its own actions; discover and @@ -32,6 +35,10 @@ no installed action covers it; there is no source-enum fallback. write reached the provider is **unknown**. Never blind-retry: read the state back (or send the user to `/inbox`) before re-sending. A pure read is safe to retry. +- **`kind:"clarification"`** → nothing ran. Answer its `question`, then + resend the full tuple. +- **`kind:"noop"`** → nothing ran. Relay its `reason` and point the user to + `/integrations` when the integration is unavailable. - **`requires_confirmation` absent from the frontmatter** → the page predates the field, so the outcome cannot be predicted. Never assume it will draft: dispatch the full tuple, branch on the returned `kind`, and report diff --git a/tests/plugin-contract/run.ts b/tests/plugin-contract/run.ts index d4b0301..fcae5bf 100644 --- a/tests/plugin-contract/run.ts +++ b/tests/plugin-contract/run.ts @@ -1,7 +1,8 @@ #!/usr/bin/env bun -import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; @@ -44,6 +45,7 @@ const codexMcp = readJson(join(PLUGIN, ".mcp.json")); const turnHook = readFileSync(join(PLUGIN, "hooks", "brains-turn.sh"), "utf8"); const core = readFileSync(join(PLUGIN, "core.md"), "utf8"); const writeSkill = readFileSync(join(PLUGIN, "skills", "brains-write", "SKILL.md"), "utf8"); +const capabilityManifest = readJson(join(PLUGIN, "generated", "capability-catalog.json")); const coreNormalized = core.replace(/\s+/g, " "); const writeSkillNormalized = writeSkill.replace(/\s+/g, " "); @@ -111,9 +113,28 @@ for (const signal of [ } assert(core.length < 3_000, "always-loaded core must stay below 3,000 characters"); -// This public plugin is a sixth model-visible copy of the act contract, outside -// the monorepo's ACT_CONTRACT_COPIES gate. Mirror its four load-bearing rules -// here so a future compaction cannot drift independently again. +// The public face is generated from the monorepo capability catalog. Verify its +// immutable artifact digest locally; installation never fetches a mutable copy. +assert(capabilityManifest.schema_version === 1, "capability manifest schema mismatch"); +assert(capabilityManifest.catalog_schema_version === 1, "catalog schema mismatch"); +assert(capabilityManifest.renderer_version === 1, "catalog renderer mismatch"); +assert(capabilityManifest.capability_id === "integration-actions", "capability id mismatch"); +assert( + capabilityManifest.artifact_path === "plugins/brains/skills/brains-write/SKILL.md", + "generated artifact path mismatch", +); +assert( + capabilityManifest.artifact_sha256 === + createHash("sha256").update(writeSkill, "utf8").digest("hex"), + "generated brains-write artifact digest mismatch", +); +assert( + !/automation_secret|adminPool|handler_source|telegram_push|grant_token/.test(writeSkill), + "public skill leaked an internal-only capability", +); + +// Keep independent semantic assertions: digest equality proves provenance, not +// that the canonical source itself kept the load-bearing safety rules. assert(writeSkillNormalized.includes("install_id=<…> action_name=<…> input={…}"), "structured action tuple missing"); assert(writeSkillNormalized.includes("call `get_page` on the selected result"), "action discovery must resolve frontmatter"); assert( @@ -135,6 +156,8 @@ assert( writeSkillNormalized.includes('**`kind:"rate_limited"`** → nothing ran and no upstream call was made'), "rate-limited actions must be documented as not attempted", ); +assert(writeSkillNormalized.includes('**`kind:"clarification"`**'), "clarification result kind missing"); +assert(writeSkillNormalized.includes('**`kind:"noop"`**'), "noop result kind missing"); assert( writeSkillNormalized.includes("whether an outbound write reached the provider is **unknown**"), "auto-failed actions must preserve unknown-outcome guidance", From 35492a60488c70eb83816fcd45c00dd7d4bb0eda Mon Sep 17 00:00:00 2001 From: stefan-ssv-labs Date: Wed, 29 Jul 2026 10:04:33 +0300 Subject: [PATCH 2/5] fix(core): regenerate capability artifact v2 [BRNS-CORE-008] --- .../brains/generated/capability-catalog.json | 6 +- plugins/brains/skills/brains-write/SKILL.md | 110 +++++++----------- tests/plugin-contract/run.ts | 33 +++--- 3 files changed, 62 insertions(+), 87 deletions(-) diff --git a/plugins/brains/generated/capability-catalog.json b/plugins/brains/generated/capability-catalog.json index 04dee55..45bdb53 100644 --- a/plugins/brains/generated/capability-catalog.json +++ b/plugins/brains/generated/capability-catalog.json @@ -1,11 +1,11 @@ { "schema_version": 1, "catalog_schema_version": 1, - "renderer_version": 1, + "renderer_version": 2, "capability_id": "integration-actions", "source_repository": "https://github.com/ssvlabs/brains", "source_generator": "apps/mcp/scripts/generate-capability-catalog.ts", - "catalog_sha256": "9f9f9fbbfb57db0578f4759759ceb41028f36f98d94415a90a6ef432e8d56efc", + "catalog_sha256": "1952b377d9aea55847855d35f1c929cde470538d9318031571aeed286c713b52", "artifact_path": "plugins/brains/skills/brains-write/SKILL.md", - "artifact_sha256": "b0401fca02774f55d324b902ba64070fd1d753b8679d7e45c40c2ce57ef301d8" + "artifact_sha256": "57f3839e5ddffa6c3d5fdad931b1a8f2bea16baac71fe0d68d05888b3caf1ddd" } diff --git a/plugins/brains/skills/brains-write/SKILL.md b/plugins/brains/skills/brains-write/SKILL.md index 5b2d02f..6bcde9c 100644 --- a/plugins/brains/skills/brains-write/SKILL.md +++ b/plugins/brains/skills/brains-write/SKILL.md @@ -8,75 +8,47 @@ description: How to take ACTIONS on the user's integrations through brains — s # Acting on integrations (writes) -Codex-first. Each connected integration declares its own actions; discover and -dispatch them generically. If nothing matches, refine the discovery query or say -no installed action covers it; there is no source-enum fallback. - -## The codex path (preferred) - -1. **Discover** — `query type=integration_action text=""`. - It returns ranked page slugs; call `get_page` on the selected result to read - its frontmatter: `install_id`, `action_name`, `description`, `input_schema`, - `requires_confirmation`, and `side_effect`. The page body carries example - requests — strong hints for shaping `input`. -2. **Dispatch** — `act_on_integration install_id=<…> action_name=<…> input={…}` - (input matches `input_schema`; do not send a free-form `request`). - Build `input` yourself from the user's words; if the ask is fuzzy, the - discovery `query` + the action's `examples` tell you which action and shape. - -### Return kinds — know which is the success state - -- **`requires_confirmation: false`** → executes inline now, returns - `{kind:"auto_executed", result, action_record_id}`. **Done** — do NOT try to - confirm it, and do not assume it was read-only. -- **`kind:"rate_limited"`** → nothing ran and no upstream call was made. Retry - after `retry_after_seconds`. -- **`kind:"auto_failed"`** → the action was attempted, so whether an outbound - write reached the provider is **unknown**. Never blind-retry: read the state - back (or send the user to `/inbox`) before re-sending. A pure read is safe to - retry. -- **`kind:"clarification"`** → nothing ran. Answer its `question`, then - resend the full tuple. -- **`kind:"noop"`** → nothing ran. Relay its `reason` and point the user to - `/integrations` when the integration is unavailable. -- **`requires_confirmation` absent from the frontmatter** → the page predates - the field, so the outcome cannot be predicted. Never assume it will draft: - dispatch the full tuple, branch on the returned `kind`, and report - `auto_executed` in the past tense. -- **`requires_confirmation: true`** → returns - `{kind:"draft", draft_id, action, preview, payload, confirm_hint, expires_at}`. - Relay the `preview` and `confirm_hint`, then stop. The user confirms through - the real controls in `/inbox` (web/mobile) or Telegram. Do **not** call - `confirm_action` from this agent loop. If the user cancels or corrects the - draft, call `discard_action`; for a correction, draft the structured action - again with the new input. Destructive sends (email, calendar invite, doc - create) live here. After 1 hour a draft moves to the `/inbox` Expired tab but - remains approvable, so discard the old draft before redrafting. - -The out-of-band surfaces hold the confirmation capability; this chat does not. - -### Live Gmail search - -gmail-inbox ships `search_emails` (`requires_confirmation: false`) — runs native -Gmail search at runtime for mail the ingested pages don't cover. `input={query: -"", limit: 1..50}`; pass `write_pages: true` to ALSO persist each -match as an `email` page. Reach for it AFTER `list_pages`/`search` come up short, -not before. - -## Gmail / Calendar / Drive - -These are codex installs like any other. `query type=integration_action` finds -their action pages; `get_page` reveals the `install_id`. Dispatch the same -`install_id` + `action_name` + `input` tuple. A bare `source` drafts nothing. +Each connected integration declares its own actions. If nothing matches, refine +the discovery query or say no installed action covers it; there is no +source-enum fallback. + +Discover with `query type=integration_action text=""`. +It returns slug/title/snippet, so call `get_page` on the selected +slug to read frontmatter. Use its `install_id`, `action_name`, and structured +`input` in `act_on_integration`; this tuple is the only call +shape. `requires_confirmation:false` means the action runs inline. If +`requires_confirmation` is absent, the page predates the field: treat whether +it drafts or runs as unknown. Inline external writes include `rsvp_event`, +`create_draft`, and `add_labels`; do not infer safety from read vs write. + +| `kind` | What happened | What you do | +|---|---|---| +| `draft` | Nothing sent; it carries `preview`, `confirm_hint`, and `expires_at`. | Relay the preview and hint, then stop. | +| `auto_executed` | It already ran; it carries `result` and `action_record_id`. | Report what happened, past tense. | +| `auto_failed` | It was attempted; whether an outbound write reached the provider is **unknown**. | Don't blind-retry. Read provider state or `/inbox` before re-sending; a pure read is safe to retry. | +| `rate_limited` | Nothing ran and no upstream call occurred. | Retry after `retry_after_seconds`. | +| `clarification` | The tuple was incomplete or ambiguous. | Answer its `question`, then resend the full tuple. | +| `noop` | The integration is unavailable. | Relay its `reason` and point to `/integrations`. | + +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. + +The selected action page also carries `description`, `input_schema`, +`side_effect`, and example requests. Build structured `input` from those +facts; do not send a free-form `request`. + +## Live Gmail search + +`gmail-inbox` ships `search_emails` with `requires_confirmation:false`. +Use `input={query:"", limit:1..50}`; `write_pages:true` also +persists each match. Reach for it only after `list_pages` or `search` comes +up short. ## Routing a generic "message someone" -No hardcoded default channel. For a -generic "send a msg / message X / ping / DM" with no channel named, use the codex -discovery above (`query type=integration_action`) and dispatch to whichever -messaging integration the user actually has connected. Use `gmail` only when they -say "email," give an email address, or are replying to/forwarding a thread. -`calendar`/`drive` only when explicit. - -**Always relay a destructive action's preview and confirmation hint, then leave -the decision to the user's out-of-band approval surface.** +Do not hardcode a default channel. Discover the messaging actions the user +actually has installed. Use Gmail only when they say "email", provide an email +address, or are replying to or forwarding a thread. \ No newline at end of file diff --git a/tests/plugin-contract/run.ts b/tests/plugin-contract/run.ts index fcae5bf..a3ed1cb 100644 --- a/tests/plugin-contract/run.ts +++ b/tests/plugin-contract/run.ts @@ -117,7 +117,7 @@ assert(core.length < 3_000, "always-loaded core must stay below 3,000 characters // immutable artifact digest locally; installation never fetches a mutable copy. assert(capabilityManifest.schema_version === 1, "capability manifest schema mismatch"); assert(capabilityManifest.catalog_schema_version === 1, "catalog schema mismatch"); -assert(capabilityManifest.renderer_version === 1, "catalog renderer mismatch"); +assert(capabilityManifest.renderer_version === 2, "catalog renderer mismatch"); assert(capabilityManifest.capability_id === "integration-actions", "capability id mismatch"); assert( capabilityManifest.artifact_path === "plugins/brains/skills/brains-write/SKILL.md", @@ -135,17 +135,20 @@ assert( // Keep independent semantic assertions: digest equality proves provenance, not // that the canonical source itself kept the load-bearing safety rules. -assert(writeSkillNormalized.includes("install_id=<…> action_name=<…> input={…}"), "structured action tuple missing"); -assert(writeSkillNormalized.includes("call `get_page` on the selected result"), "action discovery must resolve frontmatter"); assert( - writeSkillNormalized.includes("`requires_confirmation` absent from the frontmatter") && - writeSkillNormalized.includes("the outcome cannot be predicted") && - writeSkillNormalized.includes("Never assume it will draft"), + writeSkillNormalized.includes("its `install_id`, `action_name`, and structured `input` in `act_on_integration`") && + writeSkillNormalized.includes("this tuple is the only call shape"), + "structured action tuple missing", +); +assert(writeSkillNormalized.includes("call `get_page` on the selected"), "action discovery must resolve frontmatter"); +assert( + writeSkillNormalized.includes("`requires_confirmation` is absent") && + writeSkillNormalized.includes("treat whether it drafts or runs as unknown"), "absent requires_confirmation must remain unknown rather than predict a draft", ); assert( - writeSkillNormalized.includes('**`requires_confirmation: false`** → executes inline now, returns') && - writeSkillNormalized.includes('{kind:"auto_executed", result, action_record_id}'), + writeSkillNormalized.includes("`requires_confirmation:false` means the action runs inline") && + writeSkillNormalized.includes("| `auto_executed` | It already ran; it carries `result` and `action_record_id`."), "requires_confirmation:false must be documented as already executed", ); assert(writeSkillNormalized.includes("there is no source-enum fallback"), "legacy source-enum fallback must stay removed"); @@ -153,22 +156,22 @@ assert(!writeSkillNormalized.includes("Fall back to the legacy source-enum"), "s assert(writeSkillNormalized.includes("action_record_id"), "auto-executed result must expose action_record_id"); assert(!writeSkillNormalized.includes("audit_id"), "stale auto-executed audit_id field must not return"); assert( - writeSkillNormalized.includes('**`kind:"rate_limited"`** → nothing ran and no upstream call was made'), + writeSkillNormalized.includes("| `rate_limited` | Nothing ran and no upstream call occurred."), "rate-limited actions must be documented as not attempted", ); -assert(writeSkillNormalized.includes('**`kind:"clarification"`**'), "clarification result kind missing"); -assert(writeSkillNormalized.includes('**`kind:"noop"`**'), "noop result kind missing"); +assert(writeSkillNormalized.includes("| `clarification` |"), "clarification result kind missing"); +assert(writeSkillNormalized.includes("| `noop` |"), "noop result kind missing"); assert( writeSkillNormalized.includes("whether an outbound write reached the provider is **unknown**"), "auto-failed actions must preserve unknown-outcome guidance", ); -assert(writeSkillNormalized.includes("Never blind-retry"), "auto-failed external writes must not be blindly retried"); -assert(writeSkillNormalized.includes("The out-of-band surfaces hold the confirmation capability"), "approval boundary missing"); -assert(writeSkillNormalized.includes("Do **not** call `confirm_action`"), "agent self-confirm prohibition missing"); +assert(writeSkillNormalized.includes("Don't blind-retry"), "auto-failed external writes must not be blindly retried"); +assert(writeSkillNormalized.includes("out-of-band") && writeSkillNormalized.includes("confirmation secret"), "approval boundary missing"); +assert(writeSkillNormalized.includes("never call `confirm_action` yourself"), "agent self-confirm prohibition missing"); assert(writeSkillNormalized.includes("call `discard_action`"), "agent-side draft discard path missing"); assert(!writeSkillNormalized.includes("never call `discard_action`"), "draft discard guidance must remain actionable"); assert(writeSkillNormalized.includes("remains approvable"), "expired drafts must not be described as inert"); -assert(writeSkillNormalized.includes("A bare `source` drafts nothing"), "source-only action fallback must stay prohibited"); +assert(writeSkillNormalized.includes("this tuple is the only call shape"), "source-only action fallback must stay prohibited"); assert(!/act_on_integration[^.]{0,200}request=/.test(writeSkillNormalized), "free-form action request must not return"); assert( From f28d324f0d0d24225696734242297628a97367d7 Mon Sep 17 00:00:00 2001 From: stefan-ssv-labs Date: Wed, 29 Jul 2026 12:07:35 +0300 Subject: [PATCH 3/5] fix(core): regenerate action safety contract [BRNS-CORE-008] --- plugins/brains/generated/capability-catalog.json | 4 ++-- plugins/brains/skills/brains-write/SKILL.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/brains/generated/capability-catalog.json b/plugins/brains/generated/capability-catalog.json index 45bdb53..d2c4afa 100644 --- a/plugins/brains/generated/capability-catalog.json +++ b/plugins/brains/generated/capability-catalog.json @@ -5,7 +5,7 @@ "capability_id": "integration-actions", "source_repository": "https://github.com/ssvlabs/brains", "source_generator": "apps/mcp/scripts/generate-capability-catalog.ts", - "catalog_sha256": "1952b377d9aea55847855d35f1c929cde470538d9318031571aeed286c713b52", + "catalog_sha256": "3dbd82bdcd875359ab55efef1eb3c9bc325d3805246ecf50bc6617fcdfb43ed7", "artifact_path": "plugins/brains/skills/brains-write/SKILL.md", - "artifact_sha256": "57f3839e5ddffa6c3d5fdad931b1a8f2bea16baac71fe0d68d05888b3caf1ddd" + "artifact_sha256": "52f9f4f66a63f739478bf387ed060e365cb30596827fd40010735774d209e39c" } diff --git a/plugins/brains/skills/brains-write/SKILL.md b/plugins/brains/skills/brains-write/SKILL.md index 6bcde9c..f71e484 100644 --- a/plugins/brains/skills/brains-write/SKILL.md +++ b/plugins/brains/skills/brains-write/SKILL.md @@ -25,10 +25,10 @@ it drafts or runs as unknown. Inline external writes include `rsvp_event`, |---|---|---| | `draft` | Nothing sent; it carries `preview`, `confirm_hint`, and `expires_at`. | Relay the preview and hint, then stop. | | `auto_executed` | It already ran; it carries `result` and `action_record_id`. | Report what happened, past tense. | -| `auto_failed` | It was attempted; whether an outbound write reached the provider is **unknown**. | Don't blind-retry. Read provider state or `/inbox` before re-sending; a pure read is safe to retry. | +| `auto_failed` | It was attempted; whether an outbound write reached the provider is **unknown**. | Don't blind-retry. Read provider state or `/inbox` before re-sending; retry only when the action is idempotent or the error proves nothing was sent. A pure read is safe to retry. | | `rate_limited` | Nothing ran and no upstream call occurred. | Retry after `retry_after_seconds`. | | `clarification` | The tuple was incomplete or ambiguous. | Answer its `question`, then resend the full tuple. | -| `noop` | The integration is unavailable. | Relay its `reason` and point to `/integrations`. | +| `noop` | The integration is unavailable, or the action was suppressed because an automation is running in verify mode. | Relay its `reason`. If it starts with `verify_mode:`, report that the action was safely suppressed; otherwise point to `/integrations`. | 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. From ae41e56cbff29cfae7c5ca06e24d62516f805895 Mon Sep 17 00:00:00 2001 From: stefan-ssv-labs Date: Wed, 29 Jul 2026 12:39:27 +0300 Subject: [PATCH 4/5] fix(core): regenerate side-effect safety contract [BRNS-CORE-008] --- plugins/brains/generated/capability-catalog.json | 4 ++-- plugins/brains/skills/brains-write/SKILL.md | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/brains/generated/capability-catalog.json b/plugins/brains/generated/capability-catalog.json index d2c4afa..3368ed8 100644 --- a/plugins/brains/generated/capability-catalog.json +++ b/plugins/brains/generated/capability-catalog.json @@ -5,7 +5,7 @@ "capability_id": "integration-actions", "source_repository": "https://github.com/ssvlabs/brains", "source_generator": "apps/mcp/scripts/generate-capability-catalog.ts", - "catalog_sha256": "3dbd82bdcd875359ab55efef1eb3c9bc325d3805246ecf50bc6617fcdfb43ed7", + "catalog_sha256": "b4846aca314bf10b984d30e4c6357f92c6a0d50bf46826f8748f0380c47fb737", "artifact_path": "plugins/brains/skills/brains-write/SKILL.md", - "artifact_sha256": "52f9f4f66a63f739478bf387ed060e365cb30596827fd40010735774d209e39c" + "artifact_sha256": "4f8a8da0d4897bfe554a6db64485f090c154cffca016f8cab38833e03b1b4438" } diff --git a/plugins/brains/skills/brains-write/SKILL.md b/plugins/brains/skills/brains-write/SKILL.md index f71e484..84828a1 100644 --- a/plugins/brains/skills/brains-write/SKILL.md +++ b/plugins/brains/skills/brains-write/SKILL.md @@ -18,7 +18,10 @@ slug to read frontmatter. Use its `install_id`, `action_name`, and structured `input` in `act_on_integration`; this tuple is the only call shape. `requires_confirmation:false` means the action runs inline. If `requires_confirmation` is absent, the page predates the field: treat whether -it drafts or runs as unknown. Inline external writes include `rsvp_event`, +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. | `kind` | What happened | What you do | From 1aa2662eff461d0be0eb8d1f1dff31141af72c16 Mon Sep 17 00:00:00 2001 From: stefan-ssv-labs Date: Thu, 30 Jul 2026 12:02:02 +0300 Subject: [PATCH 5/5] fix(core): harden generated action safety contract [BRNS-CORE-008] --- .../brains/generated/capability-catalog.json | 4 ++-- plugins/brains/skills/brains-write/SKILL.md | 14 +++++++---- tests/plugin-contract/run.ts | 24 +++++++++++++++++-- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/plugins/brains/generated/capability-catalog.json b/plugins/brains/generated/capability-catalog.json index 3368ed8..c349311 100644 --- a/plugins/brains/generated/capability-catalog.json +++ b/plugins/brains/generated/capability-catalog.json @@ -5,7 +5,7 @@ "capability_id": "integration-actions", "source_repository": "https://github.com/ssvlabs/brains", "source_generator": "apps/mcp/scripts/generate-capability-catalog.ts", - "catalog_sha256": "b4846aca314bf10b984d30e4c6357f92c6a0d50bf46826f8748f0380c47fb737", + "catalog_sha256": "c46b9cf2f88ed3cefe0f5766665c12f06dafaa0e56dba1f5586ab9cd56c39ac2", "artifact_path": "plugins/brains/skills/brains-write/SKILL.md", - "artifact_sha256": "4f8a8da0d4897bfe554a6db64485f090c154cffca016f8cab38833e03b1b4438" + "artifact_sha256": "f14839e4ed852771fc8dd91b0196f9caeeb63ce56dead747a53d83fd83dd9475" } diff --git a/plugins/brains/skills/brains-write/SKILL.md b/plugins/brains/skills/brains-write/SKILL.md index 84828a1..e8f4e6f 100644 --- a/plugins/brains/skills/brains-write/SKILL.md +++ b/plugins/brains/skills/brains-write/SKILL.md @@ -16,21 +16,25 @@ Discover with `query type=integration_action text=""`. It returns slug/title/snippet, so call `get_page` on the selected slug to read frontmatter. Use its `install_id`, `action_name`, and structured `input` in `act_on_integration`; this tuple is the only call -shape. `requires_confirmation:false` means the action runs inline. If +shape. Partial tuples error; only bare legacy `source` returns `clarification`. +`requires_confirmation:true` drafts for out-of-band approval; +`requires_confirmation:false` runs inline. 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. +Automation `dry_run` suppresses external writes to no-call `[DRY RUN]` drafts. | `kind` | What happened | What you do | |---|---|---| -| `draft` | Nothing sent; it carries `preview`, `confirm_hint`, and `expires_at`. | Relay the preview and hint, then stop. | -| `auto_executed` | It already ran; it carries `result` and `action_record_id`. | Report what happened, past tense. | +| `draft` | Nothing sent; it carries `preview`, `confirm_hint`, and `expires_at`. | Relay the preview and hint, then stop; only `draft` carries `confirm_hint`. | +| `auto_executed` | It already ran; it carries `result` and `action_record_id`. | Report what happened, past tense; never say it is awaiting approval. | | `auto_failed` | It was attempted; whether an outbound write reached the provider is **unknown**. | Don't blind-retry. Read provider state or `/inbox` before re-sending; retry only when the action is idempotent or the error proves nothing was sent. A pure read is safe to retry. | | `rate_limited` | Nothing ran and no upstream call occurred. | Retry after `retry_after_seconds`. | -| `clarification` | The tuple was incomplete or ambiguous. | Answer its `question`, then resend the full tuple. | +| `clarification` | A `source`-only legacy call reached the shim; no action ran. | Answer its `question`, then resend the full tuple. | | `noop` | The integration is unavailable, or the action was suppressed because an automation is running in verify mode. | Relay its `reason`. If it starts with `verify_mode:`, report that the action was safely suppressed; otherwise point to `/integrations`. | You can't confirm a draft from this loop. Only the user's out-of-band @@ -54,4 +58,4 @@ up short. Do not hardcode a default channel. Discover the messaging actions the user actually has installed. Use Gmail only when they say "email", provide an email -address, or are replying to or forwarding a thread. \ No newline at end of file +address, or are replying to or forwarding a thread. diff --git a/tests/plugin-contract/run.ts b/tests/plugin-contract/run.ts index a3ed1cb..8aa3181 100644 --- a/tests/plugin-contract/run.ts +++ b/tests/plugin-contract/run.ts @@ -147,9 +147,15 @@ assert( "absent requires_confirmation must remain unknown rather than predict a draft", ); assert( - writeSkillNormalized.includes("`requires_confirmation:false` means the action runs inline") && + writeSkillNormalized.includes("`requires_confirmation:true` drafts for out-of-band approval") && + writeSkillNormalized.includes("`requires_confirmation:false` runs inline") && writeSkillNormalized.includes("| `auto_executed` | It already ran; it carries `result` and `action_record_id`."), - "requires_confirmation:false must be documented as already executed", + "both requires_confirmation branches must retain their distinct behavior", +); +assert( + writeSkillNormalized.includes("Partial tuples error") && + writeSkillNormalized.includes("only bare legacy `source` returns `clarification`"), + "partial tuples must error; only the bare legacy source shim may clarify", ); assert(writeSkillNormalized.includes("there is no source-enum fallback"), "legacy source-enum fallback must stay removed"); assert(!writeSkillNormalized.includes("Fall back to the legacy source-enum"), "stale legacy fallback pointer must not return"); @@ -159,6 +165,10 @@ assert( writeSkillNormalized.includes("| `rate_limited` | Nothing ran and no upstream call occurred."), "rate-limited actions must be documented as not attempted", ); +assert( + writeSkillNormalized.includes("30 auto-executions/install/60s"), + "auto-execution rate-limit context missing", +); assert(writeSkillNormalized.includes("| `clarification` |"), "clarification result kind missing"); assert(writeSkillNormalized.includes("| `noop` |"), "noop result kind missing"); assert( @@ -166,6 +176,16 @@ assert( "auto-failed actions must preserve unknown-outcome guidance", ); assert(writeSkillNormalized.includes("Don't blind-retry"), "auto-failed external writes must not be blindly retried"); +assert( + writeSkillNormalized.includes("only `draft` carries `confirm_hint`") && + writeSkillNormalized.includes("never say it is awaiting approval"), + "draft and auto-executed reporting gates must remain distinct", +); +assert( + writeSkillNormalized.includes("`dry_run` suppresses external writes to no-call `[DRY RUN]` drafts"), + "dry-run external-write suppression missing", +); +assert(writeSkill.endsWith("\n"), "generated public skill must end with a newline"); assert(writeSkillNormalized.includes("out-of-band") && writeSkillNormalized.includes("confirmation secret"), "approval boundary missing"); assert(writeSkillNormalized.includes("never call `confirm_action` yourself"), "agent self-confirm prohibition missing"); assert(writeSkillNormalized.includes("call `discard_action`"), "agent-side draft discard path missing");