# cellos-projector
Project CellOS CloudEvents into current state. Reads the event log, runs
`CellStateProjection`, emits snapshots, renders audit docs.
## What it is
`cellos-projector` ships three binaries, one library, and zero ambition to
talk to a host. It is a pure consumer of CloudEvents — given a JSONL file
or a live JetStream stream, it folds events into `CellStateProjection`s
(defined in `cellos-core`) and answers two questions: "what is the current
state of this cell?" and "what does the audit document for this evidence
bundle look like?"
The crate sits at L8 of the layer model — strictly read-side, downstream
of everything else. It consumes the CloudEvent vocabulary owned by
`cellos-core`, the per-event signing wrapper introduced by
`cellos-supervisor` (I5), and the signed-trust-keyset envelopes verified
inside `cellos-core::verify_signed_trust_keyset_envelope`. ADR-0011
(HTTP control plane) explicitly delegates the replay-and-project work to
this crate, and ADR-0014 (formation CloudEvent state model) is the
contract for the formation aggregations.
What `cellos-projector` deliberately does NOT do:
- It does not write to the event log. Every binary is read-only.
- It does not run cells, admit specs, or mutate authority. The state
model it produces is *derived* — purely a function of the event log.
- It does not depend on `cellos-supervisor`. The supervisor's per-event
signing wrapper is decoded by a transport-type string constant in
`src/main.rs:16`, not by linking the producer crate.
- It does not bundle `cellos-host-telemetry`. The audit-doc renderer
reads only the on-wire JSON shape, not upstream typed schemas
(`src/lib.rs:20`).
## Public API surface
The library is intentionally small.
- `audit_doc::render_audit_doc(&serde_json::Value) -> String` — pure,
deterministic Markdown projection of a single
`cell.evidence_bundle.v1.emitted` CloudEvent. Returns a single-line
Markdown error sentinel rather than panicking on bad input (D11).
`src/audit_doc.rs:50`.
- `audit_doc::EXPECTED_TYPE` — the canonical CloudEvent type string the
renderer accepts. `src/audit_doc.rs:39`.
- `audit_doc::ERROR_SENTINEL_PREFIX` — stable single-line error marker
tests rely on. `src/audit_doc.rs:43`.
- `event_decode::decode_event(&[u8], &EventVerifierConfig) -> Result<CloudEventV1>`
— shared decode path used by both the JSONL projector and
`cellos-state-server`. Accepts raw `CloudEventV1` lines and
`SignedEventEnvelopeV1` wrappers transparently; verifies envelopes when
a keyring is configured. `src/event_decode.rs:100`.
- `event_decode::EventVerifierConfig` — verification config sourced from
process env. `src/event_decode.rs:31`.
- `build_info::BUILD_SHA`, `short_sha`, `version_line`,
`print_version_if_requested` — the `--version` / `-V` contract every
binary in this crate honours. `src/build_info.rs:20`.
### Binaries
- `cellos-projector` — read a JSONL stream, project it, print snapshots.
Default: print all snapshots as JSON; `--cell-id ID` / `--spec-id ID`
selects a single snapshot; `--pretty` enables pretty-printing.
`src/main.rs`.
- `cellos-state-server` — live fleet state projector over NATS
JetStream. Replays all events, tails new arrivals, serves current
snapshots via HTTP (`GET /healthz`, `GET /cells`, `GET /cells/<id>`,
`GET /compliance/export`). `src/bin/cellos-state-server.rs`.
- `cellos-audit-justification` — SEC-19 fleet audit. Replays
`cell.compliance.v1.summary` events from JetStream and reports
monoculture / template patterns in `dnsEgressJustification` strings.
Read-only — does not change admission gates.
`src/bin/cellos-audit-justification.rs`.
## Architecture / how it works
```
┌────────────────────────────────────────┐
│ JSONL file or NATS JetStream stream │
│ (cellos.events.>) │
└────────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ event_decode::decode_event │
│ - parse CloudEventV1 │
│ - unwrap SignedEventEnvelopeV1 │
│ - verify if keyring configured │
└────────────────────┬──────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ cellos_core::CellStateProjection (one per cell or spec) │
│ apply(event) → ProjectionLifecycleStage, │
│ ProjectionIdentityStage, │
│ ProjectionExportStage, ... │
│ snapshot() → CellStateSnapshot │
└─────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────┐ ┌──────────────────┐
│ cellos-projector (binary) │ │ cellos-state- │
│ JSON / pretty stdout │ │ server (HTTP) │
└─────────────────────────────────┘ └──────────────────┘
┌─────────────────────────────────┐
│ audit_doc::render_audit_doc │
│ Markdown audit page │
└─────────────────────────────────┘
```
The on-disk projection is keyed by `cell:<cellId>` or `spec:<specId>`
(`src/main.rs:233`), preserving correlation when events carry only one
of the two. Lines that fail to parse abort the run with a line-numbered
error; signed envelopes that fail verification are fatal when any
verifier keyring is configured (the producer-opt-in / consumer-opt-in
pair from `src/main.rs:152`).
The audit-doc renderer (`src/audit_doc.rs`) writes six sections — header,
lifecycle table, host-probe series summary, guest-event sample, residue
class footer, integrity attestations — in a deterministic order. The
output is byte-identical for byte-identical input, so it round-trips
cleanly through git.
## Configuration
### `cellos-projector` (JSONL binary)
| `CELLOS_EVENT_VERIFY_KEYS_PATH` | JSON keyring (kid → base64url Ed25519 pubkey). Empty/unset → unwrap signed envelopes without verifying. `src/main.rs:182`. |
| `CELLOS_EVENT_VERIFY_HMAC_KEYS_PATH` | HMAC-SHA256 keyring. Same shape; value is base64url raw bytes. `src/main.rs:198`. |
Argv: `cellos-projector <events.jsonl> [--cell-id ID | --spec-id ID] [--pretty]`. Also: `--version` / `-V`, `--help` / `-h`.
### `cellos-state-server`
| `CELLOS_NATS_URL` | `nats://localhost:4222` | NATS server URL. |
| `CELLOS_NATS_STREAM` | `CELLOS` | JetStream stream name. |
| `CELLOS_NATS_CONSUMER` | `cellos-state` | Durable consumer name. |
| `CELLOS_EVENT_SUBJECT` | `cellos.events.>` | Subject filter. |
| `CELLOS_STATE_ADDR` | `0.0.0.0:8080` | HTTP listen address. |
| `CELLOS_EVENT_VERIFY_KEYS_PATH` | unset | Same as the JSONL binary; envelope verification keys. |
| `CELLOS_EVENT_REQUIRE_SIGNED` | unset | When `1`/`true`/`yes`/`on`, reject raw `CloudEventV1` lines. `src/event_decode.rs:1`. |
### `cellos-audit-justification`
| `CELLOS_NATS_URL` | `nats://localhost:4222` | NATS server URL. |
| `CELLOS_NATS_STREAM` | `CELLOS_EVENTS` | JetStream stream name. |
| `CELLOS_EVENT_SUBJECT` | `cellos.events.>` | Subject filter. |
| `CELLOS_AUDIT_MONOCULTURE_THRESHOLD` | `5` | Min distinct cells per justification to flag. |
| `CELLOS_AUDIT_DRAIN_TIMEOUT_MS` | `2000` | Per-message timeout to detect stream drain. |
Stdout: JSON report. Stderr: human summary plus structured logs.
## Examples
Project a JSONL stream and print every snapshot pretty:
```bash
cargo run -p cellos-projector --bin cellos-projector -- \
/tmp/events.jsonl --pretty
```
Render an audit document from a single evidence-bundle event:
```rust
use cellos_projector::audit_doc::{render_audit_doc, EXPECTED_TYPE};
use serde_json::json;
let event = json!({
"specversion": "1.0",
"type": EXPECTED_TYPE,
"id": "uuid-1",
"source": "//cellos/supervisor",
"data": {
"cellId": "cell-abc",
"runId": "run-2026-05-16-001",
"specId": "spec-789",
"specSignatureHash": "sha256:...",
"lifecycle": [],
"hostSeries": {},
"guestEvents": [],
"residueClass": "none",
"attestations": {},
}
});
let markdown = render_audit_doc(&event);
println!("{markdown}");
```
Replay JetStream into live cell snapshots:
```bash
CELLOS_NATS_URL=nats://127.0.0.1:4222 \
CELLOS_NATS_STREAM=CELLOS \
CELLOS_STATE_ADDR=127.0.0.1:9090 \
cargo run -p cellos-projector --bin cellos-state-server
# in another shell
curl http://127.0.0.1:9090/cells
```
Audit DNS-egress justification entropy across a fleet:
```bash
CELLOS_NATS_URL=nats://127.0.0.1:4222 \
CELLOS_NATS_STREAM=CELLOS_EVENTS \
CELLOS_AUDIT_MONOCULTURE_THRESHOLD=10 \
cargo run -p cellos-projector --bin cellos-audit-justification \
> /tmp/justification-report.json
```
## Testing
```bash
cargo test -p cellos-projector
```
Tests under `crates/cellos-projector/tests/` exercise the pure paths:
- `audit_doc_round_trip.rs` — pin the byte-deterministic Markdown output
against a golden file.
- `i5_sign_emit_to_projector_e2e.rs` — end-to-end signing path: the
supervisor's `SigningEventSink` emits a wrapped envelope, the
projector decodes and verifies it.
- `signed_envelope_round_trip.rs` — `decode_event` against both raw and
wrapped lines.
- `smoke.rs` — projection key resolution and snapshot shape.
The two NATS-backed binaries (`cellos-state-server`,
`cellos-audit-justification`) are smoke-checked in workspace integration
tests; running them locally needs `nats-server -js` on the configured
URL.
## Related crates
- [`cellos-core`](../cellos-core/README.md) — owns
`CellStateProjection`, `CellStateSnapshot`, `CloudEventV1`,
`SignedEventEnvelopeV1`, the event-decoding primitives.
- [`cellos-supervisor`](../cellos-supervisor/README.md) — producer of
the wrapped `dev.cellos.events.signed_envelope.v1` transport this
crate unwraps.
- [`cellos-server`](../cellos-server/README.md) — the online HTTP API
that mirrors what `cellos-state-server` does over NATS directly.
## ADRs
- [ADR-0011](../../docs/adr/0011-cellos-server-http-control-plane.md) —
HTTP control plane; `cellos-server` delegates the replay/projection
contract this crate implements offline.
- [ADR-0014](../../docs/adr/0014-formation-cloudevent-state-model.md) —
formation event model; the `CellStateProjection` reducer honours it.