Per-node attic push tokens issued by the control plane (SPEC §7.3.1) #13

Manually merged
krisbuild merged 9 commits from attic-tokens into main 2026-09-27 23:57:55 +02:00
Owner

The control plane issues short-lived, per-node attic tokens over the agent connection (new SPEC §7.3.1). Pushing to the shared cache needs no hand-minted credential on the node.

Control plane

  • New optional [attic] table: endpoint, cache, token_secret_env, token_validity_secs (default 3600), cache_push_min_trust (default 1). Without the table nothing changes.
  • The token is a JWT signed with atticd's own secret. The control plane detects which key it has: RS256 for a PEM key, otherwise HS256. A bad secret fails startup.
  • Claims: sub = kb-agent:<node>:token-<id>, pull and push on one cache only, nbf backdated 60 s. The tests verify tokens with jwt-simple using atticd's exact verification options, and pin the claim JSON.
  • Tokens go only to connections that presented an agent token and whose effective trust is at least cache_push_min_trust. Push access means a node can poison what every other node substitutes, so it is a deliberate grant.

Protocol (additive)

  • New message CpToAgent::AtticToken (its Debug output hides the token).
  • New field NodeAd.accepts (serde default). The control plane sends the token only to agents that list attic-token there, so old agents are unaffected. No def_hash change.
  • The first token is sent before the node is registered, and renewed at half-life. Revoking the agent token closes the session, and the last attic token expires within its TTL.

Agent

  • Writes an attic config.toml atomically, mode 0600, under <workDir>/attic/.
  • Uses it for its own pushes and hands it to kb-eval-nix's attic child.

NixOS module

  • controlPlane.extraEnvironmentFiles
  • agent.cachePush renders [cache_push] (closes that TODO item)
  • agent.attic.watchStore runs a watch-store unit as krisbuild that restarts when the token rotates

🤖 Generated with Claude Code

The control plane issues short-lived, per-node attic tokens over the agent connection (new SPEC §7.3.1). Pushing to the shared cache needs no hand-minted credential on the node. **Control plane** - New optional `[attic]` table: `endpoint`, `cache`, `token_secret_env`, `token_validity_secs` (default 3600), `cache_push_min_trust` (default 1). Without the table nothing changes. - The token is a JWT signed with atticd's own secret. The control plane detects which key it has: RS256 for a PEM key, otherwise HS256. A bad secret fails startup. - Claims: `sub = kb-agent:<node>:token-<id>`, pull and push on one cache only, `nbf` backdated 60 s. The tests verify tokens with `jwt-simple` using atticd's exact verification options, and pin the claim JSON. - Tokens go only to connections that presented an agent token and whose effective trust is at least `cache_push_min_trust`. Push access means a node can poison what every other node substitutes, so it is a deliberate grant. **Protocol (additive)** - New message `CpToAgent::AtticToken` (its Debug output hides the token). - New field `NodeAd.accepts` (serde default). The control plane sends the token only to agents that list `attic-token` there, so old agents are unaffected. No `def_hash` change. - The first token is sent before the node is registered, and renewed at half-life. Revoking the agent token closes the session, and the last attic token expires within its TTL. **Agent** - Writes an attic `config.toml` atomically, mode 0600, under `<workDir>/attic/`. - Uses it for its own pushes and hands it to kb-eval-nix's attic child. **NixOS module** - `controlPlane.extraEnvironmentFiles` - `agent.cachePush` renders `[cache_push]` (closes that TODO item) - `agent.attic.watchStore` runs a watch-store unit as `krisbuild` that restarts when the token rotates 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Per-node attic tokens issued over the agent connection (SPEC §7.3.1)
Some checks failed
krisbuild/kris/krisbuild/nix/hello cached
krisbuild/kris/krisbuild/nix/world cached
krisbuild/kris/krisbuild/nix/workspace-deps succeeded
krisbuild/kris/krisbuild/nix/build succeeded
krisbuild/kris/krisbuild/nix/clippy succeeded
krisbuild/kris/krisbuild/nix/test succeeded
krisbuild/kris/krisbuild/nix/kb-check succeeded
krisbuild/kris/krisbuild krisbuild kris/krisbuild: all tasks succeeded
krisbuild/queue the merge conflicts in crates/kb-agent/src/config.rs
7fe17aaeb8
A node's one credential is now its agent token: the control plane signs
short-lived attic JWTs with atticd's own secret ([attic] token_secret_env,
RS256 or HS256 detected from the value) in attic's claim format
(sub = kb-agent:<node>:token-<id>, exp, nbf backdated 60s, pull+push on one
cache) and sends them as CpToAgent::AtticToken right after hello, renewed at
half-life. Only token-authenticated connections at effective trust >=
cache_push_min_trust (default 1) qualify, and only agents listing
"attic-token" in NodeAd.accepts, so older agents never see the message.

The agent writes the token as attic's client config at
<work_dir>/attic/config.toml (atomic, 0600), uses it for its own pushes and
for kb-eval-nix (KB_CACHE_PUSH_ATTIC_CONFIG_HOME), and falls back to the
token's cache as push target when [cache_push] is unset. The NixOS module
gains controlPlane.extraEnvironmentFiles, agent.cachePush (renders
[cache_push]), agent.attic.package on PATH and agent.attic.watchStore, a
watch-store unit on the agent-written config restarted by a path unit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Owner

Only nix/kb-check failed, and on infrastructure (the kb-check .drv was not yet in the cache when a node fetched it); tests, clippy, build green.

@krisbuild rerun

Only `nix/kb-check` failed, and on infrastructure (the kb-check .drv was not yet in the cache when a node fetched it); tests, clippy, build green. @krisbuild rerun
Collaborator

unknown merge-queue command rerun.

unknown merge-queue command `rerun`.
Author
Owner

Reviewed: tokens are issued only to token-authenticated connections, after the trust cap and name binding are applied to the Hello (covered by anonymous_low_trust_and_old_agents_get_none); claims pinned against atticd's own verifier; old agents never receive the message. CI green on rerun (graph 576).

@krisbuild r+

Reviewed: tokens are issued only to token-authenticated connections, after the trust cap and name binding are applied to the Hello (covered by `anonymous_low_trust_and_old_agents_get_none`); claims pinned against atticd's own verifier; old agents never receive the message. CI green on rerun (graph 576). @krisbuild r+
Collaborator

Removed from the merge queue: the merge conflicts in crates/kb-agent/src/config.rs.

Removed from the merge queue: the merge conflicts in crates/kb-agent/src/config.rs.
kris force-pushed attic-tokens from 7fe17aaeb8
Some checks failed
krisbuild/kris/krisbuild/nix/hello cached
krisbuild/kris/krisbuild/nix/world cached
krisbuild/kris/krisbuild/nix/workspace-deps succeeded
krisbuild/kris/krisbuild/nix/build succeeded
krisbuild/kris/krisbuild/nix/clippy succeeded
krisbuild/kris/krisbuild/nix/test succeeded
krisbuild/kris/krisbuild/nix/kb-check succeeded
krisbuild/kris/krisbuild krisbuild kris/krisbuild: all tasks succeeded
krisbuild/queue the merge conflicts in crates/kb-agent/src/config.rs
to 6869993bfb
Some checks reported errors
krisbuild/kris/krisbuild/nix/hello cached
krisbuild/kris/krisbuild/nix/workspace-deps cached
krisbuild/kris/krisbuild/nix/world cached
krisbuild/kris/krisbuild/nix/clippy succeeded
krisbuild/kris/krisbuild/nix/build succeeded
krisbuild/kris/krisbuild/nix/kb-check succeeded
krisbuild/kris/krisbuild/nix/test superseded
krisbuild/kris/krisbuild krisbuild kris/krisbuild: superseded by 88e5afcf144096251d5d5a5a1bb048c18a5f25e7
2026-09-27 19:25:44 +02:00
Compare
tests: fold attic_tokens into the control plane's single test binary
Some checks failed
krisbuild/kris/krisbuild/nix/hello cached
krisbuild/kris/krisbuild/nix/world cached
krisbuild/kris/krisbuild/nix/workspace-deps succeeded
krisbuild/kris/krisbuild/nix/clippy succeeded
krisbuild/kris/krisbuild/nix/build succeeded
krisbuild/kris/krisbuild/nix/kb-check succeeded
krisbuild/kris/krisbuild/nix/test-deps succeeded
krisbuild/kris/krisbuild/nix/test succeeded
krisbuild/kris/krisbuild krisbuild kris/krisbuild: all tasks succeeded
krisbuild/queue the pull-request head changed
88e5afcf14
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Owner

Merged main (#18) and moved attic_tokens into tests/it/. Build, clippy and kb-check were green on the rebased head (graph 591); its test run was superseded by this push.

@krisbuild r+

Merged main (#18) and moved `attic_tokens` into `tests/it/`. Build, clippy and kb-check were green on the rebased head (graph 591); its test run was superseded by this push. @krisbuild r+
Collaborator

The pull request head's own build failed (graph 601).

  • nix/dep/cargo-package-anyhow: exit-code on nuxbox, exit 1 — log
  • nix/dep/cargo-package-autocfg: exit-code on nuxbox, exit 1 — log
  • nix/dep/cargo-package-binstring: exit-code on nuxbox, exit 1 — log
  • nix/dep/cargo-package-byteorder: exit-code on nuxbox, exit 1 — log
  • nix/dep/cargo-package-coarsetime: exit-code on nuxbox, exit 1 — log
  • nix/dep/cargo-package-const-oid: exit-code on nuxbox, exit 1 — log
  • nix/dep/cargo-package-ct-codecs: exit-code on nuxbox, exit 1 — log
  • nix/dep/cargo-package-ecdsa: exit-code on nuxbox, exit 1 — log
  • nix/dep/cargo-package-hmac: exit-code on nuxbox, exit 1 — log
  • nix/dep/cargo-package-hmac-sha256: exit-code on nuxbox, exit 1 — log
  • nix/dep/cargo-package-hmac-sha512: exit-code on nuxbox, exit 1 — log
  • nix/dep/cargo-package-jwt-simple: exit-code on nuxbox, exit 1 — log
  • nix/dep/cargo-package-libm: exit-code on nuxbox, exit 1 — log
  • nix/dep/cargo-package-num-bigint: exit-code on nuxbox, exit 1 — log
  • nix/dep/cargo-package-num-integer: exit-code on nuxbox, exit 1 — log
  • nix/dep/cargo-package-num-iter: exit-code on nuxbox, exit 1 — log
  • nix/dep/cargo-package-num-traits: exit-code on nuxbox, exit 1 — log
  • nix/dep/cargo-package-pem-rfc7468: exit-code on nuxbox, exit 1 — log
  • nix/dep/cargo-package-powerfmt: exit-code on nuxbox, exit 1 — log
  • nix/dep/cargo-package-rand: exit-code on nuxbox, exit 1 — log
  • … and 10 more

Push a fix, or comment @krisbuild retry to rerun the failed tasks and requeue.

The pull request head's own build failed ([graph 601](http://192.168.0.2:1337/ui/graphs/601)). - `nix/dep/cargo-package-anyhow`: exit-code on nuxbox, exit 1 — [log](http://192.168.0.2:1337/ui/instances/17331) - `nix/dep/cargo-package-autocfg`: exit-code on nuxbox, exit 1 — [log](http://192.168.0.2:1337/ui/instances/17355) - `nix/dep/cargo-package-binstring`: exit-code on nuxbox, exit 1 — [log](http://192.168.0.2:1337/ui/instances/17323) - `nix/dep/cargo-package-byteorder`: exit-code on nuxbox, exit 1 — [log](http://192.168.0.2:1337/ui/instances/17368) - `nix/dep/cargo-package-coarsetime`: exit-code on nuxbox, exit 1 — [log](http://192.168.0.2:1337/ui/instances/17349) - `nix/dep/cargo-package-const-oid`: exit-code on nuxbox, exit 1 — [log](http://192.168.0.2:1337/ui/instances/17386) - `nix/dep/cargo-package-ct-codecs`: exit-code on nuxbox, exit 1 — [log](http://192.168.0.2:1337/ui/instances/17384) - `nix/dep/cargo-package-ecdsa`: exit-code on nuxbox, exit 1 — [log](http://192.168.0.2:1337/ui/instances/17379) - `nix/dep/cargo-package-hmac`: exit-code on nuxbox, exit 1 — [log](http://192.168.0.2:1337/ui/instances/17376) - `nix/dep/cargo-package-hmac-sha256`: exit-code on nuxbox, exit 1 — [log](http://192.168.0.2:1337/ui/instances/17403) - `nix/dep/cargo-package-hmac-sha512`: exit-code on nuxbox, exit 1 — [log](http://192.168.0.2:1337/ui/instances/17393) - `nix/dep/cargo-package-jwt-simple`: exit-code on nuxbox, exit 1 — [log](http://192.168.0.2:1337/ui/instances/17344) - `nix/dep/cargo-package-libm`: exit-code on nuxbox, exit 1 — [log](http://192.168.0.2:1337/ui/instances/17397) - `nix/dep/cargo-package-num-bigint`: exit-code on nuxbox, exit 1 — [log](http://192.168.0.2:1337/ui/instances/17351) - `nix/dep/cargo-package-num-integer`: exit-code on nuxbox, exit 1 — [log](http://192.168.0.2:1337/ui/instances/17373) - `nix/dep/cargo-package-num-iter`: exit-code on nuxbox, exit 1 — [log](http://192.168.0.2:1337/ui/instances/17343) - `nix/dep/cargo-package-num-traits`: exit-code on nuxbox, exit 1 — [log](http://192.168.0.2:1337/ui/instances/17396) - `nix/dep/cargo-package-pem-rfc7468`: exit-code on nuxbox, exit 1 — [log](http://192.168.0.2:1337/ui/instances/17347) - `nix/dep/cargo-package-powerfmt`: exit-code on nuxbox, exit 1 — [log](http://192.168.0.2:1337/ui/instances/17334) - `nix/dep/cargo-package-rand`: exit-code on nuxbox, exit 1 — [log](http://192.168.0.2:1337/ui/instances/17364) - … and 10 more Push a fix, or comment `@krisbuild retry` to rerun the failed tasks and requeue.
# Conflicts:
#	crates/kb-agent/src/config.rs
#	crates/kb-agent/src/conn.rs
#	crates/kb-control-plane/tests/it/common/mod.rs
#	docs/TODO.md
Merge remote-tracking branch 'origin/attic-tokens' into attic-tokens
Some checks failed
krisbuild/kris/krisbuild/nix/hello cached
krisbuild/kris/krisbuild/nix/world cached
krisbuild/kris/krisbuild/nix/workspace-deps succeeded
krisbuild/kris/krisbuild/nix/clippy succeeded
krisbuild/kris/krisbuild/nix/build succeeded
krisbuild/kris/krisbuild/nix/kb-check succeeded
krisbuild/kris/krisbuild/nix/test-deps succeeded
krisbuild/kris/krisbuild/nix/test succeeded
krisbuild/kris/krisbuild krisbuild kris/krisbuild: all tasks succeeded
krisbuild/queue the merge conflicts in SPEC.md, crates/kb-agent/src/config.rs, crates/kb-agent/src/conn.rs, crates/kb-control-plane/src/
a8ee16c344
Collaborator

Removed from the merge queue: the pull-request head changed.

Removed from the merge queue: the pull-request head changed.
Author
Owner

Merged main (#8, #10, #17, #20, #22, #23): kept enrollment's early work_dir resolution and both connection tests, unified the test harness's header/accepts connect helpers. Fully green on this head (graph 651). Off by default (no [attic] table), so it is safe to deploy ahead of any config change.

@krisbuild r+

Merged main (#8, #10, #17, #20, #22, #23): kept enrollment's early `work_dir` resolution and both connection tests, unified the test harness's header/`accepts` connect helpers. Fully green on this head (graph 651). Off by default (no `[attic]` table), so it is safe to deploy ahead of any config change. @krisbuild r+
Collaborator

Removed from the merge queue: the merge conflicts in SPEC.md, crates/kb-agent/src/config.rs, crates/kb-agent/src/conn.rs, crates/kb-control-plane/src/agent_auth.rs, crates/kb-control-plane/src/scheduler/matchmaking.rs, crates/kb-control-plane/src/scheduler/sizing.rs, crates/kb-control-plane/src/ws.rs, crates/kb-control-plane/tests/it/common/mod.rs, crates/kb-core/src/node.rs, crates/kb-core/src/protocol.rs.

Removed from the merge queue: the merge conflicts in SPEC.md, crates/kb-agent/src/config.rs, crates/kb-agent/src/conn.rs, crates/kb-control-plane/src/agent_auth.rs, crates/kb-control-plane/src/scheduler/matchmaking.rs, crates/kb-control-plane/src/scheduler/sizing.rs, crates/kb-control-plane/src/ws.rs, crates/kb-control-plane/tests/it/common/mod.rs, crates/kb-core/src/node.rs, crates/kb-core/src/protocol.rs.
Merge remote-tracking branch 'origin/main' into attic-tokens
Some checks failed
krisbuild/kris/krisbuild/nix/test-deps cached
krisbuild/kris/krisbuild/nix/hello cached
krisbuild/kris/krisbuild/nix/workspace-deps cached
krisbuild/kris/krisbuild/nix/world cached
krisbuild/kris/krisbuild/nix/clippy succeeded
krisbuild/kris/krisbuild/nix/test succeeded
krisbuild/kris/krisbuild/nix/build succeeded
krisbuild/kris/krisbuild/nix/deployed succeeded
krisbuild/kris/krisbuild/nix/kb-check succeeded
krisbuild/kris/krisbuild krisbuild kris/krisbuild: all tasks succeeded
krisbuild/queue the merge conflicts in crates/kb-control-plane/Cargo.toml
9268d0faa5
# Conflicts:
#	SPEC.md
#	crates/kb-agent/src/config.rs
#	crates/kb-agent/src/conn.rs
#	crates/kb-control-plane/src/agent_auth.rs
#	crates/kb-control-plane/src/scheduler/matchmaking.rs
#	crates/kb-control-plane/src/scheduler/sizing.rs
#	crates/kb-control-plane/src/ws.rs
#	crates/kb-control-plane/tests/it/common/mod.rs
#	crates/kb-core/src/node.rs
#	crates/kb-core/src/protocol.rs
Author
Owner

Merged main (#14, #15, #24, #26). NodeAd.accepts is folded into #15's capabilities: the agent advertises attic-token via caps::advertised, issuance is gated on ad.has_cap(caps::ATTIC_TOKEN) (so proto-0 agents never receive AtticToken), and the hook runs after the trust cap and the handshake's proto check. Nodes without an issued token push exactly as before, so #26's fail-on-push-failure is unaffected. CI green on this head.

@krisbuild r+

Merged main (#14, #15, #24, #26). `NodeAd.accepts` is folded into #15's capabilities: the agent advertises `attic-token` via `caps::advertised`, issuance is gated on `ad.has_cap(caps::ATTIC_TOKEN)` (so proto-0 agents never receive `AtticToken`), and the hook runs after the trust cap and the handshake's proto check. Nodes without an issued token push exactly as before, so #26's fail-on-push-failure is unaffected. CI green on this head. @krisbuild r+
Collaborator

Removed from the merge queue: the merge conflicts in crates/kb-control-plane/Cargo.toml.

Removed from the merge queue: the merge conflicts in crates/kb-control-plane/Cargo.toml.
Merge remote-tracking branch 'origin/main' into pr13
Some checks failed
krisbuild/kris/krisbuild/nix/hello cached
krisbuild/kris/krisbuild/nix/world cached
krisbuild/kris/krisbuild/nix/test-deps succeeded
krisbuild/kris/krisbuild/nix/workspace-deps succeeded
krisbuild/kris/krisbuild/nix/test succeeded
krisbuild/kris/krisbuild/nix/clippy succeeded
krisbuild/kris/krisbuild/nix/build succeeded
krisbuild/kris/krisbuild/nix/kb-check succeeded
krisbuild/kris/krisbuild/nix/deployed succeeded
krisbuild/kris/krisbuild krisbuild kris/krisbuild: all tasks succeeded
krisbuild/queue the merge conflicts in Cargo.toml, SPEC.md, crates/kb-agent/src/conn.rs, crates/kb-agent/src/exec.rs, crates/kb-control-
37cf847ac5
# Conflicts:
#	crates/kb-control-plane/Cargo.toml
Author
Owner

Merged main (#27): the only conflict was kb-control-plane/Cargo.toml, where #27's console and this PR's jsonwebtoken were added on the same line; both kept, lockfile consistent (cargo check --locked clean). CI green on this head.

@krisbuild r+

Merged main (#27): the only conflict was `kb-control-plane/Cargo.toml`, where #27's `console` and this PR's `jsonwebtoken` were added on the same line; both kept, lockfile consistent (`cargo check --locked` clean). CI green on this head. @krisbuild r+
Collaborator

Removed from the merge queue: the merge conflicts in Cargo.toml, SPEC.md, crates/kb-agent/src/conn.rs, crates/kb-agent/src/exec.rs, crates/kb-control-plane/Cargo.toml, crates/kb-control-plane/src/ws.rs, flake.nix.

Removed from the merge queue: the merge conflicts in Cargo.toml, SPEC.md, crates/kb-agent/src/conn.rs, crates/kb-agent/src/exec.rs, crates/kb-control-plane/Cargo.toml, crates/kb-control-plane/src/ws.rs, flake.nix.
# Conflicts:
#	Cargo.toml
#	SPEC.md
#	crates/kb-agent/src/conn.rs
#	crates/kb-agent/src/exec.rs
#	crates/kb-control-plane/Cargo.toml
#	crates/kb-control-plane/src/ws.rs
#	flake.nix
Merge remote-tracking branch 'origin/attic-tokens' into attic-tokens
All checks were successful
krisbuild/kris/krisbuild/nix/hello cached
krisbuild/kris/krisbuild/nix/world cached
krisbuild/kris/krisbuild/nix/workspace-deps succeeded
krisbuild/kris/krisbuild/nix/clippy succeeded
krisbuild/kris/krisbuild/nix/build succeeded
krisbuild/kris/krisbuild/nix/deployed succeeded
krisbuild/kris/krisbuild/nix/kb-check succeeded
krisbuild/kris/krisbuild/nix/test-deps succeeded
krisbuild/kris/krisbuild krisbuild kris/krisbuild: all tasks succeeded
krisbuild/kris/krisbuild/nix/test succeeded
krisbuild/queue merged
e643d415b0
# Conflicts:
#	crates/kb-control-plane/Cargo.toml
Author
Owner

Merged main (#12, #21, #28). AtticToken stays an unsequenced control-plane→agent message, outside #21's agent→CP seq/acked_seq accounting; the agent now ignores a token whose expiry isn't newer than the one it holds, so duplicates and reconnect races can't roll it back. Adopted runs (#28) read the token file under work_dir, which renewal replaces in place. The per-connection renewal task ends on every session exit, including #12's restart close and #21's replacement kick. CI green on this head; priority so it lands before main moves again.

@krisbuild r+ p=5

Merged main (#12, #21, #28). `AtticToken` stays an unsequenced control-plane→agent message, outside #21's agent→CP seq/acked_seq accounting; the agent now ignores a token whose expiry isn't newer than the one it holds, so duplicates and reconnect races can't roll it back. Adopted runs (#28) read the token file under `work_dir`, which renewal replaces in place. The per-connection renewal task ends on every session exit, including #12's restart close and #21's replacement kick. CI green on this head; priority so it lands before main moves again. @krisbuild r+ p=5
krisbuild manually merged commit e843070652 into main 2026-09-27 23:57:55 +02:00
Collaborator

Merged as e843070652.

Merged as e84307065218.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kris/krisbuild!13
No description provided.