v3/docs/adr/ADR-325-claim-federation-zero-trust-capability-plane.md
Status: Proposed — security-critical integration work required before production federation
Date: 2026-07-28
Decision owners: Ruflo security, claims, federation, and policy maintainers
Supersedes: ADR-101's production-readiness conclusion, but not its component design
Related: ADR-086, ADR-101, ADR-109, ADR-144, ADR-320, ADR-324,
ADR-326, ADR-327
Ruflo's claim federation is not optimally integrated today.
The repository contains useful federation components: work-claim domain models, hybrid logical clocks, vector clocks, signed federation envelopes, discovery, PII controls, A2A card support, and the ADR-324 policy engine. These pieces do not yet form one authenticated, authorized, replay-resistant, durable, and partition-safe production path.
This ADR adopts a single zero-trust capability plane with four distinct responsibilities:
The implementation will preserve local, single-node behavior. Federated ownership-changing operations will remain disabled until the critical correctness gates in this ADR pass.
The design uses existing standards at interoperability boundaries:
Ruflo does not require SPIRE, OAuth, or an external identity provider for local use. Provider interfaces retain the current local Ed25519 identity as the default compatibility profile.
Two companion profiles make this umbrella decision implementable without turning it into one unbounded programme:
Neither companion changes this ADR's trust model. Product actions and development leases are both evaluated by ADR-324 policy, but they remain different resource and capability domains.
ADR-101 correctly identified the components needed to move work claims across nodes. It then described those components as implemented. A production-path audit in July 2026 found that component presence and end-to-end enforcement had been conflated.
The term claim also names three different concepts in the current system:
| Current surface | Meaning | Current persistence/enforcement |
|---|---|---|
@claude-flow/claims | Exclusive work ownership, handoff, and stealing | Domain package with local in-memory adapters and optional federation wrappers |
ruflo claims | Local capability-like grants | .claude-flow/claims.json |
Federation PolicyEngine claims | Message-type authorization requirements | Independent enum; production dependency currently returns true |
A fourth, more complete authorization system now exists in
@claude-flow/security: the ADR-324 AgenticPolicyEngine. Maintaining four
partially overlapping authorities makes fail-open behavior and confused-deputy
errors likely.
This ADR uses the following normative vocabulary:
Legacy command and API names remain as compatibility aliases, but new code MUST use these terms.
The findings below describe the baseline that motivated this ADR. They are retained for traceability; the implementation-progress section immediately after the table records the opt-in remediations now present on this branch.
@claude-flow/claims has a clean domain/application/infrastructure split.FederationBridge, FederatedClaimEventStore, HLC, and vector-clock
components provide a useful protocol substrate.claim-event and agent-handoff, and
maps them to capability requirements.| Severity | Finding | Repository evidence | Consequence |
|---|---|---|---|
| Critical | Nested message values are not reliably covered by the outer signature | canonicalizeEnvelopeForVerify() uses a JSON.stringify property allowlist derived only from top-level keys | A nested payload mutation can canonicalize to the same signed bytes |
| Critical | Federation authorization is stubbed | Plugin constructs PolicyEngine({ checkClaim: () => true }) | Required claims do not restrict production sends |
| High | Inbound policy enforcement is absent | dispatchInbound() verifies peer state and the outer signature, then emits an event | No ingress authorization, PII/AI scan, rate limit, audience, tenant, expiry, or replay decision |
| High | Claim federation is not composed at runtime | Federation plugin has no dependency on @claude-flow/claims; no production construction of FederatedClaimRepository, FederatedClaimEventStore, or FederationBridge was found | Implemented components are not an operational feature |
| High | No inbound claim/handoff handler is registered | No production listener for federation:inbound:claim-event or federation:inbound:agent-handoff was found | Remote work events are delivered to no domain transaction |
| High | Exclusive ownership is eventually replicated without a fencing authority | Concurrent vector-clock writes are rejected locally, but no lease, quorum, or authoritative epoch exists | A partition can produce two actors that each believe they own the same work |
| High | Storage is neither durable nor atomic | Federated store wraps InMemoryClaimEventStore; publish failure reaches into a private event array to roll back | Crash boundaries can leave contradictory local and remote histories |
| Medium | Federated query is a placeholder | includeRemote is documented as a no-op | Callers may overestimate federation-wide visibility |
| Medium | The default message allowlist excludes claim messages | DEFAULT_SECURITY_POLICY.allowedMessageTypes omits claim-event and agent-handoff | Properly authorized sends are rejected unless policy is overridden |
| Medium | Replay fields are not enforced | Envelope metadata carries time/session data, but ingress has no nonce cache or freshness/audience check | A valid old ownership message can be replayed |
| Medium | A2A discovery is descriptive rather than trusted | Cards expose optional security fields, but local conversion does not populate them by default | A peer's advertised capability is not an authorization grant |
| Medium | Legacy local capability loading can fail open | A malformed/missing local claims file defaults non-admin checks to permissive behavior | Corruption can expand effective privilege |
The federation plugin now provides these opt-in foundations:
jcs-v1, advertised by default by upgraded peers and trusted only from a
signature-verified peer manifest, while retaining byte-for-byte
legacy-v1 verification;legacy, observe, and enforce authorization modes backed by a
concrete claim checker instead of an anonymous always-true callback;heartbeat and status-broadcast.Consequential and unknown legacy message types—including work claims, handoffs, task assignments, context/memory transfer, trust, topology, and spawn operations—are rejected before authorization or emission and cannot be sent from an upgraded peer to an endpoint-only or legacy-only peer. Endpoint-only joins remain untrusted and negotiate no optional protocol; providing the peer's signed manifest enables JCS without treating the current process as the remote handshake responder. This preserves low-risk health/status interoperability with previous installations without silently downgrading consequential traffic. Nested JCS tampering and verify-before-authorize-before-emit ordering are covered by tests.
This closes the first three baseline findings for the implemented plugin path, but it does not make the overall federation production-ready. Runtime composition with the claims repository, durable ingress/outbox storage, federated fencing authority, grant exchange, identity federation, revocation, and the production Cognitum enforcement points remain pending.
The concurrent Cognitum audit also found that its current website harness has
valuable path/resource leases, heartbeats, messages, ports, and run receipts,
but several writers share one dirty worktree and receipts bind only HEAD.
That coordination state cannot prove the exact source snapshot or fence a stale
writer. ADR-327 adopts the useful harness contract while retaining one
worktree per writer and adding source-state identity and fencing epochs.
The product audit found no production Ruflo verifier in Cognitum Functions. Firebase tenancy, Meta-LLM account identity, AgentBBS identity, and RuView edge authority must therefore remain separate until ADR-326's verified principal links, audience-bound grants, and local enforcement points are implemented.
ADR-101 remains useful as a component-level design record. Its assertion that federated claims are production-wired is superseded by this audit until the acceptance tests below pass.
For every federated action:
authenticated workload
+ trusted federation relationship
+ valid, narrower delegated grant
+ current policy authorization
+ valid work-ownership transition
+ durable receipt
= eligible execution
No one term can substitute for another:
ADR-324 AgenticPolicyEngine becomes the sole policy decision point for Ruflo
federation. The federation plugin becomes a policy enforcement point at both
ingress and egress.
Work claims remain in @claude-flow/claims; they are not converted into
authorization tokens. Instead, work-ownership facts become one input to policy:
subject: user:alice
actor: spiffe://team-a.example/agent/coder-7
action: work.handoff
resource: ruflo://repo/acme/api/issues/136
workOwner: agent:planner-2
tenant: acme
environment: production
The engine returns allow, deny, or require_approval and a signed decision
receipt. Federation code MUST NOT contain an independent checkClaim: () => true authorization path.
The capability plane defines these provider interfaces:
interface WorkloadIdentityProvider {
currentIdentity(): Promise<WorkloadIdentity>;
verify(assertion: IdentityAssertion): Promise<VerifiedIdentity>;
}
interface TrustBundleProvider {
resolve(trustDomain: string): Promise<VersionedTrustBundle>;
watch(onChange: (bundle: VersionedTrustBundle) => void): () => void;
}
interface AuthorizationGrantProvider {
exchange(request: DelegationRequest): Promise<AuthorizationGrant>;
verify(grant: AuthorizationGrant, proof: SenderProof): Promise<VerifiedGrant>;
revoke(grantId: string, reason: string): Promise<void>;
}
The default LocalKeyIdentityProvider wraps Ruflo's existing Ed25519 node
keypair. An optional SpiffeIdentityProvider obtains X.509-SVID or JWT-SVID
material through the SPIFFE Workload API.
Federation relationships are one-way records binding:
trustDomain
bundleEndpoint
endpointProfile
acceptedAlgorithms
policyProfile
maximumDelegationDepth
statusFeed
This follows SPIFFE Federation's central rule: the configured trust-domain name must remain bound to the correct rotating bundle. Mutual trust requires two explicit relationships.
For small deployments, relationships remain directly configured and pinned. OpenID Federation is an optional resolver for larger, multilateral federations. A resolved trust chain MUST be rooted in a locally configured anchor, MUST apply metadata policy monotonically, and MUST fail closed on a conflict.
Cross-agent delegation uses an RFC 8693-compatible subject/actor model. A delegated grant MUST include:
issuer
subject
actor (and bounded actor chain)
audience/resource
tenant
actions
resource selectors
not-before and expiry
grant ID
parent grant hash
delegation depth
proof-of-possession key thumbprint
policy version
Token exchange MUST only produce a grant whose actions, resources, audience, expiry, budget, and delegation depth are equal to or narrower than its inputs. The subject and actor remain separately attributable.
Bearer-only grants are prohibited for federated ownership mutations. Deployments use one of:
Every resource server validates the intended audience. Ruflo never passes the incoming token through to a downstream MCP server or agent; it exchanges it for a narrower, audience-specific grant.
Federation v2 envelopes use:
canonical form: RFC 8785 JSON Canonicalization Scheme (JCS)
payload subset: I-JSON
signature: Ed25519
content ID: sha256:<lowercase hex>
timestamp: RFC 3339 UTC
The protected signed object includes the protocol name, version, message type, message ID, source identity, target identity/audience, tenant, session ID, issued-at, expiry, nonce, content type, complete nested payload, authorization grant hash, and policy profile.
Signing and verification MUST use the same recursive canonicalizer. Duplicate keys, non-finite numbers, negative zero, values outside the I-JSON safe interoperability range, unknown critical headers, and unsupported versions are rejected before policy evaluation.
Legacy v1 envelopes may be read only in legacy or observe mode between
explicitly pinned peers. They MUST NOT authorize work acquisition, release,
steal, handoff, promotion, tool execution, or administrative changes.
Inbound processing order is normative:
wire size and framing
-> strict schema/version/I-JSON validation
-> JCS content ID and Ed25519 verification
-> target, audience, tenant, time, session, and nonce/replay checks
-> peer identity and trust-bundle validation
-> key/revocation/status validation
-> PII, prompt-injection, and content policy
-> ADR-324 authorization decision
-> idempotent domain transaction
-> decision + domain receipt append
-> typed event after commit
Egress performs the corresponding checks before transmission, including PII scanning, authorization, audience-specific token exchange, and an outbox transaction. A message is never considered delivered merely because a local event was appended.
The dispatcher has no unsigned production mode. Legacy tests can inject a verifier only through an explicitly named test adapter.
Vector clocks and HLCs remain useful for causal replication, diagnostics, progress updates, notes, and other mergeable observations. They are insufficient to guarantee exclusive ownership during a network partition.
Ownership-changing transitions use a CP authority:
interface WorkOwnershipRecord {
claimId: string;
authorityDomain: string;
owner: string | null;
epoch: bigint; // monotonic fencing token
leaseExpiresAt: string;
version: bigint;
lastReceiptId: string;
}
Each work resource has one configured authority domain. acquire, steal,
handoff, release, and lease renewal are compare-and-swap transactions at
that authority:
commit only when:
authority == configuredAuthority
current.version == request.expectedVersion
current.epoch == request.expectedEpoch
grant permits the exact transition
idempotencyKey has not committed a different request
A successful ownership transition increments the fencing epoch. Every worker must present that epoch to protected side effects; a resource adapter rejects stale epochs. A lease expiry permits reassignment but never makes an old epoch valid again.
The first implementation may use an existing linearizable store with
transaction, lease, and revision support. A pluggable OwnershipAuthority
keeps local single-node CAS as the default and permits an etcd/consensus-backed
provider for multi-node deployments.
During loss of quorum, ownership mutations fail closed. Progress telemetry and other AP-safe data may continue to replicate.
Federated work events use a transactional outbox and durable inbox:
Code must not mutate a private in-memory event array to simulate rollback.
Short-lived grants are the baseline. The trust plane also consumes signed status events for:
The transport uses a Shared Signals/CAEP-compatible adapter where an enterprise issuer supports it. Local and bilateral deployments use the same internal event model without requiring an external provider.
High-risk actions re-check current status online. Lower-risk actions may use a bounded cache. Revocation delay is visible in decision receipts.
For HTTP MCP:
For A2A:
securitySchemes and securityRequirements in Agent Cards;A2A and MCP are protocol adapters into the same identity, grant, and ADR-324 decision model. Neither protocol becomes an alternate authority.
Trust configuration / rotating bundles
+-----------------------------------------------+
| SPIFFE provider | Local key provider | OIDF |
+-------------------------+---------------------+
|
v
+----------+ signed v2 envelope +----------------------+
| Ruflo A | ---------------------> | Federation ingress |
| egress | | PEP |
+----+-----+ +----------+-----------+
| |
| narrow grant v
| +---------------------+
+------------------------------>| ADR-324 policy PDP |
+----------+----------+
|
allow + receipt
|
+--------------------------+----------+
| |
v v
+----------------------+ +---------------------+
| Ownership authority | | AP event replica |
| CAS + lease + epoch | | HLC + vector clock |
+----------+-----------+ +---------------------+
|
v
Durable inbox/outbox + receipt ledger
The design explicitly covers:
Compromise of both an actor and its protected proof-of-possession key remains out of scope for cryptographic replay prevention; blast radius is bounded by short lifetimes, narrow audiences, budgets, policy, and revocation.
Rejected. Component tests do not establish runtime composition, authorization, durability, or exclusive ownership under partition.
Rejected. Ownership and authorization have different lifecycle, delegation, revocation, and consistency rules. Conflating them makes a work assignment an ambient privilege escalation.
Rejected. Notes, progress, and observations benefit from availability and can merge causally. Only exclusive ownership transitions and their security receipts require the CP path.
Rejected. Exclusive ownership cannot be guaranteed during a partition without an authority, lease, quorum, or fencing mechanism.
Rejected. It would break offline and single-developer workflows. Standards are implemented behind provider interfaces; the local profile remains first-class.
Not selected for the first implementation. Macaroon-, Biscuit-, or
UCAN-inspired attenuation can be evaluated behind AuthorizationGrantProvider,
but OAuth token exchange and sender-constrained tokens provide stronger
interoperability with MCP, A2A, and enterprise identity infrastructure. The
ADR-324 policy state and receipts remain authoritative.
ClaimService APIs and local repositories retain behavior.legacy; observe evaluates the new
pipeline without authorizing remote mutation; enforce requires migration
checks and explicit administrator activation.ruflo claims remains an alias while new ruflo authz commands are added.
The alias emits a deprecation notice but preserves output schemas for one
major release.claim-event and agent-handoff behind an explicit disabled-by-default
policy profile rather than an accidental allowlist omission.No federated ownership mutation may ship before Phase 0 passes.
checkClaim: () => true with typed ADR-324 requests.OwnershipAuthority with local CAS and a linearizable distributed
provider.The feature is production-ready only when all of these pass:
These are acceptance targets, not claims about current performance:
| Metric | Target |
|---|---|
| Warm local envelope verification + replay + policy, 1 KiB payload | p99 <= 2 ms, measured separately from network and domain work |
| Added allocation per verified 1 KiB message | <= 32 KiB |
| Duplicate delivery | No duplicate domain side effect |
| Ownership race | Exactly one winner for 100 concurrent requests |
| Revocation propagation, high-risk profile | p99 <= 5 s |
| Revocation propagation, standard profile | p99 <= 60 s |
| Crash recovery | No acknowledged message or receipt lost |
| Local-mode regression | <= 5% p95 latency and no API/output-schema break |
Benchmarks MUST publish hardware, runtime, key algorithm, payload distribution, policy size, warm-up, sample count, and confidence interval. Optimization may not remove a security check; cached results include identity, audience, resource, action, tenant, policy version, grant ID, proof key, and revocation epoch in the cache key.
ruflo federation doctor --claims will report:
The command must never print private keys, bearer grants, DPoP private material, or unredacted work metadata.
Review this ADR when any of the following occurs: