feat: add versioned client and provider IPC - #362
Conversation
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| 🔵 In progress View logs |
secretspec | be91afe | Aug 20 2026, 05:55 PM |
2b080cc to
337950c
Compare
Filed the check-stdout report as cachix#372 and opened cachix#373 against it. The PR is built on upstream/main rather than cherry-picked from sudo-main, so it carries only the secrets.rs and check_report_stream.rs hunks plus a hand-written Changed entry under upstream's own Unreleased -- the fork's CHANGELOG diff is 458 fork-local insertions and could not be lifted. All three regression tests were verified against a pure dfa4b10 base, not just against our merged tree. Posting also turned up a hole in a draft marked READY TO POST: the ELI5 section said "if you try to do the obvious thing:" and then jumped straight to "...you get nothing", with the example command block missing entirely. Restored before sending. The larger find is upstream PR cachix#362, which the ledger did not track at all -- it was visible only as a pointer in a comment on cachix#64. It introduces SecretSpec IPC v1 for 0.20+, including `secretspec broker --stdio`, and is close enough in vocabulary to this fork that the distinction has to be written down: upstream's broker is an IPC endpoint inside the caller's own trust domain, not a privilege boundary. Its initialize accepts a caller-supplied manifest, provider and profile, which is exactly what this fork's control plane exists to remove, and its audit is fail-open where ours is fail-closed and hash-chained. The practical consequence is favourable: `secretspec.provider/1` is the exec:// mechanism cachix#345 asked for, and a privileged endpoint can be registered as data without patching upstream internals. Recorded in docs/design/upstream-ipc-v1-and-the-fork.md, along with the finding that cachix#362 does NOT retire the codegen-schema shape debt -- no manifest-shape reflection anywhere in the client protocol, so cachix#371 remains the only route. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tier 2 handoff e439, parent 7c73 (deterministic). Covers releasing 0.19.1-sudo.15, posting upstream issue cachix#372 and PR cachix#373, tracking upstream's IPC v1 PR cachix#362 -- and the vault truncation incident this session caused. The incident is the reason this document leads with it rather than the release: plain `install` without --adopt-existing truncated /var/db/sudo-secretspec/.env to 0 bytes, destroying every stored value. Both shipped docs specify the flag (SKILL.md:197, AI-GUIDANCE.md:87) and I handed over the command without it. The audit ledger brackets the loss to 26 seconds after the install, and Arq's Aug 17 02:10 SYSTEM record predates it, so recovery is available. Three failed approaches are recorded in full because each was expensive: the merge hypothesis presented to the operator before it was cheaply falsifiable, reading a green template-check as reassurance when it was evidence of the overwrite, and concluding the loss predated the session because fs::copy on macOS preserves source mtime. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Operator request 2026-08-17, sequenced after the .16 release, cachix#370 manifest-edit, and the cachix#362 comment. Motivated by the vault truncation incident: the boundary keeps no history of its own, so logical loss currently depends on external backup tooling to recover. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…view draft The design doc asserted /Library/Application Support is admin-group writable on macOS. Verified false against this machine (26.6.1): root:admin 0755, no ACL. Replaced with what actually is verifiable in external.rs as of PR head 337950c -- the unix trust check is blind to macOS ACLs (only the Windows path validates ACLs), both checks follow symlinks via fs::metadata, and trust genuinely does stop at the immediate parent with no verification above it. Landed the corrected review comment as a tracked file (docs/design/pr362-comment.md) rather than leaving it in scratch, since it was drafted but not yet posted when work paused. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Following up from #345 (closed into #64) — One piece of review feedback from operating that kind of boundary, aimed at the unix registration trust checks in
Happy to turn this into (a) a small hardening PR against |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
a2109d8 to
37cc1ad
Compare
Parent 7a1c. The two-database merge is superseded: the gate between the upstreamable provider and this privileged fork already exists upstream as secretspec.provider/1 (PR cachix#362), verified against feat/ipc-v1 at a393a27. Records three retracted findings and the method that caught them, the new djbclark/secretspec-sqlite fork, and the endpoint prototype worktree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
macOS:
|
d53dd82 to
111fa48
Compare
111fa48 to
a69bbfc
Compare
|
The INI It was unrelated to the IPC work and only shared a commit with it, so the two can now be reviewed and merged independently. This branch is unchanged otherwise: the first commit was rewritten to drop the INI hunks and the remaining 18 were replayed on top, so the IPC diff is byte-identical to what it was. Created by Claude Code (see cli/cli#13904 for why this note is here rather than in the GitHub API). |
bb96dbe to
7118678
Compare
40356cb to
bfcfbfb
Compare
Adds SecretSpec 0.20 local IPC across two boundaries with different authority. The Secret Resolution Protocol resolves an exact declared name through a complete configuration for an application or SDK. The Secret Provider Protocol implements one provider behind SecretSpec's resolver, receiving provider addresses and values but not SecretSpec's storage or resolver internals. Both ride one wire protocol: JSON-RPC 2.0 in bounded length-prefixed frames over a child process's stdin and stdout. `secretspec serve` runs one resolver session as a private child of whoever launched it. It never prompts on the protocol streams and exits with its parent. `--read-only` refuses any resolution that would write to a provider, not just the store and remove methods: resolving is not always a read, since a `generate = true` declaration with no stored value is minted and written back, and a `prompt = true` one is written back after a person answers. The protocol ships two first-class implementations tested against one language-neutral conformance suite plus differential state-machine tests, so a bug shared between them is less likely to redefine the contract. `libsecretspec-resolver` is a portable C11 resolution client, C rather than a C ABI over Rust, with opaque handles, explicit-length buffers, and no callbacks into foreign runtimes. `secretspec-ipc` is the Rust client/server crate, with typed sessions, endpoint helpers, and a `blocking` feature for consumers that must not acquire an async runtime. A declaration with `prompt = true` resolves over IPC by asking the process that launched the resolver, which has the terminal. A client declares during initialization whether it can answer, so a headless consumer is never asked and never waits out a deadline. The C client answers prompts without handing a function pointer across the ABI: a waiting call reports a pending prompt, and the caller takes it, answers or declines, and waits again. `resolver.reject` discards SecretSpec's cached copy of a value the consumer was refused, since expiry only retires what the clock invalidated and a token revoked at its issuer stays fresh by the clock. Optional `resolver.set` and `resolver.delete` let a consumer store or remove one declared secret on the route the session already reads from. Unknown error kinds and value sources decode rather than failing the session, so a later revision can name a new failure or origin without breaking a deployed peer. Deadlines live once on the request envelope and are clamped to a 300 second horizon by both clients so a peer cannot hold an in-flight slot indefinitely. Windows ACL isolation covers provider discovery and resolver lease files. An endpoint that writes a banner or stack trace to the frame stream is reported as having written non-protocol text, without echoing the bytes. The embedded C ABI is renamed to `libsecretspec`, with `libsecretspec.so`, `.dylib`, `secretspec.dll`, `libsecretspec.a`, and `libsecretspec.pc` as its public artifacts. Runtime SDK loaders still recognize the pre-0.20 `secretspec-ffi` filenames. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bfcfbfb to
be91afe
Compare
Summary
This PR adds two versioned, private-child-process IPC protocols for SecretSpec:
secretspec.resolver/1secretspec servesecretspec.provider/1Both protocols use strict JSON-RPC over 4-byte length-prefixed stdio frames. They share initialization, version and capability negotiation, limits, deadlines, cancellation, structured errors, and orderly shutdown, while keeping their method sets separate.
This is deliberately not a daemon or socket protocol: every session launches a directly connected private child process without a shell.
What this PR adds
secretspec-ipcRust crate with async client/server primitives, protocol types, lifecycle management, and a blocking resolver client.secretspec-ipc-conformancecrate that exercises Rust and C implementations against shared malformed-input and lifecycle cases.secretspec serveandsecretspec serve --read-onlyas the resolver endpoint used by applications and SDKs.libsecretspec-resolver, with CMake and Meson support and vendored yyjson.Resolver protocol
Applications launch
secretspec servedirectly and selectsecretspec.resolver/1during initialization. Initialization fixes the manifest source, base directory, profile, provider selection, scope, and reason for the lifetime of the session.Core methods:
resolver.getrequests exactly one named secret and returns either an inline value or a leased file.resolver.releasereleases leased material; outstanding leases are also cleaned up on disconnect.resolver.rejectinvalidates only the derived or cached copy that the caller rejected. It remains available in read-only sessions and is required in the 0.20+ protocol surface.resolver.setandresolver.deleteare optional 0.20+ capabilities advertised by writable endpoints.client.promptlets the resolver request user input when the client advertises prompt support.secretspec serve --read-onlydoes not advertise mutation methods and rejects resolution paths that would need to persist generated or prompted values. Headless clients receive an immediate missing/error result instead of leaving an unanswered prompt pending.The C client exposes pending prompts through take/answer/decline/wait operations rather than invoking callbacks into an unknown foreign runtime. Prompt answers are validated before they are consumed, including strict UTF-8 validation.
External provider protocol
External providers are registered as trusted executable definitions with an absolute executable path, fixed arguments, and declared credential environment names. SecretSpec launches one private child per provider URI and reason, then selects
secretspec.provider/1during initialization.The protocol covers provider naming and addressing,
get,get_many,exists,set, expiring writes,delete,clear, writability checks, descriptions, preflight checks, and reflection. Capabilities are negotiated independently, so narrow or write-only providers do not need to pretend to support unrelated operations.Provider failures retain structured error information. Session loss and reconnect behavior distinguish replay-safe requests from mutations that must not be replayed automatically.
Library boundaries
secretspec-ipcis the Rust implementation of both IPC protocols.libsecretspec-resolveris the pure-C11 client forsecretspec.resolver/1only.libsecretspecremains the embedded, in-process resolver ABI used by existing SDK integrations. It was renamed fromsecretspec-ffi; the three exported C symbols remain unchanged, and loader compatibility is documented and tested.Keeping the embedded ABI separate from the child-process protocol makes the security and lifecycle model explicit for each integration style.
Security and lifecycle hardening
FILE_DELETE_CHILD.Conformance and testing
The conformance suite now runs a shared malformed-input corpus across the Rust resolver client, C resolver client, provider endpoint, and external-provider adapter. It covers empty and batch messages, duplicate keys, invalid UTF-8, truncated headers and payloads, oversized frames, and unknown response IDs.
Lifecycle coverage includes:
/procassertion that startup-timeout children are reaped;Focused gates run during the hardening pass:
The full Linux, macOS, Windows, SDK, packaging, feature, and documentation matrix also runs in CI.
Documentation
PR #98 feedback incorporated
This design preserves the useful direction from #98—version negotiation, capability discovery, explicit limits, structured errors, preflight checks, and external-provider extensibility—while tightening process ownership, least-access resolution, cancellation/deadline behavior, non-replayable mutations, lease cleanup, prompt handling, executable trust validation, and cross-language conformance.