Watch
1
0
Fork
You've already forked RedFlag
0
RedFlag/protocol
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Fimeg 320ad46e00 feat(helper): accept dormant mutation envelopes
Cut 2 adds a verify-envelope path beside the closure-token executor. The helper parses an envelope, binds it to the independently provisioned agent identity, validates time and lifetime bounds, selects the trusted key, verifies the signed contract, checks backend payload shape, and then refuses with backend_not_migrated.

Every path emits an unsigned MutationReceipt. No backend executes, no replay slot is consumed, and no artifact-custody claim is made until the first existing backend migrates.
2026-08-26 18:39:41 -04:00
..
testdata feat: pin complete mutation authority contract 2026-08-26 18:39:29 -04:00
README.md feat(helper): accept dormant mutation envelopes 2026-08-26 18:39:41 -04:00

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

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:

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:

redflag.resolved-action
kind
identity
payload

Evidence field order:

redflag.evidence
kind
digest

Authorization field order:

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:

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, and the helper's own envelope-path tests live in helper/src/main.rs.

Executor status

redflag-helper verify-envelope --envelope-file <path> [--receipt-file <path>] verifies an envelope through the helper's real pipeline — parse, host binding, trusted keyring, signature, time, lifetime ceiling, backend payload shape — and then refuses with backend_not_migrated, because no backend executes through this path yet.

It records no replay state. Execute mode records a token only after a plan is built, so an operation that cannot run never burns its slot; nothing here can run, so nothing here records. Replay identity stays unproven until the first backend migrates.

It also enforces no artifact custody. When a backend carries a local cached artifact as executor input, the helper must rehash it before use. Remote repository execution keeps the limits RAF/security/05-supply-chain-gate.md already states.