Skip to content

v3.6.0

Released 2026-07-17.

Security: provenance trust classes with lineage-propagated egress confinement (#2513)

Worker context ingests tool results from sources with very different trust levels: web and browser output, tracker issue bodies, third-party MCP results. The capability matrix tagged tools statically, so UNTRUSTED_INPUT was a property of the tool, not of the bytes. Once a result entered context, downstream egress checks could not tell whether the exact bytes they acted on derived from content an outsider could have written. The structural gate closed the direct path; the data-flow path stayed open.

  • Every tool result is recorded as a content-addressed lineage entry carrying a trust_class (operator, workspace, first-party, third-party, public). The label lives inside the signed, HMAC-chained lineage entry, and the taint verdict is a pure projection over the graph's parent_hashes edges, so the confinement is anchored to the lineage graph rather than to a runtime tag.
  • The effective trust of any artefact is the minimum trust class over its lineage closure, a deterministic projection recomputable offline. Absence of provenance is fail-closed to the lowest class.
  • bernstein audit taint <artefact> recomputes the verdict offline: two verifiers on different machines produce a byte-identical result with no live process. It runs the lineage gate first, so a mutated provenance record or a reparented edge fails verification exactly like a tampered lineage entry fails the existing gate.
  • The capability matrix now carries UNTRUSTED_INPUT by data: a chain that unions private data, a tainted-derived operand, and external communication is denied even when every tool's static tags would pass. Auto-approve never returns APPROVE for a command whose text derives from a tainted artefact.
  • Untrusted payloads pass through a quarantined structural parser (schema-validated fields only) before anything reaches prompt context; instruction-bearing free text is withheld and represented by a content hash, and the extraction is a lineage edge back to the tainted source.
  • The source-to-trust-class map is reviewed data (templates/provenance/trust_sources.yaml); an unlisted source fails closed.

Content written by outsiders can no longer launder into approved egress unnoticed, and every egress decision becomes reconstructable, offline, to the exact bytes it acted on and their signed origin.

Adapters: enforced security floor with signed refusal receipts (#2515)

The spawn boundary is the most privileged hand-off in the system: the CLI binary Bernstein execs receives the task context, the workspace, and the worker's credential scope. The minimum-safe-version floor is now an enforced spawn-time contract rather than an advisory a doctor warning may never surface.

  • Spawning a tracked adapter below its security floor is refused by default. An ordinary version check prints a warning; here the refusal itself is the proof artefact: a content-addressed adapter.spawn_preflight_receipt anchored to the HMAC audit chain and verifiable offline.
  • Every preflight decision, including permits and warn-only overrides, is chain-anchored. Given a contiguous audit-chain slice, a verifier can prove no below-floor adapter spawn was permitted during a window, and a gap in the slice is detectable rather than silently exculpatory.
  • bernstein doctor seals the environment's version posture into an adapter.version_posture receipt, so the posture is provable offline instead of living only in console text.
  • The conformance canary refuses to certify a below-floor version and records the refusal; the last-green projection can never advance onto a below-floor version.
  • Each receipt pins the floor map's content hash, so a mutated map is flagged at verification. A floor bump lands as a data-only diff accompanied by a signed update receipt (scripts/refresh_adapter_floors.py).
  • Opt-out: BERNSTEIN_ADAPTER_FLOOR_POLICY=warn permits a below-floor spawn and records a warn-override receipt rather than refusing.

See docs/adapters/security-floor.md.

Audit: intent capsules with deterministic drift escalation (#2514)

Approving an unattended run approves a sentence, not a contract: nothing bound the running worker's action stream to the goal the operator signed off on. Scope drift was only discoverable in retrospect by reading the journal. Intent capsules close that gap.

  • At approval time the goal is compiled into an intent capsule: a canonical structured envelope listing the allowed action classes, file-scope globs, permitted adapters, egress classes, a cost-envelope reference, and an expiry. The capsule is written to the HMAC audit chain (intent.capsule) and its hash is bound into the run journal, so every subsequent step is attributable to one approved capsule. The goal is bound by digest, never stored verbatim.
  • A deterministic drift monitor maps observed journal events to action classes and compares them against the capsule. The conformance verdict is a pure function of (journal, capsule): two verifiers on different machines recompute the byte-identical verdict offline. No model runs on the drift-decision path.
  • On divergence the monitor emits a signed escalation receipt reusing the stall escalation shape, binding the capsule hash and the divergent events. The receipt passes bernstein escalation verify, so stalls and drift now produce the same class of verifiable artefact.
  • bernstein intent verify <task-id> recomputes conformance offline: it checks the capsule hash against the audit chain, walks the run journal Merkle chain, and maps action classes against the capsule. A tampered capsule or a reordered journal fails verification with no live process required.
  • Rollout defaults to warn-only (drift is surfaced and escalated but not blocked); a blocking policy is opt-in.

See docs/operations/intent-capsules.md.

Orchestration: receipt-backed fleet steering (#2508)

Once a worker was running a task an operator had no mid-task controls: no pause, no way to queue guidance, no redirect of the objective, no clean abort. The only recourse was to abort the whole run and respawn, losing the budget already spent, or to edit files under a running worker and leave no record, so an audited run a human touched could no longer be explained end to end. Fleet steering closes that gap without weakening the run's verifiability: every action is a receipt first and an effect second.

  • Five steering kinds (pause, resume, guidance, redirect, abort) each record a signed steering.receipt on the HMAC audit chain before any effect runs. The delivered effect references that receipt's chain HMAC, so an effect with no matching receipt is rejected; the receipt binds the exact command payload the operator confirmed, and a request whose confirmed payload differs from the executed command fails before the chain is touched.
  • Delivery rides the existing task mailbox journal as new steer.* message kinds, so queued guidance reaches the worker in chain append order, exactly once, even mid-tool-call. The worker records each consumed steering message as a first-class step in the per-step journal, so a steered run replays byte-identically and a second host walking the same journal computes identical step hashes.
  • Divergence detection distinguishes an operator-steered run from a tampered one: a run that verifies and carries steering steps is steered; mutating a recorded receipt or steering step fails verification at exactly its chain position and is labelled tampered.
  • Pause checkpoints the running worker and parks its claim; resume continues from the checkpoint without restarting the task. Abort is enforced by the scheduler on the worker process through a filesystem stop signal, never routed through the model, and touches only the target session, so steering one worker leaves other workers' worktrees and claims untouched.
  • Steering requires an authorised operator-scoped dashboard token; a viewer token or a request with no valid credential is recorded in the denial tracker and rejected. Surfaces: POST /tasks/{task_id}/steer, the bernstein fleet steer CLI subcommand, and steering controls with a queued-guidance indicator on the fleet screen.

Scheduling: content-addressed registered recipes with collision receipts and definition lineage (#2546)

A recurring run under governed configuration was split across three surfaces: file-discovered recipes with no registered identity, content-hash schedules that could only speak UTC 5-field cron and had no pause state, and a separate scenario registry. The supervisor also fired over a still-running previous fire, so a slow nightly run that overran its window double-wrote with no record that an overlap was even evaluated.

  • A registered recipe is now a content-addressed run definition: the recipe id is the sha256 of its canonical body, which pins the git commit plus adapter, model, and prompt-pack hashes. Two operators registering the same manifest at the same commit derive the byte-identical hash; changing any pinned input changes it. Registration seals the canonical bytes into the lineage spine and writes a recipe.register receipt. There is no mutable registry row: the live name -> hash mapping is a projection of the receipt history.
  • Schedules gain interval+anchor and RRULE kinds beside cron, plus a declared IANA timezone and an explicit DST ambiguity policy. Local-time resolution goes through stdlib zoneinfo only, so two hosts in different system timezones resolve the DST-forward gap and the DST-back overlap to the byte-identical instant, and the declared zone folds into the fire projection hash.
  • Concurrency collisions are a pure decide_collision(policy, running_fire_state) evaluated in the supervisor tick, emitting a chain-anchored collision receipt for every overlap considered. CANCEL_NEW never dispatches a second task graph over a running fire; SUPERSEDE_WITH_HANDOFF resumes the new fire from the stale run's recorded checkpoint, downgrading to a cold start when that checkpoint row fails chain verification.
  • Every definition change is an offline-verifiable chain of hashes: re-registering a changed body writes an operator-signed supersede receipt, recipes rollback re-points a name at a prior hash via a new receipt (nothing is deleted), and pause/resume are definition-level state records. A paused recipe fires nothing yet keeps its identity, and its pause window reconstructs from receipts alone. recipes history <name> --verify walks register, supersede, rollback, and pause receipts against the HMAC chain with no server running and exits non-zero on any broken or reordered link.
  • A declarative fleet manifest makes rebuilding the whole set reviewable: recipes plan emits a byte-reproducible diff with a plan_hash, and recipes apply --plan <hash> refuses to run against any other registry state, naming the divergent recipe.

No new runtime dependency (zoneinfo is stdlib); existing zero-block recipe manifests and existing sched_ schedules keep working unchanged.