feat: add draft data, gap analysis report, and workspace config
Some checks failed
CI / test (3.11) (push) Failing after 1m37s
CI / test (3.12) (push) Failing after 57s

This commit is contained in:
2026-04-06 18:47:15 +02:00
parent 4f310407b0
commit 2506b6325a
189 changed files with 62649 additions and 0 deletions

View File

@@ -0,0 +1,261 @@
# ACT (Agent Compact Token) — Medium Blog Post Series
## Series Title: "Securing the Agentic Web"
Target audience: Backend/platform engineers, AI practitioners, security-minded architects building multi-agent systems.
---
## Post 1: "Why OAuth Isn't Enough for AI Agents"
**Hook**: OAuth was built for humans clicking "Allow." Autonomous agents don't click buttons — they chain tasks, delegate to sub-agents, and need cryptographic proof of what they did, not just what they were allowed to do.
**Key points**:
- OAuth assumes a human in the loop (consent screens, redirect flows)
- Agents need machine-to-machine auth that works without an Authorization Server
- The missing piece: **accountability** — OAuth proves authorization, but not execution
- Agents operate across organizational boundaries; federated trust is essential
- Existing alternatives and their gaps:
- **SPIFFE/SPIRE**: Workload identity, but no capability scoping or execution records
- **ZCAP-LD**: Capability delegation, but JSON-LD complexity and no execution phase
- **Macaroons**: Contextual caveats, but no standardized structure for agent workflows
- **OPA/Cedar**: Policy engines, but no portable token format
**Diagram idea**: Side-by-side comparison — OAuth flow vs. ACT flow for an agent task
**Call to action**: Introduce ACT as "authorization + accountability in a single token"
---
## Post 2: "One Token, Two Lives — The ACT Lifecycle"
**Hook**: What if your authorization token could *grow up* — transforming from a permission slip into a receipt?
**Key points**:
- **Phase 1 — Authorization Mandate**: Signed by the issuing agent
- "Agent A authorizes Agent B to do X, with these constraints, until time T"
- JOSE header: `typ: "act+jwt"`, `alg: "EdDSA"`, `kid`
- Claims: `iss`, `sub`, `aud`, `exp`, `jti` (UUID v4), `task`, `cap`
- The `task` object: purpose, data sensitivity, created_by
- The `cap` array: fine-grained actions with constraints
- **Phase 2 — Execution Record**: Re-signed by the executing agent
- Same token, new claims added: `exec_act`, `par`, `exec_ts`, `status`
- Critical: header `kid` switches from issuer's key to executor's key
- Optional: `inp_hash` and `out_hash` (SHA-256 of actual I/O data)
- The `err` object for failure cases
**Code example**: Show a Phase 1 JWT payload, then the same payload with Phase 2 claims added
**Why it matters**: One token = complete audit trail. No separate logging system needed. The token *is* the evidence.
**Analogy**: A signed work order (Phase 1) that becomes a signed completion certificate (Phase 2)
---
## Post 3: "Zero to PKI — ACT's Trust Tier Progression"
**Hook**: Most security specs start with "first, set up your PKI infrastructure." ACT starts with "first, share a key over Signal."
**Key points**:
- **Tier 1 — Pre-Shared Keys** (mandatory to implement)
- Ed25519 key pairs shared out-of-band
- `kid` is just an opaque string both parties agree on
- Zero infrastructure. Works on day one. Perfect for prototyping and internal agents
- Key registry: simple dict mapping `kid → public_key_bytes`
- **Tier 2 — PKI / X.509**
- `kid` = SHA-256 thumbprint of DER-encoded certificate
- `x5c` JOSE header carries the certificate chain
- Standard X.509 chain validation against trusted CA store
- For enterprises with existing PKI
- **Tier 3 — Decentralized Identifiers (DIDs)**
- `did:key` — self-contained, no resolution needed (Ed25519 public key in the DID itself)
- `did:web` — HTTP-resolvable, cacheable with TTL
- For cross-organizational federation without shared CA
**Diagram idea**: Three-rung ladder — each tier adds infrastructure but also adds trust guarantees
**Why it matters**: You can start using ACT in 10 minutes with pre-shared keys, then upgrade to PKI or DIDs as your deployment matures. No rip-and-replace.
---
## Post 4: "Delegation Without Escalation — How ACT Prevents Rogue Sub-Agents"
**Hook**: When Agent A delegates to Agent B who delegates to Agent C... how do you ensure C can't do more than A originally allowed?
**Key points**:
- The `del` claim: `{ depth, max_depth, chain[] }`
- `chain` is ordered root → immediate parent
- Each entry: `{ delegator, jti, sig }`
- **Capability attenuation**: child's `cap` MUST be a subset of parent's `cap`
- Constraints can only get MORE restrictive, never less
- Example: parent allows `data.read` with `max_rows: 1000` → child can set `max_rows: 100` but not `max_rows: 5000`
- **Chain verification**:
- `sig = Sign(delegator.private_key, SHA-256(parent_act_compact_bytes))`
- Each chain entry verified against delegator's public key
- **Rejection conditions** (5 ways delegation fails):
1. `depth > max_depth`
2. `chain.length != depth`
3. Any chain signature fails
4. `cap` contains actions not in parent's `cap`
5. Any constraint is less restrictive than parent's
**Code example**: Three-agent delegation chain with progressively narrower capabilities
**Analogy**: Power of attorney — you can give someone authority to act on your behalf, and they can sub-delegate, but each level can only narrow the scope, never widen it
---
## Post 5: "Following the Thread — DAG-Based Execution Tracking"
**Hook**: In a world where agents spawn agents that spawn agents, how do you answer "what happened and in what order?"
**Key points**:
- Every Phase 2 ACT has a `par` (parents) array — JTIs of predecessor tasks
- `[]` for root tasks (no parents)
- Multiple parents = fan-in (joining parallel branches)
- The `jti` itself serves as the task ID
- **DAG validation rules**:
1. `jti` uniqueness within `wid` (workflow) scope
2. Every parent `jti` must exist as a verified Phase 2 ACT in the ledger
3. Temporal ordering: `parent.exec_ts < child.exec_ts + 30s` (clock skew tolerance)
4. Acyclicity: max 10,000-node traversal limit
5. `exec_act` must match one of the `cap[].action` values
- **Workflow grouping**: Optional `wid` (workflow ID) groups related ACTs
**Diagram idea**: A DAG of 6-8 tasks showing fan-out (parallel dispatch) and fan-in (result aggregation)
**Real-world example**: ML pipeline — data fetch → preprocess (fan-out to 3 shards) → train (fan-in) → evaluate → deploy
**Why it matters**: Compliance, debugging, incident response. "Show me every action taken in workflow X, in causal order, with cryptographic proof."
---
## Post 6: "Humans in the Loop — ACT's Oversight Mechanism"
**Hook**: Full autonomy is terrifying. Full control is impractical. ACT's oversight mechanism is the middle ground.
**Key points**:
- The `oversight` claim:
- `requires_approval_for`: array of action strings that need human sign-off
- `approval_ref`: reference to the approval record
- How it works in practice:
- Agent receives mandate with `oversight.requires_approval_for: ["data.delete"]`
- Before executing `data.delete`, agent must obtain approval
- Approval reference stored in `oversight.approval_ref`
- Selective oversight: Only specific high-risk actions require approval, not everything
- Composable with delegation: oversight requirements propagate down the chain
**Why it matters**: Regulatory compliance (GDPR right-to-delete, financial transactions), enterprise risk management, building trust in agent systems incrementally
---
## Post 7: "Threat Model Deep Dive — What ACT Defends Against"
**Hook**: Every security spec claims to be secure. Here's exactly what ACT protects against — and what it doesn't.
**Key points**:
- **Defended threats**:
- Token forgery (Ed25519 signatures, no symmetric algs, no "alg: none")
- Privilege escalation (capability attenuation in delegation chains)
- Replay attacks (jti uniqueness, exp enforcement, clock skew tolerance ≤300s)
- Execution fabrication (Phase 2 re-signature by sub's key, not iss's key)
- Audit trail tampering (hash-chained append-only ledger)
- Man-in-the-middle on delegation (chain signatures bind to parent token bytes)
- **Security constraints in implementation**:
- Algorithm allowlist: EdDSA (Ed25519), ES256 only. No HS*, no "none"
- Key material zeroed on deletion
- iat must not be unreasonably future (≤30s)
- aud verification mandatory
- **Out of scope** (honest about limitations):
- Token revocation (not in v00 — mentioned as future work)
- Confidentiality of token contents (JWS, not JWE)
- Compromised agent keys (standard key management applies)
**Table**: Threat → ACT mitigation → Residual risk
---
## Post 8: "ACT Meets the IETF — Why Standardization Matters for Agent Interop"
**Hook**: Your agents and my agents need to talk. Without a standard, we're back to building custom integrations for every partner.
**Key points**:
- The IETF process: Internet-Draft → RFC pathway
- `draft-nennemann-act-00` is the first submission
- Building on existing standards: RFC 7519 (JWT), RFC 7515 (JWS), RFC 7518 (JWA)
- ABNF notation for action names, formal CDDL-style structures
- Why an IETF RFC vs. a blog post or GitHub repo:
- Interoperability testing with multiple implementations
- Formal security review
- Stable reference for contracts and regulations
- Media type registration: `application/act+jwt`
- **Interoperability requirements**:
- MUST implement Tier 1 (pre-shared keys)
- MUST support EdDSA (Ed25519)
- MUST verify delegation chains
- MUST enforce DAG validation
- Comparison with other agent-related standards efforts
**Why it matters**: As AI agents become infrastructure, we need the same rigor we applied to HTTP, TLS, and OAuth
---
## Post 9: "Building Your First ACT Agent — A Hands-On Tutorial"
**Hook**: Enough theory. Let's build two agents that authorize, execute, and audit a task using ACT.
**Key points** (step-by-step tutorial):
1. Install the `act` Python package
2. Generate Ed25519 key pairs for two agents
3. Create a Phase 1 mandate (Agent A → Agent B: "read customer data")
4. Agent B validates the mandate
5. Agent B executes and creates Phase 2 record (with inp_hash/out_hash)
6. Append to audit ledger
7. Verify the complete chain
8. Add delegation: Agent B delegates subset to Agent C
9. Verify Agent C's execution in the DAG
**Code examples**: Complete working Python code for each step, using the reference implementation
**Real-world scenario**: E-commerce — order service authorizes inventory agent to check stock, inventory agent delegates to warehouse-specific sub-agents
---
## Post 10: "The Road Ahead — ACT's Evolution and the Agentic Future"
**Hook**: ACT v00 is a foundation. Here's what's coming and how you can shape it.
**Key points**:
- **Future work mentioned in the draft**:
- Token revocation mechanisms
- Formal capability algebra (lattice-based constraint reasoning)
- Performance benchmarks across implementations
- Integration patterns with existing auth infrastructure
- Privacy-preserving execution records (selective disclosure)
- **Community and contribution**:
- Reference implementation as the interoperability baseline
- Test vectors (Appendix B) as the conformance suite
- How to write a second implementation (in Rust, Go, etc.)
- **The bigger picture**:
- Agent-to-agent economy needs trust primitives
- ACT as one layer in the agentic stack
- Composability with other standards (OpenID for agents, SCIM, etc.)
---
## Cross-Series Themes
1. **Progressive complexity**: Each post builds on the previous, but each can stand alone
2. **Code-first**: Every concept illustrated with real Python snippets from the reference implementation
3. **Honest trade-offs**: Acknowledge what ACT doesn't solve (yet)
4. **Standards matter**: Thread the IETF standardization story throughout
5. **Real-world grounding**: Each post connects to concrete use cases
## Publication Strategy
- **Cadence**: 1 post per week, 10-week series
- **Length**: 1,500-2,500 words per post (8-12 min read)
- **Tags**: AI, Security, Authentication, Distributed Systems, IETF
- **Series landing page**: Link all posts, provide a "start here" guide
- **Code repo**: Link to the reference implementation throughout
- **Post 9** can be published on dev.to / Hashnode in addition to Medium for developer reach

View File

@@ -0,0 +1,312 @@
# Master Prompt: ACT Reference Implementation
## Context
You are implementing the reference implementation for the Agent Compact
Token (ACT), as defined in `draft-nennemann-act-00`. The full draft is
attached or provided in context. Your implementation must be a clean,
well-documented Python package that serves as the normative reference
for the specification — meaning it is the ground truth for
interoperability testing.
The ACS (Agent Compliance Seal) reference implementation (~1,900 lines,
Python) exists as a prior art reference for code style and structure.
ACT follows the same philosophy: minimal dependencies, sub-millisecond
performance for the hot path, Ed25519 as the primary algorithm.
---
## Deliverables
Produce the following files in a single `act/` package directory:
```
act/
├── __init__.py # Public API exports
├── token.py # ACTMandate and ACTRecord dataclasses + serialization
├── crypto.py # Key management: Tier 1 (pre-shared), Tier 2 (PKI),
│ # Tier 3 (DID:key, DID:web); sign/verify primitives
├── lifecycle.py # Phase 1 → Phase 2 transition logic (re-signing)
├── delegation.py # Delegation chain construction and verification
├── dag.py # DAG validation (uniqueness, parent existence,
│ # temporal ordering, acyclicity, capability
│ # consistency)
├── ledger.py # In-memory append-only audit ledger (for testing;
│ # interface suitable for external backends)
├── verify.py # Unified verification entry point (Phase 1 + Phase 2)
├── errors.py # All ACT-specific exception types
└── vectors.py # Generates and validates all Appendix B test vectors
tests/
├── test_token.py
├── test_crypto.py
├── test_lifecycle.py
├── test_delegation.py
├── test_dag.py
├── test_ledger.py
├── test_verify.py
└── test_vectors.py # Must pass all vectors defined in vectors.py
```
---
## Specification Summary
### Token Structure
An ACT is a JWT (JWS Compact Serialization). It has two phases:
**Phase 1 — Authorization Mandate** (signed by issuing agent):
JOSE Header:
```json
{
"alg": "EdDSA", // Ed25519; also support ES256
"typ": "act+jwt",
"kid": "<key-id>"
// optional: "x5c" (Tier 2), "did" (Tier 3)
}
```
Required JWT claims:
- `iss` — issuer agent identifier (opaque string / X.509 DN / DID)
- `sub` — target agent identifier (same format as iss)
- `aud` — intended recipient(s); string or array
- `iat` — issuance time (NumericDate)
- `exp` — expiration time (NumericDate); SHOULD be iat + ≤900s for automated flows
- `jti` — UUID v4; doubles as task identifier for DAG par references
Optional:
- `wid` — workflow UUID grouping related ACTs
Required ACT-specific claims:
- `task` — object: { purpose (str, REQUIRED), data_sensitivity (str, OPTIONAL),
created_by (str, OPTIONAL), expires_at (NumericDate, OPTIONAL) }
- `cap` — array of { action (str, REQUIRED), constraints (object, OPTIONAL) }
action names conform to ABNF: component *("." component)
component = ALPHA *(ALPHA / DIGIT / "-" / "_")
- `del` — object: { depth (int), max_depth (int), chain (array) }
chain entries: { delegator (str), jti (str), sig (base64url str) }
chain is ordered root → immediate parent (chain[0] = root authority)
If `del` is absent: treat as root mandate, depth=0, delegation forbidden
Optional:
- `oversight` — { requires_approval_for (array of action strings),
approval_ref (str, OPTIONAL) }
**Phase 2 — Execution Record** (re-signed by executing agent, i.e. `sub`):
All Phase 1 claims are preserved unchanged. Additional required claims:
- `exec_act` — string; MUST match one of the cap[].action values
- `par` — array of jti strings (parent task IDs in DAG); [] for root tasks
- `exec_ts` — NumericDate; actual execution time; MUST be >= iat; SHOULD be <= exp
(if exec_ts > exp: log warning, do NOT reject)
- `status` — one of: "completed", "failed", "partial"
Additional optional claims:
- `inp_hash` — base64url(SHA-256(raw input bytes)), no padding
- `out_hash` — base64url(SHA-256(raw output bytes)), no padding
- `err` — { code (str), detail (str) }; present when status != "completed"
**Critical**: In Phase 2, the JOSE header `kid` MUST reference the `sub`
agent's key (not the `iss` agent's key). The re-signature is produced by
the executing agent over the complete Phase 2 payload (all Phase 1 claims
+ execution claims combined).
---
### Trust Tiers
**Tier 1 — Pre-Shared Keys (mandatory-to-implement)**
- Keys: Ed25519 (primary) or P-256
- `kid`: opaque string agreed out-of-band
- Key registry: a dict mapping kid → public key bytes, configured at init time
- No external resolution needed
**Tier 2 — PKI / X.509**
- `kid`: SHA-256 thumbprint of DER-encoded certificate
- `x5c` JOSE header MAY carry the certificate chain
- Verification: standard X.509 chain validation against trusted CA store
**Tier 3 — DID**
- Support `did:key` (self-contained, no resolution needed)
- Support `did:web` (requires HTTP resolution; cache with configurable TTL)
- `kid`: DID key fragment (e.g. `did:key:z6Mk...#key-1`)
- `did` JOSE header MAY carry the full DID for resolution
---
### Delegation Chain
When issuing a delegated ACT (Agent A → Agent B):
1. `del.depth` = parent ACT's `del.depth` + 1
2. `del.max_depth` ≤ parent ACT's `del.max_depth`
3. `cap` must be a subset of parent ACT's `cap` with constraints at least
as restrictive
4. Each chain entry `sig` = Sign(A.private_key, SHA-256(parent_act_compact_bytes))
where `parent_act_compact_bytes` is the raw bytes of the parent ACT's
JWS Compact Serialization (UTF-8 encoded)
Verification of chain entry:
- Retrieve public key for entry.delegator
- Recompute SHA-256(parent_act_compact_bytes)
- Verify entry.sig against that hash using entry.delegator's public key
Rejection conditions:
- `del.depth` > `del.max_depth`
- `del.chain` length != `del.depth`
- Any chain entry sig fails verification
- `cap` contains actions not in parent ACT's `cap`
- Any constraint in `cap` is less restrictive than in parent ACT
---
### DAG Validation (Phase 2)
The ACT ledger (or set of received parent ACTs) is the ECT store.
Required checks on receiving a Phase 2 ACT:
1. `jti` uniqueness within `wid` scope (or globally if `wid` absent)
2. Every `jti` in `par` exists in the ledger/store as a verified Phase 2 ACT
3. For each parent: `parent.exec_ts < child.exec_ts + 30s` (clock skew tolerance)
4. No cycle: following `par` references must not return to current `jti`
— enforce max traversal limit of 10,000 nodes
5. `exec_act` matches one of the `cap[].action` values in the Phase 1 claims
---
### Verification Procedure
**Phase 1 verification** (ACTVerifier.verify_mandate):
1. Parse JWS Compact Serialization
2. Check `typ` == "act+jwt"
3. Check `alg` in allowlist (must include EdDSA/Ed25519, ES256; MUST NOT include
"none" or any HS* algorithm)
4. Resolve public key for `kid` per trust tier
5. Verify JWS signature
6. Check `exp` not passed (clock skew tolerance: ≤300s)
7. Check `iat` not unreasonably future (≤30s ahead)
8. Check `aud` contains verifier's own identifier
9. Check `iss` is trusted per local policy
10. Check `sub` matches verifier's own identifier (when verifier is the target)
11. Check all required claims present and well-formed
12. If `del.chain` non-empty: verify delegation chain
**Phase 2 verification** (ACTVerifier.verify_record):
All Phase 1 steps, plus:
13. Check `exec_act` present and matches a `cap[].action`
14. Check `par` present; perform DAG validation
15. Check `exec_ts` present and >= `iat`; if > `exp` log warning but do NOT reject
16. Check `status` present and valid
17. Check re-signature was produced by `sub` agent's key (kid in Phase 2
header must correspond to sub's public key, not iss's key)
18. Optionally verify `inp_hash`/`out_hash` against provided data
---
### Audit Ledger Interface
`ACTLedger` (in-memory reference implementation):
- `append(act_record: ACTRecord) -> int` — returns sequence number
- `get(jti: str) -> ACTRecord | None`
- `list(wid: str | None) -> list[ACTRecord]`
- `verify_integrity() -> bool` — verifies no records have been tampered with
(hash-chain over sequence-ordered records)
The ledger must enforce append-only semantics: once appended, a record
cannot be modified or deleted. Raise `ACTLedgerImmutabilityError` on
any attempt.
---
### Error Types
Define in `errors.py`:
```python
ACTError # base
ACTValidationError # malformed token structure
ACTSignatureError # signature verification failed
ACTExpiredError # token expired
ACTAudienceMismatchError # aud does not contain verifier identity
ACTCapabilityError # no matching capability / capability escalation
ACTDelegationError # delegation chain invalid
ACTDAGError # DAG validation failed (cycle, missing parent, etc.)
ACTPhaseError # wrong phase for operation (e.g. mandate used as record)
ACTKeyResolutionError # cannot resolve kid to public key
ACTLedgerImmutabilityError # attempt to modify ledger
ACTPrivilegeEscalationError # delegated cap exceeds parent cap
```
---
### Test Vectors (Appendix B)
`vectors.py` must generate and validate all of the following. Each vector
must include: description, input parameters, expected output (encoded token
or expected exception class).
**Valid vectors:**
- B.1: Phase 1 ACT — root mandate, Tier 1 (Ed25519 pre-shared key), no delegation
- B.2: Phase 2 ACT — completed execution, transition from B.1 mandate
- B.3: Phase 2 ACT — fan-in, two parent jti values from parallel branches
- B.4: Phase 1 ACT — delegated mandate (depth=1), chain entry with sig
- B.5: Phase 2 ACT — delegated execution record
**Invalid vectors (must raise specified exception):**
- B.6: `del.depth` > `del.max_depth` → ACTDelegationError
- B.7: `cap` escalation in delegated ACT → ACTPrivilegeEscalationError
- B.8: `exec_act` not in `cap` → ACTCapabilityError
- B.9: DAG cycle (par references own jti) → ACTDAGError
- B.10: Missing parent jti in DAG → ACTDAGError
- B.11: Tampered payload (bit flip in claims) → ACTSignatureError
- B.12: Expired token → ACTExpiredError
- B.13: Wrong audience → ACTAudienceMismatchError
- B.14: Phase 2 re-signed by iss key instead of sub → ACTSignatureError
- B.15: Algorithm "none" → ACTValidationError
---
## Implementation Constraints
**Dependencies**: use only the Python standard library plus:
- `cryptography` (for Ed25519, P-256, X.509)
- `pyjwt` OR manual JWS implementation (prefer manual for spec fidelity)
- `pytest` (test runner only)
**Performance target**: Phase 1 creation ≤ 500µs mean on modern hardware.
Benchmark in a `bench/` directory.
**Code style**:
- Type-annotated throughout (Python 3.11+)
- Dataclasses for token structures
- No global mutable state
- All public API functions documented with docstrings referencing the
relevant draft section (e.g. `# ACT §8.1`)
**Security constraints**:
- MUST NOT use symmetric algorithms (HS256 etc.) anywhere
- MUST NOT implement "alg: none" path
- Ed25519 signing MUST use bound key-pair APIs (private key object that
carries the public key) — never pass raw private key bytes
- All secret key material must be zeroed on deletion where the
cryptography library supports it
**What NOT to implement**:
- DID:web resolution with live HTTP calls in the reference implementation
(stub it with a configurable resolver callback instead)
- Token revocation infrastructure
- Persistence (ledger is in-memory only)
---
## Output Format
Produce each file completely, in order. After all files, produce a
`README.md` for the `act/` package that includes:
- Installation instructions
- Quick-start example (Phase 1 mandate → Phase 2 record → verify)
- Running the test suite
- Running the test vectors
- Performance benchmark instructions
At the end, confirm: "All Appendix B test vectors pass."

View File

@@ -0,0 +1,119 @@
"""Agent Compact Token (ACT) — Reference Implementation.
A JWT-based format for autonomous AI agents that unifies authorization
and execution accountability in a single token lifecycle.
Reference: draft-nennemann-act-00.
"""
from .errors import (
ACTAudienceMismatchError,
ACTCapabilityError,
ACTDAGError,
ACTDelegationError,
ACTError,
ACTExpiredError,
ACTKeyResolutionError,
ACTLedgerImmutabilityError,
ACTPhaseError,
ACTPrivilegeEscalationError,
ACTSignatureError,
ACTValidationError,
)
from .token import (
ACTMandate,
ACTRecord,
Capability,
Delegation,
DelegationEntry,
ErrorClaim,
Oversight,
TaskClaim,
decode_jws,
encode_jws,
parse_token,
)
from .crypto import (
ACTKeyResolver,
KeyRegistry,
PublicKey,
PrivateKey,
X509TrustStore,
b64url_sha256,
compute_sha256,
did_key_from_ed25519,
generate_ed25519_keypair,
generate_p256_keypair,
resolve_did_key,
sign,
verify,
)
from .lifecycle import transition_to_record
from .delegation import (
create_delegated_mandate,
verify_capability_subset,
verify_delegation_chain,
)
from .dag import validate_dag, ACTStore
from .ledger import ACTLedger
from .verify import ACTVerifier
from .vectors import generate_vectors, validate_vectors
__all__ = [
# Errors
"ACTError",
"ACTValidationError",
"ACTSignatureError",
"ACTExpiredError",
"ACTAudienceMismatchError",
"ACTCapabilityError",
"ACTDelegationError",
"ACTDAGError",
"ACTPhaseError",
"ACTKeyResolutionError",
"ACTLedgerImmutabilityError",
"ACTPrivilegeEscalationError",
# Token structures
"ACTMandate",
"ACTRecord",
"TaskClaim",
"Capability",
"Delegation",
"DelegationEntry",
"Oversight",
"ErrorClaim",
# Token serialization
"encode_jws",
"decode_jws",
"parse_token",
# Crypto
"generate_ed25519_keypair",
"generate_p256_keypair",
"sign",
"verify",
"compute_sha256",
"b64url_sha256",
"resolve_did_key",
"did_key_from_ed25519",
"KeyRegistry",
"X509TrustStore",
"ACTKeyResolver",
"PublicKey",
"PrivateKey",
# Lifecycle
"transition_to_record",
# Delegation
"create_delegated_mandate",
"verify_capability_subset",
"verify_delegation_chain",
# DAG
"validate_dag",
"ACTStore",
# Ledger
"ACTLedger",
# Verify
"ACTVerifier",
# Vectors
"generate_vectors",
"validate_vectors",
]

467
workspace/act/act/crypto.py Normal file
View File

@@ -0,0 +1,467 @@
"""ACT cryptographic primitives and key management.
Provides sign/verify operations and key resolution across all three
ACT trust tiers:
- Tier 1: Pre-shared Ed25519 and P-256 keys
- Tier 2: PKI / X.509 certificate chains
- Tier 3: DID (did:key self-contained, did:web via resolver callback)
Reference: ACT §5 (Trust Model), §8 (Verification Procedure).
"""
from __future__ import annotations
import base64
import hashlib
import re
from typing import Any, Callable, Protocol
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ec import (
ECDSA,
SECP256R1,
EllipticCurvePrivateKey,
EllipticCurvePublicKey,
generate_private_key as ec_generate_private_key,
)
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PrivateKey,
Ed25519PublicKey,
)
from cryptography.hazmat.primitives.hashes import SHA256
from cryptography.x509 import (
Certificate,
load_der_x509_certificate,
)
from .errors import (
ACTKeyResolutionError,
ACTSignatureError,
ACTValidationError,
)
# Type aliases for public/private keys supported by ACT.
PublicKey = Ed25519PublicKey | EllipticCurvePublicKey
PrivateKey = Ed25519PrivateKey | EllipticCurvePrivateKey
# Callback type for DID:web resolution.
DIDResolver = Callable[[str], PublicKey | None]
def generate_ed25519_keypair() -> tuple[Ed25519PrivateKey, Ed25519PublicKey]:
"""Generate an Ed25519 key pair for ACT signing.
Returns a (private_key, public_key) tuple. The private key object
carries its associated public key per ACT security requirements.
Reference: ACT §5.2 (Tier 1 pre-shared keys).
"""
private_key = Ed25519PrivateKey.generate()
return private_key, private_key.public_key()
def generate_p256_keypair() -> tuple[EllipticCurvePrivateKey, EllipticCurvePublicKey]:
"""Generate a P-256 (ES256) key pair for ACT signing.
Returns a (private_key, public_key) tuple.
Reference: ACT §5.2 (Tier 1 pre-shared keys).
"""
private_key = ec_generate_private_key(SECP256R1())
return private_key, private_key.public_key()
def sign(private_key: PrivateKey, data: bytes) -> bytes:
"""Sign data using the appropriate algorithm for the key type.
Uses Ed25519 for Ed25519PrivateKey, ECDSA with SHA-256 for P-256.
Returns raw signature bytes (for Ed25519: 64 bytes; for ES256:
raw r||s format per RFC 7518 §3.4).
Reference: ACT §5, RFC 7515 §5.1.
Raises:
ACTValidationError: If the key type is not supported.
"""
if isinstance(private_key, Ed25519PrivateKey):
return private_key.sign(data)
elif isinstance(private_key, EllipticCurvePrivateKey):
from cryptography.hazmat.primitives.asymmetric.utils import (
decode_dss_signature,
)
# Sign with DER-encoded signature, then convert to raw r||s
der_sig = private_key.sign(data, ECDSA(SHA256()))
r, s = decode_dss_signature(der_sig)
# P-256 uses 32-byte integers
return r.to_bytes(32, "big") + s.to_bytes(32, "big")
else:
raise ACTValidationError(f"Unsupported key type: {type(private_key)}")
def verify(public_key: PublicKey, signature: bytes, data: bytes) -> None:
"""Verify a signature against the given public key and data.
Reference: ACT §8.1 step 5.
Raises:
ACTSignatureError: If the signature is invalid.
ACTValidationError: If the key type is not supported.
"""
try:
if isinstance(public_key, Ed25519PublicKey):
public_key.verify(signature, data)
elif isinstance(public_key, EllipticCurvePublicKey):
from cryptography.hazmat.primitives.asymmetric.utils import (
encode_dss_signature,
)
# Convert raw r||s back to DER
r = int.from_bytes(signature[:32], "big")
s = int.from_bytes(signature[32:], "big")
der_sig = encode_dss_signature(r, s)
public_key.verify(der_sig, data, ECDSA(SHA256()))
else:
raise ACTValidationError(
f"Unsupported key type: {type(public_key)}"
)
except InvalidSignature as e:
raise ACTSignatureError("Signature verification failed") from e
def compute_sha256(data: bytes) -> bytes:
"""Compute SHA-256 hash of data.
Used for delegation chain signatures and inp_hash/out_hash claims.
Reference: ACT §6.1 (delegation sig), §4.3 (inp_hash, out_hash).
"""
return hashlib.sha256(data).digest()
def b64url_sha256(data: bytes) -> str:
"""Compute base64url(SHA-256(data)) without padding.
Used for inp_hash and out_hash claims.
Reference: ACT §4.3.
"""
digest = compute_sha256(data)
return base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
def x509_kid(cert_der: bytes) -> str:
"""Compute the Tier 2 kid: SHA-256 thumbprint of DER certificate.
Reference: ACT §5.3 (Tier 2 kid format).
"""
return hashlib.sha256(cert_der).hexdigest()
class KeyRegistry:
"""Tier 1 pre-shared key registry.
Maps kid strings to public keys. Configured at initialization time
with no external resolution needed.
Reference: ACT §5.2 (Tier 1 Pre-Shared Keys).
"""
def __init__(self) -> None:
self._keys: dict[str, PublicKey] = {}
def register(self, kid: str, public_key: PublicKey) -> None:
"""Register a public key under the given kid.
Reference: ACT §5.2.
"""
self._keys[kid] = public_key
def get(self, kid: str) -> PublicKey | None:
"""Retrieve the public key for a kid, or None if not found."""
return self._keys.get(kid)
def __contains__(self, kid: str) -> bool:
return kid in self._keys
def __len__(self) -> int:
return len(self._keys)
class X509TrustStore:
"""Tier 2 PKI/X.509 trust store.
Holds trusted CA certificates and resolves kid (certificate
thumbprint) to public keys. Supports x5c header chain validation.
Reference: ACT §5.3 (Tier 2 PKI).
"""
def __init__(self) -> None:
self._trusted_certs: dict[str, Certificate] = {}
def add_trusted_cert(self, cert: Certificate) -> str:
"""Add a trusted certificate to the store.
Returns the kid (SHA-256 thumbprint of DER encoding).
Reference: ACT §5.3.
"""
from cryptography.hazmat.primitives.serialization import Encoding
der_bytes = cert.public_bytes(Encoding.DER)
kid = x509_kid(der_bytes)
self._trusted_certs[kid] = cert
return kid
def resolve(self, kid: str) -> PublicKey | None:
"""Resolve kid to a public key from a trusted certificate.
Reference: ACT §5.3, §8.1 step 4.
"""
cert = self._trusted_certs.get(kid)
if cert is None:
return None
pub = cert.public_key()
if isinstance(pub, (Ed25519PublicKey, EllipticCurvePublicKey)):
return pub
return None
def resolve_x5c(self, x5c: list[str]) -> PublicKey | None:
"""Resolve public key from x5c certificate chain.
The first entry in x5c is the end-entity certificate.
Validates that the chain terminates in a trusted CA.
Reference: ACT §4.1 (x5c header), §5.3.
"""
if not x5c:
return None
try:
# Decode certificates from base64 DER
certs = [
load_der_x509_certificate(base64.b64decode(c)) for c in x5c
]
except Exception:
return None
# Check if any cert in the chain is in our trust store
from cryptography.hazmat.primitives.serialization import Encoding
for cert in certs:
der_bytes = cert.public_bytes(Encoding.DER)
kid = x509_kid(der_bytes)
if kid in self._trusted_certs:
# End-entity cert is the first one
ee_pub = certs[0].public_key()
if isinstance(ee_pub, (Ed25519PublicKey, EllipticCurvePublicKey)):
return ee_pub
return None
return None
# --- Tier 3: DID Support ---
# Multicodec prefixes for did:key
_ED25519_MULTICODEC = b"\xed\x01"
_P256_MULTICODEC = b"\x80\x24"
def _multibase_decode(encoded: str) -> bytes:
"""Decode a multibase-encoded string (base58btc 'z' prefix).
Reference: ACT §5.4 (Tier 3 DID:key).
"""
if not encoded.startswith("z"):
raise ACTKeyResolutionError(
f"Unsupported multibase encoding prefix: {encoded[0]!r}"
)
return _base58btc_decode(encoded[1:])
def _base58btc_decode(s: str) -> bytes:
"""Decode a base58btc string."""
alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
n = 0
for ch in s:
idx = alphabet.index(ch)
n = n * 58 + idx
# Compute byte length
byte_length = (n.bit_length() + 7) // 8
result = n.to_bytes(byte_length, "big") if byte_length > 0 else b""
# Preserve leading zeros
leading_zeros = len(s) - len(s.lstrip("1"))
return b"\x00" * leading_zeros + result
def resolve_did_key(did: str) -> PublicKey:
"""Resolve a did:key identifier to a public key.
Supports Ed25519 and P-256 key types. The did:key method is
self-contained — no external resolution is needed.
Reference: ACT §5.4 (Tier 3 DID:key).
Raises:
ACTKeyResolutionError: If the DID cannot be resolved.
"""
# Strip fragment if present (e.g., did:key:z6Mk...#z6Mk...)
did_base = did.split("#")[0]
if not did_base.startswith("did:key:"):
raise ACTKeyResolutionError(
f"Not a did:key identifier: {did!r}"
)
multibase_value = did_base[len("did:key:"):]
try:
decoded = _multibase_decode(multibase_value)
except Exception as e:
raise ACTKeyResolutionError(
f"Failed to decode did:key multibase value: {e}"
) from e
if decoded[:2] == _ED25519_MULTICODEC:
raw_key = decoded[2:]
if len(raw_key) != 32:
raise ACTKeyResolutionError(
f"Ed25519 public key must be 32 bytes, got {len(raw_key)}"
)
return Ed25519PublicKey.from_public_bytes(raw_key)
elif decoded[:2] == _P256_MULTICODEC:
raw_key = decoded[2:]
from cryptography.hazmat.primitives.asymmetric.ec import (
EllipticCurvePublicKey as ECPub,
)
from cryptography.hazmat.primitives.serialization import (
load_der_public_key,
)
# P-256 compressed point (33 bytes) or uncompressed (65 bytes)
# Wrap in SubjectPublicKeyInfo for loading
try:
return EllipticCurvePublicKey.from_encoded_point(
SECP256R1(), raw_key
)
except Exception as e:
raise ACTKeyResolutionError(
f"Failed to load P-256 key from did:key: {e}"
) from e
else:
raise ACTKeyResolutionError(
f"Unsupported multicodec prefix in did:key: {decoded[:2]!r}"
)
def did_key_from_ed25519(public_key: Ed25519PublicKey) -> str:
"""Create a did:key identifier from an Ed25519 public key.
Reference: ACT §5.4 (Tier 3 DID:key).
"""
from cryptography.hazmat.primitives.serialization import (
Encoding,
PublicFormat,
)
raw = public_key.public_bytes(Encoding.Raw, PublicFormat.Raw)
multicodec = _ED25519_MULTICODEC + raw
encoded = "z" + _base58btc_encode(multicodec)
return f"did:key:{encoded}"
def _base58btc_encode(data: bytes) -> str:
"""Encode bytes as base58btc."""
alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
# Count leading zeros
leading_zeros = 0
for b in data:
if b == 0:
leading_zeros += 1
else:
break
n = int.from_bytes(data, "big")
if n == 0:
return "1" * leading_zeros
chars: list[str] = []
while n > 0:
n, remainder = divmod(n, 58)
chars.append(alphabet[remainder])
return "1" * leading_zeros + "".join(reversed(chars))
class ACTKeyResolver:
"""Unified key resolver across all trust tiers.
Tries Tier 1 (pre-shared), then Tier 2 (X.509), then Tier 3 (DID)
to resolve a kid to a public key.
Reference: ACT §5 (Trust Model), §8.1 step 4.
"""
def __init__(
self,
registry: KeyRegistry | None = None,
x509_store: X509TrustStore | None = None,
did_web_resolver: DIDResolver | None = None,
) -> None:
self._registry = registry or KeyRegistry()
self._x509_store = x509_store or X509TrustStore()
self._did_web_resolver = did_web_resolver
@property
def registry(self) -> KeyRegistry:
"""Access the Tier 1 key registry."""
return self._registry
@property
def x509_store(self) -> X509TrustStore:
"""Access the Tier 2 X.509 trust store."""
return self._x509_store
def resolve(
self,
kid: str,
header: dict[str, Any] | None = None,
) -> PublicKey:
"""Resolve a kid to a public key, trying all configured tiers.
Resolution order:
1. Tier 1: Pre-shared key registry lookup by kid
2. Tier 2: X.509 certificate lookup by kid (thumbprint)
or x5c header chain validation
3. Tier 3: DID resolution (did:key or did:web)
Reference: ACT §5 (Trust Model), §8.1 step 4.
Raises:
ACTKeyResolutionError: If no key can be resolved for the kid.
"""
header = header or {}
# Tier 1: Pre-shared keys
key = self._registry.get(kid)
if key is not None:
return key
# Tier 2: X.509
key = self._x509_store.resolve(kid)
if key is not None:
return key
# Tier 2: x5c chain in header
x5c = header.get("x5c")
if x5c:
key = self._x509_store.resolve_x5c(x5c)
if key is not None:
return key
# Tier 3: DID
did_value = header.get("did") or kid
if did_value.startswith("did:key:"):
try:
return resolve_did_key(did_value)
except ACTKeyResolutionError:
pass
if did_value.startswith("did:web:") and self._did_web_resolver:
resolved = self._did_web_resolver(did_value)
if resolved is not None:
return resolved
raise ACTKeyResolutionError(
f"Cannot resolve kid {kid!r} to a public key via any trust tier"
)

136
workspace/act/act/dag.py Normal file
View File

@@ -0,0 +1,136 @@
"""ACT DAG validation for Phase 2 execution records.
Validates the directed acyclic graph formed by par (parent) references
in Phase 2 ACTs, ensuring uniqueness, parent existence, temporal ordering,
acyclicity, and capability consistency.
Reference: ACT §7 (DAG Structure and Causal Ordering).
"""
from __future__ import annotations
from typing import Protocol
from .errors import ACTCapabilityError, ACTDAGError
from .token import ACTRecord
# Maximum ancestor traversal limit for cycle detection — ACT §7.1 step 4.
MAX_TRAVERSAL_LIMIT: int = 10_000
# Clock skew tolerance for temporal ordering — ACT §7.1 step 3.
DAG_CLOCK_SKEW_TOLERANCE: int = 30
class ACTStore(Protocol):
"""Protocol for an ACT store used in DAG validation.
Any object implementing get() and has() can serve as the store.
The ACTLedger in ledger.py implements this protocol.
"""
def get(self, jti: str) -> ACTRecord | None:
"""Retrieve a Phase 2 ACT record by jti."""
...
def validate_dag(
record: ACTRecord,
store: ACTStore,
*,
clock_skew_tolerance: int = DAG_CLOCK_SKEW_TOLERANCE,
) -> None:
"""Validate the DAG constraints for a Phase 2 execution record.
Performs all five DAG validation checks defined in ACT §7.1:
1. jti uniqueness within wid scope (or globally)
2. Parent existence in store
3. Temporal ordering with clock skew tolerance
4. Acyclicity (max traversal limit)
5. Capability consistency (exec_act matches cap[].action)
Reference: ACT §7.1 (DAG Validation).
Args:
record: The Phase 2 ACTRecord to validate.
store: An ACT store providing get() for parent lookup.
clock_skew_tolerance: Seconds of allowed clock skew (default 30).
Raises:
ACTDAGError: If any DAG constraint is violated.
ACTCapabilityError: If exec_act does not match cap actions.
"""
# Step 1: jti uniqueness — ACT §7.1 step 1
existing = store.get(record.jti)
if existing is not None:
raise ACTDAGError(
f"Duplicate jti {record.jti!r} already exists in store"
)
# Step 5: Capability consistency — ACT §7.1 step 5
cap_actions = {c.action for c in record.cap}
if record.exec_act not in cap_actions:
raise ACTCapabilityError(
f"exec_act {record.exec_act!r} does not match any "
f"cap[].action: {sorted(cap_actions)}"
)
# Step 2 & 3: Parent existence and temporal ordering
for parent_jti in record.par:
parent = store.get(parent_jti)
if parent is None:
raise ACTDAGError(
f"Parent jti {parent_jti!r} not found in store"
)
# Temporal ordering: parent.exec_ts < child.exec_ts + tolerance
if parent.exec_ts >= record.exec_ts + clock_skew_tolerance:
raise ACTDAGError(
f"Temporal ordering violation: parent {parent_jti!r} "
f"exec_ts={parent.exec_ts} >= child exec_ts="
f"{record.exec_ts} + tolerance={clock_skew_tolerance}"
)
# Step 4: Acyclicity — ACT §7.1 step 4
_check_acyclicity(record.jti, record.par, store)
def _check_acyclicity(
current_jti: str,
parent_jtis: list[str],
store: ACTStore,
) -> None:
"""Check that following par references does not lead back to current_jti.
Uses breadth-first traversal with a maximum node limit.
Reference: ACT §7.1 step 4.
Raises:
ACTDAGError: If a cycle is detected or traversal limit exceeded.
"""
visited: set[str] = set()
queue: list[str] = list(parent_jtis)
nodes_visited = 0
while queue:
if nodes_visited >= MAX_TRAVERSAL_LIMIT:
raise ACTDAGError(
f"DAG traversal limit ({MAX_TRAVERSAL_LIMIT}) exceeded; "
f"possible cycle or excessively deep DAG"
)
jti = queue.pop(0)
if jti == current_jti:
raise ACTDAGError(
f"DAG cycle detected: jti {current_jti!r} appears in "
f"its own ancestor chain"
)
if jti in visited:
continue
visited.add(jti)
nodes_visited += 1
parent = store.get(jti)
if parent is not None:
queue.extend(parent.par)

View File

@@ -0,0 +1,333 @@
"""ACT delegation chain construction and verification.
Handles peer-to-peer delegation where Agent A authorizes Agent B
with reduced privileges, building a cryptographic chain of authority.
Reference: ACT §6 (Delegation Chain).
"""
from __future__ import annotations
import base64
from typing import Any
from .crypto import (
PrivateKey,
PublicKey,
compute_sha256,
sign as crypto_sign,
verify as crypto_verify,
)
from .errors import (
ACTDelegationError,
ACTPrivilegeEscalationError,
ACTValidationError,
)
from .token import (
ACTMandate,
Capability,
Delegation,
DelegationEntry,
_b64url_encode,
_b64url_decode,
encode_jws,
)
def create_delegated_mandate(
parent_mandate: ACTMandate,
parent_compact: str,
delegator_private_key: PrivateKey,
*,
sub: str,
kid: str,
iss: str,
aud: str | list[str],
iat: int,
exp: int,
jti: str,
cap: list[Capability],
task: Any,
alg: str = "EdDSA",
wid: str | None = None,
max_depth: int | None = None,
oversight: Any | None = None,
) -> tuple[ACTMandate, str]:
"""Create a delegated ACT mandate from a parent mandate.
Agent A (delegator) creates a new mandate for Agent B (sub) with
reduced privileges. The delegation chain is extended with a new
entry linking back to the parent ACT.
Reference: ACT §6.1 (Peer-to-Peer Delegation).
Args:
parent_mandate: The parent ACT that authorizes delegation.
parent_compact: JWS compact serialization of the parent ACT.
delegator_private_key: The delegator's private key for chain sig.
sub: Target agent identifier.
kid: Key identifier for the new mandate's signing key.
iss: Issuer identifier (the delegator).
aud: Audience for the new mandate.
iat: Issuance time.
exp: Expiration time.
jti: Unique identifier for the new mandate.
cap: Capabilities (must be subset of parent).
task: TaskClaim for the new mandate.
alg: Algorithm (default EdDSA).
wid: Workflow identifier (optional).
max_depth: Max delegation depth (must be <= parent's).
oversight: Oversight claim (optional).
Returns:
Tuple of (ACTMandate, needs to be signed by delegator).
Raises:
ACTDelegationError: If delegation depth would exceed max_depth.
ACTPrivilegeEscalationError: If cap exceeds parent capabilities.
"""
# Determine parent delegation state
if parent_mandate.delegation is not None:
parent_depth = parent_mandate.delegation.depth
parent_max_depth = parent_mandate.delegation.max_depth
parent_chain = list(parent_mandate.delegation.chain)
else:
# Root mandate without del claim — delegation not permitted
raise ACTDelegationError(
"Parent mandate has no 'del' claim; delegation is not permitted"
)
new_depth = parent_depth + 1
# Validate depth constraints — ACT §6.3 step 3
if new_depth > parent_max_depth:
raise ACTDelegationError(
f"Delegation depth {new_depth} exceeds max_depth {parent_max_depth}"
)
# Validate max_depth — ACT §6.1 step 4
if max_depth is None:
effective_max_depth = parent_max_depth
else:
if max_depth > parent_max_depth:
raise ACTDelegationError(
f"Requested max_depth {max_depth} exceeds parent max_depth "
f"{parent_max_depth}"
)
effective_max_depth = max_depth
# Validate capability subset — ACT §6.2
verify_capability_subset(parent_mandate.cap, cap)
# Compute chain entry signature — ACT §6.1 step 5
parent_hash = compute_sha256(parent_compact.encode("utf-8"))
chain_sig = crypto_sign(delegator_private_key, parent_hash)
chain_sig_b64 = _b64url_encode(chain_sig)
# Build new chain entry
new_entry = DelegationEntry(
delegator=iss,
jti=parent_mandate.jti,
sig=chain_sig_b64,
)
# Extend chain — ordered root → immediate parent
new_chain = parent_chain + [new_entry]
delegation = Delegation(
depth=new_depth,
max_depth=effective_max_depth,
chain=new_chain,
)
mandate = ACTMandate(
alg=alg,
kid=kid,
iss=iss,
sub=sub,
aud=aud,
iat=iat,
exp=exp,
jti=jti,
wid=wid if wid is not None else parent_mandate.wid,
task=task,
cap=cap,
delegation=delegation,
oversight=oversight,
)
return mandate, ""
def verify_capability_subset(
parent_caps: list[Capability],
child_caps: list[Capability],
) -> None:
"""Verify that child capabilities are a subset of parent capabilities.
Each child capability action must exist in the parent. Constraints
must be at least as restrictive.
Reference: ACT §6.2 (Privilege Reduction Requirements).
Raises:
ACTPrivilegeEscalationError: If child cap exceeds parent cap.
"""
parent_actions = {c.action: c for c in parent_caps}
for child_cap in child_caps:
if child_cap.action not in parent_actions:
raise ACTPrivilegeEscalationError(
f"Capability action {child_cap.action!r} not present in "
f"parent capabilities: {sorted(parent_actions.keys())}"
)
parent_cap = parent_actions[child_cap.action]
_verify_constraints_subset(
parent_cap.constraints, child_cap.constraints, child_cap.action
)
def _verify_constraints_subset(
parent_constraints: dict[str, Any] | None,
child_constraints: dict[str, Any] | None,
action: str,
) -> None:
"""Verify child constraints are at least as restrictive as parent.
Reference: ACT §6.2 (Privilege Reduction Requirements).
Rules:
- Numeric values: child must be <= parent (lower = more restrictive)
- data_sensitivity enum: child must be >= parent in ordering
- Unknown/domain-specific: must be byte-for-byte identical
Raises:
ACTPrivilegeEscalationError: If child constraint is less restrictive.
"""
if parent_constraints is None:
# Parent has no constraints — child may add constraints (more restrictive)
return
if child_constraints is None:
# Parent has constraints but child does not — escalation
raise ACTPrivilegeEscalationError(
f"Capability {action!r}: parent has constraints but child does not"
)
# Sensitivity ordering per ACT §6.2
_SENSITIVITY_ORDER = {
"public": 0,
"internal": 1,
"confidential": 2,
"restricted": 3,
}
for key, parent_val in parent_constraints.items():
if key not in child_constraints:
# Missing constraint in child = less restrictive
raise ACTPrivilegeEscalationError(
f"Capability {action!r}: constraint {key!r} present in "
f"parent but missing in child"
)
child_val = child_constraints[key]
if key == "data_sensitivity" or key == "data_classification_max":
# Enum comparison — higher = more restrictive
p_ord = _SENSITIVITY_ORDER.get(parent_val)
c_ord = _SENSITIVITY_ORDER.get(child_val)
if p_ord is not None and c_ord is not None:
if c_ord < p_ord:
raise ACTPrivilegeEscalationError(
f"Capability {action!r}: constraint {key!r} "
f"value {child_val!r} is less restrictive than "
f"parent value {parent_val!r}"
)
continue
if isinstance(parent_val, (int, float)) and isinstance(child_val, (int, float)):
# Numeric: lower/equal = more restrictive
if child_val > parent_val:
raise ACTPrivilegeEscalationError(
f"Capability {action!r}: numeric constraint {key!r} "
f"value {child_val} exceeds parent value {parent_val}"
)
continue
# Unknown/domain-specific: must be identical — ACT §6.2
if child_val != parent_val:
raise ACTPrivilegeEscalationError(
f"Capability {action!r}: constraint {key!r} value "
f"{child_val!r} differs from parent value {parent_val!r} "
f"(non-comparable constraints must be identical)"
)
def verify_delegation_chain(
mandate: ACTMandate,
resolve_key: Any,
resolve_parent_compact: Any | None = None,
) -> None:
"""Verify the delegation chain of a mandate.
Reference: ACT §6.3 (Delegation Verification).
Args:
mandate: The ACT mandate to verify.
resolve_key: Callable(delegator_id: str) -> PublicKey to resolve
the public key of a delegator.
resolve_parent_compact: Optional callable(jti: str) -> str|None
to retrieve the parent ACT compact form.
Required for full chain sig verification.
Raises:
ACTDelegationError: If the chain is structurally invalid.
ACTPrivilegeEscalationError: If capabilities were escalated.
"""
if mandate.delegation is None:
# No delegation — root mandate, nothing to verify
return
delegation = mandate.delegation
# Step 3: depth <= max_depth
if delegation.depth > delegation.max_depth:
raise ACTDelegationError(
f"Delegation depth {delegation.depth} exceeds "
f"max_depth {delegation.max_depth}"
)
# Step 4: chain length == depth
if len(delegation.chain) != delegation.depth:
raise ACTDelegationError(
f"Delegation chain length {len(delegation.chain)} does not "
f"match depth {delegation.depth}"
)
# Step 2: verify each chain entry
for i, entry in enumerate(delegation.chain):
# Step 2a: resolve delegator's public key
try:
pub_key = resolve_key(entry.delegator)
except Exception as e:
raise ACTDelegationError(
f"Cannot resolve key for delegator {entry.delegator!r} "
f"at chain index {i}: {e}"
) from e
# Step 2b: verify signature if parent compact is available
if resolve_parent_compact is not None:
parent_compact = resolve_parent_compact(entry.jti)
if parent_compact is not None:
parent_hash = compute_sha256(
parent_compact.encode("utf-8")
)
sig_bytes = _b64url_decode(entry.sig)
try:
crypto_verify(pub_key, sig_bytes, parent_hash)
except Exception as e:
raise ACTDelegationError(
f"Chain entry signature verification failed at "
f"index {i} (delegator={entry.delegator!r}): {e}"
) from e

131
workspace/act/act/errors.py Normal file
View File

@@ -0,0 +1,131 @@
"""ACT-specific exception types.
All exceptions defined in this module correspond to specific failure
modes in the Agent Compact Token lifecycle as defined in
draft-nennemann-act-00.
Reference: ACT §8 (Verification Procedure), §6 (Delegation Chain),
§7 (DAG Structure), §10 (Audit Ledger Interface).
"""
from __future__ import annotations
class ACTError(Exception):
"""Base exception for all ACT operations.
All ACT-specific exceptions inherit from this class, allowing
callers to catch any ACT error with a single except clause.
Reference: draft-nennemann-act-00.
"""
class ACTValidationError(ACTError):
"""Malformed token structure or invalid field values.
Raised when an ACT fails structural validation: missing required
claims, invalid claim types, unsupported algorithm ("none", HS*),
or invalid typ header.
Reference: ACT §4 (Token Format), §8.1 steps 2-3, 11.
"""
class ACTSignatureError(ACTError):
"""Signature verification failed.
Raised when a JWS signature cannot be verified against the
resolved public key, or when a Phase 2 token is signed by the
wrong key (e.g., iss key instead of sub key).
Reference: ACT §8.1 step 5, §8.2 step 17.
"""
class ACTExpiredError(ACTError):
"""Token has expired.
Raised when the current time exceeds exp + clock_skew_tolerance.
The default clock skew tolerance is 300 seconds.
Reference: ACT §8.1 step 6.
"""
class ACTAudienceMismatchError(ACTError):
"""The aud claim does not contain the verifier's identity.
Reference: ACT §8.1 step 8.
"""
class ACTCapabilityError(ACTError):
"""No matching capability or exec_act not in cap actions.
Raised when exec_act does not match any cap[].action value,
or when a requested action is not authorized by any capability.
Reference: ACT §8.2 step 13, §4.2.2 (cap).
"""
class ACTDelegationError(ACTError):
"""Delegation chain is invalid.
Raised when delegation chain verification fails: depth > max_depth,
chain length != depth, or any chain entry signature fails.
Reference: ACT §6 (Delegation Chain), §8.1 step 12.
"""
class ACTDAGError(ACTError):
"""DAG validation failed.
Raised on cycle detection, missing parent jti, temporal ordering
violations, or traversal limit exceeded.
Reference: ACT §7 (DAG Structure and Causal Ordering).
"""
class ACTPhaseError(ACTError):
"""Wrong phase for the requested operation.
Raised when a mandate is used where a record is expected, or
vice versa. Phase is determined by the presence of exec_act.
Reference: ACT §3 (Lifecycle), §8.
"""
class ACTKeyResolutionError(ACTError):
"""Cannot resolve kid to a public key.
Raised when the kid in the JOSE header cannot be resolved to a
public key via any configured trust tier (pre-shared, PKI, DID).
Reference: ACT §5 (Trust Model), §8.1 step 4.
"""
class ACTLedgerImmutabilityError(ACTError):
"""Attempt to modify or delete a ledger record.
The audit ledger enforces append-only semantics. Once appended,
a record cannot be modified or deleted.
Reference: ACT §10 (Audit Ledger Interface).
"""
class ACTPrivilegeEscalationError(ACTError):
"""Delegated capability exceeds parent capability.
Raised when a delegated ACT contains actions not present in the
parent ACT's cap array, or when constraints are less restrictive
than the parent's constraints.
Reference: ACT §6.2 (Privilege Reduction Requirements).
"""

152
workspace/act/act/ledger.py Normal file
View File

@@ -0,0 +1,152 @@
"""ACT in-memory append-only audit ledger.
Provides an in-memory reference implementation of the audit ledger
interface. Enforces append-only semantics and hash-chain integrity.
Reference: ACT §10 (Audit Ledger Interface).
"""
from __future__ import annotations
import hashlib
import json
from typing import Any
from .errors import ACTLedgerImmutabilityError
from .token import ACTRecord
class ACTLedger:
"""In-memory append-only audit ledger for ACT execution records.
Records are stored in insertion order with monotonically increasing
sequence numbers. A hash chain provides integrity verification.
Reference: ACT §10.
This is a reference implementation suitable for testing. Production
deployments should use a persistent backend implementing the same
interface.
"""
def __init__(self) -> None:
self._records: list[tuple[int, ACTRecord, str]] = []
# jti → index mapping for efficient lookup
self._jti_index: dict[str, int] = {}
# wid → list of indices for workflow queries
self._wid_index: dict[str | None, list[int]] = {}
self._seq_counter: int = 0
# Hash chain: each entry's hash includes the previous hash
self._chain_hashes: list[bytes] = []
def append(self, act_record: ACTRecord) -> int:
"""Append an execution record to the ledger.
Returns the sequence number assigned to the record.
Reference: ACT §10, requirement 1 (append-only), requirement 2 (ordering).
Raises:
ACTLedgerImmutabilityError: If a record with the same jti
already exists.
"""
if act_record.jti in self._jti_index:
raise ACTLedgerImmutabilityError(
f"Record with jti {act_record.jti!r} already exists in ledger"
)
seq = self._seq_counter
self._seq_counter += 1
# Compute hash chain entry
record_hash = self._hash_record(act_record, seq)
if self._chain_hashes:
chained = hashlib.sha256(
self._chain_hashes[-1] + record_hash
).digest()
else:
chained = record_hash
self._chain_hashes.append(chained)
idx = len(self._records)
self._records.append((seq, act_record, act_record.jti))
self._jti_index[act_record.jti] = idx
wid = act_record.wid
if wid not in self._wid_index:
self._wid_index[wid] = []
self._wid_index[wid].append(idx)
return seq
def get(self, jti: str) -> ACTRecord | None:
"""Retrieve a record by jti.
Reference: ACT §10, requirement 3 (lookup).
"""
idx = self._jti_index.get(jti)
if idx is None:
return None
return self._records[idx][1]
def list(self, wid: str | None = None) -> list[ACTRecord]:
"""List records, optionally filtered by workflow id.
If wid is None, returns all records. If wid is a string,
returns only records with that wid value.
Reference: ACT §10.
"""
if wid is None:
return [r[1] for r in self._records]
indices = self._wid_index.get(wid, [])
return [self._records[i][1] for i in indices]
def verify_integrity(self) -> bool:
"""Verify the hash chain integrity of the ledger.
Recomputes the hash chain from scratch and compares against
stored chain hashes. Returns True if all hashes match.
Reference: ACT §10, requirement 4 (integrity).
"""
if not self._records:
return True
prev_hash: bytes | None = None
for i, (seq, record, _jti) in enumerate(self._records):
record_hash = self._hash_record(record, seq)
if prev_hash is not None:
expected = hashlib.sha256(prev_hash + record_hash).digest()
else:
expected = record_hash
if i >= len(self._chain_hashes):
return False
if self._chain_hashes[i] != expected:
return False
prev_hash = expected
return True
def __len__(self) -> int:
return len(self._records)
def _hash_record(self, record: ACTRecord, seq: int) -> bytes:
"""Compute a deterministic hash of a record for chain integrity."""
claims = record.to_claims()
# Include sequence number in hash for ordering integrity
claims["_seq"] = seq
canonical = json.dumps(claims, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(canonical.encode("utf-8")).digest()
def _immutable_guard(self) -> None:
"""Internal method — not callable externally.
The ledger has no update/delete methods by design.
This exists to make the intent explicit.
"""
raise ACTLedgerImmutabilityError(
"Ledger records cannot be modified or deleted"
)

View File

@@ -0,0 +1,96 @@
"""ACT Phase 1 to Phase 2 transition logic.
Handles the transition from Authorization Mandate to Execution Record,
including re-signing by the executing agent (sub).
Reference: ACT §3.2, §3.3 (Lifecycle State Machine).
"""
from __future__ import annotations
import time
from typing import Any
from .crypto import PrivateKey, sign as crypto_sign
from .errors import ACTCapabilityError, ACTPhaseError, ACTValidationError
from .token import (
ACTMandate,
ACTRecord,
ErrorClaim,
encode_jws,
)
def transition_to_record(
mandate: ACTMandate,
*,
sub_kid: str,
sub_private_key: PrivateKey,
exec_act: str,
par: list[str] | None = None,
exec_ts: int | None = None,
status: str = "completed",
inp_hash: str | None = None,
out_hash: str | None = None,
err: ErrorClaim | None = None,
) -> tuple[ACTRecord, str]:
"""Transition a Phase 1 mandate to a Phase 2 execution record.
The executing agent (sub) adds execution claims and re-signs the
complete token with its own private key. The kid in the Phase 2
JOSE header MUST reference sub's key, not iss's key.
All Phase 1 claims are preserved unchanged in the Phase 2 token.
Reference: ACT §3.2, §8.2 step 17.
Args:
mandate: The Phase 1 ACTMandate to transition.
sub_kid: The kid for the sub agent's signing key.
sub_private_key: The sub agent's private key for re-signing.
exec_act: The action actually performed (must match a cap[].action).
par: Parent task jti values (DAG dependencies). Empty list for root tasks.
exec_ts: Execution timestamp (defaults to current time).
status: Execution status: "completed", "failed", or "partial".
inp_hash: Base64url SHA-256 hash of input data (optional).
out_hash: Base64url SHA-256 hash of output data (optional).
err: Error details when status is "failed" or "partial".
Returns:
Tuple of (ACTRecord, JWS compact serialization string).
Raises:
ACTPhaseError: If the mandate is already a Phase 2 token.
ACTCapabilityError: If exec_act does not match any cap[].action.
ACTValidationError: If the resulting record fails validation.
"""
if mandate.is_phase2():
raise ACTPhaseError("Cannot transition: token is already Phase 2")
# Verify exec_act matches a capability
cap_actions = {c.action for c in mandate.cap}
if exec_act not in cap_actions:
raise ACTCapabilityError(
f"exec_act {exec_act!r} does not match any cap[].action: "
f"{sorted(cap_actions)}"
)
record = ACTRecord.from_mandate(
mandate,
kid=sub_kid,
exec_act=exec_act,
par=par if par is not None else [],
exec_ts=exec_ts if exec_ts is not None else int(time.time()),
status=status,
inp_hash=inp_hash,
out_hash=out_hash,
err=err,
)
record.validate()
# Re-sign with sub's private key
signature = crypto_sign(sub_private_key, record.signing_input())
compact = encode_jws(record, signature)
return record, compact

734
workspace/act/act/token.py Normal file
View File

@@ -0,0 +1,734 @@
"""ACT token structures and JWS Compact Serialization.
Defines ACTMandate (Phase 1) and ACTRecord (Phase 2) dataclasses,
plus JWS encoding/decoding primitives for ACT tokens.
Reference: ACT §3 (Lifecycle), §4 (Token Format).
"""
from __future__ import annotations
import base64
import json
import re
import time
import uuid
from dataclasses import dataclass, field
from typing import Any
from .errors import ACTPhaseError, ACTValidationError
# Allowed algorithms per ACT §4.1 — symmetric and "none" are forbidden.
ALLOWED_ALGORITHMS: frozenset[str] = frozenset({"EdDSA", "ES256"})
# Forbidden algorithm prefixes/values per ACT §4.1.
_FORBIDDEN_ALGORITHMS: frozenset[str] = frozenset({
"none", "HS256", "HS384", "HS512",
})
# Required typ value per ACT §4.1.
ACT_TYP: str = "act+jwt"
# ABNF for action names: component *("." component)
# component = ALPHA *(ALPHA / DIGIT / "-" / "_")
_ACTION_RE = re.compile(
r"^[A-Za-z][A-Za-z0-9\-_]*(?:\.[A-Za-z][A-Za-z0-9\-_]*)*$"
)
def _b64url_encode(data: bytes) -> str:
"""Base64url encode without padding (RFC 7515 §2)."""
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
def _b64url_decode(s: str) -> bytes:
"""Base64url decode with padding restoration."""
s = s + "=" * (-len(s) % 4)
return base64.urlsafe_b64decode(s)
def validate_action_name(action: str) -> None:
"""Validate an action name against ACT ABNF grammar.
Reference: ACT §4.2.2 (cap action names).
Raises:
ACTValidationError: If action does not match the required grammar.
"""
if not _ACTION_RE.match(action):
raise ACTValidationError(
f"Action name {action!r} does not conform to ACT ABNF grammar"
)
@dataclass(frozen=True)
class TaskClaim:
"""The 'task' claim object.
Reference: ACT §4.2.2.
"""
purpose: str
data_sensitivity: str | None = None
created_by: str | None = None
expires_at: int | None = None
def to_dict(self) -> dict[str, Any]:
d: dict[str, Any] = {"purpose": self.purpose}
if self.data_sensitivity is not None:
d["data_sensitivity"] = self.data_sensitivity
if self.created_by is not None:
d["created_by"] = self.created_by
if self.expires_at is not None:
d["expires_at"] = self.expires_at
return d
@classmethod
def from_dict(cls, d: dict[str, Any]) -> TaskClaim:
if "purpose" not in d:
raise ACTValidationError("task.purpose is required")
return cls(
purpose=d["purpose"],
data_sensitivity=d.get("data_sensitivity"),
created_by=d.get("created_by"),
expires_at=d.get("expires_at"),
)
@dataclass(frozen=True)
class Capability:
"""A single capability entry in the 'cap' array.
Reference: ACT §4.2.2.
"""
action: str
constraints: dict[str, Any] | None = None
def __post_init__(self) -> None:
validate_action_name(self.action)
def to_dict(self) -> dict[str, Any]:
d: dict[str, Any] = {"action": self.action}
if self.constraints is not None:
d["constraints"] = self.constraints
return d
@classmethod
def from_dict(cls, d: dict[str, Any]) -> Capability:
if "action" not in d:
raise ACTValidationError("cap[].action is required")
return cls(
action=d["action"],
constraints=d.get("constraints"),
)
@dataclass(frozen=True)
class DelegationEntry:
"""A single entry in del.chain.
Reference: ACT §4.2.2 (del), §6 (Delegation Chain).
"""
delegator: str
jti: str
sig: str
def to_dict(self) -> dict[str, str]:
return {"delegator": self.delegator, "jti": self.jti, "sig": self.sig}
@classmethod
def from_dict(cls, d: dict[str, Any]) -> DelegationEntry:
for key in ("delegator", "jti", "sig"):
if key not in d:
raise ACTValidationError(f"del.chain[].{key} is required")
return cls(
delegator=d["delegator"], jti=d["jti"], sig=d["sig"]
)
@dataclass(frozen=True)
class Delegation:
"""The 'del' claim object.
Reference: ACT §4.2.2 (del), §6 (Delegation Chain).
"""
depth: int
max_depth: int
chain: list[DelegationEntry] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return {
"depth": self.depth,
"max_depth": self.max_depth,
"chain": [e.to_dict() for e in self.chain],
}
@classmethod
def from_dict(cls, d: dict[str, Any]) -> Delegation:
for key in ("depth", "max_depth"):
if key not in d:
raise ACTValidationError(f"del.{key} is required")
chain_raw = d.get("chain", [])
chain = [DelegationEntry.from_dict(e) for e in chain_raw]
return cls(depth=d["depth"], max_depth=d["max_depth"], chain=chain)
@dataclass(frozen=True)
class Oversight:
"""The 'oversight' claim object.
Reference: ACT §4.2.2 (oversight).
"""
requires_approval_for: list[str] = field(default_factory=list)
approval_ref: str | None = None
def to_dict(self) -> dict[str, Any]:
d: dict[str, Any] = {
"requires_approval_for": self.requires_approval_for
}
if self.approval_ref is not None:
d["approval_ref"] = self.approval_ref
return d
@classmethod
def from_dict(cls, d: dict[str, Any]) -> Oversight:
return cls(
requires_approval_for=d.get("requires_approval_for", []),
approval_ref=d.get("approval_ref"),
)
@dataclass(frozen=True)
class ErrorClaim:
"""The 'err' claim object for failed/partial execution.
Reference: ACT §4.3.
"""
code: str
detail: str
def to_dict(self) -> dict[str, str]:
return {"code": self.code, "detail": self.detail}
@classmethod
def from_dict(cls, d: dict[str, Any]) -> ErrorClaim:
for key in ("code", "detail"):
if key not in d:
raise ACTValidationError(f"err.{key} is required")
return cls(code=d["code"], detail=d["detail"])
@dataclass
class ACTMandate:
"""Phase 1 Authorization Mandate.
Represents a signed authorization from an issuing agent to a target
agent, encoding capabilities, constraints, and delegation provenance.
Reference: ACT §3.1, §4.1, §4.2.
"""
# JOSE header fields
alg: str
kid: str
x5c: list[str] | None = None
did: str | None = None
# Required JWT claims
iss: str = ""
sub: str = ""
aud: str | list[str] = ""
iat: int = 0
exp: int = 0
jti: str = field(default_factory=lambda: str(uuid.uuid4()))
# Optional standard claims
wid: str | None = None
# Required ACT claims
task: TaskClaim = field(default_factory=lambda: TaskClaim(purpose=""))
cap: list[Capability] = field(default_factory=list)
# Optional ACT claims
delegation: Delegation | None = None
oversight: Oversight | None = None
def validate(self) -> None:
"""Validate structural correctness of this mandate.
Reference: ACT §4.1, §4.2, §8.1 step 11.
Raises:
ACTValidationError: If any required field is missing or invalid.
"""
_validate_algorithm(self.alg)
if not self.kid:
raise ACTValidationError("kid is required in JOSE header")
for claim_name in ("iss", "sub", "aud", "jti"):
val = getattr(self, claim_name)
if not val:
raise ACTValidationError(f"{claim_name} claim is required")
if self.iat <= 0:
raise ACTValidationError("iat must be a positive NumericDate")
if self.exp <= 0:
raise ACTValidationError("exp must be a positive NumericDate")
if not self.task.purpose:
raise ACTValidationError("task.purpose is required")
if not self.cap:
raise ACTValidationError("cap must contain at least one capability")
def to_header(self) -> dict[str, Any]:
"""Build JOSE header dict.
Reference: ACT §4.1.
"""
h: dict[str, Any] = {
"alg": self.alg,
"typ": ACT_TYP,
"kid": self.kid,
}
if self.x5c is not None:
h["x5c"] = self.x5c
if self.did is not None:
h["did"] = self.did
return h
def to_claims(self) -> dict[str, Any]:
"""Build JWT claims dict (Phase 1 claims only).
Reference: ACT §4.2.
"""
c: dict[str, Any] = {
"iss": self.iss,
"sub": self.sub,
"aud": self.aud,
"iat": self.iat,
"exp": self.exp,
"jti": self.jti,
"task": self.task.to_dict(),
"cap": [cap.to_dict() for cap in self.cap],
}
if self.wid is not None:
c["wid"] = self.wid
if self.delegation is not None:
c["del"] = self.delegation.to_dict()
if self.oversight is not None:
c["oversight"] = self.oversight.to_dict()
return c
def signing_input(self) -> bytes:
"""Compute the JWS signing input (header.payload) as bytes.
Reference: RFC 7515 §5.1.
"""
header_b64 = _b64url_encode(
json.dumps(self.to_header(), separators=(",", ":")).encode()
)
payload_b64 = _b64url_encode(
json.dumps(self.to_claims(), separators=(",", ":")).encode()
)
return f"{header_b64}.{payload_b64}".encode("ascii")
def is_phase2(self) -> bool:
"""Return False; mandates are always Phase 1."""
return False
@classmethod
def from_claims(
cls,
header: dict[str, Any],
claims: dict[str, Any],
) -> ACTMandate:
"""Construct an ACTMandate from parsed header and claims dicts.
Reference: ACT §4.1, §4.2.
Raises:
ACTValidationError: If required fields are missing.
ACTPhaseError: If exec_act is present (this is a Phase 2 token).
"""
if "exec_act" in claims:
raise ACTPhaseError(
"Token contains exec_act; use ACTRecord.from_claims instead"
)
del_raw = claims.get("del")
delegation = Delegation.from_dict(del_raw) if del_raw else None
oversight_raw = claims.get("oversight")
oversight_obj = Oversight.from_dict(oversight_raw) if oversight_raw else None
task_raw = claims.get("task")
if task_raw is None:
raise ACTValidationError("task claim is required")
cap_raw = claims.get("cap")
if cap_raw is None:
raise ACTValidationError("cap claim is required")
return cls(
alg=header.get("alg", ""),
kid=header.get("kid", ""),
x5c=header.get("x5c"),
did=header.get("did"),
iss=claims.get("iss", ""),
sub=claims.get("sub", ""),
aud=claims.get("aud", ""),
iat=claims.get("iat", 0),
exp=claims.get("exp", 0),
jti=claims.get("jti", ""),
wid=claims.get("wid"),
task=TaskClaim.from_dict(task_raw),
cap=[Capability.from_dict(c) for c in cap_raw],
delegation=delegation,
oversight=oversight_obj,
)
@dataclass
class ACTRecord:
"""Phase 2 Execution Record.
Contains all Phase 1 claims preserved unchanged, plus execution
claims added by the executing agent. Re-signed by sub's key.
Reference: ACT §3.2, §4.3.
"""
# JOSE header fields (Phase 2 header uses sub's kid)
alg: str
kid: str
x5c: list[str] | None = None
did: str | None = None
# Phase 1 claims (preserved)
iss: str = ""
sub: str = ""
aud: str | list[str] = ""
iat: int = 0
exp: int = 0
jti: str = ""
wid: str | None = None
task: TaskClaim = field(default_factory=lambda: TaskClaim(purpose=""))
cap: list[Capability] = field(default_factory=list)
delegation: Delegation | None = None
oversight: Oversight | None = None
# Phase 2 claims (execution)
exec_act: str = ""
par: list[str] = field(default_factory=list)
exec_ts: int = 0
status: str = ""
inp_hash: str | None = None
out_hash: str | None = None
err: ErrorClaim | None = None
def validate(self) -> None:
"""Validate structural correctness of this record.
Reference: ACT §4.3, §8.2 steps 13-16.
Raises:
ACTValidationError: If any required field is missing or invalid.
"""
_validate_algorithm(self.alg)
if not self.kid:
raise ACTValidationError("kid is required in JOSE header")
for claim_name in ("iss", "sub", "aud", "jti"):
val = getattr(self, claim_name)
if not val:
raise ACTValidationError(f"{claim_name} claim is required")
if self.iat <= 0:
raise ACTValidationError("iat must be a positive NumericDate")
if self.exp <= 0:
raise ACTValidationError("exp must be a positive NumericDate")
if not self.task.purpose:
raise ACTValidationError("task.purpose is required")
if not self.cap:
raise ACTValidationError("cap must contain at least one capability")
if not self.exec_act:
raise ACTValidationError("exec_act is required in Phase 2")
validate_action_name(self.exec_act)
if self.exec_ts <= 0:
raise ACTValidationError("exec_ts must be a positive NumericDate")
if self.status not in ("completed", "failed", "partial"):
raise ACTValidationError(
f"status must be one of completed/failed/partial, got {self.status!r}"
)
def to_header(self) -> dict[str, Any]:
"""Build JOSE header dict for Phase 2.
In Phase 2, kid MUST reference the sub agent's key.
Reference: ACT §4.1, §8.2 step 17.
"""
h: dict[str, Any] = {
"alg": self.alg,
"typ": ACT_TYP,
"kid": self.kid,
}
if self.x5c is not None:
h["x5c"] = self.x5c
if self.did is not None:
h["did"] = self.did
return h
def to_claims(self) -> dict[str, Any]:
"""Build JWT claims dict (Phase 1 + Phase 2 claims).
Reference: ACT §4.2, §4.3.
"""
c: dict[str, Any] = {
"iss": self.iss,
"sub": self.sub,
"aud": self.aud,
"iat": self.iat,
"exp": self.exp,
"jti": self.jti,
"task": self.task.to_dict(),
"cap": [cap.to_dict() for cap in self.cap],
"exec_act": self.exec_act,
"par": self.par,
"exec_ts": self.exec_ts,
"status": self.status,
}
if self.wid is not None:
c["wid"] = self.wid
if self.delegation is not None:
c["del"] = self.delegation.to_dict()
if self.oversight is not None:
c["oversight"] = self.oversight.to_dict()
if self.inp_hash is not None:
c["inp_hash"] = self.inp_hash
if self.out_hash is not None:
c["out_hash"] = self.out_hash
if self.err is not None:
c["err"] = self.err.to_dict()
return c
def signing_input(self) -> bytes:
"""Compute the JWS signing input (header.payload) as bytes.
Reference: RFC 7515 §5.1.
"""
header_b64 = _b64url_encode(
json.dumps(self.to_header(), separators=(",", ":")).encode()
)
payload_b64 = _b64url_encode(
json.dumps(self.to_claims(), separators=(",", ":")).encode()
)
return f"{header_b64}.{payload_b64}".encode("ascii")
def is_phase2(self) -> bool:
"""Return True; records are always Phase 2."""
return True
@classmethod
def from_mandate(
cls,
mandate: ACTMandate,
*,
kid: str,
exec_act: str,
par: list[str] | None = None,
exec_ts: int | None = None,
status: str = "completed",
inp_hash: str | None = None,
out_hash: str | None = None,
err: ErrorClaim | None = None,
) -> ACTRecord:
"""Create an ACTRecord by transitioning a mandate to Phase 2.
The kid MUST be the sub agent's key identifier.
Reference: ACT §3.2, §4.3.
"""
return cls(
alg=mandate.alg,
kid=kid,
x5c=mandate.x5c,
did=mandate.did,
iss=mandate.iss,
sub=mandate.sub,
aud=mandate.aud,
iat=mandate.iat,
exp=mandate.exp,
jti=mandate.jti,
wid=mandate.wid,
task=mandate.task,
cap=mandate.cap,
delegation=mandate.delegation,
oversight=mandate.oversight,
exec_act=exec_act,
par=par if par is not None else [],
exec_ts=exec_ts if exec_ts is not None else int(time.time()),
status=status,
inp_hash=inp_hash,
out_hash=out_hash,
err=err,
)
@classmethod
def from_claims(
cls,
header: dict[str, Any],
claims: dict[str, Any],
) -> ACTRecord:
"""Construct an ACTRecord from parsed header and claims dicts.
Reference: ACT §4.1, §4.2, §4.3.
Raises:
ACTValidationError: If required fields are missing.
ACTPhaseError: If exec_act is absent (this is a Phase 1 token).
"""
if "exec_act" not in claims:
raise ACTPhaseError(
"Token does not contain exec_act; use ACTMandate.from_claims instead"
)
del_raw = claims.get("del")
delegation = Delegation.from_dict(del_raw) if del_raw else None
oversight_raw = claims.get("oversight")
oversight_obj = Oversight.from_dict(oversight_raw) if oversight_raw else None
task_raw = claims.get("task")
if task_raw is None:
raise ACTValidationError("task claim is required")
cap_raw = claims.get("cap")
if cap_raw is None:
raise ACTValidationError("cap claim is required")
err_raw = claims.get("err")
err_obj = ErrorClaim.from_dict(err_raw) if err_raw else None
return cls(
alg=header.get("alg", ""),
kid=header.get("kid", ""),
x5c=header.get("x5c"),
did=header.get("did"),
iss=claims.get("iss", ""),
sub=claims.get("sub", ""),
aud=claims.get("aud", ""),
iat=claims.get("iat", 0),
exp=claims.get("exp", 0),
jti=claims.get("jti", ""),
wid=claims.get("wid"),
task=TaskClaim.from_dict(task_raw),
cap=[Capability.from_dict(c) for c in cap_raw],
delegation=delegation,
oversight=oversight_obj,
exec_act=claims["exec_act"],
par=claims.get("par", []),
exec_ts=claims.get("exec_ts", 0),
status=claims.get("status", ""),
inp_hash=claims.get("inp_hash"),
out_hash=claims.get("out_hash"),
err=err_obj,
)
# --- JWS Compact Serialization ---
def encode_jws(
token: ACTMandate | ACTRecord,
signature: bytes,
) -> str:
"""Encode a token and signature as JWS Compact Serialization.
Returns header.payload.signature (three base64url segments).
Reference: RFC 7515 §3.1, ACT §4.
"""
signing_input = token.signing_input().decode("ascii")
sig_b64 = _b64url_encode(signature)
return f"{signing_input}.{sig_b64}"
def decode_jws(compact: str) -> tuple[dict[str, Any], dict[str, Any], bytes, bytes]:
"""Decode a JWS Compact Serialization string.
Returns (header_dict, claims_dict, signature_bytes, signing_input_bytes).
Reference: RFC 7515 §5.2, ACT §4.
Raises:
ACTValidationError: If the token is malformed.
"""
parts = compact.split(".")
if len(parts) != 3:
raise ACTValidationError(
f"JWS Compact Serialization requires 3 parts, got {len(parts)}"
)
try:
header = json.loads(_b64url_decode(parts[0]))
except (json.JSONDecodeError, Exception) as e:
raise ACTValidationError(f"Invalid JOSE header: {e}") from e
try:
claims = json.loads(_b64url_decode(parts[1]))
except (json.JSONDecodeError, Exception) as e:
raise ACTValidationError(f"Invalid JWT claims: {e}") from e
try:
signature = _b64url_decode(parts[2])
except Exception as e:
raise ACTValidationError(f"Invalid signature encoding: {e}") from e
signing_input = f"{parts[0]}.{parts[1]}".encode("ascii")
# Validate header requirements per ACT §4.1
typ = header.get("typ")
if typ != ACT_TYP:
raise ACTValidationError(
f"typ must be {ACT_TYP!r}, got {typ!r}"
)
alg = header.get("alg", "")
_validate_algorithm(alg)
if "kid" not in header:
raise ACTValidationError("kid is required in JOSE header")
return header, claims, signature, signing_input
def parse_token(compact: str) -> ACTMandate | ACTRecord:
"""Parse a JWS compact string into an ACTMandate or ACTRecord.
Determines phase by presence of exec_act claim.
Reference: ACT §3 (phase determination).
Returns:
ACTMandate for Phase 1, ACTRecord for Phase 2.
"""
header, claims, _, _ = decode_jws(compact)
if "exec_act" in claims:
return ACTRecord.from_claims(header, claims)
return ACTMandate.from_claims(header, claims)
def _validate_algorithm(alg: str) -> None:
"""Check algorithm is allowed per ACT §4.1.
Raises:
ACTValidationError: If algorithm is forbidden or unsupported.
"""
if alg in _FORBIDDEN_ALGORITHMS or alg.upper() in _FORBIDDEN_ALGORITHMS:
raise ACTValidationError(
f"Algorithm {alg!r} is forbidden by ACT specification"
)
if alg not in ALLOWED_ALGORITHMS:
raise ACTValidationError(
f"Unsupported algorithm {alg!r}; allowed: {sorted(ALLOWED_ALGORITHMS)}"
)

View File

@@ -0,0 +1,639 @@
"""ACT Appendix B test vectors.
Generates and validates all 15 test vectors from Appendix B of
draft-nennemann-act-00. Each vector includes description, input
parameters, and expected output or exception.
Reference: ACT Appendix B (Test Vectors).
"""
from __future__ import annotations
import time
import uuid
from dataclasses import dataclass, field
from typing import Any
from .crypto import (
ACTKeyResolver,
KeyRegistry,
PrivateKey,
PublicKey,
b64url_sha256,
compute_sha256,
generate_ed25519_keypair,
sign as crypto_sign,
verify as crypto_verify,
)
from .dag import validate_dag
from .delegation import create_delegated_mandate, verify_capability_subset
from .errors import (
ACTAudienceMismatchError,
ACTCapabilityError,
ACTDAGError,
ACTDelegationError,
ACTExpiredError,
ACTPrivilegeEscalationError,
ACTSignatureError,
ACTValidationError,
)
from .ledger import ACTLedger
from .lifecycle import transition_to_record
from .token import (
ACTMandate,
ACTRecord,
Capability,
Delegation,
DelegationEntry,
ErrorClaim,
Oversight,
TaskClaim,
_b64url_encode,
decode_jws,
encode_jws,
)
from .verify import ACTVerifier
@dataclass
class TestVector:
"""A single test vector."""
id: str
description: str
valid: bool
expected_exception: type[Exception] | None = None
compact: str = ""
record: ACTMandate | ACTRecord | None = None
def generate_vectors() -> tuple[list[TestVector], dict[str, Any]]:
"""Generate all Appendix B test vectors.
Returns a list of TestVector objects and a context dict containing
keys and other state needed for validation.
Reference: ACT Appendix B.
"""
# Fixed timestamp for deterministic vectors
base_time = 1772064000
# Generate key pairs for test agents
iss_priv, iss_pub = generate_ed25519_keypair()
sub_priv, sub_pub = generate_ed25519_keypair()
agent_c_priv, agent_c_pub = generate_ed25519_keypair()
# Fixed JTIs for cross-referencing
jti_b1 = "550e8400-e29b-41d4-a716-446655440001"
jti_b2 = "550e8400-e29b-41d4-a716-446655440002"
jti_b3_parent1 = "550e8400-e29b-41d4-a716-446655440003"
jti_b3_parent2 = "550e8400-e29b-41d4-a716-446655440004"
jti_b3 = "550e8400-e29b-41d4-a716-446655440005"
jti_b4 = "550e8400-e29b-41d4-a716-446655440006"
jti_b5 = "550e8400-e29b-41d4-a716-446655440007"
wid = "a0b1c2d3-e4f5-6789-abcd-ef0123456789"
# Key registry
registry = KeyRegistry()
registry.register("iss-key", iss_pub)
registry.register("sub-key", sub_pub)
registry.register("agent-c-key", agent_c_pub)
resolver = ACTKeyResolver(registry=registry)
vectors: list[TestVector] = []
compacts: dict[str, str] = {} # jti → compact for delegation refs
# --- B.1: Phase 1 — Root mandate, Tier 1, Ed25519, no delegation ---
mandate_b1 = ACTMandate(
alg="EdDSA",
kid="iss-key",
iss="agent-issuer",
sub="agent-subject",
aud=["agent-subject", "https://ledger.example.com"],
iat=base_time,
exp=base_time + 900,
jti=jti_b1,
wid=wid,
task=TaskClaim(
purpose="validate_data",
data_sensitivity="restricted",
),
cap=[
Capability(action="read.data", constraints={"max_records": 10}),
Capability(action="write.result"),
],
delegation=Delegation(depth=0, max_depth=2, chain=[]),
)
mandate_b1.validate()
sig_b1 = crypto_sign(iss_priv, mandate_b1.signing_input())
compact_b1 = encode_jws(mandate_b1, sig_b1)
compacts[jti_b1] = compact_b1
vectors.append(TestVector(
id="B.1",
description="Phase 1 ACT — root mandate, Tier 1 (Ed25519), no delegation",
valid=True,
compact=compact_b1,
record=mandate_b1,
))
# --- B.2: Phase 2 — Completed execution from B.1 ---
record_b2, compact_b2 = transition_to_record(
mandate_b1,
sub_kid="sub-key",
sub_private_key=sub_priv,
exec_act="read.data",
par=[],
exec_ts=base_time + 300,
status="completed",
inp_hash=b64url_sha256(b"test input data"),
out_hash=b64url_sha256(b"test output data"),
)
compacts[jti_b2] = compact_b2
vectors.append(TestVector(
id="B.2",
description="Phase 2 ACT — completed execution, transition from B.1 mandate",
valid=True,
compact=compact_b2,
record=record_b2,
))
# --- B.3: Phase 2 — Fan-in, two parent jti values ---
# Create two parent records first
parent1_mandate = ACTMandate(
alg="EdDSA", kid="iss-key",
iss="agent-issuer", sub="agent-subject",
aud="agent-subject",
iat=base_time, exp=base_time + 900,
jti=jti_b3_parent1, wid=wid,
task=TaskClaim(purpose="branch_a"),
cap=[Capability(action="compute.result")],
delegation=Delegation(depth=0, max_depth=1, chain=[]),
)
sig_p1 = crypto_sign(iss_priv, parent1_mandate.signing_input())
compact_p1 = encode_jws(parent1_mandate, sig_p1)
parent1_record, parent1_compact = transition_to_record(
parent1_mandate, sub_kid="sub-key", sub_private_key=sub_priv,
exec_act="compute.result", par=[], exec_ts=base_time + 100,
status="completed",
)
parent2_mandate = ACTMandate(
alg="EdDSA", kid="iss-key",
iss="agent-issuer", sub="agent-subject",
aud="agent-subject",
iat=base_time, exp=base_time + 900,
jti=jti_b3_parent2, wid=wid,
task=TaskClaim(purpose="branch_b"),
cap=[Capability(action="compute.result")],
delegation=Delegation(depth=0, max_depth=1, chain=[]),
)
sig_p2 = crypto_sign(iss_priv, parent2_mandate.signing_input())
compact_p2 = encode_jws(parent2_mandate, sig_p2)
parent2_record, parent2_compact = transition_to_record(
parent2_mandate, sub_kid="sub-key", sub_private_key=sub_priv,
exec_act="compute.result", par=[], exec_ts=base_time + 150,
status="completed",
)
# Fan-in record depends on both parents
fanin_mandate = ACTMandate(
alg="EdDSA", kid="iss-key",
iss="agent-issuer", sub="agent-subject",
aud="agent-subject",
iat=base_time, exp=base_time + 900,
jti=jti_b3, wid=wid,
task=TaskClaim(purpose="merge_results"),
cap=[Capability(action="compute.result")],
delegation=Delegation(depth=0, max_depth=1, chain=[]),
)
sig_fi = crypto_sign(iss_priv, fanin_mandate.signing_input())
fanin_record, fanin_compact = transition_to_record(
fanin_mandate, sub_kid="sub-key", sub_private_key=sub_priv,
exec_act="compute.result",
par=[jti_b3_parent1, jti_b3_parent2],
exec_ts=base_time + 200,
status="completed",
)
vectors.append(TestVector(
id="B.3",
description="Phase 2 ACT — fan-in, two parent jti values from parallel branches",
valid=True,
compact=fanin_compact,
record=fanin_record,
))
# --- B.4: Phase 1 — Delegated mandate (depth=1) ---
delegated_b4, _ = create_delegated_mandate(
parent_mandate=mandate_b1,
parent_compact=compact_b1,
delegator_private_key=iss_priv,
sub="agent-c",
kid="iss-key",
iss="agent-issuer",
aud="agent-c",
iat=base_time + 10,
exp=base_time + 600,
jti=jti_b4,
cap=[Capability(action="read.data", constraints={"max_records": 5})],
task=TaskClaim(purpose="delegated_read"),
)
sig_b4 = crypto_sign(iss_priv, delegated_b4.signing_input())
compact_b4 = encode_jws(delegated_b4, sig_b4)
compacts[jti_b4] = compact_b4
vectors.append(TestVector(
id="B.4",
description="Phase 1 ACT — delegated mandate (depth=1), chain entry with sig",
valid=True,
compact=compact_b4,
record=delegated_b4,
))
# --- B.5: Phase 2 — Delegated execution record ---
record_b5, compact_b5 = transition_to_record(
delegated_b4,
sub_kid="agent-c-key",
sub_private_key=agent_c_priv,
exec_act="read.data",
par=[],
exec_ts=base_time + 350,
status="completed",
)
vectors.append(TestVector(
id="B.5",
description="Phase 2 ACT — delegated execution record",
valid=True,
compact=compact_b5,
record=record_b5,
))
# --- B.6: del.depth > del.max_depth → ACTDelegationError ---
bad_depth_mandate = ACTMandate(
alg="EdDSA", kid="iss-key",
iss="agent-issuer", sub="agent-subject",
aud="agent-subject",
iat=base_time, exp=base_time + 900,
jti=str(uuid.uuid4()),
task=TaskClaim(purpose="bad_depth"),
cap=[Capability(action="read.data")],
delegation=Delegation(depth=3, max_depth=2, chain=[
DelegationEntry(delegator="a", jti="j1", sig="sig1"),
DelegationEntry(delegator="b", jti="j2", sig="sig2"),
DelegationEntry(delegator="c", jti="j3", sig="sig3"),
]),
)
sig_b6 = crypto_sign(iss_priv, bad_depth_mandate.signing_input())
compact_b6 = encode_jws(bad_depth_mandate, sig_b6)
vectors.append(TestVector(
id="B.6",
description="del.depth > del.max_depth → ACTDelegationError",
valid=False,
expected_exception=ACTDelegationError,
compact=compact_b6,
))
# --- B.7: cap escalation in delegated ACT → ACTPrivilegeEscalationError ---
vectors.append(TestVector(
id="B.7",
description="cap escalation in delegated ACT → ACTPrivilegeEscalationError",
valid=False,
expected_exception=ACTPrivilegeEscalationError,
))
# --- B.8: exec_act not in cap → ACTCapabilityError ---
bad_exec_mandate = ACTMandate(
alg="EdDSA", kid="iss-key",
iss="agent-issuer", sub="agent-subject",
aud="agent-subject",
iat=base_time, exp=base_time + 900,
jti=str(uuid.uuid4()),
task=TaskClaim(purpose="bad_exec"),
cap=[Capability(action="read.data")],
delegation=Delegation(depth=0, max_depth=1, chain=[]),
)
sig_b8m = crypto_sign(iss_priv, bad_exec_mandate.signing_input())
# Manually construct Phase 2 with wrong exec_act
bad_exec_record = ACTRecord(
alg="EdDSA", kid="sub-key",
iss="agent-issuer", sub="agent-subject",
aud="agent-subject",
iat=base_time, exp=base_time + 900,
jti=bad_exec_mandate.jti,
task=TaskClaim(purpose="bad_exec"),
cap=[Capability(action="read.data")],
exec_act="delete.everything",
par=[], exec_ts=base_time + 100, status="completed",
)
sig_b8 = crypto_sign(sub_priv, bad_exec_record.signing_input())
compact_b8 = encode_jws(bad_exec_record, sig_b8)
vectors.append(TestVector(
id="B.8",
description="exec_act not in cap → ACTCapabilityError",
valid=False,
expected_exception=ACTCapabilityError,
compact=compact_b8,
))
# --- B.9: DAG cycle (par references own jti) → ACTDAGError ---
cycle_jti = str(uuid.uuid4())
cycle_record = ACTRecord(
alg="EdDSA", kid="sub-key",
iss="agent-issuer", sub="agent-subject",
aud="agent-subject",
iat=base_time, exp=base_time + 900,
jti=cycle_jti,
task=TaskClaim(purpose="cycle_test"),
cap=[Capability(action="read.data")],
exec_act="read.data",
par=[cycle_jti],
exec_ts=base_time + 100, status="completed",
)
sig_b9 = crypto_sign(sub_priv, cycle_record.signing_input())
compact_b9 = encode_jws(cycle_record, sig_b9)
vectors.append(TestVector(
id="B.9",
description="DAG cycle (par references own jti) → ACTDAGError",
valid=False,
expected_exception=ACTDAGError,
compact=compact_b9,
))
# --- B.10: Missing parent jti in DAG → ACTDAGError ---
missing_parent_record = ACTRecord(
alg="EdDSA", kid="sub-key",
iss="agent-issuer", sub="agent-subject",
aud="agent-subject",
iat=base_time, exp=base_time + 900,
jti=str(uuid.uuid4()),
task=TaskClaim(purpose="missing_parent"),
cap=[Capability(action="read.data")],
exec_act="read.data",
par=["nonexistent-parent-jti"],
exec_ts=base_time + 100, status="completed",
)
sig_b10 = crypto_sign(sub_priv, missing_parent_record.signing_input())
compact_b10 = encode_jws(missing_parent_record, sig_b10)
vectors.append(TestVector(
id="B.10",
description="Missing parent jti in DAG → ACTDAGError",
valid=False,
expected_exception=ACTDAGError,
compact=compact_b10,
))
# --- B.11: Tampered payload (bit flip) → ACTSignatureError ---
# Take a valid compact and flip a byte in the payload
parts = compact_b1.split(".")
payload_bytes = bytearray(parts[1].encode("ascii"))
# Flip a character in the payload
flip_idx = len(payload_bytes) // 2
payload_bytes[flip_idx] = (payload_bytes[flip_idx] + 1) % 128
if payload_bytes[flip_idx] == 0:
payload_bytes[flip_idx] = 65 # 'A'
tampered_compact = f"{parts[0]}.{payload_bytes.decode('ascii')}.{parts[2]}"
vectors.append(TestVector(
id="B.11",
description="Tampered payload (bit flip in claims) → ACTSignatureError",
valid=False,
expected_exception=ACTSignatureError,
compact=tampered_compact,
))
# --- B.12: Expired token → ACTExpiredError ---
expired_mandate = ACTMandate(
alg="EdDSA", kid="iss-key",
iss="agent-issuer", sub="agent-subject",
aud="agent-subject",
iat=base_time - 3600,
exp=base_time - 2700, # expired 45 minutes ago
jti=str(uuid.uuid4()),
task=TaskClaim(purpose="expired_test"),
cap=[Capability(action="read.data")],
)
sig_b12 = crypto_sign(iss_priv, expired_mandate.signing_input())
compact_b12 = encode_jws(expired_mandate, sig_b12)
vectors.append(TestVector(
id="B.12",
description="Expired token → ACTExpiredError",
valid=False,
expected_exception=ACTExpiredError,
compact=compact_b12,
))
# --- B.13: Wrong audience → ACTAudienceMismatchError ---
wrong_aud_mandate = ACTMandate(
alg="EdDSA", kid="iss-key",
iss="agent-issuer", sub="wrong-agent",
aud="wrong-agent",
iat=base_time, exp=base_time + 900,
jti=str(uuid.uuid4()),
task=TaskClaim(purpose="wrong_aud_test"),
cap=[Capability(action="read.data")],
)
sig_b13 = crypto_sign(iss_priv, wrong_aud_mandate.signing_input())
compact_b13 = encode_jws(wrong_aud_mandate, sig_b13)
vectors.append(TestVector(
id="B.13",
description="Wrong audience → ACTAudienceMismatchError",
valid=False,
expected_exception=ACTAudienceMismatchError,
compact=compact_b13,
))
# --- B.14: Phase 2 re-signed by iss key instead of sub → ACTSignatureError ---
record_b14 = ACTRecord.from_mandate(
mandate_b1,
kid="sub-key", # claims to be sub's key
exec_act="read.data",
par=[], exec_ts=base_time + 300, status="completed",
)
# But signed with ISS's private key (wrong signer)
sig_b14 = crypto_sign(iss_priv, record_b14.signing_input())
compact_b14 = encode_jws(record_b14, sig_b14)
vectors.append(TestVector(
id="B.14",
description="Phase 2 re-signed by iss key instead of sub → ACTSignatureError",
valid=False,
expected_exception=ACTSignatureError,
compact=compact_b14,
))
# --- B.15: Algorithm "none" → ACTValidationError ---
# Manually construct a JWS with alg: none
import json
import base64
none_header = base64.urlsafe_b64encode(
json.dumps({"alg": "none", "typ": "act+jwt", "kid": "k"}, separators=(",", ":")).encode()
).rstrip(b"=").decode()
none_payload = base64.urlsafe_b64encode(
json.dumps({"iss": "a", "sub": "b"}, separators=(",", ":")).encode()
).rstrip(b"=").decode()
compact_b15 = f"{none_header}.{none_payload}."
vectors.append(TestVector(
id="B.15",
description='Algorithm "none" → ACTValidationError',
valid=False,
expected_exception=ACTValidationError,
compact=compact_b15,
))
context = {
"iss_priv": iss_priv,
"iss_pub": iss_pub,
"sub_priv": sub_priv,
"sub_pub": sub_pub,
"agent_c_priv": agent_c_priv,
"agent_c_pub": agent_c_pub,
"registry": registry,
"resolver": resolver,
"base_time": base_time,
"compacts": compacts,
"parent1_record": parent1_record,
"parent2_record": parent2_record,
"mandate_b1": mandate_b1,
}
return vectors, context
def validate_vectors() -> bool:
"""Run all test vectors and validate results.
Returns True if all vectors pass.
Reference: ACT Appendix B.
"""
vectors, ctx = generate_vectors()
resolver = ctx["resolver"]
base_time = ctx["base_time"]
verifier = ACTVerifier(
key_resolver=resolver,
verifier_id="agent-subject",
trusted_issuers={"agent-issuer"},
)
passed = 0
failed = 0
for v in vectors:
try:
if v.id == "B.7":
# Special case: test cap escalation during delegation creation
try:
from .delegation import verify_capability_subset
verify_capability_subset(
[Capability(action="read.data", constraints={"max_records": 10})],
[Capability(action="read.data", constraints={"max_records": 100})],
)
print(f" FAIL {v.id}: Expected {v.expected_exception.__name__}")
failed += 1
except ACTPrivilegeEscalationError:
print(f" PASS {v.id}: {v.description}")
passed += 1
continue
if v.valid:
# Valid vectors: should parse and verify without error
header, claims, sig, si = decode_jws(v.compact)
kid = header["kid"]
pub = resolver.resolve(kid, header=header)
crypto_verify(pub, sig, si)
print(f" PASS {v.id}: {v.description}")
passed += 1
else:
# Invalid vectors: should raise the expected exception
try:
if v.expected_exception == ACTDelegationError:
header, claims, sig, si = decode_jws(v.compact)
kid = header["kid"]
pub = resolver.resolve(kid, header=header)
crypto_verify(pub, sig, si)
# Parse and check delegation
from .token import ACTMandate as _M
m = _M.from_claims(header, claims)
from .delegation import verify_delegation_chain
verify_delegation_chain(m, lambda d: resolver.resolve(d))
print(f" FAIL {v.id}: Expected {v.expected_exception.__name__}")
failed += 1
elif v.expected_exception == ACTCapabilityError:
header, claims, sig, si = decode_jws(v.compact)
kid = header["kid"]
pub = resolver.resolve(kid, header=header)
crypto_verify(pub, sig, si)
r = ACTRecord.from_claims(header, claims)
cap_actions = {c.action for c in r.cap}
if r.exec_act not in cap_actions:
raise ACTCapabilityError("exec_act mismatch")
print(f" FAIL {v.id}: Expected {v.expected_exception.__name__}")
failed += 1
elif v.expected_exception == ACTDAGError:
header, claims, sig, si = decode_jws(v.compact)
kid = header["kid"]
pub = resolver.resolve(kid, header=header)
crypto_verify(pub, sig, si)
r = ACTRecord.from_claims(header, claims)
ledger = ACTLedger()
validate_dag(r, ledger)
print(f" FAIL {v.id}: Expected {v.expected_exception.__name__}")
failed += 1
elif v.expected_exception == ACTExpiredError:
verifier.verify_mandate(v.compact, check_sub=False)
print(f" FAIL {v.id}: Expected {v.expected_exception.__name__}")
failed += 1
elif v.expected_exception == ACTAudienceMismatchError:
verifier.verify_mandate(
v.compact,
now=base_time + 100,
check_sub=False,
)
print(f" FAIL {v.id}: Expected {v.expected_exception.__name__}")
failed += 1
elif v.expected_exception == ACTSignatureError:
header, claims, sig, si = decode_jws(v.compact)
kid = header["kid"]
pub = resolver.resolve(kid, header=header)
crypto_verify(pub, sig, si)
print(f" FAIL {v.id}: Expected {v.expected_exception.__name__}")
failed += 1
elif v.expected_exception == ACTValidationError:
decode_jws(v.compact)
print(f" FAIL {v.id}: Expected {v.expected_exception.__name__}")
failed += 1
else:
print(f" SKIP {v.id}: Unknown expected exception type")
failed += 1
except Exception as e:
if isinstance(e, v.expected_exception):
print(f" PASS {v.id}: {v.description}")
passed += 1
else:
print(f" FAIL {v.id}: Expected {v.expected_exception.__name__}, "
f"got {type(e).__name__}: {e}")
failed += 1
except Exception as e:
print(f" FAIL {v.id}: Unexpected error: {type(e).__name__}: {e}")
failed += 1
print(f"\nResults: {passed} passed, {failed} failed out of {len(vectors)}")
return failed == 0

323
workspace/act/act/verify.py Normal file
View File

@@ -0,0 +1,323 @@
"""ACT unified verification entry point.
Provides ACTVerifier with verify_mandate (Phase 1) and verify_record
(Phase 2) methods implementing the full verification procedures.
Reference: ACT §8 (Verification Procedure).
"""
from __future__ import annotations
import logging
import time
from typing import Any
from .crypto import ACTKeyResolver, PublicKey, verify as crypto_verify
from .dag import ACTStore, validate_dag
from .delegation import verify_delegation_chain
from .errors import (
ACTAudienceMismatchError,
ACTCapabilityError,
ACTExpiredError,
ACTPhaseError,
ACTSignatureError,
ACTValidationError,
)
from .token import (
ACTMandate,
ACTRecord,
decode_jws,
)
logger = logging.getLogger(__name__)
# Default clock skew tolerance for exp check — ACT §8.1 step 6.
DEFAULT_EXP_CLOCK_SKEW: int = 300 # 5 minutes
# Default clock skew tolerance for iat future check — ACT §8.1 step 7.
DEFAULT_IAT_FUTURE_TOLERANCE: int = 30 # 30 seconds
class ACTVerifier:
"""Unified ACT verification entry point.
Implements the full verification procedure for both Phase 1
(Authorization Mandate) and Phase 2 (Execution Record) tokens.
Reference: ACT §8.
"""
def __init__(
self,
key_resolver: ACTKeyResolver,
*,
verifier_id: str | None = None,
trusted_issuers: set[str] | None = None,
exp_clock_skew: int = DEFAULT_EXP_CLOCK_SKEW,
iat_future_tolerance: int = DEFAULT_IAT_FUTURE_TOLERANCE,
resolve_parent_compact: Any | None = None,
) -> None:
"""Initialize the verifier.
Args:
key_resolver: Key resolver for all trust tiers.
verifier_id: This verifier's own identifier (for aud/sub checks).
trusted_issuers: Set of trusted issuer identifiers.
If None, iss check is skipped.
exp_clock_skew: Maximum clock skew for expiration (seconds).
iat_future_tolerance: Maximum future iat tolerance (seconds).
resolve_parent_compact: Callback to resolve parent ACT compact
form by jti (for delegation chain).
"""
self._key_resolver = key_resolver
self._verifier_id = verifier_id
self._trusted_issuers = trusted_issuers
self._exp_clock_skew = exp_clock_skew
self._iat_future_tolerance = iat_future_tolerance
self._resolve_parent_compact = resolve_parent_compact
def verify_mandate(
self,
compact: str,
*,
now: int | None = None,
check_aud: bool = True,
check_sub: bool = True,
) -> ACTMandate:
"""Verify a Phase 1 Authorization Mandate.
Implements ACT §8.1 verification steps 1-13.
Args:
compact: JWS Compact Serialization of the Phase 1 ACT.
now: Current time override (for testing). Defaults to time.time().
check_aud: Whether to check aud contains verifier_id.
check_sub: Whether to check sub matches verifier_id.
Returns:
Verified ACTMandate.
Raises:
ACTValidationError: Malformed token (steps 2-3, 11).
ACTSignatureError: Signature failure (step 5).
ACTExpiredError: Token expired (step 6).
ACTAudienceMismatchError: Wrong audience (step 8).
ACTDelegationError: Invalid delegation chain (step 12).
"""
current_time = now if now is not None else int(time.time())
# Step 1: Parse JWS Compact Serialization
header, claims, signature, signing_input = decode_jws(compact)
# Steps 2-3: typ and alg checked by decode_jws
# Phase check: must NOT have exec_act
if "exec_act" in claims:
raise ACTPhaseError(
"Token contains exec_act — this is a Phase 2 token, "
"not a Phase 1 mandate"
)
# Step 4: Resolve public key for kid
kid = header["kid"]
public_key = self._key_resolver.resolve(kid, header=header)
# Step 5: Verify JWS signature
crypto_verify(public_key, signature, signing_input)
# Build mandate object for claim validation
mandate = ACTMandate.from_claims(header, claims)
# Step 6: Check exp not passed
if current_time > mandate.exp + self._exp_clock_skew:
raise ACTExpiredError(
f"Token expired: exp={mandate.exp}, "
f"now={current_time}, skew={self._exp_clock_skew}"
)
# Step 7: Check iat not unreasonably future
if mandate.iat > current_time + self._iat_future_tolerance:
raise ACTValidationError(
f"Token iat is too far in the future: iat={mandate.iat}, "
f"now={current_time}, tolerance={self._iat_future_tolerance}"
)
# Step 8: Check aud contains verifier's identity
if check_aud and self._verifier_id is not None:
aud = mandate.aud
if isinstance(aud, str):
aud_list = [aud]
else:
aud_list = aud
if self._verifier_id not in aud_list:
raise ACTAudienceMismatchError(
f"Verifier id {self._verifier_id!r} not in aud: {aud_list}"
)
# Step 9: Check iss is trusted
if self._trusted_issuers is not None:
if mandate.iss not in self._trusted_issuers:
raise ACTValidationError(
f"Issuer {mandate.iss!r} is not trusted"
)
# Step 10: Check sub matches verifier's identity
if check_sub and self._verifier_id is not None:
if mandate.sub != self._verifier_id:
raise ACTValidationError(
f"sub {mandate.sub!r} does not match verifier id "
f"{self._verifier_id!r}"
)
# Step 11: Check all required claims (done by from_claims + validate)
mandate.validate()
# Step 12: Verify delegation chain
if mandate.delegation is not None and mandate.delegation.chain:
def _resolve_key(delegator_id: str) -> PublicKey:
return self._key_resolver.resolve(delegator_id)
verify_delegation_chain(
mandate,
resolve_key=_resolve_key,
resolve_parent_compact=self._resolve_parent_compact,
)
return mandate
def verify_record(
self,
compact: str,
store: ACTStore | None = None,
*,
now: int | None = None,
check_aud: bool = True,
) -> ACTRecord:
"""Verify a Phase 2 Execution Record.
Implements all Phase 1 steps (§8.1) plus Phase 2 steps (§8.2).
Args:
compact: JWS Compact Serialization of the Phase 2 ACT.
store: ACT store for DAG validation. If None, DAG checks
are limited to capability consistency only.
now: Current time override (for testing).
check_aud: Whether to check aud contains verifier_id.
Returns:
Verified ACTRecord.
Raises:
ACTValidationError: Malformed token.
ACTSignatureError: Signature failure or wrong signer.
ACTExpiredError: Token expired.
ACTAudienceMismatchError: Wrong audience.
ACTCapabilityError: exec_act not in cap.
ACTDAGError: DAG validation failure.
"""
current_time = now if now is not None else int(time.time())
# Step 1: Parse JWS
header, claims, signature, signing_input = decode_jws(compact)
# Phase check
if "exec_act" not in claims:
raise ACTPhaseError(
"Token does not contain exec_act — this is a Phase 1 "
"mandate, not a Phase 2 record"
)
# Step 4: Resolve key — in Phase 2, kid MUST be sub's key
kid = header["kid"]
public_key = self._key_resolver.resolve(kid, header=header)
# Step 5: Verify JWS signature (Step 17: by sub's key)
crypto_verify(public_key, signature, signing_input)
# Build record
record = ACTRecord.from_claims(header, claims)
# Step 6: Check exp
if current_time > record.exp + self._exp_clock_skew:
raise ACTExpiredError(
f"Token expired: exp={record.exp}, "
f"now={current_time}, skew={self._exp_clock_skew}"
)
# Step 7: iat future check
if record.iat > current_time + self._iat_future_tolerance:
raise ACTValidationError(
f"Token iat is too far in the future: iat={record.iat}"
)
# Step 8: aud check
if check_aud and self._verifier_id is not None:
aud = record.aud
if isinstance(aud, str):
aud_list = [aud]
else:
aud_list = aud
if self._verifier_id not in aud_list:
raise ACTAudienceMismatchError(
f"Verifier id {self._verifier_id!r} not in aud: {aud_list}"
)
# Step 9: iss trust check
if self._trusted_issuers is not None:
if record.iss not in self._trusted_issuers:
raise ACTValidationError(
f"Issuer {record.iss!r} is not trusted"
)
# Step 11: required claims validation
record.validate()
# Step 12: delegation chain
if record.delegation is not None and record.delegation.chain:
def _resolve_key(delegator_id: str) -> PublicKey:
return self._key_resolver.resolve(delegator_id)
# Reuse verify_delegation_chain with ACTRecord fields
# (it accesses .delegation which exists on ACTRecord too)
from .delegation import verify_delegation_chain as _vdc
# Create a temporary mandate-like view — delegation chain
# verification only needs delegation and cap fields
mandate_view = ACTMandate(
alg=record.alg, kid=record.kid,
iss=record.iss, sub=record.sub, aud=record.aud,
iat=record.iat, exp=record.exp, jti=record.jti,
task=record.task, cap=record.cap,
delegation=record.delegation,
)
_vdc(
mandate_view,
resolve_key=_resolve_key,
resolve_parent_compact=self._resolve_parent_compact,
)
# Phase 2 step 13: exec_act matches cap[].action
cap_actions = {c.action for c in record.cap}
if record.exec_act not in cap_actions:
raise ACTCapabilityError(
f"exec_act {record.exec_act!r} does not match any "
f"cap[].action: {sorted(cap_actions)}"
)
# Phase 2 step 14: DAG validation
if store is not None:
validate_dag(record, store)
# Phase 2 step 15: exec_ts checks
if record.exec_ts < record.iat:
raise ACTValidationError(
f"exec_ts {record.exec_ts} is before iat {record.iat}"
)
if record.exec_ts > record.exp:
logger.warning(
"exec_ts %d is after exp %d — execution after mandate expiry",
record.exec_ts, record.exp,
)
# Phase 2 step 16: status validation (done by record.validate())
return record

View File

@@ -0,0 +1,174 @@
"""ACT performance benchmarks.
Measures Phase 1 creation time (construct + sign + encode) against
the 500µs target from the specification.
"""
import time
import uuid
import statistics
from act import (
ACTMandate,
ACTRecord,
Capability,
TaskClaim,
encode_jws,
decode_jws,
generate_ed25519_keypair,
generate_p256_keypair,
sign,
verify,
transition_to_record,
)
def bench_phase1_ed25519(n: int = 10000) -> None:
"""Benchmark Phase 1 creation with Ed25519."""
priv, pub = generate_ed25519_keypair()
# Warmup
for _ in range(100):
m = ACTMandate(
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
iat=1772064000, exp=1772064900, jti=str(uuid.uuid4()),
task=TaskClaim(purpose="t"), cap=[Capability(action="x.y")],
)
sig = sign(priv, m.signing_input())
encode_jws(m, sig)
times = []
for _ in range(n):
start = time.perf_counter()
m = ACTMandate(
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
iat=1772064000, exp=1772064900, jti=str(uuid.uuid4()),
task=TaskClaim(purpose="benchmark"),
cap=[Capability(action="read.data")],
)
sig = sign(priv, m.signing_input())
encode_jws(m, sig)
elapsed = time.perf_counter() - start
times.append(elapsed * 1_000_000) # µs
mean = statistics.mean(times)
median = statistics.median(times)
p99 = sorted(times)[int(n * 0.99)]
print(f"Phase 1 Ed25519 (n={n}):")
print(f" Mean: {mean:.1f} µs")
print(f" Median: {median:.1f} µs")
print(f" P99: {p99:.1f} µs")
print(f" Target: <= 500 µs {'PASS' if mean <= 500 else 'FAIL'}")
print()
def bench_phase1_p256(n: int = 5000) -> None:
"""Benchmark Phase 1 creation with P-256."""
priv, pub = generate_p256_keypair()
for _ in range(50):
m = ACTMandate(
alg="ES256", kid="k", iss="a", sub="b", aud="b",
iat=1772064000, exp=1772064900, jti=str(uuid.uuid4()),
task=TaskClaim(purpose="t"), cap=[Capability(action="x.y")],
)
sig = sign(priv, m.signing_input())
encode_jws(m, sig)
times = []
for _ in range(n):
start = time.perf_counter()
m = ACTMandate(
alg="ES256", kid="k", iss="a", sub="b", aud="b",
iat=1772064000, exp=1772064900, jti=str(uuid.uuid4()),
task=TaskClaim(purpose="benchmark"),
cap=[Capability(action="read.data")],
)
sig = sign(priv, m.signing_input())
encode_jws(m, sig)
elapsed = time.perf_counter() - start
times.append(elapsed * 1_000_000)
mean = statistics.mean(times)
median = statistics.median(times)
p99 = sorted(times)[int(n * 0.99)]
print(f"Phase 1 ES256 (n={n}):")
print(f" Mean: {mean:.1f} µs")
print(f" Median: {median:.1f} µs")
print(f" P99: {p99:.1f} µs")
print()
def bench_phase2_transition(n: int = 5000) -> None:
"""Benchmark Phase 1 -> Phase 2 transition."""
iss_priv, _ = generate_ed25519_keypair()
sub_priv, _ = generate_ed25519_keypair()
mandate = ACTMandate(
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
iat=1772064000, exp=1772064900, jti=str(uuid.uuid4()),
task=TaskClaim(purpose="t"), cap=[Capability(action="x.y")],
)
# Warmup
for _ in range(50):
transition_to_record(
mandate, sub_kid="sk", sub_private_key=sub_priv,
exec_act="x.y", par=[], status="completed",
)
times = []
for _ in range(n):
start = time.perf_counter()
transition_to_record(
mandate, sub_kid="sk", sub_private_key=sub_priv,
exec_act="x.y", par=[], status="completed",
)
elapsed = time.perf_counter() - start
times.append(elapsed * 1_000_000)
mean = statistics.mean(times)
median = statistics.median(times)
print(f"Phase 2 Transition (n={n}):")
print(f" Mean: {mean:.1f} µs")
print(f" Median: {median:.1f} µs")
print()
def bench_verify(n: int = 5000) -> None:
"""Benchmark JWS decode + verify."""
priv, pub = generate_ed25519_keypair()
m = ACTMandate(
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
iat=1772064000, exp=1772064900, jti=str(uuid.uuid4()),
task=TaskClaim(purpose="t"), cap=[Capability(action="x.y")],
)
sig = sign(priv, m.signing_input())
compact = encode_jws(m, sig)
# Warmup
for _ in range(50):
_, _, s, si = decode_jws(compact)
verify(pub, s, si)
times = []
for _ in range(n):
start = time.perf_counter()
_, _, s, si = decode_jws(compact)
verify(pub, s, si)
elapsed = time.perf_counter() - start
times.append(elapsed * 1_000_000)
mean = statistics.mean(times)
median = statistics.median(times)
print(f"Decode + Verify (n={n}):")
print(f" Mean: {mean:.1f} µs")
print(f" Median: {median:.1f} µs")
print()
if __name__ == "__main__":
bench_phase1_ed25519()
bench_phase1_p256()
bench_phase2_transition()
bench_verify()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,145 @@
"""Tests for act.crypto module."""
import pytest
from act.crypto import (
ACTKeyResolver,
KeyRegistry,
X509TrustStore,
b64url_sha256,
compute_sha256,
did_key_from_ed25519,
generate_ed25519_keypair,
generate_p256_keypair,
resolve_did_key,
sign,
verify,
)
from act.errors import ACTKeyResolutionError, ACTSignatureError
class TestEd25519:
def test_generate_keypair(self):
priv, pub = generate_ed25519_keypair()
assert priv is not None
assert pub is not None
def test_sign_verify(self):
priv, pub = generate_ed25519_keypair()
data = b"test data"
sig = sign(priv, data)
verify(pub, sig, data)
def test_verify_wrong_data(self):
priv, pub = generate_ed25519_keypair()
sig = sign(priv, b"correct data")
with pytest.raises(ACTSignatureError):
verify(pub, sig, b"wrong data")
def test_verify_wrong_key(self):
priv1, pub1 = generate_ed25519_keypair()
_, pub2 = generate_ed25519_keypair()
sig = sign(priv1, b"data")
with pytest.raises(ACTSignatureError):
verify(pub2, sig, b"data")
class TestP256:
def test_generate_keypair(self):
priv, pub = generate_p256_keypair()
assert priv is not None
assert pub is not None
def test_sign_verify(self):
priv, pub = generate_p256_keypair()
data = b"test data for p256"
sig = sign(priv, data)
assert len(sig) == 64 # r||s, 32 bytes each
verify(pub, sig, data)
def test_verify_wrong_data(self):
priv, pub = generate_p256_keypair()
sig = sign(priv, b"correct")
with pytest.raises(ACTSignatureError):
verify(pub, sig, b"wrong")
class TestSHA256:
def test_compute(self):
h = compute_sha256(b"hello")
assert len(h) == 32
def test_b64url(self):
result = b64url_sha256(b"hello world")
assert "=" not in result
assert isinstance(result, str)
class TestKeyRegistry:
def test_register_and_get(self):
reg = KeyRegistry()
_, pub = generate_ed25519_keypair()
reg.register("key-1", pub)
assert reg.get("key-1") is pub
assert "key-1" in reg
assert len(reg) == 1
def test_missing_key(self):
reg = KeyRegistry()
assert reg.get("missing") is None
assert "missing" not in reg
class TestDIDKey:
def test_ed25519_roundtrip(self):
_, pub = generate_ed25519_keypair()
did = did_key_from_ed25519(pub)
assert did.startswith("did:key:z6Mk")
resolved = resolve_did_key(did)
# Verify same key by signing/verifying
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
original_bytes = pub.public_bytes(Encoding.Raw, PublicFormat.Raw)
resolved_bytes = resolved.public_bytes(Encoding.Raw, PublicFormat.Raw)
assert original_bytes == resolved_bytes
def test_invalid_prefix(self):
with pytest.raises(ACTKeyResolutionError):
resolve_did_key("did:web:example.com")
def test_with_fragment(self):
_, pub = generate_ed25519_keypair()
did = did_key_from_ed25519(pub)
did_with_fragment = f"{did}#{did.split(':')[2]}"
resolved = resolve_did_key(did_with_fragment)
assert resolved is not None
class TestACTKeyResolver:
def test_tier1_resolution(self):
reg = KeyRegistry()
_, pub = generate_ed25519_keypair()
reg.register("my-key", pub)
resolver = ACTKeyResolver(registry=reg)
assert resolver.resolve("my-key") is pub
def test_tier3_did_key(self):
_, pub = generate_ed25519_keypair()
did = did_key_from_ed25519(pub)
resolver = ACTKeyResolver()
resolved = resolver.resolve(did)
assert resolved is not None
def test_unresolvable(self):
resolver = ACTKeyResolver()
with pytest.raises(ACTKeyResolutionError):
resolver.resolve("unknown-kid")
def test_did_web_resolver_callback(self):
_, pub = generate_ed25519_keypair()
def resolver_cb(did: str):
if did == "did:web:example.com":
return pub
return None
resolver = ACTKeyResolver(did_web_resolver=resolver_cb)
result = resolver.resolve("did:web:example.com")
assert result is pub

View File

@@ -0,0 +1,103 @@
"""Tests for act.dag module."""
import time
import pytest
from act.dag import validate_dag
from act.errors import ACTCapabilityError, ACTDAGError
from act.ledger import ACTLedger
from act.token import ACTRecord, Capability, TaskClaim
def make_record(jti, par=None, exec_act="do.thing", exec_ts=None, cap=None):
"""Helper to create a minimal ACTRecord."""
return ACTRecord(
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
iat=1772064000, exp=1772064900,
jti=jti,
task=TaskClaim(purpose="t"),
cap=cap or [Capability(action="do.thing")],
exec_act=exec_act,
par=par or [],
exec_ts=exec_ts or 1772064100,
status="completed",
)
class TestDAGValidation:
def test_root_task(self):
ledger = ACTLedger()
r = make_record("root-1")
validate_dag(r, ledger)
def test_child_with_parent(self):
ledger = ACTLedger()
parent = make_record("parent-1", exec_ts=1772064050)
ledger.append(parent)
child = make_record("child-1", par=["parent-1"], exec_ts=1772064100)
validate_dag(child, ledger)
def test_fan_in(self):
ledger = ACTLedger()
p1 = make_record("p1", exec_ts=1772064050)
p2 = make_record("p2", exec_ts=1772064060)
ledger.append(p1)
ledger.append(p2)
child = make_record("child", par=["p1", "p2"], exec_ts=1772064100)
validate_dag(child, ledger)
def test_duplicate_jti(self):
ledger = ACTLedger()
r = make_record("dup-1")
ledger.append(r)
r2 = make_record("dup-1")
with pytest.raises(ACTDAGError, match="Duplicate"):
validate_dag(r2, ledger)
def test_missing_parent(self):
ledger = ACTLedger()
r = make_record("orphan", par=["nonexistent"])
with pytest.raises(ACTDAGError, match="not found"):
validate_dag(r, ledger)
def test_self_cycle(self):
ledger = ACTLedger()
r = make_record("cycle", par=["cycle"])
with pytest.raises(ACTDAGError, match="cycle"):
validate_dag(r, ledger)
def test_indirect_cycle(self):
ledger = ACTLedger()
# a -> b -> a would be a cycle
a = make_record("a", par=["b"], exec_ts=1772064100)
b = make_record("b", par=["a"], exec_ts=1772064100)
ledger.append(b)
# When validating a, following par leads to b,
# which has par=["a"] — cycle!
with pytest.raises(ACTDAGError, match="cycle"):
validate_dag(a, ledger)
def test_temporal_ordering_violation(self):
ledger = ACTLedger()
parent = make_record("parent", exec_ts=1772064200)
ledger.append(parent)
# Child's exec_ts is way before parent
child = make_record("child", par=["parent"], exec_ts=1772064100)
with pytest.raises(ACTDAGError, match="Temporal"):
validate_dag(child, ledger)
def test_temporal_within_tolerance(self):
ledger = ACTLedger()
parent = make_record("parent", exec_ts=1772064120)
ledger.append(parent)
# Child exec_ts is slightly before parent but within 30s tolerance
child = make_record("child", par=["parent"], exec_ts=1772064100)
validate_dag(child, ledger)
def test_bad_exec_act(self):
ledger = ACTLedger()
r = make_record("bad", exec_act="not.authorized",
cap=[Capability(action="do.thing")])
with pytest.raises(ACTCapabilityError):
validate_dag(r, ledger)

View File

@@ -0,0 +1,229 @@
"""Tests for act.delegation module."""
import time
import uuid
import pytest
from act.crypto import generate_ed25519_keypair, sign, verify, compute_sha256
from act.delegation import (
create_delegated_mandate,
verify_capability_subset,
verify_delegation_chain,
)
from act.errors import (
ACTDelegationError,
ACTPrivilegeEscalationError,
)
from act.token import (
ACTMandate,
Capability,
Delegation,
DelegationEntry,
TaskClaim,
_b64url_decode,
encode_jws,
)
@pytest.fixture
def parent_setup():
iss_priv, iss_pub = generate_ed25519_keypair()
mandate = ACTMandate(
alg="EdDSA", kid="iss-key",
iss="agent-a", sub="agent-b", aud="agent-b",
iat=1772064000, exp=1772064900,
jti="parent-jti-1",
task=TaskClaim(purpose="parent_task"),
cap=[
Capability(action="read.data", constraints={"max_records": 10}),
Capability(action="write.result"),
],
delegation=Delegation(depth=0, max_depth=3, chain=[]),
)
sig = sign(iss_priv, mandate.signing_input())
compact = encode_jws(mandate, sig)
return mandate, compact, iss_priv, iss_pub
class TestCreateDelegatedMandate:
def test_basic_delegation(self, parent_setup):
mandate, compact, priv, _ = parent_setup
delegated, _ = create_delegated_mandate(
parent_mandate=mandate, parent_compact=compact,
delegator_private_key=priv,
sub="agent-c", kid="key-b", iss="agent-a", aud="agent-c",
iat=1772064010, exp=1772064600,
jti="child-jti-1",
cap=[Capability(action="read.data", constraints={"max_records": 5})],
task=TaskClaim(purpose="child_task"),
)
assert delegated.delegation.depth == 1
assert len(delegated.delegation.chain) == 1
assert delegated.delegation.chain[0].delegator == "agent-a"
def test_depth_exceeded(self, parent_setup):
mandate, compact, priv, _ = parent_setup
# Set parent to max depth
mandate.delegation = Delegation(depth=3, max_depth=3, chain=[
DelegationEntry(delegator="x", jti="j", sig="s")
for _ in range(3)
])
with pytest.raises(ACTDelegationError, match="exceeds max_depth"):
create_delegated_mandate(
parent_mandate=mandate, parent_compact=compact,
delegator_private_key=priv,
sub="c", kid="k", iss="a", aud="c",
iat=1, exp=2, jti="j",
cap=[Capability(action="read.data", constraints={"max_records": 5})],
task=TaskClaim(purpose="t"),
)
def test_no_del_claim(self):
priv, _ = generate_ed25519_keypair()
mandate = ACTMandate(
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
iat=1, exp=2,
task=TaskClaim(purpose="t"),
cap=[Capability(action="x.y")],
delegation=None, # no del claim
)
with pytest.raises(ACTDelegationError, match="not permitted"):
create_delegated_mandate(
parent_mandate=mandate, parent_compact="compact",
delegator_private_key=priv,
sub="c", kid="k", iss="a", aud="c",
iat=1, exp=2, jti="j",
cap=[Capability(action="x.y")],
task=TaskClaim(purpose="t"),
)
def test_max_depth_reduction(self, parent_setup):
mandate, compact, priv, _ = parent_setup
delegated, _ = create_delegated_mandate(
parent_mandate=mandate, parent_compact=compact,
delegator_private_key=priv,
sub="c", kid="k", iss="a", aud="c",
iat=1, exp=2, jti="j",
cap=[Capability(action="read.data", constraints={"max_records": 5})],
task=TaskClaim(purpose="t"),
max_depth=2,
)
assert delegated.delegation.max_depth == 2
def test_max_depth_escalation(self, parent_setup):
mandate, compact, priv, _ = parent_setup
with pytest.raises(ACTDelegationError, match="exceeds parent max_depth"):
create_delegated_mandate(
parent_mandate=mandate, parent_compact=compact,
delegator_private_key=priv,
sub="c", kid="k", iss="a", aud="c",
iat=1, exp=2, jti="j",
cap=[Capability(action="read.data", constraints={"max_records": 5})],
task=TaskClaim(purpose="t"),
max_depth=10,
)
class TestCapabilitySubset:
def test_valid_subset(self):
parent = [Capability(action="read.data", constraints={"max_records": 10})]
child = [Capability(action="read.data", constraints={"max_records": 5})]
verify_capability_subset(parent, child)
def test_extra_action(self):
parent = [Capability(action="read.data")]
child = [Capability(action="delete.data")]
with pytest.raises(ACTPrivilegeEscalationError):
verify_capability_subset(parent, child)
def test_numeric_escalation(self):
parent = [Capability(action="read.data", constraints={"max_records": 10})]
child = [Capability(action="read.data", constraints={"max_records": 100})]
with pytest.raises(ACTPrivilegeEscalationError):
verify_capability_subset(parent, child)
def test_sensitivity_escalation(self):
parent = [Capability(action="read.data",
constraints={"data_sensitivity": "confidential"})]
child = [Capability(action="read.data",
constraints={"data_sensitivity": "internal"})]
with pytest.raises(ACTPrivilegeEscalationError):
verify_capability_subset(parent, child)
def test_sensitivity_more_restrictive(self):
parent = [Capability(action="read.data",
constraints={"data_sensitivity": "internal"})]
child = [Capability(action="read.data",
constraints={"data_sensitivity": "restricted"})]
verify_capability_subset(parent, child) # should pass
def test_missing_constraint(self):
parent = [Capability(action="read.data",
constraints={"max_records": 10, "scope": "local"})]
child = [Capability(action="read.data",
constraints={"max_records": 5})]
with pytest.raises(ACTPrivilegeEscalationError, match="missing"):
verify_capability_subset(parent, child)
def test_domain_specific_identical(self):
parent = [Capability(action="read.data",
constraints={"custom": "value_a"})]
child = [Capability(action="read.data",
constraints={"custom": "value_a"})]
verify_capability_subset(parent, child)
def test_domain_specific_different(self):
parent = [Capability(action="read.data",
constraints={"custom": "value_a"})]
child = [Capability(action="read.data",
constraints={"custom": "value_b"})]
with pytest.raises(ACTPrivilegeEscalationError, match="identical"):
verify_capability_subset(parent, child)
class TestVerifyDelegationChain:
def test_chain_sig_verification(self, parent_setup):
mandate, compact, priv, pub = parent_setup
delegated, _ = create_delegated_mandate(
parent_mandate=mandate, parent_compact=compact,
delegator_private_key=priv,
sub="c", kid="k", iss="agent-a", aud="c",
iat=1, exp=2, jti="j",
cap=[Capability(action="read.data", constraints={"max_records": 5})],
task=TaskClaim(purpose="t"),
)
# Verify the chain
def resolve_key(delegator_id):
return pub
def resolve_compact(jti):
if jti == "parent-jti-1":
return compact
return None
verify_delegation_chain(delegated, resolve_key, resolve_compact)
def test_no_delegation(self):
mandate = ACTMandate(
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
iat=1, exp=2,
task=TaskClaim(purpose="t"),
cap=[Capability(action="x.y")],
)
verify_delegation_chain(mandate, lambda x: None) # no-op
def test_depth_exceeds_max(self):
mandate = ACTMandate(
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
iat=1, exp=2,
task=TaskClaim(purpose="t"),
cap=[Capability(action="x.y")],
delegation=Delegation(depth=5, max_depth=3, chain=[
DelegationEntry(delegator="x", jti="j", sig="s")
for _ in range(5)
]),
)
with pytest.raises(ACTDelegationError, match="exceeds"):
verify_delegation_chain(mandate, lambda x: None)

View File

@@ -0,0 +1,84 @@
"""Tests for act.ledger module."""
import pytest
from act.errors import ACTLedgerImmutabilityError
from act.ledger import ACTLedger
from act.token import ACTRecord, Capability, TaskClaim
def make_record(jti, wid=None):
return ACTRecord(
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
iat=1772064000, exp=1772064900,
jti=jti, wid=wid,
task=TaskClaim(purpose="t"),
cap=[Capability(action="do.thing")],
exec_act="do.thing", par=[], exec_ts=1772064100,
status="completed",
)
class TestACTLedger:
def test_append_and_get(self):
ledger = ACTLedger()
r = make_record("jti-1")
seq = ledger.append(r)
assert seq == 0
assert ledger.get("jti-1") is r
def test_sequential_ordering(self):
ledger = ACTLedger()
for i in range(5):
seq = ledger.append(make_record(f"jti-{i}"))
assert seq == i
def test_duplicate_rejected(self):
ledger = ACTLedger()
ledger.append(make_record("jti-1"))
with pytest.raises(ACTLedgerImmutabilityError):
ledger.append(make_record("jti-1"))
def test_get_missing(self):
ledger = ACTLedger()
assert ledger.get("missing") is None
def test_list_all(self):
ledger = ACTLedger()
ledger.append(make_record("a"))
ledger.append(make_record("b"))
records = ledger.list()
assert len(records) == 2
def test_list_by_wid(self):
ledger = ACTLedger()
ledger.append(make_record("a", wid="w1"))
ledger.append(make_record("b", wid="w2"))
ledger.append(make_record("c", wid="w1"))
assert len(ledger.list("w1")) == 2
assert len(ledger.list("w2")) == 1
assert len(ledger.list("w3")) == 0
def test_verify_integrity_empty(self):
ledger = ACTLedger()
assert ledger.verify_integrity() is True
def test_verify_integrity_with_records(self):
ledger = ACTLedger()
for i in range(10):
ledger.append(make_record(f"jti-{i}"))
assert ledger.verify_integrity() is True
def test_verify_integrity_tampered(self):
ledger = ACTLedger()
ledger.append(make_record("jti-1"))
ledger.append(make_record("jti-2"))
# Tamper with chain hash
ledger._chain_hashes[0] = b"\x00" * 32
assert ledger.verify_integrity() is False
def test_len(self):
ledger = ACTLedger()
assert len(ledger) == 0
ledger.append(make_record("a"))
assert len(ledger) == 1

View File

@@ -0,0 +1,103 @@
"""Tests for act.lifecycle module."""
import time
import uuid
import pytest
from act.crypto import generate_ed25519_keypair, sign
from act.errors import ACTCapabilityError, ACTPhaseError
from act.lifecycle import transition_to_record
from act.token import (
ACTMandate,
ACTRecord,
Capability,
Delegation,
ErrorClaim,
TaskClaim,
decode_jws,
encode_jws,
)
from act.crypto import verify
@pytest.fixture
def keys():
iss_priv, iss_pub = generate_ed25519_keypair()
sub_priv, sub_pub = generate_ed25519_keypair()
return iss_priv, iss_pub, sub_priv, sub_pub
@pytest.fixture
def mandate(keys):
iss_priv, _, _, _ = keys
m = ACTMandate(
alg="EdDSA", kid="iss-key",
iss="agent-a", sub="agent-b", aud="agent-b",
iat=1772064000, exp=1772064900,
jti=str(uuid.uuid4()),
task=TaskClaim(purpose="test"),
cap=[Capability(action="read.data"), Capability(action="write.result")],
delegation=Delegation(depth=0, max_depth=2, chain=[]),
)
return m
class TestTransitionToRecord:
def test_basic_transition(self, mandate, keys):
_, _, sub_priv, sub_pub = keys
record, compact = transition_to_record(
mandate, sub_kid="sub-key", sub_private_key=sub_priv,
exec_act="read.data", par=[], status="completed",
)
assert isinstance(record, ACTRecord)
assert record.exec_act == "read.data"
assert record.kid == "sub-key"
assert record.iss == mandate.iss # preserved
# Verify signature
_, _, sig, si = decode_jws(compact)
verify(sub_pub, sig, si)
def test_with_hashes(self, mandate, keys):
_, _, sub_priv, _ = keys
record, _ = transition_to_record(
mandate, sub_kid="k", sub_private_key=sub_priv,
exec_act="write.result", par=[], status="completed",
inp_hash="abc", out_hash="def",
)
assert record.inp_hash == "abc"
assert record.out_hash == "def"
def test_with_error(self, mandate, keys):
_, _, sub_priv, _ = keys
record, _ = transition_to_record(
mandate, sub_kid="k", sub_private_key=sub_priv,
exec_act="read.data", par=[], status="failed",
err=ErrorClaim(code="timeout", detail="request timed out"),
)
assert record.status == "failed"
assert record.err is not None
assert record.err.code == "timeout"
def test_rejects_bad_exec_act(self, mandate, keys):
_, _, sub_priv, _ = keys
with pytest.raises(ACTCapabilityError):
transition_to_record(
mandate, sub_kid="k", sub_private_key=sub_priv,
exec_act="delete.everything", par=[],
)
def test_preserves_phase1_claims(self, mandate, keys):
_, _, sub_priv, _ = keys
record, _ = transition_to_record(
mandate, sub_kid="k", sub_private_key=sub_priv,
exec_act="read.data", par=[], status="completed",
)
assert record.iss == mandate.iss
assert record.sub == mandate.sub
assert record.aud == mandate.aud
assert record.iat == mandate.iat
assert record.exp == mandate.exp
assert record.jti == mandate.jti
assert record.task == mandate.task
assert record.cap == mandate.cap

View File

@@ -0,0 +1,244 @@
"""Tests for act.token module."""
import json
import time
import uuid
import pytest
from act.token import (
ACTMandate,
ACTRecord,
Capability,
Delegation,
DelegationEntry,
ErrorClaim,
Oversight,
TaskClaim,
_b64url_decode,
_b64url_encode,
decode_jws,
encode_jws,
parse_token,
validate_action_name,
)
from act.errors import ACTPhaseError, ACTValidationError
@pytest.fixture
def base_time():
return 1772064000
@pytest.fixture
def mandate(base_time):
return ACTMandate(
alg="EdDSA",
kid="test-key",
iss="agent-a",
sub="agent-b",
aud="agent-b",
iat=base_time,
exp=base_time + 900,
jti=str(uuid.uuid4()),
task=TaskClaim(purpose="test_task"),
cap=[Capability(action="read.data")],
)
class TestBase64url:
def test_roundtrip(self):
data = b"hello world"
assert _b64url_decode(_b64url_encode(data)) == data
def test_no_padding(self):
encoded = _b64url_encode(b"test")
assert "=" not in encoded
class TestActionNameValidation:
def test_valid_simple(self):
validate_action_name("read")
def test_valid_dotted(self):
validate_action_name("read.data")
def test_valid_with_hyphens(self):
validate_action_name("read-write.data_item")
def test_invalid_starts_with_digit(self):
with pytest.raises(ACTValidationError):
validate_action_name("1read")
def test_invalid_empty(self):
with pytest.raises(ACTValidationError):
validate_action_name("")
def test_invalid_double_dot(self):
with pytest.raises(ACTValidationError):
validate_action_name("read..data")
class TestTaskClaim:
def test_roundtrip(self):
t = TaskClaim(purpose="test", data_sensitivity="restricted")
d = t.to_dict()
t2 = TaskClaim.from_dict(d)
assert t == t2
def test_missing_purpose(self):
with pytest.raises(ACTValidationError):
TaskClaim.from_dict({})
class TestCapability:
def test_roundtrip(self):
c = Capability(action="read.data", constraints={"max": 10})
d = c.to_dict()
c2 = Capability.from_dict(d)
assert c == c2
def test_validates_action(self):
with pytest.raises(ACTValidationError):
Capability(action="")
class TestDelegation:
def test_roundtrip(self):
d = Delegation(
depth=1,
max_depth=3,
chain=[DelegationEntry(delegator="a", jti="j1", sig="sig1")],
)
as_dict = d.to_dict()
d2 = Delegation.from_dict(as_dict)
assert d.depth == d2.depth
assert len(d2.chain) == 1
class TestACTMandate:
def test_validate_success(self, mandate):
mandate.validate()
def test_validate_missing_iss(self, base_time):
m = ACTMandate(
alg="EdDSA", kid="k", iss="", sub="b", aud="b",
iat=base_time, exp=base_time + 900,
task=TaskClaim(purpose="t"), cap=[Capability(action="x.y")],
)
with pytest.raises(ACTValidationError, match="iss"):
m.validate()
def test_validate_forbidden_alg(self, base_time):
m = ACTMandate(
alg="HS256", kid="k", iss="a", sub="b", aud="b",
iat=base_time, exp=base_time + 900,
task=TaskClaim(purpose="t"), cap=[Capability(action="x.y")],
)
with pytest.raises(ACTValidationError):
m.validate()
def test_validate_alg_none(self, base_time):
m = ACTMandate(
alg="none", kid="k", iss="a", sub="b", aud="b",
iat=base_time, exp=base_time + 900,
task=TaskClaim(purpose="t"), cap=[Capability(action="x.y")],
)
with pytest.raises(ACTValidationError):
m.validate()
def test_to_claims_includes_optional(self, base_time):
m = ACTMandate(
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
iat=base_time, exp=base_time + 900,
task=TaskClaim(purpose="t"), cap=[Capability(action="x.y")],
wid="w-1",
oversight=Oversight(requires_approval_for=["x.y"]),
)
claims = m.to_claims()
assert claims["wid"] == "w-1"
assert "oversight" in claims
def test_is_phase2(self, mandate):
assert mandate.is_phase2() is False
def test_from_claims_rejects_phase2(self):
with pytest.raises(ACTPhaseError):
ACTMandate.from_claims(
{"alg": "EdDSA", "typ": "act+jwt", "kid": "k"},
{"exec_act": "x", "iss": "a", "sub": "b", "aud": "b",
"iat": 1, "exp": 2, "jti": "j",
"task": {"purpose": "t"}, "cap": [{"action": "x"}]},
)
class TestACTRecord:
def test_from_mandate(self, mandate):
r = ACTRecord.from_mandate(
mandate, kid="sub-key", exec_act="read.data",
par=[], status="completed",
)
assert r.iss == mandate.iss
assert r.exec_act == "read.data"
assert r.kid == "sub-key"
def test_validate_bad_status(self, mandate):
r = ACTRecord.from_mandate(
mandate, kid="k", exec_act="read.data",
par=[], exec_ts=mandate.iat + 100, status="invalid",
)
with pytest.raises(ACTValidationError, match="status"):
r.validate()
def test_is_phase2(self, mandate):
r = ACTRecord.from_mandate(
mandate, kid="k", exec_act="read.data",
par=[], status="completed",
)
assert r.is_phase2() is True
def test_from_claims_rejects_phase1(self):
with pytest.raises(ACTPhaseError):
ACTRecord.from_claims(
{"alg": "EdDSA", "typ": "act+jwt", "kid": "k"},
{"iss": "a", "sub": "b", "aud": "b",
"iat": 1, "exp": 2, "jti": "j",
"task": {"purpose": "t"}, "cap": [{"action": "x"}]},
)
class TestJWSSerialization:
def test_decode_invalid_parts(self):
with pytest.raises(ACTValidationError):
decode_jws("only.two")
def test_decode_invalid_header(self):
with pytest.raises(ACTValidationError):
decode_jws("!!!.cGF5bG9hZA.c2ln")
def test_decode_wrong_typ(self):
header = _b64url_encode(json.dumps({"alg": "EdDSA", "typ": "jwt", "kid": "k"}).encode())
payload = _b64url_encode(json.dumps({"iss": "a"}).encode())
sig = _b64url_encode(b"sig")
with pytest.raises(ACTValidationError, match="typ"):
decode_jws(f"{header}.{payload}.{sig}")
def test_parse_token_phase1(self, mandate):
from act.crypto import generate_ed25519_keypair, sign
priv, pub = generate_ed25519_keypair()
sig = sign(priv, mandate.signing_input())
compact = encode_jws(mandate, sig)
parsed = parse_token(compact)
assert isinstance(parsed, ACTMandate)
def test_parse_token_phase2(self, mandate):
from act.crypto import generate_ed25519_keypair, sign
priv, pub = generate_ed25519_keypair()
record = ACTRecord.from_mandate(
mandate, kid="k", exec_act="read.data",
par=[], status="completed",
)
sig = sign(priv, record.signing_input())
compact = encode_jws(record, sig)
parsed = parse_token(compact)
assert isinstance(parsed, ACTRecord)

View File

@@ -0,0 +1,35 @@
"""Tests for act.vectors module — Appendix B test vectors."""
import pytest
from act.vectors import generate_vectors, validate_vectors
class TestVectorGeneration:
def test_generates_15_vectors(self):
vectors, ctx = generate_vectors()
assert len(vectors) == 15
def test_vector_ids(self):
vectors, _ = generate_vectors()
ids = [v.id for v in vectors]
expected = [f"B.{i}" for i in range(1, 16)]
assert ids == expected
def test_valid_vectors_have_compact(self):
vectors, _ = generate_vectors()
for v in vectors:
if v.valid and v.id != "B.7":
assert v.compact, f"{v.id} should have compact"
def test_invalid_vectors_have_exception(self):
vectors, _ = generate_vectors()
for v in vectors:
if not v.valid:
assert v.expected_exception is not None, \
f"{v.id} should have expected_exception"
class TestVectorValidation:
def test_all_vectors_pass(self):
assert validate_vectors() is True

View File

@@ -0,0 +1,191 @@
"""Tests for act.verify module."""
import time
import uuid
import pytest
from act.crypto import (
ACTKeyResolver,
KeyRegistry,
generate_ed25519_keypair,
sign,
)
from act.errors import (
ACTAudienceMismatchError,
ACTCapabilityError,
ACTExpiredError,
ACTPhaseError,
ACTSignatureError,
ACTValidationError,
)
from act.ledger import ACTLedger
from act.lifecycle import transition_to_record
from act.token import (
ACTMandate,
ACTRecord,
Capability,
Delegation,
TaskClaim,
encode_jws,
)
from act.verify import ACTVerifier
@pytest.fixture
def setup():
iss_priv, iss_pub = generate_ed25519_keypair()
sub_priv, sub_pub = generate_ed25519_keypair()
registry = KeyRegistry()
registry.register("iss-key", iss_pub)
registry.register("sub-key", sub_pub)
resolver = ACTKeyResolver(registry=registry)
base_time = 1772064000
return {
"iss_priv": iss_priv, "iss_pub": iss_pub,
"sub_priv": sub_priv, "sub_pub": sub_pub,
"registry": registry, "resolver": resolver,
"base_time": base_time,
}
def make_mandate(setup, **overrides):
bt = setup["base_time"]
defaults = dict(
alg="EdDSA", kid="iss-key",
iss="agent-issuer", sub="agent-subject",
aud="agent-subject",
iat=bt, exp=bt + 900,
jti=str(uuid.uuid4()),
task=TaskClaim(purpose="test"),
cap=[Capability(action="read.data")],
)
defaults.update(overrides)
return ACTMandate(**defaults)
def sign_mandate(mandate, priv_key):
sig = sign(priv_key, mandate.signing_input())
return encode_jws(mandate, sig)
class TestVerifyMandate:
def test_valid_mandate(self, setup):
verifier = ACTVerifier(
setup["resolver"],
verifier_id="agent-subject",
trusted_issuers={"agent-issuer"},
)
mandate = make_mandate(setup)
compact = sign_mandate(mandate, setup["iss_priv"])
result = verifier.verify_mandate(compact, now=setup["base_time"] + 100)
assert result.iss == "agent-issuer"
def test_expired(self, setup):
verifier = ACTVerifier(setup["resolver"], verifier_id="agent-subject")
mandate = make_mandate(setup)
compact = sign_mandate(mandate, setup["iss_priv"])
with pytest.raises(ACTExpiredError):
verifier.verify_mandate(compact, now=setup["base_time"] + 2000)
def test_wrong_audience(self, setup):
verifier = ACTVerifier(
setup["resolver"], verifier_id="other-agent",
trusted_issuers={"agent-issuer"},
)
mandate = make_mandate(setup)
compact = sign_mandate(mandate, setup["iss_priv"])
with pytest.raises(ACTAudienceMismatchError):
verifier.verify_mandate(
compact, now=setup["base_time"] + 100, check_sub=False,
)
def test_untrusted_issuer(self, setup):
verifier = ACTVerifier(
setup["resolver"], verifier_id="agent-subject",
trusted_issuers={"trusted-only"},
)
mandate = make_mandate(setup)
compact = sign_mandate(mandate, setup["iss_priv"])
with pytest.raises(ACTValidationError, match="not trusted"):
verifier.verify_mandate(compact, now=setup["base_time"] + 100)
def test_signature_failure(self, setup):
verifier = ACTVerifier(setup["resolver"], verifier_id="agent-subject")
mandate = make_mandate(setup)
compact = sign_mandate(mandate, setup["iss_priv"])
# Tamper with signature
parts = compact.split(".")
parts[2] = parts[2][:-4] + "XXXX"
tampered = ".".join(parts)
with pytest.raises(ACTSignatureError):
verifier.verify_mandate(tampered, now=setup["base_time"] + 100)
def test_phase2_as_mandate(self, setup):
verifier = ACTVerifier(setup["resolver"])
mandate = make_mandate(setup)
record, compact = transition_to_record(
mandate, sub_kid="sub-key", sub_private_key=setup["sub_priv"],
exec_act="read.data", par=[], status="completed",
exec_ts=setup["base_time"] + 100,
)
with pytest.raises(ACTPhaseError):
verifier.verify_mandate(compact, now=setup["base_time"] + 100)
def test_future_iat(self, setup):
verifier = ACTVerifier(setup["resolver"], verifier_id="agent-subject")
bt = setup["base_time"]
mandate = make_mandate(setup, iat=bt + 1000, exp=bt + 2000)
compact = sign_mandate(mandate, setup["iss_priv"])
with pytest.raises(ACTValidationError, match="future"):
verifier.verify_mandate(compact, now=bt)
class TestVerifyRecord:
def test_valid_record(self, setup):
verifier = ACTVerifier(
setup["resolver"],
verifier_id="agent-subject",
trusted_issuers={"agent-issuer"},
)
mandate = make_mandate(setup)
record, compact = transition_to_record(
mandate, sub_kid="sub-key", sub_private_key=setup["sub_priv"],
exec_act="read.data", par=[],
exec_ts=setup["base_time"] + 100, status="completed",
)
result = verifier.verify_record(
compact, now=setup["base_time"] + 200, check_aud=False,
)
assert result.exec_act == "read.data"
def test_wrong_signer(self, setup):
verifier = ACTVerifier(setup["resolver"])
mandate = make_mandate(setup)
record = ACTRecord.from_mandate(
mandate, kid="sub-key", exec_act="read.data",
par=[], exec_ts=setup["base_time"] + 100, status="completed",
)
# Sign with iss key instead of sub key
sig = sign(setup["iss_priv"], record.signing_input())
compact = encode_jws(record, sig)
with pytest.raises(ACTSignatureError):
verifier.verify_record(compact, now=setup["base_time"] + 200)
def test_with_dag_validation(self, setup):
verifier = ACTVerifier(
setup["resolver"], verifier_id="agent-subject",
trusted_issuers={"agent-issuer"},
)
ledger = ACTLedger()
mandate = make_mandate(setup)
record, compact = transition_to_record(
mandate, sub_kid="sub-key", sub_private_key=setup["sub_priv"],
exec_act="read.data", par=[],
exec_ts=setup["base_time"] + 100, status="completed",
)
result = verifier.verify_record(
compact, store=ledger,
now=setup["base_time"] + 200, check_aud=False,
)
assert result.status == "completed"

View File

@@ -0,0 +1,59 @@
Follow this workflow for all work in this repository.
## Goal
Produce publication-ready IETF draft packages from existing `ietf-draft-analyzer` data with minimal token use and strong role separation.
## Roles
- `researcher`: synthesize current evidence, identify missing evidence, and propose follow-up investigation
- `architect`: convert research into a precise spec strategy and section plan
- `author`: write the draft from the approved architecture
- `security-reviewer`: find protocol, trust, abuse, privacy, and threat-model flaws
- `software-reviewer`: find implementability, state-machine, testing, and operational gaps
- `architecture-reviewer`: find scope drift, internal inconsistency, and design weakness
- `ietf-senior-reviewer`: find IETF process, document-shape, terminology, and publishability issues
- `review-lead`: synthesize specialist reviews into one prioritized revision plan
## Token Discipline
- Read the current cycle files first, not the whole repository.
- Prefer `references/analyzer-integration.md` to rediscovering source locations.
- Load only the specific analyzer outputs needed for the current question.
- Keep handoff files short, factual, and structured.
- Reuse filenames and templates; avoid free-form notes outside the cycle folder.
## Cycle Files
Each cycle lives in `cycles/<slug>/` and uses these files:
- `00-user-spec.md`: user intent, constraints, success criteria
- `10-research-brief.md`: evidence summary, gaps, new data to fetch
- `20-architecture-brief.md`: scope, design, requirements, risks, outline
- `30-outline.md`: draft outline and section-level writing guidance
- `40-draft-v1.md`: first full draft
- `50-reviews-v1/`: specialist review folder
- `55-review-synthesis-v1.md`: merged findings and priority order
- `60-revision-plan-v1.md`: concrete changes for next draft
Continue with `v2`, `v3`, and so on.
## Operating Rules
- Do not skip the architecture step before drafting.
- Do not let the author invent core requirements that are absent from the research or architecture brief.
- Do not let specialist reviewers rewrite the whole draft when targeted changes are sufficient.
- Escalate contradictions between user specs, research evidence, and draft text.
- Track assumptions explicitly.
- Treat Security Considerations, Privacy Considerations, and IANA Considerations as first-class work items.
- Prefer parallel specialist review after each draft, then one synthesis pass.
## Done Criteria
A draft is ready for user sign-off only when:
- the architecture brief and the draft agree on scope
- major claims are backed by cited evidence or marked as hypotheses
- open issues are either resolved or explicitly listed
- specialist review findings are addressed or consciously deferred
- publishability risks are called out plainly

View File

@@ -0,0 +1,29 @@
SHELL := /bin/bash
SLUG ?=
VERSION ?= 1
ROLE ?=
.PHONY: help prepare targets status
help:
@echo "Usage:"
@echo " make prepare SLUG=<cycle-slug> VERSION=<n>"
@echo " make targets SLUG=<cycle-slug> ROLE=<role> VERSION=<n>"
@echo " make status SLUG=<cycle-slug> VERSION=<n>"
@echo ""
@echo "Roles:"
@echo " researcher architect author security-reviewer software-reviewer architecture-reviewer ietf-senior-reviewer review-lead"
prepare:
@if [[ -z "$(SLUG)" ]]; then echo "SLUG is required"; exit 1; fi
@./scripts/run-cycle.sh "$(SLUG)" "$(VERSION)"
targets:
@if [[ -z "$(SLUG)" ]]; then echo "SLUG is required"; exit 1; fi
@if [[ -z "$(ROLE)" ]]; then echo "ROLE is required"; exit 1; fi
@./scripts/role-target.sh "$(SLUG)" "$(ROLE)" "$(VERSION)"
status:
@if [[ -z "$(SLUG)" ]]; then echo "SLUG is required"; exit 1; fi
@./scripts/update-status.sh "$(SLUG)" "$(VERSION)"

View File

@@ -0,0 +1,95 @@
# IETF Draft Team
Lean multi-agent workspace for taking existing `ietf-draft-analyzer` outputs through:
1. research
2. architecture
3. drafting
4. specialist reviews
5. review synthesis
6. revision loops
This project is optimized for Codex-style execution with low token usage:
- Stable role rules live in `AGENTS.md` files instead of being repeated in prompts.
- Each cycle stores short handoff artifacts with fixed filenames.
- Every role reads only the current cycle files plus the smallest relevant source set.
- Research starts from your existing analyzer outputs before proposing new investigation.
## Default source repo
This scaffold assumes your analysis data lives in:
`/home/c/projects/ietf-draft-analyzer`
See `references/analyzer-integration.md`.
## Structure
- `AGENTS.md`: common operating rules
- `researcher/AGENTS.md`: evidence and gap analysis role
- `architect/AGENTS.md`: spec alignment and design role
- `author/AGENTS.md`: draft writing role
- `security-reviewer/AGENTS.md`: security, privacy, trust, abuse review
- `software-reviewer/AGENTS.md`: implementability and operational review
- `architecture-reviewer/AGENTS.md`: coherence and scope review
- `ietf-senior-reviewer/AGENTS.md`: IETF-style and publishability review
- `review-lead/AGENTS.md`: synthesis and revision planning role
- `templates/`: handoff templates
- `cycles/`: one folder per draft effort
- `scripts/new-cycle.sh`: initialize a new cycle
- `scripts/run-cycle.sh`: prepare a versioned run and print the execution order
- `scripts/role-target.sh`: print the target file path for a specific role
- `scripts/update-status.sh`: generate a lightweight status dashboard for a cycle/version
- `Makefile`: thin wrappers around the helper scripts
## Basic use
Create a cycle:
```bash
./scripts/new-cycle.sh dynamic-trust
```
Prepare or inspect a cycle run:
```bash
./scripts/run-cycle.sh dynamic-trust 1
```
Get the output path for a role:
```bash
./scripts/role-target.sh dynamic-trust architect 1
```
Or use `make`:
```bash
make prepare SLUG=dynamic-trust VERSION=1
make targets SLUG=dynamic-trust ROLE=author VERSION=1
make status SLUG=dynamic-trust VERSION=1
```
Then work in order against `cycles/<slug>/`:
1. Fill `00-user-spec.md`
2. Run the researcher on that cycle
3. Run the architect on the same cycle
4. Run the author on the same cycle
5. Run the specialist reviewers on the same draft version
6. Run the review lead to produce `55-review-synthesis-vN.md` and `60-revision-plan-vN.md`
7. Iterate
The user remains the final approver before publication.
## Review board
Each draft version should usually get these focused review files:
- `50-reviews-vN/security.md`
- `50-reviews-vN/software.md`
- `50-reviews-vN/architecture.md`
- `50-reviews-vN/ietf-senior.md`
This is cheaper than one broad reviewer because each role reads the same compact inputs but only reasons about one concern area.

View File

@@ -0,0 +1,43 @@
Act as the architect.
## Objective
Turn the user spec and research brief into a spec strategy that is coherent, scoped, and aligned with IETF conventions.
## Inputs
- current cycle `00-user-spec.md`
- current cycle `10-research-brief.md`
- narrow source checks from the analyzer repo only if the brief is ambiguous
## Output
Write both:
- `20-architecture-brief.md`
- `30-outline.md`
## `20-architecture-brief.md` must cover
1. Scope
2. Non-goals
3. Terminology and actors
4. Protocol or data model shape
5. Normative requirements candidates
6. Security, privacy, and abuse considerations
7. IANA impact
8. Open design questions
## `30-outline.md` must cover
- section list
- purpose of each section
- what evidence or requirements must appear there
- which issues the author must not hand-wave
## Rules
- Resolve ambiguity early and explicitly.
- Remove ideas that are interesting but out of scope.
- Favor the smallest spec that closes the chosen gap.
- Flag where experimental status is more honest than standards track.

View File

@@ -0,0 +1,30 @@
Act as the architecture reviewer.
## Objective
Find design incoherence, scope drift, unsupported requirements, and weak decomposition.
## Inputs
- current cycle `00-user-spec.md`
- current cycle `20-architecture-brief.md`
- latest `40-draft-vN.md`
## Output
Write `50-reviews-vN/architecture.md`.
## Review Areas
- mismatch between architecture brief and draft
- scope creep or hidden non-goals
- inconsistent terminology or actor model
- requirements that do not follow from the stated problem
- overdesign relative to the chosen status
## Rules
- Be strict about conceptual coherence.
- Prefer removing material over adding cleverness.
- Flag where the draft should be split into multiple documents.
- Check that the draft status matches the scope: experimental when the mechanism is exploratory, standards track only when interoperability requirements are mature.

View File

@@ -0,0 +1,30 @@
Act as the author.
## Objective
Write an Internet-Draft from the approved architecture without widening scope or inventing unsupported design choices.
## Inputs
- current cycle `00-user-spec.md`
- current cycle `20-architecture-brief.md`
- current cycle `30-outline.md`
Read `10-research-brief.md` only when a claim or citation target is unclear.
## Output
Write `40-draft-vN.md` for the current iteration.
## Rules
- Follow the outline unless there is a clear defect in it.
- Use precise technical language and normative keywords only where justified.
- Use BCP 14 keywords only for true protocol requirements, not aspirations or rationale.
- Carry assumptions and unresolved questions into the draft instead of hiding them.
- Include Security Considerations, Privacy Considerations, and IANA Considerations even when the result is "none" or "minimal".
- Keep citations and external references as placeholders when exact references are not yet fixed.
- Do not rewrite the architecture in prose before getting to the draft sections.
- Keep the document in Internet-Draft shape: abstract, terminology, protocol behavior, considerations, and references.
- Prefer small, testable protocol rules over broad framework language.
- Avoid product language, marketing claims, roadmap text, and unverifiable comparisons.

View File

@@ -0,0 +1,70 @@
# User Spec
## Topic
Agent Error Recovery and Rollback for Multi-Agent Systems
## Goal
Produce a credible IETF-style Internet-Draft for a narrowly scoped mechanism that standardizes how cooperating agents report failures, define rollback scope, and execute coordinated recovery without cascading damage.
## Intended status
Experimental.
Rationale: the problem is clearly real and under-specified, but the ecosystem is still young and the mechanism should not pretend to have full deployment consensus yet.
## Problem to solve
Current AI-agent and autonomous-operations drafts define communication, identity, and orchestration patterns, but the landscape analysis shows no common mechanism for:
- signaling execution failure in a machine-actionable way
- declaring rollback boundaries and blast radius
- coordinating rollback across dependent agents
- recording recovery outcomes for audit and future trust decisions
This creates high interoperability and safety risk for autonomous systems that act across multiple services or domains.
## What must be true in the final draft
- The draft stays tightly scoped to recovery and rollback semantics, not a full agent architecture.
- The mechanism is protocol-agnostic enough to work across multiple agent ecosystems.
- The draft defines concrete states, triggers, and recovery procedures that two implementers could follow consistently.
- Security Considerations meaningfully address spoofed rollback, unauthorized override, replay, and denial-of-service by false failure signaling.
- The text is shaped like a real Internet-Draft, not a product design memo.
- The draft clearly states what is in scope now and what is deferred to later work such as richer workflow orchestration or dynamic trust scoring.
## Constraints
- scope constraints
Keep this to rollback and recovery coordination. Do not absorb lifecycle management, full workflow DAG standardization, or human override into the core mechanism except where needed as interfaces.
- compatibility constraints
Reuse adjacent concepts where possible from existing IETF-style work on execution evidence, attestation, or agent communication. Do not invent a full new identity or transport stack.
- terminology constraints
Use conservative standards language. Prefer terms like agent, execution, checkpoint, rollback set, dependency, and recovery record. Avoid buzzwords and branding.
## Source materials to prioritize
- `/home/c/projects/ietf-draft-analyzer/data/reports/gaps.md`
- `/home/c/projects/ietf-draft-analyzer/data/reports/holistic-agent-ecosystem-draft-outlines.md`
- `/home/c/projects/ietf-draft-analyzer/data/reports/ideas.md`
- `/home/c/projects/ietf-draft-analyzer/data/reports/overview.md`
- `draft-yue-anima-agent-recovery-networks`
- `draft-li-dmsc-macp`
- `draft-fu-nmop-agent-communication-framework`
- `draft-srijal-agents-policy`
- related WIMSE or ECT materials when they help avoid redefining execution evidence
## Success criteria
- A reader can tell exactly what an agent must emit or process when a task fails.
- A reader can tell how rollback scope is determined and how dependent agents respond.
- The draft includes enough structure to support interoperability testing later.
- Specialist reviewers can criticize the draft on substance rather than on missing basic sections or obvious ambiguity.
## Questions for the team
- What is the smallest interoperable core for rollback semantics?
- Should checkpoints and recovery records be abstract objects, protocol messages, or profileable metadata on top of another carrier?
- What information is mandatory in a failure signal versus optional?
- How should rollback interact with partially completed downstream work?

View File

@@ -0,0 +1,27 @@
# Cycle Status
## Summary
- cycle: agent-error-recovery-rollback
- version: v1
- last updated: 2026-03-02 18:00 UTC
## Artifact Status
- `00-user-spec.md`: written
- `10-research-brief.md`: written
- `20-architecture-brief.md`: written
- `30-outline.md`: written
- `40-draft-v1.md`: written
- `50-reviews-v1/security.md`: written
- `50-reviews-v1/software.md`: written
- `50-reviews-v1/architecture.md`: written
- `50-reviews-v1/ietf-senior.md`: written
- `55-review-synthesis-v1.md`: written
- `60-revision-plan-v1.md`: written
## Notes
- written means the artifact contains substantive content.
- stub means the file exists but still appears to be a placeholder.
- missing means the expected file has not been created.

View File

@@ -0,0 +1,27 @@
# Cycle Status
## Summary
- cycle: agent-error-recovery-rollback
- version: v2
- last updated: 2026-03-02 18:06 UTC
## Artifact Status
- `00-user-spec.md`: written
- `10-research-brief.md`: written
- `20-architecture-brief.md`: written
- `30-outline.md`: written
- `40-draft-v2.md`: written
- `50-reviews-v2/security.md`: stub
- `50-reviews-v2/software.md`: stub
- `50-reviews-v2/architecture.md`: stub
- `50-reviews-v2/ietf-senior.md`: stub
- `55-review-synthesis-v2.md`: stub
- `60-revision-plan-v2.md`: stub
## Notes
- written means the artifact contains substantive content.
- stub means the file exists but still appears to be a placeholder.
- missing means the expected file has not been created.

View File

@@ -0,0 +1,60 @@
# Research Brief
## Problem framing
Fact: the analyzer identifies Agent Error Recovery and Rollback as a critical gap in the current IETF AI/agent landscape, especially within autonomous netops. Fact: the gap statement is specific: current drafts discuss communication and coordination, but do not define a common mechanism for machine-actionable failure signaling, rollback boundaries, or coordinated recovery across dependent agents.
Inference: this is a good first draft topic because it is narrower and more defensible than a full agent orchestration architecture, while still addressing a real interoperability and safety problem. Hypothesis: the best initial document is an experimental protocol or profile for failure, checkpoint, rollback-request, and rollback-result semantics, not a complete workflow language.
## Evidence from existing drafts
Fact: the gap report cites only six extracted ideas that partially touch this area. The strongest adjacent ideas are "Task-Oriented Multi-Agent Recovery Framework", "Inter-Agent Communication Protocol Requirements", and "State Consistency Management" from `draft-yue-anima-agent-recovery-networks`, plus "Mandatory restrictive failure behavior" from `draft-srijal-agents-policy`.
Fact: adjacent drafts in the space include `draft-li-dmsc-macp`, `draft-fu-nmop-agent-communication-framework`, `draft-mallick-muacp`, and `draft-zyyhl-agent-networks-framework`. These appear to focus on collaboration or communication frameworks, not interoperable rollback semantics.
Fact: the landscape overview shows high activity and overlap in adjacent categories, but not maturity on recovery. `draft-li-dmsc-macp` scores well overall, while `draft-fu-nmop-agent-communication-framework` is relevant but lower maturity. This suggests there is ecosystem pressure for operational coordination, yet no shared recovery core has emerged.
Fact: the ideas corpus also shows related building blocks such as agent context propagation, working memory, authorization profiles, attestation, and policy enforcement. These matter because rollback decisions depend on shared execution context and trustworthy signaling, even if the rollback draft should not standardize those mechanisms itself.
## Overlap and adjacent work
Fact: `holistic-agent-ecosystem-draft-outlines.md` already frames recovery as part of a broader family and recommends using an execution-evidence substrate such as ECT rather than inventing a second DAG or token format. That same document suggests rollback should be represented through explicit checkpoint, error, rollback-request, and rollback-result events.
Inference: the closest collision risk is not another rollback standard, but accidental overreach into three nearby topics:
- full task DAG and orchestration semantics
- human override and intervention
- dynamic trust and assurance
Inference: the architect should treat those as interfaces, not as primary scope. The rollback draft should define how recovery interacts with dependencies and checkpoints, while leaving workflow planning, trust scoring, and human escalation to companion work or future drafts.
## Gaps and unresolved questions
Fact: the current evidence does not yet establish a canonical wire format or transport for rollback signaling. Fact: the analyzer materials argue for reusing adjacent execution-evidence work, but do not prove that one specific substrate is mature enough to normatively depend on.
Open questions:
- What is the minimum mandatory information in a failure signal: task identifier, parent dependency, failure class, reversibility, checkpoint reference, and rollback scope are likely candidates, but the exact set still needs comparison against existing drafts.
- Should rollback scope be defined as explicit dependency closure, implementation-local policy, or both?
- How should partially completed downstream actions be marked when they are not cleanly reversible?
- Which failures require automatic circuit breaking versus optional operator or policy input?
- Can the draft stay protocol-agnostic while still being testable by independent implementers?
## Additional data worth investigating
- Verify whether WIMSE or ECT-related drafts already define reusable execution identifiers, parent linkage, or signed event records that would let this draft avoid inventing its own carrier.
- Inspect `draft-yue-anima-agent-recovery-networks` directly for concrete recovery states, not just its analyzer summary.
- Compare `draft-li-dmsc-macp` and `draft-fu-nmop-agent-communication-framework` for any existing error taxonomy, dependency model, or task lifecycle signaling.
- Search the ideas set for `checkpoint`, `rollback`, `error`, `failure`, `compensation`, and `circuit breaker` to see whether additional partially related mechanisms were missed by the headline gap report.
## Recommendation to the architect
Design the first draft as a narrowly scoped experimental specification for coordinated recovery semantics in multi-agent execution. Keep the document centered on:
- failure and checkpoint vocabulary
- task state transitions
- rollback request and result signaling
- dependency-aware rollback scope
- minimal security requirements for authentic and authorized recovery events
Avoid defining a new identity system, full orchestration language, human override workflow, or trust-scoring model. If a reusable execution-evidence substrate exists, bind to it; otherwise define a minimal abstract event model that can later be profiled onto specific carriers.

View File

@@ -0,0 +1,121 @@
# Architecture Brief
## Scope
Define an experimental, protocol-agnostic recovery model for multi-agent execution that standardizes:
- failure signaling
- checkpoint references
- rollback request and rollback result semantics
- dependency-aware rollback scope
- minimum task state transitions relevant to recovery
The document should be narrow enough that an existing agent protocol or execution-evidence carrier can adopt it as a profile or extension.
## Non-goals
- defining a full workflow or DAG language
- defining human override or approval workflows beyond a hook for escalation
- defining identity, authentication, or attestation systems
- defining global trust scoring or reputation exchange
- defining scheduler behavior, quota fairness, or resource arbitration beyond optional future hooks
## Terminology and actors
- `agent`: autonomous software entity performing one or more tasks
- `task`: a discrete unit of work whose execution and outcome can be referenced
- `dependency`: another task whose outcome affects whether the current task may continue or must roll back
- `checkpoint`: a recorded pre-action or recovery-safe state from which rollback may proceed
- `failure event`: a machine-actionable signal that a task or dependency failed
- `rollback set`: the set of tasks and effects that the sender requests to revert or compensate
- `recovery record`: a record of rollback attempt, success, partial success, or failure
- `coordinator`: optional role that computes rollback scope across multiple dependent agents
Actors:
- originating agent that detects failure
- dependent agent that receives failure or rollback signals
- optional coordination service or gateway
- policy authority or operator only when automatic rollback is disallowed
## Protocol or data model shape
Use an abstract event model with four core event types:
1. `checkpoint`
2. `failure`
3. `rollback-request`
4. `rollback-result`
Each event should carry a minimum common envelope:
- event identifier
- task identifier
- workflow or execution context identifier if available
- sender identity reference
- timestamp
- referenced parent task or dependency identifiers where relevant
Event-specific content:
- `checkpoint`: checkpoint identifier, reversibility class, optional expiry
- `failure`: failure class, severity, reversibility indicator, blast-radius hint, failed dependency reference
- `rollback-request`: target checkpoint or rollback boundary, requested rollback scope, reason code, urgency, idempotency token
- `rollback-result`: outcome status, actual scope applied, partial rollback indicators, residual risk or manual follow-up required
State model:
- `pending`
- `running`
- `completed`
- `failed`
- `rollback-requested`
- `rolled-back`
- `rollback-failed`
- `compensation-required`
Design choice: keep the carrier abstract in this first draft, but include a section describing how the model may bind to existing execution-evidence formats if such a substrate is available and sufficiently mature.
## Normative requirements candidates
- Agents MUST emit a failure event when a task failure can affect dependent execution outside local process scope.
- Failure events MUST identify the failed task and SHOULD identify affected dependencies when known.
- Rollback requests MUST be idempotent and uniquely identifiable.
- Agents receiving a rollback request MUST return a rollback result, even when rollback is refused or only partially completed.
- A rollback result MUST indicate one of: success, partial success, refusal, irreversible, or failure.
- Agents MUST NOT claim successful rollback unless the referenced effects were actually reverted or explicitly compensated.
- If a task is not reversible, the agent MUST signal that fact explicitly rather than silently ignoring rollback.
- Implementations SHOULD support checkpoint references when a task has externally visible side effects.
- The specification SHOULD allow policy-controlled escalation rather than requiring automatic rollback for every failure.
- The document MUST distinguish rollback of prior effects from cancellation of work that has not yet executed.
## Security, privacy, and abuse considerations
- unauthorized rollback requests could be used as denial-of-service
- spoofed failure signals could trigger cascading rollback
- replayed rollback requests could repeatedly unwind completed work
- rollback metadata may expose internal topology or sensitive task relationships
- partial rollback can create inconsistent downstream state that attackers can exploit
- signed or otherwise authenticated event carriage is strongly preferred, but the draft should avoid redefining base authentication
- the draft should require clear handling of refusal, partial rollback, and policy escalation to avoid silent unsafe states
Privacy is probably secondary but not zero: task identifiers, dependency graphs, and failure reasons can leak operational details.
## IANA impact
Most likely minimal for the first version.
If the draft defines abstract event or reason-code registries, keep them compact:
- rollback event types
- failure classes
- rollback outcome codes
If an existing registry from an underlying carrier can be reused, prefer that.
## Open design questions
- Should rollback scope be defined normatively as dependency closure, or left partially implementation-specific with mandatory disclosure of actual scope?
- Is a separate `cancellation` event needed, or is that explicitly out of scope for this draft?
- How much of checkpoint semantics should be mandatory versus profile-specific?
- Can one draft stay both carrier-agnostic and implementable, or does it need a non-normative binding example to avoid vagueness?

View File

@@ -0,0 +1,79 @@
# Draft Outline
## Abstract
State that the document defines experimental recovery semantics for multi-agent task execution, including failure signaling, rollback requests, rollback results, and checkpoint references. Make clear it is protocol-agnostic and intended to improve interoperable recovery behavior across agent ecosystems.
## Section plan
1. Introduction
2. Terminology
3. Problem Statement and Design Goals
4. Recovery Model Overview
5. Event Types and Required Fields
6. Task States and Recovery Procedures
7. Rollback Scope and Dependency Handling
8. Error Conditions and Partial Rollback
9. Security Considerations
10. Privacy Considerations
11. IANA Considerations
12. References
## Author guidance by section
### 1. Introduction
Explain why autonomous multi-agent systems need interoperable recovery behavior. Keep this grounded in failure propagation and operational safety, not generic AI rhetoric.
### 2. Terminology
Define only the core terms needed for this document: task, dependency, checkpoint, failure event, rollback set, recovery record, coordinator. Keep terms stable and conservative.
### 3. Problem Statement and Design Goals
Describe the exact gap: current drafts define communication and orchestration patterns, but no common rollback semantics. Include explicit goals such as idempotency, partial rollback transparency, and protocol-agnostic applicability.
### 4. Recovery Model Overview
Describe the model at a high level before any field-level detail. Separate local failure handling from cross-agent recovery signaling. Make clear what this document does not define.
### 5. Event Types and Required Fields
Define `checkpoint`, `failure`, `rollback-request`, and `rollback-result`. This section must specify required versus optional fields and avoid vague "metadata may include" language where interoperability depends on a field.
### 6. Task States and Recovery Procedures
Define the state transitions relevant to failure and rollback. Include procedure ordering: detect failure, emit failure event, decide rollback scope, send rollback request, emit rollback result. If escalation is possible, say when.
### 7. Rollback Scope and Dependency Handling
Define how dependencies influence rollback. Be explicit about direct versus transitive effects, what happens when scope is uncertain, and how actual applied scope is reported back.
### 8. Error Conditions and Partial Rollback
Handle non-reversible tasks, refusal, timeout, duplicate requests, and partial success. This section is important for implementability and must not collapse into generic prose.
### 9. Security Considerations
Address spoofing, replay, unauthorized rollback, false failure signaling, topology leakage, and abuse of partial rollback states. The section should be mechanism-specific.
### 10. Privacy Considerations
Address exposure of task identifiers, failure causes, dependency graphs, and sensitive operational details.
### 11. IANA Considerations
Either clearly say none, or request small registries for failure classes and rollback outcomes. Do not hand-wave this.
### 12. References
Use placeholders where necessary, but include adjacent drafts that informed the design and any underlying execution-evidence substrate if referenced.
## Issues that must not be hand-waved
- what fields are mandatory in each event
- what counts as a successful versus partial rollback
- how rollback requests remain idempotent
- what an agent does when a requested rollback is impossible
- how dependency-driven rollback scope is determined and reported
- what security properties the mechanism relies on from lower layers

View File

@@ -0,0 +1,216 @@
# Draft
## Abstract
This document defines experimental recovery semantics for multi-agent task execution. It specifies common event types for failure signaling, checkpoint reference, rollback requests, and rollback results so that cooperating agents can coordinate recovery after operational faults. The mechanism is protocol-agnostic and is intended to be profiled onto existing agent communication or execution-evidence substrates. The goal is to improve interoperability when autonomous systems must contain failures, report rollback scope, and communicate partial or unsuccessful recovery without silent divergence.
## 1. Introduction
Multi-agent systems increasingly perform coordinated work across services, tools, and administrative domains. In such systems, one task failure can invalidate downstream work, require compensating actions, or force a broader rollback of externally visible effects. Existing drafts define communication frameworks, discovery, identity, and broader orchestration concepts, but they do not define a shared recovery core that independent implementations can follow.
Absent common recovery semantics, one implementation may silently retry while another expects explicit rollback, and a third may report only local failure without describing downstream consequences. That mismatch creates interoperability risk and operational safety risk, especially when agents act without immediate human supervision.
This document defines a narrow recovery model for cross-agent failure handling. It does not define a full workflow language, a transport binding, or a human override system. Instead, it defines event semantics and minimum procedure rules so that agents can exchange recovery-relevant information consistently.
## 2. Terminology
The key words "MUST", "MUST NOT", "SHOULD", "SHOULD NOT", and "MAY" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals.
Agent: an autonomous software entity that performs one or more tasks and may exchange recovery events with peers.
Task: a discrete unit of work whose execution and outcome can be identified.
Dependency: a relationship in which one task relies on the prior completion, state, or side effects of another task.
Checkpoint: a recorded state or recovery-safe reference from which rollback can proceed.
Failure Event: a machine-actionable record that a task or dependency failed in a way that can affect other participants.
Rollback Set: the set of tasks, effects, or checkpoints that a rollback request identifies as the intended recovery scope.
Recovery Record: a record of rollback attempt, refusal, partial rollback, success, or failure.
Coordinator: an optional component that computes or distributes rollback scope across multiple agents.
Compensation: a follow-up action that mitigates an irreversible effect when direct rollback is not possible.
## 3. Problem Statement
Current agent ecosystems have uneven support for failure handling. Some drafts discuss task coordination or operational recovery, but the analyzed landscape still lacks a common method to express:
- that a task failed in a cross-agent relevant way,
- which dependencies are affected,
- which checkpoint or rollback boundary should be used, and
- whether rollback succeeded, only partially succeeded, or was impossible.
The absence of these common semantics makes independent implementation difficult. An originating agent may believe it has requested rollback, while a receiving agent may treat the same signal as informational. Similarly, partial rollback can leave downstream agents operating on inconsistent assumptions if outcome reporting is underspecified.
The design goals for this document are:
- protocol-agnostic applicability,
- minimal mandatory fields for interoperability,
- idempotent rollback requests,
- explicit reporting of partial or impossible rollback, and
- compatibility with existing lower-layer identity and integrity mechanisms.
## 4. Recovery Model Overview
This document defines four event types:
- `checkpoint`
- `failure`
- `rollback-request`
- `rollback-result`
These events MAY be carried in a message protocol, stored as execution records, or embedded in a larger workflow substrate. This document does not standardize the carrier. It standardizes the meaning of the events and the minimum information needed for interoperable recovery behavior.
Each event has a common envelope containing:
- an event identifier,
- a task identifier,
- a sender identity reference,
- a timestamp, and
- any relevant workflow or execution context identifier.
The recovery model assumes that a failure can be local or cross-agent relevant. Local failures that cannot affect any external dependency do not require signaling under this document. When a failure can affect dependent work outside local scope, the originating agent MUST emit a `failure` event.
If rollback is needed, the requester sends a `rollback-request` identifying the requested scope. The receiver returns a `rollback-result` stating whether the requested recovery succeeded, partially succeeded, was refused, was impossible, or failed.
## 5. Event Types and Required Fields
### 5.1 Checkpoint
A `checkpoint` event identifies a recovery-safe reference that later rollback may target. A checkpoint event MUST include:
- event identifier,
- task identifier,
- checkpoint identifier,
- sender identity reference,
- timestamp.
A checkpoint event SHOULD include reversibility class and MAY include checkpoint expiry or retention information.
### 5.2 Failure
A `failure` event reports a task failure that can affect dependent execution outside local process scope. A failure event MUST include:
- event identifier,
- failed task identifier,
- sender identity reference,
- timestamp,
- failure class,
- reversibility indicator.
A failure event SHOULD include affected dependency identifiers when known, and MAY include severity, blast-radius hint, or checkpoint reference.
### 5.3 Rollback Request
A `rollback-request` event asks another participant to revert or compensate previously applied effects. A rollback request MUST include:
- event identifier,
- requester identity reference,
- target task identifier or checkpoint identifier,
- requested rollback scope,
- idempotency token,
- timestamp.
A rollback request SHOULD include reason code and urgency. A rollback request MAY include dependency evidence or policy reference supporting the request.
### 5.4 Rollback Result
A `rollback-result` event reports the outcome of processing a rollback request. A rollback result MUST include:
- event identifier,
- referenced rollback-request identifier,
- responder identity reference,
- outcome code,
- timestamp,
- actual scope applied.
The outcome code MUST be one of:
- `success`
- `partial-success`
- `refused`
- `irreversible`
- `failure`
A rollback result SHOULD include residual risk description when the result is not `success`. A rollback result MAY include compensation details.
## 6. Task States and Recovery Procedures
For purposes of this document, relevant task states are:
- `pending`
- `running`
- `completed`
- `failed`
- `rollback-requested`
- `rolled-back`
- `rollback-failed`
- `compensation-required`
When an agent detects a task failure that can affect external dependents, it MUST transition the affected task to `failed` and emit a `failure` event. If policy permits automatic recovery, the originating agent or coordinator SHOULD determine the rollback set and issue one or more `rollback-request` events. If policy does not permit automatic rollback, the implementation SHOULD enter a local hold or escalation path rather than silently continuing.
An agent receiving a `rollback-request` MUST process duplicate requests idempotently. If the request can be honored, the agent applies rollback or compensation as appropriate and emits a `rollback-result`. If the request cannot be honored because the effect is irreversible or unauthorized, the agent MUST emit a `rollback-result` with the appropriate outcome code.
This document distinguishes rollback from cancellation. Cancellation of work not yet started is out of scope except where a local implementation uses cancellation internally to satisfy a rollback request.
## 7. Rollback Scope and Dependency Handling
Rollback scope is central to interoperability. A rollback request MUST identify either:
- a target checkpoint, or
- an explicit rollback set.
When transitive dependencies are known, the requester SHOULD include them or indicate that transitive evaluation is required. When dependency knowledge is incomplete, the requester MUST still identify the minimum known affected scope and the responder MUST report the actual scope applied in the rollback result.
An implementation MUST NOT report successful rollback for effects outside the applied scope. If only part of the requested rollback set is reversed, the responder MUST return `partial-success` and describe any remaining irreversible or uncompensated effects.
A coordinator MAY compute rollback scope across multiple agents, but this document does not require a coordinator role. Peers can interoperate directly as long as they provide the required event information.
## 8. Error Conditions and Partial Rollback
The following conditions require explicit handling:
- duplicate rollback requests,
- timeout while waiting for rollback completion,
- refusal due to insufficient authorization,
- irreversible effects,
- partial rollback where some effects are reversed and others remain,
- failure of the rollback procedure itself.
If a requested rollback is impossible, the responding agent MUST indicate `irreversible` or `failure` as appropriate and SHOULD indicate whether compensation is available. If a request is refused for policy reasons, the agent MUST indicate `refused` and SHOULD include a reason that is usable by the requester or an external policy authority.
Implementations SHOULD avoid silent downgrade from rollback to best-effort local cleanup. If only local cleanup occurred, the rollback result SHOULD say so clearly.
## 9. Security Considerations
Unauthorized rollback requests can be used to deny service or corrupt coordinated work. Implementations therefore need an authenticated and authorized carriage for the events defined here, even though this document does not define the underlying security protocol.
Spoofed failure events can trigger unnecessary rollback. Replay of old rollback requests can repeatedly unwind valid work. Implementations SHOULD provide replay resistance and SHOULD bind requests and results to stable task and requester identifiers.
Partial rollback is itself a security concern because it can leave downstream systems in an inconsistent state that an attacker can exploit. For that reason, responders MUST explicitly report residual scope and any remaining irreversible effects.
Failure and rollback metadata can also reveal topology, task dependencies, and operational weaknesses. Deployments SHOULD minimize unnecessary disclosure and SHOULD apply least-privilege access to recovery records.
## 10. Privacy Considerations
Task identifiers, failure classes, dependency relationships, and reason codes may expose sensitive operational details. In some deployments, these details can reveal user behavior, internal service structure, or policy logic.
Implementations SHOULD disclose only the information necessary for interoperable recovery. If a deployment requires broader analytics or audit retention, that policy is deployment-specific and outside the scope of this document.
## 11. IANA Considerations
This document currently requests no IANA action.
Future versions may request compact registries for failure classes, rollback outcome codes, or event type identifiers if implementation experience shows that fixed interoperation points are needed.
## 12. References
- [RFC2119] Key words for use in RFCs to Indicate Requirement Levels.
- [RFC8174] Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words.
- Placeholder reference for adjacent execution-evidence substrate, if adopted.
- Placeholder reference for `draft-yue-anima-agent-recovery-networks`.
- Placeholder reference for `draft-li-dmsc-macp`.
- Placeholder reference for `draft-fu-nmop-agent-communication-framework`.

View File

@@ -0,0 +1,242 @@
# Draft
## Abstract
This document defines experimental recovery semantics for multi-agent task execution. It specifies interoperable event semantics for failure signaling, checkpoint reference, rollback requests, and rollback results so that cooperating agents can coordinate recovery after operational faults. The mechanism is carrier-agnostic and is intended to be profiled onto existing agent communication or execution-evidence substrates. It addresses an interoperability gap in current agent systems: different implementations can detect the same failure yet diverge materially in how they request rollback, report applied scope, and disclose partial or irreversible outcomes.
## 1. Introduction
Multi-agent systems increasingly perform coordinated work across services, tools, and administrative domains. In such systems, one task failure can invalidate downstream work, require compensating actions, or force a broader rollback of externally visible effects. Existing drafts define communication frameworks, discovery, identity, and broader orchestration concepts, but they do not yet provide a small interoperable recovery core that independent implementations can share.
Without common recovery behavior, one implementation may silently retry while another expects explicit rollback, and a third may report only local failure without describing downstream consequences. Those differences are not just operationally inconvenient; they create genuine safety and interoperability risk when agents act without immediate human supervision.
This document therefore defines an abstract recovery protocol model for cross-agent failure handling. It does not define a workflow language, a transport binding, or a human override system. It does define required event meaning, minimum fields, authorization and replay expectations, rollback-scope reporting, and outcome reporting sufficient for interoperable recovery behavior.
The intended status of this document is Experimental.
## 2. Terminology
The key words "MUST", "MUST NOT", "SHOULD", "SHOULD NOT", and "MAY" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals.
Agent: an autonomous software entity that performs one or more tasks and may exchange recovery events with peers.
Task: a discrete unit of work whose execution and outcome can be identified.
Dependency: a relationship in which one task relies on the prior completion, state, or side effects of another task.
Checkpoint: a recorded recovery-safe reference from which rollback or compensation planning can proceed.
Failure Event: a machine-actionable record indicating that a task or dependency failed in a way that can affect other participants.
Rollback Set: the abstract set of task identifiers, checkpoint identifiers, or effect identifiers that a rollback request identifies as in scope.
Recovery Record: a record of rollback attempt, refusal, partial rollback, success, or failure.
Compensation: a follow-up action that mitigates an irreversible effect when direct rollback is not possible.
## 3. Problem Statement
Current agent ecosystems have uneven support for failure handling. Some drafts discuss task coordination or operational recovery, but the analyzed landscape still lacks a common method to express:
- that a task failed in a cross-agent relevant way,
- which dependencies are affected,
- which checkpoint or rollback boundary should be used,
- what rollback scope is being requested, and
- whether rollback succeeded, only partially succeeded, was refused, or was impossible.
The absence of these common semantics makes independent implementation difficult. An originating agent may believe it has requested rollback, while a receiving agent may treat the same signal as informational. Similarly, partial rollback can leave downstream agents operating on inconsistent assumptions if outcome reporting is underspecified.
The design goals for this document are:
- protocol-agnostic applicability,
- minimal mandatory fields for interoperability,
- idempotent rollback requests,
- explicit authorization and replay handling,
- explicit reporting of partial or impossible rollback, and
- compatibility with existing lower-layer identity and integrity mechanisms.
## 4. Recovery Model Overview
This document defines four event types:
- `checkpoint`
- `failure`
- `rollback-request`
- `rollback-result`
These events MAY be carried in a message protocol, stored as execution records, or embedded in a larger workflow substrate. This document does not standardize the carrier. It standardizes the abstract protocol behavior and the minimum information needed for interoperable recovery.
Each event has a common envelope containing:
- an event identifier,
- a task identifier,
- a sender identity reference,
- a timestamp, and
- any relevant workflow or execution context identifier.
The recovery model assumes that a failure can be local or cross-agent relevant. Local failures that cannot affect any external dependency do not require signaling under this document. When a failure can affect dependent work outside local scope, the originating agent MUST emit a `failure` event.
If rollback is needed, the requester sends a `rollback-request` identifying the requested scope. The receiver evaluates authorization, replay status, and local reversibility before acting. The receiver then returns a `rollback-result` stating whether the requested recovery succeeded, partially succeeded, was refused, was impossible, or failed.
## 5. Event Types and Required Fields
### 5.1 Checkpoint
A `checkpoint` event identifies a recovery-safe reference that later rollback may target. A checkpoint event MUST include:
- event identifier,
- task identifier,
- checkpoint identifier,
- sender identity reference,
- timestamp.
A checkpoint event SHOULD include reversibility class and MAY include checkpoint expiry or retention information.
### 5.2 Failure
A `failure` event reports a task failure that can affect dependent execution outside local process scope. A failure event MUST include:
- event identifier,
- failed task identifier,
- sender identity reference,
- timestamp,
- failure class,
- reversibility indicator.
A failure event SHOULD include affected dependency identifiers when known, and MAY include severity, blast-radius hint, or checkpoint reference.
### 5.3 Rollback Request
A `rollback-request` event asks another participant to revert or compensate previously applied effects. A rollback request MUST include:
- event identifier,
- requester identity reference,
- target task identifier or checkpoint identifier,
- requested rollback scope,
- idempotency token,
- timestamp.
A rollback request SHOULD include reason code and urgency. A rollback request MAY include dependency evidence or policy reference supporting the request.
Before applying rollback, a receiver MUST evaluate whether the requester is authorized to request rollback for the identified scope. If authorization fails, the receiver MUST NOT apply rollback and MUST emit a `rollback-result` with outcome `refused`.
### 5.4 Rollback Result
A `rollback-result` event reports the outcome of processing a rollback request. A rollback result MUST include:
- event identifier,
- referenced rollback-request identifier,
- responder identity reference,
- outcome code,
- timestamp,
- actual scope applied.
The outcome code MUST be one of:
- `success`
- `partial-success`
- `refused`
- `irreversible`
- `failure`
If the outcome code is not `success`, the rollback result MUST include enough detail to indicate remaining unapplied scope, residual irreversible effects, or refusal reason. A rollback result MAY include compensation details.
## 6. Task States and Recovery Procedures
For purposes of this document, relevant task states are:
- `pending`
- `running`
- `completed`
- `failed`
- `rollback-requested`
- `rolled-back`
- `rollback-failed`
- `compensation-required`
When an agent detects a task failure that can affect external dependents, it MUST transition the affected task to `failed` and emit a `failure` event. If policy permits automatic recovery, the originating agent SHOULD determine the rollback set and issue one or more `rollback-request` events. If policy does not permit automatic rollback, the implementation SHOULD enter a local hold or escalation path rather than silently continuing.
An agent receiving a `rollback-request` MUST process duplicate requests idempotently. To do so, the receiver MUST correlate the request identifier and idempotency token and MUST reject or safely ignore stale replayed requests according to local replay policy. A request that is recognized as stale replay MUST NOT cause a second rollback action.
If the request is authorized and can be honored, the agent applies rollback or compensation as appropriate and emits a `rollback-result`. If the request cannot be honored because the effect is irreversible, unauthorized, or operationally failed, the agent MUST emit a `rollback-result` with the appropriate outcome code.
This document distinguishes rollback from cancellation. Cancellation of work not yet started is out of scope except where a local implementation uses cancellation internally while fulfilling a rollback request.
### 6.1 State Transition Guidance
| Current State | Trigger | Next State | Required Output |
|---|---|---|---|
| `running` | cross-agent relevant failure detected | `failed` | `failure` |
| `completed` | authorized rollback requested | `rollback-requested` | none immediately |
| `rollback-requested` | rollback fully applied | `rolled-back` | `rollback-result(success)` |
| `rollback-requested` | rollback partially applied | `compensation-required` | `rollback-result(partial-success)` |
| `rollback-requested` | rollback impossible | `rollback-failed` or `compensation-required` | `rollback-result(irreversible)` |
| `rollback-requested` | processing failure | `rollback-failed` | `rollback-result(failure)` |
This table is intentionally minimal. Local implementations MAY track finer-grained states, but interoperable outputs MUST remain consistent with the transitions above.
## 7. Rollback Scope and Dependency Handling
Rollback scope is central to interoperability. A rollback request MUST identify either:
- a target checkpoint, or
- an explicit rollback set.
At minimum, a rollback set MUST identify one or more affected task identifiers, checkpoint identifiers, or effect identifiers. When transitive dependencies are known, the requester SHOULD indicate whether the scope includes only direct dependencies or includes transitive dependencies as well.
When dependency knowledge is incomplete, the requester MUST still identify the minimum known affected scope and the responder MUST report the actual scope applied in the rollback result. A responder MUST NOT report successful rollback for effects outside the applied scope.
If only part of the requested rollback set is reversed, the responder MUST return `partial-success` and MUST describe any remaining irreversible or uncompensated effects.
## 8. Error Conditions and Partial Rollback
The following conditions require explicit handling:
- duplicate rollback requests,
- stale replay of prior rollback requests,
- timeout while waiting for rollback completion,
- refusal due to insufficient authorization,
- irreversible effects,
- partial rollback where some effects are reversed and others remain,
- failure of the rollback procedure itself.
If a requested rollback is impossible, the responding agent MUST indicate `irreversible` or `failure` as appropriate and SHOULD indicate whether compensation is available. If a request times out after some scope has been applied, the responder SHOULD return `partial-success` rather than silently collapsing to generic failure.
Implementations SHOULD avoid silent downgrade from rollback to best-effort local cleanup. If only local cleanup occurred, the rollback result SHOULD say so clearly.
### 8.1 Non-Normative Example Flow
Agent A executes task `t-17`, which depends on Agent B having applied task `t-12`. Agent B later detects that `t-12` wrote invalid external state and emits `failure(failed-task=t-12, affected-dependency=t-17)`. Agent A determines that rollback is required for `t-17` and sends `rollback-request(request-id=r-8, target-task=t-17, scope={t-17, ckpt-17-precommit}, idempotency-token=abc123)`.
Agent A's peer evaluates requester authorization and replay status, applies rollback to `t-17`, but cannot reverse one externally visible notification. It therefore emits `rollback-result(ref=r-8, outcome=partial-success, actual-scope={t-17, ckpt-17-precommit}, residual=notification already delivered)`. A downstream relying party can now distinguish partial rollback from full recovery and act accordingly.
## 9. Security Considerations
Unauthorized rollback requests can be used to deny service or corrupt coordinated work. Implementations therefore need authenticated carriage and explicit authorization checks for the events defined here, even though this document does not define the underlying security protocol.
Spoofed failure events can trigger unnecessary rollback. Replay of old rollback requests can repeatedly unwind valid work. Implementations MUST prevent replayed requests from causing repeated rollback actions and SHOULD bind requests and results to stable task and requester identifiers.
Partial rollback is itself a security concern because it can leave downstream systems in an inconsistent state that an attacker can exploit. For that reason, responders MUST explicitly report residual scope and any remaining irreversible effects.
Failure and rollback metadata can also reveal topology, task dependencies, and operational weaknesses. Deployments SHOULD minimize unnecessary disclosure and SHOULD apply least-privilege access to recovery records.
## 10. Privacy Considerations
Task identifiers, failure classes, dependency relationships, and reason codes may expose sensitive operational details. In some deployments, these details can reveal user behavior, internal service structure, or policy logic.
Implementations SHOULD disclose only the information necessary for interoperable recovery. If a deployment requires broader analytics or audit retention, that policy is deployment-specific and outside the scope of this document.
## 11. IANA Considerations
This document currently requests no IANA action.
Future versions may request compact registries for failure classes, rollback outcome codes, or event type identifiers if implementation experience shows that fixed interoperation points are needed.
## 12. References
- [RFC2119] Key words for use in RFCs to Indicate Requirement Levels.
- [RFC8174] Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words.
- Placeholder reference for adjacent execution-evidence substrate, if adopted.
- Placeholder reference for `draft-yue-anima-agent-recovery-networks`.
- Placeholder reference for `draft-li-dmsc-macp`.
- Placeholder reference for `draft-fu-nmop-agent-communication-framework`.

View File

@@ -0,0 +1,24 @@
# Architecture Review
## Findings
### Medium: the draft is mostly well scoped, but it wavers between abstract event semantics and protocol behavior
The document says it is carrier-agnostic and not a transport binding, which is correct. However, several MUST-level statements already imply protocol behavior. That is acceptable, but the architecture should acknowledge that the document defines an abstract protocol model, not only vocabulary.
### Medium: coordinator role is introduced but not integrated into the model
The coordinator is defined as optional, yet no section explains how peers distinguish coordinator-computed scope from sender-local scope. That leaves a conceptual hole in the actor model.
### Medium: cancellation is declared out of scope, but the boundary with rollback is not fully clean
The text says cancellation of work not yet started is out of scope, except when used internally to satisfy rollback. That line is defensible, but it should be expressed more rigorously to prevent readers from assuming cancellation semantics are standardized here.
## Open questions
- Should the draft describe itself as an abstract recovery protocol profile rather than only "semantics"?
- Does the optional coordinator need one or two normative constraints, or should it be deferred entirely?
## Residual risk
Scope discipline is good overall. The main remaining architectural risk is ambiguity about whether this document is merely descriptive or actually defines interoperable protocol behavior. It should explicitly choose the latter in a carefully bounded way.

View File

@@ -0,0 +1,28 @@
# IETF Senior Review
## Findings
### High: the draft still reads more like a design sketch than a publishable Internet-Draft
The overall structure is right, but several sections stop at high-level intent. A publishable draft needs more disciplined distinction between required behavior, optional behavior, and explanatory rationale. Sections 5 through 8 are closest to publishable, but they still need slightly more rigor.
### Medium: the abstract is acceptable but could better state the interoperability problem and deployment value
The current abstract says what the document defines, but it could more directly explain why existing agent systems fail to interoperate during recovery and why this document matters.
### Medium: References and IANA sections are too provisional
It is fine to keep placeholders at this stage, but the text currently signals that core dependencies are undecided. Before wider circulation, the draft should either name the expected adjacent substrate or state clearly that no substrate dependency is required.
### Medium: terminology is mostly clean, but some items still need RFC-style definition form
The terms are understandable, yet a few are written more like explanations than stable definitions. Tightening the definition style would help the document feel more standards-native.
## Open questions
- Does the draft intend to progress as a standalone individual draft or as part of a family with a shared terminology base?
- Should the document explicitly call itself Experimental in the introduction rather than only in external cycle metadata?
## Residual publishability risk
This is a credible start. The remaining publishability risk is not the idea; it is the need for one more iteration of standards-style precision and dependency cleanup.

View File

@@ -0,0 +1,28 @@
# Security Review
## Findings
### High: rollback authorization is left entirely to the lower layer without a required authorization decision point
The draft says recovery events need authenticated and authorized carriage, but it never states when a receiver is required to evaluate authorization before acting on a `rollback-request`. Two compliant implementations could therefore both authenticate the requester yet differ on whether task-level rollback authority is required. The draft should require an explicit authorization check before any irreversible rollback action is attempted.
### High: replay protection is mentioned but underspecified for interoperable use
The draft says implementations SHOULD provide replay resistance, but `rollback-request` already defines an idempotency token and stable identifiers. That is enough structure to make stronger requirements possible. Without a minimum replay-handling rule, an attacker can reuse stale rollback requests in a way that different implementations will treat inconsistently.
### Medium: failure-event spoofing risk is identified, but the draft does not require correlation between failure and rollback flows
An attacker who can inject a plausible `failure` event may induce unnecessary rollback decisions. The draft should at least require that a `rollback-request` reference a specific task or failure context and that receivers preserve the linkage in the `rollback-result`.
### Medium: partial rollback can leave exploitable inconsistent state, but no minimum disclosure is mandated
The draft correctly notes the risk, yet "residual risk description" is only a SHOULD. For partial-success and irreversible outcomes, a stronger requirement is warranted so downstream agents can react safely.
## Open questions
- Should authorization be expressed as a generic requirement only, or should the document define a task-scope authorization concept for rollback actions?
- Should replay resistance be a MUST for all deployments, or only when rollback has externally visible effects?
## Residual risk
Even with the fixes above, the draft will still depend heavily on lower-layer identity and authorization systems. That is acceptable, but the security section should say so more concretely and bind protocol behavior to those assumptions.

View File

@@ -0,0 +1,28 @@
# Software Review
## Findings
### High: required fields are defined, but no concrete message shape or example flow is provided
The event model is understandable, but two implementers could still serialize or correlate it differently. A non-normative example showing `failure -> rollback-request -> rollback-result` with task identifiers, dependency references, and partial-success handling would materially reduce ambiguity.
### High: task state transitions are incomplete at the procedure level
The draft lists states but does not specify enough transition rules. For example, can a task move from `completed` directly to `rollback-requested`? Can `compensation-required` be terminal? Can `rollback-failed` later transition to `rolled-back` after manual intervention? Without a transition table or explicit rules, interoperability tests will be hard to design.
### Medium: rollback scope remains too abstract for independent implementations
The draft requires a target checkpoint or explicit rollback set, but it does not describe the structure of a rollback set or how direct and transitive dependencies are represented. The draft needs at least a minimal abstract shape for scope membership.
### Medium: timeout behavior is named but not operationalized
Timeout is listed as an error condition, but no rule says whether timeout yields `failure`, `partial-success`, or local retry. This will fragment behavior.
## Open questions
- Is a compact transition table sufficient, or does the draft need a separate state machine subsection?
- Should rollback set representation be a list of task identifiers, checkpoint identifiers, or both?
## Residual risk
The current draft is close to implementable, but it still needs one more layer of precision around flow shape and state progression before two vendors would likely build compatible behavior.

View File

@@ -0,0 +1,7 @@
# Architecture Review
## Findings
## Open questions
## Residual risk

View File

@@ -0,0 +1,7 @@
# IETF Senior Review
## Findings
## Open questions
## Residual publishability risk

View File

@@ -0,0 +1,7 @@
# Security Review
## Findings
## Open questions
## Residual risk

View File

@@ -0,0 +1,7 @@
# Software Review
## Findings
## Open questions
## Residual risk

View File

@@ -0,0 +1,26 @@
# Review Synthesis
## Blocking findings
- Add an explicit authorization-decision requirement before acting on rollback requests. The security review correctly identifies this as the biggest missing control.
- Tighten replay handling by linking idempotency, request identity, and stale-request rejection into one interoperable rule.
- Add one concrete non-normative flow example and a compact transition table. The software review is right that the draft is still too abstract for two independent implementations.
## Major findings
- Clarify whether the document is an abstract protocol model or only event vocabulary. The architecture review recommends choosing the former in a bounded way.
- Specify minimum disclosure rules for partial-success, irreversible, and refused outcomes so downstream agents can react safely.
- Clarify rollback-scope representation at the abstract level: what a rollback set minimally contains and how direct versus transitive scope is reported.
- Improve the abstract and introduction to frame the interoperability problem more directly.
## Minor findings
- Tighten terminology definitions into more RFC-like form.
- Clarify the coordinator role or remove it if not needed in this revision.
- Clarify the cancellation boundary.
- Reduce placeholder feel in References and dependency text.
## Conflicts resolved
- No meaningful reviewer conflict exists on scope. All reviewers favor keeping the document narrow.
- The only tension is between remaining carrier-agnostic and becoming implementable. Resolution: keep the model carrier-agnostic, but add one non-normative example and stronger abstract structure rather than binding to a specific substrate in v1.

View File

@@ -0,0 +1,9 @@
# Review Synthesis
## Blocking findings
## Major findings
## Minor findings
## Conflicts resolved

View File

@@ -0,0 +1,28 @@
# Revision Plan
## Blocking changes
- Add a normative requirement that receivers evaluate authorization before honoring a rollback request.
- Add a normative replay-handling rule tying request identity, idempotency token, and stale-request rejection together.
- Add a compact state-transition table covering normal failure, rollback request, partial success, irreversible outcome, and compensation-required cases.
- Add one non-normative end-to-end example flow with concrete identifiers and a partial-success outcome.
## High-value improvements
- Clarify rollback-set structure and how transitive scope is represented or reported.
- Strengthen `rollback-result` requirements for partial-success, refused, and irreversible outcomes.
- Tighten the abstract, introduction, and terminology wording to sound more like an actual I-D.
- Either define the coordinator role more clearly or remove it from this version.
## Deferred items
- Binding to a specific execution-evidence substrate
- Human override or operator approval flow
- Registries for failure classes and rollback outcomes unless implementation feedback requires them
## Draft order for next iteration
1. Revise abstract and terminology.
2. Revise Sections 5 through 8 for authorization, replay, scope shape, and state transitions.
3. Add non-normative example flow.
4. Revisit Security, Privacy, IANA, and References after the protocol text settles.

View File

@@ -0,0 +1,9 @@
# Revision Plan
## Blocking changes
## High-value improvements
## Deferred items
## Draft order for next iteration

View File

@@ -0,0 +1,69 @@
# User Spec
## Topic
Dynamic Trust and Reputation for Multi-Agent Systems
## Goal
Produce a credible IETF-style Internet-Draft for an interoperable way to represent and exchange runtime trust signals about agents, so systems can adapt authorization, routing, or collaboration decisions as agent behavior changes over time.
## Intended status
Experimental.
Rationale: the need is clear, but trust scoring models are easy to overclaim and likely need deployment experience before standards-track treatment.
## Problem to solve
The analyzer identifies Dynamic Trust and Reputation as a high-severity gap. Current work is dominated by static identity and certificate-style authentication, but long-running agent ecosystems need a way to incorporate runtime behavior, observed failures, successful execution history, and policy violations into ongoing trust decisions.
The draft should address:
- how trust-relevant events are represented
- how trust assertions or trust updates are shared
- how recipients understand freshness, confidence, and scope
- how dynamic trust interacts with but does not replace identity and authorization
## What must be true in the final draft
- The draft distinguishes identity, attestation, authorization, and trust; it must not collapse them into one concept.
- Dynamic trust is presented as supplemental runtime evidence, not magic security.
- The mechanism is narrow enough to be interoperable and testable.
- Security and privacy analysis address manipulation, collusion, replay, reputational poisoning, and unwanted disclosure.
- The document remains grounded in observable events and explicit confidence, not vague AI safety rhetoric.
## Constraints
- scope constraints
Do not try to standardize a universal reputation economy or global scoring service. Focus on exchangeable trust signals and their interpretation boundaries.
- compatibility constraints
Reuse adjacent identity, attestation, and execution-evidence work when possible. Do not redefine base authentication or token exchange.
- terminology constraints
Separate trust event, trust assertion, confidence, freshness, subject, issuer, and scope. Avoid anthropomorphic language.
## Source materials to prioritize
- `/home/c/projects/ietf-draft-analyzer/data/reports/gaps.md`
- `/home/c/projects/ietf-draft-analyzer/data/reports/holistic-agent-ecosystem-draft-outlines.md`
- `/home/c/projects/ietf-draft-analyzer/data/reports/ideas.md`
- `/home/c/projects/ietf-draft-analyzer/data/reports/overview.md`
- `draft-cosmos-protocol-specification`
- `draft-jiang-seat-dynamic-attestation`
- `draft-aylward-daap-v2`
- `draft-birkholz-verifiable-agent-conversations`
- relevant WIMSE, RATS, or attestation-adjacent drafts when they help prevent reinvention
## Success criteria
- A reader can tell what trust-relevant event data must be present and how it is scoped.
- A reader can tell how trust assertions expire, how confidence is expressed, and how misuse is limited.
- Reviewers can challenge the design on substance rather than on fuzzy terminology or missing threat analysis.
- The draft makes clear what decisions dynamic trust can inform and what it must not be trusted to do alone.
## Questions for the team
- What is the minimum interoperable trust event model?
- Should trust updates be absolute assertions, delta adjustments, or both?
- How should confidence, issuer scope, and freshness be represented?
- What privacy risks arise when sharing negative trust events across domains?

View File

@@ -0,0 +1,27 @@
# Cycle Status
## Summary
- cycle: dynamic-trust-and-reputation
- version: v1
- last updated: 2026-03-02 18:00 UTC
## Artifact Status
- `00-user-spec.md`: written
- `10-research-brief.md`: written
- `20-architecture-brief.md`: written
- `30-outline.md`: written
- `40-draft-v1.md`: written
- `50-reviews-v1/security.md`: written
- `50-reviews-v1/software.md`: written
- `50-reviews-v1/architecture.md`: written
- `50-reviews-v1/ietf-senior.md`: written
- `55-review-synthesis-v1.md`: written
- `60-revision-plan-v1.md`: written
## Notes
- written means the artifact contains substantive content.
- stub means the file exists but still appears to be a placeholder.
- missing means the expected file has not been created.

View File

@@ -0,0 +1,27 @@
# Cycle Status
## Summary
- cycle: dynamic-trust-and-reputation
- version: v2
- last updated: 2026-03-02 18:06 UTC
## Artifact Status
- `00-user-spec.md`: written
- `10-research-brief.md`: written
- `20-architecture-brief.md`: written
- `30-outline.md`: written
- `40-draft-v2.md`: written
- `50-reviews-v2/security.md`: stub
- `50-reviews-v2/software.md`: stub
- `50-reviews-v2/architecture.md`: stub
- `50-reviews-v2/ietf-senior.md`: stub
- `55-review-synthesis-v2.md`: stub
- `60-revision-plan-v2.md`: stub
## Notes
- written means the artifact contains substantive content.
- stub means the file exists but still appears to be a placeholder.
- missing means the expected file has not been created.

View File

@@ -0,0 +1,60 @@
# Research Brief
## Problem framing
Fact: the analyzer marks Dynamic Trust and Reputation as a high-severity gap in the agent identity and authorization space. Fact: the stated problem is that static authentication is not enough for long-running autonomous systems, because past behavior, policy violations, successful task history, and environmental changes can alter whether another agent should be trusted.
Inference: this topic is worth pursuing, but it is easy to overreach. The most defensible first draft is not a universal reputation system. It is a narrow mechanism for representing and exchanging trust-relevant runtime assertions with freshness, confidence, issuer scope, and revocation semantics.
## Evidence from existing drafts
Fact: the gap report identifies only five partially related ideas across the full corpus. The clearest named mechanism is `Trust Scoring` from `draft-cosmos-protocol-specification`. Other partial signals include `Trust Score-based Policy Enforcement`, `Cryptographic Proof-Based Autonomy`, and dynamic attestation work.
Fact: the gap report points to several related drafts: `draft-jiang-seat-dynamic-attestation`, `draft-cosmos-protocol-specification`, `draft-diaconu-agents-authz-info-sharing`, `draft-agent-gw`, and `draft-li-dmsc-inf-architecture`. These appear to provide fragments such as attestation, trust-native semantics, or information sharing, but not a generally reusable dynamic trust exchange core.
Fact: the broader overview shows stronger maturity in adjacent accountability and attestation drafts such as `draft-aylward-daap-v2`, `draft-guy-bary-stamp-protocol`, and `draft-birkholz-verifiable-agent-conversations`. Those are important not because they solve dynamic trust directly, but because they provide candidate evidence sources from which trust events might be derived.
Fact: the holistic ecosystem outline places dynamic trust alongside assurance, cross-domain security, and provenance rather than as a standalone identity replacement. That is a strong scope signal.
## Overlap and adjacent work
Inference: the main collision risks are:
- collapsing trust into identity or attestation
- drifting into full behavior verification and assurance profiles
- defining global reputation semantics that are impossible to standardize early
Inference: the architect should treat dynamic trust as a supplemental decision input. A trust assertion should help receivers adjust risk posture, routing, delegation, or policy thresholds, but should not replace authentication, authorization, or local policy.
There is also a likely layering opportunity: trust events may be derived from signed execution evidence, attestation results, policy compliance checks, or observed protocol outcomes. That suggests the first draft should define a trust event model and trust assertion envelope rather than inventing a new base proof system.
## Gaps and unresolved questions
Fact: the available analyzer artifacts do not yet show a shared vocabulary for freshness, confidence, negative trust evidence, or revocation of prior trust assertions. Fact: the ideas corpus surfaced less direct material than expected, which suggests the field is genuinely underdefined rather than merely fragmented.
Open questions:
- What is the minimum trust event payload: subject, issuer, event type, score or delta, confidence, freshness, scope, and evidence reference are likely candidates, but this needs careful architectural pruning.
- Should trust be represented as absolute score, bounded level, delta adjustment, or a combination?
- How should a receiver distinguish local opinion from portable inter-domain assertion?
- How should negative trust events be shared without creating privacy, defamation, or poisoning problems?
- What revocation or expiry mechanism is needed so stale trust does not silently persist?
## Additional data worth investigating
- Inspect `draft-cosmos-protocol-specification` directly for the semantics of trust scoring and whether any parts are salvageable without importing the whole model.
- Inspect `draft-jiang-seat-dynamic-attestation` for how runtime attestation changes over time and whether it offers reusable freshness or confidence patterns.
- Compare `draft-aylward-daap-v2` and `draft-birkholz-verifiable-agent-conversations` for event formats that could serve as evidence references.
- Search more deeply for `confidence`, `reputation`, `revocation`, `behavioral`, `policy violation`, and `provenance` in raw draft text if the architect needs a stronger evidence base.
## Recommendation to the architect
Design the first draft as an experimental representation for dynamic trust assertions and trust events, not as a global scoring system. Keep the document centered on:
- trust event vocabulary
- trust assertion envelope and required fields
- issuer, subject, scope, freshness, and confidence semantics
- revocation or expiry behavior
- security and privacy limits on exchanging negative or cross-domain trust information
Avoid redefining identity, token exchange, attestation, or full behavior verification. If evidence references from adjacent drafts can be reused, bind to them rather than creating a new proof substrate.

View File

@@ -0,0 +1,113 @@
# Architecture Brief
## Scope
Define an experimental, interoperable representation for dynamic trust information exchanged between agents or agent-adjacent services. The draft should standardize:
- trust event vocabulary
- trust assertion envelope
- issuer, subject, scope, freshness, and confidence semantics
- expiry or revocation behavior
- minimal rules for how receivers interpret portable trust information
The document should remain supplemental to identity, attestation, and authorization systems.
## Non-goals
- creating a global reputation network or universal score
- replacing authentication, attestation, or authorization
- standardizing all behavior-verification evidence formats
- requiring a single scoring algorithm
- defining economic incentives, penalties, or marketplace reputation
## Terminology and actors
- `trust event`: an observed runtime occurrence relevant to trust assessment
- `trust assertion`: a structured statement by an issuer about a subject's trust-relevant state
- `issuer`: the party making the assertion
- `subject`: the agent or service described by the assertion
- `confidence`: how strongly the issuer stands behind the assertion
- `freshness`: how current the assertion is and how long it remains usable
- `scope`: the context in which the assertion is intended to apply
- `evidence reference`: pointer to supporting execution, attestation, or compliance evidence
- `revocation`: withdrawal or supersession of a prior trust assertion
Actors:
- observing agent or service
- trust assertion issuer
- relying party that consumes trust information
- optional policy authority governing how trust affects decisions
## Protocol or data model shape
Use two related objects:
1. a trust event record
2. a trust assertion
Trust event record minimum fields:
- event identifier
- subject identifier
- issuer or observer identifier
- event type
- timestamp
- scope
Trust assertion minimum fields:
- assertion identifier
- subject identifier
- issuer identifier
- trust statement value
- confidence value
- freshness or expiry information
- scope
Optional fields:
- evidence reference
- delta-from-prior assertion
- revokes or supersedes assertion identifier
- explanation code
Design choice: do not require one numeric scoring model. Allow bounded levels, numeric values, or deltas as long as the representation states which model is being used and how confidence and expiry apply.
## Normative requirements candidates
- A trust assertion MUST identify both issuer and subject.
- A trust assertion MUST indicate scope and freshness.
- A trust assertion MUST NOT be treated as a substitute for authentication or authorization.
- If a trust assertion supersedes or revokes a prior assertion, it MUST identify the prior assertion.
- Receivers MUST be able to distinguish portable trust assertions from local-only trust state.
- Trust assertions SHOULD include evidence references when the underlying evidence is available and shareable.
- Implementations SHOULD define local policy for how negative assertions are consumed; this document should not hardcode one response.
- Issuers MUST NOT present stale assertions as current.
## Security, privacy, and abuse considerations
- false negative or false positive trust assertions can manipulate routing or authorization decisions
- colluding issuers could amplify reputational poisoning
- replayed stale assertions can preserve obsolete trust
- over-shared negative trust information can leak sensitive incident details
- portable trust data may be misread as global truth rather than scoped issuer opinion
The draft should strongly emphasize that trust assertions are context-bound statements requiring authenticated origin, explicit freshness, and local policy interpretation.
## IANA impact
Potentially small registries only if needed by implementation experience:
- trust event types
- trust assertion statement models
- explanation codes
Avoid large registries or score semantics that imply false precision.
## Open design questions
- Should the primary trust statement model be level-based, numeric, delta-based, or model-agnostic?
- How much explanation should be mandatory when sharing negative trust?
- How should a receiver compare assertions from different issuers with different confidence models?
- Should revocation be a first-class assertion type or simply a superseding assertion?

View File

@@ -0,0 +1,79 @@
# Draft Outline
## Abstract
State that the document defines experimental semantics for exchanging dynamic trust assertions and trust-relevant runtime events in multi-agent systems. Make clear that the mechanism supplements, but does not replace, identity, attestation, and authorization.
## Section plan
1. Introduction
2. Terminology
3. Problem Statement and Design Goals
4. Trust Model Overview
5. Trust Events
6. Trust Assertions
7. Freshness, Confidence, and Revocation
8. Receiver Processing and Policy Boundaries
9. Security Considerations
10. Privacy Considerations
11. IANA Considerations
12. References
## Author guidance by section
### 1. Introduction
Anchor the problem in long-running agent interactions where static identity is insufficient. Avoid implying that trust scores solve security by themselves.
### 2. Terminology
Define trust event, trust assertion, issuer, subject, confidence, freshness, scope, evidence reference, and revocation. Be disciplined about these distinctions.
### 3. Problem Statement and Design Goals
Explain the gap between static authentication and runtime trust decisions. State that the document aims to standardize representation and exchange, not one universal scoring algorithm.
### 4. Trust Model Overview
Show the layering clearly: identity and attestation remain below; trust assertions sit above them as supplemental runtime signals interpreted by local policy.
### 5. Trust Events
Define the observable events that can feed trust changes. Avoid overloading this section with algorithmic scoring guidance.
### 6. Trust Assertions
Define the required fields of a portable trust assertion and how issuer, subject, scope, confidence, and statement value are represented.
### 7. Freshness, Confidence, and Revocation
This is the core interoperability section. Be precise about expiry, supersession, stale data, and the difference between confidence and trust value.
### 8. Receiver Processing and Policy Boundaries
Explain what a receiver may infer and what remains local policy. This section must prevent readers from treating portable trust as universal authorization.
### 9. Security Considerations
Address poisoning, collusion, replay, spoofing, and misuse of trust assertions in access-control flows.
### 10. Privacy Considerations
Address cross-domain disclosure of incidents, behavior, and negative assertions.
### 11. IANA Considerations
Either no action or minimal registries for event types and assertion models.
### 12. References
Keep placeholders if needed, but cite adjacent attestation, accountability, and evidence-bearing drafts that influenced the layering.
## Issues that must not be hand-waved
- whether trust assertions are scoped issuer opinions or universal facts
- how freshness and expiry are represented
- how revocation or supersession works
- how confidence differs from trust value
- what evidence reference means and when it is optional
- how receivers avoid using trust as a drop-in replacement for authorization

View File

@@ -0,0 +1,172 @@
# Draft
## Abstract
This document defines experimental semantics for exchanging dynamic trust assertions and trust-relevant runtime events in multi-agent systems. The mechanism allows one party to communicate scoped, time-bounded statements about another party's observed trust-relevant behavior, together with confidence and optional evidence references. The mechanism supplements identity, attestation, and authorization systems; it does not replace them. The goal is to improve interoperability where long-running agent interactions require trust decisions that evolve over time rather than remaining fixed at initial authentication.
## 1. Introduction
Many agent systems authenticate peers once and then rely on static identity or long-lived authorization artifacts for the remainder of an interaction. That approach is often insufficient for long-running or cross-domain systems in which runtime behavior, policy violations, attestation changes, or observed failures should affect how much confidence one participant places in another.
Several existing drafts address accountability, attestation, authorization, or cross-domain information sharing. However, the current landscape still lacks a compact, reusable way to represent and exchange dynamic trust information as an interoperable runtime signal. As a result, systems that do attempt dynamic trust tend to use proprietary or locally scoped semantics that are hard to compare or consume across implementations.
This document defines a narrow mechanism for trust events and trust assertions. It standardizes how such information is represented and how relying parties distinguish issuer opinion, freshness, scope, and confidence. It does not define a single global scoring algorithm, a reputation marketplace, or a replacement for authorization policy.
## 2. Terminology
The key words "MUST", "MUST NOT", "SHOULD", "SHOULD NOT", and "MAY" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals.
Trust Event: an observed runtime occurrence relevant to trust assessment.
Trust Assertion: a structured statement by an issuer regarding the trust-relevant state of a subject.
Issuer: the party that originates a trust assertion.
Subject: the agent or service that a trust assertion describes.
Relying Party: a party that consumes a trust assertion for local decision-making.
Scope: the context in which a trust assertion is intended to apply.
Confidence: the issuer's stated level of confidence in a trust assertion.
Freshness: the temporal validity information for a trust assertion, including creation time, expiry, or other recency limits.
Evidence Reference: a pointer to supporting execution, attestation, compliance, or observational evidence.
Revocation: withdrawal or supersession of a previously issued trust assertion.
Portable Trust Assertion: a trust assertion intended for use outside the issuer's local trust store.
Local Trust State: trust information maintained only within one implementation and not intended for exchange under this document.
## 3. Problem Statement and Design Goals
Static identity answers who a peer claims to be. Dynamic trust concerns whether recent behavior, evidence, and context justify continuing to rely on that peer in the same way. In current practice, systems often blur these concepts, leading to three recurring problems:
- trust information is shared without clear scope or expiry,
- negative trust signals are propagated without confidence or evidence context, and
- receivers treat portable trust statements as universal authorization decisions.
The design goals for this document are therefore:
- to define a compact representation for trust events and trust assertions,
- to require issuer, subject, scope, freshness, and confidence information,
- to support revocation or supersession of stale assertions,
- to preserve local policy discretion, and
- to avoid false precision by not mandating one global trust algorithm.
## 4. Trust Model Overview
This document defines two related objects:
- a `trust-event`, and
- a `trust-assertion`.
A trust event is an observed occurrence that may justify a trust update. Examples include successful execution, attestation degradation, repeated policy violation, or verified protocol misbehavior. This document standardizes the representation of such events but does not require that every event be exchanged externally.
A trust assertion is a portable statement derived from local observation, policy processing, or supporting evidence. A trust assertion can be exchanged between participants when the issuer intends another relying party to consider that information.
This document is layered above identity, attestation, and authorization systems. A trust assertion MUST NOT be treated as proof of identity and MUST NOT be used as a substitute for authentication. Likewise, it MUST NOT by itself grant authorization. Instead, it provides a supplemental input to local policy.
## 5. Trust Events
A trust event record MUST include:
- event identifier,
- subject identifier,
- issuer or observer identifier,
- event type,
- timestamp,
- scope.
A trust event SHOULD include an evidence reference when supporting evidence exists and can be shared. A trust event MAY include an explanation code or local severity value.
This document does not require that all trust events be externally exchanged. An implementation MAY use local-only trust events to derive portable trust assertions. However, if a portable trust assertion references a trust event, the implementation SHOULD preserve enough linkage that a relying party can understand the event context.
Example trust-event categories include:
- successful verified execution,
- attestation downgrade,
- policy violation,
- repeated protocol error,
- trust recovery after remediation.
This list is illustrative only.
## 6. Trust Assertions
A trust assertion MUST include:
- assertion identifier,
- issuer identifier,
- subject identifier,
- trust statement value,
- confidence value,
- freshness information,
- scope.
A trust assertion MAY include:
- evidence reference,
- explanation code,
- delta-from-prior value,
- revokes or supersedes assertion identifier.
This document permits multiple trust statement models, including bounded levels, numeric values, or delta updates. If an issuer uses a given model, the assertion MUST identify that model clearly enough for the relying party to interpret the statement.
An issuer MUST distinguish portable trust assertions from local trust state. A relying party MUST be able to determine whether the received assertion is intended to travel across administrative boundaries or is only meaningful within the issuer's local environment.
## 7. Freshness, Confidence, and Revocation
Freshness is mandatory. An issuer MUST include enough temporal information for a relying party to detect stale assertions. At minimum, that means creation time and either expiry time or a validity policy that can be interpreted consistently.
Confidence is distinct from trust value. The trust statement says what the issuer believes about the subject; the confidence value says how strongly the issuer stands behind that statement. A relying party MUST NOT assume that a high trust value implies high confidence, or vice versa.
If an issuer revokes or supersedes a prior assertion, the new assertion MUST identify the prior assertion. A relying party receiving both old and new assertions SHOULD prefer the newer assertion when freshness and issuer identity indicate that supersession is valid.
Issuers MUST NOT present stale assertions as current. Relying parties SHOULD reject or downgrade stale assertions according to local policy.
## 8. Receiver Processing and Policy Boundaries
Relying parties consume trust assertions as local policy input. This document does not require one decision algorithm. However, receivers MUST preserve the following distinctions:
- issuer opinion versus objective fact,
- trust value versus confidence,
- portable assertion versus local trust state,
- trust input versus authorization decision.
A relying party MAY combine assertions from multiple issuers, but comparison across issuers is inherently local-policy dependent. This document therefore does not define issuer ranking, quorum rules, or mandatory aggregation algorithms.
When a negative assertion lacks sufficient freshness, scope, or issuer clarity, a relying party SHOULD treat it cautiously or ignore it. When a positive assertion lacks evidence reference where such evidence is normally expected, the relying party MAY reduce its weight.
## 9. Security Considerations
Dynamic trust information is vulnerable to spoofing, replay, collusion, and reputational poisoning. Implementations therefore need authenticated origin and integrity protection for portable trust assertions, even though this document does not define the underlying cryptographic transport or token format.
Replay of stale trust assertions can preserve outdated trust long after behavior has changed. For this reason, freshness is mandatory and receivers SHOULD apply explicit stale-data handling.
Colluding issuers can amplify false claims. This document does not solve collusion, but it reduces ambiguity by requiring issuer identification, scope, and confidence. Deployments SHOULD avoid treating multiple assertions as independent when they originate from closely related sources.
Trust assertions can also be misused as unauthorized access-control surrogates. Implementers MUST NOT treat a trust assertion alone as granting access absent normal authorization checks.
## 10. Privacy Considerations
Trust events and trust assertions may reveal sensitive operational information, including policy violations, remediation history, attestation degradation, or other indicators of weakness. Negative assertions may also expose behavior that a subject does not expect to be shared across domains.
Implementations SHOULD minimize disclosure to what is necessary for the intended scope. Evidence references SHOULD avoid exposing raw sensitive details when a narrower reference suffices. Cross-domain sharing of negative assertions deserves particular caution because it can create lasting reputational effects outside the original operational context.
## 11. IANA Considerations
This document currently requests no IANA action.
If implementation experience later shows clear need for shared registries, suitable candidates include trust-event categories, trust statement model identifiers, and explanation codes. Such registries should remain compact and avoid implying false precision.
## 12. References
- [RFC2119] Key words for use in RFCs to Indicate Requirement Levels.
- [RFC8174] Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words.
- Placeholder reference for `draft-cosmos-protocol-specification`.
- Placeholder reference for `draft-jiang-seat-dynamic-attestation`.
- Placeholder reference for `draft-aylward-daap-v2`.
- Placeholder reference for `draft-birkholz-verifiable-agent-conversations`.

View File

@@ -0,0 +1,190 @@
# Draft
## Abstract
This document defines experimental semantics for exchanging portable dynamic trust assertions and associated trust-relevant runtime events in multi-agent systems. The mechanism allows one party to communicate a scoped, time-bounded opinion about another party's observed trust-relevant behavior, together with model identification, confidence, freshness, and optional evidence or explanation data. The mechanism supplements identity, attestation, and authorization systems; it does not replace them. Its purpose is to improve interoperability where long-running agent interactions require trust decisions that evolve over time rather than remaining fixed at initial authentication.
## 1. Introduction
Many agent systems authenticate peers once and then rely on static identity or long-lived authorization artifacts for the remainder of an interaction. That approach is often insufficient for long-running or cross-domain systems in which runtime behavior, policy violations, attestation changes, or observed failures should affect how much confidence one participant places in another.
Several existing drafts address accountability, attestation, authorization, or cross-domain information sharing. However, the current landscape still lacks a compact, reusable way to represent and exchange dynamic trust information as an interoperable runtime signal. As a result, systems that do attempt dynamic trust tend to use proprietary or locally scoped semantics that are hard to compare or consume across implementations.
This document defines a narrow mechanism for trust assertions and supporting trust events. It standardizes how portable trust information is represented and how relying parties distinguish issuer opinion, freshness, scope, confidence, and model type. It does not define a global scoring algorithm, a reputation marketplace, or a replacement for authorization policy.
The intended status of this document is Experimental.
## 2. Terminology
The key words "MUST", "MUST NOT", "SHOULD", "SHOULD NOT", and "MAY" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals.
Trust Event: an observed runtime occurrence relevant to trust assessment.
Trust Assertion: a structured statement by an issuer regarding the trust-relevant state of a subject.
Portable Trust Assertion: a trust assertion intended for use outside the issuer's local trust store.
Issuer: the party that originates a trust assertion.
Subject: the agent or service that a trust assertion describes.
Relying Party: a party that consumes a trust assertion for local decision-making.
Scope: the context in which a trust assertion is intended to apply.
Confidence: the issuer's stated degree of confidence in a trust assertion.
Freshness: the temporal validity information for a trust assertion, including creation time and expiry or equivalent validity bound.
Model Identifier: an identifier indicating how the trust statement value is to be interpreted, such as level-based, numeric, or delta-based.
Evidence Reference: a pointer to supporting execution, attestation, compliance, or observational evidence.
Explanation Code: a compact issuer-supplied explanation label associated with a trust assertion.
Revocation: invalidation of a previously issued trust assertion.
Supersession: replacement of a prior trust assertion by a newer assertion from the same issuer.
Local Trust State: trust information maintained only within one implementation and not intended for exchange under this document.
## 3. Problem Statement and Design Goals
Static identity answers who a peer claims to be. Dynamic trust concerns whether recent behavior, evidence, and context justify continuing to rely on that peer in the same way. In current practice, systems often blur these concepts, leading to three recurring problems:
- trust information is shared without clear scope or expiry,
- negative trust signals are propagated without confidence or evidence context, and
- receivers treat portable trust statements as universal authorization decisions.
The design goals for this document are therefore:
- to define a compact portable trust assertion envelope,
- to require issuer, subject, scope, freshness, confidence, and model identification,
- to support revocation or supersession of stale assertions,
- to preserve local policy discretion, and
- to avoid false precision by not mandating one global trust algorithm.
## 4. Trust Model Overview
This document standardizes portable trust assertions as the primary interoperable object. Trust events are supporting input objects that MAY be exchanged or MAY remain local, depending on deployment needs.
A trust event is an observed occurrence that may justify a trust update. Examples include successful execution, attestation degradation, repeated policy violation, or verified protocol misbehavior. This document standardizes the minimal representation of such events, but portable trust assertions are the main interoperability target.
A portable trust assertion is a scoped issuer opinion derived from local observation, policy processing, or supporting evidence. A portable trust assertion can be exchanged between participants when the issuer intends another relying party to consider that information.
This document is layered above identity, attestation, and authorization systems. A portable trust assertion MUST NOT be treated as proof of identity and MUST NOT be used as a substitute for authentication. Likewise, it MUST NOT by itself grant authorization. Instead, it provides supplemental input to local policy.
## 5. Trust Events
A trust event record MUST include:
- event identifier,
- subject identifier,
- issuer or observer identifier,
- event type,
- timestamp,
- scope.
A trust event SHOULD include an evidence reference when supporting evidence exists and can be shared. A trust event MAY include an explanation code or local severity value.
This document does not require that all trust events be externally exchanged. An implementation MAY use local-only trust events to derive portable trust assertions. If a portable trust assertion references a trust event, the implementation SHOULD preserve enough linkage that a relying party can understand the event context.
This document does not standardize a mandatory global event vocabulary in v2. Event-type names MAY be profile-specific unless later implementation experience shows the need for shared registries.
## 6. Trust Assertions
A portable trust assertion MUST include:
- assertion identifier,
- issuer identifier,
- subject identifier,
- trust statement value,
- model identifier,
- confidence value,
- freshness information,
- scope.
A portable trust assertion MAY include:
- evidence reference,
- explanation code,
- delta-from-prior value,
- revokes assertion identifier,
- supersedes assertion identifier.
An issuer MUST distinguish portable trust assertions from local trust state. A relying party MUST be able to determine whether the received assertion is intended to travel across administrative boundaries or is only meaningful within the issuer's local environment.
If a portable trust assertion carries a negative or cautionary trust statement, it MUST include either an evidence reference or an explanation code. It MAY include both.
## 7. Freshness, Confidence, and Revocation
Freshness is mandatory. An issuer MUST include enough temporal information for a relying party to detect stale assertions. At minimum, that means creation time and either expiry time or a validity bound that can be interpreted consistently.
Confidence is distinct from trust value. The trust statement says what the issuer believes about the subject; the confidence value says how strongly the issuer stands behind that statement. A relying party MUST NOT assume that a high trust value implies high confidence, or vice versa.
Revocation and supersession are distinct. Revocation invalidates a prior assertion without necessarily replacing it with a new positive or negative assertion. Supersession replaces a prior assertion with a newer one from the same issuer. If an issuer revokes or supersedes a prior assertion, the new assertion MUST identify the prior assertion.
Issuers MUST NOT present stale assertions as current. A relying party MUST reject a clearly expired portable trust assertion as conformant input, though it MAY retain it locally for audit or diagnostic purposes.
## 8. Receiver Processing and Policy Boundaries
Portable trust assertions are local policy input. This document does not require one decision algorithm. However, receivers MUST preserve the following distinctions:
- issuer opinion versus objective fact,
- trust value versus confidence,
- portable assertion versus local trust state,
- trust input versus authorization decision.
A relying party MUST NOT treat an unauthenticated portable trust assertion as conformant input under this specification. Likewise, a relying party MUST NOT treat a portable trust assertion alone as granting access absent normal authorization checks.
A relying party MAY combine assertions from multiple issuers, but comparison across issuers is inherently local-policy dependent. This document therefore does not define issuer ranking, quorum rules, or mandatory aggregation algorithms. Implementations SHOULD take care not to treat closely related issuers as independent corroboration sources.
### 8.1 Non-Normative Assertion Example
An issuer may send a portable trust assertion with:
- assertion-id `ta-44`
- subject `agent:example:planner7`
- issuer `agent:example:gateway2`
- model `level`
- trust-value `caution`
- confidence `0.8`
- created-at `2026-03-02T17:00:00Z`
- expires-at `2026-03-02T18:00:00Z`
- scope `cross-domain-task-routing`
- explanation-code `policy-violation-recent`
### 8.2 Non-Normative Multi-Issuer Conflict Example
Issuer A sends a fresh `level=trusted` assertion with confidence `0.6` for a subject in scope `document-translation`. Issuer B sends a newer `level=caution` assertion with confidence `0.9` in the same scope, referencing a recent attestation downgrade. This document does not require one aggregation outcome. It does require that the relying party preserve issuer identity, freshness, scope, and confidence rather than collapsing the two assertions into an unexplained average.
## 9. Security Considerations
Dynamic trust information is vulnerable to spoofing, replay, collusion, and reputational poisoning. Implementations therefore need authenticated origin and integrity protection for portable trust assertions, even though this document does not define the underlying cryptographic transport or token format.
Replay of stale trust assertions can preserve outdated trust long after behavior has changed. For this reason, freshness is mandatory and clearly expired portable trust assertions MUST be rejected as valid current input.
Colluding issuers can amplify false claims. This document does not solve collusion, but it reduces ambiguity by requiring issuer identification, scope, confidence, and model identification. Deployments SHOULD avoid treating multiple assertions as independent when they originate from closely related sources.
Trust assertions can also be misused as unauthorized access-control surrogates. Implementers MUST NOT treat a portable trust assertion alone as granting access absent normal authorization checks.
## 10. Privacy Considerations
Trust events and trust assertions may reveal sensitive operational information, including policy violations, remediation history, attestation degradation, or other indicators of weakness. Negative assertions may also expose behavior that a subject does not expect to be shared across domains.
Implementations SHOULD minimize disclosure to what is necessary for the intended scope. Evidence references SHOULD avoid exposing raw sensitive details when a narrower reference suffices. Cross-domain sharing of negative assertions deserves particular caution because it can create lasting reputational effects outside the original operational context.
## 11. IANA Considerations
This document currently requests no IANA action.
If implementation experience later shows clear need for shared registries, suitable candidates include model identifiers, trust-event categories, and explanation codes. Such registries should remain compact and avoid implying false precision.
## 12. References
- [RFC2119] Key words for use in RFCs to Indicate Requirement Levels.
- [RFC8174] Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words.
- Placeholder reference for `draft-cosmos-protocol-specification`.
- Placeholder reference for `draft-jiang-seat-dynamic-attestation`.
- Placeholder reference for `draft-aylward-daap-v2`.
- Placeholder reference for `draft-birkholz-verifiable-agent-conversations`.

View File

@@ -0,0 +1,24 @@
# Architecture Review
## Findings
### Medium: scope discipline is good, but the draft risks under-specifying the portable core
The draft correctly avoids becoming a universal reputation system. The remaining risk is that so much is left to local policy that the portable assertion core becomes too thin. The architecture should define a firmer minimum portable envelope.
### Medium: the trust-event object may be more than the first revision needs
The draft has both trust events and trust assertions. That layering is sensible, but the architecture should say more directly whether trust-event interoperability is a primary goal or merely a feeder model for assertions. Otherwise readers may assume both layers are equally mature.
### Medium: revocation and supersession deserve a cleaner conceptual split
The draft treats revocation as withdrawal or supersession, but those are not always the same. One invalidates a prior assertion; the other replaces it with a newer one. This distinction should be sharper.
## Open questions
- Is the first implementable milestone portable assertions only, with trust events described as optional supporting input?
- Should revocation be kept as a general umbrella term or split explicitly into revoke and supersede actions?
## Residual risk
The document has good boundaries. The main architectural risk is not scope creep but insufficient commitment to a concrete portable core.

View File

@@ -0,0 +1,28 @@
# IETF Senior Review
## Findings
### High: the draft is credible, but it still reads more like an architecture note than a standards-ready specification
The structure is sound and the layering is disciplined. What it still lacks is the slight extra formality that makes an Internet-Draft feel publishable: clearer field requirements, fewer conceptual transitions, and less reliance on explanatory prose in Sections 5 through 8.
### Medium: the abstract should emphasize scoped issuer opinion sooner
That point is present later in the document and is central to avoiding misuse. It should appear earlier and more explicitly in the abstract.
### Medium: IANA and references remain intentionally provisional
That is acceptable at this stage, but before circulation beyond an internal drafting loop, the document should either define a tiny initial model registry or clearly state that all model identifiers are profile-specific pending later work.
### Medium: terminology is good, but a few terms could be made more standards-native
Portable Trust Assertion and Local Trust State are useful distinctions, though they currently read slightly informal. Tightening those definitions would improve the document.
## Open questions
- Is the intended status Experimental explicitly stated in the draft text anywhere, or only in the cycle metadata?
- Should the document explicitly note that it does not define trust aggregation across issuers?
## Residual publishability risk
This is a strong first version. The remaining work is mainly to replace architectural vagueness with just enough protocol discipline to withstand IETF-style scrutiny.

View File

@@ -0,0 +1,28 @@
# Security Review
## Findings
### High: assertion authenticity is assumed but not tied to required receiver behavior
The draft correctly says portable trust assertions need authenticated origin and integrity protection, but it does not make rejection behavior explicit. A receiver should not be allowed to consume an unauthenticated portable assertion and still claim conformance.
### High: replay handling depends on freshness, but the minimum stale-data rule is too soft
Freshness is mandatory, which is good, but receivers only SHOULD reject or downgrade stale assertions. For clearly expired assertions, that is too weak. A stronger interoperability floor is warranted.
### Medium: negative trust sharing creates reputational poisoning risk without minimum evidence discipline
The document warns about poisoning and privacy, yet evidence references remain entirely optional. That is reasonable for all assertions, but negative portable assertions may need a stronger requirement for explanation or evidence linkage.
### Medium: collusion risk is identified but not operationalized
The draft notes that multiple issuers may not be independent, but it gives no guidance on how a relying party should avoid double-counting related issuers. Even a brief cautionary requirement or implementation note would help.
## Open questions
- Should unauthenticated portable trust assertions be explicitly non-conformant?
- Should negative assertions require either evidence reference or explanation code?
## Residual risk
Even with improvements, dynamic trust will remain vulnerable to social and operational abuse that pure wire semantics cannot prevent. The draft should state those limits plainly.

View File

@@ -0,0 +1,28 @@
# Software Review
## Findings
### High: trust statement models are allowed to vary, but model identification is still too abstract
The draft says a trust assertion must identify its model clearly enough for interpretation, but it never sketches the minimum structure of that identifier. Implementers need at least an abstract field or named model token.
### Medium: receiver processing lacks concrete examples of multi-issuer conflict
The text is directionally correct that aggregation is local policy, but a non-normative example of conflicting assertions with different confidence and freshness would make implementation much easier.
### Medium: trust-event categories are illustrative only, which is safe, but leaves event producers with little interoperability anchor
The draft should either define a small initial event vocabulary or state more clearly that event categories are profile-specific and not intended to interoperate by name in v1.
### Medium: freshness requirements need a clearer shape
The text requires creation time and either expiry or validity policy, but two implementations could still encode validity very differently. The document would benefit from one abstract freshness shape or example.
## Open questions
- Should the document standardize a tiny base model such as `level`, `numeric`, and `delta`?
- Should it include a compact example trust assertion object?
## Residual risk
The draft is conceptually coherent, but still needs one more layer of data-shape clarity before implementation teams are likely to converge cleanly.

View File

@@ -0,0 +1,7 @@
# Architecture Review
## Findings
## Open questions
## Residual risk

View File

@@ -0,0 +1,7 @@
# IETF Senior Review
## Findings
## Open questions
## Residual publishability risk

View File

@@ -0,0 +1,7 @@
# Security Review
## Findings
## Open questions
## Residual risk

View File

@@ -0,0 +1,7 @@
# Software Review
## Findings
## Open questions
## Residual risk

View File

@@ -0,0 +1,25 @@
# Review Synthesis
## Blocking findings
- Add an explicit conformance rule that portable trust assertions require authenticated origin and integrity protection; unauthenticated portable assertions must not be treated as conformant input.
- Tighten stale-data handling so clearly expired assertions are rejected rather than merely "downgraded" at implementer discretion.
- Define a firmer minimum portable data shape for trust assertions, including explicit model identification.
## Major findings
- Clarify whether trust-event interoperability is core to the document or whether trust events are primarily feeder objects for portable assertions.
- Strengthen the handling of negative assertions by requiring either evidence reference or explanation code when such assertions are exchanged portably.
- Clarify revocation versus supersession.
- Add one compact example of conflicting assertions from different issuers to make receiver processing easier to implement.
## Minor findings
- Tighten abstract wording around scoped issuer opinion.
- Make a few terminology definitions more RFC-like.
- Reduce provisional tone in IANA and dependency text.
## Conflicts resolved
- No major reviewer conflict exists. All reviewers support the narrow scope.
- The only tension is between remaining model-agnostic and becoming implementable. Resolution: keep algorithm choice open, but define a stronger minimum portable assertion envelope and clearer stale-data behavior.

View File

@@ -0,0 +1,9 @@
# Review Synthesis
## Blocking findings
## Major findings
## Minor findings
## Conflicts resolved

View File

@@ -0,0 +1,29 @@
# Revision Plan
## Blocking changes
- Add explicit rejection behavior for unauthenticated portable trust assertions.
- Strengthen stale-data handling for expired assertions.
- Add a clearer abstract field or token for trust statement model identification.
- Clarify whether negative portable assertions require evidence reference, explanation code, or one of the two.
## High-value improvements
- Add one compact example assertion and one multi-issuer conflict example.
- Clarify revocation versus supersession.
- Decide whether trust events are first-class interoperable objects in v1 or primarily internal feeder records.
- Tighten abstract and terminology wording.
## Deferred items
- cross-issuer aggregation algorithms
- global reputation semantics
- large shared registries
- mandatory numeric scoring
## Draft order for next iteration
1. Tighten Sections 4 through 8 around portable assertion conformance.
2. Add explicit model identification and stale-data rules.
3. Add negative-assertion handling rules and examples.
4. Revisit Security, Privacy, IANA, and References for final consistency.

View File

@@ -0,0 +1,9 @@
# Revision Plan
## Blocking changes
## High-value improvements
## Deferred items
## Draft order for next iteration

View File

@@ -0,0 +1,25 @@
# User Spec
## Topic
## Goal
## Intended status
Informational, Experimental, or Standards Track.
## Problem to solve
## What must be true in the final draft
## Constraints
- scope constraints
- compatibility constraints
- terminology constraints
## Source materials to prioritize
## Success criteria
## Questions for the team

View File

@@ -0,0 +1,13 @@
# Research Brief
## Problem framing
## Evidence from existing drafts
## Overlap and adjacent work
## Gaps and unresolved questions
## Additional data worth investigating
## Recommendation to the architect

View File

@@ -0,0 +1,17 @@
# Architecture Brief
## Scope
## Non-goals
## Terminology and actors
## Protocol or data model shape
## Normative requirements candidates
## Security, privacy, and abuse considerations
## IANA impact
## Open design questions

View File

@@ -0,0 +1,9 @@
# Draft Outline
## Abstract
## Section plan
## Author guidance by section
## Issues that must not be hand-waved

View File

@@ -0,0 +1,21 @@
# Draft
## Abstract
## 1. Introduction
## 2. Terminology
## 3. Problem Statement
## 4. Protocol Overview
## 5. Detailed Specification
## 6. Security Considerations
## 7. Privacy Considerations
## 8. IANA Considerations
## 9. References

View File

@@ -0,0 +1,9 @@
# Review Report
## Findings
## Open questions
## Strengths
## Residual publishability risk

View File

@@ -0,0 +1,9 @@
# Revision Plan
## Blocking changes
## High-value improvements
## Deferred items
## Draft order for next iteration

View File

@@ -0,0 +1,25 @@
# User Spec
## Topic
## Goal
## Intended status
Informational, Experimental, or Standards Track.
## Problem to solve
## What must be true in the final draft
## Constraints
- scope constraints
- compatibility constraints
- terminology constraints
## Source materials to prioritize
## Success criteria
## Questions for the team

View File

@@ -0,0 +1,13 @@
# Research Brief
## Problem framing
## Evidence from existing drafts
## Overlap and adjacent work
## Gaps and unresolved questions
## Additional data worth investigating
## Recommendation to the architect

View File

@@ -0,0 +1,17 @@
# Architecture Brief
## Scope
## Non-goals
## Terminology and actors
## Protocol or data model shape
## Normative requirements candidates
## Security, privacy, and abuse considerations
## IANA impact
## Open design questions

View File

@@ -0,0 +1,9 @@
# Draft Outline
## Abstract
## Section plan
## Author guidance by section
## Issues that must not be hand-waved

View File

@@ -0,0 +1,21 @@
# Draft
## Abstract
## 1. Introduction
## 2. Terminology
## 3. Problem Statement
## 4. Protocol Overview
## 5. Detailed Specification
## 6. Security Considerations
## 7. Privacy Considerations
## 8. IANA Considerations
## 9. References

View File

@@ -0,0 +1,7 @@
# Architecture Review
## Findings
## Open questions
## Residual risk

View File

@@ -0,0 +1,7 @@
# IETF Senior Review
## Findings
## Open questions
## Residual publishability risk

View File

@@ -0,0 +1,7 @@
# Security Review
## Findings
## Open questions
## Residual risk

View File

@@ -0,0 +1,7 @@
# Software Review
## Findings
## Open questions
## Residual risk

View File

@@ -0,0 +1,9 @@
# Review Synthesis
## Blocking findings
## Major findings
## Minor findings
## Conflicts resolved

View File

@@ -0,0 +1,9 @@
# Revision Plan
## Blocking changes
## High-value improvements
## Deferred items
## Draft order for next iteration

View File

@@ -0,0 +1,36 @@
Act as the IETF senior reviewer.
## Objective
Review the draft like an experienced IETF participant focused on publishability, document shape, and standards hygiene.
## Inputs
- current cycle `00-user-spec.md`
- latest `40-draft-vN.md`
Load `20-architecture-brief.md` only when the draft intent is unclear.
## Output
Write `50-reviews-vN/ietf-senior.md`.
## Review Areas
- intended status fit
- IETF document structure and tone
- terminology quality
- proper separation of requirements, rationale, and examples
- missing considerations sections
- likely DISCUSS or major-comment triggers
- misuse of BCP 14 keywords
- weak abstract or introduction framing
- premature solutioning without clear problem statement
## Rules
- Review for publishability, not novelty.
- Call out where the draft sounds like product design or marketing.
- Prefer plain, process-aware feedback over line-editing.
- Expect a clean problem statement, scoped terminology, and clear distinction between protocol procedure and explanatory text.
- Treat missing or thin Security Considerations, Privacy Considerations, IANA Considerations, and References as serious issues.

View File

@@ -0,0 +1,28 @@
# Analyzer Integration
Use `/home/c/projects/ietf-draft-analyzer` as the primary evidence source unless the user overrides it.
## High-value inputs
- `README.md`: current project scope and headline findings
- `data/reports/gaps.md`: strongest starting point for candidate draft topics
- `data/reports/ideas.md`: extracted ideas and recurring mechanisms
- `data/reports/overview.md`: broad landscape summary
- `data/reports/overlap-clusters.md`: duplication and collision risk
- `data/reports/holistic-agent-ecosystem-draft-outlines.md`: idea seeds that may already exist
- `data/reports/draft-*.md`: focused analysis for specific candidate drafts
- `paper/main.tex`: current publication framing and terminology
## Suggested evidence order
1. read the relevant cycle files
2. read `gaps.md`
3. read one or two supporting reports
4. read individual draft reports only when needed
5. inspect source code or DB only when the reports are insufficient
## Research heuristics
- If the gap is already heavily covered, shift toward comparison or refinement instead of greenfield drafting.
- If the gap touches trust, security, provenance, or rollback, inspect adjacent categories to avoid missing overlapping work.
- If the user asks for a publishable draft, verify the proposed scope is small enough to defend in one document.

View File

@@ -0,0 +1,36 @@
Act as the researcher.
## Objective
Analyze existing fetched data first. Produce a concise evidence brief that helps the architect decide what should be specified and what still needs investigation.
## Inputs
- current cycle `00-user-spec.md`
- relevant analyzer outputs from `/home/c/projects/ietf-draft-analyzer`
Start with the smallest useful set:
- `data/reports/gaps.md`
- `data/reports/ideas.md`
- `data/reports/overview.md`
- specific draft reports only when needed
## Output
Write `10-research-brief.md` with these sections:
1. Problem framing
2. Evidence from existing drafts
3. Competitive or overlapping work
4. Gaps and unresolved questions
5. Additional data worth fetching or verifying
6. Recommendation to the architect
## Rules
- Prefer synthesis over raw notes.
- Distinguish fact, inference, and hypothesis.
- Propose new investigation only when it would materially change the spec.
- Do not write normative protocol text.
- Keep the brief under roughly 900 words unless the cycle genuinely demands more.

View File

@@ -0,0 +1,32 @@
Act as the review lead.
## Objective
Synthesize the specialist review files into one prioritized set of changes and a disciplined next iteration plan.
## Inputs
- current cycle `20-architecture-brief.md`
- latest `40-draft-vN.md`
- all files in `50-reviews-vN/`
## Output
Write:
- `55-review-synthesis-vN.md`
- `60-revision-plan-vN.md`
## Synthesis Method
1. deduplicate overlapping findings
2. resolve conflicts between reviewers
3. sort by blocker, major, minor
4. convert findings into exact draft edits
## Rules
- Do not add new design work unless the reviewers exposed a real gap.
- Preserve reviewer nuance when it affects severity.
- Keep the revision plan executable by the author in one pass when possible.
- When specialist reviewers disagree, prefer the smaller and more defensible standards claim.

View File

@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "usage: $0 <cycle-slug>" >&2
exit 1
fi
slug="$1"
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cycle_dir="$root/cycles/$slug"
if [[ -e "$cycle_dir" ]]; then
echo "cycle already exists: $cycle_dir" >&2
exit 1
fi
mkdir -p "$cycle_dir"
cp "$root/templates/00-user-spec.md" "$cycle_dir/00-user-spec.md"
cp "$root/templates/05-status.md" "$cycle_dir/05-status-v1.md"
cp "$root/templates/10-research-brief.md" "$cycle_dir/10-research-brief.md"
cp "$root/templates/20-architecture-brief.md" "$cycle_dir/20-architecture-brief.md"
cp "$root/templates/30-outline.md" "$cycle_dir/30-outline.md"
cp "$root/templates/40-draft.md" "$cycle_dir/40-draft-v1.md"
mkdir -p "$cycle_dir/50-reviews-v1"
cp "$root/templates/50-review-security.md" "$cycle_dir/50-reviews-v1/security.md"
cp "$root/templates/50-review-software.md" "$cycle_dir/50-reviews-v1/software.md"
cp "$root/templates/50-review-architecture.md" "$cycle_dir/50-reviews-v1/architecture.md"
cp "$root/templates/50-review-ietf-senior.md" "$cycle_dir/50-reviews-v1/ietf-senior.md"
cp "$root/templates/55-review-synthesis.md" "$cycle_dir/55-review-synthesis-v1.md"
cp "$root/templates/60-revision-plan.md" "$cycle_dir/60-revision-plan-v1.md"
echo "created $cycle_dir"

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 2 || $# -gt 3 ]]; then
echo "usage: $0 <cycle-slug> <role> [version]" >&2
exit 1
fi
slug="$1"
role="$2"
version="${3:-1}"
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cycle_dir="$root/cycles/$slug"
if [[ ! -d "$cycle_dir" ]]; then
echo "missing cycle: $cycle_dir" >&2
exit 1
fi
case "$role" in
researcher)
printf '%s\n' "$cycle_dir/10-research-brief.md"
;;
architect)
printf '%s\n%s\n' "$cycle_dir/20-architecture-brief.md" "$cycle_dir/30-outline.md"
;;
author)
printf '%s\n' "$cycle_dir/40-draft-v$version.md"
;;
security-reviewer)
printf '%s\n' "$cycle_dir/50-reviews-v$version/security.md"
;;
software-reviewer)
printf '%s\n' "$cycle_dir/50-reviews-v$version/software.md"
;;
architecture-reviewer)
printf '%s\n' "$cycle_dir/50-reviews-v$version/architecture.md"
;;
ietf-senior-reviewer)
printf '%s\n' "$cycle_dir/50-reviews-v$version/ietf-senior.md"
;;
review-lead)
printf '%s\n%s\n' "$cycle_dir/55-review-synthesis-v$version.md" "$cycle_dir/60-revision-plan-v$version.md"
;;
*)
echo "unknown role: $role" >&2
exit 1
;;
esac

View File

@@ -0,0 +1,70 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 1 || $# -gt 2 ]]; then
echo "usage: $0 <cycle-slug> [version]" >&2
exit 1
fi
slug="$1"
version="${2:-1}"
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cycle_dir="$root/cycles/$slug"
review_dir="$cycle_dir/50-reviews-v$version"
status_file="$cycle_dir/05-status-v$version.md"
if [[ ! -d "$cycle_dir" ]]; then
echo "missing cycle: $cycle_dir" >&2
exit 1
fi
draft_file="$cycle_dir/40-draft-v$version.md"
review_synthesis="$cycle_dir/55-review-synthesis-v$version.md"
revision_plan="$cycle_dir/60-revision-plan-v$version.md"
if [[ ! -f "$draft_file" ]]; then
cp "$root/templates/40-draft.md" "$draft_file"
fi
if [[ ! -f "$status_file" ]]; then
cp "$root/templates/05-status.md" "$status_file"
fi
if [[ ! -d "$review_dir" ]]; then
mkdir -p "$review_dir"
cp "$root/templates/50-review-security.md" "$review_dir/security.md"
cp "$root/templates/50-review-software.md" "$review_dir/software.md"
cp "$root/templates/50-review-architecture.md" "$review_dir/architecture.md"
cp "$root/templates/50-review-ietf-senior.md" "$review_dir/ietf-senior.md"
fi
if [[ ! -f "$review_synthesis" ]]; then
cp "$root/templates/55-review-synthesis.md" "$review_synthesis"
fi
if [[ ! -f "$revision_plan" ]]; then
cp "$root/templates/60-revision-plan.md" "$revision_plan"
fi
cat <<EOF
Cycle: $slug
Version: v$version
Run order:
1. researcher -> $cycle_dir/10-research-brief.md
2. architect -> $cycle_dir/20-architecture-brief.md and $cycle_dir/30-outline.md
3. author -> $draft_file
4. security-reviewer -> $review_dir/security.md
5. software-reviewer -> $review_dir/software.md
6. architecture-reviewer -> $review_dir/architecture.md
7. ietf-senior-reviewer -> $review_dir/ietf-senior.md
8. review-lead -> $review_synthesis and $revision_plan
Core inputs:
- $status_file
- $cycle_dir/00-user-spec.md
- $cycle_dir/10-research-brief.md
- $cycle_dir/20-architecture-brief.md
- $cycle_dir/30-outline.md
- $draft_file
EOF

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 1 || $# -gt 2 ]]; then
echo "usage: $0 <cycle-slug> [version]" >&2
exit 1
fi
slug="$1"
version="${2:-1}"
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cycle_dir="$root/cycles/$slug"
status_file="$cycle_dir/05-status-v$version.md"
if [[ ! -d "$cycle_dir" ]]; then
echo "missing cycle: $cycle_dir" >&2
exit 1
fi
status_of() {
local path="$1"
if [[ ! -f "$path" ]]; then
printf 'missing'
return
fi
if rg -q '^Pending\.$' "$path"; then
printf 'stub'
return
fi
if ! rg -q '^[[:space:]]*(-[[:space:]]+[A-Za-z0-9]|\d+\.[[:space:]]+[A-Za-z0-9]|[^#[:space:]\-`])' "$path"; then
printf 'stub'
return
fi
if rg -q '^# [A-Za-z ]+$' "$path" && [[ "$(wc -l < "$path")" -lt 8 ]]; then
printf 'stub'
return
fi
printf 'written'
}
cat > "$status_file" <<EOF
# Cycle Status
## Summary
- cycle: $slug
- version: v$version
- last updated: $(date -u +"%Y-%m-%d %H:%M UTC")
## Artifact Status
- \`00-user-spec.md\`: $(status_of "$cycle_dir/00-user-spec.md")
- \`10-research-brief.md\`: $(status_of "$cycle_dir/10-research-brief.md")
- \`20-architecture-brief.md\`: $(status_of "$cycle_dir/20-architecture-brief.md")
- \`30-outline.md\`: $(status_of "$cycle_dir/30-outline.md")
- \`40-draft-v$version.md\`: $(status_of "$cycle_dir/40-draft-v$version.md")
- \`50-reviews-v$version/security.md\`: $(status_of "$cycle_dir/50-reviews-v$version/security.md")
- \`50-reviews-v$version/software.md\`: $(status_of "$cycle_dir/50-reviews-v$version/software.md")
- \`50-reviews-v$version/architecture.md\`: $(status_of "$cycle_dir/50-reviews-v$version/architecture.md")
- \`50-reviews-v$version/ietf-senior.md\`: $(status_of "$cycle_dir/50-reviews-v$version/ietf-senior.md")
- \`55-review-synthesis-v$version.md\`: $(status_of "$cycle_dir/55-review-synthesis-v$version.md")
- \`60-revision-plan-v$version.md\`: $(status_of "$cycle_dir/60-revision-plan-v$version.md")
## Notes
- written means the artifact contains substantive content.
- stub means the file exists but still appears to be a placeholder.
- missing means the expected file has not been created.
EOF
echo "$status_file"

View File

@@ -0,0 +1,34 @@
Act as the security reviewer.
## Objective
Find concrete weaknesses in security, privacy, trust, abuse resistance, and failure handling.
## Inputs
- current cycle `00-user-spec.md`
- current cycle `20-architecture-brief.md`
- latest `40-draft-vN.md`
Load `10-research-brief.md` only when checking whether a security claim is evidence-backed.
## Output
Write `50-reviews-vN/security.md`.
## Review Areas
- threat model gaps
- weak trust assumptions
- authentication and authorization ambiguity
- downgrade, spoofing, replay, rollback, and abuse cases
- privacy leakage and data provenance gaps
- missing security and privacy considerations text
## Rules
- Lead with findings ordered by severity.
- Prefer protocol-level fixes over vague warnings.
- Call out where the draft needs stricter normative language.
- Check that Security Considerations are specific to the mechanism, not generic boilerplate.
- Flag any use of BCP 14 keywords that creates impossible or unverifiable security requirements.

View File

@@ -0,0 +1,33 @@
Act as the software reviewer.
## Objective
Find concrete issues that would make the draft hard to implement, test, operate, or interoperate.
## Inputs
- current cycle `20-architecture-brief.md`
- latest `40-draft-vN.md`
Load `00-user-spec.md` only when validating a user constraint.
## Output
Write `50-reviews-vN/software.md`.
## Review Areas
- underspecified behavior
- state-machine ambiguity
- invalid or unstable extension points
- deployment and migration problems
- observability and debugging gaps
- missing examples, wire shapes, or error handling
## Rules
- Focus on implementability, not prose polish.
- Point to exact places where two independent implementers could diverge.
- Suggest the minimum extra structure needed for interoperability.
- Review state transitions, failure codes, rollback triggers, and timeout behavior as if two vendors had to implement them independently.
- Flag where examples, message shapes, or procedure ordering are needed for an implementer to succeed.

View File

@@ -0,0 +1,25 @@
# User Spec
## Topic
## Goal
## Intended status
Informational, Experimental, or Standards Track.
## Problem to solve
## What must be true in the final draft
## Constraints
- scope constraints
- compatibility constraints
- terminology constraints
## Source materials to prioritize
## Success criteria
## Questions for the team

View File

@@ -0,0 +1,23 @@
# Cycle Status
## Summary
- cycle:
- version:
- last updated:
## Artifact Status
- `00-user-spec.md`:
- `10-research-brief.md`:
- `20-architecture-brief.md`:
- `30-outline.md`:
- `40-draft-vN.md`:
- `50-reviews-vN/security.md`:
- `50-reviews-vN/software.md`:
- `50-reviews-vN/architecture.md`:
- `50-reviews-vN/ietf-senior.md`:
- `55-review-synthesis-vN.md`:
- `60-revision-plan-vN.md`:
## Notes

View File

@@ -0,0 +1,13 @@
# Research Brief
## Problem framing
## Evidence from existing drafts
## Overlap and adjacent work
## Gaps and unresolved questions
## Additional data worth investigating
## Recommendation to the architect

View File

@@ -0,0 +1,17 @@
# Architecture Brief
## Scope
## Non-goals
## Terminology and actors
## Protocol or data model shape
## Normative requirements candidates
## Security, privacy, and abuse considerations
## IANA impact
## Open design questions

Some files were not shown because too many files have changed in this diff Show More