Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/stable-interactive-tool-rosters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@schlessera/brain-backend-claude": minor
"@schlessera/brain-backend-pi": minor
"@schlessera/brain-ui-sdk": minor
"@schlessera/brain-ui-server": minor
---

Keep interactive tool schemas stable across posture, strict fallback and temporary host capability changes. Refuse unavailable calls before privileged handlers or approval cards, preserving voice and sandbox authority.
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ on, links to or describes them.
declared with `defineModule({ name, configSchema, setup })`. Run
`brain module lint` before submitting one.
- Cite code from docs and source comments as an anchor followed by its range:
(`enforcementHook`, `packages/ui-backend-claude/src/permission-hooks.ts:182-205`),
(`enforcementHook`, `packages/ui-backend-claude/src/permission-hooks.ts:183-212`),
starting on the anchor's line. **A PR that moves lines under a citation owns
that citation**, wherever it is recorded; find them with
`git grep -n '<file>.ts:' -- docs packages`. `tests/decision-citations.test.ts`
Expand Down
1 change: 1 addition & 0 deletions api-report/ui-sdk.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9159,6 +9159,7 @@ signatures (every public export and the types it is made of)
turnBudgetMs?: number;
maxBudgetUsd?: number;
bridge: BackendBridge;
applicationAuthority?: "server";
client?: ClientEnvironment;
enforceAllowedTools?: boolean;
noGrantSurface?: boolean;
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ it is told something confidently wrong. So name what you point at, and let the
range follow it:

```md
(`enforcementHook`, `packages/ui-backend-claude/src/permission-hooks.ts:182-205`)
(`enforcementHook`, `packages/ui-backend-claude/src/permission-hooks.ts:183-212`)
```

- **The anchor** is the code span immediately before the citation, joined to it
Expand Down
6 changes: 3 additions & 3 deletions docs/decisions/async-collaboration.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ requires one enforcement mechanism with different named memberships for voice
and unattended work. Keep that requirement when implementing availability
control. Existing mandatory backend conformance supports safe rejection of an
unsupported restricted request; silently ignoring it is forbidden. The current
optional inputs are (`enforceAllowedTools?: boolean`, `packages/ui-sdk/src/server/backend.ts:529`) and (`noGrantSurface?: boolean`, `packages/ui-sdk/src/server/backend.ts:553`). These are permission primitives,
optional inputs are (`enforceAllowedTools?: boolean`, `packages/ui-sdk/src/server/backend.ts:535`) and (`noGrantSurface?: boolean`, `packages/ui-sdk/src/server/backend.ts:559`). These are permission primitives,
not a claim of filesystem or network containment.

The ordinary Claude assembly loads project settings and appends bridge tools
(`createClaudeSdkTurn`, `packages/ui-backend-claude/src/sdk-options.ts:85-264`).
(`createClaudeSdkTurn`, `packages/ui-backend-claude/src/sdk-options.ts:85-263`).
Its environment is already filtered (`envSnapshot`,
`packages/ui-backend-claude/src/config/env.ts:182-190`), with profile credentials
and operator extras. An autonomous envelope needs its own narrower credential
Expand All @@ -85,7 +85,7 @@ is historical. Preserve subscription billing and the selected runtime identity.
Pi disables built-in tools but currently loads resources and extensions
(`createSessionResources`, `packages/ui-backend-pi/src/session-resources.ts:29-144`)
and gates extension calls (`createPermissionGate`,
`packages/ui-backend-pi/src/permission-gate.ts:77-104`). Removing four curated
`packages/ui-backend-pi/src/permission-gate.ts:95-124`). Removing four curated
tools cannot prove that extensions, MCP, scratch writers or in-process code have
no egress or write access. Both first-party runtimes owe executable evidence;
the plan's earlier “pi is easy” claim is not a containment result.
Expand Down
4 changes: 2 additions & 2 deletions docs/decisions/claude-2.1.287-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,9 @@ call, including when settings or a rewrite could otherwise auto-approve it.
The backend builds the actual turn options and production permission handlers
(`createClaudeSdkTurn`, `packages/ui-backend-claude/src/sdk-options.ts:85-202`).
The explicit ask lives in
(`enforcementHook`, `packages/ui-backend-claude/src/permission-hooks.ts:182-205`)
(`enforcementHook`, `packages/ui-backend-claude/src/permission-hooks.ts:183-212`)
and is registered under enforcement in
(`...(enforced ?`, `packages/ui-backend-claude/src/permission-hooks.ts:529-532`).
(`...(enforced ?`, `packages/ui-backend-claude/src/permission-hooks.ts:540-543`).

`scripts/measure-claude-enforcement.ts` runs `createClaudeBackend().startTurn()`
and forwards to the SDK resolved from that backend. Its query wrapper only
Expand Down
12 changes: 6 additions & 6 deletions docs/decisions/claude-code-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ next to it.
`packages/ui-server/src/agent/backend.ts:497`), read back as a string
(`const claudeCodePath`, `packages/ui-backend-claude/src/module.ts:266-270`)
and handed to the SDK (`backend.claudeCodePath`,
`packages/ui-backend-claude/src/sdk-options.ts:263`). Because of the `||`
`packages/ui-backend-claude/src/sdk-options.ts:262`). Because of the `||`
default the value was never empty, so **the server always overrode the SDK's
own binary**. The variable is withheld from every subprocess
(`CLAUDE_CODE_PATH: NONE`,
Expand Down Expand Up @@ -276,7 +276,7 @@ a permission boundary:
- **A refusal would not buy what it appears to.** The measured version is not
known-safe either: the list of mechanisms that skip `canUseTool` is what has
been measured, not a closed set (`AT LEAST three`,
`permission-hooks.ts:150-153`). A version gate would separate "probed" from "not
`permission-hooks.ts:151-154`). A version gate would separate "probed" from "not
probed", not "safe" from "unsafe", and it would say the second thing to
whoever reads it.
- **It would block the path a CLI fix takes.** The built-in binary moves only
Expand Down Expand Up @@ -356,7 +356,7 @@ so it holds itself to these rules:
about another.
- **Composition, not only parts.** The enforcement hook's `ask` must leave
another hook's `updatedInput` intact (`An explicit "ask" beats all three`,
`permission-hooks.ts:177-179`; `(e) a hook's`,
`permission-hooks.ts:178-180`; `(e) a hook's`,
`allowlist-enforcement.test.ts:18-21`). That is its own case: a rewrite, an
`ask` and a `canUseTool` decision in one call, asserting the input the
callback saw and the input that executed.
Expand All @@ -383,11 +383,11 @@ until re-measured.

| Site | What it asserts | Re-checked by |
| --- | --- | --- |
| `DO NOT WEAKEN THIS INTO A FALLTHROUGH`, `packages/ui-backend-claude/src/permission-hooks.ts:149-180` | Three things admit a tool before `canUseTool` — the safe-command classifier (`echo hi` with an empty allowlist), a built-in tool's own check (`ToolSearch`), a project-settings `PreToolUse` hook returning `allow` — and an explicit `ask` beats all three. `permissions.allow` rules and `defaultMode: "bypassPermissions"` do not bypass. | Probe cases for each of the three, each with and without the `ask`, plus the two negative controls. |
| `DO NOT WEAKEN THIS INTO A FALLTHROUGH`, `packages/ui-backend-claude/src/permission-hooks.ts:150-181` | Three things admit a tool before `canUseTool` — the safe-command classifier (`echo hi` with an empty allowlist), a built-in tool's own check (`ToolSearch`), a project-settings `PreToolUse` hook returning `allow` — and an explicit `ask` beats all three. `permissions.allow` rules and `defaultMode: "bypassPermissions"` do not bypass. | Probe cases for each of the three, each with and without the `ask`, plus the two negative controls. |
| `Both hooks exist to rewrite`, `packages/ui-backend-claude/src/input-rewrite-hooks.ts:8-15` | A hook's `updatedInput` applies with no decision, and the permission path sees the rewritten input. | Probe case: rewrite with no decision; assert the executed input and the `canUseTool` input. |
| `The runtime precedence modelled by`, `packages/ui-backend-claude/tests/allowlist-enforcement.test.ts:11-20` | The precedence `runToolCall` models, (a)–(e). The test cannot re-measure it. | The cases above plus the composition case. A changed result changes the model in the test in the same PR. |
| `the one answer that beats`, `packages/ui-backend-claude/tests/no-grant-surface.test.ts:13-18` | The same three opinions, and that `ask` is what forces the decision. | Same probe cases. |
| `That third vector is stated here`, `docs/decisions/voice-permission.md:629-655` | An in-process `deny` beats a project-settings `allow`. | Probe case: settings `allow` against in-process `deny`, with the positive control of the settings hook alone running the tool. |
| `That third vector is stated here`, `docs/decisions/voice-permission.md:645-671` | An in-process `deny` beats a project-settings `allow`. | Probe case: settings `allow` against in-process `deny`, with the positive control of the settings hook alone running the tool. |
| `Three measured examples from the Claude SDK`, `docs/extending/agent-backends.md:291-304` | The same three mechanisms and the `ask`, restated for backend authors with no version attached. | Updated in the same PR as the constant whenever a probe result changes. |
| `createSdkMcpServer({ alwaysLoad: true })`, `docs/decisions/design-kit/tool-loading.md:21-44` (D44) | Two different kinds of claim. That `createSdkMcpServer({ alwaysLoad })` stamps `_meta["anthropic/alwaysLoad"]` is SDK behaviour, asserted keylessly by `"anthropic/alwaysLoad"`, `packages/ui-backend-claude/tests/sdk-options-mcp.test.ts:82` and `"anthropic/alwaysLoad"`, `tests/bridge-tools.test.ts:816,834`. That the CLI honours the stamp, and that first-frame latency did not move, is CLI behaviour. | The SDK half by the existing tests. The CLI half needs a live run of both arms — stamp set and unset — on the new pair, recording the pair from `init` and observing whether the bridge tools reached the model undeferred. `scripts/measure-show-block.ts` can run either arm (with and without `--always-load`; `ALWAYS_LOAD`, `scripts/measure-show-block.ts:304,369`), but it records no version and nothing in it compares the two arms or checks deferral, so it does not re-check this as it stands. Extending it is part of #209. `--tokens` prices schemas through the API and never runs the CLI, so it re-checks nothing here. |

Expand All @@ -408,7 +408,7 @@ brain-hosting-template.

None of the live sites is accepted as unverifiable. What stays unverifiable is
the absence of a *fourth* mechanism — `AT LEAST three`,
`permission-hooks.ts:150-153` already says three is what has been measured, not a
`permission-hooks.ts:151-154` already says three is what has been measured, not a
closed set — and no probe can close that.

## Where the work goes
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/design-kit/show-block-brief.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ no route to `trend`, `bars` or `contact`:
**Why the no-brief arm is near zero, which is the actual finding.** Not
reluctance. The SDK **defers an MCP server's tools behind tool search by
default** — they are not in the model's context at all until it runs
`ToolSearch` — and `packages/ui-backend-claude/src/ask-user-tool.ts:107` does
`ToolSearch` — and `packages/ui-backend-claude/src/ask-user-tool.ts:109` does
not pass `alwaysLoad`, so this is production's behaviour and the harness
inherits it. Over the two runs below — **run A**, 108 turns, taken before the
turn budget was enforced and therefore losing none, and **run B**, the 108-turn
Expand Down
6 changes: 3 additions & 3 deletions docs/decisions/governed-writable-workers.md
Original file line number Diff line number Diff line change
Expand Up @@ -969,8 +969,8 @@ classification:
indexing. `write_file` and `edit_file` remain the way to write under
`strict`.
- **Native tools return under `governed`.** In Claude the
`Write`/`Edit`/`NotebookEdit` disallow is lifted
(`disallowedTools`, `packages/ui-backend-claude/src/sdk-options.ts:226`). In
`Write`/`Edit`/`NotebookEdit` stay registered under #2048 A; strict/voice
refusal moves to call time (`toolAvailabilityRefusal`, `packages/ui-backend-claude/src/tool-availability.ts:16-27`). In
pi, the direct tools and a `bash` whose working directory is the brain root
return (`const bash`, `packages/ui-backend-pi/src/tools.ts:541`).
- **Approvals do not change.** Confirmation patterns and destructive-operation
Expand All @@ -991,7 +991,7 @@ canonical bytes for a frozen view (`createWorkerFileObserver`,
read cannot establish a new version. Claude's native `file_unchanged` response
also preserves the previous observation: it supplied no new bytes
(`const finishNativeFile`,
`packages/ui-backend-claude/src/permission-hooks.ts:89`).
`packages/ui-backend-claude/src/permission-hooks.ts:90`).

Claude keeps these observations in its backend-owned session map
(`const fileVersionsBySession`,
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/live-conversation.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ tool admission and execution, permissions, cancellation, terminal state and
resynchronization. Engine task requests are advisory until mapped to committed
user input. No native search, code-execution or alternate permission loop is
enabled. Both adapters can delegate through the existing `AgentBackend`
(`export interface AgentBackend`, `packages/ui-sdk/src/server/backend.ts:634-664`);
(`export interface AgentBackend`, `packages/ui-sdk/src/server/backend.ts:640-670`);
the host serializes work in a session and uses capability-checked follow-ups.

The [voice-permission decision](voice-permission.md) binds this interface.
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/policy-write-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ application, refusal and recovery.

This is a change to the execution architecture. Current Claude assembly creates
SDK options and in-process bridge tools (`createClaudeSdkTurn`,
`packages/ui-backend-claude/src/sdk-options.ts:85-262`); pi builds its worker-local resource
`packages/ui-backend-claude/src/sdk-options.ts:85-261`); pi builds its worker-local resource
loader and calls `reload()` (`createSessionResources`,
`packages/ui-backend-pi/src/session-resources.ts:29-113`). A child-only wrapper
cannot be treated as proof for those parent-side paths. The investigation records
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/session-principals.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Both reviewers verified every row below against the source.
| WS upgrade | `ws/connection.ts:262-264` | `createWsUpgrade` ignores the request context |
| WS admission | `ws/connection.ts:84` | `clients.add(ws)` — the socket's identity is unknown |
| Turn record | `ws/run-session.ts:134-142` | recorder built from `{turnId, sessionId, billing}`; `RunningTurn` (`ws/turns.ts:55-77`) has no actor |
| Follow-up queue | `QueuedFollowUp`, `ws/turns.ts:61-103`; `slot.queue.push(entry)`, `run-session.ts:895` | queued entries re-mint `turnId` in the same slot and may come from another socket |
| Follow-up queue | `QueuedFollowUp`, `ws/turns.ts:61-103`; `slot.queue.push(entry)`, `run-session.ts:896` | queued entries re-mint `turnId` in the same slot and may come from another socket |
| Password login | `acquirePasswordVerification(key)`, `auth.ts:740` | argon2id verify, failure counting, in-flight reservation, then `issueSessionCookie` |
| Passkey login | `issueLoginSession`, `packages/ui-server/src/middleware/passkeys.ts:442` | assertion verified, then `issueSessionCookie`; `row.id` is in scope |
| Passkey **registration** | `INSERT INTO passkey_credentials`, `passkeys.ts:540` | inserts a credential; calls **neither** helper |
Expand Down
Loading
Loading