@atrib/daemon
v0.4.3
Published
Local daemon for atrib. Serves the attest/recall verbs plus the legacy alias tools from one stateless-native process over Streamable HTTP or stdio.
Maintainers
Readme
atribd
Local daemon for atrib. Serves the two-verb cognitive surface plus the legacy alias tools from one stateless-native process over Streamable HTTP or stdio.
Per the attest/recall rename
(D164),
atribd mounts three primitives in process: the attest write home
(@atrib/attest), the recall read home (@atrib/recall), and
@atrib/summarize. Together they serve the seventeen-tool union: the
fifteen legacy tool names plus attest plus recall. A record signed
through the daemon is byte-identical to one signed through the standalone
per-primitive binary: same handler code paths, same _local.producer
sidecar labels, same resolveChainRoot chain selection. The daemon is the
recommended local topology; the standalone binaries keep shipping and keep
working.
Install
Published as @atrib/daemon; the daemon and its binary are named atribd:
npm install -g @atrib/daemon
atribd --helpOr run without installing:
npx --package @atrib/daemon atribd --helpFrom the workspace (development):
pnpm --filter @atrib/daemon... build
node services/atribd/dist/index.js --helpQuick start
One daemon per profile (D120 partition axis). Start the HTTP daemon:
atribd --transport streamable-http --port 8796 --jsonPoint an MCP client at http://127.0.0.1:8796/mcp. Startup-spawn harnesses
that can only spawn stdio children use the proxy shim:
atribd --transport stdio-http-proxy --endpoint http://127.0.0.1:8796/mcpDirect stdio (no shared daemon) also works:
atribdHealth lives at <endpoint>/health and carries the D127-D130 gates: recall
contract freshness, per-package tool-surface contracts, non-mutating
behavioral probes (write primitives stay skipped), plus request counters.
There is no sessions block; the daemon has no sessions. The
report.compatibility block separately counts modern and legacy requests,
labels the declared protocol and client when known, and reports whether the
legacy adapter has seen a regression after modern traffic.
Compatibility observations persist by profile under
~/.atrib/state/atribd-mcp-compat-<profile>.json. The file contains bounded
counts, labels, and timestamps. It never contains request bodies, arguments,
context IDs, network addresses, or user-agent strings. Set
ATRIBD_MCP_COMPAT_STATE_FILE to choose another local path. Set it to an empty
string to disable persistence. ATRIBD_MCP_EXPECT_MODERN=1 makes the updater
reject legacy traffic observed after modern traffic. The same regression emits
the structured mcp_legacy_after_modern error event and degrades the health
response. Check one or more profiles without restarting them:
pnpm monitor:mcp-compatibility -- \
--endpoint http://127.0.0.1:8792/mcp/health \
--endpoint http://127.0.0.1:8795/mcp/health \
--endpoint http://127.0.0.1:8796/mcp/healthThe command exits nonzero for a regression, an unavailable endpoint, or an
invalid health response. The default removal gate requires 30 days with no
legacy request followed by an explicit deprecation announcement.
ATRIBD_MCP_LEGACY_ZERO_WINDOW_MS changes that local evidence window; it does
not remove the announcement requirement.
Stateless transport
Every HTTP request is self-describing and any request can land on any instance:
- No
initializehandshake is required. A legacyinitializePOST gets a valid response with no session id issued; a legacyMcp-Session-Idheader is ignored, never a 404. Mcp-Method/Mcp-Namerouting headers (SEP-2243) are validated against the body when present; a mismatch is HTTP 400 with nothing routed.- Inbound context carriers travel in per-request
_meta(SEP-414) and resolve through the spec §1.5.4 ladder with the §1.5.3X-Atrib-Chainfallback. tools/listresponses carryttlMsandcacheScope(SEP-2549) so clients can cache the tool catalogue. The defaultttlMsis 5 minutes during the alias window (operator-tunable via--tools-list-ttl-ms/ATRIBD_TOOLS_LIST_TTL_MS).
The transport binding sits behind src/transport-adapter.ts. HTTP requests
for the 2026-07-28 protocol use the stable v2 MCP TypeScript SDK and its
native per-request handler. The endpoint also routes 2025-era HTTP traffic
to the existing v1 stateless adapter, which keeps deployed clients on their
JSON response behavior during the compatibility window. Direct stdio and the
stdio-to-HTTP proxy use the v2 era negotiator. Both accept 2025-era clients
and clients pinned to 2026-07-28.
Context identity on HTTP
Write primitives require an explicit context per request:
- An explicit 32-hex
context_idtool argument wins. - Otherwise the daemon resolves the inbound
_metacarriers; a resolved trace context injectscontext_id, and a resolved propagation token seedschain_rooton tools that accept it. - Otherwise the write returns a typed tool error:
atrib: context_id required on stateless transport.
Read primitives that support unscoped queries proceed per their own scope
rules. A single-tenant daemon can opt back into ambient env and profile-file
discovery (D078/D083) with --ambient-context or ATRIBD_AMBIENT_CONTEXT=1;
the flag name is a
D148
open question. The stdio surfaces keep the ambient ladder unchanged.
Write serialization
The daemon serializes calls to any write-union tool name (attest, emit,
atrib-annotate, atrib-revise) per resolved context_id: read-tail,
sign, append runs one writer at a time per context, so concurrent writes
routed through one daemon, regardless of which tool name the caller used,
yield a linear chain. Writers that append to the mirror corpus without
routing through the daemon sit outside this boundary and can still fork a
chain; the spec/conformance/atribd/cases/concurrent-writer-serialization/
family pins both sides of that line.
Duplicate-safe writes
A write caller can set
params._meta["dev.atrib/idempotencyKey"] to a stable 16 to 128 character
visible-ASCII key. atribd binds the key to the request's context, tool, and
complete arguments. The first call reserves the binding before dispatch and
stores the complete result before returning it. A retry with the same binding
returns that result without signing again. The same key with changed arguments
fails before dispatch.
The default store is
~/.atrib/state/atribd-idempotency-<profile>.json. It hashes caller keys and
keeps completed results for seven days. ATRIBD_IDEMPOTENCY_STATE_FILE
selects another path or disables persistence when set to an empty string.
Unresolved pending entries never expire automatically. After a crash, such a
key returns an indeterminate-outcome error until the operator reconciles it.
Caller-facing timeouts do not release the per-context write lock. The lock stays held until the underlying primitive settles. If the late call succeeds, atribd stores its result before the next write in that context begins.
Request security and cancellation
HTTP authorization belongs to each request, not to the connection. Embedders
can pass bearerAuth to bindAtribdHttpHost(). atribd then runs the MCP SDK's
token verifier on every POST before dispatch. The verifier owns token validity,
expiry, required scopes, and revocation checks.
The optional rateLimit hook also runs per POST. It receives the method, tool,
protocol era, action class, and verified principal fields. It does not receive
the bearer token. Neither a reused socket nor
io.modelcontextprotocol/clientInfo grants authority.
Cancellation follows the request signal supplied by the MCP SDK. Reads pass that signal to the mounted primitive, so a disconnected or timed-out request can stop its work. Writes use a stricter rule once dispatch starts. The caller gets a cancellation or timeout error, but the write keeps settling under its per-context lock. A late success completes the idempotency entry. Health diagnostics report cancellation and timeout counts separately, including settlements that arrive after either event.
atribd does not use MCP requestState. A future multi-round tool must verify
and integrity-protect that caller-carried value before it can affect
authorization or routing. See
D186.
Process replacement
HTTP clients do not repair or recreate a session after atribd restarts. They retry reads as new requests. A write retry uses the same action-bound idempotency key and complete arguments. Completed writes replay their original result. A pending entry stays indeterminate instead of dispatching a second write.
The daemon test suite hard-kills a real atribd process on a fixed endpoint, starts a fresh process, and proves discovery, reads, negotiated receipts, and completed-write replay. The retry produces one mirror record. The same suite exercises 32 contexts concurrently, queues competing writes within one context, and proves another context can progress while that queue is busy. A failure matrix stops and kills the daemon with discovery, tool listing, reads, and a negotiated-receipt write pending at the client. Every request recovers by retrying its complete request. The write uses the same idempotency key and produces one mirror record.
The same real-process suite pins broad local-host budgets that catch request
path regressions without treating CI jitter as product latency: first
server/discover under 2 seconds; p95 cached tools/list and compatibility
routing under 500 milliseconds; p95 reads under 1.5 seconds; and p95 signed
writes with receipt generation under 2 seconds. These budgets cover daemon
work after startup and exclude public-log delivery, which runs through the
degradation queue.
tools/list remains cacheScope: "private". During the alias window its
default ttlMs is five minutes and operators can lower it. A client may use a
cached catalogue until that advertised TTL expires, then must refresh before it
assumes a removed or renamed tool still exists.
Degradation
The §5.8 contract is absolute. Log submission, mirror writes, and health
probing fail silently with atrib:-prefixed logging and never block a
primary tool call. With the log endpoint unreachable, a write still returns
a signed record_hash and the record lands in the local mirror. Probe and
call timeouts degrade the health report; they never kill the process. The
daemon binds 127.0.0.1 by default and is never a public service; the key
and mirror stay on the host.
Migration from @atrib/primitives-runtime
- LaunchAgents. Migrate through the D128 updater:
node scripts/update-primitives-runtime.mjs --runtime atribd. It discoverscom.nader.atribd.*LaunchAgents running this package'sdist/index.js, builds the dependency closure, restarts, probes health and the direct MCP surface, and gates on the daemon health shape. All operator profiles now run@atrib/daemon; the topology gate validates the daemon shape. - Deprecated session flags.
--session-idle-msandATRIB_PRIMITIVES_SESSION_IDLE_MSare accepted and ignored with a one-line stderr notice, never a fatal error. The stateless daemon has no sessions to expire. - Environment.
ATRIBD_*variables take precedence; the legacyATRIB_PRIMITIVES_HTTP_HOST/_PORT/_PATH/_TOOL_TIMEOUT_MSvalues are honored so existing LaunchAgent plists migrate without config churn.ATRIB_REQUIRE_EXPLICIT_CONTEXT_IDis redundant on HTTP (explicit is the default) and keeps its meaning on stdio. - Old MCP clients. Session-era clients work through the v1 compatibility
adapter on HTTP, or through the stdio shim indefinitely. Modern HTTP
clients negotiate
server/discoverwithoutinitializeorMcp-Session-Id. Remove the HTTP adapter only after every expected-modern profile reports zero regressions, the sustained-zero window completes, and the removal has been announced. - Rollback. Re-point the harness MCP config at the per-primitive
binaries or at
atrib-primitives. Rollback is a config change, not a data migration; no signed byte differs between the topologies.
Request diagnosis
Diagnose one bounded request, not a connection:
- Record the profile, endpoint, timestamp, MCP method, and tool name.
- Check
Mcp-Protocol-Version,Mcp-Method, andMcp-Nameagainst the JSON body. For modern traffic the protocol version is2026-07-28. - Inspect request
_metafor protocol version, client info, capabilities, explicit atrib context, trace context, and the write idempotency key. Redact bearer tokens and private tool arguments. - Inspect the response status, JSON-RPC error or result, attribution receipt, and health counters. A receipt proves a signed commitment, not the truth of the underlying claim.
- Retry reads as fresh requests. Retry writes only with the same idempotency key and byte-equivalent action. A changed action must use a new key.
- After process replacement, probe
server/discover,tools/list, and one non-mutating read. Do not attempt session repair.
The outer atrib server can be modern while a configured wrapper still talks to an older upstream MCP server. Diagnose the outer and upstream legs separately. The outer receipt proves what the atrib layer signed. It does not upgrade the upstream server's transport or make an upstream claim true.
Verify locally
pnpm --filter @atrib/daemon... build
pnpm --filter @atrib/daemon testThe test suite includes the reference tests for
spec/conformance/atribd/, which pin the
stateless transport contract, routing-header rejection, the context ladder,
record byte parity across surfaces, health gates, degradation posture, and
write serialization. It also pins a real stable-v2-client negotiation and
tools/list call against the daemon.
