lib/daemon_runtime/neotoma_signed.NeotomaWriter is the write client daemons use
to reach Neotoma with a verified identity (ateles#1270). This page is how to move
one daemon's writes from the shared bearer token onto its own signature. It is
written for Anthus, the first daemon on the client; the steps are the same for
any other, with the daemon's name substituted.
- Governance writes are always signed. A write to a governance type
(
GOVERNANCE_ENTITY_TYPES, matched under any spelling Neotoma folds: case, separators, accents, simple plurals), toissue.gate_status, onto a governance entity by relationship, or onto an existing entity named by id whose real type turns out to be governance, is signed or refused. The ids resolved are a store entity'starget_id(fields written through it are judged against the target's real type, sogate_statusonto an issue counts), a correct'sentity_id, and every relationship endpoint. A failed lookup counts as governance. The switch below does not apply to it and there is no bearer fallback. - Only known endpoints. The client sends
store,correct,create_relationshipandcreate_relationships, each classified on the keys Neotoma reads for that endpoint. Any other path is refused before anything is sent. - Every other write follows the daemon's switch. Unset, the daemon keeps writing with the bearer exactly as before.
- The identity is pinned. The daemon signs as
<agent>@ateles-swarm, with the key file's ownsubrequired to match and the issuer taken from the key file (else the default). AmbientNEOTOMA_AAUTH_SUB/NEOTOMA_AAUTH_ISSare ignored.
ATELES_SIGNED_WRITES_<DAEMON>, the daemon's name upper-cased (Anthus:
ATELES_SIGNED_WRITES_ANTHUS).
| Value | Non-governance writes |
|---|---|
unset / off |
Bearer, as today. The default. |
shadow |
Signed. If signing fails or the server refuses the signed identity, log at ERROR and retry once with the bearer. |
on |
Signed. Any failure raises; nothing falls back. |
| anything else | Read as on, the restrictive branch. |
Set it in the daemon's LaunchAgent EnvironmentVariables, or in the shared env
file lib/daemon_runtime/__init__.py loads at import. That file is materialized
from the SOPS snapshot (see secrets management), so
a hand edit there can be overwritten; change the snapshot instead.
-
Key.
<agent>.jwk.jsonexists in the agent keys directory in the canonical format (keys): EC P-256,sub: <agent>@ateles-swarm, akid. Check field names only; never print the file. -
Grant. The daemon's
agent_grant(today, admits on itssuband issuer alone) must allow every operation and type the daemon writes. For Anthus:store_structuredondaemon_report,strategy_revision_proposalandstrategy_drift_signal;correctonstrategy_revision_proposal(the generalizer adds new evidence to an open proposal'sdrift_signal_refs);- never
agent_policy: the generalizer only proposes rules, and a signed write toagent_policymust stay refused. - When widening this (or any) grant, pin
match_thumbprintto the agent's own key, notsubalone — this is where widening belongs in the switch-on order below, ahead ofshadow.match_thumbprintis the RFC 7638 thumbprint of the agent's public key — the same value the approval-rule signer check pins (seedocs/data_types.md'sstrategy_revision_proposalsection, and Falco's PR #1274 round-3 finding). The check compares the key thumbprint Neotoma records for the observation, not thesub/issuer label alone; pinningmatch_thumbprinton the grant keeps admission bound to the same key that value identifies. The client-side signer check below (confirm_attribution/ the approval rule) independently pins the key regardless of grant admission (Falco, PR #1274 round 4).
Probe before switching: a signed dry-run store (
commit: false, nothing persisted) through the client for each type. An admitted type answerswould_create; a type the grant lacks is refused, currently as HTTP 500 with "is not permitted to" in the message, which the client treats as a refusal of the signed identity. -
What happens if the grant is short. Under
on, refused writes raise and the daemon logs them; for Anthus, generalization stops until the grant is widened (it fails closed). Undershadow, they fall back to the bearer.
- Widen the grant (operator).
- Set the switch to
shadowand restart the daemon from its deployment checkout. Verify the new process: a new pid, and the variable present in the running process's environment. - Confirm attribution on the next write (below).
- Set
on, restart, confirm again.
In code, NeotomaWriter.confirm_attribution(result) reads each written
observation back and passes only when provenance.agent_sub is the expected
sub AND provenance.agent_thumbprint equals this writer's own key thumbprint
(NeotomaWriter.thumbprint), at a verified-signature tier (software,
operator_attested, hardware). The thumbprint check is required, not
optional: the check compares the key thumbprint Neotoma records for the
observation, which is what the signature actually proves, rather than
relying on the agent_sub label alone. By hand, for the entity the daemon
just wrote:
# substitute the entity id the daemon just wrote
curl -s -H "Authorization: Bearer $NEOTOMA_BEARER_TOKEN" \
-H 'content-type: application/json' "$NEOTOMA_BASE_URL/observations/query" \
-d '{"entity_id":"<ent_id>","limit":5}' | \
jq '.observations | sort_by(.observed_at) | last | .provenance | {agent_sub, attribution_tier, agent_thumbprint}'Expect {"agent_sub": "anthus@ateles-swarm", "attribution_tier": "software", "agent_thumbprint": "<Anthus's own key thumbprint>"}. A null sub, an
unsigned tier, or a thumbprint that does not match Anthus's key file means
the write did not verifiably come from Anthus's own key.
To get Anthus's own thumbprint to compare against, print it from the key file — a thumbprint is a hash of the public key, safe to display, and this reads the key file without printing it:
python3 -c "from lib.daemon_runtime.neotoma_signed import NeotomaWriter; print(NeotomaWriter('anthus').thumbprint)"Run from the repo root.
Unset the variable (or set off) and restart. Governance writes stay signed
regardless; everything else returns to the bearer.