Files
sherlock/docs/rpc.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

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:

  1. CLI dials $XDG_RUNTIME_DIR/sherlock.sock.
  2. On ECONNREFUSED/ENOENT: fork+exec sherlock-broker daemon with setsid, stdin/out/err pointed at $XDG_RUNTIME_DIR/sherlock.log.
  3. CLI polls the socket every 50ms for up to 2s, then retries the dial.
  4. Broker holds an exclusive flock() on sherlock.pid; a second daemon invocation exits silently if the lock is taken.
  5. Broker auto-exits after SHERLOCK_BROKER_IDLE of no RPC activity (default 1h).