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>
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():
- Writes a fixed sentinel value under service
sherlock-preflight, accountprobe. - Reads it back, verifies the round-trip.
- 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.