Problem Statement
RecourseOS evaluates consequences of actions before execution. But there's no way to prove that evaluation happened.
Today, an agent can claim "I checked with RecourseOS and it said allow" without verification. The agent might have:
- Lied about checking
- Hallucinated the response
- Skipped evaluation entirely
- Received a different verdict than claimed
This makes consequence evaluation advisory. The agent can ignore it with no evidence trail.
Solution: Cryptographic Attestation
Attestations provide cryptographic proof that:
- A specific input was evaluated
- By a specific RecourseOS instance
- At a specific time
- Producing a specific verdict
With attestations, third parties (CI systems, approval workflows, audit logs, agent platforms) can require proof of evaluation before allowing execution. The advisory tool becomes a gating tool.
The killer feature is non-repudiation: agents cannot claim they checked without producing a signed artifact from RecourseOS.
Attestation Schema
interface ConsequenceAttestation {
// Protocol version
version: 'recourse.attestation.v1';
// What was evaluated
input: {
hash: string; // SHA-256 of canonical input
source: 'terraform' | 'shell' | 'mcp';
timestamp: string; // ISO 8601, evaluation time
};
// Replay/freshness protection
nonce: string; // Random 128-bit value, hex-encoded
expires_at: string; // ISO 8601, attestation validity window
// Chain to related attestations
chain_id?: string; // Links approval attestations to evaluation
parent_attestation?: string; // Hash of attestation this one references
// The evaluation result
evaluation: {
riskAssessment: 'allow' | 'warn' | 'escalate' | 'block';
worstTier: 1 | 2 | 3 | 4 | 5;
reportHash: string; // SHA-256 of full ConsequenceReport
mutationCount: number;
hasUnrecoverable: boolean;
};
// Who evaluated
evaluator: {
instanceId: string; // Unique RecourseOS instance identifier
version: string; // RecourseOS version
publicKey: string; // PEM-encoded public key
keyAttestation?: string; // Optional: HSM/TPM provenance
};
// Cryptographic binding
signature: string; // Ed25519 signature over all fields
signatureAlgorithm: 'Ed25519';
}
Schema Design Notes
| Field | Purpose |
|---|---|
nonce | Prevents replay attacks. Each evaluation generates a fresh nonce. |
expires_at | Verdicts have a shelf life. Default: 15 minutes. Configurable. |
chain_id | Links approval attestations to evaluation attestations for audit trails. |
keyAttestation | Future-proofing for HSM/TPM-backed keys. Not required for v1. |
§4 Canonicalization
This section uses RFC 2119 keywords (MUST, MUST NOT, SHOULD, MAY).
4.1 Signed Payload Construction
The signed payload MUST consist of all attestation fields except the signature field. The key_id field MUST be included in the signed payload; it is not metadata.
To construct the signed payload from an attestation object:
- Remove the
signaturefield from the attestation object. All other fields MUST remain. - Serialize the resulting object according to RFC 8785 (JSON Canonicalization Scheme).
- The output is the canonical byte sequence to be signed.
4.2 RFC 8785 Compliance
Implementations MUST serialize the signed payload according to RFC 8785. This specification does not define a subset or extension of RFC 8785; full compliance is required.
Key requirements from RFC 8785 that implementations MUST observe:
- Object key ordering: Keys MUST be sorted lexicographically by UTF-16 code unit value.
- Number representation: Numbers MUST NOT have leading zeros, trailing decimal zeros, or a positive exponent sign. Numbers MUST use the shortest representation that preserves the value.
- String escaping: Only characters that MUST be escaped per RFC 8259 (control characters, quotation mark, reverse solidus) are escaped. Unicode characters above U+001F MUST NOT be escaped.
- Whitespace: No whitespace between tokens except within strings.
- Unicode normalization: Strings MUST be in NFC (Canonical Decomposition, followed by Canonical Composition) before serialization.
Implementations MUST pass the RFC 8785 test vectors published at test/canonicalization-vectors.json. These vectors include RFC 8785 reference examples and derived edge cases for number representation, Unicode normalization, and key ordering.
4.3 Signing Procedure
To sign an attestation:
- Construct the signed payload per Section 4.1.
- Compute the Ed25519 signature over the canonical byte sequence using the signing key identified by
key_id. - Encode the signature as base64url (RFC 4648 Section 5, no padding).
- Set the
signaturefield to the encoded value.
The key_id field MUST be set before signing. The signing key MUST be in the active state (see Section 5.2).
4.4 Verification Procedure
To verify an attestation signature:
- Extract the
signaturefield and decode from base64url. - Construct the signed payload per Section 4.1 (removing
signature, canonicalizing the remainder). - Retrieve the public key corresponding to
key_idfrom the key registry (see Section 5.3). - If no key exists for
key_id, or the key is in thependingorcompromisedstate, verification MUST fail. - Verify the Ed25519 signature over the canonical byte sequence using the retrieved public key.
- If verification succeeds and the key is in the
active,deprecated, orretiredstate, the attestation is valid.
§5 Key Management
5.1 Key Identifier
Each signing key MUST have a unique identifier (key_id). The key_id MUST be a non-empty string consisting of printable ASCII characters (U+0021 through U+007E).
Recommended format: {instance_id}-{sequence_number} where sequence_number is a monotonically increasing integer starting at 1. Example: recourse-prod-1, recourse-prod-2.
Implementations MUST NOT reuse a key_id for different key material, even after the key transitions to compromised.
5.2 Key Lifecycle States
Each key exists in exactly one of five states:
| State | Signing | Verification | Description |
|---|---|---|---|
pending | MUST NOT | MUST fail | Key generated but not yet valid. Used to pre-publish keys before rotation. |
active | MAY | MUST succeed | Key available for signing new attestations. |
deprecated | MUST NOT | MUST succeed | Key no longer signs but existing attestations remain verifiable. Normal state after scheduled rotation. SHOULD remain in this state for at least 90 days. |
retired | MUST NOT | MUST succeed | Key permanently out of rotation. Attestations remain verifiable indefinitely. Terminal state for keys that were never compromised. |
compromised | MUST NOT | MUST fail | Key cannot be trusted. Attestations signed by this key MUST be rejected regardless of when they were signed. |
An instance MUST NOT have more than one key in the active state at any time.
State transitions:
pending → active → deprecated → retired
↓ ↓ ↓ ↓
└─────────┴──────────┴──────────┴───→ compromised
Legal transitions:
pending→active(activation)pending→deprecated(key generated but never used, superseded)pending→compromised(key material leaked before activation)active→deprecated(scheduled rotation)active→compromised(key material leaked while active)deprecated→retired(retention period complete)deprecated→compromised(key material leaked after rotation)retired→compromised(historical compromise discovered)
No state may transition backwards. No state may transition to pending or active from any other state.
State transitions are operator-initiated. A transition MUST be reflected in the key registry with an incremented registry_version before the transition takes effect for signing or verification. A key MUST NOT sign attestations in a state until that state is published in the registry; a verifier MUST NOT apply a state change until it has fetched a registry reflecting that change.
5.3 Key Registry
Each RecourseOS instance MUST publish a key registry containing all non-compromised keys. The registry MAY also include compromised keys for audit purposes.
The registry MUST be available at: {instance_base_url}/.well-known/recourse-keys.json
The registry URL is the canonical reference for key material. Instances deployed behind load balancers, CDNs, or multiple domains MUST ensure all paths resolve to the same registry content. The instance_base_url used in attestations MUST be consistent; verifiers are not required to follow redirects or resolve aliases.
Registry schema:
{
"instance_id": "recourse-prod",
"keys": [
{
"key_id": "recourse-prod-1",
"algorithm": "Ed25519",
"public_key": "<base64url-encoded public key>",
"state": "deprecated",
"valid_from": "2026-01-15T00:00:00Z",
"valid_until": "2026-04-15T00:00:00Z",
"deprecated_at": "2026-04-01T00:00:00Z"
},
{
"key_id": "recourse-prod-2",
"algorithm": "Ed25519",
"public_key": "<base64url-encoded public key>",
"state": "active",
"valid_from": "2026-04-01T00:00:00Z",
"valid_until": null
}
],
"registry_version": 2,
"updated_at": "2026-04-01T12:00:00Z"
}
Field requirements:
valid_from: Timestamp when key enteredactivestate. REQUIRED foractive,deprecated, andretiredkeys.valid_until: Timestamp when key will transition todeprecated. MAY be null for the currentactivekey.deprecated_at: Timestamp when key entereddeprecatedstate. REQUIRED fordeprecatedandretiredkeys.registry_version: Monotonically increasing integer. MUST increment on any change.
5.4 Key Rotation Procedure
To rotate from key A to key B:
- Generate key B. Set state to
pending. Publish in key registry. - Set
valid_untilon key A to a future timestamp (rotation time). - At rotation time: Set key B to
activewithvalid_fromequal to rotation time. Set key A todeprecatedwithdeprecated_atequal to rotation time. - New attestations MUST be signed with key B. Attestations signed with key A remain verifiable.
- After a retention period (operator-defined, SHOULD be at least 90 days), key A MAY transition to
retired.
The overlap window (steps 3-4) ensures verifiers fetching the key registry mid-rotation can verify attestations signed by either key.
5.5 Verifier Key Caching
Verifiers MAY cache the key registry. The default cache TTL is 24 hours; operators MAY configure shorter TTLs for high-security deployments or longer TTLs for stable deployments.
Cached registries MUST be refreshed when:
- A
key_idis encountered that is not in the cache, OR - The cache TTL has expired, OR
- Verification fails with a cached key (to check for state changes).
Rollback protection:
When refreshing, verifiers MUST compare registry_version of the fetched registry against the cached version:
- If fetched version > cached version: Accept the fetched registry, update cache.
- If fetched version = cached version: Accept (no change).
- If fetched version < cached version: Reject the fetched registry. This indicates a rollback attack or stale server response. Verification MUST fail.
Cache desync recovery:
If a verifier's cache becomes corrupted or desynchronized (e.g., restored from backup, clock skew), rollback protection may prevent normal refresh. To recover:
Verifiers MAY implement a "force refresh" operation that discards the cached registry_version and accepts the fetched registry unconditionally. This operation:
- MUST require explicit operator action (not automatic).
- MUST NOT be triggered by failed verifications alone.
- MUST log the event as a security-relevant operation.
The log entry MUST include: timestamp of the force refresh, operator identity if authentication is available, the discarded cached registry_version (or "none" if cache was empty), the accepted registry_version, and any operator-provided justification. Implementations without authentication MAY omit operator identity but MUST log the other fields.
After a force refresh, normal rollback protection resumes from the newly cached registry_version.
Implementations that do not support force refresh MUST document this limitation. Operators of such implementations recover from cache desync by clearing all cached state, which is equivalent to a new verifier bootstrapping.
§6 Transport
6.1 Attestation Delivery
Attestations MUST be delivered via two channels:
- Embedded: Included in the MCP tool response that returns the evaluation result.
- URL: Retrievable at a stable URL for later verification.
Both channels deliver the identical attestation. The attestation_uri field in the attestation provides the URL for the durable copy.
6.2 Embedded Delivery
When RecourseOS returns an evaluation result via MCP, the response MUST include an attestation field containing the complete attestation object.
{
"verdict": "recoverable-from-backup",
"tier": 3,
"reasoning": "...",
"attestation": {
"key_id": "recourse-prod-2",
"attestation_uri": "https://recourse.example/.well-known/attestations/abc123.json",
"input": { ... },
"output": { ... },
"evaluator": { ... },
"timestamp": "2026-05-01T14:30:00Z",
"signature": "..."
}
}
The attestation object is the same object that would be retrieved from attestation_uri.
6.3 URL Delivery
Each attestation MUST be retrievable at the URL specified in its attestation_uri field. The URL MUST remain stable for the attestation's retention period (see Section 6.5).
URL format: {instance_base_url}/.well-known/attestations/{attestation_id}.json
Attestation ID derivation:
The attestation_id MUST be derived from the attestation content fields, excluding fields that depend on the ID itself. Specifically:
- Construct an object containing exactly these fields from the attestation:
input,output,evaluator,timestamp,key_id. - Canonicalize this object per RFC 8785.
- Compute the SHA-256 hash of the canonical byte sequence.
- Take the first 16 bytes of the hash and encode as lowercase hexadecimal (32 characters).
The attestation_id is this 32-character hex string.
The attestation_uri and signature fields are NOT inputs to the attestation_id derivation. This avoids circularity: the ID can be computed before the URI is constructed, and the URI can be constructed before signing.
Signing order:
When creating an attestation:
- Populate
input,output,evaluator,timestamp,key_id. - Derive
attestation_idper the procedure above. - Construct
attestation_uriusing the derived ID. - Add
attestation_urito the attestation object. - Sign the attestation per Section 4.3 (signature covers all fields including
attestation_uri).
The attestation_uri IS part of the signed payload; an attacker cannot redirect verifiers to a different URL without invalidating the signature. Only the ID derivation excludes it to break the circularity.
The response MUST be the attestation object with Content-Type: application/json.
6.4 Consistency Requirement
The embedded attestation and the URL-retrieved attestation MUST be byte-for-byte identical when canonicalized per RFC 8785.
A verifier that receives an embedded attestation MAY fetch the URL copy and compare. The cross-check is performed by parsing both attestation copies as JSON, canonicalizing each independently per RFC 8785, and comparing the resulting byte sequences. Any difference in the canonical forms—including field ordering, whitespace, or encoding—indicates tampering or implementation error and MUST cause verification to fail.
Verifiers are NOT REQUIRED to perform this cross-check, but high-assurance deployments SHOULD.
6.5 Retention
Attestations MUST remain retrievable at their attestation_uri for at least the retention period of the signing key (from valid_from to when the key enters retired or compromised state).
After the signing key is retired or compromised, the attestation URL MAY return 410 Gone. The attestation remains cryptographically verifiable if a verifier has the key's public key, but the authoritative URL is no longer obligated to serve it.
6.6 Failure Modes
If the URL endpoint is unavailable, verifiers MUST NOT treat this as attestation invalidity. The embedded copy remains verifiable using the key registry.
If the key registry is unavailable, verification MUST fail. The attestation cannot be verified without access to the public key.
§7 Agent-Side Opt-In Semantics
This section uses RFC 2119 keywords. Sections 7.1–7.8 are normative. Section 7.9 is non-normative guidance.
7.1 Response Structure
When attestation is enabled, RecourseOS responses include an attestation field at the root level of the consequence report:
{
"schemaVersion": "recourse.consequence.v1",
"riskAssessment": "escalate",
"assessmentReason": "Recoverability needs human review",
"summary": { ... },
"mutations": [ ... ],
"attestation": {
"input": { "source": "shell", "input": "rm -rf /data" },
"output": { ... },
"evaluator": "recourse:blast-radius:1.0.0",
"timestamp": "2026-05-01T14:30:00Z",
"key_id": "recourse-prod-1",
"attestation_uri": "https://recourse.example/.well-known/attestations/abc123.json",
"signature": "..."
}
}
The attestation field is a sibling of decision, summary, and mutations. It is metadata about the evaluation, not part of the evaluation result.
When attestation is disabled (default), the attestation field is absent from the response. Agents MUST NOT assume its presence. Agents MUST check for field existence before accessing attestation data.
7.2 Attestation Content
The attestation.output field contains a copy of the consequence report at the time of signing. This copy excludes the attestation field itself to avoid circular embedding.
Size consideration: The attestation roughly doubles response size because output embeds the full consequence report. For evaluations with many mutations (e.g., large Terraform plans), this overhead may be significant. Agents operating under tight context limits MAY:
- Verify the attestation immediately, then discard
attestation.outputand retain only the signature and URI for audit purposes. - Store the full attestation externally and reference it by
attestation_uri.
Agents MUST NOT modify attestation.output if they intend to verify the signature later; any modification invalidates the signature.
7.3 Attestation Modes
Agents operate in one of four attestation modes. Each mode has explicit requirements:
| Mode | Requirements | Use Case |
|---|---|---|
| ignore | Agent MUST NOT verify. Agent MAY log attestation presence. Agent proceeds based on decision field alone. | Development, low-stakes operations, agents without verification capability. |
| log | Agent MUST record the attestation (or its hash and URI) before proceeding. Agent MUST NOT block on verification. Agent MAY verify asynchronously after proceeding. Agent proceeds based on decision field. | Compliance environments requiring audit trails without blocking on verification. |
| verify | Agent MUST verify before proceeding. Agent MUST refuse to proceed on verification failure. If attestation is absent, agent MAY proceed with a warning logged. | High-assurance deployments where verification failure is meaningful. |
| require | Agent MUST verify before proceeding. Agent MUST refuse to proceed on verification failure. If attestation is absent, agent MUST refuse to proceed. | Zero-trust deployments; agents that must prove they checked. |
The protocol does not mandate which mode agents use. The opt-in model allows gradual adoption: agents can start in log mode, then enable verify or require as confidence grows.
7.4 Verification Procedure
This section describes a RECOMMENDED verification sequence. Implementations MAY reorder steps if the functional result is equivalent, but all validation checks MUST be performed.
An agent in verify or require mode SHOULD perform these steps before acting on the evaluation result:
- Check attestation presence: If
attestationfield is absent, behavior depends on mode. Inverifymode, log warning and proceed. Inrequiremode, MUST reject. - Check trusted instance: If
trusted_instancesis non-empty, the scheme and host ofattestation_uriMUST match an entry. If no match, MUST reject. - Fetch key registry: HTTP GET to
{instance_base_url}/.well-known/recourse-keys.json. Theinstance_base_urlis the scheme and host portion ofattestation_uri. Cache rules per Section 5.5 apply. - Locate public key: Find the entry in
keyswherekey_idmatchesattestation.key_id. If not found, MUST reject. - Check key state: If the key's
stateispendingorcompromised, MUST reject. - Construct signed payload: Remove the
signaturefield from the attestation. Canonicalize the remainder per RFC 8785. - Verify signature: Decode
signaturefrom base64url. Verify Ed25519 signature over the canonical payload using the public key. If verification fails, MUST reject. - Accept: If all checks pass and key state is
active,deprecated, orretired, the attestation is valid. Agent MAY proceed according to thedecisionfield.
The entire verification procedure requires two HTTP requests (evaluation + key registry) plus local cryptographic operations. Verification latency is typically under 100ms on local networks.
7.5 Cross-Check (Optional)
For high-assurance verification, agents MAY perform the cross-check described in Section 6.4:
- Fetch the attestation from
attestation_uri. - Canonicalize both the embedded attestation and the fetched attestation per RFC 8785.
- Compare the canonical byte sequences.
- If they differ, MUST reject the attestation.
This cross-check detects tampering between the MCP response and the durable attestation store. It adds one HTTP request to the verification flow.
Cross-check is NOT REQUIRED for verification. The signature alone provides integrity; the cross-check provides defense-in-depth against implementation bugs or man-in-the-middle attacks that modify the embedded copy.
7.6 Verification Failure Handling
When verification fails, agents in verify or require mode:
- MUST NOT proceed with the action that triggered evaluation.
- MUST log the failure with reason:
signature_invalid,key_not_found,key_compromised,key_pending,instance_not_trusted,cross_check_mismatch, ornetwork_error. - SHOULD escalate to a human operator if the agent has escalation capability (see below).
Escalation patterns: Escalation means halting autonomous execution and requesting human review. Agents MAY implement escalation as:
- Blocking prompt: Pause execution and display the failure to the user, waiting for explicit approval to continue or abort.
- Notification: Send an alert (message, email, webhook) to a designated operator and either pause or abort.
- Ticket creation: Create an incident ticket and abort execution, requiring human resolution before retry.
The choice of escalation pattern is implementation-defined. The requirement is that autonomous execution MUST NOT continue; a human MUST be involved in the decision to proceed.
Verification failure vs. verdict: Verification failure is independent of the evaluation verdict. Even if decision is block, verification failure adds information: the verdict itself may be fabricated. An agent that would have blocked anyway SHOULD still log the verification failure as a security event.
Retry behavior: Agents MAY retry verification for transient failures (network_error). Agents MUST NOT retry on cryptographic or trust failures (signature_invalid, key_compromised, key_pending, instance_not_trusted, cross_check_mismatch).
7.7 Absence of Attestation
If the attestation field is absent from a response, the agent is receiving an unauthenticated evaluation. This occurs when:
- The RecourseOS instance has attestation disabled (default).
- The evaluation failed before attestation could be generated.
- The response is from an older RecourseOS version without attestation support.
Mode-specific behavior:
ignore: Proceed normally. Absence is expected.log: Log that no attestation was received. Proceed normally.verify: Log a warning (attestation_absent). MAY proceed. The operator accepts that some evaluations will be unauthenticated.require: MUST reject. MUST NOT proceed without attestation. The agent fails closed.
The absence of attestation is represented by field omission, not by a null or empty value. Agents check for attestation with if ('attestation' in response) or equivalent.
Security consideration: In require mode, absence of attestation is a security signal, not a benign default. It may indicate the RecourseOS instance was downgraded, replaced by an impersonator, or evaluation was bypassed. The conservative response is to refuse to proceed.
7.8 Agent Configuration
Agent operators SHOULD be able to configure attestation behavior:
| Setting | Values | Default |
|---|---|---|
attestation_mode | ignore, log, verify, require | ignore |
trusted_instances | List of instance_base_url values | [] (accept any) |
key_cache_ttl | Duration in seconds | 86400 (24 hours) |
cross_check | true, false | false |
trusted_instances semantics:
This is an allow-list, not a trust-without-verification list. When non-empty:
- Attestations from listed instances are verified normally per Section 7.4.
- Attestations from unlisted instances MUST be rejected with reason
instance_not_trusted. - An empty list means any instance is accepted for verification (but still verified).
Matching is performed on the scheme and host portion of attestation_uri. Example: if trusted_instances contains https://recourse.example, an attestation with attestation_uri of https://recourse.example/.well-known/attestations/abc.json is accepted.
key_cache_ttl and rollback protection:
Agent-side key caches MUST follow the rollback protection rules from Section 5.5. When a cached registry's registry_version is higher than a fetched registry's version, the fetched registry MUST be rejected. The key_cache_ttl controls how long before a cache refresh is attempted, not whether rollback protection applies.
7.9 Reasoning About Attestations (Non-Normative)
This section provides guidance for LLM-based agents. It is non-normative; implementations are not required to support these patterns.
Agents with reasoning capability can interpret attestation fields directly:
evaluatoridentifies the evaluation logic version.timestampindicates when the evaluation occurred.key_ididentifies the signing key.attestation_uriprovides a durable reference.signatureis opaque to reasoning but can be verified programmatically.
An agent asked to "verify the attestation you just received" can:
- Extract the
attestation_uriandkey_idfrom context. - Invoke a verification tool (if available) or describe the verification steps.
- Report whether the signature is valid.
The field names are chosen for clarity when read by both humans and language models. An agent that understands "this is the input that was evaluated, this is the output that was produced, and this signature proves the binding" has correctly interpreted the attestation.
7.10 End-to-End Example
This example walks through a complete verification flow for an agent in require mode.
Step 1: Agent receives evaluation response
POST /api/evaluate → 200 OK
{
"schemaVersion": "recourse.consequence.v1",
"riskAssessment": "block",
"assessmentReason": "Unrecoverable data loss",
"mutations": [...],
"attestation": {
"input": {"source": "shell", "input": "rm -rf /data/production"},
"output": {"schemaVersion": "...", "riskAssessment": "block", ...},
"evaluator": "recourse:blast-radius:1.0.0",
"timestamp": "2026-05-01T14:30:00.000Z",
"key_id": "recourse-prod-1",
"attestation_uri": "https://recourse.example/.well-known/attestations/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6.json",
"signature": "4-n_0Z2l4sRQjAvu8_tqe22sbFKuvrxHnYZjiAMuXtpj0cq_YoDjEHC5V4fn1O71zDL5mhAjsE6Fu-tPu2hjCw"
}
}
Step 2: Agent checks trusted instances
Config: trusted_instances: ["https://recourse.example"]
Check: attestation_uri host is recourse.example → matches → proceed.
Step 3: Agent fetches key registry
GET https://recourse.example/.well-known/recourse-keys.json → 200 OK
{
"instance_id": "recourse-prod",
"keys": [{
"key_id": "recourse-prod-1",
"algorithm": "Ed25519",
"public_key": "X8RJHBSyeubU49i9QMl1cfGfy3VjpTfyOLGKPjXagQ8",
"state": "active",
"valid_from": "2026-04-01T00:00:00Z"
}],
"registry_version": 3,
"updated_at": "2026-04-15T12:00:00Z"
}
Step 4: Agent locates key and checks state
Find: key_id "recourse-prod-1" → found.
Check: state is "active" → valid for verification.
Step 5: Agent constructs signed payload
Remove signature field. Canonicalize remainder per RFC 8785:
{"attestation_uri":"https://recourse.example/.well-known/attestations/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6.json","evaluator":"recourse:blast-radius:1.0.0","input":{"input":"rm -rf /data/production","source":"shell"},"key_id":"recourse-prod-1","output":{...},"timestamp":"2026-05-01T14:30:00.000Z"}
Step 6: Agent verifies Ed25519 signature
Decode public_key from base64url → 32 bytes.
Decode signature from base64url → 64 bytes.
Verify signature over canonical payload → valid.
Step 7: Agent acts on verdict
Attestation valid. riskAssessment is "block". Agent MUST NOT execute the command.
Agent logs: {"event": "evaluation_blocked", "attestation_uri": "...", "riskAssessment": "block", "verification": "passed"}
Verification Model
Four verification models exist. v1 targets opt-in inline by the agent.
| Model | Description | v1? |
|---|---|---|
| Inline (execution) | Target system refuses to execute without attestation | No |
| Async (audit) | Actions execute, attestations logged, audit later | Partial |
| Hybrid | Inline for high-stakes, async otherwise | Future |
| Opt-in by agent | Agent decides whether to verify before proceeding | Yes |
Why opt-in by agent for v1: RecourseOS doesn't control terraform, kubectl, or AWS CLI. It does influence agent behavior through MCP. The agent's decision loop is the boundary where RecourseOS has leverage.
Trust Anchor
v1: Self-signed, published keys
Each RecourseOS instance:
- Generates an Ed25519 keypair on first run
- Persists the keypair in a config directory
- Exposes the public key via API endpoint
- Signs all attestations with the private key
Trust model: Agent operators choose which RecourseOS instances to trust and fetch those public keys. Similar to SSH host key verification.
v2+ (future)
- Organizational CAs for enterprise deployments
- Transparency logs
- Federation between organizations
Non-Goals (v1)
The following are explicitly out of scope for v1:
| Non-Goal | Reason |
|---|---|
| Execution attestation | Requires cooperation from AWS/kubectl/etc. RecourseOS provides evaluation attestation. |
| Distributed witnesses | Threshold signatures add complexity. Reserve schema slot, defer implementation. |
| Credit systems | Agent economy thinking is orthogonal. Attestation-as-proof is cleaner than attestation-as-currency. |
| Centralized service | Conflicts with offline/local-first architecture. Self-signed keys are the right default. |
Threat Model
What the protocol protects against
- Agents that fabricate or hallucinate consequence reports
- Compromised agents that bypass evaluation entirely
- Audit trail tampering
- Repudiation by agent operators
What it does NOT protect against
- Compromised RecourseOS instances (attacker can sign anything)
- Compromised signing keys
- Coercion (operators can force specific verdicts)
- Out-of-band actions that skip evaluation
Honest framing: The protocol provides integrity of the evaluation chain, not correctness of the evaluation itself.
v1 Implementation Scope
Deliverables
- Specification document — complete schema, signing/verification procedures, versioning
- RecourseOS integration — attestation in ConsequenceReport, keypair generation, public key API,
recourse_verify_attestationMCP tool - Verification library — standalone TypeScript, no RecourseOS dependency
- Documentation — how agents request and verify attestations
- Partner integration — one MCP server or agent platform consuming attestations
Estimated effort
8-12 weeks of focused work for complete v1 with documentation and partner integration.
Why Attestation Matters
The shift from advisory to verifiable changes RecourseOS's security posture:
Advisory (today)
Agent: "Should I delete this bucket?"
RecourseOS: "Block: unrecoverable data loss"
Agent: *deletes bucket anyway*
Audit log: *nothing*
Verifiable (with attestation)
Agent: "Should I delete this bucket?"
RecourseOS: ConsequenceReport + Attestation(block)
Agent: *attempts to delete*
CI system: "Show attestation"
Agent: *presents block attestation*
CI system: "Attestation says block. Denied."
The attestation protocol doesn't change how evaluation works. It changes whether evaluation matters.
Related: Agent Interface · MCP Setup · Schema Gaps · View markdown source