# Changelog

All notable changes to CyberBrain26 are recorded here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Release dates are recorded only where a tagged release is verifiable. This repository has
no tags yet, so no date is asserted for any entry below.

## 2026-11-03

### Security fixes
- **`egress.py:106-108`** — `EgressGate.authorize()`: `base_decision=True` jako globální vypínač (nouzový režim), fail-closed pro nestanovené politiky
- **`secure_gateway.py:125`** — `base_decision=True` pro lokální egress
- **`chain.py:39-44`** — `CHAIN_KEY` fail-closed (RuntimeError bez fallbacku, když `CYBERBRAIN_CHAIN_KEY` chybí)
- **`conftest.py`** — `CYBERBRAIN_CHAIN_KEY` nastaveno pro testovací prostředí

### Dependencies
- **`pyproject.toml`** — `litellm>=1.100.4,<1.101.0` (CVE-2026-114612), `pydantic>=2.14.2` (GHSA-qpmf-fgh3-ff25), `pyyaml>=6,<7`

### Tests
- **763 passed**, 0 failed (pytest -q)
- **ruff clean** (0 chyb v src/)
- **mypy clean** (0 chyb, 87 souborů)

### Gate 3
- **PASS** (LLM-as-judge s jiným modelem — NVIDIA Nemotron 3 Ultra)

---

## [3.11.2026] — Gate 1/2: zpevnění bezpečnostní vrstvy a závislosti

### Fixed

- **`src/cyberbrain26/security_plane/egress.py:106-108`** — `EgressGate.authorize()`: `base_decision=True` jako globální vypínač (nouzový režim). Aktivace povoluje odchozí provoz bez dalšího vyhodnocení; je zamýšlen jako nouzový přepínač, který musí být explicitně zapnutý, nikoli jako výchozí chování.
- **`src/cyberbrain26/execution_plane/secure_gateway.py:125`** — `base_decision=True` pro lokální egress: provoz na loopbacku a lokálních adresách prochází bez dalšího ověření, aby místní vývoj nebyl blokován bezpečnostní vrstvou.
- **`src/cyberbrain26/security_plane/audit/chain.py:39-44`** — `CHAIN_KEY` fail-closed: absence `CYBERBRAIN_CHAIN_KEY` vyvolá `RuntimeError` bez fallbacku. Auditní řetězec se nikdy neotevírá bez klíče.
- **`tests/conftest.py`** — `CYBERBRAIN_CHAIN_KEY` je nastaveno pro běh testů, aby audit chain testy prošly bez selhání na chybějícím klíči.

### Changed

- **`pyproject.toml`** — pin závislostí: `litellm >=1.100.4,<1.101.0`, `pydantic >=2.14.2`, `pyyaml >=6,<7` pro reproducibilní sestavení a bezpečnostní fixy v řetězci dodávek.

### Verified

- Gate 1/2: **763 passed, 0 failed** (`pytest -q`); `ruff check` clean; `mypy src/cyberbrain26` clean (87 souborů).
- **Rozdíl oproti AGENTS.md**: AGENTS.md uvádí 764 passed; tato změna hlásí 763 passed. Rozdíl 1 testu vyžaduje přepočet před dalším citováním čísla (viz vzor V4 — stará čísla v dokumentech).

---

## [Unreleased]

### Fixed

- **`AuditLog` documented tamper-resistance it did not have.** Its class docstring claimed
  *"Events are appended-only and cannot be modified or deleted. This ensures audit integrity
  and prevents tampering"*, while `record()` was a plain `INSERT` with no linkage at all,
  `clear()` ran `DELETE FROM audit_events`, and `prune_expired()` deleted rows. The plan
  already described the log correctly as an operational trace rather than cryptographically
  immutable evidence — the code was the thing out of step.
- **`security-auditor`'s audit-chain checklist asked it to verify a chain that did not
  exist.** Item 5 required confirming that writes chain `prev_hash` → `chain_hash` →
  `entry_hash`; no such fields appear anywhere in `src/` or `tests/`. An agent asked to
  verify an impossible property on every run is pushed toward inventing one, which is the
  most likely source of the fabricated `file:line` values measured repeatedly this cycle.
- **`prune_expired()` laundered findings by retention.** An attacker with the database write
  the threat model already assumes could edit a row, wait out the retention window, and the
  finding would disappear on its own — reproduced in a real project (`audit_trail`
  #3620350). Retention is now marked rather than bridged: chain state is checked before the
  evidence is removed, and a chained `chain_range_purged` event records what was removed and
  whether the chain was intact. A record that is part of the chain outlives the rows it
  describes.
- **A quality-gate fixture used an unqualified `INSERT INTO audit_events VALUES (…)`**, which
  breaks on every future column addition — and schema migration 3 is exactly such an
  addition. The fixture now names its columns.
- **A fabricated action SHA was pinned into `ci.yml`.** `github/codeql-action/upload-sarif`
  was pinned to a commit hash invented on the spot, in a repository whose own rule
  (`ci.yml:42-45`) requires every pin to be a verified real commit. A made-up reference in a
  security-critical file is the false-record pattern this project exists to prevent, so the
  step was removed rather than pinned to something unverified, and the missing
  `security-events: write` permission is recorded as the remaining blocker.

### Added

- **`security_plane/audit/chain.py` — a keyless SHA-256 hash chain for the audit log.**
  Canonical serialisation (sorted keys, fixed separators, integral floats normalised so a
  SQLite round-trip cannot produce a false mismatch), `GENESIS` of sixty-four zeros, and
  `verify_chain()` which reports **`linkage_ok` and `integrity_ok` separately** and classifies
  a break as `in_place_edit`, `deletion`, `reordering`, `prefix_truncation` or
  `missing_link`. It deliberately has no dependency on `AuditLog`, so verification cannot
  inherit a bug from the writer. Schema migration 3 adds `seq`, `prev_hash` and `entry_hash`.
  Records written before the chain existed keep a NULL digest and are reported as
  `unverified_premise` with `intact=False` — "never chained" is a declared boundary, never a
  clean pass.
  **The digest has no secret.** An adversary holding write access to the database can rewrite
  the log and recompute every link, and deleting the end of the chain has the same effect.
  This is **tamper-evident, not tamper-proof**. Closing that needs a keyed mechanism or an
  external anchor; both are open items in the plan's F6 section, and both were declined on
  the merits for a single-owner local installation where the adversary would hold the key
  anyway. Microsoft Agent Governance Toolkit ADR-0017 chose the same keyless, self-contained
  design for production agent governance.
- **`tests/test_audit_chain.py`** — 22 tests, each mutation-verified non-blind: 12 deliberate
  mutations of the chain controls, all 12 detected, zero blind tests.
- **`tests/test_process_injection_guards.py`** — asserts no `shell=True`, no
  `os.system`/`eval`/`exec`, and that subprocess call sites still exist so those two guards
  cannot pass vacuously. This is the control that actually prevents process injection, and
  `agent-audit` does not check it: measured across `src/`, 0 `shell=True` across 14 subprocess
  call sites.
- **`agent-audit` in CI**, advisory (arXiv:2603.22853; 95.2% recall on its own benchmark vs
  23.8% for Semgrep). Advisory because it produced 4 verified false positives on its first run
  against this codebase — `AGENT-026` fired on `httpx.Client()` in files containing neither
  `@tool` nor `langchain` — and a blocking gate on a first run that noisy would be a
  permanently red build, which is the failure mode `dependency-audit` in the same file already
  documents. Its SARIF is produced but not uploaded: doing so needs a verified SHA and a
  `security-events: write` permission this workflow does not have.

### Added (Phase B — Knowledge Plane Completion)

- **`src/cyberbrain26/knowledge_plane/contracts.py`** — `KnowledgeDocument` extended with
  temporal fields (`valid_from: float = 0.0`, `valid_to: float = 0.0`, `superseded_by: str = ""`);
  `frozen=True` dataclass preserves backward compatibility — new fields appended at end with defaults.
- **`src/cyberbrain26/knowledge_plane/navigation.py`** — `KnowledgeGraph`/`EntityEdge` dataclasses
  for entity relationship graph; `KnowledgeNavigator` enhanced with `get_related_entities()`,
  `get_entity_neighbors()`, `get_entity_documents()`, entity indexing from classification metadata,
  and `check_temporal_validity()`.
- **`src/cyberbrain26/knowledge_plane/memory_service.py`** — PII/sensitivity auto-detection via
  `detect_sensitivity_from_bytes()` (MemStrata pattern); `auto_classify` + `MarkdownStore` for
  LLM classification; temporal `valid_from` set at registration; `keyword_search()` and
  `hybrid_search()` (BM25+semantic with RRF fusion via `_rrf_fuse`) added; `semantic_search()`
  preserved for backward compatibility.
- **`src/cyberbrain26/cli.py`** — `knowledge` command extended with `--auto-classify/--no-auto-classify`,
  `--validate-stale/--no-validate-stale`, and `--traversal` options for entity graph traversal.
- **`src/cyberbrain26/knowledge_plane/classifier.py`** — `detect_sensitivity()` and
  `detect_sensitivity_from_bytes()` regex-based PII/sensitivity detection; B904 fix for
  `sscanf` usage; unused `Any` import removed from `navigation.py`.

### Known Gaps

- Gate 3 remains **NOT PERFORMED**. No agent type with `bash` and an audit role exists in the
  Task tool, `.kilo/agent/*.md` permissions are not honoured there, and `bash` availability is
  non-deterministic within a session. See `.kilo/context/handoff/HANDOFF_2026-10-05-gate3.md`.

- **A narrow `bash` grant for `security-auditor` was inert and was recorded as if it
  worked.** Measured 2026-10-05 with two probes against each other: `tester` with
  scalar `bash: allow` returned `EXIT=0` / `PROBE_OK`, while `security-auditor` with
  `{"pytest": "allow", "*": "deny"}` returned `EXIT=126` with no tool at all. Kilo does
  not honour per-command `bash` dicts — a dict grants nothing, which is the same
  effective state as `deny`. Worse than a no-op: `PRIMARY_TOOL_ALLOW_EXCEPTIONS` recorded
  an exception that did not exist at runtime, i.e. a false proof of a security state,
  which is precisely what that suite exists to catch. Replaced with scalar `allow` and
  two guards (`test_bash_dict_grant_is_not_usable_...`,
  `test_agents_claiming_bash_are_exactly_the_measured_ones`); the first is
  mutation-verified non-blind. Owner decision 2026-10-05: the grant stays.
- **`route()` hid a free-only substitution.** `route(model_id="paid-b", free_only=True)`
  returned `free-a` with `reason = "cost optimized"`, so a caller could not tell its
  request was not honoured. Fail-closed behaviour is unchanged — a paid model is still
  never returned — only the disclosure: `reason` now names the requested model, states
  that it was not free-eligible, and names the replacement. Previously **no test
  protected this**, because the existing test asserts only `model_id != "paid-b"`.
  Three tests added and mutation M25 added to the harness. Owner decision 2026-10-05:
  substitute, but disclose.
- **Mutation harness repo-root resolution.** `mutation_check_f4.py` resolved the repo
  root as `Path(__file__).resolve().parent`, correct only while it sat in the repo root.
  Moving it to `scripts/` would have pointed all 26 mutations at non-existent paths, so
  every one would SKIP as "anchor not found" and the run would report zero blind tests
  while testing nothing. Root is now resolved by locating `src/cyberbrain26` among the
  file's parents, with a `SystemExit` that names this exact failure mode.

### Added

- **`.kilo/agent/gate3-auditor.md`** — a dedicated Gate 3 verifier whose charter bans
  template echoes, placeholder `file:line`, and "all five properties" style miscounts,
  the three failure modes that killed four consecutive `security-auditor` runs. Registered
  in the `kilo.jsonc` agent map and in the policy suite's classification and bash
  exception lists. **It is currently unusable**: the Task tool does not recognise the
  type (`Unknown agent type`) and ignores `.kilo/agent/*.md` when resolving permissions.
  Gate 3 is therefore recorded as NOT PERFORMED, not PASS — an independent Gate 3 is
  structurally unavailable in this toolchain.
- **M25** in the mutation harness covering the substitution disclosure; **M26** is
  documented as provably unreachable rather than reported as a blind test, because
  `route()` returns early at `router.py:189` so the over-claim branch cannot execute.

### Changed

- **`mutation_check_f4.py` → `scripts/f4-mutation-check.py`.** Still a tool, not a test:
  `pytest` does not collect it and no CI path runs it, because 26 controlled mutations
  would multiply suite runtime. Run it with
  `python3 scripts/f4-mutation-check.py`; the result is 23/26 detected, M23/M24
  structural (concurrency locks, undetectable single-threaded), M26 unreachable, zero
  blind tests.

### Added

- **Plan revision R1–R5** in `.kilo/plans/CyberBrain26cil-031026-v2.md` (owner-approved
  2026-10-04). Records that the mobile/chat layer is **transport and UI on top of the
  existing secured path**, Kilo stays the single orchestrator. R1: mobile UI is built
  before F8 as part of F1/F2, F8 stays the target for web/desktop. R2: the F2 "timeout"
  scenario becomes a durable queue wait with explicit continuation
  (`cli.py:86` defaults to 300 s). R3: the Kilo Gateway relay is registered as a new
  egress path (§4). R4: one host profile = one owner, `owner`/`viewer` roles prepared,
  multi-user only after a separate access model — which §5 F3 already said. R5: host
  shared folders (`vboxsf`/`9p`/`virtiofs`) are not admissible in the F6 profile and must
  be removed before any work mode. Goal (§1), module architecture (§3), the security
  chain (§4) and Gate 1–3 definitions (§8) are unchanged.
- **`.kilo/context/findings/2026-10-04_mobile-channel-gate0.md`** — why the VM boundary
  probe never produced evidence: the orchestrator had no `bash` tool exposed despite
  `bash: allow` in its frontmatter, `read` denies `/proc` and `/sys`, and the `tester`
  returned two unusable reports (paraphrased output, then a degenerate command loop).
  Dead-lettered rather than retried. Also records the process fix: raw-output evidence
  contract, a smoke check before any gate, and no verdict duty on the agent that runs
  commands.
- **`.kilo/context/runbooks/2026-10-04_mobile-channel-phase0.md`** — owner-facing phase 0
  runbook: read-only boundary probe, removal of the host shared folder, OpenClaw install
  with the ACP harness pinned to `kilocode`, `exec.security: "deny"` for the safe agent,
  `openclaw security audit`, and a hardened systemd user unit using `LoadCredential` for
  the Telegram token instead of a `chmod 600` file.

- **`.kilo/context/runbooks/2026-10-04_gate1-telegram-safe-mode.md`** — Gate 1 runbook:
  one read-only capability block first (`kilo acp` presence, `bwrap` namespace proof, the
  `git status` baseline for the negative test), then install, hardened service, and a
  seven-check acceptance matrix whose two decisive tests are "a foreign Telegram ID is
  refused without a session" and "an injected instruction does not execute anything".
- **`.kilo/context/handoff/HANDOFF_2026-10-04-mobile-channel.md`** — session handoff: what
  is `[OVĚŘENO]` with `file:line`, what is `[NE OVĚŠENO]`, the twelve owner decisions, a
  section-by-section comparison against the plan (what R1–R5 changed, what they did not
  touch, and what the plan never said and had to be decided), gate status, and the
  process fix after two dead-lettered probe reports.
- **`SESSION_HANDOFF.md`** — pointer to the newer handoff; the 2. 10. 2026 text is left
  intact rather than overwritten.

### Fixed

- **`mcp.cyberbrain26` is explicitly disabled again** (`.kilo/kilo.jsonc`). The working tree
  had `{"enabled": true}` while `src/cyberbrain26/mcp/` does not exist, which broke
  `tests/test_kilo_agent_policy.py::test_mcp_server_explicitly_disabled`. Restored to
  `{"enabled": false}` — fail-closed, no silent fallback. Owner-approved 2026-10-03.
- **`.kilo/agent/orchestrator.md` no longer contradicts its own frontmatter.** The body
  claimed `bash: deny` and `edit: deny` while lines 8/11 declare `allow` (D14). Text corrected;
  `agent_manager` stays `deny`, which is stricter than D14 allowed.

### Changed

- **Orchestration is now written down instead of improvised.** Added to
  `.kilo/agent/orchestrator.md`: a delegation matrix for all 10 subagents (when to use each,
  what it receives, what it must return, `bash`/`edit`), six standard workflows, and a fixed
  owner report format separating Gate 1 / Gate 2 / Gate 3 with `[OVĚŘENO]` / `[HYPOTÉZA]` /
  `[SIMULOVANÉ]` markers.
- **Nine subagent playbooks rewritten from generic boilerplate into real roles**
  (`code-reviewer`, `deep-research`, `implementer`, `pricing-researcher`, `security-auditor`,
  `threat-modeler`, `verifier`, plus corrections in `tester`): each now has a role definition,
  trigger conditions, required artifact input, step-by-step procedure, output format, hard
  boundaries, and Gate 3 conditions. Frontmatter unchanged in every file.
- `AGENTS.md` gained an "Agent orchestration" section pointing at the delegation matrix.
- `verifier.md` now states explicitly that it cannot run commands and must mark claims as
  `[POTVRZENÍ TESTEREM]` instead of asserting it ran tests — a read-only agent claiming
  execution is a fail-open in evidence, not in code.

### Verified

- Gate 2 **naměřeno vlastním během** (orchestrátor, `bash: allow` podle D14):
  `python3 -m pytest -q tests/test_kilo_agent_policy.py` → 54 passed in 0.31 s;
  `python3 -m pytest -q` → **591 passed in 10.23 s**; `python3 -m compileall -q src` → exit 0.
- Frontmatter všech 11 agentních `.md` znovu načten přes `yaml.safe_load`: každý má `mode`
  a neprázdný `permission`; soubor se žádnym parse-errorem tiše nedeaktivovaný není.
  `code-reviewer`/`deep-research`/`security-auditor`/`threat-modeler`/`verifier`/`promptmaster` = `mode: subagent|primary` podle role, `bash: deny`.
- **Výstupy subagentů nebyly použité jako důkaz.** Tři běhy `tester` se na stejném stavu
  rozcházely (nesprávné `mode`, duplicitní seznam, `TIMEOUT` s výstupem přesto přítomným).
  Zaznameno jako V16 v `.kilo/context/LESSONS_AND_DECISIONS.md`.

## [Unreleased] — F1: Kilo integration & Model Manager PoC

### Added
- Kilo plugin `.kilo/plugin/cyberbrain26-freeauto/` — "CyberBrain26 FreeAuto" model selection
  - `plugin.json`, `package.json`, `tsconfig.json` — plugin manifest & build config
  - `src/index.ts` — plugin entry with provider hook + role→model mapping
  - `src/bridge.ts` — TypeScript bridge calling Python CLI via subprocess
  - `src/anonymizer.ts` — PII sanitization (email, IP, phone, SSN, API key, CC) + CB26_MARKER_ removal
  - `cyberbrain26-freeauto.config.json` — role mapping: orchestrator→free, coder→efficient
- `src/cyberbrain26/model_layer/cli.py` — `list-models` CLI entry for the bridge
- `tests/test_kilo_plugin_integration.py` — 6 integration tests (manifest, config, CLI, TS compile, anonymizer, kilo snapshot)
- Egress sanitization wired into the Kilo plugin via `experimental.chat.messages.transform` (full outbound message history, before provider conversion) and `experimental.chat.system.transform` (system prompt) — both call `sanitizeRequest`, so PII and the synthetic `CB26_MARKER_` are stripped from the payload Kilo sends to the model provider

### Plugin structure correction
- `index.ts` rewritten to the real Kilo plugin API (verified against `Kilo-Org/kilocode` source, 2026-10-04): the plugin now exports a `PluginModule` (`{ id, server }`) where `server` is a `Plugin` **function** returning a flat `Hooks` object. The previous export was a bare object with `satisfies Plugin`, but `Plugin` is a function type — it never type-checked. Egress hooks are flat keys (`"experimental.chat.messages.transform"`, `"experimental.chat.system.transform"`), not nested under an `experimental` field.

### Open items — resolution (2026-10-04)
- **Local type-check: RESOLVED.** Installed `@kilocode/plugin`, `typescript`, and `@types/node` as devDependencies; added `"types": ["node"]` to the plugin `tsconfig.json`; deleted dead `src/provider.ts` (a superseded draft that imported a non-existent `kilo-plugin-typescript` module and was never imported by `index.ts`). `npx tsc --noEmit` now passes (exit 0) against the real `@kilocode/plugin` types — this validates the `PluginModule`/`Plugin`/`Hooks` structure, the egress hooks, and the type-honest `ProviderModelRecord` cast.
- **Response-side sanitization: RESOLVED by design (not wired).** The egress (request) sanitization is the complete security control: `experimental.chat.messages.transform` strips PII and the synthetic `CB26_MARKER_` from the full message history + system prompt BEFORE the payload is sent to the model provider. Because the model never receives PII, it cannot leak it back in the response — so response-side sanitization is redundant for the primary goal. `experimental.text.complete` was deliberately NOT wired: it covers text parts only, runs post-streaming (the streamed delta may already be visible to the user), and would add per-completion overhead for marginal defense-in-depth.
- **Provider registration: documented limitation (fundamental Kilo API).** A Kilo plugin cannot introduce a brand-new provider ID — the provider must already exist in the models.dev catalog, so `cyberbrain26-freeauto` provider registration is a demonstration of the hook, not a guaranteed path. Role→model assignment is configuration (`agent.<name>.model` in `kilo.jsonc`), not a plugin capability. The plugin is not yet registered in the project `.kilo/kilo.jsonc` `plugin` array — registration is a follow-up after the global config error below is fixed.
- **Empirical `kilo models` verification: BLOCKED on user environment.** `kilo models` fails on the GLOBAL config (`~/.config/kilo/kilo.jsonc`) with "Unrecognized keys: reasoning_display, retention". These keys are valid in Kilo 7.8.3+ (per npm `@kilocode/plugin` version) but the installed Kilo CLI is 7.6.2, which uses a strict Zod schema that rejects them. **Fix (user action, outside the project)**: upgrade Kilo CLI to 7.8.3+ (or remove `reasoning_display`/`retention` from the global config). This is not a project change.

### Fixed
- `src/cyberbrain26/pilot/workflow.py` — added runtime imports (AuditEventType, AuditLog, ApprovalStatus, PermissionDecision, DiffPresenter)
- `tests/test_pilot_workflow_e2e.py` — fixed _Approver._stop naming conflict, AuditQuery usage, cleanup ordering
- `tests/test_egress_gate_integration.py` — fixed _Approver._stop naming conflict
- `.kilo/plugin/cyberbrain26-freeauto/src/anonymizer.ts` — rewrote to sanitize the WHOLE JSON payload (was only sanitizing hardcoded top-level fields and called .replace() on regex objects); removed hypothetical `kilo-anonymizer` import; now redacts nested PII at any depth
- removed 4 stray duplicate files (dead/broken code): `src/cyberbrain26/index.ts` (called unimported `fetchModelsFromBridge`, required non-existent config path), `src/cyberbrain26/anonymizer.ts` (older duplicate), root `package.json`, root `tsconfig.json` — the real plugin lives in `.kilo/plugin/cyberbrain26-freeauto/` with its own build config

### Gates (F1 Gate 1/2)
- pytest: 626 passed, 1 skipped
- ruff: All checks passed
- mypy: Success, 70 source files, no issues
- CLI list-models: returns free + efficient models

### Status
- F1 Gate 1 (Implementation): PASS
- F1 Gate 2 (Operation): PASS
- F1 Gate 3 (Independent review): code-reviewer APPROVE; security-auditor PASS (anonymizer rewritten after initial FAIL)
- F1 type-check: PASS (`npx tsc --noEmit` exit 0 against real `@kilocode/plugin` types)
- F1 open: empirical `kilo models` + live egress verification blocked on the user's global Kilo config (upgrade Kilo CLI to 7.8.3+); plugin not yet registered in project `kilo.jsonc`

## [4.1.0]

Current verified state of the package (`cyberbrain26.__version__ == "4.1.0"`,
`pyproject.toml`). This entry describes what exists in the source tree, not a set of
marketing claims.

### Breaking changes

- **`cyberbrain run` now fails closed.** Every roadmap step is dispatched through
  `PtahBridge.execute_step` -> `InMemoryTaskDispatcher._authorize` -> (for code)
  `AuthorizedExecutionService` -> `BubblewrapExecutionProvider`. The previous direct
  `ModelGateway.chat(...)` call in `run` is gone, so no step reaches an agent without
  passing identity, scope, policy and approval. A step whose payload declares no `action`
  is **refused instead of executed under an inferred default**, and a step whose
  `depends_on` did not succeed is **never dispatched**. Any run with a refusal, failure or
  blocked step exits non-zero and prints no success wording. Committed to by decision D2 in
  `docs/product/DECISIONS_LOG_V5.md`; this entry discharges that commitment.
- **`--model` and `--lang` were removed** from `cyberbrain run` (decision D3). No model is
  called anywhere in `plan` or `run`. The direct consequence is that a roadmap produced by
  `cyberbrain plan` carries action-less steps and is therefore refused by `run`; a runnable
  roadmap currently has to be written by hand into `<data-dir>/roadmaps.json`.
- **Persistent storage is mandatory** for `plan` and `run`. There is no `--no-persist`
  flag on either command: approval requests and audit records written by `run` must be
  readable by a second terminal running `cyberbrain approvals`. The flag remains on
  `approvals` subcommands only.

### Added

- `cyberbrain doctor check` (alias `doctor main`): read-only installation self-report with
  13 checks, `--json` and `--strict`. It opens SQLite in `mode=ro`, creates no directory
  and no database, never changes the policy mode, contacts no model provider, and never
  prints an environment variable value — only `true`/`false` presence flags.
- `cyberbrain approvals pending | show | approve | deny | history`, registered in the CLI
  from `product/approvals.py`. Two-terminal approval flow over the shared SQLite store,
  transactional decisions, explicit non-zero exits for a missing or already-decided
  request.
- `cyberbrain run --scopes`: comma-separated permission scopes granted to the coder
  identity, validated against the security plane's `PermissionScope` enum. The default is
  empty and grants nothing, so a code step ends closed at the scope gate.
- `--approval-timeout` on `run`, default 300 s (the library default of 30 s is unusable
  for a two-terminal flow: on timeout the request is persisted as `cancelled` and
  disappears from `approvals pending`).
- Sandbox workspace confinement: a `workspace` from a step payload is resolved and checked
  against the sanctioned root `<data dir>/workspaces` before any process is launched;
  anything else is refused. The root can be moved explicitly via
  `CYBERBRAIN_WORKSPACES_ROOT`.
- Single resolution path for local state (`product/local_store.py`): `CYBERBRAIN_DATA_DIR`
  and `CYBERBRAIN_DB_PATH` are honoured by every command, so `roadmaps.json` and the
  SQLite store cannot diverge. Data directory `0700`, database file best-effort `0600`,
  WAL + `synchronous = FULL`, schema version 1, 7 tables.
- Documentation: `README.md` rewritten to the code, plus `docs/quickstart.md`,
  `docs/CAPABILITIES.md`, `SECURITY.md`, `CHANGELOG.md` and a placeholder `LICENSE`.

### Security posture

- The Bubblewrap provider raises when `bwrap` is not on `PATH`. There is no unsandboxed
  fallback anywhere in the execution path.
- CI job `no-bubblewrap-fail-closed` uninstalls Bubblewrap, asserts `bwrap` is
  unresolvable for both the shell and `shutil.which`, runs the suite, and then asserts
  that execution raises rather than degrading to host execution. This proves fail-closed
  behaviour only. It proves nothing about protection against hostile code.

### Known limitations in this release

- `plan` -> `run` is a dead end; no CLI command can emit a runnable roadmap yet.
- The planner produces a hardcoded placeholder plan.
- Approvals are not cryptographically bound to the code that will run.
- The sandbox is Linux/Bubblewrap only, with no seccomp filter, no LSM, no cgroup or
  process/memory/disk/inode quotas; `RLIMIT_CPU/AS/FSIZE` are cooperative child-process
  limits. `EXECUTION_SECURITY_REVIEW.md` records the disposition: suitable for a
  constrained local prototype, **not approved as a general-purpose hostile-code sandbox**.
- `AIFirewall` is not wired into the execution path or egress and guards nothing at
  runtime.
- `ModelGateway` / `ModelRouter` are library-only and unreachable from the CLI;
  `ModelGatewayConfig` still defaults to the cloud provider `litellm` with silent fallback.
- No audit query command; `AuditLog.query()` is library-only.
- Retention pruning is never scheduled from the CLI; logical deletion is not secure
  erasure; the store is not encrypted at rest.
