Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6d9da74
feat(hermes): link the native plugin into each profile instead of cop…
chhhee10 Sep 29, 2026
5a34c87
fix(update): migrate Hermes profiles to the linked plugin and fail wh…
chhhee10 Sep 29, 2026
53fc126
docs(hermes): linked plugin, update migration, and the cron gap
chhhee10 Sep 29, 2026
ff95959
collector: read Jev's jevMode "observe" and ship one value
chhhee10 Sep 29, 2026
f7a58b6
jev: rename the "shadow" mode to "observe", reading "shadow" as an alias
chhhee10 Sep 29, 2026
c4ee567
docs: Jev's log-only mode is "observe"
chhhee10 Sep 29, 2026
02ea9f1
fp: show Jev machines per mode in guardrails summary, as observe
chhhee10 Sep 29, 2026
ecb62e6
Merge pre108/t3-hermes-update: update migrates Hermes to the linked p…
chhhee10 Sep 29, 2026
a8f44a6
Merge pre108/t2-observe: Jev shadow mode renamed observe
chhhee10 Sep 29, 2026
ca375f4
feat(jev): ship no Jev checks; ask only what installed packs declare
chhhee10 Sep 29, 2026
24877c0
test(jev): a Jev-configured machine with no Jev pack is the unconfigu…
chhhee10 Sep 29, 2026
75bd5d5
docs(jev): Jev's checks come from FailproofAI/jev-policies, not the p…
chhhee10 Sep 29, 2026
c14f6df
Merge pre108/t1-unbundle-jev: no pack, no Jev checks
chhhee10 Sep 29, 2026
c360f18
fix(packs): a Jev-only pack does not retire the regex migration shim
chhhee10 Sep 29, 2026
9205643
refactor(jev): drop the `shadow` mode alias; observe is the only name
chhhee10 Sep 29, 2026
af9e856
test: mock hasInstalledRegexPacks, and stop testing a compiled-in Jev…
chhhee10 Sep 29, 2026
8aaa98a
fix: address review on #868 — survey shim check, unknown-mode clears,…
chhhee10 Sep 29, 2026
ddee8b8
test(harness): extra paths survive the config.json writes `config` an…
chhhee10 Sep 29, 2026
dda3d0d
fix: write agent configs crash-safely; never rewrite one that does no…
chhhee10 Sep 29, 2026
e3f2abf
Merge origin/main into feat/pre-1.0.8
chhhee10 Sep 29, 2026
3794e46
chore: bump version to 1.0.9-beta.1
chhhee10 Sep 29, 2026
42d23c5
fix(hermes): a plugin listed in plugins.disabled is not enabled (SEC-…
chhhee10 Sep 29, 2026
6e4e2d5
chore: bump version to 1.0.9-beta.2
chhhee10 Sep 29, 2026
af95edb
fix(update): skip the daemon reinstall when it already runs this version
chhhee10 Sep 29, 2026
86232f2
fix: close Hermes review F8–F11 (backup symlink, dangling config link…
chhhee10 Sep 29, 2026
689b585
docs(changelog): cut the 1.0.9 stable section
chhhee10 Sep 29, 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
43 changes: 42 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,51 @@
# Changelog

## 1.0.9 — 2026-09-29

Action needed if you use Jev: its log-only mode is now `observe`, a `jev.json` still set to `shadow` is refused (Jev stays off until `failproofai jev setup` is run again), and Jev's checks now come only from `failproofai policies add FailproofAI/jev-policies`. For Hermes, `failproofai update` moves every profile from the old shell hooks (never run for cron jobs) to the native plugin, and every agent config failproofai edits is written crash-safely with a `.failproofai-backup`. Collects 1.0.9-beta.0 to beta.2 below.

### Fixes

- **A planted link at `<config>.failproofai-backup` could redirect the backup into another file.** The previous version is now copied to an exclusively created temp file and renamed over the backup path, so a symlink there is replaced, never followed — for a project-scoped config in a cloned repository, the file it pointed at is untouched.
- **A config that is a dangling symlink is refused instead of replaced.** Writing used to swap the link for a regular file (silently detaching a dotfiles checkout); it now stops with the link and its missing target named. Files failproofai generates and owns (the OpenCode plugin shim, the Hermes link record) replace a planted link rather than writing through it.
- **A same-named Hermes plugin is only replaced when failproofai can prove it is its own.** A link counts as failproofai's when it matches the ownership record written beside it (`plugins/.failproofai-link`) or points into an npm `failproofai` package (links from the 1.0.9 betas are adopted and recorded); a look-alike directory named `hermes-plugin` with a `name: failproofai` manifest is left alone and `config --status` says another plugin occupies the name.
- **One Hermes profile that cannot be inspected no longer stops `update` for the rest.** Each profile is handled on its own, the redundant second read of `config.yaml` is gone, and a failure is that profile's line in the report. Re-enabling a plugin listed in `plugins.disabled` now reads "plugin re-enabled".
- **`failproofai update` exited 1 on a machine whose daemon was already current.** It reinstalled the service on every run, which needs root: interactively it asked for a password for nothing, and with no TTY (a fleet box, CI) it failed with "root privileges are required". When the service is running and `VERSION` records this CLI's version with its binary on disk, it now says the daemon is already current and asks root for nothing.

### Dependencies

- Routine dependency bumps: `next` and `eslint-config-next` 16.3.6, `posthog-node` 5.54.1, `vitest` 5.0.2, `jsdom` 30.1.1, `lucide-react` 1.48.0, `@types/node` 26.6.2, `@anthropic-ai/sdk` 0.128.0, the Rust and Python dependency groups, and the `actions/setup-node` 7 and `actions/create-github-app-token` 3 workflow actions (#853–#865).

## 1.0.9-beta.2 — 2026-09-29

### Fixes

- **A Hermes plugin listed in `plugins.disabled` was reported healthy and skipped by `update`.** Hermes checks `plugins.disabled` before `plugins.enabled`, so a profile listing `failproofai` in both never loads it, but health, installed detection and `update`'s "already current" check only read `plugins.enabled`. They now require enabled and not disabled: `config --status` says "plugin disabled (listed in plugins.disabled)" and `update` removes the disabling entry.

## 1.0.9-beta.1 — 2026-09-29

### Fixes

- **`jev status` and the dashboard under-counted a machine with only `FailproofAI/jev-policies`.** Their coverage survey still retired the `enabledPolicies` shim when any pack was installed, while hooks keep enforcing those builtins until a pack with regex policies arrives. The survey now uses the same `hasInstalledRegexPacks` check, so it counts what is really enforced and reviewable.
- **Old Jev activity rows no longer inflate "cleared".** A row whose mode this build does not know (written as `shadow` before the rename) lost only its mode, so `jev status` and the dashboard counted its would-have clears as enforced clears. Such a row now drops its clears too.
- The Hermes plugin's package path is found by walking up to the directory that holds `hermes-plugin/plugin.yaml` when `FAILPROOFAI_PACKAGE_ROOT` is unset, instead of a fixed three parents that overshoot from the bundled `dist/cli.mjs`. On Windows, replacing an existing plugin junction falls back to the move-aside swap when a direct rename fails.
- **Agent configs are written crash-safely and never rewritten from a file that does not parse.** Every integration wrote the user's agent config (`~/.hermes/config.yaml`, `~/.claude/settings.json`, `~/.openclaw/openclaw.json`, …) in place, so an interruption mid-write left a truncated config and an agent that would not start; and an existing Hermes/YAML config that did not parse was read as empty, so the next install would have replaced every other setting in it. Writes now go to a temp file in the same directory, are fsynced, keep the previous version as `<name>.failproofai-backup`, and are atomically renamed into place, preserving the file's permissions and writing through a symlinked config. A config that exists but cannot be read or parsed is refused with the file and the reason, left byte-for-byte, and reported by `config --status`. The collector also stops shipping clears from rows whose Jev mode it does not know.

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

### Features

- Jev's log-only mode is now **`observe`**, the word already used for a policy rollout that is evaluated but not enforced; Jev's modes are `off`, `observe` and `enforce`. `failproofai jev setup --mode observe`, the dashboard's Jev settings, `jev status`, `config --token` (which now turns Jev on in observe mode) and the docs all say observe; `jev.json` takes `mode: "observe"`, hook-activity rows carry `jevMode: "observe"`, `verdicts.jsonl` carries `applied: "observe"`, and `jev status --json` stats report `observeClearsByPolicy` and `modes.observe`. The collector ships `jev_mode: "observe"` to FailproofAI Cloud. The dashboard's Jev pill reads "jev observe".

### Fixes

- **Hermes cron jobs ran unchecked after an upgrade.** Legacy Hermes shell hooks (≤1.0.5) are never run for cron jobs — each cron fire builds its own hook scope that only discovered plugins join — and `failproofai update` never touched Hermes, so upgraded machines stayed on them silently. `update` now moves every Hermes profile that already uses FailproofAI (shell hooks or a copied plugin) to the native plugin, prints a per-profile report, and leaves profiles without FailproofAI alone. When the running daemon cannot serve the plugin (no `policyEvaluation`, e.g. a sudo system daemon `update` could not replace) the shell hooks are kept and `update` exits 1 pointing at `failproofai config`; it also exits 1 whenever the daemon swap or a layout migration failed. The plugin is now **linked** into each profile (`plugins/failproofai` → the package's `hermes-plugin/`, a marked copy where symlinks are unavailable), so npm upgrades apply with no reinstall; uninstall removes only the link. `failproofai config --status` reports a profile still on shell hooks as unhealthy: "Hermes cron jobs are not checked".
- **The npm package no longer ships Jev's checks; a machine asks them only after `failproofai policies add FailproofAI/jev-policies`.** The sixteen semantic checks were compiled in and used whenever no installed pack declared any, so configuring Jev — BYOK, or `failproofai config --token` with a Cloud machine key — started asking them without anyone opting in. Now the checks, and the names `reviewedBy` may use, come only from installed packs. With none declaring checks Jev is inert whether or not it is configured: no request is sent (not even the injection or task probes), no prompt is recorded for it, and every `reviewable` policy resolves `hard`, so nothing is cleared and hooks answer exactly as on a machine without Jev. An unreadable pack list asks nothing too, rather than falling back to a built-in set. `failproofai jev status` (and its `--json`, as `reviewablePolicies.jevChecks`), the dashboard's Jev settings and `config --token`'s output say "Jev has no checks installed" and name the command. `publish` still reserves the sixteen names and judges a regex-only pack's `reviewedBy` against them.
- **Adding `FailproofAI/jev-policies` could switch off a machine's regex policies.** A machine that enforced built-in policies from `enabledPolicies` (upgraded, no core pack) stopped enforcing them as soon as ANY pack was installed, and `failproofai policies add <name>` then tried to enable the name on the installed packs instead of fetching `FailproofAI/policies`. Now that Jev's checks arrive only as the Jev-only `FailproofAI/jev-policies` pack, both steps count only packs that carry regex policies (`hasInstalledRegexPacks`).

### Docs

- Split Jev documentation into session evaluations under Find failures, live policy review under Prevent failures, and provider/configuration detail under Reference. Add a Use Jev page after Core concepts in Start with eval and policy setup tabs, plus a short quickstart link, dashboard screenshots, and CLI steps. Move sentiment analysis into Find failures and show its Jev-scored dashboard flow. Clarify shadow-mode verification and the Cloud machine key's `jev:evaluate` permission.
- Split Jev documentation into session evaluations under Find failures, live policy review under Prevent failures, and provider/configuration detail under Reference. Add a Use Jev page after Core concepts in Start with eval and policy setup tabs, plus a short quickstart link, dashboard screenshots, and CLI steps. Move sentiment analysis into Find failures and show its Jev-scored dashboard flow. Clarify observe-mode verification and the Cloud machine key's `jev:evaluate` permission.

### Dependencies

Expand Down
15 changes: 10 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,10 +323,15 @@ we run in-repo. Hermes is a **dual-pillar** integration: an **audit** adapter

Hermes enforcement uses the shipped **native Python plugin** under each profile's
`plugins/failproofai/` directory. The profile's YAML config enables it through
`plugins.enabled: [failproofai]`. `integrations.ts` copies the plugin atomically,
marks the directory as FailproofAI-managed, refuses to overwrite an unmanaged
directory with the same name, and uses the `yaml` package's comment-preserving
`Document` API for config changes.
`plugins.enabled: [failproofai]`. `integrations.ts` symlinks that directory to the
package's `hermes-plugin/` (Hermes' scan follows symlinks, so npm upgrades apply
with no reinstall — the OpenClaw `plugins.load.paths` model), falls back to an
atomic copy marked `.failproofai-managed` where a link cannot be created, replaces
only a marked copy or a link into a FailproofAI `hermes-plugin/`, refuses anything
else with the same name, and uses the `yaml` package's comment-preserving
`Document` API for config changes. `failproofai update` migrates profiles already
using FailproofAI (legacy shell hooks or a copy) to the link, gated on the daemon
answering `policyEvaluation`; legacy shell hooks never run for Hermes cron jobs.

Settings file paths:

Expand All @@ -335,7 +340,7 @@ Settings file paths:
| user | `~/.hermes/config.yaml` |

Hermes is **user-scope only** — there is no project config, so `getSettingsPath`
ignores scope/cwd. Every default and named profile receives its own plugin copy and
ignores scope/cwd. Every default and named profile receives its own plugin link and
enablement entry. Installed-state detection requires both the complete managed plugin
directory and the config entry; a missing file, disabled plugin, newly-created profile,
or leftover legacy shell hook is reported as unhealthy.
Expand Down
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.9-beta.0"
version = "1.0.9-beta.3"
edition = "2024"
license-file = "LICENSE"
repository = "https://github.com/FailproofAI/failproofai"
8 changes: 4 additions & 4 deletions __tests__/actions/jev-mode-action.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @vitest-environment node
/**
* The /settings Jev panel's FailproofAI Cloud controls, against the real
* loader: `setJevModeAction` (the on/off switch and shadow/enforce) and the
* loader: `setJevModeAction` (the on/off switch and observe/enforce) and the
* Cloud connection row in `getJevSettingsAction`.
*
* 1. **It rewrites `mode` and nothing else** — every other byte-level field
Expand Down Expand Up @@ -33,7 +33,7 @@ const INGEST_KEY = ["fp", "ingest", "0badc0ffee123456"].join("-");
const POLICY_KEY = ["fp", "policy", "feedfacecafe7890"].join("-");
const BYOK_KEY = ["ts", "byok", "0123456789abcdef"].join("-");
const ORIGIN = "https://app.befailproof.ai";
const CLOUD_FILE = { provider: "failproofai", baseUrl: `${ORIGIN}/enforcement/v1/jev`, mode: "shadow" };
const CLOUD_FILE = { provider: "failproofai", baseUrl: `${ORIGIN}/enforcement/v1/jev`, mode: "observe" };

let home: string;
let prevHome: string | undefined;
Expand Down Expand Up @@ -93,7 +93,7 @@ describe("setJevModeAction", () => {
connect();
// A field this build does not know, and one it does not show: both must survive.
seed({ ...CLOUD_FILE, timeoutMs: 2500, fromANewerBuild: { x: 1 } });
for (const mode of ["enforce", "off", "shadow"] as const) {
for (const mode of ["enforce", "off", "observe"] as const) {
const res = await setJevModeAction(mode);
expect(res.ok).toBe(true);
expect(onDisk()).toEqual({ ...CLOUD_FILE, timeoutMs: 2500, fromANewerBuild: { x: 1 }, mode });
Expand Down Expand Up @@ -122,7 +122,7 @@ describe("setJevModeAction", () => {
expect(res.ok).toBe(true);
expect(onDisk()).toEqual({ provider: "typesafe", apiKey: BYOK_KEY, mode: "off" });
secretFree(res);
expect((await setJevModeAction("shadow")).ok).toBe(true);
expect((await setJevModeAction("observe")).ok).toBe(true);
expect(loadJevConfig()?.apiKey).toBe(BYOK_KEY);
});

Expand Down
41 changes: 34 additions & 7 deletions __tests__/actions/jev-reviewability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import { tmpdir } from "node:os";
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 { JEV_POLICIES_ADD_COMMAND, NO_JEV_CHECKS_PROBLEM, RETAKE_PACK_COMMAND } from "../../src/hooks/policy-reviewability";
import { JEV_PACK_SEMANTIC_ENTRIES } from "../fixtures/jev-policies";

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

function installPack(policies: Array<Record<string, unknown>>): void {
/** The core pack, beside FailproofAI/jev-policies unless `withJev` is false. */
function installPack(policies: Array<Record<string, unknown>>, withJev = true): 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 });
Expand All @@ -95,6 +97,19 @@ function installPack(policies: Array<Record<string, unknown>>): void {
sha256: digest,
policies,
},
...(withJev
? [
{
id: "FailproofAI/jev-policies",
version: "0.2.0",
source: "github:FailproofAI/jev-policies@v0.2.0",
entry: `artifacts/${digest}.mjs`,
sha256: digest,
policies: [],
semantic: JEV_PACK_SEMANTIC_ENTRIES,
},
]
: []),
],
}),
);
Expand Down Expand Up @@ -134,21 +149,31 @@ 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, with the core pack and its Jev checks", 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("says Jev has no checks installed, and the command, when no pack declares any", async () => {
writeConfig({ enabledPolicies: POLICY_CATALOG.map((p) => p.name) });
turnJevOn();

const view = await getJevSettingsAction();
expect(view.reviewable).toMatchObject({ enabled: POLICY_CATALOG.length, reviewable: 0, problem: NO_JEV_CHECKS_PROBLEM });
expect(view.reviewable?.problem).toContain(JEV_POLICIES_ADD_COMMAND);
});

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 +190,9 @@ describe("getJevSettingsAction — what Jev may clear", () => {
turnJevOn();

const view = await getJevSettingsAction();
expect(view.reviewable).toMatchObject({ enabled: POLICY_CATALOG.length, reviewable: 15, problem: null });
// All of the launch project's builtins are counted; none is reviewable,
// because no pack gives Jev the checks they name.
expect(view.reviewable).toMatchObject({ enabled: POLICY_CATALOG.length, reviewable: 0, problem: NO_JEV_CHECKS_PROBLEM });
} finally {
rmSync(launch, { recursive: true, force: true });
}
Expand Down
Loading
Loading