From 7d8e490a1f46f768490b65fae4e09beef1c5e9ea Mon Sep 17 00:00:00 2001 From: chhhee10 Date: Mon, 2 Nov 2026 12:04:29 +0530 Subject: [PATCH 1/2] fix(hooks): enforce path policies on Antigravity's current file tools agy delivers read_file {Path}, write_file {Path, Content} and edit_file {Path, Target, Replacement}. None of those keys were mapped and write_file was not a known tool, so path policies and the always-on guard over failproofai's own state saw no path and allowed the call. Live on agy 1.4.12 against the unfixed code, an agent printed .env, overwrote and edited it, created id_rsa, and overwrote .failproofai/policies-config.json. All five are refused with this change, and normal reads, writes and edits inside the project still go through. The older view_file and write_to_file spellings stay mapped. Fixes #961 --- CHANGELOG.md | 4 ++ CLAUDE.md | 8 ++- __tests__/e2e/helpers/payloads.ts | 12 ++++ .../hooks/antigravity-integration.e2e.test.ts | 69 +++++++++++++++++++ .../hooks/antigravity-canonicalize.test.ts | 42 +++++++++++ src/hooks/types.ts | 37 +++++----- 6 files changed, 153 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1277f9307..8250cb0b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 1.0.13-beta.0 — 2026-10-27 +### Fixes + +- **Path policies enforce on Antigravity's current file tools again.** agy delivers `read_file {Path}`, `write_file {Path, Content}` and `edit_file {Path, Target, Replacement}`, none of which were mapped, so every path-based policy and the always-on guard over failproofai's own state saw no path and allowed the call: live on agy 1.4.12, an agent printed `.env`, overwrote and edited it, created `id_rsa`, and overwrote `.failproofai/policies-config.json`. All are now denied, and the older spellings stay mapped (#961) + ### Features - **`fp-cloud-cli` cuts `0.0.2` stable.** The `0.0.2b0` line opened after `0.0.1` was never published; it is cut stable rather than published, so `pipx install fp-cloud-cli` resolves the `fp` that drives what FailproofAI Cloud now serves (entries in `fp-cloud-cli/CHANGELOG.md`). This is the PyPI package's own line and does not move the npm version, which stays at `1.0.13-beta.0` (#936) diff --git a/CLAUDE.md b/CLAUDE.md index b09272271..0a1f0231d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -684,8 +684,12 @@ No `local` scope. `timeout` is in **seconds** (30). Command: project After that the existing extraction works. `run_command`'s args are PascalCase (`CommandLine`/`Cwd`) → `ANTIGRAVITY_TOOL_INPUT_MAP` (keyed by canonical `Bash`) maps them to `command`/`cwd`. Tool names via `ANTIGRAVITY_TOOL_MAP` -(`run_command→Bash` VERIFIED; `view_file→Read`, `edit_file→Edit`, … best-effort; -unknown tools pass through). +(`run_command→Bash`, `read_file→Read`, `write_file→Write`, `edit_file→Edit` VERIFIED +live against agy 1.4.12; unknown tools pass through). The file tools have moved once: +1.4.9 and 1.4.12 deliver `read_file {Path}`, `write_file {Path, Content}` and +`edit_file {Path, Target, Replacement}`, where earlier builds delivered `view_file +{AbsolutePath}` and `write_to_file {TargetFile, CodeContent}`. Both spellings stay +mapped. A missed name or key raises no error: the policy sees no path and allows. **Response shapes (Antigravity's OWN — `policy-evaluator.ts` `cli === "antigravity"`):** diff --git a/__tests__/e2e/helpers/payloads.ts b/__tests__/e2e/helpers/payloads.ts index fb9918b3f..c9d4dac75 100644 --- a/__tests__/e2e/helpers/payloads.ts +++ b/__tests__/e2e/helpers/payloads.ts @@ -758,6 +758,18 @@ export const AntigravityPayloads = { }, }; }, + // A native tool call, exactly as agy delivers it: `name` and `args` are the + // vendor's own, un-canonicalized. + tool(name: string, args: Record, cwd: string): Record { + return { + conversationId: ANTIGRAVITY_CONVERSATION_ID, + workspacePaths: [cwd], + transcriptPath: TRANSCRIPT_PATH, + modelName: "auto", + stepIdx: 19, + toolCall: { name, args }, + }; + }, }, postToolUse: { bash(command: string, cwd: string): Record { diff --git a/__tests__/e2e/hooks/antigravity-integration.e2e.test.ts b/__tests__/e2e/hooks/antigravity-integration.e2e.test.ts index 45dde1e0b..dc5222687 100644 --- a/__tests__/e2e/hooks/antigravity-integration.e2e.test.ts +++ b/__tests__/e2e/hooks/antigravity-integration.e2e.test.ts @@ -160,6 +160,75 @@ describe("E2E: Antigravity integration — hook protocol", () => { }); }); +// Payload shapes captured live from agy 1.4.9 and 1.4.12 with a recorder hook +// (#961). Every case here was a silent allow before the maps learned this tool set. +describe("E2E: Antigravity integration — native file tools", () => { + const deniedCases: Array<[string, string[], string, (cwd: string) => Record]> = [ + ["read_file .env", ["block-env-files"], "read_file", (cwd) => ({ Path: `${cwd}/.env` })], + ["read_file outside the project", ["block-read-outside-cwd"], "read_file", () => ({ Path: "/etc/passwd" })], + ["view_file with Path", ["block-env-files"], "view_file", (cwd) => ({ Path: `${cwd}/.env` })], + ["write_file .env", ["block-env-files"], "write_file", (cwd) => ({ Path: `${cwd}/.env`, Content: "X=1" })], + ["write_file a private key", ["block-secrets-write"], "write_file", (cwd) => ({ Path: `${cwd}/id_rsa`, Content: "k" })], + ["edit_file .env", ["block-env-files"], "edit_file", (cwd) => ({ Path: `${cwd}/.env`, Target: "a", Replacement: "b" })], + // No policy enabled at all: this one is the always-on guard, and writing this + // file is how an agent would switch every other policy off. + ["write_file over failproofai's own config", [], "write_file", (cwd) => ({ Path: `${cwd}/.failproofai/policies-config.json`, Content: "{}" })], + ["edit_file on failproofai's own config", [], "edit_file", (cwd) => ({ Path: `${cwd}/.failproofai/policies-config.json`, Target: "a", Replacement: "b" })], + ]; + + it.each(deniedCases)("denies %s", (_label, policies, tool, args) => { + const env = createAntigravityEnv(); + try { + writeConfig(env.cwd, policies); + const result = runHook( + "PreToolUse", + AntigravityPayloads.preToolUse.tool(tool, args(env.cwd), env.cwd), + { homeDir: env.home, cli: "antigravity" }, + ); + assertAntigravityDeny(result); + } finally { + env.cleanup(); + } + }); + + // agy reads its own skills and artifacts with read_file. Those reads only became + // visible to block-read-outside-cwd once Path mapped, so the ~/.gemini allowlist + // now carries them. + it("still allows agy reading its own files under the user's ~/.gemini", () => { + const env = createAntigravityEnv(); + try { + writeConfig(env.cwd, ["block-read-outside-cwd"]); + const result = runHook( + "PreToolUse", + AntigravityPayloads.preToolUse.tool( + "read_file", + { Path: `${env.home}/.gemini/antigravity-cli/builtin/skills/agy-customizations/docs/hooks.md` }, + env.cwd, + ), + { homeDir: env.home, cli: "antigravity" }, + ); + assertAllow(result); + } finally { + env.cleanup(); + } + }); + + it("still allows an ordinary read inside the project", () => { + const env = createAntigravityEnv(); + try { + writeConfig(env.cwd, ["block-env-files", "block-read-outside-cwd"]); + const result = runHook( + "PreToolUse", + AntigravityPayloads.preToolUse.tool("read_file", { Path: `${env.cwd}/notes.txt` }, env.cwd), + { homeDir: env.home, cli: "antigravity" }, + ); + assertAllow(result); + } finally { + env.cleanup(); + } + }); +}); + describe("E2E: Antigravity integration — install/uninstall", () => { it("policies --install --cli antigravity --scope project writes .agents/hooks.json with named-hook schema", () => { const env = createAntigravityEnv(); diff --git a/__tests__/hooks/antigravity-canonicalize.test.ts b/__tests__/hooks/antigravity-canonicalize.test.ts index a12d7ba19..c197b1d77 100644 --- a/__tests__/hooks/antigravity-canonicalize.test.ts +++ b/__tests__/hooks/antigravity-canonicalize.test.ts @@ -45,6 +45,7 @@ describe("Antigravity tool-name canonicalization", () => { it("canonicalizes agy tool ids to Claude builtins", () => { expect(canonicalizeToolName("run_command", "antigravity")).toBe("Bash"); expect(canonicalizeToolName("write_to_file", "antigravity")).toBe("Write"); + expect(canonicalizeToolName("write_file", "antigravity")).toBe("Write"); expect(canonicalizeToolName("read_file", "antigravity")).toBe("Read"); expect(canonicalizeToolName("view_file", "antigravity")).toBe("Read"); expect(canonicalizeToolName("edit_file", "antigravity")).toBe("Edit"); @@ -100,6 +101,47 @@ describe("Antigravity tool-input canonicalization", () => { expect(out.file_path).toBe("/repo/.env"); }); + // The tool set agy delivers now, captured live from 1.4.9 and 1.4.12 (#961). + it("maps read_file's Path to file_path on Read, keeping its range args", () => { + const out = canonicalizeToolInput( + "Read", + { Path: "/repo/.env", StartLine: 1, EndLine: 9, ByteOffset: 0 }, + "antigravity", + ) as Record; + expect(out.file_path).toBe("/repo/.env"); + expect(out.Path).toBeUndefined(); + expect(out.StartLine).toBe(1); + }); + + it("maps write_file's Path/Content to file_path/content on Write", () => { + const out = canonicalizeToolInput( + "Write", + { Path: "/repo/.env", Content: "SECRET=x", Overwrite: true, Append: false }, + "antigravity", + ) as Record; + expect(out.file_path).toBe("/repo/.env"); + expect(out.content).toBe("SECRET=x"); + expect(out.Overwrite).toBe(true); + }); + + it("maps edit_file's Path/Target/Replacement to file_path/old_string/new_string on Edit", () => { + const out = canonicalizeToolInput( + "Edit", + { Path: "/repo/.env", Target: "alpha", Replacement: "gamma" }, + "antigravity", + ) as Record; + expect(out.file_path).toBe("/repo/.env"); + expect(out.old_string).toBe("alpha"); + expect(out.new_string).toBe("gamma"); + }); + + it("keeps the older aliases, which agy versions still in the wild deliver", () => { + for (const key of ["TargetFile", "AbsolutePath", "File"]) { + const out = canonicalizeToolInput("Read", { [key]: "/repo/a" }, "antigravity") as Record; + expect(out.file_path, key).toBe("/repo/a"); + } + }); + it("leaves a tool with no map entry untouched (reference-equal)", () => { const input = { pattern: "TODO" }; expect(canonicalizeToolInput("Grep", input, "antigravity")).toBe(input); diff --git a/src/hooks/types.ts b/src/hooks/types.ts index b17ec73f9..dc9a87d19 100644 --- a/src/hooks/types.ts +++ b/src/hooks/types.ts @@ -863,16 +863,19 @@ export const ANTIGRAVITY_EVENT_MAP: Record = { run_command: "Bash", write_to_file: "Write", + write_file: "Write", read_file: "Read", view_file: "Read", edit_file: "Edit", @@ -886,20 +889,20 @@ export const ANTIGRAVITY_TOOL_MAP: Record = { /** * Antigravity tool args are PascalCase. Keyed by the CANONICAL tool name - * (canonicalizeToolInput runs after canonicalizeToolName). VERIFIED live: - * `run_command` delivers `CommandLine`/`Cwd`; `write_to_file` delivers the path - * as `TargetFile` and body as `CodeContent`. Without the Write/Edit/Read entries - * the path/content builtins (`block-env-files`, `block-secrets-write`, - * `block-read-outside-cwd`) never saw a `file_path` and silently no-op'd on - * Antigravity file operations. Read/Edit path keys are best-effort within the - * same tool family (all file tools operate on `TargetFile`); extra keys are - * harmless (only remapped when present). + * (canonicalizeToolInput runs after canonicalizeToolName). VERIFIED live against + * agy 1.4.12: `run_command` delivers `CommandLine`; `read_file` delivers `Path`; + * `write_file` delivers `Path`/`Content`; `edit_file` delivers + * `Path`/`Target`/`Replacement`. Earlier builds delivered `AbsolutePath` on + * `view_file` and `TargetFile`/`CodeContent` on `write_to_file`, kept because + * extra keys are harmless (only remapped when present). Without these entries + * the path/content builtins, and the always-on guard over failproofai's own + * state, never see a `file_path` and silently allow the call. */ export const ANTIGRAVITY_TOOL_INPUT_MAP: Record> = { Bash: { CommandLine: "command", Cwd: "cwd" }, - Write: { TargetFile: "file_path", CodeContent: "content" }, - Edit: { TargetFile: "file_path" }, - Read: { TargetFile: "file_path", AbsolutePath: "file_path", File: "file_path" }, + Write: { Path: "file_path", Content: "content", TargetFile: "file_path", CodeContent: "content" }, + Edit: { Path: "file_path", Target: "old_string", Replacement: "new_string", TargetFile: "file_path" }, + Read: { Path: "file_path", TargetFile: "file_path", AbsolutePath: "file_path", File: "file_path" }, }; // ── Goose (codename goose, Block) ──────────────────────────────────────────── From 164a975c6fac568266fee357f6412a353e079dad Mon Sep 17 00:00:00 2001 From: chhhee10 Date: Mon, 2 Nov 2026 12:17:37 +0530 Subject: [PATCH 2/2] chore(release): 1.0.13-beta.3 Skips beta.1 and beta.2, which are already published to npm from other branches. The changelog entry moves to this version's section. --- CHANGELOG.md | 4 +++- Cargo.lock | 6 +++--- Cargo.toml | 2 +- hermes-plugin/plugin.yaml | 2 +- package.json | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8250cb0b3..f987e8ca6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,13 @@ # Changelog -## 1.0.13-beta.0 — 2026-10-27 +## 1.0.13-beta.3 — 2026-11-02 ### Fixes - **Path policies enforce on Antigravity's current file tools again.** agy delivers `read_file {Path}`, `write_file {Path, Content}` and `edit_file {Path, Target, Replacement}`, none of which were mapped, so every path-based policy and the always-on guard over failproofai's own state saw no path and allowed the call: live on agy 1.4.12, an agent printed `.env`, overwrote and edited it, created `id_rsa`, and overwrote `.failproofai/policies-config.json`. All are now denied, and the older spellings stay mapped (#961) +## 1.0.13-beta.0 — 2026-10-27 + ### Features - **`fp-cloud-cli` cuts `0.0.2` stable.** The `0.0.2b0` line opened after `0.0.1` was never published; it is cut stable rather than published, so `pipx install fp-cloud-cli` resolves the `fp` that drives what FailproofAI Cloud now serves (entries in `fp-cloud-cli/CHANGELOG.md`). This is the PyPI package's own line and does not move the npm version, which stays at `1.0.13-beta.0` (#936) diff --git a/Cargo.lock b/Cargo.lock index 5fd2dd1ce..d07d214a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -298,7 +298,7 @@ dependencies = [ [[package]] name = "failproofaid" -version = "1.0.13-beta.0" +version = "1.0.13-beta.3" dependencies = [ "fpai-collect", "fpai-ipc", @@ -361,7 +361,7 @@ dependencies = [ [[package]] name = "fpai-collect" -version = "1.0.13-beta.0" +version = "1.0.13-beta.3" dependencies = [ "axum", "getrandom 0.4.3", @@ -382,7 +382,7 @@ dependencies = [ [[package]] name = "fpai-ipc" -version = "1.0.13-beta.0" +version = "1.0.13-beta.3" dependencies = [ "libc", "proptest", diff --git a/Cargo.toml b/Cargo.toml index b849a4d33..203bd902b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "3" members = ["crates/*"] [workspace.package] -version = "1.0.13-beta.0" +version = "1.0.13-beta.3" edition = "2024" license-file = "LICENSE" repository = "https://github.com/FailproofAI/failproofai" diff --git a/hermes-plugin/plugin.yaml b/hermes-plugin/plugin.yaml index be7d1ca00..9d86dbb90 100644 --- a/hermes-plugin/plugin.yaml +++ b/hermes-plugin/plugin.yaml @@ -1,5 +1,5 @@ name: failproofai -version: 1.0.13-beta.0 +version: 1.0.13-beta.3 description: Real-time FailproofAI policy enforcement for Hermes manifest_version: 2 api_version: 1 diff --git a/package.json b/package.json index 66d88efdb..cb2470df4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "failproofai", - "version": "1.0.13-beta.0", + "version": "1.0.13-beta.3", "description": "Observability and enforcement for AI agent harnesses. 40 built-in policies hooked into 12 of them — Claude Code, Codex, Cursor, Hermes, OpenClaw and more — blocking the tool call before it runs. Local dashboard included, no account needed.", "bin": { "failproofai": "./dist/cli.mjs",