diff --git a/plugins/brains/.claude-plugin/plugin.json b/plugins/brains/.claude-plugin/plugin.json index 5482872..70b13ff 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.8.3", + "version": "2.9.0", "author": { "name": "brains (ssvlabs)" }, diff --git a/plugins/brains/.codex-plugin/plugin.json b/plugins/brains/.codex-plugin/plugin.json index f715ee6..52ab452 100644 --- a/plugins/brains/.codex-plugin/plugin.json +++ b/plugins/brains/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "brains", - "version": "2.8.3", + "version": "2.9.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 4171ad4..5d698d7 100644 --- a/plugins/brains/generated/capability-catalog.json +++ b/plugins/brains/generated/capability-catalog.json @@ -9,13 +9,13 @@ "capability_id": "integration-actions", "catalog_sha256": "29f58ddc5dab294814934fe7af0d808f4b7dc441e3c3d29bb0e67fa705d7add4", "artifact_path": "plugins/brains/skills/brains-write/SKILL.md", - "artifact_sha256": "b27acd2ffc1ba4914890a88dba00e71f7052bd738369f28f9d86a1566bd2f548" + "artifact_sha256": "21f7cd40bedb7602efbbd43f718c1e5e78e41a19de3d96f3dd599b95fb17732d" }, { "capability_id": "brains-features", - "catalog_sha256": "6086cbe9877b2005b3cea06611ba74ab7b527eaed04af39e5a815c94c8806b37", + "catalog_sha256": "a1677c30594d44e516a3aaa8e6524829360e44d90f8304d2ab8291b7e5ed6a3a", "artifact_path": "plugins/brains/skills/brains-build/SKILL.md", - "artifact_sha256": "753afa4d8b4833b91622fa8c55d2bc76304e9a9170faed3365c7febd53836ae0" + "artifact_sha256": "f9196254dd49af3cd8bb1d4d93b1ba36ff10d8efc0bb039fb464901e143e143f" }, { "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": "e71b5648d93e611de49fd81549217db8232427262e282af6d1c1d70c9c2e17c9" + "artifact_sha256": "78c63d6a09ffdf55f80f3b686b1cd0d1000585bc1d5d148323ea03ebda018bc2" }, { "capability_id": "procedure:workflow", @@ -36,5 +36,5 @@ "artifact_sha256": "b22814fd630ed78964d54440ec327e3aebcde8c812aaa9985fff9c4d345d3f09" } ], - "source_commit": "864c45fa7e1c4b4d60116b6697abc596a272fbff" + "source_commit": "e332512644b2f06a6b7068dcc9196eab2d31c05a" } diff --git a/plugins/brains/skills/brains-automation/SKILL.md b/plugins/brains/skills/brains-automation/SKILL.md index c536dbf..4cb3388 100644 --- a/plugins/brains/skills/brains-automation/SKILL.md +++ b/plugins/brains/skills/brains-automation/SKILL.md @@ -149,8 +149,9 @@ 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 `brains.act`; this tuple is the only call 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:true` drafts for out-of-band approval unless this +automation's owner set `write_policy:'auto_confirm_safe'`, which executes it +inline; `requires_confirmation:false` runs inline either way. 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; diff --git a/plugins/brains/skills/brains-build/SKILL.md b/plugins/brains/skills/brains-build/SKILL.md index 65b4feb..2ca0059 100644 --- a/plugins/brains/skills/brains-build/SKILL.md +++ b/plugins/brains/skills/brains-build/SKILL.md @@ -15,6 +15,7 @@ Don't restate that artifact here — open it. |---|---|---| | **Board** | A spreadsheet-like dataset on a brain — rows are JSON, shared and queryable. | `create_board_flow` if your client exposes it (follow the playbook it returns), otherwise `create_board` | | **Board skill** | A named, saved LLM action over a board's rows. | `create_board_skill` | +| **Board form** | A shareable page where someone else answers your questions in chat — one board row per submission. | `create_board_form` | | **Bulk upload** | Load a large or arbitrary-format file into a board without reading it into context. | `create_import_grant` → `bulk_append_rows` | | **Dashboard** | A live, interactive view of a board that you author as a full HTML app. | `get_dashboard` → `set_dashboard` | | **Page** | A first-class note in a brain. | `create_page` | @@ -22,9 +23,11 @@ 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` | | **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` | +| **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 | Reach for a `*_flow` tool for any create/set-up/scaffold ask: it returns the diff --git a/plugins/brains/skills/brains-write/SKILL.md b/plugins/brains/skills/brains-write/SKILL.md index 4be3069..e98d9e7 100644 --- a/plugins/brains/skills/brains-write/SKILL.md +++ b/plugins/brains/skills/brains-write/SKILL.md @@ -17,8 +17,13 @@ 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. 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:true` normally drafts for out-of-band approval; +`requires_confirmation:false` runs inline. A user may additionally enable direct +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 `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; diff --git a/tests/plugin-contract/run.ts b/tests/plugin-contract/run.ts index 1670e62..0b9d9ba 100644 --- a/tests/plugin-contract/run.ts +++ b/tests/plugin-contract/run.ts @@ -1602,6 +1602,62 @@ for (const dir of skillDirs) { ); } +// The whole rendered action contract, pinned verbatim. The `includes` assertions +// below stay — they name WHICH rule broke, which "the paragraph changed" cannot — +// but presence is not enough on its own: `includes` proves a sentence is there and +// says nothing about what sits NEXT to it. Demonstrated during review: leaving +// every pinned sentence intact and INSERTING "the setting is off for almost +// everyone, so plan on the draft" straight after the `draft_id` rule kept every +// substring check green while reversing what the contract tells an agent to do. +// Only whole-region equality refuses an addition. +// +// BOUNDARIES. Start: the `Discover with …` line that opens the contract. End: the +// `| \`kind\` |` table header, EXCLUSIVE — so the region covers the entire prose +// block and an insertion anywhere in it, including just past the `draft_id` +// sentence where the review's attack landed, falls INSIDE the pin. Stopping at the +// `draft_id` sentence would leave that exact evasion one character outside the +// region and change nothing. Both anchors are structural rather than editorial: +// the opener is the first line every rendered face of this contract starts with, +// and the table header is a fixed column row, so neither moves under a reword of +// the prose between them. +// +// COST, accepted deliberately: this reddens on EVERY upstream reword of the +// paragraph, including harmless ones. That is the point — this text tells an agent +// when a call may fire an external write without asking, so a human should have to +// read each change to it before it ships to users. +// +// TO UPDATE (the only correct procedure): read the upstream diff and satisfy +// yourself the new wording is true of what the server does, then replace this +// constant with the new bytes. Do NOT relax it back to a substring or trim the +// region to make a sync green — that reintroduces the additive hole above. +const WRITE_ACTION_CONTRACT_REGION = [ + "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. Partial tuples error; only bare legacy `source` returns `clarification`.", + "`requires_confirmation:true` normally drafts for out-of-band approval;", + "`requires_confirmation:false` runs inline. A user may additionally enable direct", + "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", + "`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.", +].join("\n"); +// Asserted AFTER the per-rule checks below, deliberately: they run first and name +// the specific rule that broke, and this one is the catch-all for everything they +// cannot see — an addition, a reflow, a sentence nobody thought to pin. Ordered the +// other way it fires first on every in-region edit and the precise messages never +// surface, which is the whole reason for keeping them. + // Keep independent semantic assertions: digest equality proves provenance, not // that the canonical source itself kept the load-bearing safety rules. assert( @@ -1615,12 +1671,29 @@ assert( writeSkillNormalized.includes("treat whether it drafts or runs as unknown"), "absent requires_confirmation must remain unknown rather than predict a draft", ); +// `normally` is load-bearing, not hedging: a user who enables direct execution +// has an allowlisted action run inline from a local CLI despite +// `requires_confirmation:true`. The absolute wording this used to pin became +// false server-side, so the pin follows the truth rather than the other way +// round. The other two clauses are unchanged. assert( - writeSkillNormalized.includes("`requires_confirmation:true` drafts for out-of-band approval") && + writeSkillNormalized.includes("`requires_confirmation:true` normally 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`."), "both requires_confirmation branches must retain their distinct behavior", ); +// The rule that carries the weight once `true` can execute: the caller must not +// plan on a draft. Pinned as ONE CONTIGUOUS run of the rendered sentence, not as +// separate `includes` calls for "decided server-side" and "never assume a +// `draft_id`" — two fragments can each be present in different sentences, in +// different sections, or under a contradicting condition, which pins the +// vocabulary while letting the rule itself be reworded away. +assert( + writeSkillNormalized.includes( + "The mode is decided server-side per call, so treat any call as potentially executing and never assume a `draft_id`.", + ), + "per-call server-side mode and the never-assume-a-draft_id rule must stay one intact sentence", +); assert( writeSkillNormalized.includes("Partial tuples error") && writeSkillNormalized.includes("only bare legacy `source` returns `clarification`"), @@ -1663,6 +1736,20 @@ assert(writeSkillNormalized.includes("remains approvable"), "expired drafts must 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"); +// The catch-all for the contract paragraph — see WRITE_ACTION_CONTRACT_REGION above. +const writeContractStart = writeSkill.indexOf("Discover with `query type=integration_action"); +const writeContractEnd = writeSkill.indexOf("| `kind` | What happened |"); +assert(writeContractStart > 0, "brains-write must keep its action-contract opener"); +assert( + writeContractEnd > writeContractStart, + "brains-write's action contract must precede the kind table — the slice below depends on it", +); +assert( + normalizeRegion(writeSkill.slice(writeContractStart, writeContractEnd)) === + WRITE_ACTION_CONTRACT_REGION, + "brains-write's action contract must match the approved copy exactly — an inserted sentence can contradict a rule the per-rule checks above still find (regenerate upstream, re-read the change, then update WRITE_ACTION_CONTRACT_REGION)", +); + // Same treatment for brains-automation, and for the same reason: the digest proves // these bytes came from the catalog, not that the catalog kept the rails. Nothing // here would notice if a regeneration DELETED the secret-hygiene guidance — the