Skip to content

[1.0.0] Design: failproofaid — Rust daemon, thin client, and unified collector - #601

Draft
NiveditJain wants to merge 2 commits into
mainfrom
1.0.0
Draft

[1.0.0] Design: failproofaid — Rust daemon, thin client, and unified collector#601
NiveditJain wants to merge 2 commits into
mainfrom
1.0.0

Conversation

@NiveditJain

@NiveditJain NiveditJain commented Jul 27, 2026

Copy link
Copy Markdown
Member

What this is

A design document for review — no implementation. It proposes re-architecting failproofai
from a stateless, cold-start-per-event hook into a long-lived daemon that also absorbs
agenteye-collector, so one process per machine both enforces policy and collects
agent sessions.

📄 docs/superpowers/specs/2026-07-27-failproofaid-daemon-design.md

Deliberately a draft. The expectation is several rounds of review before any code is written.

Revision 2 — two review changes: the dashboard's re-architecture moves into scope,
and the doc gains an update system (auto-update plus vendor drift detection). See
Revision 2 at the bottom for what changed and why.

The shape

A Rust supervisor owns the socket, the verdict, the durable spool, the shipper, and the
transcript tailers. It drives unprivileged Node workers that host the existing TypeScript
policy engine unchanged.

That is not a compromise between two decisions — it is the same answer arrived at twice. The
runtime decision (the collector is already Rust, and it merges into one binary) and the
privilege-separation decision land on identical topology; the unprivileged worker simply
happens to be a JavaScript runtime.

Three rules hold it together:

  1. There is exactly one policy engine, and it is TypeScript. Written down now, while it is
    cheap, because the dominant failure mode of "Rust daemon + TS engine" is someone later
    porting "just the hot builtins" for speed — and then the Rust one says allow while the JS
    one says deny.
  2. The worker computes; root decides. Workers return a verdict, never bytes and never an
    exit code. Composition is a lattice meet, never a join, so no worker message can turn a
    root DENY into an ALLOW. Absence is never allow.
  3. Root never opens a file under any user's home. Otherwise a symlink in a user-writable
    transcript directory is a one-line exfiltration primitive.

Why it's worth doing

  • Policies that remember. require-tests-before-stop is unwritable today because "did
    tests run this session?" has no answer. The five existing stop-gates fake state by
    re-deriving it every fire — require-ci-green-before-stop spawns ~4 subprocesses including
    a 15-second gh call on every Stop.
  • One collector, twelve CLIs. failproofai already parses 12 agent CLIs; the collector
    ships 3. The audit pillar is already a collector that ships nowhere.
  • A joined timeline. Hook events carry the policy verdict; transcripts carry the model
    output and tool results. Neither product can compute the join alone.
  • A tool that keeps itself current. Vendors change their schemas without notice and things
    quietly stop working. Nothing in the product checks whether it is out of date — and only a
    long-lived process can notice that a vendor has moved.

What reviewers should push on

Three recommended deviations from decisions already made — raised explicitly rather than
folded in silently, because they change agreed scope:

  • D1 — fail-closed should mean "couldn't reach the daemon and couldn't evaluate
    locally", not "the socket was missing". As specified, the first stale socket after a reboot
    stops every agent on every machine.
  • D2 — ship --user as the default and the only npm-installable mode. A root daemon
    installable from npm means a compromised release runs as root on every developer machine
    in the org
    .
  • D3 — don't give up npx -y failproofai; under per-platform optionalDependencies you
    don't have to.

Nine findings in the current codebase that this architecture would amplify, several worth
fixing regardless. The sharpest: the builtins shell out to git/gh with a cwd taken
verbatim from the client payload — harmless in a user process, root RCE on demand in a
root one. Also: the activity store writes 0644 on shared boxes, a cloned repo's policy file is
executed on the first hook event with no prompt, and the collector's documented install pipes
a remote installer straight into a shell — a pattern failproofai's own block-curl-pipe-sh
builtin denies. (That builtin blocked the first attempt to open this PR, because the body
described the finding literally. Working as intended.)

Eighteen ranked silent-enforcement-loss risks, each with a mechanical guard rather than a
review note — this repo's characteristic bug is the one where everything looks green and
nothing is enforced. Notably, an unresolvable optionalDependency is a successful npm
install, and custom policies would silently vanish after event #1 in any long-lived process
because no existing test fires two events in one process.

Nine open questions, including one load-bearing dependency: whether the AgentEye ingest
endpoint upserts on event_id. Much of the design's simplicity rests on it.

Sequencing

Sixteen numbered stages plus four lettered sub-stages, ordered so value lands before the Rust
risk. Stages 0–2 need no Rust at all — they make every existing user's hook materially
faster and fix five present-tense vulnerabilities, so the project has shipped something real
even if the Rust work slips.

Point of no return is stage 14, the first release where an install can transmit session
content. Before it, local-first is a property of the artifact; after it, a configuration.

Revision 2

The dashboard is in scope, and needs more than repackaging

Previously listed as out of scope with "its internals do not change here." That was wrong.
What the dashboard actually is today, verified:

  • Two page files import the twelve lib/<cli>-projects / -sessions modules directly and
    re-parse transcripts from disk on every request — a second, independent reader of the exact
    files the daemon will already be tailing incrementally.
  • update-hooks-config.ts and update-policy-params.ts call writeHooksConfig directly
    a second writer that bypasses every precedence rule the daemon would enforce.
  • There is no middleware.ts in the repo at all. app/api/auth/ is an identity cache
    for the audit feature; nothing under app/actions/ or app/project/ consults it. It is not
    an access gate.

That shape is merely redundant in single-user mode. Under a root, multi-user daemon it is a
confused deputy sitting on top of the authority boundary the whole design is built around — a
Next server that connects as whoever started it and answers to whoever can reach it holds
strictly more authority than the person looking at the screen.

So: reads and writes both move to the daemon, the dashboard acquires a uid-bound identity, and
it goes live instead of polled. Lands as 4b (leaves the main tarball) and 13b (onto the
daemon). A side effect worth noting — 13b resolves open question 3 by making rusqlite the
only SQLite reader in the product.

Staying current — the update channel and drift detection

The reported problem: vendors change a schema and the system starts breaking, with no
automatic way to pick up the fix.

There is no update path in the product at all — no version check, no update notifier, no
update command anywhere in the repo (new finding F9). The two install shapes rot
differently, and backwards: project scope writes npx -y failproofai, which re-resolves
and drifts forward on its own, paying a registry round trip on the hot path. User scope
the recommended one — bakes the result of which failproofai into the vendor's settings file
at install time and then sits there until a human upgrades it manually. A machine set up in
March is running March's parsers today.

Two halves, deliberately separated:

  • Updating (stage 7b). Auto-update on the stable channel by default, with channels,
    pinning, and an org disable. Health-gated swap: verify signature, stage, exec the staged
    binary against a canned deny vector, swap atomically, keep N-1, and roll back
    automatically if the post-swap health check fails
    . Drain and hand over the listener rather
    than dropping in-flight requests — under fail-closed, a refused connection blocks the user's
    agent. Plus failproofai update with --check, --channel, --rollback. The hard
    constraint: a release is only visible to updaters after every platform artifact passes the
    verify-platform gate
    — otherwise auto-update turns R12's partial publish from "some new
    installs break" into "the fleet breaks within one check interval."
  • Noticing drift (stage 11b). Auto-update only helps once a fix exists upstream; the real
    gap is the lag before anyone knows. Three signals: unrecognized transcript shapes counted
    rather than silently skipped; vendor settings files re-read and compared against what we
    wrote (self-healing, which is the agreed careless-human threat model); and silence — the
    CLI is installed, its session file is growing, and zero hook events arrived. That last one
    is "enforcement is off and nobody noticed" stated directly, and it is structurally
    unobservable in a per-event process, because the process that would notice is the one that
    never ran.

One fork flagged for an explicit decision (open question 9): shipping the parts that rot
fastest — hook config key names, event spellings, path layouts — as a signed data manifest on
a faster cadence than the binary. Recommended for parsing and detection only, never the verdict
path, because a manifest a root daemon fetches and acts on is one slip from being a remote code
channel.

Open question 8 was rewritten: it previously proposed no self-update by default,
deferring to the OS package manager. This reverses that. What still needs sign-off is the
signing key and its custody — that key now authorizes root code on every customer machine.

Note

.github/workflows/ci.yml currently triggers only on main, so PRs into 1.0.0 will run
zero CI and appear green.
(This PR gets CI because its own base is main; the stage PRs
based on 1.0.0 would not.) That fix is documented in the design but deliberately not made
here — this PR changes no workflow file and no CHANGELOG.md; it is documentation only. Both
belong to the first implementation PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ex49L3LF4YmcWicwwZWZXg

Design document for v1.0.0, for review. No implementation.

Proposes turning failproofai from a stateless cold-start-per-event hook
into a long-lived daemon that also absorbs agenteye-collector, so one
process per machine both enforces policy and collects agent sessions.

The shape: a Rust supervisor owning the socket, verdict, spool, shipper
and tailers, driving unprivileged Node workers that host the existing
TypeScript policy engine unchanged. The runtime decision (the collector
is Rust) and the privilege-separation decision resolve to the same
topology — the unprivileged worker simply happens to be a JS runtime.

Three things it unlocks that are impossible today: policies that
remember (require-tests-before-stop), one collector covering all 12
CLIs instead of 3, and a timeline joining hook verdicts to transcript
content that neither product can compute alone.

Includes eight findings in the current codebase that this architecture
would amplify — chief among them that the builtins shell out to git/gh
with a cwd taken verbatim from the client payload, which is harmless in
a user process and root RCE in a root one — plus fifteen ranked
silent-enforcement-loss risks, each with a mechanical guard rather than
a review note, and a 16-stage delivery sequence that front-loads the
value which needs no Rust.

Three recommended deviations from earlier decisions are flagged for the
review rather than folded in silently, and eight open questions are
listed, including one load-bearing dependency on the AgentEye ingest
endpoint being an upsert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ex49L3LF4YmcWicwwZWZXg
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f41cf7d6-d65a-48b8-bdc1-f1e49507a6f6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Two review changes to the failproofaid design doc.

1. The dashboard's re-architecture moves from out of scope to in scope. It
   is not just repackaged: today two page files import the twelve parser
   modules directly and re-parse transcripts per request, two Server Actions
   call writeHooksConfig with no gate, there is no middleware.ts in the repo
   at all, and app/api/auth is an identity cache for the audit feature rather
   than an access gate. That shape is redundant in single-user mode and a
   confused deputy under a root, multi-user daemon. Reads and writes both
   move to the daemon, the dashboard acquires a uid-bound identity, and it
   goes live instead of polled. Lands as stages 4b and 13b.

2. Adds "Staying current" — the update channel and vendor drift detection.
   There is no update path in the product today: no version check, no update
   notifier, no update command. Project scope drifts forward through npx;
   user scope bakes an absolute binary path into the vendor settings file at
   install and freezes. So a vendor schema change surfaces as "it quietly
   stopped catching things." The daemon auto-updates on the stable channel
   with a health-gated swap and automatic rollback, gains a failproofai
   update command, and detects drift three ways — unrecognized transcript
   shapes, vendor config that no longer matches what we wrote, and silence
   (a session file growing while zero hook events arrive). Lands as stages
   7b and 11b.

Also: F9 (nothing knows it is out of date), R16-R18, tests 9-11, and open
question 8 rewritten — it previously proposed no self-update by default,
which this reverses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ex49L3LF4YmcWicwwZWZXg
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant