Watch
1
0
Fork
You've already forked RedFlag
0
RedFlag/protocol/README.md
Fimeg 517f1aca20 feat: pin complete mutation authority contract
Cut 1 completes the dormant MutationEnvelope contract without changing any runtime path. Bind target_id to the provisioned agent identity, require canonical UUID v4 authorization IDs, cap authorization lifetime at the fleet TTL, and add an unsigned MutationReceipt carrying the audit join.

Server, Agent, and helper share the same canonical bytes, key identity, receipt digest, and Ed25519 golden vectors. Current APT/DNF and Windows execution remains untouched.
2026-08-26 18:39:29 -04:00

211 lines
6.8 KiB
Markdown

# Mutation manifest wire contract
This directory owns the language-independent bytes shared by RedFlag Server, Agent, and
helper. The current closure-based capability token remains the only runtime execution path;
the mutation envelope is pinned and tested but not wired to a backend yet.
## Envelope
```text
MutationEnvelope
├── MutationManifest
│ ├── protocol_version
│ ├── operation_id
│ ├── target_id
│ ├── backend
│ ├── operation
│ ├── resolved_actions[]
│ │ ├── kind
│ │ ├── identity
│ │ └── payload exact backend-owned UTF-8 JSON bytes
│ └── evidence[]
│ ├── kind
│ └── digest SHA-256 hex
└── MutationAuthorization
├── protocol_version
├── authorization_id
├── manifest_hash
├── authority_kind
├── authority_id
├── target_id
├── issued_at / not_before / expires_at
├── decision
├── key_id
└── signature
MutationReceipt the response half; unsigned, produced by the executor
├── protocol_version
├── operation_id / manifest_hash / authorization_id the audit join
├── target_id
├── backend / operation
├── decision / reason
├── executed
├── verified_actions
├── exit_code
├── error
└── timestamp
```
The protocol envelope begins at format `1` in its own namespace. It is not a new numbered
generation of `CapabilityToken`; the two named contracts coexist until a backend migrates.
Unknown format values fail closed.
## Target identity
`target_id` names the body being mutated. For RedFlag today the mapping is normative and
narrow:
> **`target_id` MUST equal the locally provisioned RedFlag `agent_id`** — the identity the
> executor reads for itself from a root-owned, SEC-021-validated file, never from the
> envelope.
The manifest and the authorization each carry it, both inside signed bytes, and a verifier
requires them equal. An executor binds the envelope by comparing them with the host
identity it read independently.
The field keeps a generic name so a later protocol may define another target namespace
deliberately. Until one exists, there is no second namespace and no `body_id` — inventing
vocabulary ahead of the thing it names would weaken a binding that already works.
## Authorization discipline
`authorization_id` MUST be a canonical UUID v4 (8-4-4-4-12 lowercase hex, version nibble
`4`, variant nibble `8`/`9`/`a`/`b`) — the same discipline the standalone mint already
applies to `request_id`. It is the identifier an executor will eventually record as a
replay key, and a newline-delimited ledger matched line-by-line has no defence against an
identifier that contains a newline. The shape is checked before that day arrives.
`expires_at - not_before` MUST NOT exceed **3600 seconds**. That ceiling is derived, not
chosen: it is `DefaultTokenTTL`, the fleet minter's own window. Standalone mint is tighter
still at 600s. Both the signer and the verifier enforce it, so an over-long authorization
cannot be minted, not merely refused at the end.
## Canonical records
Every value is encoded as UTF-8 `decimal_byte_length:value`, with no separator between
fields. Each record begins with a length-prefixed domain string.
Manifest field order:
```text
redflag.mutation-manifest
protocol_version
operation_id
target_id
backend
operation
resolved_action_count
sorted(length-prefixed resolved-action records)
evidence_count
sorted(length-prefixed evidence records)
```
Resolved action field order:
```text
redflag.resolved-action
kind
identity
payload
```
Evidence field order:
```text
redflag.evidence
kind
digest
```
Authorization field order:
```text
redflag.mutation-authorization
protocol_version
authorization_id
manifest_hash
authority_kind
authority_id
target_id
issued_at
not_before
expires_at
decision
key_id
```
`manifest_hash` is lowercase hex SHA-256 over the manifest canonical bytes. The Ed25519
signature is over the authorization canonical bytes. The signature itself is not included
in those bytes; every other authorization field, including `key_id`, is.
## Collection semantics
Resolved actions and evidence are unordered multisets. Their canonical records are sorted
bytewise for hashing. Exact duplicates remain present, increment the count, and change the
hash. No implementation may convert either collection to a set.
A backend that needs ordered execution encodes the sequence inside one signed payload. The
common layer never infers package, WUA, Winget, Docker, or self-update semantics from that
payload.
Provenance is evidence. Cache paths, URLs, repository selectors, WUA identities, and other
execution inputs belong in the exact signed backend payload or must be derived
deterministically from it.
Evidence carries **digests, not prose**. A gate verdict, an operator identity, and an
override reason are the authority's own record and stay in its journal; what travels in
the manifest is a digest binding the decision to the evidence it was made over. The
SHA-256 shape check is what enforces that — reason text cannot be smuggled into a signed
manifest through an evidence value.
## Receipt
The receipt is the response half of the contract. It carries the audit join — operation
ID, manifest hash, authorization ID — so a local enforcement record and a Server history
row join on a tuple neither side had to guess. `decision` and `reason` keep the existing
`PolicyResult` taxonomy rather than inventing a second one.
**The receipt is not signed.** The executor is not a second cryptographic authority; this
is a record produced inside the trust boundary that already ran, or refused, the
operation. Its canonical bytes exist so a ledger can digest one without re-deriving field
order from JSON, not so anyone can verify it.
Receipt field order:
```text
redflag.mutation-receipt
protocol_version
operation_id
manifest_hash
authorization_id
target_id
backend
operation
decision
reason
executed "true" | "false"
verified_actions
exit_code
error
timestamp
```
Every field is present even when empty: a refusal that happens before the envelope parses
still produces a receipt, and its emptiness is part of the record.
## Golden fixture
`testdata/mutation-golden.json` pins:
- manifest canonical bytes and hash;
- authorization canonical bytes;
- authority key ID;
- deterministic Ed25519 signature;
- action/evidence ordering behavior;
- duplicate retention;
- receipt canonical bytes and digest;
- authorization_id shape and the authorization lifetime ceiling;
- tamper refusal for provenance, execution location, target, backend, resolved action,
authorization metadata, decision, time window, and unknown formats.
Go tests live in both capability packages. Rust tests live in `helper/src/mutation_protocol.rs`.