Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/bright-requests-cancel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@atrib/daemon': minor
---

Add per-request bearer verification and rate limiting, abort read primitives on
cancellation, and preserve late write settlement through idempotent replay.
6 changes: 6 additions & 0 deletions .changeset/calm-retries-recover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@atrib/daemon': patch
---

Carry modern request metadata into duplicate-write binding, and prove discovery,
reads, receipts, and replay across hard process replacement.
5 changes: 5 additions & 0 deletions .changeset/slow-demons-retry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atrib/daemon": patch
---

Add real-process in-flight kill coverage and enforce local request-path latency budgets.
5 changes: 5 additions & 0 deletions .changeset/warm-pens-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atrib/mcp-wrap": patch
---

Run the filesystem smoke through the stable MCP 2026-07-28 client.
2 changes: 1 addition & 1 deletion .gitleaks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ paths = [
[[allowlists]]
description = "atrib protocol field names, never credentials"
regexes = [
'''(creator_key|revoked_key|successor_key|lookup_for_key|signing_key|key_thumbprint|signature|chain_root|content_id|record_hash|provenance_token|session_token)''',
'''(creator_key|revoked_key|successor_key|lookup_for_key|signing_key|key_thumbprint|signature|chain_root|content_id|record_hash|provenance_token|session_token|dev\.atrib/idempotencyKey)''',
]
# regexTarget = "line" matches the entire flagged line. The atrib protocol
# field names appear as JSON keys NEXT TO their high-entropy values, not
Expand Down
5 changes: 3 additions & 2 deletions CLAUDE.md

Large diffs are not rendered by default.

113 changes: 113 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9568,6 +9568,119 @@ localhost endpoint: can sign fixture records into operator state.
**Protocol impact.** None. The change implements existing MCP 2026-07-28 and
`dev.atrib/attribution` v0.1 carriage. It changes no atrib signed byte.

## D186: Stateless MCP security and cancellation are request-scoped

**Date:** 2026-07-29

**Status:** Accepted and implemented

**Extends:** [D169](#d169-protected-mcp-execution-requires-a-one-time-server-side-permit),
[D184](#d184-stateless-mcp-writes-use-action-bound-idempotency-keys), and
[D185](#d185-client-sdks-carry-complete-stateless-mcp-request-context).

**Context.** Removing MCP sessions also removes the connection as a valid
place to keep authorization, rate-limit, or cancellation state. A reused HTTP
connection says nothing about who sent the next request. A client name is
self-asserted metadata. Cancellation creates a second problem for writes: the
caller can leave after the signing side effect starts, so "cancelled" cannot
mean "nothing happened."

**Decision.**

1. atribd can require the MCP SDK's bearer verifier on every MCP POST. The
verifier checks token validity, expiry, required scopes, and operator-owned
revocation state before dispatch. Health stays unauthenticated so a process
supervisor can diagnose a broken verifier.
2. Verified `authInfo` is request-local. The rate-limit hook receives the
client id, scopes, expiry, resource, and optional verified attributes. It
never receives the bearer token. atribd does not write authentication
material into signed records or daemon diagnostics.
3. The rate-limit hook runs once per POST. It receives the MCP method, tool
name when present, protocol era, and action class. It cannot key authority
from a socket, session id, or client name.
4. Incoming cancellation reaches atribd through the official MCP handler
signal. Read calls forward that signal to the mounted primitive. A timeout
aborts the same path, releases the routed request, and records the outcome
in daemon diagnostics.
5. A write is not aborted after dispatch. Its caller receives the cancellation
or timeout error, while the per-context lock stays held until the primitive
settles. A late success completes the [D184](#d184-stateless-mcp-writes-use-action-bound-idempotency-keys)
entry and becomes replayable. Diagnostics distinguish late settlement after
cancellation from late settlement after timeout.
6. One-time execution permits remain bound to the exact action through
[D169](#d169-protected-mcp-execution-requires-a-one-time-server-side-permit).
Write retry keys remain bound to context, tool, and complete arguments
through [D184](#d184-stateless-mcp-writes-use-action-bound-idempotency-keys).
Missing, changed, expired, replayed, and revoked cases fail before the
protected dispatch boundary.
7. atribd does not use MCP `requestState` for authority or routing. Any future
multi-round tool that does so must configure the SDK verification hook,
integrity-protect the value, set an expiry, and bind it to the authenticated
principal and method.

**Guarantee boundary.** The default local daemon still binds loopback without
bearer authentication. Operators that expose it beyond a trusted local process
boundary must configure bearer verification and a request limiter. Cancellation
releases read-request resources, but a dispatched write can still complete. Its
idempotency state and late-settlement counters are the evidence for that
uncertain interval.

**Alternatives rejected.** Trust `clientInfo.name`: the caller controls it.
Authorize once per connection: HTTP connection reuse crosses requests and
principals. Abort every timed-out write: the side effect may already have
happened, and forgetting it permits a duplicate retry. Pass raw bearer tokens
to the limiter: the limiter needs verified identity facts, not reusable
credentials. Accept raw `requestState`: caller-carried continuation state has
no authority without integrity and binding checks.

**Protocol impact.** None. This applies MCP 2026-07-28 request lifecycle and
OAuth hooks to atribd. It changes no atrib record field or signed byte.

## D187: Keep one atribd daemon per operator profile

**Date:** 2026-07-29

**Status:** Accepted and implemented

**Context.** Stateless MCP allows any request for one security domain to reach
any instance configured for that domain. It does not make separate signing,
storage, or policy domains interchangeable. The operator machine runs
`claude-code`, `claude-desktop`, and `codex` profiles. Each has a distinct
loopback port, `ATRIB_AGENT`, mirror file, autochain source, and local-substrate
endpoint.

**Decision.** Keep one atribd process per operator profile. Do not add shared
profile routing to the daemon.

The current process boundary provides:

1. one fixed signing and Keychain scope per process;
2. one mirror, autochain source, idempotency store, and coordinator route;
3. profile-labeled health, logs, restarts, and rollback;
4. independent failure and resource-pressure domains; and
5. no caller-controlled profile selector to authenticate or misroute.

**Evidence.** The three installed LaunchAgents differ on ports 8795, 8792, and
8796. They set profile-specific `ATRIB_AGENT`, `ATRIB_MIRROR_FILE`,
`ATRIB_AUTOCHAIN_SOURCE`, and `ATRIB_LOCAL_SUBSTRATE_ENDPOINT` values. atribd
does not implement authenticated per-request selection for that complete
resource set or a scheduler that prevents one profile from starving another.

**Revisit gate.** A future shared supervisor requires authenticated profile
identity, key and mirror non-interference tests, profile-scoped idempotency and
serialization, explicit authorization policy, per-profile health and logs,
fairness limits, and a one-command rollback to separate daemons. Process-count
reduction alone is not enough.

**Alternatives rejected.** One fully shared daemon would turn a routing bug
into a signing or storage boundary violation. Isolated workers behind one
supervisor add lifecycle coupling without removing the worker processes that
provide isolation. The current three-daemon topology is small and already
operable through the profile-aware runtime updater.

**Protocol impact.** None. This is a deployment boundary. It changes no MCP
message and no atrib signed byte.

# Pending decisions

These will get full ADRs when we act on them. Recorded here so they remain findable and don't silently drop. Per the global Deferred Decision Logging convention, this section uses the forward-looking pattern (forward-looking decisions that will become numbered ADRs when codified).
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,20 @@ second `tool_call` record. The private integration package pins this rule in
[`packages/integration/src/host-runtime-proof.ts`](packages/integration/src/host-runtime-proof.ts)
so future OpenClaw, Hermes, and other host proofs share the same vocabulary.

## Stateless MCP attribution

Add verifiable records to a tool call without adopting a new agent runtime or
maintaining an atrib transport session. A native server can advertise
`dev.atrib/attribution`; an existing server can sit behind `@atrib/mcp-wrap`;
and the TypeScript and Python SDKs carry the complete request envelope
automatically. The tool result can include a signed receipt that a verifier
checks without trusting atrib's hosted service.

The [stateless MCP attribution guide](docs/stateless-mcp-attribution-guide.md)
shows the request, result, receipt, integration choices, runnable proofs, retry
rules, and the boundary between proof of a signed claim and proof that the
claim is true.

## How it works

- Each record is signed by the actor's Ed25519 key and JCS-canonicalized
Expand Down
4 changes: 4 additions & 0 deletions STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ The package inventory, grouped current / deprecated / private, lives in the [REA
- Record signing ([§1](atrib-spec.md#1-attribution-record-format)) including `INFORMED_BY` ([D041](DECISIONS.md#d041-informed_by-linking-primitive-and-informed_by-edge-type)), `PROVENANCE_OF` ([D044](DECISIONS.md#d044-provenance_token-field-for-cross-session-causal-anchoring)), ANNOTATES ([D058](DECISIONS.md#d058-promote-annotation-to-atrib-normative-event_type-byte-0x05)), REVISES ([D059](DECISIONS.md#d059-promote-revision-to-atrib-normative-event_type-byte-0x06)), and `directory_anchor` ([D056](DECISIONS.md#d056-promote-directory_anchor-to-atrib-normative-event_type-byte-0x04)) event types.
- Native MCP v2 transport coverage ([D148](DECISIONS.md#d148-atribd-is-the-public-stateless-native-local-daemon-for-the-primitive-runtime)): the 13 atrib-owned MCP entrypoints negotiate protocol `2026-07-28`, including 12 published surfaces across standalone stdio binaries, atribd's three transport modes, and `atrib-wrap`. A machine-readable inventory binds each entrypoint to its process proof. Doc sync checks source ownership, while release readiness builds the packages and inspects every packed public artifact. Upstream simulation fixtures stay explicitly excluded. Health probes use a collision-resistant absent context so valid operator data cannot turn a healthy runtime into a false degradation.
- Stateless client request carriage ([D185](DECISIONS.md#d185-client-sdks-carry-complete-stateless-mcp-request-context)): `@atrib/sdk` and the Python `atrib` package declare `dev.atrib/attribution` by default, carry explicit context plus W3C and compatibility metadata on each daemon request, advance valid receipt tokens, and expose negotiated protocol and extension facts. Python includes an independent standard-library MCP 2026-07-28 HTTP client. Both implementations consume the same request-carriage corpus.
- Request-scoped daemon security and lifecycle ([D186](DECISIONS.md#d186-stateless-mcp-security-and-cancellation-are-request-scoped)): atribd can verify OAuth bearer tokens and required scopes on every MCP POST, then apply a per-request limiter using verified principal facts without exposing the token. Read cancellation reaches the primitive. Dispatched writes keep settling under their context lock, and diagnostics separate cancellation, timeout, and late settlement. Connection reuse, session headers, client names, and raw `requestState` grant no authority.
- Stateless process replacement: a real fixed-endpoint test hard-kills atribd, starts a fresh process, and proves modern discovery, reads, negotiated receipts, and one-record completed-write replay without session repair. Modern v2 request metadata reaches the same context and idempotency policy as legacy requests. The concurrency proof runs 32 independent contexts and keeps one context linear while another progresses.
- Independent attribution-extension interoperability: Python's standard-library client consumes atribd. A separate standard-library Python server imports no atrib or MCP package and is consumed by `@atrib/sdk`; the client checks modern negotiation and verifies the receipt token, record hash, creator key, context, chain root, and Ed25519 signature against the language-neutral corpus.
- Profile topology ([D187](DECISIONS.md#d187-keep-one-atribd-daemon-per-operator-profile)): the three operator profiles remain separate atribd processes. Each process owns one signing scope, mirror, autochain source, idempotency store, coordinator route, health surface, and rollback target. Stateless request routing applies within a profile security domain, not across profiles.
- Parent-child producer threading now has a single same-session subagent env bundle: `ATRIB_CONTEXT_ID`, `ATRIB_CHAIN_TAIL_<context_id>`, and `ATRIB_PARENT_RECORD_HASH`, built by `@atrib/mcp` `buildSubagentProducerEnv()` when the parent dispatch hash is known before the child signs ([D104](DECISIONS.md#d104-parent-child-threading-uses-atrib_parent_record_hash), [D115](DECISIONS.md#d115-agent-to-subagent-handoff-uses-a-three-signal-producer-bundle)).
- Source-aware producer validation lets `@atrib/mcp-wrap` keep configured `informedByPaths` only when refs resolve through the wrapper mirror, local mirrors, or log lookup, while parent env seeds stay producer-owned ([D116](DECISIONS.md#d116-producer-side-informed_by-validation-is-source-aware)).
- Sandboxed producer key isolation ([§1.4.6](atrib-spec.md#146-signing-key-isolation-for-sandboxed-execution), [D102](DECISIONS.md#d102-sandboxed-signer-proxy-keeps-keys-outside-sandbox)) keeps signing keys outside sandboxed execution via a tested host signer-proxy example.
Expand Down
6 changes: 6 additions & 0 deletions atrib-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -3458,6 +3458,12 @@ serverInfo: {

### 5.4 @atrib/agent: Agent Middleware

In this section, "session state" means application-owned continuity across an
agent run. It is not MCP transport-session state. Each MCP request still carries
its complete protocol, identity, capability, trace, and atrib context metadata.
No rule in [§5.4](#54-atribagent-agent-middleware) depends on a connection,
`initialize`, or `Mcp-Session-Id`.

#### 5.4.1 Init Interface

The agent middleware exposes an interception surface that the host application or MCP client integrates at outbound tool call and inbound tool response boundaries. This is a framework-agnostic design; the middleware does not monkey-patch a specific agent implementation, because no single agent shape covers the LangChain / Mastra / AI SDK / direct-MCP-client landscape. Instead, the middleware returns an interceptor object that the caller hooks into their own request/response lifecycle.
Expand Down
87 changes: 87 additions & 0 deletions docs/extensions/dev.atrib-attribution/independent-server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# SPDX-License-Identifier: Apache-2.0
"""Minimal MCP 2026-07-28 attribution server using only Python's stdlib.

Run from the repository root:

python3 docs/extensions/dev.atrib-attribution/independent-server.py \
spec/conformance/mcp-extension/cases/receipt--consistent.json
"""

from __future__ import annotations

import json
import signal
import socketserver
import sys
from http.server import BaseHTTPRequestHandler
from pathlib import Path
from typing import Any, cast

receipt_case = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
receipt_block = receipt_case["input"]["result_block"]


class Handler(BaseHTTPRequestHandler):
def do_POST(self) -> None: # noqa: N802
length = int(self.headers.get("Content-Length", "0"))
body = cast(dict[str, Any], json.loads(self.rfile.read(length)))
method = body["method"]
if method == "server/discover":
result: dict[str, Any] = {
"supportedVersions": ["2026-07-28"],
"capabilities": {
"tools": {},
"extensions": {
"dev.atrib/attribution": {
"version": "0.1",
"receipts": ["token", "record"],
}
},
},
"ttlMs": 0,
"cacheScope": "private",
"resultType": "complete",
}
elif method == "tools/call":
meta = cast(dict[str, Any], body["params"]["_meta"])
declaration = cast(
dict[str, Any],
cast(dict[str, Any], meta["io.modelcontextprotocol/clientCapabilities"])[
"extensions"
]["dev.atrib/attribution"],
)
if declaration["version"] != "0.1":
self.send_error(400)
return
result = {
"content": [{"type": "text", "text": json.dumps({"ok": True})}],
"_meta": {"dev.atrib/attribution": receipt_block},
"resultType": "complete",
}
else:
self.send_error(404)
return
encoded = json.dumps(
{"jsonrpc": "2.0", "id": body["id"], "result": result},
separators=(",", ":"),
).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(encoded)))
self.end_headers()
self.wfile.write(encoded)

def log_message(self, _format: str, *_args: object) -> None:
return


class Server(socketserver.ThreadingMixIn, socketserver.TCPServer):
allow_reuse_address = True
daemon_threads = True


server = Server(("127.0.0.1", 0), Handler)
signal.signal(signal.SIGTERM, lambda _signum, _frame: server.shutdown())
host, port = server.server_address
print(json.dumps({"endpoint": f"http://{host}:{port}/mcp"}), flush=True)
server.serve_forever()
15 changes: 15 additions & 0 deletions docs/extensions/dev.atrib-attribution/v0.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,21 @@ SHOULD validate against the corpus; the vectors are the shared contract
between independent implementations in any language, with no atrib package
required.

### 10.1 Runnable independent-server proof

[`independent-server.py`](independent-server.py) is a minimal MCP 2026-07-28
server written against this document. It uses Python's standard library and
imports no atrib or MCP package. It declares the extension, checks the client's
per-request declaration, and returns the real signed receipt from the
language-neutral corpus.

The `@atrib/sdk` test suite starts that server as a separate process, consumes
it with the TypeScript daemon client, checks modern protocol negotiation, and
verifies the token, record hash, creator key, context, chain root, and Ed25519
signature. The opposite direction uses Python's standard-library
`StatelessMcpClient` against atribd. Together they prove independent client and
server interoperability across both language and implementation boundaries.

## 11. References

- atrib protocol specification: <https://github.com/creatornader/atrib/blob/main/atrib-spec.md>: record format ([§1.2](../../../atrib-spec.md#12-the-attribution-record)), canonical serialization ([§1.3](../../../atrib-spec.md#13-canonical-serialization)), propagation token ([§1.5.2](../../../atrib-spec.md#152-http-transport-tracestate)), MCP transport ([§1.5.4](../../../atrib-spec.md#154-mcp-transport-params_meta)), multi-producer chain composition ([§1.2.3.1](../../../atrib-spec.md#1231-multi-producer-chain-composition)), log submission ([§5.3.5](../../../atrib-spec.md#535-log-submission)), degradation contract ([§5.8](../../../atrib-spec.md#58-degradation-contract)), salted-commitment posture ([§8.3](../../../atrib-spec.md#83-salted-commitment-posture)).
Expand Down
Loading
Loading