Files
sherlock/docs/storage.md
T
amacocian 24f77e7b74 Phase 1: login broker + agent-agnostic wrapper
Lock and implement Phase 1 decisions:

- #8 token storage: OS keyring (zalando/go-keyring) with strict probe at
  startup of both sherlock and sherlock-broker; fail fast with exit 3
  and a per-OS hint if Secret Service / Keychain / Credential Manager
  is missing.
- #9 RPC framing: JSON-over-newline on the UDS at
  $XDG_RUNTIME_DIR/sherlock.sock, debuggable with socat.
- #10 broker lifecycle: forked child process (setsid-detached), per-user
  PID-file flock prevents double-start, auto-exit after
  SHERLOCK_BROKER_IDLE (default 1h). No systemd.
- #11 loopback port: 127.0.0.1:6990 for the Authentik PKCE callback.
  Actual Authentik provider creation deferred; login_start returns a
  clean 'not_configured' error mentioning the env vars to set, and the
  full OIDC path is exercised by an integration test against a stub
  Authentik (httptest + go-jose ES256 signer).

New packages (all green under `go test -race`):
- internal/xdg, internal/rpc, internal/socket — primitives
- internal/keyring (+ fake/) — Probe, Store, TokenSet
- internal/authn — discovery, PKCE, loopback flow, single-flight refresh
- internal/broker — lifecycle, server, spawn, RPC methods
- internal/agent — TOML profile loader (embedded + user overlay),
  MCP-config renderer, argv/env builder, syscall.Exec wrapper

CLI:
- cmd/sherlock: login / logout [--shutdown] / status / run <agent> /
  <agent-name> alias dispatch
- cmd/sherlock-broker: daemon subcommand wiring all of the above

Deps: zalando/go-keyring, BurntSushi/toml, coreos/go-oidc/v3,
golang.org/x/oauth2, golang.org/x/sync. go directive bumped to 1.25;
CI Go version bumped to 1.26.3 to match.

Docs: new docs/storage.md and docs/rpc.md; auth-model, conventions,
README, plan.md all updated to reflect the locked decisions.

End-to-end verified locally: auto-spawn broker, status, login refused
with not_configured, agent alias execs through with the rendered MCP
config path, logout --shutdown brings the socket down.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-24 22:39:34 +02:00

2.6 KiB

Storage

How sherlock persists secrets and runtime state.

Decisions

Topic Decision
Token storage OS keyring via github.com/zalando/go-keyring. No on-disk credential files, no age blobs, no plaintext.
Pre-flight Both sherlock and sherlock-broker call keyring.Probe() at startup. A missing/locked keyring fails fast with a platform-appropriate hint and exit code 3.
Service name sherlock for real tokens; sherlock-preflight for the probe sentinel.
Account key The Authentik sub claim once logged in, default before.
Runtime files $XDG_RUNTIME_DIR/sherlock.sock, sherlock.pid, sherlock.log, sherlock/<agent>.mcp.json. All 0600 (files) / 0700 (dirs).
Config files $XDG_CONFIG_HOME/sherlock/agents.d/*.toml (operator overrides) and services.d/*.toml (Phase 2+).

TokenSet

What we store under the sherlock keyring service:

type TokenSet struct {
    IDToken      string
    AccessToken  string
    RefreshToken string
    IDExpiresAt  time.Time
    RefreshExpAt time.Time
    Issuer       string
    Subject      string
    Email        string
    Name         string
}

Serialized as a single JSON blob — the keyring exposes one secret per (service, account) pair and we don't want to round-trip multiple secrets.

Pre-flight semantics

keyring.Probe():

  1. Writes a fixed sentinel value under service sherlock-preflight, account probe.
  2. Reads it back, verifies the round-trip.
  3. Deletes it.

If any step fails the call returns keyring.ErrUnavailable wrapping the underlying cause. keyring.IsUnavailable(err) is the test; keyring.RemediationHint() returns a per-OS one-liner.

CLI behaviour: any failure prints the wrapped error and the hint, then exits with code 3.

Platform notes

OS Backend Common setup snag
Linux Secret Service (D-Bus) gnome-keyring-daemon, KWallet, or keepassxc with Secret Service enabled must be running for the session. Headless boxes need gnome-keyring-daemon --components=secrets started inside the session bus.
macOS Keychain Works out of the box. First write may prompt for unlock.
Windows Credential Manager Works out of the box.

Why not files

We considered an age-encrypted token blob and dropped it: the keyring gives us OS-managed locking, session affinity, and consistent multi-user behaviour for free, and avoids inventing a new key management story. The trade-off — Linux headless setups need a deliberate session keyring — is the right one for a homelab operator tool where the operator already has a desktop session.