# Security Policy

## Supported versions

CyberBrain26 is a **tested local Python prototype**, not a production release. Only the
current working state is supported. There are no tags and no released version stream yet,
so the table below is deliberately short.

| Version | Supported | Notes |
|---|---|---|
| Working tree / `4.1.0` (`pyproject.toml`) | Yes | The only state anyone has reviewed. |
| Anything earlier | No | No release exists; there is nothing to upgrade from. |

Security fixes are applied to the working tree. There is no CVE process, no patch
back-porting, and no signed release.

## Reporting a vulnerability

> **PLACEHOLDER — MUST BE COMPLETED BEFORE ANY RELEASE.**
> The contact below is a placeholder. Replace it with a monitored address owned by the
> project (a security mailbox, a `security.txt`, or a private GitHub Security Advisory
> page). Do not publish a release while this section still says "placeholder", and do not
> accept reports in a personal inbox.

- Reporting address: **`REPLACE_ME@example.invalid`** (placeholder, monitored by nobody).
- Encryption: none offered. Do not send secrets; send plain text.
- Expected response: to be defined. Nothing has been agreed yet.
- Do **not** open a public issue for a suspected vulnerability.

Because there is no remote configured on this repository, do not assume any disclosure
channel exists. Establish one first.

## Threat model and scope

This is the part that matters. Read it before trusting this product with anything.

### Deployment model assumed

A single Linux workstation, a single local operator, a local SQLite store in an
owner-only directory (`0700`), and code an agent wants to execute. No network listener
exists. No multi-user deployment is supported.

### What IS in scope and genuinely protected

1. **Fail-closed authorization of agent actions.** Every step executed by
   `cyberbrain run` passes identity (trust level), permission scope, policy and approval
   before any process is started. Missing identity, missing scope, policy denial, a
   missing or denied approval, a timeout, and a cancellation all end the action and write
   an audit record. The default is deny: an action that is not explicitly permitted does
   not run. The action vocabulary is itself closed: `security_plane/actions.py:76-95`
   normalises every action string (`strip().lower()`), and an action that is neither
   scope-mapped, nor classified sensitive, nor listed as a known-safe non-mutating action
   is denied before anything else is consulted
   (`security_plane/permission/__init__.py:156-185`). Opting a new action in is a code
   change, not a runtime string.
2. **Bounded sandbox execution with no network.** Python or shell runs inside
   `bwrap --unshare-all --die-with-parent --new-session`: read-only `/usr`, fresh `/dev`
   and `/proc`, tmpfs `/tmp`, a cleared environment, an isolated network namespace with
   no external interface, and exactly one read-write bind mount, itself confined to
   `<data dir>/workspaces`. If `bwrap` is missing, execution is refused; there is no
   unsandboxed fallback.
3. **A local audit trail that is tamper-evident in practice, not tamper-proof.** Security
   decisions are recorded as structured events in the local SQLite store, with WAL and
   `synchronous = FULL`. Accurate wording: the trail is *append-oriented, detailed and
   hard to lose by accident*. It is **not** tamper-evident against the operator, because
   the operator owns the file. There is no hash chain, no signature and no separate
   storage. Anyone who can edit the database can edit the record of what happened.
4. **Operator-gated human approval, requested once per code step.** Approval requests
   are persisted, decisions are transactional, and a two-terminal flow (`run` in one
   terminal, `cyberbrain approvals approve` in another) is the only way an action
   proceeds. A code step is authorised once, not twice: the outer gate propagates its
   decision as an `ExecutionAuthorization` grant and the inner gateway re-verifies that
   grant instead of raising a second request. See open gap 1 for exactly what that
   re-verification does and does not prove.

### What is explicitly OUT OF SCOPE

Do not rely on this product for any of the following. None of it is implemented, and no
part of it is promised by this document:

- **Hostile-code execution.** No guarantee of protection against malicious or arbitrarily
  hostile code. No seccomp filter, no Landlock or other LSM, no cgroup, PID, memory, disk
  or inode quotas; `RLIMIT_CPU` / `RLIMIT_AS` / `RLIMIT_FSIZE` are cooperative limits of
  the child process, not enforcement. The full host `/usr`, including site-packages, is
  readable and executable inside the sandbox. The project's own
  `EXECUTION_SECURITY_REVIEW.md` states the disposition: **"suitable for a constrained
  local prototype; NOT approved as a general-purpose hostile-code sandbox."**
- **Kernel exploits, container escapes, or general host isolation.**
- **Multi-user isolation, team administration, role management, SSO.**
- **Central or shipped audit**, log shipping, SIEM integration, alerting.
- **Compliance certification** of any kind: no ISO 27001, SOC 2, GDPR or equivalent
  claim is made or implied.
- **macOS and Windows.** The execution path is Linux/Bubblewrap only. `doctor` and
  `approvals` work elsewhere; `run` does not execute code.
- **Encryption at rest.** The local store is a plain SQLite file with owner-only
  permissions on a best-effort basis. Permissions are `chmod`-based; a failure to tighten
  them is swallowed.
- **Secure erasure.** Retention pruning and logical deletion are not secure erasure;
  deleted rows can persist in the SQLite file and in filesystem slack.
- **Automatic decisions about whether data may leave the device.** The product makes no
  egress decision, and the model layer's default configuration would silently fall back to
  a cloud provider if a library consumer called it directly.
- **Availability, durability, backup or restore guarantees.**
- **Change review.** There is no git worktree, no `git diff` review, and no merge step. The
  operator is responsible for reviewing what a run changed.

## Open gaps a security reviewer must see

These are unresolved at this commit. Each item below carries the file:line that shows
it is still open. Cross-references: `docs/CAPABILITIES.md` and
`docs/SECURITY_ONE_PAGER.md` §10.

1. **The execution payload is digest-verified between the two authorization gates, but
   not signed.** This gap changed materially, so it is stated precisely: both the old
   wording and an over-corrected one would mislead a reviewer.
   *What is now true:* the outer gate mints an `ExecutionAuthorization` carrying a
   SHA-256 digest over the exact provider inputs — `code`, normalised `language`,
   `workspace`, `timeout_seconds` (`security_plane/actions.py:118-147`) — and the
   inner `AuthorizedExecutionService` re-verifies identity, canonical action, approval
   status and that digest (`execution_plane/secure_gateway.py:208-241`). A payload that
   changed after the operator approved it is **refused** as
   `approval_denied / payload_changed_after_approval`
   (`execution_plane/secure_gateway.py:232`), audited, and **no second approval request
   is created**.
   *What is still open:* that is **verification, not a signature**. The digest lives in
   the same trust domain as the payload, so an attacker with write access to the
   approval record before the inner gate runs, or with the ability to substitute the
   payload at exactly the right moment, is not stopped by this control. Two further
   residuals follow from the mechanism itself:
   - The grant travels in a `ContextVar` set around the dispatcher's own `await`
     (`security_plane/actions.py:217-236`). Code that hands the agent call to a plain
     thread executor or `run_in_executor` loses the context, so the gateway falls back
     to raising a fresh approval request. That direction is safe — it fails closed —
     but it is a UX regression: the operator is asked twice and the run can hang until
     the approval timeout.
   - The digest covers exactly those four provider inputs. A future fifth
     security-relevant parameter would be **unverified** unless the digest is extended
     alongside it. It is not a general-purpose payload commitment.
   *Stale wording elsewhere:* `cyberbrain approvals approve` still prints "not
   cryptographically bound to the code that will run"
   (`product/approvals.py:432`), and the module docstring says the same
   (`product/approvals.py:24-27`). That sentence is no longer a complete description of
   the control; treat this item as the accurate one.
2. **`ModelGatewayConfig` defaults to the cloud provider `litellm` with silent
   fallback.** `default_provider = "litellm"`
   (`control_plane/model_gateway/__init__.py:43`), and a failed primary call is retried
   against the fallback chain with only a `logger.warning`
   (`control_plane/model_gateway/__init__.py:251-253`, `:325-348`). Unreachable from
   the CLI today, so no CLI path can egress — but the library default contradicts the
   "never silently route local work to a cloud provider" promise for any future consumer.
3. **`AIFirewall` guards nothing.** 727 lines of threat inspection
   (`security_plane/firewall/__init__.py`) that no runtime path calls, on execution or
   egress. The only instantiations in the repository are inside that module itself and
   in `tests/test_security_gate.py`; `cli.py`, `orchestration/ptah_bridge.py`,
   `extension_layer/registry_impl.py` and `execution_plane/secure_gateway.py` never
   construct one.
4. **The event bus does not authorise on the publish side.** `AsyncEventBus.publish`
   still fans a `*_TASK_REQUEST` out to every subscriber with no permission check.
   What changed is the receiving side, and it is a real closure: an agent handles a bus
   task request only through an injected authorizer and **refuses when none is present**
   — `BaseAgent.authorizer` defaults to `None`
   (`agents/orchestrator.py:159-180`), a missing authorizer counts as a refusal, and any
   raise from the authorizer produces a recorded refusal instead of work
   (`agents/orchestrator.py:200-206`, `:220-227`). A published bus event therefore can
   no longer drive an unauthorised execution. What remains open is narrower: the bus is
   still a shared in-process channel with no publish-side authorisation and no subscriber
   allow-list, so any component holding it can still cause denial-of-service or audit-log
   noise, and the receiving control is only as good as whoever wired the authorizer in.
5. **Quality gates partly self-report.** `no_hardcoded_secrets`
   (`security_plane/quality_gates/__init__.py:241-244`) and `default_deny_policy`
   (`security_plane/quality_gates/__init__.py:457-460`) are constructed with
   `status=GateStatus.PASS` as a literal and never inspect anything.
6. **File permissions are best-effort.** Four `chmod` calls catch `OSError` and `pass`
   silently: `product/local_store.py:107`, `data_plane/sqlite_store.py:142`,
   `data_plane/sqlite_store.py:185` and `data_plane/sqlite_store.py:262`. A fifth site,
   `product/local_store.py:243-250`, was tightened to log a warning instead of
   swallowing the failure; the other four still do not report it.
7. **Retention is never scheduled** from the CLI. `prune_expired` exists
   (`security_plane/audit/__init__.py:271`, `security_plane/approval/__init__.py:290`,
   `data_plane/sqlite_store.py:195`) but has no call site in `cli.py` or anywhere under
   `src/cyberbrain26/product/`; the only caller is `tests/test_sqlite_persistence.py:269`.
   The 90-day window is a configuration value, not an enforced behaviour.
8. **The audit trail is only as trustworthy as the operator** who owns the file, as
   described in the protected-controls section above. No hash chain, no signature and no
   separate storage, and `AuditLog.clear()` still issues `DELETE FROM audit_events`
   (`security_plane/audit/__init__.py:265-269`).
9. **Bubblewrap version is not pinned or verified.** The binary is resolved from `PATH`
   with `shutil.which` and there is no `--version` or minimum-version check anywhere in
   `execution_plane/bubblewrap_provider.py:129-135`.
10. **The sandbox has no syscall filter and no resource quotas.** No `--seccomp`, no
    Landlock or other LSM, no cgroup, and no PID, memory, disk or inode quotas. The only
    limits are cooperative child-process `RLIMIT_CPU` / `RLIMIT_AS` / `RLIMIT_FSIZE`,
    applied by a script the child itself runs
    (`execution_plane/bubblewrap_provider.py:225-230`).
11. **Two quality-gate detectors can never pass in any shipped path,** and one
    `ThreatType` value is never constructed anywhere in the source. The only production
    invocation of the gate runner passes `agents=None`
    (`product/doctor.py:839-846`), so `all_agents_have_identity`
    (`security_plane/quality_gates/__init__.py:118-128`) and `no_untrusted_agents`
    (`:131-149`) always report `SKIP`. `ThreatType.MALICIOUS_DOCUMENT`
    (`security_plane/firewall/__init__.py:41`) has zero constructions under `src/`.

## Controls closed since the previous revision of this document

Recorded so a reviewer does not re-report them, and so they are not silently
over-credited either. Each is a control **in code with test coverage**, not a claim of
production assurance.

- **Deny-by-default for unregistered actions.** Closed. An action outside the canonical
  vocabulary is now denied rather than skipped by the scope gate; action strings are
  normalised; the four previously-unreachable sensitive actions carry scope
  requirements (`publish_content`, `send_external_message`, `data_upload` →
  `PermissionScope.NETWORK`; `credential_access` → `PermissionScope.SECRETS`,
  `security_plane/actions.py:264-271`); and `InMemoryTaskDispatcher.dispatch()` refuses
  a payload with no `action` instead of inferring one
  (`extension_layer/registry_impl.py:297-308`).
- **The API gateway is fail-closed.** Closed. A route with `requires_auth=True` and no
  auth provider now returns 401 instead of running unauthenticated, anonymous routes
  require an explicit `allow_unauthenticated=True`, keys are stored as sha256 digests
  and compared with `hmac.compare_digest`, `claims` never carry the raw key, rate
  limiting keys on principal / client address / a shared `unidentified` bucket instead
  of being silently skipped, and a 5xx returns a generic body plus an opaque
  `correlation_id` unless `debug=True` (`control_plane/api/__init__.py:224-244`,
  `:250`, `:401-437`).
  *Accurate nuance:* that module has **zero registered routes and zero importers from
  `src/`** — a repository-wide search for `control_plane.api` finds no importing
  module. It is therefore a hardened, tested building block, **not a hardened exposed
  endpoint**, and it is not counted as one anywhere in this document.
- **Sandbox workspace confinement.** Closed. The workspace must resolve inside
  `<data dir>/workspaces`, overridable via `CYBERBRAIN_WORKSPACES_ROOT`, or execution
  is refused before any process is launched; symlink escape and `..` traversal are
  handled by resolving both sides
  (`execution_plane/bubblewrap_provider.py:91-94`, `:122-127`, `:167`).

## Honest status

No independent security review of this product exists. The CI workflow has never run
(this repository has no remote configured), so no CI result may be cited as evidence. Any
deployment beyond a local prototype requires your own review.
[OVĚŘENO] AIFirewall zůstává mrtvý kód — žádná instance v src/
[SIMULOVANÉ] Pruning není naplánováno — nutné ruční volání prune_expired()
