Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
fdbbc14
docs: add v1 design documents directory
github-actions[bot] Jul 30, 2026
a16399f
docs: link design docs changelog entry to PR
NiveditJain Jul 30, 2026
bda251b
docs: draft failproofaid architecture
NiveditJain Jul 30, 2026
1c8a823
docs: design centralized policy management
NiveditJain Jul 30, 2026
eea0076
docs: split failproofaid design by concern
NiveditJain Jul 30, 2026
30ef934
docs: make policy evaluation location configurable
NiveditJain Jul 30, 2026
18aa936
docs: scope cloud evaluation beyond v1
NiveditJain Jul 30, 2026
95b8e58
docs: preserve standalone OSS policy workflows
NiveditJain Jul 30, 2026
6e94b39
docs: scope failproofaid v1 to linux and macos
NiveditJain Jul 30, 2026
bed7c98
docs: define login and OSS setup choice
NiveditJain Jul 30, 2026
66d938e
docs: harden setup delivery and update contracts
NiveditJain Jul 30, 2026
ef37b44
docs: narrow v1 distribution to npm
NiveditJain Jul 30, 2026
b947a96
docs: close setup and spool crash gaps
NiveditJain Jul 30, 2026
87127f7
docs: design user and system daemon scopes
NiveditJain Jul 30, 2026
e393e43
docs: prefer tamper-resistant enforcement scope
NiveditJain Jul 30, 2026
61ae486
docs: reconcile removed harness hooks
NiveditJain Jul 30, 2026
9a1c1c0
docs: replace binary updater with hook schemas
NiveditJain Jul 30, 2026
ec28e46
docs: make the enforcement boundary a service account, not root
NiveditJain Jul 30, 2026
4171a46
docs: close four holes in the service-account boundary
NiveditJain Jul 30, 2026
5cb4d84
docs: give the local dashboard an access model
NiveditJain Jul 30, 2026
bfb2b2c
docs: ship one service scope and split v1 into two phases
NiveditJain Jul 30, 2026
9799459
docs: move the whole collector into Phase 1
NiveditJain Jul 30, 2026
fe1cd14
docs: add the Phase 1 implementation plan
NiveditJain Jul 30, 2026
9de5c4f
feat: land Phase 1 Stage 0 and Stage 1 of failproofaid
NiveditJain Jul 30, 2026
da0fa55
fix: withdraw the corrupt bench baseline and gate the daemon's defaul…
NiveditJain Jul 30, 2026
47bcd33
fix: four defects an adversarial review found in the Stage 1 daemon
NiveditJain Jul 30, 2026
509e6e6
docs: record the four review fixes in the changelog
NiveditJain Jul 30, 2026
ca4c771
fix: make the bench harness's own integrity gate non-vacuous
NiveditJain Jul 30, 2026
2b3a61d
docs: record the bench-harness gate fix
NiveditJain Jul 30, 2026
a2cb9ee
fix: make two concurrent bench captures impossible
NiveditJain Jul 30, 2026
05ed3bb
docs: record the bench capture lock
NiveditJain Jul 30, 2026
912ff1b
fix(ci): run CI on every pull request, not only those targeting main
NiveditJain Jul 30, 2026
443c681
Revert "fix(ci): run CI on every pull request, not only those targeti…
NiveditJain Jul 30, 2026
9bf1311
fix: two failures CI caught that local runs never would have
NiveditJain Jul 30, 2026
4e00e93
docs: fold the Stage 0 amendments into the design set (in progress)
NiveditJain Jul 30, 2026
2c0f0a3
docs: finish folding the Stage 0 amendments, and record where each la…
NiveditJain Jul 30, 2026
35f890e
docs: record the amendment fold in the changelog
NiveditJain Jul 30, 2026
40a1d11
docs: name the sealed runtime accurately in the tier table
NiveditJain Jul 30, 2026
5dc478e
docs: cross-reference the per-user agent exception where the rule is …
NiveditJain Jul 30, 2026
4535acd
fix: make the remaining hooks-config mocks additive
NiveditJain Jul 30, 2026
d2a7b9e
perf: capture the cold-start latency baseline, cleanly this time
NiveditJain Jul 30, 2026
07f3998
refactor: run entirely in user scope for v1.0.0
NiveditJain Jul 30, 2026
11f211c
docs: record the user-scope simplification in the changelog
NiveditJain Jul 30, 2026
91298cb
docs: rewrite the user experience and harness integration for user sc…
NiveditJain Jul 30, 2026
3b6bed2
docs: state what hook reconciliation can and cannot do in user scope
NiveditJain Jul 30, 2026
e57f40b
refactor: finish the user-scope sweep across code and tests
NiveditJain Jul 30, 2026
d957171
docs: rewrite the daemon, service, collector and release docs for use…
NiveditJain Jul 30, 2026
122034f
refactor: make the client's path resolution pure, mirroring the Rust
NiveditJain Jul 30, 2026
8ce4e45
docs: rewrite the stages and verification layers for user scope
NiveditJain Jul 30, 2026
d67e6e8
docs: replace L3's boundary probes with assertions user scope can act…
NiveditJain Jul 30, 2026
3fe1c0b
fix: resolve the daemon socket after parsing arguments, not before
NiveditJain Jul 30, 2026
038b81d
docs: record which amendments the scope decision superseded
NiveditJain Jul 30, 2026
8209abf
docs: follow the scope decision into Phase 2's enrollment model
NiveditJain Jul 30, 2026
7d4dff9
test: drive the real binary in the path-agreement check
NiveditJain Jul 30, 2026
d71f806
docs: final consistency pass on the user-scope rewrite
NiveditJain Jul 30, 2026
c3f3f12
docs: state that the delivery key is readable by the user's agent
NiveditJain Jul 30, 2026
5f74c5b
fix: actually refuse a peer whose uid is not the daemon's
NiveditJain Jul 30, 2026
14b0b44
fix(ci): build dist/index.js before the unit suite
NiveditJain Jul 30, 2026
72967bb
fix: three defects an adversarial sweep found, all in code from this …
NiveditJain Jul 31, 2026
17192ff
docs: record the three adversarial-sweep fixes
NiveditJain Jul 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
25 changes: 25 additions & 0 deletions .github/expected-pack-files.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Top-level entries in the tarball `npm publish` would upload, one per line.
#
# Compared by scripts/check-pack-allowlist.mjs against the first path segment
# of every file in `npm pack --dry-run --json`. Granularity is deliberate: the
# tarball has ~1,700 files, nearly all under .next/standalone/node_modules, and
# a file-level manifest would churn on every dependency bump.
#
# If this check fails, the tarball's shape changed. Decide whether that was
# intended, then regenerate with:
#
# bun run build && node scripts/check-pack-allowlist.mjs --write
#
# `.next` and `dist` are gitignored build output and only appear after a build.

.next
LICENSE
README.md
bin
dist
lib
openclaw-plugin
package.json
pi-extension
scripts
src
162 changes: 134 additions & 28 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
- uses: actions/cache@v6
with:
path: ~/.bun/install/cache
key: bun-${{ runner.os }}-${{ hashFiles('bun.lockb') }}
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
restore-keys: bun-${{ runner.os }}-

- name: Install dependencies
Expand All @@ -35,31 +35,40 @@ jobs:
timeout_minutes: 5
command: bun install --frozen-lockfile

# Replaces the inline shell that used to live here. That shell only ever
# looked at `packages/*/package.json` and `packages/wrapper/package.json`,
# neither of which exists in this repository, so it passed vacuously from
# the initial import onward. The script keeps those semantics and adds the
# Cargo workspace version, crate version inheritance, and a ban on npm
# lifecycle scripts (see scripts/check-versions.mjs). Unit-tested in
# __tests__/scripts/check-versions.test.ts.
- name: Check version consistency
run: node scripts/check-versions.mjs

# Tripwire on the published tarball's top-level shape, so a new directory
# can neither silently ship nor silently fail to ship. This job does not
# build, so the two gitignored build outputs (`dist`, `.next`) are skipped
# with a notice here and enforced in the `build` job below, which runs the
# same check against a real build.
- name: Check pack allowlist
run: node scripts/check-pack-allowlist.mjs

# The sealed worker bundle is committed under crates/generated/ and
# embedded into the daemon binary with include_str!. If it drifts from
# src/policy-runtime/, the daemon evaluates policy source that no longer
# exists in the tree — a divergence that produces a *wrong verdict*
# rather than an error, and that no other test would notice.
- name: Check sealed worker bundle is current
run: bun scripts/build-sealed-bundle.ts --check

# Same contract for the canonicalization tables, which are generated from
# src/hooks/types.ts and likewise compiled into the daemon.
- name: Check canonicalization tables are current
run: |
ROOT_VERSION=$(jq -r .version package.json)
echo "Root version: $ROOT_VERSION"
MISMATCH=0
for pkg in packages/*/package.json; do
[ -f "$pkg" ] || continue
PKG_VERSION=$(jq -r .version "$pkg")
if [ "$PKG_VERSION" != "$ROOT_VERSION" ]; then
echo "::error file=$pkg::Version mismatch: $pkg has $PKG_VERSION, expected $ROOT_VERSION"
MISMATCH=1
fi
done
# Check optionalDependencies in wrapper
for dep_version in $(jq -r '.optionalDependencies // {} | values[]' packages/wrapper/package.json 2>/dev/null || true); do
if [ "$dep_version" != "$ROOT_VERSION" ]; then
echo "::error file=packages/wrapper/package.json::Dependency version mismatch: $dep_version, expected $ROOT_VERSION"
MISMATCH=1
fi
done
if [ "$MISMATCH" -eq 1 ]; then
echo "::error::Version mismatch detected across package.json files"
exit 1
fi
echo "All versions match: $ROOT_VERSION"
bun scripts/gen-canon-tables.ts
git diff --exit-code -- crates/generated/canonicalization-tables.json \
crates/generated/enforcement-capability.json \
|| { echo "::error::crates/generated/*.json is stale. Regenerate: bun scripts/gen-canon-tables.ts"; exit 1; }

- name: Lint
uses: nick-fields/retry@v4
Expand Down Expand Up @@ -97,7 +106,7 @@ jobs:
- uses: actions/cache@v6
with:
path: ~/.bun/install/cache
key: bun-${{ runner.os }}-${{ hashFiles('bun.lockb') }}
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
restore-keys: bun-${{ runner.os }}-

- name: Install dependencies
Expand All @@ -107,6 +116,29 @@ jobs:
timeout_minutes: 5
command: bun install --frozen-lockfile

# `dist/index.js` must exist before the unit suite runs, and this step is
# what replaced the mechanism that used to guarantee it.
#
# The custom-policy loader writes a `.__failproofai_tmp__.mjs` shim beside
# each policy file and dynamically imports it; that shim resolves the bare
# specifier `failproofai` through `findDistIndex()`, which looks for
# `dist/index.js`. Without it, every convention and custom policy fails to
# load with `Cannot find package 'failproofai'`, and four suites fail on
# empty results rather than on anything that names the cause.
#
# `"prepare": "bun run build"` used to make this true as a side effect of
# `bun install`. Removing it was correct — it was also the only thing
# populating `dist/` before `npm publish`, so a lifecycle script was
# silently load-bearing in two unrelated places. This makes the second one
# explicit, matching the `Build E2E fixtures` step the `test-e2e` job has
# had all along.
#
# Only `src/index.ts` is built, not the full `bun run build`: the unit
# suite needs the public-API bundle and nothing else, and the Next.js
# build would add minutes per matrix leg for no coverage.
- name: Build the public API bundle
run: bun build src/index.ts --outdir dist --target node --format cjs

- name: Test (${{ matrix.env-config.name }})
uses: nick-fields/retry@v4
env: ${{ matrix.env-config.env }}
Expand All @@ -130,7 +162,7 @@ jobs:
- uses: actions/cache@v6
with:
path: ~/.bun/install/cache
key: bun-${{ runner.os }}-${{ hashFiles('bun.lockb') }}
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
restore-keys: bun-${{ runner.os }}-

- name: Install dependencies
Expand All @@ -147,6 +179,80 @@ jobs:
timeout_minutes: 10
command: bun run build

# Same check the quality job runs, but against a built tree — so the
# "would NOT be published" direction is fatal here. This is what stands
# between `publish.yml` and shipping a package with an empty `dist/`.
- name: Check pack allowlist (built tree)
run: node scripts/check-pack-allowlist.mjs

rust-quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.1

# rust-toolchain.toml pins the channel and asks for rustfmt + clippy, so a
# bare `rustup toolchain install` (no argument) reads the file and installs
# exactly that. Doing it explicitly keeps the download out of the timing of
# the checks, and asserting both component versions here means a toolchain
# that installed without them fails on this step rather than several steps
# later with "no such command: `fmt`".
- name: Install pinned toolchain
run: |
set -euxo pipefail
rustup toolchain install
rustup show active-toolchain
rustc --version
cargo fmt --version
cargo clippy --version

# Plain actions/cache rather than Swatinem/rust-cache: that action derives
# its key from `cargo metadata` plus a lockfile, and this workspace has
# zero members and therefore no Cargo.lock, which is not a case it is
# documented to handle. The requirement here is that the job be green from
# the first commit, so the cache is kept dumb. Revisit once crates exist.
- uses: actions/cache@v6
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
target
key: cargo-${{ runner.os }}-${{ hashFiles('rust-toolchain.toml', 'Cargo.lock', 'crates/*/Cargo.toml') }}
restore-keys: cargo-${{ runner.os }}-

# The workspace is deliberately empty for now (Stage 0 lands the plumbing
# before the first crate). Verified against cargo 1.97.1: `cargo metadata`
# succeeds on a zero-member workspace, but `cargo fmt --all` exits 1 with
# "Failed to find targets" and `cargo clippy`/`cargo test` exit 101 with
# "the manifest is virtual, and the workspace has no members". So the three
# checks are gated on a crate actually existing, and the manifest itself is
# validated unconditionally. The gate turns itself on with the first crate.
- name: Detect crates
id: crates
run: |
if ls crates/*/Cargo.toml >/dev/null 2>&1; then
echo "present=true" >> "$GITHUB_OUTPUT"
echo "Crates found — running fmt, clippy and test."
else
echo "present=false" >> "$GITHUB_OUTPUT"
echo "::notice::No crates under crates/ yet — validating the workspace manifest only."
fi

- name: Validate workspace manifest
run: cargo metadata --no-deps --format-version 1 > /dev/null

- name: cargo fmt
if: steps.crates.outputs.present == 'true'
run: cargo fmt --all -- --check

- name: cargo clippy
if: steps.crates.outputs.present == 'true'
run: cargo clippy --workspace --all-targets -- -D warnings

- name: cargo test
if: steps.crates.outputs.present == 'true'
run: cargo test --workspace

docs:
runs-on: ubuntu-latest
steps:
Expand All @@ -159,7 +265,7 @@ jobs:
- uses: actions/cache@v6
with:
path: ~/.bun/install/cache
key: bun-${{ runner.os }}-${{ hashFiles('bun.lockb') }}
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
restore-keys: bun-${{ runner.os }}-

- name: Install dependencies
Expand Down Expand Up @@ -204,7 +310,7 @@ jobs:
- uses: actions/cache@v6
with:
path: ~/.bun/install/cache
key: bun-${{ runner.os }}-${{ hashFiles('bun.lockb') }}
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
restore-keys: bun-${{ runner.os }}-

- name: Install dependencies
Expand Down
37 changes: 35 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ jobs:
permissions:
contents: write
id-token: write
env:
FAILPROOFAI_TELEMETRY_DISABLED: "1"
NEXT_TELEMETRY_DISABLED: "1"
steps:
# Token for the version-bot GitHub App — a bypass actor on the org-level
# `failproofai-rules` ruleset (PR + 1 review required on main). The default
Expand Down Expand Up @@ -38,7 +41,7 @@ jobs:
- uses: actions/cache@v6
with:
path: ~/.bun/install/cache
key: bun-${{ runner.os }}-${{ hashFiles('bun.lockb') }}
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
restore-keys: bun-${{ runner.os }}-

- name: Install dependencies
Expand Down Expand Up @@ -103,8 +106,38 @@ jobs:
npm version "${{ steps.version.outputs.publish_version }}" --no-git-tag-version
echo "Updated package.json to ${{ steps.version.outputs.publish_version }}"

# LOAD-BEARING. `dist/` and `.next/standalone/` are gitignored but sit in
# package.json's `files` allowlist, so the tarball is only non-empty
# because something built them first. That something used to be the
# `prepare` lifecycle script, which `bun install --frozen-lockfile` above
# ran implicitly; `prepare` is gone (see scripts/check-versions.mjs, which
# now fails the build if any lifecycle script comes back), so this step is
# the ONLY thing standing between this workflow and publishing an empty
# package. It runs after the version bump above because `build:cli`
# inlines package.json's `version` into dist/cli.mjs — building earlier
# would ship a binary that reports the previous version.
- name: Build publishable artifacts
uses: nick-fields/retry@v4
with:
max_attempts: 3
timeout_minutes: 15
command: bun run build

- name: Verify build output is present
run: |
set -euo pipefail
for f in dist/cli.mjs dist/index.js .next/standalone/server.js; do
if [ ! -s "$f" ]; then
echo "::error file=$f::Missing or empty after \`bun run build\` — refusing to publish an incomplete package"
exit 1
fi
echo "ok: $f ($(wc -c < "$f") bytes)"
done

# `--ignore-scripts` so a re-added lifecycle script can never silently
# re-enter the publish path. The build above is explicit and verified.
- name: Publish
run: npm publish --provenance --tag ${{ steps.version.outputs.dist_tag }}
run: npm publish --provenance --ignore-scripts --tag ${{ steps.version.outputs.dist_tag }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/translate-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ jobs:
- uses: actions/cache@v6
with:
path: ~/.bun/install/cache
key: bun-${{ runner.os }}-${{ hashFiles('bun.lockb') }}
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
restore-keys: bun-${{ runner.os }}-

- name: Install dependencies
Expand Down
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@
/build
/dist

# rust
# `Cargo.lock` is deliberately NOT ignored: this workspace will hold binaries
# (failproofaid, failproofai-cli), and a binary workspace should commit its lock
# file so a release builds from the same dependency graph that CI tested. There
# are no crates yet, so cargo produces no lock file to commit.
/target

# misc
.DS_Store
*.pem
Expand Down Expand Up @@ -100,3 +107,11 @@ COMMIT_MSG.tmp
/canary.env
/integration-suite-state.json
/cli-integration-state.json

# Scratch from scripts/bench-hook.ts. It snapshots dist/ here at startup so a
# concurrent build cannot corrupt an in-flight measurement run.
/.bench-hook-tmp/

# Mutual-exclusion lock held by scripts/bench-hook.ts for the duration of a
# capture. Two captures share one working directory and destroy each other.
/.bench-hook.lock
Loading