Skip to content

Instantly share code, notes, and snippets.

@sini
Created August 20, 2026 21:48
Show Gist options
  • Select an option

  • Save sini/327c41331c5af65ba66a6a21f082c25a to your computer and use it in GitHub Desktop.

Select an option

Save sini/327c41331c5af65ba66a6a21f082c25a to your computer and use it in GitHub Desktop.
OPKSSH Token Refresh Daemon — delivery plan and decision log (companion to the design spec)

Refresh Daemon — Delivery Plan

Companion to refresh_daemon_spec.md (the ratified design, 2026-08-20). All design forks from the earlier draft of this file are resolved; the decision log below records the rulings. Branch: feat/refresh-daemon, stacked on feat/agent-lifetime (PR 1 = openpubkey/opkssh#611).

Sequenced deliverables

0. Predicate PR — storage layout (own design discussion; not yet designed) XDG base-dir support ($XDG_CONFIG_HOME/opk, $XDG_STATE_HOME/opk, ~/.opk fallback) + per-provider key-file naming replacing single id_ecdsa. Correctness prerequisite for increment 2's multi-session disk sync (fixed path clobbers). Scope to settle in its discussion: migration/back-compat for existing key paths, configureSSH IdentityFile references, logout coverage of both layouts.

1. Daemon PR, increment 1 — engine + foreground host. No dependencies; can start immediately. Extract LoginWithRefresh's loop into daemon/ (engine.go, session.go, agent.go, discovery-free), with: injected clock; merged tick with one-tick refresh lookahead; backoff ladder capped at backoff_max, retries independent of exp (degraded state, post-exp recovery); exp from FreshIDToken; per-session refreshability determination; exp-bound certs + exp+5m agent lifetimes for refresh-capable sessions (--lifetime flag always wins for agent lifetime); Add-before-Remove rotation with access-token re-embed; login --auto-refresh (daemonless) hosts it in-process. login's PR-1 agent add refactors onto daemon.DialAgent. Fixes a live upstream bug en passant (flag in the PR): current payloadFromCompactPkt reads the fresh ID token's protected header as the payload (compact format is colon-joined + dot-appended fresh token), so the tracked exp never updates after the first refresh and --auto-refresh hot-loops refresh requests against the OP.

2. Daemon PR, increment 2 — daemon process. opkssh daemon + flow.go (daemon-owned auth via OpenBrowser:false + SetLoginURIHook), ipc server/client (ndjson, singleton socket), reconcile loop over registered sessions + monitor-only discovery, opkssh status, logout drop integration, daemon: config block. Multi-identity disk sync gated on deliverable 0.

3. Documented follow-ups (named in spec §12 and the PR body; not built): persistence/keyring (carries the upstream GetRefreshToken accessor dependency), Windows transports, desktop notifications, service manifests, agent-backed key-binding signer.

Decision log (ratified 2026-08-20)

Decision Ruling
Process model Daemon standalone, user-started, never spawned by login
Auth ownership Daemon owns flow + secrets; CLI handles all browser interaction (launch/print URL)
Login gating Daemon manages every login when running; --auto-refresh = daemonless compat, not a gate; --no-daemon escape
Restart model Lost daemon = lost refresh; monitor-only rehydration; persistence = follow-up
Refresh token placement Never in cert/agent/socket (rejected with analysis, spec §10); disk store deferred to follow-up on predicate layout
Reconcile One merged pass, agent_sync_frequency 60s; reconcile-authority over ssh-add -D; prune rule deleted (lifetimes self-clean)
Cert expiry Refresh-managed: ValidBefore = exp (Ethan-ratified); daemonless one-shot keeps PR-1 semantics
IPC format ndjson + versioned envelope (repo precedent: JSON only; no gRPC)
Config naming agent_sync_frequency, sock_path, ssh_auth_sock (named for $SSH_AUTH_SOCK)
Package shape Flat top-level daemon/; _unix/_windows suffix files; commands structs + main.go wiring; config in commands/config
Upstream openpubkey Zero changes needed in core (verified: SetLoginURIHook exists); accessor moves to persistence follow-up

Adversarial-review amendments (2026-08-20, verdict SOUND-WITH-FIXES)

All findings verified at primaries and folded into the spec:

  • Refreshability carve-out (C1): determined per session at flow completion (Refreshable provider AND refresh token granted); non-refreshable sessions mint PR-1 style, no worker; CI/CD providers bypass the daemon by design (owner ruling).
  • Retry terminus (I1): retries independent of exp (oidc_refreshed accepts post-exp refresh); degraded state; terminal = invalid_grant only.
  • Margin vs tick (I2): one-tick lookahead on the due condition.
  • Chooser + invariant scoping (I3): start-login has an explicit chooser variant (daemon builds the list from its config); zero-credential invariant scoped with the CLI-supplied client_secret exception.
  • Singleton (I4): flock lockfile authority, probe-timeout = running, socket self-stat per reconcile pass (XDG cleanup survival).
  • --lifetime precedence (I5): explicit flag always wins for agent lifetime; config agent_lifetime = non-refresh sessions only.
  • Session key (I6): (issuer, client_id, sub); email/comments display-only.
  • Agent-socket drift (I7): status prints the managed socket; CLI warns on SSH_AUTH_SOCK mismatch.
  • Minor: ssh_auth_sock rename, access-token re-embed on rotation, provider writer plumbing (SetDefaultWriters), TempDir fallback wording, observable-state wording, flow-collision UX.

Next actions

  1. Publish both documents as public gists (owner-directed); share with Ethan on #606 (design changed since the gist he blessed — daemon-owned auth, exp-binding scope, follow-up roster) before or alongside implementation.
  2. File the upstream payloadFromCompactPkt hot-loop bug as an issue (verified live on main; increment 1 fixes it in opkssh).
  3. Write the implementation plan for increment 1 (writing-plans), then build.
  4. Schedule the predicate-PR design discussion (deliverable 0) — required before increment 2 lands, not before increment 1 starts.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment