Skip to content
Closed
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .bunfig.toml

This file was deleted.

48 changes: 48 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Generated and vendored content, marked so GitHub's Linguist stops counting it.
#
# Why this file exists: 658 of this repo's ~1,428 tracked files — 46% — are
# machine-generated translations regenerated by .github/workflows/translate-docs.yml.
# Unmarked, they dominate the language bar, expand by default in every pull
# request, and make a two-line change to an English page look like a 600-file
# diff. `linguist-generated` collapses each one to a single line in the diff
# view and drops it from the language statistics. Nothing about the build,
# the tests, or the published package changes — this is git metadata only.
#
# The 14 language codes below are the same set as LANGUAGES in
# scripts/translate-docs/config.ts. __tests__/scripts/translate-docs/config.test.ts
# asserts the two lists agree, so adding a language to the translator without
# adding it here fails CI rather than silently un-collapsing a new locale.

# --- Translated documentation (regenerated; edit the English source instead) ---
docs/ar/** linguist-generated=true
docs/de/** linguist-generated=true
docs/es/** linguist-generated=true
docs/fr/** linguist-generated=true
docs/he/** linguist-generated=true
docs/hi/** linguist-generated=true
docs/it/** linguist-generated=true
docs/ja/** linguist-generated=true
docs/ko/** linguist-generated=true
docs/pt-br/** linguist-generated=true
docs/ru/** linguist-generated=true
docs/tr/** linguist-generated=true
docs/vi/** linguist-generated=true
docs/zh/** linguist-generated=true

# --- Translated READMEs (regenerated from the root README.md) ---
docs/i18n/README.*.md linguist-generated=true

# --- Lockfiles: resolver output, never hand-edited ---
bun.lock linguist-generated=true
Cargo.lock linguist-generated=true

# --- Release history: append-only, and the single noisiest file in any diff ---
CHANGELOG.md linguist-generated=true

# --- The audit design lab: a standalone reference kit, not application source.
# Its .jsx/.html/.css are read by humans and by nothing else in the build. ---
assets/audit/** linguist-vendored=true

# --- Keep shell scripts and .mjs hooks executable-safe across platforms ---
*.sh text eol=lf
*.mjs text eol=lf
158 changes: 158 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Contributing to Failproof AI

## Start here

Every top-level directory now carries its own `README.md` — `src/`, `app/`, `lib/`,
`crates/`, `bin/`, `scripts/`, `docs/`, `examples/`, `__tests__/`, `assets/`,
`public/`, `pi-extension/`, `openclaw-plugin/`, `integration-suite/`,
`docker-hook-sync/`, `skills/`. Read the one for the directory you are about to
change before you change it. `CLAUDE.md` at the repo root (1,200+ lines) is the
de-facto architecture manual — every per-CLI hook contract is documented there and
nowhere else.

## Five products, one package.json

This is not one codebase. It is five, sharing a lockfile:

| Product | Lives in | What it is |
|---------|----------|------------|
| CLI | `src/hooks/`, `src/audit/`, `bin/`, parts of `lib/` | Installs hook configs into 12 agent CLIs; evaluates policies when those hooks fire. Also the `failproofai audit` product. |
| Dashboard | `app/`, `public/`, `proxy.ts`, `instrumentation.ts`, `next.config.ts`, most of `lib/` | Next.js 16 app router — session viewer, policy config, audit results. |
| Daemon | `crates/` (`failproofaid`, `fpai-collect`, `fpai-ipc`) | ~41k lines of Rust. Socket server, service lifecycle, worker supervision, and session collectors for 12 CLIs. |
| Docs site | `docs/` | Mintlify. 48 hand-written English `.mdx`; the other 644 are machine translations regenerated by `.github/workflows/translate-docs.yml`. `docs/agenteye/` is a **different product's** docs sharing the site. |
| Plugin packages | `pi-extension/`, `openclaw-plugin/` | Static packages shipped inside the npm tarball (`"files"` in `package.json`). Their directory names are frozen — installed users' settings files reference them by path. |

The 12 supported agent CLIs: `claude`, `codex`, `copilot`, `cursor`, `opencode`,
`pi`, `hermes`, `openclaw`, `factory`, `devin`, `antigravity`, `goose`.

On a machine that finished `failproofai config`, the **daemon is the only
evaluator** and an unreachable daemon **denies**. In-process evaluation survives
only where `daemonConfigured` is false — including this repo's own dogfood configs.

## Prerequisites

- **Bun >= 1.3.0** — required, not optional. `bun install` runs `prepare` →
`bun run build`, which shells out to `bun build`. There is no Node-only setup path.
- **Node.js >= 20.9.0** — what the published package targets, and what runs the
in-repo dev hooks' launcher.
- **Rust** (`rust-toolchain.toml` pins it) only if you touch `crates/`.

## Development setup

```bash
git clone https://github.com/failproofai/failproofai.git
cd failproofai
bun install # runs `prepare` → `bun run build` (dist/ + a full Next.js build)
bun run dev # dev server at http://localhost:8020
```

`bun install --frozen-lockfile --ignore-scripts` skips that `prepare` build. It is
what the `rust-quality` CI job uses, and it is the fast install when you only need
`node_modules` (Rust work, docs work) and not `dist/`. Nothing else documents it.

**`bun run dev` and `bun run start` are not read-only.** Their `predev`/`prestart`
scripts run `bun run build:cli && bun link`, which symlinks a global `failproofai`
binary pointing at this working copy. If a `failproofai` on your PATH starts
behaving like your branch, that is why. `bun unlink` in the repo root undoes it.

### Build before the in-repo dev hooks will work

This repo **dogfoods failproofai on itself**, and the dot-directories at the root
are how. They are not all the same thing, and the difference matters:

| | Directories | How enforcement is wired |
|---|---|---|
| **Shell hooks** | `.claude/` `.codex/` `.cursor/` `.devin/` `.factory/` `.agents/` `.github/hooks/` | each config runs `node scripts/dev-hook.mjs --hook <Event> --cli <cli>` |
| **In-process plugins** | `.opencode/` `.pi/` | OpenCode and Pi have no shell-hook system, so these register a plugin instead — `.opencode/plugins/failproofai.mjs` and `.pi/settings.json` pointing at `../pi-extension`. Neither names `dev-hook.mjs`. |
| **Policy files** | `.failproofai/` | not hooks at all — the policy config and the convention policies this repo enforces on itself |

The shell-hook configs use `dev-hook.mjs` and **never `npx -y failproofai`** — the
`npx` form self-references the package being developed here. `internals/dogfood.md`
explains why each vendor's path is what it is.

> Do **not** run `failproofai policies --install` inside this repo. It rewrites
> those configs to the production `npx -y failproofai` form and breaks dogfooding.
> `__tests__/hooks/dogfood-configs.test.ts` is the tripwire that catches it.

`scripts/dev-hook.mjs` locates `bun` (across `PATH`, `$BUN_INSTALL/bin`,
`~/.bun/bin`, Homebrew, and every `~/.nvm/versions/node/*/bin`), builds
`dist/index.js` if it is missing, then hands off to `bin/failproofai.mjs`. See its
header, and the CLAUDE.md section, for why node fronts a bun-only binary. The
policies in `.failproofai/policies/*.mjs` `import` the `failproofai` package, which
resolves against the compiled **`dist/index.js`** bundle — not your live `src/`.

If `dist/` is missing the launcher rebuilds it and says so on stderr. If it is
**stale**, nothing detects that and you get policies enforcing yesterday's code.
Rebuild whenever you change `src/`:

```bash
bun run build # full build (dist/index.js + dist/cli.mjs + dist/worker.mjs + Next.js)
# …or just the hook bundle, much faster while iterating on policies:
bun build --target=node --format=cjs --outfile=dist/index.js src/index.ts
```

## The files you touch

| Task | Files |
|------|-------|
| Add a builtin policy | `src/hooks/builtin-policies.ts` (define it, `registerPolicy`), `src/hooks/policy-presets.ts` (which preset ships it on), `__tests__/hooks/builtin-policies.test.ts`, `docs/built-in-policies.mdx` |
| Add support for a new agent CLI | ~28 files. Enforcement: `src/hooks/integrations.ts`, `types.ts` (event/tool/input maps), `handler.ts`, `policy-evaluator.ts`, `enforcement-capability.ts`, `normalize-cli-payload.ts`, `tool-name-canonicalize.ts`, `resolve-transcript-path.ts`. Audit: `src/audit/cli-adapters/<cli>.ts` + `index.ts`, `lib/<cli>-sessions.ts`, `lib/<cli>-projects.ts`, `lib/projects.ts`, `lib/cli-registry.ts`, `lib/download-session.ts`. Daemon: `crates/fpai-collect/src/sources/<cli>/{mod,transform}.rs`, `sources/mod.rs`, `crates/failproofaid/src/main.rs`. Plus `assets/logos/<cli>-{dark,light}.svg`, the dogfood config, `scripts/dev-hook.mjs`, `integration-suite/`, and a canonicalize test per CLI. It is three products at once — that is why it is 28 files, not four. |
| Fix a dashboard bug | The route under `app/` (e.g. `app/project/[name]/page.tsx`), its server action in `app/actions/`, the parser in `lib/` it reads from, and a test in `__tests__/components/` or `__tests__/lib/` |
| Change a CLI's hook verdict shape | `src/hooks/policy-evaluator.ts` (the `cli === "<name>"` branch), `src/hooks/enforcement-capability.ts` (the machine-readable capability table), `__tests__/hooks/inert-deny-shapes.test.ts`, `__tests__/hooks/enforcement-capability.test.ts` |
| Add a daemon session collector | `crates/fpai-collect/src/sources/<cli>/{mod.rs,transform.rs}`, `crates/fpai-collect/src/sources/mod.rs`, `crates/failproofaid/src/main.rs` (task wiring + the harness-key list), `crates/fpai-collect/tests/<cli>_source.rs` |
| Edit the documentation | The English `.mdx` under `docs/` **only** — never a file under `docs/{ar,de,es,fr,he,hi,it,ja,ko,pt-br,ru,tr,vi,zh}/`, which `translate-docs.yml` regenerates. Add new pages to `docs/docs.json` nav or `mintlify validate` fails. |

Never edit a language directory by hand, and never rename `pi-extension/` or
`openclaw-plugin/`.

## Available scripts

| Script | Description |
|--------|-------------|
| `bun run dev` | Dev server on :8020 (also `bun link`s a global binary) |
| `bun run build` | Full build: `dist/index.js`, `dist/cli.mjs`, `dist/worker.mjs`, Next.js standalone |
| `bun run lint` | ESLint |
| `bunx tsc --noEmit` | Type-check |
| `bun run test:run` | Vitest once (`bun run test` for watch) |
| `bun run test:e2e` | E2E suite (`vitest.config.e2e.mts`) |
| `bun run validate:mdx` | Parse every MDX page and resolve image references |
| `cargo test --workspace` | Rust tests — spawns the real TS worker via `bun`, so `bun install` first |

## Environment variables

| Variable | Description |
|----------|-------------|
| `CLAUDE_PROJECTS_PATH` | Path to Claude projects directory |
| `FAILPROOFAI_LOG_LEVEL` | `info`, `warn`, `error` (default `warn`) |
| `FAILPROOFAI_TELEMETRY_DISABLED` | `1` disables anonymous telemetry |
| `FAILPROOFAI_DISABLE_PAGES` | Comma-separated: `policies`, `projects` |
| `FAILPROOFAI_DIST_PATH` | Where custom policies resolve `failproofai` from |
| `FAILPROOFAI_NO_DOWNLOAD` | `1` blocks fetching the daemon binary (air-gapped) |

## Pull requests

1. **One PR per branch.** Check `gh pr list --head <branch>` first; if one exists,
push to the same branch.
2. **Your branch must contain all of `main`.** `git fetch origin &&
git log --oneline origin/main ^HEAD` must print nothing; rebase if it does not.
3. **Update `CHANGELOG.md`.** Every PR. One line under the current
`## <version> — <YYYY-MM-DD>` heading, in Features / Fixes / Docs /
Dependencies. There is no `Unreleased` section.
4. **Add tests for new behaviour** in `__tests__/`. Do not edit an existing test to
make it pass — fix the code instead. The exception is a test asserting the exact
value you intentionally changed.
5. **CI must be green after every push.** Six jobs in `.github/workflows/ci.yml`:
`quality` (lint + tsc + version consistency, including `Cargo.toml` against root
`package.json`), `rust-quality` (fmt + clippy + `cargo test --workspace`),
`test` (unit, three env configs), `build`, `test-e2e`, `docs`. Locally:

```bash
bun run lint && bunx tsc --noEmit && bun run test:run && bun run build && bun run test:e2e
```

`gh run watch` after pushing. Never leave CI red.

## Reporting issues

[Open an issue](https://github.com/failproofai/failproofai/issues) — the templates
ask for the details we need.
4 changes: 2 additions & 2 deletions SECURITY.md → .github/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ for supply-chain threats before it can merge, via two complementary layers.

### 1. OSV-Scanner — the blocking CI gate

[`.github/workflows/osv-scanner.yml`](.github/workflows/osv-scanner.yml) runs
[`.github/workflows/osv-scanner.yml`](workflows/osv-scanner.yml) runs
[OSV-Scanner](https://google.github.io/osv-scanner/) against the resolved
dependency tree (`bun.lock`). It checks every direct and transitive package
against [OSV.dev](https://osv.dev), which aggregates GitHub/npm security
Expand Down Expand Up @@ -55,7 +55,7 @@ When the OSV-Scanner gate fails on a PR:

1. **Prefer fixing it.** Bump the affected dependency to a patched version. For a
transitive dependency that a parent pins to a vulnerable version, add a minimal
[`overrides`](package.json) entry (as we do for `postcss`) and let CI validate
[`overrides`](../package.json) entry (as we do for `postcss`) and let CI validate
the build.
2. **Only if there is no fix**, add a justified, time-boxed entry to
[`osv-scanner.toml`](osv-scanner.toml) (`id`, `reason`, `ignoreUntil`). Never
Expand Down
File renamed without changes.
9 changes: 0 additions & 9 deletions .github/smoke-test/expected/policies.html

This file was deleted.

9 changes: 0 additions & 9 deletions .github/smoke-test/expected/projects.html

This file was deleted.

7 changes: 0 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,6 @@ jobs:
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
# The daemon binaries DO ship as npm platform packages
# (@failproofai/failproofaid-<os>-<arch>), but their pins are injected
# into package.json at publish time by
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/osv-scanner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
# without needing a second channel. Posting is itself optional: it silently
# no-ops when the SLACK_WEBHOOK_URL repository secret isn't set.
#
# Triage / allow-listing unfixable advisories: see SECURITY.md and osv-scanner.toml
# (auto-loaded from the repo root by OSV-Scanner).
# Triage / allow-listing unfixable advisories: see .github/SECURITY.md and
# .github/osv-scanner.toml. OSV-Scanner only auto-discovers a config sitting in
# the SCANNED file's own directory — it does not walk parents — so the config
# lives next to this workflow and is passed explicitly via `--config` below.
#
# Third-party actions are pinned to a commit SHA (we're a supply-chain tool —
# practice what we preach).
Expand Down Expand Up @@ -59,6 +61,7 @@ jobs:
uses: google/osv-scanner-action/osv-scanner-action@f4cfcc01edc9c8b756a9b873b7a623ca674da51e # v2.3.8
with:
scan-args: |-
--config=.github/osv-scanner.toml
--lockfile=bun.lock
--lockfile=Cargo.lock
# Only the schedule run notifies — nothing on main touched the lockfile,
Expand Down
Loading