opencode: persist custody telemetry to a bounded file, and say so when serving

Merged
IC
2d ago
+613-30

The custody plugin's telemetry was going nowhere. It logs structured JSON via console.log/console.error; under the OpenCode TUI that is the pty, which nothing persists — opencode.log had 0 such lines, the daemon journal had 0. And the plugin was silent on the happy path (1 debug / 0 info / 2 warn / 2 error call sites), so "no news" was indistinguishable from "not running". The only witness that it was serving at all was the vault's audit chain, which only works when one operator owns both ends.

What this adds

A bounded JSONL file sink, on by default, alongside the console sink.

  • $CLAUSTRUM_CUSTODY_LOG if set, else ${XDG_STATE_HOME:-~/.local/state}/cortexkit/opencode-plugin/custody.jsonl. Dir 0700, file 0600.
  • Rotates to <path>.1 past 5 MiB; one generation kept.
  • CLAUSTRUM_CUSTODY_LOG=off|0|false|no disables it.
  • Fail-open for telemetry: if the path cannot be created or written, one console warn and serving continues on the console sink. The inverse of the credential path, deliberately — a logging failure must never refuse a request.

Two happy-path lines, both bounded.

  • At the config hook, one info per provider with the cell decision in the plugin's existing vocabulary (serving / refusal states).
  • On the first successful serve per provider per process, one info with {provider, label, credentialId, recordVersion, state:"served"}. Never per request.

The property that matters: the file cannot carry a secret

Entries written to the file pass through an explicit allowlist (FILE_FIELDS: level, provider, label, credentialId, recordVersion, state, httpStatus, cooldownUntil, errorClass, errorCode). errorMessage is not in it — Bun's JSON.parse quotes adjacent tokens into its error message, which is how a hand-edited handle file leaks a bearer handle (fixed once already in #28). No free-text field reaches disk.

Pinned by a canary that drives a fault path whose error text contains a fake 47-char ckh_ handle and a fake key, then asserts neither appears in the file. Mutation-proved independently of the implementer: adding errorMessage to the written record (exactly one site) turns it red —

(fail) custody logger > file sink excludes free-text error messages
 6 pass · 1 fail

— byte-identical restore, 7 pass.

Verified on the installed bundle

Exercised the built bundle's config hook in-process against a scratch XDG_STATE_HOME on the live box: wrote {"level":"info","provider":"minimax-coding-plan","state":"serving","ts":…,"pid":…} (the one provider under custody here), file 600 / dir 700, secret-shape scan clean. Bundle shape unchanged: single {id, server} default export, Node builtins only.

Hermetic 152/152, gate green, exit census updated (plugin 33/41, serve 13/15).


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Fixes custody telemetry that printed into the OpenCode pty and was lost, while the plugin stayed silent on the happy path. Telemetry now persists to a bounded JSONL file (on by default), and no plugin log level reaches the TUI anymore — warn and error included.

File sink

  • Writes to $CLAUSTRUM_CUSTODY_LOG if set, else ~/.local/state/cortexkit/opencode-plugin/custody.jsonl (dir 0700, file 0600); rotates to .1 past 5 MiB, and off|0|false|no disables it.
  • Fails open: on write failure, one console warn, then all levels are dropped rather than reaching the screen.
  • Entries gain process-generated ts and pid, and pass a field allowlist plus shape rules that drop errorMessage and secret-shaped values.

Log lines and safety

  • Logs one info per provider at config time with the decision, and one served line per provider per process — never per request.
  • The client's unknown-error logger is routed into the same sink.
  • Provider and label validation reuse the client parser's exported identifierIsValid, so the sink accepts only what the parser would.
  • A canary drives a malformed handle file through the real config hook and asserts no secret appears; console-silence tests stub all four console channels.