Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
139 changes: 111 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 Down Expand Up @@ -130,7 +139,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 +156,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 +242,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 +287,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
Loading