Repository navigation
test(ui-react): pair the remaining Cartesian theme cells in answer and journal runtime - #3345
schlessera wants to merge 126 commits into
Conversation
A bind mount does not change the type readdir reports for its mount point, so a socket bind-mounted onto a file path read as the empty regular file bwrap made for it and the walk never reported it. The walk now lstats every mount point listed in the walking process's /proc/self/mountinfo, in the fixture's baseline and in each worker. A socket file-bound into a path every profile inherits is the positive control. Closes #2745 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…3290) GitHub's workflow dispatch answers with an empty body unless the request sets return_run_details, so request-heavy-feedback.ts dispatched a run and then failed without printing its URL. The test fake now answers like GitHub. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…-owned authentication (#3297) * feat(ui-sdk): withhold host-only secrets from every subprocess Declare APIFY_TOKEN as a host-only secret (#3159). Unlike a server-only descriptor entry, neither BRAIN_UI_SUBPROCESS_ENV_EXTRA nor a per-spawn extra name can admit it, so no agent, brain CLI or cron child receives it. Shift the decision citations the inserted lines moved. Refs #3159 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * CONTRACT: feat(ui-server): require explicit Apify enablement and host-owned authentication Add the strict optional governance block `apify: { enabled }`, false by default, reported in `brain policy check --json` as `effective.apify`. An invalid policy disables Apify rather than keeping an earlier enablement. The server reads APIFY_TOKEN only on the host and contacts no Apify endpoint while the policy leaves the provider disabled. When enabled, it binds the account GET /v2/users/me reports on the fixed origin, sends the token only as a bearer header, refuses redirects and token query parameters, and keeps the token out of statuses, errors, records and URLs. Authority is an opaque generation: disablement, a missing or rejected token, or a different account starts a new one, so earlier bindings go stale; a token rotated to the same account keeps it, and a different account needs an owner's confirmation. Owned cleanup (one run's status or abort) stays possible after disablement while the account still matches. Internal GET /api/apify and POST /api/apify/account expose the disabled, authentication_required, configured and unavailable states to live human owners only. Document the policy family, the environment input, the routes and the contract entry, and shift the citations the change moved. Refs #3159 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…so reading it taints the session (#3287) * CONTRACT: feat(ui-server): let a user mark a file untrusted so reading it taints the session Marks are host records in the UI database keyed by brain-relative path. Any principal with read access may mark a file; only the owner may clear one or see retained missing records and marker labels. An admitted file-tool read or search naming a marked file records taint with source user_marked. Refs #2673 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ui-react): mark a file untrusted from Files, with cues and the Marked untrusted list The toolbar gains Mark untrusted between Copy path and Share, the owner a pressed Untrusted: clear mark, and every reader a purple untrusted cue on the file and its tree row. A Marked untrusted list replaces the disabled Untrusted only filter. Every visible change waits for the host; races, refusals and uncertain replies use the approved copy and focus table. Refs #2673 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ui-react): keep Files focus and the viewer's single status with untrusted marks The Marked untrusted entry row now waits for the host's list, so a row that an unsupported server would remove never takes the drawer's initial focus. The marks announcer is a polite live region without the status role, so the viewer's own File status stays the surface's one status. Refs #2673 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ui-react): draw the Marked untrusted entry only from a host answer, and inventory its runtime proof An unreachable or unsupported host draws no entry row, so the row never appears late inside a settled Files drawer. The Chrome runtime test joins the runtime build inventory. Refs #2673 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…rete Actor consent (#3300) * feat(ui-server): reach the Apify catalog through the hosted MCP server Add ApifyAuthority.catalog for the two catalog tools the contract selects, search-actors and fetch-actor-details, on the fixed MCP origin. The URL loads only those tools and turns Apify's telemetry off, so no execution helper is ever connected and no session id or use-case text is reported. The exchange is a minimal Streamable HTTP session (initialize, initialized, one tools/call, a best-effort close) that accepts JSON or event-stream replies and refuses redirects, oversize bodies, unknown protocol versions and malformed envelopes as upstream_incompatible. The token still leaves the module only as a bearer header, now on one shared transport for the REST and MCP origins, and a call refuses unless the binding is current, exactly like a REST request. Refs #3160 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * CONTRACT: feat(ui-server): separate Apify catalog discovery from concrete Actor consent The governance policy's apify block gains discovery and actors, each deny/ask/allow and ask by default, and actor_grants in the contract's exact shape {account_id, actor_id, build_number, profile_id, schema_sha256, pricing_sha256, origins, expires_at, max_run_usd}. brain policy check --json reports them under effective.apify. Grants name the immutable Actor id and an exact build number, never a name or tag; an unquoted YAML timestamp normalizes to the same ISO instant. createApifyConsent is the host-side mechanism the CLI/MCP operations and both backend bridges will call (#3162): - discover sends a bounded catalog query (a few plain words, no addresses, links or long numbers, at most 10 results, no use-case text) under the discovery mode, after the provider-folder routing check (G2). Results are untrusted: the caller is told before it receives them (G6). - inspect follows discovery policy because it sends the chosen identifier, and reads build, input schema, permission level and the full price list from the REST API, refusing a redirect alias, a renamed Actor or a substituted build. - prepare checks input against the inspected schema (no unknown, secret or missing fields; no credentials, private, local or link-local targets) and binds account, authority generation, principal, session, turn, policy hash, Actor id, build id and number, schema and pricing fingerprints, qualified profile, exact input, target origins and the run cap. - admitRun re-reads all of it before and after the approval, admits a run without review only for an exact unexpired grant under actors: allow, asks otherwise through the existing per-use command approval (never a rememberable tool grant), refuses declined, edited, cancelled and surfaceless approvals, and consumes the proposal once before handing a frozen ticket to the dispatch #3161 will own. No build is qualified yet, so every Actor can be inspected and none can run. Document the policy fields, the CLI envelope addition and the contract entry, add the changeset, and shift the citations the schema moved. Refs #3160 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… incident issue (#3279) * feat(ci): record a red main centrally, gate doomed proof and coordinate incidents Adds the repository-level red-main record to the shared coordination ref (red-main.json beside reservation.json, exact --force-with-lease CAS), a deterministic flake estimator, a coordinator workflow on main CI completion (one incident issue per record, triage and bisect probes, cancellation of doomed PR proof, detection of squashes that land while red, clearing on a green run that included the red set), a hosted gate as the first PR step, integrate.ts main-status/declare-fix and merge refusal while red, and main runs that select the open red set and run proof even when metadata fails. Refs #2942 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(ci): record the red-main controller decisions and permissions Refs #2942 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: follow the moved CI install citation Refs #2942 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ci): replay the red-main coordinator read-only against a recorded run Refs #2942 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ci): refuse draft heavy feedback on a head containing main's recorded bad commit The feedback workflow's admission job takes the red-main gate step from ci.yml by its command, so the generator fails if ci.yml loses it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ci): pin red-set widening with a stub planner instead of two whole-repo plans The hosted unit shard timed the test out at 35.7 s. The real planner still runs end to end in the executable test against a fixture repository. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…stem squashes (#3296) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…Actor lifecycles (#3306) * feat(ui-server): add the Apify run ledger and qualified-profile checks Two internal building blocks for metered Apify runs. The ledger is private operational state: one host-owned SQLite file (mode 0600, outside every brain, never brain.db) in WAL mode. Every process that opens it shares its ledger_id. Admission is a single BEGIN IMMEDIATE transaction that checks the run, turn, session, account-wide day and brain-day allowances, the runs-per-turn and account concurrency bounds and the turn's request count, then writes the dispatch intent. Pending reservations count as spent. An unsettled run holds liability on its start day and every later UTC day. Charge reports are recorded once by content and only ever raise the liability they belong to. Result cursors are HMAC-bound to one run. A qualified profile pins one exact build's input schema and price list. It qualifies only all-in pay-per-event builds with a verified currency cap and every event priced, exactly-targeted fetching, mapped target and work-limit fields, a site authorization (robots or a written permission), fixed run options, and a bounded lifecycle (requests, abort and remote retention). profileInput validates the full input against that schema, collects URLs only from the target fields and bounds the work. No production build is qualified. Refs #3161 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * CONTRACT: feat(ui-server): meter and bound Apify requests and remote Actor lifecycles The governance policy's apify block gains limits: run_usd, turn_usd, session_usd, day_usd, max_runs_per_turn, max_requests_per_turn, max_concurrent_runs, max_runtime_seconds, max_result_items, max_result_bytes and max_retention_seconds. brain policy check --json reports them as effective.apify.limits, which is null when unset. With no limits there is no spending entitlement and no run is admitted. Once present, every field is required and bounded, and zero refuses what it bounds. day_usd and max_concurrent_runs are account-wide. createApifyRuns is the trusted run service that #3162 exposes: - run: admitRun consumes the proposal. The dispatch then validates the input against the build's qualified profile, checks site permission and the minimum charge, and intersects apify.limits with the global budgets less inference already spent. It reserves the cap plus the bounded lifecycle and storage in the shared ledger, then sends one start, never retried, with build, timeout, memory, maxTotalChargeUsd, waitForFinish=0 and restartOnError=false as run options. It waits a bounded time within the run's reserved status reads. - A lost, 5xx or unreadable start is remote_outcome_unknown. Its reservation is kept and it is never sent again. An operator resolves it with the run's own identity or an attestation that stays visible. - The returned run must match the account, Actor, build and options, with no metamorph. Otherwise it is aborted and its results withheld. - Settlement uses the run's charged-event report at the higher of the qualified and reported prices, exactly once. A missing or unpriced report keeps the reservation. After settlement the lifecycle requests and remote storage stay owed; local expiry releases nothing. - status, results and abort require an owned opaque handle (brain, principal, session). Results come only from the validated run's dataset, within item, byte, request and retention bounds, behind the untrusted hook. Redirects and signed URLs are never followed. - Cancellation, stop (timeout or budget exhaustion), disablement and reconcile send an owned abort through cleanupRequest. A failed abort is unresolved, and no abort claims a zero bill. - coverage reports per-scope liability, known charges, held reservations, residuals and unknown runs beside APIFY_COVERAGE. Consent now: - pins a profile to its exact schema and price fingerprints; - validates prepare input through the profile; - refuses a cap below Apify's minimum charge; - adds the fingerprints to the run ticket; - counts actual requests in the shared durable meter under max_requests_per_turn. New refusal codes: accounting_unavailable, remote_outcome_unknown and result_expired. This adds no CLI command, MCP tool, wire frame or route. The contract index, supported inputs, CLI envelope note and changeset ship here. Citations moved by the schema are updated. Refs #3161 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…with a kept disposition (#3288) * CONTRACT: feat(core): count media awaiting review from one inventory with a kept disposition (#2780) One media inventory now feeds both offline `brain sync`'s count and hygiene review: the assessed uncommitted MEDIA/LARGE files plus conversation-pinned scratch files (review-only, even when media.ignore matches; no scratch walk; 5,000-path bound with a trusted-host override). Each file is one `media` finding (MEDIA info, LARGE warning) fingerprinted by bytes, classes, policy entries, references and the pins of its own path. A fifth hygiene state, `kept` (context/hygiene/kept.md, never created empty), holds while the copy exists and is tracked. `mediaReviewCounts` applies the disposition predicates without trusting the containing file. Sync reports only `Media awaiting review: N` and no longer hands media to the agent. Review's Leave pending is a per-pass deferral (`next --defer`). Additive contract per rev 6.1: list gains copy/copySha256/keptOn, reconcile gains kept, next gains counts.kept and media, run.leftovers gains mediaPending/mediaSetAside; the review state, effect and outcome schemas gain their defer/kept fields with parse defaults. Citations moved by these edits are updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ui-server): gate hygiene next --defer on the media CLI release floor (#2780) Review sends --defer only to leave a media finding pending, which only a 0.45.0 CLI reports. Record its release evidence as a feature floor and fail the 0.45.0 version bump until MIN_BRAIN_CLI_VERSION reaches it, as for the index directory. Also: the env descriptor list, a same-pass deferral lapse test (the earlier one passed with deferrals matched by ID only), the Keep-a-permanent-copy eligibility fixture, and moved citations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * CONTRACT: docs(api-report): record the ui-sdk review state, defer effect and deferred outcome (#2780) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * CONTRACT: feat(core): record a kept media original with brain hygiene keep-copy (#2780) The kept state's write path (rev 5 §5.7): reconcile's expected-fingerprint request {kind: "kept", id, expectFingerprint, copy, copySha256, journal}, refused with nothing written unless the finding is media and the copy already holds - a contained regular file outside .brain/ and workspaces/, kept by media.track, with the stated bytes. Keep a permanent copy (#2781) calls it after making and verifying the copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(core): prove sync and review agree for snoozed, due and every pin snapshot state (#2780) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#3298) * CONTRACT: feat(ui-sdk): negotiate native history removal on the backend seam Add the optional `historyRemoval` capability and `removeSessionHistory()` method to `AgentBackend` (#3229), with content-free `removed`, `partial` and `unknown` outcomes that map onto the retirement ledger's per-store outcomes. A backend that does not advertise and implement both is unsupported, so a host refuses deletion before retiring anything; the conformance suite checks that the flag and method agree. The shared internal fence writes a durable, content-free tombstone before anything is removed, holds sessions for turns and readers, keeps one removal per session across a bounded caller wait, and removes only named artifacts, refusing links, hard links, special files and redirected roots. The display, prompt and observation stores expose the names they already used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(backends): remove a retired chat's Claude and Pi native history Both first-party backends advertise history removal (#3229). Claude removes the live transcript and subordinate tree, the archived, staged and conflict copies and the index entry; Pi removes every SessionManager tree whose own header names the session in this brain, under the canonical and the older SDK-managed name, so inactive branches go too. Both remove display and prompt metadata and file observations, and confirm through the installed runtime's own listing. Listing, reading, resume, follow-up, archive and restore refuse a retired session, after a restart too. Turns and history reads hold their session, so a removal waits for their last native write, and a read never publishes what it found once the chat is retired. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ui-server): record native removal under the chat retirement `removableBackendIds` selects the retirement's removable backends from the capability and the method together, and `removeRetiredNativeHistory` asks the chat's backend to remove each retired session under the retirement's own operation, recording the native store's combined outcome (#3229). A missing or unsupported backend, a rejection or an elapsed wait never completes the store. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: re-anchor moved backend citations and record the built native removal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(backends): await refusals and name the ownership assertions in native removal tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…al turn budgets (#3323) The global turn, conversation and UTC-day budgets saw only inference and relayed CLI providers, so a turn could keep spending inference up to block_usd after Apify had already reserved part of the same budget. createTurnBudgets now takes an optional ApifyBudgetSource, read where each call is made and passed to the ledger's transaction (so it crosses to the write worker). A read adds the ledger's turn, session and brain-day liability to each scope's costUsd once, reported again in the new optional apifyUsd, and every admission, mid-turn stop, relay refusal and the SDK backstop see it. apifyBudgetSource builds the source from the shared ledger. A failed or non-finite read keeps inference's behaviour and states Apify accounting is unavailable; it is never read as zero. Without a source the receipt is unchanged. inferenceSpend reports inference without Apify's own liability, for Apify admission's ApifyInferenceSpend, so neither side counts it twice. TurnBudgetCoverage gains two values (additive, as #1673's coverage change). The wire component and contract index describe them; shifted citations move with the code. Refs #3307 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ttached and settled (#3334) Main run 41488767631 failed REDUCED_MOTION_STATIC_LOADING once at the light 1280 case; three hosted probes of the file then passed. The check stays a single strict measurement, now taken after an attached ghost and two frames, and a failure prints each ghost's animation name. The cause is #3333. Refs #3315 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…d journal runtime The Other Submit family in answer-delivery-runtime and the configuration coverage family in turn-journal-runtime ran all four 320/1280 x dark/light cells with no theme-specific reason. Theme selects only colour tokens and color-scheme, so both now run 320px dark and 1280px light, keeping every width-dependent oracle at both widths and both painted themes once. The removed copies' theme paint moves to central consumer stories that run in both Storybook projects: a new Conversation/Answer theme story mounts the actual consumer AskUserCard Other field and Submit (the kit story disables contrast at its meta), and Conversation/Journal theme now renders configuration coverage, its badge, search and inventory. Refs #3329 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e cells Refs #3329 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
No uploads on this PR yet This PR changes UI files (
Tips for agentsLabel each shot so reviewers know what they're seeing. Labels apply to every file in a call, so run one uploads put after.png --pr 3345 --meta path=/settings --state after --caption "Saved settings toast"
uploads put chat.png --pr 3345 --app ios --screen "Chat" --state afterNo CLI? With the Uploads MCP, call This comment updates with each upload, so you don't need to post the images yourself. A shot or two in the PR description is still welcome. Check what's attached with Workspace admins can turn this comment off in Settings → GitHub · docs |
|
Collision report ( |
…ence (#3311) * feat(ci): claim the red-main fix atomically and collect incident evidence (#2944) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): narrow the fix-claim liveness check for the typechecker (#2944) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(ci): record the conditional concurrency direction for #2934 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…CP and both chat backends (#3346) * refactor(core): move the Apify service into core so every front door shares it The authority, consent, ledger, profile and run modules move unchanged in behaviour from ui-server to core (`@schlessera/brain/internal/apify`), so the `brain` CLI, the core MCP server and the hosted server can call one implementation (#3162; the contract has core own the operations). Consent no longer names a UI type: the approval surface is a structural `requestPermission` (each decision field read once), and provider-folder routing (G2) is passed in by the host, which supplies the rules turn admission uses. Without routing, any provider rule refuses as unproven. Candidates and proposals sit behind `ApifyConsentState`: in this process by default, or in the shared ledger for a standalone process, where a proposal's single admission is decided under the ledger's write lock. The ledger gains the reference and private-settings tables that needs. Refs #3162 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * CONTRACT: feat(core): expose governed Apify operations through CLI, MCP and both chat backends Seven fixed operations reach Apify, discover, inspect, prepare, run, status, results and abort, with one strict schema each in `@schlessera/brain-common/internal/apify-operations`. `brain apify <op> --json`, the core MCP server's `brain_apify_*` tools and the same-named bridge tools in the Claude and Pi backends validate that schema and call one dispatcher over the core consent and run services, so no transport has a second consent, account or billing path. Every door returns the same envelope; refusals keep the contract's codes, plus the additive `provider_unavailable` and `invalid_input`. The hosted server builds each call's context from the authenticated turn (principal, named session, turn, posture, the turn's own approval surface, recorded provider exposures), records the session's taint as the new `apify_result` source before remote text returns, takes inference spend from the turn budgets, opens the shared ledger at `BRAIN_APIFY_LEDGER` or beside the UI database, reconciles open runs at boot and every five minutes, and aborts a cancelled, timed-out or budget-stopped turn's runs. Both backends register the tools in every interactive session's fixed roster and auto-allow them; voice, no-grant-surface and restricted autonomous turns keep their memberships, and the host refuses any non-interactive turn. A standalone CLI or MCP process uses its own `APIFY_TOKEN` and the shared ledger, keeps candidates and proposals there under the operator's session, never asks on a terminal, and admits a run only by an exact standing grant. Neither variable reaches a worker, so a shell `brain apify` in a chat turn refuses. The integration-contract index, CLI, MCP and package-API components, the user CLI/MCP references, supported inputs, the governance-event taint list, the decision record, API reports, moved citations and the MCP schema snapshot are updated here. Tests cover every front door against a loopback Apify, the server's own wiring, a real Pi worker over the worker protocol, and both backends' provider-history guards through the installed Claude CLI and a native Pi worker. Refs #3162 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * CONTRACT: fix(common): keep the Apify service loadable without the optional core peer The UI server reaches `@schlessera/brain` only as an optional peer (#697), so a static import of core's Apify modules broke a server installed without core (the packed ui-server probe could not load `app.ts`). The authority, consent, ledger, profile and run modules and the dispatcher move to `@schlessera/brain-common/internal/apify`, which every front door already depends on; core keeps the standalone CLI/MCP glue at `@schlessera/brain/internal/apify`. Behaviour is unchanged. The package-API entry list, the decision record and the API reports follow. Refs #3162 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * CONTRACT: test(backends): count the fixed Apify tools in the installed Claude turn surface The installed-CLI surface capture now sees the seven `mcp__brain-ui__brain_apify_*` bridge tools loaded eagerly (31 bridge tools) and the core server's seven `mcp__brain__brain_apify_*` tools deferred like every core tool. The MCP component says why the worker-side core tools are harmless: a worker holds neither the token nor the ledger, so they refuse. Refs #3162 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…cache (#3359) A request that awaited its cache read first could miss, resolve after another request had fetched, written and finished, and fetch the same geometry again. The cache read now runs inside the shared in-flight promise. Closes #3358 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…main failure (#3344) A failing unit shard carries only tmp/unit-worker-<k>-report.xml; the merged unit-report.xml exists only once every worker's report reconciles. The coordinator looked for the merged name alone, so every unit failure was recorded with test: null. It now prefers the merged report and otherwise reads every worker report. The test fake now mirrors the real artifact. Closes #3324 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
) (#3310) * refactor(common): hold the Claude subscription billing guard once (#2493) core's CLI runner and the chat backend each carried a copy of the cleared credential table, the neutralised flag settings, the settings refusal and the account check, and core justified it with a comment saying the two packages share no dependency. Both depend on @schlessera/brain-common, so the guard now lives in its new ./internal/claude-subscription entry and both callers import it. A newly found credential or routing variable is added in one place. The shared shape is the backend's: settingsRefusal(report, pinned = {}) and a { ok, via } subscriptionVerdict. core adapts the verdict to its reason string locally; behaviour is unchanged for both callers. The backend keeps its own message, SDK error classes, observed billing and credential fields. Refs #2493 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(changeset): note the single Claude subscription guard (#2493) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ain video watch in keyless workers (#3312) The host's provider relay now carries the Files lifecycle a local video needs (#2738): a resumable upload start, the bytes to an upload URL the host validated and registered under a token, the poll and delete of the handles this relay's own uploads created, and a generation naming them. A worker-chosen upload URL, a foreign handle (poll, delete or fileUri) and a blocked budget are refused before contact; poll and delete never wait on a budget, and the turn's leftover files are deleted with the host key when its relay stops. Files attempts are priced at a known zero. The core Gemini provider stops refusing relayed video and routes its cleanup delete through the relay. Contract (additive): docs/integration-contract/cli.md lists the relayed Files routes; a relayed worker no longer reports capability=video. Refs #2738 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
CONTRACT: feat(core): land epic #1777 batch 2 — governed media effects and Undo from hygiene review
# Conflicts: # docs/integration-contract.md # packages/ui-sdk/src/internal.ts
…use and record a skip (#3382) `brain-ui-cron crontab` added module jobs only when discovery succeeded at the moment cron started; a transient failure fell back to core jobs with no signal until the next restart (#3381). Discovery moves into `cron/module-discovery.ts`. Each attempt qualifies the host afresh and reports why it yielded nothing: the `brain module list` exit code and stderr tail, unreadable output, a host not reconciled, or a missing native configuration. Retryable causes get two more attempts, after 5s and 15s. The final outcome is recorded as a finished `modules` job: a cron_runs row (`/api/status` cronJobs) and a `cron` root span in Activity, `error` with `module cron skipped: <cause>` or `success`. It writes only to a UI database outside the brain that exists or sits behind a qualified host, so the unqualified fallback still creates no UI state. Successful crontab bytes are unchanged. Additive contract: integration-contract index and wire component updated. Closes #3381 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…e release (#3408) * fix(ui-sdk): keep a gated payload out when the gate holder dies before release A status-gated live launch gated its payload with bubblewrap's --block-fd, which starts the sandbox on the gate's end-of-file. The server holds the gate's only writer, so a server that died between spawn and release let the unqualified payload run. The launch now drops --block-fd and keeps --json-status-fd 3. The fixed host launch shell still opens the gate FIFO as fd 4 after the bootstrap's descriptor filter; inside the sandbox the trusted /bin/sh reads one line from fd 4, requires exactly `go`, closes fd 4 and only then execs the payload. End-of-file, a partial line or any other line exits 125. release() writes `go\n`. #3387's kill-before-close ordering stays as a second guard. Records the ruling in policy-write-boundary.md, updates the STATE_GATE comment and "Worker startup status", and moves the citations the launcher edit shifted. Closes #3392 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: re-anchor a citation moved by the main merge --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Any PR that moves lines in a cited file drifts other PRs' citations, so failing the merge queue on drift turned main red for documentation alone (#3496). Maintainer ruling, 2026-11-03: drift is a pre-release check. - experiments/decision-citations.test.ts now holds the tree-wide drift checks: every citation anchored or excepted, exceptions exact, and the convention's own example. The experiment tier runs it, and `bun run release` refuses to publish without that tier's receipt for the exact tree. - tests/decision-citations.test.ts keeps the checker's rules on in-memory trees and the one tree-wide rule that cannot drift with code: no citation into a private repository (#303). - AGENTS.md, docs/decisions/README.md and the release skill say so. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # docs/integration-contract.md
…g a running session clears nothing (#3416) * test(ui-react): end the phone's scroll-up swipes at rest, so no fling swallows Scroll to latest The 390 phone case scrolled C's transcript up with three fast finger swipes that each lifted mid-motion. Chromium turned them into a fling that kept moving the transcript after scrollUp returned. A tap that lands on a running fling only stops it, and Chromium suppresses that tap, so the press on "Scroll to latest" could do nothing and C never read as seen. Each swipe now carries its own event times and holds the finger still for 200ms before lifting, so the gesture leaves no fling at any dispatch speed. Every assertion and deadline is unchanged. Refs #3377 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ui-react): an answer still being written is not seen, so reopening a running session clears nothing The seen observer matched the last message's turnId against the latest turn without asking whether that message had ended. While a session streams in view, its live frames stamp their turn onto the answer; the history replay that follows a reopen strips it, since history links a turn only once it has ended. When the observer's first committed render after selecting a running session beat that replay, the stale frame turn matched and the observer recorded proof for a turn still running: the tracker read cleared before the turn ended (#3406). The observer now refuses a last message that is still being written. That is what it already saw once the replay arrived, so only the race window closes; a turn that ends while the reader is at its end is still seen. Closes #3406 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…orker (#2865) (#3494) The masked taxonomy and private-index workers passed the fallback's bare `bun` to the worker launcher, which admits only an absolute executable, so a protected fallback refused as an unavailable worker boundary. workerBrainCliCommand resolves that `bun` to the server's own runtime and keeps the script relative, so it resolves in the immutable startup view. Other relative executables refuse by name before launch, and a failed run's detail skips Bun's trailing version banner. Shifted client.ts citations move with the code. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…g it (#3493) The per-figure SVG/PNG/PDF case clicked the share trigger and immediately looked up the menu item; when the menu commits after the click resolves, the lookup returned undefined and WebKit threw at the second click. Poll for the actual requested item under the ordinary expect.poll deadline. Refs #2946 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ff the event loop (#3495) * fix(backend-claude): wait on the idle-session archive move's fsyncs off the event loop The sweep's per-session move, source removal, conflict set-aside and reindex now await their file and directory syncs. Removal rechecks the live turn set and the archived copy in the same synchronous step as its validation and deletion, and an index write overtaken by a restore or removal is redone. Restore stays synchronous as the scoped residual. Refs #2898 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(backend-claude): follow moved transcript-archive citations and record the changeset Refs #2898 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…waits instead of refusing (#3497) The policy publisher rechecks the turn inside its own journal observation. A scope cap that observation itself reached was a terminal refusal: waiting there would hold the observation, which keeps Continue refused, and throwing out of it would fail the observation and stop the turn's workers. The callback now stages nothing and returns, so the observation completes and releases its hold. The publisher then reports the hold outside it, the brain lock unwinds, and the existing bounded wait runs under the request's one deadline. Continue starts a fresh attempt that rechecks authority, validation, ancestry and the base hash under a new observation. Stop, cancellation, revocation and expiry publish nothing; a no-publication attempt records no policy-updated event. Refs #2870 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…the boundary on read-only writes (#3384) A scheduled module job runs in a maintenance worker while the host execution boundary holds. The host, homes and XDG state are read-only or per-run there, so a module that kept state in a host directory failed on its first mkdir with a bare EROFS (#3380). Per the maintainer's 2026-11-02 ruling, core now defines a per-module state area, `<brain>/.brain-ui/modules/<module-name>/`, gitignored by its own `.gitignore` and published through output staging like any brain write. A module command reaches it as the new optional `CommandContext.stateDir()`, resolved against the CLI's brain root, which is the worker's writable data root rather than its read-only configuration view. `@schlessera/brain/module` exports `MODULE_STATE_DIR`, `moduleStatePath`, `moduleStateDir` and `ModuleStateError`. A module command's EROFS outside the brain now exits 2 with a message naming the path, the host execution boundary and the state area. An EROFS inside the brain keeps the `read_only_brain` refusal. The cron runner adds the same explanation when a worker job's own subprocess prints an unexplained EROFS. Contract: docs/integration-contract.md and the package-api component; the ruling is recorded in docs/decisions/governed-writable-workers.md. Moved ModuleContribution citations follow the type. Refs #3380 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # docs/integration-contract.md
…pare the turn journal while idle (#3386) * CONTRACT: feat(ui-server): attribute governed admission and journal begin to timed steps `ActivityTurnLifecycle` gains optional `subPhases` (#3355). Governed admission records the inference-origin lookup, gate acquire, startup checkpoint(s), boundary prepare, sandbox resolution, private index, data tree and record commits; journal begin records the hold wait, initialize, observe (checkpoint, delta, metadata, capture, record, commit), expiry, turn insert and policy. Steps ride on the next milestone or terminal write, so timing adds no write to the turn path, and an unfinished step follows the phase failure rule. Refs #3355 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(native-proof): time admission steps in cold scenarios under --observation-timing The observation-timing run now reads each turn's lifecycle record and writes its phases and steps to observation-timing.json for the process's first governed turn, the first turn after 300 notes changed while no turn ran, and the first turn after a backlog of turns past retention, beside five warm turns (#3355). The backlog comes from a journal clock held 31 days behind while those turns run. Refs #3355 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(ui-server): prepare the turn journal while no turn is active The first admission after start or idle paid for the observer's full metadata walk, the delta of everything changed while no turn ran and the expiry of every turn past retention (#3355). The hosted app now runs that preparation once startup recovery commits and every minute while no turn is active. With no active turn an observation attributes nothing and retains no content, so the journal records what it did before; the admission keeps the same steps, now with little left to do. Refs #3355 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ui-server): close abandons an idle journal pass, and app-close tests scope their own checkpoint Closing the journal aborts an in-flight idle observation instead of waiting for its native work. The app-close lease tests now let the journal's startup observation settle and hold only their own checkpoint's release, since the journal now holds an observed root from startup (#3355). Refs #3355 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: follow citations and API reports past the admission timing lines Refs #3355 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # docs/decisions/governed-writable-workers.md # packages/ui-server/src/brain/governed-turn.ts
…iptors (#3476) (#3500) The operator restore checked `.brain-ui/inbox` and each arrival with realpath once, then created directories, reaped leftover temporaries and published files by pathname, so a link swapped in after the check was followed. Restore now opens the staging chain and each arrival without following links (the #2907 helpers), reaps and publishes through those descriptors, re-verifies the visible chain before each change and at the end, and reads the final comparison through the descriptors too. A link refuses with the existing `inbox_staging_symlink` code. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…3503) #3484 added packages/ui-react/tests/ended-question-runtime.test.ts with a runtimePackageBuild hook but did not list it in scripts/runtime-test-builds.ts, so tests/runtime-build-selection.test.ts fails on main (expected 18 hooks, found 19). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…s busy refusal (#3488) (#3501) When two concurrent resumes of one non-resident session both open a worker, the one that returns second now decides busy or reuse (and claims a reused entry) synchronously, then awaits its own worker's shared inactive abort before re-raising the original outcome. The refusal settles only after the losing worker has exited, its turn temp is closed and its stdio release has been awaited. A refused cleanup is logged as a warning through the backend log and cannot replace the busy refusal; the reservation stays charged and the abort stays retryable. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
CONTRACT: fix(ui-server): land epic #1645 batch 1 — provider-relay taint exemption, managed-storage turn refusal and mount-point socket walk
Refs #3329
This closes #3329 when the
epic/1849-test-costbatch lands onmain.What changed
Two runtime families stop running the full 320/1280 × dark/light matrix. Each now runs only the paired cells 320px dark and 1280px light:
1887: Other has explicit current-text Submitinanswer-delivery-runtime.test.ts, andjournal configuration coverage keeps small changes first and preserves inspectioninturn-journal-runtime.test.ts, which now uses the file's existingjournalRuntimeCells. The scenario bodies are unchanged; the answer-delivery diff is a re-indent (git diff -wshows the loop header only). Their theme paint moves to central consumer stories that run in bothstorybookandstorybook-light:Conversation/Answer theme(Phone320,Desktop1280) mounts the actual consumerAskUserCardwith the built consumer stylesheet. It opens Other and checkspainted, the field's computedcolor-scheme, blank Submit disabled, then enabled, the ≥44px Submit, field and Submit bounds, and scoped WCAG A/AA, contrast included, on the field and the Submit. It also checks the submit dispatch.Conversation/Journal themenow rendersconfigurationCoverage: "incomplete"with two inputs. It checks badge visibility and truncation, the ≥44px coverage disclosure and search, the inventory count and bounds, and scoped WCAG A/AA on the journal with coverage open.docs/process/theme-parity.mdgains an "Answer and journal coverage runtime cells" section with the assertion map, and addsAnswerTheme.storiesto the focused run command.Why this shape
Option 1 from the issue (pair the cells). Both are option 1 because no theme-specific failure mode exists. The theme selector sets only
color-scheme, which picks one half of eachlight-dark()colour token (packages/ui-kit/src/tokens.css,[data-theme]blocks). No geometry, focus, key or settlement path in these surfaces branches on it. The ui-react card wraps the kit card in theme-independentspace-y-*utilities only.The issue's fallback applied to the Other card. The existing kit
Decisions/AskUserCard/OtherOpenstory cannot own the theme paint, because its meta disables axecolor-contrastfor a known selected-option gap. Mutation A below shows it stays green. So I added the minimal consumer story rather than keeping a runtime copy. That story'saccessibleis scoped to the Other field and Submit, so it does not inherit that gap. The journal stories did not render configuration coverage at all, so I extended them the same minimal way.Assertion map (removed copies: 320 light, 1280 dark)
OTHER_LAYOUT_USES_REAL_THEMEAnswer theme/Phone,Desktop(painted+ fieldcolor-scheme) in both projectsBLANK_OTHER_SUBMIT_DISABLED, blank Enter sends nothing, draft kept across switching,OTHER_SUBMIT_KEYBOARD_REACHABLE,OTHER_SUBMIT_TOUCH_HEIGHT,OTHER_CARD_NO_OVERFLOWOTHER_CURRENT_TEXT_SETTLES_ORIGINAL_REQUEST_ONCE, oneask_user_responseframeJournal theme/Phone,Desktopadd badge, ≥44px, inventory and scoped contrast in both projectsSettlement, frames, Revert/Undo, download bytes, keyboard and touch paths still run at both widths. This change touches only the two families.
Proof
Tested base
origin/epic/1849-test-cost=3bee06dce265; heade3df429bf5d5.Storybook (pinned image via
scripts/visual.mjs):--project=storybook --project=storybook-light --filter=AnswerTheme.stories --filter=JournalTheme.stories→ 8 passed (4 files).Mutation A (Other card):
AskOtherFieldinputcolor: light-dark(#fbf8f1, <ink>), a light-only break, then rebuilt.Answer theme/PhoneandDesktopfailed instorybook-lightonly:expected [ { id: 'color-contrast', nodes: [ [ 'input' ] ] } ] to deeply equal []ataccessible(_consumer-parity.tsx:126).storybookstayed green. Under the same mutation the kitDecisions/AskUserCardstories stay green (22/22) instorybook-light. Restored.Mutation B (journal coverage): inventory
<ul>color: light-dark(#fbf8f1, var(--bk-color-ink)).Journal theme/PhoneandDesktopfailed instorybook-lightonly oncolor-contrast(.font-mono > .break-all) at the newaccessiblecall (JournalTheme.stories.tsx:125in the play). With the journal story as it was before this PR, the same mutation stays green (2/2). Restored, then rebuilt.Runtime (
turn-journal-runtime.test.ts), run in the pinned Playwright image with--network=none:(pass) journal configuration coverage … dark 320,(pass) … light 1280; 21 pass. The one failure,keep-both refreshes the actual disposable index, is untouched by this PR. It failed because the image lacks bubblewrap (Agent worker boundary unavailable: bubblewrap is missing).Case counts: the Other family goes from 4 to 2 cases, and the journal coverage family from 4 to 2. That is 8 → 4 runtime cases, about 11 s at the audit's hosted run 41382713082. The new central cost is 4 story executions (
Answer theme: about 0.3–0.5 s each locally). The journal stories gain the coverage steps within their existing 4 executions.Refresh on 2026-11-03 (head
7ff87a9a9)Merged
epic/1849-test-costatdfc528d80andorigin/mainat559fe7b41, which carries the merged blocker #3385. Both merges were clean; this PR's test, story and theme-parity changes are byte-identical toe3df429bf.The two runtime files have no new local receipt. This host has no system Chrome. With the Playwright Chromium and its missing libraries supplied, Chromium still exits
No usable sandbox!because AppArmor restricts unprivileged user namespaces (apparmor_restrict_unprivileged_userns=1). The tests keepchromiumSandbox: true, so the hosted ready proof is their receipt.Checklist
docs/reflect the change, or nothing documented moved.docs/decisions/provider-history.md): the provider-history guard passes, or nothing on a turn's path changed.Anything left open
answer-delivery-runtime.test.tshas no local runtime receipt. It requires sandboxed Chrome (chromiumSandbox: true). The host has no Chrome libraries. The pinned image runs only as root under rootless Docker, and Chrome refuses root without--no-sandbox. A--no-sandboxwrapper hungnewContextfor every case, untouched ones included. Hosted ready-candidate runtime proof is the receipt for the paired320px in dark/1280px in lightcases.e3df429bf5d5: closure-only (shared affected test closure), nopathcollisions; see the PR comment.🤖 Generated with Claude Code