The division of labour, settled 2026-08-18
This repo keeps privilege separation only: vault files owned by a dedicated
service user, unreadable and unwritable by the calling user, mediated through
sudo. The sqlite provider — including its opt-in history feature — goes
upstream via djbclark/secretspec-sqlite, not here.
Privilege separation is not a provider concern, and this repo's own code
already says so:
Confidentiality comes entirely from filesystem permissions on the database
file — the schema and connection carry no credential of their own. That is
what lets identical code serve both an ordinary user-owned store and a
privilege-boundary vault file guarded by a 0600 service-user-owned path:
the privilege model stays external to the provider.
— secretspec/src/provider/sqlite.rs:7-11
The Provider trait is get/set/delete/reflect/preflight. It has no notion of
caller UID or daemon lifecycle, and adding one would force every provider to
implement a security model it does not need.
The gate between the two halves: upstream PR cachix#362
This was already worked out on 2026-08-16 and then went untracked — hence this
issue. See docs/design/upstream-ipc-v1-and-the-fork.md.
cachix/secretspec PR #362
("SecretSpec IPC v1 for 0.20+", authored by the maintainer) defines two
protocols over one framed JSON-RPC layer:
| boundary |
protocol |
| app / SDK → secretspec broker |
secretspec.client/1 |
| secretspec → external provider endpoint |
secretspec.provider/1 |
secretspec.provider/1 is the gate: this fork becomes an out-of-tree
privileged provider endpoint, with no patching of upstream provider
internals.
Load-bearing distinction — upstream's "broker" is not a privilege boundary.
It is a private child process of the caller, in the caller's own trust domain.
Forwarding secret authority is a stated upstream non-goal. Upstream built the
plumbing and explicitly fenced off this fork's territory, so cachix#362 grants the
out-of-tree mechanism cachix#345 asked for.
Already engaged: a supportive comment was posted to cachix#362 on 2026-08-17
(cachix#362 (comment), text at
docs/design/pr362-comment.md), announcing intent to ship the first out-of-tree
privileged endpoint, and offering three verified gaps in upstream's
external.rs trust checks as review feedback — POSIX-mode checks blind to macOS
ACLs, std::fs::metadata following symlinks, and trust stopping at the
immediate parent. This fork's drift.rs check_ancestor_chain walks the resolved
chain to / and rejects all three.
Work items
Superseded by the above
- The two-database file merge (
secrets.db | broker.sqlite3) — the
"cut by owner" rationale that broke a 2–2 reviewer tie assumed the provider
and the broker are permanently separate owners. Under the plan above the
provider goes upstream and this repo keeps only privilege separation, so that
boundary is not the one to design around. Do not build it without revisiting.
- Its
transactions FK follow-up, for the same reason. Note if it is ever
revisited: SQLite cannot add a FK to an existing table and FKs cannot span
files (both verified), so splitting the FK from the merge means rewriting the
audit trail twice.
Still live, independent of all of the above
drift/doctor never verifies the manifest chain at rest.
drift.rs:1036 verifies only audit::verify_read_only; there is no history
equivalent, and every history::verify caller is test-only (broker.rs:1805,
1832, 1855, all after #[cfg(test)] at broker.rs:1560). The chain is
verified on every capture (history.rs:387), so this is a detection-at-rest
gap, not silent rot. Worth closing: a deliberate integrity check should not be
asymmetric between two ledgers of equal standing.
Retracted, do not re-report
"No busy_timeout anywhere in the CLI crate" is FALSE. rusqlite 0.31.0
calls sqlite3_busy_timeout(db, 5000) unconditionally on every
Connection::open (rusqlite-0.31.0/src/inner_connection.rs:121). Two
independent reviewers reported it as a P0; a fix was written, mutation-tested,
found to be a no-op, and reverted. grep finding no explicit call is not
evidence the behavior is absent when a library sets a default. See handoff
7a1c.
Related
Companion issue in the upstreaming fork: djbclark#1 — djbclark/secretspec-sqlite (upstreaming the provider)
The division of labour, settled 2026-08-18
This repo keeps privilege separation only: vault files owned by a dedicated
service user, unreadable and unwritable by the calling user, mediated through
sudo. The sqlite provider — including its opt-in history feature — goesupstream via
djbclark/secretspec-sqlite, not here.Privilege separation is not a provider concern, and this repo's own code
already says so:
The
Providertrait is get/set/delete/reflect/preflight. It has no notion ofcaller UID or daemon lifecycle, and adding one would force every provider to
implement a security model it does not need.
The gate between the two halves: upstream PR cachix#362
This was already worked out on 2026-08-16 and then went untracked — hence this
issue. See
docs/design/upstream-ipc-v1-and-the-fork.md.cachix/secretspecPR #362("SecretSpec IPC v1 for 0.20+", authored by the maintainer) defines two
protocols over one framed JSON-RPC layer:
secretspec.client/1secretspec.provider/1secretspec.provider/1is the gate: this fork becomes an out-of-treeprivileged provider endpoint, with no patching of upstream provider
internals.
Load-bearing distinction — upstream's "broker" is not a privilege boundary.
It is a private child process of the caller, in the caller's own trust domain.
Forwarding secret authority is a stated upstream non-goal. Upstream built the
plumbing and explicitly fenced off this fork's territory, so cachix#362 grants the
out-of-tree mechanism cachix#345 asked for.
Already engaged: a supportive comment was posted to cachix#362 on 2026-08-17
(cachix#362 (comment), text at
docs/design/pr362-comment.md), announcing intent to ship the first out-of-treeprivileged endpoint, and offering three verified gaps in upstream's
external.rstrust checks as review feedback — POSIX-mode checks blind to macOSACLs,
std::fs::metadatafollowing symlinks, and trust stopping at theimmediate parent. This fork's
drift.rs check_ancestor_chainwalks the resolvedchain to
/and rejects all three.Work items
secretspec.provider/1endpoint againstfeat/ipc-v1in a scratch worktree. Small shim; it proves the "noupstream patching" claim instead of asserting it. DONE 2026-08-18 —
ss-ipc-protoata204898,provider.lifecyclepasses,git diffinthe upstream checkout is empty. See the comment below for the four
findings.
--clientpath to the live vault end to end. Itis implemented, cancellation kills the privileged child, but it has never
been run against the live boundary. This is the next action.
sigtimedwaitportability fix upstream on feat: add versioned client and provider IPC cachix/secretspec#362.libsecretspec-ipc/src/process_posix.c:237makes the conformance suiteunbuildable on Darwin; a portable
sigpending+sigwaitfix is ready asa patch file in the prototype crate.
secretspec-ipctypes for this fork's ownclient↔broker hop, keeping
sudoas the authority mechanism.does not address it at all. Risk: "SecretSpec has a broker, with audit and
reasons" will sound to the ecosystem like it already covers the local-AI-
agent threat model.
transport with peer credentials, it genuinely enters this fork's
space. Counter is to be the incumbent reference privileged deployment
shaping that spec — which argues for engaging now, not after 0.20 ships.
Superseded by the above
secrets.db|broker.sqlite3) — the"cut by owner" rationale that broke a 2–2 reviewer tie assumed the provider
and the broker are permanently separate owners. Under the plan above the
provider goes upstream and this repo keeps only privilege separation, so that
boundary is not the one to design around. Do not build it without revisiting.
transactionsFK follow-up, for the same reason. Note if it is everrevisited: SQLite cannot add a FK to an existing table and FKs cannot span
files (both verified), so splitting the FK from the merge means rewriting the
audit trail twice.
Still live, independent of all of the above
drift/doctornever verifies the manifest chain at rest.drift.rs:1036verifies onlyaudit::verify_read_only; there is no historyequivalent, and every
history::verifycaller is test-only (broker.rs:1805,1832,1855, all after#[cfg(test)]atbroker.rs:1560). The chain isverified on every capture (
history.rs:387), so this is a detection-at-restgap, not silent rot. Worth closing: a deliberate integrity check should not be
asymmetric between two ledgers of equal standing.
Retracted, do not re-report
"No
busy_timeoutanywhere in the CLI crate" is FALSE. rusqlite 0.31.0calls
sqlite3_busy_timeout(db, 5000)unconditionally on everyConnection::open(rusqlite-0.31.0/src/inner_connection.rs:121). Twoindependent reviewers reported it as a P0; a fix was written, mutation-tested,
found to be a no-op, and reverted.
grepfinding no explicit call is notevidence the behavior is absent when a library sets a default. See handoff
7a1c.Related
Companion issue in the upstreaming fork: djbclark#1 — djbclark/secretspec-sqlite (upstreaming the provider)