Add ledger-optional operational modes (point-to-point, deferred, full)
ECTs can now be deployed without a centralized ledger. Three modes are defined: point-to-point (agents pass parent ECTs inline via HTTP headers), deferred ledger (collect ECTs in-flight, submit later), and full ledger (immediate append, RECOMMENDED for regulated environments). DAG validation is generalized to work against an "ECT store" which can be either a ledger or the set of inline parent ECTs received in the request. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -79,6 +79,8 @@ informative:
|
||||
author:
|
||||
- org: Cloud Native Computing Foundation
|
||||
I-D.ietf-scitt-architecture:
|
||||
I-D.ietf-oauth-transaction-tokens:
|
||||
I-D.ietf-oauth-transaction-tokens-for-agents:
|
||||
|
||||
--- abstract
|
||||
|
||||
@@ -184,6 +186,8 @@ This document defines:
|
||||
- Integration with the WIMSE identity framework
|
||||
({{wimse-integration}})
|
||||
- An HTTP header for ECT transport ({{http-header}})
|
||||
- Operational modes including ledger-optional deployment
|
||||
({{operational-modes}})
|
||||
- Audit ledger interface requirements ({{ledger-interface}})
|
||||
|
||||
The following are out of scope and are handled by WIMSE:
|
||||
@@ -729,6 +733,13 @@ enabling auditors to reconstruct the complete workflow and verify
|
||||
that required predecessor tasks were recorded before dependent
|
||||
tasks.
|
||||
|
||||
DAG validation can be performed against an audit ledger (when
|
||||
available) or against parent ECTs received inline via
|
||||
Execution-Context headers (in point-to-point mode per
|
||||
{{operational-modes}}). The validation rules below use the term
|
||||
"ECT store" to refer to either the ledger or the set of inline
|
||||
parent ECTs available to the verifier.
|
||||
|
||||
## Validation Rules
|
||||
|
||||
When receiving and verifying an ECT, implementations MUST perform
|
||||
@@ -736,13 +747,14 @@ the following DAG validation steps:
|
||||
|
||||
1. Task ID Uniqueness: The "tid" claim MUST be unique within the
|
||||
applicable scope (the workflow identified by "wid", or the
|
||||
entire ledger if "wid" is absent). If a task with the same
|
||||
entire ECT store if "wid" is absent). If a task with the same
|
||||
"tid" already exists, the ECT MUST be rejected.
|
||||
|
||||
2. Parent Existence: Every task identifier listed in the "par"
|
||||
array MUST correspond to a task that has been previously
|
||||
recorded in the ledger. If any parent task is not found, the
|
||||
ECT MUST be rejected.
|
||||
array MUST correspond to a task that is available in the ECT
|
||||
store (either previously recorded in the ledger or received
|
||||
inline as a verified parent ECT). If any parent task is not
|
||||
found, the ECT MUST be rejected.
|
||||
|
||||
3. Temporal Ordering: The "iat" value of every parent task MUST
|
||||
NOT be greater than the "iat" value of the current task plus a
|
||||
@@ -751,7 +763,7 @@ the following DAG validation steps:
|
||||
clock_skew_tolerance`. The tolerance accounts for clock skew
|
||||
between agents; it does not guarantee strict causal ordering
|
||||
from timestamps alone. Causal ordering is primarily enforced
|
||||
by the DAG structure (parent existence in the ledger), not by
|
||||
by the DAG structure (parent existence in the ECT store), not by
|
||||
timestamps. If any parent task violates this constraint, the
|
||||
ECT MUST be rejected.
|
||||
|
||||
@@ -776,14 +788,15 @@ the following DAG validation steps:
|
||||
The following pseudocode describes the DAG validation procedure:
|
||||
|
||||
~~~ pseudocode
|
||||
function validate_dag(ect, ledger, clock_skew_tolerance):
|
||||
function validate_dag(ect, ect_store, clock_skew_tolerance):
|
||||
// ect_store: ledger or local cache of verified ECTs
|
||||
// Step 1: Uniqueness check
|
||||
if ledger.contains(ect.tid, ect.wid):
|
||||
return error("Task ID already exists in ledger")
|
||||
if ect_store.contains(ect.tid, ect.wid):
|
||||
return error("Task ID already exists")
|
||||
|
||||
// Step 2: Parent existence and temporal ordering
|
||||
for parent_id in ect.par:
|
||||
parent = ledger.get(parent_id)
|
||||
parent = ect_store.get(parent_id)
|
||||
if parent is null:
|
||||
return error("Parent task not found: " + parent_id)
|
||||
if parent.iat >= ect.iat + clock_skew_tolerance:
|
||||
@@ -791,15 +804,15 @@ function validate_dag(ect, ledger, clock_skew_tolerance):
|
||||
|
||||
// Step 3: Cycle detection (with traversal limit)
|
||||
visited = set()
|
||||
result = has_cycle(ect.tid, ect.par, ledger, visited,
|
||||
result = has_cycle(ect.tid, ect.par, ect_store, visited,
|
||||
max_ancestor_limit)
|
||||
if result is error or result is true:
|
||||
return error("Circular dependency or depth limit exceeded")
|
||||
|
||||
return success
|
||||
|
||||
function has_cycle(target_tid, parent_ids, ledger, visited,
|
||||
max_depth):
|
||||
function has_cycle(target_tid, parent_ids, ect_store,
|
||||
visited, max_depth):
|
||||
if visited.size() >= max_depth:
|
||||
return error("Maximum ancestor traversal limit exceeded")
|
||||
for parent_id in parent_ids:
|
||||
@@ -808,9 +821,9 @@ function has_cycle(target_tid, parent_ids, ledger, visited,
|
||||
if parent_id in visited:
|
||||
continue
|
||||
visited.add(parent_id)
|
||||
parent = ledger.get(parent_id)
|
||||
parent = ect_store.get(parent_id)
|
||||
if parent is not null:
|
||||
result = has_cycle(target_tid, parent.par, ledger,
|
||||
result = has_cycle(target_tid, parent.par, ect_store,
|
||||
visited, max_depth)
|
||||
if result is error or result is true:
|
||||
return result
|
||||
@@ -885,8 +898,10 @@ verification steps in order:
|
||||
|
||||
14. Perform DAG validation per {{dag-validation}}.
|
||||
|
||||
15. If all checks pass, the ECT MUST be appended to the audit
|
||||
ledger.
|
||||
15. If all checks pass and an audit ledger is available, the ECT
|
||||
SHOULD be appended to the audit ledger. In point-to-point
|
||||
mode ({{operational-modes}}), the verified ECT is retained
|
||||
locally by the receiving agent.
|
||||
|
||||
If any verification step fails, the ECT MUST be rejected and the
|
||||
failure MUST be logged for audit purposes. Error messages
|
||||
@@ -965,22 +980,91 @@ function verify_ect(ect_jws, verifier_id,
|
||||
["approved", "rejected", "pending_human_review"]:
|
||||
return reject("Invalid pol_decision value")
|
||||
|
||||
// Validate DAG
|
||||
result = validate_dag(payload, ledger,
|
||||
// Validate DAG (against ledger or inline parent ECTs)
|
||||
result = validate_dag(payload, ect_store,
|
||||
clock_skew_tolerance)
|
||||
if result is error:
|
||||
return reject("DAG validation failed")
|
||||
|
||||
// All checks passed; append to ledger
|
||||
// All checks passed
|
||||
if ledger is available:
|
||||
ledger.append(payload)
|
||||
else:
|
||||
// Point-to-point mode: retain locally
|
||||
local_ect_cache.store(payload)
|
||||
return accept
|
||||
~~~
|
||||
{: #fig-verification title="ECT Verification Pseudocode"}
|
||||
|
||||
# Operational Modes {#operational-modes}
|
||||
|
||||
ECTs can be deployed in three operational modes depending on the
|
||||
availability and requirements of the deployment environment. All
|
||||
modes use the same ECT format and verification procedure; they
|
||||
differ in how parent ECTs are resolved during DAG validation and
|
||||
where verified ECTs are stored.
|
||||
|
||||
## Point-to-Point Mode {#point-to-point-mode}
|
||||
|
||||
In point-to-point mode, agents pass parent ECTs directly to
|
||||
downstream agents via multiple Execution-Context HTTP headers.
|
||||
No centralized ledger is required. The receiving agent verifies
|
||||
each parent ECT independently and validates the DAG against the
|
||||
set of ECTs received in the request.
|
||||
|
||||
This mode is suitable for:
|
||||
|
||||
- Cross-organizational workflows where no shared ledger exists
|
||||
- Lightweight deployments where infrastructure is constrained
|
||||
- Early adoption scenarios before ledger infrastructure is
|
||||
available
|
||||
|
||||
Limitations of point-to-point mode:
|
||||
|
||||
- No persistent audit trail unless agents independently retain
|
||||
ECTs
|
||||
- Global replay detection relies solely on "jti" caches at each
|
||||
agent; there is no centralized "tid" uniqueness check
|
||||
- The parent ECT chain grows with each hop, increasing HTTP
|
||||
header size
|
||||
- Post-hoc audit reconstruction requires collecting ECTs from
|
||||
multiple agents
|
||||
|
||||
Agents operating in point-to-point mode MUST retain verified
|
||||
parent ECTs for at least the duration of the workflow to support
|
||||
DAG validation of downstream requests. Agents SHOULD persist
|
||||
ECTs locally for audit purposes even when no centralized ledger
|
||||
is available.
|
||||
|
||||
## Deferred Ledger Mode {#deferred-ledger-mode}
|
||||
|
||||
In deferred ledger mode, agents create and verify ECTs in-flight
|
||||
using point-to-point delivery, and submit collected ECTs to an
|
||||
audit ledger after the workflow completes or at periodic intervals.
|
||||
|
||||
This mode decouples real-time workflow execution from ledger
|
||||
availability. DAG validation during execution uses inline parent
|
||||
ECTs; full DAG validation against the complete workflow is
|
||||
performed at ledger submission time.
|
||||
|
||||
Agents MUST include all collected ECTs when submitting to the
|
||||
ledger. The ledger MUST re-validate the complete DAG upon
|
||||
submission.
|
||||
|
||||
## Full Ledger Mode {#full-ledger-mode}
|
||||
|
||||
In full ledger mode, every verified ECT is immediately appended
|
||||
to an audit ledger. DAG validation is performed against the
|
||||
ledger, which serves as the authoritative ECT store. This is
|
||||
the RECOMMENDED mode for regulated environments where persistent,
|
||||
centralized audit trails are required.
|
||||
|
||||
# Audit Ledger Interface {#ledger-interface}
|
||||
|
||||
## Overview
|
||||
|
||||
Use of an audit ledger is RECOMMENDED for regulated environments
|
||||
and any deployment requiring persistent, centralized audit trails.
|
||||
ECTs are designed to be recorded in an immutable audit ledger for
|
||||
compliance verification and post-hoc analysis. This specification
|
||||
defines the logical interface for the ledger but does not mandate
|
||||
@@ -1185,6 +1269,7 @@ a cryptographic link to the original task:
|
||||
"aud": "spiffe://bank.example/system/ledger",
|
||||
"iat": 1772150550,
|
||||
"exp": 1772151150,
|
||||
"jti": "e4f5a6b7-c8d9-0123-ef01-234567890abc",
|
||||
"wid": "d3e4f5a6-b7c8-9012-def0-123456789012",
|
||||
"tid": "550e8400-e29b-41d4-a716-446655440099",
|
||||
"exec_act": "initiate_trade_rollback",
|
||||
@@ -1654,16 +1739,63 @@ ECTs record "what did this agent do?" and "what policy was
|
||||
evaluated?" Together they form an identity-plus-accountability
|
||||
framework for regulated agentic systems.
|
||||
|
||||
## OAuth 2.0 Token Exchange
|
||||
## OAuth 2.0 Token Exchange and the "act" Claim
|
||||
{:numbered="false"}
|
||||
|
||||
{{RFC8693}} defines the OAuth 2.0 Token Exchange protocol and
|
||||
registers the "act" (Actor) claim in the JWT Claims registry.
|
||||
The "act" claim creates nested JSON objects representing a
|
||||
delegation chain: "who is acting on behalf of whom." While
|
||||
the nesting superficially resembles a chain, it is strictly
|
||||
linear (each "act" object contains at most one nested "act"),
|
||||
represents authorization delegation rather than task execution,
|
||||
and carries no task identifiers, policy decisions, or
|
||||
input/output integrity data. The "act" chain cannot represent
|
||||
branching (fan-out) or convergence (fan-in) and therefore
|
||||
cannot form a DAG.
|
||||
|
||||
ECTs intentionally use the distinct claim name "exec_act" for the
|
||||
action/task type to avoid collision with the "act" claim.
|
||||
Transaction tokens in OAuth establish API authorization context;
|
||||
ECTs serve the complementary purpose of recording execution
|
||||
accountability across multi-step workflows.
|
||||
action/task type to avoid collision with the "act" claim. The
|
||||
two concepts are orthogonal: "act" records "who authorized whom,"
|
||||
ECTs record "what was done, in what order, with what policy
|
||||
outcomes."
|
||||
|
||||
## Transaction Tokens
|
||||
{:numbered="false"}
|
||||
|
||||
OAuth Transaction Tokens {{I-D.ietf-oauth-transaction-tokens}}
|
||||
propagate authorization context across workload call chains.
|
||||
The Txn-Token "req_wl" claim accumulates a comma-separated list
|
||||
of workloads that requested replacement tokens, which is the
|
||||
closest existing mechanism to call-chain recording.
|
||||
|
||||
However, "req_wl" cannot form a DAG because:
|
||||
|
||||
- It is linear: a comma-separated string with no branching or
|
||||
merging representation. When a workload fans out to multiple
|
||||
downstream services, each receives the same "req_wl" value and
|
||||
the branching is invisible.
|
||||
- It is incomplete: only workloads that request a replacement
|
||||
token from the Transaction Token Service appear in "req_wl";
|
||||
workloads that forward the token unchanged are not recorded.
|
||||
- It carries no task-level granularity, no parent references,
|
||||
no policy evaluation outcomes, and no execution content.
|
||||
|
||||
Extensions for agentic use cases
|
||||
({{I-D.ietf-oauth-transaction-tokens-for-agents}}) add agent
|
||||
identity and constraints ("agentic_ctx") but no execution
|
||||
ordering or DAG structure.
|
||||
|
||||
ECTs and Transaction Tokens are complementary: a Txn-Token
|
||||
propagates authorization context ("this request is authorized
|
||||
for scope X on behalf of user Y"), while an ECT records
|
||||
execution accountability ("task T was performed, depending on
|
||||
tasks P1 and P2, with policy Z evaluated and approved"). An
|
||||
agent request could carry both a Txn-Token for authorization
|
||||
and an ECT for execution recording. The WPT "tth" claim
|
||||
defined in {{I-D.ietf-wimse-s2s-protocol}} can hash-bind a
|
||||
WPT to a co-present Txn-Token; a similar binding mechanism
|
||||
for ECTs is a potential future extension.
|
||||
|
||||
## Distributed Tracing (OpenTelemetry)
|
||||
{:numbered="false"}
|
||||
@@ -1737,7 +1869,9 @@ A minimal conforming implementation needs to:
|
||||
3. Verify ECT signatures against WIT public keys.
|
||||
4. Perform DAG validation (parent existence, temporal ordering,
|
||||
cycle detection).
|
||||
5. Append verified ECTs to an audit ledger.
|
||||
5. Store verified ECTs (append to audit ledger in full ledger
|
||||
mode, or retain locally in point-to-point mode per
|
||||
{{operational-modes}}).
|
||||
|
||||
## Storage Recommendations
|
||||
{:numbered="false"}
|
||||
@@ -1817,6 +1951,7 @@ ECT Payload:
|
||||
"aud": "spiffe://example.com/agent/validator",
|
||||
"iat": 1772064150,
|
||||
"exp": 1772064750,
|
||||
"jti": "1a2b3c4d-e5f6-7890-abcd-ef0123456701",
|
||||
"wid": "b1c2d3e4-f5a6-7890-bcde-f01234567890",
|
||||
"tid": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"exec_act": "fetch_patient_data",
|
||||
@@ -1840,6 +1975,7 @@ task, and creates its own ECT:
|
||||
"aud": "spiffe://example.com/system/ledger",
|
||||
"iat": 1772064160,
|
||||
"exp": 1772064760,
|
||||
"jti": "2b3c4d5e-f6a7-8901-bcde-f01234567802",
|
||||
"wid": "b1c2d3e4-f5a6-7890-bcde-f01234567890",
|
||||
"tid": "550e8400-e29b-41d4-a716-446655440002",
|
||||
"exec_act": "validate_safety",
|
||||
@@ -1875,6 +2011,7 @@ Task 1 (Spec Review Agent):
|
||||
"aud": "spiffe://meddev.example/agent/code-gen",
|
||||
"iat": 1772064150,
|
||||
"exp": 1772064750,
|
||||
"jti": "3c4d5e6f-a7b8-9012-cdef-012345678903",
|
||||
"wid": "c2d3e4f5-a6b7-8901-cdef-012345678901",
|
||||
"tid": "a1b2c3d4-0001-0000-0000-000000000001",
|
||||
"exec_act": "review_requirements_spec",
|
||||
@@ -1897,6 +2034,7 @@ Task 2 (Code Generation Agent):
|
||||
"aud": "spiffe://meddev.example/agent/test-runner",
|
||||
"iat": 1772064200,
|
||||
"exp": 1772064800,
|
||||
"jti": "4d5e6f7a-b8c9-0123-def0-123456789004",
|
||||
"wid": "c2d3e4f5-a6b7-8901-cdef-012345678901",
|
||||
"tid": "a1b2c3d4-0001-0000-0000-000000000002",
|
||||
"exec_act": "implement_module",
|
||||
@@ -1917,6 +2055,7 @@ Task 3 (Autonomous Test Agent):
|
||||
"aud": "spiffe://meddev.example/agent/build",
|
||||
"iat": 1772064260,
|
||||
"exp": 1772064860,
|
||||
"jti": "5e6f7a8b-c9d0-1234-ef01-234567890005",
|
||||
"wid": "c2d3e4f5-a6b7-8901-cdef-012345678901",
|
||||
"tid": "a1b2c3d4-0001-0000-0000-000000000003",
|
||||
"exec_act": "execute_test_suite",
|
||||
@@ -1937,6 +2076,7 @@ Task 4 (Build Agent):
|
||||
"aud": "spiffe://meddev.example/human/release-mgr-42",
|
||||
"iat": 1772064310,
|
||||
"exp": 1772064910,
|
||||
"jti": "6f7a8b9c-d0e1-2345-f012-345678900006",
|
||||
"wid": "c2d3e4f5-a6b7-8901-cdef-012345678901",
|
||||
"tid": "a1b2c3d4-0001-0000-0000-000000000004",
|
||||
"exec_act": "build_release_artifact",
|
||||
@@ -1957,6 +2097,7 @@ Task 5 (Human Release Manager Approval):
|
||||
"aud": "spiffe://meddev.example/system/ledger",
|
||||
"iat": 1772064510,
|
||||
"exp": 1772065110,
|
||||
"jti": "7a8b9c0d-e1f2-3456-0123-456789000007",
|
||||
"wid": "c2d3e4f5-a6b7-8901-cdef-012345678901",
|
||||
"tid": "a1b2c3d4-0001-0000-0000-000000000005",
|
||||
"exec_act": "approve_release",
|
||||
@@ -2025,6 +2166,7 @@ Task 004 ECT payload:
|
||||
"aud": "spiffe://bank.example/system/ledger",
|
||||
"iat": 1772064250,
|
||||
"exp": 1772064850,
|
||||
"jti": "8b9c0d1e-f2a3-4567-1234-567890000008",
|
||||
"wid": "d3e4f5a6-b7c8-9012-def0-123456789012",
|
||||
"tid": "f1e2d3c4-0004-0000-0000-000000000004",
|
||||
"exec_act": "execute_trade",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user