feat: add draft data, gap analysis report, and workspace config
This commit is contained in:
BIN
data/drafts.sql.gz
Normal file
BIN
data/drafts.sql.gz
Normal file
Binary file not shown.
@@ -0,0 +1,658 @@
|
|||||||
|
Internet-Draft AI/Agent WG
|
||||||
|
Intended status: standards-track March 2026
|
||||||
|
Expires: September 10, 2026
|
||||||
|
|
||||||
|
|
||||||
|
Cross-Organizational AI Agent Liability Attribution Framework (COALAF)
|
||||||
|
draft-ai-ai-agent-liability-00
|
||||||
|
|
||||||
|
Abstract
|
||||||
|
|
||||||
|
As AI agents increasingly operate autonomously across
|
||||||
|
organizational boundaries, determining liability when harm occurs
|
||||||
|
becomes complex and legally ambiguous. This document defines a
|
||||||
|
standardized framework for establishing liability attribution
|
||||||
|
chains when AI agents from different organizations interact and
|
||||||
|
cause harm. The framework introduces liability anchor points,
|
||||||
|
cross-organizational liability contracts, and standardized
|
||||||
|
evidence collection mechanisms that integrate with existing
|
||||||
|
accountability protocols. COALAF enables insurance providers,
|
||||||
|
legal systems, and organizations to establish clear liability
|
||||||
|
boundaries before autonomous interactions occur, reducing
|
||||||
|
litigation costs and enabling broader AI agent deployment. The
|
||||||
|
framework builds upon existing cryptographic delegation protocols
|
||||||
|
and execution tracing standards to create tamper-evident liability
|
||||||
|
trails that can be validated across jurisdictions. This
|
||||||
|
specification addresses the gap between single-organization AI
|
||||||
|
safety standards and the reality of multi-party autonomous agent
|
||||||
|
ecosystems, providing a foundation for sustainable cross-
|
||||||
|
organizational AI collaboration.
|
||||||
|
|
||||||
|
Status of This Memo
|
||||||
|
|
||||||
|
This Internet-Draft is submitted in full conformance with the
|
||||||
|
provisions of BCP 78 and BCP 79.
|
||||||
|
|
||||||
|
This document is intended to have standards-track status.
|
||||||
|
Distribution of this memo is unlimited.
|
||||||
|
|
||||||
|
Table of Contents
|
||||||
|
|
||||||
|
1. Introduction ................................................ 3
|
||||||
|
2. Terminology ................................................. 4
|
||||||
|
3. Problem Statement ........................................... 5
|
||||||
|
4. Liability Attribution Architecture .......................... 6
|
||||||
|
5. Cross-Organizational Liability Contracts .................... 7
|
||||||
|
6. Evidence Collection and Validation .......................... 8
|
||||||
|
7. Liability Resolution Procedures ............................. 9
|
||||||
|
8. Security Considerations ..................................... 10
|
||||||
|
9. IANA Considerations ......................................... 11
|
||||||
|
|
||||||
|
1. Introduction
|
||||||
|
|
||||||
|
As artificial intelligence agents become increasingly autonomous
|
||||||
|
and capable of operating across organizational boundaries, the
|
||||||
|
question of liability attribution when these systems cause harm
|
||||||
|
has emerged as a critical challenge for both technical and legal
|
||||||
|
communities. Unlike traditional software systems where liability
|
||||||
|
typically follows clear organizational ownership patterns,
|
||||||
|
autonomous AI agents may make decisions, enter into agreements,
|
||||||
|
and cause harm through complex chains of interaction that span
|
||||||
|
multiple organizations, jurisdictions, and legal frameworks.
|
||||||
|
Current liability attribution mechanisms, designed primarily for
|
||||||
|
single-organization contexts or human-mediated transactions, prove
|
||||||
|
insufficient when autonomous agents from different organizations
|
||||||
|
interact independently to produce harmful outcomes.
|
||||||
|
|
||||||
|
The proliferation of cross-organizational AI agent interactions in
|
||||||
|
domains such as automated trading, supply chain management, and
|
||||||
|
autonomous vehicle coordination has exposed fundamental gaps in
|
||||||
|
existing accountability frameworks. When an AI agent from
|
||||||
|
Organization A interacts with an agent from Organization B, and
|
||||||
|
their combined autonomous decisions result in harm to Organization
|
||||||
|
C, determining which organization bears primary liability requires
|
||||||
|
examination of decision-making processes, data contributions, and
|
||||||
|
contractual relationships that may not have been explicitly
|
||||||
|
documented or agreed upon in advance. Traditional approaches that
|
||||||
|
rely on post-incident investigation and human testimony become
|
||||||
|
inadequate when dealing with autonomous systems that may process
|
||||||
|
thousands of interactions per second across multiple
|
||||||
|
organizational boundaries.
|
||||||
|
|
||||||
|
Existing technical standards for AI accountability, including
|
||||||
|
execution tracing protocols defined in various industry frameworks
|
||||||
|
and cryptographic delegation mechanisms outlined in emerging
|
||||||
|
Internet-Drafts, address intra-organizational liability but do not
|
||||||
|
provide mechanisms for cross-organizational liability attribution.
|
||||||
|
Legal frameworks similarly struggle with autonomous agent
|
||||||
|
liability, as they typically assume human decision-makers can be
|
||||||
|
identified and held accountable for system behavior. The resulting
|
||||||
|
ambiguity creates significant barriers to cross-organizational AI
|
||||||
|
collaboration, as organizations face unlimited and unpredictable
|
||||||
|
liability exposure when their agents interact with external
|
||||||
|
autonomous systems.
|
||||||
|
|
||||||
|
This document addresses these challenges by defining the Cross-
|
||||||
|
Organizational AI Agent Liability Attribution Framework (COALAF),
|
||||||
|
which establishes standardized mechanisms for pre-establishing
|
||||||
|
liability boundaries, collecting tamper-evident evidence of cross-
|
||||||
|
organizational agent interactions, and resolving liability
|
||||||
|
disputes through automated and semi-automated procedures. The
|
||||||
|
framework builds upon existing cryptographic protocols and extends
|
||||||
|
current accountability standards to support multi-party scenarios
|
||||||
|
where autonomous agents operate independently across
|
||||||
|
organizational boundaries. By providing clear technical and
|
||||||
|
procedural foundations for liability attribution, COALAF enables
|
||||||
|
organizations to engage in cross-organizational AI collaboration
|
||||||
|
while maintaining predictable and manageable liability exposure.
|
||||||
|
|
||||||
|
2. Terminology
|
||||||
|
|
||||||
|
This section defines terminology used throughout this
|
||||||
|
specification. The key words "MUST", "MUST NOT", "REQUIRED",
|
||||||
|
"SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT
|
||||||
|
RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be
|
||||||
|
interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and
|
||||||
|
only when, they appear in all capitals, as shown here.
|
||||||
|
|
||||||
|
**Liability Anchor Point**: A cryptographically-identified
|
||||||
|
decision point within an AI agent's execution where liability
|
||||||
|
attribution can be definitively established. Each anchor point
|
||||||
|
MUST contain sufficient contextual information to determine the
|
||||||
|
responsible organization, the decision rationale, and the
|
||||||
|
preceding chain of interactions that led to the decision.
|
||||||
|
Liability anchor points serve as immutable checkpoints in the
|
||||||
|
attribution chain and MUST be implemented using tamper-evident
|
||||||
|
cryptographic signatures as specified in [RFC9162].
|
||||||
|
|
||||||
|
**Cross-Organizational Liability Contract (COLC)**: A machine-
|
||||||
|
readable contract that establishes liability boundaries,
|
||||||
|
attribution procedures, and resolution mechanisms between two or
|
||||||
|
more organizations before their AI agents interact. COLCs MUST
|
||||||
|
specify liability caps, evidence requirements, dispute resolution
|
||||||
|
procedures, and applicable jurisdictions. These contracts build
|
||||||
|
upon existing smart contract frameworks but include specific
|
||||||
|
provisions for autonomous agent interactions and MUST be digitally
|
||||||
|
signed by authorized representatives from each participating
|
||||||
|
organization.
|
||||||
|
|
||||||
|
**Liability Attribution Chain**: An ordered sequence of liability
|
||||||
|
anchor points that traces the causal path from an autonomous
|
||||||
|
interaction to resulting harm. Each chain MUST maintain
|
||||||
|
cryptographic integrity through hash-linked structures and SHOULD
|
||||||
|
include timestamps, decision contexts, and inter-organizational
|
||||||
|
handoff points. Attribution chains serve as the primary evidence
|
||||||
|
artifact for liability determination and MUST be constructed in
|
||||||
|
real-time during agent interactions to ensure completeness and
|
||||||
|
authenticity.
|
||||||
|
|
||||||
|
**Autonomous Interaction Context**: The operational environment
|
||||||
|
and circumstances under which AI agents from different
|
||||||
|
organizations interact without direct human supervision. This
|
||||||
|
context MUST include the triggering conditions, available
|
||||||
|
resources, active constraints, and applicable liability contracts.
|
||||||
|
The context serves as the foundational framework for liability
|
||||||
|
attribution and MUST be established and agreed upon by all
|
||||||
|
participating organizations before autonomous interactions
|
||||||
|
commence.
|
||||||
|
|
||||||
|
**Liability Attribution Authority (LAA)**: An entity responsible
|
||||||
|
for validating attribution chains, interpreting cross-
|
||||||
|
organizational liability contracts, and facilitating dispute
|
||||||
|
resolution. LAAs MAY be implemented as distributed systems, third-
|
||||||
|
party arbitrators, or consortium-managed services. Each LAA MUST
|
||||||
|
maintain cryptographic credentials for chain validation and SHOULD
|
||||||
|
provide standardized APIs for liability inquiry and resolution as
|
||||||
|
defined in Section 7.
|
||||||
|
|
||||||
|
**Cross-Organizational Harm Event**: An occurrence where an
|
||||||
|
autonomous AI agent interaction between multiple organizations
|
||||||
|
results in measurable damage, loss, or negative impact to external
|
||||||
|
parties or participating organizations. Harm events trigger the
|
||||||
|
liability attribution process and MUST be reported to all
|
||||||
|
participating organizations within the timeframe specified in the
|
||||||
|
applicable COLC. The definition of harm MUST be established in the
|
||||||
|
cross-organizational liability contract and MAY include financial
|
||||||
|
loss, privacy violations, safety incidents, or regulatory
|
||||||
|
compliance failures.
|
||||||
|
|
||||||
|
3. Problem Statement
|
||||||
|
|
||||||
|
Current liability frameworks operate under the assumption that AI
|
||||||
|
agents function within well-defined organizational boundaries with
|
||||||
|
clear chains of command and responsibility. However, as autonomous
|
||||||
|
agents increasingly interact across organizational boundaries to
|
||||||
|
accomplish complex tasks, these frameworks encounter fundamental
|
||||||
|
limitations. When an AI agent from Organization A delegates a
|
||||||
|
subtask to an agent from Organization B, and that interaction
|
||||||
|
subsequently causes harm to a third party, existing legal and
|
||||||
|
technical systems lack standardized mechanisms to determine which
|
||||||
|
organization bears primary liability, secondary liability, or
|
||||||
|
contributory responsibility.
|
||||||
|
|
||||||
|
Consider a scenario where a logistics AI agent from Company A
|
||||||
|
contracts with a financial AI agent from Company B to process a
|
||||||
|
payment, which then interacts with a regulatory compliance agent
|
||||||
|
from Company C to verify transaction legality. If this chain of
|
||||||
|
autonomous interactions results in regulatory violations and
|
||||||
|
financial harm, current frameworks provide no standardized method
|
||||||
|
to trace liability attribution across the three organizations.
|
||||||
|
Each organization's internal accountability systems may function
|
||||||
|
correctly, but the lack of interoperable liability tracking
|
||||||
|
creates gaps where responsibility becomes legally ambiguous. The
|
||||||
|
problem intensifies when organizations operate under different
|
||||||
|
jurisdictions with varying liability standards and when agents
|
||||||
|
make autonomous decisions that were not explicitly programmed by
|
||||||
|
their respective organizations.
|
||||||
|
|
||||||
|
Existing accountability protocols such as those defined in draft-
|
||||||
|
ietf-rats-architecture focus primarily on attestation and
|
||||||
|
verification within single administrative domains. While these
|
||||||
|
protocols provide excellent foundations for establishing trust and
|
||||||
|
traceability, they do not address the legal and contractual
|
||||||
|
complexities that arise when autonomous agents create binding
|
||||||
|
commitments across organizational boundaries. Current AI
|
||||||
|
governance frameworks similarly concentrate on single-organization
|
||||||
|
risk management and fail to provide standardized mechanisms for
|
||||||
|
liability attribution when multiple organizations' agents
|
||||||
|
contribute to harmful outcomes through their autonomous
|
||||||
|
interactions.
|
||||||
|
|
||||||
|
The absence of standardized cross-organizational liability
|
||||||
|
attribution mechanisms creates several critical problems:
|
||||||
|
organizations become reluctant to allow their agents to interact
|
||||||
|
autonomously with external agents due to unclear liability
|
||||||
|
exposure, insurance providers cannot accurately assess risks
|
||||||
|
associated with multi-party AI agent interactions, and legal
|
||||||
|
systems lack consistent frameworks for resolving disputes when
|
||||||
|
autonomous agents cause harm through cross-organizational
|
||||||
|
collaborations. These gaps significantly limit the potential for
|
||||||
|
beneficial AI agent collaboration and create barriers to the
|
||||||
|
development of robust multi-organizational autonomous systems that
|
||||||
|
could provide substantial economic and social benefits.
|
||||||
|
|
||||||
|
4. Liability Attribution Architecture
|
||||||
|
|
||||||
|
The COALAF liability attribution architecture consists of three
|
||||||
|
core components that work together to establish clear liability
|
||||||
|
boundaries across organizational boundaries. The architecture
|
||||||
|
builds upon existing accountability protocols defined in RFC 8520
|
||||||
|
(Manufacturer Usage Description) and draft standards for AI agent
|
||||||
|
traceability to ensure compatibility with current organizational
|
||||||
|
infrastructure. Each component operates independently while
|
||||||
|
maintaining cryptographic links to create an immutable attribution
|
||||||
|
chain that can be validated by legal systems and insurance
|
||||||
|
providers.
|
||||||
|
|
||||||
|
Liability anchor points serve as the foundational elements of the
|
||||||
|
attribution architecture, representing specific moments in cross-
|
||||||
|
organizational agent interactions where liability responsibility
|
||||||
|
transfers between organizations. Each anchor point MUST contain a
|
||||||
|
unique identifier, timestamp, organizational context, agent state
|
||||||
|
information, and cryptographic proof of the interaction state at
|
||||||
|
the moment of transfer. Anchor points are established through
|
||||||
|
mutual agreement between participating organizations and are
|
||||||
|
digitally signed by both parties to prevent later disputes about
|
||||||
|
the interaction context. The anchor point structure follows a
|
||||||
|
standardized JSON schema that includes fields for liability
|
||||||
|
limits, coverage boundaries, and escalation procedures that were
|
||||||
|
pre-negotiated in the cross-organizational liability contracts.
|
||||||
|
|
||||||
|
Attribution chain structures provide the mechanism for linking
|
||||||
|
liability anchor points across multiple organizational boundaries
|
||||||
|
and agent interactions. Each attribution chain MUST maintain a
|
||||||
|
chronological sequence of anchor points, cryptographic hashes
|
||||||
|
linking each point to the next, and metadata describing the nature
|
||||||
|
of each inter-organizational transfer. The chain structure uses a
|
||||||
|
directed acyclic graph (DAG) format to handle complex scenarios
|
||||||
|
where multiple agents from different organizations contribute to a
|
||||||
|
single harmful outcome. Chain validation requires that each
|
||||||
|
participating organization can independently verify the integrity
|
||||||
|
of the entire chain using standard cryptographic verification
|
||||||
|
procedures defined in RFC 8032 (EdDSA signatures).
|
||||||
|
|
||||||
|
Cross-organizational contract templates define the standardized
|
||||||
|
formats and negotiation protocols that organizations use to
|
||||||
|
establish liability boundaries before agent interactions occur.
|
||||||
|
These templates MUST specify liability limits, coverage areas,
|
||||||
|
evidence collection requirements, and dispute resolution
|
||||||
|
procedures in a machine-readable format that autonomous agents can
|
||||||
|
process during runtime. The templates integrate with existing
|
||||||
|
contract negotiation protocols and support dynamic modification
|
||||||
|
based on interaction context, allowing organizations to adjust
|
||||||
|
liability boundaries for different types of agent tasks or risk
|
||||||
|
levels. Contract templates include provisions for insurance
|
||||||
|
integration, regulatory compliance across jurisdictions, and
|
||||||
|
compatibility with existing organizational risk management
|
||||||
|
frameworks.
|
||||||
|
|
||||||
|
The integration layer connects COALAF components with existing
|
||||||
|
accountability protocols through standardized APIs and data
|
||||||
|
exchange formats. Organizations MUST implement COALAF-compatible
|
||||||
|
interfaces that can generate liability anchor points, maintain
|
||||||
|
attribution chains, and enforce contract terms without requiring
|
||||||
|
modifications to existing agent architectures. The integration
|
||||||
|
layer supports both real-time liability tracking during agent
|
||||||
|
operations and post-incident reconstruction for liability
|
||||||
|
resolution procedures. This approach ensures that organizations
|
||||||
|
can adopt COALAF incrementally while maintaining compatibility
|
||||||
|
with current AI safety and accountability systems.
|
||||||
|
|
||||||
|
5. Cross-Organizational Liability Contracts
|
||||||
|
|
||||||
|
Cross-organizational liability contracts provide the foundational
|
||||||
|
legal and technical framework for establishing liability
|
||||||
|
boundaries before AI agents interact autonomously across
|
||||||
|
organizational boundaries. These contracts MUST be established
|
||||||
|
between participating organizations prior to enabling autonomous
|
||||||
|
agent interactions and SHOULD specify liability allocation
|
||||||
|
percentages, coverage limits, and dispute resolution mechanisms.
|
||||||
|
The contracts serve as legally binding agreements that define how
|
||||||
|
liability will be distributed when harm occurs during cross-
|
||||||
|
organizational agent interactions, eliminating the need for post-
|
||||||
|
incident liability negotiations that can result in prolonged
|
||||||
|
litigation.
|
||||||
|
|
||||||
|
The framework defines three standardized contract templates that
|
||||||
|
organizations MAY adopt based on their risk tolerance and
|
||||||
|
operational requirements: proportional liability contracts that
|
||||||
|
allocate liability based on each agent's contribution to the
|
||||||
|
harmful outcome, primary-secondary liability contracts that
|
||||||
|
designate one organization as primarily liable with fallback
|
||||||
|
provisions, and joint liability contracts where organizations
|
||||||
|
share equal responsibility regardless of individual agent
|
||||||
|
contributions. Each template MUST include mandatory fields for
|
||||||
|
liability caps, insurance requirements, governing jurisdiction,
|
||||||
|
and compatibility with existing accountability protocols as
|
||||||
|
defined in Section 4. Organizations SHOULD negotiate contract
|
||||||
|
terms through the standardized Liability Contract Negotiation
|
||||||
|
Protocol (LCNP) which enables automated contract parameter
|
||||||
|
exchange and compatibility verification between different
|
||||||
|
organizational liability frameworks.
|
||||||
|
|
||||||
|
Contract formats MUST be machine-readable to enable autonomous
|
||||||
|
agent processing during runtime liability decisions and evidence
|
||||||
|
collection procedures. The framework specifies the Cross-
|
||||||
|
Organizational Liability Contract Language (COLCL), an extension
|
||||||
|
of existing contract specification languages that includes
|
||||||
|
liability-specific constructs for dynamic liability calculation,
|
||||||
|
real-time insurance verification, and automated escalation
|
||||||
|
triggers. COLCL contracts MUST be digitally signed by authorized
|
||||||
|
organizational representatives and SHOULD be registered with
|
||||||
|
designated liability contract repositories to enable third-party
|
||||||
|
validation and enforcement. The language includes support for
|
||||||
|
conditional liability clauses that can adjust liability allocation
|
||||||
|
based on runtime factors such as agent behavior patterns,
|
||||||
|
environmental conditions, or detected security incidents.
|
||||||
|
|
||||||
|
Liability contracts MUST specify integration requirements with
|
||||||
|
existing cryptographic delegation protocols and execution tracing
|
||||||
|
standards to ensure evidence collected during agent interactions
|
||||||
|
can be properly attributed to contractual obligations. Contracts
|
||||||
|
SHOULD define liability anchor points that correspond to specific
|
||||||
|
interaction phases, enabling granular liability attribution when
|
||||||
|
multiple agents contribute to complex multi-step processes that
|
||||||
|
result in harm. The framework requires contracts to include
|
||||||
|
standardized liability resolution procedures that specify
|
||||||
|
automated calculation methods for damages, insurance claim
|
||||||
|
procedures, and escalation mechanisms for disputes that cannot be
|
||||||
|
resolved through automated processes.
|
||||||
|
|
||||||
|
Organizations MAY establish liability contract hierarchies for
|
||||||
|
complex multi-party scenarios where agents from more than two
|
||||||
|
organizations interact simultaneously. These hierarchical
|
||||||
|
contracts MUST maintain consistency with bilateral contracts and
|
||||||
|
SHOULD specify conflict resolution mechanisms when overlapping
|
||||||
|
liability boundaries create ambiguous attribution scenarios. The
|
||||||
|
framework supports contract amendments and versioning to
|
||||||
|
accommodate evolving organizational requirements while maintaining
|
||||||
|
backward compatibility with existing agent deployments and
|
||||||
|
ensuring that liability boundaries remain clearly defined
|
||||||
|
throughout the contract lifecycle.
|
||||||
|
|
||||||
|
6. Evidence Collection and Validation
|
||||||
|
|
||||||
|
During cross-organizational AI agent interactions, evidence
|
||||||
|
collection mechanisms MUST create tamper-evident logs that can be
|
||||||
|
independently verified by all participating organizations and
|
||||||
|
external auditors. The evidence collection system SHALL implement
|
||||||
|
cryptographic integrity protection using mechanisms compatible
|
||||||
|
with RFC 3161 timestamping services and MUST maintain
|
||||||
|
chronological ordering of all inter-agent communications and
|
||||||
|
decision points. Each participating organization MUST deploy
|
||||||
|
evidence collection endpoints that implement standardized logging
|
||||||
|
interfaces defined in this framework, ensuring that evidence
|
||||||
|
trails remain consistent across organizational boundaries even
|
||||||
|
when agents operate with different internal architectures.
|
||||||
|
|
||||||
|
Evidence records MUST include interaction context metadata, agent
|
||||||
|
decision rationales, and complete message traces between
|
||||||
|
organizations as specified in Section 4. The logging format SHALL
|
||||||
|
be based on structured data formats such as JSON-LD or Protocol
|
||||||
|
Buffers to ensure machine readability across different
|
||||||
|
organizational systems. Evidence collection points MUST capture
|
||||||
|
not only successful interactions but also failed attempts, timeout
|
||||||
|
conditions, and any agent behavior that deviates from pre-
|
||||||
|
established cross-organizational contracts. Each evidence record
|
||||||
|
SHALL include cryptographic signatures from all participating
|
||||||
|
agents and MUST reference the specific liability anchor points
|
||||||
|
established during interaction initiation.
|
||||||
|
|
||||||
|
Integration with existing execution tracing protocols SHOULD
|
||||||
|
leverage established frameworks such as OpenTelemetry distributed
|
||||||
|
tracing while extending them with liability-specific metadata
|
||||||
|
requirements. The evidence collection system MUST support real-
|
||||||
|
time evidence sharing between organizations during ongoing
|
||||||
|
interactions, allowing each party to maintain synchronized
|
||||||
|
evidence trails without exposing sensitive internal agent
|
||||||
|
architectures. Evidence validation procedures SHALL implement
|
||||||
|
multi-party verification protocols where each organization can
|
||||||
|
cryptographically attest to the accuracy of evidence records
|
||||||
|
without requiring trust in external parties.
|
||||||
|
|
||||||
|
Long-term evidence preservation requirements mandate that evidence
|
||||||
|
records remain accessible and verifiable for periods defined by
|
||||||
|
the applicable legal frameworks in each participating
|
||||||
|
jurisdiction, typically ranging from seven to twenty years.
|
||||||
|
Evidence storage systems MUST implement redundant backup
|
||||||
|
mechanisms and SHALL provide standardized APIs for evidence
|
||||||
|
retrieval during liability resolution procedures. The framework
|
||||||
|
defines evidence portability standards that enable migration
|
||||||
|
between different storage providers while maintaining
|
||||||
|
cryptographic integrity, ensuring that evidence remains valid even
|
||||||
|
as organizational infrastructure evolves. Evidence access controls
|
||||||
|
MUST balance transparency requirements for liability resolution
|
||||||
|
with privacy protections for sensitive business operations,
|
||||||
|
implementing role-based access mechanisms that can be audited by
|
||||||
|
regulatory authorities.
|
||||||
|
|
||||||
|
7. Liability Resolution Procedures
|
||||||
|
|
||||||
|
This section defines standardized procedures for resolving
|
||||||
|
liability disputes arising from cross-organizational AI agent
|
||||||
|
interactions. The liability resolution process operates in three
|
||||||
|
phases: automated attribution calculation, evidence validation,
|
||||||
|
and escalation procedures. Organizations deploying AI agents MUST
|
||||||
|
implement liability resolution endpoints that can process
|
||||||
|
attribution requests and respond with liability calculations based
|
||||||
|
on pre-established contracts and collected evidence. The
|
||||||
|
resolution procedures are designed to minimize human intervention
|
||||||
|
in straightforward cases while providing clear escalation paths
|
||||||
|
for complex disputes that require legal or technical review.
|
||||||
|
|
||||||
|
The automated liability calculation phase begins when a harm event
|
||||||
|
triggers the liability attribution process. The affected party's
|
||||||
|
liability resolution system MUST collect all relevant evidence
|
||||||
|
from the liability anchor points identified in the attribution
|
||||||
|
chain, validate the cryptographic integrity of the evidence using
|
||||||
|
the procedures defined in Section 6, and apply the liability
|
||||||
|
calculation rules specified in the applicable cross-organizational
|
||||||
|
liability contracts. The calculation engine SHOULD utilize
|
||||||
|
standardized liability algorithms that consider factors including
|
||||||
|
agent autonomy levels, contract-specified liability caps, and
|
||||||
|
proportional responsibility based on causal contribution to the
|
||||||
|
harm. If multiple organizations are involved in the attribution
|
||||||
|
chain, the system MUST coordinate liability calculations across
|
||||||
|
all parties and produce a preliminary liability distribution that
|
||||||
|
reflects each organization's contractual obligations and causal
|
||||||
|
involvement.
|
||||||
|
|
||||||
|
Evidence validation procedures ensure that liability calculations
|
||||||
|
are based on tamper-evident and cryptographically verifiable data.
|
||||||
|
Resolution systems MUST verify the integrity of all evidence
|
||||||
|
artifacts using the cryptographic signatures and hash chains
|
||||||
|
established during the original agent interactions. When evidence
|
||||||
|
validation fails or when evidence is missing from critical points
|
||||||
|
in the attribution chain, the system SHOULD flag the case for
|
||||||
|
manual review and MAY apply conservative liability assumptions as
|
||||||
|
specified in the relevant contracts. Organizations MUST maintain
|
||||||
|
evidence validation logs that record the success or failure of
|
||||||
|
each validation step, providing an audit trail for subsequent
|
||||||
|
legal proceedings if automated resolution is unsuccessful.
|
||||||
|
|
||||||
|
Escalation procedures activate when automated liability
|
||||||
|
calculation cannot produce a definitive resolution within the
|
||||||
|
confidence thresholds specified in the cross-organizational
|
||||||
|
contracts. Common escalation triggers include conflicting evidence
|
||||||
|
from different liability anchor points, liability calculations
|
||||||
|
that exceed contractual caps, or disputes involving organizations
|
||||||
|
that have not implemented compatible versions of COALAF. The
|
||||||
|
escalation process MUST preserve all evidence and preliminary
|
||||||
|
calculations while transferring the dispute to human reviewers or
|
||||||
|
designated arbitration systems. Organizations SHOULD implement
|
||||||
|
graduated escalation procedures that attempt technical resolution
|
||||||
|
through expert system review before proceeding to formal
|
||||||
|
arbitration or legal proceedings.
|
||||||
|
|
||||||
|
The liability resolution system MUST generate standardized
|
||||||
|
resolution reports that document the final liability attribution,
|
||||||
|
the evidence used in the calculation, and any escalation decisions
|
||||||
|
made during the process. These reports serve as the authoritative
|
||||||
|
record for insurance claims, legal proceedings, and organizational
|
||||||
|
accountability processes. Resolution reports MUST include machine-
|
||||||
|
readable sections that allow automated processing by insurance
|
||||||
|
systems and legal databases, as well as human-readable summaries
|
||||||
|
that explain the liability determination in accessible terms.
|
||||||
|
Organizations MAY implement resolution report notification systems
|
||||||
|
that automatically inform affected parties of liability
|
||||||
|
determinations and provide mechanisms for formal dispute of the
|
||||||
|
automated calculations within specified time frames.
|
||||||
|
|
||||||
|
8. Security Considerations
|
||||||
|
|
||||||
|
The security of cross-organizational liability attribution systems
|
||||||
|
presents unique challenges that extend beyond traditional single-
|
||||||
|
organization security models. Liability attribution chains MUST be
|
||||||
|
protected against tampering, unauthorized modification, and replay
|
||||||
|
attacks throughout their entire lifecycle, from initial contract
|
||||||
|
establishment through final dispute resolution. Organizations
|
||||||
|
implementing COALAF MUST employ cryptographic integrity protection
|
||||||
|
mechanisms that ensure liability evidence remains tamper-evident
|
||||||
|
across organizational boundaries and jurisdictional transfers. The
|
||||||
|
distributed nature of cross-organizational interactions creates
|
||||||
|
expanded attack surfaces where malicious actors may attempt to
|
||||||
|
manipulate liability assignments, forge evidence, or exploit
|
||||||
|
differences in security implementations between participating
|
||||||
|
organizations.
|
||||||
|
|
||||||
|
Evidence collection systems MUST implement strong cryptographic
|
||||||
|
signatures and hash-based integrity verification to prevent post-
|
||||||
|
hoc manipulation of liability-relevant data. Each liability anchor
|
||||||
|
point MUST cryptographically sign all evidence records using
|
||||||
|
organization-specific private keys, with public key verification
|
||||||
|
available through standardized certificate authorities or
|
||||||
|
blockchain-based key distribution systems. The evidence collection
|
||||||
|
mechanism SHOULD implement tamper-evident timestamps using trusted
|
||||||
|
timestamping services as specified in RFC 3161, ensuring that
|
||||||
|
liability events can be temporally ordered across different
|
||||||
|
organizational systems. Organizations MUST maintain cryptographic
|
||||||
|
audit trails that link evidence collection events to specific AI
|
||||||
|
agent actions, preventing evidence injection or selective omission
|
||||||
|
attacks that could skew liability determinations.
|
||||||
|
|
||||||
|
Contract manipulation represents a critical threat vector where
|
||||||
|
malicious organizations might attempt to alter liability terms
|
||||||
|
after autonomous interactions have commenced but before liability
|
||||||
|
events occur. Cross-organizational liability contracts MUST be
|
||||||
|
cryptographically sealed using multi-party digital signatures that
|
||||||
|
require explicit consent from all participating organizations for
|
||||||
|
any modifications. The contract verification system SHOULD
|
||||||
|
implement immutable storage mechanisms, such as distributed ledger
|
||||||
|
technologies or cryptographically-linked append-only logs, that
|
||||||
|
prevent unauthorized contract alterations. Organizations MUST
|
||||||
|
implement contract versioning systems that maintain complete
|
||||||
|
change histories and require cryptographic proof of authorized
|
||||||
|
modifications, ensuring that liability terms cannot be
|
||||||
|
retroactively altered to avoid responsibility.
|
||||||
|
|
||||||
|
Privacy protection mechanisms MUST balance the need for
|
||||||
|
comprehensive liability evidence with organizational
|
||||||
|
confidentiality requirements and regulatory compliance obligations
|
||||||
|
such as GDPR or CCPA. Evidence collection systems SHOULD implement
|
||||||
|
selective disclosure protocols that allow liability-relevant
|
||||||
|
information to be shared without exposing sensitive operational
|
||||||
|
data or proprietary algorithms. Organizations MAY employ zero-
|
||||||
|
knowledge proof systems to demonstrate compliance with liability
|
||||||
|
contracts without revealing underlying business logic or training
|
||||||
|
data. The framework MUST support privacy-preserving liability
|
||||||
|
calculations that enable automated liability distribution without
|
||||||
|
requiring full disclosure of internal agent decision processes to
|
||||||
|
external parties.
|
||||||
|
|
||||||
|
Cryptographic key management across organizational boundaries
|
||||||
|
introduces additional complexity that MUST be addressed through
|
||||||
|
standardized key exchange and rotation protocols. Organizations
|
||||||
|
MUST implement secure key escrow mechanisms that ensure liability
|
||||||
|
evidence remains accessible even if participating organizations
|
||||||
|
cease operations or become uncooperative during dispute resolution
|
||||||
|
processes. The liability attribution system SHOULD support
|
||||||
|
hierarchical key structures that allow delegation of signing
|
||||||
|
authority while maintaining clear chains of cryptographic
|
||||||
|
accountability. Cross-organizational key validation MUST be
|
||||||
|
supported through standardized certificate authorities or
|
||||||
|
decentralized key verification systems that remain operational
|
||||||
|
across different jurisdictional and organizational contexts.
|
||||||
|
|
||||||
|
Denial of service attacks against liability attribution systems
|
||||||
|
could prevent proper evidence collection during critical
|
||||||
|
autonomous interactions, potentially allowing harmful agents to
|
||||||
|
operate without adequate accountability mechanisms. Organizations
|
||||||
|
MUST implement redundant evidence collection systems and
|
||||||
|
distributed liability anchor points that maintain functionality
|
||||||
|
even when individual components are compromised or unavailable.
|
||||||
|
The framework SHOULD include fallback mechanisms that ensure
|
||||||
|
liability attribution continues to function during partial system
|
||||||
|
failures, network partitions, or coordinated attacks against
|
||||||
|
attribution infrastructure. Organizations MUST establish incident
|
||||||
|
response procedures for security breaches that affect liability
|
||||||
|
attribution systems, including mechanisms for evidence
|
||||||
|
preservation, stakeholder notification, and liability framework
|
||||||
|
recovery.
|
||||||
|
|
||||||
|
9. IANA Considerations
|
||||||
|
|
||||||
|
This document requires the creation of several new IANA registries
|
||||||
|
to support standardized cross-organizational AI agent liability
|
||||||
|
attribution. The registries are necessary to ensure consistent
|
||||||
|
identification and processing of liability-related information
|
||||||
|
across different organizations, legal jurisdictions, and technical
|
||||||
|
implementations. All registry entries MUST include sufficient
|
||||||
|
metadata to enable automated processing by AI agents while
|
||||||
|
maintaining human readability for legal and regulatory review.
|
||||||
|
|
||||||
|
IANA is requested to create a "Cross-Organizational AI Liability
|
||||||
|
Contract Types" registry under the "Artificial Intelligence
|
||||||
|
Parameters" category. This registry SHALL contain standardized
|
||||||
|
identifiers for different classes of liability contracts as
|
||||||
|
defined in Section 5, including but not limited to strict
|
||||||
|
liability contracts, proportional liability contracts, and
|
||||||
|
hierarchical liability contracts. Each registry entry MUST include
|
||||||
|
the contract type identifier (a case-sensitive string), a human-
|
||||||
|
readable description, the specification document reference, and
|
||||||
|
any required contract parameters. Registration of new contract
|
||||||
|
types requires Specification Required as defined in RFC 8126, with
|
||||||
|
the designated expert evaluating legal soundness, technical
|
||||||
|
feasibility, and compatibility with existing liability frameworks.
|
||||||
|
|
||||||
|
IANA is requested to establish the "AI Agent Liability Attribution
|
||||||
|
Chain Formats" registry to standardize the structure and encoding
|
||||||
|
of liability attribution chains described in Section 4. Each
|
||||||
|
format entry MUST specify the format identifier, the data
|
||||||
|
structure specification, cryptographic requirements, and
|
||||||
|
validation procedures. The registry SHALL include the default
|
||||||
|
JSON-LD format specified in this document as well as provisions
|
||||||
|
for compact binary formats and blockchain-based attribution
|
||||||
|
chains. New format registrations require Expert Review with
|
||||||
|
evaluation criteria including cryptographic security, cross-
|
||||||
|
jurisdictional compatibility, and integration capabilities with
|
||||||
|
existing accountability protocols such as those defined in RFC
|
||||||
|
9000 series documents.
|
||||||
|
|
||||||
|
A "Cross-Organizational Liability Status Codes" registry is
|
||||||
|
required to standardize the response codes used in liability
|
||||||
|
resolution procedures outlined in Section 7. The registry SHALL
|
||||||
|
use a three-digit numeric scheme similar to HTTP status codes,
|
||||||
|
with ranges allocated as follows: 1xx for informational liability
|
||||||
|
status, 2xx for successful liability resolution, 3xx for liability
|
||||||
|
redirection, 4xx for liability attribution errors, and 5xx for
|
||||||
|
system errors in liability processing. Each status code entry MUST
|
||||||
|
include the numeric code, canonical reason phrase, detailed
|
||||||
|
description, and applicable resolution procedures. Registration of
|
||||||
|
new status codes in the 1xx-3xx ranges requires IETF Review, while
|
||||||
|
4xx-5xx codes require Specification Required to ensure consistency
|
||||||
|
with error handling procedures across implementations.
|
||||||
|
|
||||||
|
The designated expert for all liability-related registries SHOULD
|
||||||
|
have demonstrated expertise in both AI system architecture and
|
||||||
|
legal liability frameworks. Registry maintenance procedures MUST
|
||||||
|
include periodic review of registered entries for continued
|
||||||
|
relevance and compatibility with evolving legal standards. All
|
||||||
|
registry entries SHALL include sunset clauses requiring renewal
|
||||||
|
every five years unless superseded by updated specifications,
|
||||||
|
ensuring that deprecated liability mechanisms do not accumulate in
|
||||||
|
the registries over time.
|
||||||
|
|
||||||
|
Author's Address
|
||||||
|
|
||||||
|
Generated by IETF Draft Analyzer
|
||||||
|
2026-03-09
|
||||||
261
workspace/act/act-blog-outline.md
Normal file
261
workspace/act/act-blog-outline.md
Normal 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
|
||||||
312
workspace/act/act-implementation-master-prompt.md
Normal file
312
workspace/act/act-implementation-master-prompt.md
Normal 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."
|
||||||
119
workspace/act/act/__init__.py
Normal file
119
workspace/act/act/__init__.py
Normal 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
467
workspace/act/act/crypto.py
Normal 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
136
workspace/act/act/dag.py
Normal 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)
|
||||||
333
workspace/act/act/delegation.py
Normal file
333
workspace/act/act/delegation.py
Normal 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
131
workspace/act/act/errors.py
Normal 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
152
workspace/act/act/ledger.py
Normal 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"
|
||||||
|
)
|
||||||
96
workspace/act/act/lifecycle.py
Normal file
96
workspace/act/act/lifecycle.py
Normal 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
734
workspace/act/act/token.py
Normal 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)}"
|
||||||
|
)
|
||||||
639
workspace/act/act/vectors.py
Normal file
639
workspace/act/act/vectors.py
Normal 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
323
workspace/act/act/verify.py
Normal 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
|
||||||
174
workspace/act/bench/bench_act.py
Normal file
174
workspace/act/bench/bench_act.py
Normal 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()
|
||||||
1427
workspace/act/draft-nennemann-act-00.md
Normal file
1427
workspace/act/draft-nennemann-act-00.md
Normal file
File diff suppressed because it is too large
Load Diff
145
workspace/act/tests/test_crypto.py
Normal file
145
workspace/act/tests/test_crypto.py
Normal 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
|
||||||
103
workspace/act/tests/test_dag.py
Normal file
103
workspace/act/tests/test_dag.py
Normal 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)
|
||||||
229
workspace/act/tests/test_delegation.py
Normal file
229
workspace/act/tests/test_delegation.py
Normal 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)
|
||||||
84
workspace/act/tests/test_ledger.py
Normal file
84
workspace/act/tests/test_ledger.py
Normal 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
|
||||||
103
workspace/act/tests/test_lifecycle.py
Normal file
103
workspace/act/tests/test_lifecycle.py
Normal 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
|
||||||
244
workspace/act/tests/test_token.py
Normal file
244
workspace/act/tests/test_token.py
Normal 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)
|
||||||
35
workspace/act/tests/test_vectors.py
Normal file
35
workspace/act/tests/test_vectors.py
Normal 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
|
||||||
191
workspace/act/tests/test_verify.py
Normal file
191
workspace/act/tests/test_verify.py
Normal 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"
|
||||||
59
workspace/draft-team/AGENTS.md
Normal file
59
workspace/draft-team/AGENTS.md
Normal 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
|
||||||
29
workspace/draft-team/Makefile
Normal file
29
workspace/draft-team/Makefile
Normal 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)"
|
||||||
95
workspace/draft-team/README.md
Normal file
95
workspace/draft-team/README.md
Normal 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.
|
||||||
43
workspace/draft-team/architect/AGENTS.md
Normal file
43
workspace/draft-team/architect/AGENTS.md
Normal 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.
|
||||||
30
workspace/draft-team/architecture-reviewer/AGENTS.md
Normal file
30
workspace/draft-team/architecture-reviewer/AGENTS.md
Normal 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.
|
||||||
30
workspace/draft-team/author/AGENTS.md
Normal file
30
workspace/draft-team/author/AGENTS.md
Normal 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.
|
||||||
@@ -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?
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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?
|
||||||
@@ -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
|
||||||
@@ -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`.
|
||||||
@@ -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`.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Architecture Review
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
## Residual risk
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# IETF Senior Review
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
## Residual publishability risk
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Security Review
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
## Residual risk
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Software Review
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
## Residual risk
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Review Synthesis
|
||||||
|
|
||||||
|
## Blocking findings
|
||||||
|
|
||||||
|
## Major findings
|
||||||
|
|
||||||
|
## Minor findings
|
||||||
|
|
||||||
|
## Conflicts resolved
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Revision Plan
|
||||||
|
|
||||||
|
## Blocking changes
|
||||||
|
|
||||||
|
## High-value improvements
|
||||||
|
|
||||||
|
## Deferred items
|
||||||
|
|
||||||
|
## Draft order for next iteration
|
||||||
@@ -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?
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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?
|
||||||
@@ -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
|
||||||
@@ -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`.
|
||||||
@@ -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`.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Architecture Review
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
## Residual risk
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# IETF Senior Review
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
## Residual publishability risk
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Security Review
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
## Residual risk
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Software Review
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
## Residual risk
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Review Synthesis
|
||||||
|
|
||||||
|
## Blocking findings
|
||||||
|
|
||||||
|
## Major findings
|
||||||
|
|
||||||
|
## Minor findings
|
||||||
|
|
||||||
|
## Conflicts resolved
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Revision Plan
|
||||||
|
|
||||||
|
## Blocking changes
|
||||||
|
|
||||||
|
## High-value improvements
|
||||||
|
|
||||||
|
## Deferred items
|
||||||
|
|
||||||
|
## Draft order for next iteration
|
||||||
25
workspace/draft-team/cycles/example-topic/00-user-spec.md
Normal file
25
workspace/draft-team/cycles/example-topic/00-user-spec.md
Normal 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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
9
workspace/draft-team/cycles/example-topic/30-outline.md
Normal file
9
workspace/draft-team/cycles/example-topic/30-outline.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Draft Outline
|
||||||
|
|
||||||
|
## Abstract
|
||||||
|
|
||||||
|
## Section plan
|
||||||
|
|
||||||
|
## Author guidance by section
|
||||||
|
|
||||||
|
## Issues that must not be hand-waved
|
||||||
21
workspace/draft-team/cycles/example-topic/40-draft-v1.md
Normal file
21
workspace/draft-team/cycles/example-topic/40-draft-v1.md
Normal 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
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Review Report
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
## Strengths
|
||||||
|
|
||||||
|
## Residual publishability risk
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Revision Plan
|
||||||
|
|
||||||
|
## Blocking changes
|
||||||
|
|
||||||
|
## High-value improvements
|
||||||
|
|
||||||
|
## Deferred items
|
||||||
|
|
||||||
|
## Draft order for next iteration
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Draft Outline
|
||||||
|
|
||||||
|
## Abstract
|
||||||
|
|
||||||
|
## Section plan
|
||||||
|
|
||||||
|
## Author guidance by section
|
||||||
|
|
||||||
|
## Issues that must not be hand-waved
|
||||||
21
workspace/draft-team/cycles/review-board-test/40-draft-v1.md
Normal file
21
workspace/draft-team/cycles/review-board-test/40-draft-v1.md
Normal 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
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Architecture Review
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
## Residual risk
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# IETF Senior Review
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
## Residual publishability risk
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Security Review
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
## Residual risk
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Software Review
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
## Residual risk
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Review Synthesis
|
||||||
|
|
||||||
|
## Blocking findings
|
||||||
|
|
||||||
|
## Major findings
|
||||||
|
|
||||||
|
## Minor findings
|
||||||
|
|
||||||
|
## Conflicts resolved
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Revision Plan
|
||||||
|
|
||||||
|
## Blocking changes
|
||||||
|
|
||||||
|
## High-value improvements
|
||||||
|
|
||||||
|
## Deferred items
|
||||||
|
|
||||||
|
## Draft order for next iteration
|
||||||
36
workspace/draft-team/ietf-senior-reviewer/AGENTS.md
Normal file
36
workspace/draft-team/ietf-senior-reviewer/AGENTS.md
Normal 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.
|
||||||
28
workspace/draft-team/references/analyzer-integration.md
Normal file
28
workspace/draft-team/references/analyzer-integration.md
Normal 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.
|
||||||
36
workspace/draft-team/researcher/AGENTS.md
Normal file
36
workspace/draft-team/researcher/AGENTS.md
Normal 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.
|
||||||
32
workspace/draft-team/review-lead/AGENTS.md
Normal file
32
workspace/draft-team/review-lead/AGENTS.md
Normal 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.
|
||||||
33
workspace/draft-team/scripts/new-cycle.sh
Executable file
33
workspace/draft-team/scripts/new-cycle.sh
Executable 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"
|
||||||
49
workspace/draft-team/scripts/role-target.sh
Executable file
49
workspace/draft-team/scripts/role-target.sh
Executable 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
|
||||||
70
workspace/draft-team/scripts/run-cycle.sh
Executable file
70
workspace/draft-team/scripts/run-cycle.sh
Executable 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
|
||||||
75
workspace/draft-team/scripts/update-status.sh
Executable file
75
workspace/draft-team/scripts/update-status.sh
Executable 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"
|
||||||
34
workspace/draft-team/security-reviewer/AGENTS.md
Normal file
34
workspace/draft-team/security-reviewer/AGENTS.md
Normal 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.
|
||||||
33
workspace/draft-team/software-reviewer/AGENTS.md
Normal file
33
workspace/draft-team/software-reviewer/AGENTS.md
Normal 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.
|
||||||
25
workspace/draft-team/templates/00-user-spec.md
Normal file
25
workspace/draft-team/templates/00-user-spec.md
Normal 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
|
||||||
23
workspace/draft-team/templates/05-status.md
Normal file
23
workspace/draft-team/templates/05-status.md
Normal 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
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user