Skip to content

Local identity rotation API for in-place consumer re-key (no overlap) #338

Open

Description

@schickling-assistant

Problem

Smalltalk needs an operator-triggered in-place Fabric identity rotation: st fleet transports rekey fabric should obtain a new local NodeID, re-expose its existing replication protocol, and publish the replacement endpoint without changing the Smalltalk member identity.

At Fabric main 97ba98678c580e455d85f96ed70bab6fd18c7ba0, src/main.rs exposes only key gen --out PATH for identity key management; src/control.rs has restart/recycle operations but no identity rotation. Generating a separate key file is not a daemon identity cutover. Smalltalk must not rewrite Fabric-owned identity files or guess managed-service restart semantics.

Remote policy replacement is already available: docs/runtime-peers.md describes one atomic batch removing the old owner record, adding the new NodeID, and setting exact grants. Smalltalk's membership adapter already emits these operations in one batch. This request is only the missing local identity rotation capability, not overlap/expiry support.

Smallest proposed surface — direction review requested

Add one Fabric-owned local command/control operation that durably generates and installs a new identity, restarts/rebinds the endpoint using that identity, and returns machine-readable old/new public NodeIDs after the new daemon identity is active. Command naming and response schema need Fabric owner agreement before Smalltalk consumes them. No secret key material should cross this interface.

Required properties:

  • Preserve the resolved Fabric home, static/runtime peer policy and existing service declarations.
  • Immediate replacement only; no old/new authorization overlap requested.
  • Serialize against concurrent rotation/restart; no success while the live endpoint still uses the old NodeID.
  • Return explicit failure/uncertain-outcome information so a caller cannot blindly rotate again after a lost reply.
  • Keep key persistence, permissions, old-key retirement and managed-service behavior inside Fabric.

Open questions for Nathan

  1. What command/control request and JSON result should Smalltalk consume? Is a durable request ID or compare-against-old-NodeID guard appropriate for lost-reply recovery?
  2. Should rotation restart through the existing service-aware path, or can Fabric safely recreate its endpoint in-process? How does success prove the active NodeID?
  3. How should other Fabric consumers (sync author identity, durable sessions/dials and runtime receipts) behave across identity replacement? Which state is preserved and which must be explicitly retired?
  4. What is the failure contract after the new key is durable but before the new endpoint is serving, including process crash or managed-service restart failure?

Acceptance

  • A fake-free Fabric integration test rotates a running isolated home, observes a different live NodeID and verifies persisted identity after restart.
  • Existing peer policy and exposure declarations survive; the old endpoint is no longer serving after success.
  • Failure/crash and concurrent-operation tests establish the agreed recovery contract.
  • Published JSON/CLI contract lets Smalltalk implement its wrapper without reading or writing secret identity files.

Related: #312; Smalltalk transport ordering prerequisite compoundingtech/fractal#3642.

Posted on behalf of @schickling
field value
agent_identity agent/dotfiles/fabric-mesh
session 01a1ecd5
agent_persona generalist
agent_supervisor unavailable
agent_tool OMP
agent_tool_version 18.9.1
agent_runtime OMP 18.9.1
agent_model anthropic/claude-opus-5-5
worktree smalltalk/2026-10-22-fleet-fabric-rekey-q37

Activity

  1. schickling-assistant commented on Oct 22, 2026

    @schickling-assistant
    ContributorAuthor

    @myobie, a direction input for this issue from Fabric-mesh (agent/dotfiles/fabric-mesh).

    Johannes decided on 2026-10-22 (axe q39) that an st member re-keys in place with a bounded overlap of at most 10 minutes. The reason: once Fabric is the only st carrier, an immediate cutover strands the machine. Peers can learn the new NodeID only over Fabric, and they do not trust the new NodeID yet.

    The st side needs two Fabric pieces:

    1. Local dual-identity serving. The daemon serves a new identity next to the current identity during the overlap. A local command prepares and activates the new identity.
    2. Overlap expiry on the trusting side. This is the planned overlap batch from docs/runtime-peers.md 159-162: install the new NodeID, and set an immutable expiry of at most 10 minutes on the old record.

    The st adapter already sends the NodeID replace as one batch (smalltalk#3644 draft, reconcile_tests.rs:167), so the st side waits on the command contract for these two pieces. Please confirm or change this shape. Fabric-mesh can open the Fabric PR once you agree on the shape.

  2. myobie commented on Oct 25, 2026

    @myobie
    Collaborator

    Direction for #338 (decided; open the PR against this).

    Shape: prepare, publish over the old identity, activate. No dual-identity serving. A second live endpoint in the daemon is a large change and is not needed. The strand risk comes from activating before peers trust the new NodeID, so the order removes it: the member prepares a new identity, tells peers over the still-working old identity, peers install it, and only then does the machine switch.

    1. fabric identity prepare [--json] generates the new key durably (0600, inside Fabric's home, never printed) and returns {"current":"<id>","next":"<id>"}. Idempotent: while a prepared key exists it returns the same next. Nothing changes for the running daemon.
    2. The st side sends the existing atomic batch: add next with the exact grants, set an immutable expiry of at most 10 minutes on the old record (the planned overlap batch in docs/runtime-peers.md). That expiry is a separate runtime-peers slice in this repo; open it as its own PR first (record field expires_at, immutable once set, revocation receipts on expiry like any other revoke).
    3. fabric identity activate --expect-current <id> --expect-next <id> [--json] swaps the key by one atomic rename, restarts through the existing service-aware path, and returns only after the live daemon reports next as its NodeID: {"previous":"<id>","active":"<id>","outcome":"activated"}.
    4. fabric identity status [--json] returns {"current":"<id>","next":"<id>|null","state":"active|prepared"}.

    Lost reply / guard. The expect flags are the guard; no request IDs. If current equals the expected next, activate returns outcome:"already_active" (success). If current is neither, it fails with identity_mismatch and changes nothing. Operations serialize behind one identity lock; concurrent callers get identity_busy.

    Crash contract. The swap is a single rename, so after a crash the home is either prepared (old key active, new key pending) or active. The daemon starts from whichever the rename left; status tells the caller which. Restart failure after the rename leaves active with a daemon that is not serving: activate returns a non-zero restart_failed with both IDs, and repeating the same command retries the restart only. The old key is moved to a retired file and deleted when the old record's expiry has passed; the daemon never serves it again after activation.

    Other consumers (the PR must state and test each). Preserved: static and runtime peer policy, exposures, receipts and revocation history, audit log. Retired on activation: tunnel sessions, mux connections and queued dials bound to the old NodeID (peers see a new identity, so they re-establish). Sync author identity: if it is derived from the NodeID, document the behavior; if independent, it is unchanged.

    Acceptance. Your list, plus: prepare is idempotent, activate lost-reply retry is a no-op success, crash between rename and restart recovers, and the old NodeID does not accept a connection after success.

  3. schickling-assistant commented on Oct 25, 2026

    @schickling-assistant
    ContributorAuthor

    Implementation drafts are open bottom-up: #340 (immutable runtime-peer expiry) → #341 (prepare/publish/activate, no dual serving). #340 has independent correctness/security source-review GOs at 3578f43. #341 includes review fixes but still needs exact-head re-review. Both remain draft: targeted local execution could not find Cargo in the host environment, and hosted CI is still running. The two conservative open questions are recorded in #341; no merge or auto-merge is enabled.

    Posted on behalf of @schickling
    field value
    agent_identity agent/dotfiles/fabric-mesh
    session 01a1ecd5
    agent_persona generalist
    agent_supervisor unavailable
    agent_tool OMP
    agent_tool_version 18.12.5
    agent_runtime OMP 18.12.5
    agent_model anthropic/claude-opus-5-5
    worktree fabric/identity-rekey338
  4. schickling-assistant commented on Oct 25, 2026

    @schickling-assistant
    ContributorAuthor

    Implemented Nathan’s prepare → publish over the old identity → activate direction, with no dual-identity serving:

    Both exact heads have independent correctness and security GOs. Gated local acceptance checks pass; #341 test and Nix CI are green, and #340 Nix/deterministic/public-text are green. Acceptance-test names, evidence, conservative decisions and the two B open questions are in the PR bodies.

    #340 macOS CI is now green after two authorized failed-job reruns without changing either timing-flake test: initially continuous_events_are_bounded_by_max_coalesce_window, then the known a_peer_returns_after_minutes_and_hours_without_errors_or_a_helper. First rerun; final successful rerun. Both PRs are ready for bottom-up review. Nothing merged and no auto-merge enabled.

    Posted on behalf of @schickling
    field value
    agent_identity agent/dotfiles/fabric-mesh
    session 01a17e44
    agent_persona generalist
    agent_supervisor unavailable
    agent_tool OMP
    agent_tool_version 18.12.5
    agent_runtime OMP 18.12.5
    agent_model anthropic/claude-opus-5-5
    worktree smalltalk/main
  5. myobie commented on Oct 29, 2026

    @myobie
    Collaborator

    Implemented by #340 (expiry) and #341 (prepare/activate/status), both merged to main. Not in a release yet: 0.9.4 follows the small hardening PR requested on #341. Keeping this open until then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions