Authoritative component of the integration contract. Its shared scope and versioning policy apply to every section below.
The supported HTTP specification and complete inventory are part of this contract. They deliberately select routes for independent clients and integrations, preserve existing promises and SDK dependencies, and explicitly exclude internal UI transport from independent raw-HTTP compatibility guarantees. Support is separate from public accessibility: authenticated APIs can be supported, while a UI-only probe can be internal. Authentication, inputs, outputs, errors and specified behavior on supported rows follow the versioning rules at the top of this document. The selection decision records #343 Q6; no route redesign or immediate 1.0 freeze is implied.
The runtime coverage matrix maps supported operations to real app mounting checks and named behavior tests. Its inventory also accounts for internal routes, conditional static serving, HEAD dispatch and configured CORS preflight without promoting internal payloads to guarantees.
The detailed stats promises below remain binding. The specification records remaining gaps; a gap does not revoke a guarantee. Login, passkey registration/rename and capture refuse malformed/non-object JSON with JSON 400 errors. Capture validates content/type/title/tags before CLI dispatch; valid object defaults and pre-handler authentication/owner checks remain binding.
The three authenticated session/turn journal GET routes are specified in
the HTTP inventory. They resolve retained
observations owned by that exact session and turn; callers cannot name a CAS
hash directly. Missing/expired raw details never grant a download bypass.
createBrainApi adds turnJournal(sessionId, turnId, signal?),
turnJournalPreview(sessionId, turnId, sequence, signal?). Reader download
links use the embedding root's configured API base.
Those read helpers do not execute mutations.
Four internal /api/files/untrusted operations mark a readable brain file
untrusted, read one path's mark, list the marks a principal may see and let the
owner clear one. They are paired UI transport, specified with their authority
and disclosure rules in the HTTP inventory.
The taint governance event's shape is unchanged: its reserved
source: "user_marked" value is now produced.
GET /api/journal/configuration-coverage returns ConfigurationCoverageRead
(ConfigurationCoverageRead, packages/ui-sdk/src/turn-journal.ts:183-185):
the brain's current set of configuration inputs steering could not trace. It
is { state: "known", hash, paths, count, turnId, observedAt } for the set the
most recent complete turn recorded from a configuration inventory: paths is
sorted, count is its length (possibly 0), hash is the same identity as the
summary's configurationCoverageSet, and observedAt is that turn's
end. When no complete turn has recorded one it is { state: "unknown" }, never
an empty list. A partial, expired or still recording turn never replaces the
current set, and neither does a turn that changed nothing: capture takes the
inventory only for an observation that saw a change. The route is authenticated
like the other journal reads and is Cache-Control: private, no-store. The
per-turn missing entries are unchanged.
The existing journal routes also return successful external native-memory saves
with class: "steering", scope: "claude_memory" and a logical
path: "claude-memory://<direct-note.md>". That identity is distinct from a
brain-relative path; no saved directory, credential, transcript or settings
location is returned. Exact raw before/after versions, creation/replacement,
originating turn/session/model/profile and recorded taint use the existing
fields and bounded retained-version reader. memory_persistence_unconfirmed
in missing reports an interrupted/refused staged save, never success or a
complete recovery promise.
Existing authenticated human per-file/whole-turn revert and Undo resolve only
the privately retained destination for an eligible change in that source turn.
Client paths cannot select a host directory. Later note bytes, changed store
identity or unavailable raw history refuse recovery. Ordinary persistence
continues to save only validated direct .md notes and never deletes memory;
an explicitly reviewed human revert of a created note may remove that note.
Journal cap, active pins, retention and voice/agent/autonomous exclusions stay
unchanged. The initial in-brain steering delivery remains 0.42.0; external
persisted memory is additive in 0.44.0.
The existing journal routes also return intentional edits to managed reference
worktrees, observed from each worktree's own checkpoints. Such a change has
class: "content", scope: "reference_worktree" and a logical
path: "reference-worktree://<worktree id>/<path>". That identity is distinct
from a brain-relative path; no host or worker path is returned. Versions,
ignored state, overlap and taint fields keep their existing meaning, and a
worktree change is attributed only to the turn whose worker held the worktree.
An unreadable worktree entry appears in missing under the same logical path
and makes the turn partial. These changes count toward the turn's scope
summary like workspace edits. History revert and Undo exclude them as
non_reversible_metadata; nothing is written into the brain or the worktree.
The behaviour is recorded in the
managed-worktree decision.
The brain root's disposable index family is journaled by metadata only:
.brain-index/brain.db, its -wal, -shm and -journal sidecars, the
embeddings lock database .brain-index/brain.db.embedding-lock.db and its
-journal, -wal and -shm sidecars (#2903), and a
.brain-index/.maintenance-<uuid> index publication copy, matched by exact
journal-relative path. Each of their versions keeps kind, mode, bytes and
ignored, and adds metadataOnly: true and an opaque change stamp
(JournalVersion in @schlessera/brain-ui-sdk/protocol); it has no hash and
no symlink target. The host still records every observed change of these
paths, telling versions of equal size apart by their stamp, so history shows
when and how far the index changed. Their bytes are never retained: preview
answers { inline:false, reason:"metadata_only" } and raw download is 404. A
metadata-only version is never a missing version: it neither adds a missing
entry nor makes an otherwise complete turn partial. No shared-window
conflict is offered between versions that were not retained.
History revert and Undo exclude these paths with the reason metadata_only,
including versions retained by releases before 0.44.0, and leave the index for
the refresh that follows the operation; that exclusion alone does not make the
operation partial. Rebuild the index from Markdown with brain index --force;
metadata is not a restore source. Every other path keeps its exact retained
bytes, including other names inside .brain-index/, another root's
.brain-index/ and similarly named files elsewhere.
This is breaking: index versions were byte-retained, readable and restorable
before, and the maintainer approved narrowing them on #2873 (2026-10-25,
option B). History recorded by earlier releases keeps its index rows, which
present as metadata-only too, without hash or stamp. Their retained bytes
are purged (#2903): the first journal open after the upgrade (or a later one,
if a capacity or storage refusal defers it) rewrites each such
stored version to that metadata-only form, in turn history and in revert
records, drops the pins only those versions explained, and collects the
objects left unpinned. An object that any other record also names (a
version of another path, a memory note or an attachment removal's retained
original) is kept, and so is any pin of a turn that has not ended. The purge runs once, resumes after a crash, and returns
the bytes to the journal's capacity. The decision is recorded in
the governed-workers decision
and its purge.
The four authenticated human /revert operations are specified in
the HTTP inventory. createBrainApi
adds turnRevertPlan, turnRevertLatest, turnRevert, and turnRevertReceipt.
Only turnRevert POSTs; receipt readback never repeats execution. The caller's
operation ID is bound to principal/session/source/selection. Stale raw state
conflicts, unavailable detail, unknown outcomes and partial work remain explicit;
voice, agent and autonomous execution do not gain a history action. This is
additive in 0.42.0; no existing read or CLI/MCP shape changes.
@schlessera/brain-ui-react exports createBrainApi and BrainApi; the
embedding service root.api uses the same helpers. health() resolves to
{ status: string; uptime: number; timestamp: string }, matching the minimal
public route. The previously declared version: string never existed in that
response and is removed under the maintainer's #693 ruling.
This is an approved pre-1.0 breaking correction shipping in a minor. Migrate
health().version reads to authenticated status() when software identity is
needed; health never fabricates identity or requests protected status.
brainSync() POSTs through its configured base getter/request transport and
consumes complete SSE events. A valid terminal done maps success and text
to the existing { success: boolean; message: string } result; terminal false
resolves as a completed unsuccessful sync. Progress and keepalive comments
are not completion. Missing/malformed terminal data, premature EOF and
transport failure reject as incomplete; non-2xx responses retain
ApiRequestError. No automatic POST retry or stream resumption is added.
Disconnect does not establish cancellation: the server continues draining and
reserves the canonical repository until its child exits, as specified in
the HTTP sync contract.
SystemStatus in @schlessera/brain-ui-sdk/protocol declares
software: { release: string; sourceCommit: string }, the object authenticated
GET /api/status already returns (HTTP reference). release is
the installed @schlessera/brain-ui-server package version and sourceCommit
the configured source commit, equal to version. version stays the source
commit: it is never a release and clients must not parse it as one. The served
response is unchanged; this only types it. The Activity bug report prints
server: {release} and server commit: {sourceCommit} only when this read
succeeded, and no server line otherwise.
Authenticated GET /api/status gains runtime.workerHost, typed as
SystemStatus.runtime.workerHost (WorkerHostStatus) in
@schlessera/brain-ui-sdk/protocol. It is the latest real probe of the agent
worker boundary per mode: ordinary for chat, voice and handoff turns,
restricted for autonomous turns. Each present mode is { available, checkedAt, trigger }, with requirement when available is false; that is
the probe's named requirement, the same text a refused turn names. Boot probes
ordinary, and restricted only while autonomous work is enabled. A failed
boot probe is logged once and never refuses the boot. Every later probe
re-records its mode: each turn's own gate (trigger: "turn") and each
explicit check ("check").
Authenticated POST /api/status/worker-host takes no body, re-runs the boot's
probes and answers 200 with the same WorkerHostStatus object. In
@schlessera/brain-ui-react, BrainApi.status() types runtime.workerHost
and BrainApi.checkWorkerHost() makes this request.
The status reports; it does not decide. A pass is never cached and a known
failure never short-circuits a turn: every turn runs its own probe and refuses
with worker_host_unsupported naming the requirement
(maintainer ruling).
Clients must tolerate either mode being absent.
WorkerHostStatus gains rootServer: { warning }, present only while the
server runs as uid 0 (real or effective). Its workers then run as uid 0 too
and keep what root can do without writing: they can read root-owned files and
connect to root-owned and abstract Unix sockets. warning is the sentence the
boot log emits once, as a WARN, and names exactly that. It appears on
GET /api/status as runtime.workerHost.rootServer and in the answer of
POST /api/status/worker-host, and stays for the life of the process. The
server is not refused for running as root
(maintainer ruling).
The app shows a standing notice above the composer while it is present.
Clients must tolerate it being absent.
Successful worker-host checks may include identity: "separate_uid" and
workerUid, the actual nonzero outside UID verified by that execution's host
probe. GovernedStatus.identity additionally accepts "separate_uid" with
the same optional workerUid. With no identity helper configured the governed
status retains "shared_uid". A configured but unqualified helper does not
produce a separate-UID receipt or silently fall back to shared identity.
Clients tolerate omitted identity/UID fields and new named qualification
requirements. See the host protocol.
The optional host input BRAIN_UI_WORKER_IDENTITY_HELPER is one absolute
root-owned executable path, never a shell prefix. Its versioned descriptor
protocol, required group access and best-effort recovery limitations are
documented with the host boundary. Production helper installation belongs to
the public hosting template; the packages ship no production setuid binary.
Version 1 gains an additive view operation and launch field in 0.44.0
(#2696). A governed launch that retains provider folders requires it, and a
helper without it refuses those launches before payload start.
Authenticated GET /api/governance/events reads the governance events the
server keeps with session history: one machine-readable record per sandbox or
governance intervention in a turn. It filters by session, turn and kind
and pages with after and limit, answering { events, nextAfter } in id
order. Each event is { id, sessionId, turnId, at, kind, schemaVersion, actor, model, profile, payload }. The HTTP specification
holds the parameters, the errors, the attribution values and every kind's
payload fields.
The additive egress_connection kind records the concrete turn proxy's host,
port, visible HTTP method/path, bytes, accounting scope, duration and outcome.
CONNECT records opaque bytes without a path or content claim; the inference
tag comes only from the actual relay. Existing egress_denial remains named
and versioned as before. Header values, query strings and bodies are excluded.
The additive provider_mask_refusal kind (#2690) records a host bridge tool
that refused a path the turn's provider folder masks hide: the tool as the
agent called it, the brain-relative path, the providers rule and the policy
hash. One event is written per refused call.
The outward_action kind's action gains compute_run and the taint
kind's source gains compute_output (both additive, #3108): a hosted
external compute run's approval decision, its target the instance id, and a
collected job's output. Version 1 is unchanged otherwise.
Authenticated GET /api/status gains runtime.compute (additive, #3108):
{ enabled, reason, core, stateDir, stateMasked, bindings, instances, openJobs }. reason says why hosted compute refuses every run, or is null.
core is present, missing or unknown. Each binding is { provider, package, remoteExpiry, defaultKeyEnv, credentials }, with each credential { name, implicit, present, leakedToWorkers } (names only, never a value).
instances is the last configuration a turn read, each { id, provider, builtin, credential, bound, present, refusal }, or null before any read.
No vendor is called to produce it.
The kinds and each kind's payload are versioned separately. A payload change
that is not purely additive bumps that kind's schemaVersion, and stored
events keep the version they were written with, so a new version or a new
kind is an additive change. Clients must ignore unknown kinds and unknown
payload fields. A kind no release emits yet may still change its version 1
payload before its emitter ships.
The server emits roadblock from #1671 on, so its version 1 payload is now
extended additively by #2034 with remedy abandoned; existing values and stored
version-1 evidence retain their meaning and are never rewritten. Unresolved
waste now stops at the first success starting strictly after the last failure
was known, without inventing semantic resolution. A single such failure with
no retry is abandoned; repeated unresolved failures remain capability.
The report's optional remedy filter matches query_activity: actionable
(default, excludes abandoned before aggregation), all, or one of guidance,
capability, abandoned. Counts add abandoned. This is an approved breaking meaning/default change before 1.0, shipped in a
minor under the recorded #2034 maintainer ruling. Its signature is a grouping key whose format the HTTP specification
defines, so readers such as a roadblock report can group episodes across
sessions by comparing signatures for equality.
The events are host operational state in the server's own database. They are
never written to brain.db or the brain, never authoritative for anything,
and never shown to the user. They survive the Activity span-detail prune and
are deleted with their session.
WorkerHostStatus gains limits: { enforced, absent }
(WorkerLimitsStatus in @schlessera/brain-ui-sdk/protocol), re-read with
every probe. Each enforced entry is { limit, source, value }; each
absent entry is { limit, source, reason }. limit is one of processes,
data, file_size, open_files (source: "rlimit", per process) or
memory, pids, cpu (source: "cgroup", for the whole worker). value is
bytes for data, file_size and memory, CPUs for cpu, and a count
otherwise. reason says why a limit is not enforced: no delegated cgroup, no
prlimit, no nsdelegate on the cgroup2 mount, uid 0 for processes, a
malformed BRAIN_UI_WORKER_LIMITS, or the host turned it off.
It appears on GET /api/status as runtime.workerHost.limits and in the
answer of POST /api/status/worker-host. Clients must tolerate it being
absent and must not assume the list of limit names is closed.
WorkerHostStatus gains governed, typed as GovernedStatus in
@schlessera/brain-ui-sdk/protocol: whether the governed worker profile can
run on this server, and where the server evaluates the brain's own code.
{ available: true, hostExecution: "worker" }: governed can run, so every evaluation of the brain's config and its imports, every server-spawnedbraincommand, every cron job and every git invocation against the brain runs in a maintenance worker, never in a host process.{ available: false, reason, detail, hostExecution }: governed cannot run, and interactive and voice turns run understrict.reasonisserver_cwd_in_brain(the server was started inside the brain, so Bun already loaded the brain'sbunfig.tomland.envinto it),database_in_brain, orworker_host_unavailable(with the probe's requirement asdetail).hostExecutionstays"worker"once a governed turn has ever run on this brain or a real probe has proven the worker boundary in this process; otherwise it is"host".
Boot records it, and POST /api/status/worker-host re-records it in its
answer. Each change is logged once: a warning, except for
worker_host_unavailable, which the worker host entry already warns about.
Clients must tolerate it being absent.
The supported POST /api/internal/inbox/poke operation uses an independent
boot-minted bearer token and the actual loopback socket, before general API
authentication in every auth mode. Forwarding headers never authorize it.
Its runtime-file configuration, rotation, exact success/error shapes and
bounded stopped-interval recovery are specified in inbox-runtime.md.
The generated host consumes this operation; its /internal path does not
exclude it from HTTP compatibility. The shipped app wiring performs recovery
and heartbeat work with no production dispatcher. An unavailable or closed
runtime never grants access to ordinary APIs or enables autonomous execution.
GET /api/files/html?path=… is an internal UI transport for the file
viewer's script-running preview and its "Open in new tab" link. Its raw path
is not an independent API. Its isolation is a security property that a
consumer and a reviewer may rely on: it serves .html/.htm files under the
raw route's path, auth and size rules, with a CSP sandbox allow-scripts
directive (an opaque origin), connect-src 'none', form-action 'none' and
frame-ancestors 'self'. It is the only response sent with
X-Frame-Options: SAMEORIGIN; every other response keeps DENY. The
HTTP specification has
the full header set and the residual risks accepted in #1084. Removing a
sandbox restriction or widening a source list is a reviewed change, and the
real-Chrome test fails on it. GET /api/files/content?raw=1 and its CSP are
unchanged. The SDK's default service-worker policy never answers an /api
navigation from the app shell.
GET /api/vpn-check → 200 { vpn: true, accountKey?: string }
The authenticated connectivity probe now also names the account the caller
is signed in as, for device-local partitions. accountKey is an opaque
22-character base64url digest. It is the same for every sign-in as the same
account on the same host and root, and differs between hosts (each UI
database has its own random seed) and between brain roots. It is a partition
name, not a credential: holding it grants nothing on the host. A client must
not parse it, and must not use server_hello.principalKey in its place,
because that one changes at every sign-in.
| Auth mode | Who gets a key | Same key across sign-out and sign-in |
|---|---|---|
password, with a password or a passkey |
every owner login, one key | yes: every owner login is the one owner |
tailscale |
every admitted client, the owner's key | yes: the mode has no sign-in |
proxy |
each upstream user, its own key | yes, per upstream user |
none |
every client, the owner's key | yes: the mode has no sign-in |
| any mode, an agent principal | nobody: the field is absent | — |
A refused probe (401, 403) carries no key. A host older than this field sends none, and the client then keeps nothing in an account partition.
@schlessera/brain-ui-react uses the key to keep the composer's work
context (drafts, images, voice review text, selection, focus and transcript
position) in IndexedDB, in a partition it opens only while it holds the same
key. That storage layout is client behaviour, not wire contract. It is a
boundary inside the client, not encryption, and not protection against
someone with access to the device.
Passes brain stats --history --json through untouched, behind the auth
guard with the other /api/brain/* routes; the shape is under "Stats
history" above. A brain CLI older than 0.40.0 rejects the flag, and the route
answers 500 with { error }. A consumer treats that, and a 404 from an
older server, as no history.
Hosted applications index their Markdown after they return (see the
application contract). GET /api/brain/search, /api/brain/list,
/api/brain/stats, /api/brain/stats/history, /api/brain/briefing and every
/api/graph/* route first wait for the index update covering every
application committed before the request. When that update fails, the route
answers 503 with { error, code: "index_stale" } instead of a stale result;
the error names the committed paths and the index failure, and a retry tries
the update again. Shapes are otherwise unchanged; the status and code are additive.
The runtime half of a stats surface. GET /api/brain/stats passes
brain stats --json through and stays the corpus channel; this route reports
what the server's own database holds — sessions, runs, tokens, cost — and
never reads brain.db. A consumer calls both and merges. It sits behind the
auth guard with the other /api/activity/* routes. ?days=N picks the
window (default 30, clamped to 1–90). The shape is ActivityRuntimeStats in
@schlessera/brain-ui-sdk/protocol; timestamps are ms epoch:
{
generatedAt,
lifetime: { scope: "lifetime", sessions, turns, costUsd,
firstActivityAt | null, lastActivityAt | null, elapsedDays,
averages: { costUsdPerSession, turnsPerSession,
costUsdPerDay, costUsdPerMonth } },
window: { scope: "window", days, since, until,
recordedSince | null, coveredDays,
detailRetention: { days, cutoffAt, insideWindow },
detailPrunedRuns,
runs, failures, costUsd, effectiveCostUsd,
unpricedRuns, unpricedListCostRuns,
inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens,
averages: { runsPerDay, costUsdPerDay, costUsdPerMonth,
effectiveCostUsdPerDay, effectiveCostUsdPerMonth } },
database: { sizeBytes }
}
Rules a consumer may rely on:
- Every figure is labelled with what it covers.
lifetimeis read from the never-pruned session catalog;windowfrom the run rollups, over exactly[since, until]— a closed interval whose upper end is enforced, so a run dated afteruntil(a clock corrected backwards leaves such rows) is not summed. The two do not agree and are not meant to: the catalog predates the activity record, and the two count different things. - The window says how much of itself it can vouch for. Rollup rows
outlive detail pruning, so the sums are complete back to
recordedSince(the oldest run in the record at or beforeuntil) — and no further;coveredDaysis the span the per-day averages divide by.detailRetention.cutoffAtis where drill-in detail stops,insideWindowsays whether that boundary falls inside the window, anddetailPrunedRunscounts the runs in it that are already rollup-only. Say "detail older than N days is pruned"; do not present a window as a total. - Unknown never reads as $0, on EITHER cost axis. Each cost sum is a sum
of known values, and each carries its own excluded count, because the
two columns are independently nullable:
costUsd(list price) excludesunpricedListCostRuns,effectiveCostUsdexcludesunpricedRuns. A subscription-billed run with no backend-reported cost is in the first counter and not the second — its effective cost is a known $0 while its list price is unknown. Render both the same way: "≥ $X · N unpriced" when the counter is nonzero, and wholly unknown when it equalsruns— never as the0that a sum of no known values carries. - An average is
nullrather than a fabricated rate. Every average isnullwhen its denominator is zero,costUsdPerDay/PerMontharenullwheneverunpricedListCostRuns > 0, andeffectiveCostUsdPerDay/PerMonthwheneverunpricedRuns > 0— a rate over a partial sum would hide the hole the sum shows.lifetime.elapsedDaysis0, and its per-day and per-month figuresnull, when the catalog is empty or its oldest session is dated aftergeneratedAt; the lifetime totals still include such a session, since it happened. - The route does no rounding or formatting, while
GET /api/activity/rollupsrounds each completed day/job/session sum on both cost axes to 4 decimal places, only at the response boundary; stored costs retain their original precision. Over the same window the two therefore report0.299997and0.3for one quantity. Round at render time, identically for both, rather than treating either as pre-formatted. This channel stays raw on purpose: rounding a sum to 4 dp turns a real sub-$0.0001 cost into a0that reads as free. lifetime.costUsdis a floor, and cannot be better than one. The session catalog folds an unreported cost into0at write time, so no unpriced counter is recoverable at read time;windowis the channel that separates unknown from zero.lifetime.turnshas the same shape.database.sizeBytesis the logical size of the server database (pages × page size, the WAL sidecar aside). It is a rebuild-cost figure for a disposable store, not a claim that the file holds authoritative state.
GET /api/governance/events also accepts and returns kind scratch_overage,
version 1, with {bytes, pinnedFiles, pinnedBytes, overageBytes}. Periodic scratch
pruning records it with actor server, null session/turn/model/profile, and the
existing unbound-event retention. A failed event write falls back to the overage
log; pinned files remain protected. The closed kind/payload specification is in
the HTTP reference.
runtime.workerHost.governed adds optional profile (governed or strict),
protectedPaths (objects with path, mode and reason), profileFallback,
identity (shared_uid) and configurationView (live or startup). A mode
is read_only, mask_file, mask_directory or absent. These describe the
current ordinary-turn default; each turn resolves its protections again.
The existing availability and host-execution fields keep their meaning.
runtime.workerHost.governed.egress (additive, 0.44.0, #1665) is
{ active, detail }: whether governed and strict turns run on their own turn
egress proxy. The server boots only on managed native storage (#1793), so it is
active: true on every running server, and detail says which turns are
routed. The field is not removed: active: false remains for a brain that is
not on managed native storage, where detail says that every governed and
strict turn is refused rather than run without the proxy (#3314).
The host boundary status reason adds private_state_in_brain alongside
database_in_brain, with the same migration detail the boot refusal uses.
Every hosted profile refuses boot as OperationalStateInBrainError when
DB_PATH or private poke-token state resolves inside the brain. The error
carries the unavailable diagnostic as status; no HTTP listener is created.
The hosting guide documents migration and the retained directory masks.
Authenticated hosts advertising capabilities.sessionProfiles support
GET and PUT /api/sessions/:id/profile for a catalogued chat, and
GET and PUT /api/drafts/:id/profile for the existing unbound composer
identity. The draft route keeps only a preference; it creates no session,
reservation or dummy text. A PUT body is { preference: "governed" | "strict" }.
Requests and responses are not cached. JSON bodies are bounded to 4 KiB.
Successful responses carry SessionWorkerProfile: confirmed preference,
current lower-bound policyProfile, nullable policyHash, origin
(interactive or voice), optional actualProfile of the latest admitted
worker, and limits ({ name, value, onHit? }[]). Draft responses additionally
carry draftPreferenceStored, true only after a successful durable save.
Unknown enforcement facts are explicitly Unknown; parsed future budgets
and scope caps are not represented as active enforcement. The actual worker
may be stricter than the preference because of policy or launcher fallback.
Unknown catalog sessions return 404; invalid values/identities return 400; revoked/expired principals return 401. The store revalidates durable authority inside each save transaction after reading the body. A governed save below a strict policy, or any voice save, returns 409. Failed saves preserve the confirmed preference. A change affects subsequent interactive admission, never the authority already frozen for a running turn.
Draft preferences are principal-bound and capped by the existing host draft count. At capacity the host refuses a new preference with 507 instead of evicting a live confirmed choice. Expired-principal rows are removed on save; a successful first-session binding consumes that draft preference into the session catalog. Another principal or draft cannot borrow it.
The internal authenticated GET /api/policy-proposals/:turnId/:requestId
returns only the existing principal-owned PolicyChangeReview; an optional
session must match its bound session. Reads never approve, write or revive
execution. GET /api/agent-policy reads the admission policy cache and recorded
worker-limit facts, without an editor or another policy cache. Their shapes,
refusal states and recovery behavior are specified in the
HTTP reference.
The governance event API additionally accepts/returns policy_change with
request id, exact base/proposed hashes, changed sections and updated outcome,
with actor user.
Journal details may add tainted: true and taintSourceEventId to a steering
change only when an existing taint record preceded the observation in that
session or turn. The ID is retained with the change; replay never derives it
from later events. A missing flag is not a claim that untrusted-input coverage
is complete. Policy changes remain class policy, with user attribution, and
are excluded from steeringFiles.
The governance event API accepts/returns steering_change, version 1, with
path, journal sequence, nullable beforeHash/afterHash, nullable
taintSourceEventId, and overlappingTurnIds. It records an observation, not
an exclusive writer or per-write interception. Configuration inventory that
cannot identify a retained local input is named in journal missing with
configuration_inputs_not_fully_identified; raw-version availability remains
independent of that classification limitation.
TurnJournalChange.conflict may contain versions and latest, each an
explicit retained { turnId, sequence, side: "after" } observation reference.
The host marks a conflict only after two distinct after-version identities were
observed in a shared window. A single version with overlap remains shared
attribution, not a conflict or an exclusive writer claim. Raw versions
superseded between observations remain unrecoverable.
Existing user /revert POST and createBrainApi.turnRevert additionally accept
keep_version or keep_both, path, version, expected, and (for keep both)
otherVersion. All references must belong to that path's original retained
turn; expected names its reviewed latest observation. At application the
actual raw state must still match; later work is skipped. The durable operation
identity includes the selected references. Receipt resolution preserves them;
Check again never repeats an effect or revives authority.
Keep both creates the other regular-file version with exclusive creation beside
the selected file, using sync-style -remote.md, -remote-2.md naming (for other
suffixes, -remote follows the whole name). Existing names are never overwritten.
The generated sibling receives explicit journal coverage and index refresh.
Both effects remain user journal entries, with existing conflict-safe Undo.
Partial writes or unavailable storage remain partial/unknown, not an atomic
merge guarantee. Existing owner/ambient, voice, expired-detail, protected-path
and cross-session restrictions remain binding.
The six Storage inventory operations and
SDK StorageStatus, StorageSnapshot, StoragePage, StorageFileEntry,
StorageReferenceEvidence, StorageTreePage, StorageTreeNode and related types are additive contracts. They bind
authorization, configured-root scope, bounded output/lifetime, opaque cursors,
object/capacity deduplication and explicit partial/unknown semantics. This
transport foundation does not register a pane or authorize cleanup.
Storage pages distinguish fileLogicalTotal (logical bytes per matching regular-file
entry, including aliases) from totals (the deduplicated physical-object union).
Empty unavailable ownership views carry null bytes, not zero. Capacity kind
identifies tmpfs as memory-backed; filesystem does not promise exclusive disk
ownership and unknown cannot be drawn as healthy capacity.
Hierarchy pages separate per-subtree object-union totals from disjoint sibling
area, preserve alias names and unknown coverage, and bind cursors to the tree
operation and its search/order/ownership criteria. Projection uses retained scan
evidence only. Entry search/order is additive; absent options preserve ordering.
The six attachment removal operations
and SDK StorageRemoval* review/capability/receipt types are additive contracts.
They bind current human-owner authority, exact versioned selections and durable
identities to charged retained originals. Unknown outcomes never authorize
replay. Undo is separately reviewed, cannot overwrite a replacement, and expires
with actual journal detail. Existing Markdown application semantics are unchanged.
The two history/guidance operations
and SDK StorageHistoryResponse, StorageGuidanceResponse, named observation,
pressure, rate and scenario types are version 1 additive contracts. They retain
current human-owner authorization, nullable unknown quantities, actual endpoint
and window times, independent causes, explicit stale/scope gaps and transport
uncertainty. Public types do not expose compact persistence tuples or host paths.
The next compatible server/SDK minor includes these endpoints; clients must
retain an unavailable state when an older host does not provide them. No core
CLI, MCP, frontmatter, config or content-index contract changes.
GET /api/activity/runs adds lifecycles: ActivityTurnLifecycle[], bounded by
its list limit and filtered by session when supplied. Backend live/history
summaries add optional lifecycle; existing list membership, filtering,
backend durations and costs remain unchanged. Envelope-only attempts are in
lifecycles, never disguised as backend runs. GET /api/activity/runs/:runId
adds optional lifecycle plus its lifecycleSeq high-water, also on its rollup, and resolves an envelope-only
turn with no spans/rollup and detailPruned: false. Historical records omit
it. The agent Activity query returns the same envelope on run/recent reads.
See host turn lifecycle for timestamp,
terminal, missing-phase and historical semantics. No inference aggregation
includes envelope records.
Retained GET /api/activity/runs/:runId detail adds optional cacheDiagnostics:
firstRequest: nullable{ inputTokens, cacheReadTokens, cacheCreationTokens, source, coverage }. Each counter is a nonnegative integer ornullwhen unreported;sourceisclaude_root_streamorpi_main_message. Claude selects the first rootmessage_start; Pi selects the first completed non-error assistantmessage_end.coverageisprovider_reportedfor Claude andruntime_normalizedfor Pi. Installed Pi normalization fills absent provider cache-read/write fields with zero; a Pi zero is a normalized runtime value, not evidence that the provider explicitly reported zero. Later responses, child channels and title-helper evidence cannot replace it, including when helper and main use the same model.prefix: nullable{ tools, system, model, thinking, gitStatusIncluded, coverage: "assembled_inputs" }. Each component is a lowercase 64-character SHA-256 digest ornullwhen unavailable.gitStatusIncludedis boolean ornullwhen unknown. No prompt, tool description, schema or message text is retained in these diagnostics.resumed: nullable boolean;suspectedBreak: true only for a known resumed turn with first cache-read zero and creation positive, false for a fresh turn or a known nonmatching split, otherwisenull.changed: an array of changed component names (tools,system,model,thinking,gitStatusIncluded),unchanged, orunknownwhen a comparison baseline or required component is unavailable. This compares assembled inputs against the preceding retained conversation turn, excluding auxiliary accounting runs. A pruned baseline is unknown.
Fingerprints cover backend assembly, not complete provider wire bytes. Claude's roster digest includes the ordered assembled brain-ui schemas and explicit SDK built-in selection/exclusions; CLI-owned built-in schemas and project MCP configuration remain opaque. Its system digest includes the preset and exact append passed to the SDK; runtime/model defaults are unknown when unspecified. Pi computes its ordered active tool schemas, system prompt, model and effort inside the actual worker before prompting. Subsequent runtime extensions may still alter provider assembly. Neither backend's digest establishes a provider cache key, cache eligibility, first-party request equality or savings.
GET /api/activity/stats adds optional window.cacheDiagnostics with
scope: "retained_detail", observedRuns, runtimeNormalizedRuns, unknownRuns, sums of first-request
inputTokens, cacheReadTokens, cacheCreationTokens, suspectedBreaks and
unknownBreakRuns. Token sums include only runs with all three reported
counters; old, auxiliary, uninstrumented and pruned runs count as unknown,
not zero cache usage. runtimeNormalizedRuns counts included Pi records so
normalized zeros remain distinguishable from provider-reported evidence. Detail pruning removes these diagnostics with the root
span; existing lifetime/window accounting and turn totals retain their meaning.
No historical rows are backfilled, and these fields do not assert a live cache
hit. Existing root attributes also expose the hash-only records to Activity's
span/query consumers.
GET, POST /api/auth/principals/:id/reference-grants and
DELETE /api/auth/principals/:id/reference-grants/:grantId are specified in
the HTTP inventory. A grant binds the served brain, one live
owner or agent principal, a canonical repository prefix and one named existing
host credential from a fixed list, or none. Private reference source is fetched
only while a live grant covers the requesting principal and repository; a
configured host credential never grants access by itself. Revoking the grant or
its principal applies to the next authority check, including in-flight private
refreshes. The routes add no credential storage and return no credential value.
The grant decision records the ruling.
Authenticated GET /api/status includes optional contentRenderers, an array of
{ module, version, name, fences, extensions }. module is the manifest name,
version is the installed package version or null when unknown, name is the
renderer contribution key, and fences/extensions are arrays of declared
strings. Only active configured modules contribute. An empty array means the
server knows there are no declarations; absence means its CLI cannot report
metadata or the lookup failed. Clients retain code fallback in either case.
The payload contains no client/static entry specifier and authorizes no imports. The build registers trusted client packs. Missing packs retain source with a “Renderer not bundled.” note. Explicit development mode warns once per root and known differing version pair; unknown versions do not imply a mismatch.