Skip to content
Open
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 1.0.9-beta.0 — 2026-09-27

### Changed

- **Jev's checks now come only from installed packs.** This build no longer asks the 16 built-in semantic checks (`destructive-deletion`, `credential-exfiltration`, …) on its own: they ship in `FailproofAI/jev-policies`, which carries all 16, and are asked only where that pack is installed (`failproofai policies add FailproofAI/jev-policies`). With Jev configured and no pack that supplies a check, Jev is idle — hooks behave exactly as with no Jev config: no request to the provider, no added latency, no Jev deny and no clear, no intent capture, and no per-policy authority warnings. The built-in policies marked reviewable keep their `authority`/`reviewedBy`, and resolve hard until a pack supplies the checks they name. `config` (connect / `--token`), `jev setup` and `jev status` print one line naming the pack when Jev is on and idle, `jev status` titles it `idle (no Jev checks installed)`, `jev status --json` carries `jevChecks: {installed, names, idle, fix}`, and the dashboard's Jev panel shows the same line; nothing is installed for you. `jev test` is unchanged (it asks its own probe). The 16 names stay reserved to FailproofAI's packs, and a FailproofAI Jev verdict is now filed under the pack that supplied it (`packId: FailproofAI/jev-policies`).
- **A pack of Jev checks alone no longer switches off the built-in regex policies.** Installing any pack used to stop `enabledPolicies` from registering, so following the hint above (`policies add FailproofAI/jev-policies`, which carries no regex policies) would have taken `block-rm-rf`, `block-sudo` and the rest away. Only a pack that carries regex policies now replaces them; with jev-policies alone the built-ins keep enforcing, the 15 marked reviewable resolve reviewable, and `jev status`, `policies`, `policies --install` and the audit's closing hint all apply the same rule.
- **The Jev question budget no longer reserves room for built-in checks at load time.** A FailproofAI pack is held to the whole request (27,591 characters) and spends it first; any other pack gets what the installed FailproofAI packs actually leave, and the whole request when none is installed. `failproofai publish` still holds a pack from outside FailproofAI to what `FailproofAI/jev-policies` leaves (now measured as that pack's manifest compiles, 18,478 characters, so 9,113 are left), so a pack that publishes always fits beside it.

## 1.0.8-beta.0 — 2026-09-26

### Added
Expand Down
55 changes: 34 additions & 21 deletions __tests__/actions/jev-reviewability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { join } from "node:path";
import { getJevSettingsAction } from "../../app/actions/get-jev-config";
import { POLICY_CATALOG } from "../../src/hooks/policy-catalog";
import { RETAKE_PACK_COMMAND } from "../../src/hooks/policy-reviewability";
import { NO_JEV_CHECKS_HINT } from "../../src/hooks/effective-reviewers";
import { installJevPoliciesPack } from "../fixtures/jev-policies-pack";

/** A token no provider issued. Nothing here should ever send it anywhere. */
const TOKEN = "jevtoken-0123456789-3f2a";
Expand Down Expand Up @@ -77,27 +79,22 @@ function turnJevOn(): void {
chmodSync(path, 0o600);
}

/** The core pack, with `FailproofAI/jev-policies` beside it — the only source of the checks it names. */
function installPack(policies: Array<Record<string, unknown>>): void {
const artifact = "// a pack artifact this test never executes\n";
const digest = createHash("sha256").update(artifact).digest("hex");
mkdirSync(join(packRoot, "artifacts"), { recursive: true });
writeFileSync(join(packRoot, "artifacts", `${digest}.mjs`), artifact);
writeFileSync(
join(packRoot, "installed.json"),
JSON.stringify({
schemaVersion: 1,
packs: [
{
id: "FailproofAI/policies",
version: "0.9.0",
source: "github:FailproofAI/policies@v0.9.0",
entry: `artifacts/${digest}.mjs`,
sha256: digest,
policies,
},
],
}),
);
installJevPoliciesPack(packRoot, [
{
id: "FailproofAI/policies",
version: "0.9.0",
source: "github:FailproofAI/policies@v0.9.0",
entry: `artifacts/${digest}.mjs`,
sha256: digest,
policies,
},
]);
}

/** A pre-release pack's entries: the policies, without the two authority fields. */
Expand Down Expand Up @@ -134,21 +131,35 @@ describe("getJevSettingsAction — what Jev may clear", () => {
expect(JSON.stringify(view)).not.toContain(TOKEN);
});

it("reports the seven reviewable builtins and no problem", async () => {
writeConfig({ enabledPolicies: POLICY_CATALOG.map((p) => p.name) });
it("reports the fifteen reviewable policies and no problem, from a pack built by this release", async () => {
writeConfig({ enabledPolicies: [] });
installPack(PACKABLE as unknown as Array<Record<string, unknown>>);
turnJevOn();

const view = await getJevSettingsAction();
expect(view.reviewable).toEqual({
enabled: POLICY_CATALOG.length,
enabled: PACKABLE.length + 1,
reviewable: 15,
summary:
`15 of ${POLICY_CATALOG.length} enabled policies are reviewable: ` +
`15 of ${PACKABLE.length + 1} enabled policies are reviewable: ` +
"Jev may clear a deny or an instruction from those, and from no others.",
problem: null,
});
});

it("counts this build's builtins hard, and names jev-policies, while no pack supplies a check", async () => {
writeConfig({ enabledPolicies: POLICY_CATALOG.map((p) => p.name) });
turnJevOn();

const view = await getJevSettingsAction();
expect(view.reviewable).toEqual({
enabled: POLICY_CATALOG.length,
reviewable: 0,
summary: `0 of ${POLICY_CATALOG.length} enabled policies are reviewable.`,
problem: NO_JEV_CHECKS_HINT,
});
});

it("counts the launch directory's project config, not the server's own cwd", async () => {
// The standalone server chdirs into the package directory, so the project a
// person launched the dashboard from arrives only as FAILPROOFAI_LAUNCH_CWD —
Expand All @@ -165,7 +176,9 @@ describe("getJevSettingsAction — what Jev may clear", () => {
turnJevOn();

const view = await getJevSettingsAction();
expect(view.reviewable).toMatchObject({ enabled: POLICY_CATALOG.length, reviewable: 15, problem: null });
// Every builtin the launch directory enables, and none reviewable: no
// pack supplies the checks they name.
expect(view.reviewable).toMatchObject({ enabled: POLICY_CATALOG.length, reviewable: 0, problem: NO_JEV_CHECKS_HINT });
} finally {
rmSync(launch, { recursive: true, force: true });
}
Expand Down
31 changes: 31 additions & 0 deletions __tests__/audit/audit-cli-telemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const h = vi.hoisted(() => ({
writeDashboardCache: vi.fn(() => true),
openWhenReady: vi.fn(),
launch: vi.fn(),
enabledPolicies: [] as string[],
}));

vi.mock("../../src/hooks/hook-telemetry", () => ({ trackHookEvent: h.trackHookEvent }));
Expand All @@ -34,6 +35,10 @@ vi.mock("../../src/audit/dashboard-cache", () => ({ writeDashboardCache: h.write
vi.mock("../../src/audit/open-browser", () => ({ openWhenReady: h.openWhenReady }));
vi.mock("../../scripts/launch", () => ({ launch: h.launch }));
vi.mock("../../lib/telemetry-id", () => ({ getInstanceId: () => "test-instance" }));
vi.mock("../../src/hooks/hooks-config", async (orig) => ({
...(await orig<typeof import("../../src/hooks/hooks-config")>()),
readMergedHooksConfig: () => ({ enabledPolicies: h.enabledPolicies }),
}));

import { runAuditCli, runPostSetupAudit } from "../../src/audit/cli";

Expand Down Expand Up @@ -235,3 +240,29 @@ describe("post-setup (onboarding) audit telemetry", () => {
expect(exitInfo).toBeNull();
});
});

// The closing hint after the onboarding audit says nothing is enforced. That is
// only true when no regex pack is installed AND `enabledPolicies` is empty: with
// no regex pack, the handler registers the enabledPolicies built-ins.
describe("post-setup audit enforcement hint", () => {
const out = () =>
(process.stdout.write as unknown as { mock: { calls: unknown[][] } }).mock.calls.map((c) => String(c[0])).join("");

afterEach(() => {
h.enabledPolicies = [];
});

it("says nothing is enforced when no regex pack and no built-in policy is on", async () => {
h.enabledPolicies = [];
h.runAudit.mockResolvedValue(result({ eventsScanned: 100, totals: { hits: 2, projectsWithHits: 1 } }));
await runPostSetupAudit();
expect(out()).toContain("none of this is being enforced yet");
});

it("stays quiet when enabledPolicies built-ins are enforcing without a regex pack", async () => {
h.enabledPolicies = ["block-rm-rf", "block-sudo"];
h.runAudit.mockResolvedValue(result({ eventsScanned: 100, totals: { hits: 2, projectsWithHits: 1 } }));
await runPostSetupAudit();
expect(out()).not.toContain("none of this is being enforced yet");
});
});
130 changes: 130 additions & 0 deletions __tests__/fixtures/jev-policies-pack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* A stand-in for the published `FailproofAI/jev-policies` pack: FailproofAI's
* sixteen Jev checks, as a pack's manifest declares them.
*
* This build asks no Jev check of its own — the checks come only from
* installed packs — so every test that exercises Jev's decisions on the
* sixteen has to install them first, exactly as a user runs
* `policies add FailproofAI/jev-policies`. Built from `SEMANTIC_POLICIES`, the
* definitions that pack is written from, and through the loader's own parser,
* so what a test installs is what a machine reading the real manifest holds.
*
* Two ways in:
*
* - {@link installJevPoliciesPack} writes a real `installed.json` and a
* digest-pinned artifact into a pack directory, for tests that go through
* the real reader and have no builtin regex policies to keep.
* - {@link withJevPoliciesPack} adds the pack to a `readInstalledPacks()`
* result, for a `vi.mock` of `pack-manifest`. It leaves `installed.json`
* alone, so the handler's migration shim keeps registering this build's
* builtin regex policies beside it — the regex half those tests are about.
*/
import { createHash } from "node:crypto";
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
// Types only from `pack-manifest`: tests `vi.mock` that module with a factory
// that imports THIS file, and a runtime import back into it would deadlock.
import type { ResolvedPack, SemanticManifestEntry } from "../../src/hooks/pack-manifest";
import { SEMANTIC_POLICIES } from "../../src/hooks/semantic/policies";

export const JEV_POLICIES_ID = "FailproofAI/jev-policies";
export const JEV_POLICIES_VERSION = "0.2.0";
export const JEV_POLICIES_SOURCE = `github:${JEV_POLICIES_ID}@v${JEV_POLICIES_VERSION}`;

/**
* The precondition NAME the pack gives each builtin predicate. A manifest
* cannot carry a function, so the two gated checks name theirs; the bodies in
* `preconditions.ts` are the same tests `policies.ts` writes inline, which
* `pack-preconditions.test.ts` pins.
*/
const PRECONDITION_NAMES: Record<string, string> = {
"commit-on-protected-branch": "protected_branch",
"read-outside-workspace": "paths_outside_project",
};

/**
* The sixteen as `FailproofAI/jev-policies` declares them, already in the shape
* the loader's parser returns: `jev-checks-pack-only.test.ts` pins that each
* survives `parsePackSemanticPolicy` unchanged.
*/
export const JEV_POLICIES_SEMANTIC: SemanticManifestEntry[] = SEMANTIC_POLICIES.map((p) => {
const precondition = PRECONDITION_NAMES[p.name];
if (p.precondition && !precondition) throw new Error(`no precondition name for ${p.name}`);
return {
name: p.name,
title: p.title,
appliesTo: [...p.appliesTo],
mode: p.mode,
userCanOverride: p.userCanOverride,
probes: p.probes.map((probe) => ({ ...probe })),
// The parser keys an exemption `exempt` whatever the manifest wrote.
...(p.exempt ? { exempt: { ...p.exempt, id: "exempt" } } : {}),
...(precondition ? { precondition } : {}),
guidance: p.guidance,
} as SemanticManifestEntry;
});

/** An entry that registers nothing: the pack is Jev checks only. */
const ARTIFACT = "export {};\n";
const DIGEST = createHash("sha256").update(ARTIFACT).digest("hex");

/** The manifest record `policies add` writes for the pack. */
export function jevPoliciesRecord(): Record<string, unknown> {
return {
id: JEV_POLICIES_ID,
version: JEV_POLICIES_VERSION,
source: JEV_POLICIES_SOURCE,
entry: `artifacts/${DIGEST}.mjs`,
sha256: DIGEST,
effect: "enforce",
policies: [],
semantic: JEV_POLICIES_SEMANTIC,
};
}

let scratch: string | undefined;

/** Write the artifact into `packDir` (a scratch directory when none), returning its absolute path. */
function writeArtifact(packDir?: string): string {
if (!packDir) packDir = scratch ??= mkdtempSync(join(tmpdir(), "fpai-jev-policies-"));
mkdirSync(join(packDir, "artifacts"), { recursive: true });
const path = join(packDir, "artifacts", `${DIGEST}.mjs`);
writeFileSync(path, ARTIFACT);
return path;
}

/**
* Install the pack for real: `installed.json` (with any `others` records first)
* and its artifact. Note that an installed pack switches the handler's legacy
* builtin shim off, as it does on a real machine.
*/
export function installJevPoliciesPack(packDir: string, others: Record<string, unknown>[] = []): void {
writeArtifact(packDir);
writeFileSync(join(packDir, "installed.json"), JSON.stringify({ schemaVersion: 1, packs: [...others, jevPoliciesRecord()] }));
}

/** The pack as `readInstalledPacks` resolves it, with its artifact written into `packDir`. */
export function jevPoliciesResolvedPack(packDir?: string): ResolvedPack {
return {
id: JEV_POLICIES_ID,
version: JEV_POLICIES_VERSION,
source: JEV_POLICIES_SOURCE,
path: writeArtifact(packDir),
sha256: DIGEST,
effect: "enforce",
policies: [],
semantic: JEV_POLICIES_SEMANTIC,
enabled: null,
clis: null,
};
}

/**
* A `readInstalledPacks()` result with the pack added, for a `vi.mock` of
* `pack-manifest`. `packDir` is where its (empty) artifact is written.
*/
export function withJevPoliciesPack<T extends { packs: ResolvedPack[] }>(result: T, packDir?: string): T {
if (result.packs.some((p) => p.id === JEV_POLICIES_ID)) return result;
return { ...result, packs: [...result.packs, jevPoliciesResolvedPack(packDir)] };
}
7 changes: 6 additions & 1 deletion __tests__/hooks/cloud-connect-jev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import { resolve } from "node:path";
import { connectToCloud, configuredPaths, describeOutcome } from "../../src/hooks/cloud-connection";
import { runConnectCommand, runDisconnectCommand } from "../../src/hooks/cloud-enrollment-cli";
import { readCredentials, writeJevCloudCredential } from "../../src/hooks/fp-config";
import { credentialsFile, jevConfigFile } from "../../src/hooks/fp-home";
import { credentialsFile, jevConfigFile, packsDir } from "../../src/hooks/fp-home";
import { installJevPoliciesPack } from "../fixtures/jev-policies-pack";
import { inspectJevConfig, loadJevConfig, validateJevConfig } from "../../src/hooks/semantic/jev-config";
import { writeCloudJevConfigIfAbsent } from "../../src/hooks/jev-cloud-connection";
import { introspectKey, type IntrospectResult } from "../../src/hooks/cloud-introspect";
Expand All @@ -46,6 +47,10 @@ beforeEach(() => {
home = mkdtempSync(resolve(tmpdir(), "fpai-connect-jev-"));
process.env.FAILPROOFAI_HOME = home;
chmodSync(home, 0o700);
// FailproofAI's Jev checks, which come only from this pack: without it Jev
// is idle and every "on" line here gains the jev-policies hint (pinned in
// `jev-checks-pack-only.test.ts`).
installJevPoliciesPack(packsDir());
});

afterEach(() => {
Expand Down
2 changes: 2 additions & 0 deletions __tests__/hooks/fail-closed-force-decision.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ vi.mock("../../src/hooks/pack-manifest", () => ({
// The handler asks this per event to decide whether the migration shim
// still applies. Mirrors the mocked readInstalledPacks above.
hasInstalledPacks: vi.fn(() => false),
// The shim's own test: only a pack carrying regex policies replaces the builtins.
hasRegexPacks: vi.fn(() => false),
}));

import { evaluateHookEvent } from "../../src/hooks/handler";
Expand Down
2 changes: 2 additions & 0 deletions __tests__/hooks/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ vi.mock("../../src/hooks/pack-manifest", () => ({
// The handler asks this on every event to decide whether the migration shim
// still applies. Mocked for the same reason as the line above.
hasInstalledPacks: vi.fn(() => false),
// The shim's own test: only a pack carrying regex policies replaces the builtins.
hasRegexPacks: vi.fn(() => false),
}));

describe("hooks/handler", () => {
Expand Down
Loading
Loading