Skip to content

[desgin-docs] v1.0.0 design set, Phase 1 implementation plan, and Stage 0 + Stage 1 of failproofaid - #625

Open
NiveditJain wants to merge 58 commits into
mainfrom
luv-629
Open

[desgin-docs] v1.0.0 design set, Phase 1 implementation plan, and Stage 0 + Stage 1 of failproofaid#625
NiveditJain wants to merge 58 commits into
mainfrom
luv-629

Conversation

@NiveditJain

@NiveditJain NiveditJain commented Jul 30, 2026

Copy link
Copy Markdown
Member

The v1.0.0 design set for failproofaid, the implementation plan, and Stage 0 + Stage 1 implemented.

Originally two PRs; #630 was stacked on this branch and has been merged into it, so this is now the single PR against main.

The enforcement plane has a working privileged evaluator behind a Unix socket. It is dead code unless FAILPROOFAI_DAEMON_MODE is set — no configuration default, no wizard default. A revert is one git revert; a field rollback is one environment variable.


The thing that would have been silently broken

Execution tiers are derived from a policy's resolved import graph. All 39 builtins shared one module importing child_process, so derivation would have routed every one of them to user-context and left the sealed tier empty — an architecture that looks implemented and delivers no verdict integrity at all. The 39 now split 32 / 7 by capability, and a test walks the real transitive graph rather than trusting the file layout.

That test caught itself. Its import-specifier regex used a newline-excluding character class, which matches none of this codebase's multi-line imports — so the walk stopped at the entry file and the suite went green while payload-only.ts transitively imported node:fs. Found by injecting node:fs and watching nothing fail.

Stage 0

P1 39 builtins → builtin/payload-only.ts (32) + builtin/host-access.ts (7). Registry order, categories and defaults snapshot-identical.
P2 home / projectDir travel as request data. The daemon evaluates for another UID, so its own homedir() is the service account's — and isAgentInternalPath / block-read-outside-cwd both widen the allow set, the direction that does not announce itself.
P3 evaluatePoliciesevaluateVerdicts + encodeResponse. The twelve-vendor response matrix moved character-identically.
P4 Canonical request envelope with per-field provenance. resolveCodexMode's unbounded scan of ~/.codex/sessions bounded to a 256 KiB head window — measured: first turn_context at line 8 in every real transcript sampled, 3.07 ms → 0.54 ms on a 2.7 MB file.

Plus 5,568 parity fixtures, a coverage tripwire over all 348 (cli, event) cells, generated canonicalization tables with drift gates, a Cargo workspace and rust-quality job, and a cold-start latency harness.

Stage 1

The sealed runtime bundles the 32 payload-only builtins, the registry and the full response encoder into one file evaluated inside QuickJS-ng with no bindings registered. node:path is replaced by a port proven equivalent to node:path.posix differentially over 8,000+ generated cases; host modules by stubs that throw a named capability; hook-logger / hook-telemetry / telemetry-id by inert no-ops — which removed a fetch() to PostHog from a tier the design requires to perform no unbounded I/O.

Three Rust crates: fpai-ipc (framing with a 1 MiB cap validated before allocation, envelope, peer credentials, combination lattice), fpai-canon (generated tables), failproofaid (one warm sealed worker, one enforcement lane, a watchdog thread).

home is derived at the socket from getpwuid_r(peer_uid) and a client-supplied one is rejected, not overwritten. Overwriting makes the attack a no-op but leaves the protocol looking like it accepts the field. cwd / project_dir / env_facts cannot be derived, so they ride as client-asserted and any decision reading one is reported sealed_unattested — the honest version of "unforgeable".

Verification

  1. The sealed bundle runs in a node:vm context with no host globals at all.
  2. The warm-worker soak — a 5,220-row corpus twice through one warm worker, then shuffled, then against a fresh context per row. The last is the one that matters: self-consistency alone is satisfied by a worker that is consistently wrong.
  3. All 5,220 rows byte-identical to the legacy evaluator.
  4. 70,656 generated property cases proving that adding any number of user_context results never lowers a sealed deny — the formal statement of the two-tier argument.
  5. 16 daemon E2E tests over a real socket, including the negatives: forged home refused, unknown env fact refused, oversize frame refused without allocating.
  6. The real TypeScript client against the real Rust daemon — the only test that catches two independent implementations of one prose spec disagreeing.

Defects found and fixed during review

An adversarial review found four, two severe and reproduced end-to-end:

  • The enforcement deadline could not fire. The interrupt flag was only set from inside the microtask pump, which never gets a turn while a synchronous policy body runs. block-curl-pipe-sh is default-enabled and its regex backtracks quadratically: 80 KB of "curl " took 30 seconds against a 200 ms deadline and still returned success, wedging the single enforcement lane permanently. Now a watchdog thread. The prior test armed the interrupt before calling eval — it proved the handler worked and said nothing about whether anything armed it.
  • The daemon enforced a policy set the user never configured. It hardcoded the 11 default builtins and ignored the client's resolved set. Against this repo's own config (30 enabled), rm -rf / returned allow from the daemon and deny from legacy. The documented needs_user_context fallback was structurally unreachable.
  • Connection threads parked forever on a bare recv(), leaking one per hook event until the listener died.
  • PROTOCOL.md documented a canonicalization check that never existed and is not implementable from the current envelope.

Live bugs in the existing repo, found by survey

  • Removing "prepare" alone would have published an empty package.
  • 17 design-doc files were shipping to npm on every publish (prune-standalone.mjs pruned "design-docs"; the directory is desgin-docs).
  • The bun cache has never invalidated since the initial import — every key hashed a bun.lockb that does not exist.
  • The version-consistency check has passed vacuously since the initial import.
  • A stacked PR ran no CI at all — which is why this work went unvalidated until it was consolidated here.
  • handler.ts armed a 10-second timeout per custom hook and never cleared it.

Test status

Unit 2,934 passing
E2E 330 passing
Rust 123 passing
tsc / eslint / cargo fmt / clippy -D warnings clean

The 15 remaining unit failures are pre-existing window.localStorage failures in project-list.test.tsx (jsdom/env), unchanged from before this branch.

What is NOT in this PR

Stages 2–5 of Phase 1. Stage 2 is twelve-CLI shadow-mode parity against real vendor CLIs; Stage 3 the privileged install and _failproofai account; Stage 4 the native client, per-user agent and signed catalog; Stage 5 collector convergence, blocked on an external dependencyagenteye-collector is a separate repository whose conformance corpus must be vendored first.

The root-owned machine.json is designed and documented but not implemented: Stage 1's daemon evaluates the client's resolved enabled set, which is client-asserted. 01-user-experience.md's "genuinely tamper-proof" claim has been corrected to say what is actually true today.

No latency baseline artifact is committed. The harness is complete and gated; four capture attempts were destroyed by environmental collisions (concurrent builds, then two instances of the script sharing a working directory), and the gates now guarantee it writes a valid artifact or nothing.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds the FailproofAI v1.0.0 design-document set covering scoped setup, harness integration, daemon enforcement, cloud policy, schema catalogs, delivery, and npm packaging. It also indexes the documents and updates the changelog.

Changes

FailproofAI design documentation

Layer / File(s) Summary
Setup and enforcement contracts
desgin-docs/v1.0.0/failproofaid/01-user-experience.md
Defines scoped setup, transactional enrollment, execution tiers, CLI behavior, and uninstall semantics.
Harness integration and daemon enforcement
desgin-docs/v1.0.0/failproofaid/02-harness-integration.md, 03-daemon-architecture.md
Defines adapter contracts, IPC, hook reconciliation, peer admission, policy verification, execution tiers, and health state.
Cloud policy management
desgin-docs/v1.0.0/failproofaid/04-cloud-policy-management.md
Defines signed policy releases, assignments, reconciliation, rollout, local-policy coexistence, and offline behavior.
Scoped service and harness schema lifecycle
desgin-docs/v1.0.0/failproofaid/06-service-and-updates.md, 07-delivery-plan.md
Replaces automatic binary updates with scoped services, signed harness schema catalogs, version reconciliation, and catalog lifecycle decisions.
Release packaging and documentation index
desgin-docs/v1.0.0/failproofaid/08-release-and-packaging.md, desgin-docs/v1.0.0/failproofaid/README.md, desgin-docs/v1.0.0/README.md, CHANGELOG.md
Defines npm bootstrap and native release verification, installation layout, signing and promotion, and documentation indexing.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Suggested labels: enhancement

Suggested reviewers: hermes-exosphere

Poem

A rabbit reads each guarded tome,
Through scoped installs and catalogs roam.
Cloud policies softly align,
Signed schemas keep hooks in line.
Thump, thump—docs bloom like springtime foam!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed but does not follow the required template sections, missing Type of Change and Checklist items. Add the required Description, Type of Change, and Checklist sections, and complete the four checkbox items.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly reflects the v1.0.0 design-doc set and Stage 0/1 failproofaid implementation work.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Line 6: Update the failproofai v1.0.0 design-documents entry in CHANGELOG.md
to append the actual pull request number, keeping the entry as a single-line
description.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 47488ff9-9f40-43e2-9960-c3d371715dbb

📥 Commits

Reviewing files that changed from the base of the PR and between debb1fd and fdbbc14.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • desgin-docs/v1.0.0/README.md

Comment thread CHANGELOG.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@desgin-docs/v1.0.0/failproofaid/01-user-experience.md`:
- Around line 58-71: The setup flow described in “failproofai setup” must also
roll back cloud side effects when a later step fails. Either defer organization
enrollment and machine-identity creation until local integration, installation,
and end-to-end verification succeed, or add an idempotent compensating rollback
that revokes issued credentials and removes or marks the cloud machine identity
incomplete.

In `@desgin-docs/v1.0.0/failproofaid/03-daemon-architecture.md`:
- Around line 45-51: Update the candidate validation sequence in the local
reload/cloud reconciliation section so publisher signature and
organization-machine binding checks run only for cloud-provenance inputs.
Preserve schema, digest, compatibility, and local-policy target validation for
local inputs, while retaining all applicable checks for cloud releases.

In `@desgin-docs/v1.0.0/failproofaid/05-collector-integration.md`:
- Around line 27-43: Update the Durable spool and Delivery behavior sections to
define crash-durable acknowledgement before deleting pending records: persist
the record, require durable backend acknowledgement, durably write an
acknowledgement or tombstone state, and fsync that state before removing the
spool record. Specify that crashes during or after backend acknowledgement
replay the record using its stable ID, preserving idempotent delivery and
preventing loss across ambiguous transitions.

In `@desgin-docs/v1.0.0/failproofaid/06-service-and-updates.md`:
- Around line 27-28: Define the native-install activation and startup-recovery
algorithm around the versioned release directories and current pointer: specify
durable write, fsync, directory persistence, and switch ordering; record
recovery metadata for interrupted transactions; and ensure startup validates
releases and selects the last complete release when a crash occurs between
daemon shutdown and pointer switching. Add tests covering power loss at each
activation step and verifying current never references an incomplete release.

In `@desgin-docs/v1.0.0/failproofaid/08-release-and-packaging.md`:
- Around line 57-69: Close the bootstrap trust gap by authenticating and pinning
the npm bootstrapper before execution at
desgin-docs/v1.0.0/failproofaid/08-release-and-packaging.md:57-69, and replace
the curl | sh flow with a pinned or signed installer, or a verified
download-then-execute process, at
desgin-docs/v1.0.0/failproofaid/08-release-and-packaging.md:77-83; both paths
must invoke only trusted bootstrap artifacts before native manifest
verification.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 87c1d3fe-5c3f-4804-b16e-898645b97982

📥 Commits

Reviewing files that changed from the base of the PR and between a16399f and bed7c98.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • desgin-docs/v1.0.0/README.md
  • desgin-docs/v1.0.0/failproofaid/01-user-experience.md
  • desgin-docs/v1.0.0/failproofaid/02-harness-integration.md
  • desgin-docs/v1.0.0/failproofaid/03-daemon-architecture.md
  • desgin-docs/v1.0.0/failproofaid/04-cloud-policy-management.md
  • desgin-docs/v1.0.0/failproofaid/05-collector-integration.md
  • desgin-docs/v1.0.0/failproofaid/06-service-and-updates.md
  • desgin-docs/v1.0.0/failproofaid/07-delivery-plan.md
  • desgin-docs/v1.0.0/failproofaid/08-release-and-packaging.md
  • desgin-docs/v1.0.0/failproofaid/README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md

Comment thread desgin-docs/v1.0.0/failproofaid/01-user-experience.md Outdated
Comment thread desgin-docs/v1.0.0/failproofaid/03-daemon-architecture.md Outdated
Comment thread desgin-docs/v1.0.0/failproofaid/06-service-and-updates.md Outdated
Comment thread desgin-docs/v1.0.0/failproofaid/08-release-and-packaging.md Outdated
@coderabbitai coderabbitai Bot added the enhancement New feature or request label Jul 30, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
desgin-docs/v1.0.0/failproofaid/08-release-and-packaging.md (1)

54-73: 🔒 Security & Privacy | 🟠 Major

Make the npm bootstrap trust mechanism concrete.

The documented command is plain npx failproofai@1.0.0 setup, but the contract requires publisher/provenance verification before the package entry point runs. No enforcing npm wrapper or invocation is specified, and the fallback cannot be selected by code that has already begun executing. Replace this with a concrete verified-wrapper command, or remove plain npx as a supported bootstrap path.

#!/bin/bash
rg -n "npx failproofai@1\.0\.0|publisher|provenance|registry signature|wrapper" .
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@desgin-docs/v1.0.0/failproofaid/08-release-and-packaging.md` around lines 54
- 73, Update the exact-version npm bootstrap section around “npx
failproofai@1.0.0 setup” to specify a concrete npm client or verified wrapper
that enforces registry signature, package integrity, and expected FailproofAI
publisher/provenance checks before execution. If no such wrapper can be named
and invoked, remove plain npx from the supported bootstrap path and direct users
to the verified Homebrew or direct-download flow instead; do not rely on
fallback logic inside the package entry point.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@desgin-docs/v1.0.0/failproofaid/01-user-experience.md`:
- Around line 62-71: Update the transactional setup rules around step 9 and the
failure-compensation paragraph to cover identities activated before local
convergence completes. Define stable activation idempotency keys for retries and
an idempotent deactivation or revocation path for activated cloud identities,
including ambiguous activation responses, so failures cannot leave an active
machine without a healthy local service.

In `@desgin-docs/v1.0.0/failproofaid/05-collector-integration.md`:
- Around line 33-41: Update the crash-durable delivery state machine so step 3
durably commits the acknowledged tombstone/state transition and fsyncs its
containing directory before step 4 deletes pending payload bytes. Preserve the
existing recovery guarantee that an acknowledged record is never reclassified as
unsent after a crash.

In `@desgin-docs/v1.0.0/failproofaid/08-release-and-packaging.md`:
- Around line 82-84: Update the installer verification instructions around the
minisign command to replace the placeholder with the exact published FailproofAI
installer public key or a deterministic local key reference, and independently
document the pinned fingerprint for that key. Ensure the resulting command is
executable as written and uses the pinned verification material.

---

Outside diff comments:
In `@desgin-docs/v1.0.0/failproofaid/08-release-and-packaging.md`:
- Around line 54-73: Update the exact-version npm bootstrap section around “npx
failproofai@1.0.0 setup” to specify a concrete npm client or verified wrapper
that enforces registry signature, package integrity, and expected FailproofAI
publisher/provenance checks before execution. If no such wrapper can be named
and invoked, remove plain npx from the supported bootstrap path and direct users
to the verified Homebrew or direct-download flow instead; do not rely on
fallback logic inside the package entry point.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 00b147cf-7189-4d6c-9665-f860f81cd6c6

📥 Commits

Reviewing files that changed from the base of the PR and between bed7c98 and 66d938e.

📒 Files selected for processing (5)
  • desgin-docs/v1.0.0/failproofaid/01-user-experience.md
  • desgin-docs/v1.0.0/failproofaid/03-daemon-architecture.md
  • desgin-docs/v1.0.0/failproofaid/05-collector-integration.md
  • desgin-docs/v1.0.0/failproofaid/06-service-and-updates.md
  • desgin-docs/v1.0.0/failproofaid/08-release-and-packaging.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • desgin-docs/v1.0.0/failproofaid/06-service-and-updates.md

Comment thread desgin-docs/v1.0.0/failproofaid/01-user-experience.md Outdated
Comment thread desgin-docs/v1.0.0/failproofaid/08-release-and-packaging.md Outdated
NiveditJain and others added 6 commits July 30, 2026 14:07
Replace the root daemon with a dedicated `_failproofai` service account as
the recommended `failproofaid` enforcement boundary, and resolve the three
problems that follow from it.

Setup now offers `managed` (daemon runs as the service account, one sudo at
install, no root at runtime), `system` (the same service as root, retained
for fleet-managed `/etc` configuration and root-owned agents), and `user`
(unchanged, cooperative).

Three corrections come with it:

- Protected artifacts move out of `~/.failproofai` entirely. Rename and
  delete permission derive from the *parent* directory, so a user who owns
  `~` can rename an unwritable subdirectory aside and substitute their own
  regardless of its mode. The same reasoning makes the socket directory
  service-account-owned, closing the substitution attack where an agent
  unlinks the socket and binds an impostor that answers `allow`.

- Only removal is privileged, not addition. Results combine `deny` over
  `instruct` over `allow`, so a user-added policy can only tighten
  enforcement and can never shadow a protected one. Convention discovery
  keeps working without elevation.

- A non-root daemon cannot `setuid` to an enrolled user, so evaluation
  splits into a `sealed` tier (service account, pinned runtime, no
  filesystem/subprocess/network, verdict unforgeable) and a `user-context`
  tier (requesting UID, verdict forgeable but additive-only, so a forged
  `allow` is worthless). The tier is *derived* from declared capabilities
  rather than chosen, because it describes what is physically achievable.
  Payload-only builtins like `block-sudo` land in `sealed`.

The release ships its own pinned runtime so the daemon never executes an
nvm/fnm interpreter the user can rewrite, and constructs worker
environments instead of inheriting `NODE_OPTIONS`/`NODE_PATH` from the
agent's process. Promotion into the protected store compiles a policy and
its full import graph into one content-addressed artifact, leaving
authoring and dependency management unprivileged and unchanged.
Enforcement performs no unbounded I/O: policies needing remote state read a
cache the collection lane refreshes, rather than making GitHub's uptime a
precondition for running a command.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

🧹 Nitpick comments (1)
desgin-docs/v1.0.0/failproofaid/07-delivery-plan.md (1)

76-76: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Treat user-context spawn selection as a security-critical invariant.

The docs define reduced groups, explicit environment, resource limits, and platform sandboxing for user-context, but these can vary by launch path. Replacing “not a security one” with an explicit isolation invariant that applies to per-user service, helper, and hook-client launches prevents the chosen mechanism from inheriting different descriptors, environment, groups, IPC access, or sandbox profile.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@desgin-docs/v1.0.0/failproofaid/07-delivery-plan.md` at line 76, Update item
15’s conclusion about user-context spawn selection: replace the claim that it is
not a security concern with an explicit invariant requiring identical isolation
for per-user service, privileged helper, and hook-client launches. State that
every path must enforce the defined groups, environment, resource limits,
descriptor/IPC restrictions, and platform sandbox profile, while leaving
supervision and cold-start latency as operational choices.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@desgin-docs/v1.0.0/failproofaid/01-user-experience.md`:
- Around line 153-157: Revise the managed-scope security contract to keep
protected policy artifacts unwritable by the `_failproofai` runtime account.
Specify a separate privileged installer or root-owned read-only store, or
another enforced equivalent, and limit service-account ownership to
runtime-writable state such as spool and caches while preserving the existing
managed/system scope distinctions.
- Around line 316-324: Update the uninstall contract to explicitly exclude local
machine credentials from preserved state: when offline, securely remove or
zeroize the credential and retain only a non-secret revocation tombstone, while
connected uninstall continues revocation. Clarify the preservation rule in the
uninstall flow and keep other local policy, logs, pending events, and
configuration preservation unchanged.
- Around line 175-182: Update the tier-selection and sealed-tier description to
avoid trusting policy-declared capabilities as the security boundary. State that
the runtime independently enforces capabilities deny-by-default, including
validation of imports and transitive dependencies, or explicitly ensure sealed
execution cannot perform filesystem, subprocess, or network operations
regardless of declarations; retain user-context execution for policies requiring
those operations.
- Line 52: Update the managed scope description near the service-scope overview
to remove the claim that its configuration, policy store, and state are outside
root’s authority. State that the isolation is from the enrolled agent user,
while acknowledging that root can inspect, modify, or impersonate the service
account; preserve the existing installation and post-installation behavior.

In `@desgin-docs/v1.0.0/failproofaid/03-daemon-architecture.md`:
- Around line 88-94: Clarify the “Pinned policy runtime” section so the sealed
tier is available only for managed/system installations with a root-owned
runtime. Explicitly state that user-scope installations must disable or
downgrade sealed execution unless they define an equivalent enforceable
ownership and integrity mechanism; do not leave standalone user mode able to
claim sealed verdict guarantees.
- Around line 152-156: The scope description around the canonical
~/.failproofai/ root contradicts the managed/system trust boundary. Revise it so
user-authored source policies are authoritative only in user scope; for
managed/system scope, either exclude them from enforcement inputs or explicitly
define them as additive, non-authoritative inputs while keeping protected state
under trusted paths.
- Around line 96-100: Update the “Dependencies and admission” section to
prohibit native addons in the sealed tier unless they execute under explicitly
specified process-level isolation with an OS capability allowlist. Preserve the
existing pinned-digest requirement, and state that admission must either refuse
native addons or enforce this sandbox before allowing them into sealed.

In `@desgin-docs/v1.0.0/failproofaid/04-cloud-policy-management.md`:
- Line 5: Update the user-facing terminology in the FailproofAI OSS description
from “builtin” to “built-in,” preserving the surrounding meaning and wording.

In `@desgin-docs/v1.0.0/failproofaid/06-service-and-updates.md`:
- Around line 17-23: Update the final paragraph around the “Only removal is
privileged” sentence to state that only adding local user policies is
unprivileged, while removing, disabling, or altering admitted policies requires
privileged authorization. Explicitly distinguish mutable local user/project
policies from protected or cloud-managed revisions, preserving the existing
policy precedence and enforcement behavior.
- Around line 13-21: Clarify the protected managed-state design around the
protected policy store, per-user protected state, and socket metadata: define a
separate privileged writer boundary instead of relying on `_failproofai`
ownership, specify which paths are read-only or immutable to the daemon, and
identify the installer/helper authorized to create or update them. Preserve the
existing requirement that policy revisions and active protected state cannot be
rewritten by the daemon or another process running as its UID.
- Around line 72-75: Expand the catalog activation flow around candidate
persistence, active catalog pointer updates, and hook registration
reconciliation to define crash-consistent transaction metadata and recovery
selection on startup. Specify required file and directory fsync ordering,
including recovery after crashes between catalog activation and registration, so
startup restores a complete matching catalog/registration pair. Add
fault-injection tests covering each activation boundary and verifying recovery
never leaves mismatched state.
- Around line 43-53: Update the catalog specification to define a closed
execution grammar for adapter executable/version detection, including fixed
executable and explicit argument allowlists, bounded command behavior, and
required execution controls. Explicitly prohibit shell expansion, inherited
environment or file descriptors, network access, and unrestricted commands or
arguments; ensure remotely refreshed catalogs can only activate checks within
these constraints.

In `@desgin-docs/v1.0.0/failproofaid/08-release-and-packaging.md`:
- Around line 131-148: Extend the “Build and test” release gate to require an
end-to-end clean Docker-container installation test, covering native artifact
verification, setup, service readiness, and uninstall. Keep the existing clean
npm-environment tarball check, and specify that the container test must mimic a
real installation before publication.
- Around line 71-75: Reconcile the scope contradiction by explicitly limiting
the sealed-tier/protected-runtime guarantee to managed and system scopes. Update
the pinned-runtime section in
desgin-docs/v1.0.0/failproofaid/08-release-and-packaging.md (lines 71-75),
clarify the user-scope layout and weaker protection in its user-scope section
(lines 91-105), and update desgin-docs/v1.0.0/failproofaid/README.md (line 28)
to match.

In `@desgin-docs/v1.0.0/failproofaid/README.md`:
- Line 26: Update the statement in the documentation to explicitly scope
privilege to policy removal, using wording such as “Only policy removal is
privileged,” while preserving the existing explanation of policy precedence and
unprivileged authoring.
- Line 32: Qualify the README’s offline-usability statement to clarify that
offline operation is supported after installation, not necessarily during
installation. Update the installation and packaging guidance in
desgin-docs/v1.0.0/failproofaid/08-release-and-packaging.md lines 5-11 to
document the npm bootstrap network requirement and deferred air-gapped support;
do not add an offline distribution path.

---

Nitpick comments:
In `@desgin-docs/v1.0.0/failproofaid/07-delivery-plan.md`:
- Line 76: Update item 15’s conclusion about user-context spawn selection:
replace the claim that it is not a security concern with an explicit invariant
requiring identical isolation for per-user service, privileged helper, and
hook-client launches. State that every path must enforce the defined groups,
environment, resource limits, descriptor/IPC restrictions, and platform sandbox
profile, while leaving supervision and cold-start latency as operational
choices.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 865b8392-f46b-4c4d-9842-4d1fd0e87dbc

📥 Commits

Reviewing files that changed from the base of the PR and between 66d938e and ec28e46.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • desgin-docs/v1.0.0/failproofaid/01-user-experience.md
  • desgin-docs/v1.0.0/failproofaid/02-harness-integration.md
  • desgin-docs/v1.0.0/failproofaid/03-daemon-architecture.md
  • desgin-docs/v1.0.0/failproofaid/04-cloud-policy-management.md
  • desgin-docs/v1.0.0/failproofaid/05-collector-integration.md
  • desgin-docs/v1.0.0/failproofaid/06-service-and-updates.md
  • desgin-docs/v1.0.0/failproofaid/07-delivery-plan.md
  • desgin-docs/v1.0.0/failproofaid/08-release-and-packaging.md
  • desgin-docs/v1.0.0/failproofaid/README.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • desgin-docs/v1.0.0/failproofaid/02-harness-integration.md
  • CHANGELOG.md
  • desgin-docs/v1.0.0/failproofaid/05-collector-integration.md

Comment thread desgin-docs/v1.0.0/failproofaid/01-user-experience.md Outdated
Comment thread desgin-docs/v1.0.0/failproofaid/01-user-experience.md Outdated
Comment thread desgin-docs/v1.0.0/failproofaid/01-user-experience.md Outdated
Comment thread desgin-docs/v1.0.0/failproofaid/01-user-experience.md Outdated
Comment thread desgin-docs/v1.0.0/phase-1-local-enforcement/03-daemon-architecture.md Outdated
Comment thread desgin-docs/v1.0.0/phase-1-local-enforcement/07-release-and-packaging.md Outdated
Comment thread desgin-docs/v1.0.0/failproofaid/README.md Outdated
Comment thread desgin-docs/v1.0.0/failproofaid/README.md Outdated
NiveditJain and others added 5 commits July 30, 2026 16:14
Review found four places where the guarantee was narrower than the
sentence claiming it.

The protected surface — executables, pinned runtime, policy store, schema
catalog, machine configuration — is now root-owned and read-only to the
service account the daemon runs as, which owns only mutable runtime state.
The first draft made `_failproofai` the owner of the store it evaluates, so
a compromised daemon could rewrite the sealed policies it was supposed to
enforce. That is the same self-rewrite argument already applied to the
binaries and not, then, to the store.

Tier selection no longer trusts a policy's own capability declaration. The
author controls that declaration, so under-declaring would have been a
route into `sealed`. Admission now derives the tier from the resolved
import graph, and the `sealed` context is deny-by-default with no
filesystem, process, or network bindings — an under-declared policy fails
inside the tier rather than escaping it.

Native addons are refused from `sealed` outright. A pinned digest prevents
substitution but constrains nothing about what native code does once
loaded.

`sealed` is stated as managed/system-only. The protected runtime is a
property of the privileged install layout, not of the shipped artifact, so
user scope makes no verdict-integrity claim.

Also from review: `~/.failproofai` sources in a privileged scope are
defined as additive non-authoritative inputs that only route to
`user-context` and only tighten, resolving a contradiction with the
out-of-home rule; "only removal is privileged" is scoped to policy
administration so it stops reading as a claim about installation;
uninstall unconditionally erases machine credentials rather than
preserving them as configuration; catalog version detection gains a closed
execution grammar; catalog activation gains fsync ordering, transaction
metadata, and startup re-reconciliation; the release gate gains a
clean-container install run; and the offline claim is qualified to
operation-after-installation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjFAYdzWkGUPE2sMWWBC6E
The dashboard was promised four times in the design set and specified
nowhere — no command, no owner, no port, no access control. That mattered
once the daemon stopped running as the user: a browser cannot speak the
Unix socket, and a TCP listener carries no peer credentials, so whatever
binds the port becomes the identity for everything behind it. Today it is
a fixed localhost:8020 started by `failproofai audit`, with server actions
that write policy config; on a multi-user managed box that is both a port
collision and a cross-user read of other people's session transcripts.

The resolution keeps the daemon out of it. `failproofai dashboard
start|stop|status` spawns the bundled server from the CLI, which already
runs as the invoking user, so the daemon is only a data source over the
existing socket and peer credentials scope every read with no second
identity mechanism. The daemon has no dashboard concept; a pidfile in the
user's runtime directory handles reattach and stop.

Routing the spawn through the daemon was considered and rejected: it would
inherit the service account at the TCP door, and a managed daemon cannot
`setuid` to the requesting user anyway — the same wall as policy workers.

Reads go through a new `Query` operation class, filtered by peer credential
rather than by any field the caller supplies. Writes split by tier so the
common case is untouched: `mutable` policies toggle directly, since they
are additive-only and cannot weaken a protected one, while protected
revisions are display-only and the UI composes a copyable `sudo failproofai
policies disable --revision <digest>`. The dashboard never holds, requests,
or brokers elevation, and protected changes stay on the CLI path that
already produces an audit record.

The listener is loopback-only on an ephemeral port, gated by a capability
token carried in a header rather than the query string where `Referer`
would leak it, with `Origin`/`Host` validation and no state-changing `GET`,
because any page the user visits can reach a localhost port. It is
on-demand rather than supervised, expiring on `stop`, terminal exit, or a
30-minute default TTL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjFAYdzWkGUPE2sMWWBC6E
Cut the three-way managed/system/user choice down to one shipped scope,
managed. Setup has no privilege decision to make, so the step that offered
the choice becomes a disclosure of the boundary — affected users, created
paths, per-harness hook protection, service manager, and the single sudo —
followed by a confirmation.

The point of this is what it removes. Every guarantee in the doc set was
qualified three ways ("in managed and system scope...", "user scope makes
no verdict-integrity claim"); those qualifications are gone and the claims
are unconditional. The sealed/user-context split survives untouched,
because it keys off a policy's resolved import graph rather than the
installed scope — its table loses only the "available in" column.

Both dropped scopes are recorded in a Deferred scopes section with their
layouts and the exact guarantee each gains or loses, so adding one later
is a service-registration change rather than a redesign, and neither is
reachable as an automatic fallback.

The cost is stated instead of hidden. One privileged scope means install
now requires administrator access and a running systemd/launchd, so a
machine with neither is refused in preflight — no silent degrade to an
unsupervised process. The release gate's clean-container run therefore
needs a real init as PID 1, with the refusal asserted as its own case.

Second change: split the design set into phase-1-local-enforcement/
(daemon, harness integration, execution tiers, service and schema
catalog, local dashboard, npm distribution — no account, nothing in the
decision path needing a network) and phase-2-cloud/ (login, machine
identity, the transactional enrollment sequence, the machine credential,
centrally assigned policy, fleet health, collector convergence).

The collector moves with Phase 2 rather than Phase 1 because taking over
delivery means taking over the credential that authorizes it; Phase 1
leaves the standalone collector running untouched and indexes sessions
only for local activity, audits, and the dashboard. Owning capture but
not delivery would strand data where nobody is watching.

What stays in Phase 1 is contract shape, not dormant features — canonical
location-independent request/result, end-to-end deadlines, stable decision
identity, bounded lanes, a versioned health snapshot — so Phase 2 extends
rather than reshapes, with no configuration key or client added ahead of
time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjFAYdzWkGUPE2sMWWBC6E
The phase line was drawn in the wrong place. It put collector convergence
in Phase 2 on the reasoning that taking over delivery means taking over the
credential that authorizes it — which conflated two unrelated credentials.

The collector authenticates to the customer's own self-hosted Failproof AI
Observability server with an operator-issued API key holding events:add.
That is not a FailproofAI cloud login and not a machine identity. Delivery
therefore pulls no account into Phase 1, and `failproofai auth login`
already ships today regardless.

So the split now runs on one test: can a customer already do this today? If
yes it is Phase 1, whatever it touches. Everything agenteye-collector ships
— capture, backfill, durable spooling, delivery, quarantine, health — is
current behavior the compatibility promise covers, and owning capture
without delivery would have been the worst of both: two processes reading
the same transcripts, and data stranded in whichever spool nobody watches.

Phase 1 regains the collection and delivery lanes, the durable spool and
its crash-durable acknowledgement state machine, the collector status and
flush commands, the Flush operation, spool health and quotas, and the
legacy-collector migration with its ownership lock and rollback window.
Setup regains the observability step, which now names the destination
server and its key alongside the sources, and defaults to capturing
nothing.

Phase 1's claim of no account, not a dormant one, was false on two counts
and is narrowed to what is true: no FailproofAI organization is required,
and no policy decision depends on a network service. The delivery key is
named as the one secret Phase 1 handles — machine-scoped, in privileged
configuration the service account reads and enrolled users cannot, never in
a unit file or process argument, erased unconditionally on uninstall.

Phase 2 keeps only what does not exist yet: machine enrollment, centrally
assigned policy, targeting, fleet health, staged rollout. Its machine
credential is documented as the third and separate one, so enrolling does
not replace the collector's key or change where captured data goes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjFAYdzWkGUPE2sMWWBC6E
Grounded in a full survey of the current code, not the design docs alone.
The sequencing is built around one idea: the existing TypeScript
implementation is not replaced, it is promoted to oracle. It stays in the
tree, keeps its tests, answers when the daemon cannot, and is what every
new implementation is diffed against byte-for-byte.

Also lands the per-user agent section in 03-daemon-architecture.md, which
closes the question of how a daemon running as _failproofai reads session
transcripts under 0700 homes: it does not. Capture and user-context
evaluation both run in one per-user agent at the requesting UID, because
both want the same thing and residency is already required by the
enforcement deadline. Granting the service account ACLs into every
enrolled user's home was considered and rejected — a chmod by the harness
silently caps the ACL mask, the SQLite sources need write rather than
read, and it would make a daemon compromise a read of every developer's
transcripts while buying no tamper-resistance.

Three decisions settled:

- The sealed tier runs QuickJS-ng, ~1 MB against V8's +30-45 MB on each of
  four tarballs npx downloads, with deny-by-default structural rather than
  a syscall filter. Gated by a Stage-0 spike against the real regex corpus.
- The 39 builtins are not ported to Rust. The boundary is the process, UID
  and absent bindings, not the language; porting buys no security, does
  nothing for user-authored policies, and the regex crate cannot express
  extractAbsolutePaths' lookbehind.
- Protected policy enablement moves to a root-owned machine.json. It
  currently lives in a user-writable file, so an agent deletes block-sudo
  from a JSON array and the unforgeable verdict never runs — the
  tamper-proof claim was not true as written.

The survey turned up four live bugs Stage 0 fixes: prepare is the only
thing populating dist/ before npm publish and publish.yml has no build
step, so removing it alone ships an empty package; prune-standalone.mjs
prunes "design-docs" while the directory is desgin-docs; every CI cache
key hashes a bun.lockb that does not exist, so the bun cache has never
invalidated; and the packages/wrapper version check has no-opped since the
initial import.

It also found the least visible failure mode in the project: all 39
builtins share a module importing child_process, so import-graph tier
derivation would route every one of them to user-context and leave the
sealed tier empty — an architecture that looks implemented and delivers no
verdict integrity.

Verification is seven layers ending in a full-stack Docker gate: a
contract-faithful events:add stub plus a debian:12 machine running systemd
as PID 1 with node and npm only, installed through the real npx setup.
Twelve assertions cover install, enforcement across 12 CLIs, both tiers,
the no-agent path, protected-vs-mutable, capture through spool to
delivery, crash durability with strace-verified fsync ordering, dashboard
access control, catalog rollback and uninstall. Two legs must never be
skipped: the no-init preflight refusal, and tamper refusing before
anything is written under /opt.

Six design-doc amendments are recorded. macOS codesigning and
notarization is the most urgent — absent from all eight documents, and an
unsigned LaunchDaemon is killed on macOS 15, which blocks two of the four
targets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjFAYdzWkGUPE2sMWWBC6E
NiveditJain and others added 2 commits July 30, 2026 23:37
A stacked PR — one opened against another feature branch — ran no CI at all.
Not a reduced set: none. quality, test, build, docs, test-e2e and rust-quality
were all silently skipped, while the third-party checks that do run on any PR
still painted a green checkmark. That is the worst possible presentation of
'nothing was verified'.

Found the hard way. Phase 1 Stage 0 and Stage 1 — several thousand tests, three
Rust crates, a daemon — were pushed to a PR stacked on the design-doc branch,
and CI had not executed once. Retargeting that PR at main would have dragged 22
unrelated commits into its diff, so the trigger filter is what needed fixing.

The push trigger keeps its main-only filter; this only widens pull_request,
which by definition already has a review gate attached.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@socket-security

socket-security Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcargo/​tokio@​1.53.15810093100100
Addedcargo/​nix@​0.31.3801009310070
Addedcargo/​thiserror@​2.0.198010093100100
Addedcargo/​serde@​1.0.2298110093100100
Addedcargo/​serde_json@​1.0.1518210093100100
Addedcargo/​proptest@​1.11.09910093100100
Addedcargo/​rquickjs@​0.12.210010093100100

View full report

@socket-security

socket-security Bot commented Jul 30, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: cargo tokio is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: Cargo.lockcargo/tokio@1.53.1

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/tokio@1.53.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: cargo zerocopy is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?cargo/proptest@1.11.0cargo/zerocopy@0.8.55

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/zerocopy@0.8.55. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

NiveditJain and others added 5 commits July 30, 2026 23:48
The first CI run this work has ever had — the branch's PR was stacked, so the
workflow's `branches: [main]` filter had skipped every job — went red on two
things, both of which pass locally.

## The protocol conformance test was right and I was wrong

`crates/fpai-ipc/tests/protocol_conformance.rs` pastes PROTOCOL.md's examples
in verbatim, parses them into the Rust types, and asserts both that the
round-trip matches and that the pasted text is still a substring of the
document. Adding `enabled_policies` to the envelope updated the type and the
document but not the test's copy, so all three drifted apart and the test said
so in exactly the terms it was written to say:

    this example is no longer present verbatim in crates/PROTOCOL.md.
    The wire format changed; update this test *and* the Rust types together.

Local `cargo test --workspace` had run before the document edit, so it passed.
That is the whole argument for the test existing: three artifacts that must
agree, and nothing else notices when they stop.

## A test-isolation landmine my new file stepped on

Four convention-policy suites failed in CI with `No "syncConventionPolicies"
export is defined on the "../../src/hooks/hooks-config" mock`. Those suites
carry no mocks at all — they use the real modules. The mock was leaking in from
another file in the same worker.

`vi.mock(spec, () => ({ … }))` *replaces* a module: every binding the factory
does not name becomes undefined for anything that resolves it while the mock is
registered. `handler.test.ts` has stubbed `hooks-config` with
`readMergedHooksConfig` alone since long before this branch, silently deleting
its other nine exports. It stayed invisible because vitest's worker packing
happened to keep it away from the convention suites — until
`daemon-client.test.ts` became a fifth file mocking the same module and shifted
the arrangement.

Both files now spread the real module first. That is strictly safer and makes a
partial mock actually partial. `handler.test.ts` is pre-existing and outside
this branch's subject, but the hazard is one I destabilized, and leaving a known
landmine because it was already armed would be the wrong call.

Worth recording what this cost: I could not reproduce either failure locally,
including with `--no-file-parallelism` to force the five files into one worker.
Local runs are not evidence about worker packing, which depends on core count.
The only proof the fix works is CI going green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
03-risks-and-amendments.md opens by saying its six items should be folded into
the documents one directory up "as Stage 0 lands, rather than discovering them
during implementation". Stage 0 landed several commits ago and the folding had
not been done — an outstanding deliverable, not a nice-to-have, since the
amendments exist precisely because the design set currently states things that
are not true.

Six documents are updated so far, including the macOS codesigning and
notarization section that was absent from all eight and blocks two of the four
release targets. All 720 pages still parse.

Committed mid-flight because the working tree needed to be clean, not because
the work is finished: the amendments file itself still has to be rewritten to
record where each correction now lives, so the reasoning behind each one
survives the fold. That is the next commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nded

Completes the fold the previous commit started. All six amendments are now in
the design set, and 03-risks-and-amendments.md is rewritten from "here is what
needs correcting" to "here is what was wrong, what replaced it, and where".

The file is kept rather than deleted deliberately. Once a correction is folded
in it reads as ordinary design; the part that does not survive being merged
into surrounding prose is *why* it exists — the specific failure each one
prevents. A reader who later wonders why the daemon derives `home` instead of
accepting it, or why enablement has to come from a root-owned file, needs the
counterexample, not the rule.

Two corrections are worth calling out because they change what the documents
claim rather than just adding to them.

01-user-experience.md's "genuinely tamper-proof" was **false as written**.
Enablement lives in the user-writable merge of `.failproofai/policies-config
.json`, so an agent with its user's authority deletes `block-sudo` from a JSON
array and the unforgeable verdict is never reached. Nothing is forged; the
policy simply does not run. The sentence is gone, replaced by the tier's honest
scope — it makes evaluation unforgeable, not selection — and the root-owned
`machine.json` design is now written down in 03-daemon-architecture.md with an
explicit statement that **it does not ship yet**. Stage 1's daemon evaluates the
client's resolved enabled set, which is client-asserted and carries exactly the
trust the file the legacy path already reads carries. Saying so plainly beats
letting the architecture section imply a property the code does not have.

macOS codesigning and notarization appeared in none of the eight documents,
while an unsigned LaunchDaemon is killed outright on macOS 15 — two of the four
release targets, blocked by an omission rather than a decision. 07-release-and-
packaging.md now carries it as its own section with four acceptance criteria,
and the LaunchDaemon row in 04 points at it, because that row does not work
without it.

720 pages still parse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"the pinned runtime" reads as the vendored Node the release ships for the
user-context tier. The sealed tier does not use it — it evaluates inside the
daemon's own QuickJS-ng context with no bindings registered, which is a
different thing and the reason the tier's guarantee holds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@NiveditJain NiveditJain changed the title [desgin-docs] Add v1.0.0 design document home [desgin-docs] v1.0.0 design set, Phase 1 implementation plan, and Stage 0 + Stage 1 of failproofaid Jul 30, 2026
NiveditJain and others added 20 commits July 30, 2026 23:54
…stated

Amendment 4's exception — the per-user agent's unit file necessarily lives in a
user-writable home — was written up in full, but the two places that state the
absolute rule ("nothing enforcement depends on lives under a user-owned root")
still stated it absolutely. A reader hitting either one first would take the
rule at face value and never reach the qualification.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit fixed two of the five files that stub `hooks-config`, so
CI stayed red for the same reason. Three more — new-telemetry, llm-client and
manager — still replaced the whole module, and any one of them landing in a
worker with the convention suites deletes `syncConventionPolicies` for them.

Scoped to the `hooks-config` mock specifically rather than every partial mock
in the suite. A whole-module mock is a hazard generally, but some tests
deliberately want a module fully replaced because its real import has side
effects they are avoiding. `hooks-config` is the one where the hazard is
proven rather than theoretical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
37,524 iterations, zero failures, 12 repeatability rows. The fifth attempt; the
first four were destroyed by a concurrent build, then by measuring the two
variants as sequential passes over the whole matrix, then twice by two
instances of the script sharing a working directory.

The numbers are coherent in the way the broken runs were not. `spawn` differs
by 0.05 ms between the default and custom-policy variants — a phase custom
policies do not touch, and the tell that exposed the earlier drift bug, where
it differed by 18 ms and made custom policies look 17 ms *faster*.
Run-to-run noise is bounded at 3.9%.

What it establishes, which is the whole reason the plan asks for it before any
Rust exists: spawn is 60.97 ms of a 65.4 ms end-to-end p50, roughly 93%. The
daemon removes config+load, evaluate and encode — about 1 ms — and cannot
touch spawn until the native client lands at Stage 4. So the Stage 1-3
acceptance question is "did phases 2-4 go to zero", not "did end-to-end
drop", and anyone measuring the latter early would conclude the daemon does
nothing.

The other number worth having: a single custom policy file costs 3.49 ms of
config+load on every hook event, which is the temp file the loader writes next
to the user's source per tool call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A deliberate simplification for this version, decided by the product owner:
no `_failproofai` service account, no privileged install, no `sudo`. The daemon
runs as the invoking user and keeps its state in the two roots the product
already uses — `~/.failproofai/` and `~/.agenteye/`. Nothing migrates.

## What this removes, which is the point

The managed scope needed a service account, a root-owned `/opt` tree, a
`/var/lib` state tree, a `/run` socket directory with an ownership split, a
journal-backed transactional privileged installer, and a preflight that refused
machines without `sudo` or a system service manager.

It also needed a **per-user agent** — a whole second resident process — for one
reason: a service account cannot traverse `0700` homes, cannot `inotify` them,
and cannot attach to a live WAL SQLite database in a user's directory, so
`user-context` evaluation and session capture had to happen somewhere else and
be dispatched over an outbound connection with daemon-side checkpoints. Running
as the user, the daemon reads all of it directly. The agent, the
`--oneshot-user-context` continuation, and the per-UID checkpoint design go
with it.

And it needed a root-owned `machine.json`, because policy enablement lived in a
user-writable file and could therefore be edited by the agent. In user scope
everything is user-writable by design, so `.failproofai` in user scope is
simply authoritative and there is nothing to protect.

Two processes ship: `failproofaid` and the `failproofai` CLI.

## What it costs, stated once and not softened

**The sealed tier no longer claims verdict integrity.** That argument required
the daemon to run as a UID the governed agent could not administer. Here they
are the same user: it can `ptrace` the daemon, preload into it, replace the
binary, or edit the config. Any sentence claiming a verdict cannot be forged is
false in this version.

What the tier still buys is real and is not the same thing: a warm resident
evaluator rather than a fresh interpreter per event; no `.__failproofai_tmp__
.mjs` written next to the user's source on every tool call, measured at 3.49 ms
per event; a deadline a watchdog actually enforces; and a deny-by-default
sandbox that contains a *buggy or over-reaching* policy. Protection against
mistakes, not against an adversary who is already this user. Collapsing those
two is how a security claim becomes marketing, so `crates/PROTOCOL.md` now says
which one this is.

The managed and system scopes that would restore the integrity claim stay
designed and deliberately deferred.

## Paths

`crates/failproofaid/src/paths.rs` is the one place they are resolved, with six
tests including one asserting nothing lands under `/opt`, `/var/lib`, `/etc`,
`/Library` or `/usr`. The socket prefers `$XDG_RUNTIME_DIR/failproofai/` and
falls back to `~/.failproofai/run/` — not defensive padding: `XDG_RUNTIME_DIR`
is unset over a plain `ssh` session on several distributions and on macOS
generally, which is exactly where an agent CLI runs.

`src/hooks/daemon-client.ts` mirrors it. A client and daemon that disagree
about the socket path do not fail loudly — the client finds nothing, returns
`null`, and every hook silently takes the legacy path forever — so the two are
cross-checked by test rather than by comment.

Peer credentials stay, for a narrower reason: the daemon serves one user, so
`SO_PEERCRED` is now a "this connection is mine" check that keeps *other* users
on a shared machine out. Deriving `home` from `getpwuid_r(peer_uid)` also
stays, because `isAgentInternalPath` and `block-read-outside-cwd` both widen the
allow set and a bug in the client is now the threat model this catches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ope (in progress)

First three documents of the scope rewrite: the Phase 1 README, the setup and
operation flow, and harness integration. Setup loses its privilege-disclosure
screen, its single sudo, and its scope question — there is no scope to choose.

Committed mid-flight because the working tree had to be clean, not because the
rewrite is done: 03 through 07 and the implementation plan still describe the
service account, the per-user agent, and the privileged layout.

720 pages still parse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Persistent tampering degrades health" implied a boundary that user scope does
not have. The daemon and the agent are the same user, so an agent that keeps
removing its own hook registration cannot be prevented from doing so — but the
repair loop, the audit record and the health degradation still make it loud,
which is a real property and the one worth claiming.

Same correction for peer credentials in that section: a routing and isolation
rule between users on a shared machine, not a privilege boundary within one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follows the scope decision through everywhere the managed design had leaked
into code, comments and test fixtures, and fixes two problems in the paths
module I added with it.

## The paths module no longer uses `unsafe`, and no longer needs to

It resolved `HOME` with a `getpwuid_r` fallback behind `unsafe { libc::getuid() }`,
and its tests mutated process environment — which `std::env::set_var` makes
`unsafe` in edition 2024, because it is a data race in a multi-threaded
process. Both tripped `clippy -D warnings` against the workspace's
`unsafe_code = "warn"`, and the test half would have needed either a global
lock or accepted flakiness.

Allow-listing the lint would have been the smaller diff and the wrong answer.
The resolvers now take `home` and `runtime_dir` as arguments and are pure, with
thin `*_from_env` wrappers doing the reads. No `unsafe`, no lock, no ordering
dependence between tests, and the table of cases is exhaustive because nothing
global is involved. The `getpwuid_r` fallback is dropped: a daemon started with
no `HOME` at all is already an environment where `--socket` is the answer.

## Comments that described a boundary that no longer exists

`sealed-entry.ts` said host context comes from the request because "the daemon
runs as the service account, so its own homedir belongs to `_failproofai`".
That reasoning is gone, but the *conclusion* is not — and the real reason is
better: the daemon is resident and answers for sessions it did not start, so
its own ambient homedir is whatever its launch environment said (a systemd user
unit with a minimal `Environment=`, a container entrypoint, a test harness),
and it would be that same wrong value for every session at once. Same
correction in `pure-path.ts`'s cwd fallback, `payload-only.ts`'s
`isAgentInternalPath`, `runtime-stubs.ts`, and the host-context test suite.

The distinction matters because someone reading the old comment after the scope
change would conclude the threading was now pointless and delete it.

## Verified

Rust 129 tests, `clippy -D warnings` and `cargo fmt --check` clean. TypeScript
158/159 files — the one failure is the pre-existing jsdom `localStorage` issue
in `project-list.test.tsx`, unchanged from before this branch. `tsc` clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…r scope (in progress)

Continues the scope rewrite through 03-07, the delivery plan, and the
implementation stages. The per-user agent section is gone — it existed only
because a service account cannot traverse 0700 homes, and running as the user
removes the problem rather than bridging it.

Still in progress: several documents retain references that need a final pass
to separate the deliberate ones — the deferred-scopes section names the service
account on purpose, since recording what managed scope would buy is the point
of keeping it — from the ones that are simply stale.

720 pages still parse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same shape as crates/failproofaid/src/paths.rs: resolution takes its inputs as
arguments, with a thin wrapper reading the environment. Both sides are then
testable without mutating process state, which is what lets a cross-check
assert they agree rather than assert each in isolation.

That cross-check is the point. A client and daemon that disagree about the
socket path do not fail loudly — the client finds nothing, returns null, and
every hook silently takes the legacy path forever. Nobody notices until someone
asks why the daemon is idle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stage 3 was "privileged install and the boundary" — the _failproofai account,
the /opt and /var/lib ownership split, and a journal-backed transactional setup
whose twelve steps each recorded an inverse. In user scope it becomes a
user-scope service install, which is most of that machinery gone rather than
relocated.

Verification layer L3 loses the assertions that were its substance: no
`sudo -u _failproofai test -w` on the policy store, no ProtectHome=yes proving
the daemon cannot read a user's transcripts, no socket-substitution case. Those
tested a boundary that no longer exists, and keeping them as ceremony against a
daemon running as the user would be worse than removing them — a green
assertion that means nothing is how a suite stops being believed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ually make

The old negative assertions were the layer's substance and all of them tested
the service-account boundary: that _failproofai could not write the policy
store, that ProtectHome=yes stopped the daemon reading a user's transcripts,
that a user could not unlink the socket and bind an impostor. None of those is
a property of this version.

What replaces them is stronger in one specific way. The old positive assertion
was a golden table of owners and modes for every installed path; the new one is
`find / -newer <stamp>` excluding $HOME and /tmp, asserted empty. The first
says the paths we created have the owners we expected. The second says we
created nothing outside the user's tree at all — which is the actual claim user
scope makes, and it catches a path nobody thought to put in the table.

Two additions worth noting because they are real operational exposure rather
than restatements. The suite now runs setup in an image with **no sudo
installed** and requires exit 0, which makes "needs no privilege" a test
rather than an assurance. And it asserts the service returns after a simulated
logout and login both with and without `loginctl enable-linger` — a systemd
*user* unit is stopped at logout unless lingering is enabled, so the linger-off
case pins the documented behaviour instead of letting it be discovered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`--help` and `--version` exited 2 when neither $HOME nor $XDG_RUNTIME_DIR was
set, because the socket was resolved at the top of main before any argument was
looked at. That is precisely the environment where someone reaches for
`--help` — a stripped container, a systemd unit with a minimal `Environment=`,
a CI shell — so the one command that explains how to pass `--socket` was the
one that refused to run without it.

Resolution now happens at the point of needing a socket, after argument
parsing, so `--help` prints the preference order and `--version` answers
regardless of environment.

Adds `__tests__/hooks/daemon-paths.test.ts`: 21 cases asserting the TypeScript
client and `crates/failproofaid/src/paths.rs` resolve identically across
explicit override, XDG_RUNTIME_DIR set, both unset, and XDG_RUNTIME_DIR set but
empty. Disagreement between them is silent — the client finds no socket,
returns null, and every hook takes the legacy path forever — so it needs a test
rather than a comment asking both sides to stay in step.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Amendments 2 and 4 no longer describe anything. Amendment 2 required a
root-owned machine.json because policy enablement lived in a user-writable file
the agent could edit; in user scope everything is user-writable by design, so
there is no privileged surface to protect and .failproofai in user scope is
simply authoritative. Amendment 4 documented the per-user agent's unit file as
the one exception to "nothing enforcement depends on lives under a user-owned
root" — there is no per-user agent, and the rule it was an exception to is gone
with it.

Marked superseded rather than deleted. Both were found by review and each
records a specific way the design was wrong at the time; a later reader
wondering why enablement is not protected deserves the history, and someone
proposing to add the managed scope back needs to know these two come with it.

Amendment 1 stands unchanged and is worth repeating because the simplification
does not touch it: a LaunchAgent is signed and notarized exactly like a
LaunchDaemon. Dropping the privilege story does not drop the signing story.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 2 assumed Phase 1's privileged install: setup ran under sudo, and the
machine credential was stored "where the daemon's service account can read it
and enrolled users cannot — inside the privileged state tree, never under ~".
Neither holds. The sudo is gone from the example, and the credential lives in
the user's own configuration alongside the delivery key.

The consequence is larger than a path change and is now stated rather than left
to be discovered: **an enrolled identity is a user's installation on a host,
not the host.** Two developers on one workstation enrol twice. A fleet that
needs one identity per machine, held where the machine's own users cannot read
it, needs the managed scope first — and Phase 2 should not try to simulate one
on top of user scope, because a credential every enrolled user can read is not
a machine identity however it is labelled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The cross-implementation half now spawns the built `failproofaid --help` with a
deliberately empty environment and reads its "Resolved for this environment:"
line, rather than re-deriving what the Rust would do. Parsing paths.rs would
have asserted that two files agree about what they say; running the binary
asserts the two implementations agree about what they do.

Skipped when the binary is absent, so a checkout without a Rust toolchain does
not fail — but skipped visibly rather than silently passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The managed design put the events:add key "in privileged configuration the
service account reads and enrolled users cannot". In user scope there is no
such place: it lives in the user's own configuration, so the agent running as
that user can read it. That needed saying outright rather than being left to
follow from the scope change.

The exposure is bounded and the document now bounds it. It is an events:add
key — it grants writing events to the destination the user already sends events
to, not reading anything back and not reaching another machine's data. It is
also unchanged from what the standalone collector has today, so this is a
property being made explicit rather than one being introduced. The operator
answer is the one that was always right for this key: issue per machine, scope
to events:add alone, rotate. A key an agent could not reach needs the daemon to
run as an account the agent is not, which is the deferred managed scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The check was documented before it existed. `serve_connection` read peer
credentials and used the uid only to derive `home`, while PROTOCOL.md and the
commit that introduced it both described peer credentials as a "this connection
is mine" check keeping other users out. It was not one: a peer of any uid was
served, with *their* home derived, and the only thing actually keeping a
stranger out was the socket's file mode.

Found by an agent that ran the daemon rather than read it. That is the same
class of defect as the four vacuous guards already fixed on this branch — a
claim that reads as verified and is not — except this one was in a security
sentence, which is worse.

Now `serve_connection` compares `peer.uid` against `fpai_ipc::current_uid()`
and drops the connection otherwise. Defence in depth rather than redundancy:
`0600` on the socket and `0700` on its directory are the first line and are
sufficient on a correctly-created install, but they are filesystem state and
filesystem state drifts — a permissive umask, a restore from backup, a `chmod`
by a well-meaning script, a future code path that binds elsewhere. Two integers
from the kernel do not drift.

`current_uid()` goes in `fpai-ipc`'s peer module, where the credential concerns
already live, via `nix::unistd::Uid::current()` — safe, so it does not
reintroduce the `unsafe` just purged from `paths.rs`.

Two tests, and the positive one is not filler: a check that refused *everyone*
would satisfy every negative assertion in that file while the daemon answered
nothing, so the own-uid case has to be asserted too. The cross-uid case needs a
second real account and belongs in the service-lifecycle suite; here the source
assertion is what stops the check regressing back into a comment.

## PROTOCOL.md caught up with the scope decision

Three passages still described the managed design. The enabled set no longer
awaits a root-owned `machine.json` — client-asserted is not a compromise when
the client and daemon are the same user. `needs_user_context` no longer means
"no per-user agent was attached"; there is no per-user agent, and it now names
what it actually lists: the seven host-access builtins and every custom policy.
And the `host.home` rule is re-justified as correctness rather than defence,
because a rule whose stated reason has evaporated is a rule someone deletes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removing "prepare": "bun run build" broke CI's test job, and it took a CI log
to find out. That script was load-bearing in two unrelated places: it populated
dist/ before npm publish (the reason it was removed, correctly) and it also
made dist/index.js exist for the test job as a side effect of bun install.

The custom-policy loader writes a .__failproofai_tmp__.mjs shim beside each
policy file and dynamically imports it; the shim resolves the bare specifier
'failproofai' through findDistIndex(), which needs dist/index.js. Without it
every convention and custom policy fails to load, and four suites fail on empty
results — 'expected [] to have a length of 1' — which names nothing. Only the
CI log carried the actual cause: Cannot find package 'failproofai'.

It reproduced nowhere locally, including with dist/ deleted, because a machine
that has ever run bun link resolves the bare specifier through node_modules
instead. That is worth recording: the local run was not a weaker version of the
CI run, it was a different environment that cannot see this class of bug at all.

test-e2e has had an explicit build step since long before this branch, which is
why it stayed green while test went red — the asymmetry was the clue.

Builds only src/index.ts rather than the full bun run build: the unit suite
needs the public-API bundle and nothing else, and a Next.js build would add
minutes to each of three matrix legs for no coverage.

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant