Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
3bdaf30
Make the guard against disabling failproofai the one policy nobody ca…
chhhee10 Aug 20, 2026
d0164ac
Separate what a policy IS from what it does
chhhee10 Aug 20, 2026
7a9718c
Give policies a delivery path that is not the npm package
chhhee10 Aug 20, 2026
f92a6d7
Let a policy that is not a builtin declare params that actually arrive
chhhee10 Aug 20, 2026
f3ea30e
Put a pack on a machine, and refuse the ones that could not work
chhhee10 Aug 21, 2026
c2482de
Take the pack source a person actually has
chhhee10 Aug 21, 2026
074fe6f
Prove a pack stops a tool call, not merely that it loads
chhhee10 Aug 21, 2026
6e97c69
Stop four handler tests reading whoever-runs-them's real pack directory
chhhee10 Aug 21, 2026
43f505f
Stop a pack's verdict being recorded as nobody's and its crash as ours
chhhee10 Aug 21, 2026
ff357a1
Ship the builtins as a pack, and compare it against the compiled ones
chhhee10 Aug 21, 2026
bd924c6
Stop `policies --install` deleting the policies it cannot name
chhhee10 Aug 21, 2026
641f0e6
File a pack's decision under the pack that made it
chhhee10 Aug 21, 2026
9859e78
Install what a pack recommends, not everything it contains
chhhee10 Aug 21, 2026
c40f76f
Install the builtins pack from the package, with no network
chhhee10 Aug 21, 2026
f844335
Refuse when a pack this machine enforces is not running
chhhee10 Aug 21, 2026
53a680d
Deny for the guards a pack declared but could not import
chhhee10 Aug 23, 2026
ebe23e0
Print every command in one visual language, not six
chhhee10 Aug 23, 2026
9e391a9
List everything that enforces, and stop cutting what must be copied
chhhee10 Aug 23, 2026
b8ea9a9
Make a pack something a stranger can publish, and a machine can trust
chhhee10 Aug 23, 2026
e6589a4
Get a policy pack from the dashboard, and stop it claiming what does …
chhhee10 Aug 23, 2026
64fe57b
Say 39 built-in policies, because there are 39
chhhee10 Aug 23, 2026
b8d6926
Let the audit run the policies from the pack, not from the build
chhhee10 Aug 24, 2026
3a7bf10
Credit the policy that actually decided, and see it when it changes
chhhee10 Aug 24, 2026
a89b6b1
Say what the audit is doing, not a count that was never right
chhhee10 Aug 24, 2026
55863c2
Stop warning on every tool call about a state three screens already show
chhhee10 Aug 24, 2026
7bd1f19
Name the policies something a person will type
chhhee10 Aug 24, 2026
69de557
Let people read a pack before they trust it
chhhee10 Aug 24, 2026
a25ef98
Give the dashboard what the terminal just learned
chhhee10 Aug 24, 2026
368c025
Enforce from packs, and stop registering the builtins
chhhee10 Aug 24, 2026
77be767
Take the builtins off every screen, and out of the pack's name
chhhee10 Aug 24, 2026
496cadc
Make the tests fail the way CI does, then stop them failing
chhhee10 Aug 24, 2026
f6fbfed
Close six ways enforcement could switch off without anyone noticing
chhhee10 Aug 24, 2026
b4ae049
Make it one noun, one screen, and one command to publish
chhhee10 Aug 24, 2026
a888bbd
Stop a long policy name from eating its own description
chhhee10 Aug 24, 2026
8280960
Cover the unified surface, and refuse before looking at what is insta…
chhhee10 Aug 24, 2026
f501ba7
Make the new tests survive a machine that has not run the build
chhhee10 Aug 24, 2026
c861207
WIP: stop shipping policies in the package — core is fetched
chhhee10 Aug 24, 2026
9992cfa
Finish the fetch migration, and stop telling users to run the wrong c…
chhhee10 Aug 25, 2026
d9fbc9e
Open 1.0.2-beta.1, because beta.0 is already on the registry
chhhee10 Aug 25, 2026
b175653
Choose the policies at install time, and ask setup one question at a …
chhhee10 Aug 25, 2026
72f73aa
Draw each prompt frame once, and let the wrapper wrap
chhhee10 Aug 25, 2026
ac7bc2c
Open 1.0.2-beta.2, because beta.1 shipped from the commit before this…
chhhee10 Aug 25, 2026
e6b3988
Take "none" for an answer when the picker is emptied
chhhee10 Aug 25, 2026
a4a9f47
Stop the audit cache turning over on a change it cannot see
chhhee10 Aug 25, 2026
bfd9147
Make publishing two commands, and let a pack pick its agents
chhhee10 Aug 25, 2026
4724ba8
Make the one command the docs headline actually run, and stop warning…
internal-cpu Aug 25, 2026
59e6b39
Ask the one question publish cannot answer, instead of requiring the …
internal-cpu Aug 25, 2026
6e73381
Read opencode's database instead of launching a process to ask it
internal-cpu Aug 25, 2026
5049abc
Resume a transcript that grew, instead of reading it from byte zero a…
internal-cpu Aug 25, 2026
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
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,11 @@ sdk/python/.pytest_cache/

# blog drafts (local, not for commit yet)
/blog/

# Generated by `bun run build:pack` — the builtins emitted as a policy pack.
policy-pack/

# `failproofai publish` writes its three release assets here by default.
# They are uploaded, not source — and a publisher running the command inside
# their own repo should not find them staged.
dist-pack/
110 changes: 110 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ resolver = "3"
members = ["crates/*"]

[workspace.package]
version = "1.0.2-beta.0"
version = "1.0.2-beta.5"
edition = "2024"
license-file = "LICENSE"
repository = "https://github.com/FailproofAI/failproofai"
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
Wherever your agents run, we see it — and we can say no. Failproof hooks 12 agent
harnesses — coding CLIs like Claude Code and Codex, chat gateways like Hermes,
self-hosted assistants like OpenClaw — capturing every run and blocking dangerous
tool calls before they execute. 40 built-in policies. Zero latency. Runs locally.
tool calls before they execute. 39 built-in policies. Zero latency. Runs locally.

</div>

Expand Down Expand Up @@ -140,7 +140,7 @@ failproofai policies --install # or just run `failproofai` and accept the firs
failproofai
```

40 built-in policies activate immediately. Dashboard at `localhost:8020`. Disable the first-run prompt with `FAILPROOFAI_NO_FIRST_RUN=1`.
39 built-in policies activate immediately. Dashboard at `localhost:8020`. Disable the first-run prompt with `FAILPROOFAI_NO_FIRST_RUN=1`.

---

Expand All @@ -160,7 +160,7 @@ failproofai
The first five apply to any agent that can call a tool. The last three are the
developer favourites — coding CLIs are the harness class we cover deepest.

→ [All 40 built-in policies](https://docs.befailproof.ai/policies/builtin)
→ [All 39 built-in policies](https://docs.befailproof.ai/policies/builtin)

---

Expand Down Expand Up @@ -242,7 +242,7 @@ own cluster is available on the Enterprise plan.

| Enforce | |
|---|---|
| [Built-in policies](https://docs.befailproof.ai/policies/builtin) | All 40 policies with parameters |
| [Built-in policies](https://docs.befailproof.ai/policies/builtin) | All 39 policies with parameters |
| [Custom policies](https://docs.befailproof.ai/policies/custom) | Write your own |
| [Configuration](https://docs.befailproof.ai/policies/local-configuration) | Config scopes and merge rules |

Expand Down
128 changes: 128 additions & 0 deletions __tests__/audit/engine-version-packs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// @vitest-environment node
/**
* The audit cache key, and the property that decides whether anybody eats a cold
* rescan.
*
* `engineVersion` keys on-disk audit cache entries, and it hashes the builtin
* policy bodies and NOTHING else. It used to fold in installed pack identities
* too, on the reasoning that packs change what a machine would have caught —
* true of enforcement, and never true of this replay: `initReplay` registers
* `BUILTIN_POLICIES` and never reads the installed packs, so a pack cannot move
* an audit result. Keying on one meant every install or removal cold-rescanned
* the whole history (~104s, per the note on CACHE_TTL_MS) to reproduce answers
* it already had — survivable while packs were rare, and not once policies ARE
* packs.
*
* The pre-pack formula is still the reference, and still has to match exactly:
* that is what makes this change free for a machine with no packs, and one
* rescan for a machine with one.
*/
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { createHash } from "node:crypto";
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { BUILTIN_POLICIES } from "@/src/hooks/builtin-policies";

/** Exactly the pre-pack formula, reproduced here so the test is independent of
* the implementation it checks. */
function prePackEngineVersion(): string {
const blob = BUILTIN_POLICIES.map((p) => `${p.name}|${p.fn.toString()}`).sort().join("\n");
return createHash("sha1").update(blob).digest("hex").slice(0, 16);
}

const ARTIFACT = "export const hooks = [];\n";
const DIGEST = createHash("sha256").update(ARTIFACT).digest("hex");

let root: string;
let prevEnv: string | undefined;

/** Fresh module each time — engineVersion memoizes per process. */
async function engineVersion(): Promise<string> {
const { getEngineVersionForTest } = await import("@/src/audit/cache");
return getEngineVersionForTest();
}

beforeEach(() => {
root = mkdtempSync(join(tmpdir(), "fpai-ev-packs-"));
mkdirSync(join(root, "artifacts"), { recursive: true });
writeFileSync(join(root, "artifacts", `${DIGEST}.mjs`), ARTIFACT);
prevEnv = process.env.FAILPROOFAI_PACK_DIR;
process.env.FAILPROOFAI_PACK_DIR = root;
vi.resetModules();
});

afterEach(() => {
if (prevEnv === undefined) delete process.env.FAILPROOFAI_PACK_DIR;
else process.env.FAILPROOFAI_PACK_DIR = prevEnv;
rmSync(root, { recursive: true, force: true });
});

import { vi } from "vitest";

function installPack(id: string, version: string, artifact = ARTIFACT): void {
const digest = createHash("sha256").update(artifact).digest("hex");
writeFileSync(join(root, "artifacts", `${digest}.mjs`), artifact);
writeFileSync(
join(root, "installed.json"),
JSON.stringify({
schemaVersion: 1,
packs: [{
id, version,
source: `github:${id}@${version}`,
entry: `artifacts/${digest}.mjs`,
sha256: digest,
policies: [],
}],
}),
);
}

describe("engineVersion with packs", () => {
it("is UNCHANGED from the pre-pack formula when no pack is installed", async () => {
// The upgrade-cost guarantee. If this ever fails, shipping the change cold-
// rescans every existing user's whole transcript history.
expect(await engineVersion()).toBe(prePackEngineVersion());
});

it("does NOT change when a pack is installed", async () => {
// The reported symptom: install core, audit, remove it, and every transcript
// is re-scanned to produce identical results.
const before = await engineVersion();
installPack("acme/finance", "1.2.0");
expect(await engineVersion()).toBe(before);
});

it("does NOT change when the same pack moves to a new version", async () => {
installPack("acme/finance", "1.2.0");
const before = await engineVersion();
installPack("acme/finance", "2.0.0");
expect(await engineVersion()).toBe(before);
});

it("does NOT change when the installed artifact digest changes", async () => {
installPack("acme/finance", "1.2.0");
const before = await engineVersion();
// A different artifact means a different digest, which is what the old key
// folded in most eagerly.
installPack("acme/finance", "1.2.0", "export const hooks = [1];\n");
expect(await engineVersion()).toBe(before);
});

it("is the pre-pack hash whether or not a pack is installed", async () => {
// The two halves of the guarantee in one assertion: a machine with no packs
// keeps the key it already had, and a machine WITH one converges on the same
// key rather than carrying its own.
installPack("acme/finance", "1.2.0");
expect(await engineVersion()).toBe(prePackEngineVersion());
});

it("falls back to the builtin-only hash when the manifest is unreadable", async () => {
// A corrupt manifest must not change the cache key: the packs did not load,
// so the audit that runs is a builtin-only audit and should hit the cache a
// builtin-only audit wrote.
writeFileSync(join(root, "installed.json"), "not json");
vi.resetModules();
expect(await engineVersion()).toBe(prePackEngineVersion());
});
});
Loading