24f77e7b74
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>
3.0 KiB
3.0 KiB
Broker RPC
The wire protocol between sherlock (CLI) and sherlock-broker (daemon).
Framing — Decision #9
JSON-over-newline on a Unix domain socket at
$XDG_RUNTIME_DIR/sherlock.sock.
- One JSON object per line,
\n-terminated, both directions. - UTF-8.
- Each connection is independent; the client serialises its own writes, the server fans out one goroutine per connection.
- Debuggable with
socat - UNIX-CONNECT:$XDG_RUNTIME_DIR/sherlock.sock.
Request
{ "id": 7, "method": "status", "params": {} }
Response
{ "id": 7, "result": { ... } }
or
{ "id": 7, "error": { "code": "not_logged_in", "message": "no Authentik tokens stored" } }
id echoes the request id. result and error are mutually
exclusive; exactly one is present (the other is null/omitted).
Method catalogue
| Method | Params | Result |
|---|---|---|
status |
{} |
{ logged_in, sub?, name?, email?, id_expires_at?, refresh_expires_at?, issuer?, authn_configured, agents[] } |
whoami |
{} |
{ sub, name, email } — errors with not_logged_in if no tokens. |
login_start |
{} |
{ auth_url, state } — starts the loopback listener if not already up. |
login_wait |
{ state, timeout_seconds } |
{} — blocks until the OAuth callback for state arrives or the timeout elapses. |
logout |
{} |
{} — idempotent; clears the keyring entry. |
get_id_token |
{} |
{ token, expires_at } — refreshes via refresh_token if needed (single-flight). |
shutdown |
{} |
{} — broker exits after flushing the response. |
Error codes
| Code | Meaning |
|---|---|
invalid_request |
Malformed JSON, missing method, or bad params. |
unknown_method |
Server doesn't recognise method. |
not_logged_in |
Operation requires a live token set; none is stored. |
not_configured |
Broker is missing Authentik issuer/client ID (Decision #11 deferral). The error message names the env vars to set. |
flow_expired |
login_wait timed out or the in-flight state was cleared. |
keyring_unavailable |
Keyring write/read failed mid-session (rare; probe runs at startup). |
internal |
Anything else — message contains the underlying error. |
Codes are stable strings, not numbers. The CLI matches on them when it
needs branching behaviour (e.g. sherlock login suppresses
already_in_flight and just calls login_wait).
Lifecycle interaction
Per Decision #10, the wrapper CLI auto-forks the broker when the socket isn't there. Concretely:
- CLI dials
$XDG_RUNTIME_DIR/sherlock.sock. - On
ECONNREFUSED/ENOENT:fork+exec sherlock-broker daemonwithsetsid, stdin/out/err pointed at$XDG_RUNTIME_DIR/sherlock.log. - CLI polls the socket every 50ms for up to 2s, then retries the dial.
- Broker holds an exclusive
flock()onsherlock.pid; a seconddaemoninvocation exits silently if the lock is taken. - Broker auto-exits after
SHERLOCK_BROKER_IDLEof no RPC activity (default 1h).