RACT

RACT Provenance and Separation Statement

RACT is independent of any private system, and every artifact it writes is cryptographically bound to its origin. This document points at commands and source symbols rather than prose.

What a Rootknot attests

Every file the recursion loop writes carries a Rootknot — a signed provenance capability (src/ract/core/rootknot.py, Rootknot). A Rootknot binds an artifact to six things:

The signature is produced by Rootknot.sign(key) and checked by Rootknot.verify(pubkey). The signing primitive is cryptography’s ed25519 (src/ract/core/keys.py, SessionKey), a public, audited library. There is no proprietary crypto anywhere in the chain.

Extended attestations (v0.4). v0.4 substrate sidecars add environment_signature, acceptance_suite_digest, predicate_results, manifest_digest (RK-3); v0.4-ALM adds antilazy_signature, gate_results, reversal_taint (AL-1). Both extend the same signed binding; see the Sidecar schemas table below.

How RACT stays independent of private systems

RACT depends only on its declared, public dependencies (pyproject.toml): pyyaml, httpx, zstandard, rich, and cryptography.

How to verify a Rootknot without the tool

Each indexed artifact is stored two ways: a SQLite index at .rack/rootknots.db in the workspace root (ProvenanceIndex, src/ract/core/provenance.py) and a sidecar file .<artifact>.rootknot.json beside the artifact.

To verify by hand: read the sidecar, recompute the canonical bytes as Rootknot.canonical_bytes() does — v0.5.1 module_03 uses ract.canonical.dumps_jcs() (RFC 8785 JCS, NFC-normalised, codepoint-sorted keys, no whitespace) — and check the ed25519 signature against the generator’s public key. The CLI verb

ract provenance verify <path>

automates this: loads the sidecar, recomputes the artifact digest, resolves the generator’s public key from the local key store (including archived keys), checks the signature, and prints valid / invalid with exit code 0 / 1. Session public keys live in <state_dir>/ract/keys/*.pem.

What happens if a Rootknot is missing or invalid

Before every recursion step, verify_workspace(...) checks three invariants across every indexed artifact (src/ract/core/provenance.py):

If any fails, the loop halts immediately with TerminationCause.PROVENANCE_FAILURE (T3) and names the sub-clause that tripped. It does not continue, does not write further artifacts, and does not silently repair.

Sidecar schemas (v0.4)

Reader dispatches on the top-level schema field.

Offline verification. v2 sidecars embed sandbox_pubkey_b64 for RK-3.1; v3 sidecars also embed alm_pubkey_b64 for AL-1.1; save sites may embed generator_pubkey_b64. Recompute canonical bytes, ed25519-verify against embedded pubkeys, check digest fields against the registered set.

Authorship bound. The sidecar proves its own consistency. Whether the embedded pubkeys are the ones the operator expected is out-of-band work. The v0.4-ALM design REQUIRES cross-checking the ALM verifier pubkey against .rack/alm/archive/ or an operator registry (see ADR-0023).

Sidecar RK-1 RK-2 RK-3 AL-1 --strict
sidecar/v1 required required skipped (warn) skipped (warn) refused
sidecar/v2 required required required skipped (warn) refused
sidecar/v3 required required required required required
sidecar/v4 required required required required required (canonical bytes via JCS; workspace_digest, prompt_digest, run_id bound into the signed surface)

v0.5.1 additions

v0.5.2 additions – Rootknot signature hardening (module_01)

Closes deep-audit A F-1 / F-2 / F-5 (systemic v4-label attack surface) and Ox Alpha M-1 (DOWNGRADE via relabel-and-resign) and M-2 (forward-compat drift on unknown majors).

Operator-visible CLI changeract provenance verify grows one flag:

ract provenance verify PATH --min-schema INT

When --min-schema 4 is set and the sidecar labels schema_version=1, the verifier prints:

invalid
schema_version=1 below policy floor 4; refusing the weaker
attestation (deep-audit A M-1 DOWNGRADE defence).

When the sidecar labels schema_version=4 but the v4 fields were stripped by an attacker:

invalid
v4 schema-label but v4 fields empty: ['workspace_digest',
'prompt_digest', 'run_id']; the label carries no attestation
guarantee (deep-audit A F-1). Re-sign under a v3 factory or
supply the missing fields.

Regression tests (all under tests/):

v0.5.2 module_06 read-boundary refusal (module_01 Q3 fold)

ract.core.provenance._knot_from_json refuses to load a sidecar whose schema field carries an unknown named literal (e.g. sidecar/v9). Previously the reader silently downgraded to the v1 shape and the module_01 verifier then refused it – but by then the reader had already committed to wrong fields. Pairing this read-side refusal with module_04’s write_sidecar_header primitive means unknown sidecar formats fail loudly at ingest.

Regression test: tests/unit/test_module_06_carryover_folds.py.

v0.5.2 module_06 ambient run_id boundary regex (module_04 C-6 fold)

ract.runtime.bootstrap_ambient_from_env validates the RACT_RUN_ID env value against ^[A-Za-z0-9_-]{1,240}$ at subagent boot. Rationale: module_05’s per-run {run_id}.verify.json sidecar takes this value straight into a filesystem path, so a path-separator / shell-metacharacter / dot-traversal value in the env is a direct path-shape vector on a trust boundary module_04 itself created. On format failure the runtime emits runtime.run_id.env_rejected, logs a WARN, and falls through to synthetic-orphan generation – the subagent runs; only the poisoned identifier is discarded.

Regression test: tests/unit/test_module_06_carryover_folds.py.