# cellos-cortex
The CellOS ↔ Cortex bridge. The *only* crate that owns wire shapes spanning
both systems.
## What it is
Two directions of flow cross this crate, and only this crate:
- **Cortex → CellOS**: Cortex hands a bounded [`ContextPack`] (memory
digest + doctrine refs + task description + optional expiry) to
[`CortexCellRunner`], which translates it into a
[`cellos_core::types::ExecutionCellDocument`] and submits it through a
[`CellSubmitter`] (typically backed by `cellos-supervisor`).
- **CellOS → Cortex**: every CellOS lifecycle
[`cellos_core::types::CloudEventV1`] is serialized into a Cortex-shaped
ledger row by [`CellosLedgerEmitter`], which implements
[`cellos_core::ports::EventSink`].
The bridge sits at L6 of the layer model, beside `cellos-supervisor`.
The discipline (ADR-0008) is hard: neither `cellos-*` nor `cortex-*`
imports the other directly. This crate owns the simplified `ContextPack`
wire shape (intentionally flatter than Cortex's internal
`cortex_context::ContextPack`) and the `CortexLedgerRow` ingest shape;
either side may evolve internal types without breaking the bridge.
What `cellos-cortex` deliberately does NOT do:
- It does not depend on `cortex-*` crates. Cortex `ContextPack` JSON is
parsed via `ContextPack::from_cortex_json` rather than by linking the
upstream type (`src/context.rs:23`).
- It does not depend on CellOS supervisor internals beyond what `lib.rs`
exposes — submissions go through the `CellSubmitter` trait, not a
direct method call.
- It does not own Cortex's signed canonical ledger format. This crate
emits the *ingest* row (`CortexLedgerRow`); Cortex anchors and re-signs
on receipt.
- It does not perform agent reasoning. The pack's `task` is appended to
the cell's `argv` and the agent inside the cell does the work.
## Public API surface
The full re-export list lives in `src/lib.rs:30`. By module:
### `context`
- `ContextPack` — the bridge-shaped pack (memory digest, doctrine refs,
task, optional expiry). `src/context.rs:48`.
- `ContextPack::new(task)` — convenience constructor for tests.
- `ContextPack::from_cortex_json(&[u8])` — parse a real Cortex wire-form
pack and project to the bridge shape (see source TODO for the
compatibility-gap rationale).
- `ContextPack::is_expired(now_ms)` — `expires_at` comparator used by
the runner to refuse stale packs.
### `runner`
- `CortexCellRunner` — the dispatcher. `src/runner.rs:123`.
- `CellSubmitter` — the abstract submission trait. `src/runner.rs:113`.
- `CellSubmissionOutcome` — the submitter's return value: cell id,
optional exit code, captured lifecycle events. `src/runner.rs:90`.
- `CortexCellResult` — the bridge-shaped result of a completed cell
run (exit code, success, destroyed-at, export paths, doctrine refs
propagated from the dispatched pack). `src/runner.rs:77`.
- `ContextPackTranslation` — recorded pack → spec translation, useful
for audit. `src/runner.rs:105`.
- `wait_for_result_from_jsonl(cell_id, jsonl_path, timeout)` —
free-function result-reception helper that tails the supervisor's
JSONL stream and builds a `CortexCellResult`. `src/runner.rs:415`.
- `CELL_OS_JSONL_EVENTS_ENV` — env-var name (`CELL_OS_JSONL_EVENTS`)
the helper reads. `src/runner.rs:55`.
### `ledger`
- `CellosLedgerEmitter` — the `EventSink` adapter that turns CloudEvents
into ledger rows. `src/ledger.rs:171`.
- `CellosLedgerEmitter::new(sink)` — unsigned legacy path.
- `CellosLedgerEmitter::with_signing_key(sink, Option<SigningKey>)` —
explicit Ed25519 signing key.
- `CellosLedgerEmitter::with_env_signing(sink)` — read the Ed25519 seed
from `CELLOS_CORTEX_LEDGER_SIGNING_KEY_BASE64`. Pair with the free
associated function `CellosLedgerEmitter::from_env_signing_key()` if
you want the raw `Option<SigningKey>`.
- `LedgerSink` — pluggable destination trait. `src/ledger.rs:125`.
- `NdjsonLedgerSink` — append-NDJSON file sink. `src/ledger.rs:132`.
- `CortexLedgerRow` — the Cortex-shaped ingest row.
`src/ledger.rs:70`.
- `EmittedLedgerEntry` — wire shape with optional detached signature.
`src/ledger.rs:110`.
- `LEDGER_SIGNING_KEY_ENV` — `"CELLOS_CORTEX_LEDGER_SIGNING_KEY_BASE64"`.
`src/ledger.rs:178`.
- `ledger::http_sink::HttpLedgerSink` — HTTP transport, gated behind the
`http-ledger` cargo feature. `src/ledger.rs:277`.
### `policy`
- `DoctrineAuthorityRule` — the per-doctrine constraint
(`max_ttl_seconds`, `require_secret_delivery`, `forbid_egress`,
`require_egress_justification`, `correlation_label`).
`src/policy.rs:47`.
- `DoctrineAuthorityPolicy` — the doctrine id → rule table.
`src/policy.rs:88`.
- `DoctrineAuthorityPolicy::empty()` — no-op policy.
- `DoctrineAuthorityPolicy::built_in()` — the canonical ADR-0009
defaults.
- `DoctrineAuthorityPolicy::load_from_env()` /
`load_from_path(&Path)` — merge operator overrides on top of the
built-ins (per-id keys win, missing keys fall back).
- `apply_policy(policy, pack, spec)` — apply every doctrine rule that
matches the pack's `doctrine_refs`. `src/policy.rs:238`.
## Architecture / how it works
```
┌────────────────────────────────────────────────────────────┐
│ Cortex (separate workspace) │
│ │
│ ContextPack (rich) ── reduce to wire JSON ─► │
└───────────────────────────────────┬────────────────────────┘
▼
┌────────────────────────────────────────────────────┐
│ cellos-cortex (this crate) │
│ │
│ ContextPack::from_cortex_json │
│ │ │
│ ▼ │
│ CortexCellRunner.translate(pack) │
│ 1. structural: task → argv, expires_at → TTL │
│ 2. apply_policy(DoctrineAuthorityPolicy, ...) │
│ 3. ExecutionCellDocument │
│ │ │
│ ▼ │
│ CellSubmitter.submit(document) ─────────────────►── cellos-supervisor
│ │ │
│ ▼ │
│ cloud_event_v1_cortex_dispatched → event_sink │
│ │
│ │
│ CellosLedgerEmitter (EventSink) │
│ CloudEventV1 → CortexLedgerRow │
│ → optional Ed25519 sign │
│ → LedgerSink::append │
│ │
└───────────┬────────────────────────────────────────┘
▼
NdjsonLedgerSink | HttpLedgerSink | (operator-supplied)
```
Doctrine policies are monotonic toward *least* authority
(`src/policy.rs:13`):
- `max_ttl_seconds` only clamps TTL *down*.
- `require_secret_delivery` only *strengthens* secret delivery; a broker
mode is never downgraded to `Env`.
- `forbid_egress: true` empties `authority.egressRules`.
- `require_egress_justification: true` strips rules whose
`dnsEgressJustification` is missing.
- `correlation_label` records *which* doctrine rule fired in
`spec.correlation.labels`.
Rules that would *raise* authority are silently dropped (ADR-0009
§Consequences).
Signed ledger entries (session 14 doctrine, `src/ledger.rs:17`): when
constructed with a signing key, each emitted `EmittedLedgerEntry`
carries `cellos_sig` — a base64 (URL-safe, no-pad) detached Ed25519
signature over the canonical JSON bytes of the inner `event`. Cortex
verifies entries against the operator-pinned verifying key; unsigned
entries preserve the legacy wire shape exactly (`cellos_sig` omitted via
`#[serde(skip_serializing_if = "Option::is_none")]`).
## Configuration
| `CELLOS_CORTEX_LEDGER_SIGNING_KEY_BASE64` | URL-safe, no-pad base64 of the 32-byte Ed25519 seed used to sign ledger entries. Read by `CellosLedgerEmitter::with_env_signing` (the constructor) and `CellosLedgerEmitter::from_env_signing_key` (the raw-key helper). Empty/unset → unsigned. `src/ledger.rs:178`. |
| `CELLOS_CORTEX_POLICY_PATH` | Path to a JSON file holding a `DoctrineAuthorityPolicy`. Merged on top of `built_in()`. Read by `DoctrineAuthorityPolicy::load_from_env`. `src/policy.rs:204`. |
| `CELL_OS_JSONL_EVENTS` | JSONL CloudEvent stream `wait_for_result_from_jsonl` tails to build `CortexCellResult`. `src/runner.rs:55`. |
| `CORTEX_LEDGER_ENDPOINT` | HTTP endpoint for `http_sink::HttpLedgerSink::from_env` (feature `http-ledger`). `src/ledger.rs:13`. |
### Cargo features
| `http-ledger` | Pull in `reqwest` and build `http_sink::HttpLedgerSink`. |
## Examples
Build a runner that dispatches Cortex packs to an in-process submitter
(typically a `cellos-supervisor` adapter), with the built-in doctrine
policy:
```rust
use std::sync::Arc;
use cellos_cortex::{
CellSubmitter, ContextPack, CortexCellRunner,
CellosLedgerEmitter, NdjsonLedgerSink,
DoctrineAuthorityPolicy,
};
// Sink for CellOS → Cortex ledger rows.
let sink = Arc::new(NdjsonLedgerSink::new("/var/log/cellos-cortex.ndjson"));
let emitter = Arc::new(
CellosLedgerEmitter::with_env_signing(sink).expect("signing key parse"),
);
// `submitter: Arc<dyn CellSubmitter>` is built at the composition root —
// in tests we use an in-process fake; in production it wraps the supervisor.
let submitter: Arc<dyn CellSubmitter> = /* ... */;
let runner = CortexCellRunner::new(
submitter,
vec!["/usr/local/bin/cortex-agent".into()],
)
.with_default_ttl_seconds(600)
.with_policy(DoctrineAuthorityPolicy::load_from_env().expect("load policy"))
.with_event_sink(emitter);
// Dispatch a pack:
let pack = ContextPack::new("review pull-request #42");
let outcome = runner.dispatch(&pack).await?;
println!("dispatched cell {}", outcome.cell_id);
```
Verify a signed ledger entry on the Cortex side (see
`tests::verify_signature_roundtrip` for a worked example).
## Testing
```bash
cargo test -p cellos-cortex
```
Tests under `crates/cellos-cortex/tests/` exercise the bridge
end-to-end: pack → spec translation, policy application, ledger
signature round-trip, JSONL result reception. No live broker or
network required.
The `http-ledger` feature is gated by default; to exercise it:
```bash
cargo test -p cellos-cortex --features http-ledger
```
## Related crates
- [`cellos-core`](../cellos-core/README.md) — owns `CloudEventV1`,
`ExecutionCellDocument`, `EventSink`, the lifecycle event builders.
- [`cellos-supervisor`](../cellos-supervisor/README.md) — the typical
`CellSubmitter` backend (linked here but not the other way).
- `cellos-sink-jsonl` — pairs naturally with
`wait_for_result_from_jsonl` on the receiver side.
## ADRs
- [ADR-0008](../../docs/adr/0008-cellos-cortex-integration-boundary.md)
— this crate's reason for existing; the import-direction discipline.
- [ADR-0009](../../docs/adr/0009-cortex-doctrine-to-cellos-authority-mapping.md)
— doctrine → authority mapping; the contract `apply_policy` enforces.