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.
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:
plan_id, step_id),assumption_digest),GeneratorRef: model name/version,
session id, public key id),parent_digests, a DAG),artifact_digest, SHA-256 of the artifact bytes),signature, 64 bytes).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.
RACT depends only on its declared, public dependencies (pyproject.toml):
pyyaml, httpx, zstandard, rich, and cryptography.
src/ract/ imports nothing private. The lint
test tests/test_public_provenance.py fails the build on a forbidden
import.ract.yaml; the
default local adapter points at 127.0.0.1..rack/. Nothing leaves
the machine.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.
Before every recursion step, verify_workspace(...) checks three invariants
across every indexed artifact (src/ract/core/provenance.py):
acceptance_suite_digest is a currently registered suite (RK-3.2);
the predicate_results tuple is non-empty (RK-3.3); the manifest_digest
is a currently registered manifest (RK-3.4).GateResult in gate_results has
passed=True OR carries a handshake_id that appears in the
operator’s approved-handshake set (AL-1.2); reversal_taint is
"clean" OR the run’s plan_id appears in the operator’s
accepted_partial_taint_runs set (AL-1.3).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.
Reader dispatches on the top-level schema field.
sidecar/v1 (v0.3) — no schema field. Carries signature.
RK-3 skipped with DeprecationWarning; --strict refuses.sidecar/v2 (v0.4 substrate) — schema: sidecar/v2. Adds
generator_signature, environment_signature,
acceptance_suite_digest, predicate_results, manifest_digest.sidecar/v3 (v0.4 ALM) — schema: sidecar/v3. Adds
antilazy_signature, gate_results (tuple of eight per-gate
records), reversal_taint ("clean" or "partial"), and the
base64 raw ALM verifier pubkey (alm_pubkey_b64) so AL-1 can be
verified from the sidecar plus an out-of-sidecar registry check.sidecar/v4 (v0.5.1) — schema: sidecar/v4. Adds three
OPT-IN fields: workspace_digest, prompt_digest, run_id
(see ract.core.workspace_digest, IntentCompiler.compile,
ract.runtime.get_current_run_id). Canonical bytes are RFC
8785 JCS (module_03).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) |
Rootknot.canonical_bytes() dispatches on
schema_version; v0.5.0 sidecars (sidecar/v3) verify
unchanged. A v0.5.1 knot without the v4 fields produces
byte-identical output; with them writes sidecar/v4. Test:
tests/unit/test_schema_version_backread..ract/manifest_ledger.jsonl; details in
docs/THREAT_MODEL.md). Durability layer for RK-3: sidecar
proves point-in-time attestation, ledger proves witness. ract
verify walks both; missing-entry or broken-chain fails
verify_chain.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).
_KNOWN_SCHEMA_VERSIONS =
frozenset({1, 2, 3, 4}). Any knot with a schema_version
outside the allowlist is refused at construction time
(Rootknot.__post_init__ raises RootknotSchemaViolation) and
at verify time (RK-UNKNOWN-SCHEMA predicate). This is
fail-closed by design: a v0.5 tool reading a v0.6 sidecar
refuses with an actionable reason (“upgrade the verifier”)
rather than silently reinterpreting under weaker semantics.schema_version == 4 whose workspace_digest,
prompt_digest, or run_id is empty is refused at
construction (F-1 closure) AND at verify
(RK-V4-LABEL-MISMATCH, F-2 closure). The verifier check is
authoritative because copy / pickle deserialisation paths
bypass __post_init__.verify_workspace(...) and
verify_artifact(...) grow a min_schema_version: int | None
kwarg. When set, any knot with schema_version <
min_schema_version is refused with RK-DOWNGRADE-REFUSED.
Default None preserves the backward-compat contract that
v0.5.0 v1/v2/v3 payloads still verify. ract provenance verify
--min-schema 4 sets strict.Operator-visible CLI change – ract 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/):
property/test_rootknot_v4_post_init_validation.py (F-1 / F-5)unit/test_rootknot_downgrade_defense.py (M-1)unit/test_rootknot_forward_compat_reject.py (M-2)unit/test_rootknot_v4_missing_field_verify_fails.py (F-2)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.
ract.core.provenance.RootknotUnknownSidecarFormat
with the offending literal in the message.schema legacy v0.3 v1 payloads continue to load
unchanged.sidecar/v2, sidecar/v3, sidecar/v4.Regression test: tests/unit/test_module_06_carryover_folds.py.
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.
_normalize_run_id_or_raise public
helper): ract.runtime.RunIdFormatError with the offending
value truncated to 80 chars in the message.Regression test: tests/unit/test_module_06_carryover_folds.py.