[desgin-docs] v1.0.0 design set, Phase 1 implementation plan, and Stage 0 + Stage 1 of failproofaid - #625
[desgin-docs] v1.0.0 design set, Phase 1 implementation plan, and Stage 0 + Stage 1 of failproofaid#625NiveditJain wants to merge 58 commits into
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds 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. ChangesFailproofAI design documentation
Estimated code review effort: 3 (Moderate) | ~30 minutes Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
CHANGELOG.mddesgin-docs/v1.0.0/README.md
There was a problem hiding this comment.
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
📒 Files selected for processing (11)
CHANGELOG.mddesgin-docs/v1.0.0/README.mddesgin-docs/v1.0.0/failproofaid/01-user-experience.mddesgin-docs/v1.0.0/failproofaid/02-harness-integration.mddesgin-docs/v1.0.0/failproofaid/03-daemon-architecture.mddesgin-docs/v1.0.0/failproofaid/04-cloud-policy-management.mddesgin-docs/v1.0.0/failproofaid/05-collector-integration.mddesgin-docs/v1.0.0/failproofaid/06-service-and-updates.mddesgin-docs/v1.0.0/failproofaid/07-delivery-plan.mddesgin-docs/v1.0.0/failproofaid/08-release-and-packaging.mddesgin-docs/v1.0.0/failproofaid/README.md
🚧 Files skipped from review as they are similar to previous changes (1)
- CHANGELOG.md
There was a problem hiding this comment.
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 | 🟠 MajorMake 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 plainnpxas 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
📒 Files selected for processing (5)
desgin-docs/v1.0.0/failproofaid/01-user-experience.mddesgin-docs/v1.0.0/failproofaid/03-daemon-architecture.mddesgin-docs/v1.0.0/failproofaid/05-collector-integration.mddesgin-docs/v1.0.0/failproofaid/06-service-and-updates.mddesgin-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
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
There was a problem hiding this comment.
Actionable comments posted: 16
🧹 Nitpick comments (1)
desgin-docs/v1.0.0/failproofaid/07-delivery-plan.md (1)
76-76: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winTreat
user-contextspawn 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
📒 Files selected for processing (10)
CHANGELOG.mddesgin-docs/v1.0.0/failproofaid/01-user-experience.mddesgin-docs/v1.0.0/failproofaid/02-harness-integration.mddesgin-docs/v1.0.0/failproofaid/03-daemon-architecture.mddesgin-docs/v1.0.0/failproofaid/04-cloud-policy-management.mddesgin-docs/v1.0.0/failproofaid/05-collector-integration.mddesgin-docs/v1.0.0/failproofaid/06-service-and-updates.mddesgin-docs/v1.0.0/failproofaid/07-delivery-plan.mddesgin-docs/v1.0.0/failproofaid/08-release-and-packaging.mddesgin-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
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
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>
…ng main" This reverts commit 912ff1b.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
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.
|
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>
…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>
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_MODEis set — no configuration default, no wizard default. A revert is onegit 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 touser-contextand 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.tstransitively importednode:fs. Found by injectingnode:fsand watching nothing fail.Stage 0
builtin/payload-only.ts(32) +builtin/host-access.ts(7). Registry order, categories and defaults snapshot-identical.home/projectDirtravel as request data. The daemon evaluates for another UID, so its ownhomedir()is the service account's — andisAgentInternalPath/block-read-outside-cwdboth widen the allow set, the direction that does not announce itself.evaluatePolicies→evaluateVerdicts+encodeResponse. The twelve-vendor response matrix moved character-identically.resolveCodexMode's unbounded scan of~/.codex/sessionsbounded to a 256 KiB head window — measured: firstturn_contextat 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 andrust-qualityjob, 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:pathis replaced by a port proven equivalent tonode:path.posixdifferentially over 8,000+ generated cases; host modules by stubs that throw a named capability;hook-logger/hook-telemetry/telemetry-idby inert no-ops — which removed afetch()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).homeis derived at the socket fromgetpwuid_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_factscannot be derived, so they ride as client-asserted and any decision reading one is reportedsealed_unattested— the honest version of "unforgeable".Verification
node:vmcontext with no host globals at all.user_contextresults never lowers asealeddeny — the formal statement of the two-tier argument.homerefused, unknown env fact refused, oversize frame refused without allocating.Defects found and fixed during review
An adversarial review found four, two severe and reproduced end-to-end:
block-curl-pipe-shis 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 callingeval— it proved the handler worked and said nothing about whether anything armed it.rm -rf /returned allow from the daemon and deny from legacy. The documentedneeds_user_contextfallback was structurally unreachable.recv(), leaking one per hook event until the listener died.PROTOCOL.mddocumented a canonicalization check that never existed and is not implementable from the current envelope.Live bugs in the existing repo, found by survey
"prepare"alone would have published an empty package.prune-standalone.mjspruned"design-docs"; the directory isdesgin-docs).bun.lockbthat does not exist.handler.tsarmed a 10-second timeout per custom hook and never cleared it.Test status
cargo fmt/clippy -D warningsThe 15 remaining unit failures are pre-existing
window.localStoragefailures inproject-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
_failproofaiaccount; Stage 4 the native client, per-user agent and signed catalog; Stage 5 collector convergence, blocked on an external dependency —agenteye-collectoris a separate repository whose conformance corpus must be vendored first.The root-owned
machine.jsonis 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