Skip to content
Merged
15 changes: 15 additions & 0 deletions .changeset/host-backend-exec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@schlessera/brain-ui-sdk": minor
"@schlessera/brain-backend-claude": minor
"@schlessera/brain-backend-pi": minor
"@schlessera/brain-ui-server": minor
---

Pass each app's resolved exec wrapper through backend construction, runtime
probes, Claude SDK workers and Pi tools. Explicit empty host configuration
disables the optional wrapper; standalone callers omitting it retain the
ambient fallback at the existing turn and tool-spawn boundaries.
Pi workers see each configured helper file, and only that file, in ordinary and
restricted views; a wrapper or killer that is not a regular file is refused
before any relay or worker starts, as is a nonregular wrapper for an ordinary
Claude worker.
1 change: 1 addition & 0 deletions api-report/ui-backend-claude.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ signatures (every public export and the types it is made of)
}
ClaudeBackendOptions (packages/ui-backend-claude/src/options.ts)
export interface ClaudeBackendOptions {
exec?: import("@schlessera/brain-ui-sdk/server").ExecWrapperConfig;
maintenance?: import("@schlessera/brain-ui-sdk/internal").MaintenanceHost;
versionRequirements?: BackendVersionRequirements;
brainPath: string;
Expand Down
1 change: 1 addition & 0 deletions api-report/ui-backend-pi.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ signatures (every public export and the types it is made of)
export type BackendLogFn = (level: "debug" | "info" | "warn" | "error", message: string, attrs?: Record<string, string | number | boolean>) => void;
CreatePiBackendOptions (packages/ui-backend-pi/src/backend-options.ts)
export interface CreatePiBackendOptions {
exec?: import("@schlessera/brain-ui-sdk/server").ExecWrapperConfig;
maintenance?: import("@schlessera/brain-ui-sdk/internal").MaintenanceHost;
versionRequirements?: BackendVersionRequirements;
brainPath: string;
Expand Down
2 changes: 2 additions & 0 deletions api-report/ui-sdk.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,7 @@ export "./internal" (./src/internal.ts)
webSearchProvider
workerCleanupUnconfirmed
workerCommandPolicyNote
workerExecHelpers
workerHostBoundary
workerHostProven
workerIdentityHelper
Expand Down Expand Up @@ -5108,6 +5109,7 @@ signatures (every public export and the types it is made of)
}
BackendModuleContext (packages/ui-sdk/src/server/backend-module.ts)
export interface BackendModuleContext {
exec?: import("./exec-wrapper.js").ExecWrapperConfig;
maintenance?: import("./maintenance-worker.js").MaintenanceHost;
brainPath: string;
config: Readonly<Record<string, unknown>>;
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/async-collaboration.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ and configuration audience; the old plan's “full host environment” descripti
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`)
(`createSessionResources`, `packages/ui-backend-pi/src/session-resources.ts:29-145`)
and gates extension calls (`createPermissionGate`,
`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
Expand Down
4 changes: 2 additions & 2 deletions docs/decisions/claude-code-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ next to it.
`packages/ui-server/src/config/env.ts:506-506`) and is null when unset
(`claudeCodePath`, `packages/ui-server/src/config/env.ts:1075`). The whole `agent` block is copied into the backend's module
config (`config: { ...agent }`,
`packages/ui-server/src/agent/backend.ts:500`), read back as a string
`packages/ui-server/src/agent/backend.ts:505`), 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:351`). Because of the `||`
Expand All @@ -59,7 +59,7 @@ next to it.
spawned it directly at this baseline. #1037 subsequently made worker execution
unconditional; #1808 retired this unused adapter. The optional exec wrapper
now runs inside the worker (`createWorkerSpawn`,
`packages/ui-backend-claude/src/worker-spawn.ts:86-291`).
`packages/ui-backend-claude/src/worker-spawn.ts:86-293`).
- **Updated.** Nothing in the tree installs, updates, pins or reads the version
of this binary. The only version probe in the server is for the `brain` CLI
(`Probe the selected content CLI`,
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/design-kit/answer-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ turns the complete roster the model reached for was `bash`, `show_block`,
`brain_read`, `grep`, `brain_search`, `read_file`, `brain_list`, `brain_graph`:
no search-then-load round trip, ever. The brief is in the prompt on every turn
unconditionally (`block: "show_block"`,
`packages/ui-backend-pi/src/session-resources.ts:200`, not behind a capability
`packages/ui-backend-pi/src/session-resources.ts:201`, not behind a capability
check like the four bridge tools beside it). So pi is the structural twin of
D43's `--always-load` arm and has never run any other configuration.

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 @@ -204,7 +204,7 @@ is in the prompt on every pi turn by construction. That makes pi's shipping
configuration the structural equivalent of this record's `--always-load`
**brief** arm — and only that one. pi has no no-brief arm and no supported way
to have one: `block: "show_block"`,
`packages/ui-backend-pi/src/session-resources.ts:200` passes the
`packages/ui-backend-pi/src/session-resources.ts:201` passes the
block brief unconditionally, where the four lines above it gate their briefs on
a capability. So pi can corroborate the loaded *rate* and can say nothing at
all about whether the brief matters; the 77%/77% here and #148's 76%/77% are
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/design-kit/tool-loading.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ Two things stop that residue being read as a like-for-like gap, and both cut
against reading pi as a second replication of the brief result. pi is on a
different harness driving the model directly, and — this is the one that matters
— **pi has no no-brief arm and no supported way to have one.**
`block: "show_block"`, `packages/ui-backend-pi/src/session-resources.ts:200`
`block: "show_block"`, `packages/ui-backend-pi/src/session-resources.ts:201`
passes the block brief into `buildSystemPromptAppend` unconditionally, not
behind a capability check like `askUser`, `location`, `activity` and `mask` on
the lines above it. So every pi number was measured with the brief present. The
Expand Down
8 changes: 4 additions & 4 deletions docs/decisions/governed-writable-workers.md
Original file line number Diff line number Diff line change
Expand Up @@ -1078,7 +1078,7 @@ classification:
`Write`/`Edit`/`NotebookEdit` disallow is lifted
(`disallowedTools`, `packages/ui-backend-claude/src/sdk-options.ts:312`). 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`).
return (`const bash`, `packages/ui-backend-pi/src/tools.ts:543`).
- **Approvals do not change.** Confirmation patterns and destructive-operation
approvals still run in the permission hooks. The journal adds the ability to
undo; it removes no approval.
Expand Down Expand Up @@ -1710,11 +1710,11 @@ helper the host provides:

- **brain-kit owns the launcher seam** (#1677). An optional host-supplied
identity helper sits in front of `bwrap`. It follows the exec wrapper's
rules (`export const EXEC_WRAPPER_ENV`, `packages/ui-sdk/src/server/exec-wrapper.ts:39`):
rules (`export const EXEC_WRAPPER_ENV`, `packages/ui-sdk/src/server/exec-wrapper.ts:42`):
an absolute path in argv position 0, and the worker as its own process
group. `kill(2)` across uids returns `EPERM`, so every signal goes through
the kill helper. The helper's protocol
(`<killer> <pgid> <TERM|KILL|INT>`, `packages/ui-sdk/src/server/exec-wrapper.ts:53`)
(`<killer> <pgid> <TERM|KILL|INT>`, `packages/ui-sdk/src/server/exec-wrapper.ts:56`)
gains `STOP` and `CONT`. A scope cap without a delegated cgroup stops and
resumes the worker through them (see "Scope caps"). Signalling the process
group is not enough. `bwrap --new-session` puts the payload in a session and
Expand Down Expand Up @@ -2178,7 +2178,7 @@ is now consumer-qualified independently of reference classification (#2028). The
This mapping was checked against core's (`loadModules`,
`packages/core/src/lib/module-loader.ts:95-103`), (`importManifest`,
`packages/core/src/lib/module-loader.ts:192-224`), Pi's (`build`,
`packages/ui-backend-pi/src/session-resources.ts:85-129`) and the installed
`packages/ui-backend-pi/src/session-resources.ts:86-130`) and the installed
Pi 0.99.2 package-manager/settings consumers and Claude SDK 0.3.293 settings
schema. Nothing imports or evaluates repository configuration on the host.
Followed JSON resources do not acquire configuration-consumer semantics merely
Expand Down
8 changes: 4 additions & 4 deletions docs/decisions/http-api-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ explicit supported rows, not silently excluded by this distinction.

The source audit on 2026-09-30 used
`e0875269b21b707482545be9e94901acb897893b`. The public health mount precedes auth
(`createHealthRoutes`, `packages/ui-server/src/app.ts:770`), the API guard follows
public ceremonies (`authGuard`, `packages/ui-server/src/app.ts:791`), and the
WebSocket admission is separate (`"/ws"`, `packages/ui-server/src/app.ts:973`).
(`createHealthRoutes`, `packages/ui-server/src/app.ts:771`), the API guard follows
public ceremonies (`authGuard`, `packages/ui-server/src/app.ts:792`), and the
WebSocket admission is separate (`"/ws"`, `packages/ui-server/src/app.ts:974`).
Static serving is conditional (`if (options.staticRoot)`,
`packages/ui-server/src/app.ts:993-1014`). That order establishes access;
`packages/ui-server/src/app.ts:994-1015`). That order establishes access;
it does not select compatibility status.

A hermetic real-app launch, with temporary brain root, in-memory database,
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/module-mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ failed.

- **The pi backend.** It does not use `brain mcp`. It builds its brain tools
in-process with its own names (`export const TOOL_RISK`,
`packages/ui-backend-pi/src/tools.ts:223`), and it keeps `bash`, so
`packages/ui-backend-pi/src/tools.ts:225`), and it keeps `bash`, so
`brain jobs review` is reachable there already. Serving module tools on pi
would need collision rules against pi's flat namespace (`read_file`,
`ask_user` and `web_search` all parse as `<module>_<local>`). It needs its
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/pi-endpoint-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ The concrete adapter integration points are:
| Declared-model validation (`export function toModel`, `packages/ui-backend-pi/src/profiles.ts:57-77`) | Keep built-in validation and identity refusal, then select the configured model from the inference runtime. The current function returns the raw catalog object. |
| Ordinary/autonomous new sessions (`async newSession`, `packages/ui-backend-pi/src/native-session-runtime.ts:41-72`) | Supply the native configured runtime and its selected model to the real `createAgentSession` call. The worker uses `SessionManager.inMemory`; the server persists ordinary JSONL over pipes, and autonomous turns remain nonpersistent. |
| Ordinary persisted resumes (`async openSession`, `packages/ui-backend-pi/src/native-session-runtime.ts:74-109`) | Supply the configured runtime without a new model override; reject/dispose if `modelFallbackMessage` is present. |
| Native resources (`const agentDir = getAgentDir()`, `packages/ui-backend-pi/src/session-resources.ts:89-128`) | Use the same native agent-directory discovery as settings/resources. Preserve loader failure behavior and its inline permission gate. |
| Native resources (`const agentDir = getAgentDir()`, `packages/ui-backend-pi/src/session-resources.ts:90-129`) | Use the same native agent-directory discovery as settings/resources. Preserve loader failure behavior and its inline permission gate. |

Runtime construction belongs at these concrete session boundaries. No runtime
factory option, new public seam or authentication wrapper is needed. Each hosted turn reconstructs its native inference runtime inside a fresh
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 @@ -45,7 +45,7 @@ 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:96-472`); 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
`packages/ui-backend-pi/src/session-resources.ts:29-114`). A child-only wrapper
cannot be treated as proof for those parent-side paths. The investigation records
the concrete unsandboxed extension-initialization escape.

Expand Down
4 changes: 2 additions & 2 deletions docs/decisions/runtime-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,8 @@ reciprocal host-version negotiation is involved.
| `probeBrainCliVersion` / `brainCliCommand` (`export async function probeBrainCliVersion`, `packages/ui-server/src/brain/client.ts:122-189`) | Reuse executable selection, wrapper, deadline and cleanup; add host floor composition, strict comparison and invocation verification without duplicating the 0.33.0 constant. |
| `installedAgentSdkVersion` (`export function installedAgentSdkVersion`, `packages/ui-backend-claude/src/runtime-probe.ts:55-61`) | Retain importer-relative resolution; validate package name/version and the owner's manifest range. |
| `selectedSpawn` / `probeClaudeRuntime` (`export function selectedSpawn`, `packages/ui-backend-claude/src/runtime-probe.ts:69-96`; `export async function probeClaudeRuntime`, `packages/ui-backend-claude/src/runtime-probe.ts:127-147`) | Keep SDK-selected command capture, native/JS override argv, environment, timeout and group cleanup; add strict identity validation and composed runtime floors. |
| `probeBackendRuntimes` (`export async function probeBackendRuntimes`, `packages/ui-server/src/agent/backend.ts:392-439`) | Forward validated host requirements; retain active-backend selection and separate unmeasured-pair logging. |
| `BackendModuleContext` / `BackendRuntimeReport` (`export interface BackendModuleContext`, `packages/ui-sdk/src/server/backend-module.ts:96-109`; `export interface BackendRuntimeReport`, `packages/ui-sdk/src/server/backend-module.ts:116-129`) | Extend the existing construction context for optional minimums. Keep report identities and `measured` meanings; a compatibility result must never occupy `measured.matches`. |
| `probeBackendRuntimes` (`export async function probeBackendRuntimes`, `packages/ui-server/src/agent/backend.ts:394-444`) | Forward validated host requirements; retain active-backend selection and separate unmeasured-pair logging. |
| `BackendModuleContext` / `BackendRuntimeReport` (`export interface BackendModuleContext`, `packages/ui-sdk/src/server/backend-module.ts:96-111`; `export interface BackendRuntimeReport`, `packages/ui-sdk/src/server/backend-module.ts:118-131`) | Extend the existing construction context for optional minimums. Keep report identities and `measured` meanings; a compatibility result must never occupy `measured.matches`. |
| `MEASURED_RUNTIME`, its guard and `scripts/measure-claude-runtime.ts` | Keep the constant, exact pair guard, keyless behavior probe and dated measurement receipts. A permitted pair can still be unmeasured. |
| Turn `reportRuntime` / `init` (`const reportRuntime`, `packages/ui-backend-claude/src/turn-runner.ts:169-176`; `if (msg.type === "system"`, `packages/ui-backend-claude/src/turn-runner.ts:587-602`) | Preserve actual per-turn SDK/runtime observations, billing/permission guards and normal failure terminal behavior. |
| Index schema and graph floors | Remain reader-specific integer schema checks; do not compare them as SemVer or turn `brain.db` into authoritative state. |
Expand Down
6 changes: 3 additions & 3 deletions docs/decisions/session-principals.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ Both reviewers verified every row below against the source.
| Cookie verify | `auth.ts:265-296` | strict `^(\d+)\.(\d+)$`, safe integers, future/expired rejection, `cookieEpoch === sessionsEpoch(db)` |
| Global epoch | `auth.ts:302-318` | `settings` row; missing = 0; non-integer **throws** |
| Global revoke | `auth.ts:321-330` | `setSetting` (overflow-guarded) + `clients.closeAll(1008, …)` |
| Auth routes | `authRoutes`, `app.ts:776` | mounted **before** the guard at `authGuard(authMode`, `app.ts:791`; logout guards itself (`resolveCookiePrincipal(c, auth, deps.db)`, `auth.ts:808`) |
| Passkey management | `passkeyManagementRoutes`, `app.ts:801` | mounted **after** the guard ("Mount AFTER the auth guard") |
| WS guard | `"/ws"`, `app.ts:973-990` | origin → `isWsAuthorized` (returns a boolean, `auth.ts:221-237`) → capacity → upgrade |
| Auth routes | `authRoutes`, `app.ts:777` | mounted **before** the guard at `authGuard(authMode`, `app.ts:792`; logout guards itself (`resolveCookiePrincipal(c, auth, deps.db)`, `auth.ts:808`) |
| Passkey management | `passkeyManagementRoutes`, `app.ts:802` | mounted **after** the guard ("Mount AFTER the auth guard") |
| WS guard | `"/ws"`, `app.ts:974-991` | origin → `isWsAuthorized` (returns a boolean, `auth.ts:221-237`) → capacity → upgrade |
| 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 |
Expand Down
4 changes: 2 additions & 2 deletions docs/decisions/voice-permission.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ description is the SDK's own (`canUseTool` passes `description:
opts.description` straight through to `createToolPermissionRequest`,
`permission-hooks.ts:300-310`) and is not
written to be heard. And `ClaudeBackendOptions.confirmBashPatterns`
(`Regex sources`, `packages/ui-backend-claude/src/options.ts:53-63`)
(`Regex sources`, `packages/ui-backend-claude/src/options.ts:55-65`)
lets a deployment supply its own patterns, which have no phrase when given as
bare regex sources (the `{ pattern, effect }` form carries one). Both fall
back to the same payload-free
Expand Down Expand Up @@ -486,7 +486,7 @@ The mechanism is the one the repo already has: a declared tool allowlist bound
to a turn — `InferenceProfile.allowedTools`
(`allowedTools?: string[]`, `packages/ui-backend-claude/src/profiles.ts:27`) and
`ClaudeBackendOptions.allowedTools` (`Backend-wide tool allowlist`,
`options.ts:50-51`), resolved into the SDK's `allowedTools` per turn (the
`options.ts:52-53`), resolved into the SDK's `allowedTools` per turn (the
`allowed` array, from `const allowed`, `sdk-options.ts:180`, and what it
becomes, `allowedTools: allowed`, `:309`). The voice posture is one
named entry in that mechanism.
Expand Down
1 change: 1 addition & 0 deletions docs/integration-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ Original fragment identities remain available at this entry point.
<a id="exec-wrapper-in-app-configuration-additive-1363"></a>

- [Exec wrapper in app configuration (additive, #1363)](integration-contract/package-api.md#exec-wrapper-in-app-configuration-additive-1363) (original line 410).
- [Backend host exec configuration (additive, #1429)](integration-contract/package-api.md#backend-host-exec-configuration-additive-1429).

<a id="logo-assets-additive-1424-1425"></a>

Expand Down
Loading
Loading