diff --git a/.agents/hooks.json b/.agents/hooks.json new file mode 100644 index 00000000..f7b02da4 --- /dev/null +++ b/.agents/hooks.json @@ -0,0 +1,46 @@ +{ + "failproofai": { + "PreToolUse": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook PreToolUse --cli antigravity", + "timeout": 30, + "__failproofai_hook__": true + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook PostToolUse --cli antigravity", + "timeout": 30, + "__failproofai_hook__": true + } + ] + } + ], + "PreInvocation": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook PreInvocation --cli antigravity", + "timeout": 30, + "__failproofai_hook__": true + } + ], + "Stop": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook Stop --cli antigravity", + "timeout": 30, + "__failproofai_hook__": true + } + ] + } +} diff --git a/.agents/plugins/failproofai/hooks/hooks.json b/.agents/plugins/failproofai/hooks/hooks.json new file mode 100644 index 00000000..3e01bf19 --- /dev/null +++ b/.agents/plugins/failproofai/hooks/hooks.json @@ -0,0 +1,54 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook SessionStart --cli goose" + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook UserPromptSubmit --cli goose" + } + ] + } + ], + "PreToolUse": [ + { + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook PreToolUse --cli goose" + } + ] + } + ], + "PostToolUse": [ + { + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook PostToolUse --cli goose" + } + ] + } + ], + "SessionEnd": [ + { + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook SessionEnd --cli goose" + } + ] + } + ] + } +} diff --git a/.devin/config.json b/.devin/config.json new file mode 100644 index 00000000..3b311ccd --- /dev/null +++ b/.devin/config.json @@ -0,0 +1,88 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook SessionStart --cli devin", + "timeout": 60, + "__failproofai_hook__": true + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook UserPromptSubmit --cli devin", + "timeout": 60, + "__failproofai_hook__": true + } + ] + } + ], + "PreToolUse": [ + { + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook PreToolUse --cli devin", + "timeout": 60, + "__failproofai_hook__": true + } + ] + } + ], + "PostToolUse": [ + { + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook PostToolUse --cli devin", + "timeout": 60, + "__failproofai_hook__": true + } + ] + } + ], + "PermissionRequest": [ + { + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook PermissionRequest --cli devin", + "timeout": 60, + "__failproofai_hook__": true + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook Stop --cli devin", + "timeout": 60, + "__failproofai_hook__": true + } + ] + } + ], + "SessionEnd": [ + { + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook SessionEnd --cli devin", + "timeout": 60, + "__failproofai_hook__": true + } + ] + } + ] + } +} diff --git a/.factory/hooks.json b/.factory/hooks.json new file mode 100644 index 00000000..b2fa3660 --- /dev/null +++ b/.factory/hooks.json @@ -0,0 +1,112 @@ +{ + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook SessionStart --cli factory", + "timeout": 30, + "__failproofai_hook__": true + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook UserPromptSubmit --cli factory", + "timeout": 30, + "__failproofai_hook__": true + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook PreToolUse --cli factory", + "timeout": 30, + "__failproofai_hook__": true + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook PostToolUse --cli factory", + "timeout": 30, + "__failproofai_hook__": true + } + ] + } + ], + "Notification": [ + { + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook Notification --cli factory", + "timeout": 30, + "__failproofai_hook__": true + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook Stop --cli factory", + "timeout": 30, + "__failproofai_hook__": true + } + ] + } + ], + "SubagentStop": [ + { + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook SubagentStop --cli factory", + "timeout": 30, + "__failproofai_hook__": true + } + ] + } + ], + "PreCompact": [ + { + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook PreCompact --cli factory", + "timeout": 30, + "__failproofai_hook__": true + } + ] + } + ], + "SessionEnd": [ + { + "hooks": [ + { + "type": "command", + "command": "bun bin/failproofai.mjs --hook SessionEnd --cli factory", + "timeout": 30, + "__failproofai_hook__": true + } + ] + } + ] +} diff --git a/.gemini/settings.json b/.gemini/settings.json deleted file mode 100644 index 781e6d5c..00000000 --- a/.gemini/settings.json +++ /dev/null @@ -1,147 +0,0 @@ -{ - "hooks": { - "SessionStart": [ - { - "matcher": "*", - "hooks": [ - { - "type": "command", - "command": "bun $GEMINI_PROJECT_DIR/bin/failproofai.mjs --hook SessionStart --cli gemini", - "timeout": 60000, - "__failproofai_hook__": true - } - ] - } - ], - "SessionEnd": [ - { - "matcher": "*", - "hooks": [ - { - "type": "command", - "command": "bun $GEMINI_PROJECT_DIR/bin/failproofai.mjs --hook SessionEnd --cli gemini", - "timeout": 60000, - "__failproofai_hook__": true - } - ] - } - ], - "BeforeAgent": [ - { - "matcher": "*", - "hooks": [ - { - "type": "command", - "command": "bun $GEMINI_PROJECT_DIR/bin/failproofai.mjs --hook BeforeAgent --cli gemini", - "timeout": 60000, - "__failproofai_hook__": true - } - ] - } - ], - "AfterAgent": [ - { - "matcher": "*", - "hooks": [ - { - "type": "command", - "command": "bun $GEMINI_PROJECT_DIR/bin/failproofai.mjs --hook AfterAgent --cli gemini", - "timeout": 60000, - "__failproofai_hook__": true - } - ] - } - ], - "BeforeModel": [ - { - "matcher": "*", - "hooks": [ - { - "type": "command", - "command": "bun $GEMINI_PROJECT_DIR/bin/failproofai.mjs --hook BeforeModel --cli gemini", - "timeout": 60000, - "__failproofai_hook__": true - } - ] - } - ], - "AfterModel": [ - { - "matcher": "*", - "hooks": [ - { - "type": "command", - "command": "bun $GEMINI_PROJECT_DIR/bin/failproofai.mjs --hook AfterModel --cli gemini", - "timeout": 60000, - "__failproofai_hook__": true - } - ] - } - ], - "BeforeToolSelection": [ - { - "matcher": "*", - "hooks": [ - { - "type": "command", - "command": "bun $GEMINI_PROJECT_DIR/bin/failproofai.mjs --hook BeforeToolSelection --cli gemini", - "timeout": 60000, - "__failproofai_hook__": true - } - ] - } - ], - "BeforeTool": [ - { - "matcher": "*", - "hooks": [ - { - "type": "command", - "command": "bun $GEMINI_PROJECT_DIR/bin/failproofai.mjs --hook BeforeTool --cli gemini", - "timeout": 60000, - "__failproofai_hook__": true - } - ] - } - ], - "AfterTool": [ - { - "matcher": "*", - "hooks": [ - { - "type": "command", - "command": "bun $GEMINI_PROJECT_DIR/bin/failproofai.mjs --hook AfterTool --cli gemini", - "timeout": 60000, - "__failproofai_hook__": true - } - ] - } - ], - "PreCompress": [ - { - "matcher": "*", - "hooks": [ - { - "type": "command", - "command": "bun $GEMINI_PROJECT_DIR/bin/failproofai.mjs --hook PreCompress --cli gemini", - "timeout": 60000, - "__failproofai_hook__": true - } - ] - } - ], - "Notification": [ - { - "matcher": "*", - "hooks": [ - { - "type": "command", - "command": "bun $GEMINI_PROJECT_DIR/bin/failproofai.mjs --hook Notification --cli gemini", - "timeout": 60000, - "__failproofai_hook__": true - } - ] - } - ] - } -} diff --git a/.github/workflows/translate-docs.yml b/.github/workflows/translate-docs.yml index 4da4e25f..ff7c2fd8 100644 --- a/.github/workflows/translate-docs.yml +++ b/.github/workflows/translate-docs.yml @@ -4,19 +4,19 @@ on: push: branches: [main] paths: - - 'docs/*.mdx' - - 'docs/**/*.mdx' - - 'README.md' + - "docs/*.mdx" + - "docs/**/*.mdx" + - "README.md" workflow_dispatch: inputs: force: - description: 'Ignore cache and re-translate everything' + description: "Ignore cache and re-translate everything" type: boolean default: false languages: - description: 'Comma-separated language codes (leave empty for all)' + description: "Comma-separated language codes (leave empty for all)" type: string - default: '' + default: "" concurrency: group: translate-docs @@ -67,7 +67,9 @@ jobs: restore-keys: bun-${{ runner.os }}- - name: Install dependencies - run: bun install --frozen-lockfile + # Translation only runs TypeScript tooling. Avoid the package prepare + # hook, which builds the full Next.js application once per language. + run: bun install --frozen-lockfile --ignore-scripts - name: Restore translation cache uses: actions/cache/restore@v6 @@ -87,7 +89,7 @@ jobs: docs/${{ matrix.lang }}/ docs/i18n/README.${{ matrix.lang }}.md retention-days: 1 - if-no-files-found: warn + if-no-files-found: error - name: Upload cache fragment uses: actions/upload-artifact@v7 @@ -95,12 +97,14 @@ jobs: name: cache-${{ matrix.lang }} path: scripts/translate-docs/.translation-cache.json retention-days: 1 - if-no-files-found: warn + if-no-files-found: error include-hidden-files: true consolidate: needs: [prepare, translate] - if: always() && needs.translate.result != 'cancelled' + # Never publish a partial translation set. A failed language produces no + # artifact and would otherwise leave that locale silently stale. + if: needs.translate.result == 'success' runs-on: ubuntu-latest permissions: contents: write @@ -114,10 +118,9 @@ jobs: bun-version: latest - name: Install dependencies - run: bun install --frozen-lockfile + run: bun install --frozen-lockfile --ignore-scripts - name: Download all translations - continue-on-error: true uses: actions/download-artifact@v8 with: pattern: translations-* @@ -129,8 +132,21 @@ jobs: bun scripts/translate-docs/cli.ts --update-nav --languages zh,ja,ko,es,pt-br,de,fr,ru,hi,tr,vi,it,ar,he + - uses: actions/setup-node@v6 + with: + node-version: 22 + + - name: Install Mintlify CLI + run: npm install -g mintlify@4.2.680 + + - name: Validate generated docs config + working-directory: docs + run: mintlify validate + + - name: Validate translated MDX pages + run: bun run validate:mdx + - name: Download cache fragments - continue-on-error: true uses: actions/download-artifact@v8 with: pattern: cache-* @@ -227,6 +243,11 @@ jobs: git checkout -b "$BRANCH" fi + # Validate again after overlaying an existing translation branch so + # every file that will be committed is covered by both validators. + (cd docs && mintlify validate) + bun run validate:mdx + git add -A if git diff --cached --quiet; then echo "No new translation changes to push. Done." diff --git a/CHANGELOG.md b/CHANGELOG.md index 05a04f41..f142953e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## 0.0.14-beta.1 — 2026-07-14 + +### Features +- Add Goose (codename goose, Block) as the 12th CLI/agent integration — **dual-pillar** like Hermes/OpenClaw/Factory/Devin/Antigravity: real-time policy enforcement **and** offline audit + dashboard. Goose is a **local, MCP-based** dev-agent; enforcement uses its **"hooks" system** (the cross-agent **Open Plugins** spec) — an auto-discovered plugin dir at `~/.agents/plugins/failproofai/hooks/hooks.json` (user) / `/.agents/plugins/failproofai/hooks/hooks.json` (project) whose `command` runs `failproofai --hook --cli goose`. The entire contract was verified **live against goose v1.43.0**: (1) Deny is `{"decision":"block","reason"}` JSON on stdout at exit 0 (also exit 2), honored on **`PreToolUse` ONLY** (shipped goose ≥ **v1.37.0**, PR block/goose#9304); any other error fails **open**. `PreToolUse` fires for the shell tool AND **inside delegated subagents**, so it is the single sufficient deny point. Goose has **no `Stop` event** (the 5 `require-*-before-stop` builtins are inapplicable, like Hermes) and does not honor deny on `UserPromptSubmit`/`PostToolUse`. (2) Event names are already PascalCase (no `GOOSE_EVENT_MAP`, no handler branch), but the stdin payload uses `event`/`working_dir` — the handler normalizes `working_dir`→`cwd`. (3) Tool names arrive **both** bare (`shell`, `write`, `edit`, `view`, `read_image`, `tree`, `delegate`) **and** `__` namespaced (`todo__todo_write`); `GOOSE_TOOL_MAP` covers both, and `GOOSE_TOOL_INPUT_MAP` maps path-bearing tools' `path`/`source` → `file_path`. `instruct()` degrades to allow + stderr note (no additional-context channel). The installer just **drops the plugin dir** — Goose auto-discovers it and self-registers it into `config.yaml` (no config edit needed). Install via `failproofai policies --install --cli goose` (user + project scope). Also surfaces Goose sessions in the dashboard's audit + history browser: reads the SQLite DB at `~/.local/share/goose/sessions/sessions.db` (schema_version 15; `sessions` rows carry a real `working_dir` → per-project cwd grouping like Devin; `messages.content_json` is a Claude-style typed-block array parsed via the same block model) — `session_type='hidden'` (`--no-session`) scratch runs are filtered — via `lib/goose-sessions.ts` (pure, unit-tested parser) and `lib/goose-projects.ts`, wired through `src/audit/cli-adapters/goose.ts`, `lib/cli-registry.ts` (lime badge), `lib/projects.ts`, `lib/download-session.ts` (SQLite→JSONL export), and the `app/project/[name]` routes. `GOOSE_HOME` / `GOOSE_DB_PATH` override the data dir for tests. (#508) +- Add Antigravity CLI (`agy`) as the 11th CLI/agent integration — **dual-pillar** like Hermes/OpenClaw/Factory/Devin: real-time policy enforcement **and** offline audit + dashboard. Unlike Factory/Devin, Antigravity has its **OWN** hook contract (NOT a Claude-clone), verified **live against agy v1.1.2**. (1) `hooks.json` uses a **NAMED-hook schema** — the top-level key is a hook *name* (`"failproofai"`) whose value is an event→handlers map; tool events (`PreToolUse`/`PostToolUse`) wrap handlers in `{matcher:"*", hooks:[…]}`, while `PreInvocation`/`Stop` are **flat** handler arrays. Config at `~/.gemini/config/hooks.json` (user) / `/.agents/hooks.json` (project); no `local` scope. (2) The stdin payload is **camelCase protojson** (`toolCall:{name,args}`, `conversationId`, `workspacePaths`, `transcriptPath`) — the handler normalizes it to canonical snake_case before policies run; `run_command`'s args are PascalCase (`CommandLine`/`Cwd`) → canonicalized via `ANTIGRAVITY_TOOL_INPUT_MAP`. (3) Response shapes are **Antigravity's own**: deny → `{decision:"deny", reason}` (exit 0); deny/instruct on `Stop` → `{decision:"continue", reason}` (re-enters the loop, so the 5 `require-*-before-stop` builtins enforce); instruct on `PreInvocation` (→ `UserPromptSubmit`) → `{injectSteps:[{ephemeralMessage}]}`. Adds `ANTIGRAVITY_EVENT_MAP` (`PreInvocation→UserPromptSubmit`) + `ANTIGRAVITY_TOOL_MAP` (`run_command→Bash`, `view_file→Read`, …). Install via `failproofai policies --install --cli antigravity` (user + project scope). Also surfaces Antigravity sessions in the dashboard's audit + history browser: reads the plain-JSONL transcripts at `~/.gemini/antigravity-cli/brain//.system_generated/logs/transcript_full.jsonl` (pairing `PLANNER_RESPONSE` tool_calls with their following result step) via `lib/antigravity-sessions.ts` (pure, unit-tested parser) and the SQLite conversation index (`conversation_summaries.db`) via `lib/antigravity-projects.ts`, wired through `src/audit/cli-adapters/antigravity.ts`, `lib/cli-registry.ts` (cyan badge), `lib/projects.ts`, `lib/download-session.ts` (real-file download), and the `app/project/[name]` routes. `ANTIGRAVITY_HOME` overrides the data dir for tests. (#508) +- Add Devin CLI (`devin`, Cognition) as the 10th CLI/agent integration — **dual-pillar** like Hermes/OpenClaw/Factory: real-time policy enforcement **and** offline audit + dashboard. Devin is a **pure Claude-clone** verified **live against devin v3000.1.27** — same PascalCase event names (no `DEVIN_EVENT_MAP`, no handler branch), same snake_case stdin payload (no normalization), and the standard Claude `"hooks"`-wrapper config schema. Config lives under the `"hooks"` key of `~/.config/devin/config.json` (user) / `/.devin/config.json` (project) — merge-preserving so the file's other keys (`org_id`, `theme_mode`, …) survive. Deny is `{"decision":"block","reason"}` JSON on stdout at exit 0 for **every** event (verified — the block overrode `--permission-mode dangerous`); on `Stop` the reason carries the MANDATORY-ACTION force-retry wording, so the 5 `require-*-before-stop` builtins enforce. Adds `DEVIN_TOOL_MAP` (`exec→Bash`; `tool_input.command` already canonical). Install via `failproofai policies --install --cli devin` (user + project scope). Also surfaces Devin sessions in the dashboard's audit + history browser: reads the SQLite DB at `~/.local/share/devin/cli/sessions.db` (`sessions` table carries a real `working_directory` → per-project cwd grouping like Claude; `message_nodes.chat_message` is OpenAI-style JSON) via `lib/devin-sessions.ts` (pure, unit-tested parser) and `lib/devin-projects.ts`, wired through `src/audit/cli-adapters/devin.ts`, `lib/cli-registry.ts` (violet badge), `lib/projects.ts`, `lib/download-session.ts` (SQLite→JSONL export), and the `app/project/[name]` routes. `DEVIN_HOME` / `DEVIN_DB_PATH` override the data dir for tests. (#508) +- Add Factory (droid) as the 9th CLI/agent integration — **dual-pillar** like Hermes/OpenClaw: real-time policy enforcement **and** offline audit + dashboard. droid ships a Claude-compatible external-command hook system, but with two schema quirks verified **live against droid v0.171.0**: (1) event names live at the **TOP LEVEL** of `~/.factory/hooks.json` — there is **no `"hooks"` wrapper** (droid rejects one with `WARN Ignoring unknown hook event keys keys:["hooks"]`); tool events (`PreToolUse`/`PostToolUse`) carry `"matcher": "*"`, non-tool events omit it. (2) Deny is driven by **exit code 2 + stderr**, not a JSON decision (droid ignores `{decision:…}` on tool events: `Hook returned exit code 2, throwing ToolExecutionControlError`); the `Stop` event is the exception — there droid honors `{decision:"block", reason}` on stdout at exit 0. Event names are already PascalCase (no `FACTORY_EVENT_MAP`, no handler branch) and the payload is Claude snake_case (no normalization). Adds `FACTORY_TOOL_MAP` (`Execute→Bash`, `Create→Write`, `FetchUrl→WebFetch`, …). Install via `failproofai policies --install --cli factory` (user + project scope). Also surfaces droid sessions in the dashboard's audit + history browser: reads the real JSONL transcripts at `~/.factory/sessions//.jsonl` (Claude-style encoded-cwd folders) via `lib/factory-sessions.ts` (pure, unit-tested parser) and `lib/factory-projects.ts`, wired through `src/audit/cli-adapters/factory.ts`, `lib/cli-registry.ts` (rose badge), `lib/projects.ts`, `lib/download-session.ts` (real-file download), and the `app/project/[name]` routes. (#508) +- Remove the Gemini CLI integration entirely. Google retired Gemini CLI (consumer accounts stopped serving on 2026-06-18) in favor of the closed-source **Antigravity CLI** (`agy`), which failproofai is migrating to. Drops the `gemini` id from `INTEGRATION_TYPES`, the `INTEGRATIONS`/`ADAPTERS` registries, `KNOWN_CLI_IDS`, all `GEMINI_*` event/tool maps, the `gemini` audit adapter + `lib/gemini-{projects,sessions}.ts`, the `--cli gemini` install/audit paths, and the dashboard's Gemini session provider. The `~/.gemini/` config-file protections in `block-*` builtins are **kept and re-pointed at Antigravity** (which reuses that directory). Fixes a latent bug where OpenClaw sessions without a cwd were mislabeled "Gemini CLI" in the session viewer. (#508) + +- Add OpenClaw (openclaw gateway) as the 9th CLI/agent integration — **dual-pillar** like Hermes: real-time policy enforcement **and** offline audit + dashboard. Enforcement uses OpenClaw's **in-process plugin hooks** (its file-based "internal hooks" are observation-only and cannot block), so failproofai ships a static plugin package (`openclaw-plugin/`, like `pi-extension/`) that **async-spawns** the binary (never `spawnSync` — the gateway is long-running and multi-channel) and maps a flat `{permission, reason}` verdict to each hook's native return shape: `before_tool_call → {block:true, blockReason}` (PreToolUse), `before_agent_run → {outcome:"block", reason}` (UserPromptSubmit), and `before_agent_finalize → {action:"revise", reason}` (Stop — a **real turn-end gate**, unlike Hermes which has none, so the 5 `require-*-before-stop` builtins enforce). Install (`failproofai policies --install --cli openclaw`) registers the plugin in `~/.openclaw/openclaw.json` (`plugins.load.paths` + `plugins.entries.failproofai` with `hooks.allowConversationAccess: true`), preserving operator config. Adds `OPENCLAW_EVENT_MAP` / `OPENCLAW_TOOL_MAP` (`exec→Bash`, `read→Read`, …) / `OPENCLAW_TOOL_INPUT_MAP`; canonicalization stays binary-side (no inline maps in the shim). **User-scope only** (OpenClaw has no project config). Verified live against **openclaw v2026.7.1** (tool block confirmed end-to-end). (#489) +- Surface OpenClaw sessions in the dashboard's audit + history browser: reads the real JSONL transcripts at `~/.openclaw/agents//sessions/.jsonl` (skipping the heavy `.trajectory.jsonl` OTel traces) via `lib/openclaw-sessions.ts` (a pure, unit-tested type-discriminated parser that pairs assistant `toolCall` blocks with their `toolResult` by `toolCallId`) and `lib/openclaw-projects.ts` (reads the `sessions.json` index, groups by agentId into `openclaw-` projects, parses channel metadata from the sessionKey). Wired through `src/audit/cli-adapters/openclaw.ts`, `lib/cli-registry.ts` (teal badge), `lib/projects.ts`, `lib/download-session.ts` (real-file download — no synthesis needed), and the `app/project/[name]` routes. (#489) + +### Fixes +- Make documentation translation publishing atomic: require every locale and cache artifact, pin the Mintlify validator, validate the final overlaid PR tree, and emit the canonical `pt-BR` Mintlify locale while preserving `pt-br` paths. +- Fix the Pi live `pi list` roundtrip e2e tests against pi-coding-agent ≥0.80: newer Pi no longer trusts project-local `.pi/settings.json` by default, so the tests now detect the installed Pi version and pass `pi list --approve` on ≥0.80 (older Pi trusts project settings without the flag and would reject it) to include the project-scope package failproofai's installer writes (previously `pi list` printed "No packages installed." — failing the install roundtrip and making the uninstall roundtrip pass vacuously). Gated behind `detectPiVersion()`, so it only runs where `pi` is installed. (#491) +- Fix Antigravity file writes slipping past path/content policies. `ANTIGRAVITY_TOOL_MAP` had best-effort tool names; the real ones (verified against the `agy` binary + live transcripts) are `write_to_file` (not `write_file`), `list_dir` (not `list_directory`), and `find_by_name` (not `find_filepath`), and there was **no** input-key map for file tools — so `write_to_file`'s path (delivered as `TargetFile`) never became `file_path` and `block-env-files` / `block-secrets-write` silently allowed `.env` writes on Antigravity. Corrected the tool names and added `Write`/`Edit`/`Read` input maps (`TargetFile`→`file_path`, `CodeContent`→`content`). Verified live: `agy` now denies a `.env` write. (#508) +- Keep informational allow-notes out of the agent-facing hook stdout on events with no additional-context channel (`Stop`, `SubagentStop`, `Session*`, `PreCompact`, …). Previously a *passing* `Stop` emitted its allow-reasons as `{reason}` on stdout, so agents like droid rendered a "…skipping commit check…skipping PR check…" wall on a perfectly fine turn. Those notes now go to stderr + the activity store only; stdout stays clean on channel-less events. (#508) +- Fix duplicated Devin session transcripts in the dashboard. Devin stores a session's messages as a **forest** (`node_id`/`parent_node_id`) and replays earlier context under fresh roots each turn, so a 10-message conversation is stored as 26-31 nodes — the parser read every node and rendered each message 2-4×. Now reconstructs the real conversation (walk `parent_node_id` from the newest leaf to its root, reversed) via `devinActiveConversationPath`. (#508) + +### Docs +- Backfill **OpenClaw** into the user-facing supported-CLI docs (README + `configuration` / `getting-started` / `introduction`) — it shipped as a wired integration but was never added to those lists. (#508) +- Add all 12 supported-CLI logos to the README (added openclaw/factory/devin/antigravity/goose with light+dark variants), link each logo to its official site, and lay them out as 2 rows of 6. (#508) +- Document that **VS Code Copilot agent mode** (Preview) is already covered by the `copilot` / `claude` integrations: its agent hooks load from `.github/hooks/*.json`, `~/.copilot/hooks/*.json`, and `~/.claude/settings.json` — the exact paths failproofai already writes — so `--cli copilot` (or `--cli claude`) enforces in VS Code agent-mode sessions with no dedicated `vscode` id. (#508) + ## 0.0.13 — 2026-07-14 ### Release @@ -13,6 +37,9 @@ ### Fixes - Fix the Pi live `pi list` roundtrip e2e tests against pi-coding-agent ≥0.80: newer Pi no longer trusts project-local `.pi/settings.json` by default, so the tests now detect the installed Pi version and pass `pi list --approve` on ≥0.80 (older Pi trusts project settings without the flag and would reject it) to include the project-scope package failproofai's installer writes (previously `pi list` printed "No packages installed." — failing the install roundtrip and making the uninstall roundtrip pass vacuously). Gated behind `detectPiVersion()`, so it only runs where `pi` is installed. (#491) +### Docs +- Document OpenClaw across `CLAUDE.md` (new "OpenClaw hooks" section), the README (install examples + logo), and the reference docs — `configuration` / `getting-started` / `dashboard` / `introduction`. (#489) + ## 0.0.13-beta.2 — 2026-07-10 ### Fixes diff --git a/CLAUDE.md b/CLAUDE.md index b5e98fd5..384737ec 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -253,68 +253,9 @@ workaround (queue the instruction in `tool_call`, drain in the next `context` handler by inserting a system message into `event.messages`) is feasible but deferred until upstream Pi adds a first-class channel. -### Gemini hooks (`.gemini/settings.json`) - -This repo also ships a `.gemini/settings.json` for Gemini CLI sessions, mirroring -the `.claude/settings.json`, `.codex/hooks.json`, `.github/hooks/failproofai.json`, -`.cursor/hooks.json`, and `.opencode/` setups. Gemini's hook contract is -intentionally close to Claude Code's: same `{matcher, hooks: [{type, command, -timeout}]}` matcher-wrapper schema, same PascalCase event names, same -snake_case stdin payload field names (`session_id`, `tool_name`, `tool_input`, -`hook_event_name`, `cwd`, `transcript_path`), and a subprocess execution model. - -Verified empirically against gemini-cli v0.40.1 — drop a `.gemini/settings.json` -at the repo root and Gemini picks up failproofai hooks on the next session. -Settings file paths: - -| Scope | Path | -|---------|-----------------------------------| -| user | `~/.gemini/settings.json` | -| project | `/.gemini/settings.json` | -| system | `/etc/gemini-cli/settings.json` (documented but not exposed by failproofai) | - -Gemini exposes both `$GEMINI_PROJECT_DIR` and `$CLAUDE_PROJECT_DIR` (alias -provided for back-compat) in the hook's environment. The dogfood config in this -repo uses `$GEMINI_PROJECT_DIR` for clarity; the existing `bun -$CLAUDE_PROJECT_DIR/...` form would also work because of the alias. - -For production users (outside this repo), the recommended Gemini install is: -```bash -failproofai policies --install --cli gemini --scope project -``` -which writes a portable `npx -y failproofai --hook ... --cli gemini` command. -Same self-reference caveat applies — do **not** install the standard `npx` -form from inside this repo (it would overwrite the dev `bun bin/failproofai.mjs` -path, re-fetching failproofai on every hook). - -**Tool-name canonicalization.** Gemini's tools are snake_case -(`run_shell_command`, `read_file`, `read_many_files`, `write_file`, `replace`, -`glob`, `grep_search`, `list_directory`, `web_fetch`, `google_web_search`, -`write_todos`, `save_memory`, `ask_user`). The handler translates to Claude -PascalCase via `GEMINI_TOOL_MAP` so existing builtin policies (matching -`toolName === "Bash"` etc.) fire unchanged. Unknown tools (MCP `mcp_*`, -extensions, Skills) pass through unchanged. - -**Per-event capability matrix** (verified against gemini-cli v0.40.1 / docs as -of 2026-04-13): - -| Gemini event | Canonical | Veto / mutate? | Notes | -|------------------------|----------------------------|----------------|-------| -| `BeforeTool` | `PreToolUse` | ✅ deny | `{decision:"deny", reason}` shape; can rewrite via `hookSpecificOutput.tool_input` (not used today). | -| `AfterTool` | `PostToolUse` | observation | `additionalContext` injection supported. | -| `BeforeAgent` | `UserPromptSubmit` | ✅ deny | `{decision:"deny", reason}` + `additionalContext` injection. | -| `AfterAgent` | `Stop` | ✅ force-retry | `{decision:"block", reason}` mirrors Claude's exit-2-from-Stop "do this before stopping" semantics. Gemini's exit-2 is documented as per-action only ("turn continues"), so we use the JSON shape. | -| `SessionStart` | `SessionStart` | observation | `additionalContext` injection supported. | -| `SessionEnd` | `SessionEnd` | observation | No context-injection channel — emitted via stderr only. | -| `PreCompress` | `PreCompact` | observation | Same. | -| `Notification` | `Notification` | observation | Same. | -| `BeforeModel` | (Gemini-only, no canonical) | observation | Binary still records activity but no policies match. | -| `AfterModel` | (Gemini-only, no canonical) | observation | Same. | -| `BeforeToolSelection` | (Gemini-only, no canonical) | observation | Same. | - ### Hermes hooks (`~/.hermes/config.yaml`) -Unlike the seven CLIs above, this repo does **not** ship a dogfood Hermes config — +Unlike the six CLIs above, this repo does **not** ship a dogfood Hermes config — Hermes (hermes-agent) is a downstream client's Slack/Telegram gateway, not something we run in-repo. Hermes is a **dual-pillar** integration: an **audit** adapter (`src/audit/cli-adapters/hermes.ts`, reads `~/.hermes/state.db` directly) **and** a @@ -392,6 +333,422 @@ For production users the recommended Hermes install is: failproofai policies --install --cli hermes --scope user ``` +### OpenClaw hooks (`~/.openclaw/openclaw.json`) + +Like Hermes, this repo does **not** ship a dogfood OpenClaw config — OpenClaw +(openclaw gateway) is a downstream self-hosted assistant, not something we run +in-repo. OpenClaw is a **dual-pillar** integration: an **audit** adapter +(`src/audit/cli-adapters/openclaw.ts`, reads `~/.openclaw/agents//sessions/*.jsonl`) +**and** a **live-hook** integration (`openclaw` in `INTEGRATIONS`). + +**OpenClaw's enforcement surface is its IN-PROCESS PLUGIN hooks, not shell hooks.** +OpenClaw has two extension surfaces: file-based **internal hooks** +(`~/.openclaw/hooks/`) which are **observation-only and cannot block**, and typed +**plugin hooks** (`api.on(name, handler, {priority, timeoutMs})`) which have +block/cancel semantics. So — like OpenCode — failproofai ships a **plugin** +(`openclaw-plugin/`, a static package like `pi-extension/`, shipped in the npm +tarball via `package.json` "files") that async-spawns the binary and translates +the verdict. The install registers it in `~/.openclaw/openclaw.json` (JSON): + +- `plugins.load.paths[]` ← the shipped `openclaw-plugin/` dir (absolute path) +- `plugins.entries.failproofai = { enabled: true, hooks: { allowConversationAccess: true } }` + (`allowConversationAccess` is required for the raw-conversation hooks + `before_agent_run` / `before_agent_finalize`; verified live) + +**User-scope only** — OpenClaw has no project config (workspace plugins are +disabled by default), so `getSettingsPath` ignores scope/cwd. Uninstall only +edits `openclaw.json`; it **never deletes the shipped plugin dir**. + +**Async spawn, never `spawnSync`.** OpenClaw is a long-running multi-channel +gateway; a sync spawn on every hook would stall every channel. The shim +(`openclaw-plugin/index.js`) uses async `spawn` + a 30s guard and **fails open** +(any spawn/parse/timeout error → allow). It carries **no inline tool maps** +(unlike the OpenCode/Pi shims) — it forwards raw event/tool names and a +Claude-shaped stdin (`params→tool_input`, `toolName→tool_name`, +`transcriptPath→transcript_path`, `stopHookActive→stop_hook_active`, +`sessionKey→session_id`), and the binary canonicalizes via the `OPENCLAW_*` +maps in `types.ts` (single source of truth). + +**Verdict mapping** (`policy-evaluator.ts` emits a flat Pi-style +`{permission, reason}`; the shim maps per-hook): + +| OpenClaw plugin hook | Canonical (`OPENCLAW_EVENT_MAP`) | Veto / mutate? | Shim return on deny | +|----------------------|----------------------------------|----------------|---------------------| +| `before_tool_call` | `PreToolUse` | ✅ block | `{block: true, blockReason}` | +| `after_tool_call` | `PostToolUse` | observation | — | +| `before_agent_run` | `UserPromptSubmit`| ✅ block | `{outcome: "block", reason}` | +| `before_agent_finalize` | `Stop` | ✅ revise | `{action: "revise", reason}` | +| `session_start` / `session_end` | `SessionStart` / `SessionEnd` | observation | — | +| `subagent_ended` | `SubagentStop` | observation only | — (cannot veto) | +| `before_compaction` | `PreCompact` | observation | — | + +**`before_agent_finalize` is a real turn-end gate** (carries `transcriptPath` + +`stopHookActive`, ≈ Claude's Stop payload), so the 5 `require-*-before-stop` +builtins **enforce** on OpenClaw — a deny becomes a `{action:"revise"}` that +re-runs the turn (unlike Hermes, which has no Stop event at all). **Instruct** +degrades to allow + stderr note on non-Stop events (no additional-context +channel); on Stop it emits the MANDATORY-ACTION deny so the revise loop carries +the directive. **Omitted hooks:** `agent_end` (would double-fire Stop) and +`message_sending` (outbound-message cancel gate — an OpenClaw-only capability, +deferred). + +**No headless consent prompt** (unlike Hermes, which needed `hooks_auto_accept`) +— verified live: registering plugin hooks does not prompt on a headless gateway. + +**Setup gotcha worth knowing** (hit during live verification): a config +`env._API_KEY` is **not enough** to make a provider usable — the agent +runtime only registers a provider once an **auth profile** exists +(`openclaw models auth paste-api-key --provider

`); until then you get a +misleading `Unknown model:

/`. Also, a restrictive `plugins.allow` +gates **bundled provider discovery** (`openclaw doctor --fix` sets +`plugins.bundledDiscovery: "compat"`). + +**Audit pillar.** Sessions are real JSONL at +`~/.openclaw/agents//sessions/.jsonl` (UUID-named) + a +`sessions.json` index keyed by sessionKey. Siblings `.trajectory.jsonl` +(OTel trace) and `.trajectory-path.json` are **ignored**. `OPENCLAW_HOME` +overrides the home dir for tests. Transcript downloads stream the real file +(no synthesis, unlike Hermes). + +For production users the recommended OpenClaw install is: +```bash +failproofai policies --install --cli openclaw --scope user +``` + +### Factory droid hooks (`~/.factory/hooks.json`) + +Factory's **droid** CLI is a **dual-pillar** integration (live hooks + audit), +supporting **user + project** scope. Unlike Hermes/OpenClaw it uses a +Claude/Codex-style **external shell-hook system** — the installed command is +`bun bin/failproofai.mjs --hook --cli factory` (dev) / +`npx -y failproofai --hook --cli factory` (production project scope). +The entire contract below was **verified live against droid v0.171.0**. + +**Schema: event names at the TOP LEVEL — NO `"hooks"` wrapper.** The published +docs are **wrong**: droid rejects a `{"hooks":{…}}` wrapper with +`WARN Ignoring unknown hook event keys keys:["hooks"]`. The `hooks.json` file +**is** the events object: + +```json +{ "PreToolUse": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "…", "timeout": 30 } ] } ], + "PostToolUse": [ { "matcher": "*", "hooks": [ … ] } ], + "Stop": [ { "hooks": [ … ] } ] } +``` + +Tool events (`PreToolUse`, `PostToolUse`) MUST include `"matcher": "*"` (matches +all tools per the docs). Non-tool events use `{ "hooks": [ … ] }` with **no** +matcher. `writeHookEntries` in `integrations.ts` branches on this; the file has +**no** top-level wrapper key, so `removeHooksFromFile` / `hooksInstalledInSettings` +iterate the top-level event keys directly. + +**Events** (`FACTORY_HOOK_EVENT_TYPES`, all PascalCase): `SessionStart`, +`UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `Notification`, `Stop`, +`SubagentStop`, `PreCompact`, `SessionEnd`. Because they're already canonical +there is **no `FACTORY_EVENT_MAP` and no `handler.ts` branch**. The stdin +payload is Claude snake_case (`session_id`, `transcript_path`, `cwd`, +`permission_mode`, `hook_event_name`, `tool_name`, `tool_input:{command,…}`), so +**no payload normalization** is needed. + +**Tool-name canonicalization** (`FACTORY_TOOL_MAP`): `Execute→Bash`, `Read→Read`, +`Edit→Edit`, `Create→Write`, `Grep→Grep`, `Glob→Glob`, `LS→LS`, +`FetchUrl→WebFetch`, `WebSearch→WebSearch`, `TodoWrite→TodoWrite`, `Task→Task`. +`tool_input.command` is already the canonical Bash key, so there is **no** +`FACTORY_TOOL_INPUT_MAP`. + +**Deny contract = EXIT CODE 2 + reason on stderr** (NOT a JSON decision — droid +ignores a `{decision:…}` object on tool events and drives blocking purely off +exit code 2; verified live: `Hook returned exit code 2, throwing +ToolExecutionControlError`). The **`Stop`** event is the exception — droid does +**not** support exit-2 force-retry there, so we emit `{decision:"block", +reason}` JSON on **stdout at exit 0** (docs: "if decision is block, Droid does +not stop"). So `policy-evaluator.ts`'s `cli === "factory"` branch: Stop → +exit 0 + `{decision:"block", reason: }`; every other +event (PreToolUse, PostToolUse, …) → exit 2 + the blocked message on stderr. +`instruct()` degrades to allow + stderr note on non-Stop events (no +additional-context channel), and to the Stop `{decision:"block"}` shape on Stop. + +**Audit pillar.** Sessions are real JSONL at +`~/.factory/sessions//.jsonl` (Claude-style encoded-cwd +folders, e.g. `-home-chetan`), one per session alongside a +`.settings.json` sibling we **ignore**. JSONL lines: +`{type:"session_start", id, cwd, …}` (carries cwd), +`{type:"message", message:{role, content:[…], visibility}, timestamp, …}`, +`{type:"compaction_state", …}`. `lib/factory-sessions.ts` (pure parser, cloned +from the Claude/OpenClaw pattern) + `lib/factory-projects.ts` enumerate and +parse them; `FACTORY_HOME` overrides the home dir for tests. Transcript +downloads stream the real file (no synthesis). + +For production users the recommended Factory install is: +```bash +failproofai policies --install --cli factory --scope project +``` + +### Devin CLI hooks (`~/.config/devin/config.json` / `.devin/config.json`) + +Devin's CLI (Cognition) is a **dual-pillar** integration (live hooks + audit), +supporting **user + project** scope. It is a **pure Claude-clone** — the entire +contract below was **verified live against devin v3000.1.27** — so the +Integration in `integrations.ts` mirrors `claudeCode` verbatim, changing only +`getSettingsPath` and the `--cli devin` command flag. + +**Config lives under a Claude-style `"hooks"` key** (the file also holds +`org_id`, `theme_mode`, etc., so `readSettings`/`writeSettings` use the +merge-preserving `readJsonFile`/`writeJsonFile` helpers like Claude/Copilot): + +| Scope | Path | +|---------|-----------------------------------| +| user | `~/.config/devin/config.json` | +| project | `/.devin/config.json` | + +There is **no `local` scope**. Devin does not expose a `$DEVIN_PROJECT_DIR`; the +installed command uses `"" --hook --cli devin` (user) / +`npx -y failproofai --hook --cli devin` (project). `buildHookEntry` +emits `{type:"command", command, timeout:60, [FAILPROOFAI_HOOK_MARKER]:true}` +— Devin reads Claude's seconds-based `timeout`. + +**Events** (`DEVIN_HOOK_EVENT_TYPES`, all PascalCase → **no `DEVIN_EVENT_MAP`, +no `handler.ts` branch**): `SessionStart`, `UserPromptSubmit`, `PreToolUse`, +`PostToolUse`, `PermissionRequest`, `Stop`, `SessionEnd`. The stdin payload is +**pure Claude snake_case → no normalization**: PreToolUse `{hook_event_name, +tool_name:"exec", tool_input:{command}, tool_use_id}`; PostToolUse adds +`tool_response:{success, output, error}`; Stop `{stop_hook_active}`. + +**Tool-name canonicalization** (`DEVIN_TOOL_MAP`): `exec→Bash` only (the shell +tool; `tool_input.command` is already the canonical Bash key, so there is **no** +`DEVIN_TOOL_INPUT_MAP`). All other Devin tool names pass through unchanged. + +**Deny contract = `{"decision":"block","reason"}` JSON on stdout at exit 0** +(VERIFIED live — the block overrode `--permission-mode dangerous`). +`policy-evaluator.ts`'s `cli === "devin"` deny branch emits this shape for +**every** event: non-Stop → `{decision:"block", reason: blockedMessage}`; +Stop → `{decision:"block", reason: }` (force-retry for the +5 `require-*-before-stop` builtins). **Instruct**: Devin is Claude-compatible, so +on Stop it emits the `{decision:"block"}` MANDATORY text, and every +context-injection event falls through to the generic Claude +`{hookSpecificOutput:{hookEventName, additionalContext}}` path. + +**Audit pillar** (VERIFIED): sessions live in SQLite at +`~/.local/share/devin/cli/sessions.db`. The `sessions` table has one row per +session **with a real `working_directory`** (so sessions group by project cwd +like Claude, unlike cwd-less Hermes); `message_nodes.chat_message` is +OpenAI-style JSON (`{role, content, tool_calls?:[{id, name, arguments}], +tool_call_id?}`). `lib/devin-sessions.ts` (pure, unit-tested parser, cloned from +the Hermes SQLite pattern) + `lib/devin-projects.ts` enumerate and parse them via +the shared WAL-aware `lib/sqlite-reader.ts`; `resolve-transcript-path.ts` returns +a `devin-db://` virtual path (like opencode) and `download-session.ts` +synthesizes a JSONL export. `DEVIN_HOME` (data dir) / `DEVIN_DB_PATH` (db file) +override for tests. + +For production users the recommended Devin install is: +```bash +failproofai policies --install --cli devin --scope project +``` + +### Antigravity CLI hooks (`~/.gemini/config/hooks.json` / `.agents/hooks.json`) + +Antigravity (`agy`) is the 11th CLI — a **dual-pillar** integration (live-hook +enforcement **and** audit) that, unlike Factory/Devin, has its **OWN** hook +contract (NOT a Claude-clone). Verified **live against agy v1.1.2** (shipped +docs at `~/.gemini/antigravity-cli/builtin/skills/agy-customizations/docs/hooks.md`). +Binary probe: `agy`. + +**NAMED-hook schema.** `hooks.json`'s top-level key is a hook *name* +(`"failproofai"`), whose value is an event→handlers map. Tool events +(`PreToolUse`/`PostToolUse`) wrap handlers in a `{matcher, hooks:[…]}` group; +non-tool events (`PreInvocation`/`Stop`) are **flat** arrays of handler objects: + +```json +{ "failproofai": { + "PreToolUse": [ { "matcher": "*", "hooks": [ { "type":"command", "command":"…", "timeout":30 } ] } ], + "PostToolUse": [ { "matcher": "*", "hooks": [ { "type":"command", "command":"…", "timeout":30 } ] } ], + "PreInvocation": [ { "type":"command", "command":"…", "timeout":30 } ], + "Stop": [ { "type":"command", "command":"…", "timeout":30 } ] } } +``` + +`writeHookEntries`/`removeHooksFromFile`/`hooksInstalledInSettings` operate under +`settings.failproofai`, handling both the wrapper (`{matcher,hooks:[]}`) and flat +(`[{…}]`) shapes, and preserve any other named-hook keys (e.g. a user's +`"lint-checker"`). Multiple named hooks merge; `"enabled": false` disables one. +Settings paths: + +| Scope | Path | +|---------|-------------------------------| +| user | `~/.gemini/config/hooks.json` | +| project | `/.agents/hooks.json` | + +No `local` scope. `timeout` is in **seconds** (30). Command: project +`npx -y failproofai --hook --cli antigravity`; user +`"" --hook --cli antigravity`. `ANTIGRAVITY_HOOK_EVENT_TYPES` += `["PreToolUse","PostToolUse","PreInvocation","Stop"]`. + +**Event map.** `ANTIGRAVITY_EVENT_MAP`: `PreToolUse→PreToolUse`, +`PostToolUse→PostToolUse`, `PreInvocation→UserPromptSubmit`, `Stop→Stop`. The +`canonicalizeEventType` branch in `handler.ts` maps the `--hook` arg. + +**camelCase → snake_case normalization.** Antigravity pipes camelCase protojson. +`handler.ts` normalizes it right after `JSON.parse` (before any canonicalization): +`toolCall.{name,args}` → `tool_name`/`tool_input`, `conversationId` → +`session_id`, `workspacePaths[0]` → `cwd`, `transcriptPath` → `transcript_path`. +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). + +**Response shapes (Antigravity's OWN — `policy-evaluator.ts` `cli === "antigravity"`):** + +| Case | Shape (exit 0) | +|------|----------------| +| Deny (tool/prompt events) | `{decision:"deny", reason}` | +| Deny on `Stop` | `{decision:"continue", reason}` — `"continue"` re-enters the loop (how `require-*-before-stop` enforces) | +| Instruct on `UserPromptSubmit` (canonical for `PreInvocation`) | `{injectSteps:[{ephemeralMessage:"Instruction from failproofai: …"}]}` | +| Instruct on `Stop` | `{decision:"continue", reason}` | +| Instruct on other events | stderr note only (degrade like Hermes) | + +**Audit pillar.** Plain-JSONL transcripts at +`~/.gemini/antigravity-cli/brain//.system_generated/logs/transcript_full.jsonl` +(one step per line: `{step_index, source, type, status, created_at, content?, +tool_calls?}`). `type` enum (uppercase): `USER_INPUT` → user message, +`PLANNER_RESPONSE` (text and/or `tool_calls:[{name, args}]`) → assistant turn, +`` (e.g. `RUN_COMMAND`, the uppercased tool name) → the result step paired +back onto the matching `tool_use`, `CONVERSATION_HISTORY`/`CHECKPOINT` → skipped. +The conversation index is SQLite at `conversation_summaries.db` +(`conversation_summaries` table: `conversation_id, title, step_count, +workspace_uris, last_modified_time, …`) — read via `lib/sqlite-reader.ts` for +title/cwd enrichment, but the `brain/` transcripts are the source of truth for +existence (the DB can be checkpointed empty). `workspace_uris` gives cwd → +per-project grouping; when absent we recover cwd from the first `run_command` +`Cwd` arg, else a synthetic `antigravity` project. `lib/antigravity-sessions.ts` +(pure parser + `findAntigravityTranscript`) + `lib/antigravity-projects.ts` + +`src/audit/cli-adapters/antigravity.ts`. `ANTIGRAVITY_HOME` overrides the home +dir for tests. + +For production users the recommended Antigravity install is: +```bash +failproofai policies --install --cli antigravity --scope project +``` + +### VS Code agent hooks (covered by the `copilot` / `claude` integrations — no dedicated id) + +VS Code's built-in **Copilot Chat agent mode** (Preview, `github.copilot-chat`) +ships its own lifecycle-hooks engine, but it is NOT a separate failproofai +integration — it reuses hook-config paths failproofai already writes, so it's +covered for free. Verified live against VS Code 1.127 (`github.copilot-chat` +v0.55.0): the agent discovers and loads hook configs from **`.github/hooks/*.json`**, +**`~/.copilot/hooks/*.json`**, and **`~/.claude/settings.json`** (governed by the +`chat.hookFilesLocations` setting, whose default includes all three), using the +Claude-shaped `{hookSpecificOutput:{permissionDecision:"deny",permissionDecisionReason}}` +contract over a snake_case stdin payload. + +Those are exactly the paths the **`copilot`** integration +(`.github/hooks/failproofai.json` project, `~/.copilot/hooks/failproofai.json` +user) and the **`claude`** integration (`~/.claude/settings.json`) already write. +So **`failproofai policies --install --cli copilot`** (or `--cli claude`) already +enforces inside VS Code agent-mode sessions — no `vscode` id, no new code. (VS +Code's logs were observed loading `failproofai.json` from both `[local] .github/hooks/` +and `[user] ~/.copilot/hooks/` during a live probe.) + +**Caveat:** the hooks feature is a **Preview** and requires an active GitHub +Copilot subscription + agent mode; the OpenAI ChatGPT / Claude Code VS Code +extensions are separate runtimes (the Claude Code extension routes through +failproofai's `claude` hooks in `~/.claude/settings.json` — also already covered). + +### Goose hooks (`~/.agents/plugins/failproofai/hooks/hooks.json`) + +Goose (codename goose, Block) is a **local, MCP-based** dev-agent — a +**dual-pillar** integration (live hooks + audit) supporting **user + project** +scope. The entire contract below was **verified live against goose v1.43.0**. + +**Enforcement uses Goose's "hooks" system — the cross-agent Open Plugins spec.** +A plugin is a directory whose `hooks/hooks.json` wires shell commands into agent +events; Goose **auto-discovers** any dir under `~/.agents/plugins//` (user) +or `/.agents/plugins//` (project) at startup and **self-registers** it +into `~/.config/goose/config.yaml`. So the installer just **drops the plugin dir** +(no config edit — simpler than OpenCode). The installed command is +`bun bin/failproofai.mjs --hook --cli goose` (dev) / +`npx -y failproofai --hook --cli goose` (production). + +**Schema: an Open Plugins `hooks.json` WITH a top-level `"hooks"` wrapper** (unlike +Factory, which has none), and **the matcher is OMITTED on every event** — a bare +`"*"` is an **invalid regex that matches nothing** (verified live; omitted = match +all). failproofai owns the entire `failproofai` plugin dir, so entries stay the +clean `{type, command}` shape (**no `__failproofai_hook__` marker** — Goose parses +this file); our hooks are identified by the `--cli goose` command substring. + +```json +{ "hooks": { + "PreToolUse": [ { "hooks": [ { "type": "command", "command": "…" } ] } ], + "PostToolUse": [ { "hooks": [ … ] } ], + "SessionStart":[ { "hooks": [ … ] } ] } } +``` + +**Events** (`GOOSE_HOOK_EVENT_TYPES`, all PascalCase → **no `GOOSE_EVENT_MAP`, no +handler event branch**): `SessionStart`, `UserPromptSubmit`, `PreToolUse`, +`PostToolUse`, `SessionEnd`. The stdin payload uses `event` (not +`hook_event_name`) and `working_dir` (not `cwd`), so `handler.ts` normalizes +`working_dir`→`cwd` for goose (a small block, like Antigravity); `tool_name` / +`tool_input` are already canonical field names. + +**Deny contract = `{"decision":"block","reason"}` JSON on stdout at exit 0**, honored +on **`PreToolUse` ONLY** (shipped goose ≥ **v1.37.0**, PR block/goose#9304; exit 2 +also blocks). Any other error/timeout → **fail-open** (allow). `PreToolUse` fires +for the shell tool **and inside delegated subagents**, so it is the single +sufficient deny point. Goose has **NO `Stop` event** (the 5 +`require-*-before-stop` builtins never fire for it — inapplicable, like Hermes) and +does **not** honor deny on `UserPromptSubmit`/`PostToolUse` (observation only). So +`policy-evaluator.ts`'s `cli === "goose"` deny branch emits the block JSON for +every event (Goose honors it on PreToolUse, ignores it elsewhere — no Stop +special-case). `instruct()` degrades to **allow + stderr note** (no +additional-context channel — a non-block decision injects nothing). + +**Tool-name canonicalization** (`GOOSE_TOOL_MAP`): tool names arrive **both** bare +(`shell→Bash`, `write→Write`, `edit→Edit`, `view→Read`, `read_image→Read`, +`glob→Glob`, `grep→Grep`, `tree→LS`, `delegate→Task`) **and** `__` +namespaced (`todo__todo_write→TodoWrite`); the map covers both, unknown tools pass +through. **`GOOSE_TOOL_INPUT_MAP`** maps path-bearing tools' `path` (and +read_image's `source`) → `file_path` so path builtins fire; shell's `command` is +already canonical. + +**Audit pillar.** Sessions are SQLite at +`~/.local/share/goose/sessions/sessions.db` (schema_version 15). `sessions` rows +carry a real `working_dir`, so audit groups by project cwd like **Devin** (not +grouped-by-source like Hermes); `messages.content_json` is a **Claude-style +typed-block array** (`toolRequest`/`toolResponse`) parsed by `lib/goose-sessions.ts` +(pure, unit-tested) + `lib/goose-projects.ts`. `session_type='hidden'` +(`--no-session`) scratch runs are filtered. `GOOSE_HOME` / `GOOSE_DB_PATH` override +the data dir for tests. Transcript downloads synthesize JSONL from the rows. + +**Provider gotcha** (not failproofai's concern, but hit during live verification): +with the OS keyring disabled, goose reads the provider API key from the +**environment** (`OPENAI_API_KEY`), **not** from `config.yaml` — the YAML key is +ignored (`401 No api key passed in`). + +For production users the recommended Goose install is: +```bash +failproofai policies --install --cli goose --scope project +``` + +### Dogfood configs for Factory / Devin / Antigravity / Goose + +Like the Codex / Cursor / OpenCode / Pi setups above, this repo ships +**project-scope dogfood configs** for the four newest CLIs so failproofai +enforces on itself when you drive this repo with them. Each uses the dev +`bun bin/failproofai.mjs --hook --cli ` command (never the `npx` +production form — same self-reference caveat as the others): + +| CLI | Dogfood path | Schema | +|-----|--------------|--------| +| Factory (`droid`) | `.factory/hooks.json` | top-level event keys, `matcher:"*"` on tool events (no `"hooks"` wrapper) | +| Devin | `.devin/config.json` | Claude `"hooks"` wrapper | +| Antigravity (`agy`) | `.agents/hooks.json` | named-hook schema under the `failproofai` key | +| Goose | `.agents/plugins/failproofai/hooks/hooks.json` | Open Plugins (auto-discovered; matcher omitted — a bare `*` matches nothing) | + +These were generated from each integration's own `writeHookEntries`, so they +track the live schema. See each CLI's architecture section above for the full +contract. As with the other CLIs, do **not** run +`failproofai policies --install --cli ` from inside this repo — it would +overwrite the dev `bun bin/failproofai.mjs` path with the production `npx` form. + ## Workflow rules ### One PR per branch diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fc42a2f3..8da1b944 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,7 @@ The dev server starts at `http://localhost:8020`. ### Build before the in-repo dev hooks will work This repo **dogfoods failproofai on itself**: `.claude/settings.json` (and the -sibling `.codex/`, `.cursor/`, `.gemini/`, `.github/hooks/`, … configs) register +sibling `.codex/`, `.cursor/`, `.github/hooks/`, … configs) register hooks that run `bun bin/failproofai.mjs --hook `. Those hooks load the custom policies in `.failproofai/policies/*.mjs`, which `import` the `failproofai` package — resolved against the **compiled `dist/index.js` bundle**. diff --git a/README.md b/README.md index 9fbfcdf3..7d909470 100644 --- a/README.md +++ b/README.md @@ -29,61 +29,90 @@ before they become incidents. Zero latency. Runs locally. Claude Code -        - +      + OpenAI Codex -        - +      + GitHub Copilot -        - +      + Cursor Agent -

-

- +      + OpenCode -        - +      + Pi -        - +

+

+ - - Gemini CLI + + Hermes -        - +      + + OpenClaw + +      + - - Hermes + + Factory Droid + + +      + + Devin CLI + +      + + Antigravity CLI + +      + + + + Goose

-> Install hooks for one or any combination: `failproofai policies --install --cli opencode pi gemini` (or `--cli claude codex copilot cursor opencode pi gemini hermes`). Omit `--cli` to auto-detect installed CLIs and prompt. +> Install hooks for one or any combination: `failproofai policies --install --cli opencode pi` (or `--cli claude codex copilot cursor opencode pi hermes openclaw factory devin antigravity goose`). Omit `--cli` to auto-detect installed CLIs and prompt. > > **Hermes** (hermes-agent, a Slack/Telegram gateway) is supported for both **live-hook enforcement** (`--cli hermes` — one install intercepts tool calls from every platform and subagent) and offline **audit** replay of its gateway sessions from the single `~/.hermes/state.db`. +> +> **OpenClaw** (openclaw gateway, a self-hosted multi-channel assistant) is supported for both **live-hook enforcement** (`--cli openclaw`, user-scope) and offline **audit** replay of its JSONL sessions (`~/.openclaw/agents//sessions/*.jsonl`). Enforcement uses OpenClaw's **in-process plugin hooks** (a shipped `openclaw-plugin/` that async-spawns failproofai — its file-based internal hooks are observation-only and can't block): `before_tool_call` blocks a tool, and `before_agent_finalize` is a real turn-end gate, so the `require-*-before-stop` builtins enforce. +> +> **Factory Droid** (`droid`) is supported for both **live-hook enforcement** (`--cli factory`, user + project scope) and offline **audit** replay of its on-disk JSONL sessions. droid blocks tool calls off hook **exit code 2** (not a JSON decision) and honors `{decision:"block"}` only on the turn-end `Stop` event — failproofai emits the right shape per event automatically. +> +> **Devin CLI** (`devin`, Cognition) is supported for both **live-hook enforcement** (`--cli devin`, user + project scope) and offline **audit** replay of its SQLite sessions (`~/.local/share/devin/cli/sessions.db`). Devin is a **pure Claude-clone** — same event names, same snake_case payload, same `"hooks"`-wrapper config (`~/.config/devin/config.json` / `/.devin/config.json`) — blocking via `{decision:"block"}` JSON on every event. +> +> **Antigravity CLI** (`agy`) is supported for both **live-hook enforcement** (`--cli antigravity`, user + project scope) and offline **audit** replay of its plain-JSONL sessions (`~/.gemini/antigravity-cli/brain//…/transcript_full.jsonl`). Antigravity has its **own** contract (not a Claude-clone): a **named-hook** `hooks.json` schema (`~/.gemini/config/hooks.json` / `/.agents/hooks.json`), a camelCase stdin payload that failproofai normalizes, and its own response shapes — `{decision:"deny"}` to block a tool, `{decision:"continue"}` to force another turn at `Stop`, `{injectSteps}` to inject a reminder before the model runs. +> +> **Goose** (codename goose, Block) is supported for both **live-hook enforcement** (`--cli goose`, user + project scope) and offline **audit** replay of its SQLite sessions (`~/.local/share/goose/sessions/sessions.db`). Enforcement uses Goose's **hooks** system (the cross-agent **Open Plugins** spec) — the installer just drops a plugin dir at `~/.agents/plugins/failproofai/` and Goose auto-discovers it. Blocking is `{"decision":"block"}` JSON on the `PreToolUse` event (which fires for the shell tool and inside delegated subagents), verified live against goose v1.43.0; Goose has no turn-end `Stop` event, so the `require-*-before-stop` builtins don't apply (as with Hermes). --- diff --git a/__tests__/actions/get-hooks-config.test.ts b/__tests__/actions/get-hooks-config.test.ts index 72079bcc..b88cce8a 100644 --- a/__tests__/actions/get-hooks-config.test.ts +++ b/__tests__/actions/get-hooks-config.test.ts @@ -17,7 +17,6 @@ const installedFlags: Record = { cursor: false, opencode: false, pi: false, - gemini: false, }; const detectedFlags: Record = { @@ -27,11 +26,10 @@ const detectedFlags: Record = { cursor: false, opencode: false, pi: false, - gemini: false, }; vi.mock("@/src/hooks/integrations", () => { - const ids = ["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini"] as const; + const ids = ["claude", "codex", "copilot", "cursor", "opencode", "pi"] as const; const make = (id: (typeof ids)[number]) => ({ id, displayName: id, @@ -51,18 +49,18 @@ describe("getHooksConfigAction — clis payload", () => { // reset to baseline Object.assign(installedFlags, { claude: true, codex: false, copilot: false, cursor: false, - opencode: false, pi: false, gemini: false, + opencode: false, pi: false, }); Object.assign(detectedFlags, { claude: true, codex: true, copilot: false, cursor: false, - opencode: false, pi: false, gemini: false, + opencode: false, pi: false, }); }); it("returns one entry per CLI in registry order", async () => { const config = await getHooksConfigAction(); expect(config.clis.map((c) => c.id)).toEqual([ - "claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini", + "claude", "codex", "copilot", "cursor", "opencode", "pi", ]); }); @@ -70,14 +68,11 @@ describe("getHooksConfigAction — clis payload", () => { const config = await getHooksConfigAction(); const claude = config.clis.find((c) => c.id === "claude")!; const codex = config.clis.find((c) => c.id === "codex")!; - const gemini = config.clis.find((c) => c.id === "gemini")!; expect(claude.installed).toBe(true); expect(claude.detected).toBe(true); expect(codex.installed).toBe(false); expect(codex.detected).toBe(true); - expect(gemini.installed).toBe(false); - expect(gemini.detected).toBe(false); }); it("carries the per-CLI user-scope settingsPath", async () => { @@ -94,6 +89,5 @@ describe("getHooksConfigAction — clis payload", () => { const config = await getHooksConfigAction(); expect(config.clis.find((c) => c.id === "claude")!.label).toBe("Claude Code"); expect(config.clis.find((c) => c.id === "codex")!.label).toBe("OpenAI Codex"); - expect(config.clis.find((c) => c.id === "gemini")!.label).toBe("Gemini CLI"); }); }); diff --git a/__tests__/components/project-list.test.tsx b/__tests__/components/project-list.test.tsx index 90ce01aa..6d5440af 100644 --- a/__tests__/components/project-list.test.tsx +++ b/__tests__/components/project-list.test.tsx @@ -252,8 +252,12 @@ describe("ProjectList", () => { "Cursor Agent", "OpenCode", "Pi", - "Gemini CLI", "Hermes", + "OpenClaw", + "Factory Droid", + "Devin CLI", + "Antigravity CLI", + "Goose", ]); }); diff --git a/__tests__/e2e/helpers/hook-runner.ts b/__tests__/e2e/helpers/hook-runner.ts index 7a8ab034..caa3919c 100644 --- a/__tests__/e2e/helpers/hook-runner.ts +++ b/__tests__/e2e/helpers/hook-runner.ts @@ -40,7 +40,7 @@ export interface HookRunResult { export function runHook( event: string, payload: Record, - opts?: { homeDir?: string; cli?: "claude" | "codex" | "copilot" | "cursor" | "opencode" | "pi" | "gemini" }, + opts?: { homeDir?: string; cli?: "claude" | "codex" | "copilot" | "cursor" | "opencode" | "pi" | "hermes" | "openclaw" | "factory" | "devin" | "antigravity" | "goose" }, ): HookRunResult { const binaryPath = getBinaryPath(); @@ -144,7 +144,7 @@ export function assertCursorStopBlock(result: HookRunResult): void { // Cursor's stop / subagentStop hooks honor `{followup_message}` on stdout // (exit 0) — auto-submitted as next user message, capped at loop_limit // (default 5). The flat `{permission: "deny"}` shape is ignored on Stop. - // Mirrors assertCopilotStopBlock and assertGeminiStopBlock. + // Mirrors assertCopilotStopBlock. // Ref: https://cursor.com/docs/hooks expect(result.exitCode).toBe(0); expect(typeof result.parsed?.followup_message).toBe("string"); @@ -180,30 +180,90 @@ export function assertPiAllow(result: HookRunResult): void { } } -// ── Gemini-shaped assertions ─────────────────────────────────────────────── -// Gemini uses a flat `{decision: "deny", reason}` JSON shape per its "Golden -// Rule" exit-0 contract. Stop policies emit `{decision: "block", reason}` to -// trigger AfterAgent's force-retry. Context injection uses Claude's -// `{hookSpecificOutput: {hookEventName, additionalContext}}` shape but with -// the hookEventName carrying the raw Gemini event name (BeforeTool/AfterTool/ -// BeforeAgent/SessionStart). Ref: https://geminicli.com/docs/hooks/ +// ── Factory (droid) assertions ───────────────────────────────────────────── +// Factory drives tool blocking off EXIT CODE 2 + stderr (it ignores a JSON +// decision on tool events — verified live against droid v0.171.0). The Stop +// event is the exception: droid reads `{decision:"block", reason}` on stdout. -export function assertGeminiDeny(result: HookRunResult): void { +export function assertFactoryDeny(result: HookRunResult): void { + // Non-Stop deny: exit 2, no stdout, blocked message on stderr. + expect(result.exitCode).toBe(2); + expect(result.stdout).toBe(""); + expect(result.stderr).toMatch(/Blocked/i); +} + +export function assertFactoryStopBlock(result: HookRunResult): void { + // Stop deny/instruct: droid honors `{decision:"block", reason}` on stdout at + // exit 0 ("if decision is block, Droid does not stop"). + expect(result.exitCode).toBe(0); + expect(result.parsed?.decision).toBe("block"); + expect(typeof result.parsed?.reason).toBe("string"); + expect(result.parsed?.reason).toMatch(/MANDATORY ACTION REQUIRED/); +} + +// ── Devin (Cognition) assertions ─────────────────────────────────────────── +// Devin is a pure Claude-clone that honors `{decision:"block", reason}` on +// stdout at exit 0 for EVERY event (verified live against devin v3000.1.27 — +// the block overrode `--permission-mode dangerous`). + +export function assertDevinDeny(result: HookRunResult): void { + // Non-Stop deny: exit 0, `{decision:"block", reason}` on stdout. + expect(result.exitCode).toBe(0); + expect(result.parsed?.decision).toBe("block"); + expect(typeof result.parsed?.reason).toBe("string"); +} + +export function assertDevinStopBlock(result: HookRunResult): void { + // Stop deny/instruct: `{decision:"block", reason}` carrying the force-retry + // MANDATORY ACTION wording. + expect(result.exitCode).toBe(0); + expect(result.parsed?.decision).toBe("block"); + expect(typeof result.parsed?.reason).toBe("string"); + expect(result.parsed?.reason).toMatch(/MANDATORY ACTION REQUIRED/); +} + +// ── Antigravity (agy) assertions ─────────────────────────────────────────── +// Antigravity has its OWN response shapes (NOT Claude's), verified live against +// agy v1.1.2: tool/prompt deny → `{decision:"deny", reason}` on stdout (exit 0); +// Stop deny/instruct → `{decision:"continue", reason}` (re-enters the loop); +// UserPromptSubmit instruct → `{injectSteps:[{ephemeralMessage}]}`. + +export function assertAntigravityDeny(result: HookRunResult): void { + // Tool/prompt deny: exit 0, `{decision:"deny", reason}` on stdout. expect(result.exitCode).toBe(0); expect(result.parsed?.decision).toBe("deny"); expect(typeof result.parsed?.reason).toBe("string"); expect(result.parsed?.reason).toMatch(/Blocked/i); - // Gemini uses the flat shape — no Claude-style hookSpecificOutput wrapper. - expect(result.parsed?.hookSpecificOutput).toBeUndefined(); } -export function assertGeminiStopBlock(result: HookRunResult): void { +export function assertAntigravityStopContinue(result: HookRunResult): void { + // Stop deny/instruct: `{decision:"continue", reason}` carrying the force-retry + // MANDATORY ACTION wording — "continue" re-enters the agent loop. expect(result.exitCode).toBe(0); - expect(result.parsed?.decision).toBe("block"); + expect(result.parsed?.decision).toBe("continue"); expect(typeof result.parsed?.reason).toBe("string"); expect(result.parsed?.reason).toMatch(/MANDATORY ACTION REQUIRED/); } +export function assertAntigravityInjectSteps(result: HookRunResult): void { + // UserPromptSubmit (canonical for PreInvocation) instruct → injectSteps with + // an ephemeralMessage carrying the failproofai instruction. + expect(result.exitCode).toBe(0); + const steps = result.parsed?.injectSteps as Array> | undefined; + expect(Array.isArray(steps)).toBe(true); + expect(steps?.[0]?.ephemeralMessage).toMatch(/^Instruction from failproofai:/); +} + +// ── Goose (codename goose, Block) assertions ─────────────────────────────── +// Goose honors `{decision:"block", reason}` on stdout at exit 0, on PreToolUse +// ONLY (verified live against goose v1.43.0). It has no Stop event, and does not +// honor deny on UserPromptSubmit/PostToolUse. +export function assertGooseDeny(result: HookRunResult): void { + expect(result.exitCode).toBe(0); + expect(result.parsed?.decision).toBe("block"); + expect(typeof result.parsed?.reason).toBe("string"); +} + export function assertCopilotStopBlock(result: HookRunResult): void { // Copilot's `agentStop` honors `{decision: "block", reason}` JSON on stdout // (exit 0) — the reason becomes the next-turn prompt and the agent retries. @@ -214,10 +274,3 @@ export function assertCopilotStopBlock(result: HookRunResult): void { expect(typeof result.parsed?.reason).toBe("string"); expect(result.parsed?.reason).toMatch(/MANDATORY ACTION REQUIRED/); } - -export function assertGeminiInstruct(result: HookRunResult, hookEventName: string): void { - expect(result.exitCode).toBe(0); - const output = result.parsed?.hookSpecificOutput as Record | undefined; - expect(output?.hookEventName).toBe(hookEventName); - expect(output?.additionalContext).toMatch(/^Instruction from failproofai:/); -} diff --git a/__tests__/e2e/helpers/payloads.ts b/__tests__/e2e/helpers/payloads.ts index 939ae74e..fb9918b3 100644 --- a/__tests__/e2e/helpers/payloads.ts +++ b/__tests__/e2e/helpers/payloads.ts @@ -13,9 +13,6 @@ const SESSION_ID = "test-session-e2e-001"; */ const TRANSCRIPT_PATH = "/dev/null"; -/** ISO-8601 timestamp used by integrations that include one in stdin (Gemini). */ -const TIMESTAMP = "2026-05-03T18:00:00.000Z"; - export const Payloads = { preToolUse: { bash(command: string, cwd: string): Record { @@ -591,163 +588,287 @@ export const PiPayloads = { }, }; -const GEMINI_SESSION_ID = "g1234567-9abc-7def-0123-456789abcdef"; - /** - * Gemini CLI hook payload shapes. - * - * Gemini sends Claude-shape stdin: snake_case fields (`session_id`, - * `tool_name`, `tool_input`, `hook_event_name`, `cwd`, `transcript_path`) - * plus `timestamp`. Tool names are snake_case (`run_shell_command`, - * `read_file`, `write_file`, `replace`, etc.) — the binary canonicalizes - * these to PascalCase via GEMINI_TOOL_MAP before policy lookup. - * - * Per https://geminicli.com/docs/hooks/ as of 2026-04-13. + * Factory (droid) payload factories. droid's hook stdin is Claude snake_case + * (`session_id`, `transcript_path`, `cwd`, `permission_mode`, `hook_event_name`, + * `tool_name`, `tool_input`) with already-PascalCase event names — so no payload + * or event canonicalization is needed. droid's tool registry uses + * `Execute`/`Read`/`Edit`/`Create`/… which the handler maps to Claude builtins + * via FACTORY_TOOL_MAP (see src/hooks/types.ts). Verified live against droid + * v0.171.0. */ -export const GeminiPayloads = { - beforeTool: { - runShellCommand(command: string, cwd: string): Record { +const FACTORY_SESSION_ID = "test-session-factory-001"; + +export const FactoryPayloads = { + preToolUse: { + bash(command: string, cwd: string): Record { return { - session_id: GEMINI_SESSION_ID, + session_id: FACTORY_SESSION_ID, transcript_path: TRANSCRIPT_PATH, cwd, - hook_event_name: "BeforeTool", - timestamp: TIMESTAMP, - tool_name: "run_shell_command", + permission_mode: "default", + hook_event_name: "PreToolUse", + tool_name: "Execute", tool_input: { command }, }; }, - readFile(filePath: string, cwd: string): Record { + write(filePath: string, content: string, cwd: string): Record { return { - session_id: GEMINI_SESSION_ID, + session_id: FACTORY_SESSION_ID, transcript_path: TRANSCRIPT_PATH, cwd, - hook_event_name: "BeforeTool", - timestamp: TIMESTAMP, - tool_name: "read_file", - tool_input: { file_path: filePath }, + permission_mode: "default", + hook_event_name: "PreToolUse", + tool_name: "Create", + tool_input: { file_path: filePath, content }, }; }, - writeFile(filePath: string, content: string, cwd: string): Record { + read(filePath: string, cwd: string): Record { return { - session_id: GEMINI_SESSION_ID, + session_id: FACTORY_SESSION_ID, transcript_path: TRANSCRIPT_PATH, cwd, - hook_event_name: "BeforeTool", - timestamp: TIMESTAMP, - tool_name: "write_file", - tool_input: { file_path: filePath, content }, + permission_mode: "default", + hook_event_name: "PreToolUse", + tool_name: "Read", + tool_input: { file_path: filePath }, }; }, - replace(filePath: string, oldStr: string, newStr: string, cwd: string): Record { + }, + postToolUse: { + bash(command: string, output: string, cwd: string): Record { return { - session_id: GEMINI_SESSION_ID, + session_id: FACTORY_SESSION_ID, transcript_path: TRANSCRIPT_PATH, cwd, - hook_event_name: "BeforeTool", - timestamp: TIMESTAMP, - tool_name: "replace", - tool_input: { file_path: filePath, old_string: oldStr, new_string: newStr }, + permission_mode: "default", + hook_event_name: "PostToolUse", + tool_name: "Execute", + tool_input: { command }, + tool_response: output, }; }, - mcpExtension(toolName: string, input: Record, cwd: string): Record { + }, + userPromptSubmit(prompt: string, cwd: string): Record { + return { + session_id: FACTORY_SESSION_ID, + transcript_path: TRANSCRIPT_PATH, + cwd, + permission_mode: "default", + hook_event_name: "UserPromptSubmit", + prompt, + }; + }, + stop(cwd: string): Record { + return { + session_id: FACTORY_SESSION_ID, + transcript_path: TRANSCRIPT_PATH, + cwd, + permission_mode: "default", + hook_event_name: "Stop", + }; + }, +}; + +/** + * Devin (Cognition) payload factories. Devin is a pure Claude-clone: its hook + * stdin is Claude snake_case (`session_id`, `transcript_path`, `cwd`, + * `permission_mode`, `hook_event_name`, `tool_name`, `tool_input`) with + * already-PascalCase event names — so no payload or event canonicalization is + * needed. Devin's shell tool is `exec` (mapped to Bash via DEVIN_TOOL_MAP); + * `tool_input.command` is already canonical. Verified live against devin + * v3000.1.27. + */ +const DEVIN_SESSION_ID = "test-session-devin-001"; + +export const DevinPayloads = { + preToolUse: { + bash(command: string, cwd: string): Record { return { - session_id: GEMINI_SESSION_ID, + session_id: DEVIN_SESSION_ID, transcript_path: TRANSCRIPT_PATH, cwd, - hook_event_name: "BeforeTool", - timestamp: TIMESTAMP, - tool_name: toolName, - tool_input: input, + permission_mode: "default", + hook_event_name: "PreToolUse", + tool_name: "exec", + tool_input: { command }, + tool_use_id: "call_devin_0001", }; }, }, - afterTool: { - runShellCommand(command: string, output: string, cwd: string): Record { + postToolUse: { + bash(command: string, output: string, cwd: string): Record { return { - session_id: GEMINI_SESSION_ID, + session_id: DEVIN_SESSION_ID, transcript_path: TRANSCRIPT_PATH, cwd, - hook_event_name: "AfterTool", - timestamp: TIMESTAMP, - tool_name: "run_shell_command", + permission_mode: "default", + hook_event_name: "PostToolUse", + tool_name: "exec", tool_input: { command }, - tool_response: { llmContent: output, returnDisplay: output }, + tool_response: { success: true, output, error: null }, + tool_use_id: "call_devin_0001", }; }, }, - beforeAgent(prompt: string, cwd: string): Record { + userPromptSubmit(prompt: string, cwd: string): Record { return { - session_id: GEMINI_SESSION_ID, + session_id: DEVIN_SESSION_ID, transcript_path: TRANSCRIPT_PATH, cwd, - hook_event_name: "BeforeAgent", - timestamp: TIMESTAMP, + permission_mode: "default", + hook_event_name: "UserPromptSubmit", prompt, }; }, - afterAgent(prompt: string, response: string, cwd: string): Record { + stop(cwd: string): Record { return { - session_id: GEMINI_SESSION_ID, + session_id: DEVIN_SESSION_ID, transcript_path: TRANSCRIPT_PATH, cwd, - hook_event_name: "AfterAgent", - timestamp: TIMESTAMP, - prompt, - prompt_response: response, + permission_mode: "default", + hook_event_name: "Stop", stop_hook_active: false, }; }, - sessionStart(cwd: string, source: "startup" | "resume" | "clear" = "startup"): Record { - return { - session_id: GEMINI_SESSION_ID, - transcript_path: TRANSCRIPT_PATH, - cwd, - hook_event_name: "SessionStart", - timestamp: TIMESTAMP, - source, - }; +}; + +/** + * Antigravity (agy) payload factories. Antigravity pipes a camelCase protojson + * payload (`toolCall:{name,args}`, `conversationId`, `workspacePaths`, + * `transcriptPath`) — the handler normalizes these to snake_case before + * canonicalization. `run_command`'s args are PascalCase (`CommandLine`, `Cwd`). + * Verified live against agy v1.1.2. Note: no `hook_event_name` field — the + * event comes solely from the `--hook ` arg. + */ +const ANTIGRAVITY_CONVERSATION_ID = "test-conversation-antigravity-001"; + +export const AntigravityPayloads = { + preToolUse: { + bash(command: string, cwd: string): Record { + return { + conversationId: ANTIGRAVITY_CONVERSATION_ID, + workspacePaths: [cwd], + transcriptPath: TRANSCRIPT_PATH, + modelName: "auto", + stepIdx: 19, + toolCall: { + name: "run_command", + args: { CommandLine: command, Cwd: cwd, WaitMsBeforeAsync: 5000 }, + }, + }; + }, }, - sessionEnd(cwd: string, reason: "exit" | "clear" | "logout" | "prompt_input_exit" | "other" = "exit"): Record { + postToolUse: { + bash(command: string, cwd: string): Record { + return { + conversationId: ANTIGRAVITY_CONVERSATION_ID, + workspacePaths: [cwd], + transcriptPath: TRANSCRIPT_PATH, + modelName: "auto", + stepIdx: 20, + toolCall: { + name: "run_command", + args: { CommandLine: command, Cwd: cwd }, + }, + }; + }, + }, + // PreInvocation → canonical UserPromptSubmit. + preInvocation(cwd: string): Record { return { - session_id: GEMINI_SESSION_ID, - transcript_path: TRANSCRIPT_PATH, - cwd, - hook_event_name: "SessionEnd", - timestamp: TIMESTAMP, - reason, + conversationId: ANTIGRAVITY_CONVERSATION_ID, + workspacePaths: [cwd], + transcriptPath: TRANSCRIPT_PATH, + modelName: "auto", + invocationNum: 3, + initialNumSteps: 10, }; }, - beforeModel(cwd: string): Record { + stop(cwd: string): Record { return { - session_id: GEMINI_SESSION_ID, - transcript_path: TRANSCRIPT_PATH, - cwd, - hook_event_name: "BeforeModel", - timestamp: TIMESTAMP, - llm_request: { model: "gemini-pro", messages: [] }, + conversationId: ANTIGRAVITY_CONVERSATION_ID, + workspacePaths: [cwd], + transcriptPath: TRANSCRIPT_PATH, + modelName: "auto", + executionNum: 1, + terminationReason: "model_stop", + fullyIdle: true, }; }, - preCompress(cwd: string): Record { +}; + +/** + * Goose (codename goose, Block) payload factories. Goose pipes a hook stdin that + * uses `event` (not `hook_event_name`), `working_dir` (not `cwd`), and + * `matcher_context` (the string the matcher regex tests). tool_name is BARE + * (`shell`, `write`, `view`) — mapped to Claude builtins via GOOSE_TOOL_MAP — + * and path-bearing tools deliver the path as `path`/`source` (→ `file_path` via + * GOOSE_TOOL_INPUT_MAP). There is NO transcript_path (audit reads sessions.db). + * Verified live against goose v1.43.0. + */ +const GOOSE_SESSION_ID = "20260714_1"; + +export const GoosePayloads = { + preToolUse: { + bash(command: string, cwd: string): Record { + return { + event: "PreToolUse", + session_id: GOOSE_SESSION_ID, + matcher_context: "shell", + tool_name: "shell", + tool_input: { command }, + working_dir: cwd, + }; + }, + write(filePath: string, content: string, cwd: string): Record { + return { + event: "PreToolUse", + session_id: GOOSE_SESSION_ID, + matcher_context: "write", + tool_name: "write", + tool_input: { path: filePath, content }, + working_dir: cwd, + }; + }, + read(filePath: string, cwd: string): Record { + return { + event: "PreToolUse", + session_id: GOOSE_SESSION_ID, + matcher_context: "view", + tool_name: "view", + tool_input: { path: filePath }, + working_dir: cwd, + }; + }, + }, + postToolUse: { + bash(command: string, cwd: string): Record { + return { + event: "PostToolUse", + session_id: GOOSE_SESSION_ID, + matcher_context: "shell", + tool_name: "shell", + tool_input: { command }, + working_dir: cwd, + }; + }, + }, + userPromptSubmit(prompt: string, cwd: string): Record { return { - session_id: GEMINI_SESSION_ID, - transcript_path: TRANSCRIPT_PATH, - cwd, - hook_event_name: "PreCompress", - timestamp: TIMESTAMP, - trigger: "auto", + event: "UserPromptSubmit", + session_id: GOOSE_SESSION_ID, + matcher_context: prompt, + message: prompt, + working_dir: cwd, }; }, - notification(cwd: string, message = "test"): Record { + sessionStart(cwd: string): Record { return { - session_id: GEMINI_SESSION_ID, - transcript_path: TRANSCRIPT_PATH, - cwd, - hook_event_name: "Notification", - timestamp: TIMESTAMP, - notification_type: "ToolPermission", - message, - details: {}, + event: "SessionStart", + session_id: GOOSE_SESSION_ID, + matcher_context: null, + working_dir: cwd, }; }, }; diff --git a/__tests__/e2e/hooks/antigravity-integration.e2e.test.ts b/__tests__/e2e/hooks/antigravity-integration.e2e.test.ts new file mode 100644 index 00000000..313a8300 --- /dev/null +++ b/__tests__/e2e/hooks/antigravity-integration.e2e.test.ts @@ -0,0 +1,234 @@ +/** + * E2E: Antigravity (agy) hook integration. + * + * Exercises the full install → fire → decide flow using the real failproofai + * binary as a subprocess (no mocks). Antigravity has its OWN response contract + * (NOT Claude's), verified live against agy v1.1.2: + * • tool/prompt deny → `{decision:"deny", reason}` on stdout (exit 0) + * • Stop deny/instruct → `{decision:"continue", reason}` (re-enters the loop) + * • PreInvocation (→ UserPromptSubmit) instruct → `{injectSteps:[{ephemeralMessage}]}` + * The camelCase `toolCall:{name,args}` payload is normalized to snake_case by + * the handler before policies run. hooks.json uses a NAMED-hook schema. + */ +import { describe, it, expect } from "vitest"; +import { execSync } from "node:child_process"; +import { mkdtempSync, writeFileSync, readFileSync, existsSync, mkdirSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + runHook, + assertAllow, + assertAntigravityDeny, + assertAntigravityStopContinue, + assertAntigravityInjectSteps, +} from "../helpers/hook-runner"; +import { AntigravityPayloads } from "../helpers/payloads"; +import { createFixtureEnv } from "../helpers/fixture-env"; + +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); +const BINARY_PATH = resolve(REPO_ROOT, "bin/failproofai.mjs"); + +function createAntigravityEnv(): { home: string; cwd: string; cleanup: () => void } { + const home = mkdtempSync(join(tmpdir(), "fp-e2e-antigravity-home-")); + const cwd = mkdtempSync(join(tmpdir(), "fp-e2e-antigravity-cwd-")); + mkdirSync(resolve(cwd, ".failproofai"), { recursive: true }); + return { + home, + cwd, + cleanup() { + rmSync(home, { recursive: true, force: true }); + rmSync(cwd, { recursive: true, force: true }); + }, + }; +} + +function writeConfig(cwd: string, enabledPolicies: string[]): void { + const configPath = resolve(cwd, ".failproofai", "policies-config.json"); + mkdirSync(dirname(configPath), { recursive: true }); + writeFileSync(configPath, JSON.stringify({ enabledPolicies }, null, 2)); +} + +describe("E2E: Antigravity integration — hook protocol", () => { + it("PreToolUse: block-sudo denies via {decision:'deny'} JSON (exit 0)", () => { + const env = createAntigravityEnv(); + try { + writeConfig(env.cwd, ["block-sudo"]); + const result = runHook( + "PreToolUse", + AntigravityPayloads.preToolUse.bash("sudo apt install foo", env.cwd), + { homeDir: env.home, cli: "antigravity" }, + ); + assertAntigravityDeny(result); + } finally { + env.cleanup(); + } + }); + + it("PreToolUse: camelCase toolCall{name:run_command} is normalized + canonicalized to Bash", () => { + const env = createAntigravityEnv(); + try { + writeConfig(env.cwd, ["block-sudo"]); + // toolCall.name "run_command" → tool_name → Bash; args.CommandLine → command. + const result = runHook( + "PreToolUse", + AntigravityPayloads.preToolUse.bash("sudo rm -rf /", env.cwd), + { homeDir: env.home, cli: "antigravity" }, + ); + assertAntigravityDeny(result); + } finally { + env.cleanup(); + } + }); + + it("Stop deny emits {decision:'continue', reason} with MANDATORY ACTION wording", () => { + const env = createAntigravityEnv(); + try { + writeConfig(env.cwd, ["require-commit-before-stop"]); + // require-commit-before-stop denies when the cwd has uncommitted changes. + execSync( + "git init -q && git config user.email t@t && git config user.name t && touch tracked && git add tracked && git commit -q -m initial && echo dirty > tracked", + { cwd: env.cwd, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } }, + ); + const result = runHook( + "Stop", + AntigravityPayloads.stop(env.cwd), + { homeDir: env.home, cli: "antigravity" }, + ); + assertAntigravityStopContinue(result); + } finally { + env.cleanup(); + } + }); + + it("PreInvocation (→ UserPromptSubmit) instruct emits {injectSteps:[{ephemeralMessage}]}", () => { + const env = createFixtureEnv(); + const hookPath = env.writeHook("instruct-prompt.mjs", ` + import { customPolicies, instruct } from "failproofai"; + customPolicies.add({ + name: "instruct-on-prompt", + description: "Always instruct on prompt submit", + match: { events: ["UserPromptSubmit"] }, + fn: async () => instruct("review the plan before acting"), + }); + `); + env.writeConfig({ enabledPolicies: [], customPoliciesPath: hookPath }); + const result = runHook( + "PreInvocation", + AntigravityPayloads.preInvocation(env.cwd), + { homeDir: env.home, cli: "antigravity" }, + ); + assertAntigravityInjectSteps(result); + const steps = result.parsed?.injectSteps as Array>; + expect(String(steps[0].ephemeralMessage)).toContain("review the plan before acting"); + }); + + it("PreInvocation: allow when no policy matches", () => { + const env = createAntigravityEnv(); + try { + writeConfig(env.cwd, []); + const result = runHook( + "PreInvocation", + AntigravityPayloads.preInvocation(env.cwd), + { homeDir: env.home, cli: "antigravity" }, + ); + assertAllow(result); + } finally { + env.cleanup(); + } + }); + + it("activity entry tags decision with integration: antigravity", () => { + const env = createAntigravityEnv(); + try { + writeConfig(env.cwd, ["block-sudo"]); + runHook( + "PreToolUse", + AntigravityPayloads.preToolUse.bash("sudo cat /etc/passwd", env.cwd), + { homeDir: env.home, cli: "antigravity" }, + ); + const activityPath = resolve(env.home, ".failproofai", "cache", "hook-activity", "current.jsonl"); + expect(existsSync(activityPath)).toBe(true); + const lines = readFileSync(activityPath, "utf-8").trim().split("\n").filter(Boolean); + const last = JSON.parse(lines[lines.length - 1]) as Record; + expect(last.integration).toBe("antigravity"); + expect(last.decision).toBe("deny"); + } 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(); + try { + execSync( + `bun ${BINARY_PATH} policies --install block-sudo --cli antigravity --scope project`, + { cwd: env.cwd, env: { ...process.env, HOME: env.home, FAILPROOFAI_TELEMETRY_DISABLED: "1", FAILPROOFAI_BINARY_OVERRIDE: BINARY_PATH } }, + ); + const configPath = resolve(env.cwd, ".agents", "hooks.json"); + expect(existsSync(configPath)).toBe(true); + const settings = JSON.parse(readFileSync(configPath, "utf-8")) as Record; + // Named-hook schema: our events live under the "failproofai" key. + expect(settings.failproofai).toBeDefined(); + // Tool events → {matcher:"*", hooks:[…]} wrapper. + expect(settings.failproofai.PreToolUse[0].matcher).toBe("*"); + const toolCmd = settings.failproofai.PreToolUse[0].hooks[0].command as string; + expect(toolCmd).toContain("--cli antigravity"); + // PreInvocation / Stop → flat handler objects (no wrapper). + expect(settings.failproofai.PreInvocation[0].type).toBe("command"); + expect(settings.failproofai.PreInvocation[0].hooks).toBeUndefined(); + expect(settings.failproofai.Stop[0].command).toContain("--cli antigravity"); + } finally { + env.cleanup(); + } + }); + + it("policies --install --cli antigravity --scope local fails with friendly error", () => { + const env = createAntigravityEnv(); + try { + let err: { status?: number; stderr?: Buffer } | null = null; + try { + execSync( + `bun ${BINARY_PATH} policies --install block-sudo --cli antigravity --scope local`, + { cwd: env.cwd, env: { ...process.env, HOME: env.home, FAILPROOFAI_TELEMETRY_DISABLED: "1" }, stdio: "pipe" }, + ); + } catch (e) { + err = e as { status?: number; stderr?: Buffer }; + } + expect(err).not.toBeNull(); + const stderr = err?.stderr?.toString() ?? ""; + expect(stderr).toMatch(/local.*not supported.*Antigravity CLI/i); + } finally { + env.cleanup(); + } + }); + + it("policies --uninstall --cli antigravity removes the failproofai named hook but preserves other named hooks", () => { + const env = createAntigravityEnv(); + try { + const baseEnv = { ...process.env, HOME: env.home, FAILPROOFAI_TELEMETRY_DISABLED: "1", FAILPROOFAI_BINARY_OVERRIDE: BINARY_PATH }; + execSync( + `bun ${BINARY_PATH} policies --install block-sudo --cli antigravity --scope project`, + { cwd: env.cwd, env: baseEnv }, + ); + const configPath = resolve(env.cwd, ".agents", "hooks.json"); + expect(existsSync(configPath)).toBe(true); + // Simulate an operator's own named hook surviving uninstall. + const withOther = JSON.parse(readFileSync(configPath, "utf-8")) as Record; + withOther["lint-checker"] = { PostToolUse: [{ matcher: "run_command", hooks: [{ type: "command", command: "./lint.sh" }] }] }; + writeFileSync(configPath, JSON.stringify(withOther, null, 2)); + + execSync( + `bun ${BINARY_PATH} policies --uninstall --cli antigravity --scope project`, + { cwd: env.cwd, env: baseEnv }, + ); + const settings = JSON.parse(readFileSync(configPath, "utf-8")) as Record; + expect(settings.failproofai).toBeUndefined(); + expect(settings["lint-checker"]).toBeDefined(); + } finally { + env.cleanup(); + } + }); +}); diff --git a/__tests__/e2e/hooks/devin-integration.e2e.test.ts b/__tests__/e2e/hooks/devin-integration.e2e.test.ts new file mode 100644 index 00000000..a5b59f83 --- /dev/null +++ b/__tests__/e2e/hooks/devin-integration.e2e.test.ts @@ -0,0 +1,208 @@ +/** + * E2E: Devin (Cognition) hook integration. + * + * Exercises the full install → fire → decide flow using the real failproofai + * binary as a subprocess (no mocks). Each test runs against an isolated fixture + * HOME so we don't pollute the user's ~/.config/devin or ~/.devin. + * + * Devin is a pure Claude-clone: its deny contract is `{decision:"block", + * reason}` JSON on stdout at exit 0 for every event (verified live against + * devin v3000.1.27 — the block overrode `--permission-mode dangerous`). Config + * uses the standard Claude `"hooks"`-wrapper schema. + */ +import { describe, it, expect } from "vitest"; +import { execSync } from "node:child_process"; +import { mkdtempSync, writeFileSync, readFileSync, existsSync, mkdirSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + runHook, + assertAllow, + assertDevinDeny, + assertDevinStopBlock, +} from "../helpers/hook-runner"; +import { DevinPayloads } from "../helpers/payloads"; + +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); +const BINARY_PATH = resolve(REPO_ROOT, "bin/failproofai.mjs"); + +function createDevinEnv(): { home: string; cwd: string; cleanup: () => void } { + const home = mkdtempSync(join(tmpdir(), "fp-e2e-devin-home-")); + const cwd = mkdtempSync(join(tmpdir(), "fp-e2e-devin-cwd-")); + mkdirSync(resolve(cwd, ".failproofai"), { recursive: true }); + return { + home, + cwd, + cleanup() { + rmSync(home, { recursive: true, force: true }); + rmSync(cwd, { recursive: true, force: true }); + }, + }; +} + +function writeConfig(cwd: string, enabledPolicies: string[]): void { + const configPath = resolve(cwd, ".failproofai", "policies-config.json"); + mkdirSync(dirname(configPath), { recursive: true }); + writeFileSync(configPath, JSON.stringify({ enabledPolicies }, null, 2)); +} + +describe("E2E: Devin integration — hook protocol", () => { + it("PreToolUse: block-sudo denies via {decision:'block'} JSON (exit 0)", () => { + const env = createDevinEnv(); + try { + writeConfig(env.cwd, ["block-sudo"]); + const result = runHook( + "PreToolUse", + DevinPayloads.preToolUse.bash("sudo apt install foo", env.cwd), + { homeDir: env.home, cli: "devin" }, + ); + assertDevinDeny(result); + } finally { + env.cleanup(); + } + }); + + it("PreToolUse: devin's `exec` tool is canonicalized to Bash", () => { + const env = createDevinEnv(); + try { + writeConfig(env.cwd, ["block-sudo"]); + // tool_name: "exec" must map to Bash so block-sudo fires. + const result = runHook( + "PreToolUse", + DevinPayloads.preToolUse.bash("sudo rm -rf /", env.cwd), + { homeDir: env.home, cli: "devin" }, + ); + assertDevinDeny(result); + } finally { + env.cleanup(); + } + }); + + it("Stop deny emits {decision:'block', reason} with MANDATORY ACTION wording", () => { + const env = createDevinEnv(); + try { + writeConfig(env.cwd, ["require-commit-before-stop"]); + // require-commit-before-stop denies when the cwd has uncommitted changes. + execSync("git init -q && git config user.email t@t && git config user.name t && touch tracked && git add tracked && git commit -q -m initial && echo dirty > tracked", { + cwd: env.cwd, + env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }, + }); + const result = runHook( + "Stop", + DevinPayloads.stop(env.cwd), + { homeDir: env.home, cli: "devin" }, + ); + assertDevinStopBlock(result); + } finally { + env.cleanup(); + } + }); + + it("UserPromptSubmit: allow when no policy matches", () => { + const env = createDevinEnv(); + try { + writeConfig(env.cwd, []); + const result = runHook( + "UserPromptSubmit", + DevinPayloads.userPromptSubmit("Just a normal user prompt", env.cwd), + { homeDir: env.home, cli: "devin" }, + ); + assertAllow(result); + } finally { + env.cleanup(); + } + }); + + it("activity entry tags decision with integration: devin", () => { + const env = createDevinEnv(); + try { + writeConfig(env.cwd, ["block-sudo"]); + runHook( + "PreToolUse", + DevinPayloads.preToolUse.bash("sudo cat /etc/passwd", env.cwd), + { homeDir: env.home, cli: "devin" }, + ); + const activityPath = resolve(env.home, ".failproofai", "cache", "hook-activity", "current.jsonl"); + expect(existsSync(activityPath)).toBe(true); + const lines = readFileSync(activityPath, "utf-8").trim().split("\n").filter(Boolean); + const last = JSON.parse(lines[lines.length - 1]) as Record; + expect(last.integration).toBe("devin"); + expect(last.decision).toBe("deny"); + } finally { + env.cleanup(); + } + }); +}); + +describe("E2E: Devin integration — install/uninstall", () => { + it("policies --install --cli devin --scope project writes .devin/config.json with Claude-style hooks wrapper", () => { + const env = createDevinEnv(); + try { + execSync( + `bun ${BINARY_PATH} policies --install block-sudo --cli devin --scope project`, + { cwd: env.cwd, env: { ...process.env, HOME: env.home, FAILPROOFAI_TELEMETRY_DISABLED: "1", FAILPROOFAI_BINARY_OVERRIDE: BINARY_PATH } }, + ); + const configPath = resolve(env.cwd, ".devin", "config.json"); + expect(existsSync(configPath)).toBe(true); + const settings = JSON.parse(readFileSync(configPath, "utf-8")) as Record; + // Claude-style `hooks` wrapper — event names live under it. + expect(settings.hooks).toBeDefined(); + expect(settings.hooks.PreToolUse).toBeDefined(); + expect(settings.hooks.PostToolUse).toBeDefined(); + expect(settings.hooks.Stop).toBeDefined(); + expect(settings.hooks.SessionStart).toBeDefined(); + const cmd = settings.hooks.PreToolUse[0].hooks[0].command as string; + expect(cmd).toContain("--cli devin"); + } finally { + env.cleanup(); + } + }); + + it("policies --install --cli devin --scope local fails with friendly error", () => { + const env = createDevinEnv(); + try { + let err: { status?: number; stderr?: Buffer } | null = null; + try { + execSync( + `bun ${BINARY_PATH} policies --install block-sudo --cli devin --scope local`, + { cwd: env.cwd, env: { ...process.env, HOME: env.home, FAILPROOFAI_TELEMETRY_DISABLED: "1" }, stdio: "pipe" }, + ); + } catch (e) { + err = e as { status?: number; stderr?: Buffer }; + } + expect(err).not.toBeNull(); + const stderr = err?.stderr?.toString() ?? ""; + expect(stderr).toMatch(/local.*not supported.*Devin CLI/i); + } finally { + env.cleanup(); + } + }); + + it("policies --uninstall --cli devin removes hooks but preserves other config keys", () => { + const env = createDevinEnv(); + try { + const baseEnv = { ...process.env, HOME: env.home, FAILPROOFAI_TELEMETRY_DISABLED: "1", FAILPROOFAI_BINARY_OVERRIDE: BINARY_PATH }; + execSync( + `bun ${BINARY_PATH} policies --install block-sudo --cli devin --scope project`, + { cwd: env.cwd, env: baseEnv }, + ); + const configPath = resolve(env.cwd, ".devin", "config.json"); + expect(existsSync(configPath)).toBe(true); + // Simulate an operator's own key surviving uninstall. + const withOrg = JSON.parse(readFileSync(configPath, "utf-8")) as Record; + withOrg.org_id = "acme"; + writeFileSync(configPath, JSON.stringify(withOrg, null, 2)); + + execSync( + `bun ${BINARY_PATH} policies --uninstall --cli devin --scope project`, + { cwd: env.cwd, env: baseEnv }, + ); + const settings = JSON.parse(readFileSync(configPath, "utf-8")) as Record; + expect(settings.hooks).toBeUndefined(); + expect(settings.org_id).toBe("acme"); + } finally { + env.cleanup(); + } + }); +}); diff --git a/__tests__/e2e/hooks/factory-integration.e2e.test.ts b/__tests__/e2e/hooks/factory-integration.e2e.test.ts new file mode 100644 index 00000000..9986f308 --- /dev/null +++ b/__tests__/e2e/hooks/factory-integration.e2e.test.ts @@ -0,0 +1,223 @@ +/** + * E2E: Factory (droid) hook integration. + * + * Exercises the full install → fire → decide flow using the real failproofai + * binary as a subprocess (no mocks). Each test runs against an isolated fixture + * HOME so we don't pollute the user's ~/.factory/. + * + * Factory's deny contract is EXIT CODE 2 + stderr (droid ignores JSON decisions + * on tool events); Stop is the exception — droid honors `{decision:"block"}`. + * Verified live against droid v0.171.0. + */ +import { describe, it, expect } from "vitest"; +import { execSync } from "node:child_process"; +import { mkdtempSync, writeFileSync, readFileSync, existsSync, mkdirSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + runHook, + assertAllow, + assertFactoryDeny, + assertFactoryStopBlock, +} from "../helpers/hook-runner"; +import { FactoryPayloads } from "../helpers/payloads"; + +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); +const BINARY_PATH = resolve(REPO_ROOT, "bin/failproofai.mjs"); + +function createFactoryEnv(): { home: string; cwd: string; cleanup: () => void } { + const home = mkdtempSync(join(tmpdir(), "fp-e2e-factory-home-")); + const cwd = mkdtempSync(join(tmpdir(), "fp-e2e-factory-cwd-")); + mkdirSync(resolve(cwd, ".failproofai"), { recursive: true }); + return { + home, + cwd, + cleanup() { + rmSync(home, { recursive: true, force: true }); + rmSync(cwd, { recursive: true, force: true }); + }, + }; +} + +function writeConfig(cwd: string, enabledPolicies: string[]): void { + const configPath = resolve(cwd, ".failproofai", "policies-config.json"); + mkdirSync(dirname(configPath), { recursive: true }); + writeFileSync(configPath, JSON.stringify({ enabledPolicies }, null, 2)); +} + +describe("E2E: Factory integration — hook protocol", () => { + it("PreToolUse: block-sudo denies via EXIT CODE 2 + stderr (not JSON)", () => { + const env = createFactoryEnv(); + try { + writeConfig(env.cwd, ["block-sudo"]); + const result = runHook( + "PreToolUse", + FactoryPayloads.preToolUse.bash("sudo apt install foo", env.cwd), + { homeDir: env.home, cli: "factory" }, + ); + assertFactoryDeny(result); + } finally { + env.cleanup(); + } + }); + + it("PreToolUse: droid's `Execute` tool is canonicalized to Bash", () => { + const env = createFactoryEnv(); + try { + writeConfig(env.cwd, ["block-sudo"]); + // tool_name: "Execute" must map to Bash so block-sudo fires. + const result = runHook( + "PreToolUse", + FactoryPayloads.preToolUse.bash("sudo rm -rf /", env.cwd), + { homeDir: env.home, cli: "factory" }, + ); + assertFactoryDeny(result); + } finally { + env.cleanup(); + } + }); + + it("PostToolUse: deny also uses exit-2 + stderr", () => { + const env = createFactoryEnv(); + try { + writeConfig(env.cwd, ["sanitize-jwt"]); + const result = runHook( + "PostToolUse", + FactoryPayloads.postToolUse.bash( + "echo done", + "JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTYifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", + env.cwd, + ), + { homeDir: env.home, cli: "factory" }, + ); + assertFactoryDeny(result); + } finally { + env.cleanup(); + } + }); + + it("Stop deny emits {decision:'block', reason} JSON (droid's turn-end retry shape)", () => { + const env = createFactoryEnv(); + try { + writeConfig(env.cwd, ["require-commit-before-stop"]); + // require-commit-before-stop denies when the cwd has uncommitted changes. + execSync("git init -q && git config user.email t@t && git config user.name t && touch tracked && git add tracked && git commit -q -m initial && echo dirty > tracked", { + cwd: env.cwd, + env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }, + }); + const result = runHook( + "Stop", + FactoryPayloads.stop(env.cwd), + { homeDir: env.home, cli: "factory" }, + ); + assertFactoryStopBlock(result); + } finally { + env.cleanup(); + } + }); + + it("UserPromptSubmit: allow when no policy matches", () => { + const env = createFactoryEnv(); + try { + writeConfig(env.cwd, []); + const result = runHook( + "UserPromptSubmit", + FactoryPayloads.userPromptSubmit("Just a normal user prompt", env.cwd), + { homeDir: env.home, cli: "factory" }, + ); + assertAllow(result); + } finally { + env.cleanup(); + } + }); + + it("activity entry tags decision with integration: factory", () => { + const env = createFactoryEnv(); + try { + writeConfig(env.cwd, ["block-sudo"]); + runHook( + "PreToolUse", + FactoryPayloads.preToolUse.bash("sudo cat /etc/passwd", env.cwd), + { homeDir: env.home, cli: "factory" }, + ); + const activityPath = resolve(env.home, ".failproofai", "cache", "hook-activity", "current.jsonl"); + expect(existsSync(activityPath)).toBe(true); + const lines = readFileSync(activityPath, "utf-8").trim().split("\n").filter(Boolean); + const last = JSON.parse(lines[lines.length - 1]) as Record; + expect(last.integration).toBe("factory"); + expect(last.decision).toBe("deny"); + } finally { + env.cleanup(); + } + }); +}); + +describe("E2E: Factory integration — install/uninstall", () => { + it("policies --install --cli factory --scope project writes .factory/hooks.json with TOP-LEVEL event keys", () => { + const env = createFactoryEnv(); + try { + execSync( + `bun ${BINARY_PATH} policies --install block-sudo --cli factory --scope project`, + { cwd: env.cwd, env: { ...process.env, HOME: env.home, FAILPROOFAI_TELEMETRY_DISABLED: "1", FAILPROOFAI_BINARY_OVERRIDE: BINARY_PATH } }, + ); + const hooksPath = resolve(env.cwd, ".factory", "hooks.json"); + expect(existsSync(hooksPath)).toBe(true); + const settings = JSON.parse(readFileSync(hooksPath, "utf-8")) as Record; + // No `hooks` wrapper — event names live at the top level. + expect(settings.hooks).toBeUndefined(); + expect(settings.PreToolUse).toBeDefined(); + expect(settings.PostToolUse).toBeDefined(); + expect(settings.Stop).toBeDefined(); + expect(settings.SessionStart).toBeDefined(); + // Tool events carry matcher:"*"; Stop does not. + expect(settings.PreToolUse[0].matcher).toBe("*"); + expect(settings.Stop[0].matcher).toBeUndefined(); + } finally { + env.cleanup(); + } + }); + + it("policies --install --cli factory --scope local fails with friendly error", () => { + const env = createFactoryEnv(); + try { + let err: { status?: number; stderr?: Buffer } | null = null; + try { + execSync( + `bun ${BINARY_PATH} policies --install block-sudo --cli factory --scope local`, + { cwd: env.cwd, env: { ...process.env, HOME: env.home, FAILPROOFAI_TELEMETRY_DISABLED: "1" }, stdio: "pipe" }, + ); + } catch (e) { + err = e as { status?: number; stderr?: Buffer }; + } + expect(err).not.toBeNull(); + const stderr = err?.stderr?.toString() ?? ""; + expect(stderr).toMatch(/local.*not supported.*Factory Droid/i); + } finally { + env.cleanup(); + } + }); + + it("policies --uninstall --cli factory removes hooks from .factory/hooks.json", () => { + const env = createFactoryEnv(); + try { + const baseEnv = { ...process.env, HOME: env.home, FAILPROOFAI_TELEMETRY_DISABLED: "1", FAILPROOFAI_BINARY_OVERRIDE: BINARY_PATH }; + execSync( + `bun ${BINARY_PATH} policies --install block-sudo --cli factory --scope project`, + { cwd: env.cwd, env: baseEnv }, + ); + const hooksPath = resolve(env.cwd, ".factory", "hooks.json"); + expect(existsSync(hooksPath)).toBe(true); + + execSync( + `bun ${BINARY_PATH} policies --uninstall --cli factory --scope project`, + { cwd: env.cwd, env: baseEnv }, + ); + const settings = JSON.parse(readFileSync(hooksPath, "utf-8")) as Record; + expect(settings.PreToolUse).toBeUndefined(); + expect(settings.Stop).toBeUndefined(); + } finally { + env.cleanup(); + } + }); +}); diff --git a/__tests__/e2e/hooks/gemini-integration.e2e.test.ts b/__tests__/e2e/hooks/gemini-integration.e2e.test.ts deleted file mode 100644 index f53da58a..00000000 --- a/__tests__/e2e/hooks/gemini-integration.e2e.test.ts +++ /dev/null @@ -1,379 +0,0 @@ -/** - * E2E: Gemini CLI hook integration. - * - * Exercises the full install → fire → decide flow using the real failproofai - * binary as a subprocess (no mocks). Each test runs against an isolated - * fixture HOME so we don't pollute the user's ~/.gemini/. - * - * Verifies four invariants that distinguish Gemini from the other 6 CLIs: - * 1. Tool-name canonicalization (run_shell_command → Bash, etc.) - * 2. Event-name canonicalization (BeforeTool → PreToolUse, etc.) - * 3. Flat `{decision: "deny", reason}` shape (NOT Claude's hookSpecificOutput) - * 4. AfterAgent → `{decision: "block", reason}` for Stop policies - */ -import { describe, it, expect } from "vitest"; -import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join, resolve, dirname } from "node:path"; -import { - runHook, - assertAllow, - assertGeminiDeny, - assertGeminiStopBlock, - assertGeminiInstruct, -} from "../helpers/hook-runner"; -import { GeminiPayloads } from "../helpers/payloads"; - -function createGeminiEnv(): { home: string; cwd: string; cleanup: () => void } { - const home = mkdtempSync(join(tmpdir(), "fp-e2e-gemini-home-")); - const cwd = mkdtempSync(join(tmpdir(), "fp-e2e-gemini-cwd-")); - // Pre-create the .failproofai dir under cwd so the parent-walk finds it. - mkdirSync(resolve(cwd, ".failproofai"), { recursive: true }); - return { - home, - cwd, - cleanup() { - rmSync(home, { recursive: true, force: true }); - rmSync(cwd, { recursive: true, force: true }); - }, - }; -} - -function writeConfig(cwd: string, enabledPolicies: string[], policyParams?: Record>): void { - const configPath = resolve(cwd, ".failproofai", "policies-config.json"); - mkdirSync(dirname(configPath), { recursive: true }); - writeFileSync(configPath, JSON.stringify({ enabledPolicies, ...(policyParams ? { policyParams } : {}) }, null, 2)); -} - -describe("E2E: Gemini integration — hook protocol", () => { - describe("Tool-name canonicalization (snake_case → Claude PascalCase)", () => { - it("run_shell_command + sudo → Bash → block-sudo deny shape", () => { - const env = createGeminiEnv(); - try { - writeConfig(env.cwd, ["block-sudo"]); - const result = runHook( - "BeforeTool", - GeminiPayloads.beforeTool.runShellCommand("sudo apt install foo", env.cwd), - { homeDir: env.home, cli: "gemini" }, - ); - assertGeminiDeny(result); - expect(result.parsed?.reason).toMatch(/Bash/); - // Confirm canonicalization actually mapped run_shell_command → Bash by - // checking the deny message references the canonical tool name. - expect(result.parsed?.reason).not.toMatch(/run_shell_command/); - } finally { - env.cleanup(); - } - }); - - it("run_shell_command + rm -rf / → Bash → block-rm-rf deny shape", () => { - const env = createGeminiEnv(); - try { - writeConfig(env.cwd, ["block-rm-rf"]); - const result = runHook( - "BeforeTool", - GeminiPayloads.beforeTool.runShellCommand("rm -rf /", env.cwd), - { homeDir: env.home, cli: "gemini" }, - ); - assertGeminiDeny(result); - expect(result.parsed?.reason).toMatch(/Bash/); - } finally { - env.cleanup(); - } - }); - - it("read_file outside cwd → Read → block-read-outside-cwd deny shape", () => { - const env = createGeminiEnv(); - try { - writeConfig(env.cwd, ["block-read-outside-cwd"]); - const result = runHook( - "BeforeTool", - GeminiPayloads.beforeTool.readFile("/etc/passwd", env.cwd), - { homeDir: env.home, cli: "gemini" }, - ); - assertGeminiDeny(result); - expect(result.parsed?.reason).toMatch(/Read/); - } finally { - env.cleanup(); - } - }); - - it("write_file inside cwd → Write → allow", () => { - const env = createGeminiEnv(); - try { - writeConfig(env.cwd, ["block-rm-rf"]); - const result = runHook( - "BeforeTool", - GeminiPayloads.beforeTool.writeFile(`${env.cwd}/foo.txt`, "hello", env.cwd), - { homeDir: env.home, cli: "gemini" }, - ); - assertAllow(result); - } finally { - env.cleanup(); - } - }); - - it("MCP tool name (mcp_github_create_issue) passes through unchanged", () => { - const env = createGeminiEnv(); - try { - writeConfig(env.cwd, ["block-sudo"]); - const result = runHook( - "BeforeTool", - GeminiPayloads.beforeTool.mcpExtension("mcp_github_create_issue", { title: "x" }, env.cwd), - { homeDir: env.home, cli: "gemini" }, - ); - // Unknown tool, no policy matches → allow with empty stdout - assertAllow(result); - } finally { - env.cleanup(); - } - }); - }); - - describe("Event-name canonicalization (Gemini PascalCase → Claude canonical)", () => { - it("BeforeTool → PreToolUse — block-sudo fires on the canonical event", () => { - const env = createGeminiEnv(); - try { - writeConfig(env.cwd, ["block-sudo"]); - const result = runHook( - "BeforeTool", - GeminiPayloads.beforeTool.runShellCommand("sudo ls", env.cwd), - { homeDir: env.home, cli: "gemini" }, - ); - assertGeminiDeny(result); - } finally { - env.cleanup(); - } - }); - - it("BeforeAgent → UserPromptSubmit — fires on the canonical event", () => { - const env = createGeminiEnv(); - try { - // No deny policy for UserPromptSubmit by default; this just verifies - // the event name canonicalizes without crashing and exits 0. - writeConfig(env.cwd, []); - const result = runHook( - "BeforeAgent", - GeminiPayloads.beforeAgent("hello", env.cwd), - { homeDir: env.home, cli: "gemini" }, - ); - assertAllow(result); - } finally { - env.cleanup(); - } - }); - - it("AfterTool → PostToolUse — fires without crashing on benign payload", () => { - const env = createGeminiEnv(); - try { - writeConfig(env.cwd, []); - const result = runHook( - "AfterTool", - GeminiPayloads.afterTool.runShellCommand("ls", "file1\nfile2", env.cwd), - { homeDir: env.home, cli: "gemini" }, - ); - assertAllow(result); - } finally { - env.cleanup(); - } - }); - - it("AfterAgent → Stop — fires without crashing on benign payload", () => { - const env = createGeminiEnv(); - try { - writeConfig(env.cwd, []); - const result = runHook( - "AfterAgent", - GeminiPayloads.afterAgent("hi", "hello", env.cwd), - { homeDir: env.home, cli: "gemini" }, - ); - // No Stop policies enabled → exit 0 with no stdout - expect(result.exitCode).toBe(0); - } finally { - env.cleanup(); - } - }); - - it("BeforeModel — Gemini-only event with no canonical, exits 0 with no stdout", () => { - const env = createGeminiEnv(); - try { - writeConfig(env.cwd, ["block-sudo"]); - const result = runHook( - "BeforeModel", - GeminiPayloads.beforeModel(env.cwd), - { homeDir: env.home, cli: "gemini" }, - ); - // BeforeModel has no canonical Claude event, so getPoliciesForEvent → [] - expect(result.exitCode).toBe(0); - expect(result.stdout).toBe(""); - } finally { - env.cleanup(); - } - }); - - it("PreCompress → PreCompact — passes through (no policies match by default)", () => { - const env = createGeminiEnv(); - try { - writeConfig(env.cwd, []); - const result = runHook( - "PreCompress", - GeminiPayloads.preCompress(env.cwd), - { homeDir: env.home, cli: "gemini" }, - ); - expect(result.exitCode).toBe(0); - } finally { - env.cleanup(); - } - }); - - it("Notification — passes through without crashing", () => { - const env = createGeminiEnv(); - try { - writeConfig(env.cwd, []); - const result = runHook( - "Notification", - GeminiPayloads.notification(env.cwd), - { homeDir: env.home, cli: "gemini" }, - ); - expect(result.exitCode).toBe(0); - } finally { - env.cleanup(); - } - }); - - it("SessionStart and SessionEnd both exit 0 cleanly", () => { - const env = createGeminiEnv(); - try { - writeConfig(env.cwd, []); - const start = runHook( - "SessionStart", - GeminiPayloads.sessionStart(env.cwd), - { homeDir: env.home, cli: "gemini" }, - ); - expect(start.exitCode).toBe(0); - const end = runHook( - "SessionEnd", - GeminiPayloads.sessionEnd(env.cwd), - { homeDir: env.home, cli: "gemini" }, - ); - expect(end.exitCode).toBe(0); - } finally { - env.cleanup(); - } - }); - }); - - describe("Response-shape correctness (Gemini-specific JSON, NOT Claude's)", () => { - it("BeforeTool deny → flat {decision:'deny', reason}, NO hookSpecificOutput wrapper", () => { - const env = createGeminiEnv(); - try { - writeConfig(env.cwd, ["block-sudo"]); - const result = runHook( - "BeforeTool", - GeminiPayloads.beforeTool.runShellCommand("sudo apt update", env.cwd), - { homeDir: env.home, cli: "gemini" }, - ); - assertGeminiDeny(result); - // Crucial regression guard: not Claude's nested shape - expect(result.parsed?.hookSpecificOutput).toBeUndefined(); - // Not Cursor's `permission` field - expect(result.parsed?.permission).toBeUndefined(); - // Not Codex's PermissionRequest decision shape - const out = result.parsed?.hookSpecificOutput as Record | undefined; - expect(out?.decision).toBeUndefined(); - } finally { - env.cleanup(); - } - }); - - it("AfterAgent (Stop) deny → {decision:'block', reason} with MANDATORY ACTION REQUIRED", () => { - const env = createGeminiEnv(); - try { - // Force a Stop deny by enabling a require-* policy and missing what it requires. - // The exact policy isn't important; we just want a Stop-event deny path. - writeConfig(env.cwd, ["require-pr-before-stop"]); - const result = runHook( - "AfterAgent", - GeminiPayloads.afterAgent("hi", "hello", env.cwd), - { homeDir: env.home, cli: "gemini" }, - ); - // Either deny (no PR found) → block; or allow if branch is somehow ok. - // Branch on what we got — but assert that *if* there's a stdout JSON, it - // uses block (not deny) for AfterAgent. - if (result.stdout) { - assertGeminiStopBlock(result); - } - } finally { - env.cleanup(); - } - }); - - it("Stdin >1MB is discarded gracefully (matches handler.ts:73 cap)", () => { - const env = createGeminiEnv(); - try { - writeConfig(env.cwd, []); - // 1.1 MB padding pushes the stdin past the limit - const huge = "x".repeat(1_200_000); - const result = runHook( - "BeforeTool", - { ...GeminiPayloads.beforeTool.runShellCommand("ls", env.cwd), padding: huge }, - { homeDir: env.home, cli: "gemini" }, - ); - // The handler discards the payload and falls open to allow with no stdout - expect(result.exitCode).toBe(0); - } finally { - env.cleanup(); - } - }); - - it("Malformed stdin JSON falls open to allow (no crash)", () => { - const env = createGeminiEnv(); - try { - writeConfig(env.cwd, ["block-sudo"]); - // Manually corrupt by passing a string (runHook stringifies, so wrap in - // an object with one key whose value can't be properly used) — easier: - // pass an empty object so policies have nothing to match on. - const result = runHook( - "BeforeTool", - {} as Record, - { homeDir: env.home, cli: "gemini" }, - ); - expect(result.exitCode).toBe(0); - } finally { - env.cleanup(); - } - }); - }); - - describe("Tool-name canonicalization edge cases", () => { - it("Every documented Gemini tool name canonicalizes correctly when piped through deny path", () => { - const env = createGeminiEnv(); - try { - // Use sanitize-api-keys which fires on Read for both raw `read_file` and `read_many_files` - // — picks up the canonical "Read" name regardless of which Gemini tool name was used. - writeConfig(env.cwd, []); - const cases = [ - { gemini: "run_shell_command", input: { command: "echo hi" } }, - { gemini: "read_file", input: { file_path: `${env.cwd}/foo.txt` } }, - { gemini: "read_many_files", input: { file_paths: [`${env.cwd}/foo.txt`] } }, - { gemini: "write_file", input: { file_path: `${env.cwd}/bar.txt`, content: "x" } }, - { gemini: "replace", input: { file_path: `${env.cwd}/bar.txt`, old_string: "x", new_string: "y" } }, - { gemini: "glob", input: { pattern: "*.ts" } }, - { gemini: "grep_search", input: { pattern: "foo" } }, - { gemini: "list_directory", input: { path: env.cwd } }, - ]; - for (const c of cases) { - const result = runHook( - "BeforeTool", - GeminiPayloads.beforeTool.mcpExtension(c.gemini, c.input, env.cwd), - { homeDir: env.home, cli: "gemini" }, - ); - // No deny policy enabled → allow regardless - expect(result.exitCode).toBe(0); - } - } finally { - env.cleanup(); - } - }); - }); -}); diff --git a/__tests__/e2e/hooks/goose-integration.e2e.test.ts b/__tests__/e2e/hooks/goose-integration.e2e.test.ts new file mode 100644 index 00000000..b895a0ef --- /dev/null +++ b/__tests__/e2e/hooks/goose-integration.e2e.test.ts @@ -0,0 +1,174 @@ +/** + * E2E: Goose (codename goose, Block) hook integration. + * + * Exercises the full install → fire → decide flow using the real failproofai + * binary as a subprocess (no mocks). Each test runs against an isolated fixture + * HOME/cwd so we don't pollute the user's ~/.agents/plugins/. + * + * Goose's deny contract is `{decision:"block", reason}` JSON on stdout at exit 0, + * honored on PreToolUse ONLY (goose has no Stop event). Its stdin uses `event` / + * `working_dir` (normalized in handler.ts) and BARE tool names mapped via + * GOOSE_TOOL_MAP. Verified live against goose v1.43.0. + */ +import { describe, it, expect } from "vitest"; +import { execSync } from "node:child_process"; +import { mkdtempSync, writeFileSync, readFileSync, existsSync, mkdirSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { runHook, assertAllow, assertGooseDeny } from "../helpers/hook-runner"; +import { GoosePayloads } from "../helpers/payloads"; + +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); +const BINARY_PATH = resolve(REPO_ROOT, "bin/failproofai.mjs"); + +function createGooseEnv(): { home: string; cwd: string; cleanup: () => void } { + const home = mkdtempSync(join(tmpdir(), "fp-e2e-goose-home-")); + const cwd = mkdtempSync(join(tmpdir(), "fp-e2e-goose-cwd-")); + mkdirSync(resolve(cwd, ".failproofai"), { recursive: true }); + return { + home, + cwd, + cleanup() { + rmSync(home, { recursive: true, force: true }); + rmSync(cwd, { recursive: true, force: true }); + }, + }; +} + +function writeConfig(cwd: string, enabledPolicies: string[]): void { + const configPath = resolve(cwd, ".failproofai", "policies-config.json"); + mkdirSync(dirname(configPath), { recursive: true }); + writeFileSync(configPath, JSON.stringify({ enabledPolicies }, null, 2)); +} + +describe("E2E: Goose integration — hook protocol", () => { + it("PreToolUse: block-sudo denies via {decision:'block'} JSON (goose's shell → Bash)", () => { + const env = createGooseEnv(); + try { + writeConfig(env.cwd, ["block-sudo"]); + // tool_name "shell" must canonicalize to Bash so block-sudo fires. + const result = runHook( + "PreToolUse", + GoosePayloads.preToolUse.bash("sudo apt install foo", env.cwd), + { homeDir: env.home, cli: "goose" }, + ); + assertGooseDeny(result); + } finally { + env.cleanup(); + } + }); + + it("PreToolUse: write's `path` is canonicalized to file_path so block-env-files fires", () => { + const env = createGooseEnv(); + try { + writeConfig(env.cwd, ["block-env-files"]); + // goose `write` delivers the path as `path`; without the input map, + // block-env-files (which reads file_path) would silently no-op. + const result = runHook( + "PreToolUse", + GoosePayloads.preToolUse.write(resolve(env.cwd, ".env"), "SECRET=abc123", env.cwd), + { homeDir: env.home, cli: "goose" }, + ); + assertGooseDeny(result); + } finally { + env.cleanup(); + } + }); + + it("UserPromptSubmit: allow when no policy matches", () => { + const env = createGooseEnv(); + try { + writeConfig(env.cwd, []); + const result = runHook( + "UserPromptSubmit", + GoosePayloads.userPromptSubmit("Just a normal user prompt", env.cwd), + { homeDir: env.home, cli: "goose" }, + ); + assertAllow(result); + } finally { + env.cleanup(); + } + }); + + it("PreToolUse: allow when the command is harmless", () => { + const env = createGooseEnv(); + try { + writeConfig(env.cwd, ["block-sudo"]); + const result = runHook( + "PreToolUse", + GoosePayloads.preToolUse.bash("echo hello", env.cwd), + { homeDir: env.home, cli: "goose" }, + ); + assertAllow(result); + } finally { + env.cleanup(); + } + }); + + it("activity entry tags decision with integration: goose", () => { + const env = createGooseEnv(); + try { + writeConfig(env.cwd, ["block-sudo"]); + runHook( + "PreToolUse", + GoosePayloads.preToolUse.bash("sudo cat /etc/passwd", env.cwd), + { homeDir: env.home, cli: "goose" }, + ); + const activityPath = resolve(env.home, ".failproofai", "cache", "hook-activity", "current.jsonl"); + expect(existsSync(activityPath)).toBe(true); + const lines = readFileSync(activityPath, "utf-8").trim().split("\n").filter(Boolean); + const last = JSON.parse(lines[lines.length - 1]) as Record; + expect(last.integration).toBe("goose"); + expect(last.decision).toBe("deny"); + } finally { + env.cleanup(); + } + }); +}); + +describe("E2E: Goose integration — install/uninstall", () => { + it("policies --install --cli goose --scope project writes the Open Plugins hooks.json", () => { + const env = createGooseEnv(); + try { + execSync( + `bun ${BINARY_PATH} policies --install block-sudo --cli goose --scope project`, + { cwd: env.cwd, env: { ...process.env, HOME: env.home, FAILPROOFAI_TELEMETRY_DISABLED: "1", FAILPROOFAI_BINARY_OVERRIDE: BINARY_PATH } }, + ); + const hooksPath = resolve(env.cwd, ".agents", "plugins", "failproofai", "hooks", "hooks.json"); + expect(existsSync(hooksPath)).toBe(true); + const settings = JSON.parse(readFileSync(hooksPath, "utf-8")) as Record; + // Open Plugins schema: top-level "hooks" wrapper, matcher OMITTED. + expect(settings.hooks).toBeDefined(); + expect(settings.hooks.PreToolUse).toBeDefined(); + expect(settings.hooks.PostToolUse).toBeDefined(); + expect(settings.hooks.SessionStart).toBeDefined(); + expect(settings.hooks.PreToolUse[0].matcher).toBeUndefined(); + expect(settings.hooks.PreToolUse[0].hooks[0].command).toContain("--cli goose"); + } finally { + env.cleanup(); + } + }); + + it("policies --uninstall --cli goose removes hooks from the plugin hooks.json", () => { + const env = createGooseEnv(); + try { + const baseEnv = { ...process.env, HOME: env.home, FAILPROOFAI_TELEMETRY_DISABLED: "1", FAILPROOFAI_BINARY_OVERRIDE: BINARY_PATH }; + execSync( + `bun ${BINARY_PATH} policies --install block-sudo --cli goose --scope project`, + { cwd: env.cwd, env: baseEnv }, + ); + const hooksPath = resolve(env.cwd, ".agents", "plugins", "failproofai", "hooks", "hooks.json"); + expect(existsSync(hooksPath)).toBe(true); + + execSync( + `bun ${BINARY_PATH} policies --uninstall --cli goose --scope project`, + { cwd: env.cwd, env: baseEnv }, + ); + const settings = JSON.parse(readFileSync(hooksPath, "utf-8")) as Record; + expect(settings.hooks).toBeUndefined(); + } finally { + env.cleanup(); + } + }); +}); diff --git a/__tests__/hooks/antigravity-canonicalize.test.ts b/__tests__/hooks/antigravity-canonicalize.test.ts new file mode 100644 index 00000000..a12d7ba1 --- /dev/null +++ b/__tests__/hooks/antigravity-canonicalize.test.ts @@ -0,0 +1,107 @@ +// @vitest-environment node +// +// Locks in Antigravity (agy) event mapping + tool NAME/INPUT canonicalization. +// Verified live against agy v1.1.2: shell runs as `run_command` with PascalCase +// args (`CommandLine`, `Cwd`); PreInvocation maps to the canonical +// UserPromptSubmit event. +import { describe, it, expect } from "vitest"; +import { + canonicalizeToolName, + canonicalizeToolInput, +} from "@/src/hooks/tool-name-canonicalize"; +import { + ANTIGRAVITY_HOOK_EVENT_TYPES, + ANTIGRAVITY_EVENT_MAP, + ANTIGRAVITY_TOOL_MAP, + HOOK_EVENT_TYPES, +} from "@/src/hooks/types"; + +describe("Antigravity event mapping", () => { + it("subscribes to the 4 verified agy hook events", () => { + expect(ANTIGRAVITY_HOOK_EVENT_TYPES).toEqual([ + "PreToolUse", + "PostToolUse", + "PreInvocation", + "Stop", + ]); + }); + + it("maps every agy --hook arg to a canonical HookEventType", () => { + const canonical = new Set(HOOK_EVENT_TYPES); + for (const ev of ANTIGRAVITY_HOOK_EVENT_TYPES) { + expect(canonical.has(ANTIGRAVITY_EVENT_MAP[ev]), `${ev} → ${ANTIGRAVITY_EVENT_MAP[ev]}`).toBe(true); + } + }); + + it("PreInvocation maps to UserPromptSubmit; tool/stop pass through", () => { + expect(ANTIGRAVITY_EVENT_MAP.PreInvocation).toBe("UserPromptSubmit"); + expect(ANTIGRAVITY_EVENT_MAP.PreToolUse).toBe("PreToolUse"); + expect(ANTIGRAVITY_EVENT_MAP.PostToolUse).toBe("PostToolUse"); + expect(ANTIGRAVITY_EVENT_MAP.Stop).toBe("Stop"); + }); +}); + +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("read_file", "antigravity")).toBe("Read"); + expect(canonicalizeToolName("view_file", "antigravity")).toBe("Read"); + expect(canonicalizeToolName("edit_file", "antigravity")).toBe("Edit"); + expect(canonicalizeToolName("replace_file_content", "antigravity")).toBe("Edit"); + expect(canonicalizeToolName("list_dir", "antigravity")).toBe("LS"); + expect(canonicalizeToolName("find_by_name", "antigravity")).toBe("Glob"); + expect(canonicalizeToolName("grep_search", "antigravity")).toBe("Grep"); + expect(canonicalizeToolName("read_url_content", "antigravity")).toBe("WebFetch"); + expect(canonicalizeToolName("search_web", "antigravity")).toBe("WebSearch"); + }); + + it("passes unknown agy tools through unchanged (MCP, future tools)", () => { + expect(canonicalizeToolName("mcp__server__tool", "antigravity")).toBe("mcp__server__tool"); + expect(canonicalizeToolName("some_future_tool", "antigravity")).toBe("some_future_tool"); + }); + + it("maps every ANTIGRAVITY_TOOL_MAP key to a non-empty canonical name", () => { + for (const [raw, canonical] of Object.entries(ANTIGRAVITY_TOOL_MAP)) { + expect(canonicalizeToolName(raw, "antigravity")).toBe(canonical); + } + }); +}); + +describe("Antigravity tool-input canonicalization", () => { + it("maps run_command's PascalCase args (CommandLine/Cwd) to command/cwd on Bash", () => { + const out = canonicalizeToolInput( + "Bash", + { CommandLine: "npm test", Cwd: "/repo", WaitMsBeforeAsync: 5000 }, + "antigravity", + ) as Record; + expect(out.command).toBe("npm test"); + expect(out.cwd).toBe("/repo"); + // unknown keys pass through unchanged + expect(out.WaitMsBeforeAsync).toBe(5000); + }); + + it("maps write_to_file's TargetFile/CodeContent to file_path/content on Write", () => { + // Regression: without this, block-env-files/block-secrets-write never saw a + // file_path and silently allowed Antigravity .env writes. + const out = canonicalizeToolInput( + "Write", + { TargetFile: "/home/u/.env", CodeContent: "SECRET=x", Overwrite: true }, + "antigravity", + ) as Record; + expect(out.file_path).toBe("/home/u/.env"); + expect(out.content).toBe("SECRET=x"); + // unknown keys pass through unchanged + expect(out.Overwrite).toBe(true); + }); + + it("maps a file tool's TargetFile to file_path on Edit", () => { + const out = canonicalizeToolInput("Edit", { TargetFile: "/repo/.env" }, "antigravity") as Record; + expect(out.file_path).toBe("/repo/.env"); + }); + + 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/__tests__/hooks/devin-canonicalize.test.ts b/__tests__/hooks/devin-canonicalize.test.ts new file mode 100644 index 00000000..0c70efeb --- /dev/null +++ b/__tests__/hooks/devin-canonicalize.test.ts @@ -0,0 +1,51 @@ +// @vitest-environment node +// +// Locks in Devin (Cognition) tool NAME canonicalization (verified live against +// devin v3000.1.27: the shell tool runs as `exec` with `tool_input.command`). +// tool_input keys are already canonical, so there is no DEVIN_TOOL_INPUT_MAP. +import { describe, it, expect } from "vitest"; +import { canonicalizeToolName } from "@/src/hooks/tool-name-canonicalize"; +import { + DEVIN_HOOK_EVENT_TYPES, + DEVIN_TOOL_MAP, + HOOK_EVENT_TYPES, +} from "@/src/hooks/types"; + +describe("Devin event types", () => { + it("are all already-canonical PascalCase HookEventTypes (no event map needed)", () => { + const canonical = new Set(HOOK_EVENT_TYPES); + for (const ev of DEVIN_HOOK_EVENT_TYPES) { + expect(canonical.has(ev), `${ev} must be a HookEventType`).toBe(true); + } + }); + + it("subscribes to the 7 verified devin events", () => { + expect(DEVIN_HOOK_EVENT_TYPES).toEqual([ + "SessionStart", + "UserPromptSubmit", + "PreToolUse", + "PostToolUse", + "PermissionRequest", + "Stop", + "SessionEnd", + ]); + }); +}); + +describe("Devin tool canonicalization", () => { + it("canonicalizes the exec shell tool to Bash", () => { + expect(canonicalizeToolName("exec", "devin")).toBe("Bash"); + }); + + it("passes unknown/other devin tools through unchanged (MCP, extensions, file tools)", () => { + expect(canonicalizeToolName("mcp__server__tool", "devin")).toBe("mcp__server__tool"); + expect(canonicalizeToolName("str_replace_editor", "devin")).toBe("str_replace_editor"); + expect(canonicalizeToolName("SomeFutureTool", "devin")).toBe("SomeFutureTool"); + }); + + it("maps every DEVIN_TOOL_MAP key to a non-empty canonical name", () => { + for (const [raw, canonical] of Object.entries(DEVIN_TOOL_MAP)) { + expect(canonicalizeToolName(raw, "devin")).toBe(canonical); + } + }); +}); diff --git a/__tests__/hooks/factory-canonicalize.test.ts b/__tests__/hooks/factory-canonicalize.test.ts new file mode 100644 index 00000000..9f8bafed --- /dev/null +++ b/__tests__/hooks/factory-canonicalize.test.ts @@ -0,0 +1,63 @@ +// @vitest-environment node +// +// Locks in Factory (droid) tool NAME canonicalization (verified live against +// droid v0.171.0: shell runs as `Execute`, file writes as `Create`, URL fetches +// as `FetchUrl`). tool_input keys are already canonical (`command`, `file_path`), +// so there is no FACTORY_TOOL_INPUT_MAP. +import { describe, it, expect } from "vitest"; +import { canonicalizeToolName } from "@/src/hooks/tool-name-canonicalize"; +import { + FACTORY_HOOK_EVENT_TYPES, + FACTORY_TOOL_MAP, + HOOK_EVENT_TYPES, +} from "@/src/hooks/types"; + +describe("Factory event types", () => { + it("are all already-canonical PascalCase HookEventTypes (no event map needed)", () => { + const canonical = new Set(HOOK_EVENT_TYPES); + for (const ev of FACTORY_HOOK_EVENT_TYPES) { + expect(canonical.has(ev), `${ev} must be a HookEventType`).toBe(true); + } + }); + + it("subscribes to the 9 verified droid events", () => { + expect(FACTORY_HOOK_EVENT_TYPES).toEqual([ + "SessionStart", + "UserPromptSubmit", + "PreToolUse", + "PostToolUse", + "Notification", + "Stop", + "SubagentStop", + "PreCompact", + "SessionEnd", + ]); + }); +}); + +describe("Factory tool canonicalization", () => { + it("canonicalizes droid tool ids to Claude builtins", () => { + expect(canonicalizeToolName("Execute", "factory")).toBe("Bash"); + expect(canonicalizeToolName("Read", "factory")).toBe("Read"); + expect(canonicalizeToolName("Edit", "factory")).toBe("Edit"); + expect(canonicalizeToolName("Create", "factory")).toBe("Write"); + expect(canonicalizeToolName("Grep", "factory")).toBe("Grep"); + expect(canonicalizeToolName("Glob", "factory")).toBe("Glob"); + expect(canonicalizeToolName("LS", "factory")).toBe("LS"); + expect(canonicalizeToolName("FetchUrl", "factory")).toBe("WebFetch"); + expect(canonicalizeToolName("WebSearch", "factory")).toBe("WebSearch"); + expect(canonicalizeToolName("TodoWrite", "factory")).toBe("TodoWrite"); + expect(canonicalizeToolName("Task", "factory")).toBe("Task"); + }); + + it("passes unknown droid tools through unchanged (MCP, extensions)", () => { + expect(canonicalizeToolName("mcp__server__tool", "factory")).toBe("mcp__server__tool"); + expect(canonicalizeToolName("SomeFutureTool", "factory")).toBe("SomeFutureTool"); + }); + + it("maps every FACTORY_TOOL_MAP key to a non-empty canonical name", () => { + for (const [raw, canonical] of Object.entries(FACTORY_TOOL_MAP)) { + expect(canonicalizeToolName(raw, "factory")).toBe(canonical); + } + }); +}); diff --git a/__tests__/hooks/goose-canonicalize.test.ts b/__tests__/hooks/goose-canonicalize.test.ts new file mode 100644 index 00000000..8fabd964 --- /dev/null +++ b/__tests__/hooks/goose-canonicalize.test.ts @@ -0,0 +1,102 @@ +// @vitest-environment node +// +// Locks in Goose (codename goose, Block) tool NAME + tool-INPUT canonicalization +// (verified live against goose v1.43.0: developer tools arrive bare — `shell`, +// `write`, `edit`, `view`, `read_image`, `tree` — while other extensions +// namespace theirs, e.g. `todo__todo_write`; path-bearing tools deliver the path +// as `path` (or `source` for read_image), which builtins read as `file_path`). +import { describe, it, expect } from "vitest"; +import { canonicalizeToolName, canonicalizeToolInput } from "@/src/hooks/tool-name-canonicalize"; +import { + GOOSE_HOOK_EVENT_TYPES, + GOOSE_TOOL_MAP, + GOOSE_TOOL_INPUT_MAP, + HOOK_EVENT_TYPES, +} from "@/src/hooks/types"; + +describe("Goose event types", () => { + it("are all already-canonical PascalCase HookEventTypes (no event map needed)", () => { + const canonical = new Set(HOOK_EVENT_TYPES); + for (const ev of GOOSE_HOOK_EVENT_TYPES) { + expect(canonical.has(ev), `${ev} must be a HookEventType`).toBe(true); + } + }); + + it("subscribes to the 5 events failproofai installs (no Stop — goose has none)", () => { + expect(GOOSE_HOOK_EVENT_TYPES).toEqual([ + "SessionStart", + "UserPromptSubmit", + "PreToolUse", + "PostToolUse", + "SessionEnd", + ]); + // Goose 1.43.0 has NO Stop event — the require-*-before-stop builtins are + // inapplicable, so Stop must not be installed. + expect(GOOSE_HOOK_EVENT_TYPES).not.toContain("Stop"); + }); +}); + +describe("Goose tool-name canonicalization", () => { + it("canonicalizes bare developer tool ids to Claude builtins", () => { + expect(canonicalizeToolName("shell", "goose")).toBe("Bash"); + expect(canonicalizeToolName("write", "goose")).toBe("Write"); + expect(canonicalizeToolName("edit", "goose")).toBe("Edit"); + expect(canonicalizeToolName("view", "goose")).toBe("Read"); + expect(canonicalizeToolName("read_image", "goose")).toBe("Read"); + expect(canonicalizeToolName("glob", "goose")).toBe("Glob"); + expect(canonicalizeToolName("grep", "goose")).toBe("Grep"); + expect(canonicalizeToolName("tree", "goose")).toBe("LS"); + expect(canonicalizeToolName("delegate", "goose")).toBe("Task"); + }); + + it("canonicalizes namespaced extension tools (__)", () => { + expect(canonicalizeToolName("todo__todo_write", "goose")).toBe("TodoWrite"); + }); + + it("passes unknown / other-extension tools through unchanged", () => { + expect(canonicalizeToolName("analyze", "goose")).toBe("analyze"); + expect(canonicalizeToolName("mcp__server__tool", "goose")).toBe("mcp__server__tool"); + expect(canonicalizeToolName("SomeFutureTool", "goose")).toBe("SomeFutureTool"); + }); + + it("maps every GOOSE_TOOL_MAP key to its declared canonical name", () => { + for (const [raw, canonical] of Object.entries(GOOSE_TOOL_MAP)) { + expect(canonicalizeToolName(raw, "goose")).toBe(canonical); + } + }); +}); + +describe("Goose tool-input canonicalization", () => { + it("maps write/edit `path` → `file_path` so path builtins fire", () => { + expect(canonicalizeToolInput("Write", { path: "/tmp/x", content: "hi" }, "goose")).toEqual({ + file_path: "/tmp/x", + content: "hi", + }); + expect(canonicalizeToolInput("Edit", { path: "/tmp/x", before: "a", after: "b" }, "goose")).toEqual({ + file_path: "/tmp/x", + before: "a", + after: "b", + }); + }); + + it("maps read `path` AND read_image `source` → `file_path`", () => { + expect(canonicalizeToolInput("Read", { path: "/tmp/f.txt" }, "goose")).toEqual({ + file_path: "/tmp/f.txt", + }); + expect(canonicalizeToolInput("Read", { source: "/tmp/img.png" }, "goose")).toEqual({ + file_path: "/tmp/img.png", + }); + }); + + it("leaves shell `command` untouched (already canonical for Bash builtins)", () => { + expect(canonicalizeToolInput("Bash", { command: "echo hi" }, "goose")).toEqual({ + command: "echo hi", + }); + }); + + it("has a GOOSE_TOOL_INPUT_MAP keyed by canonical tool names only", () => { + for (const key of Object.keys(GOOSE_TOOL_INPUT_MAP)) { + expect(["Read", "Write", "Edit", "LS"]).toContain(key); + } + }); +}); diff --git a/__tests__/hooks/handler.test.ts b/__tests__/hooks/handler.test.ts index ded82a16..41be695b 100644 --- a/__tests__/hooks/handler.test.ts +++ b/__tests__/hooks/handler.test.ts @@ -612,116 +612,12 @@ describe("hooks/handler", () => { ); }); - it("canonicalizes Gemini BeforeTool → PreToolUse before evaluating", async () => { - const { evaluatePolicies } = await import("../../src/hooks/policy-evaluator"); - vi.mocked(evaluatePolicies).mockResolvedValueOnce({ - exitCode: 0, - stdout: "", - stderr: "", - policyName: null, - reason: null, - decision: "allow", - }); - mockStdin(JSON.stringify({ tool_name: "run_shell_command", hook_event_name: "BeforeTool" })); - const { persistHookActivity } = await import("../../src/hooks/hook-activity-store"); - - await handleHookEvent("BeforeTool", "gemini"); - - expect(evaluatePolicies).toHaveBeenCalledWith( - "PreToolUse", - expect.any(Object), - expect.any(Object), - expect.any(Object), - ); - expect(persistHookActivity).toHaveBeenCalledWith( - expect.objectContaining({ integration: "gemini", eventType: "PreToolUse" }), - ); - }); - - it("canonicalizes Gemini snake_case tool name run_shell_command → Bash before evaluating", async () => { - const { evaluatePolicies } = await import("../../src/hooks/policy-evaluator"); - vi.mocked(evaluatePolicies).mockResolvedValueOnce({ - exitCode: 0, - stdout: "", - stderr: "", - policyName: null, - reason: null, - decision: "allow", - }); - mockStdin(JSON.stringify({ tool_name: "run_shell_command", hook_event_name: "BeforeTool" })); - const { persistHookActivity } = await import("../../src/hooks/hook-activity-store"); - - await handleHookEvent("BeforeTool", "gemini"); - - // The mutated payload passed to evaluatePolicies should carry the canonicalized tool_name - expect(evaluatePolicies).toHaveBeenCalledWith( - "PreToolUse", - expect.objectContaining({ tool_name: "Bash" }), - expect.any(Object), - expect.any(Object), - ); - // Activity store should also see canonicalized toolName=Bash, NOT raw run_shell_command - expect(persistHookActivity).toHaveBeenCalledWith( - expect.objectContaining({ integration: "gemini", toolName: "Bash" }), - ); - }); - - it("canonicalizes every Gemini tool name in GEMINI_TOOL_MAP", async () => { - const { evaluatePolicies } = await import("../../src/hooks/policy-evaluator"); - const cases: Array<[string, string]> = [ - ["run_shell_command", "Bash"], - ["read_file", "Read"], - ["read_many_files", "Read"], - ["write_file", "Write"], - ["replace", "Edit"], - ["glob", "Glob"], - ["grep_search", "Grep"], - ["list_directory", "LS"], - ["web_fetch", "WebFetch"], - ["google_web_search", "WebSearch"], - ["write_todos", "TodoWrite"], - ["save_memory", "Memory"], - ["ask_user", "AskUser"], - ]; - for (const [raw, canonical] of cases) { - vi.mocked(evaluatePolicies).mockResolvedValueOnce({ - exitCode: 0, stdout: "", stderr: "", policyName: null, reason: null, decision: "allow", - }); - mockStdin(JSON.stringify({ tool_name: raw, hook_event_name: "BeforeTool" })); - await handleHookEvent("BeforeTool", "gemini"); - expect(evaluatePolicies).toHaveBeenLastCalledWith( - "PreToolUse", - expect.objectContaining({ tool_name: canonical }), - expect.any(Object), - expect.any(Object), - ); - } - }); - - it("passes through unknown Gemini tool names (MCP, extensions) unchanged", async () => { - const { evaluatePolicies } = await import("../../src/hooks/policy-evaluator"); - vi.mocked(evaluatePolicies).mockResolvedValueOnce({ - exitCode: 0, stdout: "", stderr: "", policyName: null, reason: null, decision: "allow", - }); - mockStdin(JSON.stringify({ tool_name: "mcp_github_create_issue", hook_event_name: "BeforeTool" })); - - await handleHookEvent("BeforeTool", "gemini"); - - // Unknown tool names are NOT in GEMINI_TOOL_MAP — must pass through unchanged so MCP tools aren't lost - expect(evaluatePolicies).toHaveBeenCalledWith( - "PreToolUse", - expect.objectContaining({ tool_name: "mcp_github_create_issue" }), - expect.any(Object), - expect.any(Object), - ); - }); - it("does NOT canonicalize tool names when cli=claude (other CLIs unaffected)", async () => { const { evaluatePolicies } = await import("../../src/hooks/policy-evaluator"); vi.mocked(evaluatePolicies).mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "", policyName: null, reason: null, decision: "allow", }); - // A Claude session that somehow has a Gemini-shaped tool name should NOT be remapped. + // A Claude session that somehow has a snake_case tool name should NOT be remapped. mockStdin(JSON.stringify({ tool_name: "run_shell_command", hook_event_name: "PreToolUse" })); await handleHookEvent("PreToolUse", "claude"); @@ -734,75 +630,6 @@ describe("hooks/handler", () => { ); }); - it("canonicalizes all 11 Gemini events to canonical names (BeforeAgent → UserPromptSubmit, etc.)", async () => { - const { evaluatePolicies } = await import("../../src/hooks/policy-evaluator"); - const cases: Array<[string, string]> = [ - ["SessionStart", "SessionStart"], - ["SessionEnd", "SessionEnd"], - ["BeforeAgent", "UserPromptSubmit"], - ["AfterAgent", "Stop"], - ["BeforeTool", "PreToolUse"], - ["AfterTool", "PostToolUse"], - ["PreCompress", "PreCompact"], - ["Notification", "Notification"], - // Gemini-only events with no Claude canonical — passthrough. - ["BeforeModel", "BeforeModel"], - ["AfterModel", "AfterModel"], - ["BeforeToolSelection", "BeforeToolSelection"], - ]; - for (const [raw, canonical] of cases) { - vi.mocked(evaluatePolicies).mockResolvedValueOnce({ - exitCode: 0, stdout: "", stderr: "", policyName: null, reason: null, decision: "allow", - }); - mockStdin(JSON.stringify({ hook_event_name: raw })); - await handleHookEvent(raw, "gemini"); - expect(evaluatePolicies).toHaveBeenLastCalledWith( - canonical, - expect.any(Object), - expect.any(Object), - expect.any(Object), - ); - } - }); - - it("tags telemetry with cli=gemini and canonicalized tool_name=Bash when invoked with --cli gemini", async () => { - const { evaluatePolicies } = await import("../../src/hooks/policy-evaluator"); - vi.mocked(evaluatePolicies).mockResolvedValueOnce({ - exitCode: 0, - stdout: '{"decision":"deny","reason":"sudo blocked"}', - stderr: "", - policyName: "block-sudo", - reason: "sudo blocked", - decision: "deny", - }); - mockStdin(JSON.stringify({ tool_name: "run_shell_command", hook_event_name: "BeforeTool" })); - const { trackHookEvent } = await import("../../src/hooks/hook-telemetry"); - - await handleHookEvent("BeforeTool", "gemini"); - - // Telemetry should see the canonicalized tool name (Bash, not run_shell_command) - expect(trackHookEvent).toHaveBeenCalledWith( - "test-instance-id", - "hook_policy_triggered", - expect.objectContaining({ cli: "gemini", event_type: "PreToolUse", tool_name: "Bash" }), - ); - }); - - it("tags activity store entry with integration=gemini and canonicalized eventType for Gemini hook fires", async () => { - const { evaluatePolicies } = await import("../../src/hooks/policy-evaluator"); - vi.mocked(evaluatePolicies).mockResolvedValueOnce({ - exitCode: 0, stdout: "", stderr: "", policyName: null, reason: null, decision: "allow", - }); - mockStdin(JSON.stringify({ tool_name: "read_file", hook_event_name: "BeforeTool" })); - const { persistHookActivity } = await import("../../src/hooks/hook-activity-store"); - - await handleHookEvent("BeforeTool", "gemini"); - - expect(persistHookActivity).toHaveBeenCalledWith( - expect.objectContaining({ integration: "gemini", eventType: "PreToolUse", toolName: "Read" }), - ); - }); - // -- OpenCode handler-side canonicalization (defense-in-depth) ---------- // // The OpenCode plugin shim already canonicalizes tool name + tool input diff --git a/__tests__/hooks/install-prompt.test.ts b/__tests__/hooks/install-prompt.test.ts index e48e8a80..e6de55f8 100644 --- a/__tests__/hooks/install-prompt.test.ts +++ b/__tests__/hooks/install-prompt.test.ts @@ -141,9 +141,9 @@ describe("hooks/install-prompt", () => { "install", ); - // 1 aggregate "all" + 2 detected + 6 undetected - expect(options).toHaveLength(9); - expect(undetected).toEqual(["copilot", "cursor", "opencode", "pi", "gemini", "hermes"]); + // 1 aggregate "all" + 2 detected + 10 undetected + expect(options).toHaveLength(13); + expect(undetected).toEqual(["copilot", "cursor", "opencode", "pi", "hermes", "openclaw", "factory", "devin", "antigravity", "goose"]); expect(options[0]).toMatchObject({ isAll: true, detected: true, value: ["claude", "codex"] }); expect(options[0].label).toBe("Install for all 2 detected"); @@ -162,8 +162,12 @@ describe("hooks/install-prompt", () => { "Cursor Agent", "OpenCode", "Pi", - "Gemini CLI", "Hermes", + "OpenClaw", + "Factory Droid", + "Devin CLI", + "Antigravity CLI", + "Goose", ]); }); @@ -180,16 +184,16 @@ describe("hooks/install-prompt", () => { expect(options.every((o) => o.detected)).toBe(true); }); - it("install with all 8 detected: no aggregate-row needed beyond the standard one, no undetected section", async () => { + it("install with all 12 detected: no aggregate-row needed beyond the standard one, no undetected section", async () => { const { buildCliMenuOptions } = await import("../../src/hooks/install-prompt"); const { options, undetected } = buildCliMenuOptions( - ["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini", "hermes"], + ["claude", "codex", "copilot", "cursor", "opencode", "pi", "hermes", "openclaw", "factory", "devin", "antigravity", "goose"], "install", ); expect(undetected).toEqual([]); - expect(options).toHaveLength(9); // aggregate + 8 detected - expect(options[0].label).toBe("Install for all 8 detected"); + expect(options).toHaveLength(13); // aggregate + 12 detected + expect(options[0].label).toBe("Install for all 12 detected"); }); it("install with 1 detected + many undetected: skips aggregate row (1 ≯ 1)", async () => { diff --git a/__tests__/hooks/integrations.test.ts b/__tests__/hooks/integrations.test.ts index fc392e8e..e525d566 100644 --- a/__tests__/hooks/integrations.test.ts +++ b/__tests__/hooks/integrations.test.ts @@ -20,8 +20,12 @@ import { cursor, opencode, pi, - gemini, hermes, + openclaw, + factory, + devin, + antigravity, + goose, getIntegration, listIntegrations, } from "../../src/hooks/integrations"; @@ -35,18 +39,18 @@ import { OPENCODE_EVENT_MAP, PI_HOOK_EVENT_TYPES, PI_EVENT_MAP, - GEMINI_HOOK_EVENT_TYPES, - GEMINI_EVENT_MAP, - GEMINI_TOOL_MAP, HERMES_HOOK_EVENT_TYPES, HERMES_EVENT_MAP, + FACTORY_HOOK_EVENT_TYPES, + DEVIN_HOOK_EVENT_TYPES, + ANTIGRAVITY_HOOK_EVENT_TYPES, + GOOSE_HOOK_EVENT_TYPES, HOOK_EVENT_TYPES, FAILPROOFAI_HOOK_MARKER, type CodexHookEventType, type CursorHookEventType, type OpenCodeHookEventType, type PiHookEventType, - type GeminiHookEventType, type HermesHookEventType, } from "../../src/hooks/types"; import { homedir } from "node:os"; @@ -73,9 +77,9 @@ afterEach(() => { }); describe("integrations registry", () => { - it("listIntegrations returns claude, codex, copilot, cursor, opencode, pi, gemini, and hermes in declared order", () => { + it("listIntegrations returns claude, codex, copilot, cursor, opencode, pi, hermes, and openclaw in declared order", () => { const ids = listIntegrations().map((i) => i.id); - expect(ids).toEqual(["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini", "hermes"]); + expect(ids).toEqual(["claude", "codex", "copilot", "cursor", "opencode", "pi", "hermes", "openclaw", "factory", "devin", "antigravity", "goose"]); }); it("getIntegration('claude') returns claudeCode", () => { @@ -102,14 +106,30 @@ describe("integrations registry", () => { expect(getIntegration("pi")).toBe(pi); }); - it("getIntegration('gemini') returns gemini", () => { - expect(getIntegration("gemini")).toBe(gemini); - }); - it("getIntegration('hermes') returns hermes", () => { expect(getIntegration("hermes")).toBe(hermes); }); + it("getIntegration('openclaw') returns openclaw", () => { + expect(getIntegration("openclaw")).toBe(openclaw); + }); + + it("getIntegration('factory') returns factory", () => { + expect(getIntegration("factory")).toBe(factory); + }); + + it("getIntegration('antigravity') returns antigravity", () => { + expect(getIntegration("antigravity")).toBe(antigravity); + }); + + it("getIntegration('goose') returns goose", () => { + expect(getIntegration("goose")).toBe(goose); + }); + + it("getIntegration('devin') returns devin", () => { + expect(getIntegration("devin")).toBe(devin); + }); + it("getIntegration throws for unknown id", () => { // @ts-expect-error — testing error path expect(() => getIntegration("unknown-cli")).toThrow(); @@ -1096,204 +1116,525 @@ describe("PI_EVENT_MAP", () => { }); }); -describe("Gemini CLI integration", () => { - it("getSettingsPath maps user → ~/.gemini/settings.json and project → /.gemini/settings.json", () => { - expect(gemini.getSettingsPath("project", tempDir)).toBe( - resolve(tempDir, ".gemini", "settings.json"), - ); - expect(gemini.getSettingsPath("user")).toMatch(/\.gemini\/settings\.json$/); +describe("OpenClaw integration", () => { + it("is user-scope only and points at ~/.openclaw/openclaw.json", () => { + expect(openclaw.scopes).toEqual(["user"]); + // getSettingsPath ignores scope/cwd (OpenClaw has no project config). + const p = openclaw.getSettingsPath("user"); + expect(p).toBe(join(homedir(), ".openclaw", "openclaw.json")); + expect(openclaw.getSettingsPath("project", "/some/where")).toBe(p); }); - it("getSettingsPath('local') falls back to project (no gemini local scope)", () => { - expect(gemini.getSettingsPath("local", tempDir)).toBe( - resolve(tempDir, ".gemini", "settings.json"), - ); + it("writeHookEntries registers the plugin path + enables the entry with allowConversationAccess", () => { + const settings: Record = {}; + openclaw.writeHookEntries(settings, ""); + const plugins = settings.plugins as Record; + expect(Array.isArray(plugins.load.paths)).toBe(true); + expect(plugins.load.paths.some((p: string) => p.endsWith("openclaw-plugin"))).toBe(true); + expect(plugins.entries.failproofai.enabled).toBe(true); + expect(plugins.entries.failproofai.hooks.allowConversationAccess).toBe(true); + }); + + it("writeHookEntries is idempotent (double install → single path entry)", () => { + const settings: Record = {}; + openclaw.writeHookEntries(settings, ""); + openclaw.writeHookEntries(settings, ""); + const plugins = settings.plugins as Record; + const ours = plugins.load.paths.filter((p: string) => p.includes("openclaw-plugin")); + expect(ours).toHaveLength(1); }); - it("scopes are user|project (no local; system scope ignored)", () => { - expect(gemini.scopes).toEqual(["user", "project"]); - expect(gemini.scopes).not.toContain("local"); + it("writeHookEntries preserves operator config (other plugins, deny list, load extras)", () => { + const settings: Record = { + plugins: { + deny: ["untrusted"], + load: { paths: ["/opt/other-plugin"], watch: true }, + entries: { other: { enabled: true, config: { x: 1 } } }, + }, + }; + openclaw.writeHookEntries(settings, ""); + const plugins = settings.plugins as Record; + expect(plugins.deny).toEqual(["untrusted"]); + expect(plugins.load.watch).toBe(true); + expect(plugins.load.paths).toContain("/opt/other-plugin"); + expect(plugins.entries.other).toEqual({ enabled: true, config: { x: 1 } }); + expect(plugins.entries.failproofai.enabled).toBe(true); }); - it("eventTypes are the 11 PascalCase Gemini events", () => { - expect(gemini.eventTypes).toEqual(GEMINI_HOOK_EVENT_TYPES); - expect(gemini.eventTypes).toContain("SessionStart"); - expect(gemini.eventTypes).toContain("SessionEnd"); - expect(gemini.eventTypes).toContain("BeforeAgent"); - expect(gemini.eventTypes).toContain("AfterAgent"); - expect(gemini.eventTypes).toContain("BeforeModel"); - expect(gemini.eventTypes).toContain("AfterModel"); - expect(gemini.eventTypes).toContain("BeforeToolSelection"); - expect(gemini.eventTypes).toContain("BeforeTool"); - expect(gemini.eventTypes).toContain("AfterTool"); - expect(gemini.eventTypes).toContain("PreCompress"); - expect(gemini.eventTypes).toContain("Notification"); - expect(gemini.eventTypes).toHaveLength(11); + it("isFailproofaiHook recognizes our load.paths string entry and the marked sentinel", () => { + expect(openclaw.isFailproofaiHook("/x/failproofai/openclaw-plugin")).toBe(true); + expect(openclaw.isFailproofaiHook("/abs/openclaw-plugin")).toBe(true); + expect(openclaw.isFailproofaiHook("/opt/other-plugin")).toBe(false); + expect(openclaw.isFailproofaiHook({ [FAILPROOFAI_HOOK_MARKER]: true })).toBe(true); }); - it("buildHookEntry uses Claude-shaped {type,command,timeout,marker} with --cli gemini", () => { - const entry = gemini.buildHookEntry("/usr/bin/failproofai", "BeforeTool", "user") as Record; - expect(entry.type).toBe("command"); - expect(entry.command).toBe('"/usr/bin/failproofai" --hook BeforeTool --cli gemini'); - expect(entry.timeout).toBe(60_000); + it("removeHooksFromFile reverses install and prunes empties, leaving operator config intact", () => { + const file = join(tempDir, "openclaw.json"); + const settings: Record = { + plugins: { deny: ["untrusted"], load: { paths: ["/opt/other-plugin"] }, entries: { other: { enabled: true } } }, + }; + openclaw.writeHookEntries(settings, ""); + writeFileSync(file, JSON.stringify(settings)); + + const removed = openclaw.removeHooksFromFile(file); + expect(removed).toBeGreaterThanOrEqual(2); // path + entry + + const after = JSON.parse(readFileSync(file, "utf-8")) as Record; + expect(after.plugins.entries.failproofai).toBeUndefined(); + expect(after.plugins.entries.other).toEqual({ enabled: true }); + expect(after.plugins.load.paths).toEqual(["/opt/other-plugin"]); + expect(after.plugins.deny).toEqual(["untrusted"]); + }); + + it("removeHooksFromFile deletes an empty plugins object when nothing else remains", () => { + const file = join(tempDir, "openclaw2.json"); + const settings: Record = {}; + openclaw.writeHookEntries(settings, ""); + writeFileSync(file, JSON.stringify(settings)); + + openclaw.removeHooksFromFile(file); + const after = JSON.parse(readFileSync(file, "utf-8")) as Record; + expect(after.plugins).toBeUndefined(); + }); +}); + +describe("Factory Droid integration", () => { + it("getSettingsPath maps user → ~/.factory/hooks.json and project → /.factory/hooks.json", () => { + expect(factory.getSettingsPath("project", tempDir)).toBe( + resolve(tempDir, ".factory", "hooks.json"), + ); + expect(factory.getSettingsPath("user")).toMatch(/\.factory\/hooks\.json$/); + }); + + it("scopes are user|project (no local)", () => { + expect(factory.scopes).toEqual(["user", "project"]); + }); + + it("buildHookEntry includes --cli factory and a 30s timeout", () => { + const entry = factory.buildHookEntry("/usr/bin/failproofai", "PreToolUse", "user"); + expect(entry.command).toContain("--cli factory"); + expect(entry.command).toContain("--hook PreToolUse"); + expect(entry.timeout).toBe(30); expect(entry[FAILPROOFAI_HOOK_MARKER]).toBe(true); - // Gemini entries use Claude's `command` field, not Copilot's bash/powershell split. - expect(entry.bash).toBeUndefined(); - expect(entry.powershell).toBeUndefined(); }); - it("project scope uses npx -y failproofai (portable across machines)", () => { - const entry = gemini.buildHookEntry("/usr/bin/failproofai", "BeforeTool", "project") as Record; - expect(entry.command).toBe("npx -y failproofai --hook BeforeTool --cli gemini"); + it("project scope uses npx -y failproofai", () => { + const entry = factory.buildHookEntry("/usr/bin/failproofai", "PreToolUse", "project"); + expect(entry.command).toBe("npx -y failproofai --hook PreToolUse --cli factory"); }); - it("writeHookEntries writes the matcher-wrapper schema for all 11 events with matcher='*'", () => { + it("writeHookEntries stores event names at the TOP LEVEL (no `hooks` wrapper)", () => { const settings: Record = {}; - gemini.writeHookEntries(settings, "/usr/bin/failproofai", "user"); - const hooks = settings.hooks as Record>>; - for (const eventType of GEMINI_HOOK_EVENT_TYPES) { - expect(hooks[eventType]).toBeDefined(); - const matchers = hooks[eventType]; - expect(matchers.length).toBeGreaterThanOrEqual(1); - // Matcher-wrapper: each element is {matcher, hooks: [{type, command, ...}]} - expect(matchers[0].matcher).toBe("*"); - const inner = matchers[0].hooks as Array>; - expect(inner).toHaveLength(1); - expect(inner[0].type).toBe("command"); - expect(typeof inner[0].command).toBe("string"); - expect(inner[0][FAILPROOFAI_HOOK_MARKER]).toBe(true); + factory.writeHookEntries(settings, "/usr/bin/failproofai", "user"); + // No wrapper key — the file IS the events object. + expect(settings.hooks).toBeUndefined(); + for (const eventType of FACTORY_HOOK_EVENT_TYPES) { + expect(Array.isArray(settings[eventType])).toBe(true); } }); - it("re-running writeHookEntries is idempotent (replaces, doesn't duplicate)", () => { + it("writeHookEntries adds matcher:'*' for tool events and omits it elsewhere", () => { const settings: Record = {}; - gemini.writeHookEntries(settings, "/usr/bin/failproofai", "user"); - gemini.writeHookEntries(settings, "/different/path/failproofai", "user"); - const hooks = settings.hooks as Record>>; - // Each event has exactly one matcher; the inner hook is the most recent. - expect(hooks.BeforeTool).toHaveLength(1); - const inner = (hooks.BeforeTool[0].hooks as Array>)[0]; - expect(inner.command).toBe('"/different/path/failproofai" --hook BeforeTool --cli gemini'); + factory.writeHookEntries(settings, "/usr/bin/failproofai", "user"); + const pre = (settings.PreToolUse as Array>)[0]; + const post = (settings.PostToolUse as Array>)[0]; + const stop = (settings.Stop as Array>)[0]; + expect(pre.matcher).toBe("*"); + expect(post.matcher).toBe("*"); + expect(stop.matcher).toBeUndefined(); }); - it("writeHookEntries preserves a hand-written user hook with the same event key", () => { - const userHook = { type: "command", command: "/my/script.sh", timeout: 5000 }; - const settings: Record = { - hooks: { BeforeTool: [{ matcher: "write_file", hooks: [userHook] }] }, - }; - gemini.writeHookEntries(settings, "/usr/bin/failproofai", "user"); - const hooks = settings.hooks as Record>>; - // User's hook is preserved at index 0, ours appended at index 1 - expect(hooks.BeforeTool).toHaveLength(2); - const userMatcher = hooks.BeforeTool[0]; - expect(userMatcher.matcher).toBe("write_file"); - expect((userMatcher.hooks as Array>)[0].command).toBe("/my/script.sh"); - const ourMatcher = hooks.BeforeTool[1]; - expect(ourMatcher.matcher).toBe("*"); - expect((ourMatcher.hooks as Array>)[0][FAILPROOFAI_HOOK_MARKER]).toBe(true); - }); - - it("removeHooksFromFile removes only failproofai-marked entries (preserves user hooks)", () => { - const userHook = { type: "command", command: "/my/script.sh", timeout: 5000 }; - const settingsPath = gemini.getSettingsPath("project", tempDir); - mkdirSync(resolve(tempDir, ".gemini"), { recursive: true }); + it("re-running writeHookEntries is idempotent", () => { + const settings: Record = {}; + factory.writeHookEntries(settings, "/usr/bin/failproofai", "user"); + factory.writeHookEntries(settings, "/different/path/failproofai", "user"); + const pre = settings.PreToolUse as Array<{ hooks: unknown[] }>; + expect(pre).toHaveLength(1); + expect(pre[0].hooks).toHaveLength(1); + }); + + it("removeHooksFromFile clears all failproofai entries (returns count)", () => { + const settingsPath = factory.getSettingsPath("project", tempDir); + const settings: Record = {}; + factory.writeHookEntries(settings, "/usr/bin/failproofai", "project"); + factory.writeSettings(settingsPath, settings); + expect(existsSync(settingsPath)).toBe(true); + + const removed = factory.removeHooksFromFile(settingsPath); + expect(removed).toBe(FACTORY_HOOK_EVENT_TYPES.length); + + const after = JSON.parse(readFileSync(settingsPath, "utf-8")) as Record; + for (const eventType of FACTORY_HOOK_EVENT_TYPES) { + expect(after[eventType]).toBeUndefined(); + } + }); + + it("removeHooksFromFile preserves a user's own hook entries", () => { + const settingsPath = factory.getSettingsPath("project", tempDir); const settings: Record = { - hooks: { BeforeTool: [{ matcher: "write_file", hooks: [userHook] }] }, + PreToolUse: [{ matcher: "*", hooks: [{ type: "command", command: "my-own-hook" }] }], }; - gemini.writeHookEntries(settings, "/usr/bin/failproofai", "project"); - gemini.writeSettings(settingsPath, settings); + factory.writeHookEntries(settings, "/usr/bin/failproofai", "project"); + factory.writeSettings(settingsPath, settings); - const removed = gemini.removeHooksFromFile(settingsPath); - // 11 events × 1 marked entry each = 11 removed - expect(removed).toBe(GEMINI_HOOK_EVENT_TYPES.length); + factory.removeHooksFromFile(settingsPath); + const after = JSON.parse(readFileSync(settingsPath, "utf-8")) as Record; + // The user's own hook survives; only the failproofai-marked entry is gone. + const flattened = (after.PreToolUse ?? []).flatMap((m: any) => m.hooks ?? []); + expect(flattened.some((h: any) => h.command === "my-own-hook")).toBe(true); + expect(flattened.some((h: any) => h[FAILPROOFAI_HOOK_MARKER] === true)).toBe(false); + }); - const after = JSON.parse(readFileSync(settingsPath, "utf-8")) as Record; - const afterHooks = after.hooks as Record; - // User's BeforeTool hook still there - expect(afterHooks.BeforeTool).toHaveLength(1); - expect((afterHooks.BeforeTool[0] as Record).matcher).toBe("write_file"); - // Other event keys (which only had failproofai entries) are deleted - expect(afterHooks.SessionStart).toBeUndefined(); + it("hooksInstalledInSettings detects installed hooks", () => { + const settingsPath = factory.getSettingsPath("project", tempDir); + const settings: Record = {}; + factory.writeHookEntries(settings, "/usr/bin/failproofai", "project"); + factory.writeSettings(settingsPath, settings); + + expect(factory.hooksInstalledInSettings("project", tempDir)).toBe(true); + }); +}); + +describe("Devin CLI integration", () => { + it("getSettingsPath maps user → ~/.config/devin/config.json and project → /.devin/config.json", () => { + expect(devin.getSettingsPath("project", tempDir)).toBe( + resolve(tempDir, ".devin", "config.json"), + ); + expect(devin.getSettingsPath("user")).toMatch(/\.config\/devin\/config\.json$/); }); - it("removeHooksFromFile clears all and removes the top-level hooks key when nothing remains", () => { - const settingsPath = gemini.getSettingsPath("project", tempDir); - mkdirSync(resolve(tempDir, ".gemini"), { recursive: true }); + it("scopes are user|project (no local)", () => { + expect(devin.scopes).toEqual(["user", "project"]); + }); + + it("subscribes to the 7 verified devin events", () => { + expect(DEVIN_HOOK_EVENT_TYPES).toEqual([ + "SessionStart", + "UserPromptSubmit", + "PreToolUse", + "PostToolUse", + "PermissionRequest", + "Stop", + "SessionEnd", + ]); + }); + + it("every devin event is a canonical HookEventType (no event map needed)", () => { + const canonical = new Set(HOOK_EVENT_TYPES); + for (const ev of DEVIN_HOOK_EVENT_TYPES) { + expect(canonical.has(ev), `${ev} must be a HookEventType`).toBe(true); + } + }); + + it("buildHookEntry includes --cli devin and a 60s timeout", () => { + const entry = devin.buildHookEntry("/usr/bin/failproofai", "PreToolUse", "user"); + expect(entry.command).toContain("--cli devin"); + expect(entry.command).toContain("--hook PreToolUse"); + expect(entry.command).toBe(`"/usr/bin/failproofai" --hook PreToolUse --cli devin`); + expect(entry.timeout).toBe(60); + expect(entry[FAILPROOFAI_HOOK_MARKER]).toBe(true); + }); + + it("project scope uses npx -y failproofai", () => { + const entry = devin.buildHookEntry("/usr/bin/failproofai", "PreToolUse", "project"); + expect(entry.command).toBe("npx -y failproofai --hook PreToolUse --cli devin"); + }); + + it("writeHookEntries stores events under a Claude-style `hooks` wrapper", () => { const settings: Record = {}; - gemini.writeHookEntries(settings, "/usr/bin/failproofai", "project"); - gemini.writeSettings(settingsPath, settings); + devin.writeHookEntries(settings, "/usr/bin/failproofai", "user"); + const hooks = settings.hooks as Record; + expect(hooks).toBeDefined(); + for (const eventType of DEVIN_HOOK_EVENT_TYPES) { + expect(Array.isArray(hooks[eventType])).toBe(true); + } + }); - const removed = gemini.removeHooksFromFile(settingsPath); - expect(removed).toBe(GEMINI_HOOK_EVENT_TYPES.length); + it("writeHookEntries preserves other top-level config keys (org_id, theme_mode)", () => { + const settings: Record = { org_id: "acme", theme_mode: "dark" }; + devin.writeHookEntries(settings, "/usr/bin/failproofai", "user"); + expect(settings.org_id).toBe("acme"); + expect(settings.theme_mode).toBe("dark"); + }); + + it("re-running writeHookEntries is idempotent", () => { + const settings: Record = {}; + devin.writeHookEntries(settings, "/usr/bin/failproofai", "user"); + devin.writeHookEntries(settings, "/different/path/failproofai", "user"); + const hooks = settings.hooks as Record>; + expect(hooks.PreToolUse).toHaveLength(1); + expect(hooks.PreToolUse[0].hooks).toHaveLength(1); + }); + + it("removeHooksFromFile clears all failproofai entries (returns count)", () => { + const settingsPath = devin.getSettingsPath("project", tempDir); + const settings: Record = {}; + devin.writeHookEntries(settings, "/usr/bin/failproofai", "project"); + devin.writeSettings(settingsPath, settings); + expect(existsSync(settingsPath)).toBe(true); + + const removed = devin.removeHooksFromFile(settingsPath); + expect(removed).toBe(DEVIN_HOOK_EVENT_TYPES.length); const after = JSON.parse(readFileSync(settingsPath, "utf-8")) as Record; expect(after.hooks).toBeUndefined(); }); + it("removeHooksFromFile preserves a user's own hook entries and other keys", () => { + const settingsPath = devin.getSettingsPath("project", tempDir); + const settings: Record = { + org_id: "acme", + hooks: { + PreToolUse: [{ hooks: [{ type: "command", command: "my-own-hook" }] }], + }, + }; + devin.writeHookEntries(settings, "/usr/bin/failproofai", "project"); + devin.writeSettings(settingsPath, settings); + + devin.removeHooksFromFile(settingsPath); + const after = JSON.parse(readFileSync(settingsPath, "utf-8")) as Record; + expect(after.org_id).toBe("acme"); + const flattened = (after.hooks?.PreToolUse ?? []).flatMap((m: any) => m.hooks ?? []); + expect(flattened.some((h: any) => h.command === "my-own-hook")).toBe(true); + expect(flattened.some((h: any) => h[FAILPROOFAI_HOOK_MARKER] === true)).toBe(false); + }); + it("hooksInstalledInSettings detects installed hooks", () => { - const settingsPath = gemini.getSettingsPath("project", tempDir); - mkdirSync(resolve(tempDir, ".gemini"), { recursive: true }); + const settingsPath = devin.getSettingsPath("project", tempDir); const settings: Record = {}; - gemini.writeHookEntries(settings, "/usr/bin/failproofai", "project"); - gemini.writeSettings(settingsPath, settings); + devin.writeHookEntries(settings, "/usr/bin/failproofai", "project"); + devin.writeSettings(settingsPath, settings); + + expect(devin.hooksInstalledInSettings("project", tempDir)).toBe(true); + }); +}); - expect(gemini.hooksInstalledInSettings("project", tempDir)).toBe(true); +describe("Antigravity CLI integration", () => { + it("getSettingsPath maps user → ~/.gemini/config/hooks.json and project → /.agents/hooks.json", () => { + expect(antigravity.getSettingsPath("project", tempDir)).toBe( + resolve(tempDir, ".agents", "hooks.json"), + ); + expect(antigravity.getSettingsPath("user")).toMatch(/\.gemini\/config\/hooks\.json$/); }); - it("hooksInstalledInSettings returns false when file is missing", () => { - expect(gemini.hooksInstalledInSettings("project", tempDir)).toBe(false); + it("scopes are user|project (no local)", () => { + expect(antigravity.scopes).toEqual(["user", "project"]); }); - it("hooksInstalledInSettings returns false on corrupt JSON (fail-open)", () => { - const settingsPath = gemini.getSettingsPath("project", tempDir); - mkdirSync(resolve(tempDir, ".gemini"), { recursive: true }); - writeFileSync(settingsPath, "{not json"); - expect(gemini.hooksInstalledInSettings("project", tempDir)).toBe(false); + it("subscribes to the 4 verified agy events", () => { + expect(ANTIGRAVITY_HOOK_EVENT_TYPES).toEqual([ + "PreToolUse", + "PostToolUse", + "PreInvocation", + "Stop", + ]); }); -}); -describe("GEMINI_EVENT_MAP", () => { - it("maps every Gemini event to a canonical HookEventType (or passthrough)", () => { - expect(GEMINI_EVENT_MAP.SessionStart).toBe("SessionStart"); - expect(GEMINI_EVENT_MAP.SessionEnd).toBe("SessionEnd"); - expect(GEMINI_EVENT_MAP.BeforeAgent).toBe("UserPromptSubmit"); - expect(GEMINI_EVENT_MAP.AfterAgent).toBe("Stop"); - expect(GEMINI_EVENT_MAP.BeforeTool).toBe("PreToolUse"); - expect(GEMINI_EVENT_MAP.AfterTool).toBe("PostToolUse"); - expect(GEMINI_EVENT_MAP.PreCompress).toBe("PreCompact"); - expect(GEMINI_EVENT_MAP.Notification).toBe("Notification"); - // Three Gemini-only events have no canonical Claude equivalent — passthrough. - expect(GEMINI_EVENT_MAP.BeforeModel).toBe("BeforeModel"); - expect(GEMINI_EVENT_MAP.AfterModel).toBe("AfterModel"); - expect(GEMINI_EVENT_MAP.BeforeToolSelection).toBe("BeforeToolSelection"); - }); - - it("GEMINI_EVENT_MAP keys exactly match GEMINI_HOOK_EVENT_TYPES", () => { - const mapKeys = Object.keys(GEMINI_EVENT_MAP).sort(); - const eventTypes = [...GEMINI_HOOK_EVENT_TYPES].sort(); - expect(mapKeys).toEqual(eventTypes); + it("buildHookEntry includes --cli antigravity and a 30s timeout", () => { + const entry = antigravity.buildHookEntry("/usr/bin/failproofai", "PreToolUse", "user"); + expect(entry.command).toContain("--cli antigravity"); + expect(entry.command).toContain("--hook PreToolUse"); + expect(entry.timeout).toBe(30); + expect(entry[FAILPROOFAI_HOOK_MARKER]).toBe(true); + }); + + it("project scope uses npx -y failproofai", () => { + const entry = antigravity.buildHookEntry("/usr/bin/failproofai", "PreToolUse", "project"); + expect(entry.command).toBe("npx -y failproofai --hook PreToolUse --cli antigravity"); + }); + + it("writeHookEntries nests events under a named 'failproofai' hook key", () => { + const settings: Record = {}; + antigravity.writeHookEntries(settings, "/usr/bin/failproofai", "user"); + const named = settings.failproofai as Record; + expect(named).toBeDefined(); + for (const eventType of ANTIGRAVITY_HOOK_EVENT_TYPES) { + expect(Array.isArray(named[eventType])).toBe(true); + } + }); + + it("tool events use a {matcher:'*', hooks} wrapper; PreInvocation/Stop are flat handler arrays", () => { + const settings: Record = {}; + antigravity.writeHookEntries(settings, "/usr/bin/failproofai", "user"); + const named = settings.failproofai as Record; + + // Tool events → wrapper with matcher "*". + const pre = named.PreToolUse[0]; + const post = named.PostToolUse[0]; + expect(pre.matcher).toBe("*"); + expect(Array.isArray(pre.hooks)).toBe(true); + expect(pre.hooks[0][FAILPROOFAI_HOOK_MARKER]).toBe(true); + expect(post.matcher).toBe("*"); + + // Flat events → the handler object sits directly in the array (no wrapper). + const preInvocation = named.PreInvocation[0]; + const stop = named.Stop[0]; + expect(preInvocation.matcher).toBeUndefined(); + expect(preInvocation.hooks).toBeUndefined(); + expect(preInvocation.type).toBe("command"); + expect(preInvocation[FAILPROOFAI_HOOK_MARKER]).toBe(true); + expect(stop.matcher).toBeUndefined(); + expect(stop.type).toBe("command"); + expect(stop[FAILPROOFAI_HOOK_MARKER]).toBe(true); + }); + + it("re-running writeHookEntries is idempotent (tool wrapper + flat handler)", () => { + const settings: Record = {}; + antigravity.writeHookEntries(settings, "/usr/bin/failproofai", "user"); + antigravity.writeHookEntries(settings, "/different/path/failproofai", "user"); + const named = settings.failproofai as Record; + expect(named.PreToolUse).toHaveLength(1); + expect(named.PreToolUse[0].hooks).toHaveLength(1); + expect(named.Stop).toHaveLength(1); + }); + + it("removeHooksFromFile clears all failproofai entries (returns count)", () => { + const settingsPath = antigravity.getSettingsPath("project", tempDir); + const settings: Record = {}; + antigravity.writeHookEntries(settings, "/usr/bin/failproofai", "project"); + antigravity.writeSettings(settingsPath, settings); + expect(existsSync(settingsPath)).toBe(true); + + const removed = antigravity.removeHooksFromFile(settingsPath); + expect(removed).toBe(ANTIGRAVITY_HOOK_EVENT_TYPES.length); + + const after = JSON.parse(readFileSync(settingsPath, "utf-8")) as Record; + // The named hook is dropped entirely once empty. + expect(after.failproofai).toBeUndefined(); + }); + + it("removeHooksFromFile preserves other named hooks", () => { + const settingsPath = antigravity.getSettingsPath("project", tempDir); + const settings: Record = { + "lint-checker": { + PostToolUse: [{ matcher: "run_command", hooks: [{ type: "command", command: "./lint.sh" }] }], + }, + }; + antigravity.writeHookEntries(settings, "/usr/bin/failproofai", "project"); + antigravity.writeSettings(settingsPath, settings); + + antigravity.removeHooksFromFile(settingsPath); + const after = JSON.parse(readFileSync(settingsPath, "utf-8")) as Record; + // The user's own named hook survives; the failproofai named hook is gone. + expect(after["lint-checker"]).toBeDefined(); + expect(after["lint-checker"].PostToolUse[0].hooks[0].command).toBe("./lint.sh"); + expect(after.failproofai).toBeUndefined(); }); - it("GeminiHookEventType is exhaustive", () => { - const sample: GeminiHookEventType = "BeforeTool"; - expect(GEMINI_EVENT_MAP[sample]).toBe("PreToolUse"); + it("hooksInstalledInSettings detects installed hooks", () => { + const settingsPath = antigravity.getSettingsPath("project", tempDir); + const settings: Record = {}; + antigravity.writeHookEntries(settings, "/usr/bin/failproofai", "project"); + antigravity.writeSettings(settingsPath, settings); + + expect(antigravity.hooksInstalledInSettings("project", tempDir)).toBe(true); }); }); -describe("GEMINI_TOOL_MAP", () => { - it("maps every documented Gemini snake_case tool name to a Claude PascalCase canonical name", () => { - expect(GEMINI_TOOL_MAP.run_shell_command).toBe("Bash"); - expect(GEMINI_TOOL_MAP.read_file).toBe("Read"); - expect(GEMINI_TOOL_MAP.read_many_files).toBe("Read"); - expect(GEMINI_TOOL_MAP.write_file).toBe("Write"); - expect(GEMINI_TOOL_MAP.replace).toBe("Edit"); - expect(GEMINI_TOOL_MAP.glob).toBe("Glob"); - expect(GEMINI_TOOL_MAP.grep_search).toBe("Grep"); - expect(GEMINI_TOOL_MAP.list_directory).toBe("LS"); - expect(GEMINI_TOOL_MAP.web_fetch).toBe("WebFetch"); - expect(GEMINI_TOOL_MAP.google_web_search).toBe("WebSearch"); - expect(GEMINI_TOOL_MAP.write_todos).toBe("TodoWrite"); - expect(GEMINI_TOOL_MAP.save_memory).toBe("Memory"); - expect(GEMINI_TOOL_MAP.ask_user).toBe("AskUser"); +describe("Goose integration", () => { + it("getSettingsPath maps user/project to the Open Plugins hooks.json in the plugin dir", () => { + expect(goose.getSettingsPath("project", tempDir)).toBe( + resolve(tempDir, ".agents", "plugins", "failproofai", "hooks", "hooks.json"), + ); + expect(goose.getSettingsPath("user")).toMatch( + /\.agents\/plugins\/failproofai\/hooks\/hooks\.json$/, + ); + }); + + it("scopes are user|project (no local)", () => { + expect(goose.scopes).toEqual(["user", "project"]); + }); + + it("subscribes to the 5 verified events and NOT Stop (goose has none)", () => { + expect(GOOSE_HOOK_EVENT_TYPES).toEqual([ + "SessionStart", + "UserPromptSubmit", + "PreToolUse", + "PostToolUse", + "SessionEnd", + ]); + expect(GOOSE_HOOK_EVENT_TYPES).not.toContain("Stop"); + }); + + it("buildHookEntry emits a clean {type, command} with --cli goose and NO marker field", () => { + const entry = goose.buildHookEntry("/usr/bin/failproofai", "PreToolUse", "user"); + expect(entry.type).toBe("command"); + expect(entry.command).toContain("--cli goose"); + expect(entry.command).toContain("--hook PreToolUse"); + // Goose parses this file, so we add NO __failproofai_hook__ marker. + expect(entry[FAILPROOFAI_HOOK_MARKER]).toBeUndefined(); + expect(entry.timeout).toBeUndefined(); + }); + + it("project scope uses npx -y failproofai", () => { + const entry = goose.buildHookEntry("/usr/bin/failproofai", "PreToolUse", "project"); + expect(entry.command).toBe("npx -y failproofai --hook PreToolUse --cli goose"); + }); + + it("writeHookEntries writes the Open Plugins schema (top-level 'hooks' wrapper, matcher OMITTED)", () => { + const settings: Record = {}; + goose.writeHookEntries(settings, "/usr/bin/failproofai", "user"); + const hooks = settings.hooks as Record; + expect(hooks).toBeDefined(); + for (const eventType of GOOSE_HOOK_EVENT_TYPES) { + expect(Array.isArray(hooks[eventType])).toBe(true); + const matcherObj = hooks[eventType][0]; + // matcher must be OMITTED — a bare "*" is an invalid regex that matches nothing. + expect(matcherObj.matcher).toBeUndefined(); + expect(Array.isArray(matcherObj.hooks)).toBe(true); + expect(matcherObj.hooks[0].command).toContain("--cli goose"); + } + }); + + it("re-running writeHookEntries is idempotent (updates in place, no dup)", () => { + const settings: Record = {}; + goose.writeHookEntries(settings, "/usr/bin/failproofai", "user"); + goose.writeHookEntries(settings, "/different/path/failproofai", "user"); + const hooks = settings.hooks as Record; + expect(hooks.PreToolUse).toHaveLength(1); + expect(hooks.PreToolUse[0].hooks).toHaveLength(1); + // Updated to the new binary path. + expect(hooks.PreToolUse[0].hooks[0].command).toContain("/different/path/failproofai"); + }); + + it("removeHooksFromFile clears all failproofai entries (returns count)", () => { + const settingsPath = goose.getSettingsPath("project", tempDir); + const settings: Record = {}; + goose.writeHookEntries(settings, "/usr/bin/failproofai", "project"); + goose.writeSettings(settingsPath, settings); + expect(existsSync(settingsPath)).toBe(true); + + const removed = goose.removeHooksFromFile(settingsPath); + expect(removed).toBe(GOOSE_HOOK_EVENT_TYPES.length); + + const after = JSON.parse(readFileSync(settingsPath, "utf-8")) as Record; + expect(after.hooks).toBeUndefined(); + }); + + it("removeHooksFromFile preserves a user's own non-failproofai plugin hooks", () => { + const settingsPath = goose.getSettingsPath("project", tempDir); + const settings: Record = { + hooks: { + PostToolUse: [{ hooks: [{ type: "command", command: "${PLUGIN_ROOT}/scripts/log.sh" }] }], + }, + }; + goose.writeHookEntries(settings, "/usr/bin/failproofai", "project"); + goose.writeSettings(settingsPath, settings); + + goose.removeHooksFromFile(settingsPath); + const after = JSON.parse(readFileSync(settingsPath, "utf-8")) as Record; + // The user's own log hook survives; failproofai's is gone. + const postHooks = after.hooks.PostToolUse.flatMap((m: any) => m.hooks); + expect(postHooks.some((h: any) => h.command.includes("log.sh"))).toBe(true); + expect(postHooks.some((h: any) => h.command.includes("--cli goose"))).toBe(false); + }); + + it("hooksInstalledInSettings detects installed hooks", () => { + const settingsPath = goose.getSettingsPath("project", tempDir); + const settings: Record = {}; + goose.writeHookEntries(settings, "/usr/bin/failproofai", "project"); + goose.writeSettings(settingsPath, settings); + + expect(goose.hooksInstalledInSettings("project", tempDir)).toBe(true); }); }); diff --git a/__tests__/hooks/openclaw-canonicalize.test.ts b/__tests__/hooks/openclaw-canonicalize.test.ts new file mode 100644 index 00000000..bb4d828f --- /dev/null +++ b/__tests__/hooks/openclaw-canonicalize.test.ts @@ -0,0 +1,75 @@ +// @vitest-environment node +// +// Locks in OpenClaw event + tool NAME + INPUT canonicalization (verified live +// against openclaw v2026.7.1: before_tool_call payload tool `exec` with params +// `{command}`; file tools deliver the path as `path`, which Claude builtins read +// as `file_path`). +import { describe, it, expect } from "vitest"; +import { canonicalizeToolName, canonicalizeToolInput } from "@/src/hooks/tool-name-canonicalize"; +import { + OPENCLAW_EVENT_MAP, + OPENCLAW_HOOK_EVENT_TYPES, + HOOK_EVENT_TYPES, +} from "@/src/hooks/types"; + +describe("OpenClaw event canonicalization", () => { + it("maps every subscribed hook to a valid canonical event", () => { + const canonical = new Set(HOOK_EVENT_TYPES); + for (const ev of OPENCLAW_HOOK_EVENT_TYPES) { + const mapped = OPENCLAW_EVENT_MAP[ev]; + expect(mapped, `${ev} must map`).toBeTruthy(); + expect(canonical.has(mapped), `${mapped} must be a HookEventType`).toBe(true); + } + }); + + it("maps the enforcement hooks to the right canonical events", () => { + expect(OPENCLAW_EVENT_MAP.before_tool_call).toBe("PreToolUse"); + expect(OPENCLAW_EVENT_MAP.after_tool_call).toBe("PostToolUse"); + expect(OPENCLAW_EVENT_MAP.before_agent_run).toBe("UserPromptSubmit"); + // before_agent_finalize is the real turn-end gate (unlike Hermes, which has none). + expect(OPENCLAW_EVENT_MAP.before_agent_finalize).toBe("Stop"); + expect(OPENCLAW_EVENT_MAP.session_start).toBe("SessionStart"); + expect(OPENCLAW_EVENT_MAP.session_end).toBe("SessionEnd"); + expect(OPENCLAW_EVENT_MAP.subagent_ended).toBe("SubagentStop"); + expect(OPENCLAW_EVENT_MAP.before_compaction).toBe("PreCompact"); + }); + + it("does not subscribe to agent_end (would double-fire Stop) or message_sending (deferred)", () => { + expect(OPENCLAW_HOOK_EVENT_TYPES).not.toContain("agent_end"); + expect(OPENCLAW_HOOK_EVENT_TYPES).not.toContain("message_sending"); + }); +}); + +describe("OpenClaw tool canonicalization", () => { + it("canonicalizes OpenClaw tool ids to Claude builtins", () => { + expect(canonicalizeToolName("exec", "openclaw")).toBe("Bash"); + expect(canonicalizeToolName("read", "openclaw")).toBe("Read"); + expect(canonicalizeToolName("write", "openclaw")).toBe("Write"); + expect(canonicalizeToolName("edit", "openclaw")).toBe("Edit"); + expect(canonicalizeToolName("grep", "openclaw")).toBe("Grep"); + expect(canonicalizeToolName("glob", "openclaw")).toBe("Glob"); + expect(canonicalizeToolName("web_search", "openclaw")).toBe("WebSearch"); + expect(canonicalizeToolName("web_fetch", "openclaw")).toBe("WebFetch"); + }); + + it("passes unknown OpenClaw tools through unchanged (browser, process, memory_*)", () => { + expect(canonicalizeToolName("browser", "openclaw")).toBe("browser"); + expect(canonicalizeToolName("process", "openclaw")).toBe("process"); + expect(canonicalizeToolName("memory_search", "openclaw")).toBe("memory_search"); + }); + + it("maps the file tools' `path` arg to `file_path` so path builtins fire", () => { + expect(canonicalizeToolInput("Read", { path: "/srv/.env" }, "openclaw")).toEqual({ file_path: "/srv/.env" }); + expect(canonicalizeToolInput("Write", { path: "/srv/x", content: "s" }, "openclaw")).toEqual({ + file_path: "/srv/x", + content: "s", + }); + expect(canonicalizeToolInput("Edit", { path: "/srv/x" }, "openclaw")).toEqual({ file_path: "/srv/x" }); + }); + + it("leaves exec's already-canonical `command` unchanged (matches Bash builtins)", () => { + expect(canonicalizeToolInput("Bash", { command: "sudo rm -rf /" }, "openclaw")).toEqual({ + command: "sudo rm -rf /", + }); + }); +}); diff --git a/__tests__/hooks/policy-evaluator.test.ts b/__tests__/hooks/policy-evaluator.test.ts index 7640c127..390aee65 100644 --- a/__tests__/hooks/policy-evaluator.test.ts +++ b/__tests__/hooks/policy-evaluator.test.ts @@ -271,6 +271,54 @@ describe("hooks/policy-evaluator", () => { expect(result.stderr).toContain("prefer git mv"); // surfaced on stderr for logs }); + it("OpenClaw deny on PreToolUse emits flat {permission:'deny'} (shim maps to {block:true})", async () => { + registerPolicy("blocker", "desc", () => ({ decision: "deny", reason: "no sudo" }), { + events: ["PreToolUse"], + }); + const result = await evaluatePolicies("PreToolUse", { tool_name: "Bash" }, { cli: "openclaw" }); + expect(result.exitCode).toBe(0); + expect(result.stderr).toBe(""); + expect(result.decision).toBe("deny"); + const parsed = JSON.parse(result.stdout) as Record; + expect(parsed.permission).toBe("deny"); + expect(parsed.reason).toContain("no sudo"); + }); + + it("OpenClaw deny on Stop emits MANDATORY-ACTION wording (shim maps to {action:'revise'})", async () => { + registerPolicy("commit", "desc", () => ({ decision: "deny", reason: "commit first" }), { + events: ["Stop"], + }); + const result = await evaluatePolicies("Stop", {}, { cli: "openclaw" }); + expect(result.exitCode).toBe(0); + expect(result.decision).toBe("deny"); + const parsed = JSON.parse(result.stdout) as Record; + expect(parsed.permission).toBe("deny"); + expect(String(parsed.reason)).toContain("MANDATORY ACTION REQUIRED"); + expect(String(parsed.reason)).toContain("commit first"); + }); + + it("OpenClaw instruct on Stop emits MANDATORY-ACTION deny (revise); on tool events degrades to allow + note", async () => { + registerPolicy("advise-stop", "desc", () => ({ decision: "instruct", reason: "run tests" }), { + events: ["Stop"], + }); + const stop = await evaluatePolicies("Stop", {}, { cli: "openclaw" }); + const stopParsed = JSON.parse(stop.stdout) as Record; + expect(stop.decision).toBe("instruct"); + expect(stopParsed.permission).toBe("deny"); // revise on Stop + expect(String(stopParsed.reason)).toContain("MANDATORY ACTION REQUIRED"); + + clearPolicies(); + registerPolicy("advise-tool", "desc", () => ({ decision: "instruct", reason: "prefer git mv" }), { + events: ["PreToolUse"], + }); + const pre = await evaluatePolicies("PreToolUse", { tool_name: "Bash" }, { cli: "openclaw" }); + expect(pre.decision).toBe("instruct"); + const preParsed = JSON.parse(pre.stdout) as Record; + expect(preParsed.permission).toBe("allow"); // does NOT block — no context channel on tool events + expect(preParsed.reason).toContain("prefer git mv"); + expect(pre.stderr).toContain("prefer git mv"); + }); + it("Cursor SubagentStop + instruct emits {followup_message} JSON (parity with Stop branch)", async () => { // The Cursor Stop instruct branch was widened to also match SubagentStop // so custom policies subscribing to SubagentStop on Cursor get the same @@ -376,25 +424,6 @@ describe("hooks/policy-evaluator", () => { expect(parsed.reason).toContain("fyi"); }); - it("Gemini Stop + instruct emits {decision:'block', reason} JSON on stdout (force-retry via AfterAgent)", async () => { - // Confirms the existing cli==="gemini" Stop arm in the instruct path - // emits the documented force-retry shape. Pairs with the deny-path - // test in the "Stop event deny format" describe block. - registerPolicy("verify", "desc", () => ({ - decision: "instruct", - reason: "needs verification", - }), { events: ["Stop"] }); - - const result = await evaluatePolicies("Stop", {}, { cli: "gemini" }); - expect(result.exitCode).toBe(0); - expect(result.stderr).toBe(""); - expect(result.decision).toBe("instruct"); - const parsed = JSON.parse(result.stdout) as { decision?: string; reason?: string }; - expect(parsed.decision).toBe("block"); - expect(parsed.reason).toContain("MANDATORY ACTION REQUIRED"); - expect(parsed.reason).toContain("needs verification"); - }); - it("accumulates multiple instruct messages", async () => { registerPolicy("first", "desc", () => ({ decision: "instruct", @@ -448,8 +477,11 @@ describe("hooks/policy-evaluator", () => { expect(result.decision).toBe("allow"); expect(result.policyName).toBe("failproofai/info1"); expect(result.policyNames).toEqual(["failproofai/info1", "failproofai/info2"]); - const parsed = JSON.parse(result.stdout); - expect(parsed.reason).toBe("Commit check passed\nPush check passed"); + // Stop has no agent-facing context channel, so the combined note is kept + // OUT of stdout (prevents droid-style "…skipping…" noise on a fine turn); + // it stays in the return `reason` + stderr for diagnostics. + expect(result.stdout).toBe(""); + expect(result.reason).toBe("Commit check passed\nPush check passed"); expect(result.stderr).toContain("[failproofai] failproofai/info1: Commit check passed"); expect(result.stderr).toContain("[failproofai] failproofai/info2: Push check passed"); }); @@ -874,25 +906,6 @@ describe("hooks/policy-evaluator", () => { expect(parsed.reason).toContain("sudo not allowed"); }); - it("Gemini Stop deny emits {decision:'block', reason} JSON on stdout (force-retry via AfterAgent)", async () => { - // Per Gemini's hooks docs (https://geminicli.com/docs/hooks/), the - // AfterAgent hook (canonical "Stop") force-retries when the hook - // returns `{decision: "block", reason}` on stdout. Exit-2 is per- - // action only ("turn continues") and would NOT trigger the retry. - registerPolicy("stop-blocker", "desc", () => ({ - decision: "deny", - reason: "tests not run", - }), { events: ["Stop"] }); - - const result = await evaluatePolicies("Stop", {}, { cli: "gemini" }); - expect(result.exitCode).toBe(0); - expect(result.stderr).toBe(""); - const parsed = JSON.parse(result.stdout) as { decision?: string; reason?: string }; - expect(parsed.decision).toBe("block"); - expect(parsed.reason).toContain("MANDATORY ACTION REQUIRED"); - expect(parsed.reason).toContain("tests not run"); - expect(result.decision).toBe("deny"); - }); }); describe("workflow policy chain integration", () => { @@ -940,11 +953,13 @@ describe("hooks/policy-evaluator", () => { expect(result.exitCode).toBe(0); expect(result.decision).toBe("allow"); expect(result.policyNames).toEqual(["failproofai/wf-commit", "failproofai/wf-push", "failproofai/wf-pr", "failproofai/wf-ci"]); - const parsed = JSON.parse(result.stdout); - expect(parsed.reason).toContain("All changes committed"); - expect(parsed.reason).toContain("All commits pushed"); - expect(parsed.reason).toContain("PR #42 exists"); - expect(parsed.reason).toContain("All CI checks passed"); + // Stop has no context channel → the combined note lives in the return + // `reason` + stderr (NOT stdout), so it isn't rendered as agent-facing noise. + expect(result.stdout).toBe(""); + expect(result.reason).toContain("All changes committed"); + expect(result.reason).toContain("All commits pushed"); + expect(result.reason).toContain("PR #42 exists"); + expect(result.reason).toContain("All CI checks passed"); }); it("allow messages from early policies are discarded when a later policy denies", async () => { @@ -994,8 +1009,9 @@ describe("hooks/policy-evaluator", () => { expect(result.decision).toBe("allow"); expect(result.policyName).toBe("failproofai/informative"); expect(result.policyNames).toEqual(["failproofai/informative"]); - const parsed = JSON.parse(result.stdout); - expect(parsed.reason).toBe("CI is green"); + // Stop note → return `reason` + stderr, stdout stays empty. + expect(result.stdout).toBe(""); + expect(result.reason).toBe("CI is green"); }); it("policy that throws is skipped — subsequent policies still run", async () => { @@ -1203,265 +1219,4 @@ describe("hooks/policy-evaluator", () => { expect(result.reason).toBe("hard block. deny hint"); }); }); - - describe("Gemini CLI response shape", () => { - const geminiSession = (hookEventName: string) => ({ - sessionId: "g-1", cwd: "/tmp", cli: "gemini" as const, hookEventName, - }); - - it("PreToolUse deny → flat {decision:'deny', reason} (NOT Claude's hookSpecificOutput shape)", async () => { - registerPolicy("blocker", "desc", () => ({ decision: "deny", reason: "sudo blocked" }), { - events: ["PreToolUse"], - }); - const result = await evaluatePolicies( - "PreToolUse", - { tool_name: "Bash", tool_input: { command: "sudo ls" } }, - geminiSession("BeforeTool"), - ); - expect(result.exitCode).toBe(0); - expect(result.stderr).toBe(""); - const parsed = JSON.parse(result.stdout); - expect(parsed.decision).toBe("deny"); - expect(parsed.reason).toContain("sudo blocked"); - expect(parsed.reason).toContain("Blocked Bash by failproofai"); - // Crucial: NOT Claude's nested shape - expect(parsed.hookSpecificOutput).toBeUndefined(); - }); - - it("BeforeAgent deny (UserPromptSubmit) → flat {decision:'deny', reason}", async () => { - registerPolicy("prompt-block", "desc", () => ({ decision: "deny", reason: "bad prompt" }), { - events: ["UserPromptSubmit"], - }); - const result = await evaluatePolicies( - "UserPromptSubmit", - { prompt: "" }, - geminiSession("BeforeAgent"), - ); - expect(result.exitCode).toBe(0); - const parsed = JSON.parse(result.stdout); - expect(parsed.decision).toBe("deny"); - expect(parsed.reason).toContain("bad prompt"); - expect(parsed.hookSpecificOutput).toBeUndefined(); - }); - - it("AfterAgent deny (Stop) → {decision:'block', reason} with MANDATORY ACTION REQUIRED prefix", async () => { - registerPolicy("must-do", "desc", () => ({ decision: "deny", reason: "missing CHANGELOG" }), { - events: ["Stop"], - }); - const result = await evaluatePolicies( - "Stop", - {}, - geminiSession("AfterAgent"), - ); - expect(result.exitCode).toBe(0); - const parsed = JSON.parse(result.stdout); - // Gemini's AfterAgent supports "Retry / Halt" via decision: "block" - expect(parsed.decision).toBe("block"); - expect(parsed.reason).toContain("MANDATORY ACTION REQUIRED"); - expect(parsed.reason).toContain("missing CHANGELOG"); - }); - - it("instruct on PreToolUse → {hookSpecificOutput:{hookEventName:'BeforeTool', additionalContext}}", async () => { - registerPolicy("advisor", "desc", () => ({ decision: "instruct", reason: "consider X" }), { - events: ["PreToolUse"], - }); - const result = await evaluatePolicies( - "PreToolUse", - { tool_name: "Bash" }, - geminiSession("BeforeTool"), - ); - expect(result.exitCode).toBe(0); - const parsed = JSON.parse(result.stdout); - // hookEventName must be the Gemini event name (not the canonical PreToolUse) - expect(parsed.hookSpecificOutput.hookEventName).toBe("BeforeTool"); - expect(parsed.hookSpecificOutput.additionalContext).toContain("Instruction from failproofai"); - expect(parsed.hookSpecificOutput.additionalContext).toContain("consider X"); - // Must NOT use Gemini's flat block shape — that's reserved for deny - expect(parsed.decision).toBeUndefined(); - }); - - it("instruct on AfterTool → {hookSpecificOutput:{hookEventName:'AfterTool', additionalContext}}", async () => { - registerPolicy("after", "desc", () => ({ decision: "instruct", reason: "post note" }), { - events: ["PostToolUse"], - }); - const result = await evaluatePolicies( - "PostToolUse", - { tool_name: "Read" }, - geminiSession("AfterTool"), - ); - const parsed = JSON.parse(result.stdout); - expect(parsed.hookSpecificOutput.hookEventName).toBe("AfterTool"); - expect(parsed.hookSpecificOutput.additionalContext).toContain("post note"); - }); - - it("instruct on SessionStart → {hookSpecificOutput:{hookEventName:'SessionStart', additionalContext}}", async () => { - registerPolicy("greet", "desc", () => ({ decision: "instruct", reason: "warm context" }), { - events: ["SessionStart"], - }); - const result = await evaluatePolicies( - "SessionStart", - {}, - geminiSession("SessionStart"), - ); - const parsed = JSON.parse(result.stdout); - expect(parsed.hookSpecificOutput.hookEventName).toBe("SessionStart"); - expect(parsed.hookSpecificOutput.additionalContext).toContain("warm context"); - }); - - it("instruct on AfterAgent (Stop) → {decision:'block', reason} with MANDATORY ACTION", async () => { - registerPolicy("must-do", "desc", () => ({ decision: "instruct", reason: "open the PR" }), { - events: ["Stop"], - }); - const result = await evaluatePolicies( - "Stop", - {}, - geminiSession("AfterAgent"), - ); - const parsed = JSON.parse(result.stdout); - // For Gemini AfterAgent, both deny and instruct map to {decision: "block"} which forces a retry. - expect(parsed.decision).toBe("block"); - expect(parsed.reason).toContain("MANDATORY ACTION REQUIRED"); - expect(parsed.reason).toContain("open the PR"); - }); - - it("multiple instruct on AfterAgent → reasons concatenated, single {decision:'block'} response", async () => { - registerPolicy("a", "desc", () => ({ decision: "instruct", reason: "first" }), { events: ["Stop"] }); - registerPolicy("b", "desc", () => ({ decision: "instruct", reason: "second" }), { events: ["Stop"] }); - const result = await evaluatePolicies( - "Stop", - {}, - geminiSession("AfterAgent"), - ); - const parsed = JSON.parse(result.stdout); - expect(parsed.decision).toBe("block"); - expect(parsed.reason).toContain("first"); - expect(parsed.reason).toContain("second"); - }); - - it("instruct on a non-context-injection event (e.g. SessionEnd) → stderr only, no stdout JSON", async () => { - registerPolicy("byebye", "desc", () => ({ decision: "instruct", reason: "after-note" }), { - events: ["SessionEnd"], - }); - const result = await evaluatePolicies( - "SessionEnd", - {}, - geminiSession("SessionEnd"), - ); - // SessionEnd is observation-only on Gemini; we don't emit a stdout JSON shape - expect(result.stdout).toBe(""); - // Reason still surfaces via stderr for visibility - expect(result.stderr).toContain("after-note"); - }); - - it("allow with informational reason on BeforeAgent → context-injection shape", async () => { - registerPolicy("info", "desc", () => ({ decision: "allow", reason: "fyi" }), { - events: ["UserPromptSubmit"], - }); - const result = await evaluatePolicies( - "UserPromptSubmit", - { prompt: "x" }, - geminiSession("BeforeAgent"), - ); - const parsed = JSON.parse(result.stdout); - expect(parsed.hookSpecificOutput.hookEventName).toBe("BeforeAgent"); - expect(parsed.hookSpecificOutput.additionalContext).toContain("Note from failproofai"); - expect(parsed.hookSpecificOutput.additionalContext).toContain("fyi"); - }); - - it("allow with informational reason on SessionEnd → stderr only", async () => { - registerPolicy("info", "desc", () => ({ decision: "allow", reason: "fyi" }), { - events: ["SessionEnd"], - }); - const result = await evaluatePolicies( - "SessionEnd", - {}, - geminiSession("SessionEnd"), - ); - expect(result.stdout).toBe(""); - expect(result.stderr).toContain("fyi"); - }); - - it("allow without reason → empty stdout, exit 0 (no extra noise)", async () => { - registerPolicy("ok", "desc", () => ({ decision: "allow" }), { events: ["PreToolUse"] }); - const result = await evaluatePolicies( - "PreToolUse", - { tool_name: "Bash" }, - geminiSession("BeforeTool"), - ); - expect(result.exitCode).toBe(0); - expect(result.stdout).toBe(""); - }); - - it("instruct falls back to session.rawHookEventName when stdin omits hook_event_name", async () => { - registerPolicy("advisor", "desc", () => ({ decision: "instruct", reason: "consider X" }), { - events: ["PreToolUse"], - }); - const result = await evaluatePolicies( - "PreToolUse", - { tool_name: "Bash" }, - // session.hookEventName intentionally undefined; rawHookEventName carries - // the raw CLI --hook arg as captured by handler.ts. - { sessionId: "g", cwd: "/tmp", cli: "gemini", rawHookEventName: "BeforeTool" }, - ); - const parsed = JSON.parse(result.stdout); - // Must use the raw Gemini event name, NOT the canonicalized "PreToolUse". - expect(parsed.hookSpecificOutput.hookEventName).toBe("BeforeTool"); - }); - - it("allow-with-context also falls back to session.rawHookEventName", async () => { - registerPolicy("info", "desc", () => ({ decision: "allow", reason: "fyi" }), { - events: ["UserPromptSubmit"], - }); - const result = await evaluatePolicies( - "UserPromptSubmit", - { prompt: "x" }, - { sessionId: "g", cwd: "/tmp", cli: "gemini", rawHookEventName: "BeforeAgent" }, - ); - const parsed = JSON.parse(result.stdout); - expect(parsed.hookSpecificOutput.hookEventName).toBe("BeforeAgent"); - }); - - it("session.hookEventName from stdin still wins over rawHookEventName when both are set", async () => { - registerPolicy("advisor", "desc", () => ({ decision: "instruct", reason: "x" }), { - events: ["PreToolUse"], - }); - const result = await evaluatePolicies( - "PreToolUse", - { tool_name: "Bash" }, - { sessionId: "g", cwd: "/tmp", cli: "gemini", hookEventName: "BeforeTool", rawHookEventName: "RawShouldLose" }, - ); - const parsed = JSON.parse(result.stdout); - expect(parsed.hookSpecificOutput.hookEventName).toBe("BeforeTool"); - }); - - it("UserPromptSubmit deny on Gemini emits event-appropriate text (NOT 'Blocked unknown tool')", async () => { - registerPolicy("prompt-block", "desc", () => ({ decision: "deny", reason: "bad prompt" }), { - events: ["UserPromptSubmit"], - }); - const result = await evaluatePolicies( - "UserPromptSubmit", - { prompt: "" }, - geminiSession("BeforeAgent"), - ); - const parsed = JSON.parse(result.stdout); - // Without ctx.toolName, the deny message used to say "Blocked unknown tool"; - // now we branch on event type. - expect(parsed.reason).toMatch(/Blocked prompt/); - expect(parsed.reason).not.toMatch(/Blocked unknown tool/); - }); - - it("SessionStart deny emits 'Blocked session start' label, not 'unknown tool'", async () => { - registerPolicy("greet-block", "desc", () => ({ decision: "deny", reason: "no greeting" }), { - events: ["SessionStart"], - }); - const result = await evaluatePolicies( - "SessionStart", - {}, - geminiSession("SessionStart"), - ); - const parsed = JSON.parse(result.stdout); - expect(parsed.reason).toMatch(/Blocked session start/); - expect(parsed.reason).not.toMatch(/Blocked unknown tool/); - }); - }); }); diff --git a/__tests__/hooks/resolve-cwd.test.ts b/__tests__/hooks/resolve-cwd.test.ts index 882fc788..2b01c7da 100644 --- a/__tests__/hooks/resolve-cwd.test.ts +++ b/__tests__/hooks/resolve-cwd.test.ts @@ -10,7 +10,6 @@ const ALL_CLIS: IntegrationType[] = [ "copilot", "cursor", "pi", - "gemini", "opencode", ]; diff --git a/__tests__/hooks/resolve-transcript-path.test.ts b/__tests__/hooks/resolve-transcript-path.test.ts index ab74cb92..d3f950e6 100644 --- a/__tests__/hooks/resolve-transcript-path.test.ts +++ b/__tests__/hooks/resolve-transcript-path.test.ts @@ -13,16 +13,12 @@ vi.mock("../../lib/cursor-sessions", () => ({ vi.mock("../../lib/pi-sessions", () => ({ findPiTranscript: vi.fn(), })); -vi.mock("../../lib/gemini-sessions", () => ({ - findGeminiTranscript: vi.fn(), -})); import { resolveTranscriptPath } from "../../src/hooks/resolve-transcript-path"; import { findCodexTranscript } from "../../lib/codex-sessions"; import { findCopilotTranscript } from "../../lib/copilot-sessions"; import { findCursorTranscript } from "../../lib/cursor-sessions"; import { findPiTranscript } from "../../lib/pi-sessions"; -import { findGeminiTranscript } from "../../lib/gemini-sessions"; import type { IntegrationType } from "../../src/hooks/types"; describe("resolveTranscriptPath", () => { @@ -37,7 +33,6 @@ describe("resolveTranscriptPath", () => { "copilot", "cursor", "pi", - "gemini", "opencode", ]; it.each(cases)( @@ -60,7 +55,6 @@ describe("resolveTranscriptPath", () => { "copilot", "cursor", "pi", - "gemini", "opencode", ]; it.each(cases)("returns undefined when no stdin path AND no sessionId (%s)", (cli) => { @@ -109,13 +103,6 @@ describe("resolveTranscriptPath", () => { expect(out).toBe("/home/u/.pi/agent/sessions/encoded/2026-05-05_sess-pi.jsonl"); }); - it("gemini → findGeminiTranscript(sessionId)", () => { - vi.mocked(findGeminiTranscript).mockReturnValue("/home/u/.gemini/tmp/projhash/chats/sess-gemini.json"); - const out = resolveTranscriptPath("gemini", {}, "sess-gemini"); - expect(findGeminiTranscript).toHaveBeenCalledWith("sess-gemini"); - expect(out).toBe("/home/u/.gemini/tmp/projhash/chats/sess-gemini.json"); - }); - it("opencode → synthetic opencode-db:// marker (transcripts live in SQLite)", () => { const out = resolveTranscriptPath("opencode", {}, "ses_test_opencode001"); expect(out).toBe("opencode-db://ses_test_opencode001"); diff --git a/__tests__/lib/cli-registry.test.ts b/__tests__/lib/cli-registry.test.ts index ec721c34..238043dd 100644 --- a/__tests__/lib/cli-registry.test.ts +++ b/__tests__/lib/cli-registry.test.ts @@ -12,7 +12,7 @@ import { describe("lib/cli-registry", () => { it("KNOWN_CLI_IDS lists all supported CLIs in stable order", () => { - expect(KNOWN_CLI_IDS).toEqual(["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini", "hermes"]); + expect(KNOWN_CLI_IDS).toEqual(["claude", "codex", "copilot", "cursor", "opencode", "pi", "hermes", "openclaw", "factory", "devin", "antigravity", "goose"]); }); it("getCliEntry returns the entry for known ids and undefined for unknown", () => { @@ -22,15 +22,14 @@ describe("lib/cli-registry", () => { expect(getCliEntry("cursor")?.label).toBe("Cursor Agent"); expect(getCliEntry("opencode")?.label).toBe("OpenCode"); expect(getCliEntry("pi")?.label).toBe("Pi"); - expect(getCliEntry("gemini")?.label).toBe("Gemini CLI"); expect(getCliEntry("hermes")?.label).toBe("Hermes"); + expect(getCliEntry("goose")?.label).toBe("Goose"); expect(getCliEntry("unknown")).toBeUndefined(); }); it("getCliLabel falls back to the id itself for unknown", () => { expect(getCliLabel("claude")).toBe("Claude Code"); expect(getCliLabel("pi")).toBe("Pi"); - expect(getCliLabel("gemini")).toBe("Gemini CLI"); expect(getCliLabel("xyz")).toBe("xyz"); }); @@ -41,8 +40,8 @@ describe("lib/cli-registry", () => { expect(getCliBadgeClasses("cursor")).toContain("emerald"); expect(getCliBadgeClasses("opencode")).toContain("amber"); expect(getCliBadgeClasses("pi")).toContain("pink"); - expect(getCliBadgeClasses("gemini")).toContain("sky"); expect(getCliBadgeClasses("hermes")).toContain("indigo"); + expect(getCliBadgeClasses("goose")).toContain("lime"); expect(getCliBadgeClasses("unknown")).toContain("orange"); // falls back to claude }); @@ -52,8 +51,8 @@ describe("lib/cli-registry", () => { expect(isKnownCli("cursor")).toBe(true); expect(isKnownCli("opencode")).toBe(true); expect(isKnownCli("pi")).toBe(true); - expect(isKnownCli("gemini")).toBe(true); expect(isKnownCli("hermes")).toBe(true); + expect(isKnownCli("goose")).toBe(true); expect(isKnownCli("nope")).toBe(false); expect(isKnownCli(null)).toBe(false); expect(isKnownCli(undefined)).toBe(false); @@ -74,7 +73,7 @@ describe("lib/cli-registry", () => { it("listExternalCliEntries excludes claude", () => { const ids = listExternalCliEntries().map((c) => c.id); - expect(ids).toEqual(["codex", "copilot", "cursor", "opencode", "pi", "gemini", "hermes"]); + expect(ids).toEqual(["codex", "copilot", "cursor", "opencode", "pi", "hermes", "openclaw", "factory", "devin", "antigravity", "goose"]); }); it("each CLI has a unique badgeClasses string", () => { diff --git a/__tests__/lib/devin-sessions.test.ts b/__tests__/lib/devin-sessions.test.ts new file mode 100644 index 00000000..4c64e7b4 --- /dev/null +++ b/__tests__/lib/devin-sessions.test.ts @@ -0,0 +1,118 @@ +// @vitest-environment node +import { describe, it, expect } from "vitest"; +import { devinRowsToLogEntries, devinActiveConversationPath } from "@/lib/devin-sessions"; +import { logEntriesToEvents } from "@/src/audit/cli-adapters/shared"; + +/** Parsed `chat_message` objects as they come off `message_nodes.chat_message` + * (OpenAI-style, verified live against devin v3000.1.27): assistant tool calls + * are flat `tool_calls[].{id, name, arguments}` where `arguments` is already an + * object, and results are separate `role:"tool"` rows keyed by `tool_call_id`. + * `_created_at` is the DB row's epoch-seconds timestamp injected by the loader. */ +const msg = (o: Record) => o; + +describe("lib/devin-sessions: devinRowsToLogEntries", () => { + it("pairs a tool result onto its tool_use by tool_call_id (flat name/arguments)", () => { + const entries = devinRowsToLogEntries([ + msg({ role: "user", content: "run it", _created_at: 1_784_016_000 }), + msg({ + role: "assistant", + content: "ok", + tool_calls: [{ id: "call_1", name: "exec", arguments: { command: "echo hi" }, index: 0, kind: "function" }], + _created_at: 1_784_016_001, + }), + msg({ role: "tool", tool_call_id: "call_1", content: "hi\nExit code: 0", _created_at: 1_784_016_002 }), + ]); + const assistant = entries.find((e) => e.type === "assistant"); + const toolUse = + assistant?.type === "assistant" + ? assistant.message.content.find((b) => b.type === "tool_use") + : undefined; + expect(toolUse).toMatchObject({ type: "tool_use", name: "exec", input: { command: "echo hi" } }); + expect(toolUse && "result" in toolUse ? toolUse.result?.content : "").toContain("hi"); + }); + + it("canonicalizes exec→Bash through logEntriesToEvents", () => { + const entries = devinRowsToLogEntries([ + msg({ role: "assistant", tool_calls: [{ id: "c1", name: "exec", arguments: { command: "ls" } }], _created_at: 1_784_016_000 }), + ]); + const events = logEntriesToEvents(entries, { cli: "devin", sessionId: "s", transcriptPath: "devin-db://s", cwd: "" }); + expect(events).toHaveLength(1); + expect(events[0]).toMatchObject({ toolName: "Bash", rawToolName: "exec", toolInput: { command: "ls" } }); + }); + + it("prefers metadata.created_at (ISO) over the injected _created_at row value", () => { + const entries = devinRowsToLogEntries([ + msg({ role: "user", content: "hi", _created_at: 1_784_016_000, metadata: { created_at: "2026-07-14T07:59:58.000Z" } }), + ]); + expect(entries[0].timestamp).toBe("2026-07-14T07:59:58.000Z"); + }); + + it("keeps system / unknown roles as system entries (never dropped)", () => { + const entries = devinRowsToLogEntries([ + msg({ role: "system", content: "env info", _created_at: 1 }), + msg({ role: "user", content: "hi", _created_at: 2 }), + ]); + expect(entries.some((e) => e.type === "system")).toBe(true); + expect(entries.some((e) => e.type === "user")).toBe(true); + }); + + it("drops an assistant turn with empty content and no tool_calls", () => { + const entries = devinRowsToLogEntries([ + msg({ role: "assistant", content: "", tool_calls: [], _created_at: 1 }), + ]); + expect(entries).toHaveLength(0); + }); + + it("returns [] for no rows", () => { + expect(devinRowsToLogEntries([])).toHaveLength(0); + }); +}); + +describe("lib/devin-sessions: devinActiveConversationPath (forest de-dup)", () => { + const node = (id: number, parent: number | null, text: string) => ({ + node_id: id, + parent_node_id: parent, + chat_message: JSON.stringify({ role: "user", content: text, message_id: `m${text}` }), + created_at: id, + }); + + it("returns only the newest leaf's root→leaf path, dropping replayed branches", () => { + // Mirrors the real Devin shape: an early branch (0→1→2) is replayed under a + // later root (3→4→5) that ends at the newest leaf. Reading all nodes would + // duplicate A/B; the active path is just the newest chain. + const rows = [ + node(0, null, "A"), + node(1, 0, "B"), + node(2, 1, "C"), // dead branch leaf + node(3, null, "A"), // replay root + node(4, 3, "B"), // replay + node(5, 4, "D"), // newest leaf + ]; + const path = devinActiveConversationPath(rows).map((r) => + JSON.parse(r.chat_message!).content, + ); + expect(path).toEqual(["A", "B", "D"]); // 5→4→3 reversed; branch 0-2 dropped + }); + + it("picks the branch to the max node_id when a parent has sibling children", () => { + // node 5 and 6 both child of 4; the newest leaf (7, under 6) wins. + const rows = [ + node(0, null, "root"), + node(4, 0, "shared"), + node(5, 4, "deadSibling"), + node(6, 4, "liveSibling"), + node(7, 6, "leaf"), + ]; + const path = devinActiveConversationPath(rows).map((r) => + JSON.parse(r.chat_message!).content, + ); + expect(path).toEqual(["root", "shared", "liveSibling", "leaf"]); + expect(path).not.toContain("deadSibling"); + }); + + it("handles a single linear chain unchanged and [] for no rows", () => { + const rows = [node(0, null, "x"), node(1, 0, "y")]; + expect(devinActiveConversationPath(rows).map((r) => JSON.parse(r.chat_message!).content)).toEqual(["x", "y"]); + expect(devinActiveConversationPath([])).toEqual([]); + }); +}); diff --git a/__tests__/lib/download-session.test.ts b/__tests__/lib/download-session.test.ts index c9e27916..032ee20c 100644 --- a/__tests__/lib/download-session.test.ts +++ b/__tests__/lib/download-session.test.ts @@ -14,7 +14,7 @@ describe("lib/download-session: isValidSessionId", () => { }); it("accepts UUIDs for non-opencode CLIs", () => { - for (const cli of ["claude", "codex", "copilot", "cursor", "pi", "gemini"] as const) { + for (const cli of ["claude", "codex", "copilot", "cursor", "pi"] as const) { expect(isValidSessionId(cli, VALID_UUID)).toBe(true); expect(isValidSessionId(cli, "ses_abc123")).toBe(false); expect(isValidSessionId(cli, "not-a-uuid")).toBe(false); @@ -41,7 +41,6 @@ describe("lib/download-session: resolveDownloadSource", () => { vi.doUnmock("@/lib/copilot-sessions"); vi.doUnmock("@/lib/cursor-sessions"); vi.doUnmock("@/lib/pi-sessions"); - vi.doUnmock("@/lib/gemini-sessions"); vi.doUnmock("@/lib/opencode-sessions"); }); @@ -85,7 +84,6 @@ describe("lib/download-session: resolveDownloadSource", () => { ["copilot", "@/lib/copilot-sessions", "findCopilotTranscript"], ["cursor", "@/lib/cursor-sessions", "findCursorTranscript"], ["pi", "@/lib/pi-sessions", "findPiTranscript"], - ["gemini", "@/lib/gemini-sessions", "findGeminiTranscript"], ] as const)( "%s: returns {kind:'file', path} when transcript is found", async (cli, modulePath, fnName) => { @@ -102,7 +100,6 @@ describe("lib/download-session: resolveDownloadSource", () => { ["copilot", "@/lib/copilot-sessions", "findCopilotTranscript"], ["cursor", "@/lib/cursor-sessions", "findCursorTranscript"], ["pi", "@/lib/pi-sessions", "findPiTranscript"], - ["gemini", "@/lib/gemini-sessions", "findGeminiTranscript"], ] as const)("%s: returns null when transcript is missing", async (cli, modulePath, fnName) => { vi.doMock(modulePath, () => ({ [fnName]: () => null })); vi.resetModules(); diff --git a/__tests__/lib/gemini-projects.test.ts b/__tests__/lib/gemini-projects.test.ts deleted file mode 100644 index c9b4eca2..00000000 --- a/__tests__/lib/gemini-projects.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -// @vitest-environment node -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; -import { tmpdir } from "node:os"; - -import { - getGeminiSessionsForCwd, - getGeminiSessionsByEncodedName, -} from "@/lib/gemini-projects"; - -const FULL_UUID = "89eb30b0-27c0-4ea3-a5b9-06fc00085610"; -const PREFIX = FULL_UUID.slice(0, 8); - -function header(sessionId: string, startTime = "2026-05-09T12:00:00.000Z"): string { - return JSON.stringify({ - sessionId, - projectHash: "deadbeef", - startTime, - lastUpdated: startTime, - kind: "chat", - }); -} - -describe("lib/gemini-projects", () => { - let originalRoot: string | undefined; - let fakeRoot: string; - - function makeProject(basename: string, cwd: string) { - const projectDir = join(fakeRoot, basename); - mkdirSync(join(projectDir, "chats"), { recursive: true }); - writeFileSync(join(projectDir, ".project_root"), cwd); - return projectDir; - } - - function writeSession(projectDir: string, prefix: string, body: string) { - // Filename matches `session-YYYY-MM-DDTHH-MM-<8hex>.jsonl` (gemini-cli v0.40.1). - const filename = `session-2026-05-09T12-00-${prefix}.jsonl`; - writeFileSync(join(projectDir, "chats", filename), body); - return filename; - } - - beforeEach(() => { - originalRoot = process.env.GEMINI_SESSIONS_DIR; - fakeRoot = mkdtempSync(join(tmpdir(), "gemini-projects-")); - process.env.GEMINI_SESSIONS_DIR = fakeRoot; - }); - - afterEach(() => { - if (originalRoot === undefined) delete process.env.GEMINI_SESSIONS_DIR; - else process.env.GEMINI_SESSIONS_DIR = originalRoot; - rmSync(fakeRoot, { recursive: true, force: true }); - }); - - it("exposes the full UUID from the JSONL metadata header — not the 8-char filename prefix", async () => { - // Regression for #337: the project page rendered links with an 8-hex - // sessionId, which the session detail route's UUID_RE check rejected (404). - // Building the link from the metadata header's full UUID round-trips. - const cwd = "/home/u/dev-purge"; - const projectDir = makeProject("dev-purge", cwd); - writeSession(projectDir, PREFIX, header(FULL_UUID) + "\n"); - - const byCwd = await getGeminiSessionsForCwd(cwd); - expect(byCwd).toHaveLength(1); - expect(byCwd[0].sessionId).toBe(FULL_UUID); - expect(byCwd[0].sessionId).not.toBe(PREFIX); - - const byName = await getGeminiSessionsByEncodedName("-home-u-dev-purge"); - expect(byName.cwd).toBe(cwd); - expect(byName.sessions).toHaveLength(1); - expect(byName.sessions[0].sessionId).toBe(FULL_UUID); - }); - - it("leaves sessionId undefined when the metadata header is malformed JSON", async () => { - // Un-parseable header → un-linked row in the dashboard. Better than a - // clickable link to the 8-hex prefix that 404s. - const cwd = "/home/u/broken"; - const projectDir = makeProject("broken", cwd); - writeSession(projectDir, PREFIX, "not json\n"); - - const byCwd = await getGeminiSessionsForCwd(cwd); - expect(byCwd).toHaveLength(1); - expect(byCwd[0].sessionId).toBeUndefined(); - - const byName = await getGeminiSessionsByEncodedName("-home-u-broken"); - expect(byName.sessions).toHaveLength(1); - expect(byName.sessions[0].sessionId).toBeUndefined(); - }); - - it("leaves sessionId undefined when the metadata header lacks a sessionId field", async () => { - const cwd = "/home/u/headerless"; - const projectDir = makeProject("headerless", cwd); - writeSession( - projectDir, - PREFIX, - JSON.stringify({ projectHash: "x", startTime: "2026-05-09T12:00:00.000Z" }) + "\n", - ); - - const byCwd = await getGeminiSessionsForCwd(cwd); - expect(byCwd).toHaveLength(1); - expect(byCwd[0].sessionId).toBeUndefined(); - }); - - it("leaves sessionId undefined when meta.sessionId is not a UUID", async () => { - const cwd = "/home/u/nonuuid"; - const projectDir = makeProject("nonuuid", cwd); - writeSession(projectDir, PREFIX, header("not-a-uuid") + "\n"); - - const byCwd = await getGeminiSessionsForCwd(cwd); - expect(byCwd).toHaveLength(1); - expect(byCwd[0].sessionId).toBeUndefined(); - }); -}); diff --git a/__tests__/lib/goose-sessions.test.ts b/__tests__/lib/goose-sessions.test.ts new file mode 100644 index 00000000..b96db26a --- /dev/null +++ b/__tests__/lib/goose-sessions.test.ts @@ -0,0 +1,131 @@ +// @vitest-environment node +// +// Unit tests for the PURE Goose message parser (gooseRowsToLogEntries) and the +// timestamp coercion helper. Uses content_json shapes captured live from goose +// v1.43.0 (sessions.db, schema_version 15): typed-block arrays with `text`, +// `toolRequest`, and `toolResponse` blocks; tool RESULTS arrive in role:"user" +// rows and must attach to the matching earlier tool_use by id. +import { describe, it, expect } from "vitest"; +import { + gooseRowsToLogEntries, + gooseTimestampToMs, + GOOSE_SESSION_ID_RE, + type GooseMessageRow, +} from "@/lib/goose-sessions"; + +function row(role: string, blocks: unknown[], ts: number): GooseMessageRow { + return { role, content_json: JSON.stringify(blocks), created_timestamp: ts }; +} + +const T0 = 1_784_000_000_000; // > 1e12 → treated as epoch ms + +describe("gooseTimestampToMs", () => { + it("passes epoch-ms through and scales epoch-seconds to ms", () => { + expect(gooseTimestampToMs(T0)).toBe(T0); + expect(gooseTimestampToMs(1_700_000_000)).toBe(1_700_000_000_000); + }); + + it("parses SQLite CURRENT_TIMESTAMP strings as UTC", () => { + // "YYYY-MM-DD HH:MM:SS" has no zone; must be read as UTC, not local. + expect(gooseTimestampToMs("2026-07-14 16:01:00")).toBe(Date.parse("2026-07-14T16:01:00Z")); + }); + + it("returns a finite number for unparseable input (fallback to now)", () => { + expect(Number.isFinite(gooseTimestampToMs("not-a-date"))).toBe(true); + expect(Number.isFinite(gooseTimestampToMs(null))).toBe(true); + }); +}); + +describe("GOOSE_SESSION_ID_RE", () => { + it("matches date-prefixed counters and rejects UUIDs / traversal", () => { + expect(GOOSE_SESSION_ID_RE.test("20260714_3")).toBe(true); + expect(GOOSE_SESSION_ID_RE.test("20260714_12")).toBe(true); + expect(GOOSE_SESSION_ID_RE.test("not-a-session")).toBe(false); + expect(GOOSE_SESSION_ID_RE.test("../etc/passwd")).toBe(false); + }); +}); + +describe("gooseRowsToLogEntries", () => { + const rows: GooseMessageRow[] = [ + row("user", [{ type: "text", text: "Run the shell command: echo AUDIT" }], T0), + row( + "assistant", + [ + { + type: "toolRequest", + id: "call_1", + toolCall: { status: "success", value: { name: "shell", arguments: { command: "echo AUDIT" } } }, + _meta: { goose_extension: "developer" }, + }, + ], + T0 + 1000, + ), + row( + "user", + [ + { + type: "toolResponse", + id: "call_1", + toolResult: { + status: "success", + value: { content: [{ type: "text", text: "AUDIT" }], structuredContent: { stdout: "AUDIT" }, isError: false }, + }, + }, + ], + T0 + 2000, + ), + row("assistant", [{ type: "text", text: "The command printed AUDIT" }], T0 + 3000), + ]; + + it("produces user + assistant(tool) + assistant(text) entries (tool-result row adds none)", () => { + const entries = gooseRowsToLogEntries(rows); + // 4 rows, but the pure tool-result row attaches to the tool_use rather than + // producing its own entry → 3 entries. + expect(entries).toHaveLength(3); + expect(entries[0].type).toBe("user"); + expect(entries[1].type).toBe("assistant"); + expect(entries[2].type).toBe("assistant"); + }); + + it("keeps the raw goose tool name (canonicalization happens downstream)", () => { + const entries = gooseRowsToLogEntries(rows); + const asst = entries[1] as unknown as { message: { content: Array> } }; + const toolUse = asst.message.content.find((b) => b.type === "tool_use") as + | { name: string; input: Record; result?: { content: string } } + | undefined; + expect(toolUse).toBeDefined(); + expect(toolUse?.name).toBe("shell"); // NOT "Bash" — logEntriesToEvents maps it later + expect((toolUse?.input as { command?: string }).command).toBe("echo AUDIT"); + }); + + it("pairs a toolResponse (in a role:user row) with its earlier toolRequest by id", () => { + const entries = gooseRowsToLogEntries(rows); + const asst = entries[1] as unknown as { message: { content: Array> } }; + const toolUse = asst.message.content.find((b) => b.type === "tool_use") as + | { result?: { content: string } } + | undefined; + expect(toolUse?.result?.content).toBe("AUDIT"); + }); + + it("carries the user text through", () => { + const entries = gooseRowsToLogEntries(rows); + const user = entries[0] as unknown as { message: { content: string } }; + expect(user.message.content).toBe("Run the shell command: echo AUDIT"); + }); + + it("skips rows with empty / non-array content_json without crashing", () => { + const bad: GooseMessageRow[] = [ + { role: "user", content_json: null, created_timestamp: T0 }, + { role: "assistant", content_json: "not json", created_timestamp: T0 }, + { role: "assistant", content_json: JSON.stringify({ not: "array" }), created_timestamp: T0 }, + ]; + expect(gooseRowsToLogEntries(bad)).toHaveLength(0); + }); + + it("does not crash on an orphan toolResponse (no matching request)", () => { + const orphan: GooseMessageRow[] = [ + row("user", [{ type: "toolResponse", id: "missing", toolResult: { value: { content: [] } } }], T0), + ]; + expect(() => gooseRowsToLogEntries(orphan)).not.toThrow(); + }); +}); diff --git a/__tests__/lib/openclaw-projects.test.ts b/__tests__/lib/openclaw-projects.test.ts new file mode 100644 index 00000000..52cf424b --- /dev/null +++ b/__tests__/lib/openclaw-projects.test.ts @@ -0,0 +1,104 @@ +// @vitest-environment node +// +// Covers OpenClaw dashboard enumeration: reads the on-disk transcripts + +// per-agent sessions.json index, groups by **channel** (from metadata fields — +// OpenClaw routes gateway sessions through the default key and records the +// channel in `lastChannel`/`origin`, verified live v2026.7.1), and names +// sessions from the human-readable `origin.label`. OPENCLAW_HOME fixture. +import { describe, it, expect, afterEach } from "vitest"; +import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + getOpenClawSessions, + getOpenClawProjects, + getOpenClawSessionsByEncodedName, +} from "@/lib/openclaw-projects"; + +const UUID_TG = "aa111111-2222-3333-4444-555555555555"; +const UUID_CLI = "f9e8516e-fed2-4e54-acbe-7a20aefc6cfa"; + +let home: string | undefined; +const prev = process.env.OPENCLAW_HOME; + +function seed(): string { + const h = mkdtempSync(join(tmpdir(), "openclaw-proj-")); + const sessions = join(h, "agents", "main", "sessions"); + mkdirSync(sessions, { recursive: true }); + writeFileSync(join(sessions, `${UUID_TG}.jsonl`), JSON.stringify({ type: "session", cwd: "/x" }) + "\n"); + writeFileSync(join(sessions, `${UUID_CLI}.jsonl`), JSON.stringify({ type: "session", cwd: "/x" }) + "\n"); + writeFileSync( + join(sessions, "sessions.json"), + JSON.stringify({ + // A Telegram gateway session — channel lives in metadata, not the key. + "agent:main:main": { + sessionId: UUID_TG, + lastInteractionAt: 5000, + lastChannel: "telegram", + lastTo: "telegram:8674922496", + chatType: "direct", + origin: { label: "Chetan (@chhhee10) id:8674922496", provider: "telegram", from: "telegram:8674922496", chatType: "direct" }, + }, + // A pure CLI/local session — no channel metadata. + "agent:main:cli": { sessionId: UUID_CLI, lastInteractionAt: 2000 }, + }), + ); + process.env.OPENCLAW_HOME = h; + return h; +} + +afterEach(() => { + if (home) rmSync(home, { recursive: true, force: true }); + home = undefined; + if (prev === undefined) delete process.env.OPENCLAW_HOME; + else process.env.OPENCLAW_HOME = prev; +}); + +describe("getOpenClawSessions", () => { + it("derives channel + label + chat metadata from sessions.json (not the key)", async () => { + home = seed(); + const sessions = await getOpenClawSessions(); + // Sorted by mtime desc → telegram (5000) before cli (2000). + expect(sessions.map((s) => s.sessionId)).toEqual([UUID_TG, UUID_CLI]); + + const tg = sessions.find((s) => s.sessionId === UUID_TG)!; + expect(tg.channel).toBe("telegram"); + expect(tg.label).toBe("Chetan (@chhhee10) id:8674922496"); + expect(tg.chatType).toBe("direct"); + expect(tg.chatId).toBe("telegram:8674922496"); + + const cli = sessions.find((s) => s.sessionId === UUID_CLI)!; + expect(cli.channel).toBe("local"); // no channel metadata → local + expect(cli.label).toBeUndefined(); + }); +}); + +describe("getOpenClawProjects / getOpenClawSessionsByEncodedName", () => { + it("groups sessions into one project per channel", async () => { + home = seed(); + const projects = await getOpenClawProjects(); + // telegram (newest) first, then local. + expect(projects.map((p) => p.name)).toEqual(["openclaw-telegram", "openclaw-local"]); + expect(projects[0].path).toBe("openclaw:telegram"); + expect(projects[0].cli).toEqual(["openclaw"]); + }); + + it("names sessions by origin.label and carries channel metadata; non-openclaw names return empty", async () => { + home = seed(); + const tg = await getOpenClawSessionsByEncodedName("openclaw-telegram"); + expect(tg.sessions).toHaveLength(1); + const s = tg.sessions[0]; + expect(s.name).toBe("Chetan (@chhhee10) id:8674922496"); // readable, not the raw key + expect(s.path).toBe(UUID_TG); // real transcript → download streams the file + expect(s.cli).toBe("openclaw"); + expect(s.channelId).toBe("telegram:8674922496"); + expect(s.channelType).toBe("direct"); + + const local = await getOpenClawSessionsByEncodedName("openclaw-local"); + expect(local.sessions).toHaveLength(1); + expect(local.sessions[0].name).toBe(UUID_CLI); // no label → falls back to id + + const none = await getOpenClawSessionsByEncodedName("claude-foo"); + expect(none.sessions).toEqual([]); + }); +}); diff --git a/__tests__/lib/openclaw-sessions.test.ts b/__tests__/lib/openclaw-sessions.test.ts new file mode 100644 index 00000000..3a3147f3 --- /dev/null +++ b/__tests__/lib/openclaw-sessions.test.ts @@ -0,0 +1,124 @@ +// @vitest-environment node +// +// Locks in the pure OpenClaw transcript parser + UUID-based transcript +// resolution. Line shapes mirror a real ~/.openclaw/agents/main/sessions/ +// .jsonl captured live from openclaw v2026.7.1: type-discriminated lines +// where `type:"message"` wraps {role, content}; assistant content is a list of +// {type:"text"} / {type:"toolCall", id, name, arguments}; results arrive as a +// {role:"toolResult", toolCallId, toolName, content, details} message. +import { describe, it, expect } from "vitest"; +import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + openclawLinesToLogEntries, + findOpenClawTranscript, + listOpenClawTranscripts, + OPENCLAW_SESSION_ID_RE, +} from "@/lib/openclaw-sessions"; +import type { AssistantEntry } from "@/lib/log-entries"; + +const UUID = "f9e8516e-fed2-4e54-acbe-7a20aefc6cfa"; + +const LINES: Record[] = [ + { type: "session", version: 3, id: UUID, timestamp: "2026-07-14T05:01:34.420Z", cwd: "/home/node/.openclaw/workspace" }, + { type: "model_change", id: "m1", timestamp: "2026-07-14T05:01:34.431Z", provider: "groq", modelId: "x" }, + { type: "custom", customType: "model-snapshot", data: {}, id: "c1", timestamp: "2026-07-14T05:01:34.527Z" }, + { type: "message", id: "u1", parentId: null, timestamp: "2026-07-14T05:10:20.000Z", message: { role: "user", content: "run echo probe" } }, + { + type: "message", + id: "a1", + parentId: "u1", + timestamp: "2026-07-14T05:10:25.000Z", + message: { + role: "assistant", + model: "llama-4-scout", + content: [ + { type: "text", text: "Running it." }, + { type: "toolCall", id: "c5sf91qpf", name: "exec", arguments: { command: "echo probe-test-123" } }, + ], + }, + }, + { + type: "message", + id: "t1", + parentId: "a1", + timestamp: "2026-07-14T05:10:26.000Z", + message: { + role: "toolResult", + toolCallId: "c5sf91qpf", + toolName: "exec", + content: [{ type: "text", text: "probe-test-123" }], + details: { status: "completed", exitCode: 0, durationMs: 19 }, + isError: false, + }, + }, +]; + +describe("openclawLinesToLogEntries", () => { + it("skips non-message metadata lines (session/model_change/custom)", () => { + const entries = openclawLinesToLogEntries(LINES); + // Only the user + assistant turns become entries (toolResult is folded in). + expect(entries.map((e) => e.type)).toEqual(["user", "assistant"]); + }); + + it("parses the user turn (string content)", () => { + const entries = openclawLinesToLogEntries(LINES); + const user = entries[0]; + expect(user.type).toBe("user"); + if (user.type === "user") expect(user.message.content).toBe("run echo probe"); + }); + + it("parses assistant text + toolCall and pairs the toolResult by toolCallId", () => { + const entries = openclawLinesToLogEntries(LINES); + const asst = entries.find((e) => e.type === "assistant") as AssistantEntry; + expect(asst.message.model).toBe("llama-4-scout"); + const [text, tool] = asst.message.content; + expect(text).toEqual({ type: "text", text: "Running it." }); + expect(tool.type).toBe("tool_use"); + if (tool.type === "tool_use") { + expect(tool.name).toBe("exec"); + expect(tool.input).toEqual({ command: "echo probe-test-123" }); + // Result folded in from the later toolResult message. + expect(tool.result?.content).toBe("probe-test-123"); + expect(tool.result?.durationMs).toBe(19); // from details.durationMs + } + }); + + it("is pure — no filesystem access, safe on empty input", () => { + expect(openclawLinesToLogEntries([])).toEqual([]); + }); +}); + +describe("OpenClaw transcript resolution", () => { + it("session-id regex accepts UUIDs and rejects traversal / non-UUID", () => { + expect(OPENCLAW_SESSION_ID_RE.test(UUID)).toBe(true); + expect(OPENCLAW_SESSION_ID_RE.test("../../etc/passwd")).toBe(false); + expect(OPENCLAW_SESSION_ID_RE.test("agent:main:main")).toBe(false); + expect(OPENCLAW_SESSION_ID_RE.test("")).toBe(false); + }); + + it("discovers transcripts under agents//sessions and skips trajectory files", () => { + const home = mkdtempSync(join(tmpdir(), "openclaw-home-")); + const sessions = join(home, "agents", "main", "sessions"); + mkdirSync(sessions, { recursive: true }); + writeFileSync(join(sessions, `${UUID}.jsonl`), LINES.map((l) => JSON.stringify(l)).join("\n")); + // Heavy OTel trace + pointer must be ignored. + writeFileSync(join(sessions, `${UUID}.trajectory.jsonl`), "{}\n"); + writeFileSync(join(sessions, `${UUID}.trajectory-path.json`), "{}\n"); + const prev = process.env.OPENCLAW_HOME; + process.env.OPENCLAW_HOME = home; + try { + const found = listOpenClawTranscripts(); + expect(found.map((t) => t.sessionId)).toEqual([UUID]); + expect(found[0].agentId).toBe("main"); + expect(findOpenClawTranscript(UUID)).toBe(join(sessions, `${UUID}.jsonl`)); + // Traversal id never resolves. + expect(findOpenClawTranscript("../../etc/passwd")).toBeNull(); + } finally { + if (prev === undefined) delete process.env.OPENCLAW_HOME; + else process.env.OPENCLAW_HOME = prev; + rmSync(home, { recursive: true, force: true }); + } + }); +}); diff --git a/__tests__/lib/projects.test.ts b/__tests__/lib/projects.test.ts index cf03caf1..94121532 100644 --- a/__tests__/lib/projects.test.ts +++ b/__tests__/lib/projects.test.ts @@ -40,9 +40,6 @@ vi.mock("@/lib/pi-projects", () => ({ getPiProjects: vi.fn(async () => []), })); -vi.mock("@/lib/gemini-projects", () => ({ - getGeminiProjects: vi.fn(async () => []), -})); // Mock hermes-projects too — getProjectFolders() calls getHermesProjects(), // which does real ~/.hermes/state.db I/O; without this the suite is non-hermetic // and fails on any box with a real gateway DB. @@ -50,6 +47,12 @@ vi.mock("@/lib/hermes-projects", () => ({ getHermesProjects: vi.fn(async () => []), })); +// Antigravity reads the real ~/.gemini/antigravity-cli/brain dir; mock it to [] +// so a developer's local Antigravity sessions don't leak into these assertions. +vi.mock("@/lib/antigravity-projects", () => ({ + getAntigravityProjects: vi.fn(async () => []), +})); + import { readdir, stat } from "fs/promises"; import { extractSessionId, getProjectFolders, getSessionFiles, type ProjectFolder } from "@/lib/projects"; import { getCodexProjects } from "@/lib/codex-projects"; @@ -57,7 +60,6 @@ import { getCopilotProjects } from "@/lib/copilot-projects"; import { getCursorProjects } from "@/lib/cursor-projects"; import { getOpenCodeProjects } from "@/lib/opencode-projects"; import { getPiProjects } from "@/lib/pi-projects"; -import { getGeminiProjects } from "@/lib/gemini-projects"; import { getHermesProjects } from "@/lib/hermes-projects"; const mockGetCodexProjects = vi.mocked(getCodexProjects); @@ -65,7 +67,6 @@ const mockGetCopilotProjects = vi.mocked(getCopilotProjects); const mockGetCursorProjects = vi.mocked(getCursorProjects); const mockGetOpenCodeProjects = vi.mocked(getOpenCodeProjects); const mockGetPiProjects = vi.mocked(getPiProjects); -const mockGetGeminiProjects = vi.mocked(getGeminiProjects); const mockGetHermesProjects = vi.mocked(getHermesProjects); const mockReaddir = vi.mocked(readdir); @@ -534,26 +535,6 @@ describe("getProjectFolders", () => { expect(result[0].cli).toEqual(["claude"]); }); - it("includes Gemini-only projects (no matching Claude/Codex/Copilot/Cursor/OpenCode/Pi folder)", async () => { - mockStat.mockResolvedValueOnce({ isDirectory: () => true } as any); - mockReaddir.mockResolvedValueOnce([] as any); - mockGetGeminiProjects.mockResolvedValueOnce([ - { - name: "-home-u-gemini-only", - path: "/home/u/gemini-only", - isDirectory: true, - lastModified: new Date("2026-08-15T00:00:00Z"), - lastModifiedFormatted: "2026-08-15T00:00:00.000Z", - cli: ["gemini"], - } satisfies ProjectFolder, - ]); - - const result = await getProjectFolders(); - expect(result).toHaveLength(1); - expect(result[0].cli).toEqual(["gemini"]); - expect(result[0].path).toBe("/home/u/gemini-only"); - }); - it("surfaces a standalone Hermes project", async () => { mockStat.mockResolvedValueOnce({ isDirectory: () => true } as any); mockReaddir.mockResolvedValueOnce([] as any); @@ -574,45 +555,6 @@ describe("getProjectFolders", () => { expect(result[0].path).toBe("hermes:slack"); }); - it("merges a Gemini project with the same encoded name into one row with both badges", async () => { - mockStat.mockResolvedValueOnce({ isDirectory: () => true } as any); - mockReaddir.mockResolvedValueOnce([ - { name: "-home-u-shared", isDirectory: () => true, isFile: () => false } as any, - ] as any); - mockStat.mockResolvedValueOnce({ mtime: new Date("2026-04-01T00:00:00Z") } as any); - mockGetGeminiProjects.mockResolvedValueOnce([ - { - name: "-home-u-shared", - path: "/home/u/shared", - isDirectory: true, - lastModified: new Date("2026-09-01T00:00:00Z"), - lastModifiedFormatted: "2026-09-01T00:00:00.000Z", - cli: ["gemini"], - } satisfies ProjectFolder, - ]); - - const result = await getProjectFolders(); - expect(result).toHaveLength(1); - // Claude registered first → cli order is ["claude", "gemini"]; lastModified - // takes the newer Gemini date (Sept > April) so the row sorts to the top. - expect(result[0].cli).toEqual(["claude", "gemini"]); - expect(result[0].lastModified.toISOString()).toBe("2026-09-01T00:00:00.000Z"); - }); - - it("falls back gracefully when getGeminiProjects rejects", async () => { - mockStat.mockResolvedValueOnce({ isDirectory: () => true } as any); - mockReaddir.mockResolvedValueOnce([ - { name: "-home-u-claude", isDirectory: () => true, isFile: () => false } as any, - ] as any); - mockStat.mockResolvedValueOnce({ mtime: new Date("2026-04-01T00:00:00Z") } as any); - mockGetGeminiProjects.mockRejectedValueOnce(new Error("scan failed")); - - const result = await getProjectFolders(); - // Claude row still surfaces even though Gemini scan blew up. - expect(result).toHaveLength(1); - expect(result[0].cli).toEqual(["claude"]); - }); - it("falls back gracefully when getCursorProjects rejects", async () => { mockStat.mockResolvedValueOnce({ isDirectory: () => true } as any); mockReaddir.mockResolvedValueOnce([ diff --git a/__tests__/scripts/translate-docs/config.test.ts b/__tests__/scripts/translate-docs/config.test.ts index 6fbfbf35..cec14327 100644 --- a/__tests__/scripts/translate-docs/config.test.ts +++ b/__tests__/scripts/translate-docs/config.test.ts @@ -15,6 +15,12 @@ describe("LANGUAGES", () => { expect(LANGUAGES).toHaveLength(14); }); + it("uses Mintlify's canonical locale without changing the folder code", () => { + const portuguese = LANGUAGES.find((language) => language.code === "pt-br"); + + expect(portuguese?.mintlifyCode).toBe("pt-BR"); + }); + it("has unique codes", () => { const codes = LANGUAGES.map((l) => l.code); expect(new Set(codes).size).toBe(codes.length); diff --git a/__tests__/scripts/translate-docs/mintlify-nav.test.ts b/__tests__/scripts/translate-docs/mintlify-nav.test.ts index 025240ef..fbbea307 100644 --- a/__tests__/scripts/translate-docs/mintlify-nav.test.ts +++ b/__tests__/scripts/translate-docs/mintlify-nav.test.ts @@ -212,4 +212,11 @@ describe("localizeProductsNavigation", () => { "es/agenteye/overview", ]); }); + + it("uses Mintlify's canonical Portuguese locale with existing paths", () => { + const portuguese = buildLanguageNav(sampleEnglishTabs, "pt-br"); + + expect(portuguese.language).toBe("pt-BR"); + expect(portuguese.tabs[0].groups[0].pages[0]).toBe("pt-br/introduction"); + }); }); diff --git a/app/audit/_components/empty-state.tsx b/app/audit/_components/empty-state.tsx index 73688e4a..4e652810 100644 --- a/app/audit/_components/empty-state.tsx +++ b/app/audit/_components/empty-state.tsx @@ -74,7 +74,7 @@ export function EmptyState({ mode, running, onStarted, onCompleted }: Props) {

run your first audit.

we'll walk every transcript across your installed CLIs — Claude Code, - Codex, Copilot, Cursor, OpenCode, Pi, Gemini — and count every wasteful + Codex, Copilot, Cursor, OpenCode, Pi — and count every wasteful or risky action. you'll get a tier, a score, and a punch-list.

diff --git a/app/policies/hooks-client.tsx b/app/policies/hooks-client.tsx index d2dcf362..6cef0b47 100644 --- a/app/policies/hooks-client.tsx +++ b/app/policies/hooks-client.tsx @@ -99,12 +99,10 @@ function SessionCell({ (transcriptPath?.includes("/.opencode/") ?? false); const isPi = integration === "pi" || (transcriptPath?.includes("/.pi/") ?? false); - const isGemini = - integration === "gemini" || (transcriptPath?.includes("/.gemini/") ?? false); - if (isCodex || isCopilot || isCursor || isOpenCode || isPi || isGemini) { + if (isCodex || isCopilot || isCursor || isOpenCode || isPi) { // The session route auto-detects CLI by file location, so [name] only // affects the breadcrumb. Encode the cwd Claude-style when we have it. - const fallbackSeg = isCodex ? "codex" : isCopilot ? "copilot" : isCursor ? "cursor" : isOpenCode ? "opencode" : isPi ? "pi" : "gemini"; + const fallbackSeg = isCodex ? "codex" : isCopilot ? "copilot" : isCursor ? "cursor" : isOpenCode ? "opencode" : "pi"; const projectSeg = cwd ? encodeCwdForUrl(cwd) : fallbackSeg; return ( !!s) .map((s) => s.lastModified) .reduce((acc, d) => (!acc || d.getTime() > acc.getTime() ? d : acc), null); @@ -110,8 +126,12 @@ export default async function ProjectPage({ params }: ProjectPageProps) { ...cursorSessions, ...opencodeSessions, ...piSessions, - ...geminiSessions, ...hermesSessions, + ...openclawSessions, + ...factorySessions, + ...devinSessions, + ...antigravitySessions, + ...gooseSessions, ].sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime()); // Path line: prefer the Claude storage dir if present (matches existing UX); diff --git a/app/project/[name]/session/[sessionId]/page.tsx b/app/project/[name]/session/[sessionId]/page.tsx index f328613b..14f87827 100644 --- a/app/project/[name]/session/[sessionId]/page.tsx +++ b/app/project/[name]/session/[sessionId]/page.tsx @@ -8,8 +8,12 @@ import { getCachedCopilotSessionLog } from "@/lib/copilot-sessions"; import { getCachedCursorSessionLog } from "@/lib/cursor-sessions"; import { getCachedOpenCodeSessionLog } from "@/lib/opencode-sessions"; import { getCachedPiSessionLog } from "@/lib/pi-sessions"; -import { getCachedGeminiSessionLog } from "@/lib/gemini-sessions"; import { getCachedHermesSessionLog } from "@/lib/hermes-sessions"; +import { getCachedOpenClawSessionLog } from "@/lib/openclaw-sessions"; +import { getCachedFactorySessionLog } from "@/lib/factory-sessions"; +import { getCachedDevinSessionLog } from "@/lib/devin-sessions"; +import { getCachedAntigravitySessionLog } from "@/lib/antigravity-sessions"; +import { getCachedGooseSessionLog } from "@/lib/goose-sessions"; import { decodeFolderName } from "@/lib/paths"; import { baseSessionId } from "@/lib/utils/session-id"; import { resolveProjectPath, UUID_RE } from "@/lib/projects"; @@ -53,7 +57,7 @@ export default async function SessionPage({ params }: SessionPageProps) { let entries: LogEntry[] | null = null; let rawLines: Record[] | null = null; let error: string | null = null; - let cli: "claude" | "codex" | "copilot" | "cursor" | "opencode" | "pi" | "gemini" | "hermes" = "claude"; + let cli: "claude" | "codex" | "copilot" | "cursor" | "opencode" | "pi" | "hermes" | "openclaw" | "factory" | "devin" | "antigravity" | "goose" = "claude"; let externalCwd: string | undefined; try { @@ -102,21 +106,53 @@ export default async function SessionPage({ params }: SessionPageProps) { externalCwd = pi.cwd; cli = "pi"; } else { - const gemini = await getCachedGeminiSessionLog(decodedSessionId); - if (gemini) { - entries = gemini.entries; - rawLines = gemini.rawLines; - externalCwd = gemini.cwd; - cli = "gemini"; + const hermes = await getCachedHermesSessionLog(decodedSessionId); + if (hermes) { + entries = hermes.entries; + rawLines = hermes.rawLines; + externalCwd = hermes.cwd; + cli = "hermes"; } else { - const hermes = await getCachedHermesSessionLog(decodedSessionId); - if (hermes) { - entries = hermes.entries; - rawLines = hermes.rawLines; - externalCwd = hermes.cwd; - cli = "hermes"; + const openclaw = await getCachedOpenClawSessionLog(decodedSessionId); + if (openclaw) { + entries = openclaw.entries; + rawLines = openclaw.rawLines; + externalCwd = openclaw.cwd; + cli = "openclaw"; } else { - error = "Session log file not found."; + const factory = await getCachedFactorySessionLog(decodedSessionId); + if (factory) { + entries = factory.entries; + rawLines = factory.rawLines; + externalCwd = factory.cwd; + cli = "factory"; + } else { + const devin = await getCachedDevinSessionLog(decodedSessionId); + if (devin) { + entries = devin.entries; + rawLines = devin.rawLines; + externalCwd = devin.cwd; + cli = "devin"; + } else { + const antigravity = await getCachedAntigravitySessionLog(decodedSessionId); + if (antigravity) { + entries = antigravity.entries; + rawLines = antigravity.rawLines; + externalCwd = antigravity.cwd; + cli = "antigravity"; + } else { + const goose = await getCachedGooseSessionLog(decodedSessionId); + if (goose) { + entries = goose.entries; + rawLines = goose.rawLines; + externalCwd = goose.cwd; + cli = "goose"; + } else { + error = "Session log file not found."; + } + } + } + } } } } @@ -142,11 +178,19 @@ export default async function SessionPage({ params }: SessionPageProps) { ? `OpenCode${externalCwd ? ` · ${externalCwd}` : ""}` : cli === "pi" ? `Pi${externalCwd ? ` · ${externalCwd}` : ""}` - : cli === "gemini" - ? `Gemini CLI${externalCwd ? ` · ${externalCwd}` : ""}` - : cli === "hermes" - ? `Hermes${externalCwd ? ` · ${externalCwd}` : ""}` - : decodedName; + : cli === "hermes" + ? `Hermes${externalCwd ? ` · ${externalCwd}` : ""}` + : cli === "openclaw" + ? `OpenClaw${externalCwd ? ` · ${externalCwd}` : ""}` + : cli === "factory" + ? `Factory Droid${externalCwd ? ` · ${externalCwd}` : ""}` + : cli === "devin" + ? `Devin CLI${externalCwd ? ` · ${externalCwd}` : ""}` + : cli === "antigravity" + ? `Antigravity CLI${externalCwd ? ` · ${externalCwd}` : ""}` + : cli === "goose" + ? `Goose${externalCwd ? ` · ${externalCwd}` : ""}` + : decodedName; return (
@@ -216,7 +260,13 @@ export default async function SessionPage({ params }: SessionPageProps) { ? "Pi" : cli === "hermes" ? "Hermes" - : "Gemini CLI")) + : cli === "openclaw" + ? "OpenClaw" + : cli === "factory" + ? "Factory Droid" + : cli === "devin" + ? "Devin CLI" + : "Antigravity CLI")) : decodedName } sessionId={decodedSessionId} diff --git a/assets/logos/antigravity.svg b/assets/logos/antigravity.svg new file mode 100644 index 00000000..3ed10ab6 --- /dev/null +++ b/assets/logos/antigravity.svg @@ -0,0 +1 @@ +Antigravity \ No newline at end of file diff --git a/assets/logos/devin.svg b/assets/logos/devin.svg new file mode 100644 index 00000000..ec2be393 --- /dev/null +++ b/assets/logos/devin.svg @@ -0,0 +1 @@ +Devin \ No newline at end of file diff --git a/assets/logos/factory-dark.png b/assets/logos/factory-dark.png new file mode 100644 index 00000000..3495c05d Binary files /dev/null and b/assets/logos/factory-dark.png differ diff --git a/assets/logos/factory-light.png b/assets/logos/factory-light.png new file mode 100644 index 00000000..a1a7b6fd Binary files /dev/null and b/assets/logos/factory-light.png differ diff --git a/assets/logos/gemini-dark.svg b/assets/logos/gemini-dark.svg deleted file mode 100644 index c79d33f6..00000000 --- a/assets/logos/gemini-dark.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - diff --git a/assets/logos/gemini-light.svg b/assets/logos/gemini-light.svg deleted file mode 100644 index 2d9f6403..00000000 --- a/assets/logos/gemini-light.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - diff --git a/assets/logos/goose-dark.svg b/assets/logos/goose-dark.svg new file mode 100644 index 00000000..8a355cb7 --- /dev/null +++ b/assets/logos/goose-dark.svg @@ -0,0 +1 @@ +Goose \ No newline at end of file diff --git a/assets/logos/goose-light.svg b/assets/logos/goose-light.svg new file mode 100644 index 00000000..43bf93b0 --- /dev/null +++ b/assets/logos/goose-light.svg @@ -0,0 +1 @@ +Goose \ No newline at end of file diff --git a/assets/logos/openclaw.svg b/assets/logos/openclaw.svg new file mode 100644 index 00000000..bcbc1e10 --- /dev/null +++ b/assets/logos/openclaw.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/bin/failproofai.mjs b/bin/failproofai.mjs index a3104d25..b7490cc0 100755 --- a/bin/failproofai.mjs +++ b/bin/failproofai.mjs @@ -65,7 +65,7 @@ const hookIdx = args.indexOf("--hook"); if (hookIdx >= 0) { if (!args[hookIdx + 1]) { console.error("Error: Missing event type after --hook"); - console.error("Usage: failproofai --hook [--cli ]"); + console.error("Usage: failproofai --hook [--cli ]"); process.exit(1); } const eventType = args[hookIdx + 1]; @@ -81,8 +81,12 @@ if (hookIdx >= 0) { || cliArg === "cursor" || cliArg === "opencode" || cliArg === "pi" - || cliArg === "gemini" || cliArg === "hermes" + || cliArg === "openclaw" + || cliArg === "factory" + || cliArg === "devin" + || cliArg === "antigravity" + || cliArg === "goose" ) ? cliArg : "claude"; @@ -130,18 +134,18 @@ COMMANDS policies, p List all available policies and their status policies --install, -i Enable policies in agent CLI settings [names...] Specific policy names to enable - --cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes + --cli claude|codex|copilot|cursor|opencode|pi|hermes|openclaw|factory|devin|antigravity|goose Agent CLI(s) to install for; space-separated - (e.g. --cli claude codex copilot cursor opencode pi gemini hermes) or repeated. + (e.g. --cli claude codex copilot cursor opencode pi hermes openclaw factory devin antigravity goose) or repeated. Default: detect installed CLIs and prompt. --scope user|project|local Config scope to write to (default: user) - (Codex / Copilot / Cursor / OpenCode / Pi / Gemini support user|project only) + (Codex / Copilot / Cursor / OpenCode / Pi support user|project only) --beta Include beta policies --custom, -c Path to a JS file of custom policies policies --uninstall, -u Disable policies or remove hooks [names...] Specific policy names to disable - --cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes + --cli claude|codex|copilot|cursor|opencode|pi|hermes|openclaw|factory|devin|antigravity|goose Agent CLI(s) to uninstall from --scope user|project|local|all Config scope to remove from (default: user) --beta Remove only beta policies @@ -176,8 +180,9 @@ EXAMPLES failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project - failproofai policies --install --cli gemini --scope project - failproofai policies --install --cli claude codex copilot cursor opencode pi gemini hermes + failproofai policies --install --cli factory --scope project + failproofai policies --install --cli devin --scope project + failproofai policies --install --cli claude codex copilot cursor opencode pi hermes openclaw factory devin antigravity goose failproofai policies --install --custom ./my-policies.js failproofai policies -i -c ./my-policies.js failproofai policies --uninstall block-sudo @@ -186,7 +191,6 @@ EXAMPLES failproofai policies --uninstall --cli cursor failproofai policies --uninstall --cli opencode failproofai policies --uninstall --cli pi - failproofai policies --uninstall --cli gemini failproofai policies --uninstall --custom LINKS @@ -226,20 +230,20 @@ USAGE OPTIONS (install) [names...] Specific policy names to enable (omit for interactive) - --cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes + --cli claude|codex|copilot|cursor|opencode|pi|hermes|openclaw|factory|devin|antigravity|goose Agent CLI(s) to install for; space-separated - (e.g. --cli claude codex copilot cursor opencode pi gemini hermes) or repeated. + (e.g. --cli claude codex copilot cursor opencode pi hermes openclaw factory devin antigravity goose) or repeated. Omit to detect installed CLIs and prompt (or auto-pick if only one is found). --scope user|project|local Config scope to write to (default: user) - (Codex / Copilot / Cursor / OpenCode / Pi / Gemini support user|project only) + (Codex / Copilot / Cursor / OpenCode / Pi support user|project only) --beta Include beta policies --custom, -c Path to a JS file of custom policies (skips interactive prompt; validates file first) OPTIONS (uninstall) [names...] Specific policy names to disable (omit to remove hooks) - --cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes + --cli claude|codex|copilot|cursor|opencode|pi|hermes|openclaw|factory|devin|antigravity|goose Agent CLI(s) to uninstall from --scope user|project|local|all Config scope to remove from (default: user) --beta Remove only beta policies @@ -254,8 +258,9 @@ EXAMPLES failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project - failproofai policies --install --cli gemini --scope project - failproofai policies --install --cli claude codex copilot cursor opencode pi gemini hermes + failproofai policies --install --cli factory --scope project + failproofai policies --install --cli devin --scope project + failproofai policies --install --cli claude codex copilot cursor opencode pi hermes openclaw factory devin antigravity goose failproofai policies --install --custom ./my-policies.js failproofai policies -i -c ./my-policies.js failproofai policies --uninstall block-sudo @@ -296,7 +301,7 @@ EXAMPLES // --cli claude codex copilot // --cli claude --cli codex // Values are consumed greedily until the next flag or end of argv. - const VALID_CLIS = new Set(["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini", "hermes"]); + const VALID_CLIS = new Set(["claude", "codex", "copilot", "cursor", "opencode", "pi", "hermes", "openclaw", "factory", "devin", "antigravity", "goose"]); const cliFlagValues = []; const cliConsumedIdxs = new Set(); const cliFlagIdxs = subArgs.map((a, i) => (a === "--cli" ? i : -1)).filter((i) => i >= 0); @@ -313,7 +318,7 @@ EXAMPLES consumed++; } if (consumed === 0) { - throw new CliError("Missing value(s) for --cli. Usage: --cli claude codex copilot cursor opencode pi gemini hermes (or any subset)"); + throw new CliError("Missing value(s) for --cli. Usage: --cli claude codex copilot cursor opencode pi hermes openclaw (or any subset)"); } } @@ -385,7 +390,7 @@ EXAMPLES } // --cli accepts one or more space-separated values; same parser as install. - const VALID_CLIS = new Set(["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini", "hermes"]); + const VALID_CLIS = new Set(["claude", "codex", "copilot", "cursor", "opencode", "pi", "hermes", "openclaw", "factory", "devin", "antigravity", "goose"]); const cliFlagValues = []; const cliConsumedIdxs = new Set(); const cliFlagIdxs = subArgs.map((a, i) => (a === "--cli" ? i : -1)).filter((i) => i >= 0); @@ -402,7 +407,7 @@ EXAMPLES consumed++; } if (consumed === 0) { - throw new CliError("Missing value(s) for --cli. Usage: --cli claude codex copilot cursor opencode pi gemini hermes (or any subset)"); + throw new CliError("Missing value(s) for --cli. Usage: --cli claude codex copilot cursor opencode pi hermes openclaw (or any subset)"); } } @@ -511,7 +516,7 @@ USAGE failproofai policy remove Disable one policy OPTIONS - --cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes + --cli claude|codex|copilot|cursor|opencode|pi|hermes|openclaw|factory|devin|antigravity|goose Agent CLI(s) to apply to; space-separated or repeated. Omit to detect installed CLIs and prompt. --scope user|project|local Config scope (default: user) @@ -549,7 +554,7 @@ EXAMPLES } // --cli accepts one or more space-separated values, optionally repeated. - const VALID_CLIS = new Set(["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini", "hermes"]); + const VALID_CLIS = new Set(["claude", "codex", "copilot", "cursor", "opencode", "pi", "hermes", "openclaw", "factory", "devin", "antigravity", "goose"]); const cliFlagValues = []; const cliConsumedIdxs = new Set(); const cliFlagIdxs = rest.map((a, i) => (a === "--cli" ? i : -1)).filter((i) => i >= 0); @@ -564,7 +569,7 @@ EXAMPLES consumed++; } if (consumed === 0) { - throw new CliError("Missing value(s) for --cli. Usage: --cli claude codex copilot cursor opencode pi gemini hermes (or any subset)"); + throw new CliError("Missing value(s) for --cli. Usage: --cli claude codex copilot cursor opencode pi hermes openclaw (or any subset)"); } } diff --git a/docs/ar/built-in-policies.mdx b/docs/ar/built-in-policies.mdx index 1c508863..2f712836 100644 --- a/docs/ar/built-in-policies.mdx +++ b/docs/ar/built-in-policies.mdx @@ -710,7 +710,7 @@ icon: shield ### دلالات Stop لكل CLI -يبدو إنفاذ Stop مختلفاً قليلاً عبر سبعة CLI مدعومة لأن كل واحد يعرّض عقد hook مختلفاً. النتيجة **متطابقة** — الوكيل لا يهرب من التوقف بينما تفشل بوابة سير العمل — لكن **الميكانيكا** تختلف. يلخص الجدول أدناه؛ فقط Pi لديه خصوصية واضحة للمستخدم تستحق الفهم قبل تفعيل سياسة `require-*-before-stop`. +يبدو إنفاذ Stop مختلفاً قليلاً عبر ستة CLI مدعومة لأن كل واحد يعرّض عقد hook مختلفاً. النتيجة **متطابقة** — الوكيل لا يهرب من التوقف بينما تفشل بوابة سير العمل — لكن **الميكانيكا** تختلف. يلخص الجدول أدناه؛ فقط Pi لديه خصوصية واضحة للمستخدم تستحق الفهم قبل تفعيل سياسة `require-*-before-stop`. | CLI | متى تعمل البوابة | ما تراه | |---|---|---| @@ -718,20 +718,19 @@ icon: shield | Codex | نفس حلقة الوكيل، فوراً | نفس Claude. | | GitHub Copilot CLI | نفس حلقة الوكيل، فوراً | نفس Claude (يستخدم قناة إعادة محاولة `{decision:"block", reason}` الخاصة بـ Copilot — تم التحقق منها تجريبياً ضد Copilot CLI 1.0.41). | | Cursor Agent | نفس حلقة الوكيل، فوراً | نفس Claude (يستخدم قناة `{followup_message}` الخاصة بـ Cursor — محدودة بـ `loop_limit`، افتراضي 5 إعادة محاولات). | -| Gemini CLI | نفس حلقة الوكيل، فوراً | نفس Claude (يستخدم قناة `{decision:"block", reason}` الخاصة بـ Gemini على `AfterAgent`). | | OpenCode | نفس حلقة الوكيل، فوراً | نفس Claude (يستخدم استدعاء SDK `client.session.prompt(...)` الخاص بـ OpenCode الموجه عبر `hookSpecificOutput.additionalContext`). | | **Pi (pi-coding-agent)** | **دور المستخدم التالي** | **يتوقف Pi بشكل واضح** عندما تعمل البوابة — تنهي حلقة الوكيل الخاصة به وتعودين إلى الموجه. تعمل البوابة بعد ذلك في المرة القادمة التي تقدّمين فيها موجهاً: يضيف failproofai توجيهاً `MANDATORY ACTION REQUIRED` إلى موجه نظام هذا الدور، يعلّم LLM بإكمال خطوة سير العمل (التزام، دفع، إلخ) قبل فعل ما طلبتِ. | -**قيد Pi.** `AgentEndEvent` الخاص بـ Pi (المعادل الأعلى لـ hook `Stop` الخاص بـ Claude) لا يملك نوع Result — بحلول الوقت الذي يعمل فيه، تكون حلقة الوكيل الخاصة بـ Pi قد خرجت بالفعل. لا يمكن إجبار Pi على إعادة محاولة نفس الحلقة بالطريقة التي يمكن بها Claude / Copilot / Cursor / Gemini / OpenCode. ينقل failproofai البوابة إلى حدث `before_agent_start` الخاص بـ Pi (الذي يعمل بعد الموجه التالي للمستخدم) لذا يبقى فحص سير العمل مفروضاً، فقط على الدور التالي بدلاً من الحالي. +**قيد Pi.** `AgentEndEvent` الخاص بـ Pi (المعادل الأعلى لـ hook `Stop` الخاص بـ Claude) لا يملك نوع Result — بحلول الوقت الذي يعمل فيه، تكون حلقة الوكيل الخاصة بـ Pi قد خرجت بالفعل. لا يمكن إجبار Pi على إعادة محاولة نفس الحلقة بالطريقة التي يمكن بها Claude / Copilot / Cursor / OpenCode. ينقل failproofai البوابة إلى حدث `before_agent_start` الخاص بـ Pi (الذي يعمل بعد الموجه التالي للمستخدم) لذا يبقى فحص سير العمل مفروضاً، فقط على الدور التالي بدلاً من الحالي. **ما يعنيه هذا عملياً:** - بعد توقف Pi، يتم التقاط رار الرفض في الذاكرة بمفتاح معرف جلسة Pi. الموجه التالي الذي تقدّمينه بالضبط في نفس عملية Pi يصرفه: يرى LLM توجيه `MANDATORY ACTION REQUIRED` في أعلى موجه النظام الخاص به، يلتزم (أو يدفع / يفتح PR / ينتظر CI)، وفقط بعد ذلك يستمر مع طلبك. رار الرفض المقبوض مرة واحدة — بمجرد صرفه، تكون البوابة واضحة. -- البوابة محدودة بعمر عملية Pi. إذا قمتِ بـ `Ctrl+C` Pi أو أغلقتِ بين الأدوار، يتم حذف الإدخال في الذاكرة مع العملية والبوابة تُفتقد. Claude و Copilot و Cursor و Gemini و OpenCode لديها نفس الحد (اقتل الوكيل والبوابة تُفتقد) — Pi فقط يجعله أكثر وضوحاً لأن الوكيل يخرج بشكل واضح قبل عمل البوابة. +- البوابة محدودة بعمر عملية Pi. إذا قمتِ بـ `Ctrl+C` Pi أو أغلقتِ بين الأدوار، يتم حذف الإدخال في الذاكرة مع العملية والبوابة تُفتقد. Claude و Copilot و Cursor و OpenCode لديها نفس الحد (اقتل الوكيل والبوابة تُفتقد) — Pi فقط يجعله أكثر وضوحاً لأن الوكيل يخرج بشكل واضح قبل عمل البوابة. - رار الرفض المعلق يتم مسحه أيضاً على `session_shutdown` لأي سبب (`new` / `resume` / `fork` / `quit`)، لذا بوابة قديمة من جلسة سابقة لا تستطيع التسرب إلى جلسة جديدة بدأت في نفس عملية Pi. -إذا احتجتِ إلى إعادة محاولة نفس الحلقة بأسلوب Claude، قومي بتشغيل سياسات `Stop` الخاصة بك تحت أي من ستة CLI الأخرى المدعومة. نحن نتابع Pi في المنبع لنوع Result مستقبلي على `AgentEndEvent` الذي سيسمح لنا بإغلاق هذه الفجوة. +إذا احتجتِ إلى إعادة محاولة نفس الحلقة بأسلوب Claude، قومي بتشغيل سياسات `Stop` الخاصة بك تحت أي من خمسة CLI الأخرى المدعومة. نحن نتابع Pi في المنبع لنوع Result مستقبلي على `AgentEndEvent` الذي سيسمح لنا بإغلاق هذه الفجوة. ### `require-commit-before-stop` diff --git a/docs/ar/cli/audit.mdx b/docs/ar/cli/audit.mdx index fd1384a9..2efba375 100644 --- a/docs/ar/cli/audit.mdx +++ b/docs/ar/cli/audit.mdx @@ -46,7 +46,7 @@ failproofai قم بتشغيل `failproofai audit -h` (أو `--help`) لرؤية الاستخدام. يعمل التدقيق **بالكامل بدون اتصال** — لا تحتاج إلى حساب أو شبكة — ولوحة التحكم تستمر في الخدمة حتى توقفها بـ `Ctrl+C`. -تقوم لوحة التحكم بفحص نصوص وكيل CLI السابقة على هذا الجهاز (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi, Gemini) وتقرر كم مرة فعل الوكيل أشياء تم بناء failproofai لإيقافها — فحوصات متغيرات البيئة، الدفعات القسرية، البادئات `cd ` الزائدة، حلقات الانتظار بالنوم، إعادة قراءة الملفات التي تم تحريرها للتو، وغير ذلك. +تقوم لوحة التحكم بفحص نصوص وكيل CLI السابقة على هذا الجهاز (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi) وتقرر كم مرة فعل الوكيل أشياء تم بناء failproofai لإيقافها — فحوصات متغيرات البيئة، الدفعات القسرية، البادئات `cd ` الزائدة، حلقات الانتظار بالنوم، إعادة قراءة الملفات التي تم تحريرها للتو، وغير ذلك. لكل نص، يتم إعادة تشغيل كل حدث استخدام أداة عبر السياسات المدمجة البالغ عددها 39 **و** عبر 8 كواشف خاصة بالتدقيق فقط التي تلتقط الأنماط التي لم تغطها السياسات الوقتية بعد. يتم تجميع الأعداد لكل سياسة / كاشف عبر جميع الجلسات. diff --git a/docs/ar/configuration.mdx b/docs/ar/configuration.mdx index 71fa9a37..9c277024 100644 --- a/docs/ar/configuration.mdx +++ b/docs/ar/configuration.mdx @@ -197,13 +197,12 @@ resolved: { allowPatterns: ["sudo systemctl status"] } ← ينتقل إلى ا - **OpenAI Codex**: `~/.codex/hooks.json` (مستخدم), `/.codex/hooks.json` (مشروع) — Codex ليس له نطاق محلي - **GitHub Copilot CLI _(نسخة تجريبية)_**: `~/.copilot/hooks/failproofai.json` (مستخدم), `/.github/hooks/failproofai.json` (مشروع) — Copilot ليس له نطاق محلي. إدخالات الخطاف تستخدم حقول أوامر Copilot المفتاحة بنظام التشغيل `bash`/`powershell` مع `timeoutSec`؛ يحمل الملف علامة `version: 1` على المستوى الأعلى. دعم GitHub Copilot CLI **نسخة تجريبية** بينما نتحقق من مخطط سجل `events.jsonl` (الذي لا تحدده المستندات العامة) مقابل جلسات حقيقية أكثر. - **Cursor Agent _(نسخة تجريبية)_**: `~/.cursor/hooks.json` (مستخدم), `/.cursor/hooks.json` (مشروع) — Cursor ليس له نطاق محلي. إدخالات الخطاف تستخدم نموذج `{type, command, timeout}` على شكل Claude (لا انقسام `bash`/`powershell`)، لكن يتم تخزينها تحت مفاتيح أحداث camelCase (`preToolUse`, `beforeSubmitPrompt`, …) في مصفوفة مسطحة وفقاً [لمخطط الخطافات](https://cursor.com/docs/hooks) الخاص بـ Cursor؛ يحمل الملف علامة `version: 1` على المستوى الأعلى. يقوم المعالج بتحويل camelCase → PascalCase عبر `CURSOR_EVENT_MAP` بحيث تعمل السياسات المدمجة الموجودة بدون تغيير. دعم Cursor Agent **نسخة تجريبية** بينما نتحقق من نسخة Cursor على القرص (غير محددة في المستندات العامة) مقابل عمليات تثبيت حقيقية أكثر. - - **OpenCode _(نسخة تجريبية)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (مستخدم), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (مشروع) — OpenCode ليس له نطاق محلي. على عكس المحررات الستة الأخرى، OpenCode **ليس له نظام خطاف أوامر خارجية**: يحمل في الذاكرة مكوّنات JavaScript/TypeScript مسجلة بشكل صريح عبر مصفوفة `plugin: []` في `opencode.json` (الاكتشاف التلقائي من `.opencode/plugins/` **ليس** كيف يتم تحميل المكونات على opencode v1.14.33). التثبيت ينقط مكون شيم صغير يستدعي ثنائي failproofai عبر subprocess ويترجم استجابة JSON على شكل Claude الخاصة بالثنائي إلى دلالات المكون: `throw new Error()` لرفض حدث الأداة (يلغي استدعاء الأداة)، `client.session.prompt(...)` للتعليمات **و** لرفض `Stop` / `SubagentStop` (يرسل سبب الرفض كرسالة المستخدم التالية — القناة الوحيدة للإعادة القسرية منذ أن `session.idle` إخطار فقط والرمي منه لا يعمل)، بدون عملية للسماح. يقوم الشيم بتحويل أسماء الأدوات (lowercase → PascalCase عبر `OPENCODE_TOOL_MAP`) ومفاتيح حجج إدخال الأداة (camelCase → snake_case عبر `OPENCODE_TOOL_INPUT_MAP` لـ `Read` / `Write` / `Edit`، مثلاً `filePath` → `file_path`, `oldString` → `old_string`) قبل إعادة التوجيه إلى الثنائي، بحيث تعمل عمليات التحقق من المسارات المدمجة مثل `block-read-outside-cwd`, `block-env-files`, و `block-secrets-write` بدون تغيير على استدعاءات أداة OpenCode. الجلسات تعيش في قاعدة بيانات OpenCode SQLite في `~/.local/share/opencode/opencode.db`؛ عارض الجلسات في لوحة التحكم يقرأها عبر `opencode db --format json` و `opencode export `. دعم OpenCode **نسخة تجريبية** بينما نتحقق من السلوك عبر الإصدارات ومقابل جلسات حقيقية أكثر. انظر [مستندات مكونات OpenCode](https://opencode.ai/docs/plugins/). + - **OpenCode _(نسخة تجريبية)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (مستخدم), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (مشروع) — OpenCode ليس له نطاق محلي. على عكس المحررات الخمسة الأخرى، OpenCode **ليس له نظام خطاف أوامر خارجية**: يحمل في الذاكرة مكوّنات JavaScript/TypeScript مسجلة بشكل صريح عبر مصفوفة `plugin: []` في `opencode.json` (الاكتشاف التلقائي من `.opencode/plugins/` **ليس** كيف يتم تحميل المكونات على opencode v1.14.33). التثبيت ينقط مكون شيم صغير يستدعي ثنائي failproofai عبر subprocess ويترجم استجابة JSON على شكل Claude الخاصة بالثنائي إلى دلالات المكون: `throw new Error()` لرفض حدث الأداة (يلغي استدعاء الأداة)، `client.session.prompt(...)` للتعليمات **و** لرفض `Stop` / `SubagentStop` (يرسل سبب الرفض كرسالة المستخدم التالية — القناة الوحيدة للإعادة القسرية منذ أن `session.idle` إخطار فقط والرمي منه لا يعمل)، بدون عملية للسماح. يقوم الشيم بتحويل أسماء الأدوات (lowercase → PascalCase عبر `OPENCODE_TOOL_MAP`) ومفاتيح حجج إدخال الأداة (camelCase → snake_case عبر `OPENCODE_TOOL_INPUT_MAP` لـ `Read` / `Write` / `Edit`، مثلاً `filePath` → `file_path`, `oldString` → `old_string`) قبل إعادة التوجيه إلى الثنائي، بحيث تعمل عمليات التحقق من المسارات المدمجة مثل `block-read-outside-cwd`, `block-env-files`, و `block-secrets-write` بدون تغيير على استدعاءات أداة OpenCode. الجلسات تعيش في قاعدة بيانات OpenCode SQLite في `~/.local/share/opencode/opencode.db`؛ عارض الجلسات في لوحة التحكم يقرأها عبر `opencode db --format json` و `opencode export `. دعم OpenCode **نسخة تجريبية** بينما نتحقق من السلوك عبر الإصدارات ومقابل جلسات حقيقية أكثر. انظر [مستندات مكونات OpenCode](https://opencode.ai/docs/plugins/). - **Pi _(نسخة تجريبية)_**: `~/.pi/agent/settings.json` (مستخدم), `/.pi/settings.json` (مشروع) — Pi ليس له نطاق محلي. يحمل Pi حزم ملحقات TypeScript عند البدء؛ ملف الإعدادات مصفوفة نصية مسطحة `{"packages": ["./relative/path", …]}`. يكتب failproofai إدخالة مصفوفة حزم واحدة تشير إلى دليل `pi-extension/` المجمع الخاص به. يشترك الملحق داخلياً في أحداث Pi `tool_call` / `user_bash` / `input` / `session_start` ويقذف إلى `failproofai --hook --cli pi`؛ يقوم المعالج بتحويل underscore_lower_snake_case → PascalCase عبر `PI_EVENT_MAP` بحيث تعمل السياسات المدمجة الموجودة بدون تغيير. يتم أيضاً تحويل حجج إدخال الأداة عبر `PI_TOOL_INPUT_MAP` (تسليم Pi للقراءة / الكتابة / التحرير `path` بدلاً من `file_path`؛ تعيين المفتاح على المستوى الأعلى يسمح بتشغيل `block-env-files` و `block-secrets-write` — `block-read-outside-cwd` كان لديه بالفعل بديل `path`). دعم Pi **نسخة تجريبية** بينما تستقر ملحقات Pi API وتخطيط سجل الجلسة. - - **Gemini CLI _(نسخة تجريبية)_**: `~/.gemini/settings.json` (مستخدم), `/.gemini/settings.json` (مشروع) — Gemini ليس له نطاق محلي (يوثق نطاق `system` على `/etc/gemini-cli/settings.json` الذي لا يعرضه failproofai). إدخالات الخطاف تستخدم نموذج Claude `{type, command, timeout}` ملفوف في مخطط مطابق Gemini `{matcher, hooks: [...]}` مع `matcher: "*"` افتراضياً. الأحداث تكون PascalCase (`SessionStart`, `BeforeAgent`, `AfterAgent`, `BeforeModel`, `AfterModel`, `BeforeToolSelection`, `BeforeTool`, `AfterTool`, `PreCompress`, `Notification`, `SessionEnd`); يقوم المعالج بالتعيين إلى أسماء Claude الأساسية عبر `GEMINI_EVENT_MAP`. أسماء الأدوات تكون snake_case (`run_shell_command`, `read_file`, `write_file`, `replace`, …) — يقوم المعالج بتحويلها عبر `GEMINI_TOOL_MAP` بحيث تعمل السياسات المدمجة الموجودة بدون تغيير. يصدر محقق السياسة شكل Gemini المسطح `{decision: "deny", reason}` (مفضل وفقاً لـ Golden Rule للخروج-0 من Gemini)، `{hookSpecificOutput: {hookEventName, additionalContext}}` لحقن السياق على BeforeAgent / AfterTool / SessionStart، و `{decision: "block", reason}` على AfterAgent لدلالات الإعادة القسرية. دعم Gemini CLI **نسخة تجريبية** بينما نوسع التغطية الحقيقية. انظر [مستندات خطافات Gemini CLI](https://geminicli.com/docs/hooks/). - **Hermes (hermes-agent)**: `~/.hermes/config.yaml` (**نطاق المستخدم فقط** — Hermes ليس له إعدادات المشروع/المحلي). Hermes هو **بوابة** Slack/Telegram، لذلك تثبيت واحد يعترض استدعاءات الأدوات من كل منصة (Slack/Telegram/cli/cron) **و** الوكلاء الفرعيين الداخليين. إدخالات الخطاف هي زوج `{command, timeout}` (المهلة الزمنية **بالثواني**) تحت خريطة `hooks:` مفتاحها بأحداث snake_case من Hermes (`pre_tool_call` / `post_tool_call` / `on_session_start` / `on_session_end` / `subagent_stop`); يقوم المعالج بتحويل الأحداث عبر `HERMES_EVENT_MAP` وأسماء الأدوات عبر `HERMES_TOOL_MAP` بحيث تعمل السياسات المدمجة بدون تغيير. يتم تحرير الإعدادات من خلال استدارة YAML `Document` حفاظاً على التعليقات بحيث تبقى الإعدادات الأخرى للمشغل، والتثبيت يعيّن `hooks_auto_accept: true` بحيث تعمل بوابة بدون رأس (لا TTY) الخطافات بدون موجه موافقة. يصدر المقيّم عقد stdout الخاص بـ Hermes `{"decision":"block","reason"}` (يتجاهل Hermes أكواد الخروج). **القيود:** Hermes ليس له حدث نهاية الدور `Stop`، لذا فإن المدمجات `require-*-before-stop` لن تعمل أبداً لـ Hermes (غير قابلة للتطبيق، وليس كسر)؛ `instruct` تنخفض إلى السماح مع ملاحظة مسجلة (لا قناة سياق إضافية)؛ وإعادة تسمية سرية الإخراج (`sanitize-*`) لا يمكن إعادة كتابة إخراج الأداة على عقد shell-hook. Hermes هو **أيضاً** مصدر **تدقيق** غير متصل — لوحة التحكم تقرأ جلسات بوابتها مباشرة من `~/.hermes/state.db`. - **`policies-config.json`** — يخبر failproofai بالسياسات التي يجب تقييمها وبأية معاملات (مشاركة عبر جميع عملاء الوكيل) -مرر `--cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes` لاستهداف وكيل محدد (مفصول بمسافات أو متكرر لأي مجموعة فرعية): +مرر `--cli claude|codex|copilot|cursor|opencode|pi|hermes` لاستهداف وكيل محدد (مفصول بمسافات أو متكرر لأي مجموعة فرعية): ```bash failproofai policies --install --cli codex --scope project @@ -211,12 +210,11 @@ failproofai policies --install --cli copilot --scope project failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project -failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user -failproofai policies --install --cli claude codex copilot cursor opencode pi gemini +failproofai policies --install --cli claude codex copilot cursor opencode pi ``` -عندما يتم حذف `--cli`، يكتشف failproofai عملاء الوكيل المثبتة (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which gemini` / `which hermes`): +عندما يتم حذف `--cli`، يكتشف failproofai عملاء الوكيل المثبتة (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which hermes`): - **تم اكتشاف عميل واحد** — يختار ذلك العميل تلقائياً بدون فور. - **عملاء متعددة مكتشفة** في محطة طرفية تفاعلية — يعرض موجه اختيار مفرد بمفاتيح الأسهم مجمع في قسم `Detected (N)` (مع صف إجمالي للتثبيت لكل عميل مكتشفة N + كل عميل مكتشفة بشكل فردي) وقسم `Not installed (M) · install hooks ahead of time` يسرد كل عميل مكتشفة مدعومة كخيار تثبيت آجل (↑↓ للتحريك، أدخل للاختيار، ^C للخروج). يعرض تدفق الإلغاء قسم Detected فقط. diff --git a/docs/ar/dashboard.mdx b/docs/ar/dashboard.mdx index fc2633a9..3aa42b0a 100644 --- a/docs/ar/dashboard.mdx +++ b/docs/ar/dashboard.mdx @@ -24,11 +24,11 @@ failproofai ### المشاريع -يسرد جميع مشاريع Claude Code و OpenAI Codex و GitHub Copilot CLI _(إصدار تجريبي)_ و Cursor Agent _(إصدار تجريبي)_ و OpenCode _(إصدار تجريبي)_ و Pi _(إصدار تجريبي)_ و Gemini CLI _(إصدار تجريبي)_ و Hermes الموجودة على جهازك. يتم اكتشاف مشاريع Claude من `~/.claude/projects/` (أو المسار المحدد بواسطة `CLAUDE_PROJECTS_PATH`); يتم اكتشاف مشاريع Codex بمسح كل نص تحت `~/.codex/sessions///
/*.jsonl` وتجميعها حسب `cwd` المسجل في السجل الأول من كل جلسة; يتم اكتشاف مشاريع Copilot CLI بمسح كل `~/.copilot/session-state//workspace.yaml` (قابل للتكوين عبر `COPILOT_HOME`) وتجميعها حسب حقل `cwd` الخاص به; يتم اكتشاف مشاريع Cursor Agent بمسح البيانات الفوقية لكل جلسة تحت `~/.cursor/agent-sessions//` (قابل للتكوين عبر `CURSOR_HOME`، مع اختبار `conversations/` و `sessions/` كبدائل) للبحث عن `cwd` في `meta.json` / `session.json` / `workspace.yaml`; يتم اكتشاف مشاريع OpenCode بالاستعلام عن قاعدة بيانات SQLite الخاصة به في `~/.local/share/opencode/opencode.db` عبر `opencode db --format json` (نحن نقرأ الجداول `session` و `project` ونجمعها حسب `project_id`); يتم اكتشاف مشاريع Pi بمسح نصوص JSONL لكل جلسة تحت `~/.pi/agent/sessions//_.jsonl` (قابل للتكوين عبر `PI_SESSIONS_DIR`) واستخراج `cwd` من السجل الأول من كل جلسة; يتم اكتشاف مشاريع Gemini CLI بمسح `~/.gemini/tmp//chats/session--.jsonl` (قابل للتكوين عبر `GEMINI_SESSIONS_DIR`) واسترجاع cwd القانوني من علامة النص المجاورة `.project_root`; تتم قراءة جلسات بوابة Hermes مباشرة من متجرها SQLite في `~/.hermes/state.db` (قابل للتكوين عبر `HERMES_DB_PATH`) وتجميعها في مشاريع `hermes-` حسب `source` (Slack/Telegram/cli/cron — جلسات البوابة لا تحتوي على cwd). يتم عرض المشروع الذي تم استخدامه بواسطة عدة CLIs كصف واحد مع جميع الشارات المطابقة. استخدم القائمة المنسدلة **CLI** فوق الجدول للتصفية حسب CLI وكيل معين; يحافظ عنوان URL على اختيارك كـ `?cli=claude|codex|copilot|cursor|opencode|pi|gemini|hermes`. +يسرد جميع مشاريع Claude Code و OpenAI Codex و GitHub Copilot CLI _(إصدار تجريبي)_ و Cursor Agent _(إصدار تجريبي)_ و OpenCode _(إصدار تجريبي)_ و Pi _(إصدار تجريبي)_ و Hermes الموجودة على جهازك. يتم اكتشاف مشاريع Claude من `~/.claude/projects/` (أو المسار المحدد بواسطة `CLAUDE_PROJECTS_PATH`); يتم اكتشاف مشاريع Codex بمسح كل نص تحت `~/.codex/sessions///
/*.jsonl` وتجميعها حسب `cwd` المسجل في السجل الأول من كل جلسة; يتم اكتشاف مشاريع Copilot CLI بمسح كل `~/.copilot/session-state//workspace.yaml` (قابل للتكوين عبر `COPILOT_HOME`) وتجميعها حسب حقل `cwd` الخاص به; يتم اكتشاف مشاريع Cursor Agent بمسح البيانات الفوقية لكل جلسة تحت `~/.cursor/agent-sessions//` (قابل للتكوين عبر `CURSOR_HOME`، مع اختبار `conversations/` و `sessions/` كبدائل) للبحث عن `cwd` في `meta.json` / `session.json` / `workspace.yaml`; يتم اكتشاف مشاريع OpenCode بالاستعلام عن قاعدة بيانات SQLite الخاصة به في `~/.local/share/opencode/opencode.db` عبر `opencode db --format json` (نحن نقرأ الجداول `session` و `project` ونجمعها حسب `project_id`); يتم اكتشاف مشاريع Pi بمسح نصوص JSONL لكل جلسة تحت `~/.pi/agent/sessions//_.jsonl` (قابل للتكوين عبر `PI_SESSIONS_DIR`) واستخراج `cwd` من السجل الأول من كل جلسة; تتم قراءة جلسات بوابة Hermes مباشرة من متجرها SQLite في `~/.hermes/state.db` (قابل للتكوين عبر `HERMES_DB_PATH`) وتجميعها في مشاريع `hermes-` حسب `source` (Slack/Telegram/cli/cron — جلسات البوابة لا تحتوي على cwd). يتم عرض المشروع الذي تم استخدامه بواسطة عدة CLIs كصف واحد مع جميع الشارات المطابقة. استخدم القائمة المنسدلة **CLI** فوق الجدول للتصفية حسب CLI وكيل معين; يحافظ عنوان URL على اختيارك كـ `?cli=claude|codex|copilot|cursor|opencode|pi|hermes`. يعرض كل مشروع: - اسم المشروع (مشتق من مسار المجلد) -- شارة CLI — `Claude Code` (برتقالي)، `OpenAI Codex` (بنفسجي)، `GitHub Copilot` (أزرق)، `Cursor Agent` (زمردي)، `OpenCode` (كهرماني)، `Pi` (وردي)، `Gemini CLI` (سماوي)، و/أو `Hermes` (نيلي) +- شارة CLI — `Claude Code` (برتقالي)، `OpenAI Codex` (بنفسجي)، `GitHub Copilot` (أزرق)، `Cursor Agent` (زمردي)، `OpenCode` (كهرماني)، `Pi` (وردي)، و/أو `Hermes` (نيلي) - تاريخ آخر نشاط جلسة انقر على مشروع لمشاهدة جلساته. @@ -47,7 +47,7 @@ failproofai ### عارض الجلسة -يجيب عارض الجلسة على السؤال الأساسي للوكلاء المستقلين: ماذا فعل الوكيل، وهل بقي على المسار الصحيح؟ تشير شارة CLI بجانب الرأس إلى ما إذا كانت الجلسة نسخة Claude Code أو OpenAI Codex أو GitHub Copilot CLI أو Cursor Agent أو OpenCode أو Pi أو Gemini CLI أو Hermes. يعرض جدول زمني لكل ما حدث في جلسة: +يجيب عارض الجلسة على السؤال الأساسي للوكلاء المستقلين: ماذا فعل الوكيل، وهل بقي على المسار الصحيح؟ تشير شارة CLI بجانب الرأس إلى ما إذا كانت الجلسة نسخة Claude Code أو OpenAI Codex أو GitHub Copilot CLI أو Cursor Agent أو OpenCode أو Pi أو Hermes. يعرض جدول زمني لكل ما حدث في جلسة: - **الرسائل** - استجابات نصية من Claude ومحفزات المستخدم - **استدعاءات الأدوات** - كل أداة استدعاها Claude، مع إدخالها وإخراجها @@ -55,7 +55,7 @@ failproofai تعرض شريط الإحصائيات في الأعلى مدة الجلسة وإجمالي استدعاءات الأدوات وملخص قرارات الخطاف (عدد السماح/الرفض/التعليمات). -انقر على زر **تحميل السجلات** لتصدير الجلسة. بالنسبة لجلسات Claude Code و Codex و Copilot و Cursor و Pi و Gemini، تحصل على نص JSONL الأصلي على القرص بالكامل; بالنسبة لـ OpenCode (التي تعيش جلساتها في SQLite وليس على القرص) تحصل على وثيقة JSON تعكس جداول `session` / `messages` / `parts` الأساسية. +انقر على زر **تحميل السجلات** لتصدير الجلسة. بالنسبة لجلسات Claude Code و Codex و Copilot و Cursor و Pi، تحصل على نص JSONL الأصلي على القرص بالكامل; بالنسبة لـ OpenCode (التي تعيش جلساتها في SQLite وليس على القرص) تحصل على وثيقة JSON تعكس جداول `session` / `messages` / `parts` الأساسية. ### التدقيق @@ -75,16 +75,16 @@ failproofai - - حدد بطاقة متعددة الاختيار CLI الذي يحمي failproofai من لوحة واحدة — Claude Code و OpenAI Codex و GitHub Copilot و Cursor Agent و OpenCode و Pi و Gemini CLI و Hermes لها جميعاً صف مع حالة التثبيت (`نشط` / `مكتشف` / `غير نشط`)، مسار إعدادات نطاق المستخدم، وتمييز ملون العلامة التجارية. قم بتحديد أو إلغاء تحديد CLIs التي تريد وانقر `تطبيق التغييرات` لتثبيت/إلغاء تثبيت الفرق في خطوة واحدة. CLIs التي تم اكتشاف ملفها الثنائي على PATH يتم تحديدها مسبقاً. + - حدد بطاقة متعددة الاختيار CLI الذي يحمي failproofai من لوحة واحدة — Claude Code و OpenAI Codex و GitHub Copilot و Cursor Agent و OpenCode و Pi و Hermes لها جميعاً صف مع حالة التثبيت (`نشط` / `مكتشف` / `غير نشط`)، مسار إعدادات نطاق المستخدم، وتمييز ملون العلامة التجارية. قم بتحديد أو إلغاء تحديد CLIs التي تريد وانقر `تطبيق التغييرات` لتثبيت/إلغاء تثبيت الفرق في خطوة واحدة. CLIs التي تم اكتشاف ملفها الثنائي على PATH يتم تحديدها مسبقاً. - بدّل السياسات الفردية على أو بيقاف مع نقرة واحدة (يكتب إلى `~/.failproofai/policies-config.json` — مشترك عبر كل CLI مثبت) - قم بتوسيع سياسة لتكوين معاملات (للسياسات التي تدعم `policyParams`) - عيّن مسار ملف سياسات مخصص - السجل الكامل المقسم على صفحات لكل حدث خطاف تم تشغيله عبر جميع الجلسات - - التصفية حسب القرار ونوع الحدث و CLI (Claude Code / OpenAI Codex / GitHub Copilot _(إصدار تجريبي)_ / Cursor Agent _(إصدار تجريبي)_ / OpenCode _(إصدار تجريبي)_ / Pi _(إصدار تجريبي)_ / Gemini CLI _(إصدار تجريبي)_ / Hermes) واسم السياسة أو معرف الجلسة - - يعرض كل صف: الطابع الزمني واسم السياسة والقرار وشارة CLI (برتقالي = Claude Code، بنفسجي = OpenAI Codex، أزرق = GitHub Copilot، زمردي = Cursor Agent، كهرماني = OpenCode، وردي = Pi، سماوي = Gemini CLI، نيلي = Hermes) واسم الأداة ومعرف الجلسة وسبب قرارات الرفض/التعليمات - - انقر على معرف جلسة لفتح النص الخاص به — الكاشف التلقائي للعارض CLI الذي أطلق الخطاف (Claude `~/.claude/projects/…`، Codex `~/.codex/sessions/…`، Copilot CLI `~/.copilot/session-state//events.jsonl`، Cursor Agent `~/.cursor/agent-sessions//events.jsonl`، OpenCode `~/.local/share/opencode/opencode.db`، Pi `~/.pi/agent/sessions//.jsonl`، Gemini CLI `~/.gemini/tmp//chats/.jsonl`، Hermes `~/.hermes/state.db`) ويعرض شارة CLI المطابقة في الرأس + - التصفية حسب القرار ونوع الحدث و CLI (Claude Code / OpenAI Codex / GitHub Copilot _(إصدار تجريبي)_ / Cursor Agent _(إصدار تجريبي)_ / OpenCode _(إصدار تجريبي)_ / Pi _(إصدار تجريبي)_ / Hermes) واسم السياسة أو معرف الجلسة + - يعرض كل صف: الطابع الزمني واسم السياسة والقرار وشارة CLI (برتقالي = Claude Code، بنفسجي = OpenAI Codex، أزرق = GitHub Copilot، زمردي = Cursor Agent، كهرماني = OpenCode، وردي = Pi، نيلي = Hermes) واسم الأداة ومعرف الجلسة وسبب قرارات الرفض/التعليمات + - انقر على معرف جلسة لفتح النص الخاص به — الكاشف التلقائي للعارض CLI الذي أطلق الخطاف (Claude `~/.claude/projects/…`، Codex `~/.codex/sessions/…`، Copilot CLI `~/.copilot/session-state//events.jsonl`، Cursor Agent `~/.cursor/agent-sessions//events.jsonl`، OpenCode `~/.local/share/opencode/opencode.db`، Pi `~/.pi/agent/sessions//.jsonl`، Hermes `~/.hermes/state.db`) ويعرض شارة CLI المطابقة في الرأس diff --git a/docs/ar/getting-started.mdx b/docs/ar/getting-started.mdx index 350cf141..b917f144 100644 --- a/docs/ar/getting-started.mdx +++ b/docs/ar/getting-started.mdx @@ -38,9 +38,9 @@ bun add -g failproofai failproofai policies --install ``` - يكتب هذا إدخالات hook في أدوات الوكيل المثبتة لديك (ملف Claude Code `~/.claude/settings.json`، ملف OpenAI Codex `~/.codex/hooks.json`، ملف GitHub Copilot CLI `~/.copilot/hooks/failproofai.json`، ملف Cursor Agent `~/.cursor/hooks.json`، مكون OpenCode المولد في `~/.config/opencode/plugins/failproofai.mjs` بالإضافة إلى إدخال التسجيل في مصفوفة `plugin` في `~/.config/opencode/opencode.json`، ملف Pi `~/.pi/agent/settings.json`، ملف Gemini CLI `~/.gemini/settings.json`، أو ملف Hermes `~/.hermes/config.yaml`). عند وجود أكثر من واحد ستُطلب منك المتابعة؛ مرر `--cli claude codex copilot cursor opencode pi gemini hermes` (أي مجموعة فرعية) لتخطي الطلب. + يكتب هذا إدخالات hook في أدوات الوكيل المثبتة لديك (ملف Claude Code `~/.claude/settings.json`، ملف OpenAI Codex `~/.codex/hooks.json`، ملف GitHub Copilot CLI `~/.copilot/hooks/failproofai.json`، ملف Cursor Agent `~/.cursor/hooks.json`، مكون OpenCode المولد في `~/.config/opencode/plugins/failproofai.mjs` بالإضافة إلى إدخال التسجيل في مصفوفة `plugin` في `~/.config/opencode/opencode.json`، ملف Pi `~/.pi/agent/settings.json`، أو ملف Hermes `~/.hermes/config.yaml`). عند وجود أكثر من واحد ستُطلب منك المتابعة؛ مرر `--cli claude codex copilot cursor opencode pi hermes` (أي مجموعة فرعية) لتخطي الطلب. - دعم GitHub Copilot CLI و Cursor Agent و OpenCode و Pi و Gemini CLI موجود في مرحلة **تجريبية** — ثبّت باستخدام `--cli copilot` أو `--cli cursor` أو `--cli opencode` أو `--cli pi` أو `--cli gemini`. يثبّت Hermes (hermes-agent، بوابة Slack/Telegram) بنطاق المستخدم باستخدام `--cli hermes` وهو أيضًا **مصدر تدقيق** دون اتصال. + دعم GitHub Copilot CLI و Cursor Agent و OpenCode و Pi موجود في مرحلة **تجريبية** — ثبّت باستخدام `--cli copilot` أو `--cli cursor` أو `--cli opencode` أو `--cli pi`. يثبّت Hermes (hermes-agent، بوابة Slack/Telegram) بنطاق المستخدم باستخدام `--cli hermes` وهو أيضًا **مصدر تدقيق** دون اتصال. ```bash failproofai policies --install --scope project @@ -49,7 +49,6 @@ bun add -g failproofai failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project - failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user failproofai policies --install block-sudo block-rm-rf sanitize-api-keys ``` diff --git a/docs/ar/introduction.mdx b/docs/ar/introduction.mdx index 2475e146..f93cccce 100644 --- a/docs/ar/introduction.mdx +++ b/docs/ar/introduction.mdx @@ -5,7 +5,7 @@ description: "FailproofAI يوفر للوكلاء الذكيين 39 سياسة [![npm weekly downloads](https://img.shields.io/npm/dw/failproofai?style=flat-square&color=2ea44f)](https://www.npmjs.com/package/failproofai) -الخطافات والسياسات لـ **معالجة أعطال الذكاء الاصطناعي** و **استرجاع الأخطاء** و **موثوقية نماذج اللغة الكبيرة**. حافظ على موثوقية وكلائك الذكيين والعمل بشكل مستقل عبر **Claude Code** و **OpenAI Codex** و **GitHub Copilot** و **Cursor Agent** و **OpenCode** و **Pi** و **Gemini CLI** و **Hermes** و **Agents SDK**. +الخطافات والسياسات لـ **معالجة أعطال الذكاء الاصطناعي** و **استرجاع الأخطاء** و **موثوقية نماذج اللغة الكبيرة**. حافظ على موثوقية وكلائك الذكيين والعمل بشكل مستقل عبر **Claude Code** و **OpenAI Codex** و **GitHub Copilot** و **Cursor Agent** و **OpenCode** و **Pi** و **Hermes** و **Agents SDK**. الوكلاء الذكيون يفشلون بطرق يمكن التنبؤ بها. يقومون بتنفيذ أوامر مدمرة، تسرب الأسرار، الانجراف عن المهمة الأساسية، الوقوع في حلقات، أو الدفع مباشرة إلى الفرع الرئيسي. إذا تركت دون مراقبة، فإن الأعطال الصغيرة قد تؤدي إلى انقطاعات، تسرب بيانات الاعتماد، وفقدان العمل. diff --git a/docs/built-in-policies.mdx b/docs/built-in-policies.mdx index 55093ffe..95fe24f5 100644 --- a/docs/built-in-policies.mdx +++ b/docs/built-in-policies.mdx @@ -710,7 +710,7 @@ All workflow policies are **fail-open**: if the required tool is not available ( ### Per-CLI Stop semantics -Stop enforcement looks slightly different across the seven supported CLIs because each one exposes a different "agent finished" hook contract. The **outcome** is the same — the agent doesn't get away with stopping while a workflow gate is failing — but the **mechanics** differ. The table below summarizes; only Pi has a user-visible quirk worth understanding before you enable a `require-*-before-stop` policy. +Stop enforcement looks slightly different across the six supported CLIs because each one exposes a different "agent finished" hook contract. The **outcome** is the same — the agent doesn't get away with stopping while a workflow gate is failing — but the **mechanics** differ. The table below summarizes; only Pi has a user-visible quirk worth understanding before you enable a `require-*-before-stop` policy. | CLI | When the gate fires | What you see | |---|---|---| @@ -718,20 +718,19 @@ Stop enforcement looks slightly different across the seven supported CLIs becaus | Codex | Same agent loop, immediately | Same as Claude. | | GitHub Copilot CLI | Same agent loop, immediately | Same as Claude (uses Copilot's `{decision:"block", reason}` retry channel — verified empirically against Copilot CLI 1.0.41). | | Cursor Agent | Same agent loop, immediately | Same as Claude (uses Cursor's `{followup_message}` channel — capped at `loop_limit`, default 5 retries). | -| Gemini CLI | Same agent loop, immediately | Same as Claude (uses Gemini's `{decision:"block", reason}` channel on `AfterAgent`). | | OpenCode | Same agent loop, immediately | Same as Claude (uses OpenCode's `client.session.prompt(...)` SDK call routed through `hookSpecificOutput.additionalContext`). | | **Pi (pi-coding-agent)** | **Next user turn** | **Pi visibly stops** when the gate fires — its agent loop exits and you're returned to the prompt. The gate then fires the next time you submit a prompt: failproofai prepends a `MANDATORY ACTION REQUIRED` directive to that turn's system prompt, instructing the LLM to complete the workflow step (commit, push, etc.) before doing whatever you asked. | -**Pi limitation.** Pi's `AgentEndEvent` (the upstream equivalent of Claude's `Stop` hook) has no Result type — by the time it fires, Pi's agent loop has already exited. Pi cannot be forced to retry the same loop the way Claude / Copilot / Cursor / Gemini / OpenCode can. failproofai shifts the gate to Pi's `before_agent_start` event (which fires after the next user prompt) so the workflow check still enforces, just on the next turn rather than the current one. +**Pi limitation.** Pi's `AgentEndEvent` (the upstream equivalent of Claude's `Stop` hook) has no Result type — by the time it fires, Pi's agent loop has already exited. Pi cannot be forced to retry the same loop the way Claude / Copilot / Cursor / OpenCode can. failproofai shifts the gate to Pi's `before_agent_start` event (which fires after the next user prompt) so the workflow check still enforces, just on the next turn rather than the current one. **What this means in practice:** - After Pi stops, the deny reason is captured in-memory keyed by Pi session id. The very next prompt you submit in the same Pi process drains it: the LLM sees the `MANDATORY ACTION REQUIRED` directive at the top of its system prompt, commits (or pushes / opens the PR / waits for CI), and only then continues with your request. The captured deny reason is one-shot — once drained, the gate is clear. -- The gate is bounded by Pi's process lifetime. If you `Ctrl+C` Pi or quit between turns, the in-memory entry is dropped along with the process and the gate is missed. Claude, Copilot, Cursor, Gemini, and OpenCode have the same bound (kill the agent and the gate is missed) — Pi just makes it more visible because the agent visibly exits before the gate fires. +- The gate is bounded by Pi's process lifetime. If you `Ctrl+C` Pi or quit between turns, the in-memory entry is dropped along with the process and the gate is missed. Claude, Copilot, Cursor, and OpenCode have the same bound (kill the agent and the gate is missed) — Pi just makes it more visible because the agent visibly exits before the gate fires. - A pending deny is also cleared on `session_shutdown` for any reason (`new` / `resume` / `fork` / `quit`), so a stale gate from a prior session cannot leak into a fresh session started in the same Pi process. -If you need Claude-style same-loop retry, run your `Stop` policies under any of the other six supported CLIs. We are tracking Pi upstream for a future Result type on `AgentEndEvent` that would let us close this gap. +If you need Claude-style same-loop retry, run your `Stop` policies under any of the other five supported CLIs. We are tracking Pi upstream for a future Result type on `AgentEndEvent` that would let us close this gap. ### `require-commit-before-stop` diff --git a/docs/cli/audit.mdx b/docs/cli/audit.mdx index 6972cbcc..cc059e45 100644 --- a/docs/cli/audit.mdx +++ b/docs/cli/audit.mdx @@ -55,7 +55,7 @@ failproofai until you stop it with `Ctrl+C`. -The dashboard scans past agent CLI transcripts on this machine (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi, Gemini) and reports how often the agent did things failproofai is built to stop — env-var checks, force pushes, redundant `cd ` prefixes, sleep-polling loops, re-reading files just edited, and more. +The dashboard scans past agent CLI transcripts on this machine (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi) and reports how often the agent did things failproofai is built to stop — env-var checks, force pushes, redundant `cd ` prefixes, sleep-polling loops, re-reading files just edited, and more. For each transcript, every tool-use event is replayed through the 39 builtin policies **and** through 8 audit-only detectors that catch patterns not yet covered by runtime policies. Counts are aggregated per policy / detector across all sessions. diff --git a/docs/configuration.mdx b/docs/configuration.mdx index b1568711..795dc884 100644 --- a/docs/configuration.mdx +++ b/docs/configuration.mdx @@ -194,15 +194,19 @@ The `policies --install` and `policies --uninstall` commands write to your agent - **Agent CLI settings** — tells the agent to call `failproofai --hook ` on each tool use: - **Claude Code**: `~/.claude/settings.json` (user), `/.claude/settings.json` (project), `/.claude/settings.local.json` (local) - **OpenAI Codex**: `~/.codex/hooks.json` (user), `/.codex/hooks.json` (project) — Codex doesn't have a `local` scope - - **GitHub Copilot CLI _(beta)_**: `~/.copilot/hooks/failproofai.json` (user), `/.github/hooks/failproofai.json` (project) — Copilot has no `local` scope. Hook entries use Copilot's OS-keyed `bash`/`powershell` command fields with `timeoutSec`; the file carries a top-level `version: 1` marker. Copilot CLI support is **beta** while we verify the `events.jsonl` record schema (which the public docs do not specify) against more real-world sessions. + - **GitHub Copilot CLI _(beta)_**: `~/.copilot/hooks/failproofai.json` (user), `/.github/hooks/failproofai.json` (project) — Copilot has no `local` scope. Hook entries use Copilot's OS-keyed `bash`/`powershell` command fields with `timeoutSec`; the file carries a top-level `version: 1` marker. Copilot CLI support is **beta** while we verify the `events.jsonl` record schema (which the public docs do not specify) against more real-world sessions. **VS Code Copilot Chat agent mode (Preview)** reads hook configs from `.github/hooks/*.json`, `~/.copilot/hooks/*.json`, and `~/.claude/settings.json` (governed by the `chat.hookFilesLocations` setting) using the same Claude-shaped `{hookSpecificOutput:{permissionDecision:"deny",…}}` contract — the exact paths this `copilot` integration and the `claude` integration (`~/.claude/settings.json`) already write, so `failproofai policies --install --cli copilot` (or `--cli claude`) **already enforces in VS Code agent mode** with no separate `vscode` integration needed (confirmed live from VS Code's discovery logs). - **Cursor Agent _(beta)_**: `~/.cursor/hooks.json` (user), `/.cursor/hooks.json` (project) — Cursor has no `local` scope. Hook entries use the Claude-shaped `{type, command, timeout}` form (no `bash`/`powershell` split), but stored under camelCase event keys (`preToolUse`, `beforeSubmitPrompt`, …) in a flat array per Cursor's [hooks schema](https://cursor.com/docs/hooks); the file carries a top-level `version: 1` marker. The handler canonicalizes camelCase → PascalCase via `CURSOR_EVENT_MAP` so existing built-in policies fire unchanged. Cursor Agent support is **beta** while we verify Cursor's transcript on-disk format (not specified in the public docs) against more real-world installs. - - **OpenCode _(beta)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (user), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (project) — OpenCode has no `local` scope. Unlike the other six CLIs, OpenCode has **no external-command hook system**: it loads in-process JS/TS plugins explicitly registered via the `plugin: []` array in `opencode.json` (auto-discovery from `.opencode/plugins/` is **not** how plugins load on opencode v1.14.33). Install drops a small generated plugin shim that subprocess-calls the failproofai binary and translates the binary's Claude-shape JSON response back into plugin semantics: `throw new Error()` for tool-event deny (cancels the tool call), `client.session.prompt(...)` for instruct AND for `Stop` / `SubagentStop` deny (submits the deny reason as the next user message — the only force-retry channel since `session.idle` is notification-only and throwing from it is a no-op), and no-op for allow. The shim canonicalizes both tool names (lowercase → PascalCase via `OPENCODE_TOOL_MAP`) and tool-input arg keys (camelCase → snake_case via `OPENCODE_TOOL_INPUT_MAP` for `Read` / `Write` / `Edit`, e.g. `filePath` → `file_path`, `oldString` → `old_string`) before forwarding to the binary, so path-checking builtins like `block-read-outside-cwd`, `block-env-files`, and `block-secrets-write` fire unchanged on OpenCode tool calls. Sessions live in opencode's SQLite DB at `~/.local/share/opencode/opencode.db`; the dashboard's session viewer reads them via `opencode db --format json` and `opencode export `. OpenCode support is **beta** while we verify behavior across versions and against more real-world sessions. See the [OpenCode plugins docs](https://opencode.ai/docs/plugins/). + - **OpenCode _(beta)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (user), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (project) — OpenCode has no `local` scope. Unlike the other five CLIs, OpenCode has **no external-command hook system**: it loads in-process JS/TS plugins explicitly registered via the `plugin: []` array in `opencode.json` (auto-discovery from `.opencode/plugins/` is **not** how plugins load on opencode v1.14.33). Install drops a small generated plugin shim that subprocess-calls the failproofai binary and translates the binary's Claude-shape JSON response back into plugin semantics: `throw new Error()` for tool-event deny (cancels the tool call), `client.session.prompt(...)` for instruct AND for `Stop` / `SubagentStop` deny (submits the deny reason as the next user message — the only force-retry channel since `session.idle` is notification-only and throwing from it is a no-op), and no-op for allow. The shim canonicalizes both tool names (lowercase → PascalCase via `OPENCODE_TOOL_MAP`) and tool-input arg keys (camelCase → snake_case via `OPENCODE_TOOL_INPUT_MAP` for `Read` / `Write` / `Edit`, e.g. `filePath` → `file_path`, `oldString` → `old_string`) before forwarding to the binary, so path-checking builtins like `block-read-outside-cwd`, `block-env-files`, and `block-secrets-write` fire unchanged on OpenCode tool calls. Sessions live in opencode's SQLite DB at `~/.local/share/opencode/opencode.db`; the dashboard's session viewer reads them via `opencode db --format json` and `opencode export `. OpenCode support is **beta** while we verify behavior across versions and against more real-world sessions. See the [OpenCode plugins docs](https://opencode.ai/docs/plugins/). - **Pi _(beta)_**: `~/.pi/agent/settings.json` (user), `/.pi/settings.json` (project) — Pi has no `local` scope. Pi loads TypeScript extension packages at startup; the settings file is a flat string array `{"packages": ["./relative/path", …]}`. failproofai writes a single packages-array entry pointing at its bundled `pi-extension/` directory. The extension internally subscribes to Pi's `tool_call` / `user_bash` / `input` / `session_start` events and shells out to `failproofai --hook --cli pi`; the handler canonicalizes underscore_lower_snake_case → PascalCase via `PI_EVENT_MAP` so existing built-in policies fire unchanged. Tool input args are also canonicalized via `PI_TOOL_INPUT_MAP` (Pi's Read / Write / Edit deliver `path` rather than `file_path`; mapping the top-level key lets `block-env-files` and `block-secrets-write` fire — `block-read-outside-cwd` already had a `path` fallback). Pi support is **beta** while Pi's extension API and session-log layout stabilize. - - **Gemini CLI _(beta)_**: `~/.gemini/settings.json` (user), `/.gemini/settings.json` (project) — Gemini has no `local` scope (it documents a `system` scope at `/etc/gemini-cli/settings.json` which failproofai does not expose). Hook entries use Claude's `{type, command, timeout}` form wrapped in Gemini's `{matcher, hooks: [...]}` matcher schema with `matcher: "*"` by default. Events are PascalCase (`SessionStart`, `BeforeAgent`, `AfterAgent`, `BeforeModel`, `AfterModel`, `BeforeToolSelection`, `BeforeTool`, `AfterTool`, `PreCompress`, `Notification`, `SessionEnd`); the handler maps to Claude canonical names via `GEMINI_EVENT_MAP`. Tool names are snake_case (`run_shell_command`, `read_file`, `write_file`, `replace`, …) — the handler canonicalizes via `GEMINI_TOOL_MAP` so existing built-in policies fire unchanged. The policy evaluator emits Gemini's flat `{decision: "deny", reason}` shape (preferred per Gemini's "Golden Rule" exit-0 contract), `{hookSpecificOutput: {hookEventName, additionalContext}}` for context injection on BeforeAgent / AfterTool / SessionStart, and `{decision: "block", reason}` on AfterAgent for force-retry semantics. Gemini CLI support is **beta** while we widen real-world coverage. See the [Gemini CLI hooks docs](https://geminicli.com/docs/hooks/). - **Hermes (hermes-agent)**: `~/.hermes/config.yaml` (**user scope only** — Hermes has no project/local config). Hermes is a Slack/Telegram **gateway**, so one install intercepts tool calls from every platform (Slack/Telegram/cli/cron) **and** internal subagents. Hook entries are a `{command, timeout}` pair (timeout in **seconds**) under a `hooks:` map keyed by Hermes's snake_case events (`pre_tool_call` / `post_tool_call` / `on_session_start` / `on_session_end` / `subagent_stop`); the handler canonicalizes events via `HERMES_EVENT_MAP` and tool names via `HERMES_TOOL_MAP` so built-in policies fire unchanged. The config is edited through a comment-preserving YAML `Document` round-trip so the operator's other settings survive, and install sets `hooks_auto_accept: true` so the headless gateway (no TTY) runs the hooks without a consent prompt. The evaluator emits Hermes's `{"decision":"block","reason"}` stdout contract (Hermes ignores exit codes). **Limitations:** Hermes has no turn-end `Stop` event, so the `require-*-before-stop` builtins never fire for it (inapplicable, not broken); `instruct` degrades to allow-with-logged-note (no additional-context channel); and output-secret redaction (`sanitize-*`) can't rewrite tool output over the shell-hook contract. Hermes is **also** an offline **audit** source — the dashboard reads its gateway sessions directly from `~/.hermes/state.db`. + - **OpenClaw (openclaw gateway)**: `~/.openclaw/openclaw.json` (**user scope only** — OpenClaw has no project/local config). Like Hermes, OpenClaw is a self-hosted multi-channel **gateway**, so one install intercepts tool calls from every channel and its internal subagents. Enforcement runs through OpenClaw's **in-process plugin hooks** (its file-based internal hooks are observation-only and cannot block), so — like OpenCode/Pi — failproofai ships a static `openclaw-plugin/` package that async-spawns the failproofai binary and translates the verdict. Install registers the shipped plugin dir in `openclaw.json`'s `plugins.load.paths[]` and enables it under `plugins.entries.failproofai` (with `hooks.allowConversationAccess: true`, required for the raw-conversation hooks). The evaluator emits a flat `{permission, reason}` verdict and the shim maps it to each hook's native return shape: `before_tool_call → {block:true, blockReason}` (**PreToolUse**), `before_agent_run → {outcome:"block", reason}` (**UserPromptSubmit**), and `before_agent_finalize → {action:"revise", reason}` (**Stop** — a real turn-end gate, so the `require-*-before-stop` builtins **enforce** on OpenClaw, unlike Hermes). Events and tool names canonicalize binary-side via `OPENCLAW_EVENT_MAP` / `OPENCLAW_TOOL_MAP` (`exec→Bash`, `read→Read`, …) so built-in policies fire unchanged; the shim fails open on any spawn/parse/timeout error. OpenClaw is **also** an offline **audit** source — the dashboard reads its JSONL sessions at `~/.openclaw/agents//sessions/.jsonl`. + - **Factory Droid (`droid`)**: `~/.factory/hooks.json` (user), `/.factory/hooks.json` (project) — Factory has no `local` scope. droid ships a Claude-style external-command hook system, but with two quirks verified live against droid v0.171.0: (1) event names live at the **top level** of `hooks.json` — there is **no `"hooks"` wrapper** (droid rejects one); tool events (`PreToolUse`/`PostToolUse`) carry `"matcher": "*"`, non-tool events omit it. (2) Deny is driven by hook **exit code 2 + stderr**, not a JSON decision — the evaluator's `factory` branch returns exit 2 for tool/prompt events and `{decision:"block", reason}` only on the turn-end `Stop` event (droid's sole force-retry channel). Events are already PascalCase (no event map) and the payload is Claude snake_case; only tool names are canonicalized via `FACTORY_TOOL_MAP` (`Execute→Bash`, `Create→Write`, `FetchUrl→WebFetch`, …). Factory is **also** an offline **audit** source — the dashboard reads its on-disk JSONL sessions at `~/.factory/sessions//.jsonl`. + - **Devin CLI (`devin`, Cognition)**: `~/.config/devin/config.json` (user), `/.devin/config.json` (project) — Devin has no `local` scope. Devin is a **pure Claude-clone** verified live against devin v3000.1.27: it uses the standard Claude `"hooks"`-wrapper schema (writes are merge-preserving so the config file's other keys — `org_id`, `theme_mode`, … — survive), already-PascalCase event names (no event map, no handler branch), and a Claude snake_case stdin payload (no normalization). The evaluator's `devin` branch denies with `{"decision":"block","reason"}` JSON on stdout at exit 0 for **every** event (verified — the block overrode `--permission-mode dangerous`); on the turn-end `Stop` event the reason carries the MANDATORY-ACTION force-retry wording so the `require-*-before-stop` builtins enforce. Only tool names are canonicalized via `DEVIN_TOOL_MAP` (`exec→Bash`; `tool_input.command` is already canonical). Devin is **also** an offline **audit** source — the dashboard reads its SQLite sessions at `~/.local/share/devin/cli/sessions.db` (each `sessions` row carries a real `working_directory`, so sessions group by project cwd like Claude). + - **Antigravity CLI (`agy`)**: `~/.gemini/config/hooks.json` (user), `/.agents/hooks.json` (project) — Antigravity has no `local` scope. Unlike Factory/Devin, Antigravity has its **own** contract (not a Claude-clone), verified live against agy v1.1.2. `hooks.json` uses a **named-hook** schema: the top-level key is a hook *name* (`"failproofai"`) whose value is an event→handlers map — tool events (`PreToolUse`/`PostToolUse`) wrap handlers in `{matcher:"*", hooks:[…]}`, while `PreInvocation`/`Stop` are **flat** handler arrays (other named hooks are preserved). The stdin payload is **camelCase protojson** (`toolCall:{name,args}`, `conversationId`, `workspacePaths`, `transcriptPath`) — failproofai normalizes it to snake_case before policies run, and maps `run_command`'s PascalCase args (`CommandLine`/`Cwd`) via `ANTIGRAVITY_TOOL_INPUT_MAP`. The evaluator's `antigravity` branch uses Antigravity's **own** response shapes: `{decision:"deny", reason}` blocks a tool/prompt (exit 0), `{decision:"continue", reason}` on the turn-end `Stop` re-enters the loop (so the `require-*-before-stop` builtins enforce), and `{injectSteps:[{ephemeralMessage}]}` injects an instruction on `PreInvocation` (→ `UserPromptSubmit`). Tool names canonicalize via `ANTIGRAVITY_TOOL_MAP` (`run_command→Bash`, `view_file→Read`, …). Antigravity is **also** an offline **audit** source — the dashboard reads its plain-JSONL transcripts at `~/.gemini/antigravity-cli/brain//.system_generated/logs/transcript_full.jsonl` (conversation index in `conversation_summaries.db`). + - **Goose (codename goose, Block)**: `~/.agents/plugins/failproofai/hooks/hooks.json` (user), `/.agents/plugins/failproofai/hooks/hooks.json` (project) — Goose has no `local` scope. Enforcement uses Goose's **hooks** system, the cross-agent **Open Plugins** spec: the installer just drops the `failproofai` plugin dir and Goose auto-discovers it at startup (self-registering it into `~/.config/goose/config.yaml`). The `hooks.json` uses an Open Plugins schema **with** a top-level `"hooks"` wrapper, and the matcher is **omitted** on every event — a bare `"*"` is an invalid regex that matches nothing (verified live against goose v1.43.0). Event names are already PascalCase (no event map); the stdin payload uses `event`/`working_dir`, which the handler normalizes to `hook_event_name`/`cwd`. The evaluator's `goose` branch denies with `{"decision":"block","reason"}` JSON on stdout at exit 0, honored on the **`PreToolUse`** event only (shipped in goose ≥ v1.37.0) — which fires for the shell tool **and inside delegated subagents**, so it is the single sufficient deny point; any other hook error fails **open**. Goose has **no `Stop` event**, so the `require-*-before-stop` builtins don't apply (as with Hermes). Tool names canonicalize via `GOOSE_TOOL_MAP` (`shell→Bash`, `write→Write`, `todo__todo_write→TodoWrite`, …) and path keys via `GOOSE_TOOL_INPUT_MAP` (`path`/`source` → `file_path`). Goose is **also** an offline **audit** source — the dashboard reads its SQLite sessions at `~/.local/share/goose/sessions/sessions.db` (each `sessions` row carries a real `working_dir`, so sessions group by project cwd like Devin; `--no-session` scratch runs are filtered). - **`policies-config.json`** — tells failproofai which policies to evaluate and with what params (shared across all agent CLIs) -Pass `--cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes` to target a specific agent (space-separated or repeated for any subset): +Pass `--cli claude|codex|copilot|cursor|opencode|pi|hermes|openclaw|factory|devin|antigravity|goose` to target a specific agent (space-separated or repeated for any subset): ```bash failproofai policies --install --cli codex --scope project @@ -210,12 +214,16 @@ failproofai policies --install --cli copilot --scope project failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project -failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user -failproofai policies --install --cli claude codex copilot cursor opencode pi gemini +failproofai policies --install --cli openclaw --scope user +failproofai policies --install --cli factory --scope project +failproofai policies --install --cli devin --scope project +failproofai policies --install --cli antigravity --scope project +failproofai policies --install --cli goose --scope project +failproofai policies --install --cli claude codex copilot cursor opencode pi hermes openclaw factory devin antigravity goose ``` -When `--cli` is omitted, `failproofai` detects which agent CLIs are installed (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which gemini` / `which hermes`): +When `--cli` is omitted, `failproofai` detects which agent CLIs are installed (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which hermes` / `which openclaw` / `which droid` / `which devin` / `which agy` / `which goose`): - **One CLI detected** — auto-selects that CLI without prompting. - **Multiple CLIs detected** in an interactive terminal — shows an arrow-key single-select prompt grouped into a `Detected (N)` section (with an `Install for all N detected` aggregate row + each detected CLI individually) and a `Not installed (M) · install hooks ahead of time` section listing every undetected supported CLI as a forward-install option (↑↓ to move, Enter to select, ^C to quit). The uninstall flow shows only the Detected section. diff --git a/docs/dashboard.mdx b/docs/dashboard.mdx index a528fb40..43ea85f2 100644 --- a/docs/dashboard.mdx +++ b/docs/dashboard.mdx @@ -16,7 +16,7 @@ failproofai Opens at `http://localhost:8020`. -The dashboard reads directly from the filesystem - your Claude Code project folders and the failproofai config files. Nothing is written to a remote service. +The dashboard reads local project, session, and failproofai configuration data directly from the filesystem. Optional authenticated features, such as audit reminders and invitations, send the information needed for those requests (including email addresses) to remote APIs. --- @@ -24,11 +24,11 @@ The dashboard reads directly from the filesystem - your Claude Code project fold ### Projects -Lists all Claude Code, OpenAI Codex, GitHub Copilot CLI _(beta)_, Cursor Agent _(beta)_, OpenCode _(beta)_, Pi _(beta)_, Gemini CLI _(beta)_, and Hermes projects found on your machine. Claude projects are discovered from `~/.claude/projects/` (or the path set by `CLAUDE_PROJECTS_PATH`); Codex projects are discovered by scanning every transcript under `~/.codex/sessions///
/*.jsonl` and grouping by the `cwd` recorded in each session's first record; Copilot CLI projects are discovered by scanning each `~/.copilot/session-state//workspace.yaml` (configurable via `COPILOT_HOME`) and grouping by its `cwd` field; Cursor Agent projects are discovered by scanning per-session metadata under `~/.cursor/agent-sessions//` (configurable via `CURSOR_HOME`, with `conversations/` and `sessions/` probed as fallbacks) for a `cwd` scalar in `meta.json` / `session.json` / `workspace.yaml`; OpenCode projects are discovered by querying its SQLite DB at `~/.local/share/opencode/opencode.db` via `opencode db --format json` (we read the `session` and `project` tables and group by `project_id`); Pi projects are discovered by scanning per-session JSONL transcripts under `~/.pi/agent/sessions//_.jsonl` (configurable via `PI_SESSIONS_DIR`) and pulling the `cwd` from each session's first record; Gemini CLI projects are discovered by scanning `~/.gemini/tmp//chats/session--.jsonl` (configurable via `GEMINI_SESSIONS_DIR`) and recovering the canonical cwd from the sibling `.project_root` text marker; Hermes gateway sessions are read directly from its SQLite store at `~/.hermes/state.db` (configurable via `HERMES_DB_PATH`) and grouped into `hermes-` projects by `source` (Slack/Telegram/cli/cron — gateway sessions have no cwd). A project that has been used by multiple CLIs renders as a single row with all matching badges. Use the **CLI** dropdown above the table to filter by a specific agent CLI; the URL preserves your selection as `?cli=claude|codex|copilot|cursor|opencode|pi|gemini|hermes`. +Lists all Claude Code, OpenAI Codex, GitHub Copilot CLI _(beta)_, Cursor Agent _(beta)_, OpenCode _(beta)_, Pi _(beta)_, Hermes, OpenClaw, Factory Droid, Devin, Antigravity, and Goose projects found on your machine. Claude projects are discovered from `~/.claude/projects/` (or the path set by `CLAUDE_PROJECTS_PATH`); Codex projects are discovered by scanning every transcript under `~/.codex/sessions///
/*.jsonl` and grouping by the `cwd` recorded in each session's first record; Copilot CLI projects are discovered by scanning each `~/.copilot/session-state//workspace.yaml` (configurable via `COPILOT_HOME`) and grouping by its `cwd` field; Cursor Agent projects are discovered by scanning per-session metadata under `~/.cursor/agent-sessions//` (configurable via `CURSOR_HOME`, with `conversations/` and `sessions/` probed as fallbacks) for a `cwd` scalar in `meta.json` / `session.json` / `workspace.yaml`; OpenCode projects are discovered by querying its SQLite DB at `~/.local/share/opencode/opencode.db` via `opencode db --format json` (we read the `session` and `project` tables and group by `project_id`); Pi projects are discovered by scanning per-session JSONL transcripts under `~/.pi/agent/sessions//_.jsonl` (configurable via `PI_SESSIONS_DIR`) and pulling the `cwd` from each session's first record; Hermes gateway sessions are read directly from its SQLite store at `~/.hermes/state.db` (configurable via `HERMES_DB_PATH`) and grouped into `hermes-` projects by `source` (Slack/Telegram/cli/cron — gateway sessions have no cwd); OpenClaw gateway sessions are read from `~/.openclaw/agents//sessions/*.jsonl` and grouped into `openclaw-` projects (also cwd-less); Factory Droid projects are discovered from the JSONL transcripts at `~/.factory/sessions//*.jsonl` and grouped by cwd; Devin projects from its SQLite DB at `~/.local/share/devin/cli/sessions.db` (grouped by each session's `working_directory`); Antigravity projects from the JSONL transcripts at `~/.gemini/antigravity-cli/brain//…/transcript_full.jsonl` and grouped by cwd; and Goose projects from its SQLite DB at `~/.local/share/goose/sessions/sessions.db` (grouped by each session's `working_dir`). A project that has been used by multiple CLIs renders as a single row with all matching badges. Use the **CLI** dropdown above the table to filter by a specific agent CLI; the URL preserves your selection as `?cli=claude|codex|copilot|cursor|opencode|pi|hermes|openclaw|factory|devin|antigravity|goose`. Each project shows: - Project name (derived from the folder path) -- A CLI badge — `Claude Code` (orange), `OpenAI Codex` (purple), `GitHub Copilot` (blue), `Cursor Agent` (emerald), `OpenCode` (amber), `Pi` (pink), `Gemini CLI` (sky), and/or `Hermes` (indigo) +- A CLI badge — `Claude Code` (orange), `OpenAI Codex` (purple), `GitHub Copilot` (blue), `Cursor Agent` (emerald), `OpenCode` (amber), `Pi` (pink), and/or `Hermes` (indigo) - Date of most recent session activity Click a project to see its sessions. @@ -47,7 +47,7 @@ Click a session to open the session viewer. ### Session viewer -The session viewer answers the key question for autonomous agents: what did the agent do, and did it stay on track? A CLI badge beside the header indicates whether the session is a Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi, Gemini CLI, or Hermes transcript. It shows a timeline of everything that happened in a session: +The session viewer answers the key question for autonomous agents: what did the agent do, and did it stay on track? A CLI badge beside the header indicates whether the session is a Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi, Hermes, OpenClaw, Factory Droid, Devin, Antigravity, or Goose transcript. It shows a timeline of everything that happened in a session: - **Messages** - Claude's text responses and user prompts - **Tool calls** - Every tool Claude invoked, with its input and output @@ -55,7 +55,7 @@ The session viewer answers the key question for autonomous agents: what did the The stats bar at the top shows session duration, total tool calls, and a summary of hook decisions (allow / deny / instruct counts). -Click the **Download Logs** button to export the session. For Claude Code, Codex, Copilot, Cursor, Pi, and Gemini sessions you get the original on-disk JSONL transcript byte-for-byte; for OpenCode (whose sessions live in SQLite, not on disk) you get a JSON document mirroring the underlying `session` / `messages` / `parts` tables. +Click the **Download Logs** button to export the session. For Claude Code, Codex, Copilot, Cursor, and Pi sessions you get the original on-disk JSONL transcript byte-for-byte; for OpenCode (whose sessions live in SQLite, not on disk) you get a JSON document mirroring the underlying `session` / `messages` / `parts` tables. ### Audit @@ -75,16 +75,16 @@ A two-tab page for managing policies and reviewing activity. - - Multi-select which agent CLIs failproofai protects from a single panel — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi, Gemini CLI, and Hermes all have a row with install status (`Active` / `Detected` / `Inactive`), the user-scope settings path, and a brand-colored accent. Check or uncheck the CLIs you want and click `Apply changes` to install/uninstall the diff in one step. CLIs whose binary is detected on PATH are pre-checked. + - Multi-select which agent CLIs failproofai protects from a single panel — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi, and Hermes all have a row with install status (`Active` / `Detected` / `Inactive`), the user-scope settings path, and a brand-colored accent. Check or uncheck the CLIs you want and click `Apply changes` to install/uninstall the diff in one step. CLIs whose binary is detected on PATH are pre-checked. - Toggle individual policies on or off with a single click (writes to `~/.failproofai/policies-config.json` — shared across every installed CLI) - Expand a policy to configure its parameters (for policies that support `policyParams`) - Set a custom policies file path - Full paginated history of every hook event that has fired across all sessions - - Filter by decision, event type, CLI (Claude Code / OpenAI Codex / GitHub Copilot _(beta)_ / Cursor Agent _(beta)_ / OpenCode _(beta)_ / Pi _(beta)_ / Gemini CLI _(beta)_ / Hermes), policy name, or session ID - - Each row shows: timestamp, policy name, decision, CLI badge (orange = Claude Code, purple = OpenAI Codex, blue = GitHub Copilot, emerald = Cursor Agent, amber = OpenCode, pink = Pi, sky = Gemini CLI, indigo = Hermes), tool name, session ID, and the reason for deny/instruct decisions - - Click a session ID to open its transcript — the viewer auto-detects which CLI fired the hook (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Gemini CLI `~/.gemini/tmp//chats/.jsonl`, Hermes `~/.hermes/state.db`) and renders the matching CLI badge in the header + - Filter by decision, event type, CLI (Claude Code / OpenAI Codex / GitHub Copilot _(beta)_ / Cursor Agent _(beta)_ / OpenCode _(beta)_ / Pi _(beta)_ / Hermes / OpenClaw / Factory Droid / Devin / Antigravity / Goose), policy name, or session ID + - Each row shows: timestamp, policy name, decision, CLI badge (orange = Claude Code, purple = OpenAI Codex, blue = GitHub Copilot, emerald = Cursor Agent, amber = OpenCode, pink = Pi, indigo = Hermes, teal = OpenClaw, rose = Factory Droid, violet = Devin, cyan = Antigravity, lime = Goose), tool name, session ID, and the reason for deny/instruct decisions + - Click a session ID to open its transcript — the viewer auto-detects which CLI fired the hook (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Hermes `~/.hermes/state.db`, OpenClaw `~/.openclaw/agents//sessions/*.jsonl`, Factory Droid `~/.factory/sessions//.jsonl`, Devin `~/.local/share/devin/cli/sessions.db`, Antigravity `~/.gemini/antigravity-cli/brain//…/transcript_full.jsonl`, Goose `~/.local/share/goose/sessions/sessions.db`) and renders the matching CLI badge in the header diff --git a/docs/de/built-in-policies.mdx b/docs/de/built-in-policies.mdx index c7bbeca4..cb5e240f 100644 --- a/docs/de/built-in-policies.mdx +++ b/docs/de/built-in-policies.mdx @@ -704,7 +704,7 @@ Alle Workflow-Richtlinien sind **fail-open**: Wenn das erforderliche Tool nicht ### Stop-Semantik je CLI -Die Stop-Durchsetzung sieht bei den sieben unterstützten CLIs etwas unterschiedlich aus, da jede eine andere Vertragsgestaltung für den Hook „Agent fertig" bietet. Das **Ergebnis** ist dasselbe — der Agent kommt nicht damit durch, anzuhalten, während ein Workflow-Gate fehlschlägt — aber die **Mechanik** unterscheidet sich. Die folgende Tabelle fasst dies zusammen; nur Pi hat eine für Benutzer sichtbare Besonderheit, die Sie verstehen sollten, bevor Sie eine `require-*-before-stop`-Richtlinie aktivieren. +Die Stop-Durchsetzung sieht bei den sechs unterstützten CLIs etwas unterschiedlich aus, da jede eine andere Vertragsgestaltung für den Hook „Agent fertig" bietet. Das **Ergebnis** ist dasselbe — der Agent kommt nicht damit durch, anzuhalten, während ein Workflow-Gate fehlschlägt — aber die **Mechanik** unterscheidet sich. Die folgende Tabelle fasst dies zusammen; nur Pi hat eine für Benutzer sichtbare Besonderheit, die Sie verstehen sollten, bevor Sie eine `require-*-before-stop`-Richtlinie aktivieren. | CLI | Wann das Gate auslöst | Was Sie sehen | |---|---|---| @@ -712,20 +712,19 @@ Die Stop-Durchsetzung sieht bei den sieben unterstützten CLIs etwas unterschied | Codex | Gleiche Agentenschleife, sofort | Wie Claude. | | GitHub Copilot CLI | Gleiche Agentenschleife, sofort | Wie Claude (verwendet Copilots `{decision:"block", reason}`-Wiederholungskanal — empirisch verifiziert gegen Copilot CLI 1.0.41). | | Cursor Agent | Gleiche Agentenschleife, sofort | Wie Claude (verwendet Cursors `{followup_message}`-Kanal — begrenzt auf `loop_limit`, standardmäßig 5 Wiederholungen). | -| Gemini CLI | Gleiche Agentenschleife, sofort | Wie Claude (verwendet Geminis `{decision:"block", reason}`-Kanal auf `AfterAgent`). | | OpenCode | Gleiche Agentenschleife, sofort | Wie Claude (verwendet OpenCodes `client.session.prompt(...)`-SDK-Aufruf, der über `hookSpecificOutput.additionalContext` geleitet wird). | | **Pi (pi-coding-agent)** | **Nächster Benutzerturn** | **Pi hält sichtbar an**, wenn das Gate auslöst — seine Agentenschleife beendet sich und Sie gelangen zurück zur Eingabeaufforderung. Das Gate löst dann beim nächsten Absenden einer Eingabe aus: failproofai stellt eine `MANDATORY ACTION REQUIRED`-Direktive an den Systempromt dieses Turns voran und weist das LLM an, den Workflow-Schritt (Commit, Push usw.) abzuschließen, bevor es das Gewünschte tut. | -**Pi-Einschränkung.** Pis `AgentEndEvent` (das Upstream-Äquivalent zu Claudes `Stop`-Hook) hat keinen Result-Typ — wenn er auslöst, hat Pis Agentenschleife bereits beendet. Pi kann nicht dazu gezwungen werden, dieselbe Schleife zu wiederholen, wie es Claude / Copilot / Cursor / Gemini / OpenCode können. failproofai verlagert das Gate auf Pis `before_agent_start`-Ereignis (das nach der nächsten Benutzereingabe auslöst), sodass die Workflow-Prüfung trotzdem greift, nur beim nächsten Turn statt beim aktuellen. +**Pi-Einschränkung.** Pis `AgentEndEvent` (das Upstream-Äquivalent zu Claudes `Stop`-Hook) hat keinen Result-Typ — wenn er auslöst, hat Pis Agentenschleife bereits beendet. Pi kann nicht dazu gezwungen werden, dieselbe Schleife zu wiederholen, wie es Claude / Copilot / Cursor / OpenCode können. failproofai verlagert das Gate auf Pis `before_agent_start`-Ereignis (das nach der nächsten Benutzereingabe auslöst), sodass die Workflow-Prüfung trotzdem greift, nur beim nächsten Turn statt beim aktuellen. **Was das in der Praxis bedeutet:** - Nachdem Pi anhält, wird der Ablehnungsgrund im Speicher gespeichert, indexiert nach Pi-Sitzungs-ID. Die nächste Eingabe, die Sie im selben Pi-Prozess absenden, entleert ihn: Das LLM sieht die `MANDATORY ACTION REQUIRED`-Direktive am Anfang seines Systemprompts, führt einen Commit durch (oder Push / öffnet den PR / wartet auf CI) und fährt erst dann mit Ihrer Anfrage fort. Der gespeicherte Ablehnungsgrund ist einmalig — sobald entleert, ist das Gate frei. -- Das Gate ist durch die Prozesslebensdauer von Pi begrenzt. Wenn Sie Pi zwischen den Turns mit `Ctrl+C` beenden oder schließen, wird der In-Memory-Eintrag zusammen mit dem Prozess gelöscht und das Gate wird verfehlt. Claude, Copilot, Cursor, Gemini und OpenCode haben dieselbe Einschränkung (Agent beenden und das Gate wird verfehlt) — bei Pi ist es nur sichtbarer, weil der Agent sichtbar beendet wird, bevor das Gate auslöst. +- Das Gate ist durch die Prozesslebensdauer von Pi begrenzt. Wenn Sie Pi zwischen den Turns mit `Ctrl+C` beenden oder schließen, wird der In-Memory-Eintrag zusammen mit dem Prozess gelöscht und das Gate wird verfehlt. Claude, Copilot, Cursor und OpenCode haben dieselbe Einschränkung (Agent beenden und das Gate wird verfehlt) — bei Pi ist es nur sichtbarer, weil der Agent sichtbar beendet wird, bevor das Gate auslöst. - Ein ausstehender Deny wird auch bei `session_shutdown` aus beliebigem Grund (`new` / `resume` / `fork` / `quit`) gelöscht, sodass ein veraltetes Gate einer vorherigen Sitzung nicht in eine neue Sitzung überlaufen kann, die im selben Pi-Prozess gestartet wurde. -Wenn Sie das Claude-ähnliche Wiederholen in derselben Schleife benötigen, führen Sie Ihre `Stop`-Richtlinien unter einer der anderen sechs unterstützten CLIs aus. Wir verfolgen Pi upstream auf einen zukünftigen Result-Typ bei `AgentEndEvent`, der es uns ermöglichen würde, diese Lücke zu schließen. +Wenn Sie das Claude-ähnliche Wiederholen in derselben Schleife benötigen, führen Sie Ihre `Stop`-Richtlinien unter einer der anderen fünf unterstützten CLIs aus. Wir verfolgen Pi upstream auf einen zukünftigen Result-Typ bei `AgentEndEvent`, der es uns ermöglichen würde, diese Lücke zu schließen. ### `require-commit-before-stop` diff --git a/docs/de/cli/audit.mdx b/docs/de/cli/audit.mdx index 833e1a9d..1b4d816d 100644 --- a/docs/de/cli/audit.mdx +++ b/docs/de/cli/audit.mdx @@ -54,7 +54,7 @@ failproofai bis du es mit `Ctrl+C` beendest. -Das Dashboard scannt vergangene Agent-CLI-Transkripte auf diesem Rechner (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi, Gemini) und meldet, wie oft der Agent Dinge getan hat, die failproofai unterbinden soll – Env-Var-Prüfungen, Force-Pushes, redundante `cd `-Präfixe, Sleep-Polling-Schleifen, erneutes Einlesen gerade bearbeiteter Dateien und mehr. +Das Dashboard scannt vergangene Agent-CLI-Transkripte auf diesem Rechner (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi) und meldet, wie oft der Agent Dinge getan hat, die failproofai unterbinden soll – Env-Var-Prüfungen, Force-Pushes, redundante `cd `-Präfixe, Sleep-Polling-Schleifen, erneutes Einlesen gerade bearbeiteter Dateien und mehr. Für jedes Transkript wird jedes Tool-Use-Ereignis durch die 39 eingebauten Richtlinien **und** durch 8 Audit-exklusive Detektoren abgespielt, die Muster erkennen, die noch nicht von Laufzeit-Richtlinien abgedeckt werden. Die Zählungen werden pro Richtlinie bzw. Detektor über alle Sitzungen hinweg aggregiert. diff --git a/docs/de/configuration.mdx b/docs/de/configuration.mdx index a0b1ad6a..95dae70a 100644 --- a/docs/de/configuration.mdx +++ b/docs/de/configuration.mdx @@ -196,13 +196,12 @@ Die Befehle `policies --install` und `policies --uninstall` schreiben in die Hoo - **OpenAI Codex**: `~/.codex/hooks.json` (Benutzer), `/.codex/hooks.json` (Projekt) – Codex hat keinen `local`-Scope - **GitHub Copilot CLI _(beta)_**: `~/.copilot/hooks/failproofai.json` (Benutzer), `/.github/hooks/failproofai.json` (Projekt) – Copilot hat keinen `local`-Scope. Hook-Einträge verwenden Copilots betriebssystemspezifische `bash`/`powershell`-Befehlsfelder mit `timeoutSec`; die Datei enthält einen `version: 1`-Marker auf oberster Ebene. Die Unterstützung von Copilot CLI ist **beta**, während wir das `events.jsonl`-Aufzeichnungsschema (das in der öffentlichen Dokumentation nicht spezifiziert ist) anhand weiterer realer Sitzungen überprüfen. - **Cursor Agent _(beta)_**: `~/.cursor/hooks.json` (Benutzer), `/.cursor/hooks.json` (Projekt) – Cursor hat keinen `local`-Scope. Hook-Einträge verwenden die Claude-ähnliche `{type, command, timeout}`-Form (ohne `bash`/`powershell`-Aufteilung), werden aber unter camelCase-Ereignisschlüsseln (`preToolUse`, `beforeSubmitPrompt`, …) in einem flachen Array gemäß Cursors [Hooks-Schema](https://cursor.com/docs/hooks) gespeichert; die Datei enthält einen `version: 1`-Marker auf oberster Ebene. Der Handler kanonisiert camelCase → PascalCase über `CURSOR_EVENT_MAP`, sodass bestehende integrierte Richtlinien unverändert ausgelöst werden. Die Unterstützung von Cursor Agent ist **beta**, während wir Cursors On-Disk-Transkriptformat (in der öffentlichen Dokumentation nicht spezifiziert) anhand weiterer realer Installationen überprüfen. - - **OpenCode _(beta)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (Benutzer), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (Projekt) – OpenCode hat keinen `local`-Scope. Im Gegensatz zu den anderen sechs CLIs verfügt OpenCode über **kein externes Befehls-Hook-System**: Es lädt In-Process-JS/TS-Plugins, die explizit über das `plugin: []`-Array in `opencode.json` registriert werden (automatische Erkennung aus `.opencode/plugins/` ist **nicht** die Art, wie Plugins in opencode v1.14.33 geladen werden). Die Installation legt ein kleines generiertes Plugin-Shim ab, das den failproofai-Binary als Subprozess aufruft und die JSON-Antwort im Claude-Format des Binaries zurück in Plugin-Semantik übersetzt: `throw new Error()` für tool-event deny (bricht den Tool-Aufruf ab), `client.session.prompt(...)` für instruct UND für `Stop` / `SubagentStop` deny (reicht den Ablehnungsgrund als nächste Benutzernachricht ein – der einzige Force-Retry-Kanal, da `session.idle` nur eine Benachrichtigung ist und das Werfen einer Exception dort ein No-op ist), und No-op für allow. Das Shim kanonisiert sowohl Tool-Namen (Kleinbuchstaben → PascalCase über `OPENCODE_TOOL_MAP`) als auch Tool-Input-Argumentschlüssel (camelCase → snake_case über `OPENCODE_TOOL_INPUT_MAP` für `Read` / `Write` / `Edit`, z. B. `filePath` → `file_path`, `oldString` → `old_string`), bevor es an das Binary weiterleitet, sodass pfadprüfende Builtins wie `block-read-outside-cwd`, `block-env-files` und `block-secrets-write` bei OpenCode-Tool-Aufrufen unverändert ausgelöst werden. Sitzungen werden in OpenCodes SQLite-Datenbank unter `~/.local/share/opencode/opencode.db` gespeichert; der Session-Viewer des Dashboards liest sie über `opencode db --format json` und `opencode export `. Die OpenCode-Unterstützung ist **beta**, während wir das Verhalten versionsübergreifend und anhand weiterer realer Sitzungen überprüfen. Siehe die [OpenCode-Plugin-Dokumentation](https://opencode.ai/docs/plugins/). + - **OpenCode _(beta)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (Benutzer), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (Projekt) – OpenCode hat keinen `local`-Scope. Im Gegensatz zu den anderen fünf CLIs verfügt OpenCode über **kein externes Befehls-Hook-System**: Es lädt In-Process-JS/TS-Plugins, die explizit über das `plugin: []`-Array in `opencode.json` registriert werden (automatische Erkennung aus `.opencode/plugins/` ist **nicht** die Art, wie Plugins in opencode v1.14.33 geladen werden). Die Installation legt ein kleines generiertes Plugin-Shim ab, das den failproofai-Binary als Subprozess aufruft und die JSON-Antwort im Claude-Format des Binaries zurück in Plugin-Semantik übersetzt: `throw new Error()` für tool-event deny (bricht den Tool-Aufruf ab), `client.session.prompt(...)` für instruct UND für `Stop` / `SubagentStop` deny (reicht den Ablehnungsgrund als nächste Benutzernachricht ein – der einzige Force-Retry-Kanal, da `session.idle` nur eine Benachrichtigung ist und das Werfen einer Exception dort ein No-op ist), und No-op für allow. Das Shim kanonisiert sowohl Tool-Namen (Kleinbuchstaben → PascalCase über `OPENCODE_TOOL_MAP`) als auch Tool-Input-Argumentschlüssel (camelCase → snake_case über `OPENCODE_TOOL_INPUT_MAP` für `Read` / `Write` / `Edit`, z. B. `filePath` → `file_path`, `oldString` → `old_string`), bevor es an das Binary weiterleitet, sodass pfadprüfende Builtins wie `block-read-outside-cwd`, `block-env-files` und `block-secrets-write` bei OpenCode-Tool-Aufrufen unverändert ausgelöst werden. Sitzungen werden in OpenCodes SQLite-Datenbank unter `~/.local/share/opencode/opencode.db` gespeichert; der Session-Viewer des Dashboards liest sie über `opencode db --format json` und `opencode export `. Die OpenCode-Unterstützung ist **beta**, während wir das Verhalten versionsübergreifend und anhand weiterer realer Sitzungen überprüfen. Siehe die [OpenCode-Plugin-Dokumentation](https://opencode.ai/docs/plugins/). - **Pi _(beta)_**: `~/.pi/agent/settings.json` (Benutzer), `/.pi/settings.json` (Projekt) – Pi hat keinen `local`-Scope. Pi lädt TypeScript-Erweiterungspakete beim Start; die Einstellungsdatei ist ein flaches String-Array `{"packages": ["./relative/path", …]}`. failproofai schreibt einen einzelnen packages-Array-Eintrag, der auf sein gebündeltes `pi-extension/`-Verzeichnis zeigt. Die Erweiterung abonniert intern Pis `tool_call` / `user_bash` / `input` / `session_start`-Ereignisse und ruft `failproofai --hook --cli pi` als Shell-Befehl auf; der Handler kanonisiert Ereignisse über `PI_EVENT_MAP` von underscore_lower_snake_case → PascalCase, sodass bestehende integrierte Richtlinien unverändert ausgelöst werden. Tool-Input-Argumente werden ebenfalls über `PI_TOOL_INPUT_MAP` kanonisiert (Pis Read / Write / Edit liefern `path` statt `file_path`; die Zuordnung des obersten Schlüssels lässt `block-env-files` und `block-secrets-write` auslösen – `block-read-outside-cwd` hatte bereits einen `path`-Fallback). Die Pi-Unterstützung ist **beta**, während sich Pis Erweiterungs-API und das Sitzungslog-Layout stabilisieren. - - **Gemini CLI _(beta)_**: `~/.gemini/settings.json` (Benutzer), `/.gemini/settings.json` (Projekt) – Gemini hat keinen `local`-Scope (es dokumentiert einen `system`-Scope unter `/etc/gemini-cli/settings.json`, den failproofai nicht verfügbar macht). Hook-Einträge verwenden Claudes `{type, command, timeout}`-Form, eingebettet in Geminis `{matcher, hooks: [...]}`-Matcher-Schema mit standardmäßig `matcher: "*"`. Ereignisse sind PascalCase (`SessionStart`, `BeforeAgent`, `AfterAgent`, `BeforeModel`, `AfterModel`, `BeforeToolSelection`, `BeforeTool`, `AfterTool`, `PreCompress`, `Notification`, `SessionEnd`); der Handler ordnet sie über `GEMINI_EVENT_MAP` kanonischen Claude-Namen zu. Tool-Namen sind snake_case (`run_shell_command`, `read_file`, `write_file`, `replace`, …) – der Handler kanonisiert über `GEMINI_TOOL_MAP`, sodass bestehende integrierte Richtlinien unverändert ausgelöst werden. Der Richtlinienauswerter gibt Geminis flache `{decision: "deny", reason}`-Form aus (bevorzugt gemäß Geminis „Goldener Regel" exit-0-Vertrag), `{hookSpecificOutput: {hookEventName, additionalContext}}` für Kontext-Injection bei BeforeAgent / AfterTool / SessionStart, und `{decision: "block", reason}` bei AfterAgent für Force-Retry-Semantik. Die Gemini CLI-Unterstützung ist **beta**, während wir die reale Abdeckung ausweiten. Siehe die [Gemini CLI Hooks-Dokumentation](https://geminicli.com/docs/hooks/). - **Hermes (hermes-agent)**: `~/.hermes/config.yaml` (**nur Benutzer-Scope** – Hermes hat keine Projekt-/lokale Konfiguration). Hermes ist ein Slack/Telegram-**Gateway**, daher fängt eine Installation Tool-Aufrufe von jeder Plattform (Slack/Telegram/cli/cron) **und** internen Subagenten ab. Hook-Einträge sind ein `{command, timeout}`-Paar (Timeout in **Sekunden**) unter einem `hooks:`-Map, der nach Hermes' snake_case-Ereignissen (`pre_tool_call` / `post_tool_call` / `on_session_start` / `on_session_end` / `subagent_stop`) geordnet ist; der Handler kanonisiert Ereignisse über `HERMES_EVENT_MAP` und Tool-Namen über `HERMES_TOOL_MAP`, sodass integrierte Richtlinien unverändert ausgelöst werden. Die Konfiguration wird durch einen kommentarerhaltenden YAML-`Document`-Round-trip bearbeitet, sodass die anderen Einstellungen des Betreibers erhalten bleiben, und die Installation setzt `hooks_auto_accept: true`, sodass das kopflose Gateway (kein TTY) die Hooks ohne Zustimmungsaufforderung ausführt. Der Auswerter gibt Hermes' stdout-Vertrag `{"decision":"block","reason"}` aus (Hermes ignoriert Exit-Codes). **Einschränkungen:** Hermes hat kein turn-end-`Stop`-Ereignis, daher werden die `require-*-before-stop`-Builtins für Hermes nie ausgelöst (nicht anwendbar, nicht defekt); `instruct` wird zu allow-with-logged-note herabgestuft (kein additional-context-Kanal); und Output-Secret-Redaktion (`sanitize-*`) kann Tool-Output über den Shell-Hook-Vertrag nicht umschreiben. Hermes ist **auch** eine Offline-**Audit**-Quelle – das Dashboard liest seine Gateway-Sitzungen direkt aus `~/.hermes/state.db`. - **`policies-config.json`** – teilt failproofai mit, welche Richtlinien ausgewertet werden sollen und mit welchen Parametern (geteilt über alle Agenten-CLIs) -Übergeben Sie `--cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes`, um einen bestimmten Agenten anzusprechen (durch Leerzeichen getrennt oder wiederholt für eine beliebige Teilmenge): +Übergeben Sie `--cli claude|codex|copilot|cursor|opencode|pi|hermes`, um einen bestimmten Agenten anzusprechen (durch Leerzeichen getrennt oder wiederholt für eine beliebige Teilmenge): ```bash failproofai policies --install --cli codex --scope project @@ -210,12 +209,11 @@ failproofai policies --install --cli copilot --scope project failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project -failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user -failproofai policies --install --cli claude codex copilot cursor opencode pi gemini +failproofai policies --install --cli claude codex copilot cursor opencode pi ``` -Wenn `--cli` weggelassen wird, erkennt `failproofai` automatisch, welche Agenten-CLIs installiert sind (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which gemini` / `which hermes`): +Wenn `--cli` weggelassen wird, erkennt `failproofai` automatisch, welche Agenten-CLIs installiert sind (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which hermes`): - **Eine CLI erkannt** – wählt diese CLI automatisch ohne Rückfrage aus. - **Mehrere CLIs erkannt** in einem interaktiven Terminal – zeigt eine Einzelauswahl-Eingabeaufforderung mit Pfeiltasten an, die in einen Abschnitt `Erkannt (N)` (mit einer `Für alle N erkannten installieren`-Sammelzeile + jeder erkannten CLI einzeln) und einen Abschnitt `Nicht installiert (M) · Hooks vorab installieren` unterteilt ist, der alle nicht erkannten unterstützten CLIs als Vorwärts-Installationsoption auflistet (↑↓ zum Bewegen, Enter zum Auswählen, ^C zum Beenden). Der Deinstallationsablauf zeigt nur den Abschnitt „Erkannt" an. diff --git a/docs/de/dashboard.mdx b/docs/de/dashboard.mdx index 610aaa73..9f8069d2 100644 --- a/docs/de/dashboard.mdx +++ b/docs/de/dashboard.mdx @@ -24,11 +24,11 @@ Das Dashboard liest direkt vom Dateisystem – aus Ihren Claude Code-Projektordn ### Projekte -Listet alle Claude Code-, OpenAI Codex-, GitHub Copilot CLI- _(Beta)_, Cursor Agent- _(Beta)_, OpenCode- _(Beta)_, Pi- _(Beta)_, Gemini CLI- _(Beta)_ und Hermes-Projekte auf, die auf Ihrem Rechner gefunden wurden. Claude-Projekte werden aus `~/.claude/projects/` erkannt (oder dem über `CLAUDE_PROJECTS_PATH` festgelegten Pfad); Codex-Projekte werden durch Scannen aller Transkripte unter `~/.codex/sessions///
/*.jsonl` ermittelt und nach dem `cwd` des ersten Eintrags jeder Sitzung gruppiert; Copilot CLI-Projekte werden durch Scannen von `~/.copilot/session-state//workspace.yaml` (konfigurierbar über `COPILOT_HOME`) und Gruppierung nach dem dortigen `cwd`-Feld erkannt; Cursor Agent-Projekte werden durch Scannen der sitzungsspezifischen Metadaten unter `~/.cursor/agent-sessions//` (konfigurierbar über `CURSOR_HOME`, mit `conversations/` und `sessions/` als Fallbacks) nach einem `cwd`-Skalar in `meta.json` / `session.json` / `workspace.yaml` erkannt; OpenCode-Projekte werden durch Abfrage der SQLite-Datenbank unter `~/.local/share/opencode/opencode.db` via `opencode db --format json` ermittelt (dabei werden die Tabellen `session` und `project` gelesen und nach `project_id` gruppiert); Pi-Projekte werden durch Scannen von sitzungsspezifischen JSONL-Transkripten unter `~/.pi/agent/sessions//_.jsonl` (konfigurierbar über `PI_SESSIONS_DIR`) und Auslesen des `cwd` aus dem ersten Eintrag jeder Sitzung erkannt; Gemini CLI-Projekte werden durch Scannen von `~/.gemini/tmp//chats/session--.jsonl` (konfigurierbar über `GEMINI_SESSIONS_DIR`) und Rückgewinnung des kanonischen cwd aus dem benachbarten `.project_root`-Textmarker erkannt; Hermes-Gateway-Sitzungen werden direkt aus dem SQLite-Store unter `~/.hermes/state.db` (konfigurierbar über `HERMES_DB_PATH`) gelesen und nach `source` (Slack/Telegram/cli/cron — Gateway-Sitzungen haben kein cwd) in `hermes-`-Projekte gruppiert. Ein Projekt, das von mehreren CLIs genutzt wurde, wird als einzelne Zeile mit allen zugehörigen Badges angezeigt. Verwenden Sie das **CLI**-Dropdown über der Tabelle, um nach einer bestimmten Agenten-CLI zu filtern; die URL speichert Ihre Auswahl als `?cli=claude|codex|copilot|cursor|opencode|pi|gemini|hermes`. +Listet alle Claude Code-, OpenAI Codex-, GitHub Copilot CLI- _(Beta)_, Cursor Agent- _(Beta)_, OpenCode- _(Beta)_, Pi- _(Beta)_ und Hermes-Projekte auf, die auf Ihrem Rechner gefunden wurden. Claude-Projekte werden aus `~/.claude/projects/` erkannt (oder dem über `CLAUDE_PROJECTS_PATH` festgelegten Pfad); Codex-Projekte werden durch Scannen aller Transkripte unter `~/.codex/sessions///
/*.jsonl` ermittelt und nach dem `cwd` des ersten Eintrags jeder Sitzung gruppiert; Copilot CLI-Projekte werden durch Scannen von `~/.copilot/session-state//workspace.yaml` (konfigurierbar über `COPILOT_HOME`) und Gruppierung nach dem dortigen `cwd`-Feld erkannt; Cursor Agent-Projekte werden durch Scannen der sitzungsspezifischen Metadaten unter `~/.cursor/agent-sessions//` (konfigurierbar über `CURSOR_HOME`, mit `conversations/` und `sessions/` als Fallbacks) nach einem `cwd`-Skalar in `meta.json` / `session.json` / `workspace.yaml` erkannt; OpenCode-Projekte werden durch Abfrage der SQLite-Datenbank unter `~/.local/share/opencode/opencode.db` via `opencode db --format json` ermittelt (dabei werden die Tabellen `session` und `project` gelesen und nach `project_id` gruppiert); Pi-Projekte werden durch Scannen von sitzungsspezifischen JSONL-Transkripten unter `~/.pi/agent/sessions//_.jsonl` (konfigurierbar über `PI_SESSIONS_DIR`) und Auslesen des `cwd` aus dem ersten Eintrag jeder Sitzung erkannt; Hermes-Gateway-Sitzungen werden direkt aus dem SQLite-Store unter `~/.hermes/state.db` (konfigurierbar über `HERMES_DB_PATH`) gelesen und nach `source` (Slack/Telegram/cli/cron — Gateway-Sitzungen haben kein cwd) in `hermes-`-Projekte gruppiert. Ein Projekt, das von mehreren CLIs genutzt wurde, wird als einzelne Zeile mit allen zugehörigen Badges angezeigt. Verwenden Sie das **CLI**-Dropdown über der Tabelle, um nach einer bestimmten Agenten-CLI zu filtern; die URL speichert Ihre Auswahl als `?cli=claude|codex|copilot|cursor|opencode|pi|hermes`. Jedes Projekt zeigt: - Projektname (abgeleitet vom Ordnerpfad) -- Ein CLI-Badge — `Claude Code` (orange), `OpenAI Codex` (lila), `GitHub Copilot` (blau), `Cursor Agent` (smaragd), `OpenCode` (bernstein), `Pi` (pink), `Gemini CLI` (himmelblau) und/oder `Hermes` (indigo) +- Ein CLI-Badge — `Claude Code` (orange), `OpenAI Codex` (lila), `GitHub Copilot` (blau), `Cursor Agent` (smaragd), `OpenCode` (bernstein), `Pi` (pink) und/oder `Hermes` (indigo) - Datum der letzten Sitzungsaktivität Klicken Sie auf ein Projekt, um dessen Sitzungen anzuzeigen. @@ -47,7 +47,7 @@ Klicken Sie auf eine Sitzung, um den Sitzungsviewer zu öffnen. ### Sitzungsviewer -Der Sitzungsviewer beantwortet die zentrale Frage bei autonomen Agenten: Was hat der Agent getan, und ist er auf Kurs geblieben? Ein CLI-Badge neben der Überschrift zeigt an, ob es sich um ein Claude Code-, OpenAI Codex-, GitHub Copilot CLI-, Cursor Agent-, OpenCode-, Pi-, Gemini CLI- oder Hermes-Transkript handelt. Er zeigt eine Zeitleiste aller Ereignisse einer Sitzung: +Der Sitzungsviewer beantwortet die zentrale Frage bei autonomen Agenten: Was hat der Agent getan, und ist er auf Kurs geblieben? Ein CLI-Badge neben der Überschrift zeigt an, ob es sich um ein Claude Code-, OpenAI Codex-, GitHub Copilot CLI-, Cursor Agent-, OpenCode-, Pi- oder Hermes-Transkript handelt. Er zeigt eine Zeitleiste aller Ereignisse einer Sitzung: - **Nachrichten** – Claudes Textantworten und Benutzeranfragen - **Tool-Aufrufe** – Jedes von Claude aufgerufene Tool mit Ein- und Ausgabe @@ -55,7 +55,7 @@ Der Sitzungsviewer beantwortet die zentrale Frage bei autonomen Agenten: Was hat Die Statistikleiste oben zeigt Sitzungsdauer, Gesamtzahl der Tool-Aufrufe und eine Zusammenfassung der Hook-Entscheidungen (allow / deny / instruct-Anzahl). -Klicken Sie auf **Logs herunterladen**, um die Sitzung zu exportieren. Für Claude Code-, Codex-, Copilot-, Cursor-, Pi- und Gemini-Sitzungen erhalten Sie das originale JSONL-Transkript vom Datenträger byte-genau; für OpenCode (dessen Sitzungen in SQLite und nicht auf dem Datenträger gespeichert sind) erhalten Sie ein JSON-Dokument, das die zugrundeliegenden Tabellen `session` / `messages` / `parts` widerspiegelt. +Klicken Sie auf **Logs herunterladen**, um die Sitzung zu exportieren. Für Claude Code-, Codex-, Copilot-, Cursor- und Pi-Sitzungen erhalten Sie das originale JSONL-Transkript vom Datenträger byte-genau; für OpenCode (dessen Sitzungen in SQLite und nicht auf dem Datenträger gespeichert sind) erhalten Sie ein JSON-Dokument, das die zugrundeliegenden Tabellen `session` / `messages` / `parts` widerspiegelt. ### Audit @@ -75,16 +75,16 @@ Eine zweiseitige Seite zur Verwaltung von Richtlinien und Überprüfung der Akti - - Wählen Sie in einem einzelnen Panel aus, welche Agenten-CLIs failproofai schützt — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi, Gemini CLI und Hermes haben jeweils eine Zeile mit Installationsstatus (`Active` / `Detected` / `Inactive`), dem benutzerbereichs-spezifischen Einstellungspfad und einem markenfarbigen Akzent. Aktivieren oder deaktivieren Sie die gewünschten CLIs und klicken Sie auf `Apply changes`, um die Änderungen in einem Schritt zu installieren/deinstallieren. CLIs, deren Binärdatei im PATH erkannt wird, sind vorab aktiviert. + - Wählen Sie in einem einzelnen Panel aus, welche Agenten-CLIs failproofai schützt — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi und Hermes haben jeweils eine Zeile mit Installationsstatus (`Active` / `Detected` / `Inactive`), dem benutzerbereichs-spezifischen Einstellungspfad und einem markenfarbigen Akzent. Aktivieren oder deaktivieren Sie die gewünschten CLIs und klicken Sie auf `Apply changes`, um die Änderungen in einem Schritt zu installieren/deinstallieren. CLIs, deren Binärdatei im PATH erkannt wird, sind vorab aktiviert. - Aktivieren oder deaktivieren Sie einzelne Richtlinien mit einem einzigen Klick (schreibt in `~/.failproofai/policies-config.json` — gemeinsam genutzt von allen installierten CLIs) - Erweitern Sie eine Richtlinie, um ihre Parameter zu konfigurieren (für Richtlinien, die `policyParams` unterstützen) - Legen Sie einen benutzerdefinierten Richtliniendateipfad fest - Vollständige paginierte Historie aller Hook-Ereignisse, die in allen Sitzungen ausgelöst wurden - - Filtern nach Entscheidung, Ereignistyp, CLI (Claude Code / OpenAI Codex / GitHub Copilot _(Beta)_ / Cursor Agent _(Beta)_ / OpenCode _(Beta)_ / Pi _(Beta)_ / Gemini CLI _(Beta)_ / Hermes), Richtlinienname oder Sitzungs-ID - - Jede Zeile zeigt: Zeitstempel, Richtlinienname, Entscheidung, CLI-Badge (orange = Claude Code, lila = OpenAI Codex, blau = GitHub Copilot, smaragd = Cursor Agent, bernstein = OpenCode, pink = Pi, himmelblau = Gemini CLI, indigo = Hermes), Tool-Name, Sitzungs-ID und den Grund für deny/instruct-Entscheidungen - - Klicken Sie auf eine Sitzungs-ID, um das zugehörige Transkript zu öffnen — der Viewer erkennt automatisch, welche CLI den Hook ausgelöst hat (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Gemini CLI `~/.gemini/tmp//chats/.jsonl`, Hermes `~/.hermes/state.db`) und zeigt das passende CLI-Badge in der Überschrift an + - Filtern nach Entscheidung, Ereignistyp, CLI (Claude Code / OpenAI Codex / GitHub Copilot _(Beta)_ / Cursor Agent _(Beta)_ / OpenCode _(Beta)_ / Pi _(Beta)_ / Hermes), Richtlinienname oder Sitzungs-ID + - Jede Zeile zeigt: Zeitstempel, Richtlinienname, Entscheidung, CLI-Badge (orange = Claude Code, lila = OpenAI Codex, blau = GitHub Copilot, smaragd = Cursor Agent, bernstein = OpenCode, pink = Pi, indigo = Hermes), Tool-Name, Sitzungs-ID und den Grund für deny/instruct-Entscheidungen + - Klicken Sie auf eine Sitzungs-ID, um das zugehörige Transkript zu öffnen — der Viewer erkennt automatisch, welche CLI den Hook ausgelöst hat (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Hermes `~/.hermes/state.db`) und zeigt das passende CLI-Badge in der Überschrift an diff --git a/docs/de/getting-started.mdx b/docs/de/getting-started.mdx index 4499b6f3..ca314e92 100644 --- a/docs/de/getting-started.mdx +++ b/docs/de/getting-started.mdx @@ -37,9 +37,9 @@ bun add -g failproofai failproofai policies --install ``` - Damit werden Hook-Einträge in die installierten Agenten-CLIs geschrieben (Claude Codes `~/.claude/settings.json`, OpenAI Codex' `~/.codex/hooks.json`, GitHub Copilot CLIs `~/.copilot/hooks/failproofai.json`, Cursor Agents `~/.cursor/hooks.json`, OpenCodes generiertem Plugin-Shim unter `~/.config/opencode/plugins/failproofai.mjs` sowie einem Registrierungseintrag im `plugin`-Array von `~/.config/opencode/opencode.json`, Pis `~/.pi/agent/settings.json`, Gemini CLIs `~/.gemini/settings.json` oder Hermes' `~/.hermes/config.yaml`). Sind mehrere installiert, wirst du zur Auswahl aufgefordert; übergib `--cli claude codex copilot cursor opencode pi gemini hermes` (beliebige Teilmenge), um die Abfrage zu überspringen. + Damit werden Hook-Einträge in die installierten Agenten-CLIs geschrieben (Claude Codes `~/.claude/settings.json`, OpenAI Codex' `~/.codex/hooks.json`, GitHub Copilot CLIs `~/.copilot/hooks/failproofai.json`, Cursor Agents `~/.cursor/hooks.json`, OpenCodes generiertem Plugin-Shim unter `~/.config/opencode/plugins/failproofai.mjs` sowie einem Registrierungseintrag im `plugin`-Array von `~/.config/opencode/opencode.json`, Pis `~/.pi/agent/settings.json` oder Hermes' `~/.hermes/config.yaml`). Sind mehrere installiert, wirst du zur Auswahl aufgefordert; übergib `--cli claude codex copilot cursor opencode pi hermes` (beliebige Teilmenge), um die Abfrage zu überspringen. - Unterstützung für GitHub Copilot CLI, Cursor Agent, OpenCode, Pi und Gemini CLI ist **Beta** – Installation mit `--cli copilot`, `--cli cursor`, `--cli opencode`, `--cli pi` oder `--cli gemini`. Hermes (hermes-agent, ein Slack/Telegram-Gateway) wird im Benutzerbereich mit `--cli hermes` installiert und ist **ebenfalls** eine Offline-Auditquelle. + Unterstützung für GitHub Copilot CLI, Cursor Agent, OpenCode und Pi ist **Beta** – Installation mit `--cli copilot`, `--cli cursor`, `--cli opencode` oder `--cli pi`. Hermes (hermes-agent, ein Slack/Telegram-Gateway) wird im Benutzerbereich mit `--cli hermes` installiert und ist **ebenfalls** eine Offline-Auditquelle. ```bash failproofai policies --install --scope project @@ -48,7 +48,6 @@ bun add -g failproofai failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project - failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user failproofai policies --install block-sudo block-rm-rf sanitize-api-keys ``` diff --git a/docs/de/introduction.mdx b/docs/de/introduction.mdx index c0c06daa..1ed93753 100644 --- a/docs/de/introduction.mdx +++ b/docs/de/introduction.mdx @@ -5,7 +5,7 @@ description: "FailproofAI gibt KI-Agenten 39 integrierte Fehlerrichtlinien, die [![npm weekly downloads](https://img.shields.io/npm/dw/failproofai?style=flat-square&color=2ea44f)](https://www.npmjs.com/package/failproofai) -Hooks und Richtlinien für **KI-Fehlerbehandlung**, **Fehlerbehebung** und **LLM-Zuverlässigkeit**. Halten Sie Ihre KI-Agenten zuverlässig und autonom am Laufen – für **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Gemini CLI**, **Hermes** und das **Agents SDK**. +Hooks und Richtlinien für **KI-Fehlerbehandlung**, **Fehlerbehebung** und **LLM-Zuverlässigkeit**. Halten Sie Ihre KI-Agenten zuverlässig und autonom am Laufen – für **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Hermes** und das **Agents SDK**. KI-Agenten scheitern auf vorhersehbare Weisen. Sie führen destruktive Befehle aus, geben Secrets preis, weichen von ihrer Aufgabe ab, stecken in Schleifen fest oder pushen direkt in den main-Branch. Werden diese Fehler nicht behoben, können kleine Probleme zu Ausfällen, geleakten Zugangsdaten und verlorenem Code führen. diff --git a/docs/docs.json b/docs/docs.json index 56e2c9da..07ba70d0 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -344,7 +344,7 @@ ] }, { - "language": "pt-br", + "language": "pt-BR", "tabs": [ { "tab": "Documentação", @@ -1275,7 +1275,7 @@ ] }, { - "language": "pt-br", + "language": "pt-BR", "tabs": [ { "tab": "Documentação", diff --git a/docs/es/built-in-policies.mdx b/docs/es/built-in-policies.mdx index 5644f1c6..67e4b297 100644 --- a/docs/es/built-in-policies.mdx +++ b/docs/es/built-in-policies.mdx @@ -710,7 +710,7 @@ Todas las políticas de flujo de trabajo son **fail-open**: si la herramienta re ### Semántica de Stop por CLI -La aplicación del Stop se ve ligeramente diferente entre los siete CLIs compatibles porque cada uno expone un contrato de hook distinto para "el agente ha terminado". El **resultado** es el mismo — el agente no puede detenerse mientras una condición de flujo de trabajo esté fallando — pero los **mecanismos** difieren. La tabla a continuación resume esto; solo Pi tiene un comportamiento visible para el usuario que vale la pena entender antes de habilitar una política `require-*-before-stop`. +La aplicación del Stop se ve ligeramente diferente entre los seis CLIs compatibles porque cada uno expone un contrato de hook distinto para "el agente ha terminado". El **resultado** es el mismo — el agente no puede detenerse mientras una condición de flujo de trabajo esté fallando — pero los **mecanismos** difieren. La tabla a continuación resume esto; solo Pi tiene un comportamiento visible para el usuario que vale la pena entender antes de habilitar una política `require-*-before-stop`. | CLI | Cuándo se activa la condición | Lo que ves | |---|---|---| @@ -718,20 +718,19 @@ La aplicación del Stop se ve ligeramente diferente entre los siete CLIs compati | Codex | En el mismo bucle del agente, inmediatamente | Igual que Claude. | | GitHub Copilot CLI | En el mismo bucle del agente, inmediatamente | Igual que Claude (usa el canal de reintento `{decision:"block", reason}` de Copilot — verificado empíricamente contra Copilot CLI 1.0.41). | | Cursor Agent | En el mismo bucle del agente, inmediatamente | Igual que Claude (usa el canal `{followup_message}` de Cursor — limitado a `loop_limit`, por defecto 5 reintentos). | -| Gemini CLI | En el mismo bucle del agente, inmediatamente | Igual que Claude (usa el canal `{decision:"block", reason}` de Gemini en `AfterAgent`). | | OpenCode | En el mismo bucle del agente, inmediatamente | Igual que Claude (usa la llamada SDK `client.session.prompt(...)` de OpenCode enrutada a través de `hookSpecificOutput.additionalContext`). | | **Pi (pi-coding-agent)** | **En el siguiente turno del usuario** | **Pi se detiene visiblemente** cuando se activa la condición — su bucle de agente termina y se te devuelve el prompt. La condición se activa la próxima vez que envíes un prompt: failproofai antepone una directiva `MANDATORY ACTION REQUIRED` al system prompt de ese turno, instruyendo al LLM a completar el paso del flujo de trabajo (commit, push, etc.) antes de hacer lo que pediste. | -**Limitación de Pi.** El `AgentEndEvent` de Pi (el equivalente upstream del hook `Stop` de Claude) no tiene tipo Result — para cuando se activa, el bucle del agente de Pi ya ha terminado. Pi no puede ser forzado a reintentar el mismo bucle como Claude / Copilot / Cursor / Gemini / OpenCode. failproofai traslada la condición al evento `before_agent_start` de Pi (que se activa después del siguiente prompt del usuario) para que la comprobación del flujo de trabajo siga siendo efectiva, solo en el siguiente turno en lugar del actual. +**Limitación de Pi.** El `AgentEndEvent` de Pi (el equivalente upstream del hook `Stop` de Claude) no tiene tipo Result — para cuando se activa, el bucle del agente de Pi ya ha terminado. Pi no puede ser forzado a reintentar el mismo bucle como Claude / Copilot / Cursor / OpenCode. failproofai traslada la condición al evento `before_agent_start` de Pi (que se activa después del siguiente prompt del usuario) para que la comprobación del flujo de trabajo siga siendo efectiva, solo en el siguiente turno en lugar del actual. **Lo que esto significa en la práctica:** - Después de que Pi se detenga, el motivo de la denegación se captura en memoria indexado por el id de sesión de Pi. El siguiente prompt que envíes en el mismo proceso de Pi lo drena: el LLM ve la directiva `MANDATORY ACTION REQUIRED` al inicio de su system prompt, hace el commit (o push / abre el PR / espera a CI), y solo entonces continúa con tu solicitud. El motivo de denegación capturado es de un solo uso — una vez drenado, la condición queda libre. -- La condición está limitada por el tiempo de vida del proceso de Pi. Si haces `Ctrl+C` en Pi o sales entre turnos, la entrada en memoria se descarta junto con el proceso y la condición se pierde. Claude, Copilot, Cursor, Gemini y OpenCode tienen el mismo límite (matar el agente hace que se pierda la condición) — Pi simplemente lo hace más visible porque el agente termina visiblemente antes de que se active la condición. +- La condición está limitada por el tiempo de vida del proceso de Pi. Si haces `Ctrl+C` en Pi o sales entre turnos, la entrada en memoria se descarta junto con el proceso y la condición se pierde. Claude, Copilot, Cursor y OpenCode tienen el mismo límite (matar el agente hace que se pierda la condición) — Pi simplemente lo hace más visible porque el agente termina visiblemente antes de que se active la condición. - Una denegación pendiente también se limpia en `session_shutdown` por cualquier motivo (`new` / `resume` / `fork` / `quit`), de modo que una condición obsoleta de una sesión anterior no puede filtrarse a una sesión nueva iniciada en el mismo proceso de Pi. -Si necesitas el reintento en el mismo bucle al estilo Claude, ejecuta tus políticas de `Stop` bajo cualquiera de los otros seis CLIs compatibles. Estamos siguiendo Pi upstream para un futuro tipo Result en `AgentEndEvent` que nos permitiría cerrar esta brecha. +Si necesitas el reintento en el mismo bucle al estilo Claude, ejecuta tus políticas de `Stop` bajo cualquiera de los otros cinco CLIs compatibles. Estamos siguiendo Pi upstream para un futuro tipo Result en `AgentEndEvent` que nos permitiría cerrar esta brecha. ### `require-commit-before-stop` diff --git a/docs/es/cli/audit.mdx b/docs/es/cli/audit.mdx index 0694d437..33aa661d 100644 --- a/docs/es/cli/audit.mdx +++ b/docs/es/cli/audit.mdx @@ -52,7 +52,7 @@ failproofai sin conexión** — no requiere cuenta ni red — y el panel sigue activo hasta que lo detengas con `Ctrl+C`. -El panel analiza las transcripciones pasadas del agente CLI en esta máquina (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi, Gemini) e informa con qué frecuencia el agente hizo cosas que failproofai está diseñado para detener — comprobaciones de variables de entorno, push forzados, prefijos `cd ` redundantes, bucles de sleep-polling, releer archivos recién editados y más. +El panel analiza las transcripciones pasadas del agente CLI en esta máquina (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi) e informa con qué frecuencia el agente hizo cosas que failproofai está diseñado para detener — comprobaciones de variables de entorno, push forzados, prefijos `cd ` redundantes, bucles de sleep-polling, releer archivos recién editados y más. Por cada transcripción, cada evento de uso de herramienta se reproduce a través de las 39 políticas integradas **y** a través de 8 detectores exclusivos de auditoría que capturan patrones que aún no están cubiertos por las políticas en tiempo real. Los recuentos se agregan por política / detector en todas las sesiones. diff --git a/docs/es/configuration.mdx b/docs/es/configuration.mdx index 591ada62..47c4236c 100644 --- a/docs/es/configuration.mdx +++ b/docs/es/configuration.mdx @@ -196,13 +196,12 @@ Los comandos `policies --install` y `policies --uninstall` escriben en el archiv - **OpenAI Codex**: `~/.codex/hooks.json` (usuario), `/.codex/hooks.json` (proyecto) — Codex no tiene ámbito `local` - **GitHub Copilot CLI _(beta)_**: `~/.copilot/hooks/failproofai.json` (usuario), `/.github/hooks/failproofai.json` (proyecto) — Copilot no tiene ámbito `local`. Las entradas de hook usan los campos de comando `bash`/`powershell` de Copilot según el sistema operativo con `timeoutSec`; el archivo lleva un marcador `version: 1` de nivel superior. El soporte de Copilot CLI está en **beta** mientras verificamos el esquema de registros `events.jsonl` (que la documentación pública no especifica) con más sesiones reales. - **Cursor Agent _(beta)_**: `~/.cursor/hooks.json` (usuario), `/.cursor/hooks.json` (proyecto) — Cursor no tiene ámbito `local`. Las entradas de hook usan la forma de Claude `{type, command, timeout}` (sin división `bash`/`powershell`), pero almacenadas bajo claves de evento en camelCase (`preToolUse`, `beforeSubmitPrompt`, …) en un array plano según el [esquema de hooks de Cursor](https://cursor.com/docs/hooks); el archivo lleva un marcador `version: 1` de nivel superior. El manejador canonicaliza camelCase → PascalCase mediante `CURSOR_EVENT_MAP`, de modo que las políticas integradas existentes se activan sin cambios. El soporte de Cursor Agent está en **beta** mientras verificamos el formato en disco de la transcripción de Cursor (no especificado en la documentación pública) con más instalaciones reales. - - **OpenCode _(beta)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (usuario), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (proyecto) — OpenCode no tiene ámbito `local`. A diferencia de las otras seis CLIs, OpenCode **no tiene un sistema de hooks de comandos externos**: carga plugins JS/TS en proceso registrados explícitamente mediante el array `plugin: []` en `opencode.json` (el autodescubrimiento desde `.opencode/plugins/` **no** es cómo se cargan los plugins en opencode v1.14.33). La instalación coloca un pequeño shim de plugin generado que llama al binario failproofai como subproceso y traduce la respuesta JSON de forma Claude del binario de vuelta a la semántica del plugin: `throw new Error()` para denegar eventos de herramienta (cancela la llamada a la herramienta), `client.session.prompt(...)` para instruct Y para `Stop` / `SubagentStop` deny (envía el motivo de denegación como el siguiente mensaje del usuario — el único canal de reintento forzado, ya que `session.idle` es solo notificación y lanzar desde él es un no-op), y no-op para allow. El shim canonicaliza tanto los nombres de herramientas (minúsculas → PascalCase mediante `OPENCODE_TOOL_MAP`) como las claves de argumentos de entrada de herramientas (camelCase → snake_case mediante `OPENCODE_TOOL_INPUT_MAP` para `Read` / `Write` / `Edit`, p. ej. `filePath` → `file_path`, `oldString` → `old_string`) antes de reenviar al binario, de modo que las políticas integradas de verificación de rutas como `block-read-outside-cwd`, `block-env-files` y `block-secrets-write` se activan sin cambios en las llamadas a herramientas de OpenCode. Las sesiones viven en la base de datos SQLite de opencode en `~/.local/share/opencode/opencode.db`; el visor de sesiones del panel las lee mediante `opencode db --format json` y `opencode export `. El soporte de OpenCode está en **beta** mientras verificamos el comportamiento en distintas versiones y con más sesiones reales. Consulta la [documentación de plugins de OpenCode](https://opencode.ai/docs/plugins/). + - **OpenCode _(beta)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (usuario), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (proyecto) — OpenCode no tiene ámbito `local`. A diferencia de las otras cinco CLIs, OpenCode **no tiene un sistema de hooks de comandos externos**: carga plugins JS/TS en proceso registrados explícitamente mediante el array `plugin: []` en `opencode.json` (el autodescubrimiento desde `.opencode/plugins/` **no** es cómo se cargan los plugins en opencode v1.14.33). La instalación coloca un pequeño shim de plugin generado que llama al binario failproofai como subproceso y traduce la respuesta JSON de forma Claude del binario de vuelta a la semántica del plugin: `throw new Error()` para denegar eventos de herramienta (cancela la llamada a la herramienta), `client.session.prompt(...)` para instruct Y para `Stop` / `SubagentStop` deny (envía el motivo de denegación como el siguiente mensaje del usuario — el único canal de reintento forzado, ya que `session.idle` es solo notificación y lanzar desde él es un no-op), y no-op para allow. El shim canonicaliza tanto los nombres de herramientas (minúsculas → PascalCase mediante `OPENCODE_TOOL_MAP`) como las claves de argumentos de entrada de herramientas (camelCase → snake_case mediante `OPENCODE_TOOL_INPUT_MAP` para `Read` / `Write` / `Edit`, p. ej. `filePath` → `file_path`, `oldString` → `old_string`) antes de reenviar al binario, de modo que las políticas integradas de verificación de rutas como `block-read-outside-cwd`, `block-env-files` y `block-secrets-write` se activan sin cambios en las llamadas a herramientas de OpenCode. Las sesiones viven en la base de datos SQLite de opencode en `~/.local/share/opencode/opencode.db`; el visor de sesiones del panel las lee mediante `opencode db --format json` y `opencode export `. El soporte de OpenCode está en **beta** mientras verificamos el comportamiento en distintas versiones y con más sesiones reales. Consulta la [documentación de plugins de OpenCode](https://opencode.ai/docs/plugins/). - **Pi _(beta)_**: `~/.pi/agent/settings.json` (usuario), `/.pi/settings.json` (proyecto) — Pi no tiene ámbito `local`. Pi carga paquetes de extensiones TypeScript al inicio; el archivo de configuración es un array de cadenas plano `{"packages": ["./relative/path", …]}`. failproofai escribe una única entrada en el array de packages apuntando a su directorio `pi-extension/` integrado. La extensión se suscribe internamente a los eventos `tool_call` / `user_bash` / `input` / `session_start` de Pi y ejecuta `failproofai --hook --cli pi` como proceso hijo; el manejador canonicaliza eventos de snake_case en minúsculas → PascalCase mediante `PI_EVENT_MAP` para que las políticas integradas existentes se activen sin cambios. Los argumentos de entrada de herramientas también se canonizan mediante `PI_TOOL_INPUT_MAP` (Pi's Read / Write / Edit entregan `path` en lugar de `file_path`; mapear la clave de nivel superior permite que `block-env-files` y `block-secrets-write` se activen — `block-read-outside-cwd` ya tenía un respaldo con `path`). El soporte de Pi está en **beta** mientras la API de extensiones de Pi y el diseño del registro de sesiones se estabilizan. - - **Gemini CLI _(beta)_**: `~/.gemini/settings.json` (usuario), `/.gemini/settings.json` (proyecto) — Gemini no tiene ámbito `local` (documenta un ámbito `system` en `/etc/gemini-cli/settings.json` que failproofai no expone). Las entradas de hook usan la forma de Claude `{type, command, timeout}` envuelta en el esquema de matcher de Gemini `{matcher, hooks: [...]}` con `matcher: "*"` por defecto. Los eventos están en PascalCase (`SessionStart`, `BeforeAgent`, `AfterAgent`, `BeforeModel`, `AfterModel`, `BeforeToolSelection`, `BeforeTool`, `AfterTool`, `PreCompress`, `Notification`, `SessionEnd`); el manejador los mapea a nombres canónicos de Claude mediante `GEMINI_EVENT_MAP`. Los nombres de herramientas están en snake_case (`run_shell_command`, `read_file`, `write_file`, `replace`, …) — el manejador los canonicaliza mediante `GEMINI_TOOL_MAP` para que las políticas integradas existentes se activen sin cambios. El evaluador de políticas emite la forma plana de Gemini `{decision: "deny", reason}` (preferida según el contrato exit-0 de la "Regla de Oro" de Gemini), `{hookSpecificOutput: {hookEventName, additionalContext}}` para inyección de contexto en BeforeAgent / AfterTool / SessionStart, y `{decision: "block", reason}` en AfterAgent para semántica de reintento forzado. El soporte de Gemini CLI está en **beta** mientras ampliamos la cobertura en el mundo real. Consulta la [documentación de hooks de Gemini CLI](https://geminicli.com/docs/hooks/). - **Hermes (hermes-agent)**: `~/.hermes/config.yaml` (**solo ámbito de usuario** — Hermes no tiene configuración de proyecto/local). Hermes es una **pasarela** de Slack/Telegram, por lo que una instalación intercepta las llamadas a herramientas de todas las plataformas (Slack/Telegram/cli/cron) **y** de los subagentes internos. Las entradas de hook son un par `{command, timeout}` (tiempo de espera en **segundos**) bajo un mapa `hooks:` indexado por los eventos snake_case de Hermes (`pre_tool_call` / `post_tool_call` / `on_session_start` / `on_session_end` / `subagent_stop`); el manejador canonicaliza los eventos mediante `HERMES_EVENT_MAP` y los nombres de herramientas mediante `HERMES_TOOL_MAP` para que las políticas integradas se activen sin cambios. La configuración se edita mediante un round-trip YAML `Document` que preserva los comentarios, de modo que los demás ajustes del operador sobreviven, y la instalación establece `hooks_auto_accept: true` para que la pasarela sin cabeza (sin TTY) ejecute los hooks sin solicitud de consentimiento. El evaluador emite el contrato stdout `{"decision":"block","reason"}` de Hermes (Hermes ignora los códigos de salida). **Limitaciones:** Hermes no tiene un evento `Stop` de fin de turno, por lo que las políticas integradas `require-*-before-stop` nunca se activan para él (no aplicable, no roto); `instruct` degrada a allow con nota registrada (sin canal de contexto adicional); y la redacción de secretos en la salida (`sanitize-*`) no puede reescribir la salida de herramientas a través del contrato de hook de shell. Hermes es también una fuente de **auditoría** sin conexión — el panel lee sus sesiones de pasarela directamente desde `~/.hermes/state.db`. - **`policies-config.json`** — indica a failproofai qué políticas evaluar y con qué parámetros (compartido entre todas las CLIs de agentes) -Pasa `--cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes` para apuntar a un agente específico (separados por espacios o repetidos para cualquier subconjunto): +Pasa `--cli claude|codex|copilot|cursor|opencode|pi|hermes` para apuntar a un agente específico (separados por espacios o repetidos para cualquier subconjunto): ```bash failproofai policies --install --cli codex --scope project @@ -210,12 +209,11 @@ failproofai policies --install --cli copilot --scope project failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project -failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user -failproofai policies --install --cli claude codex copilot cursor opencode pi gemini +failproofai policies --install --cli claude codex copilot cursor opencode pi ``` -Cuando se omite `--cli`, `failproofai` detecta qué CLIs de agentes están instaladas (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which gemini` / `which hermes`): +Cuando se omite `--cli`, `failproofai` detecta qué CLIs de agentes están instaladas (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which hermes`): - **Una CLI detectada** — la selecciona automáticamente sin solicitar confirmación. - **Múltiples CLIs detectadas** en un terminal interactivo — muestra un prompt de selección única con teclas de flecha, agrupado en una sección `Detected (N)` (con una fila agregada `Install for all N detected` + cada CLI detectada individualmente) y una sección `Not installed (M) · install hooks ahead of time` que lista cada CLI compatible no detectada como opción de instalación anticipada (↑↓ para moverse, Enter para seleccionar, ^C para salir). El flujo de desinstalación muestra solo la sección Detected. diff --git a/docs/es/dashboard.mdx b/docs/es/dashboard.mdx index b08f724b..8cb83822 100644 --- a/docs/es/dashboard.mdx +++ b/docs/es/dashboard.mdx @@ -24,11 +24,11 @@ El dashboard lee directamente desde el sistema de archivos: las carpetas de proy ### Proyectos -Lista todos los proyectos de Claude Code, OpenAI Codex, GitHub Copilot CLI _(beta)_, Cursor Agent _(beta)_, OpenCode _(beta)_, Pi _(beta)_, Gemini CLI _(beta)_ y Hermes encontrados en tu máquina. Los proyectos de Claude se descubren desde `~/.claude/projects/` (o la ruta definida por `CLAUDE_PROJECTS_PATH`); los proyectos de Codex se descubren escaneando todas las transcripciones en `~/.codex/sessions///
/*.jsonl` y agrupándolas por el `cwd` registrado en el primer registro de cada sesión; los proyectos de Copilot CLI se descubren escaneando cada `~/.copilot/session-state//workspace.yaml` (configurable mediante `COPILOT_HOME`) y agrupándolos por su campo `cwd`; los proyectos de Cursor Agent se descubren escaneando metadatos por sesión en `~/.cursor/agent-sessions//` (configurable mediante `CURSOR_HOME`, con `conversations/` y `sessions/` como alternativas de búsqueda) para un escalar `cwd` en `meta.json` / `session.json` / `workspace.yaml`; los proyectos de OpenCode se descubren consultando su base de datos SQLite en `~/.local/share/opencode/opencode.db` mediante `opencode db --format json` (leemos las tablas `session` y `project` y agrupamos por `project_id`); los proyectos de Pi se descubren escaneando transcripciones JSONL por sesión en `~/.pi/agent/sessions//_.jsonl` (configurable mediante `PI_SESSIONS_DIR`) y extrayendo el `cwd` del primer registro de cada sesión; los proyectos de Gemini CLI se descubren escaneando `~/.gemini/tmp//chats/session--.jsonl` (configurable mediante `GEMINI_SESSIONS_DIR`) y recuperando el cwd canónico desde el marcador de texto `.project_root` adyacente; las sesiones del gateway de Hermes se leen directamente desde su almacén SQLite en `~/.hermes/state.db` (configurable mediante `HERMES_DB_PATH`) y se agrupan en proyectos `hermes-` por `source` (Slack/Telegram/cli/cron — las sesiones del gateway no tienen cwd). Un proyecto que haya sido utilizado por múltiples CLIs se muestra como una sola fila con todas las insignias correspondientes. Usa el menú desplegable **CLI** sobre la tabla para filtrar por un agente CLI específico; la URL conserva tu selección como `?cli=claude|codex|copilot|cursor|opencode|pi|gemini|hermes`. +Lista todos los proyectos de Claude Code, OpenAI Codex, GitHub Copilot CLI _(beta)_, Cursor Agent _(beta)_, OpenCode _(beta)_, Pi _(beta)_ y Hermes encontrados en tu máquina. Los proyectos de Claude se descubren desde `~/.claude/projects/` (o la ruta definida por `CLAUDE_PROJECTS_PATH`); los proyectos de Codex se descubren escaneando todas las transcripciones en `~/.codex/sessions///
/*.jsonl` y agrupándolas por el `cwd` registrado en el primer registro de cada sesión; los proyectos de Copilot CLI se descubren escaneando cada `~/.copilot/session-state//workspace.yaml` (configurable mediante `COPILOT_HOME`) y agrupándolos por su campo `cwd`; los proyectos de Cursor Agent se descubren escaneando metadatos por sesión en `~/.cursor/agent-sessions//` (configurable mediante `CURSOR_HOME`, con `conversations/` y `sessions/` como alternativas de búsqueda) para un escalar `cwd` en `meta.json` / `session.json` / `workspace.yaml`; los proyectos de OpenCode se descubren consultando su base de datos SQLite en `~/.local/share/opencode/opencode.db` mediante `opencode db --format json` (leemos las tablas `session` y `project` y agrupamos por `project_id`); los proyectos de Pi se descubren escaneando transcripciones JSONL por sesión en `~/.pi/agent/sessions//_.jsonl` (configurable mediante `PI_SESSIONS_DIR`) y extrayendo el `cwd` del primer registro de cada sesión; las sesiones del gateway de Hermes se leen directamente desde su almacén SQLite en `~/.hermes/state.db` (configurable mediante `HERMES_DB_PATH`) y se agrupan en proyectos `hermes-` por `source` (Slack/Telegram/cli/cron — las sesiones del gateway no tienen cwd). Un proyecto que haya sido utilizado por múltiples CLIs se muestra como una sola fila con todas las insignias correspondientes. Usa el menú desplegable **CLI** sobre la tabla para filtrar por un agente CLI específico; la URL conserva tu selección como `?cli=claude|codex|copilot|cursor|opencode|pi|hermes`. Cada proyecto muestra: - Nombre del proyecto (derivado de la ruta de la carpeta) -- Una insignia de CLI — `Claude Code` (naranja), `OpenAI Codex` (morado), `GitHub Copilot` (azul), `Cursor Agent` (esmeralda), `OpenCode` (ámbar), `Pi` (rosa), `Gemini CLI` (celeste) y/o `Hermes` (índigo) +- Una insignia de CLI — `Claude Code` (naranja), `OpenAI Codex` (morado), `GitHub Copilot` (azul), `Cursor Agent` (esmeralda), `OpenCode` (ámbar), `Pi` (rosa) y/o `Hermes` (índigo) - Fecha de la actividad de sesión más reciente Haz clic en un proyecto para ver sus sesiones. @@ -47,7 +47,7 @@ Haz clic en una sesión para abrir el visor de sesión. ### Visor de sesión -El visor de sesión responde la pregunta clave para los agentes autónomos: ¿qué hizo el agente y se mantuvo en curso? Una insignia de CLI junto al encabezado indica si la sesión es una transcripción de Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi, Gemini CLI o Hermes. Muestra una línea de tiempo de todo lo que ocurrió en una sesión: +El visor de sesión responde la pregunta clave para los agentes autónomos: ¿qué hizo el agente y se mantuvo en curso? Una insignia de CLI junto al encabezado indica si la sesión es una transcripción de Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi o Hermes. Muestra una línea de tiempo de todo lo que ocurrió en una sesión: - **Mensajes** - Respuestas de texto de Claude y prompts del usuario - **Llamadas a herramientas** - Cada herramienta que Claude invocó, con su entrada y salida @@ -55,7 +55,7 @@ El visor de sesión responde la pregunta clave para los agentes autónomos: ¿qu La barra de estadísticas en la parte superior muestra la duración de la sesión, el total de llamadas a herramientas y un resumen de las decisiones de los hooks (recuentos de allow / deny / instruct). -Haz clic en el botón **Descargar Logs** para exportar la sesión. Para sesiones de Claude Code, Codex, Copilot, Cursor, Pi y Gemini obtienes la transcripción JSONL original en disco byte a byte; para OpenCode (cuyas sesiones residen en SQLite, no en disco) obtienes un documento JSON que refleja las tablas subyacentes `session` / `messages` / `parts`. +Haz clic en el botón **Descargar Logs** para exportar la sesión. Para sesiones de Claude Code, Codex, Copilot, Cursor y Pi obtienes la transcripción JSONL original en disco byte a byte; para OpenCode (cuyas sesiones residen en SQLite, no en disco) obtienes un documento JSON que refleja las tablas subyacentes `session` / `messages` / `parts`. ### Auditoría @@ -75,16 +75,16 @@ Una página de dos pestañas para gestionar políticas y revisar actividad. - - Selección múltiple de los CLIs de agentes que failproofai protege desde un único panel — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi, Gemini CLI y Hermes tienen una fila con el estado de instalación (`Active` / `Detected` / `Inactive`), la ruta de configuración de alcance de usuario y un acento de color de marca. Marca o desmarca los CLIs que desees y haz clic en `Apply changes` para instalar/desinstalar la diferencia en un solo paso. Los CLIs cuyo binario se detecta en PATH se marcan previamente. + - Selección múltiple de los CLIs de agentes que failproofai protege desde un único panel — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi y Hermes tienen una fila con el estado de instalación (`Active` / `Detected` / `Inactive`), la ruta de configuración de alcance de usuario y un acento de color de marca. Marca o desmarca los CLIs que desees y haz clic en `Apply changes` para instalar/desinstalar la diferencia en un solo paso. Los CLIs cuyo binario se detecta en PATH se marcan previamente. - Activa o desactiva políticas individuales con un solo clic (escribe en `~/.failproofai/policies-config.json` — compartido entre cada CLI instalado) - Expande una política para configurar sus parámetros (para políticas que admiten `policyParams`) - Establece una ruta de archivo de políticas personalizadas - Historial completo paginado de cada evento de hook que se ha activado en todas las sesiones - - Filtra por decisión, tipo de evento, CLI (Claude Code / OpenAI Codex / GitHub Copilot _(beta)_ / Cursor Agent _(beta)_ / OpenCode _(beta)_ / Pi _(beta)_ / Gemini CLI _(beta)_ / Hermes), nombre de política o ID de sesión - - Cada fila muestra: marca de tiempo, nombre de política, decisión, insignia de CLI (naranja = Claude Code, morado = OpenAI Codex, azul = GitHub Copilot, esmeralda = Cursor Agent, ámbar = OpenCode, rosa = Pi, celeste = Gemini CLI, índigo = Hermes), nombre de herramienta, ID de sesión y el motivo de las decisiones deny/instruct - - Haz clic en un ID de sesión para abrir su transcripción — el visor detecta automáticamente qué CLI activó el hook (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Gemini CLI `~/.gemini/tmp//chats/.jsonl`, Hermes `~/.hermes/state.db`) y renderiza la insignia de CLI correspondiente en el encabezado + - Filtra por decisión, tipo de evento, CLI (Claude Code / OpenAI Codex / GitHub Copilot _(beta)_ / Cursor Agent _(beta)_ / OpenCode _(beta)_ / Pi _(beta)_ / Hermes), nombre de política o ID de sesión + - Cada fila muestra: marca de tiempo, nombre de política, decisión, insignia de CLI (naranja = Claude Code, morado = OpenAI Codex, azul = GitHub Copilot, esmeralda = Cursor Agent, ámbar = OpenCode, rosa = Pi, índigo = Hermes), nombre de herramienta, ID de sesión y el motivo de las decisiones deny/instruct + - Haz clic en un ID de sesión para abrir su transcripción — el visor detecta automáticamente qué CLI activó el hook (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Hermes `~/.hermes/state.db`) y renderiza la insignia de CLI correspondiente en el encabezado diff --git a/docs/es/getting-started.mdx b/docs/es/getting-started.mdx index 51f88320..0ed8095c 100644 --- a/docs/es/getting-started.mdx +++ b/docs/es/getting-started.mdx @@ -37,9 +37,9 @@ bun add -g failproofai failproofai policies --install ``` - Esto escribe entradas de hooks en los CLIs de agentes instalados (el `~/.claude/settings.json` de Claude Code, el `~/.codex/hooks.json` de OpenAI Codex, el `~/.copilot/hooks/failproofai.json` de GitHub Copilot CLI, el `~/.cursor/hooks.json` de Cursor Agent, el shim de plugin generado de OpenCode en `~/.config/opencode/plugins/failproofai.mjs` junto con una entrada de registro en el array `plugin` de `~/.config/opencode/opencode.json`, el `~/.pi/agent/settings.json` de Pi, el `~/.gemini/settings.json` de Gemini CLI, o el `~/.hermes/config.yaml` de Hermes). Si hay más de uno presente, se te pedirá que elijas; pasa `--cli claude codex copilot cursor opencode pi gemini hermes` (cualquier subconjunto) para omitir la pregunta. + Esto escribe entradas de hooks en los CLIs de agentes instalados (el `~/.claude/settings.json` de Claude Code, el `~/.codex/hooks.json` de OpenAI Codex, el `~/.copilot/hooks/failproofai.json` de GitHub Copilot CLI, el `~/.cursor/hooks.json` de Cursor Agent, el shim de plugin generado de OpenCode en `~/.config/opencode/plugins/failproofai.mjs` junto con una entrada de registro en el array `plugin` de `~/.config/opencode/opencode.json`, el `~/.pi/agent/settings.json` de Pi, o el `~/.hermes/config.yaml` de Hermes). Si hay más de uno presente, se te pedirá que elijas; pasa `--cli claude codex copilot cursor opencode pi hermes` (cualquier subconjunto) para omitir la pregunta. - El soporte para GitHub Copilot CLI, Cursor Agent, OpenCode, Pi y Gemini CLI es **beta** — instala con `--cli copilot`, `--cli cursor`, `--cli opencode`, `--cli pi` o `--cli gemini`. Hermes (hermes-agent, una puerta de enlace para Slack/Telegram) se instala con alcance de usuario mediante `--cli hermes` y es **también** una fuente de auditoría sin conexión. + El soporte para GitHub Copilot CLI, Cursor Agent, OpenCode y Pi es **beta** — instala con `--cli copilot`, `--cli cursor`, `--cli opencode` o `--cli pi`. Hermes (hermes-agent, una puerta de enlace para Slack/Telegram) se instala con alcance de usuario mediante `--cli hermes` y es **también** una fuente de auditoría sin conexión. ```bash failproofai policies --install --scope project @@ -48,7 +48,6 @@ bun add -g failproofai failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project - failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user failproofai policies --install block-sudo block-rm-rf sanitize-api-keys ``` diff --git a/docs/es/introduction.mdx b/docs/es/introduction.mdx index b3eabeed..a8a915a7 100644 --- a/docs/es/introduction.mdx +++ b/docs/es/introduction.mdx @@ -5,7 +5,7 @@ description: "FailproofAI ofrece a los agentes de IA 39 políticas de fallo inte [![npm weekly downloads](https://img.shields.io/npm/dw/failproofai?style=flat-square&color=2ea44f)](https://www.npmjs.com/package/failproofai) -Hooks y políticas para **manejo de fallos de IA**, **recuperación de errores** y **fiabilidad de LLM**. Mantén tus agentes de IA fiables y funcionando de forma autónoma en **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Gemini CLI**, **Hermes** y el **Agents SDK**. +Hooks y políticas para **manejo de fallos de IA**, **recuperación de errores** y **fiabilidad de LLM**. Mantén tus agentes de IA fiables y funcionando de forma autónoma en **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Hermes** y el **Agents SDK**. Los agentes de IA fallan de maneras predecibles. Ejecutan comandos destructivos, filtran secretos, se desvían de sus tareas, quedan atrapados en bucles o hacen push directamente a main. Sin supervisión, los pequeños fallos se convierten en interrupciones del servicio, credenciales expuestas y trabajo perdido. diff --git a/docs/fr/built-in-policies.mdx b/docs/fr/built-in-policies.mdx index edefb9c4..8f3bceda 100644 --- a/docs/fr/built-in-policies.mdx +++ b/docs/fr/built-in-policies.mdx @@ -704,7 +704,7 @@ Toutes les politiques de workflow sont **fail-open** : si l'outil requis n'est p ### Sémantique Stop par CLI -L'application du Stop se présente légèrement différemment selon les sept CLI supportés, car chacun expose un contrat de hook "agent terminé" différent. Le **résultat** est le même — l'agent ne peut pas s'arrêter tant qu'une porte de workflow est en échec — mais les **mécanismes** diffèrent. Le tableau ci-dessous résume la situation ; seul Pi présente une particularité visible par l'utilisateur qu'il convient de comprendre avant d'activer une politique `require-*-before-stop`. +L'application du Stop se présente légèrement différemment selon les six CLI supportés, car chacun expose un contrat de hook "agent terminé" différent. Le **résultat** est le même — l'agent ne peut pas s'arrêter tant qu'une porte de workflow est en échec — mais les **mécanismes** diffèrent. Le tableau ci-dessous résume la situation ; seul Pi présente une particularité visible par l'utilisateur qu'il convient de comprendre avant d'activer une politique `require-*-before-stop`. | CLI | Quand la porte se déclenche | Ce que vous voyez | |---|---|---| @@ -712,20 +712,19 @@ L'application du Stop se présente légèrement différemment selon les sept CLI | Codex | Dans la même boucle agent, immédiatement | Identique à Claude. | | GitHub Copilot CLI | Dans la même boucle agent, immédiatement | Identique à Claude (utilise le canal de relance `{decision:"block", reason}` de Copilot — vérifié empiriquement contre Copilot CLI 1.0.41). | | Cursor Agent | Dans la même boucle agent, immédiatement | Identique à Claude (utilise le canal `{followup_message}` de Cursor — limité à `loop_limit`, 5 relances par défaut). | -| Gemini CLI | Dans la même boucle agent, immédiatement | Identique à Claude (utilise le canal `{decision:"block", reason}` de Gemini sur `AfterAgent`). | | OpenCode | Dans la même boucle agent, immédiatement | Identique à Claude (utilise l'appel SDK `client.session.prompt(...)` d'OpenCode acheminé via `hookSpecificOutput.additionalContext`). | | **Pi (pi-coding-agent)** | **Au prochain tour utilisateur** | **Pi s'arrête visiblement** lorsque la porte se déclenche — sa boucle agent se termine et vous êtes renvoyé au prompt. La porte se déclenche ensuite la prochaine fois que vous soumettez un prompt : failproofai ajoute en tête du system prompt de ce tour une directive `MANDATORY ACTION REQUIRED`, demandant au LLM de compléter l'étape de workflow (commit, push, etc.) avant de faire ce que vous avez demandé. | -**Limitation de Pi.** L'`AgentEndEvent` de Pi (l'équivalent upstream du hook `Stop` de Claude) n'a pas de type Result — au moment où il se déclenche, la boucle agent de Pi a déjà terminé. Pi ne peut pas être forcé à relancer la même boucle comme Claude / Copilot / Cursor / Gemini / OpenCode le peuvent. failproofai déplace la porte vers l'événement `before_agent_start` de Pi (qui se déclenche après le prochain prompt utilisateur) afin que la vérification de workflow s'applique quand même, simplement au tour suivant plutôt qu'au tour courant. +**Limitation de Pi.** L'`AgentEndEvent` de Pi (l'équivalent upstream du hook `Stop` de Claude) n'a pas de type Result — au moment où il se déclenche, la boucle agent de Pi a déjà terminé. Pi ne peut pas être forcé à relancer la même boucle comme Claude / Copilot / Cursor / OpenCode le peuvent. failproofai déplace la porte vers l'événement `before_agent_start` de Pi (qui se déclenche après le prochain prompt utilisateur) afin que la vérification de workflow s'applique quand même, simplement au tour suivant plutôt qu'au tour courant. **Ce que cela signifie en pratique :** - Après l'arrêt de Pi, la raison du deny est capturée en mémoire, indexée par l'identifiant de session Pi. Le tout prochain prompt soumis dans le même processus Pi la draine : le LLM voit la directive `MANDATORY ACTION REQUIRED` en tête de son system prompt, effectue le commit (ou le push / ouvre la PR / attend le CI), puis continue avec votre demande. La raison du deny capturée est à usage unique — une fois drainée, la porte est levée. -- La porte est bornée par la durée de vie du processus Pi. Si vous faites `Ctrl+C` sur Pi ou quittez entre deux tours, l'entrée en mémoire est perdue avec le processus et la porte est manquée. Claude, Copilot, Cursor, Gemini et OpenCode ont la même contrainte (tuer l'agent fait manquer la porte) — Pi la rend simplement plus visible car l'agent se termine visiblement avant que la porte ne se déclenche. +- La porte est bornée par la durée de vie du processus Pi. Si vous faites `Ctrl+C` sur Pi ou quittez entre deux tours, l'entrée en mémoire est perdue avec le processus et la porte est manquée. Claude, Copilot, Cursor et OpenCode ont la même contrainte (tuer l'agent fait manquer la porte) — Pi la rend simplement plus visible car l'agent se termine visiblement avant que la porte ne se déclenche. - Un deny en attente est également effacé à `session_shutdown` pour toute raison (`new` / `resume` / `fork` / `quit`), afin qu'une porte périmée d'une session précédente ne puisse pas fuiter dans une nouvelle session démarrée dans le même processus Pi. -Si vous avez besoin d'une relance dans la même boucle à la manière de Claude, exécutez vos politiques `Stop` sous l'un des six autres CLI supportés. Nous suivons l'évolution de Pi en amont pour un futur type Result sur `AgentEndEvent` qui nous permettrait de combler cet écart. +Si vous avez besoin d'une relance dans la même boucle à la manière de Claude, exécutez vos politiques `Stop` sous l'un des cinq autres CLI supportés. Nous suivons l'évolution de Pi en amont pour un futur type Result sur `AgentEndEvent` qui nous permettrait de combler cet écart. ### `require-commit-before-stop` diff --git a/docs/fr/cli/audit.mdx b/docs/fr/cli/audit.mdx index 50e38288..938f30c6 100644 --- a/docs/fr/cli/audit.mdx +++ b/docs/fr/cli/audit.mdx @@ -57,7 +57,7 @@ failproofai `Ctrl+C`. -Le tableau de bord analyse les transcriptions passées de l'agent CLI sur cette machine (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi, Gemini) et indique à quelle fréquence l'agent a effectué des actions que failproofai est conçu pour bloquer — vérifications de variables d'environnement, push forcés, préfixes `cd ` redondants, boucles de polling avec sleep, relecture de fichiers venant d'être édités, et bien d'autres. +Le tableau de bord analyse les transcriptions passées de l'agent CLI sur cette machine (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi) et indique à quelle fréquence l'agent a effectué des actions que failproofai est conçu pour bloquer — vérifications de variables d'environnement, push forcés, préfixes `cd ` redondants, boucles de polling avec sleep, relecture de fichiers venant d'être édités, et bien d'autres. Pour chaque transcription, chaque événement d'utilisation d'outil est rejoué à travers les 39 politiques intégrées **et** à travers 8 détecteurs exclusifs à l'audit qui repèrent des motifs non encore couverts par les politiques en temps réel. Les comptages sont agrégés par politique / détecteur sur l'ensemble des sessions. diff --git a/docs/fr/configuration.mdx b/docs/fr/configuration.mdx index 81c44d79..28427cba 100644 --- a/docs/fr/configuration.mdx +++ b/docs/fr/configuration.mdx @@ -196,13 +196,12 @@ Les commandes `policies --install` et `policies --uninstall` écrivent dans le f - **OpenAI Codex** : `~/.codex/hooks.json` (utilisateur), `/.codex/hooks.json` (projet) — Codex n'a pas de niveau `local` - **GitHub Copilot CLI _(bêta)_** : `~/.copilot/hooks/failproofai.json` (utilisateur), `/.github/hooks/failproofai.json` (projet) — Copilot n'a pas de niveau `local`. Les entrées de hook utilisent les champs de commande `bash`/`powershell` à clé OS de Copilot avec `timeoutSec` ; le fichier porte un marqueur `version: 1` au niveau racine. La prise en charge de Copilot CLI est en **bêta** pendant que nous vérifions le schéma d'enregistrement `events.jsonl` (que la documentation publique ne spécifie pas) contre davantage de sessions réelles. - **Cursor Agent _(bêta)_** : `~/.cursor/hooks.json` (utilisateur), `/.cursor/hooks.json` (projet) — Cursor n'a pas de niveau `local`. Les entrées de hook utilisent la forme `{type, command, timeout}` inspirée de Claude (sans séparation `bash`/`powershell`), mais stockées sous des clés d'événement en camelCase (`preToolUse`, `beforeSubmitPrompt`, …) dans un tableau plat selon le [schéma de hooks](https://cursor.com/docs/hooks) de Cursor ; le fichier porte un marqueur `version: 1` au niveau racine. Le gestionnaire canonicalise camelCase → PascalCase via `CURSOR_EVENT_MAP` afin que les politiques intégrées existantes se déclenchent sans modification. La prise en charge de Cursor Agent est en **bêta** pendant que nous vérifions le format de transcription sur disque de Cursor (non spécifié dans la documentation publique) contre davantage d'installations réelles. - - **OpenCode _(bêta)_** : `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (utilisateur), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (projet) — OpenCode n'a pas de niveau `local`. Contrairement aux six autres CLI, OpenCode **n'a pas de système de hook par commande externe** : il charge des plugins JS/TS en cours de processus explicitement enregistrés via le tableau `plugin: []` dans `opencode.json` (la découverte automatique depuis `.opencode/plugins/` **n'est pas** le mode de chargement des plugins sur opencode v1.14.33). L'installation dépose un petit shim de plugin généré qui appelle le binaire failproofai en sous-processus et traduit la réponse JSON en forme Claude du binaire vers la sémantique du plugin : `throw new Error()` pour le refus d'événement outil (annule l'appel d'outil), `client.session.prompt(...)` pour instruct ET pour le refus `Stop` / `SubagentStop` (soumet le motif de refus comme prochain message utilisateur — le seul canal de nouvelle tentative forcée puisque `session.idle` est uniquement pour les notifications et qu'une exception levée depuis celui-ci est un no-op), et no-op pour allow. Le shim canonicalise les noms d'outils (minuscules → PascalCase via `OPENCODE_TOOL_MAP`) et les clés d'arguments d'entrée d'outil (camelCase → snake_case via `OPENCODE_TOOL_INPUT_MAP` pour `Read` / `Write` / `Edit`, par exemple `filePath` → `file_path`, `oldString` → `old_string`) avant de transmettre au binaire, de sorte que les politiques intégrées de vérification de chemin comme `block-read-outside-cwd`, `block-env-files` et `block-secrets-write` se déclenchent sans modification sur les appels d'outils OpenCode. Les sessions vivent dans la base de données SQLite d'opencode à `~/.local/share/opencode/opencode.db` ; la visionneuse de sessions du tableau de bord les lit via `opencode db --format json` et `opencode export `. La prise en charge d'OpenCode est en **bêta** pendant que nous vérifions le comportement entre les versions et contre davantage de sessions réelles. Consultez la [documentation des plugins OpenCode](https://opencode.ai/docs/plugins/). + - **OpenCode _(bêta)_** : `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (utilisateur), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (projet) — OpenCode n'a pas de niveau `local`. Contrairement aux cinq autres CLI, OpenCode **n'a pas de système de hook par commande externe** : il charge des plugins JS/TS en cours de processus explicitement enregistrés via le tableau `plugin: []` dans `opencode.json` (la découverte automatique depuis `.opencode/plugins/` **n'est pas** le mode de chargement des plugins sur opencode v1.14.33). L'installation dépose un petit shim de plugin généré qui appelle le binaire failproofai en sous-processus et traduit la réponse JSON en forme Claude du binaire vers la sémantique du plugin : `throw new Error()` pour le refus d'événement outil (annule l'appel d'outil), `client.session.prompt(...)` pour instruct ET pour le refus `Stop` / `SubagentStop` (soumet le motif de refus comme prochain message utilisateur — le seul canal de nouvelle tentative forcée puisque `session.idle` est uniquement pour les notifications et qu'une exception levée depuis celui-ci est un no-op), et no-op pour allow. Le shim canonicalise les noms d'outils (minuscules → PascalCase via `OPENCODE_TOOL_MAP`) et les clés d'arguments d'entrée d'outil (camelCase → snake_case via `OPENCODE_TOOL_INPUT_MAP` pour `Read` / `Write` / `Edit`, par exemple `filePath` → `file_path`, `oldString` → `old_string`) avant de transmettre au binaire, de sorte que les politiques intégrées de vérification de chemin comme `block-read-outside-cwd`, `block-env-files` et `block-secrets-write` se déclenchent sans modification sur les appels d'outils OpenCode. Les sessions vivent dans la base de données SQLite d'opencode à `~/.local/share/opencode/opencode.db` ; la visionneuse de sessions du tableau de bord les lit via `opencode db --format json` et `opencode export `. La prise en charge d'OpenCode est en **bêta** pendant que nous vérifions le comportement entre les versions et contre davantage de sessions réelles. Consultez la [documentation des plugins OpenCode](https://opencode.ai/docs/plugins/). - **Pi _(bêta)_** : `~/.pi/agent/settings.json` (utilisateur), `/.pi/settings.json` (projet) — Pi n'a pas de niveau `local`. Pi charge des packages d'extension TypeScript au démarrage ; le fichier de paramètres est un tableau de chaînes plat `{"packages": ["./relative/path", …]}`. failproofai écrit une seule entrée dans le tableau packages pointant vers son répertoire `pi-extension/` intégré. L'extension s'abonne en interne aux événements `tool_call` / `user_bash` / `input` / `session_start` de Pi et exécute `failproofai --hook --cli pi` en shell ; le gestionnaire canonicalise les événements underscore_lower_snake_case → PascalCase via `PI_EVENT_MAP` afin que les politiques intégrées existantes se déclenchent sans modification. Les arguments d'entrée d'outil sont également canonicalisés via `PI_TOOL_INPUT_MAP` (les commandes Read / Write / Edit de Pi livrent `path` plutôt que `file_path` ; mapper la clé de niveau supérieur permet à `block-env-files` et `block-secrets-write` de se déclencher — `block-read-outside-cwd` avait déjà un fallback `path`). La prise en charge de Pi est en **bêta** pendant que l'API d'extension de Pi et la disposition du journal de session se stabilisent. - - **Gemini CLI _(bêta)_** : `~/.gemini/settings.json` (utilisateur), `/.gemini/settings.json` (projet) — Gemini n'a pas de niveau `local` (il documente un niveau `system` à `/etc/gemini-cli/settings.json` que failproofai n'expose pas). Les entrées de hook utilisent la forme `{type, command, timeout}` de Claude enveloppée dans le schéma de correspondance `{matcher, hooks: [...]}` de Gemini avec `matcher: "*"` par défaut. Les événements sont en PascalCase (`SessionStart`, `BeforeAgent`, `AfterAgent`, `BeforeModel`, `AfterModel`, `BeforeToolSelection`, `BeforeTool`, `AfterTool`, `PreCompress`, `Notification`, `SessionEnd`) ; le gestionnaire mappe vers les noms canoniques Claude via `GEMINI_EVENT_MAP`. Les noms d'outils sont en snake_case (`run_shell_command`, `read_file`, `write_file`, `replace`, …) — le gestionnaire canonicalise via `GEMINI_TOOL_MAP` afin que les politiques intégrées existantes se déclenchent sans modification. L'évaluateur de politiques émet la forme plate `{decision: "deny", reason}` de Gemini (recommandée selon la règle d'or exit-0 de Gemini), `{hookSpecificOutput: {hookEventName, additionalContext}}` pour l'injection de contexte sur BeforeAgent / AfterTool / SessionStart, et `{decision: "block", reason}` sur AfterAgent pour la sémantique de nouvelle tentative forcée. La prise en charge de Gemini CLI est en **bêta** pendant que nous élargissons la couverture réelle. Consultez la [documentation des hooks Gemini CLI](https://geminicli.com/docs/hooks/). - **Hermes (hermes-agent)** : `~/.hermes/config.yaml` (**niveau utilisateur uniquement** — Hermes n'a pas de configuration projet/local). Hermes est une **passerelle** Slack/Telegram, donc une seule installation intercepte les appels d'outils de toutes les plateformes (Slack/Telegram/cli/cron) **et** des sous-agents internes. Les entrées de hook sont une paire `{command, timeout}` (timeout en **secondes**) sous une map `hooks:` indexée par les événements snake_case de Hermes (`pre_tool_call` / `post_tool_call` / `on_session_start` / `on_session_end` / `subagent_stop`) ; le gestionnaire canonicalise les événements via `HERMES_EVENT_MAP` et les noms d'outils via `HERMES_TOOL_MAP` afin que les politiques intégrées se déclenchent sans modification. La configuration est modifiée via un aller-retour `Document` YAML préservant les commentaires, de sorte que les autres paramètres de l'opérateur survivent, et l'installation définit `hooks_auto_accept: true` pour que la passerelle sans tête (sans TTY) exécute les hooks sans invite de consentement. L'évaluateur émet le contrat stdout `{"decision":"block","reason"}` de Hermes (Hermes ignore les codes de sortie). **Limitations :** Hermes n'a pas d'événement `Stop` de fin de tour, donc les politiques intégrées `require-*-before-stop` ne se déclenchent jamais pour lui (non applicable, pas cassé) ; `instruct` se dégrade en allow-avec-note-journalisée (pas de canal de contexte supplémentaire) ; et la redaction des secrets en sortie (`sanitize-*`) ne peut pas réécrire la sortie d'outil via le contrat de hook shell. Hermes est **également** une source d'**audit** hors ligne — le tableau de bord lit ses sessions de passerelle directement depuis `~/.hermes/state.db`. - **`policies-config.json`** — indique à failproofai quelles politiques évaluer et avec quels paramètres (partagé entre toutes les CLI agent) -Passez `--cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes` pour cibler un agent spécifique (séparé par des espaces ou répété pour tout sous-ensemble) : +Passez `--cli claude|codex|copilot|cursor|opencode|pi|hermes` pour cibler un agent spécifique (séparé par des espaces ou répété pour tout sous-ensemble) : ```bash failproofai policies --install --cli codex --scope project @@ -210,12 +209,11 @@ failproofai policies --install --cli copilot --scope project failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project -failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user -failproofai policies --install --cli claude codex copilot cursor opencode pi gemini +failproofai policies --install --cli claude codex copilot cursor opencode pi ``` -Lorsque `--cli` est omis, `failproofai` détecte quelles CLI agent sont installées (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which gemini` / `which hermes`) : +Lorsque `--cli` est omis, `failproofai` détecte quelles CLI agent sont installées (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which hermes`) : - **Une CLI détectée** — sélectionne automatiquement cette CLI sans demander de confirmation. - **Plusieurs CLI détectées** dans un terminal interactif — affiche une invite de sélection unique par touches fléchées regroupée en une section `Detected (N)` (avec une ligne agrégée `Install for all N detected` + chaque CLI détectée individuellement) et une section `Not installed (M) · install hooks ahead of time` listant chaque CLI prise en charge non détectée comme option d'installation anticipée (↑↓ pour déplacer, Entrée pour sélectionner, ^C pour quitter). Le flux de désinstallation n'affiche que la section Detected. diff --git a/docs/fr/dashboard.mdx b/docs/fr/dashboard.mdx index 1e76570b..4c13dca2 100644 --- a/docs/fr/dashboard.mdx +++ b/docs/fr/dashboard.mdx @@ -16,7 +16,7 @@ failproofai S'ouvre à l'adresse `http://localhost:8020`. -Le tableau de bord lit directement depuis le système de fichiers — vos dossiers de projets Claude Code et les fichiers de configuration failproofai. Rien n'est écrit vers un service distant. +Le tableau de bord lit les données locales des projets, des sessions et de la configuration failproofai directement depuis le système de fichiers. Les fonctionnalités authentifiées facultatives, telles que les rappels d'audit et les invitations, envoient aux API distantes les informations nécessaires à ces requêtes, notamment les adresses e-mail. --- @@ -24,11 +24,11 @@ Le tableau de bord lit directement depuis le système de fichiers — vos dossie ### Projets -Liste tous les projets Claude Code, OpenAI Codex, GitHub Copilot CLI _(bêta)_, Cursor Agent _(bêta)_, OpenCode _(bêta)_, Pi _(bêta)_, Gemini CLI _(bêta)_ et Hermes trouvés sur votre machine. Les projets Claude sont découverts depuis `~/.claude/projects/` (ou le chemin défini par `CLAUDE_PROJECTS_PATH`) ; les projets Codex sont découverts en analysant chaque transcription sous `~/.codex/sessions///
/*.jsonl` et en les regroupant par le `cwd` enregistré dans le premier enregistrement de chaque session ; les projets Copilot CLI sont découverts en analysant chaque `~/.copilot/session-state//workspace.yaml` (configurable via `COPILOT_HOME`) et en les regroupant par leur champ `cwd` ; les projets Cursor Agent sont découverts en analysant les métadonnées par session sous `~/.cursor/agent-sessions//` (configurable via `CURSOR_HOME`, avec `conversations/` et `sessions/` comme solutions de repli) pour un scalaire `cwd` dans `meta.json` / `session.json` / `workspace.yaml` ; les projets OpenCode sont découverts en interrogeant sa base de données SQLite à `~/.local/share/opencode/opencode.db` via `opencode db --format json` (nous lisons les tables `session` et `project` et les regroupons par `project_id`) ; les projets Pi sont découverts en analysant les transcriptions JSONL par session sous `~/.pi/agent/sessions//_.jsonl` (configurable via `PI_SESSIONS_DIR`) et en extrayant le `cwd` du premier enregistrement de chaque session ; les projets Gemini CLI sont découverts en analysant `~/.gemini/tmp//chats/session--.jsonl` (configurable via `GEMINI_SESSIONS_DIR`) et en récupérant le répertoire de travail canonique depuis le marqueur texte `.project_root` associé ; les sessions de la passerelle Hermes sont lues directement depuis son stockage SQLite à `~/.hermes/state.db` (configurable via `HERMES_DB_PATH`) et regroupées en projets `hermes-` par `source` (Slack/Telegram/cli/cron — les sessions de passerelle n'ont pas de cwd). Un projet utilisé par plusieurs interfaces CLI s'affiche sur une seule ligne avec tous les badges correspondants. Utilisez le menu déroulant **CLI** au-dessus du tableau pour filtrer par interface CLI spécifique ; l'URL conserve votre sélection sous la forme `?cli=claude|codex|copilot|cursor|opencode|pi|gemini|hermes`. +Liste tous les projets Claude Code, OpenAI Codex, GitHub Copilot CLI _(bêta)_, Cursor Agent _(bêta)_, OpenCode _(bêta)_, Pi _(bêta)_ et Hermes trouvés sur votre machine. Les projets Claude sont découverts depuis `~/.claude/projects/` (ou le chemin défini par `CLAUDE_PROJECTS_PATH`) ; les projets Codex sont découverts en analysant chaque transcription sous `~/.codex/sessions///
/*.jsonl` et en les regroupant par le `cwd` enregistré dans le premier enregistrement de chaque session ; les projets Copilot CLI sont découverts en analysant chaque `~/.copilot/session-state//workspace.yaml` (configurable via `COPILOT_HOME`) et en les regroupant par leur champ `cwd` ; les projets Cursor Agent sont découverts en analysant les métadonnées par session sous `~/.cursor/agent-sessions//` (configurable via `CURSOR_HOME`, avec `conversations/` et `sessions/` comme solutions de repli) pour un scalaire `cwd` dans `meta.json` / `session.json` / `workspace.yaml` ; les projets OpenCode sont découverts en interrogeant sa base de données SQLite à `~/.local/share/opencode/opencode.db` via `opencode db --format json` (nous lisons les tables `session` et `project` et les regroupons par `project_id`) ; les projets Pi sont découverts en analysant les transcriptions JSONL par session sous `~/.pi/agent/sessions//_.jsonl` (configurable via `PI_SESSIONS_DIR`) et en extrayant le `cwd` du premier enregistrement de chaque session ; les sessions de la passerelle Hermes sont lues directement depuis son stockage SQLite à `~/.hermes/state.db` (configurable via `HERMES_DB_PATH`) et regroupées en projets `hermes-` par `source` (Slack/Telegram/cli/cron — les sessions de passerelle n'ont pas de cwd). Un projet utilisé par plusieurs interfaces CLI s'affiche sur une seule ligne avec tous les badges correspondants. Utilisez le menu déroulant **CLI** au-dessus du tableau pour filtrer par interface CLI spécifique ; l'URL conserve votre sélection sous la forme `?cli=claude|codex|copilot|cursor|opencode|pi|hermes`. Chaque projet affiche : - Le nom du projet (dérivé du chemin du dossier) -- Un badge CLI — `Claude Code` (orange), `OpenAI Codex` (violet), `GitHub Copilot` (bleu), `Cursor Agent` (émeraude), `OpenCode` (ambre), `Pi` (rose), `Gemini CLI` (bleu ciel) et/ou `Hermes` (indigo) +- Un badge CLI — `Claude Code` (orange), `OpenAI Codex` (violet), `GitHub Copilot` (bleu), `Cursor Agent` (émeraude), `OpenCode` (ambre), `Pi` (rose) et/ou `Hermes` (indigo) - La date de la dernière activité de session Cliquez sur un projet pour afficher ses sessions. @@ -47,7 +47,7 @@ Cliquez sur une session pour ouvrir le visualiseur de session. ### Visualiseur de session -Le visualiseur de session répond à la question essentielle pour les agents autonomes : qu'a fait l'agent, et est-il resté dans les limites prévues ? Un badge CLI à côté de l'en-tête indique si la session est une transcription Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi, Gemini CLI ou Hermes. Il affiche une chronologie de tout ce qui s'est passé lors d'une session : +Le visualiseur de session répond à la question essentielle pour les agents autonomes : qu'a fait l'agent, et est-il resté dans les limites prévues ? Un badge CLI à côté de l'en-tête indique si la session est une transcription Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi ou Hermes. Il affiche une chronologie de tout ce qui s'est passé lors d'une session : - **Messages** - Les réponses textuelles de Claude et les invites utilisateur - **Appels d'outils** - Chaque outil invoqué par Claude, avec ses entrées et sorties @@ -55,7 +55,7 @@ Le visualiseur de session répond à la question essentielle pour les agents aut La barre de statistiques en haut affiche la durée de la session, le nombre total d'appels d'outils et un résumé des décisions de hook (nombre de allow / deny / instruct). -Cliquez sur le bouton **Télécharger les journaux** pour exporter la session. Pour les sessions Claude Code, Codex, Copilot, Cursor, Pi et Gemini, vous obtenez la transcription JSONL originale sur disque octet par octet ; pour OpenCode (dont les sessions résident dans SQLite et non sur disque), vous obtenez un document JSON reproduisant les tables sous-jacentes `session` / `messages` / `parts`. +Cliquez sur le bouton **Télécharger les journaux** pour exporter la session. Pour les sessions Claude Code, Codex, Copilot, Cursor et Pi, vous obtenez la transcription JSONL originale sur disque octet par octet ; pour OpenCode (dont les sessions résident dans SQLite et non sur disque), vous obtenez un document JSON reproduisant les tables sous-jacentes `session` / `messages` / `parts`. ### Audit @@ -75,16 +75,16 @@ Une page à deux onglets pour gérer les politiques et examiner l'activité. - - Sélectionnez plusieurs interfaces CLI d'agents que failproofai protège depuis un seul panneau — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi, Gemini CLI et Hermes disposent chacun d'une ligne avec le statut d'installation (`Active` / `Detected` / `Inactive`), le chemin des paramètres de portée utilisateur et un accent coloré à la marque. Cochez ou décochez les CLI souhaités et cliquez sur `Apply changes` pour installer/désinstaller les différences en une seule étape. Les CLI dont le binaire est détecté dans le PATH sont pré-cochés. + - Sélectionnez plusieurs interfaces CLI d'agents que failproofai protège depuis un seul panneau — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi et Hermes disposent chacun d'une ligne avec le statut d'installation (`Active` / `Detected` / `Inactive`), le chemin des paramètres de portée utilisateur et un accent coloré à la marque. Cochez ou décochez les CLI souhaités et cliquez sur `Apply changes` pour installer/désinstaller les différences en une seule étape. Les CLI dont le binaire est détecté dans le PATH sont pré-cochés. - Activez ou désactivez des politiques individuelles d'un simple clic (écrit dans `~/.failproofai/policies-config.json` — partagé entre tous les CLI installés) - Développez une politique pour configurer ses paramètres (pour les politiques prenant en charge `policyParams`) - Définissez un chemin de fichier de politiques personnalisé - Historique paginé complet de chaque événement de hook déclenché dans toutes les sessions - - Filtrer par décision, type d'événement, CLI (Claude Code / OpenAI Codex / GitHub Copilot _(bêta)_ / Cursor Agent _(bêta)_ / OpenCode _(bêta)_ / Pi _(bêta)_ / Gemini CLI _(bêta)_ / Hermes), nom de politique ou identifiant de session - - Chaque ligne affiche : horodatage, nom de la politique, décision, badge CLI (orange = Claude Code, violet = OpenAI Codex, bleu = GitHub Copilot, émeraude = Cursor Agent, ambre = OpenCode, rose = Pi, bleu ciel = Gemini CLI, indigo = Hermes), nom de l'outil, identifiant de session et la raison des décisions deny/instruct - - Cliquez sur un identifiant de session pour ouvrir sa transcription — le visualiseur détecte automatiquement quel CLI a déclenché le hook (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Gemini CLI `~/.gemini/tmp//chats/.jsonl`, Hermes `~/.hermes/state.db`) et affiche le badge CLI correspondant dans l'en-tête + - Filtrer par décision, type d'événement, CLI (Claude Code / OpenAI Codex / GitHub Copilot _(bêta)_ / Cursor Agent _(bêta)_ / OpenCode _(bêta)_ / Pi _(bêta)_ / Hermes), nom de politique ou identifiant de session + - Chaque ligne affiche : horodatage, nom de la politique, décision, badge CLI (orange = Claude Code, violet = OpenAI Codex, bleu = GitHub Copilot, émeraude = Cursor Agent, ambre = OpenCode, rose = Pi, indigo = Hermes), nom de l'outil, identifiant de session et la raison des décisions deny/instruct + - Cliquez sur un identifiant de session pour ouvrir sa transcription — le visualiseur détecte automatiquement quel CLI a déclenché le hook (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Hermes `~/.hermes/state.db`) et affiche le badge CLI correspondant dans l'en-tête @@ -146,4 +146,4 @@ FAILPROOFAI_ALLOWED_DEV_ORIGINS=dashboard.example.com npm run dev Ceci s'applique uniquement au mode développement. Lorsque vous exécutez `failproofai` (mode production), il n'y a pas de websocket HMR ni de problème de ressource de développement cross-origin. - \ No newline at end of file + diff --git a/docs/fr/getting-started.mdx b/docs/fr/getting-started.mdx index 38af4f5f..41db3c71 100644 --- a/docs/fr/getting-started.mdx +++ b/docs/fr/getting-started.mdx @@ -37,9 +37,9 @@ bun add -g failproofai failproofai policies --install ``` - Cette commande inscrit des entrées de hook dans vos CLIs d'agent installés (le `~/.claude/settings.json` de Claude Code, le `~/.codex/hooks.json` d'OpenAI Codex, le `~/.copilot/hooks/failproofai.json` de GitHub Copilot CLI, le `~/.cursor/hooks.json` de Cursor Agent, le shim de plugin généré par OpenCode à `~/.config/opencode/plugins/failproofai.mjs` ainsi qu'une entrée d'enregistrement dans le tableau `plugin` de `~/.config/opencode/opencode.json`, le `~/.pi/agent/settings.json` de Pi, le `~/.gemini/settings.json` de Gemini CLI, ou le `~/.hermes/config.yaml` de Hermes). Si plusieurs sont présents, une invite s'affichera ; passez `--cli claude codex copilot cursor opencode pi gemini hermes` (tout sous-ensemble) pour ignorer l'invite. + Cette commande inscrit des entrées de hook dans vos CLIs d'agent installés (le `~/.claude/settings.json` de Claude Code, le `~/.codex/hooks.json` d'OpenAI Codex, le `~/.copilot/hooks/failproofai.json` de GitHub Copilot CLI, le `~/.cursor/hooks.json` de Cursor Agent, le shim de plugin généré par OpenCode à `~/.config/opencode/plugins/failproofai.mjs` ainsi qu'une entrée d'enregistrement dans le tableau `plugin` de `~/.config/opencode/opencode.json`, le `~/.pi/agent/settings.json` de Pi, ou le `~/.hermes/config.yaml` de Hermes). Si plusieurs sont présents, une invite s'affichera ; passez `--cli claude codex copilot cursor opencode pi hermes` (tout sous-ensemble) pour ignorer l'invite. - La prise en charge de GitHub Copilot CLI, Cursor Agent, OpenCode, Pi et Gemini CLI est en **bêta** — installez avec `--cli copilot`, `--cli cursor`, `--cli opencode`, `--cli pi` ou `--cli gemini`. Hermes (hermes-agent, une passerelle Slack/Telegram) s'installe en portée utilisateur avec `--cli hermes` et constitue **également** une source d'audit hors ligne. + La prise en charge de GitHub Copilot CLI, Cursor Agent, OpenCode et Pi est en **bêta** — installez avec `--cli copilot`, `--cli cursor`, `--cli opencode` ou `--cli pi`. Hermes (hermes-agent, une passerelle Slack/Telegram) s'installe en portée utilisateur avec `--cli hermes` et constitue **également** une source d'audit hors ligne. ```bash failproofai policies --install --scope project @@ -48,7 +48,6 @@ bun add -g failproofai failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project - failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user failproofai policies --install block-sudo block-rm-rf sanitize-api-keys ``` diff --git a/docs/fr/introduction.mdx b/docs/fr/introduction.mdx index 7bafb798..cca8707d 100644 --- a/docs/fr/introduction.mdx +++ b/docs/fr/introduction.mdx @@ -5,7 +5,7 @@ description: "FailproofAI offre aux agents IA 39 politiques de sécurité intég [![npm weekly downloads](https://img.shields.io/npm/dw/failproofai?style=flat-square&color=2ea44f)](https://www.npmjs.com/package/failproofai) -Hooks et politiques pour la **gestion des défaillances IA**, la **récupération après erreur** et la **fiabilité des LLM**. Gardez vos agents IA fiables et autonomes sur **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Gemini CLI**, **Hermes** et l'**Agents SDK**. +Hooks et politiques pour la **gestion des défaillances IA**, la **récupération après erreur** et la **fiabilité des LLM**. Gardez vos agents IA fiables et autonomes sur **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Hermes** et l'**Agents SDK**. Les agents IA échouent de manière prévisible. Ils exécutent des commandes destructrices, exposent des secrets, dérivent hors de leur tâche, se retrouvent bloqués dans des boucles ou poussent directement sur la branche principale. Laissées sans surveillance, de petites défaillances se transforment en pannes, en identifiants compromis et en travail perdu. diff --git a/docs/getting-started.mdx b/docs/getting-started.mdx index ae2179be..4dd58bc8 100644 --- a/docs/getting-started.mdx +++ b/docs/getting-started.mdx @@ -37,9 +37,9 @@ bun add -g failproofai failproofai policies --install ``` - This writes hook entries into your installed agent CLIs (Claude Code's `~/.claude/settings.json`, OpenAI Codex's `~/.codex/hooks.json`, GitHub Copilot CLI's `~/.copilot/hooks/failproofai.json`, Cursor Agent's `~/.cursor/hooks.json`, OpenCode's generated plugin shim at `~/.config/opencode/plugins/failproofai.mjs` plus a registration entry in `~/.config/opencode/opencode.json`'s `plugin` array, Pi's `~/.pi/agent/settings.json`, Gemini CLI's `~/.gemini/settings.json`, or Hermes's `~/.hermes/config.yaml`). When more than one is present you'll be prompted; pass `--cli claude codex copilot cursor opencode pi gemini hermes` (any subset) to skip the prompt. + This writes hook entries into your installed agent CLIs (Claude Code's `~/.claude/settings.json`, OpenAI Codex's `~/.codex/hooks.json`, GitHub Copilot CLI's `~/.copilot/hooks/failproofai.json`, Cursor Agent's `~/.cursor/hooks.json`, OpenCode's generated plugin shim at `~/.config/opencode/plugins/failproofai.mjs` plus a registration entry in `~/.config/opencode/opencode.json`'s `plugin` array, Pi's `~/.pi/agent/settings.json`, Hermes's `~/.hermes/config.yaml`, OpenClaw's `~/.openclaw/openclaw.json`, Factory Droid's `~/.factory/hooks.json`, Devin CLI's `~/.config/devin/config.json`, Antigravity CLI's `~/.gemini/config/hooks.json`, or Goose's auto-discovered plugin dir at `~/.agents/plugins/failproofai/hooks/hooks.json`). When more than one is present you'll be prompted; pass `--cli claude codex copilot cursor opencode pi hermes openclaw factory devin antigravity goose` (any subset) to skip the prompt. - GitHub Copilot CLI, Cursor Agent, OpenCode, Pi, and Gemini CLI support are **beta** — install with `--cli copilot`, `--cli cursor`, `--cli opencode`, `--cli pi`, or `--cli gemini`. Hermes (hermes-agent, a Slack/Telegram gateway) installs user-scope with `--cli hermes` and is **also** an offline audit source. + GitHub Copilot CLI, Cursor Agent, OpenCode, and Pi support are **beta** — install with `--cli copilot`, `--cli cursor`, `--cli opencode`, or `--cli pi`. Hermes (hermes-agent, a Slack/Telegram gateway) installs user-scope with `--cli hermes` and is **also** an offline audit source. OpenClaw (openclaw gateway, a self-hosted multi-channel assistant) installs user-scope with `--cli openclaw` — enforcement runs through its in-process plugin hooks (`before_agent_finalize` is a real turn-end gate, so the `require-*-before-stop` builtins enforce) — and is **also** an offline audit source. Factory Droid (`droid`) installs with `--cli factory` (user + project scope) and is **also** an offline audit source. Devin CLI (`devin`, Cognition) installs with `--cli devin` (user + project scope) and is **also** an offline audit source. Antigravity CLI (`agy`) installs with `--cli antigravity` (user + project scope) and is **also** an offline audit source. Goose (codename goose, Block) installs with `--cli goose` (user + project scope) — the installer just drops a plugin dir at `~/.agents/plugins/failproofai/` that Goose auto-discovers, and it is **also** an offline audit source. ```bash failproofai policies --install --scope project @@ -48,8 +48,12 @@ bun add -g failproofai failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project - failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user + failproofai policies --install --cli openclaw --scope user + failproofai policies --install --cli factory --scope project + failproofai policies --install --cli devin --scope project + failproofai policies --install --cli antigravity --scope project + failproofai policies --install --cli goose --scope project failproofai policies --install block-sudo block-rm-rf sanitize-api-keys ``` diff --git a/docs/he/built-in-policies.mdx b/docs/he/built-in-policies.mdx index 38dfc8bc..1b418f5d 100644 --- a/docs/he/built-in-policies.mdx +++ b/docs/he/built-in-policies.mdx @@ -710,7 +710,7 @@ failproofai מגיע עם 39 מדיניויות מובנות שתופסות מצ ### סמנטיקה Stop של לפי-CLI -אכיפת Stop נראית שונה במעט על פני שבעת ה-CLI הנתמכים כי לכל אחד יש חוזה hook של סיום סוכן שונה. ה-**outcome** זהה — הסוכן לא יוצא עם עצירה בזמן שער זרימת עבודה נכשל — אך ה-**mechanics** שונים. הטבלה להלן מסכמת; רק Pi יש פרמיה המשתמש בעיר שכדאי להבין לפני שתאפשר מדיניות `require-*-before-stop`. +אכיפת Stop נראית שונה במעט על פני ששת ה-CLI הנתמכים כי לכל אחד יש חוזה hook של סיום סוכן שונה. ה-**outcome** זהה — הסוכן לא יוצא עם עצירה בזמן שער זרימת עבודה נכשל — אך ה-**mechanics** שונים. הטבלה להלן מסכמת; רק Pi יש פרמיה המשתמש בעיר שכדאי להבין לפני שתאפשר מדיניות `require-*-before-stop`. | CLI | כשעוצר השער | מה שאתה רואה | |---|---|---| @@ -718,20 +718,19 @@ failproofai מגיע עם 39 מדיניויות מובנות שתופסות מצ | Codex | אותו לולאת סוכן, מיד | זהה ל-Claude. | | GitHub Copilot CLI | אותו לולאת סוכן, מיד | זהה ל-Claude (משתמש בערוץ ממשכה `{decision:"block", reason}` של Copilot — מאומת אמפיריות נגד Copilot CLI 1.0.41). | | Cursor Agent | אותו לולאת סוכן, מיד | זהה ל-Claude (משתמש בערוץ `{followup_message}` של Cursor — מתוקבל ב-`loop_limit`, ברירת מחדל 5 ניסיונות חוזרים). | -| Gemini CLI | אותו לולאת סוכן, מיד | זהה ל-Claude (משתמש בערוץ `{decision:"block", reason}` של Gemini על `AfterAgent`). | | OpenCode | אותו לולאת סוכן, מיד | זהה ל-Claude (משתמש בקריאת SDK `client.session.prompt(...)` של OpenCode מנוטרלת דרך `hookSpecificOutput.additionalContext`). | | **Pi (pi-coding-agent)** | **תור משתמש הבא** | **Pi עוצר בצורה נראית** כשהשער עוצר — לולאת הסוכן שלו יוצאת ואתה חוזר ללתזמון. השער אז עוצר בפעם הבאה שאתה שולח תזמון: failproofai מקדים הנחיה `MANDATORY ACTION REQUIRED` לתזמון הסיסטם של הסיבוב הזה, נוקט את ה-LLM להשלים את שלב זרימת העבודה (commit, push, וכו') לפני עשיית מה שביקשת. | -**מגבלה של Pi.** `AgentEndEvent` של Pi (המקבילה הזרם של אירוע `Stop` של Claude) אין סוג תוצאה — ברגע שהוא עוצר, לולאת הסוכן של Pi כבר יצאה. Pi לא יכול להילחם להחזור לאותה לולאה בדרך שClaude / Copilot / Cursor / Gemini / OpenCode יכולים. failproofai מעביר את השער לאירוע `before_agent_start` של Pi (שעוצר אחרי התזמון הבא של המשתמש) כדי שבדיקת זרימת העבודה עדיין אוכפת, רק בסיבוב הבא במקום הנוכחי. +**מגבלה של Pi.** `AgentEndEvent` של Pi (המקבילה הזרם של אירוע `Stop` של Claude) אין סוג תוצאה — ברגע שהוא עוצר, לולאת הסוכן של Pi כבר יצאה. Pi לא יכול להילחם להחזור לאותה לולאה בדרך שClaude / Copilot / Cursor / OpenCode יכולים. failproofai מעביר את השער לאירוע `before_agent_start` של Pi (שעוצר אחרי התזמון הבא של המשתמש) כדי שבדיקת זרימת העבודה עדיין אוכפת, רק בסיבוב הבא במקום הנוכחי. **מה זה אומר בפרקטיקה:** - אחרי שPi עוצר, סיבת ה-deny נתפסה בזיכרון מפתח אחרי מזהה סשן Pi. התזמון הבא בדיוק שאתה שולח באותו תהליך Pi סוך את זה: ה-LLM רואה את ההנחיה `MANDATORY ACTION REQUIRED` בחלק העליון של תזמון הסיסטם שלו, commits (או דוחף / פותח את ה-PR / מחכה ל-CI), רק אחרי כן ממשיך עם בקשתך. סיבת ה-deny שתפסה היא one-shot — ברגע שסוך, השער ברור. -- השער מותחם על ידי חיי תהליך של Pi. אם `Ctrl+C` Pi או יוצא בין סיבובים, הערך בזיכרון בזיכרון מושלך יחד עם התהליך ואת השער מיסה. Claude, Copilot, Cursor, Gemini ו-OpenCode יש את אותו כולמן (הרוג את הסוכן ואת השער מיסה) — Pi פשוט עושה את זה יותר נראית כי הסוכן יוצא בצורה נראית לפני ש-פיר עוצר. +- השער מותחם על ידי חיי תהליך של Pi. אם `Ctrl+C` Pi או יוצא בין סיבובים, הערך בזיכרון בזיכרון מושלך יחד עם התהליך ואת השער מיסה. Claude, Copilot, Cursor ו-OpenCode יש את אותו כולמן (הרוג את הסוכן ואת השער מיסה) — Pi פשוט עושה את זה יותר נראית כי הסוכן יוצא בצורה נראית לפני ש-פיר עוצר. - deny ממתין הוא גם ברור על `session_shutdown` לכל סיבה (`new` / `resume` / `fork` / `quit`), כך שערט שנגרד מסשן קודם לא יכול לדלוף לסשן טרי שהחל באותו תהליך Pi. -אם אתה צריך retry זהה לClaude של אותו לולאה, הרץ את המדיניויות `Stop` שלך תחת כל אחד משש ה-CLI הנתמכים האחרים. אנחנו עוקבים אחרי Pi קודם לעתיד סוג תוצאה על `AgentEndEvent` שיתן לנו לסגור את הפער הזה. +אם אתה צריך retry זהה לClaude של אותו לולאה, הרץ את המדיניויות `Stop` שלך תחת כל אחד מחמש ה-CLI הנתמכים האחרים. אנחנו עוקבים אחרי Pi קודם לעתיד סוג תוצאה על `AgentEndEvent` שיתן לנו לסגור את הפער הזה. ### `require-commit-before-stop` diff --git a/docs/he/cli/audit.mdx b/docs/he/cli/audit.mdx index b77abfba..d4abe139 100644 --- a/docs/he/cli/audit.mdx +++ b/docs/he/cli/audit.mdx @@ -52,7 +52,7 @@ failproofai עד שתעצור אותו עם `Ctrl+C`. -לוח המחוונים סורק תמלילי agent CLI קודמים במכונה זו (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi, Gemini) ודיווח כמה פעמים הסוכן עשה דברים שמבנה failproofai כדי לעצור — בדיקות משתנות סביבה, כפיות דחיפה, קידומות `cd ` מיותרות, לולאות sleep-polling, קריאה חוזרת של קבצים שנערכו זה עתה, ועוד. +לוח המחוונים סורק תמלילי agent CLI קודמים במכונה זו (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi) ודיווח כמה פעמים הסוכן עשה דברים שמבנה failproofai כדי לעצור — בדיקות משתנות סביבה, כפיות דחיפה, קידומות `cd ` מיותרות, לולאות sleep-polling, קריאה חוזרת של קבצים שנערכו זה עתה, ועוד. לכל תמליל, כל אירוע tool-use משמר מחדש דרך 39 המדיניויות המובנות **וגם** דרך 8 גלאים audit-only שתופסים דפוסים שעדיין לא מכוסים על ידי מדיניויות זמן ריצה. הספירות מצטברות לכל מדיניות / גלאי על פני כל הפעלות. diff --git a/docs/he/configuration.mdx b/docs/he/configuration.mdx index 24e3cda4..f1f81270 100644 --- a/docs/he/configuration.mdx +++ b/docs/he/configuration.mdx @@ -196,13 +196,12 @@ resolved: { allowPatterns: ["sudo systemctl status"] } ← יורד לכלל ה - **OpenAI Codex**: `~/.codex/hooks.json` (משתמש), `/.codex/hooks.json` (פרויקט) — Codex אין לו היקף `local` - **GitHub Copilot CLI _(beta)_**: `~/.copilot/hooks/failproofai.json` (משתמש), `/.github/hooks/failproofai.json` (פרויקט) — Copilot אין לו היקף `local`. רשומות Hook משתמשות בשדות פקודה `bash`/`powershell` של Copilot עם מפתח OS עם `timeoutSec`; הקובץ נושא סימן `version: 1` ברמה העליונה. תמיכת Copilot CLI היא **beta** בעודנו מאמתים את סכימת רשומת `events.jsonl` (שהדוקים הציבוריים אינם מציינים) מול יותר ישיבות בעולם האמיתי. - **Cursor Agent _(beta)_**: `~/.cursor/hooks.json` (משתמש), `/.cursor/hooks.json` (פרויקט) — Cursor אין לו היקף `local`. רשומות Hook משתמשות בצורה בעיצוב Claude `{type, command, timeout}` (לא פיצול `bash`/`powershell`), אך מאוחסנות תחת מפתחות אירוע camelCase (`preToolUse`, `beforeSubmitPrompt`, …) במערך שטוח לכל [סכימת hooks של Cursor](https://cursor.com/docs/hooks); הקובץ נושא סימן `version: 1` ברמה העליונה. המטפל מנרמל camelCase → PascalCase דרך `CURSOR_EVENT_MAP` כך שמדיניות מובנות קיימות נשרפות ללא שינוי. תמיכת Cursor Agent היא **beta** בעודנו מאמתים את קובץ הטרנסקריפט של Cursor (לא מצוין בדוקים הציבוריים) מול יותר התקנות בעולם האמיתי. - - **OpenCode _(beta)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (משתמש), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (פרויקט) — OpenCode אין לו היקף `local`. בשונה משש ה-CLI האחרים, OpenCode **אין לו מערכת hook פקודה חיצונית**: הוא טוען בתוך התהליך תוספי JS/TS שנרשמו במפורש דרך מערך `plugin: []` ב`opencode.json` (גילוי אוטומטי מ`.opencode/plugins/` **אינו** כיצד תוספים נטענים ב-opencode v1.14.33). ההתקנה מושכת שים של תוספון שנוצר בזריעה שקוראות ב-subprocess את הבינארי failproofai ומתרגמת את התגובה JSON בעיצוב Claude של הבינארי חזרה לסמנטיקה של תוספון: `throw new Error()` עבור deny של אירוע כלי (מבטל את קריאת הכלי), `client.session.prompt(...)` עבור instruct וגם עבור deny של `Stop` / `SubagentStop` (משדרת את סיבת הreject כהודעת המשתמש הבאה — הערוץ היחיד כלומל-retry מכיוון ש`session.idle` הוא התראה בלבד ו-throwing ממנו הוא no-op), ו-no-op עבור allow. השם מנרמל גם שמות כלים (אותיות קטנות → PascalCase דרך `OPENCODE_TOOL_MAP`) וגם מפתחות טיעון קלט כלי (camelCase → snake_case דרך `OPENCODE_TOOL_INPUT_MAP` עבור `Read` / `Write` / `Edit`, לדוגמה `filePath` → `file_path`, `oldString` → `old_string`) לפני שליחה קדימה לבינארי, כך שבדיקות נתיב מובנות כמו `block-read-outside-cwd`, `block-env-files`, ו`block-secrets-write` נשרפות ללא שינוי בקריאות כלים OpenCode. ישיבות חיות בבסיס נתונים SQLite של opencode ב`~/.local/share/opencode/opencode.db`; צופה הישיבה של הדשבורד קורא להם דרך `opencode db --format json` ו`opencode export `. תמיכת OpenCode היא **beta** בעודנו מאמתים התנהגות על פני גרסאות ומול יותר ישיבות בעולם האמיתי. ראה את [דוקים של תוספי OpenCode](https://opencode.ai/docs/plugins/). + - **OpenCode _(beta)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (משתמש), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (פרויקט) — OpenCode אין לו היקף `local`. בשונה מחמש ה-CLI האחרים, OpenCode **אין לו מערכת hook פקודה חיצונית**: הוא טוען בתוך התהליך תוספי JS/TS שנרשמו במפורש דרך מערך `plugin: []` ב`opencode.json` (גילוי אוטומטי מ`.opencode/plugins/` **אינו** כיצד תוספים נטענים ב-opencode v1.14.33). ההתקנה מושכת שים של תוספון שנוצר בזריעה שקוראות ב-subprocess את הבינארי failproofai ומתרגמת את התגובה JSON בעיצוב Claude של הבינארי חזרה לסמנטיקה של תוספון: `throw new Error()` עבור deny של אירוע כלי (מבטל את קריאת הכלי), `client.session.prompt(...)` עבור instruct וגם עבור deny של `Stop` / `SubagentStop` (משדרת את סיבת הreject כהודעת המשתמש הבאה — הערוץ היחיד כלומל-retry מכיוון ש`session.idle` הוא התראה בלבד ו-throwing ממנו הוא no-op), ו-no-op עבור allow. השם מנרמל גם שמות כלים (אותיות קטנות → PascalCase דרך `OPENCODE_TOOL_MAP`) וגם מפתחות טיעון קלט כלי (camelCase → snake_case דרך `OPENCODE_TOOL_INPUT_MAP` עבור `Read` / `Write` / `Edit`, לדוגמה `filePath` → `file_path`, `oldString` → `old_string`) לפני שליחה קדימה לבינארי, כך שבדיקות נתיב מובנות כמו `block-read-outside-cwd`, `block-env-files`, ו`block-secrets-write` נשרפות ללא שינוי בקריאות כלים OpenCode. ישיבות חיות בבסיס נתונים SQLite של opencode ב`~/.local/share/opencode/opencode.db`; צופה הישיבה של הדשבורד קורא להם דרך `opencode db --format json` ו`opencode export `. תמיכת OpenCode היא **beta** בעודנו מאמתים התנהגות על פני גרסאות ומול יותר ישיבות בעולם האמיתי. ראה את [דוקים של תוספי OpenCode](https://opencode.ai/docs/plugins/). - **Pi _(beta)_**: `~/.pi/agent/settings.json` (משתמש), `/.pi/settings.json` (פרויקט) — Pi אין לו היקף `local`. Pi טוען חבילות הרחבה TypeScript בעת ההתחלה; קובץ ההגדרות הוא מערך מחרוזות שטוח `{"packages": ["./relative/path", …]}`. failproofai כותב רשומת מערך פקיות אחת המצביעה על תיקיית `pi-extension/` המכוסה שלה. ההרחבה מחתום פנימית על אירועי `tool_call` / `user_bash` / `input` / `session_start` של Pi ופורקת `failproofai --hook --cli pi`; המטפל מנרמל underscore_lower_snake_case → PascalCase דרך `PI_EVENT_MAP` כך שמדיניות מובנות קיימות נשרפות ללא שינוי. טיעון קלט כלי מנורמל גם דרך `PI_TOOL_INPUT_MAP` (Read / Write / Edit של Pi משדרות `path` ולא `file_path`; מיפוי המפתח ברמה העליונה מאפשר ל`block-env-files` ו`block-secrets-write` נשרפות — `block-read-outside-cwd` כבר היה פנייה חוזרת של `path`). תמיכת Pi היא **beta** בזמן שה-API הרחבה של Pi וסכימת יומן הישיבה מתייצבות. - - **Gemini CLI _(beta)_**: `~/.gemini/settings.json` (משתמש), `/.gemini/settings.json` (פרויקט) — Gemini אין לו היקף `local` (הוא מתעד היקף `system` ב`/etc/gemini-cli/settings.json` שfalieproofai אינו חושף). רשומות Hook משתמשות בצורה `{type, command, timeout}` של Claude מגולפת בסכימת matcher של Gemini `{matcher, hooks: [...]}` עם `matcher: "*"` כברירת מחדל. אירועים הם PascalCase (`SessionStart`, `BeforeAgent`, `AfterAgent`, `BeforeModel`, `AfterModel`, `BeforeToolSelection`, `BeforeTool`, `AfterTool`, `PreCompress`, `Notification`, `SessionEnd`); המטפל ממפה לשמות הקנוניים של Claude דרך `GEMINI_EVENT_MAP`. שמות כלים הם snake_case (`run_shell_command`, `read_file`, `write_file`, `replace`, …) — המטפל מנרמל דרך `GEMINI_TOOL_MAP` כך שמדיניות מובנות קיימות נשרפות ללא שינוי. מעריך המדיניות משדרת צורה שטוחה של Gemini `{decision: "deny", reason}` (מעדיפה לפי הכלל הזהב של Gemini exit-0), `{hookSpecificOutput: {hookEventName, additionalContext}}` להזרקת הקשר על BeforeAgent / AfterTool / SessionStart, ו`{decision: "block", reason}` על AfterAgent עבור סמנטיקת כפיית retry. תמיכת Gemini CLI היא **beta** בעודנו מרחיבים כיסוי בעולם אמיתי. ראה את [דוקים של hooks של Gemini CLI](https://geminicli.com/docs/hooks/). - **Hermes (hermes-agent)**: `~/.hermes/config.yaml` (**היקף משתמש בלבד** — Hermes אין לו תצורת פרויקט/מקומית). Hermes הוא **שער** Slack/Telegram, כך שהתקנה אחת מיירטת קריאות כלים מכל פלטפורמה (Slack/Telegram/cli/cron) **וגם** תוך-אג'נטים. רשומות Hook הן זוג `{command, timeout}` (timeout בשניות) תחת מפת `hooks:` שבאופן events snake_case של Hermes (`pre_tool_call` / `post_tool_call` / `on_session_start` / `on_session_end` / `subagent_stop`); המטפל מנרמל אירועים דרך `HERMES_EVENT_MAP` וגם שמות כלים דרך `HERMES_TOOL_MAP` כך שמדיניות מובנות נשרפות ללא שינוי. התצורה נערכת דרך תעודת YAML דורכת-שומרת-הערות `Document` כך שהגדרות אחרות של המנהל שורדות, והתקנה מוגדרת `hooks_auto_accept: true` כך השער חסר-TTY מריץ את ה-hooks ללא בקשת הסכמה. המעריך משדרת חוזה `{"decision":"block","reason"}` stdout של Hermes (Hermes מתעלם מקודי יציאה). **מגבלות:** Hermes אין להשקע stop `Stop` של turn-end, כך שה`require-*-before-stop` מובנה לעולם לא נשרפים עבורו (בלא הגבלה, לא שבור); `instruct` מדרדר ל-allow-עם-logged-note (אין ערוץ הקשר נוסף); ו-redaction-secret של פלט (`sanitize-*`) לא יכול לשכתב פלט כלים דרך חוזה shell-hook. Hermes הוא **גם** מקור ביקורת **offline** — הדשבורד קורא ישיבות שער שלו ישירות מ`~/.hermes/state.db`. - **`policies-config.json`** — אומר ל-failproofai איזו מדיניות להעריך ועם אילו פרמטרים (משותף על פני כל ה-CLI של ה-agent) -עבור `--cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes` למטרה סוכן ספציפי (space-separated או חוזר לכל תת-קבוצה): +עבור `--cli claude|codex|copilot|cursor|opencode|pi|hermes` למטרה סוכן ספציפי (space-separated או חוזר לכל תת-קבוצה): ```bash failproofai policies --install --cli codex --scope project @@ -210,12 +209,11 @@ failproofai policies --install --cli copilot --scope project failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project -failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user -failproofai policies --install --cli claude codex copilot cursor opencode pi gemini +failproofai policies --install --cli claude codex copilot cursor opencode pi ``` -כאשר `--cli` מושמט, `failproofai` מגלה אילו CLI של agent מותקנות (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which gemini` / `which hermes`): +כאשר `--cli` מושמט, `failproofai` מגלה אילו CLI של agent מותקנות (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which hermes`): - **CLI אחד זוהה** — בחירה אוטומטית של ה-CLI הזה ללא הנחיות. - **מספר CLI זוהו** בטרמינל אינטראקטיבי — מציג הנחיות בחירה חד-בחירה בחצי-מקלדת מקובצות לחלק `Detected (N)` (עם שורה מצומצמת `Install for all N detected` + כל CLI זוהה בנפרד) וחלק `Not installed (M) · install hooks ahead of time` הרשום כל CLI לא זוהה שנתמך כאפשרות התקנה קדימה (↑↓ להזיז, Enter בחר, ^C יצא). זרימת ההסרה מציגה רק את החלק Detected. diff --git a/docs/he/dashboard.mdx b/docs/he/dashboard.mdx index 50ffc990..b3591357 100644 --- a/docs/he/dashboard.mdx +++ b/docs/he/dashboard.mdx @@ -25,11 +25,11 @@ failproofai ### Projects -מפרט את כל Claude Code, OpenAI Codex, GitHub Copilot CLI _(beta)_, Cursor Agent _(beta)_, OpenCode _(beta)_, Pi _(beta)_, Gemini CLI _(beta)_, ו-Hermes שנמצאו במחשב שלך. פרויקטי Claude מגולים מ-`~/.claude/projects/` (או הנתיב שנקבע על ידי `CLAUDE_PROJECTS_PATH`); פרויקטי Codex מגולים על ידי סריקת כל תמליל תחת `~/.codex/sessions///
/*.jsonl` וקיבוץ לפי `cwd` שנרשם בתיעוד הראשון של ההפעלה; פרויקטי Copilot CLI מגולים על ידי סריקת כל `~/.copilot/session-state//workspace.yaml` (ניתן להגדיר דרך `COPILOT_HOME`) וקיבוץ לפי שדה `cwd` שלו; פרויקטי Cursor Agent מגולים על ידי סריקת מטא-נתונים לכל הפעלה תחת `~/.cursor/agent-sessions//` (ניתן להגדיר דרך `CURSOR_HOME`, עם סדרי גיבוי `conversations/` ו-`sessions/`) עבור סקלר `cwd` ב-`meta.json` / `session.json` / `workspace.yaml`; פרויקטי OpenCode מגולים על ידי שאילתת SQLite DB שלה ב-`~/.local/share/opencode/opencode.db` דרך `opencode db --format json` (אנו קוראים את טבלאות `session` ו-`project` וקיבוץ לפי `project_id`); פרויקטי Pi מגולים על ידי סריקת תמליל JSONL לכל הפעלה תחת `~/.pi/agent/sessions//_.jsonl` (ניתן להגדיר דרך `PI_SESSIONS_DIR`) ושליפת `cwd` מתיעוד הראשון של כל הפעלה; פרויקטי Gemini CLI מגולים על ידי סריקת `~/.gemini/tmp//chats/session--.jsonl` (ניתן להגדיר דרך `GEMINI_SESSIONS_DIR`) והחזרת ה-cwd הקנוני ממסמן `.project_root` הסמוך; הפעלות שער Hermes נקראות ישירות מחנות SQLite שלה ב-`~/.hermes/state.db` (ניתן להגדיר דרך `HERMES_DB_PATH`) וקיבוץ לפרויקטי `hermes-` לפי `source` (Slack/Telegram/cli/cron — להפעלות שער אין cwd). פרויקט ששימש מספר CLIs מרואה כשורה יחידה עם כל התגים המתאימים. השתמש בתפריט **CLI** מעל הטבלה לסינון לפי סוכן CLI ספציפי; ה-URL משמר את הבחירה שלך כ-`?cli=claude|codex|copilot|cursor|opencode|pi|gemini|hermes`. +מפרט את כל Claude Code, OpenAI Codex, GitHub Copilot CLI _(beta)_, Cursor Agent _(beta)_, OpenCode _(beta)_, Pi _(beta)_, ו-Hermes שנמצאו במחשב שלך. פרויקטי Claude מגולים מ-`~/.claude/projects/` (או הנתיב שנקבע על ידי `CLAUDE_PROJECTS_PATH`); פרויקטי Codex מגולים על ידי סריקת כל תמליל תחת `~/.codex/sessions///
/*.jsonl` וקיבוץ לפי `cwd` שנרשם בתיעוד הראשון של ההפעלה; פרויקטי Copilot CLI מגולים על ידי סריקת כל `~/.copilot/session-state//workspace.yaml` (ניתן להגדיר דרך `COPILOT_HOME`) וקיבוץ לפי שדה `cwd` שלו; פרויקטי Cursor Agent מגולים על ידי סריקת מטא-נתונים לכל הפעלה תחת `~/.cursor/agent-sessions//` (ניתן להגדיר דרך `CURSOR_HOME`, עם סדרי גיבוי `conversations/` ו-`sessions/`) עבור סקלר `cwd` ב-`meta.json` / `session.json` / `workspace.yaml`; פרויקטי OpenCode מגולים על ידי שאילתת SQLite DB שלה ב-`~/.local/share/opencode/opencode.db` דרך `opencode db --format json` (אנו קוראים את טבלאות `session` ו-`project` וקיבוץ לפי `project_id`); פרויקטי Pi מגולים על ידי סריקת תמליל JSONL לכל הפעלה תחת `~/.pi/agent/sessions//_.jsonl` (ניתן להגדיר דרך `PI_SESSIONS_DIR`) ושליפת `cwd` מתיעוד הראשון של כל הפעלה; הפעלות שער Hermes נקראות ישירות מחנות SQLite שלה ב-`~/.hermes/state.db` (ניתן להגדיר דרך `HERMES_DB_PATH`) וקיבוץ לפרויקטי `hermes-` לפי `source` (Slack/Telegram/cli/cron — להפעלות שער אין cwd). פרויקט ששימש מספר CLIs מרואה כשורה יחידה עם כל התגים המתאימים. השתמש בתפריט **CLI** מעל הטבלה לסינון לפי סוכן CLI ספציפי; ה-URL משמר את הבחירה שלך כ-`?cli=claude|codex|copilot|cursor|opencode|pi|hermes`. כל פרויקט מציג: - שם הפרויקט (מגובש מנתיב התיקייה) -- תג CLI — `Claude Code` (כתום), `OpenAI Codex` (סגול), `GitHub Copilot` (כחול), `Cursor Agent` (אזמרגד), `OpenCode` (ענבר), `Pi` (ורוד), `Gemini CLI` (שמיים), ו/או `Hermes` (אינדיגו) +- תג CLI — `Claude Code` (כתום), `OpenAI Codex` (סגול), `GitHub Copilot` (כחול), `Cursor Agent` (אזמרגד), `OpenCode` (ענבר), `Pi` (ורוד), ו/או `Hermes` (אינדיגו) - תאריך של פעילות הפעלה האחרונה לחץ על פרויקט כדי לראות את הפעלות שלו. @@ -48,7 +48,7 @@ failproofai ### מצפה הפעלה -מצפה הפעלה עונה על השאלה המרכזית לסוכנים אוטונומיים: מה עשה הסוכן, והאם הוא נשאר בעקוב? תג CLI ליד הכותרת מציין אם ההפעלה היא Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi, Gemini CLI, או תמליל Hermes. הוא מציג ציר הזמן של כל מה שקרה בהפעלה: +מצפה הפעלה עונה על השאלה המרכזית לסוכנים אוטונומיים: מה עשה הסוכן, והאם הוא נשאר בעקוב? תג CLI ליד הכותרת מציין אם ההפעלה היא Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi, או תמליל Hermes. הוא מציג ציר הזמן של כל מה שקרה בהפעלה: - **הודעות** - תגובות טקסט של Claude והנחיות משתמש - **קריאות כלים** - כל כלי שקרא Claude, עם הקלט והפלט שלו @@ -56,7 +56,7 @@ failproofai סרגל הנתונים בחלק העליון מציג משך הפעלה, סה"כ קריאות כלים, וסיכום החלטות קורא (ספירות allow / deny / instruct). -לחץ על כפתור **Download Logs** כדי לייצא את ההפעלה. עבור Claude Code, Codex, Copilot, Cursor, Pi, ו-Gemini תקבל את תמליל JSONL המקורי על הדיסק בדיוק כפי שהוא; עבור OpenCode (שהפעלות שלה חיות ב-SQLite, לא על הדיסק) תקבל מסמך JSON המשקף את טבלאות `session` / `messages` / `parts` הבסיסיות. +לחץ על כפתור **Download Logs** כדי לייצא את ההפעלה. עבור Claude Code, Codex, Copilot, Cursor, ו-Pi תקבל את תמליל JSONL המקורי על הדיסק בדיוק כפי שהוא; עבור OpenCode (שהפעלות שלה חיות ב-SQLite, לא על הדיסק) תקבל מסמך JSON המשקף את טבלאות `session` / `messages` / `parts` הבסיסיות. ### Audit @@ -76,16 +76,16 @@ failproofai - - בחר מספר כלים את סוכני ה-CLI שעליהם failproofai מגן מלוח יחיד — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi, Gemini CLI, ו-Hermes כולם בעלי שורה עם סטטוס התקנה (`Active` / `Detected` / `Inactive`), נתיב הגדרות הטווח השנייה משתמש, והדגש צבע ממותג. בדוק או בטל את בדיקת ה-CLIs שאתה רוצה לחץ על `Apply changes` כדי להתקין/הסר את ההבדל בשלב אחד. CLIs שהבינארית שלהם מגולה ב-PATH מסומנים מראש. + - בחר מספר כלים את סוכני ה-CLI שעליהם failproofai מגן מלוח יחיד — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi, ו-Hermes כולם בעלי שורה עם סטטוס התקנה (`Active` / `Detected` / `Inactive`), נתיב הגדרות הטווח השנייה משתמש, והדגש צבע ממותג. בדוק או בטל את בדיקת ה-CLIs שאתה רוצה לחץ על `Apply changes` כדי להתקין/הסר את ההבדל בשלב אחד. CLIs שהבינארית שלהם מגולה ב-PATH מסומנים מראש. - כיבוי או הדלקת מדיניות בודדות בלחיצה יחידה (כתיבה ל-`~/.failproofai/policies-config.json` — משותפת לכל CLI מותקן) - הרחב מדיניות כדי להגדיר את הפרמטרים שלה (עבור מדיניות התומכות `policyParams`) - קבע נתיב קובץ מדיניות מותאם - מלא היסטוריית עמודים של כל אירוע קורא שנשדר על כל הפעלות - - סנן לפי החלטה, סוג אירוע, CLI (Claude Code / OpenAI Codex / GitHub Copilot _(beta)_ / Cursor Agent _(beta)_ / OpenCode _(beta)_ / Pi _(beta)_ / Gemini CLI _(beta)_ / Hermes), שם מדיניות, או מזהה הפעלה - - כל שורה מציגה: חותמת זמן, שם מדיניות, החלטה, תג CLI (כתום = Claude Code, סגול = OpenAI Codex, כחול = GitHub Copilot, אזמרגד = Cursor Agent, ענבר = OpenCode, ורוד = Pi, שמיים = Gemini CLI, אינדיגו = Hermes), שם כלים, מזהה הפעלה, והסיבה להחלטות deny/instruct - - לחץ על מזהה הפעלה כדי לפתוח את התמליל שלה — מצפה גילוי אוטומטי אילו CLI נשדרה קורא (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Gemini CLI `~/.gemini/tmp//chats/.jsonl`, Hermes `~/.hermes/state.db`) ומעניק את תג ה-CLI התואם בכותרת + - סנן לפי החלטה, סוג אירוע, CLI (Claude Code / OpenAI Codex / GitHub Copilot _(beta)_ / Cursor Agent _(beta)_ / OpenCode _(beta)_ / Pi _(beta)_ / Hermes), שם מדיניות, או מזהה הפעלה + - כל שורה מציגה: חותמת זמן, שם מדיניות, החלטה, תג CLI (כתום = Claude Code, סגול = OpenAI Codex, כחול = GitHub Copilot, אזמרגד = Cursor Agent, ענבר = OpenCode, ורוד = Pi, אינדיגו = Hermes), שם כלים, מזהה הפעלה, והסיבה להחלטות deny/instruct + - לחץ על מזהה הפעלה כדי לפתוח את התמליל שלה — מצפה גילוי אוטומטי אילו CLI נשדרה קורא (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Hermes `~/.hermes/state.db`) ומעניק את תג ה-CLI התואם בכותרת diff --git a/docs/he/getting-started.mdx b/docs/he/getting-started.mdx index 90d45c29..7a1b25fb 100644 --- a/docs/he/getting-started.mdx +++ b/docs/he/getting-started.mdx @@ -37,9 +37,9 @@ bun add -g failproofai failproofai policies --install ``` - זה כותב ערכי hook ל-CLI של סוכנים מותקנים (Claude Code של `~/.claude/settings.json`, OpenAI Codex של `~/.codex/hooks.json`, GitHub Copilot CLI של `~/.copilot/hooks/failproofai.json`, Cursor Agent של `~/.cursor/hooks.json`, OpenCode של plugin shim שנוצר ב-`~/.config/opencode/plugins/failproofai.mjs` בתוספת ערך רישום במערך `plugin` של `~/.config/opencode/opencode.json`, Pi של `~/.pi/agent/settings.json`, Gemini CLI של `~/.gemini/settings.json`, או Hermes של `~/.hermes/config.yaml`). כאשר יותר מאחד קיים תתבקע לבחור; העבר `--cli claude codex copilot cursor opencode pi gemini hermes` (כל קבוצה משנית) כדי לדלג על ההודעה. + זה כותב ערכי hook ל-CLI של סוכנים מותקנים (Claude Code של `~/.claude/settings.json`, OpenAI Codex של `~/.codex/hooks.json`, GitHub Copilot CLI של `~/.copilot/hooks/failproofai.json`, Cursor Agent של `~/.cursor/hooks.json`, OpenCode של plugin shim שנוצר ב-`~/.config/opencode/plugins/failproofai.mjs` בתוספת ערך רישום במערך `plugin` של `~/.config/opencode/opencode.json`, Pi של `~/.pi/agent/settings.json`, או Hermes של `~/.hermes/config.yaml`). כאשר יותר מאחד קיים תתבקע לבחור; העבר `--cli claude codex copilot cursor opencode pi hermes` (כל קבוצה משנית) כדי לדלג על ההודעה. - תמיכה ב-GitHub Copilot CLI, Cursor Agent, OpenCode, Pi ו-Gemini CLI היא **בטא** — התקן עם `--cli copilot`, `--cli cursor`, `--cli opencode`, `--cli pi`, או `--cli gemini`. Hermes (hermes-agent, שער Slack/Telegram) מותקן בטווח משתמש עם `--cli hermes` והוא **גם** מקור ביקורת לא מקוון. + תמיכה ב-GitHub Copilot CLI, Cursor Agent, OpenCode ו-Pi היא **בטא** — התקן עם `--cli copilot`, `--cli cursor`, `--cli opencode`, או `--cli pi`. Hermes (hermes-agent, שער Slack/Telegram) מותקן בטווח משתמש עם `--cli hermes` והוא **גם** מקור ביקורת לא מקוון. ```bash failproofai policies --install --scope project @@ -48,7 +48,6 @@ bun add -g failproofai failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project - failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user failproofai policies --install block-sudo block-rm-rf sanitize-api-keys ``` diff --git a/docs/he/introduction.mdx b/docs/he/introduction.mdx index 90dc6a3d..cc81a290 100644 --- a/docs/he/introduction.mdx +++ b/docs/he/introduction.mdx @@ -5,7 +5,7 @@ description: "FailproofAI מספקת לסוכני AI 39 מדיניויות כש [![npm weekly downloads](https://img.shields.io/npm/dw/failproofai?style=flat-square&color=2ea44f)](https://www.npmjs.com/package/failproofai) -hooks ומדיניויות ל**טיפול בכשלי AI**, **התאוששות משגיאות**, ו**אמינות LLM**. שמור על סוכני ה-AI שלך אמינים ופועלים באופן אוטונומי על פני **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Gemini CLI**, **Hermes**, וה-**Agents SDK**. +hooks ומדיניויות ל**טיפול בכשלי AI**, **התאוששות משגיאות**, ו**אמינות LLM**. שמור על סוכני ה-AI שלך אמינים ופועלים באופן אוטונומי על פני **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Hermes**, וה-**Agents SDK**. סוכני AI נכשלים בדרכים צפויות. הם מפעילים פקודות הרסניות, מדליפים סודות, חורגים מהמטלה, תקועים בלולאות, או דוחפים ישירות לענף הראשי. אם משאירים ללא פקוח עין, כשלים קטנים מתגברים להפסקות, דליפות אישורים, והפסד עבודה. diff --git a/docs/hi/built-in-policies.mdx b/docs/hi/built-in-policies.mdx index a2350188..3c54c713 100644 --- a/docs/hi/built-in-policies.mdx +++ b/docs/hi/built-in-policies.mdx @@ -705,7 +705,7 @@ detect करें जब एजेंट stuck हों या unexpectedly b ### Per-CLI Stop semantics -Stop enforcement सातों supported CLIs में थोड़ा अलग दिखता है क्योंकि प्रत्येक एक अलग "agent finished" hook contract expose करता है। **outcome** same है — एजेंट workflow gate failing होने पर stop नहीं हो सकता — लेकिन **mechanics** अलग हैं। नीचे की table summarize करती है; केवल Pi के पास एक user-visible quirk है जो समझने के लायक है इससे पहले आप एक `require-*-before-stop` नीति को enable करें। +Stop enforcement छहों supported CLIs में थोड़ा अलग दिखता है क्योंकि प्रत्येक एक अलग "agent finished" hook contract expose करता है। **outcome** same है — एजेंट workflow gate failing होने पर stop नहीं हो सकता — लेकिन **mechanics** अलग हैं। नीचे की table summarize करती है; केवल Pi के पास एक user-visible quirk है जो समझने के लायक है इससे पहले आप एक `require-*-before-stop` नीति को enable करें। | CLI | जब gate fires | आप क्या देखते हैं | |---|---|---| @@ -713,20 +713,19 @@ Stop enforcement सातों supported CLIs में थोड़ा अल | Codex | Same agent loop, immediately | Claude के समान। | | GitHub Copilot CLI | Same agent loop, immediately | Claude के समान (Copilot के `{decision:"block", reason}` retry channel का उपयोग करता है — Copilot CLI 1.0.41 के विरुद्ध empirically verified)। | | Cursor Agent | Same agent loop, immediately | Claude के समान (Cursor के `{followup_message}` channel का उपयोग करता है — `loop_limit` द्वारा capped, default 5 retries)। | -| Gemini CLI | Same agent loop, immediately | Claude के समान (Gemini के `{decision:"block", reason}` channel का उपयोग करता है `AfterAgent` पर)। | | OpenCode | Same agent loop, immediately | Claude के समान (OpenCode के `client.session.prompt(...)` SDK call का उपयोग करता है `hookSpecificOutput.additionalContext` के through routed)। | | **Pi (pi-coding-agent)** | **Next user turn** | **Pi visibly stops** जब gate fires — इसका agent loop exit होता है और आपको prompt में वापस किया जाता है। Gate फिर next time fires जब आप एक prompt submit करते हैं: failproofai एक `MANDATORY ACTION REQUIRED` directive को उस turn के system prompt में prepend करता है, LLM को instruct करते हुए workflow step (commit, push, आदि) को complete करने के लिए आप जो माँगते हैं उससे पहले। | -**Pi limitation.** Pi के `AgentEndEvent` (Claude के `Stop` hook के upstream equivalent) के पास कोई Result type नहीं है — जब यह fires, Pi के agent loop पहले ही exit हो चुका है। Pi को Claude / Copilot / Cursor / Gemini / OpenCode के तरीके से same loop को retry के लिए force नहीं किया जा सकता। failproofai gate को Pi के `before_agent_start` event में shift करता है (जो next user prompt के बाद fires) ताकि workflow check अभी भी enforce हो, बस next turn पर current के बजाय। +**Pi limitation.** Pi के `AgentEndEvent` (Claude के `Stop` hook के upstream equivalent) के पास कोई Result type नहीं है — जब यह fires, Pi के agent loop पहले ही exit हो चुका है। Pi को Claude / Copilot / Cursor / OpenCode के तरीके से same loop को retry के लिए force नहीं किया जा सकता। failproofai gate को Pi के `before_agent_start` event में shift करता है (जो next user prompt के बाद fires) ताकि workflow check अभी भी enforce हो, बस next turn पर current के बजाय। **इसका व्यावहारिक मतलब क्या है:** - Pi stop करने के बाद, deny reason को in-memory में Pi session id द्वारा keyed capture किया जाता है। बिल्कुल next prompt आप same Pi process में submit करते हैं drains it: LLM को अपने system prompt के top में `MANDATORY ACTION REQUIRED` directive दिखाई देता है, commit करता है (या push / PR खोलता है / CI wait करता है), और फिर ही आपके request के साथ continue करता है। Captured deny reason एक one-shot है — एक बार drain होने के बाद, gate clear है। -- Gate Pi के process lifetime द्वारा bounded है। यदि आप Pi को `Ctrl+C` करते हैं या turns के बीच quit करते हैं, in-memory entry process के साथ drop हो जाता है और gate miss हो जाता है। Claude, Copilot, Cursor, Gemini, और OpenCode के पास same bound है (एजेंट को kill करो और gate miss हो जाता है) — Pi बस इसे अधिक visible बनाता है क्योंकि एजेंट visibly exit होता है gate firing से पहले। +- Gate Pi के process lifetime द्वारा bounded है। यदि आप Pi को `Ctrl+C` करते हैं या turns के बीच quit करते हैं, in-memory entry process के साथ drop हो जाता है और gate miss हो जाता है। Claude, Copilot, Cursor, और OpenCode के पास same bound है (एजेंट को kill करो और gate miss हो जाता है) — Pi बस इसे अधिक visible बनाता है क्योंकि एजेंट visibly exit होता है gate firing से पहले। - Pending deny भी `session_shutdown` पर किसी भी कारण के लिए clear किया जाता है (`new` / `resume` / `fork` / `quit`), तो prior session से एक stale gate same Pi process में started fresh session में leak नहीं हो सकता। -यदि आपको Claude-style same-loop retry चाहिए, run अपनी `Stop` नीतियों के अंतर्गत किसी और छः supported CLIs के। हम Pi upstream को track कर रहे हैं एक future Result type के लिए `AgentEndEvent` पर जो हमें इस gap को close करने देगा। +यदि आपको Claude-style same-loop retry चाहिए, run अपनी `Stop` नीतियों के अंतर्गत किसी और पाँच supported CLIs के। हम Pi upstream को track कर रहे हैं एक future Result type के लिए `AgentEndEvent` पर जो हमें इस gap को close करने देगा। ### `require-commit-before-stop` diff --git a/docs/hi/cli/audit.mdx b/docs/hi/cli/audit.mdx index 8bbfbf96..f5914a15 100644 --- a/docs/hi/cli/audit.mdx +++ b/docs/hi/cli/audit.mdx @@ -44,10 +44,10 @@ failproofai - उपयोग देखने के लिए `failproofai audit -h` (या `--help`) चलाएं। ऑडिट **पूरी तरह ऑफलाइन** चलता है — कोई खाता या नेटवर्क आवश्यक नहीं — और डैशबोर्ड तब तक सेवा देता रहता है जब तक आप इसे `Ctrl+C` से रोक न दें। + उपयोग देखने के लिए `failproofai audit -h` (या `--help`) चलाएं। मुख्य ऑडिट स्कैन और रिपोर्ट जनरेशन **पूरी तरह ऑफलाइन** चलते हैं — कोई खाता या नेटवर्क आवश्यक नहीं। वैकल्पिक प्रमाणित खाता सुविधाओं के लिए नेटवर्क की आवश्यकता होती है। डैशबोर्ड तब तक सेवा देता रहता है जब तक आप इसे `Ctrl+C` से रोक न दें। -डैशबोर्ड इस मशीन पर पिछले एजेंट CLI ट्रांसक्रिप्ट्स को स्कैन करता है (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi, Gemini) और रिपोर्ट करता है कि एजेंट ने कितनी बार ऐसी चीजें कीं जिन्हें रोकने के लिए failproofai बनाया गया है — env-var चेक, force push, अनावश्यक `cd ` प्रिफिक्स, sleep-polling लूप, अभी-अभी edited फाइलें दोबारा पढ़ना, और अन्य। +डैशबोर्ड इस मशीन पर पिछले एजेंट CLI ट्रांसक्रिप्ट्स को स्कैन करता है (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi) और रिपोर्ट करता है कि एजेंट ने कितनी बार ऐसी चीजें कीं जिन्हें रोकने के लिए failproofai बनाया गया है — env-var चेक, force push, अनावश्यक `cd ` प्रिफिक्स, sleep-polling लूप, अभी-अभी edited फाइलें दोबारा पढ़ना, और अन्य। प्रत्येक ट्रांसक्रिप्ट के लिए, हर tool-use इवेंट 39 builtin policies के माध्यम से **और** 8 audit-only डिटेक्टर्स के माध्यम से फिर से चलाया जाता है जो ऐसे पैटर्न को पकड़ते हैं जो अभी तक runtime policies द्वारा कवर नहीं हैं। गिनती सभी सत्रों में प्रति policy/डिटेक्टर एकत्र की जाती है। @@ -85,4 +85,4 @@ failproofai - **कोई mutation नहीं।** ऑडिट read-only mode में फिर से चलता है। `warn-repeated-tool-calls` को छोड़ दिया जाता है क्योंकि इसका per-session sidecar अन्यथा modify हो जाएगा। - **Workflow policies छोड़ दिए जाते हैं।** `require-*-before-stop` policies केवल `Stop` events और live git state के विरुद्ध `execSync` पर fire होती हैं — इनका meaningful "what would have happened in 2025" interpretation नहीं है, इसलिए वे audit counts में दिखाई नहीं देते हैं। -- **Custom policies छोड़ दिए जाते हैं।** User-supplied custom hooks को फिर से चलाया नहीं जाता (वे original session के बाद से बदल सकते हैं)। \ No newline at end of file +- **Custom policies छोड़ दिए जाते हैं।** User-supplied custom hooks को फिर से चलाया नहीं जाता (वे original session के बाद से बदल सकते हैं)। diff --git a/docs/hi/configuration.mdx b/docs/hi/configuration.mdx index 0f0fae5b..f19342cc 100644 --- a/docs/hi/configuration.mdx +++ b/docs/hi/configuration.mdx @@ -196,13 +196,12 @@ resolved: { allowPatterns: ["sudo systemctl status"] } ← falls through to glo - **OpenAI Codex**: `~/.codex/hooks.json` (उपयोगकर्ता), `/.codex/hooks.json` (प्रोजेक्ट) — Codex के पास `local` स्कोप नहीं है - **GitHub Copilot CLI _(beta)_**: `~/.copilot/hooks/failproofai.json` (उपयोगकर्ता), `/.github/hooks/failproofai.json` (प्रोजेक्ट) — Copilot के पास कोई `local` स्कोप नहीं है। हुक एंट्रीज़ Copilot के OS-keyed `bash`/`powershell` कमांड फ़ील्ड का उपयोग करते हैं `timeoutSec` के साथ; फ़ाइल एक टॉप-लेवल `version: 1` मार्कर ले जाती है। Copilot CLI समर्थन **beta** है जबकि हम `events.jsonl` रिकॉर्ड स्कीमा को सत्यापित करते हैं (जिसे सार्वजनिक दस्तावेज़ निर्दिष्ट नहीं करते) अधिक वास्तविक-दुनिया सत्रों के विरुद्ध। - **Cursor Agent _(beta)_**: `~/.cursor/hooks.json` (उपयोगकर्ता), `/.cursor/hooks.json` (प्रोजेक्ट) — Cursor के पास कोई `local` स्कोप नहीं है। हुक एंट्रीज़ Claude-shaped `{type, command, timeout}` फॉर्म का उपयोग करते हैं (कोई `bash`/`powershell` विभाजन नहीं), लेकिन camelCase ईवेंट कुंजियों के तहत संग्रहीत (`preToolUse`, `beforeSubmitPrompt`, …) Cursor के [hooks schema](https://cursor.com/docs/hooks) के अनुसार एक फ्लैट ऐरे में; फ़ाइल एक टॉप-लेवल `version: 1` मार्कर ले जाती है। हैंडलर camelCase → PascalCase को `CURSOR_EVENT_MAP` के माध्यम से कैनोनिकलाइज़ करता है ताकि मौजूदा बिल्ट-इन नीतियां अपरिवर्तित रूप से चलें। Cursor Agent समर्थन **beta** है जबकि हम Cursor के ट्रांसक्रिप्ट को सत्यापित करते हैं (सार्वजनिक दस्तावेज़ में निर्दिष्ट नहीं) अधिक वास्तविक-दुनिया इंस्टॉल्स के विरुद्ध। - - **OpenCode _(beta)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (उपयोगकर्ता), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (प्रोजेक्ट) — OpenCode के पास कोई `local` स्कोप नहीं है। अन्य छह CLI के विपरीत, OpenCode के पास **कोई बाहरी-कमांड हुक सिस्टम नहीं है**: यह JS/TS प्लगइन को `opencode.json` में `plugin: []` ऐरे के माध्यम से स्पष्ट रूप से पंजीकृत करके इन-प्रॉसेस लोड करता है (`.opencode/plugins/` से ऑटो-डिस्कवरी **नहीं** है कि प्लगइन opencode v1.14.33 पर कैसे लोड होते हैं)। इंस्टॉल एक छोटा जेनरेटेड प्लगइन शिम छोड़ता है जो failproofai बाइनरी को subprocess-कॉल करता है और बाइनरी के Claude-shape JSON प्रतिक्रिया को प्लगइन शब्दावली में वापस अनुवाद करता है: `throw new Error()` टूल-ईवेंट deny के लिए (टूल कॉल को रद्द करता है), `client.session.prompt(...)` instruct AND के लिए `Stop` / `SubagentStop` deny (deny कारण को अगले उपयोगकर्ता संदेश के रूप में जमा करता है — एकमात्र force-retry चैनल क्योंकि `session.idle` केवल-अधिसूचना है और इससे throw करना एक no-op है), और allow के लिए no-op। शिम both टूल नाम (lowercase → PascalCase `OPENCODE_TOOL_MAP` के माध्यम से) और tool-input arg कुंजियां (camelCase → snake_case `OPENCODE_TOOL_INPUT_MAP` के माध्यम से `Read` / `Write` / `Edit` के लिए, उदा. `filePath` → `file_path`, `oldString` → `old_string`) कैनोनिकलाइज़ करता है बाइनरी में अग्रेषित करने से पहले, इसलिए path-checking builtins जैसे `block-read-outside-cwd`, `block-env-files`, और `block-secrets-write` OpenCode tool कॉल पर अपरिवर्तित रूप से चलते हैं। सत्र opencode के SQLite DB में `~/.local/share/opencode/opencode.db` में रहते हैं; डैशबोर्ड का सत्र दर्शक `opencode db --format json` और `opencode export ` के माध्यम से उन्हें पढ़ता है। OpenCode समर्थन **beta** है जबकि हम संस्करणों में व्यवहार को सत्यापित करते हैं और अधिक वास्तविक-दुनिया सत्रों के विरुद्ध। [OpenCode plugins docs](https://opencode.ai/docs/plugins/) देखें। + - **OpenCode _(beta)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (उपयोगकर्ता), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (प्रोजेक्ट) — OpenCode के पास कोई `local` स्कोप नहीं है। अन्य पाँच CLI के विपरीत, OpenCode के पास **कोई बाहरी-कमांड हुक सिस्टम नहीं है**: यह JS/TS प्लगइन को `opencode.json` में `plugin: []` ऐरे के माध्यम से स्पष्ट रूप से पंजीकृत करके इन-प्रॉसेस लोड करता है (`.opencode/plugins/` से ऑटो-डिस्कवरी **नहीं** है कि प्लगइन opencode v1.14.33 पर कैसे लोड होते हैं)। इंस्टॉल एक छोटा जेनरेटेड प्लगइन शिम छोड़ता है जो failproofai बाइनरी को subprocess-कॉल करता है और बाइनरी के Claude-shape JSON प्रतिक्रिया को प्लगइन शब्दावली में वापस अनुवाद करता है: `throw new Error()` टूल-ईवेंट deny के लिए (टूल कॉल को रद्द करता है), `client.session.prompt(...)` instruct AND के लिए `Stop` / `SubagentStop` deny (deny कारण को अगले उपयोगकर्ता संदेश के रूप में जमा करता है — एकमात्र force-retry चैनल क्योंकि `session.idle` केवल-अधिसूचना है और इससे throw करना एक no-op है), और allow के लिए no-op। शिम both टूल नाम (lowercase → PascalCase `OPENCODE_TOOL_MAP` के माध्यम से) और tool-input arg कुंजियां (camelCase → snake_case `OPENCODE_TOOL_INPUT_MAP` के माध्यम से `Read` / `Write` / `Edit` के लिए, उदा. `filePath` → `file_path`, `oldString` → `old_string`) कैनोनिकलाइज़ करता है बाइनरी में अग्रेषित करने से पहले, इसलिए path-checking builtins जैसे `block-read-outside-cwd`, `block-env-files`, और `block-secrets-write` OpenCode tool कॉल पर अपरिवर्तित रूप से चलते हैं। सत्र opencode के SQLite DB में `~/.local/share/opencode/opencode.db` में रहते हैं; डैशबोर्ड का सत्र दर्शक `opencode db --format json` और `opencode export ` के माध्यम से उन्हें पढ़ता है। OpenCode समर्थन **beta** है जबकि हम संस्करणों में व्यवहार को सत्यापित करते हैं और अधिक वास्तविक-दुनिया सत्रों के विरुद्ध। [OpenCode plugins docs](https://opencode.ai/docs/plugins/) देखें। - **Pi _(beta)_**: `~/.pi/agent/settings.json` (उपयोगकर्ता), `/.pi/settings.json` (प्रोजेक्ट) — Pi के पास कोई `local` स्कोप नहीं है। Pi स्टार्टअप पर TypeScript extension पैकेज लोड करता है; सेटिंग्स फ़ाइल एक फ्लैट स्ट्रिंग ऐरे `{"packages": ["./relative/path", …]}` है। failproofai एक single packages-array एंट्री लिखता है जो अपनी bundled `pi-extension/` निर्देशिका की ओर इशारा करता है। एक्सटेंशन आंतरिक रूप से Pi के `tool_call` / `user_bash` / `input` / `session_start` ईवेंट्स की सदस्यता लेता है और `failproofai --hook --cli pi` के लिए shell बाहर निकालता है; हैंडलर underscore_lower_snake_case → PascalCase को `PI_EVENT_MAP` के माध्यम से कैनोनिकलाइज़ करता है ताकि मौजूदा बिल्ट-इन नीतियां अपरिवर्तित रूप से चलें। Tool input args को `PI_TOOL_INPUT_MAP` के माध्यम से भी कैनोनिकलाइज़ किया जाता है (Pi के Read / Write / Edit `path` के बजाय `file_path` प्रदान करते हैं; शीर्ष-स्तरीय कुंजी को मैप करना `block-env-files` और `block-secrets-write` को चलने देता है — `block-read-outside-cwd` पहले से ही एक `path` fallback था)। Pi समर्थन **beta** है जबकि Pi का एक्सटेंशन API और session-log लेआउट स्थिर होता है। - - **Gemini CLI _(beta)_**: `~/.gemini/settings.json` (उपयोगकर्ता), `/.gemini/settings.json` (प्रोजेक्ट) — Gemini के पास कोई `local` स्कोप नहीं है (यह `/etc/gemini-cli/settings.json` पर एक `system` स्कोप दस्तावेज़ित करता है जो failproofai उजागर नहीं करता)। हुक एंट्रीज़ Claude के `{type, command, timeout}` फॉर्म का उपयोग करते हैं Gemini के `{matcher, hooks: [...]}` matcher स्कीमा में लपेटा गया `matcher: "*"` के साथ डिफ़ॉल्ट रूप से। ईवेंट्स PascalCase हैं (`SessionStart`, `BeforeAgent`, `AfterAgent`, `BeforeModel`, `AfterModel`, `BeforeToolSelection`, `BeforeTool`, `AfterTool`, `PreCompress`, `Notification`, `SessionEnd`); हैंडलर `GEMINI_EVENT_MAP` के माध्यम से Claude canonical नामों में मैप करता है। टूल नाम snake_case हैं (`run_shell_command`, `read_file`, `write_file`, `replace`, …) — हैंडलर `GEMINI_TOOL_MAP` के माध्यम से कैनोनिकलाइज़ करता है ताकि मौजूदा बिल्ट-इन नीतियां अपरिवर्तित रूप से चलें। नीति मूल्यांकनकर्ता Gemini का फ्लैट `{decision: "deny", reason}` shape (Gemini के "Golden Rule" exit-0 contract के अनुसार पसंद किया जाता है), `{hookSpecificOutput: {hookEventName, additionalContext}}` context injection के लिए BeforeAgent / AfterTool / SessionStart पर, और `{decision: "block", reason}` AfterAgent पर force-retry semantics के लिए उत्सर्जित करता है। Gemini CLI समर्थन **beta** है जबकि हम वास्तविक-दुनिया कवरेज को चौड़ा करते हैं। [Gemini CLI hooks docs](https://geminicli.com/docs/hooks/) देखें। - **Hermes (hermes-agent)**: `~/.hermes/config.yaml` (**उपयोगकर्ता स्कोप केवल** — Hermes के पास कोई project/local कॉन्फ़िग नहीं है)। Hermes एक Slack/Telegram **गेटवे** है, इसलिए एक इंस्टॉल हर प्लेटफॉर्म (Slack/Telegram/cli/cron) **और** आंतरिक subagents से tool कॉल को इंटरसेप्ट करता है। हुक एंट्रीज़ एक `{command, timeout}` pair (**सेकंड में** timeout) हैं एक `hooks:` map के तहत Hermes के snake_case ईवेंट्स द्वारा keyed (`pre_tool_call` / `post_tool_call` / `on_session_start` / `on_session_end` / `subagent_stop`); हैंडलर ईवेंट्स को `HERMES_EVENT_MAP` के माध्यम से और टूल नाम को `HERMES_TOOL_MAP` के माध्यम से कैनोनिकलाइज़ करता है ताकि बिल्ट-इन नीतियां अपरिवर्तित रूप से चलें। कॉन्फ़िग एक comment-preserving YAML `Document` round-trip के माध्यम से संपादित किया जाता है ताकि ऑपरेटर की अन्य सेटिंग्स जीवित रहें, और install `hooks_auto_accept: true` सेट करता है ताकि headless gateway (कोई TTY नहीं) consent प्रॉम्प्ट के बिना हुक चलाए। मूल्यांकनकर्ता Hermes के `{"decision":"block","reason"}` stdout contract उत्सर्जित करता है (Hermes exit कोड को अनदेखा करता है)। **सीमाएं:** Hermes के पास कोई turn-end `Stop` ईवेंट नहीं है, इसलिए `require-*-before-stop` builtins कभी इसके लिए नहीं चलते (inapplicable, broken नहीं); `instruct` allow-with-logged-note में degrade होता है (कोई additional-context चैनल नहीं); और output-secret redaction (`sanitize-*`) shell-hook contract के ऊपर tool output को rewrite नहीं कर सकता। Hermes **भी** एक offline **audit** स्रोत है — डैशबोर्ड अपने gateway सत्रों को सीधे `~/.hermes/state.db` से पढ़ता है। - **`policies-config.json`** — failproofai को बताता है कि कौन सी नीतियों का मूल्यांकन करना है और किस पैरामीटर के साथ (सभी एजेंट CLIs में साझा) -एक विशिष्ट एजेंट को target करने के लिए `--cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes` पास करें (space-separated या repeated किसी भी subset के लिए): +एक विशिष्ट एजेंट को target करने के लिए `--cli claude|codex|copilot|cursor|opencode|pi|hermes` पास करें (space-separated या repeated किसी भी subset के लिए): ```bash failproofai policies --install --cli codex --scope project @@ -210,12 +209,11 @@ failproofai policies --install --cli copilot --scope project failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project -failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user -failproofai policies --install --cli claude codex copilot cursor opencode pi gemini +failproofai policies --install --cli claude codex copilot cursor opencode pi ``` -जब `--cli` omit किया जाता है, `failproofai` पता लगाता है कि कौन से एजेंट CLIs installed हैं (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which gemini` / `which hermes`): +जब `--cli` omit किया जाता है, `failproofai` पता लगाता है कि कौन से एजेंट CLIs installed हैं (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which hermes`): - **एक CLI detected** — prompt के बिना उस CLI को auto-select करता है। - **Multiple CLIs detected** एक interactive terminal में — एक arrow-key single-select प्रॉम्प्ट दिखाता है एक `Detected (N)` section (एक `Install for all N detected` aggregate row + प्रत्येक detected CLI individually के साथ) और एक `Not installed (M) · install hooks ahead of time` section में grouped (हर undetected supported CLI को एक forward-install विकल्प के रूप में सूचीबद्ध करता है (↑↓ move करने के लिए, Enter select करने के लिए, ^C quit करने के लिए))। uninstall flow केवल Detected section दिखाता है। diff --git a/docs/hi/dashboard.mdx b/docs/hi/dashboard.mdx index b3a6c202..41dac30c 100644 --- a/docs/hi/dashboard.mdx +++ b/docs/hi/dashboard.mdx @@ -16,7 +16,7 @@ failproofai `http://localhost:8020` पर खुलता है। -डैशबोर्ड फाइलसिस्टम से सीधे पढ़ता है - आपके Claude Code प्रोजेक्ट फोल्डर और failproofai कॉन्फ़िगरेशन फाइलें। किसी रिमोट सेवा में कुछ नहीं लिखा जाता है। +डैशबोर्ड स्थानीय प्रोजेक्ट, सेशन और failproofai कॉन्फ़िगरेशन डेटा को सीधे फाइलसिस्टम से पढ़ता है। ऑडिट रिमाइंडर और आमंत्रण जैसी वैकल्पिक प्रमाणित सुविधाएँ इन अनुरोधों के लिए आवश्यक जानकारी, जिसमें ईमेल पते शामिल हैं, रिमोट API को भेजती हैं। --- @@ -24,11 +24,11 @@ failproofai ### प्रोजेक्ट -आपकी मशीन पर मिले सभी Claude Code, OpenAI Codex, GitHub Copilot CLI _(beta)_, Cursor Agent _(beta)_, OpenCode _(beta)_, Pi _(beta)_, Gemini CLI _(beta)_, और Hermes प्रोजेक्ट सूचीबद्ध करता है। Claude प्रोजेक्ट `~/.claude/projects/` से खोजे जाते हैं (या `CLAUDE_PROJECTS_PATH` द्वारा निर्धारित पथ); Codex प्रोजेक्ट `~/.codex/sessions///
/*.jsonl` के तहत प्रत्येक ट्रांसक्रिप्ट को स्कैन करके और प्रत्येक सेशन के पहले रिकॉर्ड में दर्ज `cwd` के अनुसार समूहबद्ध करके खोजे जाते हैं; Copilot CLI प्रोजेक्ट प्रत्येक `~/.copilot/session-state//workspace.yaml` को स्कैन करके खोजे जाते हैं (`COPILOT_HOME` के माध्यम से कॉन्फ़िगर करने योग्य) और इसके `cwd` फील्ड के अनुसार समूहबद्ध किए जाते हैं; Cursor Agent प्रोजेक्ट `~/.cursor/agent-sessions//` के तहत प्रति-सेशन मेटाडेटा को स्कैन करके खोजे जाते हैं (`CURSOR_HOME` के माध्यम से कॉन्फ़िगर करने योग्य, `conversations/` और `sessions/` को फॉलबैक के रूप में जांचा जाता है) `meta.json` / `session.json` / `workspace.yaml` में `cwd` स्केलर के लिए; OpenCode प्रोजेक्ट `~/.local/share/opencode/opencode.db` पर इसके SQLite DB को `opencode db --format json` के माध्यम से क्वेरी करके खोजे जाते हैं (हम `session` और `project` तालिकाएं पढ़ते हैं और `project_id` के अनुसार समूहबद्ध करते हैं); Pi प्रोजेक्ट `~/.pi/agent/sessions//_.jsonl` के तहत प्रति-सेशन JSONL ट्रांसक्रिप्ट को स्कैन करके खोजे जाते हैं (`PI_SESSIONS_DIR` के माध्यम से कॉन्फ़िगर करने योग्य) और प्रत्येक सेशन के पहले रिकॉर्ड से `cwd` निकाला जाता है; Gemini CLI प्रोजेक्ट `~/.gemini/tmp//chats/session--.jsonl` को स्कैन करके खोजे जाते हैं (`GEMINI_SESSIONS_DIR` के माध्यम से कॉन्फ़िगर करने योग्य) और अनुवर्ती `.project_root` टेक्स्ट मार्कर से विहित cwd को पुनः प्राप्त किया जाता है; Hermes गेटवे सेशन सीधे इसके SQLite स्टोर से `~/.hermes/state.db` पर पढ़े जाते हैं (`HERMES_DB_PATH` के माध्यम से कॉन्फ़िगर करने योग्य) और `source` के अनुसार `hermes-` प्रोजेक्ट में समूहबद्ध किए जाते हैं (Slack/Telegram/cli/cron — गेटवे सेशन का कोई cwd नहीं है)। एक प्रोजेक्ट जिसे कई CLI द्वारा उपयोग किया गया है, एक एकल पंक्ति के रूप में सभी मेल खाने वाले बैज के साथ प्रदर्शित होता है। सारणी के ऊपर **CLI** ड्रॉपडाउन का उपयोग करके किसी विशिष्ट एजेंट CLI द्वारा फ़िल्टर करें; URL आपके चयन को `?cli=claude|codex|copilot|cursor|opencode|pi|gemini|hermes` के रूप में संरक्षित करता है। +आपकी मशीन पर मिले सभी Claude Code, OpenAI Codex, GitHub Copilot CLI _(beta)_, Cursor Agent _(beta)_, OpenCode _(beta)_, Pi _(beta)_, और Hermes प्रोजेक्ट सूचीबद्ध करता है। Claude प्रोजेक्ट `~/.claude/projects/` से खोजे जाते हैं (या `CLAUDE_PROJECTS_PATH` द्वारा निर्धारित पथ); Codex प्रोजेक्ट `~/.codex/sessions///
/*.jsonl` के तहत प्रत्येक ट्रांसक्रिप्ट को स्कैन करके और प्रत्येक सेशन के पहले रिकॉर्ड में दर्ज `cwd` के अनुसार समूहबद्ध करके खोजे जाते हैं; Copilot CLI प्रोजेक्ट प्रत्येक `~/.copilot/session-state//workspace.yaml` को स्कैन करके खोजे जाते हैं (`COPILOT_HOME` के माध्यम से कॉन्फ़िगर करने योग्य) और इसके `cwd` फील्ड के अनुसार समूहबद्ध किए जाते हैं; Cursor Agent प्रोजेक्ट `~/.cursor/agent-sessions//` के तहत प्रति-सेशन मेटाडेटा को स्कैन करके खोजे जाते हैं (`CURSOR_HOME` के माध्यम से कॉन्फ़िगर करने योग्य, `conversations/` और `sessions/` को फॉलबैक के रूप में जांचा जाता है) `meta.json` / `session.json` / `workspace.yaml` में `cwd` स्केलर के लिए; OpenCode प्रोजेक्ट `~/.local/share/opencode/opencode.db` पर इसके SQLite DB को `opencode db --format json` के माध्यम से क्वेरी करके खोजे जाते हैं (हम `session` और `project` तालिकाएं पढ़ते हैं और `project_id` के अनुसार समूहबद्ध करते हैं); Pi प्रोजेक्ट `~/.pi/agent/sessions//_.jsonl` के तहत प्रति-सेशन JSONL ट्रांसक्रिप्ट को स्कैन करके खोजे जाते हैं (`PI_SESSIONS_DIR` के माध्यम से कॉन्फ़िगर करने योग्य) और प्रत्येक सेशन के पहले रिकॉर्ड से `cwd` निकाला जाता है; Hermes गेटवे सेशन सीधे इसके SQLite स्टोर से `~/.hermes/state.db` पर पढ़े जाते हैं (`HERMES_DB_PATH` के माध्यम से कॉन्फ़िगर करने योग्य) और `source` के अनुसार `hermes-` प्रोजेक्ट में समूहबद्ध किए जाते हैं (Slack/Telegram/cli/cron — गेटवे सेशन का कोई cwd नहीं है)। एक प्रोजेक्ट जिसे कई CLI द्वारा उपयोग किया गया है, एक एकल पंक्ति के रूप में सभी मेल खाने वाले बैज के साथ प्रदर्शित होता है। सारणी के ऊपर **CLI** ड्रॉपडाउन का उपयोग करके किसी विशिष्ट एजेंट CLI द्वारा फ़िल्टर करें; URL आपके चयन को `?cli=claude|codex|copilot|cursor|opencode|pi|hermes` के रूप में संरक्षित करता है। प्रत्येक प्रोजेक्ट दिखाता है: - प्रोजेक्ट नाम (फोल्डर पथ से व्युत्पन्न) -- एक CLI बैज — `Claude Code` (नारंगी), `OpenAI Codex` (बैंगनी), `GitHub Copilot` (नीला), `Cursor Agent` (पन्ना), `OpenCode` (एम्बर), `Pi` (गुलाबी), `Gemini CLI` (आसमानी), और/या `Hermes` (गहरा नीला) +- एक CLI बैज — `Claude Code` (नारंगी), `OpenAI Codex` (बैंगनी), `GitHub Copilot` (नीला), `Cursor Agent` (पन्ना), `OpenCode` (एम्बर), `Pi` (गुलाबी), और/या `Hermes` (गहरा नीला) - सबसे हाल की सेशन गतिविधि की तारीख एक प्रोजेक्ट पर क्लिक करके इसके सेशन देखें। @@ -47,7 +47,7 @@ failproofai ### सेशन दर्शक -सेशन दर्शक स्वायत्त एजेंट के लिए मुख्य प्रश्न का उत्तर देता है: एजेंट ने क्या किया, और क्या वह ट्रैक पर रहा? शीर्षलेख के बगल में एक CLI बैज इंगित करता है कि सेशन Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi, Gemini CLI, या Hermes ट्रांसक्रिप्ट है। यह एक सेशन में हुई हर चीज की एक टाइमलाइन दिखाता है: +सेशन दर्शक स्वायत्त एजेंट के लिए मुख्य प्रश्न का उत्तर देता है: एजेंट ने क्या किया, और क्या वह ट्रैक पर रहा? शीर्षलेख के बगल में एक CLI बैज इंगित करता है कि सेशन Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi, या Hermes ट्रांसक्रिप्ट है। यह एक सेशन में हुई हर चीज की एक टाइमलाइन दिखाता है: - **संदेश** - Claude के टेक्स्ट प्रतिक्रियाएं और उपयोगकर्ता संकेत - **टूल कॉल** - हर टूल जो Claude ने आह्वान किया, इसके इनपुट और आउटपुट के साथ @@ -55,7 +55,7 @@ failproofai शीर्ष पर सांख्यिकी बार सेशन की अवधि, कुल टूल कॉल, और हुक निर्णयों का सारांश दिखाता है (अनुमति / अस्वीकृति / निर्देश गणना)। -सेशन निर्यात करने के लिए **डाउनलोड लॉग** बटन पर क्लिक करें। Claude Code, Codex, Copilot, Cursor, Pi, और Gemini सेशन के लिए आप मूल ऑन-डिस्क JSONL ट्रांसक्रिप्ट बाइट-दर-बाइट प्राप्त करते हैं; OpenCode के लिए (जिसके सेशन SQLite में रहते हैं, डिस्क पर नहीं) आप एक JSON दस्तावेज प्राप्त करते हैं जो अंतर्निहित `session` / `messages` / `parts` तालिकाओं को प्रतिबिंबित करता है। +सेशन निर्यात करने के लिए **डाउनलोड लॉग** बटन पर क्लिक करें। Claude Code, Codex, Copilot, Cursor, और Pi सेशन के लिए आप मूल ऑन-डिस्क JSONL ट्रांसक्रिप्ट बाइट-दर-बाइट प्राप्त करते हैं; OpenCode के लिए (जिसके सेशन SQLite में रहते हैं, डिस्क पर नहीं) आप एक JSON दस्तावेज प्राप्त करते हैं जो अंतर्निहित `session` / `messages` / `parts` तालिकाओं को प्रतिबिंबित करता है। ### ऑडिट @@ -75,16 +75,16 @@ failproofai - - एकल पैनल से failproofai किस एजेंट CLI की रक्षा करता है, इसे बहु-चयन करें — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi, Gemini CLI, और Hermes सभी के पास स्थापना स्थिति (`Active` / `Detected` / `Inactive`) वाली एक पंक्ति है, उपयोगकर्ता-स्कोप सेटिंग्स पथ, और एक ब्रांड-रंगीन उच्चारण। आप जिन CLI चाहते हैं उन्हें चेक करें या अनचेक करें और `Apply changes` पर क्लिक करके एक चरण में इंस्टॉल/अनइंस्टॉल करने के लिए अंतर लागू करें। CLI जिनके बाइनरी को PATH पर पता चला है, पूर्व-जांच किए जाते हैं। + - एकल पैनल से failproofai किस एजेंट CLI की रक्षा करता है, इसे बहु-चयन करें — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi, और Hermes सभी के पास स्थापना स्थिति (`Active` / `Detected` / `Inactive`) वाली एक पंक्ति है, उपयोगकर्ता-स्कोप सेटिंग्स पथ, और एक ब्रांड-रंगीन उच्चारण। आप जिन CLI चाहते हैं उन्हें चेक करें या अनचेक करें और `Apply changes` पर क्लिक करके एक चरण में इंस्टॉल/अनइंस्टॉल करने के लिए अंतर लागू करें। CLI जिनके बाइनरी को PATH पर पता चला है, पूर्व-जांच किए जाते हैं। - एकल क्लिक के साथ व्यक्तिगत नीतियों को चालू या बंद करें (`~/.failproofai/policies-config.json` में लिखते हैं — हर इंस्टॉल किए गए CLI में साझा) - एक नीति का विस्तार करके इसके पैरामीटर को कॉन्फ़िगर करें (उन नीतियों के लिए जो `policyParams` को समर्थन करती हैं) - एक कस्टम नीति फाइल पथ सेट करें - सभी सेशन में फायर किए गए प्रत्येक हुक ईवेंट का पूर्ण पृष्ठांकित इतिहास - - निर्णय, ईवेंट प्रकार, CLI (Claude Code / OpenAI Codex / GitHub Copilot _(beta)_ / Cursor Agent _(beta)_ / OpenCode _(beta)_ / Pi _(beta)_ / Gemini CLI _(beta)_ / Hermes), नीति नाम, या सेशन ID द्वारा फ़िल्टर करें - - प्रत्येक पंक्ति दिखाती है: टाइमस्टैम्प, नीति नाम, निर्णय, CLI बैज (नारंगी = Claude Code, बैंगनी = OpenAI Codex, नीला = GitHub Copilot, पन्ना = Cursor Agent, एम्बर = OpenCode, गुलाबी = Pi, आसमानी = Gemini CLI, गहरा नीला = Hermes), टूल नाम, सेशन ID, और अस्वीकृति/निर्देश निर्णयों का कारण - - ट्रांसक्रिप्ट खोलने के लिए एक सेशन ID पर क्लिक करें — दर्शक स्वचालित रूप से पता लगाता है कि कौन सा CLI हुक को आग लगाया (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Gemini CLI `~/.gemini/tmp//chats/.jsonl`, Hermes `~/.hermes/state.db`) और शीर्षलेख में मेल खाने वाले CLI बैज को प्रदान करता है + - निर्णय, ईवेंट प्रकार, CLI (Claude Code / OpenAI Codex / GitHub Copilot _(beta)_ / Cursor Agent _(beta)_ / OpenCode _(beta)_ / Pi _(beta)_ / Hermes), नीति नाम, या सेशन ID द्वारा फ़िल्टर करें + - प्रत्येक पंक्ति दिखाती है: टाइमस्टैम्प, नीति नाम, निर्णय, CLI बैज (नारंगी = Claude Code, बैंगनी = OpenAI Codex, नीला = GitHub Copilot, पन्ना = Cursor Agent, एम्बर = OpenCode, गुलाबी = Pi, गहरा नीला = Hermes), टूल नाम, सेशन ID, और अस्वीकृति/निर्देश निर्णयों का कारण + - ट्रांसक्रिप्ट खोलने के लिए एक सेशन ID पर क्लिक करें — दर्शक स्वचालित रूप से पता लगाता है कि कौन सा CLI हुक को आग लगाया (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Hermes `~/.hermes/state.db`) और शीर्षलेख में मेल खाने वाले CLI बैज को प्रदान करता है @@ -146,4 +146,4 @@ FAILPROOFAI_ALLOWED_DEV_ORIGINS=dashboard.example.com npm run dev यह केवल dev मोड पर लागू होता है। `failproofai` (उत्पादन मोड) चलाते समय, कोई HMR वेबसॉकेट नहीं है और कोई क्रॉस-ऑरिजिन dev संसाधन समस्या नहीं है। - \ No newline at end of file + diff --git a/docs/hi/getting-started.mdx b/docs/hi/getting-started.mdx index b7f3787d..4d641af3 100644 --- a/docs/hi/getting-started.mdx +++ b/docs/hi/getting-started.mdx @@ -37,9 +37,9 @@ bun add -g failproofai failproofai policies --install ``` - यह आपके इंस्टॉल किए गए agent CLIs में hook entries लिखता है (Claude Code के `~/.claude/settings.json`, OpenAI Codex के `~/.codex/hooks.json`, GitHub Copilot CLI के `~/.copilot/hooks/failproofai.json`, Cursor Agent के `~/.cursor/hooks.json`, OpenCode के generated plugin shim में `~/.config/opencode/plugins/failproofai.mjs` plus `~/.config/opencode/opencode.json` के `plugin` array में एक registration entry, Pi के `~/.pi/agent/settings.json`, Gemini CLI के `~/.gemini/settings.json`, या Hermes के `~/.hermes/config.yaml`)। जब एक से अधिक मौजूद हों तो आपसे prompt किया जाएगा; prompt को छोड़ने के लिए `--cli claude codex copilot cursor opencode pi gemini hermes` (कोई भी subset) पास करें। + यह आपके इंस्टॉल किए गए agent CLIs में hook entries लिखता है (Claude Code के `~/.claude/settings.json`, OpenAI Codex के `~/.codex/hooks.json`, GitHub Copilot CLI के `~/.copilot/hooks/failproofai.json`, Cursor Agent के `~/.cursor/hooks.json`, OpenCode के generated plugin shim में `~/.config/opencode/plugins/failproofai.mjs` plus `~/.config/opencode/opencode.json` के `plugin` array में एक registration entry, Pi के `~/.pi/agent/settings.json`, या Hermes के `~/.hermes/config.yaml`)। जब एक से अधिक मौजूद हों तो आपसे prompt किया जाएगा; prompt को छोड़ने के लिए `--cli claude codex copilot cursor opencode pi hermes` (कोई भी subset) पास करें। - GitHub Copilot CLI, Cursor Agent, OpenCode, Pi, और Gemini CLI सपोर्ट **beta** है — `--cli copilot`, `--cli cursor`, `--cli opencode`, `--cli pi`, या `--cli gemini` के साथ इंस्टॉल करें। Hermes (hermes-agent, एक Slack/Telegram gateway) user-scope के साथ `--cli hermes` के साथ इंस्टॉल होता है और **साथ ही** एक offline audit source है। + GitHub Copilot CLI, Cursor Agent, OpenCode, और Pi सपोर्ट **beta** है — `--cli copilot`, `--cli cursor`, `--cli opencode`, या `--cli pi` के साथ इंस्टॉल करें। Hermes (hermes-agent, एक Slack/Telegram gateway) user-scope के साथ `--cli hermes` के साथ इंस्टॉल होता है और **साथ ही** एक offline audit source है। ```bash failproofai policies --install --scope project @@ -48,7 +48,6 @@ bun add -g failproofai failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project - failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user failproofai policies --install block-sudo block-rm-rf sanitize-api-keys ``` diff --git a/docs/hi/introduction.mdx b/docs/hi/introduction.mdx index 1055e6cc..b83299f0 100644 --- a/docs/hi/introduction.mdx +++ b/docs/hi/introduction.mdx @@ -6,13 +6,13 @@ description: "FailproofAI आपके AI एजेंट्स को 39 बि [![npm weekly downloads](https://img.shields.io/npm/dw/failproofai?style=flat-square&color=2ea44f)](https://www.npmjs.com/package/failproofai) -**AI फेलियर हैंडलिंग**, **एरर रिकवरी**, और **LLM रिलायबिलिटी** के लिए हुक्स और पॉलिसीज। अपने AI एजेंट्स को **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Gemini CLI**, **Hermes**, और **Agents SDK** में विश्वसनीय और स्वायत्त रूप से चलाते रहें। +**AI फेलियर हैंडलिंग**, **एरर रिकवरी**, और **LLM रिलायबिलिटी** के लिए हुक्स और पॉलिसीज। अपने AI एजेंट्स को **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Hermes**, और **Agents SDK** में विश्वसनीय और स्वायत्त रूप से चलाते रहें। AI एजेंट्स पूर्वानुमानित तरीकों से फेल होते हैं। वे डिस्ट्रक्टिव कमांड्स चलाते हैं, सीक्रेट्स लीक करते हैं, अपने टास्क से भटकते हैं, लूप्स में फंसते हैं, या सीधे मेन में पुश करते हैं। जब बिना निरीक्षण के छोड़ दिया जाता है, तो छोटी-मोटी विफलताएं आउटेजेस, लीक किए गए क्रेडेंशियल्स और खोए हुए कार्य में बदल जाती हैं। FailproofAI इसे **पॉलिसीज** से हल करता है। ये नियम प्रत्येक एजेंट टूल कॉल में हुक करते हैं ताकि **फेलियर्स को डिटेक्ट** करें, **उन्हें कम करें** (ब्लॉक, इंस्ट्रक्ट, सैनिटाइज), और **आपको अलर्ट** करें जब कुछ ध्यान देने की जरूरत हो। एक लोकल डैशबोर्ड आपको बाद में प्रत्येक टूल कॉल, एजेंट फेलियर और रिकवरी एक्शन की समीक्षा करने देता है। -कोई डेटा आपकी मशीन से बाहर नहीं जाता। +ट्रांसक्रिप्ट और पॉलिसी मूल्यांकन आपकी मशीन पर रहते हैं। डेटा केवल तब भेजा जाता है जब आप कोई ऑनलाइन सुविधा स्पष्ट रूप से उपयोग करते हैं, जैसे प्रमाणित ऑडिट रिमाइंडर या आमंत्रण। ## शुरुआत करें @@ -55,4 +55,4 @@ failproofai policies --install # enable policies (या स्किप क failproofai # डैशबोर्ड लॉन्च करें ``` -पूर्ण वॉकथ्रू के लिए [गेटिंग स्टार्टेड](/hi/getting-started) गाइड देखें। \ No newline at end of file +पूर्ण वॉकथ्रू के लिए [गेटिंग स्टार्टेड](/hi/getting-started) गाइड देखें। diff --git a/docs/i18n/README.ar.md b/docs/i18n/README.ar.md index e5f60f40..61f83367 100644 --- a/docs/i18n/README.ar.md +++ b/docs/i18n/README.ar.md @@ -74,13 +74,6 @@        - - - - Gemini CLI - - -        @@ -89,7 +82,7 @@

-> تثبيت الخطافات لواحدة أو أي مزيج: `failproofai policies --install --cli opencode pi gemini` (أو `--cli claude codex copilot cursor opencode pi gemini hermes`). احذف `--cli` للكشف التلقائي عن واجهات سطر الأوامر المثبتة والمطالبة. +> تثبيت الخطافات لواحدة أو أي مزيج: `failproofai policies --install --cli opencode pi` (أو `--cli claude codex copilot cursor opencode pi hermes`). احذف `--cli` للكشف التلقائي عن واجهات سطر الأوامر المثبتة والمطالبة. > > **Hermes** (hermes-agent، بوابة Slack/Telegram) مدعومة لكل من **فرض الخطاف الحي** (`--cli hermes` — التثبيت الواحد يعترض استدعاءات الأدوات من كل منصة ووكيل فرعي) والتدقيق غير المتصل **إعادة تشغيل** لجلسات البوابة من قاعدة البيانات الواحدة `~/.hermes/state.db`. diff --git a/docs/i18n/README.de.md b/docs/i18n/README.de.md index 5c6f03dd..d4540a7c 100644 --- a/docs/i18n/README.de.md +++ b/docs/i18n/README.de.md @@ -72,13 +72,6 @@ bevor sie zu Vorfällen werden. Keine Latenz. Läuft lokal.        - - - - Gemini CLI - - -        @@ -87,7 +80,7 @@ bevor sie zu Vorfällen werden. Keine Latenz. Läuft lokal.

-> Hooks für eine oder eine beliebige Kombination installieren: `failproofai policies --install --cli opencode pi gemini` (oder `--cli claude codex copilot cursor opencode pi gemini hermes`). `--cli` weglassen, um installierte CLIs automatisch zu erkennen und zur Auswahl aufzufordern. +> Hooks für eine oder eine beliebige Kombination installieren: `failproofai policies --install --cli opencode pi` (oder `--cli claude codex copilot cursor opencode pi hermes`). `--cli` weglassen, um installierte CLIs automatisch zu erkennen und zur Auswahl aufzufordern. > > **Hermes** (hermes-agent, ein Slack/Telegram-Gateway) wird sowohl für **Live-Hook-Durchsetzung** (`--cli hermes` — eine Installation fängt Tool-Aufrufe von jeder Plattform und jedem Subagenten ab) als auch für das Offline-**Audit**-Replay seiner Gateway-Sitzungen aus der einzelnen `~/.hermes/state.db` unterstützt. diff --git a/docs/i18n/README.es.md b/docs/i18n/README.es.md index 8859f6b9..7ee277d5 100644 --- a/docs/i18n/README.es.md +++ b/docs/i18n/README.es.md @@ -72,13 +72,6 @@ antes de que se conviertan en incidentes. Cero latencia. Se ejecuta localmente.        - - - - Gemini CLI - - -        @@ -87,7 +80,7 @@ antes de que se conviertan en incidentes. Cero latencia. Se ejecuta localmente.

-> Instala hooks para uno o cualquier combinación: `failproofai policies --install --cli opencode pi gemini` (o `--cli claude codex copilot cursor opencode pi gemini hermes`). Omite `--cli` para detectar automáticamente los CLIs instalados y recibir una solicitud de confirmación. +> Instala hooks para uno o cualquier combinación: `failproofai policies --install --cli opencode pi` (o `--cli claude codex copilot cursor opencode pi hermes`). Omite `--cli` para detectar automáticamente los CLIs instalados y recibir una solicitud de confirmación. > > **Hermes** (hermes-agent, una pasarela de Slack/Telegram) es compatible tanto con la **aplicación de hooks en vivo** (`--cli hermes` — una sola instalación intercepta las llamadas a herramientas desde cada plataforma y subagente) como con la **auditoría** offline de sus sesiones de pasarela desde el único `~/.hermes/state.db`. diff --git a/docs/i18n/README.fr.md b/docs/i18n/README.fr.md index 47f8ff7e..f17bd886 100644 --- a/docs/i18n/README.fr.md +++ b/docs/i18n/README.fr.md @@ -72,13 +72,6 @@ avant qu'ils ne deviennent des incidents. Zéro latence. Fonctionne en local.        - - - - Gemini CLI - - -        @@ -87,7 +80,7 @@ avant qu'ils ne deviennent des incidents. Zéro latence. Fonctionne en local.

-> Installez les hooks pour un ou plusieurs CLI au choix : `failproofai policies --install --cli opencode pi gemini` (ou `--cli claude codex copilot cursor opencode pi gemini hermes`). Omettez `--cli` pour détecter automatiquement les CLI installés et recevoir une invite de sélection. +> Installez les hooks pour un ou plusieurs CLI au choix : `failproofai policies --install --cli opencode pi` (ou `--cli claude codex copilot cursor opencode pi hermes`). Omettez `--cli` pour détecter automatiquement les CLI installés et recevoir une invite de sélection. > > **Hermes** (hermes-agent, une passerelle Slack/Telegram) est pris en charge à la fois pour l'**application des hooks en temps réel** (`--cli hermes` — une seule installation intercepte les appels d'outils de chaque plateforme et sous-agent) et pour la **relecture d'audit** hors ligne de ses sessions de passerelle depuis l'unique `~/.hermes/state.db`. diff --git a/docs/i18n/README.he.md b/docs/i18n/README.he.md index 7022d350..365cf06c 100644 --- a/docs/i18n/README.he.md +++ b/docs/i18n/README.he.md @@ -74,13 +74,6 @@        - - - - Gemini CLI - - -        @@ -89,7 +82,7 @@

-> התקן hooks לאחד או לכל שילוב: `failproofai policies --install --cli opencode pi gemini` (או `--cli claude codex copilot cursor opencode pi gemini hermes`). השמט `--cli` לגילוי אוטומטי של CLIs מותקנים ולהנחיה. +> התקן hooks לאחד או לכל שילוב: `failproofai policies --install --cli opencode pi` (או `--cli claude codex copilot cursor opencode pi hermes`). השמט `--cli` לגילוי אוטומטי של CLIs מותקנים ולהנחיה. > > **Hermes** (hermes-agent, שער Slack/Telegram) נתמך גם ל**אכיפת hook חי** (`--cli hermes` — התקנה אחת מיירטת קריאות כלים מכל פלטפורמה וסוכן משנה) וגם **ביקורת** במצב לא מקוון של הפעלות השער שלו מ`~/.hermes/state.db` הבודד. diff --git a/docs/i18n/README.hi.md b/docs/i18n/README.hi.md index d25e5512..4f9cce51 100644 --- a/docs/i18n/README.hi.md +++ b/docs/i18n/README.hi.md @@ -72,13 +72,6 @@ Claude Code और Codex में हुक करता है। लूप,        - - - - Gemini CLI - - -        @@ -87,7 +80,7 @@ Claude Code और Codex में हुक करता है। लूप,

-> एक या किसी भी संयोजन के लिए हुक इंस्टॉल करें: `failproofai policies --install --cli opencode pi gemini` (या `--cli claude codex copilot cursor opencode pi gemini hermes`)। स्वचालित रूप से स्थापित CLIs का पता लगाने और संकेत देने के लिए `--cli` को छोड़ दें। +> एक या किसी भी संयोजन के लिए हुक इंस्टॉल करें: `failproofai policies --install --cli opencode pi` (या `--cli claude codex copilot cursor opencode pi hermes`)। स्वचालित रूप से स्थापित CLIs का पता लगाने और संकेत देने के लिए `--cli` को छोड़ दें। > > **Hermes** (hermes-agent, एक Slack/Telegram गेटवे) **लाइव-हुक प्रवर्तन** (`--cli hermes` — एक इंस्टॉल प्रत्येक प्लेटफॉर्म और सबएजेंट से टूल कॉल को रोकता है) और इसके गेटवे सत्रों की ऑफलाइन **ऑडिट** रिप्ले दोनों के लिए समर्थित है। diff --git a/docs/i18n/README.it.md b/docs/i18n/README.it.md index c6a4bac7..4091c1a7 100644 --- a/docs/i18n/README.it.md +++ b/docs/i18n/README.it.md @@ -72,13 +72,6 @@ prima che diventino incidenti. Latenza zero. Eseguito localmente.        - - - - Gemini CLI - - -        @@ -87,7 +80,7 @@ prima che diventino incidenti. Latenza zero. Eseguito localmente.

-> Installa gli hook per uno o qualsiasi combinazione: `failproofai policies --install --cli opencode pi gemini` (o `--cli claude codex copilot cursor opencode pi gemini hermes`). Ometti `--cli` per rilevare automaticamente le CLI installate e ricevere un prompt. +> Installa gli hook per uno o qualsiasi combinazione: `failproofai policies --install --cli opencode pi` (o `--cli claude codex copilot cursor opencode pi hermes`). Ometti `--cli` per rilevare automaticamente le CLI installate e ricevere un prompt. > > **Hermes** (hermes-agent, un gateway Slack/Telegram) è supportato sia per l'**applicazione dei hook in diretta** (`--cli hermes` — un'unica installazione intercetta le chiamate ai tool da ogni piattaforma e subagente) che per il **controllo** offline della riproduzione delle sue sessioni gateway dal singolo `~/.hermes/state.db`. diff --git a/docs/i18n/README.ja.md b/docs/i18n/README.ja.md index 521e8204..9ef5129b 100644 --- a/docs/i18n/README.ja.md +++ b/docs/i18n/README.ja.md @@ -72,13 +72,6 @@ Claude Code と Codex にフックして、ループ・危険な操作・シー        - - - - Gemini CLI - - -        @@ -87,7 +80,7 @@ Claude Code と Codex にフックして、ループ・危険な操作・シー

-> 1つまたは任意の組み合わせでフックをインストールできます: `failproofai policies --install --cli opencode pi gemini`(または `--cli claude codex copilot cursor opencode pi gemini hermes`)。`--cli` を省略するとインストール済み CLI を自動検出してプロンプトを表示します。 +> 1つまたは任意の組み合わせでフックをインストールできます: `failproofai policies --install --cli opencode pi`(または `--cli claude codex copilot cursor opencode pi hermes`)。`--cli` を省略するとインストール済み CLI を自動検出してプロンプトを表示します。 > > **Hermes**(hermes-agent、Slack/Telegram ゲートウェイ)は、**ライブフック強制実行**(`--cli hermes` — 1回のインストールですべてのプラットフォームとサブエージェントのツール呼び出しをインターセプト)と、単一の `~/.hermes/state.db` からのゲートウェイセッションのオフライン**監査**リプレイの両方に対応しています。 diff --git a/docs/i18n/README.ko.md b/docs/i18n/README.ko.md index 2b49b7e7..b38c022b 100644 --- a/docs/i18n/README.ko.md +++ b/docs/i18n/README.ko.md @@ -72,13 +72,6 @@ Claude Code 및 Codex에 연동됩니다. 루프, 위험한 동작, 시크릿        - - - - Gemini CLI - - -        @@ -87,7 +80,7 @@ Claude Code 및 Codex에 연동됩니다. 루프, 위험한 동작, 시크릿

-> 하나 또는 여러 CLI를 조합하여 훅을 설치할 수 있습니다: `failproofai policies --install --cli opencode pi gemini` (또는 `--cli claude codex copilot cursor opencode pi gemini hermes`). `--cli`를 생략하면 설치된 CLI를 자동으로 감지하고 선택을 안내합니다. +> 하나 또는 여러 CLI를 조합하여 훅을 설치할 수 있습니다: `failproofai policies --install --cli opencode pi` (또는 `--cli claude codex copilot cursor opencode pi hermes`). `--cli`를 생략하면 설치된 CLI를 자동으로 감지하고 선택을 안내합니다. > > **Hermes** (hermes-agent, Slack/Telegram 게이트웨이)는 **실시간 훅 적용** (`--cli hermes` — 한 번의 설치로 모든 플랫폼과 서브에이전트의 툴 호출을 가로챔)과 단일 `~/.hermes/state.db`의 게이트웨이 세션 오프라인 **감사** 재생을 모두 지원합니다. diff --git a/docs/i18n/README.pt-br.md b/docs/i18n/README.pt-br.md index 967087e9..4d0a4f7d 100644 --- a/docs/i18n/README.pt-br.md +++ b/docs/i18n/README.pt-br.md @@ -72,13 +72,6 @@ antes que se tornem incidentes. Latência zero. Roda localmente.        - - - - Gemini CLI - - -        @@ -87,7 +80,7 @@ antes que se tornem incidentes. Latência zero. Roda localmente.

-> Instale hooks para um ou qualquer combinação: `failproofai policies --install --cli opencode pi gemini` (ou `--cli claude codex copilot cursor opencode pi gemini hermes`). Omita `--cli` para detectar automaticamente os CLIs instalados e ser solicitado a escolher. +> Instale hooks para um ou qualquer combinação: `failproofai policies --install --cli opencode pi` (ou `--cli claude codex copilot cursor opencode pi hermes`). Omita `--cli` para detectar automaticamente os CLIs instalados e ser solicitado a escolher. > > **Hermes** (hermes-agent, um gateway para Slack/Telegram) é suportado tanto para **aplicação de hooks em tempo real** (`--cli hermes` — uma única instalação intercepta chamadas de ferramentas de todas as plataformas e subagentes) quanto para **auditoria** offline a partir de sessões do gateway gravadas no `~/.hermes/state.db`. diff --git a/docs/i18n/README.ru.md b/docs/i18n/README.ru.md index e84b9461..cdd69b6b 100644 --- a/docs/i18n/README.ru.md +++ b/docs/i18n/README.ru.md @@ -72,13 +72,6 @@        - - - - Gemini CLI - - -        @@ -87,7 +80,7 @@

-> Установите hooks для одного или любой комбинации: `failproofai policies --install --cli opencode pi gemini` (или `--cli claude codex copilot cursor opencode pi gemini hermes`). Опустите `--cli` для автоматического обнаружения установленных CLI и подсказок. +> Установите hooks для одного или любой комбинации: `failproofai policies --install --cli opencode pi` (или `--cli claude codex copilot cursor opencode pi hermes`). Опустите `--cli` для автоматического обнаружения установленных CLI и подсказок. > > **Hermes** (hermes-agent, шлюз Slack/Telegram) поддерживается как для **live-hook принудительного исполнения** (`--cli hermes` — одна установка перехватывает вызовы инструментов с каждой платформы и субагента), так и для автономного **аудита** воспроизведения сессий шлюза из отдельного `~/.hermes/state.db`. diff --git a/docs/i18n/README.tr.md b/docs/i18n/README.tr.md index 579f34ba..314e3504 100644 --- a/docs/i18n/README.tr.md +++ b/docs/i18n/README.tr.md @@ -72,13 +72,6 @@ olaylar haline gelmeden önce yakalar. Sıfır gecikme. Yerel olarak çalışır        - - - - Gemini CLI - - -        @@ -87,7 +80,7 @@ olaylar haline gelmeden önce yakalar. Sıfır gecikme. Yerel olarak çalışır

-> Bir veya herhangi bir kombinasyon için kanca yükleyin: `failproofai policies --install --cli opencode pi gemini` (veya `--cli claude codex copilot cursor opencode pi gemini hermes`). Yüklü CLI'ları otomatik olarak algılamak ve istemek için `--cli` öğesini atlayın. +> Bir veya herhangi bir kombinasyon için kanca yükleyin: `failproofai policies --install --cli opencode pi` (veya `--cli claude codex copilot cursor opencode pi hermes`). Yüklü CLI'ları otomatik olarak algılamak ve istemek için `--cli` öğesini atlayın. > > **Hermes** (hermes-agent, bir Slack/Telegram ağ geçidi) hem **canlı kanca uygulaması** (`--cli hermes` — bir kurulum her platformdan ve alt ajantan araç çağrılarını keser) hem de çevrimdışı **denetim** için desteklenir. tek `~/.hermes/state.db` dosyasından ağ geçidi oturumlarının tekrar oynatılması. diff --git a/docs/i18n/README.vi.md b/docs/i18n/README.vi.md index d0ff72b8..c84f2b3e 100644 --- a/docs/i18n/README.vi.md +++ b/docs/i18n/README.vi.md @@ -72,13 +72,6 @@ trước khi chúng trở thành sự cố. Độ trễ bằng không. Chạy c        - - - - Gemini CLI - - -        @@ -87,7 +80,7 @@ trước khi chúng trở thành sự cố. Độ trễ bằng không. Chạy c

-> Cài đặt hooks cho một hoặc bất kỳ tổ hợp nào: `failproofai policies --install --cli opencode pi gemini` (hoặc `--cli claude codex copilot cursor opencode pi gemini hermes`). Bỏ qua `--cli` để tự động phát hiện các CLI được cài đặt và nhắc lựa chọn. +> Cài đặt hooks cho một hoặc bất kỳ tổ hợp nào: `failproofai policies --install --cli opencode pi` (hoặc `--cli claude codex copilot cursor opencode pi hermes`). Bỏ qua `--cli` để tự động phát hiện các CLI được cài đặt và nhắc lựa chọn. > > **Hermes** (hermes-agent, một gateway Slack/Telegram) được hỗ trợ cho cả **thực thi live-hook** (`--cli hermes` — một cài đặt chặn các lệnh gọi công cụ từ mọi nền tảng và subagent) và **kiểm toán** ngoại tuyến của các phiên gateway của nó từ `~/.hermes/state.db` duy nhất. diff --git a/docs/i18n/README.zh.md b/docs/i18n/README.zh.md index b6112fe9..32269ce7 100644 --- a/docs/i18n/README.zh.md +++ b/docs/i18n/README.zh.md @@ -71,13 +71,6 @@        - - - - Gemini CLI - - -        @@ -86,7 +79,7 @@

-> 可为一个或多个 CLI 安装 hooks:`failproofai policies --install --cli opencode pi gemini`(或 `--cli claude codex copilot cursor opencode pi gemini hermes`)。省略 `--cli` 参数将自动检测已安装的 CLI 并提示选择。 +> 可为一个或多个 CLI 安装 hooks:`failproofai policies --install --cli opencode pi`(或 `--cli claude codex copilot cursor opencode pi hermes`)。省略 `--cli` 参数将自动检测已安装的 CLI 并提示选择。 > > **Hermes**(hermes-agent,一个 Slack/Telegram 网关)同时支持**实时 hook 执行**(`--cli hermes` — 一次安装即可拦截所有平台和子智能体的工具调用)以及离线**审计**回放,可从单一的 `~/.hermes/state.db` 文件回放其网关会话。 diff --git a/docs/introduction.mdx b/docs/introduction.mdx index 3d19c679..d260daf5 100644 --- a/docs/introduction.mdx +++ b/docs/introduction.mdx @@ -5,13 +5,13 @@ description: "FailproofAI gives AI agents 39 built-in failure policies that catc [![npm weekly downloads](https://img.shields.io/npm/dw/failproofai?style=flat-square&color=2ea44f)](https://www.npmjs.com/package/failproofai) -Hooks and policies for **AI failure handling**, **error recovery**, and **LLM reliability**. Keep your AI agents reliable and running autonomously across **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Gemini CLI**, **Hermes**, and the **Agents SDK**. +Hooks and policies for **AI failure handling**, **error recovery**, and **LLM reliability**. Keep your AI agents reliable and running autonomously across **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Hermes**, **OpenClaw**, **Factory Droid**, **Devin CLI**, **Antigravity CLI**, and the **Agents SDK**. AI agents fail in predictable ways. They run destructive commands, leak secrets, drift off-task, get stuck in loops, or push directly to main. Left unattended, small failures cascade into outages, leaked credentials, and lost work. FailproofAI solves this with **policies**. These rules hook into every agent tool call to **detect failures**, **mitigate them** (block, instruct, sanitize), and **alert you** when something needs attention. A local dashboard lets you review every tool call, agent failure, and recovery action afterward. -No data leaves your machine. +Transcripts and policy evaluation stay on your machine. Data is sent only when you explicitly use an online feature, such as authenticated audit reminders or invitations. ## Get started diff --git a/docs/it/built-in-policies.mdx b/docs/it/built-in-policies.mdx index b13f76c4..287dc107 100644 --- a/docs/it/built-in-policies.mdx +++ b/docs/it/built-in-policies.mdx @@ -712,20 +712,19 @@ L'applicazione di Stop varia leggermente tra i sette CLI supportati perché ognu | Codex | Stesso loop dell'agent, immediatamente | Come Claude. | | GitHub Copilot CLI | Stesso loop dell'agent, immediatamente | Come Claude (usa il canale di retry `{decision:"block", reason}` di Copilot — verificato empiricamente contro Copilot CLI 1.0.41). | | Cursor Agent | Stesso loop dell'agent, immediatamente | Come Claude (usa il canale `{followup_message}` di Cursor — limitato a `loop_limit`, default 5 retry). | -| Gemini CLI | Stesso loop dell'agent, immediatamente | Come Claude (usa il canale `{decision:"block", reason}` di Gemini su `AfterAgent`). | | OpenCode | Stesso loop dell'agent, immediatamente | Come Claude (usa la chiamata SDK `client.session.prompt(...)` di OpenCode instradato tramite `hookSpecificOutput.additionalContext`). | | **Pi (pi-coding-agent)** | **Turno utente successivo** | **Pi si ferma visibilmente** quando il gate si attiva — il suo loop di agent esce e sei riportato al prompt. Il gate si attiva quindi la prossima volta che invii un prompt: failproofai prepone una direttiva `MANDATORY ACTION REQUIRED` al prompt di sistema di quel turno, istruendo il LLM a completare il passo del workflow (commit, push, ecc.) prima di fare tutto quello che hai chiesto. | -**Limitazione di Pi.** L'evento `AgentEndEvent` di Pi (l'equivalente upstream dell'hook `Stop` di Claude) non ha un tipo Result — nel momento in cui si attiva, il loop di agent di Pi è già uscito. Pi non può essere forzato a riprovare lo stesso loop nel modo in cui Claude / Copilot / Cursor / Gemini / OpenCode possono. failproofai sposta il gate all'evento `before_agent_start` di Pi (che si attiva dopo il prossimo prompt dell'utente) in modo che il controllo del workflow ancora si applichi, solo al turno successivo piuttosto che al turno corrente. +**Limitazione di Pi.** L'evento `AgentEndEvent` di Pi (l'equivalente upstream dell'hook `Stop` di Claude) non ha un tipo Result — nel momento in cui si attiva, il loop di agent di Pi è già uscito. Pi non può essere forzato a riprovare lo stesso loop nel modo in cui Claude / Copilot / Cursor / OpenCode possono. failproofai sposta il gate all'evento `before_agent_start` di Pi (che si attiva dopo il prossimo prompt dell'utente) in modo che il controllo del workflow ancora si applichi, solo al turno successivo piuttosto che al turno corrente. **Cosa significa nella pratica:** - Dopo che Pi si ferma, il motivo della negazione viene catturato in memoria keyed dall'ID sessione di Pi. Il prossimo prompt che invii nello stesso processo Pi lo scarica: l'LLM vede la direttiva `MANDATORY ACTION REQUIRED` in cima al suo prompt di sistema, fa il commit (o push / apre il PR / aspetta CI), e solo allora continua con la tua richiesta. Il motivo della negazione catturato è monouso — una volta scaricato, il gate è pulito. -- Il gate è limitato dalla durata del processo di Pi. Se fai `Ctrl+C` su Pi o esci tra i turni, la voce in memoria viene eliminata insieme al processo e il gate viene mancato. Claude, Copilot, Cursor, Gemini e OpenCode hanno lo stesso limite (uccidi l'agent e il gate viene mancato) — Pi lo rende solo più visibile perché l'agent si ferma visibilmente prima che il gate si attivi. +- Il gate è limitato dalla durata del processo di Pi. Se fai `Ctrl+C` su Pi o esci tra i turni, la voce in memoria viene eliminata insieme al processo e il gate viene mancato. Claude, Copilot, Cursor e OpenCode hanno lo stesso limite (uccidi l'agent e il gate viene mancato) — Pi lo rende solo più visibile perché l'agent si ferma visibilmente prima che il gate si attivi. - Una negazione in sospeso viene anche cancellata su `session_shutdown` per qualsiasi motivo (`new` / `resume` / `fork` / `quit`), quindi un gate stantio da una sessione precedente non può fuoriuscire in una sessione fresca avviata nello stesso processo Pi. -Se hai bisogno del retry dello stesso loop nello stile di Claude, esegui le tue politiche di `Stop` sotto uno dei sei altri CLI supportati. Stiamo monitorando Pi upstream per un tipo Result futuro su `AgentEndEvent` che ci permetterebbe di chiudere questo gap. +Se hai bisogno del retry dello stesso loop nello stile di Claude, esegui le tue politiche di `Stop` sotto uno dei cinque altri CLI supportati. Stiamo monitorando Pi upstream per un tipo Result futuro su `AgentEndEvent` che ci permetterebbe di chiudere questo gap. ### `require-commit-before-stop` diff --git a/docs/it/cli/audit.mdx b/docs/it/cli/audit.mdx index 7e463043..08b68e37 100644 --- a/docs/it/cli/audit.mdx +++ b/docs/it/cli/audit.mdx @@ -52,7 +52,7 @@ failproofai finché non lo arresti con `Ctrl+C`. -Il dashboard scansiona i transcript passati da agent CLI su questa macchina (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi, Gemini) e segnala con che frequenza l'agente ha fatto cose che failproofai è costruito per bloccare — controlli di variabili d'ambiente, push forzati, prefissi `cd ` ridondanti, loop di polling con sleep, ri-letture di file appena modificati, e altro. +Il dashboard scansiona i transcript passati da agent CLI su questa macchina (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi) e segnala con che frequenza l'agente ha fatto cose che failproofai è costruito per bloccare — controlli di variabili d'ambiente, push forzati, prefissi `cd ` ridondanti, loop di polling con sleep, ri-letture di file appena modificati, e altro. Per ogni transcript, ogni evento tool-use viene rieseguito attraverso le 39 policy integrate **e** attraverso 8 rilevatori solo audit che catturano pattern non ancora coperti da policy runtime. I conteggi vengono aggregati per policy / rilevatore in tutte le sessioni. diff --git a/docs/it/configuration.mdx b/docs/it/configuration.mdx index ad260ca3..e9d6f33e 100644 --- a/docs/it/configuration.mdx +++ b/docs/it/configuration.mdx @@ -196,13 +196,12 @@ I comandi `policies --install` e `policies --uninstall` scrivono nel file di imp - **OpenAI Codex**: `~/.codex/hooks.json` (user), `/.codex/hooks.json` (project) — Codex non ha un ambito `local` - **GitHub Copilot CLI _(beta)_**: `~/.copilot/hooks/failproofai.json` (user), `/.github/hooks/failproofai.json` (project) — Copilot non ha un ambito `local`. Le voci hook usano i campi di comando `bash`/`powershell` di Copilot con chiave OS e `timeoutSec`; il file porta un marcatore `version: 1` di alto livello. Il supporto di Copilot CLI è **beta** mentre verifichiamo lo schema del record `events.jsonl` (che la documentazione pubblica non specifica) rispetto a più sessioni del mondo reale. - **Cursor Agent _(beta)_**: `~/.cursor/hooks.json` (user), `/.cursor/hooks.json` (project) — Cursor non ha un ambito `local`. Le voci hook usano la forma `{type, command, timeout}` a forma di Claude (senza split `bash`/`powershell`), ma memorizzate sotto chiavi di evento camelCase (`preToolUse`, `beforeSubmitPrompt`, …) in un array piatto per lo [schema degli hook](https://cursor.com/docs/hooks) di Cursor; il file porta un marcatore `version: 1` di alto livello. Il gestore canonicalizza camelCase → PascalCase tramite `CURSOR_EVENT_MAP` quindi le policy built-in esistenti si attivano invariate. Il supporto di Cursor Agent è **beta** mentre verifichiamo il formato del transcript di Cursor su disco (non specificato nella documentazione pubblica) rispetto a più installazioni del mondo reale. - - **OpenCode _(beta)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (user), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (project) — OpenCode non ha un ambito `local`. A differenza degli altri sei CLI, OpenCode ha **nessun sistema di hook per comando esterno**: carica plugin JS/TS in-process esplicitamente registrati tramite l'array `plugin: []` in `opencode.json` (l'auto-discovery da `.opencode/plugins/` **non** è come i plugin vengono caricati su opencode v1.14.33). L'installazione rilascia un piccolo shim plugin generato che subprocess-chiama il binario failproofai e traduce la risposta JSON a forma di Claude del binario di nuovo in semantica plugin: `throw new Error()` per tool-event deny (cancella la chiamata dello strumento), `client.session.prompt(...)` per instruct E per deny di `Stop` / `SubagentStop` (invia la ragione di deny come prossimo messaggio utente — l'unico canale force-retry dal momento che `session.idle` è notification-only e gettare da essa è un no-op), e no-op per allow. Lo shim canonicalizza sia i nomi di strumento (lowercase → PascalCase tramite `OPENCODE_TOOL_MAP`) che le chiavi degli argomenti di input dello strumento (camelCase → snake_case tramite `OPENCODE_TOOL_INPUT_MAP` per `Read` / `Write` / `Edit`, ad es. `filePath` → `file_path`, `oldString` → `old_string`) prima di inoltrarsi al binario, così il controllo dei percorsi built-in come `block-read-outside-cwd`, `block-env-files`, e `block-secrets-write` si attivano invariate sulle chiamate agli strumenti di OpenCode. Le sessioni vivono nel DB SQLite di opencode presso `~/.local/share/opencode/opencode.db`; il visualizzatore di sessioni della dashboard li legge tramite `opencode db --format json` e `opencode export `. Il supporto di OpenCode è **beta** mentre verifichiamo il comportamento attraverso versioni e rispetto a più sessioni del mondo reale. Vedi la [documentazione dei plugin di OpenCode](https://opencode.ai/docs/plugins/). + - **OpenCode _(beta)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (user), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (project) — OpenCode non ha un ambito `local`. A differenza degli altri cinque CLI, OpenCode ha **nessun sistema di hook per comando esterno**: carica plugin JS/TS in-process esplicitamente registrati tramite l'array `plugin: []` in `opencode.json` (l'auto-discovery da `.opencode/plugins/` **non** è come i plugin vengono caricati su opencode v1.14.33). L'installazione rilascia un piccolo shim plugin generato che subprocess-chiama il binario failproofai e traduce la risposta JSON a forma di Claude del binario di nuovo in semantica plugin: `throw new Error()` per tool-event deny (cancella la chiamata dello strumento), `client.session.prompt(...)` per instruct E per deny di `Stop` / `SubagentStop` (invia la ragione di deny come prossimo messaggio utente — l'unico canale force-retry dal momento che `session.idle` è notification-only e gettare da essa è un no-op), e no-op per allow. Lo shim canonicalizza sia i nomi di strumento (lowercase → PascalCase tramite `OPENCODE_TOOL_MAP`) che le chiavi degli argomenti di input dello strumento (camelCase → snake_case tramite `OPENCODE_TOOL_INPUT_MAP` per `Read` / `Write` / `Edit`, ad es. `filePath` → `file_path`, `oldString` → `old_string`) prima di inoltrarsi al binario, così il controllo dei percorsi built-in come `block-read-outside-cwd`, `block-env-files`, e `block-secrets-write` si attivano invariate sulle chiamate agli strumenti di OpenCode. Le sessioni vivono nel DB SQLite di opencode presso `~/.local/share/opencode/opencode.db`; il visualizzatore di sessioni della dashboard li legge tramite `opencode db --format json` e `opencode export `. Il supporto di OpenCode è **beta** mentre verifichiamo il comportamento attraverso versioni e rispetto a più sessioni del mondo reale. Vedi la [documentazione dei plugin di OpenCode](https://opencode.ai/docs/plugins/). - **Pi _(beta)_**: `~/.pi/agent/settings.json` (user), `/.pi/settings.json` (project) — Pi non ha un ambito `local`. Pi carica pacchetti di estensioni TypeScript all'avvio; il file di impostazioni è un array di stringhe piatto `{"packages": ["./relative/path", …]}`. failproofai scrive una singola voce dell'array dei pacchetti che punta alla sua directory `pi-extension/` in bundle. L'estensione internamente si sottoscrive agli eventi `tool_call` / `user_bash` / `input` / `session_start` di Pi e guscio fuori a `failproofai --hook --cli pi`; il gestore canonicalizza underscore_lower_snake_case → PascalCase tramite `PI_EVENT_MAP` quindi le policy built-in esistenti si attivano invariate. Gli argomenti di input dello strumento sono anche canonicalizzati tramite `PI_TOOL_INPUT_MAP` (Pi's Read / Write / Edit forniscono `path` piuttosto che `file_path`; mappare la chiave di top-level lascia che `block-env-files` e `block-secrets-write` si attivino — `block-read-outside-cwd` già aveva un fallback `path`). Il supporto di Pi è **beta** mentre l'API di estensione di Pi e il layout del log di sessione si stabilizzano. - - **Gemini CLI _(beta)_**: `~/.gemini/settings.json` (user), `/.gemini/settings.json` (project) — Gemini non ha un ambito `local` (documenta un ambito `system` presso `/etc/gemini-cli/settings.json` che failproofai non espone). Le voci hook usano la forma `{type, command, timeout}` di Claude avvolta nello schema matcher `{matcher, hooks: [...]}` di Gemini con `matcher: "*"` di default. Gli eventi sono PascalCase (`SessionStart`, `BeforeAgent`, `AfterAgent`, `BeforeModel`, `AfterModel`, `BeforeToolSelection`, `BeforeTool`, `AfterTool`, `PreCompress`, `Notification`, `SessionEnd`); il gestore mappa ai nomi canonici Claude tramite `GEMINI_EVENT_MAP`. I nomi di strumento sono snake_case (`run_shell_command`, `read_file`, `write_file`, `replace`, …) — il gestore canonicalizza tramite `GEMINI_TOOL_MAP` quindi le policy built-in esistenti si attivano invariate. L'evaluator di policy emette la forma piatta di Gemini `{decision: "deny", reason}` (preferita per il contratto Golden Rule di Gemini exit-0), `{hookSpecificOutput: {hookEventName, additionalContext}}` per l'iniezione di contesto su BeforeAgent / AfterTool / SessionStart, e `{decision: "block", reason}` su AfterAgent per la semantica force-retry. Il supporto di Gemini CLI è **beta** mentre allargheremo la copertura del mondo reale. Vedi la [documentazione degli hook di Gemini CLI](https://geminicli.com/docs/hooks/). - **Hermes (hermes-agent)**: `~/.hermes/config.yaml` (**ambito utente solo** — Hermes non ha configurazione project/local). Hermes è un gateway **Slack/Telegram**, quindi un'installazione intercetta le chiamate di strumento da ogni piattaforma (Slack/Telegram/cli/cron) **e** subagent interni. Le voci hook sono una coppia `{command, timeout}` (timeout in **secondi**) sotto una mappa `hooks:` codificata dagli eventi snake_case di Hermes (`pre_tool_call` / `post_tool_call` / `on_session_start` / `on_session_end` / `subagent_stop`); il gestore canonicalizza gli eventi tramite `HERMES_EVENT_MAP` e i nomi di strumento tramite `HERMES_TOOL_MAP` quindi le policy built-in si attivano invariate. La configurazione è modificata tramite una `Document` YAML di preservazione dei commenti round-trip in modo che le altre impostazioni dell'operatore sopravvivano, e l'installazione imposta `hooks_auto_accept: true` così il gateway headless (no TTY) esegue gli hook senza un prompt di consenso. L'evaluator emette il contratto stdout `{"decision":"block","reason"}` di Hermes (Hermes ignora i codici di uscita). **Limitazioni:** Hermes non ha un evento end-turn `Stop`, quindi le policy built-in `require-*-before-stop` non si attivano mai per esso (inapplicabile, non rotto); `instruct` degrada a allow-with-logged-note (nessun canale additional-context); e la redazione di secret di output (`sanitize-*`) non può riscrivere l'output dello strumento sul contratto dell'hook di shell. Hermes è **anche** una fonte di **audit** offline — la dashboard legge le sue sessioni di gateway direttamente da `~/.hermes/state.db`. - **`policies-config.json`** — dice a failproofai quali policy valutare e con quali parametri (condiviso su tutti gli agent CLI) -Passa `--cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes` per indirizzare un agent specifico (space-separated o ripetuto per qualsiasi subset): +Passa `--cli claude|codex|copilot|cursor|opencode|pi|hermes` per indirizzare un agent specifico (space-separated o ripetuto per qualsiasi subset): ```bash failproofai policies --install --cli codex --scope project @@ -210,12 +209,11 @@ failproofai policies --install --cli copilot --scope project failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project -failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user -failproofai policies --install --cli claude codex copilot cursor opencode pi gemini +failproofai policies --install --cli claude codex copilot cursor opencode pi ``` -Quando `--cli` è omesso, `failproofai` rileva quali agent CLI sono installati (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which gemini` / `which hermes`): +Quando `--cli` è omesso, `failproofai` rileva quali agent CLI sono installati (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which hermes`): - **Un CLI rilevato** — auto-seleziona quel CLI senza chiedere. - **Più CLI rilevati** in un terminale interattivo — mostra un prompt single-select con tasti freccia raggruppati in una sezione `Detected (N)` (con una riga aggregata `Install for all N detected` + ogni CLI rilevato singolarmente) e una sezione `Not installed (M) · install hooks ahead of time` che elenca ogni CLI non rilevato supportato come opzione forward-install (↑↓ per muoversi, Enter per selezionare, ^C per uscire). Il flusso di disinstallazione mostra solo la sezione Detected. diff --git a/docs/it/dashboard.mdx b/docs/it/dashboard.mdx index a4dcc440..8736ff77 100644 --- a/docs/it/dashboard.mdx +++ b/docs/it/dashboard.mdx @@ -16,7 +16,7 @@ failproofai Si apre su `http://localhost:8020`. -Il dashboard legge direttamente dal filesystem - le tue cartelle di progetti Claude Code e i file di configurazione failproofai. Nulla viene scritto su un servizio remoto. +Il dashboard legge i dati locali di progetti, sessioni e configurazione failproofai direttamente dal filesystem. Le funzionalità autenticate facoltative, come i promemoria di audit e gli inviti, inviano alle API remote le informazioni necessarie per tali richieste, inclusi gli indirizzi email. --- @@ -24,11 +24,11 @@ Il dashboard legge direttamente dal filesystem - le tue cartelle di progetti Cla ### Progetti -Elenca tutti i progetti Claude Code, OpenAI Codex, GitHub Copilot CLI _(beta)_, Cursor Agent _(beta)_, OpenCode _(beta)_, Pi _(beta)_, Gemini CLI _(beta)_ e Hermes trovati sulla tua macchina. I progetti Claude vengono rilevati da `~/.claude/projects/` (o dal percorso impostato da `CLAUDE_PROJECTS_PATH`); i progetti Codex vengono rilevati scansionando ogni trascrizione in `~/.codex/sessions///
/*.jsonl` e raggruppando per il `cwd` registrato nel primo record di ogni sessione; i progetti Copilot CLI vengono rilevati scansionando ogni `~/.copilot/session-state//workspace.yaml` (configurabile tramite `COPILOT_HOME`) e raggruppando per il suo campo `cwd`; i progetti Cursor Agent vengono rilevati scansionando i metadati per sessione in `~/.cursor/agent-sessions//` (configurabile tramite `CURSOR_HOME`, con `conversations/` e `sessions/` testati come fallback) per uno scalare `cwd` in `meta.json` / `session.json` / `workspace.yaml`; i progetti OpenCode vengono rilevati interrogando il suo DB SQLite in `~/.local/share/opencode/opencode.db` tramite `opencode db --format json` (leggiamo le tabelle `session` e `project` e raggruppiamo per `project_id`); i progetti Pi vengono rilevati scansionando trascrizioni JSONL per sessione in `~/.pi/agent/sessions//_.jsonl` (configurabile tramite `PI_SESSIONS_DIR`) e estraendo il `cwd` dal primo record di ogni sessione; i progetti Gemini CLI vengono rilevati scansionando `~/.gemini/tmp//chats/session--.jsonl` (configurabile tramite `GEMINI_SESSIONS_DIR`) e recuperando il cwd canonico dal marcatore di testo `.project_root` adiacente; le sessioni del gateway Hermes vengono lette direttamente dal suo archivio SQLite in `~/.hermes/state.db` (configurabile tramite `HERMES_DB_PATH`) e raggruppate in progetti `hermes-` per `source` (Slack/Telegram/cli/cron — le sessioni del gateway non hanno cwd). Un progetto utilizzato da più CLI viene visualizzato come una singola riga con tutti i badge corrispondenti. Utilizza il dropdown **CLI** sopra la tabella per filtrare per uno specifico agente CLI; l'URL preserva la tua selezione come `?cli=claude|codex|copilot|cursor|opencode|pi|gemini|hermes`. +Elenca tutti i progetti Claude Code, OpenAI Codex, GitHub Copilot CLI _(beta)_, Cursor Agent _(beta)_, OpenCode _(beta)_, Pi _(beta)_ e Hermes trovati sulla tua macchina. I progetti Claude vengono rilevati da `~/.claude/projects/` (o dal percorso impostato da `CLAUDE_PROJECTS_PATH`); i progetti Codex vengono rilevati scansionando ogni trascrizione in `~/.codex/sessions///
/*.jsonl` e raggruppando per il `cwd` registrato nel primo record di ogni sessione; i progetti Copilot CLI vengono rilevati scansionando ogni `~/.copilot/session-state//workspace.yaml` (configurabile tramite `COPILOT_HOME`) e raggruppando per il suo campo `cwd`; i progetti Cursor Agent vengono rilevati scansionando i metadati per sessione in `~/.cursor/agent-sessions//` (configurabile tramite `CURSOR_HOME`, con `conversations/` e `sessions/` testati come fallback) per uno scalare `cwd` in `meta.json` / `session.json` / `workspace.yaml`; i progetti OpenCode vengono rilevati interrogando il suo DB SQLite in `~/.local/share/opencode/opencode.db` tramite `opencode db --format json` (leggiamo le tabelle `session` e `project` e raggruppiamo per `project_id`); i progetti Pi vengono rilevati scansionando trascrizioni JSONL per sessione in `~/.pi/agent/sessions//_.jsonl` (configurabile tramite `PI_SESSIONS_DIR`) e estraendo il `cwd` dal primo record di ogni sessione; le sessioni del gateway Hermes vengono lette direttamente dal suo archivio SQLite in `~/.hermes/state.db` (configurabile tramite `HERMES_DB_PATH`) e raggruppate in progetti `hermes-` per `source` (Slack/Telegram/cli/cron — le sessioni del gateway non hanno cwd). Un progetto utilizzato da più CLI viene visualizzato come una singola riga con tutti i badge corrispondenti. Utilizza il dropdown **CLI** sopra la tabella per filtrare per uno specifico agente CLI; l'URL preserva la tua selezione come `?cli=claude|codex|copilot|cursor|opencode|pi|hermes`. Ogni progetto mostra: - Nome del progetto (derivato dal percorso della cartella) -- Un badge CLI — `Claude Code` (arancione), `OpenAI Codex` (viola), `GitHub Copilot` (blu), `Cursor Agent` (smeraldo), `OpenCode` (ambra), `Pi` (rosa), `Gemini CLI` (cielo), e/o `Hermes` (indaco) +- Un badge CLI — `Claude Code` (arancione), `OpenAI Codex` (viola), `GitHub Copilot` (blu), `Cursor Agent` (smeraldo), `OpenCode` (ambra), `Pi` (rosa), e/o `Hermes` (indaco) - Data dell'attività di sessione più recente Fai clic su un progetto per vedere le sue sessioni. @@ -47,7 +47,7 @@ Fai clic su una sessione per aprire il visualizzatore di sessione. ### Visualizzatore di sessione -Il visualizzatore di sessione risponde alla domanda chiave per gli agenti autonomi: cosa ha fatto l'agente e ha mantenuto il percorso corretto? Un badge CLI accanto all'intestazione indica se la sessione è una trascrizione di Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi, Gemini CLI o Hermes. Mostra una cronologia di tutto ciò che è accaduto in una sessione: +Il visualizzatore di sessione risponde alla domanda chiave per gli agenti autonomi: cosa ha fatto l'agente e ha mantenuto il percorso corretto? Un badge CLI accanto all'intestazione indica se la sessione è una trascrizione di Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi o Hermes. Mostra una cronologia di tutto ciò che è accaduto in una sessione: - **Messaggi** - Risposte di testo di Claude e prompt dell'utente - **Chiamate agli strumenti** - Ogni strumento invocato da Claude, con il suo input e output @@ -55,7 +55,7 @@ Il visualizzatore di sessione risponde alla domanda chiave per gli agenti autono La barra delle statistiche in alto mostra la durata della sessione, il totale delle chiamate agli strumenti e un riepilogo delle decisioni hook (conteggi allow / deny / instruct). -Fai clic sul pulsante **Download Logs** per esportare la sessione. Per le sessioni Claude Code, Codex, Copilot, Cursor, Pi e Gemini ricevi la trascrizione JSONL originale su disco byte-per-byte; per OpenCode (le cui sessioni si trovano in SQLite, non su disco) ricevi un documento JSON che rispecchia le tabelle `session` / `messages` / `parts` sottostanti. +Fai clic sul pulsante **Download Logs** per esportare la sessione. Per le sessioni Claude Code, Codex, Copilot, Cursor e Pi ricevi la trascrizione JSONL originale su disco byte-per-byte; per OpenCode (le cui sessioni si trovano in SQLite, non su disco) ricevi un documento JSON che rispecchia le tabelle `session` / `messages` / `parts` sottostanti. ### Audit @@ -75,16 +75,16 @@ Una pagina con due schede per gestire i criteri e rivedere l'attività. - - Seleziona quale agenti CLI failproofai protegge da un singolo pannello — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi, Gemini CLI e Hermes hanno tutti una riga con stato di installazione (`Active` / `Detected` / `Inactive`), il percorso delle impostazioni dell'ambito utente e un accento di colore del marchio. Seleziona o deseleziona i CLI che desideri e fai clic su `Apply changes` per installare/disinstallare il diff in un unico passaggio. I CLI il cui binario viene rilevato su PATH sono pre-selezionati. + - Seleziona quale agenti CLI failproofai protegge da un singolo pannello — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi e Hermes hanno tutti una riga con stato di installazione (`Active` / `Detected` / `Inactive`), il percorso delle impostazioni dell'ambito utente e un accento di colore del marchio. Seleziona o deseleziona i CLI che desideri e fai clic su `Apply changes` per installare/disinstallare il diff in un unico passaggio. I CLI il cui binario viene rilevato su PATH sono pre-selezionati. - Attiva o disattiva i singoli criteri con un singolo clic (scrive su `~/.failproofai/policies-config.json` — condiviso tra ogni CLI installata) - Espandi un criterio per configurare i suoi parametri (per i criteri che supportano `policyParams`) - Imposta un percorso file criteri personalizzato - Cronologia completa impaginata di ogni evento hook che si è attivato in tutte le sessioni - - Filtra per decisione, tipo di evento, CLI (Claude Code / OpenAI Codex / GitHub Copilot _(beta)_ / Cursor Agent _(beta)_ / OpenCode _(beta)_ / Pi _(beta)_ / Gemini CLI _(beta)_ / Hermes), nome del criterio o ID della sessione - - Ogni riga mostra: timestamp, nome del criterio, decisione, badge CLI (arancione = Claude Code, viola = OpenAI Codex, blu = GitHub Copilot, smeraldo = Cursor Agent, ambra = OpenCode, rosa = Pi, cielo = Gemini CLI, indaco = Hermes), nome dello strumento, ID della sessione e il motivo delle decisioni deny/instruct - - Fai clic su un ID della sessione per aprire la sua trascrizione — il visualizzatore rileva automaticamente quale CLI ha attivato l'hook (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Gemini CLI `~/.gemini/tmp//chats/.jsonl`, Hermes `~/.hermes/state.db`) e visualizza il badge CLI corrispondente nell'intestazione + - Filtra per decisione, tipo di evento, CLI (Claude Code / OpenAI Codex / GitHub Copilot _(beta)_ / Cursor Agent _(beta)_ / OpenCode _(beta)_ / Pi _(beta)_ / Hermes), nome del criterio o ID della sessione + - Ogni riga mostra: timestamp, nome del criterio, decisione, badge CLI (arancione = Claude Code, viola = OpenAI Codex, blu = GitHub Copilot, smeraldo = Cursor Agent, ambra = OpenCode, rosa = Pi, indaco = Hermes), nome dello strumento, ID della sessione e il motivo delle decisioni deny/instruct + - Fai clic su un ID della sessione per aprire la sua trascrizione — il visualizzatore rileva automaticamente quale CLI ha attivato l'hook (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Hermes `~/.hermes/state.db`) e visualizza il badge CLI corrispondente nell'intestazione @@ -146,4 +146,4 @@ FAILPROOFAI_ALLOWED_DEV_ORIGINS=dashboard.example.com npm run dev Questo si applica solo alla modalità di sviluppo. Quando esegui `failproofai` (modalità di produzione), non c'è websocket HMR e nessun problema di risorsa di sviluppo cross-origin. - \ No newline at end of file + diff --git a/docs/it/getting-started.mdx b/docs/it/getting-started.mdx index fb583f43..eb79afe0 100644 --- a/docs/it/getting-started.mdx +++ b/docs/it/getting-started.mdx @@ -38,9 +38,9 @@ bun add -g failproofai failproofai policies --install ``` - Questo scrive voci di hook nei tuoi agent CLI installati (il file `~/.claude/settings.json` di Claude Code, il file `~/.codex/hooks.json` di OpenAI Codex, il file `~/.copilot/hooks/failproofai.json` di GitHub Copilot CLI, il file `~/.cursor/hooks.json` di Cursor Agent, lo shim del plugin generato di OpenCode in `~/.config/opencode/plugins/failproofai.mjs` più una voce di registrazione nell'array `plugin` di `~/.config/opencode/opencode.json`, il file `~/.pi/agent/settings.json` di Pi, il file `~/.gemini/settings.json` di Gemini CLI, o il file `~/.hermes/config.yaml` di Hermes). Quando è presente più di uno, ti verrà chiesto; passa `--cli claude codex copilot cursor opencode pi gemini hermes` (qualsiasi sottoinsieme) per saltare la richiesta. + Questo scrive voci di hook nei tuoi agent CLI installati (il file `~/.claude/settings.json` di Claude Code, il file `~/.codex/hooks.json` di OpenAI Codex, il file `~/.copilot/hooks/failproofai.json` di GitHub Copilot CLI, il file `~/.cursor/hooks.json` di Cursor Agent, lo shim del plugin generato di OpenCode in `~/.config/opencode/plugins/failproofai.mjs` più una voce di registrazione nell'array `plugin` di `~/.config/opencode/opencode.json`, il file `~/.pi/agent/settings.json` di Pi, o il file `~/.hermes/config.yaml` di Hermes). Quando è presente più di uno, ti verrà chiesto; passa `--cli claude codex copilot cursor opencode pi hermes` (qualsiasi sottoinsieme) per saltare la richiesta. - Il supporto per GitHub Copilot CLI, Cursor Agent, OpenCode, Pi e Gemini CLI è in **beta** — installa con `--cli copilot`, `--cli cursor`, `--cli opencode`, `--cli pi` o `--cli gemini`. Hermes (hermes-agent, un gateway Slack/Telegram) si installa con ambito utente con `--cli hermes` ed è **anche** una fonte di audit offline. + Il supporto per GitHub Copilot CLI, Cursor Agent, OpenCode e Pi è in **beta** — installa con `--cli copilot`, `--cli cursor`, `--cli opencode` o `--cli pi`. Hermes (hermes-agent, un gateway Slack/Telegram) si installa con ambito utente con `--cli hermes` ed è **anche** una fonte di audit offline. ```bash failproofai policies --install --scope project @@ -49,7 +49,6 @@ bun add -g failproofai failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project - failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user failproofai policies --install block-sudo block-rm-rf sanitize-api-keys ``` diff --git a/docs/it/introduction.mdx b/docs/it/introduction.mdx index 0a1d097f..ea697485 100644 --- a/docs/it/introduction.mdx +++ b/docs/it/introduction.mdx @@ -5,13 +5,13 @@ description: "FailproofAI fornisce agli agenti AI 39 politiche di fallimento int [![npm weekly downloads](https://img.shields.io/npm/dw/failproofai?style=flat-square&color=2ea44f)](https://www.npmjs.com/package/failproofai) -Hook e politiche per **gestione dei fallimenti dell'AI**, **recupero degli errori** e **affidabilità dell'LLM**. Mantieni i tuoi agenti AI affidabili e in esecuzione autonoma su **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Gemini CLI**, **Hermes** e **Agents SDK**. +Hook e politiche per **gestione dei fallimenti dell'AI**, **recupero degli errori** e **affidabilità dell'LLM**. Mantieni i tuoi agenti AI affidabili e in esecuzione autonoma su **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Hermes** e **Agents SDK**. Gli agenti AI falliscono in modi prevedibili. Eseguono comandi distruttivi, perdono segreti, si allontanano dall'obiettivo, rimangono bloccati in loop, o effettuano push direttamente al main. Se non controllati, piccoli fallimenti si trasformano in interruzioni, credenziali compromesse e lavoro perso. FailproofAI risolve questo con le **politiche**. Queste regole si intercettano in ogni chiamata di strumento dell'agente per **rilevare i fallimenti**, **mitigarli** (bloccare, istruire, pulire) e **avvisarti** quando qualcosa richiede attenzione. Un dashboard locale ti permette di esaminare ogni chiamata di strumento, fallimento dell'agente e azione di recupero in seguito. -Nessun dato lascia il tuo computer. +Le trascrizioni e la valutazione delle policy rimangono sul tuo computer. I dati vengono inviati solo quando utilizzi esplicitamente una funzionalità online, come promemoria di audit o inviti autenticati. ## Inizia subito @@ -54,4 +54,4 @@ failproofai policies --install # abilita le politiche (o salta — `failproofa failproofai # avvia il dashboard ``` -Vedi la guida [Inizia subito](/it/getting-started) per la procedura dettagliata completa. \ No newline at end of file +Vedi la guida [Inizia subito](/it/getting-started) per la procedura dettagliata completa. diff --git a/docs/ja/built-in-policies.mdx b/docs/ja/built-in-policies.mdx index f36abfd2..f0b1b435 100644 --- a/docs/ja/built-in-policies.mdx +++ b/docs/ja/built-in-policies.mdx @@ -704,7 +704,7 @@ failproofai には、エージェントの一般的な障害モードを検出 ### CLI ごとの Stop セマンティクス -Stop の強制は、サポートされている7つの CLI によって若干異なります。各 CLI が異なる「エージェント終了」フックコントラクトを公開しているためです。**結果**は同じです(ワークフローゲートが失敗している間、エージェントは停止できません)が、**仕組み**は異なります。以下の表に要約します。`require-*-before-stop` ポリシーを有効にする前に理解しておく価値のある、ユーザーに見える挙動があるのは Pi のみです。 +Stop の強制は、サポートされている6つの CLI によって若干異なります。各 CLI が異なる「エージェント終了」フックコントラクトを公開しているためです。**結果**は同じです(ワークフローゲートが失敗している間、エージェントは停止できません)が、**仕組み**は異なります。以下の表に要約します。`require-*-before-stop` ポリシーを有効にする前に理解しておく価値のある、ユーザーに見える挙動があるのは Pi のみです。 | CLI | ゲートが発火するタイミング | 表示される内容 | |---|---|---| @@ -712,20 +712,19 @@ Stop の強制は、サポートされている7つの CLI によって若干異 | Codex | 同じエージェントループ内で即座に | Claude と同じです。 | | GitHub Copilot CLI | 同じエージェントループ内で即座に | Claude と同じです(Copilot の `{decision:"block", reason}` リトライチャネルを使用 — Copilot CLI 1.0.41 で実証的に検証済み)。 | | Cursor Agent | 同じエージェントループ内で即座に | Claude と同じです(Cursor の `{followup_message}` チャネルを使用 — デフォルト5回の `loop_limit` に上限あり)。 | -| Gemini CLI | 同じエージェントループ内で即座に | Claude と同じです(`AfterAgent` 上の Gemini の `{decision:"block", reason}` チャネルを使用)。 | | OpenCode | 同じエージェントループ内で即座に | Claude と同じです(`hookSpecificOutput.additionalContext` を通じてルーティングされた OpenCode の `client.session.prompt(...)` SDK 呼び出しを使用)。 | | **Pi (pi-coding-agent)** | **次のユーザーターン** | **Pi はゲートが発火すると目に見えて停止します** — エージェントループが終了し、プロンプトに戻ります。次にプロンプトを送信した際にゲートが発火します:failproofai はそのターンのシステムプロンプトの先頭に `MANDATORY ACTION REQUIRED` ディレクティブを付加し、あなたのリクエストを実行する前にワークフローステップ(コミット、プッシュなど)を完了するよう LLM に指示します。 | -**Pi の制限事項。** Pi の `AgentEndEvent`(Claude の `Stop` フックに相当するアップストリーム)には Result タイプがありません。発火した時点で Pi のエージェントループはすでに終了しています。Claude / Copilot / Cursor / Gemini / OpenCode のように Pi を同じループで再試行させることはできません。failproofai はゲートを Pi の `before_agent_start` イベント(次のユーザープロンプトの後に発火)にシフトするため、ワークフローチェックは依然として強制されますが、現在のターンではなく次のターンで適用されます。 +**Pi の制限事項。** Pi の `AgentEndEvent`(Claude の `Stop` フックに相当するアップストリーム)には Result タイプがありません。発火した時点で Pi のエージェントループはすでに終了しています。Claude / Copilot / Cursor / OpenCode のように Pi を同じループで再試行させることはできません。failproofai はゲートを Pi の `before_agent_start` イベント(次のユーザープロンプトの後に発火)にシフトするため、ワークフローチェックは依然として強制されますが、現在のターンではなく次のターンで適用されます。 **実際の動作:** - Pi が停止した後、deny の理由は Pi のセッション ID をキーとしてメモリ内に保存されます。同じ Pi プロセスで次に送信したプロンプトがそれを消費します:LLM はシステムプロンプトの先頭で `MANDATORY ACTION REQUIRED` ディレクティブを確認し、コミット(またはプッシュ / PR 作成 / CI 待機)してから、あなたのリクエストを続行します。保存された deny の理由は一度消費されるとクリアされ、ゲートは解除されます。 -- ゲートは Pi のプロセスの存続期間に縛られます。ターンの間に Pi を `Ctrl+C` で終了するか終了した場合、メモリ内のエントリはプロセスとともに削除され、ゲートは失われます。Claude、Copilot、Cursor、Gemini、OpenCode も同じ制約があります(エージェントを強制終了するとゲートは失われます)。Pi はエージェントがゲートの発火前に目に見えて終了するため、これがより顕著に現れます。 +- ゲートは Pi のプロセスの存続期間に縛られます。ターンの間に Pi を `Ctrl+C` で終了するか終了した場合、メモリ内のエントリはプロセスとともに削除され、ゲートは失われます。Claude、Copilot、Cursor、OpenCode も同じ制約があります(エージェントを強制終了するとゲートは失われます)。Pi はエージェントがゲートの発火前に目に見えて終了するため、これがより顕著に現れます。 - 保留中の deny は、任意の理由(`new` / `resume` / `fork` / `quit`)による `session_shutdown` でもクリアされるため、前のセッションの古いゲートが同じ Pi プロセスで開始された新しいセッションに漏れることはありません。 -Claude スタイルの同ループ再試行が必要な場合は、他の6つのサポートされている CLI のいずれかで `Stop` ポリシーを実行してください。`AgentEndEvent` に Result タイプを追加するためのアップストリームの Pi の更新を追跡しており、これによりこのギャップを解消できる可能性があります。 +Claude スタイルの同ループ再試行が必要な場合は、他の5つのサポートされている CLI のいずれかで `Stop` ポリシーを実行してください。`AgentEndEvent` に Result タイプを追加するためのアップストリームの Pi の更新を追跡しており、これによりこのギャップを解消できる可能性があります。 ### `require-commit-before-stop` diff --git a/docs/ja/cli/audit.mdx b/docs/ja/cli/audit.mdx index 4b218e71..1f30cd28 100644 --- a/docs/ja/cli/audit.mdx +++ b/docs/ja/cli/audit.mdx @@ -44,10 +44,10 @@ failproofai - 使い方を確認するには `failproofai audit -h`(または `--help`)を実行してください。監査は**完全オフライン**で動作し、アカウントやネットワーク接続は不要です。`Ctrl+C` で停止するまでダッシュボードはサービスを継続します。 + 使い方を確認するには `failproofai audit -h`(または `--help`)を実行してください。中核となる監査スキャンとレポート生成は**完全オフライン**で動作し、アカウントやネットワーク接続は不要です。オプションの認証済みアカウント機能にはネットワーク接続が必要です。`Ctrl+C` で停止するまでダッシュボードはサービスを継続します。 -ダッシュボードはこのマシン上の過去のエージェント CLI トランスクリプト(Claude Code、Codex、Copilot、Cursor、OpenCode、Pi、Gemini)をスキャンし、failproofai が防止するように設計された操作の発生頻度を報告します。対象は環境変数チェック、フォースプッシュ、冗長な `cd ` プレフィックス、スリープポーリングループ、編集直後のファイル再読み込みなどです。 +ダッシュボードはこのマシン上の過去のエージェント CLI トランスクリプト(Claude Code、Codex、Copilot、Cursor、OpenCode、Pi)をスキャンし、failproofai が防止するように設計された操作の発生頻度を報告します。対象は環境変数チェック、フォースプッシュ、冗長な `cd ` プレフィックス、スリープポーリングループ、編集直後のファイル再読み込みなどです。 各トランスクリプトについて、すべてのツール使用イベントが 39 のビルトインポリシー**および** 8 つの監査専用検出器で再実行されます(後者はランタイムポリシーでまだカバーされていないパターンを検出します)。カウントはポリシー・検出器ごとにすべてのセッションで集計されます。 @@ -85,4 +85,4 @@ failproofai - **変更なし。** 監査は読み取り専用モードで再実行されます。`warn-repeated-tool-calls` はセッションごとのサイドカーが変更されてしまうためスキップされます。 - **ワークフローポリシーのスキップ。** `require-*-before-stop` ポリシーは `Stop` イベント時にのみ発火し、ライブの git 状態に対して `execSync` を実行します。「2025 年に何が起きたか」という文脈では意味のある解釈ができないため、監査カウントには含まれません。 -- **カスタムポリシーのスキップ。** ユーザーが定義したカスタムフックは再実行されません(元のセッション以降に変更された可能性があるため)。 \ No newline at end of file +- **カスタムポリシーのスキップ。** ユーザーが定義したカスタムフックは再実行されません(元のセッション以降に変更された可能性があるため)。 diff --git a/docs/ja/configuration.mdx b/docs/ja/configuration.mdx index 9c428489..3249b28b 100644 --- a/docs/ja/configuration.mdx +++ b/docs/ja/configuration.mdx @@ -196,13 +196,12 @@ AI 呼び出しを行うポリシーのための LLM クライアント設定。 - **OpenAI Codex**: `~/.codex/hooks.json`(ユーザー)、`/.codex/hooks.json`(プロジェクト)— Codex には `local` スコープがありません - **GitHub Copilot CLI _(beta)_**: `~/.copilot/hooks/failproofai.json`(ユーザー)、`/.github/hooks/failproofai.json`(プロジェクト)— Copilot には `local` スコープがありません。フックエントリは Copilot の OS キー付き `bash`/`powershell` コマンドフィールドと `timeoutSec` を使用し、ファイルにはトップレベルの `version: 1` マーカーが付きます。`events.jsonl` レコードスキーマ(公開ドキュメントに記載なし)を実際の使用環境で検証中のため、Copilot CLI のサポートは**ベータ版**です。 - **Cursor Agent _(beta)_**: `~/.cursor/hooks.json`(ユーザー)、`/.cursor/hooks.json`(プロジェクト)— Cursor には `local` スコープがありません。フックエントリは Claude 形式の `{type, command, timeout}` を使用しますが、Cursor の[フックスキーマ](https://cursor.com/docs/hooks)に従いキャメルケースのイベントキー(`preToolUse`、`beforeSubmitPrompt` など)のフラット配列として保存され、ファイルにはトップレベルの `version: 1` マーカーが付きます。ハンドラーは `CURSOR_EVENT_MAP` を通じてキャメルケース → PascalCase に正規化するため、既存の組み込みポリシーはそのまま動作します。Cursor のトランスクリプトのオンディスクフォーマット(公開ドキュメントに未記載)を実際の環境で検証中のため、Cursor Agent のサポートは**ベータ版**です。 - - **OpenCode _(beta)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs`(ユーザー)、`/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs`(プロジェクト)— OpenCode には `local` スコープがありません。他の6つの CLI とは異なり、OpenCode には**外部コマンドフックシステムがありません**。`opencode.json` の `plugin: []` 配列で明示的に登録された JS/TS プラグインをインプロセスでロードします(`.opencode/plugins/` からの自動検出は opencode v1.14.33 でのプラグインロード方法では**ありません**)。インストール時に小さな生成プラグインシムが配置され、failproofai バイナリをサブプロセスで呼び出し、バイナリの Claude 形式 JSON レスポンスをプラグインセマンティクスに変換します:ツールイベントの deny には `throw new Error()`(ツール呼び出しをキャンセル)、instruct および `Stop` / `SubagentStop` の deny には `client.session.prompt(...)`(deny の理由を次のユーザーメッセージとして送信 — `session.idle` が通知専用で例外をスローしても無効なため、強制リトライの唯一のチャネル)、allow には no-op を使用します。シムはツール名(小文字 → `OPENCODE_TOOL_MAP` を通じた PascalCase)とツール入力引数のキー(`OPENCODE_TOOL_INPUT_MAP` を通じたキャメルケース → スネークケース:`Read` / `Write` / `Edit` の `filePath` → `file_path`、`oldString` → `old_string` など)を正規化してからバイナリに転送するため、`block-read-outside-cwd`、`block-env-files`、`block-secrets-write` などのパスチェック組み込みポリシーは OpenCode のツール呼び出しでもそのまま動作します。セッションは `~/.local/share/opencode/opencode.db` の OpenCode の SQLite DB に保存され、ダッシュボードのセッションビューアーは `opencode db --format json` と `opencode export ` を通じて読み取ります。バージョン間の動作や実際の使用環境での検証中のため、OpenCode のサポートは**ベータ版**です。[OpenCode プラグインドキュメント](https://opencode.ai/docs/plugins/)を参照してください。 + - **OpenCode _(beta)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs`(ユーザー)、`/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs`(プロジェクト)— OpenCode には `local` スコープがありません。他の5つの CLI とは異なり、OpenCode には**外部コマンドフックシステムがありません**。`opencode.json` の `plugin: []` 配列で明示的に登録された JS/TS プラグインをインプロセスでロードします(`.opencode/plugins/` からの自動検出は opencode v1.14.33 でのプラグインロード方法では**ありません**)。インストール時に小さな生成プラグインシムが配置され、failproofai バイナリをサブプロセスで呼び出し、バイナリの Claude 形式 JSON レスポンスをプラグインセマンティクスに変換します:ツールイベントの deny には `throw new Error()`(ツール呼び出しをキャンセル)、instruct および `Stop` / `SubagentStop` の deny には `client.session.prompt(...)`(deny の理由を次のユーザーメッセージとして送信 — `session.idle` が通知専用で例外をスローしても無効なため、強制リトライの唯一のチャネル)、allow には no-op を使用します。シムはツール名(小文字 → `OPENCODE_TOOL_MAP` を通じた PascalCase)とツール入力引数のキー(`OPENCODE_TOOL_INPUT_MAP` を通じたキャメルケース → スネークケース:`Read` / `Write` / `Edit` の `filePath` → `file_path`、`oldString` → `old_string` など)を正規化してからバイナリに転送するため、`block-read-outside-cwd`、`block-env-files`、`block-secrets-write` などのパスチェック組み込みポリシーは OpenCode のツール呼び出しでもそのまま動作します。セッションは `~/.local/share/opencode/opencode.db` の OpenCode の SQLite DB に保存され、ダッシュボードのセッションビューアーは `opencode db --format json` と `opencode export ` を通じて読み取ります。バージョン間の動作や実際の使用環境での検証中のため、OpenCode のサポートは**ベータ版**です。[OpenCode プラグインドキュメント](https://opencode.ai/docs/plugins/)を参照してください。 - **Pi _(beta)_**: `~/.pi/agent/settings.json`(ユーザー)、`/.pi/settings.json`(プロジェクト)— Pi には `local` スコープがありません。Pi は起動時に TypeScript 拡張パッケージをロードします。設定ファイルはフラットな文字列配列 `{"packages": ["./relative/path", …]}` です。failproofai はバンドルされた `pi-extension/` ディレクトリを指す単一の packages 配列エントリを書き込みます。拡張機能は内部で Pi の `tool_call` / `user_bash` / `input` / `session_start` イベントを購読し、`failproofai --hook --cli pi` をシェルアウトします。ハンドラーは `PI_EVENT_MAP` を通じてアンダースコア付き小文字スネークケース → PascalCase に正規化するため、既存の組み込みポリシーはそのまま動作します。ツール入力引数も `PI_TOOL_INPUT_MAP` を通じて正規化されます(Pi の Read / Write / Edit は `file_path` ではなく `path` を使用。トップレベルキーのマッピングにより `block-env-files` と `block-secrets-write` が動作します — `block-read-outside-cwd` にはすでに `path` フォールバックがあります)。Pi の拡張 API とセッションログのレイアウトが安定化するまで Pi のサポートは**ベータ版**です。 - - **Gemini CLI _(beta)_**: `~/.gemini/settings.json`(ユーザー)、`/.gemini/settings.json`(プロジェクト)— Gemini には `local` スコープがありません(failproofai が公開していない `/etc/gemini-cli/settings.json` の `system` スコープを文書化しています)。フックエントリは Claude の `{type, command, timeout}` 形式を Gemini の `{matcher, hooks: [...]}` マッチャースキーマでラップし、デフォルトで `matcher: "*"` を使用します。イベントは PascalCase(`SessionStart`、`BeforeAgent`、`AfterAgent`、`BeforeModel`、`AfterModel`、`BeforeToolSelection`、`BeforeTool`、`AfterTool`、`PreCompress`、`Notification`、`SessionEnd`)で、ハンドラーは `GEMINI_EVENT_MAP` を通じて Claude の標準名にマッピングします。ツール名はスネークケース(`run_shell_command`、`read_file`、`write_file`、`replace` など)で、ハンドラーは `GEMINI_TOOL_MAP` を通じて正規化するため、既存の組み込みポリシーはそのまま動作します。ポリシー評価器は Gemini のフラットな `{decision: "deny", reason}` 形式(Gemini の「ゴールデンルール」exit-0 契約に基づく推奨)、BeforeAgent / AfterTool / SessionStart でのコンテキスト注入用 `{hookSpecificOutput: {hookEventName, additionalContext}}`、AfterAgent での強制リトライセマンティクス用 `{decision: "block", reason}` を出力します。実際の使用環境でのカバレッジを拡大中のため、Gemini CLI のサポートは**ベータ版**です。[Gemini CLI フックドキュメント](https://geminicli.com/docs/hooks/)を参照してください。 - **Hermes (hermes-agent)**: `~/.hermes/config.yaml`(**ユーザースコープのみ** — Hermes にはプロジェクト/ローカル設定がありません)。Hermes は Slack/Telegram の**ゲートウェイ**であるため、1回のインストールでSlack/Telegram/cli/cron など全プラットフォームからのツール呼び出しと内部サブエージェントを傍受します。フックエントリは Hermes のスネークケースイベント(`pre_tool_call` / `post_tool_call` / `on_session_start` / `on_session_end` / `subagent_stop`)をキーとする `hooks:` マップ下の `{command, timeout}` ペア(タイムアウトは**秒**単位)です。ハンドラーは `HERMES_EVENT_MAP` でイベントを、`HERMES_TOOL_MAP` でツール名を正規化するため、組み込みポリシーはそのまま動作します。設定はコメントを保持する YAML `Document` のラウンドトリップで編集されるため、オペレーターの他の設定は保持されます。インストール時に `hooks_auto_accept: true` が設定されるため、ヘッドレスゲートウェイ(TTY なし)は同意プロンプトなしでフックを実行します。評価器は Hermes の `{"decision":"block","reason"}` stdout 契約を出力します(Hermes は終了コードを無視)。**制限事項:** Hermes にはターン終了の `Stop` イベントがないため、`require-*-before-stop` 組み込みポリシーは動作しません(非適用、バグではありません)。`instruct` は allow-with-logged-note に降格します(追加コンテキストチャネルなし)。出力シークレットのリダクション(`sanitize-*`)はシェルフックの契約上ツール出力を書き換えられません。Hermes は**オフライン監査**ソースでもあり、ダッシュボードはゲートウェイセッションを `~/.hermes/state.db` から直接読み取ります。 - **`policies-config.json`** — failproofai が評価するポリシーとそのパラメーターを指定します(すべてのエージェント CLI で共有) -特定のエージェントを対象にするには `--cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes` を渡します(スペース区切りまたは繰り返しで複数指定可能): +特定のエージェントを対象にするには `--cli claude|codex|copilot|cursor|opencode|pi|hermes` を渡します(スペース区切りまたは繰り返しで複数指定可能): ```bash failproofai policies --install --cli codex --scope project @@ -210,12 +209,11 @@ failproofai policies --install --cli copilot --scope project failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project -failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user -failproofai policies --install --cli claude codex copilot cursor opencode pi gemini +failproofai policies --install --cli claude codex copilot cursor opencode pi ``` -`--cli` を省略すると、`failproofai` はインストール済みのエージェント CLI を自動検出します(`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which gemini` / `which hermes`): +`--cli` を省略すると、`failproofai` はインストール済みのエージェント CLI を自動検出します(`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which hermes`): - **CLI が1つ検出された場合** — プロンプトなしでその CLI を自動選択します。 - **複数の CLI が検出され、インタラクティブターミナルの場合** — `Detected (N)` セクション(`Install for all N detected` の集約行と検出された各 CLI)と `Not installed (M) · install hooks ahead of time` セクション(検出されなかったサポート対象 CLI を前もってインストールするオプションとして一覧表示)にグループ化された矢印キー操作の単一選択プロンプトを表示します(↑↓で移動、Enterで選択、^Cで終了)。アンインストールフローでは Detected セクションのみ表示されます。 diff --git a/docs/ja/dashboard.mdx b/docs/ja/dashboard.mdx index 5462eed4..d2bbebf6 100644 --- a/docs/ja/dashboard.mdx +++ b/docs/ja/dashboard.mdx @@ -16,7 +16,7 @@ failproofai `http://localhost:8020` で開きます。 -ダッシュボードはファイルシステムから直接読み取ります — Claude Code のプロジェクトフォルダと failproofai の設定ファイルを参照します。リモートサービスへの書き込みは一切行われません。 +ダッシュボードはローカルのプロジェクト、セッション、failproofai 設定データをファイルシステムから直接読み取ります。監査リマインダーや招待などのオプションの認証済み機能は、そのリクエストに必要な情報(メールアドレスを含む)をリモート API に送信します。 --- @@ -24,11 +24,11 @@ failproofai ### プロジェクト -マシン上で見つかったすべての Claude Code、OpenAI Codex、GitHub Copilot CLI _(ベータ)_、Cursor Agent _(ベータ)_、OpenCode _(ベータ)_、Pi _(ベータ)_、Gemini CLI _(ベータ)_、および Hermes プロジェクトを一覧表示します。Claude プロジェクトは `~/.claude/projects/`(または `CLAUDE_PROJECTS_PATH` で設定されたパス)から検出されます。Codex プロジェクトは `~/.codex/sessions///
/*.jsonl` 以下のすべてのトランスクリプトをスキャンし、各セッションの最初のレコードに記録された `cwd` でグループ化することで検出されます。Copilot CLI プロジェクトは各 `~/.copilot/session-state//workspace.yaml`(`COPILOT_HOME` で設定可能)をスキャンし、`cwd` フィールドでグループ化することで検出されます。Cursor Agent プロジェクトは `~/.cursor/agent-sessions//`(`CURSOR_HOME` で設定可能。`conversations/` と `sessions/` をフォールバックとして探索)以下のセッションごとのメタデータをスキャンし、`meta.json` / `session.json` / `workspace.yaml` 内の `cwd` スカラーから検出されます。OpenCode プロジェクトは `opencode db --format json` を通じて `~/.local/share/opencode/opencode.db` の SQLite DB を照会することで検出されます(`session` テーブルと `project` テーブルを読み取り、`project_id` でグループ化)。Pi プロジェクトは `~/.pi/agent/sessions//_.jsonl`(`PI_SESSIONS_DIR` で設定可能)以下のセッションごとの JSONL トランスクリプトをスキャンし、各セッションの最初のレコードから `cwd` を取得することで検出されます。Gemini CLI プロジェクトは `~/.gemini/tmp//chats/session--.jsonl`(`GEMINI_SESSIONS_DIR` で設定可能)をスキャンし、隣接する `.project_root` テキストマーカーから正規の cwd を復元することで検出されます。Hermes ゲートウェイセッションは `~/.hermes/state.db`(`HERMES_DB_PATH` で設定可能)の SQLite ストアから直接読み取られ、`source`(Slack/Telegram/cli/cron — ゲートウェイセッションには cwd がありません)によって `hermes-` プロジェクトにグループ化されます。複数の CLI で使用されたプロジェクトは、該当するすべてのバッジを付けた1行として表示されます。テーブル上部の **CLI** ドロップダウンを使用して特定のエージェント CLI でフィルタリングできます。URLには選択内容が `?cli=claude|codex|copilot|cursor|opencode|pi|gemini|hermes` として保持されます。 +マシン上で見つかったすべての Claude Code、OpenAI Codex、GitHub Copilot CLI _(ベータ)_、Cursor Agent _(ベータ)_、OpenCode _(ベータ)_、Pi _(ベータ)_、および Hermes プロジェクトを一覧表示します。Claude プロジェクトは `~/.claude/projects/`(または `CLAUDE_PROJECTS_PATH` で設定されたパス)から検出されます。Codex プロジェクトは `~/.codex/sessions///
/*.jsonl` 以下のすべてのトランスクリプトをスキャンし、各セッションの最初のレコードに記録された `cwd` でグループ化することで検出されます。Copilot CLI プロジェクトは各 `~/.copilot/session-state//workspace.yaml`(`COPILOT_HOME` で設定可能)をスキャンし、`cwd` フィールドでグループ化することで検出されます。Cursor Agent プロジェクトは `~/.cursor/agent-sessions//`(`CURSOR_HOME` で設定可能。`conversations/` と `sessions/` をフォールバックとして探索)以下のセッションごとのメタデータをスキャンし、`meta.json` / `session.json` / `workspace.yaml` 内の `cwd` スカラーから検出されます。OpenCode プロジェクトは `opencode db --format json` を通じて `~/.local/share/opencode/opencode.db` の SQLite DB を照会することで検出されます(`session` テーブルと `project` テーブルを読み取り、`project_id` でグループ化)。Pi プロジェクトは `~/.pi/agent/sessions//_.jsonl`(`PI_SESSIONS_DIR` で設定可能)以下のセッションごとの JSONL トランスクリプトをスキャンし、各セッションの最初のレコードから `cwd` を取得することで検出されます。Hermes ゲートウェイセッションは `~/.hermes/state.db`(`HERMES_DB_PATH` で設定可能)の SQLite ストアから直接読み取られ、`source`(Slack/Telegram/cli/cron — ゲートウェイセッションには cwd がありません)によって `hermes-` プロジェクトにグループ化されます。複数の CLI で使用されたプロジェクトは、該当するすべてのバッジを付けた1行として表示されます。テーブル上部の **CLI** ドロップダウンを使用して特定のエージェント CLI でフィルタリングできます。URLには選択内容が `?cli=claude|codex|copilot|cursor|opencode|pi|hermes` として保持されます。 各プロジェクトには以下が表示されます: - プロジェクト名(フォルダパスから導出) -- CLI バッジ — `Claude Code`(オレンジ)、`OpenAI Codex`(パープル)、`GitHub Copilot`(ブルー)、`Cursor Agent`(エメラルド)、`OpenCode`(アンバー)、`Pi`(ピンク)、`Gemini CLI`(スカイ)、および/または `Hermes`(インディゴ) +- CLI バッジ — `Claude Code`(オレンジ)、`OpenAI Codex`(パープル)、`GitHub Copilot`(ブルー)、`Cursor Agent`(エメラルド)、`OpenCode`(アンバー)、`Pi`(ピンク)、および/または `Hermes`(インディゴ) - 最新のセッションアクティビティの日付 プロジェクトをクリックするとそのセッションが表示されます。 @@ -47,7 +47,7 @@ failproofai ### セッションビューアー -セッションビューアーは、自律エージェントにとっての核心的な問いに答えます:エージェントは何を行い、正しく動作していたか?ヘッダー横の CLI バッジは、セッションが Claude Code、OpenAI Codex、GitHub Copilot CLI、Cursor Agent、OpenCode、Pi、Gemini CLI、または Hermes のトランスクリプトであるかを示します。セッション内で起きたすべてのことのタイムラインが表示されます: +セッションビューアーは、自律エージェントにとっての核心的な問いに答えます:エージェントは何を行い、正しく動作していたか?ヘッダー横の CLI バッジは、セッションが Claude Code、OpenAI Codex、GitHub Copilot CLI、Cursor Agent、OpenCode、Pi、または Hermes のトランスクリプトであるかを示します。セッション内で起きたすべてのことのタイムラインが表示されます: - **メッセージ** - Claude のテキスト応答とユーザープロンプト - **ツールコール** - Claude が呼び出したすべてのツール(入力と出力を含む) @@ -55,7 +55,7 @@ failproofai 上部のステータスバーには、セッション時間、総ツールコール数、フック判定のサマリー(allow / deny / instruct の件数)が表示されます。 -**ログをダウンロード** ボタンをクリックするとセッションをエクスポートできます。Claude Code、Codex、Copilot、Cursor、Pi、Gemini のセッションはディスク上の JSONL トランスクリプトをバイト単位でそのまま取得できます。OpenCode(セッションがディスク上ではなく SQLite に保存されている)の場合は、基礎となる `session` / `messages` / `parts` テーブルを反映した JSON ドキュメントが取得されます。 +**ログをダウンロード** ボタンをクリックするとセッションをエクスポートできます。Claude Code、Codex、Copilot、Cursor、Pi のセッションはディスク上の JSONL トランスクリプトをバイト単位でそのまま取得できます。OpenCode(セッションがディスク上ではなく SQLite に保存されている)の場合は、基礎となる `session` / `messages` / `parts` テーブルを反映した JSON ドキュメントが取得されます。 ### 監査 @@ -75,16 +75,16 @@ failproofai - - 1つのパネルから failproofai が保護するエージェント CLI を複数選択できます — Claude Code、OpenAI Codex、GitHub Copilot、Cursor Agent、OpenCode、Pi、Gemini CLI、および Hermes にはそれぞれインストール状況(`Active` / `Detected` / `Inactive`)、ユーザースコープの設定パス、ブランドカラーのアクセントを持つ行があります。対象の CLI にチェックを入れるか外して `Apply changes` をクリックすると、差分を1ステップでインストール/アンインストールできます。PATH 上でバイナリが検出された CLI は事前にチェックされています。 + - 1つのパネルから failproofai が保護するエージェント CLI を複数選択できます — Claude Code、OpenAI Codex、GitHub Copilot、Cursor Agent、OpenCode、Pi、および Hermes にはそれぞれインストール状況(`Active` / `Detected` / `Inactive`)、ユーザースコープの設定パス、ブランドカラーのアクセントを持つ行があります。対象の CLI にチェックを入れるか外して `Apply changes` をクリックすると、差分を1ステップでインストール/アンインストールできます。PATH 上でバイナリが検出された CLI は事前にチェックされています。 - 個別のポリシーをワンクリックで有効/無効に切り替えます(`~/.failproofai/policies-config.json` に書き込まれます — インストールされたすべての CLI で共有) - パラメーターをサポートするポリシーの設定を展開して変更します(`policyParams` をサポートするポリシーの場合) - カスタムポリシーファイルのパスを設定します - すべてのセッションで発動したすべてのフックイベントの完全なページ分割履歴 - - 判定、イベントタイプ、CLI(Claude Code / OpenAI Codex / GitHub Copilot _(ベータ)_ / Cursor Agent _(ベータ)_ / OpenCode _(ベータ)_ / Pi _(ベータ)_ / Gemini CLI _(ベータ)_ / Hermes)、ポリシー名、またはセッション ID でフィルタリング - - 各行には:タイムスタンプ、ポリシー名、判定、CLI バッジ(オレンジ = Claude Code、パープル = OpenAI Codex、ブルー = GitHub Copilot、エメラルド = Cursor Agent、アンバー = OpenCode、ピンク = Pi、スカイ = Gemini CLI、インディゴ = Hermes)、ツール名、セッション ID、deny/instruct 判定の理由が表示されます - - セッション ID をクリックするとトランスクリプトが開きます — ビューアーはどの CLI がフックを発動させたかを自動検出し(Claude `~/.claude/projects/…`、Codex `~/.codex/sessions/…`、Copilot CLI `~/.copilot/session-state//events.jsonl`、Cursor Agent `~/.cursor/agent-sessions//events.jsonl`、OpenCode `~/.local/share/opencode/opencode.db`、Pi `~/.pi/agent/sessions//.jsonl`、Gemini CLI `~/.gemini/tmp//chats/.jsonl`、Hermes `~/.hermes/state.db`)、ヘッダーに対応する CLI バッジを表示します + - 判定、イベントタイプ、CLI(Claude Code / OpenAI Codex / GitHub Copilot _(ベータ)_ / Cursor Agent _(ベータ)_ / OpenCode _(ベータ)_ / Pi _(ベータ)_ / Hermes)、ポリシー名、またはセッション ID でフィルタリング + - 各行には:タイムスタンプ、ポリシー名、判定、CLI バッジ(オレンジ = Claude Code、パープル = OpenAI Codex、ブルー = GitHub Copilot、エメラルド = Cursor Agent、アンバー = OpenCode、ピンク = Pi、インディゴ = Hermes)、ツール名、セッション ID、deny/instruct 判定の理由が表示されます + - セッション ID をクリックするとトランスクリプトが開きます — ビューアーはどの CLI がフックを発動させたかを自動検出し(Claude `~/.claude/projects/…`、Codex `~/.codex/sessions/…`、Copilot CLI `~/.copilot/session-state//events.jsonl`、Cursor Agent `~/.cursor/agent-sessions//events.jsonl`、OpenCode `~/.local/share/opencode/opencode.db`、Pi `~/.pi/agent/sessions//.jsonl`、Hermes `~/.hermes/state.db`)、ヘッダーに対応する CLI バッジを表示します @@ -146,4 +146,4 @@ FAILPROOFAI_ALLOWED_DEV_ORIGINS=dashboard.example.com npm run dev これは開発モードにのみ適用されます。`failproofai`(本番モード)を実行する場合、HMR WebSocket もクロスオリジン開発リソースの問題も発生しません。 - \ No newline at end of file + diff --git a/docs/ja/getting-started.mdx b/docs/ja/getting-started.mdx index 59b11f83..2f26c6a0 100644 --- a/docs/ja/getting-started.mdx +++ b/docs/ja/getting-started.mdx @@ -37,9 +37,9 @@ bun add -g failproofai failproofai policies --install ``` - このコマンドは、インストール済みのエージェント CLI にフックエントリを書き込みます(Claude Code の `~/.claude/settings.json`、OpenAI Codex の `~/.codex/hooks.json`、GitHub Copilot CLI の `~/.copilot/hooks/failproofai.json`、Cursor Agent の `~/.cursor/hooks.json`、OpenCode の `~/.config/opencode/plugins/failproofai.mjs` と `~/.config/opencode/opencode.json` の `plugin` 配列へのエントリ、Pi の `~/.pi/agent/settings.json`、Gemini CLI の `~/.gemini/settings.json`、または Hermes の `~/.hermes/config.yaml`)。複数が存在する場合はプロンプトで選択を求められます。`--cli claude codex copilot cursor opencode pi gemini hermes`(任意のサブセット)を渡すとプロンプトをスキップできます。 + このコマンドは、インストール済みのエージェント CLI にフックエントリを書き込みます(Claude Code の `~/.claude/settings.json`、OpenAI Codex の `~/.codex/hooks.json`、GitHub Copilot CLI の `~/.copilot/hooks/failproofai.json`、Cursor Agent の `~/.cursor/hooks.json`、OpenCode の `~/.config/opencode/plugins/failproofai.mjs` と `~/.config/opencode/opencode.json` の `plugin` 配列へのエントリ、Pi の `~/.pi/agent/settings.json`、または Hermes の `~/.hermes/config.yaml`)。複数が存在する場合はプロンプトで選択を求められます。`--cli claude codex copilot cursor opencode pi hermes`(任意のサブセット)を渡すとプロンプトをスキップできます。 - GitHub Copilot CLI、Cursor Agent、OpenCode、Pi、Gemini CLI のサポートは**ベータ版**です — それぞれ `--cli copilot`、`--cli cursor`、`--cli opencode`、`--cli pi`、`--cli gemini` でインストールしてください。Hermes(hermes-agent、Slack/Telegram ゲートウェイ)は `--cli hermes` でユーザースコープにインストールでき、オフライン監査ソースとしても機能します。 + GitHub Copilot CLI、Cursor Agent、OpenCode、Pi のサポートは**ベータ版**です — それぞれ `--cli copilot`、`--cli cursor`、`--cli opencode`、`--cli pi` でインストールしてください。Hermes(hermes-agent、Slack/Telegram ゲートウェイ)は `--cli hermes` でユーザースコープにインストールでき、オフライン監査ソースとしても機能します。 ```bash failproofai policies --install --scope project @@ -48,7 +48,6 @@ bun add -g failproofai failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project - failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user failproofai policies --install block-sudo block-rm-rf sanitize-api-keys ``` diff --git a/docs/ja/introduction.mdx b/docs/ja/introduction.mdx index aa59b0ff..306e2b72 100644 --- a/docs/ja/introduction.mdx +++ b/docs/ja/introduction.mdx @@ -5,13 +5,13 @@ description: "FailproofAI は AI エージェントに39種類の組み込み障 [![npm weekly downloads](https://img.shields.io/npm/dw/failproofai?style=flat-square&color=2ea44f)](https://www.npmjs.com/package/failproofai) -**AI 障害ハンドリング**・**エラーリカバリー**・**LLM の信頼性**向上のためのフックとポリシー。**Claude Code**、**OpenAI Codex**、**GitHub Copilot**、**Cursor Agent**、**OpenCode**、**Pi**、**Gemini CLI**、**Hermes**、そして **Agents SDK** 上で、AI エージェントを安定かつ自律的に動作させ続けます。 +**AI 障害ハンドリング**・**エラーリカバリー**・**LLM の信頼性**向上のためのフックとポリシー。**Claude Code**、**OpenAI Codex**、**GitHub Copilot**、**Cursor Agent**、**OpenCode**、**Pi**、**Hermes**、そして **Agents SDK** 上で、AI エージェントを安定かつ自律的に動作させ続けます。 AI エージェントの障害は、予測可能なパターンで発生します。破壊的なコマンドの実行、シークレットの漏洩、タスクからの逸脱、ループへの陥没、main ブランチへの直接プッシュ——放置すれば、小さな障害がサービス停止、認証情報の流出、作業の消失へと連鎖します。 FailproofAI はこれを**ポリシー**で解決します。これらのルールはエージェントのすべてのツール呼び出しにフックし、**障害を検出**し、**軽減**(ブロック・指示・サニタイズ)し、対応が必要な場合に**アラートを通知**します。ローカルダッシュボードで、あとからすべてのツール呼び出し・エージェント障害・リカバリーアクションを確認できます。 -データはマシンの外に出ません。 +トランスクリプトとポリシー評価はマシン内に保持されます。認証済みの監査リマインダーや招待など、オンライン機能を明示的に使用した場合にのみデータが送信されます。 ## はじめに @@ -54,4 +54,4 @@ failproofai policies --install # enable policies (or skip — `failproofai` wi failproofai # launch the dashboard ``` -詳細な手順については [Getting started](/ja/getting-started) ガイドをご覧ください。 \ No newline at end of file +詳細な手順については [Getting started](/ja/getting-started) ガイドをご覧ください。 diff --git a/docs/ko/agenteye/audits.mdx b/docs/ko/agenteye/audits.mdx index 19f0dc9e..141083d3 100644 --- a/docs/ko/agenteye/audits.mdx +++ b/docs/ko/agenteye/audits.mdx @@ -1,107 +1,106 @@ --- -title: "감사 — 에이전틱 개선 탐지" -description: "AgentEye 감사 — 에이전틱 개선 탐지 문서." +title: "감사(Audits) — 에이전트 개선 사항 탐지" +description: "AgentEye Audits — 에이전트 개선 사항 탐지 문서입니다." --- -감사(Audit)는 **세션 전반**에 걸쳐 에이전트 로그를 분석해 개선할 사항을 찾아내는 반복 실행 작업입니다. 알림(alert)이 이미 알고 있는 특정 지표를 거의 실시간으로 감시한다면, 감사는 *조사*를 수행합니다. 설정한 일정에 따라 해당 기간에 대한 결정적(deterministic) 정책 검사를 실행한 뒤, **AI 신뢰성 에이전트**를 세션에 풀어 데이터를 직접 쿼리하고, 의심스러운 대화록을 읽으며, 필요한 경우 간단한 분석 스크립트를 실행합니다. 그 결과로 각 근거가 뒷받침된 **개선 권고사항**을 작성합니다. +감사(Audit)는 **여러 세션에 걸쳐** 에이전트 로그를 분석하여 개선이 필요한 사항을 찾아내는 반복 실행 작업입니다. 알림(alert)이 이미 알고 있는 특정 지표를 거의 실시간으로 감시하는 것과 달리, 감사는 *조사*를 수행합니다. 설정한 일정에 따라 해당 기간 동안 결정론적 정책 검사를 실행한 뒤, **AI 안정성 에이전트**를 세션 전반에 걸쳐 자유롭게 투입합니다. 에이전트는 데이터를 직접 쿼리하고 의심스러운 트랜스크립트를 읽으며, 필요한 경우 소규모 분석 스크립트를 실행한 후 각 항목의 근거와 함께 **개선 권고사항**을 작성합니다. -감사는 "내 에이전트에서 무엇을 수정하거나 개선해야 하는가?"라는 질문에 답하고, 알림은 특정 임계값이 넘는 순간 즉시 알려줍니다. 모든 개선 사항은 배경이 된 정확한 세션과 쿼리로 연결되며, 클릭 한 번으로 재발을 감지하는 알림을 미리 채워진 상태로 생성할 수 있습니다. +감사는 "내 에이전트에서 무엇을 수정하거나 개선해야 하는가?"라는 질문에 답하는 데 활용하고, 알림은 특정 임계값이 초과되는 순간 즉시 통보받기 위해 사용하세요. 모든 개선 사항은 근거가 된 정확한 세션 및 쿼리와 연결되며, 클릭 한 번으로 재발을 감지하는 알림을 미리 채워진 상태로 생성할 수 있습니다. -대시보드 화면은 **`//audits`** (사이드바 → *분석* → *감사*)이며, `audits:read` / `audits:write` 권한이 필요합니다. +대시보드 화면은 **`//audits`** (사이드바 → *분석* → *감사*)이며, `audits:read` / `audits:write` 권한으로 접근을 제한합니다. --- ## 실행 방식 -각 실행은 결정적(deterministic) 기반 레이어와 에이전틱 조사 레이어, 두 단계로 구성됩니다. +각 실행은 결정론적 기반 계층과 에이전트 조사 계층, 두 가지 단계로 구성됩니다. -### 1. 정책 검사 (결정적) +### 1. 정책 검사 (결정론적) -어떤 모델도 실행되기 전에, 감사는 해당 기간에 대해 소규모 **SQL 정책 검사** 카탈로그를 실행합니다. 이 카탈로그는 경계가 있는 집계 쿼리로 알려진 문제 패턴을 플래그하고, 일치하는 텍스트 자체가 아닌 *몇 건의* 이벤트 / *어느* 세션이 해당되는지를 보고합니다. 카탈로그 항목은 다음과 같습니다: +어떤 모델도 실행되기 전에, 감사는 해당 기간에 대해 소규모 **SQL 정책 검사** 카탈로그를 실행합니다. 이는 알려진 문제 패턴을 감지하고 일치하는 이벤트 *수* / 세션이 *무엇인지*를 보고하는 범위가 제한된 집계 쿼리이며, 일치한 텍스트 자체는 보고하지 않습니다. 카탈로그에는 다음이 포함됩니다: -- 이벤트 페이로드의 **비밀 / 자격증명 유출** — AWS 액세스 키, `sk-…` API 키, PEM 개인 키, JWT / 베어러 토큰, `KEY=…` 자격증명 할당 -- **프롬프트 주입 마커** — "이전 지침을 무시하라", "시스템 프롬프트를 공개하라" 등 -- **개인식별정보(PII)** — 주민등록번호 형식 숫자(휴리스틱) -- **도구 권한 거부** 및 **도구 호출 루프 폭주** +- 이벤트 페이로드의 **비밀/자격증명 유출** — AWS 액세스 키, `sk-…` API 키, PEM 개인 키, JWT / 베어러 토큰, `KEY=…` 형식의 자격증명 할당. +- **프롬프트 인젝션 마커** — "이전 지시를 무시하라", "시스템 프롬프트를 공개하라" 등 유사한 표현. +- **개인식별정보(PII)** — SSN 형식의 숫자 (휴리스틱 기반). +- **도구 권한 거부** 및 **무한 도구 호출 루프**. -정책 위반은 (종류: `policy`) 발견 사항으로 저장되며, **항상 표시**됩니다(실행별 상한선에 의해 제거되지 않음). 또한 AI 에이전트에 초기 단서로 전달됩니다. 이 레이어는 모델이 필요 없으므로, AI 에이전트를 사용할 수 없는 경우에도 감사는 가장 중요한 보안 신호를 생성합니다. +정책 적중 사항은 (`policy` 종류의) 발견 사항으로 영속 저장되며 **항상 표시됩니다** (실행당 상한선에 의해 잘리지 않습니다). 또한 AI 에이전트에게 초기 단서로 전달됩니다. 이 계층은 모델이 필요하지 않기 때문에, AI 에이전트를 사용할 수 없는 경우에도 감사는 가장 중요한 보안 신호를 계속 생성합니다. -### 2. 에이전틱 조사 (AI) +### 2. 에이전트 조사 (AI) -감사는 이어서 **자율 신뢰성 에이전트**(대시보드 어시스턴트를 구동하는 것과 동일한 Claude Agent SDK 서비스이며, 감사 전용 프롬프트 적용)를 실행합니다. 에이전트는 감사의 **범위**(선택된 에이전트 × 환경)와 **시간 창**을 바탕으로: +이후 감사는 **자율 안정성 에이전트**(대시보드 어시스턴트를 구동하는 것과 동일한 Claude Agents SDK 서비스에 감사 전용 프롬프트 적용)를 실행합니다. 에이전트는 감사의 **범위** (선택된 에이전트 × 환경) 및 **기간**을 기반으로 다음을 수행합니다: -- 분석 테이블에 대해 읽기 전용 SQL 쿼리를 실행하고, -- 대표적인 세션 대화록 일부를 읽으며, -- SQL로 표현할 수 없는 분석(오류 클러스터링, 분포 계산, 이미 가져온 페이로드 스윕 등)을 위해 **네트워크와 파일시스템 접근이 차단된 인-팟 샌드박스** (비밀 정보 제거)에서 짧은 **Python 스크립트**를 작성·실행하고, -- 근거가 충분히 확인된 **개선 사항**을 기록합니다. +- 분석 테이블에 대해 읽기 전용 SQL 쿼리 실행, +- 대표적인 세션 트랜스크립트 일부 읽기, +- SQL로 표현할 수 없는 분석(오류 클러스터링, 분포 계산, 이미 가져온 페이로드 스위핑)을 위해 필요시 **격리된 인팟(in-pod) 샌드박스**에서 짧은 **Python 스크립트를 작성하고 실행** (네트워크 없음, 파일시스템 접근 없음, 비밀 정보 제거), +- 충분한 근거가 있는 각 **개선 사항** 기록. -에이전트는 감사의 **민감도**(낮음 / 보통 / 높음)에 따라 오류 클러스터링, 기준선 대비 드리프트, 대화록의 목표 실패, 도구 오용, 품질/비용 균형, 커버리지 공백 등 여러 조사 방향을 탐색합니다. 모든 개선 사항에는 **반드시 근거가 있어야 합니다**: 에이전트가 실제로 검사한 세션 ID 및/또는 실행한 SQL. 서버는 인용된 세션의 존재를 검증하고 **근거가 없는 개선 사항은 폐기**하므로, 에이전트는 조사하지만 결코 꾸며내지 않습니다. +에이전트는 감사의 **민감도** (낮음 / 중간 / 높음)에 따라 오류 클러스터링, 기준선 대비 드리프트, 트랜스크립트의 목표 달성 실패, 도구 오용, 품질/비용 절충, 커버리지 공백 등 여러 조사 항목을 검토합니다. 모든 개선 사항은 **근거를 인용해야** 합니다. 즉, 에이전트가 실제로 검사한 세션 ID 및/또는 실행한 SQL이 있어야 합니다. 서버는 인용된 세션이 존재하는지 검증하고, **유효한 근거가 없는 개선 사항은 폐기**합니다. 에이전트는 조사하지만 결코 날조하지 않습니다. 각 개선 사항에는 다음이 포함됩니다: -- **권고사항** (구체적인 변경 내용 — 프롬프트 수정, 도구 스키마 수정, 재시도 정책, 가드레일, 평가 커버리지 확대), -- **예상 영향** 및 **노력** 추정치 (낮음 / 보통 / 높음), -- **중요도** — `big` (운영자에게 즉시 알림 필요), `medium` (실행 보고서에 포함), `small` (대시보드 컨텍스트 수준), -- 안정적인 **핑거프린트** (이번 실행의 세션이 아닌 이슈의 카테고리 + 범위 기반) — 근거가 바뀌어도 실행에 걸쳐 동일 이슈를 추적 가능, -- 재발을 감지할 수 있는 간단한 결정적 감시자를 만들 수 있을 때, 클릭 한 번으로 생성 가능한 **알림 제안**. +- **권고사항** (구체적인 변경 내용 — 프롬프트 수정, 도구 스키마 수정, 재시도 정책, 가드레일, 추가 평가 커버리지), +- **예상 효과** 및 **작업량** 추정치 (낮음 / 중간 / 높음), +- **중요도** — `big` (운영자에게 즉시 알림 필요), `medium` (실행 보고서에 포함), `small` (대시보드 컨텍스트), +- 안정적인 **지문** (이번 실행의 세션이 아닌 이슈의 카테고리 + 범위 기반) — 근거가 바뀌더라도 동일한 이슈를 실행 간에 추적할 수 있도록, +- 재발을 감지할 수 있는 단순한 결정론적 감시가 가능한 경우, 클릭 한 번으로 생성할 수 있는 **제안 알림**. -> **AI 레이어는 선택 사항이지만 권장됩니다.** 감사 파이프라인에 AI 에이전트가 구성되어 있지 않아도 실행은 계속되며, 정책 발견 사항은 저장됩니다. 에이전틱 레이어에 대해서는 조용히 통과시키는 대신 "분석 불가"로 정직하게 보고합니다. +> **AI 계층은 선택 사항이지만 권장됩니다.** 감사 파이프라인에 AI 에이전트가 구성되어 있지 않더라도 실행은 계속되며, 정책 발견 사항은 영속 저장됩니다. 에이전트 계층에 대해서는 조용히 통과시키는 대신 "분석 불가"로 정직하게 보고합니다. ### 실패 모드 -개선 사항은 조직의 영속적인 **실패 모드 카탈로그**로 분류되거나 새로운 모드를 제안합니다. 모드는 실행 전반과 장기적인 재발 추적에 걸쳐 패턴에 안정적인 정체성을 부여합니다. +개선 사항은 조직의 영속적인 **실패 모드 카탈로그**로 분류되거나 새로운 모드를 제안합니다. 모드는 실행 간 및 장기적인 재발 추적에서 패턴에 안정적인 식별자를 부여합니다. -## 심사(Triage) 라이프사이클 +## 분류 생명주기 -발견 사항 페이지 (`/audits//findings/`)에서: +발견 사항 페이지(`/audits//findings/`)에서: -| 액션 | 효과 | +| 작업 | 효과 | |---|---| -| **인지(acknowledge)** | 발견 사항을 계속 표시하되 우선순위를 절반으로 낮춥니다. | -| **해결(resolve)** | 수정 완료로 표시합니다. 이후 패턴이 실제로 다시 나타나면 **신규**로 재오픈됩니다 — 회귀가 조용히 기록에 묻히지 않고 눈에 띄게 표시됩니다. | -| **음소거(mute)** / **기각(dismiss)** | 영구 억제: 패턴의 핑거프린트가 기억되어 실행 전반에 걸쳐 다시 표시되지 않습니다. mute는 "알고 있으며 수용함", dismiss는 "유용하지 않음"에 사용합니다. | -| **재오픈(reopen)** | 억제 / 해결을 취소하고 패턴의 우선순위를 다시 부여합니다. | -| **할당(assign)** | 소유권을 위해 발견 사항을 운영자(조직 멤버)에게 라우팅합니다. 우선순위와 억제 상태는 변경되지 않습니다. | +| **acknowledge(확인)** | 발견 사항을 표시 상태로 유지하되 우선순위를 절반으로 낮춥니다. | +| **resolve(해결)** | 수정된 것으로 표시합니다. 이후 해당 패턴이 실제로 재발하면 **신규**로 재개됩니다 — 회귀가 조용히 기록에 묻히지 않고 명확히 드러납니다. | +| **mute(음소거)** / **dismiss(무시)** | 영속적 억제: 패턴의 지문이 기억되어 실행이 바뀌어도 다시 표시되지 않습니다. mute는 "알려진, 수용된 문제"에, dismiss는 "유용하지 않은 항목"에 사용하세요. | +| **reopen(재열기)** | 억제/해결을 해제하고 패턴의 순위를 다시 적용합니다. | -저신호 노이즈는 에이전틱 개선에 대한 실행별 발견 사항 상한선(`top_k`)으로 감사별 제어됩니다. 정책 발견 사항은 보안 관련 항목으로 항상 표시되며 상한선을 우회합니다. 상한선에 의해 잘린 항목은 실행 통계에 포함되며, 조용히 삭제되지 않습니다. +저신호 노이즈는 에이전트 개선 사항에 대한 실행당 발견 상한선(`top_k`)으로 감사별로 제어됩니다. 정책 발견 사항은 보안과 관련이 있어 항상 표시되므로 상한선을 적용받지 않습니다. 상한선으로 제외된 항목은 실행 통계에 집계되며, 아무것도 조용히 삭제되지 않습니다. ## 스케줄링 -- **주기** (`schedule_interval_secs`): 매시간부터 매주까지; **기본값은 매일**. 감사는 의도적으로 알림보다 주기가 깁니다 — 에이전틱 조사는 전체 창을 스캔하며 수 분이 소요됩니다. -- **창(Window)**: 고정된 롤링 룩백(예: "각 실행은 지난 7일을 스캔") 또는 **마지막 실행 이후**(기본값) — 각 실행은 이전 성공적인 실행이 끝난 지점부터 시작하며, 경계 이벤트를 놓치지 않기 위해 작은 겹침(overlap)이 있습니다. -- 다음 실행은 이전 실행이 **완료된** 후 전체 주기만큼 지나 예약되므로, 느린 실행이 동일 감사의 두 번째 동시 실행을 쌓지 않습니다. -- 감사 페이지의 **지금 실행**을 누르면 즉시 실행 예약됩니다. +- **주기** (`schedule_interval_secs`): 시간별부터 주별; **기본값은 일별**입니다. 감사는 의도적으로 알림보다 주기가 깁니다 — 에이전트 조사는 전체 기간을 스캔하며 수 분이 소요됩니다. +- **기간**: 고정 롤링 룩백(예: "각 실행은 지난 7일을 스캔") 또는 **마지막 실행 이후**(기본값) — 각 실행은 이전의 성공적인 실행이 끝난 지점부터 소폭 겹치는 구간을 포함하여 시작하므로 경계 이벤트를 놓치지 않습니다. +- 다음 실행은 이전 실행이 **완료된** 후 전체 주기만큼 지난 시점에 예약되므로, 느린 실행이 동일한 감사의 두 번째 동시 실행을 생성하지 않습니다. +- 감사 페이지의 **지금 실행**을 클릭하면 즉시 실행 대기 상태가 됩니다. ## 모델 선택 -감사를 생성할 때 **운영자가 에이전트 서비스에 구성한 모델 목록**에서 조사에 사용할 모델을 선택할 수 있습니다. 단일 모델이 구성된 경우 선택기는 해당 모델을 캡션으로 표시하며, 여러 모델이 있으면 선택할 수 있습니다. 설정하지 않으면 구성된 기본값이 사용됩니다. +감사를 생성할 때 조사에 사용할 모델을 **운영자가 에이전트 서비스에 구성한 모델 목록**에서 선택할 수 있습니다. 모델이 하나만 구성된 경우 선택기는 해당 모델을 캡션으로 표시하고, 여러 개인 경우 직접 선택합니다. 설정하지 않으면 구성된 기본값을 사용합니다. ## 알림 -실행에서 **신규** 발견 사항이 나타나면, 감사는 조직의 구성된 채널에 알림을 보냅니다 — 알림 파이프라인이 사용하는 것과 동일한 `alerts.enabled_channels` 게이트 및 설정: +실행에서 **신규** 발견 사항이 나타나면 감사는 조직에 구성된 채널로 알림을 전송합니다. 알림 파이프라인과 동일한 `alerts.enabled_channels` 게이트 및 설정을 사용합니다: -- **Slack** — 중요한(`big`) 신규 항목 요약과 딥 링크 -- **이메일** — 감사에 **이메일** 채널이 연결되어 있고 신규 발견 사항이 하나 이상 있을 때, 새로운 개선 사항(최고 심각도, 항목별 권고사항, 딥 링크)을 나열한 **감사 보고서**를 전송 +- **Slack** — 중요한(`big`) 신규 항목 요약과 딥 링크. +- **이메일** — 신규 개선 사항(최고 심각도, 항목별 권고사항, 딥 링크)을 정리한 **감사 보고서**. 감사에 **이메일** 채널이 연결되어 있고 신규 발견 사항이 하나 이상 있을 때 전송됩니다. -반복되지만 알려진 발견 사항은 재알림을 보내지 않습니다. +이미 알려진 반복 발견 사항은 재알림을 보내지 않습니다. ## 구성 참조 -감사 정의는 대시보드(`/audits/new`) 또는 API를 통해 관리합니다. 감사별 설정에는 스케줄 주기와 창, 범위(`{"environments": [...], "agent_ids": [...]}`), 민감도(`low` / `medium` / `high`), 알림 채널, 실행별 발견 사항 상한선(`top_k`), 모델(`llm_budget.model`을 통해)이 포함됩니다. 운영자 수준 서버 설정(타임아웃, 샌드박스, 에이전트 서비스 URL)은 [deployment.md](/ko/agenteye/deployment)에 문서화되어 있습니다. +감사 정의는 대시보드(`/audits/new`) 또는 API를 통해 관리합니다. 감사별 설정에는 스케줄 주기 및 기간, 범위(`{"environments": [...], "agent_ids": [...]}`), 민감도(`low` / `medium` / `high`), 알림 채널, 실행당 발견 상한선(`top_k`), 모델(`llm_budget.model` 경유)이 포함됩니다. 운영자 수준 서버 설정(타임아웃, 샌드박스, 에이전트 서비스 URL)은 [deployment.md](/ko/agenteye/deployment)에 문서화되어 있습니다. ## API -모든 엔드포인트는 조직 범위이며 표준 베어러 키 인증을 따릅니다([api-keys.md](/ko/agenteye/api-keys) 참조). +모든 엔드포인트는 조직 범위로 제한되며 표준 베어러 키 인증을 따릅니다([api-keys.md](/ko/agenteye/api-keys) 참조). | 엔드포인트 | 권한 | 목적 | |---|---|---| -| `GET /audits` · `POST /audits` | `audits:read` / `audits:write` | 감사 정의 목록 조회 / 생성 | -| `GET` / `PUT` / `DELETE /audits/:id` | read / write / write | 감사 조회, 수정, 삭제 | -| `POST /audits/:id/run` | `audits:write` | 감사를 즉시 실행 예약 | -| `GET /audits/:id/runs` | `audits:read` | 실행 기록 (창, 상태, 통계, 발견 사항 수) | -| `GET /audits/findings` | `audits:read` | 조직 전체 발견 사항, `audit_id`, `status`로 필터링 가능; 우선순위 순 정렬 | -| `GET /audits/findings/:fid` | `audits:read` | 발견 사항 전체 상세 정보 (권고사항, 근거, 우선순위) | -| `POST /audits/findings/:fid/status` | `audits:write` | 심사: `{"action": "ack" \| "mute" \| "dismiss" \| "resolve" \| "reopen" \| "assign"}` | - -"감사를 실행했지만 아무것도 찾지 못함", "코드 샌드박스가 비활성화됨", "감사 이메일이 전달되지 않음"에 대해서는 [troubleshooting.md](/ko/agenteye/troubleshooting#audits)를 참조하세요. \ No newline at end of file +| `GET /audits` · `POST /audits` | `audits:read` / `audits:write` | 감사 정의 목록 조회 / 생성. | +| `GET` / `PUT` / `DELETE /audits/:id` | read / write / write | 감사 조회, 수정, 삭제. | +| `POST /audits/:id/run` | `audits:write` | 감사를 즉시 실행 대기 상태로 만듭니다. | +| `GET /audits/:id/runs` | `audits:read` | 실행 기록 (기간, 상태, 통계, 발견 사항 수). | +| `GET /audits/findings` | `audits:read` | 조직 전체 발견 사항 — `audit_id`, `status`로 필터링 가능, 우선순위 순 정렬. | +| `GET /audits/findings/:fid` | `audits:read` | 발견 사항 전체 상세 정보 (권고사항, 근거, 우선순위). | +| `POST /audits/findings/:fid/status` | `audits:write` | 분류: `{"action": "ack" \| "mute" \| "dismiss" \| "resolve" \| "reopen" \| "assign"}`. | + +"감사가 실행되었으나 아무것도 발견하지 못한 경우", "코드 샌드박스가 비활성화된 경우", "감사 이메일이 전달되지 않은 경우"에 대해서는 [troubleshooting.md](/ko/agenteye/troubleshooting#audits)를 참조하세요. \ No newline at end of file diff --git a/docs/ko/agenteye/cli-recipes.mdx b/docs/ko/agenteye/cli-recipes.mdx index a066eb9b..f79e2bbf 100644 --- a/docs/ko/agenteye/cli-recipes.mdx +++ b/docs/ko/agenteye/cli-recipes.mdx @@ -1,30 +1,30 @@ --- -title: "에이전트용 CLI 레시피" +title: "에이전트를 위한 CLI 레시피" description: "에이전트를 위한 AgentEye CLI 레시피 문서." --- -스크립트나 코딩 에이전트에서 직접 세션, 이벤트, 평가 데이터를 가져오고(재평가 트리거 포함), `jq`로 바로 파이프할 수 있는 깔끔한 JSON을 stdout에 출력합니다. 이 레시피들은 AgentEye의 관측 가능성 데이터를 터미널 사용자나 AI 코딩 에이전트(Claude Code, Cursor)가 대시보드를 클릭하지 않고도 조회하고 자동화할 수 있는 형태로 변환합니다. +스크립트나 코딩 에이전트에서 세션, 이벤트, 평가 데이터를 직접 가져오고(재평가도 트리거할 수 있으며), `jq`로 바로 파이프할 수 있는 깔끔한 JSON을 stdout에 출력합니다. 이 레시피들은 AgentEye의 관찰 가능성 데이터를 터미널 사용자나 AI 코딩 에이전트(Claude Code, Cursor)가 대시보드를 클릭하지 않고도 쿼리하고 자동화할 수 있는 형태로 만들어 줍니다. 아래 패턴들은 AgentEye CLI(`agenteye`)에서 바로 복사해서 사용할 수 있습니다. 설치, 인증, 전체 옵션 목록은 [CLI](/ko/agenteye/cli)를 참고하세요. 내장 도움말은 `agenteye -h` 또는 `agenteye -h`로 확인할 수 있습니다. ## 기본 원칙 -1. **전역 옵션은 커맨드 *앞에* 붙입니다.** `agenteye --json sessions`가 올바른 형태이며, `agenteye sessions --json`은 올바르지 않습니다. 전역 옵션은 `--json`, `--base-url`, `--org`, `--token`, `--insecure`/`--secure`, `--timeout`, `--quiet`, `--no-color`입니다. -2. **출력을 파싱할 때는 반드시 `--json`을 전달합니다.** 데이터는 JSON 형태로 **stdout**에 출력되고, 상태 메시지와 오류는 **stderr**로 출력되어 stdout을 `jq`로 파이프하기 깔끔하게 유지됩니다. -3. **stderr 텍스트가 아닌 종료 코드를 기준으로 분기합니다.** `0` 성공 · `2` 잘못된 인수 · `3` 대시보드 연결 불가 · `4` 미로그인 또는 세션 만료 · `5` 권한 없음. -4. **`-h`로 탐색합니다.** 모든 커맨드에서 필터, 값 형식, JSON 형태를 문서로 확인할 수 있습니다. +1. **전역 옵션은 명령어 *앞*에 붙입니다.** `agenteye --json sessions`가 올바른 형태이며, `agenteye sessions --json`은 올바르지 않습니다. 전역 옵션은 `--json`, `--base-url`, `--org`, `--token`, `--insecure`/`--secure`, `--timeout`, `--quiet`, `--no-color`입니다. +2. **출력을 파싱할 때는 반드시 `--json`을 전달하세요.** 데이터는 JSON 형태로 **stdout**에 출력되고, 사람이 읽는 상태 메시지와 오류는 **stderr**로 출력되므로 stdout은 `jq`로 파이프하기에 깔끔한 상태를 유지합니다. +3. **stderr 텍스트가 아닌 종료 코드로 분기하세요.** `0` 성공 · `2` 잘못된 인수 · `3` 대시보드에 연결할 수 없음 · `4` 로그인되지 않았거나 만료됨 · `5` 권한 없음. +4. **`-h`로 탐색하세요.** 모든 명령어는 필터, 값 형식, JSON 구조를 문서화하고 있습니다. -## 초기 설정 +## 최초 설정 ```bash -export AGENTEYE_DASHBOARD_URL=https://agenteye.example.com # --base-url을 반복하지 않아도 됨 +export AGENTEYE_DASHBOARD_URL=https://agenteye.example.com # --base-url을 반복하지 않아도 됩니다 agenteye login --email you@example.com # 이메일로 받은 코드를 붙여넣기; 약 24시간 유효 ``` ## 작업 전 인증 확인 -`whoami`는 세션이 없거나 만료된 경우에도 오류를 발생시키지 않고 `logged_in:false`를 반환하므로, 에이전트가 인증 상태를 안전하게 확인할 수 있습니다. (단, 베이스 URL이 설정되지 않았거나 대시보드에 연결할 수 없는 경우에는 여전히 비정상 종료될 수 있습니다.) +`whoami`는 세션이 없거나 만료된 경우에도 오류를 발생시키지 않고 `logged_in:false`를 반환하므로, 에이전트가 안전하게 인증 상태를 확인할 수 있습니다. (베이스 URL이 설정되지 않았거나 대시보드에 연결할 수 없는 경우에는 여전히 non-zero로 종료될 수 있습니다.) ```bash if [ "$(agenteye --json whoami | jq -r .logged_in)" != "true" ]; then @@ -32,44 +32,44 @@ if [ "$(agenteye --json whoami | jq -r .logged_in)" != "true" ]; then fi ``` -## 실패하거나 낮은 점수의 세션 찾기 +## 실패하거나 점수가 낮은 세션 찾기 ```bash -# 평가가 오류 상태인 최근 24시간 세션 +# 지난 24시간 내 평가가 오류인 세션 agenteye --json sessions --since 24h --status error | jq -r '.sessions[].session_id' -# 특정 에이전트에서 helpfulness 점수가 0.5 이하인 평가 +# 특정 에이전트에서 도움성 점수가 0.5 이하인 평가 agenteye --json evals --agent-id checkout-bot --score helpfulness:..0.5 \ | jq '.evaluations[] | {session_id, scores}' ``` -점수 필터링은 `sessions`가 아닌 **`evals`**에서 적용됩니다. `--score KEY:MIN..MAX`는 반복 사용 가능하며 AND 조건으로 결합됩니다. 양쪽 경계값은 선택 사항입니다(`..0.5`는 ≤ 0.5, `0.9..`는 ≥ 0.9를 의미합니다). 요청당 최대 20개의 점수 필터를 전달할 수 있으며, 초과하면 HTTP 400이 반환됩니다. `sessions`는 `evals`와 `--env`, `--status`, `--agent-id`, `--session-id`, 시간 범위 필터를 공유하지만 `--score`는 없습니다. +점수 필터링은 `sessions`가 아닌 **`evals`**에서 처리됩니다. `--score KEY:MIN..MAX`는 반복 사용 가능하며 AND 조건으로 결합됩니다. 각 경계는 선택 사항입니다(`..0.5`는 ≤ 0.5, `0.9..`는 ≥ 0.9를 의미합니다). 요청당 최대 20개의 점수 필터를 전달할 수 있으며, 그 이상이면 HTTP 400을 반환합니다. `sessions`는 `evals`와 `--env`, `--status`, `--agent-id`, `--session-id`, 시간 범위 필터를 공유하지만 `--score`는 없습니다. -## 하나의 세션 전체 읽기 +## 세션 전체 읽기 -단일 `session show` 커맨드는 없으며, 이벤트 이력과 세션 평가를 조합합니다. +단일 `session show` 명령어는 없습니다 — 이벤트 기록과 세션 평가를 조합하세요: ```bash # 세션의 최신 평가 (상태 + 점수) agenteye --json evals --session-id run-001 | jq '.evaluations[0] | {status, scores}' -# 실행의 모든 이벤트 (전체 조회를 위해 --limit 늘리기) +# 실행의 모든 이벤트 (전체 조회를 위해 --limit 값을 높이세요) agenteye --json events --session-id run-001 --all --limit 1000 | jq '.events[] | {ts, event_type}' -# 세션에서 도구 호출만 +# 세션 내 도구 호출만 조회 agenteye --json events --session-id run-001 --event-type tool_use,tool_result --all \ | jq '.events[].payload' ``` -## 전체 데이터 가져오기 (페이지네이션) +## 전체 가져오기 (페이지네이션) -결과는 최신순으로 정렬되며 커서 기반 페이지네이션을 사용합니다. +결과는 최신순으로 반환되며 커서 기반 페이지네이션을 사용합니다. ```bash -# 한 번에: 200행 페이지 단위로 최대 500행 가져오기 +# 한 번에: 200행 단위 페이지로 최대 500행 가져오기 agenteye --json events --session-id run-001 --limit 500 --all > events.json -# 수동 페이지 이동: next_cursor를 다시 전달 +# 수동 페이징: next_cursor를 다시 전달 page=$(agenteye --json events --limit 100) cursor=$(echo "$page" | jq -r '.next_cursor // empty') [ -n "$cursor" ] && agenteye --json events --limit 100 --cursor "$cursor" @@ -77,62 +77,62 @@ cursor=$(echo "$page" | jq -r '.next_cursor // empty') ## --fields로 출력 줄이기 -에이전트가 읽어야 할 내용을 줄이기 위해 키를 제한합니다(테이블과 `--json` 모두 적용). +에이전트가 읽어야 할 양을 줄이기 위해 키를 제한합니다(테이블과 `--json` 모두 적용). ```bash agenteye --json sessions --since 7d --fields session_id,status,scores | jq -c '.sessions[]' agenteye --json events --session-id run-001 --fields ts,event_type --all ``` -알 수 없는 필드 이름은 유효한 목록과 함께 거부됩니다(종료 코드 `2`). 이를 통해 필드 이름을 간편하게 탐색할 수 있습니다. +알 수 없는 필드명은 유효한 목록과 함께 거부됩니다(종료 코드 `2`). 필드명을 확인하는 간편한 방법입니다. ## 유효한 필터 값 탐색 ```bash -agenteye --json list envs | jq -r '.values[]' # --env에 사용할 값 +agenteye --json list envs | jq -r '.values[]' # --env 값 목록 agenteye --json list tools | jq -r '.values[]' # 도구 이름; agents, models, event_types 등도 가능 agenteye --json list score_filters | jq -r '.values[]' # --score KEY:MIN..MAX의 유효한 KEY ``` ## 조직 선택 (멀티 테넌트) -둘 이상의 조직에 속해 있는 경우, 로그인 시 활성 테넌트를 선택합니다(저장됨): +여러 조직에 속한 경우, 로그인 시 활성 테넌트를 선택할 수 있습니다(저장됩니다): ```bash agenteye login --org acme --email you@corp.com # 로그인과 동시에 테넌트 설정 agenteye --json orgs list | jq -r '.orgs[].org_slug' -agenteye --org globex --json sessions --since 24h # 단일 커맨드에 대해 재정의 +agenteye --org globex --json sessions --since 24h # 단일 명령어에 대해 재정의 ``` -`--org` 없이 여러 조직에 로그인하면 비정상 종료되며 선택 가능한 조직 목록이 출력됩니다. +`--org` 없이 여러 조직에 로그인하면 non-zero로 종료되고 선택 가능한 조직 목록이 출력됩니다. -## SDK/콜렉터용 API 키 발급 +## SDK/수집기용 API 키 생성 ```bash -# 시크릿은 한 번만 출력됨 — --json 사용 시 .key 필드에서 확인 +# 시크릿은 한 번만 출력됩니다 — --json 사용 시 .key 필드에 있습니다 key=$(agenteye --json keys create ci-bot --add events:read.add | jq -r '.key') -agenteye keys regenerate ci-bot --yes # 키 교체; 폐기는 agenteye keys disable ci-bot --yes +agenteye keys regenerate ci-bot --yes # 교체; 폐기하려면 agenteye keys disable ci-bot --yes ``` -## 저장된 쿼리 또는 임시 쿼리 실행 +## 저장된 쿼리 또는 즉석 쿼리 실행 ```bash agenteye --json query run --sql "select count(*) from analytics.events" | jq '.rows' agenteye --json query run errs --arg prod | jq '.rows' # 저장된 쿼리 + 위치 인수 $1 ``` -## 인시던트 비대화형 트리아지 +## 인시던트 비대화형으로 처리 ```bash id=$(agenteye --json incidents list --state firing | jq -r '.incidents[0].id') agenteye incidents ack "$id" -agenteye incidents assign "$id" you@corp.com +agenteye incidents assign "$id" --assignee you@corp.com agenteye incidents resolve "$id" --yes ``` -> 뮤테이션은 `--json` 사용 시 또는 stdin이 TTY가 아닌 경우 확인 프롬프트를 자동으로 건너뜁니다. 따라서 에이전트가 멈추는 일이 없으며, 다른 곳에서 명시적으로 건너뛰려면 `--yes`/`-y`를 전달하세요. +> 변경(mutation) 명령어는 `--json` 사용 시 또는 stdin이 TTY가 아닐 때 확인 프롬프트를 자동으로 건너뛰므로 에이전트가 멈추지 않습니다. 다른 상황에서 명시적으로 건너뛰려면 `--yes`/`-y`를 전달하세요. -## 스크립트에서 종료 코드 처리 +## 스크립트에서의 종료 코드 처리 ```bash out=$(agenteye --json sessions --since 1h) || code=$? @@ -145,9 +145,9 @@ case "${code:-0}" in esac ``` -## JSON 출력 형태 +## JSON 출력 구조 -| 커맨드 | stdout JSON (`--json` 사용 시) | +| 명령어 | stdout JSON (`--json` 사용 시) | |---|---| | `whoami` | `{"logged_in": true, "id", "email", "is_instance_admin", "active_org", "permissions": [...], "memberships": [...]}` 또는 `{"logged_in": false}` | | `orgs list` | `{"active_org", "orgs": [{"org_slug","org_name","permission_set","permissions"}]}` | @@ -160,11 +160,11 @@ esac | `query run` | `{columns: [{name,type}], rows: [[...]], truncated, elapsed_ms}` | | `users list` / `settings list` | `{"users": [...]}` / `{"settings": [...]}` | | `alerts list` / `incidents list` | `{"alerts": [...]}` / `{"incidents": [...]}` | -| create/update/delete (모두) | 리소스 객체, 또는 삭제 시 `{"deleted": true, "id"}` | -| 실패 (모두, `--json` 사용 시) | stdout에 `{"error": "...", "exit_code": , "status"?: , "hint"?: "..."}` | +| create/update/delete (모든 경우) | 리소스 객체, 또는 삭제 시 `{"deleted": true, "id"}` | +| 실패 (모든 경우, `--json` 사용 시) | stdout에 `{"error": "...", "exit_code": , "status"?: , "hint"?: "..."}` | -- **이벤트** 항목(`events`) 각각: `id, session_id, agent_id, event_type, ts, payload, environment`. -- **평가** 항목(`evals`) 각각: `id, session_id, agent_id, environment, status, scores, reasoning, summary, error, attempt_count, duration_ms, completed_at, created_at`. -- **세션** 항목(`sessions`) 각각: `session_id, agent_id, environment, status, scores, event_count, started_at, last_event_at, first_event_id, last_event_id, latest_evaluation`. +- 각 **이벤트** 항목(`events`): `id, session_id, agent_id, event_type, ts, payload, environment`. +- 각 **평가** 항목(`evals`): `id, session_id, agent_id, environment, status, scores, reasoning, summary, error, attempt_count, duration_ms, completed_at, created_at`. +- 각 **세션** 항목(`sessions`): `session_id, agent_id, environment, status, scores, event_count, started_at, last_event_at, first_event_id, last_event_id, latest_evaluation`. -각 커맨드의 `--fields`는 해당 항목의 필드 이름만 허용합니다. `sessions`와 `evals`는 필드 집합이 다르므로, 한쪽에서 유효한 이름이 다른 쪽에서는 거부될 수 있습니다. \ No newline at end of file +각 명령어의 `--fields`는 해당 항목의 필드명만 허용합니다 — `sessions`와 `evals`의 필드 집합이 다르므로, 한쪽에서 유효한 이름이 다른 쪽에서는 거부될 수 있습니다. \ No newline at end of file diff --git a/docs/ko/agenteye/deployment.mdx b/docs/ko/agenteye/deployment.mdx index 58cb9e4d..db6afa7a 100644 --- a/docs/ko/agenteye/deployment.mdx +++ b/docs/ko/agenteye/deployment.mdx @@ -1,6 +1,6 @@ --- title: "배포" -description: "AgentEye 배포 문서" +description: "AgentEye 배포 문서." --- 이 가이드는 프로덕션 환경에서 AgentEye 서버와 대시보드를 배포하는 방법을 다룹니다. @@ -29,12 +29,12 @@ description: "AgentEye 배포 문서" +-----------+ ``` -- **Server**: Rust HTTP 서비스. 이벤트 배치를 수신하여 ClickHouse에 기록하고, PostgreSQL에 관계형 상태를 유지합니다. +- **Server**: Rust HTTP 서비스. 이벤트 배치를 수신하고 ClickHouse에 기록하며 PostgreSQL에서 관계형 상태를 유지합니다. - **Dashboard**: Next.js 웹 앱. 서버 API를 통해서만 읽기/쓰기를 수행합니다. - **agenteye-collector**: 서버 호스트가 아닌 에이전트 머신에 배포됩니다. -- **Postgres 15+**: **필수.** (멀티테넌트 릴리스에서 14에서 15로 상향됨. org-membership 스키마에서 컬럼 목록 `ON DELETE SET NULL` 외래 키를 사용하는데, 이는 Postgres 15+에서만 지원됩니다. 이 버전을 배포하기 전에 Postgres를 업그레이드하세요.) OLTP 상태를 저장합니다: `api_keys`, `users`, `sessions`, `evaluation_jobs` (큐), `dashboards`, `saved_queries`, `otp_codes`, 그리고 멀티테넌트 테이블 `orgs`, `org_memberships`, `org_settings`. -- **ClickHouse 24+**: **필수.** 수집된 모든 이벤트에 대한 분석 저장소입니다. 엔진: `ReplacingMergeTree`, 월별 파티션, `(session_id, ts, dedup_key)` 순서로 정렬됩니다. 서버는 `CLICKHOUSE_URL`을 통해 연결됩니다. 번들로 제공되는 `deploy/base/clickhouse/`는 성능이 최적화된 단일 노드 구성을 포함합니다. **멀티테넌트 요구사항:** 번들 구성은 SQL 접근 관리와 `users_without_row_policies_can_read_rows=false`를 활성화하여, 서버가 조직별로 읽기 전용 ClickHouse 사용자와 행 정책을 생성할 수 있도록 합니다(SQL 편집기와 AI 에이전트에 대한 엔진 수준의 격리 경계). 직접 ClickHouse 구성을 제공하는 경우 이 설정을 유지하세요(`deploy/base/clickhouse/configmap.yaml` 참조). -- **Redis 7+**: *선택 사항*인 공유 캐시 + 속도 제한 백엔드입니다. 서버와 대시보드 모두 `REDIS_URL`을 통해 연결합니다. 없는 경우 두 서비스 모두 Postgres 전용 경로로 자연스럽게 폴백합니다. 아래의 **Redis(선택 사항 캐시)** 섹션을 참조하세요. +- **Postgres 15+**: 필수 항목입니다. (멀티테넌트 릴리스에서 14에서 상향; org 멤버십 스키마가 Postgres 15+ 전용인 컬럼 목록 `ON DELETE SET NULL` 외래 키를 사용합니다. 이 버전 배포 전에 Postgres를 업그레이드하세요.) OLTP 상태를 저장합니다: `api_keys`, `users`, `sessions`, `evaluation_jobs`(큐), `dashboards`, `saved_queries`, `otp_codes`, 그리고 멀티테넌트 테이블 `orgs`, `org_memberships`, `org_settings`. +- **ClickHouse 24+**: 필수 항목입니다. 수집된 모든 이벤트의 분석 저장소입니다. 엔진: `ReplacingMergeTree`, 월별 파티션, `(session_id, ts, dedup_key)` 순으로 정렬됩니다. 서버는 `CLICKHOUSE_URL`을 통해 연결합니다. 번들로 제공되는 `deploy/base/clickhouse/`에는 성능 최적화된 단일 노드 구성이 포함됩니다. **멀티테넌트 요구사항:** 번들 구성은 SQL 액세스 관리와 `users_without_row_policies_can_read_rows=false`를 활성화하여 서버가 조직마다 읽기 전용 ClickHouse 사용자와 행 정책을 생성할 수 있도록 합니다(SQL 에디터 및 AI 에이전트의 엔진 수준 격리 경계). 자체 ClickHouse 구성을 사용하는 경우 이 설정을 가져오세요(`deploy/base/clickhouse/configmap.yaml` 참조). +- **Redis 7+**: *선택적* 공유 캐시 + 속도 제한 백엔드입니다. 서버와 대시보드 모두 `REDIS_URL`을 통해 연결합니다. 없는 경우 두 서비스 모두 Postgres 전용 경로로 정상 저하됩니다. 아래 **Redis (선택적 캐시)** 섹션을 참조하세요. --- @@ -47,77 +47,76 @@ echo $AGENTEYE_TOKEN | docker login ghcr.io -u x --password-stdin docker pull ghcr.io/agenteye-enterprise/server:beta-latest ``` -> 현재 빌드는 `beta-latest`로 게시됩니다. `latest`는 안정적인 릴리스에만 할당됩니다. 프로덕션 환경에서는 특정 `:v` 태그를 고정하세요. [사용 가능한 이미지 태그](#available-image-tags)를 참조하세요. +> 현재 빌드는 `beta-latest`로 게시됩니다. `latest`는 안정 릴리스에만 할당됩니다. 프로덕션에서는 특정 `:v` 태그를 지정하세요. [사용 가능한 이미지 태그](#available-image-tags)를 참조하세요. ### 환경 변수 | 변수 | 필수 여부 | 기본값 | 설명 | |---|---|---|---| -| `DATABASE_URL` | 예 | 없음 | Postgres DSN. `postgres://` 스키마를 사용하는 표준 libpq 연결 문자열 형식. `?sslmode=require` 및 기타 libpq 파라미터를 지원합니다. 비밀번호에 `/`, `+`, `=` 문자가 포함되면 안 됩니다. URL 안전 비밀번호 생성에는 `openssl rand -hex`를 사용하세요. | -| `ADMIN_KEY` | 아니오 | 없음 | 부트스트랩 관리자 API 키. 매 시작 시 모든 권한으로 업서트됩니다. 값을 변경하고 재시작하여 순환할 수 있습니다. | +| `DATABASE_URL` | 예 | 없음 | Postgres DSN. `postgres://` 스킴을 사용하는 표준 libpq 연결 문자열 형식. `?sslmode=require` 및 기타 libpq 파라미터를 지원합니다. 비밀번호에 `/`, `+`, `=`가 포함되면 안 됩니다. URL 안전 비밀번호 생성에는 `openssl rand -hex`를 사용하세요. | +| `ADMIN_KEY` | 아니오 | 없음 | 부트스트랩 관리자 API 키. 시작 시마다 모든 권한으로 upsert됩니다. 값을 변경하고 재시작하면 교체됩니다. | | `LISTEN_ADDR` | 아니오 | `0.0.0.0:8080` | 바인딩할 TCP 주소 | | `MAX_BODY_BYTES` | 아니오 | `134217728` (128 MB) | 최대 요청 본문 크기 | -| `ADMIN_EMAIL` | 아니오 | 없음 | 부트스트랩 관리자 사용자 이메일. 매 시작 시 모든 권한으로 업서트되며 보호됨으로 표시됩니다. 대시보드/API를 통해 비활성화하거나 권한을 수정할 수 없습니다. 부트스트랩 관리자를 순환하려면 `ADMIN_EMAIL`을 변경하고 재시작하세요. 새 이메일이 보호됨으로 업서트되고, 이전 이메일은 데이터베이스에서 수동으로 초기화할 때까지 보호 상태를 유지합니다. | -| `ALLOWED_EMAILS` | 아니오 | 없음 (모두 차단됨) | 사용자 생성 및 로그인에 허용된 이메일의 쉼표로 구분된 목록. 정확한 주소(`user@example.com`)와 도메인 와일드카드(`*@example.com`)를 지원합니다. 설정하지 않으면 사용자를 생성하거나 로그인할 수 없습니다. **첫 번째 부트 시드 전용**: 첫 번째 부팅 시 기본 조직의 허용 목록을 시드합니다. 이후에는 각 조직의 [`//settings`](#operational-settings) 페이지가 진실의 원천이 되며, 이 환경 변수를 변경해도 효력이 없습니다. | -| `SMTP_HOST` | 아니오 | 없음 | OTP 이메일 발송을 위한 SMTP 서버 호스트명. 설정하지 않으면 OTP 코드가 stdout에 기록됩니다. | +| `ADMIN_EMAIL` | 아니오 | 없음 | 부트스트랩 관리자 사용자 이메일. 시작 시마다 모든 권한으로 upsert되며 보호됨으로 표시됩니다: 대시보드/API를 통해 비활성화하거나 권한을 수정할 수 없습니다. 부트스트랩 관리자를 교체하려면 `ADMIN_EMAIL`을 변경하고 재시작하세요. 새 이메일이 보호됨으로 upsert되며, 이전 이메일은 데이터베이스에서 수동으로 초기화할 때까지 보호 상태를 유지합니다. | +| `ALLOWED_EMAILS` | 아니오 | 없음 (모두 차단) | 사용자 생성 및 로그인이 허용된 이메일의 쉼표 구분 목록. 정확한 주소(`user@example.com`)와 도메인 와일드카드(`*@example.com`)를 지원합니다. 설정하지 않으면 사용자를 생성하거나 로그인할 수 없습니다. **첫 번째 부팅 시드 전용**: 첫 번째 부팅 시 기본 org의 허용 목록을 시드합니다. 이후에는 각 org의 [`//settings`](#operational-settings) 페이지가 신뢰 소스가 되며, 이 환경 변수를 변경해도 효과가 없습니다. | +| `SMTP_HOST` | 아니오 | 없음 | OTP 이메일 발송을 위한 SMTP 서버 호스트명. 설정하지 않으면 OTP 코드가 stdout에 로깅됩니다. | | `SMTP_PORT` | 아니오 | `587` | SMTP 서버 포트 | | `SMTP_USERNAME` | 아니오 | 없음 | SMTP 인증 사용자명 | | `SMTP_PASSWORD` | 아니오 | 없음 | SMTP 인증 비밀번호 | | `SMTP_FROM` | 아니오 | 없음 | OTP 이메일의 발신자 이메일 주소 | -| `SMTP_TLS` | 아니오 | STARTTLS | 명시적으로 비활성화하지 않는 한 STARTTLS가 사용됩니다. `false` 또는 `0`은 일반 텍스트(TLS 없음)로 전송하고, 설정하지 않은 경우를 포함한 다른 모든 값은 STARTTLS를 활성화합니다. | -| `DASHBOARD_URL` | 아니오 | 내장 기본값 | OTP 이메일 매직 링크 및 알림의 인시던트 매직 링크 생성에 사용되는 대시보드 원본 주소. 설정하지 않으면 내장 기본값으로 폴백하고(OTP의 경우에만 대시보드에서 파생된 요청 원본 주소로 먼저 폴백). 이메일과 Slack/인시던트 링크가 모두 대시보드를 가리키도록 도메인이 분리된 설정에서는 이 값을 설정하세요. 아래의 **이메일 매직 링크 URL** 참조. 대부분의 운영자는 설정할 필요가 없습니다. | -| `SESSION_TTL_SECS` | 아니오 | `86400` (24시간) | 대시보드 세션 지속 시간(초). **첫 번째 부트 시드 전용**: 첫 번째 배포 후 [`//settings`](#operational-settings)에서 조직별로 수정하세요. | -| `OTP_TTL_SECS` | 아니오 | `600` (10분) | OTP 코드 유효 기간(초). **첫 번째 부트 시드 전용**: 첫 번째 배포 후 [`//settings`](#operational-settings)에서 조직별로 수정하세요. | -| `REDIS_URL` | 아니오 | 없음 | 선택 사항인 공유 캐시 + 속도 제한 백엔드. 예: `redis://redis:6379/0`. 설정하면 서버는 인증된 API 키 조회, 대시보드의 `/models` 집계, 세션 목록, env 목록 패싯을 캐시합니다. 또한 OTP 요청 속도 제한을 Postgres COUNT에서 Redis INCR로 전환합니다. 설정하지 않거나 연결할 수 없으면 캐시 없이 실행됩니다(OTP 제한은 Postgres로 폴백, 나머지 캐시 호출은 진실의 원천으로 폴스루). 아래의 **Redis(선택 사항 캐시)** 참조. | -| `CLICKHOUSE_URL` | **예** | 없음 | ClickHouse 인스턴스의 기본 URL. 예: `http://clickhouse:8123`. 서버는 매 시작 시 이 데이터베이스에 이벤트 스키마를 적용하며, ClickHouse에 연결할 수 없으면 부팅을 거부합니다. 아래의 **ClickHouse(필수 분석 저장소)** 참조. | +| `SMTP_TLS` | 아니오 | STARTTLS | 명시적으로 비활성화하지 않으면 STARTTLS가 사용됩니다: `false` 또는 `0`은 평문(TLS 없음)으로 전송하며, 설정하지 않은 경우를 포함한 다른 모든 값은 STARTTLS를 활성화합니다. | +| `DASHBOARD_URL` | 아니오 | 내장 기본값 | OTP 이메일 매직 링크와 알림의 인시던트 매직 링크를 구성하는 데 사용되는 대시보드 오리진. 설정하지 않으면 내장 기본값으로 폴백하며(OTP만의 경우 대시보드에서 파생된 요청 오리진으로 먼저 폴백). 이메일과 Slack/인시던트 링크가 모두 대시보드를 가리키도록 스플릿 도메인 설정에서 사용하세요. 아래 **이메일 매직 링크 URL** 참조. 대부분의 운영자는 설정할 필요가 없습니다. | +| `SESSION_TTL_SECS` | 아니오 | `86400` (24시간) | 대시보드 세션 유지 시간(초). **첫 번째 부팅 시드 전용**: 첫 번째 배포 후 [`//settings`](#operational-settings)에서 org별로 편집하세요. | +| `OTP_TTL_SECS` | 아니오 | `600` (10분) | OTP 코드 유효 기간(초). **첫 번째 부팅 시드 전용**: 첫 번째 배포 후 [`//settings`](#operational-settings)에서 org별로 편집하세요. | +| `REDIS_URL` | 아니오 | 없음 | 선택적 공유 캐시 + 속도 제한 백엔드, 예: `redis://redis:6379/0`. 설정 시 서버는 인증된 API 키 조회, 대시보드의 `/models` 집계, 세션 목록, env 목록 패싯을 캐시합니다. OTP 요청 속도 제한도 Postgres COUNT 대신 Redis INCR로 이동합니다. 설정하지 않거나 연결할 수 없는 경우 서버는 캐시 없이 실행됩니다(OTP 제한은 Postgres로 폴백하고 다른 모든 캐시 호출은 신뢰 소스로 폴백). 아래 **Redis (선택적 캐시)** 참조. | +| `CLICKHOUSE_URL` | **예** | 없음 | ClickHouse 인스턴스의 기본 URL, 예: `http://clickhouse:8123`. 서버는 시작 시 이 데이터베이스에 이벤트 스키마를 적용하며, ClickHouse에 연결할 수 없으면 부팅을 거부합니다. 아래 **ClickHouse (필수 분석 저장소)** 참조. | | `CLICKHOUSE_DATABASE` | 아니오 | `agenteye` | ClickHouse 데이터베이스(스키마) 이름. 존재하지 않으면 서버가 시작 시 생성합니다. | -| `ORG_CH_SECRET` | 아니오(단일 테넌트) / **예(멀티 조직)** | 개발 기본값 | 각 조직의 테넌트별 ClickHouse 비밀번호를 파생하는 데 사용되는 HMAC 키. SQL 편집기와 AI 에이전트의 `run_query`는 조직 자체의 읽기 전용 ClickHouse 사용자로 실행되며, 해당 사용자의 행 정책이 엔진에서 테넌트 격리를 강제합니다. 단일 테넌트 배포는 내장된 개발 기본값으로 정상 부팅됩니다. **두 번째 조직을 프로비저닝하기 전에 강력하고 안정적인 값을 반드시 설정하세요.** `agenteye-orgctl org create` CLI는 내장된 개발 기본값에서 실행을 거부합니다. 값을 변경하면 다음 시작 시 부트 타임 조정이 자동으로 복구할 때까지 모든 조직의 ClickHouse 사용자가 고아 상태가 됩니다. 복제본 전체에서 비밀로 유지하고 변경하지 마세요. 조직 프로비저닝 자체는 운영자 전용입니다. 아래의 **조직(멀티테넌시)** 참조. | -| `DEFAULT_ORG_NAME` | 아니오 | `Default` | 내장된 기본 조직에 시드되는 표시 이름. **첫 번째 부트 시드 전용**이며, 조직이 아직 새로 마이그레이션된 일반 ID를 유지하는 동안에만 시작 시 적용된 후 무시됩니다. 조직 이름을 변경(`agenteye-orgctl org rename`)하면 해당 이름이 신뢰할 수 있는 값이 되고 이 환경 변수는 더 이상 효력이 없습니다. | -| `DEFAULT_ORG_SLUG` | 아니오 | `default` | 내장된 기본 조직의 URL 슬러그. 대시보드에서 조직이 위치하는 경로(`//…`). `DEFAULT_ORG_NAME`과 동일한 첫 번째 부트 전용/초기 상태 전용 시맨틱을 갖습니다. 1-40자의 소문자 영숫자로 구성되며 단일 내부 하이픈이 허용되고 [예약어](#organizations-multi-tenancy)는 사용할 수 없습니다. 잘못된 값은 무시됩니다(조직은 `default`를 유지). 단일 테넌트 설치를 배포 후 CLI 단계 없이 `/default` 대신 `/acme`와 같은 경로로 표시할 수 있습니다. | -| `RUST_LOG` | 아니오 | `info` | 로그 상세도 (`debug`, `warn`, `error`, `agenteye_server=trace`) | -| `EVALUATOR_ENDPOINT` | 아니오 | 없음 | 평가자 서비스의 기본 URL (예: `http://evaluator:9000`). 설정하지 않으면 전체 평가 파이프라인이 no-op이 됩니다. 큐 행이 기록되지 않고 워커도 실행되지 않습니다. [평가 스위트](/ko/agenteye/evaluation-suite) 참조. | -| `EVALUATOR_TOKEN` | 아니오 | 없음 | 평가자에게 `Authorization: Bearer `으로 전송됩니다. **평가자 서비스에 구성된 값과 동일해야 합니다.** 평가자가 토큰 없이 구성된 경우에만 선택 사항입니다. | +| `ORG_CH_SECRET` | 아니오(단일 테넌트) / **예(멀티 org)** | 개발 기본값 | 각 조직의 테넌트별 ClickHouse 비밀번호를 파생하는 HMAC 키. SQL 에디터 및 AI 에이전트의 `run_query`는 org의 자체 읽기 전용 ClickHouse 사용자로 실행되며, 행 정책이 엔진에서 테넌트 격리를 적용합니다. 단일 테넌트 배포는 내장 개발 기본값으로도 정상 부팅됩니다. **두 번째 org를 프로비저닝하기 전에 강력하고 안정적인 값을 반드시 설정해야 합니다.** `agenteye-orgctl org create` CLI는 내장 개발 기본값에서 실행을 거부합니다. 교체하면 다음 시작 시 부팅 시간 조정이 자동으로 복구할 때까지 모든 org의 ClickHouse 사용자가 고아 상태가 됩니다. 복제본 전체에서 비밀로 유지하고 변경하지 마세요. org 프로비저닝 자체는 운영자 전용입니다. 아래 **Organizations (멀티테넌시)** 참조. | +| `DEFAULT_ORG_NAME` | 아니오 | `Default` | 내장 기본 org에 시드되는 표시 이름. **첫 번째 부팅 시드 전용**, org가 새로 마이그레이션된 일반 ID를 유지하는 동안에만 시작 시 적용되고 이후 무시됩니다. org를 이름 변경(`agenteye-orgctl org rename`)하면 그것이 권위 있는 값이 되고 이 환경 변수는 효과가 없습니다. | +| `DEFAULT_ORG_SLUG` | 아니오 | `default` | 내장 기본 org의 URL 슬러그, 대시보드 경로(`//…`). `DEFAULT_ORG_NAME`과 동일한 첫 번째 부팅 전용/초기 상태 전용 의미론. 단일 내부 하이픈을 포함한 1-40자의 소문자 영숫자여야 하며 [예약어](#organizations-multi-tenancy)가 아니어야 합니다. 잘못된 값은 무시됩니다(org는 `default`를 유지). 사후 배포 CLI 단계 없이 단일 테넌트 설치가 `/default` 대신 `/acme`로 표시되도록 할 수 있습니다. | +| `RUST_LOG` | 아니오 | `info` | 로그 상세도(`debug`, `warn`, `error`, `agenteye_server=trace`) | +| `EVALUATOR_ENDPOINT` | 아니오 | 없음 | 평가자 서비스의 기본 URL(예: `http://evaluator:9000`). 설정하지 않으면 전체 평가 파이프라인이 no-op가 됩니다. 큐 행이 기록되지 않고 워커가 실행되지 않습니다. [평가 스위트](/ko/agenteye/evaluation-suite) 참조. | +| `EVALUATOR_TOKEN` | 아니오 | 없음 | 평가자에게 `Authorization: Bearer `으로 전송됩니다. **평가자 서비스에 구성된 것과 동일한 값이어야 합니다.** 평가자가 토큰 없이 구성된 경우에만 선택 사항입니다. | | `EVALUATOR_WORKERS` | 아니오 | `2` | 동시성: 평가를 디스패치하는 서버 인스턴스당 워커 태스크 수. 수평 확장된 여러 서버에서 안전하게 실행할 수 있습니다. | -| `EVALUATOR_CLAIM_BATCH` | 아니오 | `4` | 단일 워커가 틱당 클레임하는 최대 평가 수. 배치는 **동시에** 디스패치되므로 평가자 엔드포인트의 총 동시성은 `EVALUATOR_WORKERS × EVALUATOR_CLAIM_BATCH`입니다. | -| `EVALUATOR_POLL_IDLE_SECS` | 아니오 | `2` | 디스패치할 항목이 없을 때 워커가 디스패치 시도 사이에 대기하는 시간. | -| `EVALUATOR_POLLING_INTERVAL_SECS` | 아니오 | `10` | 평가자가 응답별 `next_poll_secs`를 반환하지 않고 `GET /config`에서 `default_poll_interval_secs`를 광고하지 않는 경우 `GET /evaluate/{id}` 폴링의 최종 폴백 주기(초). | -| `EVALUATOR_REQUEST_TIMEOUT_MS` | 아니오 | `30000` | 평가자에 대한 HTTP 요청당 타임아웃(밀리초). | -| `EVALUATOR_MAX_ATTEMPTS` | 아니오 | `5` | 이 횟수만큼 실패하면 평가가 최종 `error`(실패가 요청 타임아웃이었던 경우 `timeout`)로 기록됩니다. | -| `EVALUATOR_CONFIG_REFRESH_SECS` | 아니오 | `300` (5분) | 서버가 평가자로부터 `GET /config`를 다시 가져오는 주기. | -| `EVALUATOR_MAX_POLL_DURATION_SECS` | 아니오 | `3600` (1시간) | AgentEye가 세션을 `timeout`으로 종료하기 전까지 폴링 큐에 남아 있을 수 있는 최대 실시간 시간. 영원히 `pending`을 반환하는 평가자로부터 보호합니다. | +| `EVALUATOR_CLAIM_BATCH` | 아니오 | `4` | 단일 워커가 틱당 요청하는 최대 평가 수. 배치는 **동시에** 디스패치되므로 평가자 엔드포인트의 총 동시성은 `EVALUATOR_WORKERS × EVALUATOR_CLAIM_BATCH`입니다. | +| `EVALUATOR_POLL_IDLE_SECS` | 아니오 | `2` | 대기 중인 항목이 없을 때 워커가 디스패치 시도 사이에 대기하는 시간. | +| `EVALUATOR_POLLING_INTERVAL_SECS` | 아니오 | `10` | 평가자가 응답별 `next_poll_secs`를 반환하지 않고 `GET /config`에서 `default_poll_interval_secs`도 알리지 않을 때 `GET /evaluate/{id}` 폴링의 최종 폴백 주기(초). | +| `EVALUATOR_REQUEST_TIMEOUT_MS` | 아니오 | `30000` | 평가자에 대한 HTTP 요청별 타임아웃(밀리초). | +| `EVALUATOR_MAX_ATTEMPTS` | 아니오 | `5` | 이 횟수만큼 실패하면 평가가 최종 `error`(또는 실패가 요청 타임아웃이었으면 `timeout`)로 기록됩니다. | +| `EVALUATOR_CONFIG_REFRESH_SECS` | 아니오 | `300` (5분) | 서버가 평가자에서 `GET /config`를 다시 가져오는 빈도. | +| `EVALUATOR_MAX_POLL_DURATION_SECS` | 아니오 | `3600` (1시간) | 세션이 폴링 큐에 남아 있을 수 있는 최대 벽시계 시간. 이를 초과하면 AgentEye가 `timeout`으로 종료합니다. 평가자가 영구적으로 `pending`을 반환하는 경우를 방지합니다. | | `ALERT_WORKERS` | 아니오 | `1` | 동시성: 알림 규칙을 평가하는 서버 인스턴스당 워커 태스크 수. [알림](/ko/agenteye/alerts) 참조. | -| `ALERT_CLAIM_BATCH` | 아니오 | `16` | 단일 워커가 틱당 클레임하는 최대 알림 수. | +| `ALERT_CLAIM_BATCH` | 아니오 | `16` | 단일 워커가 틱당 요청하는 최대 알림 수. | | `ALERT_POLL_IDLE_SECS` | 아니오 | `5` | 큐가 비었을 때 알림 워커가 대기하는 시간. | -| `ALERT_REQUEST_TIMEOUT_MS` | 아니오 | `15000` | 트리거 평가당 타임아웃(ClickHouse 쿼리 + 아웃바운드 채널 HTTP). | -| `ALERT_MAX_ATTEMPTS` | 아니오 | `5` | 지수 백오프 대신 정상 주기로 알림을 재예약하기 전 연속 일시적 실패 횟수. | +| `ALERT_REQUEST_TIMEOUT_MS` | 아니오 | `15000` | 트리거 평가별 타임아웃(ClickHouse 쿼리 + 아웃바운드 채널 HTTP). | +| `ALERT_MAX_ATTEMPTS` | 아니오 | `5` | 지수 백오프 대신 정상 주기로 알림이 재예약되기 전 연속 일시적 실패 횟수. | | `AUDIT_WORKERS` | 아니오 | `1` | 동시성: 감사를 실행하는 서버 인스턴스당 워커 태스크 수. [감사](/ko/agenteye/audits) 참조. | -| `AUDIT_CLAIM_BATCH` | 아니오 | `1` | 단일 워커가 틱당 클레임하는 최대 예정된 감사 수. 에이전트 조사는 긴 루프이므로 기본값은 1입니다. | +| `AUDIT_CLAIM_BATCH` | 아니오 | `1` | 단일 워커가 틱당 요청하는 최대 감사 수. 에이전틱 조사는 하나의 긴 루프이므로 기본값은 1입니다. | | `AUDIT_POLL_IDLE_SECS` | 아니오 | `30` | 예정된 감사가 없을 때 감사 워커가 대기하는 시간. | -| `AUDIT_REQUEST_TIMEOUT_MS` | 아니오 | `30000` | ClickHouse에 대한 정책 쿼리당 타임아웃(밀리초). | -| `AUDIT_LLM_TIMEOUT_MS` | 아니오 | `1440000` | AI 어시스턴트 서비스에 대한 에이전트 조사 호출의 타임아웃. 전체 에이전트 루프는 몇 분간 실행됩니다. 에이전트가 서버가 포기하기 전에 부분 결과를 반환할 수 있도록 에이전트 자체의 `AGENTEYE_AUDIT_TIMEOUT_MS`보다 높게 유지하세요. | -| `AUDIT_MAX_ATTEMPTS` | 아니오 | `5` | 지수 백오프 대신 정상 주기로 감사를 재예약하기 전 연속 일시적 실패 횟수. | -| `AGENTEYE_AGENT_URL` / `AGENTEYE_AGENT_TOKEN` | 아니오 | — | 감사의 에이전트 조사는 어시스턴트와 동일한 연결을 재사용하여 AI 어시스턴트 `agent` 서비스를 호출합니다. 따라서 이 두 값을 **서버**에도 설정하세요(번들 매니페스트/컴포즈에서 이미 설정됨). 둘 다 설정되면 감사에서 AI 조사가 실행됩니다. 하나라도 설정되지 않으면 감사는 **정책 전용**으로 실행됩니다(결정론적 SQL 정책 패스는 여전히 실행됨). 감사별 `llm_enabled` 플래그와 무관합니다. 에이전트에도 LLM이 구성되어 있어야 합니다. [assistant.md](/ko/agenteye/assistant) 참조. | +| `AUDIT_REQUEST_TIMEOUT_MS` | 아니오 | `30000` | ClickHouse에 대한 정책 쿼리별 타임아웃(밀리초). | +| `AUDIT_LLM_TIMEOUT_MS` | 아니오 | `1440000` | AI 어시스턴트 서비스에 대한 에이전틱 조사 호출 타임아웃. 전체 에이전트 루프는 수 분 동안 실행됩니다. 에이전트가 부분 결과를 반환하기 전에 서버가 포기하지 않도록 에이전트 자체의 `AGENTEYE_AUDIT_TIMEOUT_MS`보다 높게 유지하세요. | +| `AUDIT_MAX_ATTEMPTS` | 아니오 | `5` | 지수 백오프 대신 정상 주기로 감사가 재예약되기 전 연속 일시적 실패 횟수. | +| `AGENTEYE_AGENT_URL` / `AGENTEYE_AGENT_TOKEN` | 아니오 | — | 감사의 에이전틱 조사는 어시스턴트와 동일한 연결을 재사용하는 AI 어시스턴트 `agent` 서비스를 호출합니다. 따라서 이 두 값을 **서버**에도 설정해야 합니다(번들 매니페스트/컴포즈가 이를 수행). 둘 다 설정 → 감사가 AI 조사를 실행합니다. 둘 중 하나라도 설정되지 않으면 → per-audit `llm_enabled` 플래그와 관계없이 감사가 **정책 전용**(결정론적 SQL 정책 패스만 실행)으로 실행됩니다. 에이전트에도 LLM이 구성되어 있어야 합니다 — [assistant.md](/ko/agenteye/assistant) 참조. | -**AI 어시스턴트 서비스 — 감사 + 샌드박스 설정.** 에이전트 조사와 인팟 Python 샌드박스는 **에이전트 서비스**(서버가 아님)에서 `AGENTEYE_AUDIT_*` 접두사로 모두 선택 사항으로 조정됩니다: +**AI 어시스턴트 서비스 — 감사 + 샌드박스 설정.** 에이전틱 조사 및 파드 내 Python 샌드박스는 서버가 아닌 **에이전트 서비스**에서 `AGENTEYE_AUDIT_*` 접두사로 튜닝되며, 모두 선택 사항입니다: | 변수 | 기본값 | 의미 | |---|---|---| | `AGENTEYE_AUDIT_MAX_STEPS` | `200` | 조사당 최대 에이전트 턴 수. | -| `AGENTEYE_AUDIT_TIMEOUT_MS` | `1200000` | 하나의 조사에 대한 실시간 타임아웃(20분). 서버의 `AUDIT_LLM_TIMEOUT_MS`보다 **낮게** 유지해야 합니다. | -| `AGENTEYE_AUDIT_MAX_CONCURRENCY` | `1` | 에이전트 파드당 동시 조사 수(채팅 어시스턴트 예산과 별개). | -| `AGENTEYE_AUDIT_SANDBOX_TIMEOUT_MS` / `_MEM_MB` / `_CPU_SECS` / `_OUTPUT_MAX_BYTES` / `_SCRIPT_MAX_BYTES` | `20000` / `768` / `10` / `64000` / `64000` | bubblewrap 샌드박스에 대한 스크립트당 제한. | +| `AGENTEYE_AUDIT_TIMEOUT_MS` | `1200000` | 단일 조사의 벽시계 시간(20분). 서버의 `AUDIT_LLM_TIMEOUT_MS`보다 **낮게** 유지해야 합니다. | +| `AGENTEYE_AUDIT_MAX_CONCURRENCY` | `1` | 에이전트 파드당 동시 조사 수(채팅 어시스턴트 예산과 별도). | +| `AGENTEYE_AUDIT_SANDBOX_TIMEOUT_MS` / `_MEM_MB` / `_CPU_SECS` / `_OUTPUT_MAX_BYTES` / `_SCRIPT_MAX_BYTES` | `20000` / `768` / `10` / `64000` / `64000` | bubblewrap 샌드박스의 스크립트별 제한. | -**샌드박스 플랫폼 요구사항.** 감사 코드 샌드박스는 bubblewrap 감옥 내에서 모델의 Python을 실행하며, **비권한 사용자 네임스페이스**가 필요합니다. 에이전트 파드는 `clone()` 플래그를 허용해야 합니다. k8s에서는 `seccompProfile: Unconfined`를, 컴포즈에서는 `security_opt: [seccomp:unconfined]`를 에이전트에 설정하세요. 노드 커널이 비권한 사용자 네임스페이스를 비활성화한 경우(예: 일부 GKE COS 이미지), 샌드박스 **프리플라이트가 실패하고 감사자는 자동으로 SQL 전용으로 저하됩니다.** 오류 없이 에이전트의 `/health`에 `sandbox_available: false`만 표시됩니다. +**샌드박스 플랫폼 요구사항.** 감사 코드 샌드박스는 모델의 Python을 bubblewrap 감옥 내에서 실행하며, 이는 **비권한 사용자 네임스페이스**가 필요합니다. 에이전트 파드는 `clone()` 플래그를 허용해야 합니다 — 에이전트에 `seccompProfile: Unconfined`(k8s) 또는 `security_opt: [seccomp:unconfined]`(컴포즈)를 설정하세요. 노드 커널이 비권한 사용자 네임스페이스를 비활성화하는 경우(예: 일부 GKE COS 이미지), 샌드박스 **사전 점검이 실패하고 감사기는 SQL 전용으로 자동 저하**됩니다 — 오류 없이 에이전트의 `/health`에 `sandbox_available: false`만 표시됩니다. ### 실행 -환경에서 `DATABASE_URL`과 `CLICKHOUSE_URL`을 설정하고(서버는 ClickHouse 없이 부팅을 거부합니다), 컨테이너로 전달하세요: +환경에 `DATABASE_URL`을 설정한 다음 컨테이너에 전달하세요: ```bash docker run -d --restart unless-stopped \ --name agenteye-server \ -e DATABASE_URL="$DATABASE_URL" \ - -e CLICKHOUSE_URL="$CLICKHOUSE_URL" \ -e ADMIN_KEY="$ADMIN_KEY" \ -p 8080:8080 \ ghcr.io/agenteye-enterprise/server:beta-latest @@ -128,30 +127,30 @@ docker run -d --restart unless-stopped \ ### 헬스 체크 ``` -GET /health # liveness - always {"status":"ok"} once the process is up -GET /ready # readiness - 200 when Postgres + ClickHouse are reachable, else 503 +GET /health # 활성 상태 - 프로세스가 시작되면 항상 {"status":"ok"} +GET /ready # 준비 상태 - Postgres + ClickHouse에 연결 가능하면 200, 아니면 503 ``` -인증이 필요하지 않습니다. **활성(liveness)** 프로브에는 `/health`를, **준비(readiness)**/로드 밸런서 프로브에는 `/ready`를 사용하세요. `/ready`는 서버가 서비스를 제공할 수 없는 하드 의존성(Postgres + ClickHouse)을 확인합니다. 따라서 실행 중이지만 데이터베이스에 연결할 수 없는 서버는 로테이션에서 제외되고 `NotReady`로 표시됩니다. Redis는 보고되지만 준비 상태 실패를 유발하지 않습니다. 번들로 제공되는 Kubernetes 매니페스트에서 준비 프로브는 이미 `/ready`를 가리키고 활성 프로브는 `/health`를 유지합니다. Slack으로의 선택적 Kubernetes 네이티브 파드 실패 알림을 포함한 전체 내용은 [enterprise-docs/health-monitoring.md](/ko/agenteye/health-monitoring)를 참조하세요. +인증이 필요하지 않습니다. **활성 상태** 프로브에는 `/health`를, **준비 상태**/로드밸런서 프로브에는 `/ready`를 사용하세요. `/ready`는 서버가 제공할 수 없는 필수 종속성(Postgres + ClickHouse)을 확인합니다. 따라서 실행 중이지만 데이터베이스에 연결할 수 없는 서버는 순환에서 제외되고 `NotReady`로 표시됩니다. Redis는 보고되지만 준비 상태를 실패시키지 않습니다. 번들 Kubernetes 매니페스트에서 준비 상태 프로브는 이미 `/ready`를 가리키고 활성 상태는 `/health`를 유지합니다. 전체 내용(Slack에 대한 옵트인 Kubernetes 네이티브 파드 장애 알림 포함)은 [enterprise-docs/health-monitoring.md](/ko/agenteye/health-monitoring)를 참조하세요. ### 이메일 매직 링크 URL -OTP 로그인 이메일에는 **대시보드 열기** 원탭 버튼이 포함됩니다. 클릭하면 사용자가 `/login?token=&email=
`로 이동하고, 대시보드는 해당 쌍을 세션으로 교환하여 앱으로 리디렉션합니다. 코드를 수동으로 다시 입력할 필요가 없습니다. 서버는 링크 생성에 사용되는 대시보드 원본 주소를 세 단계로 결정합니다: +OTP 로그인 이메일에는 원탭 **대시보드 열기** 버튼이 포함됩니다. 클릭하면 `/login?token=&email=
`로 이동합니다. 대시보드는 해당 쌍을 세션으로 교환하고 앱으로 리디렉션하며, 수동 코드 재입력이 필요하지 않습니다. 서버는 링크를 구성하는 데 사용할 대시보드 오리진을 세 단계로 확인합니다: -1. **`X-AgentEye-Dashboard-Url` 헤더**: 대시보드의 `/api/auth/otp/request` 프록시가 자체 공개 원본 주소에서 자동으로 설정합니다. 동일 원본 배포(서버와 대시보드가 프록시 헤더를 전달하는 하나의 인그레스 뒤에 있는 경우)에서는 **구성이 필요하지 않습니다.** -2. **`DASHBOARD_URL` 환경 변수**: 대시보드가 서버의 OTP 요청 엔드포인트가 보는 원본 주소와 다른 원본 주소(`api.example.com` / `app.example.com` 분리)로 접근 가능한 경우, 또는 인그레스가 공개 호스트를 대시보드 파드로 전파하지 않는 경우(`request.nextUrl.origin`이 `0.0.0.0:3000`과 같은 와일드카드 바인드로 해석될 수 있음) 이 값을 설정하세요. 예: `DASHBOARD_URL=https://app.example.com`. -3. **기본값**: `https://app.befailproof.ai`. 위의 두 가지가 모두 없는 경우에만 사용됩니다. +1. **`X-AgentEye-Dashboard-Url` 헤더**: 대시보드의 `/api/auth/otp/request` 프록시가 자체 공개 오리진에서 자동으로 설정합니다. 동일 오리진 배포(서버와 대시보드가 프록시 헤더를 전달하는 하나의 인그레스 뒤에서 호스트를 공유)에서는 **구성이 필요하지 않습니다**. +2. **`DASHBOARD_URL` 환경 변수**: 대시보드가 서버의 OTP 요청 엔드포인트가 보는 것과 다른 오리진에서 접근 가능하거나(`api.example.com` / `app.example.com` 분리), 인그레스가 공개 호스트를 대시보드 파드에 전파하지 않는 경우(그러면 `request.nextUrl.origin`이 `0.0.0.0:3000`과 같은 와일드카드 바인드로 확인됨) 설정하세요. 예: `DASHBOARD_URL=https://app.example.com`. +3. **기본값**: 위 두 가지가 모두 없을 때만 사용되는 `https://app.befailproof.ai`. -헤더 값이 검증됩니다. `https://*` 및 루프백(`http://localhost*`, `http://127.0.0.1*`) 원본만 허용되며, `https://` 스키마가 있더라도 와일드카드 바인드 주소(`0.0.0.0`, `[::]`)는 거부됩니다. 그 외의 경우는 2단계로 폴스루됩니다. +헤더 값의 유효성이 검사됩니다: `https://*` 및 루프백(`http://localhost*`, `http://127.0.0.1*`) 오리진만 허용되며, 와일드카드 바인드 주소(`0.0.0.0`, `[::]`)는 `https://` 스킴이 있어도 거부됩니다. 그 외의 것은 2단계로 폴백됩니다. -파일이나 kustomize 재빌드 없이 원라이너로 실행 중인 클러스터에 설정하세요: +한 줄 명령으로 실행 중인 클러스터에 설정합니다. 파일이나 kustomize 재빌드가 필요하지 않습니다: ```bash kubectl set env deployment/server -n agenteye \ DASHBOARD_URL=https://app.example.com ``` -이렇게 하면 롤아웃이 트리거되고 새 파드가 첫 번째 요청 시 값을 가져옵니다. 재정의는 Deployment에만 존재합니다. 이후에 오버레이에 대해 `kustomize build | kubectl apply`를 실행하면 동일한 환경 변수를 오버레이의 `server-env.yaml` 패치에 추가하지 않는 한 이 값이 지워집니다. +이는 롤아웃을 트리거합니다. 새 파드는 첫 번째 요청 시 값을 가져옵니다. 재정의는 Deployment에만 존재한다는 점에 유의하세요. 오버레이에 대해 `kustomize build | kubectl apply`를 이후에 실행하면 오버레이의 `server-env.yaml` 패치에 동일한 환경 변수를 추가하지 않는 한 제거됩니다. --- @@ -167,14 +166,14 @@ docker pull ghcr.io/agenteye-enterprise/dashboard:beta-latest | 변수 | 필수 여부 | 기본값 | 설명 | |---|---|---|---| -| `AGENTEYE_SERVER_URL` | 예 | 없음 | 서버의 기본 URL. 예: `http://localhost:8080` | -| `AGENTEYE_API_KEY` | 예 | 없음 | 대시보드가 서버 인증에 사용하는 API 키. 모든 권한이 필요합니다(관리자 키 권장). | -| `AE_LOG_LEVEL` | 아니오 | `info` | 서버 측 로그 상세도: `debug`, `info`, `warn`, `error`. 문제 진단 시 업스트림 요청/응답 줄과 세션 검증 추적을 보려면 `debug`로 설정하세요. | -| `AE_LOG_JSON` | 아니오 | 자동 | `1`은 JSON 한 줄 출력 강제, `0`은 사람이 읽을 수 있는 출력 강제. 설정하지 않으면 `NODE_ENV=production`인 경우 JSON이 자동으로 활성화됩니다. 프로덕션에서는 `jq`나 로그 집계기로 깔끔하게 파싱할 수 있도록 JSON을 권장합니다. | -| `AE_ANALYTICS_DISABLED` | 아니오 | 없음 | `1`/`true`로 설정하면 대시보드의 익명 제품 사용 텔레메트리가 비활성화됩니다. 아래의 [텔레메트리 및 개인정보 보호](#telemetry--privacy) 참조. | -| `REDIS_URL` | 아니오 | 없음 | 선택 사항인 공유 캐시 백엔드. 예: `redis://redis:6379/0`. 설정하면 대시보드는 복제본 전체에서 `validateSession()` 결과를 캐시하고 지연 집계/env 목록 프록시 라우트에 대한 Next.js 페치 캐시를 공유합니다. 엣지 OTP 요청 및 검증 속도 제한도 Redis가 있으면 Redis를 사용합니다(Redis에 연결할 수 없으면 열린 상태로 실패하며, 서버 측 제한이 보안 백스톱). 아래의 **Redis(선택 사항 캐시)** 참조. | -| `AGENTEYE_AGENT_URL` | 아니오 | 없음 | 선택 사항인 AI 어시스턴트 `agent` 서비스의 기본 URL. 예: `http://agent:9100`. **어시스턴트를 완전히 숨기려면 설정하지 마세요.** 대시보드에 어시스턴트 버블이 나타나지 않습니다. [enterprise-docs/assistant.md](/ko/agenteye/assistant) 참조. | -| `AGENTEYE_AGENT_TOKEN` | 아니오 | 없음 | 대시보드가 `agent` 서비스에 제공하는 공유 비밀. 에이전트에 구성된 `AGENTEYE_AGENT_TOKEN`과 일치해야 합니다. [enterprise-docs/assistant.md](/ko/agenteye/assistant) 참조. | +| `AGENTEYE_SERVER_URL` | 예 | 없음 | 서버의 기본 URL, 예: `http://localhost:8080` | +| `AGENTEYE_API_KEY` | 예 | 없음 | 대시보드가 서버에 인증하는 데 사용하는 API 키. 모든 권한이 필요합니다(관리자 키 권장). | +| `AE_LOG_LEVEL` | 아니오 | `info` | 서버 측 로그 상세도: `debug`, `info`, `warn`, `error`. 문제 진단 시 업스트림 요청/응답 라인과 세션 유효성 검사 추적을 보려면 `debug`로 설정하세요. | +| `AE_LOG_JSON` | 아니오 | 자동 | `1`은 JSON 라인별 출력을 강제합니다. `0`은 사람이 읽을 수 있는 출력을 강제합니다. 설정하지 않으면 `NODE_ENV=production`일 때 JSON이 자동으로 활성화됩니다. 프로덕션에서는 `jq` 또는 로그 집계기로 깔끔하게 파싱할 수 있도록 JSON을 권장합니다. | +| `AE_ANALYTICS_DISABLED` | 아니오 | 없음 | `1`/`true`로 설정하면 대시보드의 익명 제품 사용 텔레메트리가 비활성화됩니다. 아래 [텔레메트리 및 개인정보](#telemetry--privacy)를 참조하세요. | +| `REDIS_URL` | 아니오 | 없음 | 선택적 공유 캐시 백엔드, 예: `redis://redis:6379/0`. 설정 시 대시보드는 복제본 간에 `validateSession()` 결과를 캐시하고 레이턴시 집계/env 목록 프록시 경로에 대한 Next.js 페치 캐시를 공유합니다. 엣지 측 OTP 요청 및 확인 속도 제한도 Redis가 있으면 사용합니다(Redis에 연결할 수 없으면 열린 상태로 폴백하며, 서버 측 제한이 보안 백스톱). 아래 **Redis (선택적 캐시)** 참조. | +| `AGENTEYE_AGENT_URL` | 아니오 | 없음 | 선택적 AI 어시스턴트 `agent` 서비스의 기본 URL, 예: `http://agent:9100`. **설정하지 않으면 어시스턴트가 완전히 숨겨집니다**: 대시보드에 어시스턴트 버블이 표시되지 않습니다. [enterprise-docs/assistant.md](/ko/agenteye/assistant) 참조. | +| `AGENTEYE_AGENT_TOKEN` | 아니오 | 없음 | 대시보드가 `agent` 서비스에 제시하는 공유 시크릿. 에이전트에 구성된 `AGENTEYE_AGENT_TOKEN`과 일치해야 합니다. [enterprise-docs/assistant.md](/ko/agenteye/assistant) 참조. | ### 실행 @@ -187,91 +186,91 @@ docker run -d --restart unless-stopped \ ghcr.io/agenteye-enterprise/dashboard:beta-latest ``` -### 텔레메트리 및 개인정보 보호 +### 텔레메트리 및 개인정보 -대시보드는 **익명 제품 사용 분석**을 Exosphere의 분석 서비스(PostHog)로 전송합니다. 대시보드 페이지 조회 및 API 키 생성이나 세션 재평가 같은 일부 UI 액션이 포함됩니다. 이 사용 신호는 기능 우선순위 결정에 활용됩니다. +대시보드는 **익명 제품 사용 분석**을 Exosphere의 분석 서비스(PostHog)로 전송합니다: 어떤 대시보드 페이지가 조회되었는지, API 키 생성 또는 세션 재평가와 같은 몇 가지 UI 동작이 포함됩니다. 이 사용 신호는 어떤 기능을 우선순위에 둘지 결정하는 데 활용됩니다. -- **에이전트, 세션 또는 이벤트 데이터는 인프라 외부로 전혀 전송되지 않습니다.** 오직 대시보드 UI 사용만 보고됩니다. 페이지 URL은 전송 전에 식별자가 제거되며, 운영자는 이메일이 아닌 불투명한 내부 ID로만 식별됩니다. +- **에이전트, 세션 또는 이벤트 데이터는 절대 인프라 밖으로 나가지 않습니다.** 대시보드 UI 사용만 보고됩니다. 페이지 URL은 전송 전에 식별자가 제거되며, 운영자는 이메일이 아닌 불투명한 내부 ID로만 식별됩니다. - 텔레메트리는 **기본적으로 활성화**되어 있습니다. 완전히 끄려면 대시보드 컨테이너에 `AE_ANALYTICS_DISABLED=1`을 설정하고 재시작하세요. -- 분석은 대시보드의 `/ingest` 경로로 전송되며, 대시보드는 이를 PostHog(`https://us.i.posthog.com`)로 역방향 프록시합니다. 요청을 퍼스트 파티로 유지하면 브라우저 광고 차단기가 차단하지 않습니다. **대시보드 컨테이너**는 PostHog에 대한 아웃바운드 액세스가 필요합니다. 차단된 경우 텔레메트리는 조용히 아무것도 하지 않고 대시보드는 영향을 받지 않습니다. +- 분석은 대시보드의 `/ingest` 경로로 전송되며, 대시보드가 이를 PostHog(`https://us.i.posthog.com`)로 역방향 프록시합니다. 요청을 퍼스트파티로 유지하면 브라우저 광고 차단기가 이를 차단하지 않습니다. **대시보드 컨테이너**는 PostHog로 아웃바운드 접근이 필요합니다. 차단된 경우 텔레메트리는 조용히 동작하지 않으며 대시보드는 영향을 받지 않습니다. --- ## AI 어시스턴트 (선택 사항) -인대시보드 AI 어시스턴트를 통해 팀원들이 자연어로 에이전트 데이터에 대한 질문을 할 수 있습니다(세션 요약, `/queries` 편집기를 위한 SQL 초안 작성, 저장된 쿼리를 대시보드 타일로 변환). 대시보드를 떠날 필요가 없으며, 대시보드만 접근할 수 있는 별도의 내부 `agent` 컨테이너(Claude Agents SDK 기반)로 실행됩니다. **LLM 엔드포인트를 구성할 때까지 비활성화 상태입니다.** +대시보드 내 AI 어시스턴트를 통해 팀이 에이전트 데이터를 자연어로 질의할 수 있습니다(세션 요약, `/queries` 에디터용 SQL 초안 작성, 저장된 쿼리를 대시보드 타일로 변환). 대시보드를 벗어날 필요가 없습니다. Claude Agents SDK 기반의 별도 내부 `agent` 컨테이너로 실행되며 대시보드만 접근할 수 있고, **LLM 엔드포인트를 구성할 때까지 비활성화 상태**로 유지됩니다. -활성화하려면 `agent` 서비스에 LLM 연결(**Portkey**: `PORTKEY_API_KEY` + 모델 카탈로그 슬러그 `AGENTEYE_AGENT_MODEL=@/`, 직접 Anthropic: `ANTHROPIC_API_KEY`, 다른 게이트웨이: `ANTHROPIC_BASE_URL`, 또는 Bedrock/Vertex), **전용** 데이터 키, 그리고 대시보드와 일치하는 공유 `AGENTEYE_AGENT_TOKEN`을 설정합니다. 대시보드 사용자는 추가로 `agent:use` 권한이 필요합니다. +활성화하려면 `agent` 서비스에 LLM 연결(**Portkey** via `PORTKEY_API_KEY` + 모델 카탈로그 슬러그 `AGENTEYE_AGENT_MODEL=@/`, 직접 Anthropic via `ANTHROPIC_API_KEY`, 다른 게이트웨이 via `ANTHROPIC_BASE_URL`, 또는 Bedrock/Vertex), **전용** 데이터 키, 대시보드와 일치하는 공유 `AGENTEYE_AGENT_TOKEN`을 설정합니다. 대시보드 사용자에게는 추가로 `agent:use` 권한이 필요합니다. -어시스턴트의 데이터 키는 직접 생성하지 않아도 됩니다. 랜덤 비밀을 선택하여 `agent`에 `AGENTEYE_API_KEY`로 설정하고 `server`에 `AGENT_API_KEY`로 설정하면, 서버가 시작 시 고정된 권한 세트로 시드합니다. 데이터 접근은 읽기 전용(`events:read`, `evaluations:read`, `dashboards:read`, `queries:read`)이며, 승인 게이트 작성 범위(`dashboards:write`, `queries:write`, `queries:run`)도 보유하여 사용자를 대신해 저장된 쿼리를 초안 작성/검증하고 대시보드 타일을 빌드할 수 있습니다. 모든 SQL은 여전히 조직의 읽기 전용 ClickHouse 역할을 통해 실행되므로, 어시스턴트가 작성할 수 있는 범위가 넓어지는 것이지 접근할 수 있는 데이터 범위가 넓어지는 것은 아닙니다. 범위는 코드에서 고정되며 구성으로 확장할 수 없습니다. 해당 키는 보호됩니다. API를 통해 비활성화하거나 재생성할 수 없으며, 값을 변경하고 재시작하여 순환할 수 있습니다. 이 용도로 관리자/대시보드 키를 재사용하지 마세요. +어시스턴트의 데이터 키는 직접 생성할 필요가 없습니다: 임의의 시크릿을 선택하여 `agent`에는 `AGENTEYE_API_KEY`로, `server`에는 `AGENT_API_KEY`로 설정하면 서버가 시작 시 고정된 권한 세트로 시드합니다. 데이터 접근은 읽기 전용(`events:read`, `evaluations:read`, `dashboards:read`, `queries:read`)이며, 승인이 필요한 저작 범위(`dashboards:write`, `queries:write`, `queries:run`)도 보유하여 사용자를 대신해 저장된 쿼리를 초안 작성, 검증하고 대시보드 타일을 구축할 수 있습니다. 모든 SQL은 여전히 org의 읽기 전용 ClickHouse 역할을 통해 실행되므로, 어시스턴트가 저작할 수 있는 내용이 넓어지는 것이지 접근 가능한 데이터가 넓어지는 것이 아닙니다. 범위는 코드에 고정되어 있으며 구성으로 넓힐 수 없습니다. 해당 키는 보호되어 있으며 API를 통해 비활성화하거나 재생성할 수 없고, 값을 변경하고 재시작하는 방식으로만 교체할 수 있습니다. 어시스턴트에 관리자/대시보드 키를 재사용하지 마세요. -전체 설정, 완전한 환경 변수 레퍼런스, 텔레메트리 옵션, 보안 모델은 **[enterprise-docs/assistant.md](/ko/agenteye/assistant)**에 있습니다. +전체 설정, 완전한 환경 변수 참조, 텔레메트리 옵션, 보안 모델은 **[enterprise-docs/assistant.md](/ko/agenteye/assistant)**에 있습니다. --- ## ClickHouse (필수 분석 저장소) -ClickHouse는 높은 이벤트 볼륨에서도 대시보드의 응답성을 유지하고, `/queries` SQL 편집기가 이벤트, 평가, 세션을 단일 저장소에서 조인할 수 있게 합니다. 수집된 모든 이벤트, 모든 최종 평가 결과, 파생된 세션별 집계에 대한 필수 정규 저장소입니다. PostgreSQL은 관계형/가변 상태 테이블(api_keys, users, otp_codes, evaluation_jobs, dashboards, saved_queries)을 보유하고, 분석 표면은 ClickHouse에 있어 대시보드 롤업과 사용자 정의 SQL 쿼리가 크로스 데이터베이스 라운드트립 없이 기본적으로 스캔하고 조인할 수 있습니다. 서버는 `CLICKHOUSE_URL` 없이 부팅을 거부합니다. +ClickHouse는 높은 이벤트 볼륨에서도 대시보드를 빠르게 유지하며, `/queries` SQL 에디터가 단일 저장소에서 이벤트, 평가, 세션을 조인할 수 있게 합니다. 수집된 모든 이벤트, 모든 최종 평가 결과, 파생된 세션별 집계의 필수 표준 저장소입니다. PostgreSQL은 관계형/가변 상태 테이블(api_keys, users, otp_codes, evaluation_jobs, dashboards, saved_queries)을 보유하며, 분석 표면은 ClickHouse에 있어 대시보드 롤업과 자체 SQL 쿼리가 크로스 데이터베이스 왕복 없이 네이티브로 스캔하고 조인할 수 있습니다. 서버는 `CLICKHOUSE_URL` 없이 부팅을 거부합니다. ### 스키마 -서버 시작 시 세 개의 ClickHouse 객체가 생성됩니다. 모두 멱등적입니다(`CREATE IF NOT EXISTS`): +서버 시작 시 세 가지 ClickHouse 객체가 생성됩니다. 모두 멱등적입니다(`CREATE IF NOT EXISTS`): -- **`agenteye.events`**: `ReplacingMergeTree(ingested_at)`, `toYYYYMM(ts)`로 파티션됨, `(session_id, ts, dedup_key)` 순서로 정렬됨. 중복 삽입(콜렉터 재시도)은 병합 시 단일 행으로 합쳐집니다. 서버는 모든 이벤트에 대해 결정론적 SHA-256 `dedup_key`를 계산하므로 재시도가 안전합니다. -- **`agenteye.evaluations`**: `ReplacingMergeTree(ingested_at)`, `toYYYYMM(finished_at)`로 파티션됨, `(session_id, finished_at, dedup_key)` 순서로 정렬됨. 평가자 파이프라인이 최종 평가 결과마다 한 번 기록합니다. `events`와 동일한 dedup 키 모델. -- **`agenteye.agent_sessions`**: 물리적 테이블이 아닌 `agenteye.events`에 대한 **뷰**. 모든 컬럼이 파생됩니다(`started_at = min(ts)`, `last_event_at = max(ts)`, `ended_at = max(if event_type='agent_end', ts, NULL)`, `event_count = count()` 등). 이벤트별 업서트나 별도 백필 없이 뷰가 `events`의 내용을 자동으로 반영합니다. +- **`agenteye.events`**: `ReplacingMergeTree(ingested_at)`, `toYYYYMM(ts)`로 파티션됨, `(session_id, ts, dedup_key)` 순 정렬. 중복 삽입(수집기 재시도)은 병합 시 단일 행으로 축소됩니다. 서버는 모든 이벤트에 대해 결정론적 SHA-256 `dedup_key`를 계산하므로 재시도가 안전합니다. +- **`agenteye.evaluations`**: `ReplacingMergeTree(ingested_at)`, `toYYYYMM(finished_at)`로 파티션됨, `(session_id, finished_at, dedup_key)` 순 정렬. 평가자 파이프라인이 최종 평가 결과당 한 번 씁니다. `events`와 동일한 중복 제거 키 모델. +- **`agenteye.agent_sessions`**: 물리적 테이블이 아닌 `agenteye.events`에 대한 **VIEW**. 모든 컬럼이 파생됩니다(`started_at = min(ts)`, `last_event_at = max(ts)`, `ended_at = max(if event_type='agent_end', ts, NULL)`, `event_count = count()` 등). 이벤트별 upsert와 별도 백필이 없으며, 뷰는 `events`에 있는 내용을 자동으로 반영합니다. -`analytics.evaluations` / `analytics.sessions`를 참조하는 저장된 쿼리와의 하위 호환성을 위해 서버는 `agenteye.*` 테이블 위에 뷰가 있는 `analytics` ClickHouse 데이터베이스도 생성합니다. `analytics.events`, `analytics.evaluations`, `analytics.agent_sessions`, `analytics.sessions`가 모두 올바르게 해석됩니다. +`analytics.evaluations` / `analytics.sessions`를 참조하는 저장된 쿼리와의 하위 호환성을 위해 서버는 `agenteye.*` 테이블에 대한 뷰가 포함된 `analytics` ClickHouse 데이터베이스도 생성합니다. `analytics.events`, `analytics.evaluations`, `analytics.agent_sessions`, `analytics.sessions`가 모두 올바르게 확인됩니다. ### 구성 -번들로 제공되는 docker-compose와 `deploy/base/clickhouse/`는 AgentEye의 워크로드에 최적화된 ClickHouse 서비스를 포함합니다: +번들 docker-compose 및 `deploy/base/clickhouse/`는 AgentEye 워크로드에 맞게 최적화된 ClickHouse 서비스를 제공합니다: -- 번들 기본 오버레이에서 2 GiB 요청 / 4 GiB 제한 메모리(작은 POC/스테이징 노드에 맞게 조정됨). 프로덕션 고객은 오버레이를 조정해야 합니다. 권장 최솟값은 2c / 4Gi 요청, 6c / 8Gi 제한입니다. `max_server_memory_usage_to_ram_ratio=0.9` +- 제공된 기본 오버레이에서 요청 2 GiB / 제한 4 GiB 메모리(소규모 POC/스테이징 노드에 맞게 크기 조정). 프로덕션 고객은 오버레이를 업사이징해야 합니다. 권장 최소 사양은 요청 2c/4Gi, 제한 6c/8Gi입니다. `max_server_memory_usage_to_ram_ratio=0.9` - 5 GiB 마크 캐시 + 8 GiB 비압축 캐시 - `background_pool_size=16`, `background_merges_mutations_concurrency_ratio=2` - MergeTree: `parts_to_throw_insert=3000`, `parts_to_delay_insert=1500`, `non_replicated_deduplication_window=1000` - `local_io_method=auto` (지원되는 커널에서 io_uring) -- `fsync_metadata=0`: 최소 1회 이상 수집 + ReplacingMergeTree dedup으로 인해 허용됨 -- `query_log`은 30일 TTL로 활성화됨. `query_thread_log`는 제거됨(높은 QPS에서 비용이 많이 듦) +- `fsync_metadata=0`: 최소 한 번 수집 + ReplacingMergeTree 중복 제거 때문에 허용됨 +- 30일 TTL을 가진 `query_log` 활성화. 높은 QPS에서 비용이 많이 드는 `query_thread_log` 제거됨 - 사용자 측 쿼리에 대한 `max_execution_time=30` -- StatefulSet 템플릿에 100 GiB PVC(고객 오버레이는 프로덕션에서 빠른 SSD 스토리지 클래스로 재정의해야 함) +- StatefulSet 템플릿에 100 GiB PVC (고객 오버레이는 프로덕션을 위해 빠른 SSD 스토리지 클래스로 재정의해야 함) ### 백업 -전체 데이터셋은 매일 밤 단일 복원 가능한 아카이브로 캡처되므로, 클러스터 또는 스토리지 손실 시 복구할 수 있습니다. ClickHouse는 일일 `agenteye-backup` CronJob에 의해 자동으로 백업됩니다. 이 CronJob은 PostgreSQL과 ClickHouse를 한 번에 덤프합니다. ClickHouse는 HTTP API를 통해 읽힙니다. `agenteye.events`와 `agenteye.evaluations`는 ClickHouse 네이티브 형식으로 덤프되며(뷰와 행 정책은 서버 시작 시 재생성되므로 테이블 데이터가 완전한 그림), Postgres 덤프와 함께 단일 압축 아카이브로 번들되어 객체 스토리지에 업로드됩니다. +전체 데이터셋이 매일 밤 단일 복원 가능한 아카이브로 캡처되므로 클러스터 또는 스토리지 손실에서 복구할 수 있습니다. ClickHouse는 일별 `agenteye-backup` CronJob에 의해 자동으로 백업되며, 이는 PostgreSQL과 ClickHouse를 한 번에 덤프합니다. ClickHouse는 HTTP API를 통해 읽힙니다: `agenteye.events`와 `agenteye.evaluations`가 ClickHouse 네이티브 형식으로 덤프되고(뷰와 행 정책은 서버 시작 시 재생성되므로 테이블 데이터가 완전한 그림), Postgres 덤프와 함께 단일 압축 아카이브로 번들되어 객체 스토리지에 업로드됩니다. -대상 버킷과 클라우드 자격 증명은 오버레이별로 구성됩니다. 업로드 구성 및 복원 단계는 [enterprise-docs/kubernetes-deployment.md](/ko/agenteye/kubernetes-deployment)의 **백업** 섹션을 참조하세요. +대상 버킷과 클라우드 자격증명은 오버레이별로 구성됩니다. 업로드 구성 및 복원 단계는 [enterprise-docs/kubernetes-deployment.md](/ko/agenteye/kubernetes-deployment)의 **백업** 섹션을 참조하세요. --- -## Redis (선택 사항 캐시) +## Redis (선택적 캐시) -Redis는 서버와 대시보드가 사용하는 **선택 사항**인 공유 캐시 + 속도 제한 백엔드입니다. Redis를 배포하고 두 서비스 모두에 `REDIS_URL`을 설정하면: +Redis는 서버와 대시보드에서 사용하는 **선택적** 공유 캐시 + 속도 제한 백엔드입니다. Redis를 배포하고 두 서비스 모두에 `REDIS_URL`을 설정하면: - **서버**는 인증된 API 키 조회, `/events/environments` + `/evaluations/environments` 목록, `/events/latency_aggregate` 롤업(대시보드가 폴링하는 가장 무거운 쿼리), `/sessions` 목록을 캐시하고, OTP 요청 속도 제한을 Postgres `COUNT(*)`에서 Redis `INCR + EXPIRE`로 전환합니다. -- **대시보드**는 `validateSession()` 결과를 캐시하여 일반적인 페이지 로드에서 발생하는 10-20개의 인증된 API 호출이 하나의 업스트림 세션 확인을 공유합니다. 또한 대시보드 엣지에서 OTP 요청 및 검증 속도를 제한합니다. +- **대시보드**는 `validateSession()` 결과를 캐시하여 일반적인 페이지 로드에서 발생하는 10-20개의 인증된 API 호출이 모두 하나의 업스트림 세션 확인을 공유합니다. 또한 대시보드 엣지에서 OTP 요청 및 확인을 속도 제한합니다. -**Redis에 연결할 수 없으면 두 서비스 모두 자연스럽게 저하됩니다.** 모든 캐시 호출은 제한된 타임아웃 내에 `Err`를 반환하고, 호출자는 진실의 원천으로 폴백합니다(서버는 Postgres, 대시보드는 업스트림 Rust 서버). OTP 속도 제한은 서버의 Postgres `COUNT(*)` 경로로 폴백합니다(보안 속성 유지). 대시보드의 엣지 OTP 제한은 열린 상태로 실패하지만 서버 측 제한은 여전히 유효합니다. Redis 다운 시 정확성이 아닌 지연 시간이 저하됩니다. +**두 서비스 모두 Redis에 연결할 수 없는 경우 정상적으로 저하됩니다.** 모든 캐시 호출은 제한된 타임아웃 내에 `Err`를 반환하고 호출자는 신뢰 소스(서버의 경우 Postgres, 대시보드의 경우 업스트림 Rust 서버)로 폴백합니다. OTP 속도 제한은 서버의 Postgres `COUNT(*)` 경로로 폴백합니다(보안 속성은 유지됨). 대시보드의 엣지 OTP 제한은 열린 상태로 실패하지만 서버 측 제한은 여전히 유지됩니다. Redis가 다운되면 정확성이 아닌 레이턴시가 저하됩니다. ### 구성 -docker-compose 번들에는 이미 Redis 서비스가 포함되어 있으며, 서버와 대시보드에 `REDIS_URL=redis://redis:6379/0`이 연결됩니다. 외부 Redis를 사용하려면 `REDIS_URL`을 엔드포인트로 설정하고 컴포즈 파일에서 `redis` 서비스를 제거하세요. +docker-compose 번들에는 이미 Redis 서비스가 포함되어 있으며 서버와 대시보드에 `REDIS_URL=redis://redis:6379/0`이 연결되어 있습니다. 외부 Redis를 사용하려면 `REDIS_URL`을 해당 엔드포인트로 설정하고 컴포즈 파일에서 `redis` 서비스를 제거하세요. ### 메모리 + 지속성 -번들 Redis 이미지는 `--appendonly yes --appendfsync everysec --maxmemory 256mb --maxmemory-policy allkeys-lru`로 실행됩니다. AOF 지속성은 컨테이너 재시작 후에도 캐시가 유지됨을 의미합니다. `everysec`는 마지막 1초의 캐시 쓰기 손실이 무해하기 때문에 올바른 내구성/성능 균형입니다. LRU 제거는 메모리 증가를 제한합니다. +번들 Redis 이미지는 `--appendonly yes --appendfsync everysec --maxmemory 256mb --maxmemory-policy allkeys-lru`로 실행됩니다. AOF 지속성은 캐시가 컨테이너 재시작 후에도 살아남도록 합니다. `everysec`은 마지막 1초의 캐시 쓰기 손실이 무해하므로 올바른 내구성/성능 균형입니다. LRU 축출은 메모리 증가를 제한합니다. ### Redis를 배포하지 않아야 할 때 -- 단일 인스턴스 개발/QA 환경. 서버의 인프로세스 캐시만으로도 복제본당 이점의 대부분을 제공합니다. Redis는 단일 인스턴스 설정에서는 필요하지 않은 크로스 복제본 공유를 추가합니다. -- 서비스 하나를 더 운영하는 비용이 지연 시간 이점보다 큰 에어갭 설치 환경. +- 단일 인스턴스 개발/QA 환경. 서버의 인프로세스 캐시만으로도 복제본별 이점의 대부분을 제공합니다. Redis는 단일 인스턴스 설정에서 필요하지 않은 복제본 간 공유를 추가합니다. +- 추가 서비스 운영 비용이 레이턴시 이점보다 큰 에어갭 설치 환경. --- ## Docker Compose (권장) -`docker-compose.yml`은 `agenteye-enterprise/releases` 저장소에서 사용할 수 있습니다. 단일 명령으로 Postgres, ClickHouse, Redis, 서버, 대시보드를 시작합니다. +`docker-compose.yml`은 `agenteye-enterprise/releases` 저장소에서 사용할 수 있습니다. 단일 명령으로 Postgres, 서버, 대시보드를 시작합니다. ```bash GITHUB_TOKEN=$AGENTEYE_TOKEN gh release download \ @@ -323,42 +322,48 @@ docker compose down -v ## 운영 설정 -환경 변수로 고정되던 일부 운영 설정은 이제 대시보드의 **`//settings`** 페이지에서 조직별로 편집할 수 있습니다. 각 조직이 독립적으로 구성합니다. 변경 사항은 재시작이나 재배포 없이 수 초 내에 적용됩니다. +환경 변수로 고정되던 일부 운영 설정은 이제 대시보드의 **`//settings`** 페이지에서 조직별로 편집할 수 있습니다. 각 org가 자체적으로 구성합니다. 변경 사항은 재시작이나 재배포 없이 수 초 내에 적용됩니다. -| 설정 | 부트스트랩 환경 변수 | 제어 대상 | +| 설정 | 부트스트랩 환경 변수 | 제어 내용 | |---|---|---| | 허용된 로그인 | `ALLOWED_EMAILS` | OTP를 받고 사용자로 추가될 수 있는 이메일(또는 `*@domain.com` 와일드카드) | -| 기본 사용자 권한 | `DEFAULT_USER_PERMISSIONS` | 관리자가 **+ new user**를 열 때 미리 선택되는 쉼표로 구분된 권한 토큰. 각 토큰은 [API 키 권한](/ko/agenteye/api-keys)에 나열된 문자열 중 하나여야 합니다. 기본값은 `standard` 프리셋: 읽기 전용 접근 및 일상적인 온콜 액션(재평가 트리거, 쿼리 실행, 인시던트 확인, 어시스턴트 사용). | -| 세션 수명 | `SESSION_TTL_SECS` | 재인증 전까지 대시보드 로그인 유효 기간. 대시보드는 매 5초마다 업스트림 세션을 재확인하므로, `//users`에서의 권한 업데이트는 재로그인 없이 다음 요청에서 해당 사용자에게 적용됩니다. | -| 일회용 코드 수명 | `OTP_TTL_SECS` | OTP/매직 링크 사용 가능 기간 | -| 알림 채널 | `ALERTS_ENABLED_CHANNELS` | 알림 디스패처가 사용할 수 있는 채널 종류의 쉼표 구분 목록: `email`, `slack`, `webhook`. 알림별 구성은 여전히 `//alerts/`에서 작성되지만, 디스패처는 이 세트를 통해 모든 아웃바운드 전달을 필터링합니다. 여기서 비활성화된 채널은 `skipped_disabled` 감사 행으로 단락됩니다. `dashboard` 채널(로컬 감사 삽입)은 항상 허용됩니다. 기본값은 세 채널 모두 활성화. | +| 기본 사용자 권한 | `DEFAULT_USER_PERMISSIONS` | 관리자가 **+ 새 사용자**를 열 때 미리 선택되는 쉼표 구분 권한 토큰. 각 토큰은 [API 키 권한](/ko/agenteye/api-keys)에 나열된 문자열 중 하나여야 합니다. 기본값은 `standard` 프리셋: 읽기 전용 접근 및 일상적인 온콜 작업(재평가 트리거, 쿼리 실행, 인시던트 확인, 어시스턴트 사용). | +| 세션 유효 기간 | `SESSION_TTL_SECS` | 재인증 전 대시보드 로그인 유효 시간. 대시보드는 5초마다 업스트림 세션을 재확인하므로 `//users`에서의 권한 업데이트는 재로그인 없이 다음 요청에서 해당 사용자에게 적용됩니다. | +| 일회용 코드 유효 기간 | `OTP_TTL_SECS` | OTP/매직 링크의 사용 가능 시간 | +| 알림 채널 | `ALERTS_ENABLED_CHANNELS` | 알림 디스패처가 사용할 수 있는 채널 종류의 쉼표 구분 목록: `email`, `slack`, `webhook`. 알림별 구성은 여전히 `//alerts/`에서 작성되지만, 디스패처는 모든 아웃바운드 전달을 이 집합을 통해 필터링합니다. 여기서 비활성화된 채널은 `skipped_disabled` 감사 행으로 단락됩니다. `dashboard` 채널(로컬 감사 삽입)은 항상 허용됩니다. 기본값은 세 가지 모두 활성화입니다. | -### 부트스트랩 동작 방식 +### 부트스트랩 작동 방식 -설정은 `org_settings`에 조직별로 저장됩니다. 첫 번째 부팅 시 서버는 일치하는 환경 변수(또는 환경 변수가 설정되지 않은 경우 적절한 기본값)에서 기본 조직의 누락된 행을 시드합니다. 이후에는 **저장된 값이 진실의 원천이 되며 환경 변수는 무시됩니다.** 이후 재시작 시 환경 변수를 변경해도 라이브 조직의 값에 영향을 주지 않으며, 추가 조직은 기본값에서 시작하여 독립적으로 구성합니다. +설정은 `org_settings`에 조직별로 저장됩니다. 첫 번째 부팅 시 서버는 일치하는 환경 변수(환경 변수가 설정되지 않은 경우 적절한 기본값)에서 기본 org의 누락된 행을 시드합니다. 이후 **저장된 값이 신뢰 소스가 되고 환경 변수는 무시됩니다**. 이후 재시작 시 환경 변수를 변경해도 라이브 org의 값에 영향을 미치지 않으며, 추가 org는 기본값에서 시작하여 자체적으로 구성합니다. -이는 다음을 의미합니다: +즉: -- 새로운 배포에서는 위에 표시된 대로 환경 변수를 설정하면 기본 조직이 첫 번째 부팅 시 읽습니다. -- 나중에 값을 변경하려면 대시보드에 로그인하여 `//settings`에서 편집하세요. 변경 사항은 모든 서버 복제본에 수 초 내에 적용됩니다. 재시작이 필요하지 않습니다. -- 시작 로그 줄에 시드된 항목과 이미 존재하는 항목이 기록되어 부트스트랩이 적용되었는지 확인할 수 있습니다: +- 새 배포의 경우 위에 표시된 대로 환경 변수를 설정하면 기본 org가 첫 번째 부팅 시 읽습니다. +- 나중에 값을 변경하려면 대시보드에 로그인하고 `//settings`에서 편집하세요. 변경 사항은 수 초 내에 모든 서버 복제본에 적용됩니다. 재시작이 필요하지 않습니다. +- 시작 로그 라인에 시드된 것과 이미 존재하는 것이 기록되므로 부트스트랩이 적용되었는지 확인할 수 있습니다: ```text INFO settings bootstrap: seeded default-org row key=allowed_sign_ins env_var=ALLOWED_EMAILS seeded_from_env=true ``` -#### 조직 간 로그인 시맨틱 +#### 조직 간 로그인 의미론 -세션과 OTP는 단일 조직이 아닌 사용자 전체에 적용되므로, 로그인 시 조직별 설정을 조정하는 두 가지 규칙이 있습니다: +세션과 OTP는 단일 org가 아닌 사용자에게 전역적이므로, 로그인 시 org별 설정을 조율하는 두 가지 규칙이 있습니다: -- **세션/OTP 수명**: 사용자가 속한 조직 중 가장 엄격한(가장 짧은) 수명이 적용됩니다. -- **허용된 로그인**: 게이트는 모든 조직의 허용 목록과 조직 멤버십을 OR로 결합합니다. 어떤 조직의 허용 목록이 이메일을 허용하거나 사용자가 이미 어떤 조직의 멤버인 경우 OTP를 요청할 수 있습니다. +- **세션/OTP 유효 기간**: 사용자가 속한 org 중 가장 엄격한(가장 짧은) 유효 기간이 적용됩니다. +- **허용된 로그인**: 게이트는 모든 org의 허용 목록과 org 멤버십을 OR로 결합합니다. 이메일이 어느 org의 허용 목록에든 허용되거나 이미 어느 org의 멤버이면 OTP를 요청할 수 있습니다. ### 권한 -`//settings` 페이지 접근은 두 가지 권한으로 제어됩니다: +`//settings` 페이지 접근은 두 가지 권한으로 제한됩니다: -- `settings:read`: 페이지 및 현재 값 보기. -- `settings:write`: 변경 사항 저장. +- `settings:read`: 페이지와 현재 값을 볼 수 있습니다. +- `settings:write`: 변경 사항을 저장할 수 있습니다. -부트스트랩 관리자(`ADMIN_EMAIL`에서 시드됨)는 다른 모든 권한과 함께 두 권한을 자동으로 받습니다. 필요에 따라 `//users`에서 다른 사 \ No newline at end of file +부트스트랩 관리자 사용자(`ADMIN_EMAIL`에서 시드됨)는 다른 모든 권한과 함께 자동으로 둘 다 부여받습니다. 필요에 따라 `//users`에서 다른 사용자에게 부여하세요. + +--- + +## Organizations (멀티테넌시) + +단일 배포가 여러 격리된 **조직**(테넌트)을 제공할 수 있습니다. 모든 데이터 행은 정확히 하나의 org에 속하며 격리는 데이터베이스 엔진에서 적용됩니다. 단일 테넌트 설치에서는 아무것도 필요하지 않습니다. 모든 데이터는 내장된 `default` org에 있습니다. (첫 \ No newline at end of file diff --git a/docs/ko/agenteye/getting-started.mdx b/docs/ko/agenteye/getting-started.mdx index 94f4c602..b74167f6 100644 --- a/docs/ko/agenteye/getting-started.mdx +++ b/docs/ko/agenteye/getting-started.mdx @@ -4,32 +4,32 @@ description: "AgentEye 시작하기 문서입니다." --- -이 가이드는 AgentEye의 전체 설정 과정을 안내합니다. 서버와 대시보드 배포, 에이전트 머신에 수집기 설치, 그리고 Python 에이전트 코드 계측까지 다룹니다. +이 가이드는 AgentEye의 전체 설정 과정을 안내합니다. 서버와 대시보드 배포, 에이전트 머신에 컬렉터 설치, Python 에이전트 코드 계측까지 단계별로 살펴봅니다. --- -## AgentEye란 무엇인가요? +## AgentEye란? -AgentEye는 **AI 에이전트를 위한 자체 호스팅 관찰 가능성 및 평가 플랫폼**입니다. 에이전트가 수행하는 모든 작업 — 실행의 각 단계 — 을 기록하고, 완료된 각 실행의 품질을 자동으로 평가합니다. 덕분에 프로덕션 환경에서 에이전트의 동작을 파악하고, 사용자보다 먼저 회귀를 발견할 수 있습니다. +AgentEye는 **AI 에이전트를 위한 자체 호스팅 방식의 관측성 및 평가 플랫폼**입니다. 에이전트가 수행하는 모든 작업을 실행 단계별로 기록하고, 완료된 각 실행의 품질을 자동으로 채점합니다. 이를 통해 프로덕션 환경에서 에이전트의 동작을 파악하고, 사용자가 문제를 경험하기 전에 회귀를 미리 감지할 수 있습니다. -데이터는 단방향으로 흐릅니다. 에이전트 코드가 **Python SDK**를 통해 **이벤트**를 내보내면 → 경량 **수집기** 데몬이 이를 일괄 처리해 **서버**로 전송하고 → 이벤트와 분석 데이터는 **ClickHouse**에 저장됩니다(조직, 사용자, API 키, 대시보드, 저장된 쿼리와 같은 운영 상태는 **Postgres**에 보관됩니다) → 모든 데이터를 **대시보드**에서 탐색할 수 있습니다. +데이터는 단방향으로 흐릅니다. 에이전트 코드가 **Python SDK**를 통해 **이벤트**를 발생시키면 → 경량 **컬렉터** 데몬이 이를 배치로 묶어 **서버**로 전송하고 → 이벤트와 분석 데이터는 **ClickHouse**에 저장됩니다(조직, 사용자, API 키, 대시보드, 저장된 쿼리 등 운영 상태는 **Postgres**에 저장) → **대시보드**에서 모든 데이터를 탐색할 수 있습니다. -제공되는 기능: +제공 기능: -- **이벤트** — 모든 에이전트 실행의 단계별 원시 기록 (도구 호출, 모델 호출, 훅, 오류). -- **세션** — 실행 단위로 집계된 이벤트로, 각 실행마다 **자동 평가** 및 점수가 부여됩니다. -- **평가** — 자체 평가자 서비스가 생성하는 품질 점수로, 수동 검토 없이 품질 저하를 감지합니다. -- **쿼리 및 대시보드** — 데이터에 대한 저장된 ClickHouse SQL을 조직 범위의 공유 대시보드로 시각화합니다. -- **알림 및 인시던트** — 임계값 규칙에 따라 알림(이메일, Slack, 웹훅, 대시보드 내)을 발송하고, 인시던트 분류를 위한 워크플로를 제공합니다. -- **CLI 및 AI 어시스턴트** — 터미널 클라이언트(`agenteye`)와 대시보드 내 어시스턴트로 자연어 질의를 지원합니다. +- **이벤트** — 모든 에이전트 실행의 단계별 원시 기록(도구 호출, 모델 호출, 훅, 오류). +- **세션** — 실행당 하나의 행으로 집계된 이벤트. 각 세션은 **자동으로 평가**되고 채점됩니다. +- **평가** — 직접 구성한 평가자 서비스가 생성한 품질 점수로, 수동 검토 없이도 품질 저하를 감지합니다. +- **쿼리 & 대시보드** — 데이터에 대한 저장된 ClickHouse SQL을 공유 가능한 조직 범위의 대시보드로 시각화합니다. +- **알림 & 인시던트** — 임계값 규칙을 기반으로 알림(이메일, Slack, 웹훅, 대시보드 내)을 발송하고, 트리아지를 위한 인시던트 워크플로우를 제공합니다. +- **CLI & AI 어시스턴트** — 터미널 클라이언트(`agenteye`)와 대시보드 내 어시스턴트를 통해 자연어로 질문할 수 있습니다. -모든 구성 요소를 자체 인프라에서 실행할 수 있습니다. 단일 Docker Compose 스택(이 가이드), 프로덕션 Kubernetes 설치, 또는 단일 코로케이션 파드로 구성할 수 있습니다. 이 가이드의 나머지 부분은 Compose 스택을 처음부터 끝까지 설정합니다. +이 모든 것을 자체 인프라에서 단일 Docker Compose 스택(이 가이드 기준), 프로덕션 Kubernetes 설치, 또는 단일 코로케이션 파드로 운영할 수 있습니다. 이 가이드에서는 Compose 스택을 처음부터 끝까지 설정합니다. --- ## 1단계: 인증 -모든 AgentEye 아티팩트는 `agenteye-enterprise` GitHub 조직에서 배포됩니다. 엔터프라이즈 개발자는 자체 GitHub PAT를 생성할 수 있습니다. 정확한 단계와 필요한 권한은 [enterprise-docs/github-token.md](/ko/agenteye/github-token)를 참고하세요. +모든 AgentEye 아티팩트는 `agenteye-enterprise` GitHub 조직에서 배포됩니다. 엔터프라이즈 개발자는 GitHub PAT를 직접 생성할 수 있습니다. 정확한 단계와 필요한 권한은 [enterprise-docs/github-token.md](/ko/agenteye/github-token)를 참고하세요. ```bash export AGENTEYE_TOKEN= @@ -42,9 +42,9 @@ echo $AGENTEYE_TOKEN | docker login ghcr.io -u x --password-stdin ## 2단계: 서버 및 대시보드 배포 -서버는 수집기로부터 이벤트를 수신하여 조회할 수 있게 만들고, 대시보드는 데이터를 탐색하는 공간입니다. 수집된 이벤트와 분석 데이터는 ClickHouse(필수 분석 저장소)에 저장되며, Postgres는 조직, 사용자, API 키, 대시보드, 저장된 쿼리와 같은 운영 상태를 보관합니다. +서버는 컬렉터로부터 이벤트를 수신하고 이를 쿼리 가능하게 만들며, 대시보드는 데이터를 탐색하는 공간입니다. 수집된 이벤트와 분석 데이터는 ClickHouse(필수 분석 저장소)에 저장되고, Postgres는 조직, 사용자, API 키, 대시보드, 저장된 쿼리 등 운영 상태를 보관합니다. -**게시된 compose 파일 다운로드:** +**공개된 Compose 파일 다운로드:** ```bash mkdir -p ./agenteye @@ -57,36 +57,30 @@ cd agenteye **시크릿 설정:** -기본 `admin` 자격 증명으로 배포되지 않도록 `.env` 파일을 생성하세요. 최소한 `ADMIN_KEY`와 `POSTGRES_PASSWORD`를 설정해야 합니다: +기본 `admin` 자격증명으로 배포되지 않도록 `.env` 파일을 생성합니다. 최소한 `ADMIN_KEY`와 `POSTGRES_PASSWORD`를 설정해야 합니다: ```bash POSTGRES_PASSWORD=your-db-password ADMIN_KEY=your-admin-secret ``` -이후 단계(예: 3단계의 `curl`)에서 직접 참조할 수 있도록 현재 셸에도 `ADMIN_KEY`를 내보내세요: - -```bash -export ADMIN_KEY=your-admin-secret -``` - **스택 시작:** ```bash docker compose up -d ``` -이 명령은 필수 ClickHouse 분석 저장소, 선택적 Redis 캐시, 서버, 대시보드를 포함한 전체 스택을 시작합니다. 서버가 시작되려면 ClickHouse가 정상 상태여야 합니다. +이 명령은 필수 ClickHouse 분석 저장소와 선택적 Redis 캐시를 포함한 전체 스택을 서버 및 대시보드와 함께 실행합니다. 서버가 시작되려면 ClickHouse가 정상 상태여야 합니다. -서버는 `http://localhost:8080`에서, 대시보드는 `http://localhost:3000`에서 수신 대기 중입니다. +이제 서버는 `http://localhost:8080`에서, 대시보드는 `http://localhost:3000`에서 수신 대기 중입니다. 프로덕션 배포(커스텀 Postgres, TLS, 리버스 프록시)는 [enterprise-docs/deployment.md](/ko/agenteye/deployment)를 참고하세요. --- -## 3단계: 수집기용 API 키 생성 +## 3단계: 컬렉터용 API 키 생성 -각 수집기는 범위가 지정된 API 키로 인증합니다. 2단계에서 설정한 `ADMIN_KEY`를 사용하여 키를 생성하세요: +각 컬렉터는 범위가 지정된 API 키로 인증합니다. 2단계에서 설정한 `ADMIN_KEY`를 사용해 키를 생성합니다: ```bash curl -s -X POST http://localhost:8080/keys \ @@ -95,13 +89,13 @@ curl -s -X POST http://localhost:8080/keys \ -d '{"name":"prod-collector","key":"your-collector-secret","permissions":["events:add"]}' ``` -`key` 값은 직접 지정하며, 4단계의 수집기 설정에서 사용됩니다. 전체 키 관리 방법은 [enterprise-docs/api-keys.md](/ko/agenteye/api-keys)를 참고하세요. +`key` 값은 직접 지정합니다. 이 값은 4단계의 컬렉터 설정에 사용됩니다. 전체 키 관리 방법은 [enterprise-docs/api-keys.md](/ko/agenteye/api-keys)를 참고하세요. --- -## 4단계: 수집기 설치 +## 4단계: 컬렉터 설치 -AI 에이전트가 실행되는 모든 머신에 수집기 데몬을 설치하세요. +AI 에이전트가 실행되는 모든 머신에 컬렉터 데몬을 설치합니다. **바이너리 다운로드 (Linux x86_64):** @@ -114,7 +108,7 @@ chmod +x agenteye-collector-linux-x86_64 sudo mv agenteye-collector-linux-x86_64 /usr/local/bin/agenteye-collector ``` -> 이 명령은 **Linux x86_64** 빌드를 다운로드합니다. macOS(Apple Silicon 또는 Intel), Linux arm64, Docker / systemd / launchd 설정의 경우 [collector-installation.md](/ko/agenteye/collector-installation)를 참고하세요. 각 플랫폼별 다운로드 링크가 나열되어 있습니다 — 위 명령은 다른 환경에서는 실행되지 않는 Linux 바이너리를 설치합니다. +> 이 명령은 **Linux x86_64** 빌드를 다운로드합니다. macOS(Apple Silicon 또는 Intel), Linux arm64, Docker/systemd/launchd 설정은 [collector-installation.md](/ko/agenteye/collector-installation)를 참고하세요. 각 플랫폼별 다운로드 방법이 나와 있습니다. 위 명령은 Linux 바이너리를 설치하므로 다른 환경에서는 실행되지 않습니다. **설정:** @@ -134,7 +128,7 @@ EOF agenteye-collector start ``` -원샷 플러시로 연결을 확인하세요(대기 중인 이벤트를 모두 처리한 후 종료됩니다): +원샷 플러시로 연결 상태를 확인합니다(대기 중인 이벤트를 모두 전송한 후 종료): ```bash agenteye-collector flush @@ -146,7 +140,7 @@ Docker, systemd, launchd 설정은 [enterprise-docs/collector-installation.md](/ ## 5단계: Python SDK 설치 -에이전트 코드를 계측할 각 머신에 GitHub Releases의 wheel 파일로 SDK를 설치하세요. +에이전트 코드를 계측할 각 머신에 GitHub Releases에서 wheel 파일을 설치합니다. ```bash VERSION=0.0.1b9 @@ -160,7 +154,7 @@ pip install agenteye-${VERSION}-py3-none-any.whl ## 6단계: 에이전트 계측 -에이전트 코드에 이벤트를 추가하세요. 최소한 `agent_start`와 `agent_end`를 내보내야 합니다: +에이전트 코드에 이벤트를 추가합니다. 최소한 `agent_start`와 `agent_end`를 발생시켜야 합니다: ```python import agenteye @@ -180,7 +174,7 @@ agenteye.event.agent_end( ) ``` -이벤트는 버퍼링되어 500ms마다 `$AGENTEYE_HOME/events/`(또는 `AGENTEYE_HOME`이 설정되지 않은 경우 `~/.agenteye/events/`)에 플러시됩니다. 수집기가 자동으로 이를 수집합니다. +이벤트는 버퍼에 쌓이다가 500ms마다 `$AGENTEYE_HOME/events/`(또는 `AGENTEYE_HOME`이 설정되지 않은 경우 `~/.agenteye/events/`)로 플러시됩니다. 컬렉터가 자동으로 이를 수집합니다. 전체 이벤트 API는 [enterprise-docs/python-sdk.md](/ko/agenteye/python-sdk)를 참고하세요. @@ -188,21 +182,21 @@ agenteye.event.agent_end( ## 7단계: 대시보드에서 이벤트 확인 -`http://your-dashboard-host:3000`을 열고 로그인하세요. AgentEye는 일회용 코드(또는 원클릭 매직 링크)를 이메일로 전송하므로 비밀번호를 관리할 필요가 없습니다. +`http://your-dashboard-host:3000`을 열고 로그인합니다. AgentEye는 일회용 코드(또는 원클릭 매직 링크)를 이메일로 발송하므로 비밀번호를 관리할 필요가 없습니다. -![AgentEye 로그인 화면 - 이메일로 일회용 코드를 전송합니다](/agenteye/images/login.png) +![단일 사용 코드를 이메일로 발송하는 AgentEye 로그인 화면](/agenteye/images/login.png) -로그인 후 **Events** 페이지에서 수집된 모든 이벤트의 실시간 추적을 확인할 수 있습니다. `session_id` 또는 `agent_id`로 필터링하여 특정 실행을 자세히 살펴볼 수 있습니다. +로그인 후 **이벤트** 페이지에서 수집된 모든 이벤트의 실시간 내역을 확인할 수 있습니다. `session_id` 또는 `agent_id`로 필터링하여 특정 실행을 상세 조회할 수 있습니다. -![이벤트 유형별로 색상 구분되며 환경, 에이전트, 세션으로 필터링 가능한 실시간 이벤트 스트림](/agenteye/images/events-stream.png) +![이벤트 유형별로 색상이 구분되고 환경, 에이전트, 세션으로 필터링 가능한 실시간 이벤트 스트림](/agenteye/images/events-stream.png) -**Sessions** 페이지는 해당 이벤트를 실행별 한 행으로 집계합니다. AgentEye가 완료된 세션을 자동으로 평가하므로 모든 실행에 점수가 부여되고, 수동 검토 없이 품질 회귀를 발견할 수 있습니다. 최신 평가 점수가 각 행에 한눈에 표시됩니다: +**세션** 페이지는 해당 이벤트들을 실행당 하나의 행으로 집계합니다. AgentEye는 완료된 세션을 자동으로 평가하므로 모든 실행이 채점되고 수동 검토 없이도 품질 회귀를 감지할 수 있습니다. 최신 평가 점수는 각 행에서 한눈에 확인할 수 있습니다: -![세션 목록 - 실행별 한 행, 상태 배지와 평가 점수 뱃지](/agenteye/images/sessions-list.png) +![상태 배지와 평가 점수 배지가 표시된 실행별 세션 목록](/agenteye/images/sessions-list.png) -세션 평가 방식 설정은 [enterprise-docs/evaluation-suite.md](/ko/agenteye/evaluation-suite)를 참고하세요. +세션 채점 방식 설정은 [enterprise-docs/evaluation-suite.md](/ko/agenteye/evaluation-suite)를 참고하세요. -세션을 클릭하면 **실행 그래프**를 열 수 있습니다. git 스타일 뷰로 에이전트, 도구, 훅, 모델 호출이 시간에 따라 어떻게 전개되었는지 보여주며, 병렬 서브 에이전트는 별도 레인에 표시되고 오른쪽 패널에 실행별 세부 정보가 제공됩니다: +세션을 클릭하면 **실행 그래프**가 열립니다. 이는 에이전트, 도구, 훅, 모델 호출이 시간에 따라 어떻게 전개되었는지 git 스타일로 보여주는 뷰로, 병렬 서브 에이전트는 각자의 레인에 표시되고 오른쪽 패널에서 실행별 분석을 확인할 수 있습니다: ![이벤트 타임라인 옆에 표시된 세션의 git 스타일 실행 그래프와 도구/모델/훅 분석 패널](/agenteye/images/session-detail.png) @@ -210,21 +204,21 @@ agenteye.event.agent_end( ## 8단계: 탐색, 시각화, 알림 -이벤트가 유입되기 시작하면 **분석** 페이지에서 원시 활동 데이터를 통찰로 변환할 수 있습니다. 에이전트 동작을 측정하고, 팀과 결과를 공유하며, 문제가 발생하는 즉시 알림을 받을 수 있습니다. 대시보드 페이지는 조직 범위로 지정되므로 주소창에 표시되는 URL에는 조직 슬러그(`//…`)가 접두사로 붙습니다. +이벤트가 흐르기 시작하면 **분석** 페이지에서 원시 활동 데이터를 인사이트로 변환할 수 있습니다. 에이전트 동작을 측정하고, 팀과 결과를 공유하며, 문제가 발생하는 즉시 알림을 받을 수 있습니다. 대시보드 페이지는 조직 범위로 적용되므로 주소 표시줄의 URL은 조직 슬러그(`//…`)로 시작합니다. -- **쿼리** (`//queries`): 이벤트와 평가에 대한 저장된 재사용 가능한 쿼리 라이브러리(기본 제공 프리셋 및 사용자 정의)에서 시작하세요… +- **쿼리** (`//queries`): 이벤트와 평가 데이터에 대한 저장된 재사용 가능한 쿼리 라이브러리(기본 프리셋 및 커스텀 쿼리)에서 시작하세요… -![저장된 쿼리 라이브러리: 기본 제공 프리셋과 사용자 정의 쿼리가 그리드 형식으로 표시됩니다](/agenteye/images/queries.png) +![재사용 가능한 쿼리 그리드로 구성된 저장된 쿼리 라이브러리(기본 프리셋과 커스텀 쿼리 포함)](/agenteye/images/queries.png) - …그런 다음 SQL 작성기에서 열어 수정하고 실시간 결과와 함께 실행하세요: + …그런 다음 SQL 작성기에서 쿼리를 열어 수정하고 실시간 결과와 함께 실행합니다: -![저장된 쿼리를 실행하는 SQL 쿼리 작성기 - 스키마 사이드바와 실시간 결과 그리드](/agenteye/images/query-lab.png) +![스키마 사이드바와 실시간 결과 그리드가 있는 SQL 쿼리 작성기에서 저장된 쿼리 실행 화면](/agenteye/images/query-lab.png) -- **대시보드** (`//dashboards`): 쿼리를 선 그래프, 막대 그래프, 영역 그래프, 파이 차트 타일로 고정하여 조직 전체가 공유하는 대시보드를 만드세요. +- **대시보드** (`//dashboards`): 쿼리를 라인, 바, 에어리어, 파이 타일로 고정하여 조직 전체가 공유하는 대시보드를 구성합니다. -![저장된 쿼리로 구성된 대시보드: 시간당 이벤트 선 그래프, 오류 유형별 막대 그래프, 지연 시간 영역 차트, 모델별 토큰 수](/agenteye/images/dashboard-fleet.png) +![저장된 쿼리로 구성된 대시보드: 시간당 이벤트 라인 차트, 유형별 오류 바 차트, 지연 시간 에어리어 차트, 모델별 토큰 차트](/agenteye/images/dashboard-fleet.png) -- **알림** (`//alerts`): 임계값 조건을 이메일, Slack, 웹훅, 또는 대시보드 내 알림 규칙으로 전환하세요. [enterprise-docs/alerts.md](/ko/agenteye/alerts)를 참고하세요. +- **알림** (`//alerts`): 임계값을 이메일, Slack, 웹훅, 대시보드 내 알림으로 통보하는 규칙으로 전환합니다. [enterprise-docs/alerts.md](/ko/agenteye/alerts)를 참고하세요. --- diff --git a/docs/ko/agenteye/managed-deployment.mdx b/docs/ko/agenteye/managed-deployment.mdx index a08edbbe..bb447f4b 100644 --- a/docs/ko/agenteye/managed-deployment.mdx +++ b/docs/ko/agenteye/managed-deployment.mdx @@ -1,155 +1,156 @@ --- -title: "Kubernetes 클러스터에서의 Managed Deployment" -description: "Kubernetes 클러스터에서의 AgentEye Managed Deployment 문서입니다." +title: "Kubernetes 클러스터에서의 관리형 배포" +description: "Kubernetes 클러스터에서의 AgentEye 관리형 배포 문서입니다." --- -AgentEye는 AI 및 LLM 에이전트를 위한 셀프 호스팅 관찰 가능성(observability) 및 평가 플랫폼입니다. 에이전트 세션, 도구 호출, 모델 요청, 오류를 캡처하여 검색 가능한 분석 및 평가로 변환하고, 선택적 읽기 전용 AI 어시스턴트가 포함된 대시보드에 결과를 표시합니다. -Managed Deployment 모델에서는 전용 Kubernetes 클러스터를 제공하면 Exosphere가 그 안에서 전체 플랫폼을 운영합니다. 모든 컴포넌트의 배포, 구성, 운영, 백업, 업그레이드를 Exosphere가 대신 처리합니다. 팀은 데이터베이스, 인증서, 업그레이드를 직접 관리하지 않고도 플랫폼의 가치(에이전트 가시성, 분석, 평가, 선택적 어시스턴트)를 누릴 수 있습니다. 모든 데이터는 여러분의 클라우드 계정 내에 유지됩니다. +AgentEye는 AI 및 LLM 에이전트를 위한 자체 호스팅 형태의 관측성 및 평가 플랫폼입니다. 에이전트 세션, 도구 호출, 모델 요청, 오류를 수집하여 검색 가능한 분석 및 평가 데이터로 변환하고, 선택적 읽기 전용 AI 어시스턴트를 포함한 대시보드에 결과를 표시합니다. + +관리형 배포 모델에서는 전용 Kubernetes 클러스터를 제공하면 Exosphere가 모든 컴포넌트의 배포, 구성, 운영, 백업, 업그레이드를 대신 처리하며 플랫폼 전체를 그 안에서 운영합니다. 팀은 데이터베이스, 인증서, 업그레이드 등을 직접 관리하지 않고도 플랫폼의 핵심 가치(에이전트 가시성, 분석, 평가, 선택적 어시스턴트)를 누릴 수 있습니다. 모든 데이터는 여러분의 클라우드 계정 안에 머물러 있습니다. --- ## 사전 요구 사항 -- 컨테이너 이미지 가져오기 및 아티팩트 다운로드를 위한 **GitHub PAT** ([GitHub Token 설정](/ko/agenteye/github-token) 참조) -- **전용 Kubernetes 클러스터** (아래 요구 사항 참조) +- 컨테이너 이미지 풀 및 아티팩트 다운로드를 위한 **GitHub PAT** ([enterprise-docs/github-token.md](/ko/agenteye/github-token) 참고) +- **전용 Kubernetes 클러스터** (아래 요구 사항 참고) - 데이터베이스 백업을 위한 **스토리지 버킷** -- **네트워크 연결**: 클러스터 로드 밸런서로의 인바운드 포트 443 +- **네트워크 연결**: 클러스터 로드 밸런서로 인바운드 443 포트 개방 --- ## 1단계: 전용 Kubernetes 클러스터 프로비저닝 -AgentEye 전용 Kubernetes 클러스터를 생성합니다. 다른 워크로드와 공유하지 않아야 하며, 전체 플랫폼(애플리케이션 서비스, 데이터베이스, 분석, 캐싱)이 기존 인프라에 영향을 주지 않고 격리된 환경에서 실행됩니다. +AgentEye 전용 Kubernetes 클러스터를 생성합니다. 다른 워크로드와 공유하지 않아야 플랫폼 전체(애플리케이션 서비스, 데이터베이스, 분석, 캐싱)가 격리된 환경에서 실행되어 기존 인프라에 영향을 주지 않습니다. -| 요구 사항 | 세부 사항 | +| 요구 사항 | 세부 내용 | |---|---| -| **배포판** | EKS, GKE, AKS 또는 자체 관리형 등 표준 규격을 준수하는 Kubernetes | +| **배포판** | EKS, GKE, AKS 또는 자체 관리형 등 표준 Kubernetes | | **버전** | 1.27 이상 | -| **노드 풀** | 최소 사양: **노드 3개, 각 4 vCPU / 8 GB RAM** (표준 범용 인스턴스) | +| **노드 풀** | 최소: **노드 3개, 각 4 vCPU / 8 GB RAM** (범용 표준 인스턴스) | | **스토리지** | 블록 볼륨을 프로비저닝하는 기본 StorageClass (예: AWS의 `gp3`, GCP의 `pd-ssd`) | -| **로드 밸런서** | 클러스터가 클라우드 LoadBalancer 서비스를 프로비저닝할 수 있어야 함 (EKS, GKE, AKS 기본 지원) | +| **로드 밸런서** | 클라우드 LoadBalancer 서비스 프로비저닝 가능해야 함 (EKS, GKE, AKS 기본 지원) | -> Exosphere는 클러스터 내부의 나머지 모든 것을 설치하고 관리합니다: 인그레스 컨트롤러, TLS 인증서, 데이터베이스, 캐싱, 모니터링, 모든 애플리케이션 배포. +> Exosphere는 클러스터 내부의 모든 것을 설치하고 관리합니다: 인그레스 컨트롤러, TLS 인증서, 데이터베이스, 캐싱, 모니터링, 모든 애플리케이션 배포. --- ## 2단계: AgentEye 팀에 접근 권한 부여 -Exosphere는 네임스페이스, 커스텀 리소스 정의, 인그레스 컨트롤러, 스토리지 프로비저너를 관리하기 위해 cluster-admin 권한(또는 동등한 광범위한 RBAC)이 필요합니다. +Exosphere는 네임스페이스, 커스텀 리소스 정의, 인그레스 컨트롤러, 스토리지 프로비저너를 관리하기 위해 cluster-admin 권한(또는 이에 준하는 광범위한 RBAC)이 필요합니다. -| 요구 사항 | 세부 사항 | +| 요구 사항 | 세부 내용 | |---|---| -| **접근 방법** | IAM 역할 (EKS/GKE에 권장), kubeconfig, 또는 SSO 기반 접근 | -| **VPN / 배스천** | Kubernetes API 서버가 비공개인 경우 Exosphere 운영 팀을 위한 VPN 자격 증명 또는 배스천 접근 제공 | +| **접근 방식** | IAM 역할 (EKS/GKE에 권장), kubeconfig, 또는 SSO 기반 접근 | +| **VPN / 배스천** | Kubernetes API 서버가 프라이빗 환경이라면 Exosphere 운영팀에 VPN 자격 증명 또는 배스천 접근 정보 제공 | --- ## 3단계: 네트워크 연결 구성 -네트워크 팀에서 클러스터 로드 밸런서의 **포트 443**으로의 인바운드 트래픽을 허용해야 합니다. 배포는 두 개의 별도 로드 밸런서를 운영합니다: 하나는 이벤트 수집(mTLS 보호)용, 다른 하나는 대시보드용입니다. +네트워크 팀에서 클러스터 로드 밸런서로의 **443 포트** 인바운드 트래픽을 허용해야 합니다. 배포는 두 개의 별도 로드 밸런서를 사용합니다: 하나는 이벤트 수집용(mTLS 보호), 다른 하나는 대시보드용입니다. | 트래픽 | 출발지 | 목적지 | 보안 | |---|---|---|---| -| **이벤트 수집** | 클러스터의 Collector 파드 | Ingest LoadBalancer, 포트 443 | mTLS (클라이언트 인증서) + API 키 | -| **대시보드** | 개발자 브라우저 | Dashboard LoadBalancer, 포트 443 | 도메인의 HTTPS, 패스워드리스 이메일 OTP 로그인 | +| **이벤트 수집** | 클러스터 내 수집기 파드 | 수집 LoadBalancer, 443 포트 | mTLS(클라이언트 인증서) + API 키 | +| **대시보드** | 개발자 브라우저 | 대시보드 LoadBalancer, 443 포트 | 도메인에 HTTPS, 비밀번호 없는 이메일 OTP 로그인 | -수집 엔드포인트는 상호 TLS로 보호됩니다. Collector는 모든 요청 시 유효한 클라이언트 인증서 **및** 유효한 API 키를 함께 제시해야 합니다. 대시보드는 자체 로드 밸런서와 호스트네임에서 실행되며, 허용 목록에 등록된 이메일 주소/도메인만 로그인이 가능합니다. +수집 엔드포인트는 상호 TLS로 보호되며, 수집기는 모든 요청에 유효한 클라이언트 인증서와 유효한 API 키를 **모두** 제시해야 합니다. 대시보드는 자체 로드 밸런서와 호스트명으로 운영되며, 허용 목록에 등록된 이메일 주소/도메인으로 로그인이 제한됩니다. -**DNS 레코드 (최초 1회):** 관리하는 도메인 하위에 두 개의 CNAME 레코드를 생성합니다 — 하나는 수집 엔드포인트용, 하나는 대시보드용 (예: `agenteye.your-company.example`) — Exosphere가 제공하는 로드 밸런서 호스트네임을 가리킵니다. 이후 Exosphere가 두 호스트네임에 대해 공개적으로 신뢰받는 TLS 인증서를 자동으로 프로비저닝하며 갱신도 포함됩니다. +**DNS 레코드 (최초 1회):** 관리하는 도메인 아래에 CNAME 레코드 두 개를 생성합니다. 하나는 수집 엔드포인트용, 다른 하나는 대시보드용(예: `agenteye.your-company.example`)으로, Exosphere가 제공하는 로드 밸런서 호스트명을 가리키면 됩니다. Exosphere가 두 호스트명에 대해 갱신을 포함한 공개 신뢰 TLS 인증서를 자동으로 프로비저닝합니다. -> **포트 80 참고:** 자동 인증서 발급 및 갱신은 각 로드 밸런서의 포트 80을 통한 HTTP로 유효성을 검사합니다. 보안 정책상 대시보드 로드 밸런서를 회사 IP 범위로 제한해야 하는 경우 Exosphere에 먼저 알려주세요 — DNS 기반 인증서 검증 방식으로 전환하여 (고객 측에서 DNS 레코드 하나 추가) 제한된 환경에서도 갱신이 계속 작동하도록 합니다. +> **80 포트 참고:** 자동 인증서 발급 및 갱신은 각 로드 밸런서의 80 포트 HTTP를 통해 검증됩니다. 보안 정책상 대시보드 로드 밸런서를 사내 IP 범위로 제한해야 하는 경우 Exosphere에 먼저 알려 주세요. DNS 기반 인증서 검증 방식(여러분 측에 DNS 레코드 하나 추가)으로 전환하여 제한 환경에서도 갱신이 계속 작동하도록 합니다. -> **아웃바운드:** 클러스터 노드는 `ghcr.io`에서 컨테이너 이미지를 가져오기 위해 인터넷 접근이 필요합니다. 아웃바운드 트래픽을 제한하는 네트워크 환경이라면 `ghcr.io`를 허용 목록에 추가하거나 내부 레지스트리로 이미지를 미러링하세요. +> **아웃바운드:** 클러스터 노드는 `ghcr.io`에서 컨테이너 이미지를 받기 위해 인터넷 접근이 필요합니다. 아웃바운드 트래픽을 제한하는 네트워크 환경이라면 `ghcr.io`를 허용 목록에 추가하거나 내부 레지스트리에 이미지를 미러링하세요. --- ## 4단계: 백업 스토리지 버킷 제공 -데이터베이스 백업은 고객이 소유한 클라우드 스토리지 버킷에 저장됩니다. +데이터베이스 백업은 여러분이 소유한 클라우드 스토리지 버킷에 저장됩니다. -| 요구 사항 | 세부 사항 | +| 요구 사항 | 세부 내용 | |---|---| | **서비스** | S3 (AWS), GCS (GCP), 또는 Azure Blob Storage | -| **접근 권한** | IAM 역할(IRSA on EKS, Workload Identity on GKE)을 통해 클러스터 노드에 쓰기 권한 부여 또는 자격 증명 제공 | -| **보존 기간** | 버킷의 수명 주기 정책(보존 기간, 아카이브 규칙)은 고객이 직접 제어합니다. Exosphere는 백업을 기록하고, 보존 기간은 고객이 결정합니다 | +| **접근** | IRSA (EKS) 또는 Workload Identity (GKE)를 통한 IAM 역할로 클러스터 노드에 쓰기 권한 부여, 또는 자격 증명 직접 제공 | +| **보존 기간** | 버킷의 수명 주기 정책(보존 기간, 아카이브 규칙)은 여러분이 직접 제어합니다. Exosphere는 백업을 기록하며, 보존 기간은 여러분이 결정합니다 | -하루에 한 번 PostgreSQL(관계형 상태)과 ClickHouse(이벤트 및 평가) 모두를 하나의 압축 아카이브로 덤프하여 버킷에 업로드합니다. 업그레이드 전에도 백업이 실행됩니다. +매일 1회 백업이 실행되어 PostgreSQL(관계형 상태)과 ClickHouse(이벤트 및 평가)를 하나의 압축 아카이브로 덤프하여 버킷에 업로드합니다. 백업은 모든 업그레이드 직전에도 실행됩니다. --- ## 5단계: 담당자 지정 -클러스터 수준 문제(노드 상태, 클라우드 계정 한도, 네트워크 변경)에 대응할 담당자 1인 또는 Slack/Teams 채널을 지정해 주세요. 일상적인 운영에서는 이 담당자의 개입이 필요하지 않습니다. +클러스터 수준 문제(노드 상태, 클라우드 계정 한도, 네트워크 변경)에 대응할 담당자 1명 또는 Slack/Teams 채널을 지정해 주세요. 일상적인 운영에서는 해당 담당자가 관여할 필요가 없습니다. --- -## 배포 컴포넌트 +## 배포되는 컴포넌트 Exosphere가 클러스터 접근 권한을 확보하면 다음 컴포넌트들이 배포되고 관리됩니다. | 컴포넌트 | 역할 | |---|---| -| **AgentEye Server** | Collector로부터 이벤트를 수신하고, 분석을 실행하며, 대시보드에 데이터를 제공하는 HTTP API | -| **대시보드** | 에이전트 세션, 도구 호출, 모델 요청, 오류를 조회하는 웹 인터페이스; 선택적 읽기 전용 AI 어시스턴트 호스팅 | +| **AgentEye 서버** | 수집기로부터 이벤트를 받아 분석을 실행하고 대시보드에 데이터를 제공하는 HTTP API | +| **대시보드** | 에이전트 세션, 도구 호출, 모델 요청, 오류를 확인하는 웹 인터페이스; 선택적 읽기 전용 AI 어시스턴트 포함 | | **ClickHouse** | 수집된 이벤트, 분석, 평가를 위한 필수 정규 스토어 | | **PostgreSQL** | 조직, API 키, 사용자, 대시보드, 저장된 쿼리를 위한 관계형 스토어 | -| **Redis** | 선택적 공유 캐시 및 속도 제한 백엔드; 사용 불가 시 플랫폼이 우아하게 성능 저하됨 | -| **AI 어시스턴트 (선택)** | 내부 읽기 전용 어시스턴트 컨테이너; LLM 엔드포인트가 구성될 때까지 비활성 상태 유지 | -| **인그레스 컨트롤러** | 두 개의 로드 밸런서 (mTLS 보호 수집용 및 대시보드용): 공개적으로 신뢰받는 자동 갱신 인증서로 TLS를 종료하고 수집 엔드포인트에 mTLS 적용 | +| **Redis** | 선택적 공유 캐시 및 속도 제한 백엔드; 사용 불가 시 플랫폼이 점진적으로 성능을 낮춰 계속 동작 | +| **AI 어시스턴트 (선택)** | 내부 읽기 전용 어시스턴트 컨테이너; LLM 엔드포인트가 구성될 때까지 비활성화 상태 유지 | +| **인그레스 컨트롤러** | 두 개의 로드 밸런서(mTLS 보호 수집용, 대시보드용)로 TLS를 종료하고 공개 신뢰 자동 갱신 인증서 사용, 수집 엔드포인트에 mTLS 적용 | | **cert-manager** | TLS 인증서 프로비저닝 및 mTLS 클라이언트 인증서 발급 자동화 | -| **인증서 모니터링** | 예약 작업이 인증서 만료를 확인하고 갱신 시점이 다가오면 알림 전송 (예: Slack) | +| **인증서 모니터링** | 주기적으로 인증서 만료를 확인하고 갱신 시점이 가까워지면 알림(예: Slack) 발송 | -Managed 제공에는 에이전트 활동을 평가 기준에 따라 점수화하는 플랫폼의 평가 파이프라인 운영도 포함됩니다. 이 기능들이 제공하는 내용은 [어시스턴트](/ko/agenteye/assistant) 및 [평가 스위트](/ko/agenteye/evaluation-suite)를 참조하세요. +관리형 서비스는 평가 기준에 따라 에이전트 활동을 채점하는 플랫폼의 평가 파이프라인도 운영합니다. 해당 기능에 대한 자세한 내용은 [enterprise-docs/assistant.md](/ko/agenteye/assistant) 및 [enterprise-docs/evaluation-suite.md](/ko/agenteye/evaluation-suite)를 참고하세요. --- -## 제공 항목 +## 제공 내용 -배포 완료 후 다음 항목을 받게 됩니다. +배포가 완료되면 다음 항목이 제공됩니다. -| 항목 | 세부 사항 | +| 항목 | 세부 내용 | |---|---| -| **대시보드 URL** | 고객 도메인 하위의 호스트네임 (예: `https://agenteye.your-company.example`), 공개적으로 신뢰받는 자동 갱신 TLS 인증서로 제공. 제공된 로드 밸런서 호스트네임으로 CNAME 하나를 생성하면 되며, 로그인은 패스워드리스 이메일 OTP 방식 | -| **Collector 엔드포인트** | 수집 호스트네임의 `/events` 경로 (예: `https://ingest.your-company.example/events`), mTLS 보호 | -| **클라이언트 인증서 번들** | 클러스터별: 클라이언트 인증서, 개인 키, CA 인증서가 Kubernetes Secret 매니페스트로 전달됨. 클러스터당 한 번만 적용 | -| **GitHub PAT** | Collector 바이너리 및 Python SDK 패키지 다운로드용 | -| **Collector API 키** | `events:add` 권한을 가진 범위 한정 키, Collector 배포당 하나 | -| **설치 가이드** | Collector 및 Python SDK에 대한 단계별 문서 | +| **대시보드 URL** | 여러분의 도메인 아래 호스트명(예: `https://agenteye.your-company.example`), 공개 신뢰 자동 갱신 TLS 인증서 적용. 제공된 로드 밸런서 호스트명으로 CNAME 하나 생성; 비밀번호 없는 이메일 OTP 로그인 | +| **수집기 엔드포인트** | 수집 호스트명의 `/events` 경로(예: `https://ingest.your-company.example/events`), mTLS 보호 | +| **클라이언트 인증서 번들** | 클러스터별 제공: 클라이언트 인증서, 개인 키, CA 인증서가 Kubernetes Secret 매니페스트로 전달됨. 클러스터당 1회 적용 | +| **GitHub PAT** | 수집기 바이너리 및 Python SDK 패키지 다운로드용 | +| **수집기 API 키** | `events:add` 권한이 부여된 범위 지정 키, 수집기 배포당 1개 | +| **설치 가이드** | 수집기 및 Python SDK에 대한 단계별 문서 | --- -## 설정 이후 할 일 +## 설정 이후 수행 사항 -이후 지속적인 작업은 AgentEye 클러스터가 아닌 고객의 에이전트 머신에서만 이루어집니다. +이후의 지속적인 작업은 AgentEye 클러스터가 아닌 여러분의 에이전트 머신에서만 이루어집니다. -1. AI 에이전트를 실행하는 각 Kubernetes 클러스터에 **Collector를 설치**합니다: 클라이언트 인증서를 마운트하고 엔드포인트 URL 및 API 키를 설정합니다. [Collector 설치](/ko/agenteye/collector-installation)를 참조하세요. -2. 에이전트 코드에 **Python SDK를 통합**합니다. [Python SDK](/ko/agenteye/python-sdk)를 참조하세요. +1. AI 에이전트가 실행되는 각 Kubernetes 클러스터에 **수집기를 설치합니다**: 클라이언트 인증서를 마운트하고 엔드포인트 URL과 API 키를 구성합니다. [enterprise-docs/collector-installation.md](/ko/agenteye/collector-installation) 참고. +2. 에이전트 코드에 **Python SDK를 연동합니다**. [enterprise-docs/python-sdk.md](/ko/agenteye/python-sdk) 참고. 3. 브라우저에서 **대시보드를 열어** 에이전트 활동을 확인합니다. -클러스터 운영, 데이터베이스 관리, 인증서 갱신, 업그레이드 — 모두 필요 없습니다. +클러스터 운영, 데이터베이스 관리, 인증서 갱신, 업그레이드는 모두 필요하지 않습니다. --- ## 보안 -- **데이터는 클라우드 계정 내에 유지됩니다.** 클러스터, 스토리지, 데이터베이스 모두 고객 환경에서 실행됩니다. 데이터가 경계 밖으로 나가지 않습니다. -- **접근 권한은 고객이 제어합니다.** 클러스터는 고객 계정에 있습니다. Exosphere의 접근을 언제든지 감사, 모니터링, 철회할 수 있습니다. 모든 작업은 클라우드 감사 로그(CloudTrail, GCP Audit Logs 등)를 통해 기록됩니다. -- **이벤트 수집에 mTLS 적용.** 모든 Collector 요청은 유효한 클라이언트 인증서와 API 키를 모두 필요로 합니다. 키가 유출되어도 인증서 없이는 무용지물이며, 인증서를 탈취해도 유효한 키 없이는 사용할 수 없습니다. -- **대시보드 접근 제어.** 대시보드는 이벤트 수집과 분리된 자체 로드 밸런서에서 실행되며, 로그인은 허용 목록에 등록된 이메일 주소/도메인으로 제한된 패스워드리스 이메일 OTP 방식입니다. 로드 밸런서의 IP 소스 범위 허용 목록은 요청 시 제공됩니다. 자동 인증서 갱신이 로드 밸런서에 도달해야 하므로 Exosphere는 제한과 함께 DNS 기반 인증서 검증으로 전환하여 제한 환경에서도 갱신이 계속 작동하도록 합니다. -- **클러스터별 인증서.** 각 클러스터는 고유한 클라이언트 인증서를 받습니다. 한 클러스터가 침해되더라도 해당 인증서만 독립적으로 폐기되며 다른 클러스터에는 영향이 없습니다. +- **데이터는 여러분의 클라우드 계정에 머뭅니다.** 클러스터, 스토리지, 데이터베이스 모두 여러분의 환경에서 실행됩니다. 어떠한 데이터도 경계 밖으로 나가지 않습니다. +- **접근 제어는 여러분이 합니다.** 클러스터는 여러분의 계정에 있습니다. Exosphere의 접근 권한을 언제든지 감사, 모니터링, 취소할 수 있습니다. 모든 작업은 클라우드 감사 로그(CloudTrail, GCP Audit Logs 등)에 기록됩니다. +- **이벤트 수집에 mTLS 적용.** 모든 수집기 요청은 유효한 클라이언트 인증서와 API 키를 모두 요구합니다. 키가 유출되어도 인증서 없이는 무용지물이며, 인증서가 탈취되어도 유효한 키 없이는 사용할 수 없습니다. +- **대시보드 접근 제어.** 대시보드는 이벤트 수집과 분리된 자체 로드 밸런서에서 실행되며, 로그인은 허용 목록에 등록된 이메일 주소/도메인으로 제한된 비밀번호 없는 이메일 OTP 방식입니다. 요청 시 로드 밸런서에 IP 소스 범위 허용 목록을 적용할 수 있으며, 자동 인증서 갱신이 로드 밸런서에 도달해야 하므로 Exosphere는 제한과 함께 DNS 기반 인증서 검증을 사용하여 갱신이 계속 작동하도록 합니다. +- **클러스터별 인증서.** 각 클러스터는 고유한 클라이언트 인증서를 받습니다. 특정 클러스터가 침해당하더라도 해당 인증서만 독립적으로 취소되며 다른 클러스터에는 영향을 주지 않습니다. --- ## 배포 일정 -| 단계 | 소요 시간 | 고객 참여 | +| 단계 | 기간 | 고객 참여 사항 | |---|---|---| | **클러스터 프로비저닝** | 1~2일 | 클러스터 프로비저닝 및 Exosphere 접근 권한 부여 | | **플랫폼 설정** | 1일 | 없음; Exosphere가 모든 인프라 컴포넌트 설치 | | **애플리케이션 배포** | 1일 | 없음; Exosphere가 서버, 대시보드 배포 및 API 키 생성 | -| **Collector 롤아웃** | 1~3일 | 클러스터에 Collector 설치 (Exosphere 가이드 지원) | -| **프로덕션 번인** | 1주일 | 없음; Exosphere가 모니터링 및 튜닝 | +| **수집기 롤아웃** | 1~3일 | 클러스터에 수집기 설치 (Exosphere의 안내 포함) | +| **프로덕션 안정화** | 1주 | 없음; Exosphere가 모니터링 및 튜닝 | 킥오프부터 프로덕션 준비 완료까지 일반적으로 **약 2주** 소요됩니다. @@ -157,14 +158,14 @@ Managed 제공에는 에이전트 활동을 평가 기준에 따라 점수화하 ## 지원 -문의 사항이나 이슈가 있으면 `support@exosphere.host`로 Exosphere에 연락하세요. +문의 사항이나 문제가 있으시면 `support@exosphere.host`로 Exosphere에 연락하세요. --- ## 다음 단계 -- [시작하기](/ko/agenteye/getting-started): 엔드투엔드 안내 -- [Collector 설치](/ko/agenteye/collector-installation): Collector 설치 및 구성 +- [시작하기](/ko/agenteye/getting-started): 처음부터 끝까지 전체 안내 +- [수집기 설치](/ko/agenteye/collector-installation): 수집기 설치 및 구성 - [Python SDK](/ko/agenteye/python-sdk): 에이전트 코드 계측 -- [API 키](/ko/agenteye/api-keys): 접근 및 권한 관리 -- [트러블슈팅](/ko/agenteye/troubleshooting): 일반적인 문제 및 해결 방법 \ No newline at end of file +- [API 키](/ko/agenteye/api-keys): 접근 권한 및 퍼미션 관리 +- [문제 해결](/ko/agenteye/troubleshooting): 일반적인 문제 및 해결 방법 \ No newline at end of file diff --git a/docs/ko/built-in-policies.mdx b/docs/ko/built-in-policies.mdx index 3ba3d9c5..bea77b5c 100644 --- a/docs/ko/built-in-policies.mdx +++ b/docs/ko/built-in-policies.mdx @@ -710,7 +710,7 @@ failproofai에는 일반적인 에이전트 오류 패턴을 감지하는 39가 ### CLI별 Stop 시맨틱 -Stop 적용은 지원되는 7개 CLI에서 각각 다른 "에이전트 완료" 훅 계약을 노출하기 때문에 약간씩 다르게 동작합니다. **결과**는 동일합니다 — 에이전트는 워크플로 게이트가 실패하는 동안 중단할 수 없습니다 — 하지만 **메커니즘**은 다릅니다. 아래 표에 요약되어 있습니다; `require-*-before-stop` 정책을 활성화하기 전에 이해할 가치가 있는 사용자에게 보이는 특이점은 Pi에만 있습니다. +Stop 적용은 지원되는 6개 CLI에서 각각 다른 "에이전트 완료" 훅 계약을 노출하기 때문에 약간씩 다르게 동작합니다. **결과**는 동일합니다 — 에이전트는 워크플로 게이트가 실패하는 동안 중단할 수 없습니다 — 하지만 **메커니즘**은 다릅니다. 아래 표에 요약되어 있습니다; `require-*-before-stop` 정책을 활성화하기 전에 이해할 가치가 있는 사용자에게 보이는 특이점은 Pi에만 있습니다. | CLI | 게이트 실행 시점 | 표시되는 내용 | |---|---|---| @@ -718,20 +718,19 @@ Stop 적용은 지원되는 7개 CLI에서 각각 다른 "에이전트 완료" | Codex | 동일한 에이전트 루프, 즉시 | Claude와 동일. | | GitHub Copilot CLI | 동일한 에이전트 루프, 즉시 | Claude와 동일 (Copilot의 `{decision:"block", reason}` 재시도 채널 사용 — Copilot CLI 1.0.41에 대해 경험적으로 검증됨). | | Cursor Agent | 동일한 에이전트 루프, 즉시 | Claude와 동일 (Cursor의 `{followup_message}` 채널 사용 — `loop_limit`으로 제한, 기본값 5회 재시도). | -| Gemini CLI | 동일한 에이전트 루프, 즉시 | Claude와 동일 (Gemini의 `{decision:"block", reason}` 채널을 `AfterAgent`에서 사용). | | OpenCode | 동일한 에이전트 루프, 즉시 | Claude와 동일 (OpenCode의 `client.session.prompt(...)` SDK 호출을 `hookSpecificOutput.additionalContext`를 통해 라우팅). | | **Pi (pi-coding-agent)** | **다음 사용자 턴** | **Pi는 게이트가 실행될 때 눈에 띄게 중단됩니다** — 에이전트 루프가 종료되고 프롬프트로 돌아옵니다. 그런 다음 다음에 프롬프트를 제출할 때 게이트가 실행됩니다: failproofai는 해당 턴의 시스템 프롬프트에 `MANDATORY ACTION REQUIRED` 지시문을 앞에 추가하여 LLM이 요청한 작업을 수행하기 전에 워크플로 단계(커밋, 푸시 등)를 완료하도록 안내합니다. | -**Pi 제한사항.** Pi의 `AgentEndEvent`(Claude의 `Stop` 훅에 해당하는 업스트림)에는 Result 타입이 없습니다 — 실행될 때쯤이면 Pi의 에이전트 루프가 이미 종료된 상태입니다. Pi는 Claude / Copilot / Cursor / Gemini / OpenCode처럼 동일한 루프를 강제로 재시도할 수 없습니다. failproofai는 게이트를 Pi의 `before_agent_start` 이벤트(다음 사용자 프롬프트 후에 실행됨)로 이동하므로 워크플로 확인은 현재 턴이 아닌 다음 턴에 적용됩니다. +**Pi 제한사항.** Pi의 `AgentEndEvent`(Claude의 `Stop` 훅에 해당하는 업스트림)에는 Result 타입이 없습니다 — 실행될 때쯤이면 Pi의 에이전트 루프가 이미 종료된 상태입니다. Pi는 Claude / Copilot / Cursor / OpenCode처럼 동일한 루프를 강제로 재시도할 수 없습니다. failproofai는 게이트를 Pi의 `before_agent_start` 이벤트(다음 사용자 프롬프트 후에 실행됨)로 이동하므로 워크플로 확인은 현재 턴이 아닌 다음 턴에 적용됩니다. **실제로 의미하는 것:** - Pi가 중단된 후, 거부 이유는 Pi 세션 id로 키된 인메모리에 캡처됩니다. 동일한 Pi 프로세스에서 다음에 제출하는 프롬프트가 이를 소비합니다: LLM은 시스템 프롬프트 상단에 `MANDATORY ACTION REQUIRED` 지시문을 보고, 커밋(또는 푸시 / PR 열기 / CI 대기)을 수행한 후 요청을 계속합니다. 캡처된 거부 이유는 일회성입니다 — 소비되면 게이트가 해제됩니다. -- 게이트는 Pi의 프로세스 수명에 의해 제한됩니다. 턴 사이에 Pi를 `Ctrl+C`하거나 종료하면 인메모리 항목이 프로세스와 함께 삭제되고 게이트가 놓칩니다. Claude, Copilot, Cursor, Gemini, OpenCode도 동일한 제한이 있습니다(에이전트를 종료하면 게이트가 놓침) — Pi는 에이전트가 게이트가 실행되기 전에 눈에 띄게 종료되므로 더 명확하게 보입니다. +- 게이트는 Pi의 프로세스 수명에 의해 제한됩니다. 턴 사이에 Pi를 `Ctrl+C`하거나 종료하면 인메모리 항목이 프로세스와 함께 삭제되고 게이트가 놓칩니다. Claude, Copilot, Cursor, OpenCode도 동일한 제한이 있습니다(에이전트를 종료하면 게이트가 놓침) — Pi는 에이전트가 게이트가 실행되기 전에 눈에 띄게 종료되므로 더 명확하게 보입니다. - 보류 중인 거부는 어떤 이유로든(`new` / `resume` / `fork` / `quit`) `session_shutdown` 시에도 지워지므로, 이전 세션의 오래된 게이트가 동일한 Pi 프로세스에서 시작된 새 세션으로 누출될 수 없습니다. -동일 루프 재시도가 필요하다면 다른 6개의 지원 CLI 중 하나에서 `Stop` 정책을 실행하세요. 이 격차를 해소할 수 있는 `AgentEndEvent`의 향후 Result 타입에 대해 Pi 업스트림을 추적 중입니다. +동일 루프 재시도가 필요하다면 다른 5개의 지원 CLI 중 하나에서 `Stop` 정책을 실행하세요. 이 격차를 해소할 수 있는 `AgentEndEvent`의 향후 Result 타입에 대해 Pi 업스트림을 추적 중입니다. ### `require-commit-before-stop` diff --git a/docs/ko/cli/audit.mdx b/docs/ko/cli/audit.mdx index 2162ed39..e3f1a67a 100644 --- a/docs/ko/cli/audit.mdx +++ b/docs/ko/cli/audit.mdx @@ -54,7 +54,7 @@ failproofai 대시보드는 `Ctrl+C`로 중지할 때까지 계속 서비스됩니다. -대시보드는 이 머신의 과거 에이전트 CLI 트랜스크립트(Claude Code, Codex, Copilot, Cursor, OpenCode, Pi, Gemini)를 스캔하고, failproofai가 막도록 설계된 작업들 — env-var 검사, 강제 푸시, 불필요한 `cd ` 접두사, sleep 폴링 루프, 방금 편집한 파일 재읽기 등 — 을 에이전트가 얼마나 자주 수행했는지 보고합니다. +대시보드는 이 머신의 과거 에이전트 CLI 트랜스크립트(Claude Code, Codex, Copilot, Cursor, OpenCode, Pi)를 스캔하고, failproofai가 막도록 설계된 작업들 — env-var 검사, 강제 푸시, 불필요한 `cd ` 접두사, sleep 폴링 루프, 방금 편집한 파일 재읽기 등 — 을 에이전트가 얼마나 자주 수행했는지 보고합니다. 각 트랜스크립트에 대해 모든 도구 사용 이벤트는 39개의 내장 정책 **및** 런타임 정책으로 아직 처리되지 않는 패턴을 잡아내는 8개의 감사 전용 감지기를 통해 재실행됩니다. 횟수는 모든 세션에 걸쳐 정책/감지기별로 집계됩니다. diff --git a/docs/ko/configuration.mdx b/docs/ko/configuration.mdx index c90507f9..d4a99ddf 100644 --- a/docs/ko/configuration.mdx +++ b/docs/ko/configuration.mdx @@ -196,13 +196,12 @@ AI 호출을 수행하는 정책을 위한 LLM 클라이언트 설정입니다. - **OpenAI Codex**: `~/.codex/hooks.json` (사용자), `/.codex/hooks.json` (프로젝트) — Codex는 `local` 범위가 없습니다 - **GitHub Copilot CLI _(베타)_**: `~/.copilot/hooks/failproofai.json` (사용자), `/.github/hooks/failproofai.json` (프로젝트) — Copilot에는 `local` 범위가 없습니다. 훅 항목은 Copilot의 OS별 `bash`/`powershell` 명령 필드와 `timeoutSec`를 사용하며, 파일 최상단에 `version: 1` 마커가 있습니다. `events.jsonl` 레코드 스키마(공개 문서에 명시되지 않음)를 더 많은 실제 세션을 통해 검증 중이므로 Copilot CLI 지원은 **베타** 상태입니다. - **Cursor Agent _(베타)_**: `~/.cursor/hooks.json` (사용자), `/.cursor/hooks.json` (프로젝트) — Cursor에는 `local` 범위가 없습니다. 훅 항목은 Claude 형식의 `{type, command, timeout}` 구조를 사용하지만(`bash`/`powershell` 분리 없음), Cursor의 [훅 스키마](https://cursor.com/docs/hooks)에 따라 camelCase 이벤트 키(`preToolUse`, `beforeSubmitPrompt` 등) 아래 플랫 배열로 저장되며 파일 최상단에 `version: 1` 마커가 있습니다. 핸들러는 `CURSOR_EVENT_MAP`을 통해 camelCase → PascalCase로 정규화하므로 기존 기본 제공 정책이 변경 없이 동작합니다. Cursor의 트랜스크립트 온디스크 형식(공개 문서에 명시되지 않음)을 더 많은 실제 설치를 통해 검증 중이므로 Cursor Agent 지원은 **베타** 상태입니다. - - **OpenCode _(베타)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (사용자), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (프로젝트) — OpenCode에는 `local` 범위가 없습니다. 다른 여섯 CLI와 달리 OpenCode에는 **외부 명령 훅 시스템이 없습니다**: `opencode.json`의 `plugin: []` 배열을 통해 명시적으로 등록된 인프로세스 JS/TS 플러그인을 로드합니다(`.opencode/plugins/`에서의 자동 검색은 opencode v1.14.33에서 플러그인이 로드되는 방식이 **아닙니다**). 설치 시 failproofai 바이너리를 서브프로세스로 호출하고 바이너리의 Claude 형식 JSON 응답을 플러그인 시맨틱으로 변환하는 작은 생성된 플러그인 심을 배치합니다: 도구 이벤트 거부 시 `throw new Error()`(도구 호출 취소), instruct와 `Stop`/`SubagentStop` 거부 시 `client.session.prompt(...)`(거부 이유를 다음 사용자 메시지로 제출 — `session.idle`은 알림 전용이고 거기서 throw는 no-op이므로 유일한 강제 재시도 채널), allow 시 no-op. 심은 도구 이름(소문자 → PascalCase, `OPENCODE_TOOL_MAP` 사용)과 도구 입력 인수 키(camelCase → snake_case, `Read`/`Write`/`Edit`용 `OPENCODE_TOOL_INPUT_MAP` 사용, 예: `filePath` → `file_path`, `oldString` → `old_string`)를 바이너리에 전달하기 전에 정규화하므로 `block-read-outside-cwd`, `block-env-files`, `block-secrets-write` 같은 경로 확인 기본 정책이 OpenCode 도구 호출에서도 변경 없이 동작합니다. 세션은 `~/.local/share/opencode/opencode.db`의 opencode SQLite DB에 저장되며, 대시보드의 세션 뷰어는 `opencode db --format json` 및 `opencode export `를 통해 읽습니다. 버전 간 동작 및 더 많은 실제 세션 검증 중이므로 OpenCode 지원은 **베타** 상태입니다. [OpenCode 플러그인 문서](https://opencode.ai/docs/plugins/)를 참고하세요. + - **OpenCode _(베타)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (사용자), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (프로젝트) — OpenCode에는 `local` 범위가 없습니다. 다른 다섯 CLI와 달리 OpenCode에는 **외부 명령 훅 시스템이 없습니다**: `opencode.json`의 `plugin: []` 배열을 통해 명시적으로 등록된 인프로세스 JS/TS 플러그인을 로드합니다(`.opencode/plugins/`에서의 자동 검색은 opencode v1.14.33에서 플러그인이 로드되는 방식이 **아닙니다**). 설치 시 failproofai 바이너리를 서브프로세스로 호출하고 바이너리의 Claude 형식 JSON 응답을 플러그인 시맨틱으로 변환하는 작은 생성된 플러그인 심을 배치합니다: 도구 이벤트 거부 시 `throw new Error()`(도구 호출 취소), instruct와 `Stop`/`SubagentStop` 거부 시 `client.session.prompt(...)`(거부 이유를 다음 사용자 메시지로 제출 — `session.idle`은 알림 전용이고 거기서 throw는 no-op이므로 유일한 강제 재시도 채널), allow 시 no-op. 심은 도구 이름(소문자 → PascalCase, `OPENCODE_TOOL_MAP` 사용)과 도구 입력 인수 키(camelCase → snake_case, `Read`/`Write`/`Edit`용 `OPENCODE_TOOL_INPUT_MAP` 사용, 예: `filePath` → `file_path`, `oldString` → `old_string`)를 바이너리에 전달하기 전에 정규화하므로 `block-read-outside-cwd`, `block-env-files`, `block-secrets-write` 같은 경로 확인 기본 정책이 OpenCode 도구 호출에서도 변경 없이 동작합니다. 세션은 `~/.local/share/opencode/opencode.db`의 opencode SQLite DB에 저장되며, 대시보드의 세션 뷰어는 `opencode db --format json` 및 `opencode export `를 통해 읽습니다. 버전 간 동작 및 더 많은 실제 세션 검증 중이므로 OpenCode 지원은 **베타** 상태입니다. [OpenCode 플러그인 문서](https://opencode.ai/docs/plugins/)를 참고하세요. - **Pi _(베타)_**: `~/.pi/agent/settings.json` (사용자), `/.pi/settings.json` (프로젝트) — Pi에는 `local` 범위가 없습니다. Pi는 시작 시 TypeScript 확장 패키지를 로드하며, 설정 파일은 `{"packages": ["./relative/path", …]}` 형식의 플랫 문자열 배열입니다. failproofai는 번들된 `pi-extension/` 디렉터리를 가리키는 단일 packages 배열 항목을 씁니다. 확장은 내부적으로 Pi의 `tool_call`/`user_bash`/`input`/`session_start` 이벤트를 구독하고 `failproofai --hook --cli pi`를 셸아웃합니다. 핸들러는 `PI_EVENT_MAP`을 통해 underscore_lower_snake_case → PascalCase로 이벤트를 정규화하므로 기존 기본 제공 정책이 변경 없이 동작합니다. 도구 입력 인수도 `PI_TOOL_INPUT_MAP`을 통해 정규화됩니다(Pi의 Read/Write/Edit는 `file_path` 대신 `path`를 전달하며, 최상위 키를 매핑하면 `block-env-files`와 `block-secrets-write`가 동작합니다 — `block-read-outside-cwd`는 이미 `path` 폴백이 있었습니다). Pi의 확장 API와 세션 로그 레이아웃이 안정화되는 동안 Pi 지원은 **베타** 상태입니다. - - **Gemini CLI _(베타)_**: `~/.gemini/settings.json` (사용자), `/.gemini/settings.json` (프로젝트) — Gemini에는 `local` 범위가 없습니다(failproofai가 노출하지 않는 `/etc/gemini-cli/settings.json`의 `system` 범위가 문서에 있습니다). 훅 항목은 Claude의 `{type, command, timeout}` 형식을 Gemini의 `{matcher, hooks: [...]}` 매처 스키마로 감싸며 기본적으로 `matcher: "*"`를 사용합니다. 이벤트는 PascalCase입니다(`SessionStart`, `BeforeAgent`, `AfterAgent`, `BeforeModel`, `AfterModel`, `BeforeToolSelection`, `BeforeTool`, `AfterTool`, `PreCompress`, `Notification`, `SessionEnd`). 핸들러는 `GEMINI_EVENT_MAP`을 통해 Claude 표준 이름으로 매핑합니다. 도구 이름은 snake_case입니다(`run_shell_command`, `read_file`, `write_file`, `replace` 등) — 핸들러는 `GEMINI_TOOL_MAP`을 통해 정규화하므로 기존 기본 제공 정책이 변경 없이 동작합니다. 정책 평가기는 Gemini의 플랫 `{decision: "deny", reason}` 형식(Gemini의 "Golden Rule" exit-0 계약에 따른 선호 형식), BeforeAgent/AfterTool/SessionStart의 컨텍스트 주입을 위한 `{hookSpecificOutput: {hookEventName, additionalContext}}`, 그리고 강제 재시도 시맨틱을 위한 AfterAgent의 `{decision: "block", reason}`을 반환합니다. 실제 적용 범위를 넓히는 동안 Gemini CLI 지원은 **베타** 상태입니다. [Gemini CLI 훅 문서](https://geminicli.com/docs/hooks/)를 참고하세요. - **Hermes (hermes-agent)**: `~/.hermes/config.yaml` (**사용자 범위만** — Hermes에는 프로젝트/로컬 설정이 없습니다). Hermes는 Slack/Telegram **게이트웨이**이므로, 설치 한 번으로 모든 플랫폼(Slack/Telegram/cli/cron)과 내부 서브에이전트의 도구 호출을 가로챕니다. 훅 항목은 Hermes의 snake_case 이벤트(`pre_tool_call`/`post_tool_call`/`on_session_start`/`on_session_end`/`subagent_stop`)를 키로 하는 `hooks:` 맵 아래 `{command, timeout}` 쌍(timeout은 **초** 단위)입니다. 핸들러는 `HERMES_EVENT_MAP`으로 이벤트를, `HERMES_TOOL_MAP`으로 도구 이름을 정규화하므로 기본 제공 정책이 변경 없이 동작합니다. 설정은 주석 보존 YAML `Document` 라운드트립을 통해 편집되므로 운영자의 다른 설정이 유지되며, 설치 시 `hooks_auto_accept: true`로 설정되어 헤드리스 게이트웨이(TTY 없음)가 동의 프롬프트 없이 훅을 실행합니다. 평가기는 Hermes의 `{"decision":"block","reason"}` stdout 계약을 반환합니다(Hermes는 종료 코드를 무시합니다). **제한 사항:** Hermes에는 턴 종료 `Stop` 이벤트가 없으므로 `require-*-before-stop` 기본 정책은 동작하지 않습니다(해당 없음, 오류 아님). `instruct`는 allow-with-logged-note로 격하됩니다(추가 컨텍스트 채널 없음). 출력 시크릿 편집(`sanitize-*`)은 셸 훅 계약으로 도구 출력을 재작성할 수 없습니다. Hermes는 **오프라인 감사** 소스이기도 합니다 — 대시보드는 `~/.hermes/state.db`에서 게이트웨이 세션을 직접 읽습니다. - **`policies-config.json`** — failproofai에게 어떤 정책을 어떤 파라미터로 평가할지 지시합니다(모든 에이전트 CLI에 공통 적용) -특정 에이전트를 대상으로 하려면 `--cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes`를 전달하세요(여러 개는 공백으로 구분하거나 반복 사용): +특정 에이전트를 대상으로 하려면 `--cli claude|codex|copilot|cursor|opencode|pi|hermes`를 전달하세요(여러 개는 공백으로 구분하거나 반복 사용): ```bash failproofai policies --install --cli codex --scope project @@ -210,12 +209,11 @@ failproofai policies --install --cli copilot --scope project failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project -failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user -failproofai policies --install --cli claude codex copilot cursor opencode pi gemini +failproofai policies --install --cli claude codex copilot cursor opencode pi ``` -`--cli`를 생략하면 `failproofai`가 설치된 에이전트 CLI를 자동으로 감지합니다(`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which gemini` / `which hermes`): +`--cli`를 생략하면 `failproofai`가 설치된 에이전트 CLI를 자동으로 감지합니다(`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which hermes`): - **CLI 1개 감지** — 확인 없이 해당 CLI를 자동 선택합니다. - **여러 CLI 감지** (대화형 터미널) — 화살표 키 단일 선택 프롬프트를 표시합니다. `Detected (N)` 섹션(전체 감지 CLI에 대한 `Install for all N detected` 통합 행 + 각 감지된 CLI 개별 항목)과, 사전 설치를 위한 모든 미감지 지원 CLI를 나열하는 `Not installed (M) · install hooks ahead of time` 섹션으로 구성됩니다(↑↓로 이동, Enter로 선택, ^C로 종료). 제거 흐름은 Detected 섹션만 표시합니다. diff --git a/docs/ko/dashboard.mdx b/docs/ko/dashboard.mdx index eaa09fbc..18ef7da4 100644 --- a/docs/ko/dashboard.mdx +++ b/docs/ko/dashboard.mdx @@ -24,11 +24,11 @@ failproofai ### 프로젝트 -머신에서 발견된 모든 Claude Code, OpenAI Codex, GitHub Copilot CLI _(베타)_, Cursor Agent _(베타)_, OpenCode _(베타)_, Pi _(베타)_, Gemini CLI _(베타)_, Hermes 프로젝트를 나열합니다. Claude 프로젝트는 `~/.claude/projects/`(또는 `CLAUDE_PROJECTS_PATH`로 설정된 경로)에서 검색됩니다. Codex 프로젝트는 `~/.codex/sessions///
/*.jsonl` 아래의 모든 트랜스크립트를 스캔하고 각 세션의 첫 번째 레코드에 기록된 `cwd`로 그룹화하여 검색됩니다. Copilot CLI 프로젝트는 각 `~/.copilot/session-state//workspace.yaml`(`COPILOT_HOME`으로 구성 가능)을 스캔하고 `cwd` 필드로 그룹화하여 검색됩니다. Cursor Agent 프로젝트는 `~/.cursor/agent-sessions//`(`CURSOR_HOME`으로 구성 가능, `conversations/` 및 `sessions/`가 폴백으로 검색됨) 아래의 세션별 메타데이터를 스캔하여 `meta.json` / `session.json` / `workspace.yaml`의 `cwd` 스칼라로 검색됩니다. OpenCode 프로젝트는 `opencode db --format json`을 통해 `~/.local/share/opencode/opencode.db`의 SQLite DB를 쿼리하여 검색되며(`session` 및 `project` 테이블을 읽고 `project_id`로 그룹화), Pi 프로젝트는 `~/.pi/agent/sessions//_.jsonl`(`PI_SESSIONS_DIR`으로 구성 가능) 아래의 세션별 JSONL 트랜스크립트를 스캔하고 각 세션의 첫 번째 레코드에서 `cwd`를 가져와 검색됩니다. Gemini CLI 프로젝트는 `~/.gemini/tmp//chats/session--.jsonl`(`GEMINI_SESSIONS_DIR`으로 구성 가능)을 스캔하고 인접한 `.project_root` 텍스트 마커에서 정규 cwd를 복원하여 검색됩니다. Hermes 게이트웨이 세션은 `~/.hermes/state.db`(`HERMES_DB_PATH`으로 구성 가능)의 SQLite 저장소에서 직접 읽어오며 `source`(Slack/Telegram/cli/cron — 게이트웨이 세션에는 cwd가 없음)로 `hermes-` 프로젝트로 그룹화됩니다. 여러 CLI에서 사용된 프로젝트는 일치하는 모든 배지가 있는 단일 행으로 표시됩니다. 테이블 위의 **CLI** 드롭다운을 사용하여 특정 에이전트 CLI로 필터링하세요. URL은 선택 내용을 `?cli=claude|codex|copilot|cursor|opencode|pi|gemini|hermes`로 보존합니다. +머신에서 발견된 모든 Claude Code, OpenAI Codex, GitHub Copilot CLI _(베타)_, Cursor Agent _(베타)_, OpenCode _(베타)_, Pi _(베타)_, Hermes 프로젝트를 나열합니다. Claude 프로젝트는 `~/.claude/projects/`(또는 `CLAUDE_PROJECTS_PATH`로 설정된 경로)에서 검색됩니다. Codex 프로젝트는 `~/.codex/sessions///
/*.jsonl` 아래의 모든 트랜스크립트를 스캔하고 각 세션의 첫 번째 레코드에 기록된 `cwd`로 그룹화하여 검색됩니다. Copilot CLI 프로젝트는 각 `~/.copilot/session-state//workspace.yaml`(`COPILOT_HOME`으로 구성 가능)을 스캔하고 `cwd` 필드로 그룹화하여 검색됩니다. Cursor Agent 프로젝트는 `~/.cursor/agent-sessions//`(`CURSOR_HOME`으로 구성 가능, `conversations/` 및 `sessions/`가 폴백으로 검색됨) 아래의 세션별 메타데이터를 스캔하여 `meta.json` / `session.json` / `workspace.yaml`의 `cwd` 스칼라로 검색됩니다. OpenCode 프로젝트는 `opencode db --format json`을 통해 `~/.local/share/opencode/opencode.db`의 SQLite DB를 쿼리하여 검색되며(`session` 및 `project` 테이블을 읽고 `project_id`로 그룹화), Pi 프로젝트는 `~/.pi/agent/sessions//_.jsonl`(`PI_SESSIONS_DIR`으로 구성 가능) 아래의 세션별 JSONL 트랜스크립트를 스캔하고 각 세션의 첫 번째 레코드에서 `cwd`를 가져와 검색됩니다. Hermes 게이트웨이 세션은 `~/.hermes/state.db`(`HERMES_DB_PATH`으로 구성 가능)의 SQLite 저장소에서 직접 읽어오며 `source`(Slack/Telegram/cli/cron — 게이트웨이 세션에는 cwd가 없음)로 `hermes-` 프로젝트로 그룹화됩니다. 여러 CLI에서 사용된 프로젝트는 일치하는 모든 배지가 있는 단일 행으로 표시됩니다. 테이블 위의 **CLI** 드롭다운을 사용하여 특정 에이전트 CLI로 필터링하세요. URL은 선택 내용을 `?cli=claude|codex|copilot|cursor|opencode|pi|hermes`로 보존합니다. 각 프로젝트에는 다음이 표시됩니다: - 프로젝트 이름 (폴더 경로에서 파생) -- CLI 배지 — `Claude Code` (주황색), `OpenAI Codex` (보라색), `GitHub Copilot` (파란색), `Cursor Agent` (에메랄드), `OpenCode` (호박색), `Pi` (분홍색), `Gemini CLI` (하늘색), 및/또는 `Hermes` (남색) +- CLI 배지 — `Claude Code` (주황색), `OpenAI Codex` (보라색), `GitHub Copilot` (파란색), `Cursor Agent` (에메랄드), `OpenCode` (호박색), `Pi` (분홍색), 및/또는 `Hermes` (남색) - 가장 최근 세션 활동 날짜 프로젝트를 클릭하면 해당 세션이 표시됩니다. @@ -47,7 +47,7 @@ failproofai ### 세션 뷰어 -세션 뷰어는 자율 에이전트에 대한 핵심 질문에 답합니다: 에이전트가 무엇을 했으며, 제대로 작동했는가? 헤더 옆의 CLI 배지는 세션이 Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi, Gemini CLI 또는 Hermes 트랜스크립트인지 나타냅니다. 세션에서 발생한 모든 일의 타임라인을 보여줍니다: +세션 뷰어는 자율 에이전트에 대한 핵심 질문에 답합니다: 에이전트가 무엇을 했으며, 제대로 작동했는가? 헤더 옆의 CLI 배지는 세션이 Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi 또는 Hermes 트랜스크립트인지 나타냅니다. 세션에서 발생한 모든 일의 타임라인을 보여줍니다: - **메시지** - Claude의 텍스트 응답과 사용자 프롬프트 - **툴 호출** - Claude가 호출한 모든 툴, 입력 및 출력 포함 @@ -55,7 +55,7 @@ failproofai 상단의 통계 바에는 세션 지속 시간, 총 툴 호출 수, 훅 결정 요약(allow / deny / instruct 수)이 표시됩니다. -**로그 다운로드** 버튼을 클릭하여 세션을 내보냅니다. Claude Code, Codex, Copilot, Cursor, Pi, Gemini 세션의 경우 디스크의 원본 JSONL 트랜스크립트를 바이트 단위로 그대로 받을 수 있으며, OpenCode(세션이 디스크가 아닌 SQLite에 저장됨)의 경우 기본 `session` / `messages` / `parts` 테이블을 미러링하는 JSON 문서를 받습니다. +**로그 다운로드** 버튼을 클릭하여 세션을 내보냅니다. Claude Code, Codex, Copilot, Cursor, Pi 세션의 경우 디스크의 원본 JSONL 트랜스크립트를 바이트 단위로 그대로 받을 수 있으며, OpenCode(세션이 디스크가 아닌 SQLite에 저장됨)의 경우 기본 `session` / `messages` / `parts` 테이블을 미러링하는 JSON 문서를 받습니다. ### 감사 @@ -75,16 +75,16 @@ failproofai - - 단일 패널에서 failproofai가 보호할 에이전트 CLI를 다중 선택 — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi, Gemini CLI, Hermes 각각에 설치 상태(`Active` / `Detected` / `Inactive`), 사용자 범위 설정 경로, 브랜드 색상 강조가 있는 행이 있습니다. 원하는 CLI를 체크 또는 체크 해제하고 `Apply changes`를 클릭하면 한 번에 차이를 설치/제거합니다. PATH에서 바이너리가 감지된 CLI는 미리 체크됩니다. + - 단일 패널에서 failproofai가 보호할 에이전트 CLI를 다중 선택 — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi, Hermes 각각에 설치 상태(`Active` / `Detected` / `Inactive`), 사용자 범위 설정 경로, 브랜드 색상 강조가 있는 행이 있습니다. 원하는 CLI를 체크 또는 체크 해제하고 `Apply changes`를 클릭하면 한 번에 차이를 설치/제거합니다. PATH에서 바이너리가 감지된 CLI는 미리 체크됩니다. - 단일 클릭으로 개별 정책을 켜거나 끕니다(`~/.failproofai/policies-config.json`에 기록 — 모든 설치된 CLI에서 공유됨) - 정책을 펼쳐 매개변수를 구성합니다(`policyParams`를 지원하는 정책의 경우) - 사용자 지정 정책 파일 경로 설정 - 모든 세션에서 실행된 모든 훅 이벤트의 전체 페이지네이션 기록 - - 결정, 이벤트 유형, CLI(Claude Code / OpenAI Codex / GitHub Copilot _(베타)_ / Cursor Agent _(베타)_ / OpenCode _(베타)_ / Pi _(베타)_ / Gemini CLI _(베타)_ / Hermes), 정책 이름 또는 세션 ID로 필터링 - - 각 행에는 타임스탬프, 정책 이름, 결정, CLI 배지(주황색 = Claude Code, 보라색 = OpenAI Codex, 파란색 = GitHub Copilot, 에메랄드 = Cursor Agent, 호박색 = OpenCode, 분홍색 = Pi, 하늘색 = Gemini CLI, 남색 = Hermes), 툴 이름, 세션 ID, deny/instruct 결정 이유가 표시됩니다 - - 세션 ID를 클릭하면 트랜스크립트가 열립니다 — 뷰어는 어떤 CLI가 훅을 실행했는지(Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Gemini CLI `~/.gemini/tmp//chats/.jsonl`, Hermes `~/.hermes/state.db`)를 자동으로 감지하고 헤더에 일치하는 CLI 배지를 렌더링합니다 + - 결정, 이벤트 유형, CLI(Claude Code / OpenAI Codex / GitHub Copilot _(베타)_ / Cursor Agent _(베타)_ / OpenCode _(베타)_ / Pi _(베타)_ / Hermes), 정책 이름 또는 세션 ID로 필터링 + - 각 행에는 타임스탬프, 정책 이름, 결정, CLI 배지(주황색 = Claude Code, 보라색 = OpenAI Codex, 파란색 = GitHub Copilot, 에메랄드 = Cursor Agent, 호박색 = OpenCode, 분홍색 = Pi, 남색 = Hermes), 툴 이름, 세션 ID, deny/instruct 결정 이유가 표시됩니다 + - 세션 ID를 클릭하면 트랜스크립트가 열립니다 — 뷰어는 어떤 CLI가 훅을 실행했는지(Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Hermes `~/.hermes/state.db`)를 자동으로 감지하고 헤더에 일치하는 CLI 배지를 렌더링합니다 diff --git a/docs/ko/getting-started.mdx b/docs/ko/getting-started.mdx index 6b7ff502..c3a26343 100644 --- a/docs/ko/getting-started.mdx +++ b/docs/ko/getting-started.mdx @@ -37,9 +37,9 @@ bun add -g failproofai failproofai policies --install ``` - 이 명령은 설치된 에이전트 CLI에 훅 항목을 기록합니다 (Claude Code의 `~/.claude/settings.json`, OpenAI Codex의 `~/.codex/hooks.json`, GitHub Copilot CLI의 `~/.copilot/hooks/failproofai.json`, Cursor Agent의 `~/.cursor/hooks.json`, OpenCode의 `~/.config/opencode/plugins/failproofai.mjs` 생성 플러그인 심 및 `~/.config/opencode/opencode.json`의 `plugin` 배열 등록 항목, Pi의 `~/.pi/agent/settings.json`, Gemini CLI의 `~/.gemini/settings.json`, Hermes의 `~/.hermes/config.yaml`). 둘 이상의 CLI가 설치되어 있으면 선택 프롬프트가 표시됩니다. `--cli claude codex copilot cursor opencode pi gemini hermes` (원하는 조합)를 전달하면 프롬프트를 건너뜁니다. + 이 명령은 설치된 에이전트 CLI에 훅 항목을 기록합니다 (Claude Code의 `~/.claude/settings.json`, OpenAI Codex의 `~/.codex/hooks.json`, GitHub Copilot CLI의 `~/.copilot/hooks/failproofai.json`, Cursor Agent의 `~/.cursor/hooks.json`, OpenCode의 `~/.config/opencode/plugins/failproofai.mjs` 생성 플러그인 심 및 `~/.config/opencode/opencode.json`의 `plugin` 배열 등록 항목, Pi의 `~/.pi/agent/settings.json`, Hermes의 `~/.hermes/config.yaml`). 둘 이상의 CLI가 설치되어 있으면 선택 프롬프트가 표시됩니다. `--cli claude codex copilot cursor opencode pi hermes` (원하는 조합)를 전달하면 프롬프트를 건너뜁니다. - GitHub Copilot CLI, Cursor Agent, OpenCode, Pi, Gemini CLI 지원은 **베타** 단계입니다 — `--cli copilot`, `--cli cursor`, `--cli opencode`, `--cli pi`, 또는 `--cli gemini`로 설치하세요. Hermes (hermes-agent, Slack/Telegram 게이트웨이)는 `--cli hermes`로 사용자 범위에 설치되며 오프라인 감사 소스이기도 합니다. + GitHub Copilot CLI, Cursor Agent, OpenCode, Pi 지원은 **베타** 단계입니다 — `--cli copilot`, `--cli cursor`, `--cli opencode`, 또는 `--cli pi`로 설치하세요. Hermes (hermes-agent, Slack/Telegram 게이트웨이)는 `--cli hermes`로 사용자 범위에 설치되며 오프라인 감사 소스이기도 합니다. ```bash failproofai policies --install --scope project @@ -48,7 +48,6 @@ bun add -g failproofai failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project - failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user failproofai policies --install block-sudo block-rm-rf sanitize-api-keys ``` diff --git a/docs/ko/introduction.mdx b/docs/ko/introduction.mdx index b48424a0..48222763 100644 --- a/docs/ko/introduction.mdx +++ b/docs/ko/introduction.mdx @@ -5,7 +5,7 @@ description: "FailproofAI는 단 한 번의 설치로 루프, 시크릿 유출, [![npm weekly downloads](https://img.shields.io/npm/dw/failproofai?style=flat-square&color=2ea44f)](https://www.npmjs.com/package/failproofai) -**AI 실패 처리**, **오류 복구**, **LLM 안정성**을 위한 훅과 정책 모음입니다. **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Gemini CLI**, **Hermes**, **Agents SDK** 전반에 걸쳐 AI 에이전트를 안정적으로 자율 실행할 수 있도록 지원합니다. +**AI 실패 처리**, **오류 복구**, **LLM 안정성**을 위한 훅과 정책 모음입니다. **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Hermes**, **Agents SDK** 전반에 걸쳐 AI 에이전트를 안정적으로 자율 실행할 수 있도록 지원합니다. AI 에이전트는 예측 가능한 방식으로 실패합니다. 파괴적인 명령을 실행하거나, 시크릿을 유출하거나, 작업 범위를 벗어나거나, 루프에 빠지거나, main 브랜치에 직접 푸시하기도 합니다. 방치하면 사소한 실패가 서비스 장애, 자격 증명 유출, 작업 손실로 이어질 수 있습니다. diff --git a/docs/pt-br/built-in-policies.mdx b/docs/pt-br/built-in-policies.mdx index 6d84b6c4..d35046b7 100644 --- a/docs/pt-br/built-in-policies.mdx +++ b/docs/pt-br/built-in-policies.mdx @@ -712,20 +712,19 @@ A aplicação de Stop funciona de forma ligeiramente diferente entre os sete CLI | Codex | No mesmo loop do agente, imediatamente | Igual ao Claude. | | GitHub Copilot CLI | No mesmo loop do agente, imediatamente | Igual ao Claude (usa o canal de retry `{decision:"block", reason}` do Copilot — verificado empiricamente no Copilot CLI 1.0.41). | | Cursor Agent | No mesmo loop do agente, imediatamente | Igual ao Claude (usa o canal `{followup_message}` do Cursor — limitado ao `loop_limit`, padrão 5 tentativas). | -| Gemini CLI | No mesmo loop do agente, imediatamente | Igual ao Claude (usa o canal `{decision:"block", reason}` do Gemini em `AfterAgent`). | | OpenCode | No mesmo loop do agente, imediatamente | Igual ao Claude (usa a chamada SDK `client.session.prompt(...)` do OpenCode roteada através de `hookSpecificOutput.additionalContext`). | | **Pi (pi-coding-agent)** | **Próximo turno do usuário** | **O Pi para visivelmente** quando o gate é acionado — seu loop de agente sai e você retorna ao prompt. O gate é então acionado na próxima vez que você enviar um prompt: o failproofai antepõe uma diretiva `MANDATORY ACTION REQUIRED` ao prompt do sistema daquele turno, instruindo o LLM a concluir a etapa do fluxo de trabalho (commit, push, etc.) antes de fazer o que você pediu. | -**Limitação do Pi.** O `AgentEndEvent` do Pi (o equivalente upstream do hook `Stop` do Claude) não tem tipo Result — quando ele é acionado, o loop do agente do Pi já saiu. O Pi não pode ser forçado a repetir o mesmo loop da forma que Claude / Copilot / Cursor / Gemini / OpenCode podem. O failproofai desloca o gate para o evento `before_agent_start` do Pi (que é acionado após o próximo prompt do usuário), para que a verificação do fluxo de trabalho ainda seja aplicada, apenas no próximo turno em vez do atual. +**Limitação do Pi.** O `AgentEndEvent` do Pi (o equivalente upstream do hook `Stop` do Claude) não tem tipo Result — quando ele é acionado, o loop do agente do Pi já saiu. O Pi não pode ser forçado a repetir o mesmo loop da forma que Claude / Copilot / Cursor / OpenCode podem. O failproofai desloca o gate para o evento `before_agent_start` do Pi (que é acionado após o próximo prompt do usuário), para que a verificação do fluxo de trabalho ainda seja aplicada, apenas no próximo turno em vez do atual. **O que isso significa na prática:** - Após o Pi parar, o motivo da negação é capturado na memória, indexado pelo ID de sessão do Pi. O próximo prompt que você enviar no mesmo processo do Pi o consome: o LLM vê a diretiva `MANDATORY ACTION REQUIRED` no topo de seu prompt de sistema, faz o commit (ou push / abre o PR / aguarda o CI), e só então continua com sua solicitação. O motivo de negação capturado é de uso único — uma vez consumido, o gate é liberado. -- O gate é limitado ao tempo de vida do processo do Pi. Se você der `Ctrl+C` no Pi ou sair entre turnos, a entrada na memória é descartada junto com o processo e o gate é perdido. Claude, Copilot, Cursor, Gemini e OpenCode têm o mesmo limite (matar o agente faz o gate ser perdido) — o Pi apenas torna isso mais visível porque o agente sai visivelmente antes do gate ser acionado. +- O gate é limitado ao tempo de vida do processo do Pi. Se você der `Ctrl+C` no Pi ou sair entre turnos, a entrada na memória é descartada junto com o processo e o gate é perdido. Claude, Copilot, Cursor e OpenCode têm o mesmo limite (matar o agente faz o gate ser perdido) — o Pi apenas torna isso mais visível porque o agente sai visivelmente antes do gate ser acionado. - Uma negação pendente também é limpa no `session_shutdown` por qualquer motivo (`new` / `resume` / `fork` / `quit`), então um gate obsoleto de uma sessão anterior não pode vazar para uma nova sessão iniciada no mesmo processo do Pi. -Se você precisar do retry no mesmo loop no estilo Claude, execute suas políticas `Stop` em qualquer um dos outros seis CLIs suportados. Estamos acompanhando o upstream do Pi para um futuro tipo Result no `AgentEndEvent` que nos permitiria fechar essa lacuna. +Se você precisar do retry no mesmo loop no estilo Claude, execute suas políticas `Stop` em qualquer um dos outros cinco CLIs suportados. Estamos acompanhando o upstream do Pi para um futuro tipo Result no `AgentEndEvent` que nos permitiria fechar essa lacuna. ### `require-commit-before-stop` diff --git a/docs/pt-br/cli/audit.mdx b/docs/pt-br/cli/audit.mdx index 0886e980..6ca1a458 100644 --- a/docs/pt-br/cli/audit.mdx +++ b/docs/pt-br/cli/audit.mdx @@ -55,7 +55,7 @@ failproofai até você encerrá-lo com `Ctrl+C`. -O dashboard varre transcrições anteriores do agente CLI nesta máquina (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi, Gemini) e relata com que frequência o agente fez coisas que o failproofai foi criado para impedir — verificações de variáveis de ambiente, force pushes, prefixos redundantes `cd `, loops de polling com sleep, releitura de arquivos recém-editados, e mais. +O dashboard varre transcrições anteriores do agente CLI nesta máquina (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi) e relata com que frequência o agente fez coisas que o failproofai foi criado para impedir — verificações de variáveis de ambiente, force pushes, prefixos redundantes `cd `, loops de polling com sleep, releitura de arquivos recém-editados, e mais. Para cada transcrição, cada evento de uso de ferramenta é reproduzido pelas 39 políticas integradas **e** pelos 8 detectores exclusivos do audit que capturam padrões ainda não cobertos pelas políticas em tempo real. As contagens são agregadas por política/detector em todas as sessões. diff --git a/docs/pt-br/configuration.mdx b/docs/pt-br/configuration.mdx index ba82f42c..f0d9db1c 100644 --- a/docs/pt-br/configuration.mdx +++ b/docs/pt-br/configuration.mdx @@ -196,13 +196,12 @@ Os comandos `policies --install` e `policies --uninstall` escrevem no arquivo de - **OpenAI Codex**: `~/.codex/hooks.json` (usuário), `/.codex/hooks.json` (projeto) — o Codex não possui escopo `local` - **GitHub Copilot CLI _(beta)_**: `~/.copilot/hooks/failproofai.json` (usuário), `/.github/hooks/failproofai.json` (projeto) — o Copilot não possui escopo `local`. As entradas de hook usam os campos de comando `bash`/`powershell` com chave por SO do Copilot com `timeoutSec`; o arquivo carrega um marcador `version: 1` no nível superior. O suporte ao Copilot CLI está em **beta** enquanto verificamos o esquema de registros `events.jsonl` (não especificado na documentação pública) em mais sessões reais. - **Cursor Agent _(beta)_**: `~/.cursor/hooks.json` (usuário), `/.cursor/hooks.json` (projeto) — o Cursor não possui escopo `local`. As entradas de hook usam o formato Claude `{type, command, timeout}` (sem divisão `bash`/`powershell`), mas armazenadas sob chaves de evento em camelCase (`preToolUse`, `beforeSubmitPrompt`, …) em um array plano, conforme o [esquema de hooks](https://cursor.com/docs/hooks) do Cursor; o arquivo carrega um marcador `version: 1` no nível superior. O handler canonicaliza camelCase → PascalCase via `CURSOR_EVENT_MAP`, de modo que as políticas integradas existentes disparam sem alteração. O suporte ao Cursor Agent está em **beta** enquanto verificamos o formato em disco da transcrição do Cursor (não especificado na documentação pública) em mais instalações reais. - - **OpenCode _(beta)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (usuário), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (projeto) — o OpenCode não possui escopo `local`. Diferentemente dos outros seis CLIs, o OpenCode **não possui sistema de hooks para comandos externos**: ele carrega plugins JS/TS em processo, explicitamente registrados pelo array `plugin: []` no `opencode.json` (a autodescoberta a partir de `.opencode/plugins/` **não** é como os plugins são carregados no opencode v1.14.33). A instalação deposita um pequeno shim de plugin gerado que chama o binário failproofai em subprocesso e traduz a resposta JSON no formato Claude do binário para a semântica do plugin: `throw new Error()` para negação em eventos de ferramenta (cancela a chamada da ferramenta), `client.session.prompt(...)` para instruct E para negação de `Stop` / `SubagentStop` (envia o motivo da negação como a próxima mensagem do usuário — o único canal de força de nova tentativa, já que `session.idle` é apenas de notificação e lançar exceção a partir dele é um no-op), e no-op para allow. O shim canonicaliza nomes de ferramentas (minúsculas → PascalCase via `OPENCODE_TOOL_MAP`) e chaves de argumentos de entrada de ferramentas (camelCase → snake_case via `OPENCODE_TOOL_INPUT_MAP` para `Read` / `Write` / `Edit`, por exemplo `filePath` → `file_path`, `oldString` → `old_string`) antes de encaminhar ao binário, de modo que políticas integradas de verificação de caminho como `block-read-outside-cwd`, `block-env-files` e `block-secrets-write` disparam sem alteração em chamadas de ferramentas do OpenCode. As sessões ficam no banco de dados SQLite do opencode em `~/.local/share/opencode/opencode.db`; o visualizador de sessões do dashboard as lê via `opencode db --format json` e `opencode export `. O suporte ao OpenCode está em **beta** enquanto verificamos o comportamento entre versões e em mais sessões reais. Consulte a [documentação de plugins do OpenCode](https://opencode.ai/docs/plugins/). + - **OpenCode _(beta)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (usuário), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (projeto) — o OpenCode não possui escopo `local`. Diferentemente dos outros cinco CLIs, o OpenCode **não possui sistema de hooks para comandos externos**: ele carrega plugins JS/TS em processo, explicitamente registrados pelo array `plugin: []` no `opencode.json` (a autodescoberta a partir de `.opencode/plugins/` **não** é como os plugins são carregados no opencode v1.14.33). A instalação deposita um pequeno shim de plugin gerado que chama o binário failproofai em subprocesso e traduz a resposta JSON no formato Claude do binário para a semântica do plugin: `throw new Error()` para negação em eventos de ferramenta (cancela a chamada da ferramenta), `client.session.prompt(...)` para instruct E para negação de `Stop` / `SubagentStop` (envia o motivo da negação como a próxima mensagem do usuário — o único canal de força de nova tentativa, já que `session.idle` é apenas de notificação e lançar exceção a partir dele é um no-op), e no-op para allow. O shim canonicaliza nomes de ferramentas (minúsculas → PascalCase via `OPENCODE_TOOL_MAP`) e chaves de argumentos de entrada de ferramentas (camelCase → snake_case via `OPENCODE_TOOL_INPUT_MAP` para `Read` / `Write` / `Edit`, por exemplo `filePath` → `file_path`, `oldString` → `old_string`) antes de encaminhar ao binário, de modo que políticas integradas de verificação de caminho como `block-read-outside-cwd`, `block-env-files` e `block-secrets-write` disparam sem alteração em chamadas de ferramentas do OpenCode. As sessões ficam no banco de dados SQLite do opencode em `~/.local/share/opencode/opencode.db`; o visualizador de sessões do dashboard as lê via `opencode db --format json` e `opencode export `. O suporte ao OpenCode está em **beta** enquanto verificamos o comportamento entre versões e em mais sessões reais. Consulte a [documentação de plugins do OpenCode](https://opencode.ai/docs/plugins/). - **Pi _(beta)_**: `~/.pi/agent/settings.json` (usuário), `/.pi/settings.json` (projeto) — o Pi não possui escopo `local`. O Pi carrega pacotes de extensão TypeScript na inicialização; o arquivo de configurações é um array de strings plano `{"packages": ["./relative/path", …]}`. failproofai escreve uma única entrada no array de pacotes apontando para seu diretório `pi-extension/` empacotado. A extensão subscreve internamente aos eventos `tool_call` / `user_bash` / `input` / `session_start` do Pi e executa `failproofai --hook --cli pi` em shell; o handler canonicaliza eventos via `PI_EVENT_MAP` (underscore_lower_snake_case → PascalCase) para que as políticas integradas existentes disparem sem alteração. Os argumentos de entrada de ferramentas também são canonicalizados via `PI_TOOL_INPUT_MAP` (o Read / Write / Edit do Pi entregam `path` em vez de `file_path`; mapear a chave de nível superior permite que `block-env-files` e `block-secrets-write` disparem — `block-read-outside-cwd` já tinha um fallback para `path`). O suporte ao Pi está em **beta** enquanto a API de extensão do Pi e o layout do log de sessão se estabilizam. - - **Gemini CLI _(beta)_**: `~/.gemini/settings.json` (usuário), `/.gemini/settings.json` (projeto) — o Gemini não possui escopo `local` (ele documenta um escopo `system` em `/etc/gemini-cli/settings.json`, que failproofai não expõe). As entradas de hook usam o formato Claude `{type, command, timeout}` encapsulado no esquema de matcher do Gemini `{matcher, hooks: [...]}` com `matcher: "*"` por padrão. Os eventos são PascalCase (`SessionStart`, `BeforeAgent`, `AfterAgent`, `BeforeModel`, `AfterModel`, `BeforeToolSelection`, `BeforeTool`, `AfterTool`, `PreCompress`, `Notification`, `SessionEnd`); o handler mapeia para nomes canônicos do Claude via `GEMINI_EVENT_MAP`. Os nomes de ferramentas são snake_case (`run_shell_command`, `read_file`, `write_file`, `replace`, …) — o handler os canonicaliza via `GEMINI_TOOL_MAP` para que as políticas integradas existentes disparem sem alteração. O avaliador de políticas emite o formato plano `{decision: "deny", reason}` do Gemini (preferido conforme o contrato exit-0 da "Regra de Ouro" do Gemini), `{hookSpecificOutput: {hookEventName, additionalContext}}` para injeção de contexto em BeforeAgent / AfterTool / SessionStart, e `{decision: "block", reason}` em AfterAgent para semântica de força de nova tentativa. O suporte ao Gemini CLI está em **beta** enquanto ampliamos a cobertura em situações reais. Consulte a [documentação de hooks do Gemini CLI](https://geminicli.com/docs/hooks/). - **Hermes (hermes-agent)**: `~/.hermes/config.yaml` (**somente escopo de usuário** — o Hermes não possui configuração de projeto/local). O Hermes é um **gateway** para Slack/Telegram, portanto uma única instalação intercepta chamadas de ferramentas de todas as plataformas (Slack/Telegram/cli/cron) **e** de subagentes internos. As entradas de hook são um par `{command, timeout}` (timeout em **segundos**) sob um mapa `hooks:` com chave pelos eventos snake_case do Hermes (`pre_tool_call` / `post_tool_call` / `on_session_start` / `on_session_end` / `subagent_stop`); o handler canonicaliza eventos via `HERMES_EVENT_MAP` e nomes de ferramentas via `HERMES_TOOL_MAP` para que as políticas integradas disparem sem alteração. A configuração é editada por meio de uma edição de ida e volta de `Document` YAML que preserva comentários, para que as outras configurações do operador sobrevivam, e a instalação define `hooks_auto_accept: true` para que o gateway headless (sem TTY) execute os hooks sem uma solicitação de consentimento. O avaliador emite o contrato stdout `{"decision":"block","reason"}` do Hermes (o Hermes ignora códigos de saída). **Limitações:** O Hermes não possui evento `Stop` de fim de turno, portanto as políticas integradas `require-*-before-stop` nunca disparam para ele (inaplicável, não quebrado); `instruct` é rebaixado para allow com nota registrada (sem canal de contexto adicional); e a redação de segredos na saída (`sanitize-*`) não pode reescrever a saída das ferramentas pelo contrato de hook de shell. O Hermes é **também** uma fonte de **auditoria** offline — o dashboard lê suas sessões de gateway diretamente de `~/.hermes/state.db`. - **`policies-config.json`** — informa ao failproofai quais políticas avaliar e com quais parâmetros (compartilhado entre todos os agentes CLI) -Passe `--cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes` para direcionar um agente específico (separado por espaço ou repetido para qualquer subconjunto): +Passe `--cli claude|codex|copilot|cursor|opencode|pi|hermes` para direcionar um agente específico (separado por espaço ou repetido para qualquer subconjunto): ```bash failproofai policies --install --cli codex --scope project @@ -210,12 +209,11 @@ failproofai policies --install --cli copilot --scope project failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project -failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user -failproofai policies --install --cli claude codex copilot cursor opencode pi gemini +failproofai policies --install --cli claude codex copilot cursor opencode pi ``` -Quando `--cli` é omitido, `failproofai` detecta quais agentes CLI estão instalados (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which gemini` / `which hermes`): +Quando `--cli` é omitido, `failproofai` detecta quais agentes CLI estão instalados (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which hermes`): - **Um CLI detectado** — seleciona automaticamente esse CLI sem solicitar confirmação. - **Vários CLIs detectados** em um terminal interativo — exibe um prompt de seleção única com teclas de seta, agrupado em uma seção `Detected (N)` (com uma linha agregada `Install for all N detected` + cada CLI detectado individualmente) e uma seção `Not installed (M) · install hooks ahead of time` listando todos os CLIs suportados não detectados como opções de instalação antecipada (↑↓ para mover, Enter para selecionar, ^C para sair). O fluxo de desinstalação exibe apenas a seção Detected. diff --git a/docs/pt-br/dashboard.mdx b/docs/pt-br/dashboard.mdx index 7b2ed4d0..dd1a54d9 100644 --- a/docs/pt-br/dashboard.mdx +++ b/docs/pt-br/dashboard.mdx @@ -24,11 +24,11 @@ O dashboard lê diretamente do sistema de arquivos — suas pastas de projetos d ### Projetos -Lista todos os projetos Claude Code, OpenAI Codex, GitHub Copilot CLI _(beta)_, Cursor Agent _(beta)_, OpenCode _(beta)_, Pi _(beta)_, Gemini CLI _(beta)_ e Hermes encontrados na sua máquina. Os projetos Claude são descobertos em `~/.claude/projects/` (ou no caminho definido por `CLAUDE_PROJECTS_PATH`); os projetos Codex são descobertos varrendo todas as transcrições em `~/.codex/sessions///
/*.jsonl` e agrupando pelo `cwd` registrado no primeiro registro de cada sessão; os projetos Copilot CLI são descobertos varrendo cada `~/.copilot/session-state//workspace.yaml` (configurável via `COPILOT_HOME`) e agrupando pelo campo `cwd`; os projetos Cursor Agent são descobertos varrendo metadados por sessão em `~/.cursor/agent-sessions//` (configurável via `CURSOR_HOME`, com `conversations/` e `sessions/` como alternativas) em busca de um escalar `cwd` em `meta.json` / `session.json` / `workspace.yaml`; os projetos OpenCode são descobertos consultando seu banco SQLite em `~/.local/share/opencode/opencode.db` via `opencode db --format json` (lemos as tabelas `session` e `project` e agrupamos por `project_id`); os projetos Pi são descobertos varrendo transcrições JSONL por sessão em `~/.pi/agent/sessions//_.jsonl` (configurável via `PI_SESSIONS_DIR`) e extraindo o `cwd` do primeiro registro de cada sessão; os projetos Gemini CLI são descobertos varrendo `~/.gemini/tmp//chats/session--.jsonl` (configurável via `GEMINI_SESSIONS_DIR`) e recuperando o cwd canônico a partir do marcador de texto `.project_root` adjacente; as sessões do gateway Hermes são lidas diretamente do seu armazenamento SQLite em `~/.hermes/state.db` (configurável via `HERMES_DB_PATH`) e agrupadas em projetos `hermes-` por `source` (Slack/Telegram/cli/cron — sessões do gateway não possuem cwd). Um projeto utilizado por múltiplos CLIs é exibido como uma única linha com todos os badges correspondentes. Use o menu suspenso **CLI** acima da tabela para filtrar por um agente CLI específico; a URL preserva sua seleção como `?cli=claude|codex|copilot|cursor|opencode|pi|gemini|hermes`. +Lista todos os projetos Claude Code, OpenAI Codex, GitHub Copilot CLI _(beta)_, Cursor Agent _(beta)_, OpenCode _(beta)_, Pi _(beta)_ e Hermes encontrados na sua máquina. Os projetos Claude são descobertos em `~/.claude/projects/` (ou no caminho definido por `CLAUDE_PROJECTS_PATH`); os projetos Codex são descobertos varrendo todas as transcrições em `~/.codex/sessions///
/*.jsonl` e agrupando pelo `cwd` registrado no primeiro registro de cada sessão; os projetos Copilot CLI são descobertos varrendo cada `~/.copilot/session-state//workspace.yaml` (configurável via `COPILOT_HOME`) e agrupando pelo campo `cwd`; os projetos Cursor Agent são descobertos varrendo metadados por sessão em `~/.cursor/agent-sessions//` (configurável via `CURSOR_HOME`, com `conversations/` e `sessions/` como alternativas) em busca de um escalar `cwd` em `meta.json` / `session.json` / `workspace.yaml`; os projetos OpenCode são descobertos consultando seu banco SQLite em `~/.local/share/opencode/opencode.db` via `opencode db --format json` (lemos as tabelas `session` e `project` e agrupamos por `project_id`); os projetos Pi são descobertos varrendo transcrições JSONL por sessão em `~/.pi/agent/sessions//_.jsonl` (configurável via `PI_SESSIONS_DIR`) e extraindo o `cwd` do primeiro registro de cada sessão; as sessões do gateway Hermes são lidas diretamente do seu armazenamento SQLite em `~/.hermes/state.db` (configurável via `HERMES_DB_PATH`) e agrupadas em projetos `hermes-` por `source` (Slack/Telegram/cli/cron — sessões do gateway não possuem cwd). Um projeto utilizado por múltiplos CLIs é exibido como uma única linha com todos os badges correspondentes. Use o menu suspenso **CLI** acima da tabela para filtrar por um agente CLI específico; a URL preserva sua seleção como `?cli=claude|codex|copilot|cursor|opencode|pi|hermes`. Cada projeto exibe: - Nome do projeto (derivado do caminho da pasta) -- Um badge de CLI — `Claude Code` (laranja), `OpenAI Codex` (roxo), `GitHub Copilot` (azul), `Cursor Agent` (esmeralda), `OpenCode` (âmbar), `Pi` (rosa), `Gemini CLI` (celeste) e/ou `Hermes` (índigo) +- Um badge de CLI — `Claude Code` (laranja), `OpenAI Codex` (roxo), `GitHub Copilot` (azul), `Cursor Agent` (esmeralda), `OpenCode` (âmbar), `Pi` (rosa) e/ou `Hermes` (índigo) - Data da atividade mais recente da sessão Clique em um projeto para ver suas sessões. @@ -47,7 +47,7 @@ Clique em uma sessão para abrir o visualizador de sessão. ### Visualizador de sessão -O visualizador de sessão responde à pergunta central dos agentes autônomos: o que o agente fez, e ele se manteve no curso? Um badge de CLI ao lado do cabeçalho indica se a sessão é uma transcrição do Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi, Gemini CLI ou Hermes. Ele exibe uma linha do tempo de tudo que aconteceu em uma sessão: +O visualizador de sessão responde à pergunta central dos agentes autônomos: o que o agente fez, e ele se manteve no curso? Um badge de CLI ao lado do cabeçalho indica se a sessão é uma transcrição do Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi ou Hermes. Ele exibe uma linha do tempo de tudo que aconteceu em uma sessão: - **Mensagens** — Respostas de texto do Claude e prompts do usuário - **Chamadas de ferramentas** — Cada ferramenta que o Claude invocou, com sua entrada e saída @@ -55,7 +55,7 @@ O visualizador de sessão responde à pergunta central dos agentes autônomos: o A barra de estatísticas no topo mostra a duração da sessão, total de chamadas de ferramentas e um resumo das decisões de hook (contagens de allow / deny / instruct). -Clique no botão **Download Logs** para exportar a sessão. Para sessões do Claude Code, Codex, Copilot, Cursor, Pi e Gemini, você recebe a transcrição JSONL original em disco byte a byte; para o OpenCode (cujas sessões ficam no SQLite, não em disco) você recebe um documento JSON espelhando as tabelas subjacentes `session` / `messages` / `parts`. +Clique no botão **Download Logs** para exportar a sessão. Para sessões do Claude Code, Codex, Copilot, Cursor e Pi, você recebe a transcrição JSONL original em disco byte a byte; para o OpenCode (cujas sessões ficam no SQLite, não em disco) você recebe um documento JSON espelhando as tabelas subjacentes `session` / `messages` / `parts`. ### Auditoria @@ -75,16 +75,16 @@ Uma página com duas abas para gerenciar políticas e revisar atividade. - - Selecione múltiplos CLIs de agentes que o failproofai protege em um único painel — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi, Gemini CLI e Hermes têm uma linha com status de instalação (`Active` / `Detected` / `Inactive`), o caminho de configurações de escopo do usuário e um destaque com a cor da marca. Marque ou desmarque os CLIs desejados e clique em `Apply changes` para instalar/desinstalar as alterações em uma etapa. CLIs cujo binário é detectado no PATH são pré-selecionados. + - Selecione múltiplos CLIs de agentes que o failproofai protege em um único painel — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi e Hermes têm uma linha com status de instalação (`Active` / `Detected` / `Inactive`), o caminho de configurações de escopo do usuário e um destaque com a cor da marca. Marque ou desmarque os CLIs desejados e clique em `Apply changes` para instalar/desinstalar as alterações em uma etapa. CLIs cujo binário é detectado no PATH são pré-selecionados. - Ative ou desative políticas individuais com um único clique (escreve em `~/.failproofai/policies-config.json` — compartilhado entre todos os CLIs instalados) - Expanda uma política para configurar seus parâmetros (para políticas que suportam `policyParams`) - Defina um caminho personalizado para o arquivo de políticas - Histórico paginado completo de cada evento de hook que foi acionado em todas as sessões - - Filtre por decisão, tipo de evento, CLI (Claude Code / OpenAI Codex / GitHub Copilot _(beta)_ / Cursor Agent _(beta)_ / OpenCode _(beta)_ / Pi _(beta)_ / Gemini CLI _(beta)_ / Hermes), nome da política ou ID de sessão - - Cada linha exibe: timestamp, nome da política, decisão, badge de CLI (laranja = Claude Code, roxo = OpenAI Codex, azul = GitHub Copilot, esmeralda = Cursor Agent, âmbar = OpenCode, rosa = Pi, celeste = Gemini CLI, índigo = Hermes), nome da ferramenta, ID de sessão e o motivo das decisões deny/instruct - - Clique em um ID de sessão para abrir sua transcrição — o visualizador detecta automaticamente qual CLI acionou o hook (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Gemini CLI `~/.gemini/tmp//chats/.jsonl`, Hermes `~/.hermes/state.db`) e renderiza o badge de CLI correspondente no cabeçalho + - Filtre por decisão, tipo de evento, CLI (Claude Code / OpenAI Codex / GitHub Copilot _(beta)_ / Cursor Agent _(beta)_ / OpenCode _(beta)_ / Pi _(beta)_ / Hermes), nome da política ou ID de sessão + - Cada linha exibe: timestamp, nome da política, decisão, badge de CLI (laranja = Claude Code, roxo = OpenAI Codex, azul = GitHub Copilot, esmeralda = Cursor Agent, âmbar = OpenCode, rosa = Pi, índigo = Hermes), nome da ferramenta, ID de sessão e o motivo das decisões deny/instruct + - Clique em um ID de sessão para abrir sua transcrição — o visualizador detecta automaticamente qual CLI acionou o hook (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Hermes `~/.hermes/state.db`) e renderiza o badge de CLI correspondente no cabeçalho diff --git a/docs/pt-br/getting-started.mdx b/docs/pt-br/getting-started.mdx index 133c5565..f9091965 100644 --- a/docs/pt-br/getting-started.mdx +++ b/docs/pt-br/getting-started.mdx @@ -37,9 +37,9 @@ bun add -g failproofai failproofai policies --install ``` - Isso registra entradas de hook nos CLIs de agente instalados (o `~/.claude/settings.json` do Claude Code, o `~/.codex/hooks.json` do OpenAI Codex, o `~/.copilot/hooks/failproofai.json` do GitHub Copilot CLI, o `~/.cursor/hooks.json` do Cursor Agent, o shim de plugin gerado pelo OpenCode em `~/.config/opencode/plugins/failproofai.mjs` mais uma entrada de registro no array `plugin` de `~/.config/opencode/opencode.json`, o `~/.pi/agent/settings.json` do Pi, o `~/.gemini/settings.json` do Gemini CLI ou o `~/.hermes/config.yaml` do Hermes). Se mais de um estiver presente, você será solicitado a escolher; passe `--cli claude codex copilot cursor opencode pi gemini hermes` (qualquer subconjunto) para pular a seleção. + Isso registra entradas de hook nos CLIs de agente instalados (o `~/.claude/settings.json` do Claude Code, o `~/.codex/hooks.json` do OpenAI Codex, o `~/.copilot/hooks/failproofai.json` do GitHub Copilot CLI, o `~/.cursor/hooks.json` do Cursor Agent, o shim de plugin gerado pelo OpenCode em `~/.config/opencode/plugins/failproofai.mjs` mais uma entrada de registro no array `plugin` de `~/.config/opencode/opencode.json`, o `~/.pi/agent/settings.json` do Pi ou o `~/.hermes/config.yaml` do Hermes). Se mais de um estiver presente, você será solicitado a escolher; passe `--cli claude codex copilot cursor opencode pi hermes` (qualquer subconjunto) para pular a seleção. - O suporte ao GitHub Copilot CLI, Cursor Agent, OpenCode, Pi e Gemini CLI está em **beta** — instale com `--cli copilot`, `--cli cursor`, `--cli opencode`, `--cli pi` ou `--cli gemini`. O Hermes (hermes-agent, um gateway Slack/Telegram) é instalado no escopo de usuário com `--cli hermes` e é **também** uma fonte de auditoria offline. + O suporte ao GitHub Copilot CLI, Cursor Agent, OpenCode e Pi está em **beta** — instale com `--cli copilot`, `--cli cursor`, `--cli opencode` ou `--cli pi`. O Hermes (hermes-agent, um gateway Slack/Telegram) é instalado no escopo de usuário com `--cli hermes` e é **também** uma fonte de auditoria offline. ```bash failproofai policies --install --scope project @@ -48,7 +48,6 @@ bun add -g failproofai failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project - failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user failproofai policies --install block-sudo block-rm-rf sanitize-api-keys ``` diff --git a/docs/pt-br/introduction.mdx b/docs/pt-br/introduction.mdx index aaf26213..311dca85 100644 --- a/docs/pt-br/introduction.mdx +++ b/docs/pt-br/introduction.mdx @@ -5,13 +5,13 @@ description: "FailproofAI oferece 39 políticas de falha integradas para agentes [![npm weekly downloads](https://img.shields.io/npm/dw/failproofai?style=flat-square&color=2ea44f)](https://www.npmjs.com/package/failproofai) -Hooks e políticas para **tratamento de falhas de IA**, **recuperação de erros** e **confiabilidade de LLMs**. Mantenha seus agentes de IA confiáveis e funcionando de forma autônoma no **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Gemini CLI**, **Hermes** e no **Agents SDK**. +Hooks e políticas para **tratamento de falhas de IA**, **recuperação de erros** e **confiabilidade de LLMs**. Mantenha seus agentes de IA confiáveis e funcionando de forma autônoma no **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Hermes** e no **Agents SDK**. Agentes de IA falham de maneiras previsíveis. Eles executam comandos destrutivos, vazam segredos, desviam da tarefa, ficam presos em loops ou fazem push direto para a branch principal. Sem supervisão, falhas pequenas evoluem para quedas de serviço, credenciais expostas e trabalho perdido. O FailproofAI resolve isso com **políticas**. Essas regras se integram a cada chamada de ferramenta do agente para **detectar falhas**, **mitigá-las** (bloquear, instruir, sanitizar) e **alertar você** quando algo precisa de atenção. Um painel local permite revisar todas as chamadas de ferramenta, falhas do agente e ações de recuperação posteriormente. -Nenhum dado sai da sua máquina. +As transcrições e a avaliação de políticas permanecem na sua máquina. Os dados só são enviados quando você usa explicitamente um recurso online, como lembretes de auditoria ou convites autenticados. ## Primeiros passos @@ -54,4 +54,4 @@ failproofai policies --install # enable policies (or skip — `failproofai` wi failproofai # launch the dashboard ``` -Consulte o guia de [Primeiros passos](/pt-br/getting-started) para o passo a passo completo. \ No newline at end of file +Consulte o guia de [Primeiros passos](/pt-br/getting-started) para o passo a passo completo. diff --git a/docs/ru/built-in-policies.mdx b/docs/ru/built-in-policies.mdx index 4a02f746..24211560 100644 --- a/docs/ru/built-in-policies.mdx +++ b/docs/ru/built-in-policies.mdx @@ -712,20 +712,19 @@ failproofai поставляется с 39 встроенными политик | Codex | Тот же цикл агента, сразу | То же самое, что Claude. | | GitHub Copilot CLI | Тот же цикл агента, сразу | То же самое, что Claude (использует канал повтора `{decision:"block", reason}` Copilot — проверено эмпирически на Copilot CLI 1.0.41). | | Cursor Agent | Тот же цикл агента, сразу | То же самое, что Claude (использует канал `{followup_message}` Cursor — ограничен `loop_limit`, по умолчанию 5 повторов). | -| Gemini CLI | Тот же цикл агента, сразу | То же самое, что Claude (использует канал `{decision:"block", reason}` Gemini на `AfterAgent`). | | OpenCode | Тот же цикл агента, сразу | То же самое, что Claude (использует вызов SDK `client.session.prompt(...)` OpenCode, маршрутизированный через `hookSpecificOutput.additionalContext`). | | **Pi (pi-coding-agent)** | **Следующий ход пользователя** | **Pi видимо останавливается**, когда срабатывает шлюз — его цикл агента завершается и вы возвращаетесь к приглашению. Шлюз затем срабатывает при следующей отправке приглашения: failproofai добавляет директиву `MANDATORY ACTION REQUIRED` в системное приглашение того хода, инструктируя LLM завершить шаг рабочего процесса (commit, push и т. д.) перед выполнением запрашиваемого вами действия. | -**Ограничение Pi.** `AgentEndEvent` Pi (эквивалент `Stop` хука Claude) не имеет типа Result — по времени его срабатывания цикл агента Pi уже завершился. Pi не может быть принуждена повторить тот же цикл, как Claude / Copilot / Cursor / Gemini / OpenCode. failproofai смещает шлюз к событию `before_agent_start` Pi (которое срабатывает после следующего приглашения пользователя), чтобы проверка рабочего процесса всё ещё применялась, просто при следующем ходе вместо текущего. +**Ограничение Pi.** `AgentEndEvent` Pi (эквивалент `Stop` хука Claude) не имеет типа Result — по времени его срабатывания цикл агента Pi уже завершился. Pi не может быть принуждена повторить тот же цикл, как Claude / Copilot / Cursor / OpenCode. failproofai смещает шлюз к событию `before_agent_start` Pi (которое срабатывает после следующего приглашения пользователя), чтобы проверка рабочего процесса всё ещё применялась, просто при следующем ходе вместо текущего. **Что это означает на практике:** - После остановки Pi причина deny захватывается в памяти с ключом по id сессии Pi. Самое следующее приглашение, которое вы отправляете в той же сессии Pi, его извлекает: LLM видит директиву `MANDATORY ACTION REQUIRED` в верхней части своего системного приглашения, commit-ит (или push-ит / открывает PR / ждёт CI) и только затем продолжает с вашим запросом. Захваченная причина deny — одноразовая — после извлечения шлюз свободен. -- Шлюз ограничен временем жизни процесса Pi. Если вы `Ctrl+C` Pi или выход между ходами, запись в памяти удаляется вместе с процессом и шлюз пропускается. Claude, Copilot, Cursor, Gemini и OpenCode имеют то же самое ограничение (kill агента и шлюз пропускается) — Pi просто делает это более видимым, потому что агент видимо завершает работу перед срабатыванием шлюза. +- Шлюз ограничен временем жизни процесса Pi. Если вы `Ctrl+C` Pi или выход между ходами, запись в памяти удаляется вместе с процессом и шлюз пропускается. Claude, Copilot, Cursor и OpenCode имеют то же самое ограничение (kill агента и шлюз пропускается) — Pi просто делает это более видимым, потому что агент видимо завершает работу перед срабатыванием шлюза. - Ожидающий deny также очищается на `session_shutdown` по любой причине (`new` / `resume` / `fork` / `quit`), поэтому устаревший шлюз из предыдущей сессии не может утечь в свежую сессию, начатую в том же процессе Pi. -Если вам нужен повтор в том же цикле, как Claude, запустите ваши политики `Stop` в любом из шести других поддерживаемых CLI. Мы отслеживаем Pi upstream на предмет будущего типа Result на `AgentEndEvent`, который позволил бы нам закрыть этот пробел. +Если вам нужен повтор в том же цикле, как Claude, запустите ваши политики `Stop` в любом из пяти других поддерживаемых CLI. Мы отслеживаем Pi upstream на предмет будущего типа Result на `AgentEndEvent`, который позволил бы нам закрыть этот пробел. ### `require-commit-before-stop` diff --git a/docs/ru/cli/audit.mdx b/docs/ru/cli/audit.mdx index d3693eec..a140379d 100644 --- a/docs/ru/cli/audit.mdx +++ b/docs/ru/cli/audit.mdx @@ -52,7 +52,7 @@ failproofai до тех пор, пока вы не остановите её с помощью `Ctrl+C`. -Панель сканирует прошлые транскрипты агента CLI на этой машине (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi, Gemini) и сообщает, как часто агент выполнял операции, которые failproofai создан, чтобы остановить — проверки переменных окружения, force push, повторяющиеся префиксы `cd `, sleep-polling циклы, повторное чтение только что отредактированных файлов и многое другое. +Панель сканирует прошлые транскрипты агента CLI на этой машине (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi) и сообщает, как часто агент выполнял операции, которые failproofai создан, чтобы остановить — проверки переменных окружения, force push, повторяющиеся префиксы `cd `, sleep-polling циклы, повторное чтение только что отредактированных файлов и многое другое. Для каждого транскрипта каждое событие использования инструмента воспроизводится через 39 встроенных политик **и** через 8 аудит-специфичных детекторов, которые выявляют паттерны, еще не покрытые политиками реального времени. Подсчеты агрегируются для каждой политики/детектора по всем сеансам. diff --git a/docs/ru/configuration.mdx b/docs/ru/configuration.mdx index 8a9c41e9..5445f018 100644 --- a/docs/ru/configuration.mdx +++ b/docs/ru/configuration.mdx @@ -196,13 +196,12 @@ resolved: { allowPatterns: ["sudo systemctl status"] } ← переходит - **OpenAI Codex**: `~/.codex/hooks.json` (пользователь), `/.codex/hooks.json` (проект) — Codex не имеет локальной области - **GitHub Copilot CLI _(бета)_**: `~/.copilot/hooks/failproofai.json` (пользователь), `/.github/hooks/failproofai.json` (проект) — Copilot не имеет локальной области. Записи хуков используют поля команд `bash`/`powershell` с ключом ОС и `timeoutSec` Copilot; файл содержит маркер `version: 1` верхнего уровня. Поддержка Copilot CLI находится в **бета-версии**, пока мы проверяем схему записи `events.jsonl` (которую общедоступные документы не указывают) против дополнительных реальных сессий. - **Cursor Agent _(бета)_**: `~/.cursor/hooks.json` (пользователь), `/.cursor/hooks.json` (проект) — Cursor не имеет локальной области. Записи хуков используют форму с Claude-подобной формой `{type, command, timeout}` (без разделения `bash`/`powershell`), но хранятся под camelCase ключами событий (`preToolUse`, `beforeSubmitPrompt`, …) в плоском массиве согласно [схеме хуков](https://cursor.com/docs/hooks) Cursor; файл содержит маркер `version: 1` верхнего уровня. Обработчик канонизирует camelCase → PascalCase через `CURSOR_EVENT_MAP`, поэтому существующие встроенные политики срабатывают без изменений. Поддержка Cursor Agent находится в **бета-версии**, пока мы проверяем транскрипт Cursor на диске (не указан в общедоступных документах) против дополнительных реальных установок. - - **OpenCode _(бета)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (пользователь), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (проект) — OpenCode не имеет локальной области. В отличие от остальных шести CLI, OpenCode **не имеет системы внешних команд для хуков**: он загружает в процессе плагины JS/TS, явно зарегистрированные через массив `plugin: []` в `opencode.json` (автообнаружение из `.opencode/plugins/` **не** это как загружаются плагины в opencode v1.14.33). Install помещает маленький сгенерированный шим плагина, который subprocess-вызывает бинарный файл failproofai и переводит JSON-ответ бинарного файла Claude-подобной формы обратно в семантику плагина: `throw new Error()` для отрицания tool-события (отменяет вызов инструмента), `client.session.prompt(...)` для instruct И для отрицания `Stop` / `SubagentStop` (отправляет причину отрицания как следующее пользовательское сообщение — единственный канал force-retry, так как `session.idle` только для уведомления и выброс из неё — это no-op), и no-op для allow. Шим канонизирует названия инструментов (lowercase → PascalCase через `OPENCODE_TOOL_MAP`) и ключи аргументов инструментов (camelCase → snake_case через `OPENCODE_TOOL_INPUT_MAP` для `Read` / `Write` / `Edit`, например `filePath` → `file_path`, `oldString` → `old_string`) перед отправкой в бинарный файл, поэтому встроенные проверки путей вроде `block-read-outside-cwd`, `block-env-files` и `block-secrets-write` срабатывают без изменений для вызовов инструментов OpenCode. Сессии живут в БД SQLite OpenCode в `~/.local/share/opencode/opencode.db`; средство просмотра сессий на панели инструментов читает их через `opencode db --format json` и `opencode export `. Поддержка OpenCode находится в **бета-версии**, пока мы проверяем поведение в разных версиях и против дополнительных реальных сессий. См. [документацию плагинов OpenCode](https://opencode.ai/docs/plugins/). + - **OpenCode _(бета)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (пользователь), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (проект) — OpenCode не имеет локальной области. В отличие от остальных пяти CLI, OpenCode **не имеет системы внешних команд для хуков**: он загружает в процессе плагины JS/TS, явно зарегистрированные через массив `plugin: []` в `opencode.json` (автообнаружение из `.opencode/plugins/` **не** это как загружаются плагины в opencode v1.14.33). Install помещает маленький сгенерированный шим плагина, который subprocess-вызывает бинарный файл failproofai и переводит JSON-ответ бинарного файла Claude-подобной формы обратно в семантику плагина: `throw new Error()` для отрицания tool-события (отменяет вызов инструмента), `client.session.prompt(...)` для instruct И для отрицания `Stop` / `SubagentStop` (отправляет причину отрицания как следующее пользовательское сообщение — единственный канал force-retry, так как `session.idle` только для уведомления и выброс из неё — это no-op), и no-op для allow. Шим канонизирует названия инструментов (lowercase → PascalCase через `OPENCODE_TOOL_MAP`) и ключи аргументов инструментов (camelCase → snake_case через `OPENCODE_TOOL_INPUT_MAP` для `Read` / `Write` / `Edit`, например `filePath` → `file_path`, `oldString` → `old_string`) перед отправкой в бинарный файл, поэтому встроенные проверки путей вроде `block-read-outside-cwd`, `block-env-files` и `block-secrets-write` срабатывают без изменений для вызовов инструментов OpenCode. Сессии живут в БД SQLite OpenCode в `~/.local/share/opencode/opencode.db`; средство просмотра сессий на панели инструментов читает их через `opencode db --format json` и `opencode export `. Поддержка OpenCode находится в **бета-версии**, пока мы проверяем поведение в разных версиях и против дополнительных реальных сессий. См. [документацию плагинов OpenCode](https://opencode.ai/docs/plugins/). - **Pi _(бета)_**: `~/.pi/agent/settings.json` (пользователь), `/.pi/settings.json` (проект) — Pi не имеет локальной области. Pi загружает пакеты расширений TypeScript при запуске; файл параметров — это плоский массив строк `{"packages": ["./relative/path", …]}`. failproofai записывает одну запись packages-массива, указывающую на встроенную директорию `pi-extension/`. Расширение внутренне подписывается на события Pi `tool_call` / `user_bash` / `input` / `session_start` и shell-вызывает `failproofai --hook --cli pi`; обработчик канонизирует underscore_lower_snake_case → PascalCase через `PI_EVENT_MAP`, поэтому существующие встроенные политики срабатывают без изменений. Аргументы инструментов также канонизируются через `PI_TOOL_INPUT_MAP` (Pi's Read / Write / Edit доставляют `path` вместо `file_path`; картирование верхнего уровня позволяет `block-env-files` и `block-secrets-write` срабатывать — `block-read-outside-cwd` уже имел fallback для `path`). Поддержка Pi находится в **бета-версии**, пока API расширений Pi и макет журнала сессий стабилизируются. - - **Gemini CLI _(бета)_**: `~/.gemini/settings.json` (пользователь), `/.gemini/settings.json` (проект) — Gemini не имеет локальной области (она документирует область `system` в `/etc/gemini-cli/settings.json`, которую failproofai не предоставляет). Записи хуков используют форму Claude's `{type, command, timeout}`, обёрнутую в схему matcher Gemini's `{matcher, hooks: [...]}` с `matcher: "*"` по умолчанию. События — PascalCase (`SessionStart`, `BeforeAgent`, `AfterAgent`, `BeforeModel`, `AfterModel`, `BeforeToolSelection`, `BeforeTool`, `AfterTool`, `PreCompress`, `Notification`, `SessionEnd`); обработчик картирует на имена Claude canonical через `GEMINI_EVENT_MAP`. Названия инструментов — snake_case (`run_shell_command`, `read_file`, `write_file`, `replace`, …) — обработчик канонизирует через `GEMINI_TOOL_MAP`, поэтому существующие встроенные политики срабатывают без изменений. Оценщик политик выдаёт плоскую форму Gemini's `{decision: "deny", reason}` (предпочтено по Gemini's Golden Rule контракту exit-0), `{hookSpecificOutput: {hookEventName, additionalContext}}` для внедрения контекста в BeforeAgent / AfterTool / SessionStart, и `{decision: "block", reason}` на AfterAgent для семантики force-retry. Поддержка Gemini CLI находится в **бета-версии**, пока мы расширяем реальное покрытие. См. [документацию хуков Gemini CLI](https://geminicli.com/docs/hooks/). - **Hermes (hermes-agent)**: `~/.hermes/config.yaml` (**только область пользователя** — Hermes не имеет конфига проекта/локально). Hermes — это **шлюз** Slack/Telegram, поэтому одна установка перехватывает вызовы инструментов от каждой платформы (Slack/Telegram/cli/cron) **и** внутренние подагенты. Записи хуков — это пара `{command, timeout}` (timeout в **секундах**) под картой `hooks:` с ключами от snake_case событий Hermes (`pre_tool_call` / `post_tool_call` / `on_session_start` / `on_session_end` / `subagent_stop`); обработчик канонизирует события через `HERMES_EVENT_MAP` и названия инструментов через `HERMES_TOOL_MAP`, поэтому встроенные политики срабатывают без изменений. Конфиг редактируется через коммент-сохраняющий YAML round-trip `Document`, поэтому остальные параметры оператора выживают, и install устанавливает `hooks_auto_accept: true`, чтобы headless шлюз (без TTY) запускал хуки без промпта согласия. Оценщик выдаёт контракт `{"decision":"block","reason"}` stdout Hermes (Hermes игнорирует коды выхода). **Ограничения:** Hermes не имеет event end-turn `Stop`, поэтому встроенные `require-*-before-stop` никогда не срабатывают для него (неприменимо, не сломано); `instruct` деградирует до allow-with-logged-note (нет канала дополнительного контекста); и переписывание выходных секретов (`sanitize-*`) не может переписать вывод инструмента через shell-hook контракт. Hermes — **также** оффлайн **audit** источник — панель инструментов читает сессии шлюза напрямую из `~/.hermes/state.db`. - **`policies-config.json`** — указывает failproofai, какие политики оценивать и с какими параметрами (общее для всех CLI агентов) -Передайте `--cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes` для выбора конкретного агента (разделённые пробелом или повторённые для подмножества): +Передайте `--cli claude|codex|copilot|cursor|opencode|pi|hermes` для выбора конкретного агента (разделённые пробелом или повторённые для подмножества): ```bash failproofai policies --install --cli codex --scope project @@ -210,12 +209,11 @@ failproofai policies --install --cli copilot --scope project failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project -failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user -failproofai policies --install --cli claude codex copilot cursor opencode pi gemini +failproofai policies --install --cli claude codex copilot cursor opencode pi ``` -Когда `--cli` опущен, `failproofai` обнаруживает, какие CLI агентов установлены (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which gemini` / `which hermes`): +Когда `--cli` опущен, `failproofai` обнаруживает, какие CLI агентов установлены (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which hermes`): - **Один CLI обнаружен** — автоматически выбирает тот CLI без запроса. - **Несколько CLI обнаружено** в интерактивном терминале — показывает однострочный prompt выбора со стрелками, сгруппированный в раздел `Detected (N)` (с агрегированной строкой `Install for all N detected` + каждый обнаруженный CLI отдельно) и раздел `Not installed (M) · install hooks ahead of time`, перечисляющий каждый неподдерживаемый CLI как опцию forward-install (↑↓ для перемещения, Enter для выбора, ^C для выхода). Flow разустановки показывает только раздел Detected. diff --git a/docs/ru/dashboard.mdx b/docs/ru/dashboard.mdx index 416ba866..d9ccf3d5 100644 --- a/docs/ru/dashboard.mdx +++ b/docs/ru/dashboard.mdx @@ -24,11 +24,11 @@ failproofai ### Projects -Список всех найденных на вашем компьютере проектов Claude Code, OpenAI Codex, GitHub Copilot CLI _(beta)_, Cursor Agent _(beta)_, OpenCode _(beta)_, Pi _(beta)_, Gemini CLI _(beta)_ и Hermes. Проекты Claude обнаруживаются из `~/.claude/projects/` (или пути, установленного в `CLAUDE_PROJECTS_PATH`); проекты Codex обнаруживаются путём сканирования каждой транскрипции под `~/.codex/sessions///
/*.jsonl` и группировки по `cwd`, записанному в первой записи каждого сеанса; проекты Copilot CLI обнаруживаются путём сканирования каждого `~/.copilot/session-state//workspace.yaml` (настраивается через `COPILOT_HOME`) и группировки по полю `cwd`; проекты Cursor Agent обнаруживаются путём сканирования метаданных для каждого сеанса под `~/.cursor/agent-sessions//` (настраивается через `CURSOR_HOME`, со счётом `conversations/` и `sessions/` в качестве резервных вариантов) для скаляра `cwd` в `meta.json` / `session.json` / `workspace.yaml`; проекты OpenCode обнаруживаются путём запроса к его БД SQLite на `~/.local/share/opencode/opencode.db` через `opencode db --format json` (мы читаем таблицы `session` и `project` и группируем по `project_id`); проекты Pi обнаруживаются путём сканирования транскрипций JSONL для каждого сеанса под `~/.pi/agent/sessions//_.jsonl` (настраивается через `PI_SESSIONS_DIR`) и извлечения `cwd` из первой записи каждого сеанса; проекты Gemini CLI обнаруживаются путём сканирования `~/.gemini/tmp//chats/session--.jsonl` (настраивается через `GEMINI_SESSIONS_DIR`) и восстановления канонического cwd из текстового маркера `.project_root`; сеансы шлюза Hermes читаются непосредственно из его хранилища SQLite на `~/.hermes/state.db` (настраивается через `HERMES_DB_PATH`) и группируются в проекты `hermes-` по `source` (Slack/Telegram/cli/cron — сеансы шлюза не имеют cwd). Проект, который использовался несколькими CLI, отображается как одна строка со всеми соответствующими значками. Используйте выпадающее меню **CLI** над таблицей для фильтрации по конкретному агенту; URL сохраняет ваш выбор как `?cli=claude|codex|copilot|cursor|opencode|pi|gemini|hermes`. +Список всех найденных на вашем компьютере проектов Claude Code, OpenAI Codex, GitHub Copilot CLI _(beta)_, Cursor Agent _(beta)_, OpenCode _(beta)_, Pi _(beta)_ и Hermes. Проекты Claude обнаруживаются из `~/.claude/projects/` (или пути, установленного в `CLAUDE_PROJECTS_PATH`); проекты Codex обнаруживаются путём сканирования каждой транскрипции под `~/.codex/sessions///
/*.jsonl` и группировки по `cwd`, записанному в первой записи каждого сеанса; проекты Copilot CLI обнаруживаются путём сканирования каждого `~/.copilot/session-state//workspace.yaml` (настраивается через `COPILOT_HOME`) и группировки по полю `cwd`; проекты Cursor Agent обнаруживаются путём сканирования метаданных для каждого сеанса под `~/.cursor/agent-sessions//` (настраивается через `CURSOR_HOME`, со счётом `conversations/` и `sessions/` в качестве резервных вариантов) для скаляра `cwd` в `meta.json` / `session.json` / `workspace.yaml`; проекты OpenCode обнаруживаются путём запроса к его БД SQLite на `~/.local/share/opencode/opencode.db` через `opencode db --format json` (мы читаем таблицы `session` и `project` и группируем по `project_id`); проекты Pi обнаруживаются путём сканирования транскрипций JSONL для каждого сеанса под `~/.pi/agent/sessions//_.jsonl` (настраивается через `PI_SESSIONS_DIR`) и извлечения `cwd` из первой записи каждого сеанса; сеансы шлюза Hermes читаются непосредственно из его хранилища SQLite на `~/.hermes/state.db` (настраивается через `HERMES_DB_PATH`) и группируются в проекты `hermes-` по `source` (Slack/Telegram/cli/cron — сеансы шлюза не имеют cwd). Проект, который использовался несколькими CLI, отображается как одна строка со всеми соответствующими значками. Используйте выпадающее меню **CLI** над таблицей для фильтрации по конкретному агенту; URL сохраняет ваш выбор как `?cli=claude|codex|copilot|cursor|opencode|pi|hermes`. Каждый проект показывает: - Имя проекта (производное от пути папки) -- Значок CLI — `Claude Code` (оранжевый), `OpenAI Codex` (фиолетовый), `GitHub Copilot` (синий), `Cursor Agent` (изумруд), `OpenCode` (янтарь), `Pi` (розовый), `Gemini CLI` (небесно-голубой) и/или `Hermes` (индиго) +- Значок CLI — `Claude Code` (оранжевый), `OpenAI Codex` (фиолетовый), `GitHub Copilot` (синий), `Cursor Agent` (изумруд), `OpenCode` (янтарь), `Pi` (розовый) и/или `Hermes` (индиго) - Дата последней активности сеанса Щёлкните по проекту, чтобы просмотреть его сеансы. @@ -47,7 +47,7 @@ failproofai ### Session viewer -Средство просмотра сеанса отвечает на ключевой вопрос для автономных агентов: что сделал агент и остался ли он на курсе? Значок CLI рядом с заголовком указывает, является ли сеанс транскрипцией Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi, Gemini CLI или Hermes. Он показывает временную шкалу всего, что произошло в сеансе: +Средство просмотра сеанса отвечает на ключевой вопрос для автономных агентов: что сделал агент и остался ли он на курсе? Значок CLI рядом с заголовком указывает, является ли сеанс транскрипцией Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi или Hermes. Он показывает временную шкалу всего, что произошло в сеансе: - **Messages** — текстовые ответы Claude и подсказки пользователя - **Tool calls** — каждый инструмент, который вызвал Claude, с его входными и выходными данными @@ -55,7 +55,7 @@ failproofai Панель статистики вверху показывает длительность сеанса, общее количество вызовов инструментов и сводку решений хуков (счётчики allow / deny / instruct). -Нажмите кнопку **Download Logs** для экспорта сеанса. Для сеансов Claude Code, Codex, Copilot, Cursor, Pi и Gemini вы получаете исходную транскрипцию JSONL на диске в исходном виде; для OpenCode (чьи сеансы хранятся в SQLite, а не на диске) вы получаете JSON-документ, отражающий основные таблицы `session` / `messages` / `parts`. +Нажмите кнопку **Download Logs** для экспорта сеанса. Для сеансов Claude Code, Codex, Copilot, Cursor и Pi вы получаете исходную транскрипцию JSONL на диске в исходном виде; для OpenCode (чьи сеансы хранятся в SQLite, а не на диске) вы получаете JSON-документ, отражающий основные таблицы `session` / `messages` / `parts`. ### Audit @@ -75,16 +75,16 @@ failproofai - - Выберите несколько агентов CLI, которых защищает failproofai, из одной панели — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi, Gemini CLI и Hermes все имеют строку со статусом установки (`Active` / `Detected` / `Inactive`), путём пользовательской области видимости и фирменный цветной акцент. Отметьте или снимите отметку с CLI, которые вы хотите, и нажмите `Apply changes` для установки/удаления разницы за один шаг. CLI, чьи двоичные файлы обнаружены в PATH, предварительно отмечены. + - Выберите несколько агентов CLI, которых защищает failproofai, из одной панели — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi и Hermes все имеют строку со статусом установки (`Active` / `Detected` / `Inactive`), путём пользовательской области видимости и фирменный цветной акцент. Отметьте или снимите отметку с CLI, которые вы хотите, и нажмите `Apply changes` для установки/удаления разницы за один шаг. CLI, чьи двоичные файлы обнаружены в PATH, предварительно отмечены. - Переключайте отдельные политики вкл/выкл одним щелчком (записывает в `~/.failproofai/policies-config.json` — общее для каждого установленного CLI) - Разверните политику для настройки её параметров (для политик, поддерживающих `policyParams`) - Установите путь пользовательского файла политик - Полная постраничная история каждого события хука, срабатывающего во всех сеансах - - Фильтруйте по решению, типу события, CLI (Claude Code / OpenAI Codex / GitHub Copilot _(beta)_ / Cursor Agent _(beta)_ / OpenCode _(beta)_ / Pi _(beta)_ / Gemini CLI _(beta)_ / Hermes), имени политики или ID сеанса - - Каждая строка показывает: временную метку, имя политики, решение, значок CLI (оранжевый = Claude Code, фиолетовый = OpenAI Codex, синий = GitHub Copilot, изумруд = Cursor Agent, янтарь = OpenCode, розовый = Pi, небесно-голубой = Gemini CLI, индиго = Hermes), имя инструмента, ID сеанса и причину решений deny/instruct - - Щёлкните по ID сеанса, чтобы открыть его транскрипцию — средство просмотра автоматически обнаруживает, какой CLI срабатил хук (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Gemini CLI `~/.gemini/tmp//chats/.jsonl`, Hermes `~/.hermes/state.db`) и отображает соответствующий значок CLI в заголовке + - Фильтруйте по решению, типу события, CLI (Claude Code / OpenAI Codex / GitHub Copilot _(beta)_ / Cursor Agent _(beta)_ / OpenCode _(beta)_ / Pi _(beta)_ / Hermes), имени политики или ID сеанса + - Каждая строка показывает: временную метку, имя политики, решение, значок CLI (оранжевый = Claude Code, фиолетовый = OpenAI Codex, синий = GitHub Copilot, изумруд = Cursor Agent, янтарь = OpenCode, розовый = Pi, индиго = Hermes), имя инструмента, ID сеанса и причину решений deny/instruct + - Щёлкните по ID сеанса, чтобы открыть его транскрипцию — средство просмотра автоматически обнаруживает, какой CLI срабатил хук (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Hermes `~/.hermes/state.db`) и отображает соответствующий значок CLI в заголовке diff --git a/docs/ru/getting-started.mdx b/docs/ru/getting-started.mdx index 26e9d490..fa1e20b0 100644 --- a/docs/ru/getting-started.mdx +++ b/docs/ru/getting-started.mdx @@ -37,9 +37,9 @@ bun add -g failproofai failproofai policies --install ``` - Эта команда записывает записи хуков в установленные CLI агентов (Claude Code's `~/.claude/settings.json`, OpenAI Codex's `~/.codex/hooks.json`, GitHub Copilot CLI's `~/.copilot/hooks/failproofai.json`, Cursor Agent's `~/.cursor/hooks.json`, OpenCode's generated plugin shim at `~/.config/opencode/plugins/failproofai.mjs` plus a registration entry in `~/.config/opencode/opencode.json`'s `plugin` array, Pi's `~/.pi/agent/settings.json`, Gemini CLI's `~/.gemini/settings.json`, или Hermes's `~/.hermes/config.yaml`). Если присутствует несколько, вам будет предложено выбрать; передайте `--cli claude codex copilot cursor opencode pi gemini hermes` (любое подмножество), чтобы пропустить запрос. + Эта команда записывает записи хуков в установленные CLI агентов (Claude Code's `~/.claude/settings.json`, OpenAI Codex's `~/.codex/hooks.json`, GitHub Copilot CLI's `~/.copilot/hooks/failproofai.json`, Cursor Agent's `~/.cursor/hooks.json`, OpenCode's generated plugin shim at `~/.config/opencode/plugins/failproofai.mjs` plus a registration entry in `~/.config/opencode/opencode.json`'s `plugin` array, Pi's `~/.pi/agent/settings.json`, или Hermes's `~/.hermes/config.yaml`). Если присутствует несколько, вам будет предложено выбрать; передайте `--cli claude codex copilot cursor opencode pi hermes` (любое подмножество), чтобы пропустить запрос. - GitHub Copilot CLI, Cursor Agent, OpenCode, Pi и Gemini CLI поддерживаются в режиме **бета** — установите с помощью `--cli copilot`, `--cli cursor`, `--cli opencode`, `--cli pi` или `--cli gemini`. Hermes (hermes-agent, шлюз Slack/Telegram) устанавливается в область пользователя с помощью `--cli hermes` и также является источником автономного аудита. + GitHub Copilot CLI, Cursor Agent, OpenCode и Pi поддерживаются в режиме **бета** — установите с помощью `--cli copilot`, `--cli cursor`, `--cli opencode` или `--cli pi`. Hermes (hermes-agent, шлюз Slack/Telegram) устанавливается в область пользователя с помощью `--cli hermes` и также является источником автономного аудита. ```bash failproofai policies --install --scope project @@ -48,7 +48,6 @@ bun add -g failproofai failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project - failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user failproofai policies --install block-sudo block-rm-rf sanitize-api-keys ``` diff --git a/docs/ru/introduction.mdx b/docs/ru/introduction.mdx index 4996d294..e02edcaf 100644 --- a/docs/ru/introduction.mdx +++ b/docs/ru/introduction.mdx @@ -5,7 +5,7 @@ description: "FailproofAI предоставляет AI агентам 39 вст [![npm weekly downloads](https://img.shields.io/npm/dw/failproofai?style=flat-square&color=2ea44f)](https://www.npmjs.com/package/failproofai) -Хуки и политики для **обработки сбоев AI**, **восстановления после ошибок** и **надёжности LLM**. Держите ваших AI агентов надёжными и работающими автономно на **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Gemini CLI**, **Hermes** и **Agents SDK**. +Хуки и политики для **обработки сбоев AI**, **восстановления после ошибок** и **надёжности LLM**. Держите ваших AI агентов надёжными и работающими автономно на **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Hermes** и **Agents SDK**. AI агенты падают предсказуемым образом. Они выполняют деструктивные команды, утекают секреты, отходят от задачи, застревают в циклах или пушат прямо в main. Без надзора, небольшие сбои каскадируют в отказы, утечки учётных данных и потерю работы. diff --git a/docs/styles/config/vocabularies/Mintlify/accept.txt b/docs/styles/config/vocabularies/Mintlify/accept.txt index c7f9498d..c0668800 100644 --- a/docs/styles/config/vocabularies/Mintlify/accept.txt +++ b/docs/styles/config/vocabularies/Mintlify/accept.txt @@ -4,7 +4,6 @@ Anthropic Codex Copilot Cursor -Gemini OpenCode PreToolUse PostToolUse diff --git a/docs/tr/built-in-policies.mdx b/docs/tr/built-in-policies.mdx index 240ec79d..936e1d40 100644 --- a/docs/tr/built-in-policies.mdx +++ b/docs/tr/built-in-policies.mdx @@ -712,20 +712,19 @@ Stop uygulaması yedi desteklenen CLI arasında biraz farklı görünür; çünk | Codex | Aynı ajan döngüsü, hemen | Claude ile aynı. | | GitHub Copilot CLI | Aynı ajan döngüsü, hemen | Claude ile aynı (Copilot'un `{decision:"block", reason}` yeniden deneme kanalını kullanır — ampirik olarak Copilot CLI 1.0.41'e karşı doğrulanmıştır). | | Cursor Agent | Aynı ajan döngüsü, hemen | Claude ile aynı (Cursor'un `{followup_message}` kanalını kullanır — `loop_limit`, varsayılan 5 yeniden denemeler ile sınırlandırılmış). | -| Gemini CLI | Aynı ajan döngüsü, hemen | Claude ile aynı (Gemini'nin `{decision:"block", reason}` kanalını kullanır; `AfterAgent` üzerinde). | | OpenCode | Aynı ajan döngüsü, hemen | Claude ile aynı (OpenCode'un `client.session.prompt(...)` SDK çağrısını kullanır; `hookSpecificOutput.additionalContext` aracılığıyla yönlendirilmiş). | | **Pi (pi-coding-agent)** | **Sonraki kullanıcı sırası** | **Kapı devreye girdiğinde Pi görünen şekilde durur** — ajan döngüsü çıkar ve komut isteminin başına dönersiniz. Kapı daha sonra bir sonraki iletişim gönderdiğinizde devreye girer: failproofai iş akışı adımını tamamlamadan önce (commit, push vb.) bir `MANDATORY ACTION REQUIRED` yönergesini o sıraya ait sistem komutunun başına ekler; ne istedinizi yapmadan önce. -**Pi sınırlaması.** Pi'nin `AgentEndEvent` (Claude'un `Stop` hook'unun yukarı akış eşdeğeri) hiçbir Result türü yoktur — Pi'nin ajan döngüsü zaten çıkmış olduğu süredir. Pi, Claude / Copilot / Cursor / Gemini / OpenCode'un yapabildiği şekilde aynı döngüyü yeniden denemeye zorlanamazı. failproofai kapıyı Pi'nin `before_agent_start` olayına kaydırır (sonraki kullanıcı iletişim sonrasında devreye girer) — iş akışı kontrolü hala uygulanır, yalnızca mevcut sıra yerine sonraki sıra üzerinde. +**Pi sınırlaması.** Pi'nin `AgentEndEvent` (Claude'un `Stop` hook'unun yukarı akış eşdeğeri) hiçbir Result türü yoktur — Pi'nin ajan döngüsü zaten çıkmış olduğu süredir. Pi, Claude / Copilot / Cursor / OpenCode'un yapabildiği şekilde aynı döngüyü yeniden denemeye zorlanamazı. failproofai kapıyı Pi'nin `before_agent_start` olayına kaydırır (sonraki kullanıcı iletişim sonrasında devreye girer) — iş akışı kontrolü hala uygulanır, yalnızca mevcut sıra yerine sonraki sıra üzerinde. **Bunu pratikte ne anlama gelir:** - Pi durundan sonra, reddetme nedeni oturum kimliği tarafından bellek içinde anahtarlanmış şekilde yakalanır. Aynı Pi işlemi içinde gönderdiğiniz çok sonraki istişare bunu boşaltır: LLM sistem komutunun üstünde `MANDATORY ACTION REQUIRED` yönergesini görür, commits (veya pushes / açıklık PR / CI'yi bekler) ve ancak o zaman istediğiniz şeye devam eder. Yakalanan reddetme nedeni tek atış — bir kez boşaltıldıktan sonra kapı açıktır. -- Kapı Pi'nin işlem ömrü tarafından sınırlandırılır. Turlar arasında Pi'yi `Ctrl+C` yaparsanız veya çıkarsanız, bellek içi giriş işlemle birlikte bırakılır ve kapı kaçırılır. Claude, Copilot, Cursor, Gemini ve OpenCode'un aynı sınırı vardır (ajanı öldürün ve kapı kaçırılır) — Pi sadece bunu daha görünür hale getirir; çünkü ajan kapı devreye girmeden önce görünen şekilde çıkar. +- Kapı Pi'nin işlem ömrü tarafından sınırlandırılır. Turlar arasında Pi'yi `Ctrl+C` yaparsanız veya çıkarsanız, bellek içi giriş işlemle birlikte bırakılır ve kapı kaçırılır. Claude, Copilot, Cursor ve OpenCode'un aynı sınırı vardır (ajanı öldürün ve kapı kaçırılır) — Pi sadece bunu daha görünür hale getirir; çünkü ajan kapı devreye girmeden önce görünen şekilde çıkar. - Beklemede olan bir reddetme herhangi bir nedenle `session_shutdown` üzerine de temizlenir (`new` / `resume` / `fork` / `quit`), bu nedenle önceki oturumdan eski bir kapı, aynı Pi işlemi içinde başlatılan yeni bir oturuma sızamaz. -Claude tarzı aynı döngü yeniden denemesine ihtiyacınız varsa, `Stop` ilkelerinizi desteklenen diğer altı CLI'den birinin altında çalıştırın. Pi yukarı akışını gelecek bir Result türü ile izliyoruz; `AgentEndEvent` üzerinde bu boşluğu kapatmamıza izin verecektir. +Claude tarzı aynı döngü yeniden denemesine ihtiyacınız varsa, `Stop` ilkelerinizi desteklenen diğer beş CLI'den birinin altında çalıştırın. Pi yukarı akışını gelecek bir Result türü ile izliyoruz; `AgentEndEvent` üzerinde bu boşluğu kapatmamıza izin verecektir. ### `require-commit-before-stop` diff --git a/docs/tr/cli/audit.mdx b/docs/tr/cli/audit.mdx index 3615d41a..608b0108 100644 --- a/docs/tr/cli/audit.mdx +++ b/docs/tr/cli/audit.mdx @@ -47,7 +47,7 @@ failproofai Kullanımı görmek için `failproofai audit -h` (veya `--help`) komutunu çalıştırın. Denetim **tamamen çevrimdışı** çalışır — hesap veya ağ gerekmez — ve kontrol paneli `Ctrl+C` ile durdurana kadar sunulmaya devam eder. -Kontrol paneli bu makinedeki geçmiş ajan CLI transkriptlerini tarar (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi, Gemini) ve ajanın failproofai'nin durdurmak için tasarlandığı şeyleri ne sıklıkla yaptığını bildirir — ortam değişkeni kontrolleri, zorla göndermeler, gereksiz `cd ` önekleri, uyku-yoklama döngüleri, yeni düzenlenen dosyaları yeniden okuma ve daha fazlası. +Kontrol paneli bu makinedeki geçmiş ajan CLI transkriptlerini tarar (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi) ve ajanın failproofai'nin durdurmak için tasarlandığı şeyleri ne sıklıkla yaptığını bildirir — ortam değişkeni kontrolleri, zorla göndermeler, gereksiz `cd ` önekleri, uyku-yoklama döngüleri, yeni düzenlenen dosyaları yeniden okuma ve daha fazlası. Her transkript için, her araç-kullanım olayı 39 yerleşik politika **ve** henüz çalışma zamanı politikaları tarafından kapsanmayan desenleri yakalayan 8 denetim-yalnızca dedektörü aracılığıyla yeniden oynatılır. Sayılar tüm oturumlar arasında politika / dedektör başına toplanır. diff --git a/docs/tr/configuration.mdx b/docs/tr/configuration.mdx index 245b7f07..e426bd8a 100644 --- a/docs/tr/configuration.mdx +++ b/docs/tr/configuration.mdx @@ -196,13 +196,12 @@ AI çağrıları yapan politikalar için LLM istemci yapılandırması. Çoğu k - **OpenAI Codex**: `~/.codex/hooks.json` (kullanıcı), `/.codex/hooks.json` (proje) — Codex yerel kapsamı yok - **GitHub Copilot CLI _(beta)_**: `~/.copilot/hooks/failproofai.json` (kullanıcı), `/.github/hooks/failproofai.json` (proje) — Copilot yerel kapsamı yok. Hook girdileri Copilot'un OS anahtarlı `bash`/`powershell` komut alanlarını `timeoutSec` ile kullanır; dosya üst düzey `version: 1` işaretçisini taşır. Copilot CLI desteği **beta** durumundadır ve `events.jsonl` kayıt şemasını (halk belgeleri belirtmez) daha fazla gerçek dünya oturumuna karşı doğrularız. - **Cursor Agent _(beta)_**: `~/.cursor/hooks.json` (kullanıcı), `/.cursor/hooks.json` (proje) — Cursor yerel kapsamı yok. Hook girdileri Claude şeklinde `{type, command, timeout}` formunu kullanır (`bash`/`powershell` bölümü yok) ancak camelCase olay anahtarları (`preToolUse`, `beforeSubmitPrompt`, …) altında depolanır, Cursor'un [hooks şemasına](https://cursor.com/docs/hooks) göre düz dizi halinde; dosya üst düzey `version: 1` işaretçisini taşır. İşleyici, `CURSOR_EVENT_MAP` aracılığıyla camelCase → PascalCase'i kanonikleştirir, böylece mevcut yerleşik politikalar değişmeden çalışır. Cursor Agent desteği **beta** durumundadır ve Cursor'un disk üzerindeki transkripti (açık belgelerde belirtilmez) daha fazla gerçek dünya kurulumuna karşı doğrularız. - - **OpenCode _(beta)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (kullanıcı), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (proje) — OpenCode yerel kapsamı yok. Diğer altı CLI'den farklı olarak, OpenCode **harici komut hook sistemi olmaz**: `plugin: []` dizisi aracılığıyla açıkça kaydedilen işlem içi JS/TS eklentilerini yükler (`opencode.json` içinde) (`.opencode/plugins/` dan otomatik keşif opencode v1.14.33'de eklentilerin nasıl yüklendiği **değildir**). Kurulum, ikili failproofai'yi subprocess olarak çağıran ve ikili'nin Claude şekli JSON yanıtını eklenti semantiğine geri çeviren küçük bir oluşturulan eklenti parçacığını bırakır: araç olayı reddi için `throw new Error()` (araç çağrısını iptal eder), `instruct` VE `Stop` / `SubagentStop` reddi için `client.session.prompt(...)` (reddi nedeni bir sonraki kullanıcı iletisi olarak gönderir — `session.idle` yalnızca bildirimdir ve bundan atmak no-op olduğundan tek zorla yeniden deneme kanalı), allow için no-op. Parçacık hem araç adlarını (küçük harf → PascalCase, `OPENCODE_TOOL_MAP` aracılığıyla) hem de araç giriş arg anahtarlarını (camelCase → snake_case, `OPENCODE_TOOL_INPUT_MAP` aracılığıyla `Read` / `Write` / `Edit` için, örn. `filePath` → `file_path`, `oldString` → `old_string`) kanonikleştirir, ikili'ye iletmeden önce, yol kontrol yerleşikleri gibi `block-read-outside-cwd`, `block-env-files` ve `block-secrets-write` OpenCode araç çağrılarında değişmeden çalışır. Oturumlar opencode'un SQLite DB'sinde yaşar `~/.local/share/opencode/opencode.db`; pano'nun oturum görüntüleyicisi onları `opencode db --format json` ve `opencode export ` aracılığıyla okur. OpenCode desteği **beta** durumundadır ve sürümler arasında ve daha fazla gerçek dünya oturumlarına karşı davranışı doğrularız. [OpenCode eklentileri belgeleri](https://opencode.ai/docs/plugins/) başlıklı sayfaya bakın. + - **OpenCode _(beta)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (kullanıcı), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (proje) — OpenCode yerel kapsamı yok. Diğer beş CLI'den farklı olarak, OpenCode **harici komut hook sistemi olmaz**: `plugin: []` dizisi aracılığıyla açıkça kaydedilen işlem içi JS/TS eklentilerini yükler (`opencode.json` içinde) (`.opencode/plugins/` dan otomatik keşif opencode v1.14.33'de eklentilerin nasıl yüklendiği **değildir**). Kurulum, ikili failproofai'yi subprocess olarak çağıran ve ikili'nin Claude şekli JSON yanıtını eklenti semantiğine geri çeviren küçük bir oluşturulan eklenti parçacığını bırakır: araç olayı reddi için `throw new Error()` (araç çağrısını iptal eder), `instruct` VE `Stop` / `SubagentStop` reddi için `client.session.prompt(...)` (reddi nedeni bir sonraki kullanıcı iletisi olarak gönderir — `session.idle` yalnızca bildirimdir ve bundan atmak no-op olduğundan tek zorla yeniden deneme kanalı), allow için no-op. Parçacık hem araç adlarını (küçük harf → PascalCase, `OPENCODE_TOOL_MAP` aracılığıyla) hem de araç giriş arg anahtarlarını (camelCase → snake_case, `OPENCODE_TOOL_INPUT_MAP` aracılığıyla `Read` / `Write` / `Edit` için, örn. `filePath` → `file_path`, `oldString` → `old_string`) kanonikleştirir, ikili'ye iletmeden önce, yol kontrol yerleşikleri gibi `block-read-outside-cwd`, `block-env-files` ve `block-secrets-write` OpenCode araç çağrılarında değişmeden çalışır. Oturumlar opencode'un SQLite DB'sinde yaşar `~/.local/share/opencode/opencode.db`; pano'nun oturum görüntüleyicisi onları `opencode db --format json` ve `opencode export ` aracılığıyla okur. OpenCode desteği **beta** durumundadır ve sürümler arasında ve daha fazla gerçek dünya oturumlarına karşı davranışı doğrularız. [OpenCode eklentileri belgeleri](https://opencode.ai/docs/plugins/) başlıklı sayfaya bakın. - **Pi _(beta)_**: `~/.pi/agent/settings.json` (kullanıcı), `/.pi/settings.json` (proje) — Pi yerel kapsamı yok. Pi başlangıçta TypeScript uzantı paketlerini yükler; ayarlar dosyası düz dize dizisidir `{"packages": ["./relative/path", …]}`. failproofai, paketlenmiş `pi-extension/` dizinine işaret eden tek bir packages-dizisi girişi yazar. Uzantı dahili olarak Pi'nin `tool_call` / `user_bash` / `input` / `session_start` olaylarına abone olur ve `failproofai --hook --cli pi`'ye shell çıkışı verir; işleyici underscore_lower_snake_case → PascalCase'i `PI_EVENT_MAP` aracılığıyla kanonikleştirir, böylece mevcut yerleşik politikalar değişmeden çalışır. Araç giriş argları da `PI_TOOL_INPUT_MAP` aracılığıyla kanonikleştirilir (Pi'nin Read / Write / Edit `file_path` yerine `path` iletir; üst düzey anahtarı eşlemek `block-env-files` ve `block-secrets-write`'in çalışmasını sağlar — `block-read-outside-cwd` zaten bir `path` geri dönüşü içeriyordu). Pi desteği **beta** durumundadır, Pi'nin uzantı API'si ve oturum günlüğü düzeni stabilize olurken. - - **Gemini CLI _(beta)_**: `~/.gemini/settings.json` (kullanıcı), `/.gemini/settings.json` (proje) — Gemini yerel kapsamı yok (`/etc/gemini-cli/settings.json` içinde belgelenen bir `system` kapsamı vardır, failproofai bunu göstermez). Hook girdileri Claude'un `{type, command, timeout}` formunu Gemini'nin `{matcher, hooks: [...]}` eşleşme şemasında, varsayılan olarak `matcher: "*"` ile sarılı halde kullanır. Olaylar PascalCase'tir (`SessionStart`, `BeforeAgent`, `AfterAgent`, `BeforeModel`, `AfterModel`, `BeforeToolSelection`, `BeforeTool`, `AfterTool`, `PreCompress`, `Notification`, `SessionEnd`); işleyici `GEMINI_EVENT_MAP` aracılığıyla Claude kanonik adlarına eşlenir. Araç adları snake_case'tir (`run_shell_command`, `read_file`, `write_file`, `replace`, …) — işleyici `GEMINI_TOOL_MAP` aracılığıyla kanonikleştirir, böylece mevcut yerleşik politikalar değişmeden çalışır. Politika değerlendirici Gemini'nin düz `{decision: "deny", reason}` şeklini yayar (Gemini'nin "Altın Kural" exit-0 sözleşmesine göre tercih edilen), `{hookSpecificOutput: {hookEventName, additionalContext}}` BeforeAgent / AfterTool / SessionStart'ta bağlam enjeksiyonu için ve AfterAgent'ta `{decision: "block", reason}` zorla yeniden deneme semantiği için. Gemini CLI desteği **beta** durumundadır ve gerçek dünya kapsamsını genişletirken. [Gemini CLI hooks belgeleri](https://geminicli.com/docs/hooks/) başlıklı sayfaya bakın. - **Hermes (hermes-agent)**: `~/.hermes/config.yaml` (**yalnızca kullanıcı kapsamı** — Hermes proje/yerel yapılandırması yok). Hermes bir Slack/Telegram **ağ geçididir**, bu nedenle bir kurulum her platformdan (Slack/Telegram/cli/cron) araç çağrılarını **ve** iç alt ajanları yakalar. Hook girdileri, Hermes'in snake_case olayları (`pre_tool_call` / `post_tool_call` / `on_session_start` / `on_session_end` / `subagent_stop`) tarafından anahtarlanan `hooks:` eşlemesi altında `{command, timeout}` çiftidir; işleyici `HERMES_EVENT_MAP` aracılığıyla olayları ve `HERMES_TOOL_MAP` aracılığıyla araç adlarını kanonikleştirir, böylece yerleşik politikalar değişmeden çalışır. Yapılandırma, yorum koruyan YAML `Document` turuna göre düzenlendiğinden işletmenin diğer ayarları kalır ve kurulum, başsız ağ geçidinin (TTY yok) hookları onay istemi olmadan çalıştırması için `hooks_auto_accept: true` ayarlar. Değerlendirici Hermes'in `{"decision":"block","reason"}` stdout sözleşmesini yayar (Hermes çıkış kodlarını yok sayar). **Sınırlamalar:** Hermes'in bir `Stop` olay sonu yoktur, bu nedenle `require-*-before-stop` yerleşikleri bunun için hiçbir zaman çalışmaz (uygulanamaz, kırık değil); `instruct` izin-not-ile-günlüğe-kaydedil'e degrades (ek bağlam kanalı yok); ve çıkış gizli redaksiyonu (`sanitize-*`) shell-hook sözleşmesi üzerinde araç çıkışını yeniden yazamaz. Hermes aynı zamanda çevrimdışı bir **denetim** kaynağıdır — pano, ağ geçidi oturumlarını doğrudan `~/.hermes/state.db`'den okur. - **`policies-config.json`** — failproofai'ye hangi politikaları değerlendireceğini ve hangi parametrelerle (tüm ajan CLI'ler arasında paylaşılan) söyler -Belirli bir ajanı hedeflemek için `--cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes` geçin (boşlukla ayrılmış veya herhangi bir altküme için tekrarlanan): +Belirli bir ajanı hedeflemek için `--cli claude|codex|copilot|cursor|opencode|pi|hermes` geçin (boşlukla ayrılmış veya herhangi bir altküme için tekrarlanan): ```bash failproofai policies --install --cli codex --scope project @@ -210,12 +209,11 @@ failproofai policies --install --cli copilot --scope project failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project -failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user -failproofai policies --install --cli claude codex copilot cursor opencode pi gemini +failproofai policies --install --cli claude codex copilot cursor opencode pi ``` -`--cli` atlandığında, `failproofai` hangi ajan CLI'lerin yüklendiğini algılar (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which gemini` / `which hermes`): +`--cli` atlandığında, `failproofai` hangi ajan CLI'lerin yüklendiğini algılar (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which hermes`): - **Bir CLI algılandı** — sor olmadan o CLI'yi otomatik seçer. - **Etkileşimli bir terminalde birden fazla CLI algılandı** — `Detected (N)` bölümüne gruplandırılmış ok tuşu tek seçim istemi gösterir (algılanan tüm CLI'ler için bir `Tüm N tespit edileni yükle` toplu satırı + her algılanan CLI bireysel olarak) ve her desteklenen CLI'nin yüklenmemiş olan listesini gösteren `Yüklü olmayan (M) · hookları önceden kurma` bölümü (↑↓ taşımak, Enter seçmek, ^C bırakmak için). Kaldırma akışı yalnızca Algılanan bölümü gösterir. diff --git a/docs/tr/dashboard.mdx b/docs/tr/dashboard.mdx index 6b865387..fc29ce1c 100644 --- a/docs/tr/dashboard.mdx +++ b/docs/tr/dashboard.mdx @@ -24,11 +24,11 @@ Pano, dosya sisteminden doğrudan okur — Claude Code proje klasörlerinizi ve ### Projeler -Makinenizde bulunan tüm Claude Code, OpenAI Codex, GitHub Copilot CLI _(beta)_, Cursor Agent _(beta)_, OpenCode _(beta)_, Pi _(beta)_, Gemini CLI _(beta)_ ve Hermes projelerini listeler. Claude projeleri `~/.claude/projects/` adresinden keşfedilir (veya `CLAUDE_PROJECTS_PATH` tarafından ayarlanan yol); Codex projeleri `~/.codex/sessions///
/*.jsonl` altındaki her transkripti tarayarak ve her oturumun ilk kaydında kaydedilen `cwd` ye göre gruplandırılarak keşfedilir; Copilot CLI projeleri her `~/.copilot/session-state//workspace.yaml` tarayarak keşfedilir (`COPILOT_HOME` üzerinden yapılandırılabilir) ve `cwd` alanına göre gruplandırılır; Cursor Agent projeleri `~/.cursor/agent-sessions//` altındaki oturum başına meta verileri tarayarak keşfedilir (`CURSOR_HOME` üzerinden yapılandırılabilir, `conversations/` ve `sessions/` geri dönüş olarak problanır) `meta.json` / `session.json` / `workspace.yaml` içindeki `cwd` skaler değeri için; OpenCode projeleri `~/.local/share/opencode/opencode.db` adresindeki SQLite DB'sini `opencode db --format json` üzerinden sorgulayarak keşfedilir (`session` ve `project` tablolarını okuyuz ve `project_id` ye göre gruplandırırız); Pi projeleri `~/.pi/agent/sessions//_.jsonl` altındaki oturum başına JSONL transkriptlerini tarayarak keşfedilir (`PI_SESSIONS_DIR` üzerinden yapılandırılabilir) ve her oturumun ilk kaydından `cwd` yi çekeriz; Gemini CLI projeleri `~/.gemini/tmp//chats/session--.jsonl` tarayarak keşfedilir (`GEMINI_SESSIONS_DIR` üzerinden yapılandırılabilir) ve `.project_root` metin işaretçisinden kanonik cwd'yi kurtarırız; Hermes ağ geçidi oturumları doğrudan `~/.hermes/state.db` (`HERMES_DB_PATH` üzerinden yapılandırılabilir) adresindeki SQLite deposundan okunur ve `source` (Slack/Telegram/cli/cron — ağ geçidi oturumlarının cwd'si yoktur) tarafından `hermes-` projelerine gruplandırılır. Birden çok CLI tarafından kullanılan bir proje, tüm eşleşen rozetleriyle tek bir satır olarak işlenir. Belirli bir aracı CLI'ye göre filtrelemek için tablonun üstündeki **CLI** açılır menüsünü kullanın; URL seçiminizi `?cli=claude|codex|copilot|cursor|opencode|pi|gemini|hermes` olarak korur. +Makinenizde bulunan tüm Claude Code, OpenAI Codex, GitHub Copilot CLI _(beta)_, Cursor Agent _(beta)_, OpenCode _(beta)_, Pi _(beta)_ ve Hermes projelerini listeler. Claude projeleri `~/.claude/projects/` adresinden keşfedilir (veya `CLAUDE_PROJECTS_PATH` tarafından ayarlanan yol); Codex projeleri `~/.codex/sessions///
/*.jsonl` altındaki her transkripti tarayarak ve her oturumun ilk kaydında kaydedilen `cwd` ye göre gruplandırılarak keşfedilir; Copilot CLI projeleri her `~/.copilot/session-state//workspace.yaml` tarayarak keşfedilir (`COPILOT_HOME` üzerinden yapılandırılabilir) ve `cwd` alanına göre gruplandırılır; Cursor Agent projeleri `~/.cursor/agent-sessions//` altındaki oturum başına meta verileri tarayarak keşfedilir (`CURSOR_HOME` üzerinden yapılandırılabilir, `conversations/` ve `sessions/` geri dönüş olarak problanır) `meta.json` / `session.json` / `workspace.yaml` içindeki `cwd` skaler değeri için; OpenCode projeleri `~/.local/share/opencode/opencode.db` adresindeki SQLite DB'sini `opencode db --format json` üzerinden sorgulayarak keşfedilir (`session` ve `project` tablolarını okuyuz ve `project_id` ye göre gruplandırırız); Pi projeleri `~/.pi/agent/sessions//_.jsonl` altındaki oturum başına JSONL transkriptlerini tarayarak keşfedilir (`PI_SESSIONS_DIR` üzerinden yapılandırılabilir) ve her oturumun ilk kaydından `cwd` yi çekeriz; Hermes ağ geçidi oturumları doğrudan `~/.hermes/state.db` (`HERMES_DB_PATH` üzerinden yapılandırılabilir) adresindeki SQLite deposundan okunur ve `source` (Slack/Telegram/cli/cron — ağ geçidi oturumlarının cwd'si yoktur) tarafından `hermes-` projelerine gruplandırılır. Birden çok CLI tarafından kullanılan bir proje, tüm eşleşen rozetleriyle tek bir satır olarak işlenir. Belirli bir aracı CLI'ye göre filtrelemek için tablonun üstündeki **CLI** açılır menüsünü kullanın; URL seçiminizi `?cli=claude|codex|copilot|cursor|opencode|pi|hermes` olarak korur. Her proje şunları gösterir: - Proje adı (klasör yolundan türetilmiş) -- CLI rozeti — `Claude Code` (turuncu), `OpenAI Codex` (mor), `GitHub Copilot` (mavi), `Cursor Agent` (zümrüt), `OpenCode` (kehribar), `Pi` (pembe), `Gemini CLI` (gökyüzü), ve/veya `Hermes` (çivit) +- CLI rozeti — `Claude Code` (turuncu), `OpenAI Codex` (mor), `GitHub Copilot` (mavi), `Cursor Agent` (zümrüt), `OpenCode` (kehribar), `Pi` (pembe), ve/veya `Hermes` (çivit) - En son oturum etkinliğinin tarihi Oturumlarını görmek için bir projeyi tıklayın. @@ -47,7 +47,7 @@ Oturum görüntüleyiciyi açmak için bir oturumu tıklayın. ### Oturum görüntüleyici -Oturum görüntüleyici, otonom aracılar için temel soruyu yanıtlar: aracı ne yaptı ve hız tuttu mu? Başlığın yanındaki CLI rozeti, oturumun Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi, Gemini CLI veya Hermes transkripti olup olmadığını gösterir. Bir oturumda gerçekleşen her şeyin bir zaman çizelgesini gösterir: +Oturum görüntüleyici, otonom aracılar için temel soruyu yanıtlar: aracı ne yaptı ve hız tuttu mu? Başlığın yanındaki CLI rozeti, oturumun Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi veya Hermes transkripti olup olmadığını gösterir. Bir oturumda gerçekleşen her şeyin bir zaman çizelgesini gösterir: - **İletiler** - Claude'un metin yanıtları ve kullanıcı istemleri - **Araç çağrıları** - Claude'un çağırdığı her araç, girişi ve çıktısıyla @@ -55,7 +55,7 @@ Oturum görüntüleyici, otonom aracılar için temel soruyu yanıtlar: aracı n Üstteki istatistik çubuğu oturum süresini, toplam araç çağrılarını ve kanca kararlarının bir özetini gösterir (allow / deny / instruct sayıları). -Oturumu dışa aktarmak için **İndirme Günlükleri** düğmesini tıklayın. Claude Code, Codex, Copilot, Cursor, Pi ve Gemini oturumları için orijinal disk üzerindeki JSONL transkriptini bayt-bayt alırsınız; OpenCode (oturumları diskte değil SQLite'de yaşayan) için temel `session` / `messages` / `parts` tablolarını yansıtan bir JSON belgesi alırsınız. +Oturumu dışa aktarmak için **İndirme Günlükleri** düğmesini tıklayın. Claude Code, Codex, Copilot, Cursor ve Pi oturumları için orijinal disk üzerindeki JSONL transkriptini bayt-bayt alırsınız; OpenCode (oturumları diskte değil SQLite'de yaşayan) için temel `session` / `messages` / `parts` tablolarını yansıtan bir JSON belgesi alırsınız. ### Denetim @@ -75,16 +75,16 @@ Politikaları yönetmek ve etkinliği gözden geçirmek için iki sekmeli bir sa - - Tek bir panelden hangi aracı CLI'lerin failproofai tarafından korunacağını çoklu seçim yapın — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi, Gemini CLI ve Hermes'in hepsinin yükleme durumu (`Active` / `Detected` / `Inactive`) olan bir satırı, kullanıcı kapsamı ayarları yolu ve marka renkli vurguyu vardır. İstediğiniz CLI'leri işaretleyin veya işareti kaldırın ve farkı bir adımda yüklemek/kaldırmak için `Apply changes` öğesine tıklayın. İkili PATH'de algılanan CLI'ler önceden işaretlenmiş. + - Tek bir panelden hangi aracı CLI'lerin failproofai tarafından korunacağını çoklu seçim yapın — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi ve Hermes'in hepsinin yükleme durumu (`Active` / `Detected` / `Inactive`) olan bir satırı, kullanıcı kapsamı ayarları yolu ve marka renkli vurguyu vardır. İstediğiniz CLI'leri işaretleyin veya işareti kaldırın ve farkı bir adımda yüklemek/kaldırmak için `Apply changes` öğesine tıklayın. İkili PATH'de algılanan CLI'ler önceden işaretlenmiş. - Tek tıklamayla bireysel politikaları açıp kapatın (`~/.failproofai/policies-config.json` adresine yazar — tüm yüklü CLI'ler arasında paylaşılır) - Parametrelerini yapılandırmak için bir politikayı genişletin (`policyParams` destekleyen politikalar için) - Özel politika dosyası yolunu ayarla - Tüm oturumlar arasında tetiklenen her kanca olayının tam sayfalı geçmişi - - Karar, olay türü, CLI (Claude Code / OpenAI Codex / GitHub Copilot _(beta)_ / Cursor Agent _(beta)_ / OpenCode _(beta)_ / Pi _(beta)_ / Gemini CLI _(beta)_ / Hermes), politika adı veya oturum ID'ye göre filtrele - - Her satır şunları gösterir: zaman damgası, politika adı, karar, CLI rozeti (turuncu = Claude Code, mor = OpenAI Codex, mavi = GitHub Copilot, zümrüt = Cursor Agent, kehribar = OpenCode, pembe = Pi, gökyüzü = Gemini CLI, çivit = Hermes), araç adı, oturum ID ve deny/instruct kararları için neden - - Transkripti açmak için bir oturum ID'sine tıklayın — görüntüleyici hangi CLI'nin kancayı tetiklediğini otomatik algılar (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Gemini CLI `~/.gemini/tmp//chats/.jsonl`, Hermes `~/.hermes/state.db`) ve başlıkta eşleşen CLI rozetini işler + - Karar, olay türü, CLI (Claude Code / OpenAI Codex / GitHub Copilot _(beta)_ / Cursor Agent _(beta)_ / OpenCode _(beta)_ / Pi _(beta)_ / Hermes), politika adı veya oturum ID'ye göre filtrele + - Her satır şunları gösterir: zaman damgası, politika adı, karar, CLI rozeti (turuncu = Claude Code, mor = OpenAI Codex, mavi = GitHub Copilot, zümrüt = Cursor Agent, kehribar = OpenCode, pembe = Pi, çivit = Hermes), araç adı, oturum ID ve deny/instruct kararları için neden + - Transkripti açmak için bir oturum ID'sine tıklayın — görüntüleyici hangi CLI'nin kancayı tetiklediğini otomatik algılar (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Hermes `~/.hermes/state.db`) ve başlıkta eşleşen CLI rozetini işler diff --git a/docs/tr/getting-started.mdx b/docs/tr/getting-started.mdx index 16429282..cfa11abf 100644 --- a/docs/tr/getting-started.mdx +++ b/docs/tr/getting-started.mdx @@ -37,9 +37,9 @@ bun add -g failproofai failproofai policies --install ``` - Bu, yüklü ajan CLI'lerinize hook girişleri yazar (Claude Code'un `~/.claude/settings.json`, OpenAI Codex'in `~/.codex/hooks.json`, GitHub Copilot CLI'nin `~/.copilot/hooks/failproofai.json`, Cursor Agent'ın `~/.cursor/hooks.json`, OpenCode'un `~/.config/opencode/plugins/failproofai.mjs` konumundaki oluşturulmuş plugin shim'i ve `~/.config/opencode/opencode.json` dosyasındaki `plugin` dizisinde bir kayıt girişi, Pi'nin `~/.pi/agent/settings.json`, Gemini CLI'nin `~/.gemini/settings.json` veya Hermes'in `~/.hermes/config.yaml` dosyası). Birden fazla varsa size sorulacak; `--cli claude codex copilot cursor opencode pi gemini hermes` (herhangi bir alt küme) geçerek sorguyu atlayabilirsiniz. + Bu, yüklü ajan CLI'lerinize hook girişleri yazar (Claude Code'un `~/.claude/settings.json`, OpenAI Codex'in `~/.codex/hooks.json`, GitHub Copilot CLI'nin `~/.copilot/hooks/failproofai.json`, Cursor Agent'ın `~/.cursor/hooks.json`, OpenCode'un `~/.config/opencode/plugins/failproofai.mjs` konumundaki oluşturulmuş plugin shim'i ve `~/.config/opencode/opencode.json` dosyasındaki `plugin` dizisinde bir kayıt girişi, Pi'nin `~/.pi/agent/settings.json` veya Hermes'in `~/.hermes/config.yaml` dosyası). Birden fazla varsa size sorulacak; `--cli claude codex copilot cursor opencode pi hermes` (herhangi bir alt küme) geçerek sorguyu atlayabilirsiniz. - GitHub Copilot CLI, Cursor Agent, OpenCode, Pi ve Gemini CLI desteği **beta** aşamasındadır — `--cli copilot`, `--cli cursor`, `--cli opencode`, `--cli pi` veya `--cli gemini` ile kurun. Hermes (hermes-agent, bir Slack/Telegram ağ geçidi) `--cli hermes` ile kullanıcı kapsamında yüklenir ve **ayrıca** çevrimdışı bir denetim kaynağıdır. + GitHub Copilot CLI, Cursor Agent, OpenCode ve Pi desteği **beta** aşamasındadır — `--cli copilot`, `--cli cursor`, `--cli opencode` veya `--cli pi` ile kurun. Hermes (hermes-agent, bir Slack/Telegram ağ geçidi) `--cli hermes` ile kullanıcı kapsamında yüklenir ve **ayrıca** çevrimdışı bir denetim kaynağıdır. ```bash failproofai policies --install --scope project @@ -48,7 +48,6 @@ bun add -g failproofai failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project - failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user failproofai policies --install block-sudo block-rm-rf sanitize-api-keys ``` diff --git a/docs/tr/introduction.mdx b/docs/tr/introduction.mdx index 526ec330..7d196194 100644 --- a/docs/tr/introduction.mdx +++ b/docs/tr/introduction.mdx @@ -5,7 +5,7 @@ description: "FailproofAI, AI ajanlarına döngüleri, gizli dizi sızıntılar [![npm weekly downloads](https://img.shields.io/npm/dw/failproofai?style=flat-square&color=2ea44f)](https://www.npmjs.com/package/failproofai) -**AI başarısızlık işleme**, **hata kurtarma** ve **LLM güvenilirliği** için hook'lar ve politikalar. AI ajanlarınızı **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Gemini CLI**, **Hermes** ve **Agents SDK** arasında güvenilir ve otonom halde tutun. +**AI başarısızlık işleme**, **hata kurtarma** ve **LLM güvenilirliği** için hook'lar ve politikalar. AI ajanlarınızı **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Hermes** ve **Agents SDK** arasında güvenilir ve otonom halde tutun. AI ajanları tahmin edilebilir şekillerde başarısız olur. Yıkıcı komutlar çalıştırırlar, sırları sızıntıya uğratırlar, görevlerinden saparlar, döngülere takılırlar ya da doğrudan ana dala push yaparlar. Gözetimsiz bırakıldığında, küçük başarısızlıklar kesintilere, sızıntıya uğramış kimlik bilgilerine ve kayıp işlere dönüşür. diff --git a/docs/vi/built-in-policies.mdx b/docs/vi/built-in-policies.mdx index b7eb5873..a86d27e7 100644 --- a/docs/vi/built-in-policies.mdx +++ b/docs/vi/built-in-policies.mdx @@ -718,20 +718,19 @@ Thực thi Stop trông hơi khác nhau trên bảy CLI được hỗ trợ vì m | Codex | Cùng vòng lặp agent, ngay lập tức | Giống như Claude. | | GitHub Copilot CLI | Cùng vòng lặp agent, ngay lập tức | Giống như Claude (sử dụng kênh `{decision:"block", reason}` retry của Copilot — đã xác minh theo kinh nghiệm đối với Copilot CLI 1.0.41). | | Cursor Agent | Cùng vòng lặp agent, ngay lập tức | Giống như Claude (sử dụng kênh `{followup_message}` của Cursor — bị giới hạn ở `loop_limit`, mặc định 5 lần thử lại). | -| Gemini CLI | Cùng vòng lặp agent, ngay lập tức | Giống như Claude (sử dụng kênh `{decision:"block", reason}` của Gemini trên `AfterAgent`). | | OpenCode | Cùng vòng lặp agent, ngay lập tức | Giống như Claude (sử dụng lệnh gọi SDK `client.session.prompt(...)` của OpenCode được định tuyến qua `hookSpecificOutput.additionalContext`). | | **Pi (pi-coding-agent)** | **Lần tới người dùng** | **Pi dừng lại rõ ràng** khi cổng kích hoạt — vòng lặp agent của nó thoát và bạn quay lại dấu nhắc. Cổng sau đó kích hoạt lần tiếp theo bạn gửi dấu nhắc: failproofai thêm tiền tố `MANDATORY ACTION REQUIRED` vào lời nhắc hệ thống của lần đó, hướng dẫn LLM hoàn thành bước quy trình làm việc (commit, push, v.v.) trước khi làm bất cứ điều gì bạn yêu cầu. | -**Giới hạn Pi.** `AgentEndEvent` của Pi (tương đương thượng nguồn của hook `Stop` của Claude) không có loại Result — khi nó kích hoạt, vòng lặp agent của Pi đã thoát. Pi không thể bị buộc thử lại cùng một vòng lặp theo cách Claude / Copilot / Cursor / Gemini / OpenCode. failproofai thay đổi cổng sang sự kiện `before_agent_start` của Pi (được kích hoạt sau dấu nhắc người dùng tiếp theo) để kiểm tra quy trình làm việc vẫn thực thi, chỉ trên lần tới thay vì lần hiện tại. +**Giới hạn Pi.** `AgentEndEvent` của Pi (tương đương thượng nguồn của hook `Stop` của Claude) không có loại Result — khi nó kích hoạt, vòng lặp agent của Pi đã thoát. Pi không thể bị buộc thử lại cùng một vòng lặp theo cách Claude / Copilot / Cursor / OpenCode. failproofai thay đổi cổng sang sự kiện `before_agent_start` của Pi (được kích hoạt sau dấu nhắc người dùng tiếp theo) để kiểm tra quy trình làm việc vẫn thực thi, chỉ trên lần tới thay vì lần hiện tại. **Điều này có nghĩa gì trong thực tế:** - Sau khi Pi dừng lại, lý do từ chối được ghi vào bộ nhớ trong bằng id phiên Pi. Dấu nhắc rất tiếp theo bạn gửi trong cùng quy trình Pi lấy ra nó: LLM nhìn thấy chỉ thị `MANDATORY ACTION REQUIRED` ở đầu lời nhắc hệ thống của nó, commit (hoặc push / mở PR / chờ CI) và chỉ khi đó tiếp tục với yêu cầu của bạn. Lý do từ chối được ghi lại là một lần — sau khi lấy ra, cổng rõ ràng. -- Cổng bị ràng buộc bởi vòng đời quá trình Pi. Nếu bạn `Ctrl+C` Pi hoặc thoát giữa các lần, mục bộ nhớ trong được loại bỏ cùng với quá trình và cổng bị bỏ lỡ. Claude, Copilot, Cursor, Gemini và OpenCode có cùng ranh giới (giết agent và cổng bị bỏ lỡ) — Pi chỉ làm cho nó rõ ràng hơn vì agent rõ ràng thoát trước khi cổng kích hoạt. +- Cổng bị ràng buộc bởi vòng đời quá trình Pi. Nếu bạn `Ctrl+C` Pi hoặc thoát giữa các lần, mục bộ nhớ trong được loại bỏ cùng với quá trình và cổng bị bỏ lỡ. Claude, Copilot, Cursor và OpenCode có cùng ranh giới (giết agent và cổng bị bỏ lỡ) — Pi chỉ làm cho nó rõ ràng hơn vì agent rõ ràng thoát trước khi cổng kích hoạt. - Một từ chối đang chờ cũng bị xóa trên `session_shutdown` vì lý do gì đó (`new` / `resume` / `fork` / `quit`), vì vậy cổng cũ từ phiên trước không thể bị rò rỉ vào phiên mới được khởi động trong cùng quy trình Pi. -Nếu bạn cần thử lại cùng vòng lặp kiểu Claude, hãy chạy các chính sách `Stop` của bạn dưới bất kỳ sáu CLI được hỗ trợ khác. Chúng tôi đang theo dõi Pi thượng nguồn để có loại Result trong tương lai trên `AgentEndEvent` sẽ cho phép chúng tôi đóng khoảng cách này. +Nếu bạn cần thử lại cùng vòng lặp kiểu Claude, hãy chạy các chính sách `Stop` của bạn dưới bất kỳ năm CLI được hỗ trợ khác. Chúng tôi đang theo dõi Pi thượng nguồn để có loại Result trong tương lai trên `AgentEndEvent` sẽ cho phép chúng tôi đóng khoảng cách này. ### `require-commit-before-stop` diff --git a/docs/vi/cli/audit.mdx b/docs/vi/cli/audit.mdx index e7fcf00a..f93bed5d 100644 --- a/docs/vi/cli/audit.mdx +++ b/docs/vi/cli/audit.mdx @@ -47,7 +47,7 @@ failproofai Chạy `failproofai audit -h` (hoặc `--help`) để xem cách sử dụng. Công cụ kiểm toán chạy **hoàn toàn ngoại tuyến** — không cần tài khoản hay mạng — và dashboard tiếp tục hoạt động cho đến khi bạn dừng nó bằng `Ctrl+C`. -Dashboard quét các bản ghi agent CLI quá khứ trên máy này (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi, Gemini) và báo cáo tần suất agent làm những việc mà failproofai được xây dựng để ngăn chặn — kiểm tra biến môi trường, force push, tiền tố `cd ` dư thừa, vòng lặp sleep-polling, đọc lại tệp vừa chỉnh sửa, và nhiều hơn nữa. +Dashboard quét các bản ghi agent CLI quá khứ trên máy này (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi) và báo cáo tần suất agent làm những việc mà failproofai được xây dựng để ngăn chặn — kiểm tra biến môi trường, force push, tiền tố `cd ` dư thừa, vòng lặp sleep-polling, đọc lại tệp vừa chỉnh sửa, và nhiều hơn nữa. Đối với mỗi bản ghi, mỗi sự kiện tool-use được phát lại thông qua 39 chính sách tích hợp **và** thông qua 8 detector chỉ dùng cho kiểm toán để phát hiện các mẫu chưa được bao phủ bởi các chính sách thời gian chạy. Các số được tổng hợp theo chính sách/detector trên tất cả các phiên. diff --git a/docs/vi/configuration.mdx b/docs/vi/configuration.mdx index d94dff4c..7b078039 100644 --- a/docs/vi/configuration.mdx +++ b/docs/vi/configuration.mdx @@ -196,13 +196,12 @@ Các lệnh `policies --install` và `policies --uninstall` ghi vào tệp cài - **OpenAI Codex**: `~/.codex/hooks.json` (user), `/.codex/hooks.json` (project) — Codex không có phạm vi `local` - **GitHub Copilot CLI _(beta)_**: `~/.copilot/hooks/failproofai.json` (user), `/.github/hooks/failproofai.json` (project) — Copilot không có phạm vi `local`. Các mục nhập hook sử dụng các trường lệnh `bash`/`powershell` được khóa bằng OS của Copilot với `timeoutSec`; tệp có dấu hiệu `version: 1` ở cấp cao nhất. Hỗ trợ Copilot CLI đang ở **beta** khi chúng tôi xác minh lược đồ bản ghi `events.jsonl` (không được tài liệu công khai chỉ định) so với các phiên làm việc thực tế hơn. - **Cursor Agent _(beta)_**: `~/.cursor/hooks.json` (user), `/.cursor/hooks.json` (project) — Cursor không có phạm vi `local`. Các mục nhập hook sử dụng dạng `{type, command, timeout}` tương tự Claude (không chia tách `bash`/`powershell`), nhưng được lưu trữ dưới các khóa sự kiện camelCase (`preToolUse`, `beforeSubmitPrompt`, …) trong một mảng phẳng theo [schemas hooks](https://cursor.com/docs/hooks) của Cursor; tệp có dấu hiệu `version: 1` ở cấp cao nhất. Trình xử lý chuẩn hóa camelCase → PascalCase qua `CURSOR_EVENT_MAP` vì vậy các chính sách built-in hiện có kích hoạt không thay đổi. Hỗ trợ Cursor Agent đang ở **beta** khi chúng tôi xác minh định dạng transcript trên đĩa của Cursor (không được chỉ định trong tài liệu công khai) so với các cài đặt thực tế hơn. - - **OpenCode _(beta)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (user), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (project) — OpenCode không có phạm vi `local`. Không giống như sáu CLI khác, OpenCode **không có hệ thống hook lệnh bên ngoài**: nó tải các plugin JS/TS in-process được đăng ký rõ ràng qua mảng `plugin: []` trong `opencode.json` (tự động phát hiện từ `.opencode/plugins/` **không** là cách các plugin tải trên opencode v1.14.33). Cài đặt thả một shim plugin nhỏ được tạo ra gọi đến nhị phân failproofai qua subprocess và dịch phản hồi JSON hình dạng Claude của nhị phân trở lại ngữ nghĩa plugin: `throw new Error()` cho tool-event deny (hủy lệnh gọi công cụ), `client.session.prompt(...)` cho instruct VÀ cho `Stop` / `SubagentStop` deny (gửi lý do từ chối dưới dạng thông báo người dùng tiếp theo — kênh thử lại buộc duy nhất vì `session.idle` chỉ là thông báo và ném từ nó là no-op), và no-op cho allow. Shim chuẩn hóa cả tên công cụ (chữ thường → PascalCase qua `OPENCODE_TOOL_MAP`) và khóa đối số đầu vào công cụ (camelCase → snake_case qua `OPENCODE_TOOL_INPUT_MAP` cho `Read` / `Write` / `Edit`, ví dụ: `filePath` → `file_path`, `oldString` → `old_string`) trước khi chuyển tiếp đến nhị phân, vì vậy các built-in kiểm tra đường dẫn như `block-read-outside-cwd`, `block-env-files` và `block-secrets-write` kích hoạt không thay đổi trên các lệnh gọi công cụ OpenCode. Các phiên tồn tại trong cơ sở dữ liệu SQLite của opencode tại `~/.local/share/opencode/opencode.db`; người xem phiên của bảng điều khiển đọc chúng qua `opencode db --format json` và `opencode export `. Hỗ trợ OpenCode đang ở **beta** khi chúng tôi xác minh hành vi trên các phiên bản và so với các phiên làm việc thực tế hơn. Xem [tài liệu plugins OpenCode](https://opencode.ai/docs/plugins/). + - **OpenCode _(beta)_**: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (user), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (project) — OpenCode không có phạm vi `local`. Không giống như năm CLI khác, OpenCode **không có hệ thống hook lệnh bên ngoài**: nó tải các plugin JS/TS in-process được đăng ký rõ ràng qua mảng `plugin: []` trong `opencode.json` (tự động phát hiện từ `.opencode/plugins/` **không** là cách các plugin tải trên opencode v1.14.33). Cài đặt thả một shim plugin nhỏ được tạo ra gọi đến nhị phân failproofai qua subprocess và dịch phản hồi JSON hình dạng Claude của nhị phân trở lại ngữ nghĩa plugin: `throw new Error()` cho tool-event deny (hủy lệnh gọi công cụ), `client.session.prompt(...)` cho instruct VÀ cho `Stop` / `SubagentStop` deny (gửi lý do từ chối dưới dạng thông báo người dùng tiếp theo — kênh thử lại buộc duy nhất vì `session.idle` chỉ là thông báo và ném từ nó là no-op), và no-op cho allow. Shim chuẩn hóa cả tên công cụ (chữ thường → PascalCase qua `OPENCODE_TOOL_MAP`) và khóa đối số đầu vào công cụ (camelCase → snake_case qua `OPENCODE_TOOL_INPUT_MAP` cho `Read` / `Write` / `Edit`, ví dụ: `filePath` → `file_path`, `oldString` → `old_string`) trước khi chuyển tiếp đến nhị phân, vì vậy các built-in kiểm tra đường dẫn như `block-read-outside-cwd`, `block-env-files` và `block-secrets-write` kích hoạt không thay đổi trên các lệnh gọi công cụ OpenCode. Các phiên tồn tại trong cơ sở dữ liệu SQLite của opencode tại `~/.local/share/opencode/opencode.db`; người xem phiên của bảng điều khiển đọc chúng qua `opencode db --format json` và `opencode export `. Hỗ trợ OpenCode đang ở **beta** khi chúng tôi xác minh hành vi trên các phiên bản và so với các phiên làm việc thực tế hơn. Xem [tài liệu plugins OpenCode](https://opencode.ai/docs/plugins/). - **Pi _(beta)_**: `~/.pi/agent/settings.json` (user), `/.pi/settings.json` (project) — Pi không có phạm vi `local`. Pi tải các gói mở rộng TypeScript khi khởi động; tệp cài đặt là một mảng chuỗi phẳng `{"packages": ["./relative/path", …]}`. failproofai viết một mục nhập mảng package duy nhất trỏ vào thư mục `pi-extension/` được đóng gói của nó. Tiện ích mở rộng bên trong đăng ký các sự kiện `tool_call` / `user_bash` / `input` / `session_start` của Pi và shell ra `failproofai --hook --cli pi`; trình xử lý chuẩn hóa underscore_lower_snake_case → PascalCase qua `PI_EVENT_MAP` vì vậy các chính sách built-in hiện có kích hoạt không thay đổi. Các đối số đầu vào công cụ cũng được chuẩn hóa qua `PI_TOOL_INPUT_MAP` (Read / Write / Edit của Pi cung cấp `path` thay vì `file_path`; ánh xạ khóa cấp cao nhất cho phép `block-env-files` và `block-secrets-write` kích hoạt — `block-read-outside-cwd` đã có fallback `path`). Hỗ trợ Pi đang ở **beta** khi Pi's extension API và layout nhật ký phiên ổn định. - - **Gemini CLI _(beta)_**: `~/.gemini/settings.json` (user), `/.gemini/settings.json` (project) — Gemini không có phạm vi `local` (nó ghi lại phạm vi `system` tại `/etc/gemini-cli/settings.json` mà failproofai không tiếp xúc). Các mục nhập hook sử dụng dạng `{type, command, timeout}` của Claude được bao bọc trong lược đồ matcher `{matcher, hooks: [...]}` của Gemini với `matcher: "*"` theo mặc định. Các sự kiện là PascalCase (`SessionStart`, `BeforeAgent`, `AfterAgent`, `BeforeModel`, `AfterModel`, `BeforeToolSelection`, `BeforeTool`, `AfterTool`, `PreCompress`, `Notification`, `SessionEnd`); trình xử lý ánh xạ đến tên Claude canonical qua `GEMINI_EVENT_MAP`. Tên công cụ là snake_case (`run_shell_command`, `read_file`, `write_file`, `replace`, …) — trình xử lý chuẩn hóa qua `GEMINI_TOOL_MAP` vì vậy các chính sách built-in hiện có kích hoạt không thay đổi. Trình đánh giá chính sách phát hành hình dạng phẳng `{decision: "deny", reason}` của Gemini (ưu tiên theo Golden Rule exit-0 contract của Gemini), `{hookSpecificOutput: {hookEventName, additionalContext}}` cho tiêm ngữ cảnh trên BeforeAgent / AfterTool / SessionStart, và `{decision: "block", reason}` trên AfterTool cho ngữ nghĩa thử lại buộc. Hỗ trợ Gemini CLI đang ở **beta** khi chúng tôi mở rộng phạm vi thế giới thực. Xem [tài liệu hooks Gemini CLI](https://geminicli.com/docs/hooks/). - **Hermes (hermes-agent)**: `~/.hermes/config.yaml` (**phạm vi người dùng duy nhất** — Hermes không có cấu hình dự án/cục bộ). Hermes là một **cổng** Slack/Telegram, vì vậy một cài đặt chặn các lệnh gọi công cụ từ mọi nền tảng (Slack/Telegram/cli/cron) **và** các subagent nội bộ. Các mục nhập hook là một cặp `{command, timeout}` (timeout tính bằng **giây**) dưới bản đồ `hooks:` được khóa bằng các sự kiện snake_case của Hermes (`pre_tool_call` / `post_tool_call` / `on_session_start` / `on_session_end` / `subagent_stop`); trình xử lý chuẩn hóa các sự kiện qua `HERMES_EVENT_MAP` và tên công cụ qua `HERMES_TOOL_MAP` vì vậy các chính sách built-in kích hoạt không thay đổi. Cấu hình được chỉnh sửa thông qua vòng quay YAML `Document` bảo tồn nhận xét vì vậy các cài đặt khác của nhà điều hành sống sót, và cài đặt đặt `hooks_auto_accept: true` vì vậy cổng headless (không TTY) chạy các hook mà không có lời nhắc đồng ý. Trình đánh giá phát hành hợp đồng stdout `{"decision":"block","reason"}` của Hermes (Hermes bỏ qua mã thoát). **Hạn chế:** Hermes không có sự kiện `Stop` kết thúc lượt, vì vậy các built-in `require-*-before-stop` không bao giờ kích hoạt cho nó (không áp dụng được, không bị hỏng); `instruct` suy giảm thành allow-with-logged-note (không có kênh ngữ cảnh bổ sung); và redaction bí mật đầu ra (`sanitize-*`) không thể viết lại đầu ra công cụ trên hợp đồng shell-hook. Hermes cũng là một nguồn **audit** **ngoại tuyến** — bảng điều khiển đọc các phiên cổng của nó trực tiếp từ `~/.hermes/state.db`. - **`policies-config.json`** — cho failproofai biết những chính sách nào cần đánh giá và với những tham số nào (chia sẻ trên tất cả các CLI agent) -Chuyển `--cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes` để nhắm vào một agent cụ thể (cách nhau bằng dấu cách hoặc lặp lại cho bất kỳ tập con nào): +Chuyển `--cli claude|codex|copilot|cursor|opencode|pi|hermes` để nhắm vào một agent cụ thể (cách nhau bằng dấu cách hoặc lặp lại cho bất kỳ tập con nào): ```bash failproofai policies --install --cli codex --scope project @@ -210,12 +209,11 @@ failproofai policies --install --cli copilot --scope project failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project -failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user -failproofai policies --install --cli claude codex copilot cursor opencode pi gemini +failproofai policies --install --cli claude codex copilot cursor opencode pi ``` -Khi `--cli` bị bỏ qua, `failproofai` phát hiện những agent CLI nào được cài đặt (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which gemini` / `which hermes`): +Khi `--cli` bị bỏ qua, `failproofai` phát hiện những agent CLI nào được cài đặt (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which hermes`): - **Một CLI được phát hiện** — tự động chọn CLI đó mà không cần nhắc. - **Nhiều CLI được phát hiện** trong terminal tương tác — hiển thị lời nhắc lựa chọn đơn có mũi tên được nhóm thành phần Detected (N) (với hàng tổng hợp Cài đặt cho tất cả N detected + từng CLI được phát hiện riêng lẻ) và phần Not installed (M) · install hooks ahead of time liệt kê mỗi CLI được hỗ trợ không được phát hiện dưới dạng tùy chọn cài đặt trước (↑↓ để di chuyển, Enter để chọn, ^C để thoát). Dòng gỡ cài đặt chỉ hiển thị phần Detected. diff --git a/docs/vi/dashboard.mdx b/docs/vi/dashboard.mdx index 219cbd9b..3e1e0efd 100644 --- a/docs/vi/dashboard.mdx +++ b/docs/vi/dashboard.mdx @@ -25,11 +25,11 @@ Bảng điều khiển đọc trực tiếp từ hệ thống tệp - các thư ### Dự án -Liệt kê tất cả các dự án Claude Code, OpenAI Codex, GitHub Copilot CLI _(beta)_, Cursor Agent _(beta)_, OpenCode _(beta)_, Pi _(beta)_, Gemini CLI _(beta)_ và Hermes được tìm thấy trên máy của bạn. Các dự án Claude được phát hiện từ `~/.claude/projects/` (hoặc đường dẫn được đặt bởi `CLAUDE_PROJECTS_PATH`); các dự án Codex được phát hiện bằng cách quét mỗi bản ghi tạm dưới `~/.codex/sessions///
/*.jsonl` và nhóm theo `cwd` được ghi trong bản ghi đầu tiên của mỗi phiên; các dự án Copilot CLI được phát hiện bằng cách quét từng `~/.copilot/session-state//workspace.yaml` (có thể cấu hình qua `COPILOT_HOME`) và nhóm theo trường `cwd` của nó; các dự án Cursor Agent được phát hiện bằng cách quét siêu dữ liệu cho mỗi phiên dưới `~/.cursor/agent-sessions//` (có thể cấu hình qua `CURSOR_HOME`, với `conversations/` và `sessions/` được thử như giải pháp thay thế) cho một vô hướng `cwd` trong `meta.json` / `session.json` / `workspace.yaml`; các dự án OpenCode được phát hiện bằng cách truy vấn cơ sở dữ liệu SQLite của nó tại `~/.local/share/opencode/opencode.db` qua `opencode db --format json` (chúng tôi đọc các bảng `session` và `project` và nhóm theo `project_id`); các dự án Pi được phát hiện bằng cách quét bản ghi tạm JSONL cho mỗi phiên dưới `~/.pi/agent/sessions//_.jsonl` (có thể cấu hình qua `PI_SESSIONS_DIR`) và lấy `cwd` từ bản ghi đầu tiên của mỗi phiên; các dự án Gemini CLI được phát hiện bằng cách quét `~/.gemini/tmp//chats/session--.jsonl` (có thể cấu hình qua `GEMINI_SESSIONS_DIR`) và khôi phục cwd chính tắc từ dấu `.project_root` phía bên cạnh; các phiên cổng Hermes được đọc trực tiếp từ kho lưu trữ SQLite của nó tại `~/.hermes/state.db` (có thể cấu hình qua `HERMES_DB_PATH`) và được nhóm thành các dự án `hermes-` theo `source` (Slack/Telegram/cli/cron — các phiên cổng không có cwd). Một dự án đã được sử dụng bởi nhiều CLI được hiển thị dưới dạng một hàng duy nhất với tất cả các huy hiệu phù hợp. Sử dụng menu thả xuống **CLI** phía trên bảng để lọc theo một agent CLI cụ thể; URL lưu lựa chọn của bạn dưới dạng `?cli=claude|codex|copilot|cursor|opencode|pi|gemini|hermes`. +Liệt kê tất cả các dự án Claude Code, OpenAI Codex, GitHub Copilot CLI _(beta)_, Cursor Agent _(beta)_, OpenCode _(beta)_, Pi _(beta)_ và Hermes được tìm thấy trên máy của bạn. Các dự án Claude được phát hiện từ `~/.claude/projects/` (hoặc đường dẫn được đặt bởi `CLAUDE_PROJECTS_PATH`); các dự án Codex được phát hiện bằng cách quét mỗi bản ghi tạm dưới `~/.codex/sessions///
/*.jsonl` và nhóm theo `cwd` được ghi trong bản ghi đầu tiên của mỗi phiên; các dự án Copilot CLI được phát hiện bằng cách quét từng `~/.copilot/session-state//workspace.yaml` (có thể cấu hình qua `COPILOT_HOME`) và nhóm theo trường `cwd` của nó; các dự án Cursor Agent được phát hiện bằng cách quét siêu dữ liệu cho mỗi phiên dưới `~/.cursor/agent-sessions//` (có thể cấu hình qua `CURSOR_HOME`, với `conversations/` và `sessions/` được thử như giải pháp thay thế) cho một vô hướng `cwd` trong `meta.json` / `session.json` / `workspace.yaml`; các dự án OpenCode được phát hiện bằng cách truy vấn cơ sở dữ liệu SQLite của nó tại `~/.local/share/opencode/opencode.db` qua `opencode db --format json` (chúng tôi đọc các bảng `session` và `project` và nhóm theo `project_id`); các dự án Pi được phát hiện bằng cách quét bản ghi tạm JSONL cho mỗi phiên dưới `~/.pi/agent/sessions//_.jsonl` (có thể cấu hình qua `PI_SESSIONS_DIR`) và lấy `cwd` từ bản ghi đầu tiên của mỗi phiên; các phiên cổng Hermes được đọc trực tiếp từ kho lưu trữ SQLite của nó tại `~/.hermes/state.db` (có thể cấu hình qua `HERMES_DB_PATH`) và được nhóm thành các dự án `hermes-` theo `source` (Slack/Telegram/cli/cron — các phiên cổng không có cwd). Một dự án đã được sử dụng bởi nhiều CLI được hiển thị dưới dạng một hàng duy nhất với tất cả các huy hiệu phù hợp. Sử dụng menu thả xuống **CLI** phía trên bảng để lọc theo một agent CLI cụ thể; URL lưu lựa chọn của bạn dưới dạng `?cli=claude|codex|copilot|cursor|opencode|pi|hermes`. Mỗi dự án hiển thị: - Tên dự án (được lấy từ đường dẫn thư mục) -- Một huy hiệu CLI — `Claude Code` (cam), `OpenAI Codex` (tím), `GitHub Copilot` (xanh), `Cursor Agent` (lục bảo), `OpenCode` (hổ phách), `Pi` (hồng), `Gemini CLI` (xanh da trời) và/hoặc `Hermes` (chàm) +- Một huy hiệu CLI — `Claude Code` (cam), `OpenAI Codex` (tím), `GitHub Copilot` (xanh), `Cursor Agent` (lục bảo), `OpenCode` (hổ phách), `Pi` (hồng) và/hoặc `Hermes` (chàm) - Ngày của hoạt động phiên gần đây nhất Nhấp vào một dự án để xem các phiên của nó. @@ -48,7 +48,7 @@ Nhấp vào một phiên để mở trình xem phiên. ### Trình xem phiên -Trình xem phiên trả lời câu hỏi quan trọng cho các agent tự động: agent đã làm gì, và nó có duy trì được theo dõi không? Một huy hiệu CLI bên cạnh tiêu đề cho biết phiên là bản ghi Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi, Gemini CLI hay Hermes. Nó hiển thị một dòng thời gian của mọi thứ đã xảy ra trong một phiên: +Trình xem phiên trả lời câu hỏi quan trọng cho các agent tự động: agent đã làm gì, và nó có duy trì được theo dõi không? Một huy hiệu CLI bên cạnh tiêu đề cho biết phiên là bản ghi Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi hay Hermes. Nó hiển thị một dòng thời gian của mọi thứ đã xảy ra trong một phiên: - **Tin nhắn** - Các phản hồi văn bản và lời nhắc của người dùng từ Claude - **Cuộc gọi công cụ** - Mọi công cụ mà Claude gọi, cùng với đầu vào và đầu ra @@ -56,7 +56,7 @@ Trình xem phiên trả lời câu hỏi quan trọng cho các agent tự độn Thanh thống kê ở phía trên cùng hiển thị thời lượng phiên, tổng số lệnh gọi công cụ và tóm tắt các quyết định hook (số lượng allow / deny / instruct). -Nhấp vào nút **Download Logs** để xuất phiên. Đối với các phiên Claude Code, Codex, Copilot, Cursor, Pi và Gemini, bạn sẽ nhận được bản ghi JSONL trên đĩa nguyên gốc byte-for-byte; đối với OpenCode (các phiên của nó nằm trong SQLite, không phải trên đĩa), bạn sẽ nhận được một tài liệu JSON phản ánh các bảng `session` / `messages` / `parts` cơ bản. +Nhấp vào nút **Download Logs** để xuất phiên. Đối với các phiên Claude Code, Codex, Copilot, Cursor và Pi, bạn sẽ nhận được bản ghi JSONL trên đĩa nguyên gốc byte-for-byte; đối với OpenCode (các phiên của nó nằm trong SQLite, không phải trên đĩa), bạn sẽ nhận được một tài liệu JSON phản ánh các bảng `session` / `messages` / `parts` cơ bản. ### Kiểm toán @@ -76,16 +76,16 @@ Một trang hai tab để quản lý chính sách và xem xét hoạt động. - - Chọn nhiều agent CLI nào mà failproofai bảo vệ từ một bảng điều khiển duy nhất — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi, Gemini CLI và Hermes đều có một hàng với trạng thái cài đặt (`Active` / `Detected` / `Inactive`), đường dẫn cài đặt phạm vi người dùng và một điểm nhấn có thương hiệu. Chọn hoặc bỏ chọn các CLI bạn muốn và nhấp `Apply changes` để cài đặt/gỡ cài đặt chênh lệch trong một bước. Các CLI có tệp nhị phân được phát hiện trên PATH được tính sẵn. + - Chọn nhiều agent CLI nào mà failproofai bảo vệ từ một bảng điều khiển duy nhất — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi và Hermes đều có một hàng với trạng thái cài đặt (`Active` / `Detected` / `Inactive`), đường dẫn cài đặt phạm vi người dùng và một điểm nhấn có thương hiệu. Chọn hoặc bỏ chọn các CLI bạn muốn và nhấp `Apply changes` để cài đặt/gỡ cài đặt chênh lệch trong một bước. Các CLI có tệp nhị phân được phát hiện trên PATH được tính sẵn. - Bật hoặc tắt các chính sách riêng lẻ bằng một lần nhấp (ghi vào `~/.failproofai/policies-config.json` — được chia sẻ trên mọi CLI được cài đặt) - Mở rộng một chính sách để cấu hình các tham số của nó (đối với các chính sách hỗ trợ `policyParams`) - Đặt đường dẫn tệp chính sách tùy chỉnh - Lịch sử được phân trang đầy đủ của mọi sự kiện hook đã được kích hoạt trên tất cả các phiên - - Lọc theo quyết định, loại sự kiện, CLI (Claude Code / OpenAI Codex / GitHub Copilot _(beta)_ / Cursor Agent _(beta)_ / OpenCode _(beta)_ / Pi _(beta)_ / Gemini CLI _(beta)_ / Hermes), tên chính sách hoặc ID phiên - - Mỗi hàng hiển thị: dấu thời gian, tên chính sách, quyết định, huy hiệu CLI (cam = Claude Code, tím = OpenAI Codex, xanh = GitHub Copilot, lục bảo = Cursor Agent, hổ phách = OpenCode, hồng = Pi, xanh da trời = Gemini CLI, chàm = Hermes), tên công cụ, ID phiên và lý do cho các quyết định deny/instruct - - Nhấp vào ID phiên để mở bản ghi của nó — trình xem tự động phát hiện CLI nào được kích hoạt hook (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Gemini CLI `~/.gemini/tmp//chats/.jsonl`, Hermes `~/.hermes/state.db`) và hiển thị huy hiệu CLI phù hợp trong tiêu đề + - Lọc theo quyết định, loại sự kiện, CLI (Claude Code / OpenAI Codex / GitHub Copilot _(beta)_ / Cursor Agent _(beta)_ / OpenCode _(beta)_ / Pi _(beta)_ / Hermes), tên chính sách hoặc ID phiên + - Mỗi hàng hiển thị: dấu thời gian, tên chính sách, quyết định, huy hiệu CLI (cam = Claude Code, tím = OpenAI Codex, xanh = GitHub Copilot, lục bảo = Cursor Agent, hổ phách = OpenCode, hồng = Pi, chàm = Hermes), tên công cụ, ID phiên và lý do cho các quyết định deny/instruct + - Nhấp vào ID phiên để mở bản ghi của nó — trình xem tự động phát hiện CLI nào được kích hoạt hook (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Hermes `~/.hermes/state.db`) và hiển thị huy hiệu CLI phù hợp trong tiêu đề diff --git a/docs/vi/getting-started.mdx b/docs/vi/getting-started.mdx index f81b0633..5800980a 100644 --- a/docs/vi/getting-started.mdx +++ b/docs/vi/getting-started.mdx @@ -37,9 +37,9 @@ bun add -g failproofai failproofai policies --install ``` - Lệnh này ghi các hook entries vào CLI agents đã cài đặt của bạn (Claude Code's `~/.claude/settings.json`, OpenAI Codex's `~/.codex/hooks.json`, GitHub Copilot CLI's `~/.copilot/hooks/failproofai.json`, Cursor Agent's `~/.cursor/hooks.json`, OpenCode's generated plugin shim at `~/.config/opencode/plugins/failproofai.mjs` plus a registration entry in `~/.config/opencode/opencode.json`'s `plugin` array, Pi's `~/.pi/agent/settings.json`, Gemini CLI's `~/.gemini/settings.json`, or Hermes's `~/.hermes/config.yaml`). Khi có nhiều hơn một cái có mặt, bạn sẽ được nhắc; truyền `--cli claude codex copilot cursor opencode pi gemini hermes` (bất kỳ tập hợp con nào) để bỏ qua lời nhắc. + Lệnh này ghi các hook entries vào CLI agents đã cài đặt của bạn (Claude Code's `~/.claude/settings.json`, OpenAI Codex's `~/.codex/hooks.json`, GitHub Copilot CLI's `~/.copilot/hooks/failproofai.json`, Cursor Agent's `~/.cursor/hooks.json`, OpenCode's generated plugin shim at `~/.config/opencode/plugins/failproofai.mjs` plus a registration entry in `~/.config/opencode/opencode.json`'s `plugin` array, Pi's `~/.pi/agent/settings.json`, or Hermes's `~/.hermes/config.yaml`). Khi có nhiều hơn một cái có mặt, bạn sẽ được nhắc; truyền `--cli claude codex copilot cursor opencode pi hermes` (bất kỳ tập hợp con nào) để bỏ qua lời nhắc. - GitHub Copilot CLI, Cursor Agent, OpenCode, Pi, và Gemini CLI support là **beta** — cài đặt với `--cli copilot`, `--cli cursor`, `--cli opencode`, `--cli pi`, hoặc `--cli gemini`. Hermes (hermes-agent, một gateway Slack/Telegram) cài đặt user-scope với `--cli hermes` và là **cũng** một nguồn audit offline. + GitHub Copilot CLI, Cursor Agent, OpenCode, và Pi support là **beta** — cài đặt với `--cli copilot`, `--cli cursor`, `--cli opencode`, hoặc `--cli pi`. Hermes (hermes-agent, một gateway Slack/Telegram) cài đặt user-scope với `--cli hermes` và là **cũng** một nguồn audit offline. ```bash failproofai policies --install --scope project @@ -48,7 +48,6 @@ bun add -g failproofai failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project - failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user failproofai policies --install block-sudo block-rm-rf sanitize-api-keys ``` diff --git a/docs/vi/introduction.mdx b/docs/vi/introduction.mdx index daaf6270..cd0d7efe 100644 --- a/docs/vi/introduction.mdx +++ b/docs/vi/introduction.mdx @@ -5,7 +5,7 @@ description: "FailproofAI cung cấp 39 chính sách xử lý lỗi tích hợp [![npm weekly downloads](https://img.shields.io/npm/dw/failproofai?style=flat-square&color=2ea44f)](https://www.npmjs.com/package/failproofai) -Hooks và chính sách cho **xử lý lỗi AI**, **phục hồi lỗi**, và **độ tin cậy LLM**. Giữ cho các AI agents của bạn luôn đáng tin cậy và hoạt động tự trị trên **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Gemini CLI**, **Hermes**, và **Agents SDK**. +Hooks và chính sách cho **xử lý lỗi AI**, **phục hồi lỗi**, và **độ tin cậy LLM**. Giữ cho các AI agents của bạn luôn đáng tin cậy và hoạt động tự trị trên **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Hermes**, và **Agents SDK**. Các AI agents thất bại theo những cách có thể dự đoán được. Chúng chạy các lệnh tàn phá, rò rỉ bí mật, lạc khỏi nhiệm vụ, mắc kẹt trong các vòng lặp, hoặc push trực tiếp lên main. Nếu không được giám sát, những lỗi nhỏ sẽ dẫn đến sự cố toàn hệ thống, rò rỉ thông tin xác thực, và mất công việc. diff --git a/docs/zh/built-in-policies.mdx b/docs/zh/built-in-policies.mdx index 8e2bd6da..8633a07b 100644 --- a/docs/zh/built-in-policies.mdx +++ b/docs/zh/built-in-policies.mdx @@ -704,7 +704,7 @@ Hook 处理程序对 stdin 的载荷大小有 1 MB 的限制。若要使用较 ### 各 CLI 的 Stop 语义 -由于七种受支持的 CLI 各自暴露不同的"Agent 完成"Hook 合约,Stop 强制执行在各 CLI 上的表现略有不同。**结果**是相同的——Agent 在工作流门控未通过时无法停止——但**机制**有所差异。下表作出了总结;只有 Pi 存在一个值得您在启用 `require-*-before-stop` 策略前了解的用户可见差异。 +由于六种受支持的 CLI 各自暴露不同的"Agent 完成"Hook 合约,Stop 强制执行在各 CLI 上的表现略有不同。**结果**是相同的——Agent 在工作流门控未通过时无法停止——但**机制**有所差异。下表作出了总结;只有 Pi 存在一个值得您在启用 `require-*-before-stop` 策略前了解的用户可见差异。 | CLI | 门控触发时机 | 您看到的效果 | |---|---|---| @@ -712,20 +712,19 @@ Hook 处理程序对 stdin 的载荷大小有 1 MB 的限制。若要使用较 | Codex | 同一 Agent 循环,立即触发 | 与 Claude 相同。 | | GitHub Copilot CLI | 同一 Agent 循环,立即触发 | 与 Claude 相同(使用 Copilot 的 `{decision:"block", reason}` 重试通道——已针对 Copilot CLI 1.0.41 实证验证)。 | | Cursor Agent | 同一 Agent 循环,立即触发 | 与 Claude 相同(使用 Cursor 的 `{followup_message}` 通道——上限为 `loop_limit`,默认 5 次重试)。 | -| Gemini CLI | 同一 Agent 循环,立即触发 | 与 Claude 相同(使用 Gemini 的 `{decision:"block", reason}` 通道,位于 `AfterAgent`)。 | | OpenCode | 同一 Agent 循环,立即触发 | 与 Claude 相同(使用 OpenCode 的 `client.session.prompt(...)` SDK 调用,通过 `hookSpecificOutput.additionalContext` 路由)。 | | **Pi (pi-coding-agent)** | **下一个用户轮次** | **Pi 会明显停止**——当门控触发时,其 Agent 循环退出,控制权回到提示符。下次您提交提示时门控才会触发:failproofai 会在该轮次的系统提示开头附加一条 `MANDATORY ACTION REQUIRED` 指令,要求 LLM 在执行您请求的操作之前先完成工作流步骤(提交、推送等)。 | -**Pi 的限制。** Pi 的 `AgentEndEvent`(上游等效于 Claude 的 `Stop` Hook)没有 Result 类型——当它触发时,Pi 的 Agent 循环已经退出。Pi 无法像 Claude / Copilot / Cursor / Gemini / OpenCode 那样被强制重试同一循环。failproofai 将门控转移到 Pi 的 `before_agent_start` 事件(在下一个用户提示后触发),这样工作流检查依然会被执行,只是在下一轮而非当前轮次。 +**Pi 的限制。** Pi 的 `AgentEndEvent`(上游等效于 Claude 的 `Stop` Hook)没有 Result 类型——当它触发时,Pi 的 Agent 循环已经退出。Pi 无法像 Claude / Copilot / Cursor / OpenCode 那样被强制重试同一循环。failproofai 将门控转移到 Pi 的 `before_agent_start` 事件(在下一个用户提示后触发),这样工作流检查依然会被执行,只是在下一轮而非当前轮次。 **实际影响:** - Pi 停止后,拒绝原因会以 Pi 会话 ID 为键存储在内存中。您在同一 Pi 进程中提交的下一个提示会消费它:LLM 在系统提示顶部看到 `MANDATORY ACTION REQUIRED` 指令,先完成提交(或推送/开 PR/等待 CI),然后才继续您的请求。该拒绝原因是一次性的——消费后门控即清除。 -- 门控的生命周期受 Pi 进程生命周期限制。如果您在两次轮次之间 `Ctrl+C` 或退出 Pi,内存中的条目会随进程一起丢失,门控也就错过了。Claude、Copilot、Cursor、Gemini 和 OpenCode 有相同的限制(杀掉 Agent 则门控错过)——只是 Pi 更为明显,因为 Agent 在门控触发之前就已可见地退出了。 +- 门控的生命周期受 Pi 进程生命周期限制。如果您在两次轮次之间 `Ctrl+C` 或退出 Pi,内存中的条目会随进程一起丢失,门控也就错过了。Claude、Copilot、Cursor 和 OpenCode 有相同的限制(杀掉 Agent 则门控错过)——只是 Pi 更为明显,因为 Agent 在门控触发之前就已可见地退出了。 - 待处理的拒绝在任何原因导致的 `session_shutdown` 时也会被清除(`new` / `resume` / `fork` / `quit`),因此来自之前会话的过期门控不会泄漏到在同一 Pi 进程中启动的新会话中。 -如果您需要类似 Claude 的同循环重试,请在其他六种受支持的 CLI 下运行您的 `Stop` 策略。我们正在跟踪 Pi 上游,等待 `AgentEndEvent` 上未来的 Result 类型以弥补这一差距。 +如果您需要类似 Claude 的同循环重试,请在其他五种受支持的 CLI 下运行您的 `Stop` 策略。我们正在跟踪 Pi 上游,等待 `AgentEndEvent` 上未来的 Result 类型以弥补这一差距。 ### `require-commit-before-stop` diff --git a/docs/zh/cli/audit.mdx b/docs/zh/cli/audit.mdx index d6d90c87..59abec2e 100644 --- a/docs/zh/cli/audit.mdx +++ b/docs/zh/cli/audit.mdx @@ -48,7 +48,7 @@ failproofai 运行 `failproofai audit -h`(或 `--help`)查看使用说明。审计**完全离线**运行——无需账号或网络——仪表板会持续运行,直到您按 `Ctrl+C` 停止。 -仪表板会扫描本机上过去的智能体 CLI 记录(Claude Code、Codex、Copilot、Cursor、OpenCode、Pi、Gemini),并报告智能体执行了哪些 failproofai 旨在阻止的操作——环境变量检查、强制推送、冗余的 `cd ` 前缀、轮询 sleep 循环、重复读取刚编辑的文件等。 +仪表板会扫描本机上过去的智能体 CLI 记录(Claude Code、Codex、Copilot、Cursor、OpenCode、Pi),并报告智能体执行了哪些 failproofai 旨在阻止的操作——环境变量检查、强制推送、冗余的 `cd ` 前缀、轮询 sleep 循环、重复读取刚编辑的文件等。 对于每条记录,每个工具调用事件都会通过 39 条内置策略以及 8 个仅限审计的检测器进行回放——这些检测器可识别运行时策略尚未覆盖的模式。计数按策略/检测器维度在所有会话中汇总。 diff --git a/docs/zh/configuration.mdx b/docs/zh/configuration.mdx index 21fbaf5f..0202acdc 100644 --- a/docs/zh/configuration.mdx +++ b/docs/zh/configuration.mdx @@ -196,13 +196,12 @@ resolved: { allowPatterns: ["sudo systemctl status"] } ← 回退到 global - **OpenAI Codex**:`~/.codex/hooks.json`(用户级)、`/.codex/hooks.json`(项目级)——Codex 没有 `local` 作用域 - **GitHub Copilot CLI _(beta)_**:`~/.copilot/hooks/failproofai.json`(用户级)、`/.github/hooks/failproofai.json`(项目级)——Copilot 没有 `local` 作用域。Hook 条目使用 Copilot 基于操作系统的 `bash`/`powershell` 命令字段,并带有 `timeoutSec`;文件顶层包含 `version: 1` 标记。Copilot CLI 支持目前为 **beta** 版本,我们正在针对更多实际会话验证 `events.jsonl` 记录模式(公开文档中未作说明)。 - **Cursor Agent _(beta)_**:`~/.cursor/hooks.json`(用户级)、`/.cursor/hooks.json`(项目级)——Cursor 没有 `local` 作用域。Hook 条目使用 Claude 风格的 `{type, command, timeout}` 格式(无 `bash`/`powershell` 拆分),但按照 Cursor 的 [hooks schema](https://cursor.com/docs/hooks) 以驼峰式事件键(`preToolUse`、`beforeSubmitPrompt` 等)存储在扁平数组中;文件顶层包含 `version: 1` 标记。处理器通过 `CURSOR_EVENT_MAP` 将驼峰式键规范化为帕斯卡式,使现有内置策略能够正常触发。Cursor Agent 支持目前为 **beta** 版本,我们正在针对更多实际安装验证 Cursor 的磁盘上转录格式(公开文档未作说明)。 - - **OpenCode _(beta)_**:`~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs`(用户级)、`/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs`(项目级)——OpenCode 没有 `local` 作用域。与其他六种 CLI 不同,OpenCode **没有外部命令 hook 系统**:它通过 `opencode.json` 的 `plugin: []` 数组显式注册来加载进程内 JS/TS 插件(自动发现 `.opencode/plugins/` 目录**不是** opencode v1.14.33 插件的加载方式)。安装时会生成一个小型插件 shim,以子进程方式调用 failproofai 二进制文件,并将二进制文件的 Claude 风格 JSON 响应转换回插件语义:工具事件拒绝时 `throw new Error()`(取消工具调用);instruct 以及 `Stop` / `SubagentStop` 拒绝时调用 `client.session.prompt(...)`(将拒绝原因作为下一条用户消息提交——这是唯一的强制重试通道,因为 `session.idle` 仅用于通知,从中抛出异常无效);allow 时不执行任何操作。该 shim 在转发给二进制文件前会规范化工具名称(小写 → 帕斯卡式,通过 `OPENCODE_TOOL_MAP`)和工具输入参数键(驼峰式 → 下划线式,通过 `OPENCODE_TOOL_INPUT_MAP` 适用于 `Read` / `Write` / `Edit`,例如 `filePath` → `file_path`、`oldString` → `old_string`),从而使 `block-read-outside-cwd`、`block-env-files` 和 `block-secrets-write` 等路径检查内置策略在 OpenCode 工具调用中正常触发。会话存储在 `~/.local/share/opencode/opencode.db` 的 SQLite 数据库中;仪表盘的会话查看器通过 `opencode db --format json` 和 `opencode export ` 读取它们。OpenCode 支持目前为 **beta** 版本,我们正在跨版本和更多实际会话中验证其行为。详见 [OpenCode plugins 文档](https://opencode.ai/docs/plugins/)。 + - **OpenCode _(beta)_**:`~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs`(用户级)、`/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs`(项目级)——OpenCode 没有 `local` 作用域。与其他五种 CLI 不同,OpenCode **没有外部命令 hook 系统**:它通过 `opencode.json` 的 `plugin: []` 数组显式注册来加载进程内 JS/TS 插件(自动发现 `.opencode/plugins/` 目录**不是** opencode v1.14.33 插件的加载方式)。安装时会生成一个小型插件 shim,以子进程方式调用 failproofai 二进制文件,并将二进制文件的 Claude 风格 JSON 响应转换回插件语义:工具事件拒绝时 `throw new Error()`(取消工具调用);instruct 以及 `Stop` / `SubagentStop` 拒绝时调用 `client.session.prompt(...)`(将拒绝原因作为下一条用户消息提交——这是唯一的强制重试通道,因为 `session.idle` 仅用于通知,从中抛出异常无效);allow 时不执行任何操作。该 shim 在转发给二进制文件前会规范化工具名称(小写 → 帕斯卡式,通过 `OPENCODE_TOOL_MAP`)和工具输入参数键(驼峰式 → 下划线式,通过 `OPENCODE_TOOL_INPUT_MAP` 适用于 `Read` / `Write` / `Edit`,例如 `filePath` → `file_path`、`oldString` → `old_string`),从而使 `block-read-outside-cwd`、`block-env-files` 和 `block-secrets-write` 等路径检查内置策略在 OpenCode 工具调用中正常触发。会话存储在 `~/.local/share/opencode/opencode.db` 的 SQLite 数据库中;仪表盘的会话查看器通过 `opencode db --format json` 和 `opencode export ` 读取它们。OpenCode 支持目前为 **beta** 版本,我们正在跨版本和更多实际会话中验证其行为。详见 [OpenCode plugins 文档](https://opencode.ai/docs/plugins/)。 - **Pi _(beta)_**:`~/.pi/agent/settings.json`(用户级)、`/.pi/settings.json`(项目级)——Pi 没有 `local` 作用域。Pi 在启动时加载 TypeScript 扩展包;设置文件是一个扁平字符串数组 `{"packages": ["./relative/path", …]}`。failproofai 在 packages 数组中写入一个指向其捆绑的 `pi-extension/` 目录的条目。该扩展内部订阅 Pi 的 `tool_call` / `user_bash` / `input` / `session_start` 事件,并调用 `failproofai --hook --cli pi`;处理器通过 `PI_EVENT_MAP` 将下划线小写蛇形命名规范化为帕斯卡式,使现有内置策略正常触发。工具输入参数也通过 `PI_TOOL_INPUT_MAP` 规范化(Pi 的 Read / Write / Edit 使用 `path` 而非 `file_path`;映射顶层键后 `block-env-files` 和 `block-secrets-write` 可正常触发——`block-read-outside-cwd` 本身已有 `path` 的回退逻辑)。Pi 支持目前为 **beta** 版本,有待 Pi 的扩展 API 和会话日志格式稳定。 - - **Gemini CLI _(beta)_**:`~/.gemini/settings.json`(用户级)、`/.gemini/settings.json`(项目级)——Gemini 没有 `local` 作用域(其文档记录了 `/etc/gemini-cli/settings.json` 的 `system` 作用域,failproofai 未对外暴露该作用域)。Hook 条目使用 Claude 的 `{type, command, timeout}` 格式,包裹在 Gemini 的 `{matcher, hooks: [...]}` 匹配器模式中,默认 `matcher: "*"`。事件采用帕斯卡式(`SessionStart`、`BeforeAgent`、`AfterAgent`、`BeforeModel`、`AfterModel`、`BeforeToolSelection`、`BeforeTool`、`AfterTool`、`PreCompress`、`Notification`、`SessionEnd`);处理器通过 `GEMINI_EVENT_MAP` 映射到 Claude 规范名称。工具名称采用蛇形命名(`run_shell_command`、`read_file`、`write_file`、`replace` 等)——处理器通过 `GEMINI_TOOL_MAP` 规范化,使现有内置策略正常触发。策略评估器输出 Gemini 的扁平 `{decision: "deny", reason}` 格式(符合 Gemini 的"黄金规则"exit-0 约定),在 BeforeAgent / AfterTool / SessionStart 时输出 `{hookSpecificOutput: {hookEventName, additionalContext}}` 进行上下文注入,在 AfterAgent 时输出 `{decision: "block", reason}` 实现强制重试语义。Gemini CLI 支持目前为 **beta** 版本,我们正在扩大实际场景覆盖范围。详见 [Gemini CLI hooks 文档](https://geminicli.com/docs/hooks/)。 - **Hermes (hermes-agent)**:`~/.hermes/config.yaml`(**仅用户作用域**——Hermes 没有项目/本地配置)。Hermes 是一个 Slack/Telegram **网关**,因此一次安装即可拦截来自所有平台(Slack/Telegram/cli/cron)**以及**内部子代理的工具调用。Hook 条目是 `hooks:` 映射下的 `{command, timeout}` 对(timeout 单位为**秒**),以 Hermes 的蛇形命名事件为键(`pre_tool_call` / `post_tool_call` / `on_session_start` / `on_session_end` / `subagent_stop`);处理器通过 `HERMES_EVENT_MAP` 规范化事件,通过 `HERMES_TOOL_MAP` 规范化工具名称,使内置策略正常触发。配置通过保留注释的 YAML `Document` 往返编辑,以确保操作者的其他设置不受影响;安装时将 `hooks_auto_accept: true` 设置为 true,以便无头网关(无 TTY)在无需确认提示的情况下运行 hook。评估器输出 Hermes 的 `{"decision":"block","reason"}` stdout 约定(Hermes 忽略退出码)。**限制:** Hermes 没有会话结束 `Stop` 事件,因此 `require-*-before-stop` 内置策略永远不会为其触发(不适用,并非故障);`instruct` 降级为允许并记录日志(无额外上下文通道);输出密钥清理(`sanitize-*`)无法通过 shell hook 约定重写工具输出。Hermes **同时**也是一个离线**审计**数据源——仪表盘直接从 `~/.hermes/state.db` 读取其网关会话。 - **`policies-config.json`** — 告知 failproofai 评估哪些策略及其参数(在所有代理 CLI 之间共享) -通过 `--cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes` 指定目标代理(可用空格分隔或重复指定多个): +通过 `--cli claude|codex|copilot|cursor|opencode|pi|hermes` 指定目标代理(可用空格分隔或重复指定多个): ```bash failproofai policies --install --cli codex --scope project @@ -210,12 +209,11 @@ failproofai policies --install --cli copilot --scope project failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project -failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user -failproofai policies --install --cli claude codex copilot cursor opencode pi gemini +failproofai policies --install --cli claude codex copilot cursor opencode pi ``` -省略 `--cli` 时,`failproofai` 会自动检测已安装的代理 CLI(`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which gemini` / `which hermes`): +省略 `--cli` 时,`failproofai` 会自动检测已安装的代理 CLI(`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which hermes`): - **检测到一个 CLI** — 自动选择该 CLI,无需提示。 - **在交互式终端中检测到多个 CLI** — 显示方向键单选菜单,分为 `Detected (N)` 区域(包含一个"为所有 N 个已检测到的 CLI 安装"的聚合行及各个已检测 CLI)和 `Not installed (M) · install hooks ahead of time` 区域(列出所有未检测到的受支持 CLI 作为提前安装选项)(↑↓ 移动,回车选择,^C 退出)。卸载流程仅显示 Detected 区域。 diff --git a/docs/zh/dashboard.mdx b/docs/zh/dashboard.mdx index 721b39e2..d28f794b 100644 --- a/docs/zh/dashboard.mdx +++ b/docs/zh/dashboard.mdx @@ -24,11 +24,11 @@ failproofai ### 项目 -列出在你的机器上发现的所有 Claude Code、OpenAI Codex、GitHub Copilot CLI _(beta)_、Cursor Agent _(beta)_、OpenCode _(beta)_、Pi _(beta)_、Gemini CLI _(beta)_ 和 Hermes 项目。Claude 项目从 `~/.claude/projects/`(或由 `CLAUDE_PROJECTS_PATH` 设置的路径)中发现;Codex 项目通过扫描 `~/.codex/sessions///
/*.jsonl` 下的所有记录,并按每个会话第一条记录中的 `cwd` 字段分组发现;Copilot CLI 项目通过扫描 `~/.copilot/session-state//workspace.yaml`(可通过 `COPILOT_HOME` 配置),并按其 `cwd` 字段分组发现;Cursor Agent 项目通过扫描 `~/.cursor/agent-sessions//`(可通过 `CURSOR_HOME` 配置,并以 `conversations/` 和 `sessions/` 作为备选路径)下的逐会话元数据,从 `meta.json` / `session.json` / `workspace.yaml` 中的 `cwd` 标量发现;OpenCode 项目通过 `opencode db --format json` 查询位于 `~/.local/share/opencode/opencode.db` 的 SQLite 数据库发现(读取 `session` 和 `project` 表并按 `project_id` 分组);Pi 项目通过扫描 `~/.pi/agent/sessions//_.jsonl`(可通过 `PI_SESSIONS_DIR` 配置)下的逐会话 JSONL 记录,并从每个会话的第一条记录中提取 `cwd` 发现;Gemini CLI 项目通过扫描 `~/.gemini/tmp//chats/session--.jsonl`(可通过 `GEMINI_SESSIONS_DIR` 配置),并从同级 `.project_root` 文本标记中恢复规范化 cwd 发现;Hermes 网关会话直接从 `~/.hermes/state.db`(可通过 `HERMES_DB_PATH` 配置)的 SQLite 存储中读取,并按 `source`(Slack/Telegram/cli/cron——网关会话没有 cwd)分组为 `hermes-` 项目。被多个 CLI 使用的项目在同一行显示,并附上所有匹配的徽章。使用表格上方的 **CLI** 下拉菜单按特定 Agent CLI 筛选;URL 会将你的选择保存为 `?cli=claude|codex|copilot|cursor|opencode|pi|gemini|hermes`。 +列出在你的机器上发现的所有 Claude Code、OpenAI Codex、GitHub Copilot CLI _(beta)_、Cursor Agent _(beta)_、OpenCode _(beta)_、Pi _(beta)_ 和 Hermes 项目。Claude 项目从 `~/.claude/projects/`(或由 `CLAUDE_PROJECTS_PATH` 设置的路径)中发现;Codex 项目通过扫描 `~/.codex/sessions///
/*.jsonl` 下的所有记录,并按每个会话第一条记录中的 `cwd` 字段分组发现;Copilot CLI 项目通过扫描 `~/.copilot/session-state//workspace.yaml`(可通过 `COPILOT_HOME` 配置),并按其 `cwd` 字段分组发现;Cursor Agent 项目通过扫描 `~/.cursor/agent-sessions//`(可通过 `CURSOR_HOME` 配置,并以 `conversations/` 和 `sessions/` 作为备选路径)下的逐会话元数据,从 `meta.json` / `session.json` / `workspace.yaml` 中的 `cwd` 标量发现;OpenCode 项目通过 `opencode db --format json` 查询位于 `~/.local/share/opencode/opencode.db` 的 SQLite 数据库发现(读取 `session` 和 `project` 表并按 `project_id` 分组);Pi 项目通过扫描 `~/.pi/agent/sessions//_.jsonl`(可通过 `PI_SESSIONS_DIR` 配置)下的逐会话 JSONL 记录,并从每个会话的第一条记录中提取 `cwd` 发现;Hermes 网关会话直接从 `~/.hermes/state.db`(可通过 `HERMES_DB_PATH` 配置)的 SQLite 存储中读取,并按 `source`(Slack/Telegram/cli/cron——网关会话没有 cwd)分组为 `hermes-` 项目。被多个 CLI 使用的项目在同一行显示,并附上所有匹配的徽章。使用表格上方的 **CLI** 下拉菜单按特定 Agent CLI 筛选;URL 会将你的选择保存为 `?cli=claude|codex|copilot|cursor|opencode|pi|hermes`。 每个项目显示: - 项目名称(从文件夹路径派生) -- CLI 徽章——`Claude Code`(橙色)、`OpenAI Codex`(紫色)、`GitHub Copilot`(蓝色)、`Cursor Agent`(翠绿色)、`OpenCode`(琥珀色)、`Pi`(粉色)、`Gemini CLI`(天蓝色)和/或 `Hermes`(靛蓝色) +- CLI 徽章——`Claude Code`(橙色)、`OpenAI Codex`(紫色)、`GitHub Copilot`(蓝色)、`Cursor Agent`(翠绿色)、`OpenCode`(琥珀色)、`Pi`(粉色)和/或 `Hermes`(靛蓝色) - 最近一次会话活动的日期 点击项目可查看其会话列表。 @@ -47,7 +47,7 @@ failproofai ### 会话查看器 -会话查看器回答了自主 Agent 的核心问题:Agent 做了什么,是否保持在正轨上?页眉旁的 CLI 徽章指示该会话是 Claude Code、OpenAI Codex、GitHub Copilot CLI、Cursor Agent、OpenCode、Pi、Gemini CLI 还是 Hermes 的记录。它以时间线形式展示会话中发生的所有事件: +会话查看器回答了自主 Agent 的核心问题:Agent 做了什么,是否保持在正轨上?页眉旁的 CLI 徽章指示该会话是 Claude Code、OpenAI Codex、GitHub Copilot CLI、Cursor Agent、OpenCode、Pi 还是 Hermes 的记录。它以时间线形式展示会话中发生的所有事件: - **消息** - Claude 的文本回复和用户提示 - **工具调用** - Claude 调用的每个工具,包含其输入和输出 @@ -55,7 +55,7 @@ failproofai 顶部的统计栏显示会话时长、工具调用总数,以及 Hook 决策摘要(allow / deny / instruct 计数)。 -点击**下载日志**按钮可导出会话。对于 Claude Code、Codex、Copilot、Cursor、Pi 和 Gemini 会话,你将获得原始的磁盘 JSONL 记录文件(逐字节原样);对于 OpenCode(其会话存储在 SQLite 而非磁盘上),你将获得一个 JSON 文档,镜像底层的 `session` / `messages` / `parts` 表。 +点击**下载日志**按钮可导出会话。对于 Claude Code、Codex、Copilot、Cursor 和 Pi 会话,你将获得原始的磁盘 JSONL 记录文件(逐字节原样);对于 OpenCode(其会话存储在 SQLite 而非磁盘上),你将获得一个 JSON 文档,镜像底层的 `session` / `messages` / `parts` 表。 ### 审计 @@ -75,16 +75,16 @@ failproofai - - 在单一面板中多选 failproofai 保护哪些 Agent CLI——Claude Code、OpenAI Codex、GitHub Copilot、Cursor Agent、OpenCode、Pi、Gemini CLI 和 Hermes 各有一行,显示安装状态(`Active` / `Detected` / `Inactive`)、用户范围设置路径,以及品牌色强调。勾选或取消勾选你想要的 CLI,点击 `Apply changes` 一步完成安装/卸载差异。已在 PATH 中检测到二进制文件的 CLI 会被预先勾选。 + - 在单一面板中多选 failproofai 保护哪些 Agent CLI——Claude Code、OpenAI Codex、GitHub Copilot、Cursor Agent、OpenCode、Pi 和 Hermes 各有一行,显示安装状态(`Active` / `Detected` / `Inactive`)、用户范围设置路径,以及品牌色强调。勾选或取消勾选你想要的 CLI,点击 `Apply changes` 一步完成安装/卸载差异。已在 PATH 中检测到二进制文件的 CLI 会被预先勾选。 - 一键开启或关闭单个策略(写入 `~/.failproofai/policies-config.json`——所有已安装的 CLI 共享) - 展开策略以配置其参数(适用于支持 `policyParams` 的策略) - 设置自定义策略文件路径 - 所有会话中触发的每个 Hook 事件的完整分页历史记录 - - 按决策、事件类型、CLI(Claude Code / OpenAI Codex / GitHub Copilot _(beta)_ / Cursor Agent _(beta)_ / OpenCode _(beta)_ / Pi _(beta)_ / Gemini CLI _(beta)_ / Hermes)、策略名称或会话 ID 筛选 - - 每行显示:时间戳、策略名称、决策、CLI 徽章(橙色 = Claude Code,紫色 = OpenAI Codex,蓝色 = GitHub Copilot,翠绿色 = Cursor Agent,琥珀色 = OpenCode,粉色 = Pi,天蓝色 = Gemini CLI,靛蓝色 = Hermes)、工具名称、会话 ID,以及 deny/instruct 决策的原因 - - 点击会话 ID 可打开其记录——查看器自动检测触发 Hook 的 CLI(Claude `~/.claude/projects/…`、Codex `~/.codex/sessions/…`、Copilot CLI `~/.copilot/session-state//events.jsonl`、Cursor Agent `~/.cursor/agent-sessions//events.jsonl`、OpenCode `~/.local/share/opencode/opencode.db`、Pi `~/.pi/agent/sessions//.jsonl`、Gemini CLI `~/.gemini/tmp//chats/.jsonl`、Hermes `~/.hermes/state.db`),并在页眉中渲染对应的 CLI 徽章 + - 按决策、事件类型、CLI(Claude Code / OpenAI Codex / GitHub Copilot _(beta)_ / Cursor Agent _(beta)_ / OpenCode _(beta)_ / Pi _(beta)_ / Hermes)、策略名称或会话 ID 筛选 + - 每行显示:时间戳、策略名称、决策、CLI 徽章(橙色 = Claude Code,紫色 = OpenAI Codex,蓝色 = GitHub Copilot,翠绿色 = Cursor Agent,琥珀色 = OpenCode,粉色 = Pi,靛蓝色 = Hermes)、工具名称、会话 ID,以及 deny/instruct 决策的原因 + - 点击会话 ID 可打开其记录——查看器自动检测触发 Hook 的 CLI(Claude `~/.claude/projects/…`、Codex `~/.codex/sessions/…`、Copilot CLI `~/.copilot/session-state//events.jsonl`、Cursor Agent `~/.cursor/agent-sessions//events.jsonl`、OpenCode `~/.local/share/opencode/opencode.db`、Pi `~/.pi/agent/sessions//.jsonl`、Hermes `~/.hermes/state.db`),并在页眉中渲染对应的 CLI 徽章 diff --git a/docs/zh/getting-started.mdx b/docs/zh/getting-started.mdx index 92ed9c45..5c98ba47 100644 --- a/docs/zh/getting-started.mdx +++ b/docs/zh/getting-started.mdx @@ -37,9 +37,9 @@ bun add -g failproofai failproofai policies --install ``` - 此命令会将 hook 条目写入已安装的 Agent CLI 配置文件中(Claude Code 的 `~/.claude/settings.json`、OpenAI Codex 的 `~/.codex/hooks.json`、GitHub Copilot CLI 的 `~/.copilot/hooks/failproofai.json`、Cursor Agent 的 `~/.cursor/hooks.json`、OpenCode 在 `~/.config/opencode/plugins/failproofai.mjs` 生成的插件 shim 及 `~/.config/opencode/opencode.json` 的 `plugin` 数组注册项、Pi 的 `~/.pi/agent/settings.json`、Gemini CLI 的 `~/.gemini/settings.json`,或 Hermes 的 `~/.hermes/config.yaml`)。若检测到多个 CLI,系统会提示你选择;也可以通过 `--cli claude codex copilot cursor opencode pi gemini hermes`(任意子集)跳过提示。 + 此命令会将 hook 条目写入已安装的 Agent CLI 配置文件中(Claude Code 的 `~/.claude/settings.json`、OpenAI Codex 的 `~/.codex/hooks.json`、GitHub Copilot CLI 的 `~/.copilot/hooks/failproofai.json`、Cursor Agent 的 `~/.cursor/hooks.json`、OpenCode 在 `~/.config/opencode/plugins/failproofai.mjs` 生成的插件 shim 及 `~/.config/opencode/opencode.json` 的 `plugin` 数组注册项、Pi 的 `~/.pi/agent/settings.json`,或 Hermes 的 `~/.hermes/config.yaml`)。若检测到多个 CLI,系统会提示你选择;也可以通过 `--cli claude codex copilot cursor opencode pi hermes`(任意子集)跳过提示。 - GitHub Copilot CLI、Cursor Agent、OpenCode、Pi 和 Gemini CLI 的支持目前处于 **beta** 阶段 — 请分别使用 `--cli copilot`、`--cli cursor`、`--cli opencode`、`--cli pi` 或 `--cli gemini` 进行安装。Hermes(hermes-agent,一个 Slack/Telegram 网关)使用 `--cli hermes` 安装用户级作用域,同时也是一个**离线**审计数据源。 + GitHub Copilot CLI、Cursor Agent、OpenCode 和 Pi 的支持目前处于 **beta** 阶段 — 请分别使用 `--cli copilot`、`--cli cursor`、`--cli opencode` 或 `--cli pi` 进行安装。Hermes(hermes-agent,一个 Slack/Telegram 网关)使用 `--cli hermes` 安装用户级作用域,同时也是一个**离线**审计数据源。 ```bash failproofai policies --install --scope project @@ -48,7 +48,6 @@ bun add -g failproofai failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project - failproofai policies --install --cli gemini --scope project failproofai policies --install --cli hermes --scope user failproofai policies --install block-sudo block-rm-rf sanitize-api-keys ``` diff --git a/docs/zh/introduction.mdx b/docs/zh/introduction.mdx index b741ec58..2caefee6 100644 --- a/docs/zh/introduction.mdx +++ b/docs/zh/introduction.mdx @@ -5,7 +5,7 @@ description: "FailproofAI 为 AI 智能体提供 39 条内置故障策略,一 [![npm weekly downloads](https://img.shields.io/npm/dw/failproofai?style=flat-square&color=2ea44f)](https://www.npmjs.com/package/failproofai) -专为 **AI 故障处理**、**错误恢复**和 **LLM 可靠性**设计的 Hooks 与策略系统。让你的 AI 智能体在 **Claude Code**、**OpenAI Codex**、**GitHub Copilot**、**Cursor Agent**、**OpenCode**、**Pi**、**Gemini CLI**、**Hermes** 以及 **Agents SDK** 上稳定运行、持续自主工作。 +专为 **AI 故障处理**、**错误恢复**和 **LLM 可靠性**设计的 Hooks 与策略系统。让你的 AI 智能体在 **Claude Code**、**OpenAI Codex**、**GitHub Copilot**、**Cursor Agent**、**OpenCode**、**Pi**、**Hermes** 以及 **Agents SDK** 上稳定运行、持续自主工作。 AI 智能体的失败往往有迹可循:执行破坏性命令、泄露密钥、偏离任务、陷入死循环,或直接推送到主分支。一旦无人看管,小故障就会滚雪球般演变成服务中断、凭据泄露和数据丢失。 diff --git a/lib/antigravity-projects.ts b/lib/antigravity-projects.ts new file mode 100644 index 00000000..0069c76a --- /dev/null +++ b/lib/antigravity-projects.ts @@ -0,0 +1,224 @@ +/** + * Antigravity (agy) project discovery + session enumeration — AUDIT-ONLY. + * + * Antigravity keeps a SQLite index of conversations at + * `~/.gemini/antigravity-cli/conversation_summaries.db` (table + * `conversation_summaries`: conversation_id, title, preview, step_count, + * workspace_uris, project_id, last_modified_time, agent_name, …) and one + * plain-JSONL transcript per conversation under `brain//…`. Verified live + * against agy v1.1.2. + * + * The on-disk `brain/` transcripts are the source of truth for a conversation's + * EXISTENCE (the summaries DB can lag / be checkpointed empty), so we enumerate + * from disk (lib/antigravity-sessions.ts) and ENRICH each with title + cwd from + * the SQLite index when a row is present. `workspace_uris` gives the project cwd + * → per-project grouping; when absent we recover cwd from the transcript's first + * `run_command` Cwd, else fall back to a synthetic "antigravity" project. + * + * DB / home override: set `ANTIGRAVITY_HOME` (used by tests). + */ +import { join } from "node:path"; +import { openSqliteReadonly } from "./sqlite-reader"; +import { + antigravityHome, + listAntigravityTranscripts, + getAntigravitySessionLog, +} from "./antigravity-sessions"; +import { runtimeCache } from "./runtime-cache"; +import type { ProjectFolder, SessionFile } from "./projects"; +import { encodeFolderName, decodeFolderName } from "./paths"; +import { formatDate } from "./format-date"; +import { logWarn } from "./logger"; + +/** Absolute path to the conversation-index SQLite DB. */ +export function antigravitySummariesDbPath(): string { + return join(antigravityHome(), "conversation_summaries.db"); +} + +interface SummaryRow { + conversation_id: string; + title: string | null; + step_count: number | null; + workspace_uris: string | null; + last_modified_time: string | number | null; +} + +export interface AntigravityIndexMeta { + title?: string; + cwd?: string; + stepCount?: number; + lastMs?: number; +} + +/** Parse the first workspace URI (a JSON array, or a plain string) into a cwd, + * stripping any `file://` scheme. */ +function parseWorkspaceCwd(raw: string | null): string | undefined { + if (!raw) return undefined; + let first: string | undefined; + try { + const parsed = JSON.parse(raw); + if (Array.isArray(parsed) && typeof parsed[0] === "string") first = parsed[0]; + else if (typeof parsed === "string") first = parsed; + } catch { + first = raw; + } + if (!first) return undefined; + const stripped = first.replace(/^file:\/\//, ""); + return stripped.length > 0 ? stripped : undefined; +} + +function toMs(value: string | number | null): number | undefined { + if (typeof value === "number" && Number.isFinite(value)) { + return value > 1e12 ? value : value > 1e9 ? value * 1000 : undefined; + } + if (typeof value === "string") { + const ms = Date.parse(value); + if (!Number.isNaN(ms)) return ms; + } + return undefined; +} + +/** Read the conversation index → conversationId → metadata. Returns an empty + * map when the DB is missing/unreadable/empty (fail-open). */ +export async function getAntigravityIndex(): Promise> { + const out = new Map(); + const db = await openSqliteReadonly(antigravitySummariesDbPath()); + if (!db) return out; + try { + const rows = db.query( + "SELECT conversation_id, title, step_count, workspace_uris, last_modified_time FROM conversation_summaries", + ); + for (const r of rows) { + if (!r.conversation_id) continue; + out.set(r.conversation_id, { + title: r.title ?? undefined, + cwd: parseWorkspaceCwd(r.workspace_uris), + stepCount: typeof r.step_count === "number" ? r.step_count : undefined, + lastMs: toMs(r.last_modified_time), + }); + } + } catch { + // corrupt / unexpected schema — fall open with whatever we have + } finally { + db.close(); + } + return out; +} + +interface AntigravityConversation { + conversationId: string; + cwd: string | null; + title?: string; + mtimeMs: number; +} + +/** Resolve every on-disk conversation to {cwd, title, mtime}, enriched from the + * SQLite index (cwd/title/lastMs) and, when the index lacks a cwd, from the + * transcript's first run_command Cwd. */ +async function resolveConversations(): Promise { + let transcripts; + try { + transcripts = listAntigravityTranscripts(); + } catch (error) { + logWarn("Failed to scan Antigravity brain dir:", error); + return []; + } + const index = await getAntigravityIndex(); + + const out: AntigravityConversation[] = []; + for (const t of transcripts) { + const meta = index.get(t.conversationId); + let cwd = meta?.cwd ?? null; + if (!cwd) { + // No index cwd — recover it from the transcript itself (best-effort). + try { + const log = await getAntigravitySessionLog(t.conversationId); + cwd = log?.cwd ?? null; + } catch { + cwd = null; + } + } + out.push({ + conversationId: t.conversationId, + cwd, + title: meta?.title, + mtimeMs: meta?.lastMs ?? t.mtimeMs, + }); + } + return out; +} + +/** Synthetic project name for conversations whose cwd could not be resolved. */ +const ANTIGRAVITY_UNGROUPED = "antigravity"; + +/** Returns one ProjectFolder per resolved cwd (grouped), plus a synthetic + * "antigravity" project for conversations without a cwd. */ +export async function getAntigravityProjects(): Promise { + const conversations = await resolveConversations(); + + const byName = new Map(); + for (const c of conversations) { + const name = c.cwd ? encodeFolderName(c.cwd) : ANTIGRAVITY_UNGROUPED; + const path = c.cwd ?? "antigravity"; + const existing = byName.get(name); + if (!existing || c.mtimeMs > existing.latest) { + byName.set(name, { latest: c.mtimeMs, path }); + } + } + + const folders: ProjectFolder[] = []; + for (const [name, { latest, path }] of byName) { + const lastModified = new Date(latest); + folders.push({ + name, + path, + isDirectory: true, + lastModified, + lastModifiedFormatted: formatDate(lastModified), + cli: ["antigravity"], + }); + } + folders.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime()); + return folders; +} + +export interface AntigravityProjectByName { + cwd: string | null; + sessions: SessionFile[]; +} + +/** Resolve the Antigravity conversations for a project URL slug (an encoded-cwd + * folder name, or the synthetic "antigravity" bucket). */ +export async function getAntigravitySessionsByEncodedName( + name: string, +): Promise { + const conversations = await resolveConversations(); + const matched = conversations.filter((c) => { + const encoded = c.cwd ? encodeFolderName(c.cwd) : ANTIGRAVITY_UNGROUPED; + return encoded === name; + }); + if (matched.length === 0) return { cwd: null, sessions: [] }; + + const sorted = [...matched].sort((a, b) => b.mtimeMs - a.mtimeMs); + const cwd = sorted.find((c) => c.cwd)?.cwd ?? (name === ANTIGRAVITY_UNGROUPED ? null : decodeFolderName(name)); + + const sessions: SessionFile[] = sorted.map((c) => { + const lastModified = new Date(c.mtimeMs); + return { + name: c.title ?? c.conversationId, + path: `antigravity://${c.conversationId}`, + lastModified, + lastModifiedFormatted: formatDate(lastModified), + sessionId: c.conversationId, + cli: "antigravity" as const, + }; + }); + return { cwd, sessions }; +} + +export const getCachedAntigravityProjects = runtimeCache(getAntigravityProjects, 30); +export const getCachedAntigravitySessionsByEncodedName = runtimeCache( + (name: string) => getAntigravitySessionsByEncodedName(name), + 30, + { maxSize: 50 }, +); diff --git a/lib/antigravity-sessions.ts b/lib/antigravity-sessions.ts new file mode 100644 index 00000000..798e9c2c --- /dev/null +++ b/lib/antigravity-sessions.ts @@ -0,0 +1,283 @@ +/** + * Antigravity (agy) session transcript loader + parser. + * + * AUDIT-ONLY (Pillar 2). Antigravity writes one plain-JSONL transcript per + * conversation at + * `~/.gemini/antigravity-cli/brain//.system_generated/logs/transcript_full.jsonl` + * (one step per line). Verified live against agy v1.1.2. Sibling + * `.trajectory.jsonl` / `.trajectory-path.json` files are + * IGNORED — we only read `transcript_full.jsonl`. + * + * Each transcript line is a step: + * {step_index, source, type, status, created_at, content?, tool_calls?} + * The `type` enum (uppercase) drives parsing: + * • USER_INPUT — content = user text + * • PLANNER_RESPONSE — content = assistant text, OR tool_calls = + * [{name:"run_command", args:{CommandLine, Cwd, …}}] + * • (e.g. RUN_COMMAND) — content = the tool result string; the + * `type` is the uppercased tool name, so we pair it + * with the preceding PLANNER_RESPONSE tool_call of + * the same name. + * • CONVERSATION_HISTORY / CHECKPOINT — metadata (skipped). + * + * `antigravityLinesToLogEntries` is PURE (a function of the parsed line + * objects), so it is unit-testable without touching disk. + * + * Home override: set `ANTIGRAVITY_HOME` (used by tests / to point at a copied + * brain dir). Default: `~/.gemini/antigravity-cli`. + */ +import { readFile } from "node:fs/promises"; +import { readdirSync, statSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { runtimeCache } from "./runtime-cache"; +import { + baseEntry, + formatTimestamp, + parseRawLines, + type LogEntry, + type UserEntry, + type AssistantEntry, + type GenericEntry, + type ContentBlock, + type ToolUseBlock, + type LogSource, +} from "./log-entries"; +import { formatDuration } from "./format-duration"; + +/** Antigravity conversations are UUID-named directories. */ +export const ANTIGRAVITY_CONVERSATION_ID_RE = + /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + +/** Step `type` values that are metadata, not turns. */ +const ANTIGRAVITY_META_STEP_TYPES = new Set(["CONVERSATION_HISTORY", "CHECKPOINT"]); + +/** Absolute path to Antigravity's config home (override with ANTIGRAVITY_HOME). */ +export function antigravityHome(): string { + return process.env.ANTIGRAVITY_HOME || join(homedir(), ".gemini", "antigravity-cli"); +} + +/** Absolute path to the `brain/` root (one subdir per conversation). */ +export function antigravityBrainRoot(): string { + return join(antigravityHome(), "brain"); +} + +/** Relative path of a conversation's full transcript inside its brain dir. */ +export function antigravityTranscriptRelPath(): string { + return join(".system_generated", "logs", "transcript_full.jsonl"); +} + +// ── Parsing helpers ── + +function isPlainObject(v: unknown): v is Record { + return !!v && typeof v === "object" && !Array.isArray(v); +} + +function toDate(value: unknown, fallbackMs: number): Date { + if (typeof value === "number" && Number.isFinite(value)) { + if (value > 1e12) return new Date(value); + if (value > 1e9) return new Date(value * 1000); + } + if (typeof value === "string") { + const ms = Date.parse(value); + if (!Number.isNaN(ms)) return new Date(ms); + } + return new Date(fallbackMs); +} + +// ── Pure parser: transcript lines → LogEntry[] ── + +/** + * Convert Antigravity transcript JSONL lines (parsed objects, in file order) + * into `LogEntry[]`. USER_INPUT → user turns; PLANNER_RESPONSE → assistant + * turns (text and/or `tool_use` blocks); a following tool-result step + * (type === uppercased tool name, e.g. RUN_COMMAND) is paired back onto the + * matching `tool_use` block. Metadata steps (CONVERSATION_HISTORY, CHECKPOINT) + * are skipped. Pure — unit-testable with plain line objects. + */ +export function antigravityLinesToLogEntries( + lines: Record[], + source: LogSource = "session", +): LogEntry[] { + const entries: LogEntry[] = []; + // FIFO of pending tool_use blocks keyed by uppercased tool name, so a later + // RUN_COMMAND-type result step attaches to the right run_command call. + const pendingByType = new Map>(); + const baseMs = Date.now(); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (!isPlainObject(line)) continue; + const type = typeof line.type === "string" ? line.type : ""; + if (!type || ANTIGRAVITY_META_STEP_TYPES.has(type)) continue; + + const date = toDate(line.created_at, baseMs + i); + const timestamp = date.toISOString(); + const stepIndex = typeof line.step_index === "number" ? line.step_index : i; + const raw: Record = { + uuid: `antigravity-${stepIndex}`, + parentUuid: null, + }; + const base = baseEntry(raw, timestamp, date, source); + + if (type === "USER_INPUT") { + const text = typeof line.content === "string" ? line.content : ""; + entries.push({ + type: "user", + ...base, + message: { role: "user", content: text }, + } satisfies UserEntry); + continue; + } + + if (type === "PLANNER_RESPONSE") { + const blocks: ContentBlock[] = []; + if (typeof line.content === "string" && line.content.length > 0) { + blocks.push({ type: "text", text: line.content }); + } + if (Array.isArray(line.tool_calls)) { + for (let j = 0; j < line.tool_calls.length; j++) { + const call = line.tool_calls[j]; + if (!isPlainObject(call)) continue; + const name = typeof call.name === "string" ? call.name : "tool"; + const input = isPlainObject(call.args) ? call.args : {}; + const id = `${name}-${stepIndex}-${j}`; + const block: ToolUseBlock = { type: "tool_use", id, name, input }; + blocks.push(block); + const key = name.toUpperCase(); + const queue = pendingByType.get(key) ?? []; + queue.push({ block, startMs: date.getTime() }); + pendingByType.set(key, queue); + } + } + if (blocks.length === 0) continue; + entries.push({ + type: "assistant", + ...base, + message: { role: "assistant", content: blocks }, + } satisfies AssistantEntry); + continue; + } + + // Any other step type is a tool-result step whose `type` is the uppercased + // tool name (e.g. RUN_COMMAND for a run_command call). Pair it back onto the + // oldest matching pending tool_use block. + const queue = pendingByType.get(type); + if (queue && queue.length > 0) { + const { block, startMs } = queue.shift()!; + const durationMs = Math.max(0, date.getTime() - startMs); + block.result = { + timestamp, + timestampFormatted: formatTimestamp(date), + content: typeof line.content === "string" ? line.content : "", + durationMs, + durationFormatted: formatDuration(durationMs), + }; + continue; + } + // Unpaired non-meta step — record as generic so nothing is silently lost. + entries.push({ type: "system", ...base, raw } satisfies GenericEntry); + } + + entries.sort((a, b) => a.timestampMs - b.timestampMs); + return entries; +} + +/** Best-effort cwd recovery: the first `run_command` tool_call's `Cwd` arg. */ +function recoverCwd(lines: Record[]): string | undefined { + for (const line of lines) { + if (!isPlainObject(line) || !Array.isArray(line.tool_calls)) continue; + for (const call of line.tool_calls) { + if (!isPlainObject(call) || !isPlainObject(call.args)) continue; + const cwd = call.args.Cwd ?? call.args.cwd; + if (typeof cwd === "string" && cwd.length > 0) return cwd; + } + } + return undefined; +} + +// ── Discovery + file loader ── + +export interface AntigravityTranscriptFile { + conversationId: string; + transcriptPath: string; + mtimeMs: number; + sizeBytes: number; +} + +/** Enumerate `brain//.system_generated/logs/transcript_full.jsonl` + * transcripts. Skips conversation dirs without a full transcript. */ +export function listAntigravityTranscripts(): AntigravityTranscriptFile[] { + const root = antigravityBrainRoot(); + const rel = antigravityTranscriptRelPath(); + const out: AntigravityTranscriptFile[] = []; + let convoDirs: import("node:fs").Dirent[]; + try { + convoDirs = readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()); + } catch { + return out; + } + for (const dir of convoDirs) { + const conversationId = dir.name; + if (!ANTIGRAVITY_CONVERSATION_ID_RE.test(conversationId)) continue; + const transcriptPath = join(root, conversationId, rel); + try { + const st = statSync(transcriptPath); + if (!st.isFile()) continue; + out.push({ conversationId, transcriptPath, mtimeMs: st.mtimeMs, sizeBytes: st.size }); + } catch { + // no full transcript for this conversation yet — skip + } + } + return out; +} + +/** Resolve a conversation UUID to its on-disk transcript path (host-side). + * Guards against traversal by requiring a UUID. Shared by the audit adapter, + * the hook transcript resolver, and download-session. Synchronous. */ +export function findAntigravityTranscript(conversationId: string): string | null { + if (!ANTIGRAVITY_CONVERSATION_ID_RE.test(conversationId)) return null; + const transcriptPath = join( + antigravityBrainRoot(), + conversationId, + antigravityTranscriptRelPath(), + ); + try { + if (statSync(transcriptPath).isFile()) return transcriptPath; + } catch { + // fall through + } + return null; +} + +export interface AntigravitySessionLogData { + entries: LogEntry[]; + rawLines: Record[]; + cwd?: string; + filePath: string; +} + +/** Load and parse one conversation transcript by UUID. Returns `null` when the + * file is missing/unreadable or the id fails validation. */ +export async function getAntigravitySessionLog( + conversationId: string, +): Promise { + const filePath = findAntigravityTranscript(conversationId); + if (!filePath) return null; + let content: string; + try { + content = await readFile(filePath, "utf-8"); + } catch { + return null; + } + const rawLines = parseRawLines(content, "session"); + const entries = antigravityLinesToLogEntries(rawLines, "session"); + const cwd = recoverCwd(rawLines); + return { entries, rawLines, cwd, filePath }; +} + +export const getCachedAntigravitySessionLog = runtimeCache( + (conversationId: string) => getAntigravitySessionLog(conversationId), + 60, + { maxSize: 50 }, +); diff --git a/lib/cli-registry.ts b/lib/cli-registry.ts index d7c0c517..031aea4c 100644 --- a/lib/cli-registry.ts +++ b/lib/cli-registry.ts @@ -27,7 +27,7 @@ import type { IntegrationType } from "@/src/hooks/types"; /** Canonical CLI ids the registry knows about. Mirrors `INTEGRATION_TYPES`. */ -export const KNOWN_CLI_IDS = ["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini", "hermes"] as const satisfies readonly IntegrationType[]; +export const KNOWN_CLI_IDS = ["claude", "codex", "copilot", "cursor", "opencode", "pi", "hermes", "openclaw", "factory", "devin", "antigravity", "goose"] as const satisfies readonly IntegrationType[]; export type CliId = (typeof KNOWN_CLI_IDS)[number]; /** Per-CLI metadata consumed by the dashboard. */ @@ -69,16 +69,36 @@ const CLI_ENTRIES: Record = { label: "Pi", badgeClasses: "bg-pink-500/10 text-pink-400 border-pink-500/20", }, - gemini: { - id: "gemini", - label: "Gemini CLI", - badgeClasses: "bg-sky-500/10 text-sky-400 border-sky-500/20", - }, hermes: { id: "hermes", label: "Hermes", badgeClasses: "bg-indigo-500/10 text-indigo-400 border-indigo-500/20", }, + openclaw: { + id: "openclaw", + label: "OpenClaw", + badgeClasses: "bg-teal-500/10 text-teal-400 border-teal-500/20", + }, + factory: { + id: "factory", + label: "Factory Droid", + badgeClasses: "bg-rose-500/10 text-rose-400 border-rose-500/20", + }, + devin: { + id: "devin", + label: "Devin CLI", + badgeClasses: "bg-violet-500/10 text-violet-400 border-violet-500/20", + }, + antigravity: { + id: "antigravity", + label: "Antigravity CLI", + badgeClasses: "bg-cyan-500/10 text-cyan-400 border-cyan-500/20", + }, + goose: { + id: "goose", + label: "Goose", + badgeClasses: "bg-lime-500/10 text-lime-400 border-lime-500/20", + }, }; export function getCliEntry(id: string): CliEntry | undefined { diff --git a/lib/devin-projects.ts b/lib/devin-projects.ts new file mode 100644 index 00000000..b3c44d36 --- /dev/null +++ b/lib/devin-projects.ts @@ -0,0 +1,141 @@ +/** + * Devin CLI (Cognition) session enumeration — AUDIT-ONLY. + * + * Reads the `sessions` table directly from `~/.local/share/devin/cli/sessions.db` + * via the bundled sql.js reader. Unlike Hermes (cwd-less, grouped by channel), + * each Devin session carries a real `working_directory`, so Devin sessions group + * by project cwd like Claude/Factory — one `ProjectFolder` per encoded cwd, and + * a cwd present in both Claude and Devin stores merges on the shared encoded + * slug (see `mergeProjectFolders` in lib/projects.ts). + */ +import { openSqliteReadonly } from "./sqlite-reader"; +import { devinDbPath, devinEpochToMs } from "./devin-sessions"; +import { encodeFolderName } from "./paths"; +import { runtimeCache } from "./runtime-cache"; +import type { ProjectFolder, SessionFile } from "./projects"; +import { formatDate } from "./format-date"; + +export interface DevinSessionRef { + sessionId: string; + cwd?: string; + title?: string; + /** Encoded-cwd folder slug (matches Claude's `-home-user-project` scheme). */ + projectName: string; + /** From `last_activity_at` (falls back to `created_at`) — epoch ms. */ + mtimeMs: number; +} + +interface SessionRow { + id: string; + working_directory: string | null; + title: string | null; + created_at: number | null; + last_activity_at: number | null; + hidden: number | null; +} + +/** + * List every Devin session. Returns `[]` when the DB is missing or unreadable + * (fail-open — the audit just skips Devin). Hidden sessions are excluded. + */ +export async function getDevinSessions(): Promise { + const db = await openSqliteReadonly(devinDbPath()); + if (!db) return []; + try { + const rows = db.query( + "SELECT id, working_directory, title, created_at, last_activity_at, hidden " + + "FROM sessions ORDER BY last_activity_at DESC", + ); + return rows + .filter((r) => !r.hidden) + .map((r) => { + const cwd = r.working_directory ?? undefined; + return { + sessionId: r.id, + cwd, + title: r.title ?? undefined, + projectName: cwd ? encodeFolderName(cwd) : "devin", + mtimeMs: devinEpochToMs(r.last_activity_at ?? r.created_at), + }; + }); + } catch { + return []; + } finally { + db.close(); + } +} + +export const getCachedDevinSessions = runtimeCache(getDevinSessions, 2); + +// ── Dashboard history browser (projects list + project-detail sessions) ── + +/** One `ProjectFolder` per encoded-cwd discovered in the Devin DB. */ +export async function getDevinProjects(): Promise { + const sessions = await getDevinSessions(); + const byName = new Map(); + for (const s of sessions) { + if (!s.cwd) continue; + const existing = byName.get(s.projectName); + if (!existing || s.mtimeMs > existing.latest) { + byName.set(s.projectName, { latest: s.mtimeMs, cwd: s.cwd, name: s.projectName }); + } + } + const folders: ProjectFolder[] = []; + for (const { name, cwd, latest } of byName.values()) { + const lastModified = new Date(latest); + folders.push({ + name, + path: cwd, + isDirectory: true, + lastModified, + lastModifiedFormatted: formatDate(lastModified), + cli: ["devin"], + }); + } + folders.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime()); + return folders; +} + +export interface DevinProjectByName { + /** Canonical cwd recovered from the session's `working_directory`. */ + cwd: string | null; + sessions: SessionFile[]; +} + +/** + * Resolve the Devin sessions for a project URL slug (the encoded-cwd folder + * name). Because Devin's `working_directory` re-encodes to the same slug Claude + * uses, the slug matches directly and the canonical cwd is the session's real + * `working_directory` (no lossy folder-decode needed). + */ +export async function getDevinSessionsByEncodedName( + name: string, +): Promise { + const sessions = await getDevinSessions(); + const matched = sessions.filter((s) => s.cwd && s.projectName === name); + if (matched.length === 0) return { cwd: null, sessions: [] }; + + const sorted = [...matched].sort((a, b) => b.mtimeMs - a.mtimeMs); + const cwd = sorted[0].cwd ?? null; + return { + cwd, + sessions: sorted.map((s) => { + const lastModified = new Date(s.mtimeMs); + return { + name: s.title ?? s.sessionId, + path: `devin-db://${s.sessionId}`, + lastModified, + lastModifiedFormatted: formatDate(lastModified), + sessionId: s.sessionId, + cli: "devin" as const, + }; + }), + }; +} + +export const getCachedDevinProjects = runtimeCache(getDevinProjects, 2); +export const getCachedDevinSessionsByEncodedName = runtimeCache( + (name: string) => getDevinSessionsByEncodedName(name), + 2, + { maxSize: 50 }, +); diff --git a/lib/devin-sessions.ts b/lib/devin-sessions.ts new file mode 100644 index 00000000..d0ef4c2b --- /dev/null +++ b/lib/devin-sessions.ts @@ -0,0 +1,343 @@ +/** + * Devin CLI (Cognition) session transcript loader + parser. + * + * AUDIT-ONLY (Pillar 2). Devin stores every session in one SQLite DB at + * `~/.local/share/devin/cli/sessions.db`. We read it DIRECTLY via the bundled + * sql.js driver (lib/sqlite-reader.ts) — the same reusable path Hermes/OpenClaw + * use. Unlike Hermes (which is cwd-less), each Devin `sessions` row carries a + * real `working_directory`, so Devin sessions group by project cwd like Claude. + * + * Schema (verified live against devin v3000.1.27): + * sessions(id, working_directory, backend_type, model, agent_mode, + * created_at INT, last_activity_at INT, title, workspace_dirs, metadata) + * message_nodes(row_id, session_id, node_id, parent_node_id, + * chat_message TEXT=JSON, created_at INT, metadata) + * + * `chat_message` is OpenAI-style JSON: `{role, content, tool_calls?, + * tool_call_id?, thinking?, metadata?}`. Assistant tool calls are flat + * `tool_calls[].{id, name, arguments}` (arguments is already an object, NOT a + * JSON string as in the OpenAI wire format), and results are separate + * `role:"tool"` rows keyed by `tool_call_id`. `devinRowsToLogEntries` pairs them + * (mirrors lib/hermes-sessions.ts) and is a PURE function of the parsed message + * objects, so it is unit-testable without a DB. + * + * Home override: set `DEVIN_HOME` (used by tests / to point at a copied Devin + * data dir) or `DEVIN_DB_PATH` (points directly at a sessions.db). + */ +import { homedir } from "node:os"; +import { join } from "node:path"; +import { openSqliteReadonly } from "./sqlite-reader"; +import { runtimeCache } from "./runtime-cache"; +import { + baseEntry, + formatTimestamp, + type LogEntry, + type UserEntry, + type AssistantEntry, + type GenericEntry, + type ContentBlock, + type ToolUseBlock, + type LogSource, +} from "./log-entries"; +import { formatDuration } from "./format-duration"; + +/** Devin session IDs are word-slug style (e.g. `estimated-seeker`). */ +export const DEVIN_SESSION_ID_RE = /^[A-Za-z0-9_-]+$/; + +/** Absolute path to Devin's data home (override with DEVIN_HOME). */ +export function devinHome(): string { + return process.env.DEVIN_HOME || join(homedir(), ".local", "share", "devin"); +} + +/** Absolute path to Devin's SQLite DB (override with DEVIN_DB_PATH). */ +export function devinDbPath(): string { + return process.env.DEVIN_DB_PATH || join(devinHome(), "cli", "sessions.db"); +} + +/** Coerce a Devin epoch value (seconds or ms) to epoch ms. Devin stores + * `created_at`/`last_activity_at` as INTEGER epoch seconds. */ +export function devinEpochToMs(value: unknown): number { + if (typeof value === "number" && Number.isFinite(value)) { + if (value > 1e12) return value; // already ms + if (value > 1e9) return value * 1000; // seconds + } + return Date.now(); +} + +// ── Parsing helpers ── + +function safeJsonParse(s: unknown): unknown { + if (typeof s !== "string" || s.length === 0) return undefined; + try { + return JSON.parse(s); + } catch { + return undefined; + } +} + +function isPlainObject(v: unknown): v is Record { + return !!v && typeof v === "object" && !Array.isArray(v); +} + +/** Text from a `content` field that may be a string or an array of + * `{ type:"text", text }` blocks. */ +function extractText(content: unknown): string { + if (typeof content === "string") return content; + if (Array.isArray(content)) { + return content + .map((c) => (isPlainObject(c) && typeof c.text === "string" ? (c.text as string) : "")) + .filter(Boolean) + .join("\n"); + } + return ""; +} + +function toDate(value: unknown, fallbackMs: number): Date { + if (typeof value === "number" && Number.isFinite(value)) { + if (value > 1e12) return new Date(value); + if (value > 1e9) return new Date(value * 1000); + } + if (typeof value === "string") { + const ms = Date.parse(value); + if (!Number.isNaN(ms)) return new Date(ms); + } + return new Date(fallbackMs); +} + +interface NormalizedToolCall { + id: string; + name: string; + input: Record; +} + +/** Normalize Devin `tool_calls` into `{ id, name, input }[]`. Devin's shape is + * flat: `{ id, name, arguments }` where `arguments` is already an object. Also + * tolerates the OpenAI wire shape `{ id|call_id, function:{ name, arguments }}` + * (arguments as a JSON string) for forward-compat. */ +function normalizeDevinToolCalls(raw: unknown): NormalizedToolCall[] { + const arr = Array.isArray(raw) ? raw : safeJsonParse(raw); + if (!Array.isArray(arr)) return []; + const out: NormalizedToolCall[] = []; + for (const tc of arr) { + if (!isPlainObject(tc)) continue; + const fn = isPlainObject(tc.function) ? tc.function : {}; + const name = + typeof tc.name === "string" + ? tc.name + : typeof fn.name === "string" + ? (fn.name as string) + : "tool"; + const id = + (typeof tc.id === "string" && tc.id) || + (typeof tc.call_id === "string" && tc.call_id) || + `${name}-${out.length}`; + const rawArgs = tc.arguments ?? fn.arguments; + const parsedArgs = isPlainObject(rawArgs) ? rawArgs : safeJsonParse(rawArgs); + out.push({ id, name, input: isPlainObject(parsedArgs) ? parsedArgs : {} }); + } + return out; +} + +/** Epoch ms for a parsed chat_message: prefer its high-precision + * `metadata.created_at` ISO string, else the DB row's injected `_created_at` + * (epoch seconds), else the fallback. */ +function messageDate(m: Record, fallbackMs: number): Date { + const meta = isPlainObject(m.metadata) ? m.metadata : undefined; + if (meta && meta.created_at != null) return toDate(meta.created_at, fallbackMs); + if (m._created_at != null) return toDate(m._created_at, fallbackMs); + return new Date(fallbackMs); +} + +// ── Pure parser: parsed chat_message objects → LogEntry[] ── + +/** + * Convert parsed `chat_message` objects (in node order) into `LogEntry[]`. + * Pairs each assistant `tool_calls` entry with its later `role:"tool"` result + * by `tool_call_id`. Roles other than user/assistant/tool (e.g. `system`) + * become generic system entries so nothing is dropped. Pure — unit-testable + * with plain message objects. Each object may carry an injected `_created_at` + * (the DB row's epoch-seconds timestamp) used when `metadata.created_at` is + * absent. + */ +export function devinRowsToLogEntries( + rows: Record[], + source: LogSource = "session", +): LogEntry[] { + const entries: LogEntry[] = []; + const toolUseById = new Map(); + const toolUseStartMs = new Map(); + const baseMs = Date.now(); + + for (let i = 0; i < rows.length; i++) { + const m = rows[i]; + if (!isPlainObject(m)) continue; + + const role = typeof m.role === "string" ? m.role : "system"; + const date = messageDate(m, baseMs + i); + const timestamp = date.toISOString(); + const raw: Record = { + uuid: m.message_id != null ? String(m.message_id) : `devin-${i}`, + parentUuid: null, + }; + const base = baseEntry(raw, timestamp, date, source); + + if (role === "user") { + entries.push({ + type: "user", + ...base, + message: { role: "user", content: extractText(m.content) }, + } satisfies UserEntry); + continue; + } + + if (role === "assistant") { + const blocks: ContentBlock[] = []; + const text = extractText(m.content); + if (text) blocks.push({ type: "text", text }); + for (const tc of normalizeDevinToolCalls(m.tool_calls)) { + const block: ToolUseBlock = { type: "tool_use", id: tc.id, name: tc.name, input: tc.input }; + blocks.push(block); + toolUseById.set(tc.id, block); + toolUseStartMs.set(tc.id, date.getTime()); + } + if (blocks.length === 0) continue; // empty assistant turn + entries.push({ + type: "assistant", + ...base, + message: { + role: "assistant", + content: blocks, + model: typeof m.model === "string" ? m.model : undefined, + }, + } satisfies AssistantEntry); + continue; + } + + if (role === "tool") { + const callId = typeof m.tool_call_id === "string" ? m.tool_call_id : undefined; + const block = callId ? toolUseById.get(callId) : undefined; + if (block) { + const startMs = (callId && toolUseStartMs.get(callId)) || date.getTime(); + const durationMs = Math.max(0, date.getTime() - startMs); + block.result = { + timestamp, + timestampFormatted: formatTimestamp(date), + content: extractText(m.content), + durationMs, + durationFormatted: formatDuration(durationMs), + }; + continue; + } + // Orphan tool result — fall through to a system entry. + } + + entries.push({ type: "system", ...base, raw } satisfies GenericEntry); + } + + entries.sort((a, b) => a.timestampMs - b.timestampMs); + return entries; +} + +// ── DB loader ── + +export interface DevinSessionLogData { + entries: LogEntry[]; + rawLines: Record[]; + cwd?: string; + filePath: string; // synthetic — devin keeps sessions in a DB; we use devin-db:// +} + +interface DevinMessageNodeRow { + node_id: number | null; + parent_node_id: number | null; + chat_message: string | null; + created_at: number | null; +} + +/** + * Devin stores a session's messages as a FOREST, not a flat list: each turn is a + * node (`node_id`) linked to its `parent_node_id`, and Devin replays earlier + * context under fresh roots on later turns — so the table holds many branches + * that repeat the same messages (verified live: 29 nodes / 11 distinct messages + * for a 10-message conversation). Reading every node duplicates each message + * 2-4×. The real conversation is the single path from the NEWEST leaf back to its + * root. Because Devin appends nodes in order (a child's `node_id` always exceeds + * its parent's), the newest leaf is simply the max `node_id`; walk its + * `parent_node_id` chain to the root and reverse to get root→leaf order. + */ +export function devinActiveConversationPath(rows: DevinMessageNodeRow[]): DevinMessageNodeRow[] { + if (rows.length === 0) return rows; + const byId = new Map(); + for (const r of rows) if (typeof r.node_id === "number") byId.set(r.node_id, r); + // Newest leaf = highest node_id (a leaf, since children always have a higher id). + let cur: DevinMessageNodeRow | undefined = rows.reduce((a, b) => + (a.node_id ?? -1) > (b.node_id ?? -1) ? a : b, + ); + const path: DevinMessageNodeRow[] = []; + const seen = new Set(); + while (cur && typeof cur.node_id === "number" && !seen.has(cur.node_id)) { + seen.add(cur.node_id); + path.push(cur); + cur = cur.parent_node_id != null ? byId.get(cur.parent_node_id) : undefined; + } + return path.reverse(); // root → leaf = conversation order +} + +/** Parse a session's `message_nodes` rows into the message objects the pure + * parser consumes, injecting each row's `created_at` as `_created_at`. */ +function parseNodeRows(rows: DevinMessageNodeRow[]): Record[] { + const out: Record[] = []; + for (const r of rows) { + const parsed = safeJsonParse(r.chat_message); + if (!isPlainObject(parsed)) continue; + if (r.created_at != null && parsed._created_at == null) parsed._created_at = r.created_at; + out.push(parsed); + } + return out; +} + +/** + * Load one session by ID from `sessions.db`. Returns `null` when the DB is + * unavailable or the session doesn't exist. + */ +export async function getDevinSessionLog( + sessionId: string, +): Promise { + if (!sessionId || !DEVIN_SESSION_ID_RE.test(sessionId)) return null; + const db = await openSqliteReadonly(devinDbPath()); + if (!db) return null; + try { + const sessionRows = db.query<{ working_directory: string | null }>( + "SELECT working_directory FROM sessions WHERE id = ?", + [sessionId], + ); + if (sessionRows.length === 0) return null; + const realCwd = sessionRows[0].working_directory; + + const nodeRows = db.query( + "SELECT node_id, parent_node_id, chat_message, created_at FROM message_nodes " + + "WHERE session_id = ? ORDER BY node_id ASC", + [sessionId], + ); + // Reconstruct the active conversation path (drop replayed branch duplicates). + const rawLines = parseNodeRows(devinActiveConversationPath(nodeRows)); + const entries = devinRowsToLogEntries(rawLines, "session"); + const cwd = realCwd && realCwd.length > 0 ? realCwd : undefined; + return { + entries, + rawLines, + cwd, + filePath: `devin-db://${sessionId}`, + }; + } catch { + return null; + } finally { + db.close(); + } +} + +export const getCachedDevinSessionLog = runtimeCache( + (sessionId: string) => getDevinSessionLog(sessionId), + 2, + { maxSize: 50 }, +); diff --git a/lib/download-session.ts b/lib/download-session.ts index 235812fc..1eea27f5 100644 --- a/lib/download-session.ts +++ b/lib/download-session.ts @@ -20,6 +20,18 @@ export const OPENCODE_SESSION_RE = /^ses_[A-Za-z0-9]+$/; * its download with `RangeError("Invalid session ID")`. */ export const HERMES_SESSION_RE = /^[A-Za-z0-9_-]+$/; +/** OpenClaw sessions are UUID-named files. Kept in sync with + * OPENCLAW_SESSION_ID_RE in lib/openclaw-sessions.ts. */ +export const OPENCLAW_SESSION_RE = /^[0-9a-fA-F-]{36}$/; + +/** Devin session IDs are word-slug style (e.g. `estimated-seeker`). Kept in + * sync with DEVIN_SESSION_ID_RE in lib/devin-sessions.ts. */ +export const DEVIN_SESSION_RE = /^[A-Za-z0-9_-]+$/; + +/** Goose session IDs are date-prefixed counters (e.g. `20260714_3`). Kept in + * sync with GOOSE_SESSION_ID_RE in lib/goose-sessions.ts. */ +export const GOOSE_SESSION_RE = /^\d{8}_\d+$/; + export type DownloadSource = | { kind: "file"; path: string } | { kind: "synthesized"; body: string; contentType: string; extension: string }; @@ -29,6 +41,9 @@ export type DownloadSource = export function isValidSessionId(cli: CliId, sessionId: string): boolean { if (cli === "opencode") return OPENCODE_SESSION_RE.test(sessionId); if (cli === "hermes") return HERMES_SESSION_RE.test(sessionId); + if (cli === "openclaw") return OPENCLAW_SESSION_RE.test(sessionId); + if (cli === "devin") return DEVIN_SESSION_RE.test(sessionId); + if (cli === "goose") return GOOSE_SESSION_RE.test(sessionId); return UUID_RE.test(sessionId); } @@ -74,11 +89,6 @@ export async function resolveDownloadSource( const path = findPiTranscript(sessionId); return path ? { kind: "file", path } : null; } - if (cli === "gemini") { - const { findGeminiTranscript } = await import("./gemini-sessions"); - const path = findGeminiTranscript(sessionId); - return path ? { kind: "file", path } : null; - } if (cli === "opencode") { // OpenCode keeps sessions in SQLite (~/.local/share/opencode/opencode.db) // across three tables: session / message / part. Export all three so @@ -101,6 +111,47 @@ export async function resolveDownloadSource( return { kind: "synthesized", body, contentType: "application/x-ndjson", extension: "jsonl" }; } + if (cli === "openclaw") { + // OpenClaw writes real JSONL transcripts on disk — stream the file verbatim. + const { findOpenClawTranscript } = await import("./openclaw-sessions"); + const path = findOpenClawTranscript(sessionId); + return path ? { kind: "file", path } : null; + } + + if (cli === "factory") { + // Factory (droid) writes real JSONL transcripts on disk — stream verbatim. + const { findFactoryTranscript } = await import("./factory-sessions"); + const path = findFactoryTranscript(sessionId); + return path ? { kind: "file", path } : null; + } + + if (cli === "devin") { + // Devin keeps sessions in SQLite (~/.local/share/devin/cli/sessions.db). + // Synthesize a JSONL export of the session's raw chat_message rows. + const { getDevinSessionLog } = await import("./devin-sessions"); + const result = await getDevinSessionLog(sessionId); + if (!result) return null; + const body = result.rawLines.map((r) => JSON.stringify(r)).join("\n") + "\n"; + return { kind: "synthesized", body, contentType: "application/x-ndjson", extension: "jsonl" }; + } + + if (cli === "antigravity") { + // Antigravity (agy) writes real JSONL transcripts on disk — stream verbatim. + const { findAntigravityTranscript } = await import("./antigravity-sessions"); + const path = findAntigravityTranscript(sessionId); + return path ? { kind: "file", path } : null; + } + + if (cli === "goose") { + // Goose keeps sessions in SQLite (~/.local/share/goose/sessions/sessions.db). + // Synthesize a JSONL export of the session's raw `messages` rows. + const { getGooseSessionLog } = await import("./goose-sessions"); + const result = await getGooseSessionLog(sessionId); + if (!result) return null; + const body = result.rawLines.map((r) => JSON.stringify(r)).join("\n") + "\n"; + return { kind: "synthesized", body, contentType: "application/x-ndjson", extension: "jsonl" }; + } + // Exhaustive — but TypeScript can't always see CliId is exhausted across the // if-chain above, so guard with a runtime fallback. const _exhaustive: never = cli; diff --git a/lib/factory-projects.ts b/lib/factory-projects.ts new file mode 100644 index 00000000..e17c9bd2 --- /dev/null +++ b/lib/factory-projects.ts @@ -0,0 +1,109 @@ +/** + * Factory (droid) project discovery. + * + * droid stores transcripts at `~/.factory/sessions//.jsonl`, + * using the same Claude-style encoded-cwd folder names (`-home-user-project`) + * as Claude Code. The encoded folder doubles as the URL slug for + * `/project/[name]`, so a cwd present in both stores naturally produces the same + * `name` and merges on the Claude side (see `mergeProjectFolders` in + * lib/projects.ts). + */ +import { decodeFolderName } from "./paths"; +import { listFactoryTranscripts, getFactorySessionLog } from "./factory-sessions"; +import type { ProjectFolder, SessionFile } from "./projects"; +import { runtimeCache } from "./runtime-cache"; +import { formatDate } from "./format-date"; +import { logWarn } from "./logger"; + +/** Returns one ProjectFolder per encoded-cwd folder discovered under + * ~/.factory/sessions/. */ +export async function getFactoryProjects(): Promise { + let transcripts; + try { + transcripts = listFactoryTranscripts(); + } catch (error) { + logWarn("Failed to scan Factory sessions:", error); + return []; + } + + const byName = new Map(); + for (const t of transcripts) { + const existing = byName.get(t.projectName); + if (!existing || t.mtimeMs > existing.latest) { + byName.set(t.projectName, { latest: t.mtimeMs, cwd: t.cwd, name: t.projectName }); + } + } + + const folders: ProjectFolder[] = []; + for (const { name, cwd, latest } of byName.values()) { + const lastModified = new Date(latest); + folders.push({ + name, + path: cwd, + isDirectory: true, + lastModified, + lastModifiedFormatted: formatDate(lastModified), + cli: ["factory"], + }); + } + folders.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime()); + return folders; +} + +export interface FactoryProjectByName { + /** Canonical cwd recovered from a session's `session_start` line (the folder + * decode is lossy). Null when no session could be read. */ + cwd: string | null; + sessions: SessionFile[]; +} + +/** + * Look up Factory sessions for a project URL slug (the encoded-cwd folder name). + * Because droid names its folders with the same encoding Claude uses, the slug + * matches the folder directly. The canonical cwd is recovered from the newest + * session's `session_start` record (folder decode is lossy for cwds with `-`). + */ +export async function getFactorySessionsByEncodedName( + name: string, +): Promise { + let transcripts; + try { + transcripts = listFactoryTranscripts().filter((t) => t.projectName === name); + } catch (error) { + logWarn("Failed to scan Factory sessions:", error); + return { cwd: null, sessions: [] }; + } + if (transcripts.length === 0) return { cwd: null, sessions: [] }; + + const sorted = [...transcripts].sort((a, b) => b.mtimeMs - a.mtimeMs); + + // Recover the canonical cwd from the newest transcript's session_start line. + let cwd: string | null = null; + try { + const log = await getFactorySessionLog(sorted[0].sessionId); + cwd = log?.cwd ?? null; + } catch { + // best-effort — fall back to the lossy decode below + } + if (!cwd) cwd = decodeFolderName(name); + + const sessions: SessionFile[] = sorted.map((t) => { + const lastModified = new Date(t.mtimeMs); + return { + name: t.sessionId, + path: t.transcriptPath, + lastModified, + lastModifiedFormatted: formatDate(lastModified), + sessionId: t.sessionId, + cli: "factory" as const, + }; + }); + return { cwd, sessions }; +} + +export const getCachedFactoryProjects = runtimeCache(getFactoryProjects, 30); +export const getCachedFactorySessionsByEncodedName = runtimeCache( + (name: string) => getFactorySessionsByEncodedName(name), + 30, + { maxSize: 50 }, +); diff --git a/lib/factory-sessions.ts b/lib/factory-sessions.ts new file mode 100644 index 00000000..cbaeea5b --- /dev/null +++ b/lib/factory-sessions.ts @@ -0,0 +1,312 @@ +/** + * Factory (droid) session transcript loader + parser. + * + * AUDIT-ONLY (Pillar 2). droid writes one JSONL transcript per session at + * `~/.factory/sessions//.jsonl` (Claude-style + * encoded-cwd folders — e.g. `-home-chetan-project`), alongside a + * `.settings.json` sibling we IGNORE. Verified live against droid + * v0.171.0. + * + * The transcript is type-discriminated JSONL: + * {type:"session_start", id, cwd, …} — header (carries cwd) + * {type:"message", timestamp, message:{role, content, visibility}} — the turns + * {type:"compaction_state", …} — metadata (skipped) + * + * Message content is Claude-style: a string (user text) or an array of + * {type:"text",text} / {type:"tool_use",id,name,input} / {type:"tool_result", + * tool_use_id,content} blocks. `factoryLinesToLogEntries` pairs each assistant + * `tool_use` with its later `tool_result` by id (mirrors lib/openclaw-sessions.ts) + * and is PURE, so it is unit-testable with plain line objects. + * + * Home override: set `FACTORY_HOME` (used by tests / to point at a copied + * sessions dir). + */ +import { readFile } from "node:fs/promises"; +import { readdirSync, statSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { decodeFolderName } from "./paths"; +import { runtimeCache } from "./runtime-cache"; +import { + baseEntry, + formatTimestamp, + parseRawLines, + type LogEntry, + type UserEntry, + type AssistantEntry, + type GenericEntry, + type ContentBlock, + type ToolUseBlock, + type LogSource, +} from "./log-entries"; +import { formatDuration } from "./format-duration"; + +/** Factory sessions are stored under UUID filenames. */ +export const FACTORY_SESSION_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + +/** Absolute path to Factory's config home (override with FACTORY_HOME). */ +export function factoryHome(): string { + return process.env.FACTORY_HOME || join(homedir(), ".factory"); +} + +/** Absolute path to the Factory sessions root. */ +export function factorySessionsRoot(): string { + return join(factoryHome(), "sessions"); +} + +// ── Parsing helpers ── + +function isPlainObject(v: unknown): v is Record { + return !!v && typeof v === "object" && !Array.isArray(v); +} + +/** Text from a `content` field that may be a string or an array of + * `{ type:"text", text }` blocks. */ +function extractText(content: unknown): string { + if (typeof content === "string") return content; + if (Array.isArray(content)) { + return content + .map((c) => (isPlainObject(c) && typeof c.text === "string" ? (c.text as string) : "")) + .filter(Boolean) + .join("\n"); + } + return ""; +} + +function toDate(value: unknown, fallbackMs: number): Date { + if (typeof value === "number" && Number.isFinite(value)) { + if (value > 1e12) return new Date(value); + if (value > 1e9) return new Date(value * 1000); + } + if (typeof value === "string") { + const ms = Date.parse(value); + if (!Number.isNaN(ms)) return new Date(ms); + } + return new Date(fallbackMs); +} + +// ── Pure parser: transcript lines → LogEntry[] ── + +/** + * Convert Factory transcript JSONL lines (parsed objects, in file order) into + * `LogEntry[]`. Only `type:"message"` lines carry turns; `session_start` / + * `compaction_state` / other metadata lines are skipped. Assistant `tool_use` + * blocks are paired with their later `tool_result` (delivered on a user/tool + * message) by tool_use id. Pure — unit-testable with plain line objects. + */ +export function factoryLinesToLogEntries( + lines: Record[], + source: LogSource = "session", +): LogEntry[] { + const entries: LogEntry[] = []; + const toolUseById = new Map(); + const toolUseStartMs = new Map(); + const baseMs = Date.now(); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (!isPlainObject(line)) continue; + if (line.type !== "message") continue; // skip session_start / compaction_state / … + + const m = isPlainObject(line.message) ? line.message : undefined; + if (!m) continue; + const role = typeof m.role === "string" ? m.role : "system"; + + const date = toDate(line.timestamp ?? m.timestamp, baseMs + i); + const timestamp = date.toISOString(); + const raw: Record = { + uuid: line.id != null ? String(line.id) : `factory-${i}`, + parentUuid: line.parentId != null ? String(line.parentId) : null, + }; + const base = baseEntry(raw, timestamp, date, source); + const content = m.content; + + if (role === "assistant") { + const blocks: ContentBlock[] = []; + if (Array.isArray(content)) { + for (const b of content) { + if (!isPlainObject(b)) continue; + if (b.type === "text" && typeof b.text === "string") { + blocks.push({ type: "text", text: b.text }); + } else if (b.type === "tool_use") { + const id = typeof b.id === "string" ? b.id : `${String(b.name ?? "tool")}-${blocks.length}`; + const name = typeof b.name === "string" ? b.name : "tool"; + const input = isPlainObject(b.input) ? b.input : {}; + const block: ToolUseBlock = { type: "tool_use", id, name, input }; + blocks.push(block); + toolUseById.set(id, block); + toolUseStartMs.set(id, date.getTime()); + } + } + } else { + const text = extractText(content); + if (text) blocks.push({ type: "text", text }); + } + if (blocks.length === 0) continue; // empty / failed assistant turn + entries.push({ + type: "assistant", + ...base, + message: { role: "assistant", content: blocks, model: typeof m.model === "string" ? m.model : undefined }, + } satisfies AssistantEntry); + continue; + } + + // user / tool role: may carry tool_result blocks and/or text. + if (Array.isArray(content)) { + let attachedAny = false; + const textParts: string[] = []; + for (const b of content) { + if (!isPlainObject(b)) continue; + if (b.type === "tool_result") { + const callId = + typeof b.tool_use_id === "string" + ? (b.tool_use_id as string) + : typeof b.toolUseId === "string" + ? (b.toolUseId as string) + : undefined; + const block = callId ? toolUseById.get(callId) : undefined; + if (block) { + const startMs = (callId && toolUseStartMs.get(callId)) || date.getTime(); + const durationMs = Math.max(0, date.getTime() - startMs); + block.result = { + timestamp, + timestampFormatted: formatTimestamp(date), + content: extractText(b.content), + durationMs, + durationFormatted: formatDuration(durationMs), + }; + attachedAny = true; + continue; + } + } + if (b.type === "text" && typeof b.text === "string") textParts.push(b.text); + } + if (textParts.length > 0) { + entries.push({ + type: "user", + ...base, + message: { role: "user", content: textParts.join("\n") }, + } satisfies UserEntry); + } else if (!attachedAny) { + entries.push({ type: "system", ...base, raw } satisfies GenericEntry); + } + continue; + } + + if (role === "user") { + entries.push({ + type: "user", + ...base, + message: { role: "user", content: extractText(content) }, + } satisfies UserEntry); + continue; + } + + entries.push({ type: "system", ...base, raw } satisfies GenericEntry); + } + + entries.sort((a, b) => a.timestampMs - b.timestampMs); + return entries; +} + +// ── Discovery + file loader ── + +export interface FactoryTranscriptFile { + /** Encoded folder name on disk (e.g. "-home-user-project"). */ + projectName: string; + /** Decoded cwd of the project (lossy; canonical cwd lives in session_start). */ + cwd: string; + sessionId: string; + transcriptPath: string; + mtimeMs: number; + sizeBytes: number; +} + +/** Enumerate `sessions//.jsonl` transcripts, skipping the + * `.settings.json` sibling files. */ +export function listFactoryTranscripts(): FactoryTranscriptFile[] { + const root = factorySessionsRoot(); + const out: FactoryTranscriptFile[] = []; + let projectDirs: import("node:fs").Dirent[]; + try { + projectDirs = readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()); + } catch { + return out; + } + for (const dir of projectDirs) { + const projectName = dir.name; + const cwd = decodeFolderName(projectName); + const projectPath = join(root, projectName); + let files: string[]; + try { + files = readdirSync(projectPath); + } catch { + continue; + } + for (const file of files) { + // Only `.jsonl` — not `.settings.json`. + if (!file.endsWith(".jsonl")) continue; + const sessionId = file.slice(0, -".jsonl".length); + if (!FACTORY_SESSION_ID_RE.test(sessionId)) continue; + const transcriptPath = join(projectPath, file); + try { + const st = statSync(transcriptPath); + out.push({ projectName, cwd, sessionId, transcriptPath, mtimeMs: st.mtimeMs, sizeBytes: st.size }); + } catch { + // skip unreadable + } + } + } + return out; +} + +/** Resolve a session UUID to its on-disk transcript path (host-side). Guards + * against traversal by requiring a UUID filename. Shared by the audit adapter, + * the hook transcript resolver, and download-session. Synchronous so the hook + * hot path can call it without awaits. */ +export function findFactoryTranscript(sessionId: string): string | null { + if (!FACTORY_SESSION_ID_RE.test(sessionId)) return null; + for (const t of listFactoryTranscripts()) { + if (t.sessionId === sessionId) return t.transcriptPath; + } + return null; +} + +export interface FactorySessionLogData { + entries: LogEntry[]; + rawLines: Record[]; + cwd?: string; + filePath: string; +} + +/** Load and parse one session transcript by UUID. Returns `null` when the file + * is missing/unreadable or the id fails validation. */ +export async function getFactorySessionLog( + sessionId: string, +): Promise { + const filePath = findFactoryTranscript(sessionId); + if (!filePath) return null; + let content: string; + try { + content = await readFile(filePath, "utf-8"); + } catch { + return null; + } + const rawLines = parseRawLines(content, "session"); + const entries = factoryLinesToLogEntries(rawLines, "session"); + // cwd lives on the `type:"session_start"` header line. + let cwd: string | undefined; + for (const line of rawLines) { + if (isPlainObject(line) && line.type === "session_start" && typeof line.cwd === "string" && line.cwd.length > 0) { + cwd = line.cwd; + break; + } + } + return { entries, rawLines, cwd, filePath }; +} + +export const getCachedFactorySessionLog = runtimeCache( + (sessionId: string) => getFactorySessionLog(sessionId), + 60, + { maxSize: 50 }, +); diff --git a/lib/gemini-projects.ts b/lib/gemini-projects.ts deleted file mode 100644 index 49071fbd..00000000 --- a/lib/gemini-projects.ts +++ /dev/null @@ -1,243 +0,0 @@ -/** - * Gemini CLI project discovery. - * - * Empirically verified against gemini-cli v0.40.1: - * - * • Session-state root: `~/.gemini/tmp//`. Each subdir - * corresponds to one project. The basename is the cwd's last path segment - * (lossy when two projects share a basename — but every dir carries a - * `.project_root` text file with the absolute cwd to disambiguate). - * • Project list registry: `~/.gemini/projects.json` maps absolute cwd → - * basename. Authoritative when present, but we read each `.project_root` - * anyway so the dashboard tolerates partially-pruned registries. - * • Per-session file: `~/.gemini/tmp//chats/session--.jsonl`. - * A sidecar `.jsonl.tool-calls.json` may sit alongside. - * • File format: JSONL. First line is metadata - * `{sessionId, projectHash, startTime, lastUpdated, kind}`; subsequent - * lines are message records `{id, timestamp, type, content: [{text}]}` - * and `{$set: {...}}` partial updates. - * - * As with Cursor / Pi / OpenCode, this module is intentionally permissive — a - * missing `~/.gemini/` returns `[]`, malformed JSONL falls open without - * surfacing the session. - */ -import { open, readdir, readFile, stat } from "node:fs/promises"; -import { homedir } from "node:os"; -import { join } from "node:path"; -import type { ProjectFolder, SessionFile } from "./projects"; -import { runtimeCache } from "./runtime-cache"; -import { batchAll } from "./concurrency"; -import { formatDate } from "./format-date"; -import { encodeFolderName } from "./paths"; -import { logWarn } from "./logger"; - -/** Filename pattern for a Gemini session JSONL: - * `session--<8-hex-uuid-prefix>.jsonl`. */ -const SESSION_FILE_RE = /^session-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2})-([0-9a-f]{8})\.jsonl$/i; -/** Full UUID — the filename only embeds the first 8 hex chars; the rest is on - * the JSONL metadata header line. The session detail route requires a full - * UUID, so links built from the truncated filename prefix 404. */ -const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; -/** Metadata header sits on line 1 and is well under 1 KB; 4 KB covers it - * comfortably without slurping a multi-MB transcript. */ -const FIRST_LINE_CHUNK_BYTES = 4 * 1024; - -/** Override for tests. Defaults to the live Gemini session-state root. */ -function getGeminiTmpRoot(): string { - return process.env.GEMINI_SESSIONS_DIR - || join(homedir(), ".gemini", "tmp"); -} - -interface GeminiSessionMeta { - filePath: string; - sessionFilename: string; - cwd: string; - fileMtime: Date; - /** Full UUID parsed from the JSONL metadata header (line 1). Undefined when - * the header is missing, malformed, or carries a non-UUID `sessionId`; - * callers fall through to rendering an un-linked row. */ - sessionId?: string; -} - -async function safeReaddir(dir: string) { - try { - return await readdir(dir, { withFileTypes: true }); - } catch { - return null; - } -} - -async function statMtime(path: string): Promise { - try { - return (await stat(path)).mtime; - } catch { - return null; - } -} - -/** Read the first newline-delimited line of `filePath` without slurping the - * rest. Mirrors `lib/codex-projects.ts`'s readFirstLine; the Gemini metadata - * header is always on line 1. */ -async function readFirstLine(filePath: string): Promise { - let fh: Awaited> | null = null; - try { - fh = await open(filePath, "r"); - const buf = Buffer.alloc(FIRST_LINE_CHUNK_BYTES); - const { bytesRead } = await fh.read(buf, 0, FIRST_LINE_CHUNK_BYTES, 0); - if (bytesRead === 0) return null; - const slice = buf.subarray(0, bytesRead); - const nl = slice.indexOf(0x0a); // '\n' - const end = nl === -1 ? bytesRead : nl; - return slice.subarray(0, end).toString("utf-8"); - } catch { - return null; - } finally { - if (fh) await fh.close().catch(() => {}); - } -} - -/** Extract a full-UUID `sessionId` from a JSONL metadata header line. Returns - * undefined on parse failure, missing field, or a non-UUID value. */ -function extractFullSessionId(line: string | null): string | undefined { - if (!line) return undefined; - try { - const meta = JSON.parse(line) as { sessionId?: unknown }; - if (typeof meta.sessionId !== "string") return undefined; - return UUID_RE.test(meta.sessionId) ? meta.sessionId : undefined; - } catch { - return undefined; - } -} - -/** Read `.project_root` to recover the absolute cwd for a basename folder. - * Returns null if missing or empty (caller treats the folder as un-mappable). */ -async function readProjectRoot(projectDir: string): Promise { - try { - const text = await readFile(join(projectDir, ".project_root"), "utf-8"); - const trimmed = text.trim(); - return trimmed.length > 0 ? trimmed : null; - } catch { - return null; - } -} - -async function scanGeminiSessions(): Promise { - const root = getGeminiTmpRoot(); - const projectDirs = await safeReaddir(root); - if (!projectDirs) return []; - - const out: GeminiSessionMeta[] = []; - - await batchAll( - projectDirs - .filter((d) => d.isDirectory()) - .map((d) => async () => { - const projectDir = join(root, d.name); - const cwd = await readProjectRoot(projectDir); - if (!cwd) return; - - const chatsDir = join(projectDir, "chats"); - const files = await safeReaddir(chatsDir); - if (!files) return; - - for (const f of files) { - if (!f.isFile()) continue; - if (!SESSION_FILE_RE.test(f.name)) continue; - const filePath = join(chatsDir, f.name); - const mtime = await statMtime(filePath); - if (!mtime) continue; - const sessionId = extractFullSessionId(await readFirstLine(filePath)); - out.push({ filePath, sessionFilename: f.name, cwd, fileMtime: mtime, sessionId }); - } - }), - 16, - ); - - return out; -} - -/** Returns one ProjectFolder per unique cwd that has at least one session file. - * `name` is the encoded full-cwd slug (`encodeFolderName(cwd)`), matching the - * routing scheme used by the dashboard's project URL — `mergeProjectFolders` - * unions by `name`, and `getGeminiSessionsByEncodedName` looks up by the same - * slug, so every cross-CLI merge and Gemini-only project link round-trips. */ -export async function getGeminiProjects(): Promise { - const sessions = await scanGeminiSessions(); - const byCwd = new Map(); - for (const s of sessions) { - const existing = byCwd.get(s.cwd); - if (!existing || s.fileMtime.getTime() > existing.mtime.getTime()) { - byCwd.set(s.cwd, { mtime: s.fileMtime }); - } - } - const folders: ProjectFolder[] = [...byCwd.entries()].map(([cwd, { mtime }]) => ({ - name: encodeFolderName(cwd), - path: cwd, - isDirectory: true, - lastModified: mtime, - lastModifiedFormatted: formatDate(mtime), - cli: ["gemini"], - })); - folders.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime()); - return folders; -} - -/** Returns SessionFile entries for the given absolute cwd. Empty if none. */ -export async function getGeminiSessionsForCwd(cwd: string): Promise { - const sessions = await scanGeminiSessions(); - const matches = sessions.filter((s) => s.cwd === cwd); - const files: SessionFile[] = matches.map((s) => ({ - name: s.sessionFilename, - path: s.filePath, - lastModified: s.fileMtime, - lastModifiedFormatted: formatDate(s.fileMtime), - sessionId: s.sessionId, - cli: "gemini", - })); - files.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime()); - return files; -} - -export interface GeminiProjectByName { - cwd: string | null; - sessions: SessionFile[]; -} - -/** - * Looks up Gemini sessions for a project URL slug. `decodeFolderName` is lossy - * on cwds containing `-`, so we re-encode each session's cwd via - * `encodeFolderName` and match in that direction. Returns both the canonical - * cwd and the matching sessions. When two distinct cwds collapse to the same - * encoded slug we return null to avoid mis-labeling the project. - */ -export async function getGeminiSessionsByEncodedName(name: string): Promise { - let metas: GeminiSessionMeta[]; - try { - metas = await scanGeminiSessions(); - } catch (error) { - logWarn("Failed to scan Gemini sessions:", error); - return { cwd: null, sessions: [] }; - } - const matches = metas.filter((m) => encodeFolderName(m.cwd) === name); - const uniqueCwds = Array.from(new Set(matches.map((m) => m.cwd))); - if (uniqueCwds.length !== 1) { - return { cwd: null, sessions: [] }; - } - const sessions = matches.map((s) => ({ - name: s.sessionFilename, - path: s.filePath, - lastModified: s.fileMtime, - lastModifiedFormatted: formatDate(s.fileMtime), - sessionId: s.sessionId, - cli: "gemini" as const, - })); - sessions.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime()); - return { cwd: uniqueCwds[0], sessions }; -} - -export const getCachedGeminiProjects = runtimeCache(getGeminiProjects, 30); -export const getCachedGeminiSessionsByEncodedName = runtimeCache( - (name: string) => getGeminiSessionsByEncodedName(name), - 30, - { maxSize: 50 }, -); diff --git a/lib/gemini-sessions.ts b/lib/gemini-sessions.ts deleted file mode 100644 index 601aa034..00000000 --- a/lib/gemini-sessions.ts +++ /dev/null @@ -1,365 +0,0 @@ -/** - * Gemini CLI session transcript discovery + JSONL parser. - * - * Empirically verified against gemini-cli v0.40.1: - * - * Session files live at - * `~/.gemini/tmp//chats/session--<8-hex-uuid-prefix>.jsonl` - * with a sidecar `.jsonl.tool-calls.json` of tool-call records. - * The basename component is just the cwd's last path segment; the canonical - * cwd lives at `~/.gemini/tmp//.project_root`. - * - * JSONL record schema (observed): - * • Line 1 (metadata): `{sessionId, projectHash, startTime, lastUpdated, kind}` - * • Subsequent lines: - * `{id, timestamp, type: "user" | "assistant" | ..., content: [{text}]}` (messages) - * `{$set: {lastUpdated: "..."}}` (partial update) - * - * Parser is intentionally permissive — unknown record types degrade to - * "system" entries; malformed lines are skipped without aborting; and the - * loader fall-opens (returns null) on any I/O or parse failure. - */ -import { closeSync, openSync, readFileSync, readSync, readdirSync, existsSync, statSync } from "node:fs"; -import { readFile } from "node:fs/promises"; -import { join, resolve, sep } from "node:path"; -import { homedir } from "node:os"; -import { runtimeCache } from "./runtime-cache"; -import { - baseEntry, - type LogEntry, - type UserEntry, - type AssistantEntry, - type GenericEntry, - type QueueOperationEntry, - type ContentBlock, - type LogSource, -} from "./log-entries"; - -// ── Paths ── - -const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; -/** Matches `session--<8-hex-uuid-prefix>.jsonl`. - * Empirically gemini-cli v0.40.1 emits minute-precision (`YYYY-MM-DDTHH-mm`), - * but the documented format includes seconds (`YYYY-MM-DDTHH-mm:ss`) and may - * evolve further. The first-line `sessionId` validation in findGeminiTranscript - * is the load-bearing safety check, so we accept any timestamp shape. */ -const SESSION_FILE_RE = /^session-(.+)-([0-9a-f]{8})\.jsonl$/i; - -/** Root directory for Gemini session state, honoring GEMINI_SESSIONS_DIR. */ -export function getGeminiSessionStateRoot(): string { - return process.env.GEMINI_SESSIONS_DIR - || join(homedir(), ".gemini", "tmp"); -} - -/** Reject a sessionId that isn't a UUID — defends against path traversal. */ -function isSafeSessionId(sessionId: string): boolean { - return UUID_RE.test(sessionId); -} - -/** - * Read up to the first newline from `path` without loading the whole file. - * Used by `findGeminiTranscript` to inspect the JSONL metadata header on - * candidate matches; the typical header is well under 1KB and we cap at 4KB - * for safety so a degenerate single-line file can't blow memory. - * - * Returns the first line as a UTF-8 string, or null on read failure / empty - * file. If the first 4KB contain no newline, returns whatever was read so the - * JSON.parse caller can still attempt — JSON.parse will fail for a truncated - * object, which findGeminiTranscript treats as a malformed-header miss. - */ -function readFirstLineSync(path: string): string | null { - let fd: number; - try { - fd = openSync(path, "r"); - } catch { - return null; - } - try { - const buf = Buffer.alloc(4096); - const bytesRead = readSync(fd, buf, 0, buf.length, 0); - if (bytesRead === 0) return null; - const text = buf.subarray(0, bytesRead).toString("utf-8"); - const nl = text.indexOf("\n"); - return nl >= 0 ? text.slice(0, nl) : text; - } catch { - return null; - } finally { - try { closeSync(fd); } catch { /* best-effort */ } - } -} - -/** - * Find the JSONL transcript for `sessionId`. Walks every per-project subdir's - * `chats/` directory, matches the 8-hex prefix, and verifies the first record's - * full `sessionId` field matches. Rejects path-traversal sessionIds and - * verifies resolved paths stay under the root. Returns null on miss. - */ -export function findGeminiTranscript(sessionId: string): string | null { - if (!isSafeSessionId(sessionId)) return null; - const root = resolve(getGeminiSessionStateRoot()); - const wantPrefix = sessionId.slice(0, 8).toLowerCase(); - - let projectDirs: string[]; - try { - projectDirs = readdirSync(root); - } catch { - return null; - } - - for (const projectDir of projectDirs) { - const projectPath = resolve(root, projectDir); - if (!projectPath.startsWith(`${root}${sep}`)) continue; - - const chatsDir = resolve(projectPath, "chats"); - let files: string[]; - try { - files = readdirSync(chatsDir); - } catch { - continue; - } - - for (const f of files) { - const m = SESSION_FILE_RE.exec(f); - if (!m || m[2].toLowerCase() !== wantPrefix) continue; - const candidate = resolve(chatsDir, f); - if (!candidate.startsWith(`${chatsDir}${sep}`)) continue; - if (!existsSync(candidate)) continue; - // Confirm the full sessionId — the 8-hex prefix isn't unique on its own. - // Read only the first ~4KB so large transcripts don't bloat memory; the - // metadata header sits on line 1 well within that bound. The full file - // is re-read in getGeminiSessionLog() once we've matched. - const firstLine = readFirstLineSync(candidate); - if (!firstLine) continue; - try { - const meta = JSON.parse(firstLine) as { sessionId?: unknown }; - if (typeof meta.sessionId === "string" && meta.sessionId.toLowerCase() === sessionId.toLowerCase()) { - return candidate; - } - } catch { - // Malformed first record — try next file. - continue; - } - } - } - return null; -} - -// ── Parser ── - -interface GeminiSessionRecord { - // Metadata-line fields (line 1) - sessionId?: string; - projectHash?: string; - startTime?: string; - lastUpdated?: string; - kind?: string; - // Message-line fields - id?: string; - timestamp?: string; - type?: string; - content?: Array>; - // Partial-update line: `{$set: {...}}` - $set?: Record; -} - -interface GeminiParseResult { - entries: LogEntry[]; - rawLines: Record[]; - /** Working directory pulled from the sidecar `.project_root` if available. */ - cwd?: string; -} - -function extractMessageText(content: Array> | undefined): string { - if (!Array.isArray(content)) return ""; - const parts: string[] = []; - for (const block of content) { - if (typeof block?.text === "string") parts.push(block.text); - } - return parts.join("\n\n"); -} - -function buildAssistantContent(content: Array> | undefined): ContentBlock[] { - if (!Array.isArray(content)) return []; - const blocks: ContentBlock[] = []; - for (const block of content) { - if (typeof block?.text === "string" && block.text.length > 0) { - blocks.push({ type: "text", text: block.text }); - } - } - return blocks; -} - -/** - * Parse a Gemini JSONL transcript into `LogEntry[]` plus the raw lines. - * Yields to the event loop every 200 lines so big transcripts don't block - * the request. - */ -export async function parseGeminiLog( - fileContent: string, - source: LogSource = "session", - cwdHint?: string, -): Promise { - const lines = fileContent.split("\n").filter((line) => line.trim() !== ""); - const entries: LogEntry[] = []; - const rawLines: Record[] = []; - let cwd: string | undefined = cwdHint; - let seenStart = false; - - for (let i = 0; i < lines.length; i++) { - if (i > 0 && i % 200 === 0) await new Promise((r) => setImmediate(r)); - - const line = lines[i]; - let raw: GeminiSessionRecord; - try { - raw = JSON.parse(line) as GeminiSessionRecord; - } catch { - continue; - } - - const rawCopy = { ...(raw as Record), _source: source }; - rawLines.push(rawCopy); - - // Metadata line — derive a synthetic "Session Started" entry from startTime. - if (typeof raw.sessionId === "string" && typeof raw.startTime === "string") { - const date = new Date(raw.startTime); - if (!Number.isNaN(date.getTime())) { - const label: QueueOperationEntry["label"] = seenStart ? "Session Resumed" : "Session Started"; - seenStart = true; - entries.push({ - type: "queue-operation", - ...baseEntry(rawCopy, date.toISOString(), date, source), - label, - } satisfies QueueOperationEntry); - } - continue; - } - - // Partial-update line — preserve in raw but skip rendering. - if (raw.$set) continue; - - const timestampStr = raw.timestamp; - if (!timestampStr) continue; - const date = new Date(timestampStr); - if (Number.isNaN(date.getTime())) continue; - const timestamp = date.toISOString(); - - const recType = raw.type; - - if (recType === "user") { - const text = extractMessageText(raw.content); - if (!text) continue; - entries.push({ - type: "user", - ...baseEntry(rawCopy, timestamp, date, source), - message: { role: "user", content: text }, - } satisfies UserEntry); - continue; - } - - if (recType === "assistant" || recType === "model") { - const blocks = buildAssistantContent(raw.content); - if (blocks.length === 0) { - entries.push({ - type: "system", - ...baseEntry(rawCopy, timestamp, date, source), - raw: rawCopy, - } satisfies GenericEntry); - continue; - } - entries.push({ - type: "assistant", - ...baseEntry(rawCopy, timestamp, date, source), - message: { role: "assistant", content: blocks }, - } satisfies AssistantEntry); - continue; - } - - // Unknown type — preserve raw. - entries.push({ - type: "system", - ...baseEntry(rawCopy, timestamp, date, source), - raw: rawCopy, - } satisfies GenericEntry); - } - - if (entries.length > 500) await new Promise((r) => setImmediate(r)); - entries.sort((a, b) => a.timestampMs - b.timestampMs); - - return { entries, rawLines, cwd }; -} - -// ── Public loader ── - -export interface GeminiSessionLogData { - entries: LogEntry[]; - rawLines: Record[]; - cwd?: string; - filePath: string; -} - -/** Read `.project_root` next to the chats dir to recover the absolute cwd. */ -function readSiblingProjectRoot(transcriptPath: string): string | undefined { - // transcriptPath = ...//chats/.jsonl - const chatsDir = resolve(transcriptPath, ".."); - const projectDir = resolve(chatsDir, ".."); - const rootFile = join(projectDir, ".project_root"); - try { - const text = readFileSync(rootFile, "utf-8").trim(); - return text.length > 0 ? text : undefined; - } catch { - return undefined; - } -} - -export async function getGeminiSessionLog(sessionId: string): Promise { - const filePath = findGeminiTranscript(sessionId); - if (!filePath) return null; - let fileContent: string; - try { - fileContent = await readFile(filePath, "utf-8"); - } catch { - return null; - } - const cwdHint = readSiblingProjectRoot(filePath); - let parsed: GeminiParseResult; - try { - parsed = await parseGeminiLog(fileContent, "session", cwdHint); - } catch { - return null; - } - return { - entries: parsed.entries, - rawLines: parsed.rawLines, - cwd: parsed.cwd, - filePath, - }; -} - -export const getCachedGeminiSessionLog = runtimeCache( - (sessionId: string) => getGeminiSessionLog(sessionId), - 60, - { maxSize: 50 }, -); - -// ── Test helpers ── - -export function _statGeminiTranscript(sessionId: string): { mtimeMs: number } | null { - const path = findGeminiTranscript(sessionId); - if (!path) return null; - try { - const s = statSync(path); - return { mtimeMs: s.mtimeMs }; - } catch { - return null; - } -} - -export function readGeminiTranscriptSync(sessionId: string): string | null { - const path = findGeminiTranscript(sessionId); - if (!path) return null; - try { - return readFileSync(path, "utf-8"); - } catch { - return null; - } -} diff --git a/lib/goose-projects.ts b/lib/goose-projects.ts new file mode 100644 index 00000000..20c8f57e --- /dev/null +++ b/lib/goose-projects.ts @@ -0,0 +1,148 @@ +/** + * Goose (codename goose, Block) session enumeration — AUDIT-ONLY. + * + * Reads the `sessions` table directly from + * `~/.local/share/goose/sessions/sessions.db` via the bundled sql.js reader. + * Like Devin (and unlike the cwd-less Hermes gateway), each Goose session + * carries a real `working_dir`, so Goose sessions group by project cwd like + * Claude/Factory/Devin — one `ProjectFolder` per encoded cwd, and a cwd present + * in multiple stores merges on the shared encoded slug (see `mergeProjectFolders` + * in lib/projects.ts). + * + * `session_type = 'hidden'` rows are `goose run --no-session` scratch runs and + * are excluded (mirrors Devin's `hidden` filter). + */ +import { openSqliteReadonly } from "./sqlite-reader"; +import { gooseDbPath, gooseTimestampToMs } from "./goose-sessions"; +import { encodeFolderName } from "./paths"; +import { runtimeCache } from "./runtime-cache"; +import type { ProjectFolder, SessionFile } from "./projects"; +import { formatDate } from "./format-date"; + +export interface GooseSessionRef { + sessionId: string; + cwd?: string; + title?: string; + /** Encoded-cwd folder slug (matches Claude's `-home-user-project` scheme). */ + projectName: string; + /** From `updated_at` (falls back to `created_at`) — epoch ms. */ + mtimeMs: number; +} + +interface SessionRow { + id: string; + working_dir: string | null; + description: string | null; + name: string | null; + session_type: string | null; + created_at: string | null; + updated_at: string | null; +} + +/** + * List every Goose session. Returns `[]` when the DB is missing or unreadable + * (fail-open — the audit just skips Goose). `hidden` (no-session) rows are + * excluded. + */ +export async function getGooseSessions(): Promise { + const db = await openSqliteReadonly(gooseDbPath()); + if (!db) return []; + try { + const rows = db.query( + "SELECT id, working_dir, description, name, session_type, created_at, updated_at " + + "FROM sessions ORDER BY updated_at DESC", + ); + return rows + .filter((r) => r.session_type !== "hidden") + .map((r) => { + const cwd = r.working_dir ?? undefined; + const title = (r.description && r.description.length > 0 ? r.description : r.name) ?? undefined; + return { + sessionId: r.id, + cwd, + title: title && title.length > 0 ? title : undefined, + projectName: cwd ? encodeFolderName(cwd) : "goose", + mtimeMs: gooseTimestampToMs(r.updated_at ?? r.created_at), + }; + }); + } catch { + return []; + } finally { + db.close(); + } +} + +export const getCachedGooseSessions = runtimeCache(getGooseSessions, 2); + +// ── Dashboard history browser (projects list + project-detail sessions) ── + +/** One `ProjectFolder` per encoded-cwd discovered in the Goose DB. */ +export async function getGooseProjects(): Promise { + const sessions = await getGooseSessions(); + const byName = new Map(); + for (const s of sessions) { + if (!s.cwd) continue; + const existing = byName.get(s.projectName); + if (!existing || s.mtimeMs > existing.latest) { + byName.set(s.projectName, { latest: s.mtimeMs, cwd: s.cwd, name: s.projectName }); + } + } + const folders: ProjectFolder[] = []; + for (const { name, cwd, latest } of byName.values()) { + const lastModified = new Date(latest); + folders.push({ + name, + path: cwd, + isDirectory: true, + lastModified, + lastModifiedFormatted: formatDate(lastModified), + cli: ["goose"], + }); + } + folders.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime()); + return folders; +} + +export interface GooseProjectByName { + /** Canonical cwd recovered from the session's `working_dir`. */ + cwd: string | null; + sessions: SessionFile[]; +} + +/** + * Resolve the Goose sessions for a project URL slug (the encoded-cwd folder + * name). Goose's `working_dir` re-encodes to the same slug Claude uses, so the + * slug matches directly and the canonical cwd is the session's real + * `working_dir`. + */ +export async function getGooseSessionsByEncodedName( + name: string, +): Promise { + const sessions = await getGooseSessions(); + const matched = sessions.filter((s) => s.cwd && s.projectName === name); + if (matched.length === 0) return { cwd: null, sessions: [] }; + + const sorted = [...matched].sort((a, b) => b.mtimeMs - a.mtimeMs); + const cwd = sorted[0].cwd ?? null; + return { + cwd, + sessions: sorted.map((s) => { + const lastModified = new Date(s.mtimeMs); + return { + name: s.title ?? s.sessionId, + path: `goose-db://${s.sessionId}`, + lastModified, + lastModifiedFormatted: formatDate(lastModified), + sessionId: s.sessionId, + cli: "goose" as const, + }; + }), + }; +} + +export const getCachedGooseProjects = runtimeCache(getGooseProjects, 2); +export const getCachedGooseSessionsByEncodedName = runtimeCache( + (name: string) => getGooseSessionsByEncodedName(name), + 2, + { maxSize: 50 }, +); diff --git a/lib/goose-sessions.ts b/lib/goose-sessions.ts new file mode 100644 index 00000000..222cc646 --- /dev/null +++ b/lib/goose-sessions.ts @@ -0,0 +1,296 @@ +/** + * Goose (codename goose, Block) session transcript loader + parser. + * + * AUDIT-ONLY (Pillar 2). Goose (≥ v1.10.0) stores every session in one SQLite DB + * at `~/.local/share/goose/sessions/sessions.db`. We read it DIRECTLY via the + * bundled sql.js driver (lib/sqlite-reader.ts) — the same reusable path + * Devin/Hermes/OpenClaw use. Like Devin (and unlike the cwd-less Hermes gateway), + * each Goose `sessions` row carries a real `working_dir`, so Goose sessions group + * by project cwd like Claude/Factory/Devin. + * + * Schema (verified live against goose v1.43.0, schema_version 15): + * sessions(id TEXT `YYYYMMDD_N`, name, description, session_type + * (user|hidden|subagent|scheduled|terminal|acp), working_dir, + * created_at TIMESTAMP, updated_at TIMESTAMP, total_tokens, …) + * messages(id INTEGER PK, session_id, role (user|assistant), + * content_json TEXT=JSON-array-of-blocks, created_timestamp INT, …) + * + * `content_json` is a Claude-style typed-block array (NOT OpenAI-style like + * Devin): `{type:"text",text}`, `{type:"toolRequest",id,toolCall:{value:{name, + * arguments}},_meta:{goose_extension}}`, and `{type:"toolResponse",id,toolResult: + * {value:{content:[{type:"text",text}],isError}}}`. Tool RESULTS are stored in + * `role:"user"` rows (Claude convention). `gooseRowsToLogEntries` pairs each + * toolRequest with its later toolResponse by id and is a PURE function of the + * parsed rows, so it is unit-testable without a DB. + * + * `session_type = 'hidden'` rows are `goose run --no-session` scratch runs — the + * enumerator (lib/goose-projects.ts) filters them out. + * + * Home override: set `GOOSE_HOME` (the data dir that contains `sessions/`, used + * by tests) or `GOOSE_DB_PATH` (points directly at a sessions.db). + */ +import { homedir } from "node:os"; +import { join } from "node:path"; +import { openSqliteReadonly } from "./sqlite-reader"; +import { runtimeCache } from "./runtime-cache"; +import { + baseEntry, + formatTimestamp, + type LogEntry, + type UserEntry, + type AssistantEntry, + type GenericEntry, + type ContentBlock, + type ToolUseBlock, + type LogSource, +} from "./log-entries"; +import { formatDuration } from "./format-duration"; + +/** Goose session IDs are date-prefixed counters, e.g. `20260714_3`. */ +export const GOOSE_SESSION_ID_RE = /^\d{8}_\d+$/; + +/** Absolute path to Goose's data home (override with GOOSE_HOME; respects + * XDG_DATA_HOME otherwise). */ +export function gooseHome(): string { + if (process.env.GOOSE_HOME) return process.env.GOOSE_HOME; + const xdg = process.env.XDG_DATA_HOME; + return join(xdg && xdg.length > 0 ? xdg : join(homedir(), ".local", "share"), "goose"); +} + +/** Absolute path to Goose's SQLite DB (override with GOOSE_DB_PATH). */ +export function gooseDbPath(): string { + return process.env.GOOSE_DB_PATH || join(gooseHome(), "sessions", "sessions.db"); +} + +/** Coerce a Goose timestamp to epoch ms. `messages.created_timestamp` is an + * INTEGER epoch (seconds or ms); `sessions.created_at`/`updated_at` are SQLite + * `CURRENT_TIMESTAMP` strings ("YYYY-MM-DD HH:MM:SS", UTC). */ +export function gooseTimestampToMs(value: unknown): number { + if (typeof value === "number" && Number.isFinite(value)) { + if (value > 1e12) return value; // already ms + if (value > 1e9) return value * 1000; // seconds + } + if (typeof value === "string" && value.length > 0) { + // SQLite CURRENT_TIMESTAMP has no zone and is UTC — normalize to ISO-Z so + // Date.parse doesn't treat it as local time. + const iso = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(value) + ? value.replace(" ", "T") + "Z" + : value; + const ms = Date.parse(iso); + if (!Number.isNaN(ms)) return ms; + } + return Date.now(); +} + +// ── Parsing helpers ── + +function safeJsonParse(s: unknown): unknown { + if (typeof s !== "string" || s.length === 0) return undefined; + try { + return JSON.parse(s); + } catch { + return undefined; + } +} + +function isPlainObject(v: unknown): v is Record { + return !!v && typeof v === "object" && !Array.isArray(v); +} + +/** Text from a `content` field that may be a string or an array of + * `{ type:"text", text }` blocks. */ +function extractText(content: unknown): string { + if (typeof content === "string") return content; + if (Array.isArray(content)) { + return content + .map((c) => (isPlainObject(c) && typeof c.text === "string" ? (c.text as string) : "")) + .filter(Boolean) + .join("\n"); + } + return ""; +} + +function toDate(value: unknown, fallbackMs: number): Date { + const ms = typeof value === "number" || typeof value === "string" ? gooseTimestampToMs(value) : NaN; + return Number.isNaN(ms) ? new Date(fallbackMs) : new Date(ms); +} + +/** Pull `{ id, name, input }` from a Goose `toolRequest` block: + * `{id, toolCall:{status, value:{name, arguments}}}`. */ +function extractToolRequest( + block: Record, + idx: number, +): { id: string; name: string; input: Record } { + const id = typeof block.id === "string" && block.id ? block.id : `goose-tool-${idx}`; + const toolCall = isPlainObject(block.toolCall) ? block.toolCall : {}; + const value = isPlainObject(toolCall.value) ? toolCall.value : {}; + const name = typeof value.name === "string" ? value.name : "tool"; + const input = isPlainObject(value.arguments) ? value.arguments : {}; + return { id, name, input }; +} + +/** Text of a Goose `toolResponse` block: + * `{id, toolResult:{status, value:{content:[{type:"text",text}], …}}}`. */ +function extractToolResult(block: Record): string { + const toolResult = isPlainObject(block.toolResult) ? block.toolResult : {}; + const value = toolResult.value; + if (isPlainObject(value) && Array.isArray(value.content)) return extractText(value.content); + if (typeof value === "string") return value; + if (Array.isArray(value)) return extractText(value); + return ""; +} + +// ── Pure parser: message rows → LogEntry[] ── + +export interface GooseMessageRow { + role: string | null; + content_json: string | null; + created_timestamp: number | null; +} + +/** + * Convert Goose `messages` rows (in insertion order) into `LogEntry[]`. Each + * row's `content_json` is a typed-block array; `toolRequest` blocks become + * `tool_use` blocks on an assistant entry, and later `toolResponse` blocks + * (which arrive in `role:"user"` rows) attach as the matching tool_use's result + * by id. Pure — unit-testable with plain rows. + */ +export function gooseRowsToLogEntries( + rows: GooseMessageRow[], + source: LogSource = "session", +): LogEntry[] { + const entries: LogEntry[] = []; + const toolUseById = new Map(); + const toolUseStartMs = new Map(); + const baseMs = Date.now(); + + for (let i = 0; i < rows.length; i++) { + const r = rows[i]; + const parsed = safeJsonParse(r.content_json); + if (!Array.isArray(parsed)) continue; + + const role = typeof r.role === "string" ? r.role : "system"; + const date = toDate(r.created_timestamp, baseMs + i); + const timestamp = date.toISOString(); + const raw: Record = { uuid: `goose-${i}`, parentUuid: null }; + const base = baseEntry(raw, timestamp, date, source); + + const textParts: string[] = []; + const toolReqs: Record[] = []; + const toolResps: Record[] = []; + for (const b of parsed) { + if (!isPlainObject(b)) continue; + if (b.type === "text" && typeof b.text === "string") textParts.push(b.text); + else if (b.type === "toolRequest") toolReqs.push(b); + else if (b.type === "toolResponse") toolResps.push(b); + } + + // Attach tool results to their earlier tool_use blocks (results arrive in + // role:"user" rows, so this runs before we consider the row a user turn). + for (const tr of toolResps) { + const callId = typeof tr.id === "string" ? tr.id : undefined; + const block = callId ? toolUseById.get(callId) : undefined; + if (!block) continue; + const startMs = (callId && toolUseStartMs.get(callId)) || date.getTime(); + const durationMs = Math.max(0, date.getTime() - startMs); + block.result = { + timestamp, + timestampFormatted: formatTimestamp(date), + content: extractToolResult(tr), + durationMs, + durationFormatted: formatDuration(durationMs), + }; + } + + const text = textParts.join("\n").trim(); + + if (toolReqs.length > 0 || (role === "assistant" && text)) { + const blocks: ContentBlock[] = []; + if (text) blocks.push({ type: "text", text }); + for (const req of toolReqs) { + const { id, name, input } = extractToolRequest(req, blocks.length); + const block: ToolUseBlock = { type: "tool_use", id, name, input }; + blocks.push(block); + toolUseById.set(id, block); + toolUseStartMs.set(id, date.getTime()); + } + if (blocks.length === 0) continue; + entries.push({ + type: "assistant", + ...base, + message: { role: "assistant", content: blocks }, + } satisfies AssistantEntry); + continue; + } + + if (role === "user" && text) { + entries.push({ + type: "user", + ...base, + message: { role: "user", content: text }, + } satisfies UserEntry); + continue; + } + + // Pure tool-result rows (no text, no requests) produced no new entry above. + if (text) { + entries.push({ type: "system", ...base, raw } satisfies GenericEntry); + } + } + + entries.sort((a, b) => a.timestampMs - b.timestampMs); + return entries; +} + +// ── DB loader ── + +export interface GooseSessionLogData { + entries: LogEntry[]; + rawLines: Record[]; + cwd?: string; + filePath: string; // synthetic — goose keeps sessions in a DB; we use goose-db:// +} + +/** + * Load one session by ID from `sessions.db`. Returns `null` when the DB is + * unavailable or the session doesn't exist. + */ +export async function getGooseSessionLog( + sessionId: string, +): Promise { + if (!sessionId || !GOOSE_SESSION_ID_RE.test(sessionId)) return null; + const db = await openSqliteReadonly(gooseDbPath()); + if (!db) return null; + try { + const sessionRows = db.query<{ working_dir: string | null }>( + "SELECT working_dir FROM sessions WHERE id = ?", + [sessionId], + ); + if (sessionRows.length === 0) return null; + const realCwd = sessionRows[0].working_dir; + + const msgRows = db.query( + "SELECT role, content_json, created_timestamp FROM messages " + + "WHERE session_id = ? ORDER BY id ASC", + [sessionId], + ); + const entries = gooseRowsToLogEntries(msgRows, "session"); + const cwd = realCwd && realCwd.length > 0 ? realCwd : undefined; + return { + entries, + rawLines: msgRows as unknown as Record[], + cwd, + filePath: `goose-db://${sessionId}`, + }; + } catch { + return null; + } finally { + db.close(); + } +} + +export const getCachedGooseSessionLog = runtimeCache( + (sessionId: string) => getGooseSessionLog(sessionId), + 2, + { maxSize: 50 }, +); diff --git a/lib/openclaw-projects.ts b/lib/openclaw-projects.ts new file mode 100644 index 00000000..18ed6036 --- /dev/null +++ b/lib/openclaw-projects.ts @@ -0,0 +1,182 @@ +/** + * OpenClaw (openclaw gateway) session enumeration — AUDIT-ONLY. + * + * Surfaces the on-disk transcripts (agents//sessions/.jsonl) as + * synthetic dashboard "projects" grouped by agentId. The per-agent + * `sessions.json` index maps sessionKey → {sessionId, timestamps}; we read it to + * recover the sessionKey (which encodes the channel for gateway sessions) and a + * reliable last-activity time. Verified live against openclaw v2026.7.1. + */ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { runtimeCache } from "./runtime-cache"; +import { listOpenClawTranscripts, openclawHome } from "./openclaw-sessions"; +import type { ProjectFolder, SessionFile } from "./projects"; +import { formatDate } from "./format-date"; + +export interface OpenClawSessionRef { + sessionId: string; + agentId: string; + /** Channel/source the session last ran in (from sessions.json metadata) — + * e.g. "telegram", "slack", or "local" for a CLI session. Drives grouping. */ + channel: string; + /** Human-readable label from `origin.label` (e.g. "Chetan (@chhhee10) id:…"). */ + label?: string; + /** Chat id (e.g. "telegram:8674922496") + type ("direct"/"group") for the + * gateway-metadata columns. */ + chatId?: string; + chatType?: string; + mtimeMs: number; + sizeBytes: number; +} + +interface SessionIndexMeta { + lastMs?: number; + channel?: string; + chatType?: string; + label?: string; + chatId?: string; +} + +function str(v: unknown): string | undefined { + return typeof v === "string" && v.length > 0 ? v : undefined; +} + +/** Read the per-agent sessions.json index → sessionId → routing metadata. + * OpenClaw routes gateway sessions through the agent's default key + * (`agent::main`) and records the channel in metadata fields + * (`lastChannel`, `chatType`, `origin.{label,provider,from}`, `lastTo`) rather + * than in the key — verified live against v2026.7.1. */ +function readSessionsIndex(agentId: string): Map { + const out = new Map(); + const indexPath = join(openclawHome(), "agents", agentId, "sessions", "sessions.json"); + let raw: unknown; + try { + raw = JSON.parse(readFileSync(indexPath, "utf-8")); + } catch { + return out; + } + if (!raw || typeof raw !== "object") return out; + for (const v of Object.values(raw as Record)) { + if (!v || typeof v !== "object") continue; + const e = v as Record; + const sessionId = str(e.sessionId); + if (!sessionId) continue; + const origin = (e.origin && typeof e.origin === "object" ? e.origin : {}) as Record; + const lastMs = + typeof e.lastInteractionAt === "number" + ? e.lastInteractionAt + : typeof e.updatedAt === "number" + ? (e.updatedAt as number) + : undefined; + out.set(sessionId, { + lastMs, + channel: str(e.lastChannel) ?? str(origin.provider) ?? str(origin.surface), + chatType: str(e.chatType) ?? str(origin.chatType), + label: str(origin.label), + chatId: str(e.lastTo) ?? str(origin.from), + }); + } + return out; +} + +/** List every OpenClaw session across all agents. Fail-open ([] on any error). */ +export async function getOpenClawSessions(): Promise { + const transcripts = listOpenClawTranscripts(); + const indexByAgent = new Map>(); + const refs: OpenClawSessionRef[] = []; + for (const t of transcripts) { + let idx = indexByAgent.get(t.agentId); + if (!idx) { + idx = readSessionsIndex(t.agentId); + indexByAgent.set(t.agentId, idx); + } + const meta = idx.get(t.sessionId); + refs.push({ + sessionId: t.sessionId, + agentId: t.agentId, + // Gateway sessions group by channel; CLI/local runs have none. + channel: meta?.channel ?? "local", + label: meta?.label, + chatType: meta?.chatType, + chatId: meta?.chatId, + mtimeMs: meta?.lastMs ?? t.mtimeMs, + sizeBytes: t.sizeBytes, + }); + } + refs.sort((a, b) => b.mtimeMs - a.mtimeMs); + return refs; +} + +export const getCachedOpenClawSessions = runtimeCache(getOpenClawSessions, 2); + +// ── Dashboard history browser (projects list + project-detail sessions) ── + +/** + * Surface OpenClaw sessions as synthetic "projects" grouped by **channel** + * (telegram/slack/…/local) — gateway sessions run per channel, not in a host + * repo. Mirrors Hermes's group-by-source. One ProjectFolder per channel; its + * `name` is `openclaw-`, reversed in `getOpenClawSessionsByEncodedName`. + */ +export async function getOpenClawProjects(): Promise { + const sessions = await getOpenClawSessions(); + const latestByChannel = new Map(); + for (const s of sessions) { + latestByChannel.set(s.channel, Math.max(latestByChannel.get(s.channel) ?? 0, s.mtimeMs)); + } + const out: ProjectFolder[] = []; + for (const [channel, latest] of latestByChannel) { + const lastModified = new Date(latest); + out.push({ + name: `openclaw-${channel}`, + path: `openclaw:${channel}`, + isDirectory: true, + lastModified, + lastModifiedFormatted: formatDate(lastModified), + cli: ["openclaw"], + }); + } + out.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime()); + return out; +} + +export interface OpenClawProjectByName { + cwd: string | null; + sessions: SessionFile[]; +} + +/** Resolve the OpenClaw sessions for a synthetic project name + * (`openclaw-`), for the project-detail page. Session names use the + * human-readable `origin.label` (e.g. "Chetan (@chhhee10) id:…") rather than + * the raw session key. */ +export async function getOpenClawSessionsByEncodedName( + name: string, +): Promise { + if (!name.startsWith("openclaw-")) return { cwd: null, sessions: [] }; + const channel = name.slice("openclaw-".length); + const sessions = await getOpenClawSessions(); + const matched = sessions.filter((s) => s.channel === channel); + return { + cwd: `openclaw:${channel}`, + sessions: matched.map((s) => { + const lastModified = new Date(s.mtimeMs); + return { + name: s.label ?? s.chatId ?? s.sessionId, + path: s.sessionId, + lastModified, + lastModifiedFormatted: formatDate(lastModified), + sessionId: s.sessionId, + cli: "openclaw" as const, + channelId: s.chatId, + channelType: s.chatType, + }; + }), + }; +} + +export const getCachedOpenClawProjects = runtimeCache(getOpenClawProjects, 2); +export const getCachedOpenClawSessionsByEncodedName = runtimeCache( + (name: string) => getOpenClawSessionsByEncodedName(name), + 2, + { maxSize: 50 }, +); diff --git a/lib/openclaw-sessions.ts b/lib/openclaw-sessions.ts new file mode 100644 index 00000000..fdebd2d3 --- /dev/null +++ b/lib/openclaw-sessions.ts @@ -0,0 +1,284 @@ +/** + * OpenClaw (openclaw gateway) session transcript loader + parser. + * + * AUDIT-ONLY (Pillar 2). OpenClaw writes one JSONL transcript per session at + * `~/.openclaw/agents//sessions/.jsonl` (sessionId is a + * UUID), alongside a much larger `.trajectory.jsonl` OTel trace we + * IGNORE, and a `sessions.json` index keyed by sessionKey. Verified live + * against openclaw v2026.7.1. + * + * The transcript is type-discriminated JSONL: + * {type:"session", cwd, …} — header (carries cwd) + * {type:"model_change" | "thinking_level_change" | "custom", …} — metadata + * {type:"message", timestamp, message:{role, content, …}} — the turns, where + * role ∈ "user" (content string) | "assistant" (content = list of + * {type:"text",text} / {type:"toolCall", id, name, arguments}) | + * "toolResult" ({toolCallId, toolName, content, details, isError}). + * `openclawLinesToLogEntries` pairs each assistant `toolCall` with its later + * `toolResult` by `toolCallId` (mirrors lib/hermes-sessions.ts) and is PURE, so + * it is unit-testable with plain line objects. + * + * Home override: set `OPENCLAW_HOME` (used by tests / to point at a copied + * gateway config dir). + */ +import { readFile } from "node:fs/promises"; +import { readdirSync, statSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { runtimeCache } from "./runtime-cache"; +import { + baseEntry, + formatTimestamp, + parseRawLines, + type LogEntry, + type UserEntry, + type AssistantEntry, + type GenericEntry, + type ContentBlock, + type ToolUseBlock, + type LogSource, +} from "./log-entries"; +import { formatDuration } from "./format-duration"; + +/** OpenClaw sessions are stored under UUID filenames. */ +export const OPENCLAW_SESSION_ID_RE = /^[0-9a-fA-F-]{36}$/; + +/** Absolute path to OpenClaw's config home (override with OPENCLAW_HOME). */ +export function openclawHome(): string { + return process.env.OPENCLAW_HOME || join(homedir(), ".openclaw"); +} + +// ── Parsing helpers ── + +function isPlainObject(v: unknown): v is Record { + return !!v && typeof v === "object" && !Array.isArray(v); +} + +/** Text from a `content` field that may be a string or an array of + * `{ type:"text", text }` blocks. */ +function extractText(content: unknown): string { + if (typeof content === "string") return content; + if (Array.isArray(content)) { + return content + .map((c) => (isPlainObject(c) && typeof c.text === "string" ? (c.text as string) : "")) + .filter(Boolean) + .join("\n"); + } + return ""; +} + +function toDate(value: unknown, fallbackMs: number): Date { + if (typeof value === "number" && Number.isFinite(value)) { + if (value > 1e12) return new Date(value); + if (value > 1e9) return new Date(value * 1000); + } + if (typeof value === "string") { + const ms = Date.parse(value); + if (!Number.isNaN(ms)) return new Date(ms); + } + return new Date(fallbackMs); +} + +// ── Pure parser: transcript lines → LogEntry[] ── + +/** + * Convert OpenClaw transcript JSONL lines (parsed objects, in file order) into + * `LogEntry[]`. Only `type:"message"` lines carry turns; `session` / + * `model_change` / `thinking_level_change` / `custom` lines are metadata and + * are skipped (they'd otherwise clutter the audit with empty system entries). + * Assistant `toolCall` blocks are paired with their later `toolResult` message + * by `toolCallId`. Pure — unit-testable with plain line objects. + */ +export function openclawLinesToLogEntries( + lines: Record[], + source: LogSource = "session", +): LogEntry[] { + const entries: LogEntry[] = []; + const toolUseById = new Map(); + const toolUseStartMs = new Map(); + const baseMs = Date.now(); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (!isPlainObject(line)) continue; + if (line.type !== "message") continue; // skip session/model_change/custom/… + + const m = isPlainObject(line.message) ? line.message : undefined; + if (!m) continue; + const role = typeof m.role === "string" ? m.role : "system"; + + // Outer line `timestamp` is an ISO string; message.timestamp is epoch ms. + const date = toDate(line.timestamp ?? m.timestamp, baseMs + i); + const timestamp = date.toISOString(); + const raw: Record = { + uuid: line.id != null ? String(line.id) : `openclaw-${i}`, + parentUuid: line.parentId != null ? String(line.parentId) : null, + }; + const base = baseEntry(raw, timestamp, date, source); + + if (role === "user") { + entries.push({ + type: "user", + ...base, + message: { role: "user", content: extractText(m.content) }, + } satisfies UserEntry); + continue; + } + + if (role === "assistant") { + const blocks: ContentBlock[] = []; + const content = m.content; + if (Array.isArray(content)) { + for (const b of content) { + if (!isPlainObject(b)) continue; + if (b.type === "text" && typeof b.text === "string") { + blocks.push({ type: "text", text: b.text }); + } else if (b.type === "toolCall") { + const id = typeof b.id === "string" ? b.id : `${String(b.name ?? "tool")}-${blocks.length}`; + const name = typeof b.name === "string" ? b.name : "tool"; + const input = isPlainObject(b.arguments) ? b.arguments : {}; + const block: ToolUseBlock = { type: "tool_use", id, name, input }; + blocks.push(block); + toolUseById.set(id, block); + toolUseStartMs.set(id, date.getTime()); + } + } + } else { + const text = extractText(content); + if (text) blocks.push({ type: "text", text }); + } + if (blocks.length === 0) continue; // empty / failed assistant turn + entries.push({ + type: "assistant", + ...base, + message: { role: "assistant", content: blocks, model: typeof m.model === "string" ? m.model : undefined }, + } satisfies AssistantEntry); + continue; + } + + if (role === "toolResult") { + const callId = typeof m.toolCallId === "string" ? m.toolCallId : undefined; + const block = callId ? toolUseById.get(callId) : undefined; + if (block) { + const details = isPlainObject(m.details) ? m.details : undefined; + const startMs = (callId && toolUseStartMs.get(callId)) || date.getTime(); + const durationMs = + details && typeof details.durationMs === "number" + ? details.durationMs + : Math.max(0, date.getTime() - startMs); + block.result = { + timestamp, + timestampFormatted: formatTimestamp(date), + content: extractText(m.content), + durationMs, + durationFormatted: formatDuration(durationMs), + }; + continue; + } + // Orphan tool result — fall through to a system entry. + } + + entries.push({ type: "system", ...base, raw } satisfies GenericEntry); + } + + entries.sort((a, b) => a.timestampMs - b.timestampMs); + return entries; +} + +// ── Discovery + file loader ── + +export interface OpenClawTranscriptFile { + agentId: string; + sessionId: string; + transcriptPath: string; + mtimeMs: number; + sizeBytes: number; +} + +/** Enumerate `agents//sessions/.jsonl` transcripts, skipping the + * heavy `.trajectory.jsonl` OTel traces and pointer files. */ +export function listOpenClawTranscripts(): OpenClawTranscriptFile[] { + const agentsDir = join(openclawHome(), "agents"); + const out: OpenClawTranscriptFile[] = []; + let agentIds: string[]; + try { + agentIds = readdirSync(agentsDir, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name); + } catch { + return out; + } + for (const agentId of agentIds) { + const sessionsDir = join(agentsDir, agentId, "sessions"); + let files: string[]; + try { + files = readdirSync(sessionsDir); + } catch { + continue; + } + for (const file of files) { + // Only `.jsonl` — not `.trajectory.jsonl` / `.trajectory-path.json`. + if (!file.endsWith(".jsonl") || file.endsWith(".trajectory.jsonl")) continue; + const sessionId = file.slice(0, -".jsonl".length); + if (!OPENCLAW_SESSION_ID_RE.test(sessionId)) continue; + const transcriptPath = join(sessionsDir, file); + try { + const st = statSync(transcriptPath); + out.push({ agentId, sessionId, transcriptPath, mtimeMs: st.mtimeMs, sizeBytes: st.size }); + } catch { + // skip unreadable + } + } + } + return out; +} + +/** Resolve a session UUID to its on-disk transcript path (host-side). Guards + * against traversal by requiring a UUID filename. Shared by the audit adapter + * and download-session. */ +export function findOpenClawTranscript(sessionId: string): string | null { + if (!OPENCLAW_SESSION_ID_RE.test(sessionId)) return null; + for (const t of listOpenClawTranscripts()) { + if (t.sessionId === sessionId) return t.transcriptPath; + } + return null; +} + +export interface OpenClawSessionLogData { + entries: LogEntry[]; + rawLines: Record[]; + cwd?: string; + filePath: string; +} + +/** Load and parse one session transcript by UUID. Returns `null` when the file + * is missing/unreadable or the id fails validation. */ +export async function getOpenClawSessionLog( + sessionId: string, +): Promise { + const filePath = findOpenClawTranscript(sessionId); + if (!filePath) return null; + let content: string; + try { + content = await readFile(filePath, "utf-8"); + } catch { + return null; + } + const rawLines = parseRawLines(content, "session"); + const entries = openclawLinesToLogEntries(rawLines, "session"); + // cwd lives on the `type:"session"` header line. + let cwd: string | undefined; + for (const line of rawLines) { + if (isPlainObject(line) && line.type === "session" && typeof line.cwd === "string" && line.cwd.length > 0) { + cwd = line.cwd; + break; + } + } + return { entries, rawLines, cwd, filePath }; +} + +export const getCachedOpenClawSessionLog = runtimeCache( + (sessionId: string) => getOpenClawSessionLog(sessionId), + 2, + { maxSize: 50 }, +); diff --git a/lib/projects.ts b/lib/projects.ts index f6ffeb81..fa1afeb4 100644 --- a/lib/projects.ts +++ b/lib/projects.ts @@ -16,7 +16,7 @@ import { formatDate } from "./format-date"; export const UUID_RE = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/; export const PATH_TRAVERSAL_RE = /(^|[\\/])\.\.($|[\\/])/; -export type ProjectCli = "claude" | "codex" | "copilot" | "cursor" | "opencode" | "pi" | "gemini" | "hermes"; +export type ProjectCli = "claude" | "codex" | "copilot" | "cursor" | "opencode" | "pi" | "hermes" | "openclaw" | "factory" | "devin" | "antigravity" | "goose"; export interface ProjectFolder { name: string; @@ -143,18 +143,26 @@ export async function getProjectFolders(): Promise { { getCursorProjects }, { getOpenCodeProjects }, { getPiProjects }, - { getGeminiProjects }, { getHermesProjects }, + { getOpenClawProjects }, + { getFactoryProjects }, + { getDevinProjects }, + { getAntigravityProjects }, + { getGooseProjects }, ] = await Promise.all([ import("./codex-projects"), import("./copilot-projects"), import("./cursor-projects"), import("./opencode-projects"), import("./pi-projects"), - import("./gemini-projects"), import("./hermes-projects"), + import("./openclaw-projects"), + import("./factory-projects"), + import("./devin-projects"), + import("./antigravity-projects"), + import("./goose-projects"), ]); - const [claude, codex, copilot, cursor, opencode, pi, gemini, hermes] = await Promise.all([ + const [claude, codex, copilot, cursor, opencode, pi, hermes, openclaw, factory, devin, antigravity, goose] = await Promise.all([ getClaudeProjectFolders(), getCodexProjects().catch((error) => { logError("Error reading Codex projects:", error); @@ -176,16 +184,32 @@ export async function getProjectFolders(): Promise { logError("Error reading Pi projects:", error); return [] as ProjectFolder[]; }), - getGeminiProjects().catch((error) => { - logError("Error reading Gemini projects:", error); - return [] as ProjectFolder[]; - }), getHermesProjects().catch((error) => { logError("Error reading Hermes projects:", error); return [] as ProjectFolder[]; }), + getOpenClawProjects().catch((error) => { + logError("Error reading OpenClaw projects:", error); + return [] as ProjectFolder[]; + }), + getFactoryProjects().catch((error) => { + logError("Error reading Factory projects:", error); + return [] as ProjectFolder[]; + }), + getDevinProjects().catch((error) => { + logError("Error reading Devin projects:", error); + return [] as ProjectFolder[]; + }), + getAntigravityProjects().catch((error) => { + logError("Error reading Antigravity projects:", error); + return [] as ProjectFolder[]; + }), + getGooseProjects().catch((error) => { + logError("Error reading Goose projects:", error); + return [] as ProjectFolder[]; + }), ]); - return mergeProjectFolders(claude, codex, copilot, cursor, opencode, pi, gemini, hermes); + return mergeProjectFolders(claude, codex, copilot, cursor, opencode, pi, hermes, openclaw, factory, devin, antigravity, goose); } /** diff --git a/openclaw-plugin/index.js b/openclaw-plugin/index.js new file mode 100644 index 00000000..869d8a24 --- /dev/null +++ b/openclaw-plugin/index.js @@ -0,0 +1,223 @@ +/** + * failproofai policy bridge for OpenClaw (openclaw gateway). + * + * Loaded in-process by the gateway (registered via `plugins.load.paths` + + * `plugins.entries.failproofai` in ~/.openclaw/openclaw.json). It subscribes to + * OpenClaw's typed PLUGIN hooks — the only surface that can block; OpenClaw's + * file-based "internal hooks" are observation-only — and forwards each to the + * failproofai binary as `failproofai --hook --cli openclaw`. failproofai + * prints a flat `{permission, reason}` verdict on stdout; this shim maps it to + * each hook's native return shape. + * + * Marker comment for failproofai's installer detection (do not remove): + * __failproofai_hook__: true + * + * Design notes (verified live against openclaw v2026.7.1): + * • ASYNC spawn, never spawnSync — the gateway is a long-running multi-channel + * process; a synchronous spawn on every hook would stall every channel. + * 30s guard timer; fail-open on any error (never block on infra failure). + * • Raw forwarding — we send the raw tool name / params and a Claude-shaped + * stdin (params→tool_input, toolName→tool_name, transcriptPath→ + * transcript_path, stopHookActive→stop_hook_active, sessionKey→session_id). + * The binary canonicalizes via OPENCLAW_* maps (single source of truth); the + * shim ships NO inline maps (unlike the OpenCode/Pi shims). + * • No top-level `await` — OpenClaw's plugin loader rejects it; all async work + * happens inside register()'s handlers. + * + * Binary resolution mirrors pi-extension/index.ts: prefer the bundled + * dist/cli.mjs (node-compatible), fall back to bin/failproofai.mjs with bun for + * dev; honor FAILPROOFAI_BINARY_OVERRIDE. Paths resolve relative to this file + * via import.meta.url (the gateway spawns plugins with an undefined cwd). + */ +import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; +import { spawn } from "node:child_process"; +import { existsSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const DIST_BIN = resolve(HERE, "..", "dist", "cli.mjs"); +const SRC_BIN = resolve(HERE, "..", "bin", "failproofai.mjs"); + +function resolveSpawn() { + if (process.env.FAILPROOFAI_BINARY_OVERRIDE) { + return { cmd: "node", args: [process.env.FAILPROOFAI_BINARY_OVERRIDE] }; + } + if (existsSync(DIST_BIN)) { + return { cmd: "node", args: [DIST_BIN] }; + } + return { cmd: "bun", args: [SRC_BIN] }; +} + +function debug(msg) { + if (process.env.FAILPROOFAI_OPENCLAW_DEBUG === "1") { + process.stderr.write(`[failproofai-openclaw-shim] ${msg}\n`); + } +} + +/** + * Spawn `failproofai --hook --cli openclaw`, write the JSON payload + * to stdin, and resolve the parsed flat `{permission, reason}` verdict. Async + + * fail-open: any spawn/parse error or timeout resolves to `{permission:"allow"}` + * so a failproofai fault never blocks the gateway. + */ +function callPolicy(rawEvent, payload) { + return new Promise((done) => { + const { cmd, args } = resolveSpawn(); + let child; + try { + child = spawn(cmd, [...args, "--hook", rawEvent, "--cli", "openclaw"], { + stdio: ["pipe", "pipe", "pipe"], + }); + } catch (err) { + debug(`spawn threw: ${err && err.message ? err.message : String(err)}`); + done({ permission: "allow" }); + return; + } + let out = ""; + let settled = false; + const finish = (verdict) => { + if (settled) return; + settled = true; + clearTimeout(timer); + done(verdict); + }; + const timer = setTimeout(() => { + debug(`timeout on ${rawEvent}; killing child`); + try { child.kill(); } catch { /* ignore */ } + finish({ permission: "allow" }); + }, 30_000); + child.stdout.on("data", (d) => { out += d; }); + child.on("error", (err) => { + debug(`child error: ${err && err.message ? err.message : String(err)}`); + finish({ permission: "allow" }); + }); + child.on("close", () => { + const stdout = out.trim(); + if (!stdout) { finish({ permission: "allow" }); return; } + try { + finish(JSON.parse(stdout)); + } catch (err) { + debug(`parse error: ${err && err.message ? err.message : String(err)}`); + finish({ permission: "allow" }); + } + }); + try { + child.stdin.end(JSON.stringify(payload)); + } catch (err) { + debug(`stdin write failed: ${err && err.message ? err.message : String(err)}`); + finish({ permission: "allow" }); + } + }); +} + +/** Claude-shaped stdin base. Extra keys are ignored by the binary's payload + * parser; they're forwarded for future use / activity attribution. */ +function baseMeta(payload, ctx) { + const p = payload || {}; + const c = ctx || {}; + return { + session_id: c.sessionId ?? p.sessionId ?? c.sessionKey ?? p.sessionKey, + cwd: p.cwd ?? c.workspaceDir ?? process.cwd(), + transcript_path: p.transcriptPath, + stop_hook_active: p.stopHookActive === true, + openclaw: { + agentId: c.agentId, + sessionKey: c.sessionKey ?? p.sessionKey, + runId: c.runId ?? p.runId, + provider: p.provider, + model: p.model, + }, + }; +} + +export default definePluginEntry({ + id: "failproofai", + name: "failproofai", + description: "Real-time policy enforcement for OpenClaw by failproofai", + register(api) { + // before_tool_call → PreToolUse. Full deny: return {block:true, blockReason}. + api.on( + "before_tool_call", + async (payload, ctx) => { + const p = payload || {}; + const verdict = await callPolicy("before_tool_call", { + ...baseMeta(payload, ctx), + tool_name: p.toolName, + tool_input: p.params, + hook_event_name: "before_tool_call", + }); + if (verdict.permission === "deny") { + return { block: true, blockReason: verdict.reason || "Blocked by failproofai" }; + } + return undefined; + }, + { priority: 100, timeoutMs: 60_000 }, + ); + + // before_agent_run → UserPromptSubmit. Deny: return {outcome:"block", reason}. + api.on( + "before_agent_run", + async (payload, ctx) => { + const p = payload || {}; + const verdict = await callPolicy("before_agent_run", { + ...baseMeta(payload, ctx), + prompt: p.prompt, + hook_event_name: "before_agent_run", + }); + if (verdict.permission === "deny") { + return { outcome: "block", reason: verdict.reason || "Blocked by failproofai" }; + } + return undefined; + }, + { priority: 100, timeoutMs: 60_000 }, + ); + + // before_agent_finalize → Stop. The real turn-end gate: a deny becomes a + // revise so the agent runs another pass. The evaluator supplies the + // MANDATORY ACTION wording; the payload's stop_hook_active guards the + // require-*-before-stop builtins against infinite loops. + api.on( + "before_agent_finalize", + async (payload, ctx) => { + const verdict = await callPolicy("before_agent_finalize", { + ...baseMeta(payload, ctx), + hook_event_name: "before_agent_finalize", + }); + if (verdict.permission === "deny") { + return { action: "revise", reason: verdict.reason || "Action required by failproofai" }; + } + return undefined; + }, + { priority: 100, timeoutMs: 60_000 }, + ); + + // Observation hooks — await so activity ordering is stable, ignore verdict. + // after_tool_call → PostToolUse; session_start/end → SessionStart/End; + // subagent_ended → SubagentStop (obs only); before_compaction → PreCompact. + for (const ev of [ + "after_tool_call", + "session_start", + "session_end", + "subagent_ended", + "before_compaction", + ]) { + api.on( + ev, + async (payload, ctx) => { + const p = payload || {}; + await callPolicy(ev, { + ...baseMeta(payload, ctx), + tool_name: p.toolName, + tool_input: p.params, + tool_response: p.result, + reason: p.reason, + hook_event_name: ev, + }); + return undefined; + }, + { priority: 100, timeoutMs: 60_000 }, + ); + } + }, +}); diff --git a/openclaw-plugin/openclaw.plugin.json b/openclaw-plugin/openclaw.plugin.json new file mode 100644 index 00000000..879fcde1 --- /dev/null +++ b/openclaw-plugin/openclaw.plugin.json @@ -0,0 +1,6 @@ +{ + "id": "failproofai", + "name": "failproofai", + "description": "Real-time policy enforcement for OpenClaw by failproofai", + "configSchema": {} +} diff --git a/openclaw-plugin/package.json b/openclaw-plugin/package.json new file mode 100644 index 00000000..c3dc0f89 --- /dev/null +++ b/openclaw-plugin/package.json @@ -0,0 +1,12 @@ +{ + "name": "@failproofai/openclaw-plugin", + "version": "0.0.1", + "description": "failproofai policy bridge for OpenClaw (openclaw gateway)", + "type": "module", + "main": "./index.js", + "private": true, + "keywords": [ + "openclaw-plugin", + "failproofai" + ] +} diff --git a/package.json b/package.json index b59c8550..26e4cd49 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "failproofai", - "version": "0.0.13", + "version": "0.0.14-beta.1", "description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK", "bin": { "failproofai": "./dist/cli.mjs" @@ -11,6 +11,7 @@ "scripts/", "lib/", "pi-extension/", + "openclaw-plugin/", ".next/standalone/", "dist/", "README.md" diff --git a/scripts/sync-agent-cli-harnesses-prompt.md b/scripts/sync-agent-cli-harnesses-prompt.md index 550e4107..90fd5eed 100644 --- a/scripts/sync-agent-cli-harnesses-prompt.md +++ b/scripts/sync-agent-cli-harnesses-prompt.md @@ -32,7 +32,6 @@ tracked in `src/hooks/types.ts` and written to disk by `src/hooks/integrations.t | Cursor Agent | `CURSOR_HOOK_EVENT_TYPES` | `CURSOR_EVENT_MAP` | `.cursor/hooks.json` | | OpenCode | `OPENCODE_HOOK_EVENT_TYPES` | `OPENCODE_EVENT_MAP` | `.opencode/opencode.json` + `.opencode/plugins/failproofai.mjs` | | Pi | `PI_HOOK_EVENT_TYPES` | `PI_EVENT_MAP` | `.pi/settings.json` | -| Gemini CLI | `GEMINI_HOOK_EVENT_TYPES` | `GEMINI_EVENT_MAP` | `.gemini/settings.json` | ## What "drift" means — three scopes @@ -62,7 +61,7 @@ For each verified CLI, compare the upstream docs to this repo across three scope when you only need to **inspect** a committed fixture, prefer `git show HEAD:` (e.g. `git show HEAD:.codex/hooks.json`) — it is never blocked and is the reliable way to read `.claude/settings.json`, `.codex/hooks.json`, - `.cursor/hooks.json`, `.github/hooks/failproofai.json`, `.gemini/settings.json`, + `.cursor/hooks.json`, `.github/hooks/failproofai.json`, `.pi/settings.json`, and `.opencode/*`. - Do **NOT** run `failproofai policies --install` (or any `failproofai` subcommand) to regenerate a fixture — it is blocked. Edit fixtures by hand. @@ -76,7 +75,7 @@ Spawn one subagent per CLI (seven, in parallel). Each subagent, for its CLI: **settings-file schema** (top-level keys, per-entry shape, command field name, timeout field name **and unit**, and whether `matcher` / `version` / `$schema` are present). **Use upstream casing exactly** — Codex snake_case, Cursor - camelCase, OpenCode dot.namespaced, Pi snake_case, Claude/Copilot/Gemini + camelCase, OpenCode dot.namespaced, Pi snake_case, Claude/Copilot PascalCase. Do **not** normalize. 2. Compare against this repo: the CLI's `*HOOK_EVENT_TYPES` array + paired `*EVENT_MAP` (scope 1); its `*_TOOL_MAP` / `*_TOOL_INPUT_MAP` and its branch in @@ -98,7 +97,6 @@ surface is documented in the package source, not a clean enumeration). | Cursor | https://cursor.com/docs/hooks | | OpenCode | https://opencode.ai/docs/plugins/ | | Pi | https://www.npmjs.com/package/@mariozechner/pi-coding-agent | -| Gemini | https://geminicli.com/docs/hooks/ · https://geminicli.com/docs/reference/tools/ (tool names) | ## Phase 2 — Decide what to do (do this BEFORE editing any files) @@ -122,7 +120,6 @@ changes that block a clean stop on the comment / no-op paths below. > This open sync PR is missing newly-detected drift: > - **Cursor**: upstream added event `afterEdit` — append to `CURSOR_HOOK_EVENT_TYPES` (+ a `CURSOR_EVENT_MAP` entry). - > - **Gemini**: the `run_shell_command` tool was renamed to `shell` — the `GEMINI_TOOL_MAP` key is stale. > > (hook-sync bot — please fold these into this PR rather than opening a second sync PR.) @@ -140,7 +137,7 @@ requires. - **Append** new events just before `] as const`, preserving upstream casing. - **Delete** removed events; if the CLI has an `*EVENT_MAP`, also delete the same key from that map (the `Record` is exhaustive — a stale key fails `tsc`). -- For **map-bearing CLIs** (Codex, Cursor, OpenCode, Pi, Gemini), do **NOT** invent +- For **map-bearing CLIs** (Codex, Cursor, OpenCode, Pi), do **NOT** invent the canonical mapping for a newly-added event — leave it out of the `*EVENT_MAP` so `tsc` fails intentionally, and add a reviewer-checklist item to the PR body (see below). For **Claude and Copilot** (no map), the build stays green. @@ -148,9 +145,9 @@ requires. - If `HOOK_EVENT_TYPES` (Claude) changed: in `__tests__/hooks/manager.test.ts`, update the `installs hooks for all event types` description AND both `expect(Object.keys(written.hooks)).toHaveLength()` assertions. - - If `GEMINI_HOOK_EVENT_TYPES` changed: in `__tests__/hooks/integrations.test.ts`, - update the `expect(gemini.eventTypes).toHaveLength()` assertion AND the - matching description string. + - If a CLI's `*_HOOK_EVENT_TYPES` changed: in `__tests__/hooks/integrations.test.ts`, + update the matching `expect(.eventTypes).toHaveLength()` assertion AND the + description string. - Locate these by searching for the current count number. ### Scope 2 / Scope 3 (tool schema / settings-file shape) @@ -211,7 +208,7 @@ The PR **body** must contain, in order: `__tests__/e2e/hooks/-integration.e2e.test.ts`, the seven dogfood fixtures (`.claude/settings.json`, `.codex/hooks.json`, `.cursor/hooks.json`, `.github/hooks/failproofai.json`, `.opencode/opencode.json`, - `.opencode/plugins/failproofai.mjs`, `.pi/settings.json`, `.gemini/settings.json`), + `.opencode/plugins/failproofai.mjs`, `.pi/settings.json`), and `CHANGELOG.md`. - Do **NOT** edit `.failproofai/policies-config.json` (the entrypoint manages it), `src/hooks/handler.ts`, `src/hooks/manager.ts`, or any other source file. diff --git a/scripts/translate-docs/config.ts b/scripts/translate-docs/config.ts index 83819a92..fb15e0a8 100644 --- a/scripts/translate-docs/config.ts +++ b/scripts/translate-docs/config.ts @@ -2,22 +2,60 @@ import type { LanguageConfig } from "./types"; export const LANGUAGES: LanguageConfig[] = [ // Tier 1 — largest developer populations - { code: "zh", name: "Chinese (Simplified)", nativeName: "\u7b80\u4f53\u4e2d\u6587", tier: 1 }, + { + code: "zh", + name: "Chinese (Simplified)", + nativeName: "\u7b80\u4f53\u4e2d\u6587", + tier: 1, + }, { code: "ja", name: "Japanese", nativeName: "\u65e5\u672c\u8a9e", tier: 1 }, { code: "ko", name: "Korean", nativeName: "\ud55c\uad6d\uc5b4", tier: 1 }, { code: "es", name: "Spanish", nativeName: "Espa\u00f1ol", tier: 1 }, - { code: "pt-br", name: "Portuguese (Brazil)", nativeName: "Portugu\u00eas", tier: 1 }, + { + code: "pt-br", + mintlifyCode: "pt-BR", + name: "Portuguese (Brazil)", + nativeName: "Portugu\u00eas", + tier: 1, + }, { code: "de", name: "German", nativeName: "Deutsch", tier: 1 }, { code: "fr", name: "French", nativeName: "Fran\u00e7ais", tier: 1 }, // Tier 2 — strong tech communities - { code: "ru", name: "Russian", nativeName: "\u0420\u0443\u0441\u0441\u043a\u0438\u0439", tier: 2 }, - { code: "hi", name: "Hindi", nativeName: "\u0939\u093f\u0928\u094d\u0926\u0940", tier: 2 }, + { + code: "ru", + name: "Russian", + nativeName: "\u0420\u0443\u0441\u0441\u043a\u0438\u0439", + tier: 2, + }, + { + code: "hi", + name: "Hindi", + nativeName: "\u0939\u093f\u0928\u094d\u0926\u0940", + tier: 2, + }, { code: "tr", name: "Turkish", nativeName: "T\u00fcrk\u00e7e", tier: 2 }, - { code: "vi", name: "Vietnamese", nativeName: "Ti\u1ebfng Vi\u1ec7t", tier: 2 }, + { + code: "vi", + name: "Vietnamese", + nativeName: "Ti\u1ebfng Vi\u1ec7t", + tier: 2, + }, { code: "it", name: "Italian", nativeName: "Italiano", tier: 2 }, // Tier 3 — RTL languages - { code: "ar", name: "Arabic", nativeName: "\u0627\u0644\u0639\u0631\u0628\u064a\u0629", tier: 3, rtl: true }, - { code: "he", name: "Hebrew", nativeName: "\u05e2\u05d1\u05e8\u05d9\u05ea", tier: 3, rtl: true }, + { + code: "ar", + name: "Arabic", + nativeName: "\u0627\u0644\u0639\u0631\u0628\u064a\u0629", + tier: 3, + rtl: true, + }, + { + code: "he", + name: "Hebrew", + nativeName: "\u05e2\u05d1\u05e8\u05d9\u05ea", + tier: 3, + rtl: true, + }, ]; export function getLanguagesByTier(maxTier: number): LanguageConfig[] { @@ -184,17 +222,21 @@ export const NAV_TRANSLATIONS: Record< ru: { docs: "\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f", examples: "\u041f\u0440\u0438\u043c\u0435\u0440\u044b", - gettingStarted: "\u041d\u0430\u0447\u0430\u043b\u043e \u0440\u0430\u0431\u043e\u0442\u044b", - coreConcepts: "\u041e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u043a\u043e\u043d\u0446\u0435\u043f\u0446\u0438\u0438", + gettingStarted: + "\u041d\u0430\u0447\u0430\u043b\u043e \u0440\u0430\u0431\u043e\u0442\u044b", + coreConcepts: + "\u041e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u043a\u043e\u043d\u0446\u0435\u043f\u0446\u0438\u0438", cli: "CLI", tools: "\u0418\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u044b", - advanced: "\u041f\u0440\u043e\u0434\u0432\u0438\u043d\u0443\u0442\u044b\u0439", + advanced: + "\u041f\u0440\u043e\u0434\u0432\u0438\u043d\u0443\u0442\u044b\u0439", }, hi: { docs: "\u0926\u0938\u094d\u0924\u093e\u0935\u0947\u091c\u093c", examples: "\u0909\u0926\u093e\u0939\u0930\u0923", gettingStarted: "\u0936\u0941\u0930\u0942 \u0915\u0930\u0947\u0902", - coreConcepts: "\u092e\u0942\u0932 \u0905\u0935\u0927\u093e\u0930\u0923\u093e\u090f\u0901", + coreConcepts: + "\u092e\u0942\u0932 \u0905\u0935\u0927\u093e\u0930\u0923\u093e\u090f\u0901", cli: "CLI", tools: "\u0909\u092a\u0915\u0930\u0923", advanced: "\u0909\u0928\u094d\u0928\u0924", @@ -230,7 +272,8 @@ export const NAV_TRANSLATIONS: Record< docs: "\u0627\u0644\u0645\u0633\u062a\u0646\u062f\u0627\u062a", examples: "\u0623\u0645\u062b\u0644\u0629", gettingStarted: "\u0627\u0644\u0628\u062f\u0627\u064a\u0629", - coreConcepts: "\u0627\u0644\u0645\u0641\u0627\u0647\u064a\u0645 \u0627\u0644\u0623\u0633\u0627\u0633\u064a\u0629", + coreConcepts: + "\u0627\u0644\u0645\u0641\u0627\u0647\u064a\u0645 \u0627\u0644\u0623\u0633\u0627\u0633\u064a\u0629", cli: "CLI", tools: "\u0627\u0644\u0623\u062f\u0648\u0627\u062a", advanced: "\u0645\u062a\u0642\u062f\u0645", @@ -238,7 +281,8 @@ export const NAV_TRANSLATIONS: Record< he: { docs: "\u05ea\u05d9\u05e2\u05d5\u05d3", examples: "\u05d3\u05d5\u05d2\u05de\u05d0\u05d5\u05ea", - gettingStarted: "\u05ea\u05d7\u05d9\u05dc\u05ea \u05e2\u05d1\u05d5\u05d3\u05d4", + gettingStarted: + "\u05ea\u05d7\u05d9\u05dc\u05ea \u05e2\u05d1\u05d5\u05d3\u05d4", coreConcepts: "\u05de\u05d5\u05e9\u05d2\u05d9 \u05d9\u05e1\u05d5\u05d3", cli: "CLI", tools: "\u05db\u05dc\u05d9\u05dd", diff --git a/scripts/translate-docs/mintlify-nav.ts b/scripts/translate-docs/mintlify-nav.ts index 854c4ff2..e003c6bf 100644 --- a/scripts/translate-docs/mintlify-nav.ts +++ b/scripts/translate-docs/mintlify-nav.ts @@ -1,7 +1,7 @@ import { readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { NAV_TRANSLATIONS, LANGUAGES } from "./config"; +import { getLanguageByCode, NAV_TRANSLATIONS, LANGUAGES } from "./config"; const __dirname = dirname(fileURLToPath(import.meta.url)); const DOCS_JSON_PATH = join(__dirname, "..", "..", "docs", "docs.json"); @@ -105,7 +105,10 @@ export function buildLanguageNav( })), })); - return { language: lang, tabs }; + return { + language: getLanguageByCode(lang)?.mintlifyCode ?? lang, + tabs, + }; } /** diff --git a/scripts/translate-docs/types.ts b/scripts/translate-docs/types.ts index b578a56a..22ea32f6 100644 --- a/scripts/translate-docs/types.ts +++ b/scripts/translate-docs/types.ts @@ -1,5 +1,8 @@ export interface LanguageConfig { + /** Internal code used for folders, cache keys, and translation routing. */ code: string; + /** Mintlify locale when its canonical casing differs from `code`. */ + mintlifyCode?: string; name: string; nativeName: string; tier: 1 | 2 | 3; diff --git a/src/audit/cli-adapters/antigravity.ts b/src/audit/cli-adapters/antigravity.ts new file mode 100644 index 00000000..fd02c694 --- /dev/null +++ b/src/audit/cli-adapters/antigravity.ts @@ -0,0 +1,87 @@ +/** + * Antigravity (agy) transcript adapter — AUDIT (Pillar 2). + * + * Antigravity writes plain-JSONL transcripts at + * `~/.gemini/antigravity-cli/brain//.system_generated/logs/transcript_full.jsonl` + * (one step per line), with a SQLite conversation index at + * `conversation_summaries.db`. Verified live against agy v1.1.2. + * lib/antigravity-sessions.ts enumerates + parses the transcripts into the + * shared LogEntry[] shape; lib/antigravity-projects.ts reads the SQLite index + * for cwd/title enrichment. `logEntriesToEvents` handles the rest (tool-name + + * tool-input canonicalization via the ANTIGRAVITY_* maps). + */ +import { readFile } from "node:fs/promises"; +import { + listAntigravityTranscripts, + antigravityLinesToLogEntries, +} from "../../../lib/antigravity-sessions"; +import { getAntigravityIndex } from "../../../lib/antigravity-projects"; +import { parseRawLines } from "../../../lib/log-entries"; +import { encodeFolderName } from "../../../lib/paths"; +import type { NormalizedToolEvent, TranscriptMetadata } from "../types"; +import type { ListOpts } from "./claude"; +import { logEntriesToEvents } from "./shared"; + +export async function listAntigravityTranscriptMetadata( + opts: ListOpts = {}, +): Promise { + const projectFilter = opts.projects ? new Set(opts.projects) : null; + const sinceMs = opts.sinceMs ?? 0; + const out: TranscriptMetadata[] = []; + + const index = await getAntigravityIndex(); + + for (const t of listAntigravityTranscripts()) { + if (t.mtimeMs < sinceMs) continue; + const cwd = index.get(t.conversationId)?.cwd; + // `audit --project ` filters on the conversation's workspace cwd. + if (projectFilter && (!cwd || !projectFilter.has(cwd))) continue; + out.push({ + cli: "antigravity", + projectName: cwd ? encodeFolderName(cwd) : "antigravity", + sessionId: t.conversationId, + transcriptPath: t.transcriptPath, + mtimeMs: t.mtimeMs, + sizeBytes: t.sizeBytes, + }); + } + return out; +} + +export async function streamAntigravityEvents( + meta: TranscriptMetadata, +): Promise { + let content: string; + try { + content = await readFile(meta.transcriptPath, "utf-8"); + } catch { + return []; + } + + const rawLines = parseRawLines(content, "session"); + const entries = antigravityLinesToLogEntries(rawLines, "session"); + + // Recover cwd from the transcript's first run_command Cwd (falls back to ""). + let cwd = ""; + for (const line of rawLines) { + if (!line || typeof line !== "object") continue; + const calls = (line as Record).tool_calls; + if (!Array.isArray(calls)) continue; + for (const call of calls) { + const args = call && typeof call === "object" ? (call as Record).args : undefined; + const c = args && typeof args === "object" ? (args as Record).Cwd : undefined; + if (typeof c === "string" && c.length > 0) { + cwd = c; + break; + } + } + if (cwd) break; + } + + return logEntriesToEvents(entries, { + cli: "antigravity", + sessionId: meta.sessionId, + transcriptPath: meta.transcriptPath, + cwd, + }); +} diff --git a/src/audit/cli-adapters/devin.ts b/src/audit/cli-adapters/devin.ts new file mode 100644 index 00000000..1d4b692b --- /dev/null +++ b/src/audit/cli-adapters/devin.ts @@ -0,0 +1,53 @@ +/** + * Devin CLI (Cognition) transcript adapter — AUDIT-ONLY (Pillar 2). + * + * Devin keeps every session in one SQLite DB at + * `~/.local/share/devin/cli/sessions.db`. We read it directly via the bundled + * sql.js reader (lib/devin-projects.ts enumerates the `sessions` table; + * lib/devin-sessions.ts parses each session's `message_nodes`), producing the + * same LogEntry[] shape the other adapters do — so `logEntriesToEvents` handles + * the rest. Unlike Hermes, each Devin session carries a real + * `working_directory`, so `audit --project ` filters work. + */ +import { getDevinSessions } from "../../../lib/devin-projects"; +import { getDevinSessionLog } from "../../../lib/devin-sessions"; +import type { NormalizedToolEvent, TranscriptMetadata } from "../types"; +import type { ListOpts } from "./claude"; +import { logEntriesToEvents } from "./shared"; + +export async function listDevinTranscriptMetadata( + opts: ListOpts = {}, +): Promise { + const projectFilter = opts.projects ? new Set(opts.projects) : null; + const sinceMs = opts.sinceMs ?? 0; + const sessions = await getDevinSessions(); + const out: TranscriptMetadata[] = []; + for (const s of sessions) { + if (s.mtimeMs < sinceMs) continue; + // `audit --project ` filters on the session's working directory. + if (projectFilter && (!s.cwd || !projectFilter.has(s.cwd))) continue; + out.push({ + cli: "devin", + projectName: s.projectName, + sessionId: s.sessionId, + transcriptPath: `devin-db://${s.sessionId}`, + mtimeMs: s.mtimeMs, + // mtime advances on each message, so (mtime) forms a real cache key. + sizeBytes: 0, + }); + } + return out; +} + +export async function streamDevinEvents( + meta: TranscriptMetadata, +): Promise { + const log = await getDevinSessionLog(meta.sessionId); + if (!log) return []; + return logEntriesToEvents(log.entries, { + cli: "devin", + sessionId: meta.sessionId, + transcriptPath: meta.transcriptPath, + cwd: log.cwd ?? "", + }); +} diff --git a/src/audit/cli-adapters/factory.ts b/src/audit/cli-adapters/factory.ts new file mode 100644 index 00000000..c2f66be8 --- /dev/null +++ b/src/audit/cli-adapters/factory.ts @@ -0,0 +1,75 @@ +/** + * Factory (droid) transcript adapter — AUDIT (Pillar 2). + * + * droid writes real JSONL transcripts at + * `~/.factory/sessions//.jsonl` (Claude-style + * encoded-cwd folders; verified live against droid v0.171.0). + * lib/factory-sessions.ts enumerates and parses them into the shared + * LogEntry[] shape, so `logEntriesToEvents` handles the rest. + */ +import { readFile } from "node:fs/promises"; +import { + listFactoryTranscripts, + factoryLinesToLogEntries, +} from "../../../lib/factory-sessions"; +import { parseRawLines } from "../../../lib/log-entries"; +import type { NormalizedToolEvent, TranscriptMetadata } from "../types"; +import type { ListOpts } from "./claude"; +import { logEntriesToEvents } from "./shared"; + +export async function listFactoryTranscriptMetadata( + opts: ListOpts = {}, +): Promise { + const projectFilter = opts.projects ? new Set(opts.projects) : null; + const sinceMs = opts.sinceMs ?? 0; + const out: TranscriptMetadata[] = []; + + for (const t of listFactoryTranscripts()) { + if (t.mtimeMs < sinceMs) continue; + // `audit --project ` filters on the decoded working directory. + if (projectFilter && !projectFilter.has(t.cwd)) continue; + out.push({ + cli: "factory", + projectName: t.projectName, + sessionId: t.sessionId, + transcriptPath: t.transcriptPath, + mtimeMs: t.mtimeMs, + sizeBytes: t.sizeBytes, + }); + } + return out; +} + +export async function streamFactoryEvents( + meta: TranscriptMetadata, +): Promise { + let content: string; + try { + content = await readFile(meta.transcriptPath, "utf-8"); + } catch { + return []; + } + + const rawLines = parseRawLines(content, "session"); + const entries = factoryLinesToLogEntries(rawLines, "session"); + + // cwd lives on the `type:"session_start"` header line — recover the canonical + // value rather than re-decoding the (lossy) folder name. + let cwd = ""; + for (const line of rawLines) { + if (line && typeof line === "object" && (line as Record).type === "session_start") { + const c = (line as Record).cwd; + if (typeof c === "string" && c.length > 0) { + cwd = c; + break; + } + } + } + + return logEntriesToEvents(entries, { + cli: "factory", + sessionId: meta.sessionId, + transcriptPath: meta.transcriptPath, + cwd, + }); +} diff --git a/src/audit/cli-adapters/gemini.ts b/src/audit/cli-adapters/gemini.ts deleted file mode 100644 index 2253afac..00000000 --- a/src/audit/cli-adapters/gemini.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Gemini CLI transcript adapter. - */ -import { statSync } from "node:fs"; -import { getGeminiProjects, getGeminiSessionsByEncodedName } from "../../../lib/gemini-projects"; -import { getGeminiSessionLog } from "../../../lib/gemini-sessions"; -import type { NormalizedToolEvent, TranscriptMetadata } from "../types"; -import type { ListOpts } from "./claude"; -import { logEntriesToEvents } from "./shared"; - -export async function listGeminiTranscriptMetadata( - opts: ListOpts = {}, -): Promise { - const projectFilter = opts.projects ? new Set(opts.projects) : null; - const sinceMs = opts.sinceMs ?? 0; - const out: TranscriptMetadata[] = []; - - const projects = await getGeminiProjects(); - for (const project of projects) { - const { cwd, sessions } = await getGeminiSessionsByEncodedName(project.name); - const effectiveCwd = cwd ?? ""; - if (projectFilter && !projectFilter.has(effectiveCwd)) continue; - for (const s of sessions) { - const mtimeMs = s.lastModified.getTime(); - if (mtimeMs < sinceMs) continue; - let sizeBytes = 0; - try { sizeBytes = statSync(s.path).size; } catch { /* unreadable */ } - if (!s.sessionId) continue; - out.push({ - cli: "gemini", - projectName: project.name, - sessionId: s.sessionId, - transcriptPath: s.path, - mtimeMs, - sizeBytes, - }); - } - } - return out; -} - -export async function streamGeminiEvents(meta: TranscriptMetadata): Promise { - const log = await getGeminiSessionLog(meta.sessionId); - if (!log) return []; - return logEntriesToEvents(log.entries, { - cli: "gemini", - sessionId: meta.sessionId, - transcriptPath: meta.transcriptPath, - cwd: log.cwd ?? "", - }); -} diff --git a/src/audit/cli-adapters/goose.ts b/src/audit/cli-adapters/goose.ts new file mode 100644 index 00000000..5d951825 --- /dev/null +++ b/src/audit/cli-adapters/goose.ts @@ -0,0 +1,53 @@ +/** + * Goose (codename goose, Block) transcript adapter — AUDIT-ONLY (Pillar 2). + * + * Goose keeps every session in one SQLite DB at + * `~/.local/share/goose/sessions/sessions.db`. We read it directly via the + * bundled sql.js reader (lib/goose-projects.ts enumerates the `sessions` table; + * lib/goose-sessions.ts parses each session's `messages`), producing the same + * LogEntry[] shape the other adapters do — so `logEntriesToEvents` handles the + * rest. Like Devin, each Goose session carries a real `working_dir`, so + * `audit --project ` filters work (unlike the cwd-less Hermes gateway). + */ +import { getGooseSessions } from "../../../lib/goose-projects"; +import { getGooseSessionLog } from "../../../lib/goose-sessions"; +import type { NormalizedToolEvent, TranscriptMetadata } from "../types"; +import type { ListOpts } from "./claude"; +import { logEntriesToEvents } from "./shared"; + +export async function listGooseTranscriptMetadata( + opts: ListOpts = {}, +): Promise { + const projectFilter = opts.projects ? new Set(opts.projects) : null; + const sinceMs = opts.sinceMs ?? 0; + const sessions = await getGooseSessions(); + const out: TranscriptMetadata[] = []; + for (const s of sessions) { + if (s.mtimeMs < sinceMs) continue; + // `audit --project ` filters on the session's working directory. + if (projectFilter && (!s.cwd || !projectFilter.has(s.cwd))) continue; + out.push({ + cli: "goose", + projectName: s.projectName, + sessionId: s.sessionId, + transcriptPath: `goose-db://${s.sessionId}`, + mtimeMs: s.mtimeMs, + // mtime advances on each message, so (mtime) forms a real cache key. + sizeBytes: 0, + }); + } + return out; +} + +export async function streamGooseEvents( + meta: TranscriptMetadata, +): Promise { + const log = await getGooseSessionLog(meta.sessionId); + if (!log) return []; + return logEntriesToEvents(log.entries, { + cli: "goose", + sessionId: meta.sessionId, + transcriptPath: meta.transcriptPath, + cwd: log.cwd ?? "", + }); +} diff --git a/src/audit/cli-adapters/index.ts b/src/audit/cli-adapters/index.ts index fbc4b8aa..52fbe950 100644 --- a/src/audit/cli-adapters/index.ts +++ b/src/audit/cli-adapters/index.ts @@ -17,8 +17,12 @@ import { listCopilotTranscriptMetadata, streamCopilotEvents } from "./copilot"; import { listCursorTranscriptMetadata, streamCursorEvents } from "./cursor"; import { listOpenCodeTranscriptMetadata, streamOpenCodeEvents } from "./opencode"; import { listPiTranscriptMetadata, streamPiEvents } from "./pi"; -import { listGeminiTranscriptMetadata, streamGeminiEvents } from "./gemini"; import { listHermesTranscriptMetadata, streamHermesEvents } from "./hermes"; +import { listOpenClawTranscriptMetadata, streamOpenClawEvents } from "./openclaw"; +import { listFactoryTranscriptMetadata, streamFactoryEvents } from "./factory"; +import { listAntigravityTranscriptMetadata, streamAntigravityEvents } from "./antigravity"; +import { listDevinTranscriptMetadata, streamDevinEvents } from "./devin"; +import { listGooseTranscriptMetadata, streamGooseEvents } from "./goose"; export type { ListOpts }; @@ -59,16 +63,36 @@ export const ADAPTERS: Record = { listTranscripts: listPiTranscriptMetadata, streamEvents: streamPiEvents, }, - gemini: { - cli: "gemini", - listTranscripts: listGeminiTranscriptMetadata, - streamEvents: streamGeminiEvents, - }, hermes: { cli: "hermes", listTranscripts: listHermesTranscriptMetadata, streamEvents: streamHermesEvents, }, + openclaw: { + cli: "openclaw", + listTranscripts: listOpenClawTranscriptMetadata, + streamEvents: streamOpenClawEvents, + }, + factory: { + cli: "factory", + listTranscripts: listFactoryTranscriptMetadata, + streamEvents: streamFactoryEvents, + }, + devin: { + cli: "devin", + listTranscripts: listDevinTranscriptMetadata, + streamEvents: streamDevinEvents, + }, + antigravity: { + cli: "antigravity", + listTranscripts: listAntigravityTranscriptMetadata, + streamEvents: streamAntigravityEvents, + }, + goose: { + cli: "goose", + listTranscripts: listGooseTranscriptMetadata, + streamEvents: streamGooseEvents, + }, }; export function getAdapter(cli: IntegrationType): CliAdapter { diff --git a/src/audit/cli-adapters/openclaw.ts b/src/audit/cli-adapters/openclaw.ts new file mode 100644 index 00000000..01bd1dd9 --- /dev/null +++ b/src/audit/cli-adapters/openclaw.ts @@ -0,0 +1,56 @@ +/** + * OpenClaw (openclaw gateway) transcript adapter — AUDIT (Pillar 2). + * + * OpenClaw writes real JSONL transcripts at + * `~/.openclaw/agents//sessions/.jsonl` (verified live against + * v2026.7.1). lib/openclaw-sessions.ts enumerates and parses them into the + * shared LogEntry[] shape, so `logEntriesToEvents` handles the rest. + * + * Gateway sessions run in the container workspace, not a host repo, so they + * group by agentId (`openclaw:`) and contribute nothing to a + * cwd-scoped audit. + */ +import { + listOpenClawTranscripts, + getOpenClawSessionLog, +} from "../../../lib/openclaw-sessions"; +import type { NormalizedToolEvent, TranscriptMetadata } from "../types"; +import type { ListOpts } from "./claude"; +import { logEntriesToEvents } from "./shared"; + +export async function listOpenClawTranscriptMetadata( + opts: ListOpts = {}, +): Promise { + // `audit --project ` filters on working directory; gateway sessions run + // in the container workspace, not a host repo, so OpenClaw contributes + // nothing to a cwd-scoped audit. + if (opts.projects && opts.projects.length > 0) return []; + + const sinceMs = opts.sinceMs ?? 0; + const out: TranscriptMetadata[] = []; + for (const t of listOpenClawTranscripts()) { + if (t.mtimeMs < sinceMs) continue; + out.push({ + cli: "openclaw", + projectName: `openclaw:${t.agentId}`, + sessionId: t.sessionId, + transcriptPath: t.transcriptPath, + mtimeMs: t.mtimeMs, + sizeBytes: t.sizeBytes, + }); + } + return out; +} + +export async function streamOpenClawEvents( + meta: TranscriptMetadata, +): Promise { + const log = await getOpenClawSessionLog(meta.sessionId); + if (!log) return []; + return logEntriesToEvents(log.entries, { + cli: "openclaw", + sessionId: meta.sessionId, + transcriptPath: meta.transcriptPath, + cwd: log.cwd ?? "", + }); +} diff --git a/src/audit/cli.ts b/src/audit/cli.ts index cc7f9dd2..66653c9c 100644 --- a/src/audit/cli.ts +++ b/src/audit/cli.ts @@ -49,7 +49,7 @@ USAGE WHAT IT DOES 1. Scans past sessions from every installed agent CLI (Claude, Codex, Cursor, - Copilot, OpenCode, Pi, Gemini) — entirely on your machine. + Copilot, OpenCode, Pi) — entirely on your machine. 2. Starts the local dashboard and opens http://localhost:${DASHBOARD_PORT}/audit with your results. diff --git a/src/audit/report.ts b/src/audit/report.ts index 3bf7dea7..aec9f666 100644 --- a/src/audit/report.ts +++ b/src/audit/report.ts @@ -273,7 +273,7 @@ export function formatMarkdown(result: AuditResult): string { ); } out.push(""); - out.push("> [failproofai](https://github.com/FailproofAI/failproofai) is a hook-based policy engine for Claude Code, Codex, Copilot, Cursor, OpenCode, Pi, and Gemini CLI. The `audit` command replays past agent sessions through every builtin policy to surface patterns that were (or could've been) stopped."); + out.push("> [failproofai](https://github.com/FailproofAI/failproofai) is a hook-based policy engine for Claude Code, Codex, Copilot, Cursor, OpenCode, and Pi. The `audit` command replays past agent sessions through every builtin policy to surface patterns that were (or could've been) stopped."); out.push(""); if (totalHits === 0) return out.join("\n"); diff --git a/src/audit/types.ts b/src/audit/types.ts index 33529188..3d42bd42 100644 --- a/src/audit/types.ts +++ b/src/audit/types.ts @@ -24,7 +24,7 @@ export interface NormalizedToolEvent { toolName: string; /** Canonicalized tool input (snake_case keys for OpenCode/Pi). */ toolInput: Record; - /** Pre-canonicalization tool name (e.g. "run_shell_command" for Gemini). */ + /** Pre-canonicalization tool name (e.g. "exec" for OpenClaw). */ rawToolName: string; /** * Result text from the matching tool_result block, if present in the diff --git a/src/hooks/builtin-policies.ts b/src/hooks/builtin-policies.ts index d57fc5fa..b5bfebb8 100644 --- a/src/hooks/builtin-policies.ts +++ b/src/hooks/builtin-policies.ts @@ -53,9 +53,9 @@ function isAgentInternalPath(resolved: string): boolean { * • Pi: `.pi/settings.json` (project) and `.pi/agent/settings.json` * (user); also the Pi-managed extension dir * `.pi/extensions/` / `.pi/agent/extensions/`. - * • Gemini CLI: `.gemini/settings.json` (both project and user scope — - * user is `~/.gemini/settings.json`); also the Gemini-managed - * hooks scripts dir `.gemini/hooks/`. + * • Antigravity CLI (agy): reuses `~/.gemini/` — `~/.gemini/settings.json` + * and the customization-root hook config + * `~/.gemini/config/hooks.json`. * These must NEVER be edited by the agent itself — that would let it disable * its own protections. */ @@ -74,9 +74,9 @@ function isAgentSettingsFile(resolved: string): boolean { // Pi: settings + extensions dirs (project and user-scope variants). if (/[\\/]\.pi[\\/](?:agent[\\/])?settings\.json$/.test(resolved)) return true; if (/[\\/]\.pi[\\/](?:agent[\\/])?extensions[\\/]/.test(resolved)) return true; - // Gemini: settings.json + hooks dir referenced by `command: $GEMINI_PROJECT_DIR/.gemini/hooks/...`. + // Antigravity (agy) reuses ~/.gemini/: settings.json + the customization-root hooks.json. if (/[\\/]\.gemini[\\/]settings\.json$/.test(resolved)) return true; - if (/[\\/]\.gemini[\\/]hooks[\\/]/.test(resolved)) return true; + if (/[\\/]\.gemini[\\/]config[\\/]hooks\.json$/.test(resolved)) return true; return false; } diff --git a/src/hooks/handler.ts b/src/hooks/handler.ts index 3160c47b..7ee80770 100644 --- a/src/hooks/handler.ts +++ b/src/hooks/handler.ts @@ -12,15 +12,17 @@ import type { CodexHookEventType, CursorHookEventType, PiHookEventType, - GeminiHookEventType, HermesHookEventType, + OpenClawHookEventType, + AntigravityHookEventType, } from "./types"; import { CODEX_EVENT_MAP, CURSOR_EVENT_MAP, PI_EVENT_MAP, - GEMINI_EVENT_MAP, HERMES_EVENT_MAP, + OPENCLAW_EVENT_MAP, + ANTIGRAVITY_EVENT_MAP, } from "./types"; import { canonicalizeToolName, canonicalizeToolInput } from "./tool-name-canonicalize"; import type { PolicyFunction, PolicyResult } from "./policy-types"; @@ -45,9 +47,7 @@ import { hookLogInfo, hookLogWarn } from "./hook-logger"; * `session_start`); Claude Code sends PascalCase. Copilot CLI is installed * in "VS Code compatible" PascalCase mode (see integrations.ts), so its event * NAMES arrive PascalCase already (note: tool names are a separate matter and - * are canonicalized in canonicalizeToolName below). Gemini also sends - * PascalCase event names but with different spellings (`BeforeTool`, - * `BeforeAgent`, `AfterAgent`); we map via GEMINI_EVENT_MAP. The internal + * are canonicalized in canonicalizeToolName below). The internal * registry, builtin policies, and policy.match.events all key on PascalCase. */ function canonicalizeEventType(raw: string, cli: IntegrationType): HookEventType { @@ -63,16 +63,25 @@ function canonicalizeEventType(raw: string, cli: IntegrationType): HookEventType const mapped = PI_EVENT_MAP[raw as PiHookEventType]; if (mapped) return mapped; } - if (cli === "gemini") { - const mapped = GEMINI_EVENT_MAP[raw as GeminiHookEventType]; - if (mapped) return mapped; - } if (cli === "hermes") { // Hermes sends snake_case event names (pre_tool_call, on_session_start, …); // map to PascalCase. Has no turn-end Stop event, so no Stop mapping exists. const mapped = HERMES_EVENT_MAP[raw as HermesHookEventType]; if (mapped) return mapped; } + if (cli === "openclaw") { + // The openclaw-plugin shim forwards raw snake_case plugin-hook names + // (before_tool_call, before_agent_finalize, …); map to PascalCase. + // before_agent_finalize is the real turn-end gate → Stop. + const mapped = OPENCLAW_EVENT_MAP[raw as OpenClawHookEventType]; + if (mapped) return mapped; + } + if (cli === "antigravity") { + // Antigravity's --hook args are PreToolUse|PostToolUse|PreInvocation|Stop. + // PreInvocation (before-model) → UserPromptSubmit. Verified agy v1.1.2. + const mapped = ANTIGRAVITY_EVENT_MAP[raw as AntigravityHookEventType]; + if (mapped) return mapped; + } // claude / copilot / unknown — already PascalCase, pass through. // HOOK_EVENT_TYPES type-checks downstream. return raw as HookEventType; @@ -139,6 +148,36 @@ export async function handleHookEvent( } } + // Antigravity (agy) pipes a camelCase protojson payload; normalize the fields + // the handler downstream reads to canonical snake_case BEFORE any + // canonicalization runs. `toolCall.{name,args}` → `tool_name`/`tool_input`, + // `conversationId` → `session_id`, `workspacePaths[0]` → `cwd`, + // `transcriptPath` → `transcript_path`. Verified against agy v1.1.2. + if (cli === "antigravity") { + const tc = parsed.toolCall as { name?: string; args?: unknown } | undefined; + if (tc && typeof tc === "object") { + if (tc.name !== undefined) parsed.tool_name = tc.name; + if (tc.args !== undefined) parsed.tool_input = tc.args; + } + if (typeof parsed.conversationId === "string") parsed.session_id = parsed.conversationId; + if (Array.isArray(parsed.workspacePaths) && typeof parsed.workspacePaths[0] === "string") { + parsed.cwd = parsed.workspacePaths[0]; + } + if (typeof parsed.transcriptPath === "string") parsed.transcript_path = parsed.transcriptPath; + } + + // Goose pipes `event` / `working_dir` instead of Claude's `hook_event_name` / + // `cwd` (its `tool_name` / `tool_input` are already canonical field names). + // Normalize both so resolveCwd keeps its cwd (block-read-outside-cwd) and the + // round-tripped event name is available. The --hook arg is already PascalCase, + // so canonicalizeEventType needs no goose branch. Verified goose v1.43.0. + if (cli === "goose") { + if (typeof parsed.working_dir === "string") parsed.cwd = parsed.working_dir; + if (typeof parsed.event === "string" && parsed.hook_event_name === undefined) { + parsed.hook_event_name = parsed.event; + } + } + // Canonicalize event name (Codex sends snake_case; internals expect PascalCase) const canonicalEventType = canonicalizeEventType(eventType, cli); @@ -173,8 +212,8 @@ export async function handleHookEvent( hookEventName: parsed.hook_event_name as string | undefined, // Preserve the raw CLI-side event name (eventType arg) before // canonicalization. Response shapes that round-trip the agent-emitted - // event name (e.g. Gemini's `hookSpecificOutput.hookEventName`) prefer - // this over the canonicalized form when stdin omits hook_event_name. + // event name prefer this over the canonicalized form when stdin omits + // hook_event_name. rawHookEventName: eventType, cli, }; diff --git a/src/hooks/install-prompt.ts b/src/hooks/install-prompt.ts index 1e04009b..93c3f915 100644 --- a/src/hooks/install-prompt.ts +++ b/src/hooks/install-prompt.ts @@ -82,14 +82,14 @@ export async function resolveTargetClis( // Uninstall flow: no agent CLIs detected — nothing to remove from. Default to // claude so removeHooks operates over Claude's scopes (no-op if no settings file). console.log( - "\x1B[33mWarning: no agent CLI binary found in PATH (claude, codex, copilot, cursor-agent, opencode, pi, gemini). " + + "\x1B[33mWarning: no agent CLI binary found in PATH (claude, codex, copilot, cursor-agent, opencode, pi)." + "Defaulting to Claude Code; nothing will be removed if no settings file exists.\x1B[0m", ); fireDetectionEvent(["claude"], "defaulted_to_claude"); return ["claude"]; } console.log( - "\x1B[33mWarning: no agent CLI binary found in PATH (claude, codex, copilot, cursor-agent, opencode, pi, gemini). " + + "\x1B[33mWarning: no agent CLI binary found in PATH (claude, codex, copilot, cursor-agent, opencode, pi)." + "Defaulting to Claude Code; hooks will activate when an agent is installed.\x1B[0m", ); fireDetectionEvent(["claude"], "defaulted_to_claude"); diff --git a/src/hooks/integrations.ts b/src/hooks/integrations.ts index b932ffd0..011b93bf 100644 --- a/src/hooks/integrations.ts +++ b/src/hooks/integrations.ts @@ -26,10 +26,18 @@ import { OPENCODE_HOOK_SCOPES, PI_HOOK_EVENT_TYPES, PI_HOOK_SCOPES, - GEMINI_HOOK_EVENT_TYPES, - GEMINI_HOOK_SCOPES, HERMES_HOOK_EVENT_TYPES, HERMES_HOOK_SCOPES, + OPENCLAW_HOOK_EVENT_TYPES, + OPENCLAW_HOOK_SCOPES, + FACTORY_HOOK_EVENT_TYPES, + FACTORY_HOOK_SCOPES, + DEVIN_HOOK_EVENT_TYPES, + DEVIN_HOOK_SCOPES, + ANTIGRAVITY_HOOK_EVENT_TYPES, + ANTIGRAVITY_HOOK_SCOPES, + GOOSE_HOOK_EVENT_TYPES, + GOOSE_HOOK_SCOPES, FAILPROOFAI_HOOK_MARKER, INTEGRATION_TYPES, type IntegrationType, @@ -310,8 +318,7 @@ export const codex: Integration = { type: "command", // Codex reads `timeout` in SECONDS (the field is literally `timeout`, // default 600 per https://developers.openai.com/codex/hooks) — same unit as - // Claude/Cursor/Copilot, and unlike Gemini, whose `timeout` is in - // milliseconds (default 60000). 60 = 60s, not 60000ms (~16.7h). + // Claude/Cursor/Copilot. 60 = 60s. command, timeout: 60, [FAILPROOFAI_HOOK_MARKER]: true, @@ -1286,61 +1293,456 @@ function makePiProjectRelativeEntry(extPath: string): string { // works for the local user. return extResolved; } -// ── Gemini CLI integration ────────────────────────────────────────────────── -// -// Gemini's hook contract is the closest thing to a Claude Code clone we've -// shipped: same `{matcher, hooks: [{type, command, timeout}]}` settings shape, -// PascalCase event names, snake_case stdin payload field names (session_id, -// tool_name, tool_input, hook_event_name, cwd, transcript_path), subprocess -// execution model, and `$CLAUDE_PROJECT_DIR` env-var alias on top of its own -// `$GEMINI_PROJECT_DIR`. The integration is structurally identical to -// claudeCode below, with three deltas: -// -// • Settings paths: ~/.gemini/settings.json (user) / /.gemini/settings.json (project). -// System scope (/etc/gemini-cli/settings.json) is documented but not exposed. -// -// • Matcher field: each Gemini matcher entry carries an explicit `matcher` -// regex (e.g. `"write_file|replace"`). We default to `"*"` so policies fire -// on every tool call, mirroring the failproofai default of "every event, -// every tool". Users can hand-edit settings.json to scope tighter; we -// preserve their `matcher` field across re-installs by NOT replacing -// entries that aren't failproofai-marked. -// -// • Tool name canonicalization happens in handler.ts (snake_case → -// PascalCase via GEMINI_TOOL_MAP) so policies match unchanged; not the -// install layer's concern. +// ── Hermes (hermes-agent) — live hooks (Pillar 1) ──────────────────────────── // -// Detected via the `gemini` binary on PATH. +// External-command CLI like codex/cursor, but its config is YAML +// (`~/.hermes/config.yaml`) under a `hooks:` map, so the I/O layer uses the yaml +// Document API (comment-preserving). Flat per-event arrays like cursor: +// `hooks: { pre_tool_call: [ { command, timeout, } ], … }`. Hermes +// reads a `{"decision":"block",…}` JSON response on stdout (see +// policy-evaluator.ts); exit codes are ignored. User-scope only. + +/** One hook entry as stored under a `hooks:` event key in config.yaml. */ +interface HermesHookEntry { + command: string; + timeout?: number; + [key: string]: unknown; +} + +export const hermes: Integration = { + id: "hermes", + displayName: "Hermes", + scopes: HERMES_HOOK_SCOPES, + eventTypes: HERMES_HOOK_EVENT_TYPES, + + // Hermes config is USER-scope only (`~/.hermes/config.yaml`); there is no + // project/local file, so scope/cwd are irrelevant here. + getSettingsPath() { + return resolve(homedir(), ".hermes", "config.yaml"); + }, + + readSettings(settingsPath) { + // Return the yaml Document (cast) so writeHookEntries/writeSettings can + // round-trip it while preserving the user's other keys + comments. + return readYamlDoc(settingsPath) as unknown as Record; + }, + + writeSettings(settingsPath, settings) { + writeYamlDoc(settingsPath, settings as unknown as Document); + }, + + buildHookEntry(binaryPath, eventType, scope) { + // No matcher → fires for ALL tools / all platforms (slack/telegram/cli/cron) + // and internal subagents. `timeout` is in seconds; Hermes runs the command + // via shlex.split (shell=false). + const command = + scope === "project" + ? `npx -y failproofai --hook ${eventType} --cli hermes` + : `"${binaryPath}" --hook ${eventType} --cli hermes`; + return { + command, + timeout: 30, + [FAILPROOFAI_HOOK_MARKER]: true, + }; + }, + + isFailproofaiHook: isMarkedHook, + + writeHookEntries(settings, binaryPath, scope) { + const doc = settings as unknown as Document; + // Read the current hooks map as plain JS, then re-set ONLY the `hooks` key — + // preserving comments on every other part of config.yaml. + const js = (doc.toJS() ?? {}) as { hooks?: Record }; + const hooks: Record = + js.hooks && typeof js.hooks === "object" ? js.hooks : {}; + + for (const eventType of HERMES_HOOK_EVENT_TYPES) { + const entry = this.buildHookEntry(binaryPath, eventType, scope) as unknown as HermesHookEntry; + const arr = Array.isArray(hooks[eventType]) ? hooks[eventType] : []; + const idx = arr.findIndex((h) => isMarkedHook(h)); + if (idx >= 0) arr[idx] = entry; + else arr.push(entry); + hooks[eventType] = arr; + } + doc.set("hooks", hooks); + // The headless gateway has no TTY to answer Hermes's first-use hook-consent + // prompt, so auto-accept declared hooks. Tradeoff: also auto-accepts any + // other hook the operator adds; a targeted `shell-hooks-allowlist.json` + // pre-seed is a future refinement. + doc.set("hooks_auto_accept", true); + }, + + removeHooksFromFile(settingsPath) { + if (!existsSync(settingsPath)) return 0; + const doc = readYamlDoc(settingsPath); + const js = (doc.toJS() ?? {}) as { hooks?: Record }; + const hooks = js.hooks; + + let removed = 0; + if (hooks && typeof hooks === "object") { + for (const eventType of Object.keys(hooks)) { + const entries = hooks[eventType]; + if (!Array.isArray(entries)) continue; + const before = entries.length; + const filtered = entries.filter((h) => !isMarkedHook(h)); + removed += before - filtered.length; + if (filtered.length === 0) delete hooks[eventType]; + else hooks[eventType] = filtered; + } + if (removed > 0) { + if (Object.keys(hooks).length === 0) doc.delete("hooks"); + else doc.set("hooks", hooks); + } + } + + // Always drop our headless-consent flag on uninstall — even if the hooks were + // already removed manually — so it can't silently auto-accept future operator + // hooks. `doc.delete` returns true iff the key was present. + const droppedAutoAccept = doc.delete("hooks_auto_accept"); + if (removed > 0 || droppedAutoAccept) writeYamlDoc(settingsPath, doc); + return removed; + }, + + hooksInstalledInSettings(scope, cwd) { + const settingsPath = this.getSettingsPath(scope, cwd); + if (!existsSync(settingsPath)) return false; + try { + const doc = readYamlDoc(settingsPath); + const js = (doc.toJS() ?? {}) as { hooks?: Record }; + const hooks = js.hooks; + if (!hooks || typeof hooks !== "object") return false; + for (const entries of Object.values(hooks)) { + if (Array.isArray(entries) && entries.some((h) => isMarkedHook(h))) return true; + } + } catch { + // Corrupt config — treat as not installed. + } + return false; + }, + + detectInstalled() { + return binaryExists("hermes"); + }, +}; + +// ── OpenClaw integration ──────────────────────────────────────────────────── // -// Ref: https://geminicli.com/docs/hooks/ +// OpenClaw is a self-hosted assistant gateway. Enforcement is via its in-process +// PLUGIN hooks (file-based "internal hooks" are observation-only), so failproofai +// ships a static plugin package (`openclaw-plugin/`, like Pi's pi-extension) that +// async-spawns the binary and maps verdicts back. Install registers the plugin in +// `~/.openclaw/openclaw.json` (JSON): +// • plugins.load.paths[] → the shipped openclaw-plugin dir (absolute path) +// • plugins.entries.failproofai = { enabled: true, hooks: { allowConversationAccess: true } } +// (allowConversationAccess is required for the raw-conversation hooks +// before_agent_run / before_agent_finalize; verified live v2026.7.1). +// USER scope only — OpenClaw has no project config (workspace plugins are +// disabled by default). We NEVER delete the shipped plugin dir on uninstall +// (unlike OpenCode's generated shim) — uninstall only edits openclaw.json. +// Detected via the `openclaw` binary on PATH. + +const OPENCLAW_PLUGIN_ID = "failproofai"; + +interface OpenClawPluginsSection { + load?: { paths?: string[]; [k: string]: unknown }; + entries?: Record>; + allow?: string[]; + deny?: string[]; + [k: string]: unknown; +} +interface OpenClawSettingsFile { + plugins?: OpenClawPluginsSection; + [k: string]: unknown; +} -interface GeminiHookMatcher { - matcher?: string; - hooks?: Array>; +/** Absolute path to the failproofai-shipped OpenClaw plugin package. */ +function getOpenClawPluginPath(): string { + const fromEnv = process.env.FAILPROOFAI_PACKAGE_ROOT; + if (fromEnv) return resolve(fromEnv, "openclaw-plugin"); + return resolve(fileURLToPath(import.meta.url), "..", "..", "..", "openclaw-plugin"); } -interface GeminiSettingsFile { - hooks?: Record; - [key: string]: unknown; +/** True iff a plugins.load.paths entry was written by failproofai. */ +function isFailproofaiOpenClawPath(p: unknown): boolean { + if (typeof p !== "string") return false; + return /(?:^|\/)openclaw-plugin\/?$/.test(p) || (p.includes("openclaw-plugin") && p.includes("failproofai")); } -export const gemini: Integration = { - id: "gemini", - displayName: "Gemini CLI", - scopes: GEMINI_HOOK_SCOPES, - eventTypes: GEMINI_HOOK_EVENT_TYPES, +export const openclaw: Integration = { + id: "openclaw", + displayName: "OpenClaw", + scopes: OPENCLAW_HOOK_SCOPES, + eventTypes: OPENCLAW_HOOK_EVENT_TYPES, + + // USER scope only (~/.openclaw/openclaw.json); OpenClaw has no project config. + getSettingsPath() { + return resolve(homedir(), ".openclaw", "openclaw.json"); + }, + + readSettings(settingsPath) { + return readJsonFile(settingsPath); + }, + + writeSettings(settingsPath, settings) { + writeJsonFile(settingsPath, settings); + }, + + buildHookEntry() { + // OpenClaw registers plugins at the package level — one registration covers + // all hooks (the plugin's index.js wires every api.on(...) handler). This + // sentinel satisfies the interface; writeHookEntries does the real work. + return { + [FAILPROOFAI_HOOK_MARKER]: true, + _openclawPluginPath: getOpenClawPluginPath(), + }; + }, + + isFailproofaiHook(hook) { + if (typeof hook === "string") return isFailproofaiOpenClawPath(hook); + if (!hook || typeof hook !== "object") return false; + const h = hook as Record; + if (h[FAILPROOFAI_HOOK_MARKER] === true) return true; + if (typeof h.source === "string") return isFailproofaiOpenClawPath(h.source); + return false; + }, + + writeHookEntries(settings) { + const s = settings as OpenClawSettingsFile; + const plugins = (s.plugins ??= {}); + const load = (plugins.load ??= {}); + if (!Array.isArray(load.paths)) load.paths = []; + const dir = getOpenClawPluginPath(); + // Idempotent: replace any existing failproofai path, otherwise append. + const idx = load.paths.findIndex((p) => isFailproofaiOpenClawPath(p)); + if (idx >= 0) load.paths[idx] = dir; + else load.paths.push(dir); + + // Enable the plugin + grant conversation access (needed for before_agent_run + // / before_agent_finalize). Only set our own keys — operator config on this + // entry (e.g. hooks.timeouts.) and other entries are preserved. + const entries = (plugins.entries ??= {}); + const entry = (entries[OPENCLAW_PLUGIN_ID] ??= {}); + entry.enabled = true; + const hooks = ((entry.hooks as Record) ??= {}); + hooks.allowConversationAccess = true; + }, + + removeHooksFromFile(settingsPath) { + if (!existsSync(settingsPath)) return 0; + const settings = this.readSettings(settingsPath) as OpenClawSettingsFile; + const plugins = settings.plugins; + if (!plugins) return 0; + let removed = 0; + + if (Array.isArray(plugins.load?.paths)) { + const before = plugins.load.paths.length; + plugins.load.paths = plugins.load.paths.filter((p) => !isFailproofaiOpenClawPath(p)); + removed += before - plugins.load.paths.length; + if (plugins.load.paths.length === 0) delete plugins.load.paths; + if (plugins.load && Object.keys(plugins.load).length === 0) delete plugins.load; + } + if (plugins.entries && OPENCLAW_PLUGIN_ID in plugins.entries) { + delete plugins.entries[OPENCLAW_PLUGIN_ID]; + removed += 1; + if (Object.keys(plugins.entries).length === 0) delete plugins.entries; + } + // Prune an empty plugins object so uninstall leaves openclaw.json clean. + if (Object.keys(plugins).length === 0) delete settings.plugins; + + this.writeSettings(settingsPath, settings as Record); + return removed; + }, + + hooksInstalledInSettings(scope, cwd) { + const settingsPath = this.getSettingsPath(scope, cwd); + if (!existsSync(settingsPath)) return false; + try { + const settings = this.readSettings(settingsPath) as OpenClawSettingsFile; + const paths = settings.plugins?.load?.paths; + const pathPresent = Array.isArray(paths) && paths.some((p) => isFailproofaiOpenClawPath(p)); + const entryEnabled = settings.plugins?.entries?.[OPENCLAW_PLUGIN_ID]?.enabled === true; + return pathPresent && entryEnabled; + } catch { + return false; + } + }, + + detectInstalled() { + return binaryExists("openclaw"); + }, +}; + +// ── Factory Droid (droid) integration ─────────────────────────────────────── +// +// Factory's droid CLI uses a Claude-compatible external-command hook system, +// but its hooks.json puts event names at the TOP LEVEL — there is NO `"hooks"` +// wrapper (the file IS the events object). Verified live against droid +// v0.171.0: a `{"hooks":{…}}` wrapper is rejected with `WARN Ignoring unknown +// hook event keys keys:["hooks"]`. Tool events (PreToolUse / PostToolUse) carry +// `"matcher": "*"`; non-tool events omit the matcher. Deny is exit-2 + stderr +// (see policy-evaluator.ts). Event names are already PascalCase, so no event +// canonicalization is needed. Settings paths: ~/.factory/hooks.json (user) and +// /.factory/hooks.json (project); no "local" scope. + +interface FactoryHookMatcher { + matcher?: string; + hooks: Array>; +} + +/** The Factory settings file IS the top-level events map (no wrapper key). */ +type FactorySettingsFile = Record; + +export const factory: Integration = { + id: "factory", + displayName: "Factory Droid", + scopes: FACTORY_HOOK_SCOPES, + eventTypes: FACTORY_HOOK_EVENT_TYPES, + + getSettingsPath(scope, cwd) { + const base = cwd ? resolve(cwd) : process.cwd(); + switch (scope) { + case "user": + return resolve(homedir(), ".factory", "hooks.json"); + case "project": + return resolve(base, ".factory", "hooks.json"); + case "local": + // Factory has no "local" scope; the CLI rejects --cli factory --scope + // local before reaching here, but fall back to project so callers don't + // crash. + return resolve(base, ".factory", "hooks.json"); + } + }, + + readSettings(settingsPath) { + return readJsonFile(settingsPath); + }, + + writeSettings(settingsPath, settings) { + writeJsonFile(settingsPath, settings); + }, + + buildHookEntry(binaryPath, eventType, scope) { + const command = + scope === "project" + ? `npx -y failproofai --hook ${eventType} --cli factory` + : `"${binaryPath}" --hook ${eventType} --cli factory`; + return { + type: "command", + command, + // droid reads `timeout` in SECONDS (verified against droid v0.171.0). 30s. + timeout: 30, + [FAILPROOFAI_HOOK_MARKER]: true, + }; + }, + + isFailproofaiHook: isMarkedHook, + + writeHookEntries(settings, binaryPath, scope) { + const s = settings as Record; + + for (const eventType of FACTORY_HOOK_EVENT_TYPES) { + const hookEntry = this.buildHookEntry(binaryPath, eventType, scope) as unknown as ClaudeHookEntry; + if (!Array.isArray(s[eventType])) s[eventType] = []; + const matchers: FactoryHookMatcher[] = s[eventType]; + + let found = false; + for (const matcher of matchers) { + if (!matcher.hooks) continue; + const idx = matcher.hooks.findIndex((h) => isMarkedHook(h as Record)); + if (idx >= 0) { + matcher.hooks[idx] = hookEntry; + found = true; + break; + } + } + if (!found) { + // Tool events match all tools via `matcher: "*"`; non-tool events carry + // no matcher (verified live against droid v0.171.0). + const isToolEvent = eventType === "PreToolUse" || eventType === "PostToolUse"; + matchers.push(isToolEvent ? { matcher: "*", hooks: [hookEntry] } : { hooks: [hookEntry] }); + } + } + }, + + removeHooksFromFile(settingsPath) { + const settings = this.readSettings(settingsPath) as FactorySettingsFile; + + let removed = 0; + for (const eventType of Object.keys(settings)) { + const matchers = settings[eventType]; + if (!Array.isArray(matchers)) continue; + for (let i = matchers.length - 1; i >= 0; i--) { + const matcher = matchers[i] as FactoryHookMatcher; + if (!matcher || !matcher.hooks) continue; + const before = matcher.hooks.length; + matcher.hooks = matcher.hooks.filter((h) => !isMarkedHook(h as Record)); + removed += before - matcher.hooks.length; + if (matcher.hooks.length === 0) matchers.splice(i, 1); + } + if (matchers.length === 0) delete settings[eventType]; + } + + this.writeSettings(settingsPath, settings as Record); + return removed; + }, + + hooksInstalledInSettings(scope, cwd) { + const settingsPath = this.getSettingsPath(scope, cwd); + if (!existsSync(settingsPath)) return false; + try { + const settings = this.readSettings(settingsPath) as FactorySettingsFile; + for (const matchers of Object.values(settings)) { + if (!Array.isArray(matchers)) continue; + for (const matcher of matchers as FactoryHookMatcher[]) { + if (!matcher || !matcher.hooks) continue; + if (matcher.hooks.some((h) => isMarkedHook(h as Record))) return true; + } + } + } catch { + // Corrupt settings — treat as not installed + } + return false; + }, + + detectInstalled() { + return binaryExists("droid"); + }, +}; + +// ── Devin CLI (devin) integration ──────────────────────────────────────────── +// +// Devin's CLI (Cognition) is a **pure Claude-clone** external-command hook +// system, verified live against devin v3000.1.27. It uses the standard Claude +// `"hooks"`-wrapper schema, so this Integration mirrors `claudeCode` verbatim — +// only the settings paths and the `--cli devin` command flag differ. The +// config file also carries the operator's own keys (`org_id`, `theme_mode`, +// …), so readSettings/writeSettings use the merge-preserving JSON helpers. +// +// Settings paths (verified against devin v3000.1.27): +// user → ~/.config/devin/config.json (the `"hooks"` key) +// project → /.devin/config.json (the `"hooks"` key) +// No "local" scope. Event names are already PascalCase (no event map / no +// handler branch); the stdin payload is Claude snake_case (no normalization). +// Deny/instruct semantics live in policy-evaluator.ts's `cli === "devin"` +// branch (`{decision:"block", reason}` on stdout at exit 0). + +export const devin: Integration = { + id: "devin", + displayName: "Devin CLI", + scopes: DEVIN_HOOK_SCOPES, + eventTypes: DEVIN_HOOK_EVENT_TYPES, getSettingsPath(scope, cwd) { const base = cwd ? resolve(cwd) : process.cwd(); switch (scope) { case "user": - return resolve(homedir(), ".gemini", "settings.json"); + return resolve(homedir(), ".config", "devin", "config.json"); case "project": - return resolve(base, ".gemini", "settings.json"); + return resolve(base, ".devin", "config.json"); case "local": - // Gemini has no "local" scope; CLI rejects --cli gemini --scope local + // Devin has no "local" scope; the CLI rejects --cli devin --scope local // before reaching here, but fall back to project so callers don't crash. - return resolve(base, ".gemini", "settings.json"); + return resolve(base, ".devin", "config.json"); } }, @@ -1355,12 +1757,13 @@ export const gemini: Integration = { buildHookEntry(binaryPath, eventType, scope) { const command = scope === "project" - ? `npx -y failproofai --hook ${eventType} --cli gemini` - : `"${binaryPath}" --hook ${eventType} --cli gemini`; + ? `npx -y failproofai --hook ${eventType} --cli devin` + : `"${binaryPath}" --hook ${eventType} --cli devin`; return { type: "command", command, - timeout: 60_000, + // Devin reads `timeout` in SECONDS like Claude. 60 = 60s. + timeout: 60, [FAILPROOFAI_HOOK_MARKER]: true, }; }, @@ -1368,19 +1771,14 @@ export const gemini: Integration = { isFailproofaiHook: isMarkedHook, writeHookEntries(settings, binaryPath, scope) { - const s = settings as GeminiSettingsFile; + const s = settings as ClaudeSettings; if (!s.hooks) s.hooks = {}; - for (const eventType of GEMINI_HOOK_EVENT_TYPES) { + for (const eventType of DEVIN_HOOK_EVENT_TYPES) { const hookEntry = this.buildHookEntry(binaryPath, eventType, scope) as unknown as ClaudeHookEntry; if (!s.hooks[eventType]) s.hooks[eventType] = []; - const matchers: GeminiHookMatcher[] = s.hooks[eventType]; + const matchers: ClaudeHookMatcher[] = s.hooks[eventType]; - // Idempotent: replace an existing failproofai-marked entry inside our - // own matcher; otherwise append a new `{matcher: "*", hooks: [...]}`. - // Hand-written matchers (with their own `matcher` regex) are never - // touched — we identify our matcher by checking whether ANY of its - // inner hooks are failproofai-marked. let found = false; for (const matcher of matchers) { if (!matcher.hooks) continue; @@ -1391,12 +1789,12 @@ export const gemini: Integration = { break; } } - if (!found) matchers.push({ matcher: "*", hooks: [hookEntry] }); + if (!found) matchers.push({ hooks: [hookEntry] }); } }, removeHooksFromFile(settingsPath) { - const settings = this.readSettings(settingsPath) as GeminiSettingsFile; + const settings = this.readSettings(settingsPath) as ClaudeSettings; if (!settings.hooks) return 0; let removed = 0; @@ -1423,7 +1821,7 @@ export const gemini: Integration = { const settingsPath = this.getSettingsPath(scope, cwd); if (!existsSync(settingsPath)) return false; try { - const settings = this.readSettings(settingsPath) as GeminiSettingsFile; + const settings = this.readSettings(settingsPath) as ClaudeSettings; if (!settings.hooks) return false; for (const matchers of Object.values(settings.hooks)) { if (!Array.isArray(matchers)) continue; @@ -1439,58 +1837,77 @@ export const gemini: Integration = { }, detectInstalled() { - return binaryExists("gemini"); + return binaryExists("devin"); }, }; -// ── Hermes (hermes-agent) — live hooks (Pillar 1) ──────────────────────────── +// ── Antigravity CLI (antigravity) integration ──────────────────────────────── // -// External-command CLI like codex/cursor, but its config is YAML -// (`~/.hermes/config.yaml`) under a `hooks:` map, so the I/O layer uses the yaml -// Document API (comment-preserving). Flat per-event arrays like cursor: -// `hooks: { pre_tool_call: [ { command, timeout, } ], … }`. Hermes -// reads a `{"decision":"block",…}` JSON response on stdout (see -// policy-evaluator.ts); exit codes are ignored. User-scope only. - -/** One hook entry as stored under a `hooks:` event key in config.yaml. */ -interface HermesHookEntry { - command: string; - timeout?: number; - [key: string]: unknown; +// Antigravity's `agy` CLI uses a NAMED-hook schema: the hooks.json top-level key +// is a hook *name* ("failproofai"), whose value is an event→handlers map. Tool +// events (PreToolUse / PostToolUse) wrap handlers in `{matcher, hooks:[…]}`; +// non-tool events (PreInvocation / Stop) are FLAT arrays of handler objects. +// Verified live against agy v1.1.2. Settings paths: +// user → ~/.gemini/config/hooks.json +// project → /.agents/hooks.json +// No "local" scope. Deny/instruct semantics live in policy-evaluator.ts's +// `cli === "antigravity"` branch (Antigravity's OWN `{decision:"deny"}` / +// `{decision:"continue"}` / `{injectSteps}` shapes — NOT Claude's). + +/** The Antigravity settings file is `{ "": { : … } }`. Our + * named hook is "failproofai". */ +const ANTIGRAVITY_HOOK_NAME = "failproofai"; + +interface AntigravityToolMatcher { + matcher?: string; + hooks: Array>; } +/** An Antigravity named-hook body: tool events → `{matcher, hooks}[]`; flat + * events (PreInvocation / Stop) → `handler[]`. */ +type AntigravityNamedHook = Record< + string, + AntigravityToolMatcher[] | Array> +>; -export const hermes: Integration = { - id: "hermes", - displayName: "Hermes", - scopes: HERMES_HOOK_SCOPES, - eventTypes: HERMES_HOOK_EVENT_TYPES, +const ANTIGRAVITY_TOOL_EVENTS = new Set(["PreToolUse", "PostToolUse"]); - // Hermes config is USER-scope only (`~/.hermes/config.yaml`); there is no - // project/local file, so scope/cwd are irrelevant here. - getSettingsPath() { - return resolve(homedir(), ".hermes", "config.yaml"); +export const antigravity: Integration = { + id: "antigravity", + displayName: "Antigravity CLI", + scopes: ANTIGRAVITY_HOOK_SCOPES, + eventTypes: ANTIGRAVITY_HOOK_EVENT_TYPES, + + getSettingsPath(scope, cwd) { + const base = cwd ? resolve(cwd) : process.cwd(); + switch (scope) { + case "user": + return resolve(homedir(), ".gemini", "config", "hooks.json"); + case "project": + return resolve(base, ".agents", "hooks.json"); + case "local": + // Antigravity has no "local" scope; the CLI rejects it before reaching + // here, but fall back to project so callers don't crash. + return resolve(base, ".agents", "hooks.json"); + } }, readSettings(settingsPath) { - // Return the yaml Document (cast) so writeHookEntries/writeSettings can - // round-trip it while preserving the user's other keys + comments. - return readYamlDoc(settingsPath) as unknown as Record; + return readJsonFile(settingsPath); }, writeSettings(settingsPath, settings) { - writeYamlDoc(settingsPath, settings as unknown as Document); + writeJsonFile(settingsPath, settings); }, buildHookEntry(binaryPath, eventType, scope) { - // No matcher → fires for ALL tools / all platforms (slack/telegram/cli/cron) - // and internal subagents. `timeout` is in seconds; Hermes runs the command - // via shlex.split (shell=false). const command = scope === "project" - ? `npx -y failproofai --hook ${eventType} --cli hermes` - : `"${binaryPath}" --hook ${eventType} --cli hermes`; + ? `npx -y failproofai --hook ${eventType} --cli antigravity` + : `"${binaryPath}" --hook ${eventType} --cli antigravity`; return { + type: "command", command, + // Antigravity reads `timeout` in SECONDS (verified agy v1.1.2). 30s. timeout: 30, [FAILPROOFAI_HOOK_MARKER]: true, }; @@ -1499,57 +1916,235 @@ export const hermes: Integration = { isFailproofaiHook: isMarkedHook, writeHookEntries(settings, binaryPath, scope) { - const doc = settings as unknown as Document; - // Read the current hooks map as plain JS, then re-set ONLY the `hooks` key — - // preserving comments on every other part of config.yaml. - const js = (doc.toJS() ?? {}) as { hooks?: Record }; - const hooks: Record = - js.hooks && typeof js.hooks === "object" ? js.hooks : {}; + const s = settings as Record; + if (!s[ANTIGRAVITY_HOOK_NAME] || typeof s[ANTIGRAVITY_HOOK_NAME] !== "object") { + s[ANTIGRAVITY_HOOK_NAME] = {}; + } + const named = s[ANTIGRAVITY_HOOK_NAME] as AntigravityNamedHook; - for (const eventType of HERMES_HOOK_EVENT_TYPES) { - const entry = this.buildHookEntry(binaryPath, eventType, scope) as unknown as HermesHookEntry; - const arr = Array.isArray(hooks[eventType]) ? hooks[eventType] : []; - const idx = arr.findIndex((h) => isMarkedHook(h)); - if (idx >= 0) arr[idx] = entry; - else arr.push(entry); - hooks[eventType] = arr; + for (const eventType of ANTIGRAVITY_HOOK_EVENT_TYPES) { + const hookEntry = this.buildHookEntry(binaryPath, eventType, scope) as unknown as ClaudeHookEntry; + const isToolEvent = ANTIGRAVITY_TOOL_EVENTS.has(eventType); + + if (isToolEvent) { + if (!Array.isArray(named[eventType])) named[eventType] = [] as AntigravityToolMatcher[]; + const matchers = named[eventType] as AntigravityToolMatcher[]; + let found = false; + for (const matcher of matchers) { + if (!matcher.hooks) continue; + const idx = matcher.hooks.findIndex((h) => isMarkedHook(h as Record)); + if (idx >= 0) { + matcher.hooks[idx] = hookEntry; + found = true; + break; + } + } + if (!found) matchers.push({ matcher: "*", hooks: [hookEntry] }); + } else { + // Flat array of handler objects (PreInvocation / Stop). + if (!Array.isArray(named[eventType])) named[eventType] = [] as Array>; + const handlers = named[eventType] as Array>; + const idx = handlers.findIndex((h) => isMarkedHook(h as Record)); + if (idx >= 0) handlers[idx] = hookEntry; + else handlers.push(hookEntry); + } } - doc.set("hooks", hooks); - // The headless gateway has no TTY to answer Hermes's first-use hook-consent - // prompt, so auto-accept declared hooks. Tradeoff: also auto-accepts any - // other hook the operator adds; a targeted `shell-hooks-allowlist.json` - // pre-seed is a future refinement. - doc.set("hooks_auto_accept", true); }, removeHooksFromFile(settingsPath) { - if (!existsSync(settingsPath)) return 0; - const doc = readYamlDoc(settingsPath); - const js = (doc.toJS() ?? {}) as { hooks?: Record }; - const hooks = js.hooks; + const settings = this.readSettings(settingsPath) as Record; + const named = settings[ANTIGRAVITY_HOOK_NAME]; + if (!named || typeof named !== "object") { + this.writeSettings(settingsPath, settings); + return 0; + } + const namedHook = named as AntigravityNamedHook; let removed = 0; - if (hooks && typeof hooks === "object") { - for (const eventType of Object.keys(hooks)) { - const entries = hooks[eventType]; - if (!Array.isArray(entries)) continue; - const before = entries.length; - const filtered = entries.filter((h) => !isMarkedHook(h)); + for (const eventType of Object.keys(namedHook)) { + const value = namedHook[eventType]; + if (!Array.isArray(value)) continue; + if (ANTIGRAVITY_TOOL_EVENTS.has(eventType)) { + const matchers = value as AntigravityToolMatcher[]; + for (let i = matchers.length - 1; i >= 0; i--) { + const matcher = matchers[i]; + if (!matcher || !matcher.hooks) continue; + const before = matcher.hooks.length; + matcher.hooks = matcher.hooks.filter((h) => !isMarkedHook(h as Record)); + removed += before - matcher.hooks.length; + if (matcher.hooks.length === 0) matchers.splice(i, 1); + } + if (matchers.length === 0) delete namedHook[eventType]; + } else { + const handlers = value as Array>; + const before = handlers.length; + const filtered = handlers.filter((h) => !isMarkedHook(h)); removed += before - filtered.length; - if (filtered.length === 0) delete hooks[eventType]; - else hooks[eventType] = filtered; + if (filtered.length === 0) delete namedHook[eventType]; + else namedHook[eventType] = filtered as Array>; } - if (removed > 0) { - if (Object.keys(hooks).length === 0) doc.delete("hooks"); - else doc.set("hooks", hooks); + } + // Drop the named hook entirely if it has no remaining events. + if (Object.keys(namedHook).length === 0) delete settings[ANTIGRAVITY_HOOK_NAME]; + + this.writeSettings(settingsPath, settings); + return removed; + }, + + hooksInstalledInSettings(scope, cwd) { + const settingsPath = this.getSettingsPath(scope, cwd); + if (!existsSync(settingsPath)) return false; + try { + const settings = this.readSettings(settingsPath) as Record; + const named = settings[ANTIGRAVITY_HOOK_NAME]; + if (!named || typeof named !== "object") return false; + for (const value of Object.values(named as AntigravityNamedHook)) { + if (!Array.isArray(value)) continue; + for (const item of value) { + if (isMarkedHook(item as Record)) return true; + const matcher = item as AntigravityToolMatcher; + if (matcher && Array.isArray(matcher.hooks) && matcher.hooks.some((h) => isMarkedHook(h as Record))) { + return true; + } + } } + } catch { + // Corrupt settings — treat as not installed } + return false; + }, - // Always drop our headless-consent flag on uninstall — even if the hooks were - // already removed manually — so it can't silently auto-accept future operator - // hooks. `doc.delete` returns true iff the key was present. - const droppedAutoAccept = doc.delete("hooks_auto_accept"); - if (removed > 0 || droppedAutoAccept) writeYamlDoc(settingsPath, doc); + detectInstalled() { + return binaryExists("agy"); + }, +}; + +// ── Goose (codename goose, Block) integration ──────────────────────────────── +// +// Goose's "hooks" system follows the cross-agent Open Plugins spec: a plugin +// directory whose hooks/hooks.json wires shell commands into agent events. +// failproofai owns the entire `failproofai` plugin dir and writes an Open +// Plugins hooks.json — `{"hooks": {: [{"hooks": [{type, command}]}]}}`. +// Two differences from Factory (verified live against goose v1.43.0): +// 1. the schema DOES carry the top-level "hooks" wrapper (like Claude), and +// 2. the matcher is OMITTED on every event — a bare "*" is an invalid regex +// that matches NOTHING (omitted = match all tools). +// Entries are the clean `{type, command}` shape with NO marker field — Goose +// parses this file, so we identify our hooks by the `--cli goose` command +// substring instead of injecting `__failproofai_hook__`. Goose auto-discovers +// the dir at startup (no config edit) and self-registers it into +// ~/.config/goose/config.yaml; uninstall clears our entries (the emptied +// hooks.json and the auto-added config `plugins:` entry are left for Goose to +// reconcile on next start — it logs the empty plugin and continues, harmless). +// Deny is PreToolUse-only JSON (see policy-evaluator.ts). Settings paths: +// ~/.agents/plugins/failproofai/hooks/hooks.json (user) and +// /.agents/plugins/failproofai/hooks/hooks.json (project); no "local" scope. + +interface GooseHookMatcher { + matcher?: string; + hooks: Array>; +} +/** The Open Plugins hooks file: { "hooks": { : GooseHookMatcher[] } }. */ +interface GooseHooksFile { + hooks?: Record; + [key: string]: unknown; +} + +/** failproofai owns the `failproofai` Goose plugin dir; identify our hook + * entries by the `--cli goose` command substring (the file is Goose-parsed, so + * entries stay the clean {type, command} shape with no marker field). */ +function isGooseFailproofaiHook(hook: unknown): boolean { + if (!hook || typeof hook !== "object") return false; + const cmd = (hook as { command?: unknown }).command; + return typeof cmd === "string" && cmd.includes("failproofai") && cmd.includes("--cli goose"); +} + +export const goose: Integration = { + id: "goose", + displayName: "Goose", + scopes: GOOSE_HOOK_SCOPES, + eventTypes: GOOSE_HOOK_EVENT_TYPES, + + getSettingsPath(scope, cwd) { + const base = cwd ? resolve(cwd) : process.cwd(); + switch (scope) { + case "user": + return resolve(homedir(), ".agents", "plugins", "failproofai", "hooks", "hooks.json"); + case "project": + return resolve(base, ".agents", "plugins", "failproofai", "hooks", "hooks.json"); + case "local": + // Goose has no "local" scope; the CLI rejects --scope local before + // reaching here, but fall back to project so callers don't crash. + return resolve(base, ".agents", "plugins", "failproofai", "hooks", "hooks.json"); + } + }, + + readSettings(settingsPath) { + return readJsonFile(settingsPath); + }, + + writeSettings(settingsPath, settings) { + writeJsonFile(settingsPath, settings); + }, + + buildHookEntry(binaryPath, eventType, scope) { + const command = + scope === "project" + ? `npx -y failproofai --hook ${eventType} --cli goose` + : `"${binaryPath}" --hook ${eventType} --cli goose`; + // Open Plugins command entry: { type, command } only (Goose applies its own + // timeout; no marker field — see isGooseFailproofaiHook). + return { type: "command", command }; + }, + + isFailproofaiHook: isGooseFailproofaiHook, + + writeHookEntries(settings, binaryPath, scope) { + const s = settings as GooseHooksFile; + if (!s.hooks) s.hooks = {}; + + for (const eventType of GOOSE_HOOK_EVENT_TYPES) { + const hookEntry = this.buildHookEntry(binaryPath, eventType, scope); + if (!Array.isArray(s.hooks[eventType])) s.hooks[eventType] = []; + const matchers: GooseHookMatcher[] = s.hooks[eventType]; + + let found = false; + for (const matcher of matchers) { + if (!matcher.hooks) continue; + const idx = matcher.hooks.findIndex((h) => isGooseFailproofaiHook(h)); + if (idx >= 0) { + matcher.hooks[idx] = hookEntry; + found = true; + break; + } + } + // matcher OMITTED on every event (a bare "*" matches nothing; omitted = + // match all tools — verified live against goose v1.43.0). + if (!found) matchers.push({ hooks: [hookEntry] }); + } + }, + + removeHooksFromFile(settingsPath) { + const settings = this.readSettings(settingsPath) as GooseHooksFile; + if (!settings.hooks) return 0; + + let removed = 0; + for (const eventType of Object.keys(settings.hooks)) { + const matchers = settings.hooks[eventType]; + if (!Array.isArray(matchers)) continue; + for (let i = matchers.length - 1; i >= 0; i--) { + const matcher = matchers[i]; + if (!matcher || !matcher.hooks) continue; + const before = matcher.hooks.length; + matcher.hooks = matcher.hooks.filter((h) => !isGooseFailproofaiHook(h)); + removed += before - matcher.hooks.length; + if (matcher.hooks.length === 0) matchers.splice(i, 1); + } + if (matchers.length === 0) delete settings.hooks[eventType]; + } + if (Object.keys(settings.hooks).length === 0) delete settings.hooks; + + this.writeSettings(settingsPath, settings as Record); return removed; }, @@ -1557,21 +2152,23 @@ export const hermes: Integration = { const settingsPath = this.getSettingsPath(scope, cwd); if (!existsSync(settingsPath)) return false; try { - const doc = readYamlDoc(settingsPath); - const js = (doc.toJS() ?? {}) as { hooks?: Record }; - const hooks = js.hooks; - if (!hooks || typeof hooks !== "object") return false; - for (const entries of Object.values(hooks)) { - if (Array.isArray(entries) && entries.some((h) => isMarkedHook(h))) return true; + const settings = this.readSettings(settingsPath) as GooseHooksFile; + if (!settings.hooks) return false; + for (const matchers of Object.values(settings.hooks)) { + if (!Array.isArray(matchers)) continue; + for (const matcher of matchers) { + if (!matcher || !matcher.hooks) continue; + if (matcher.hooks.some((h) => isGooseFailproofaiHook(h))) return true; + } } } catch { - // Corrupt config — treat as not installed. + // Corrupt settings — treat as not installed } return false; }, detectInstalled() { - return binaryExists("hermes"); + return binaryExists("goose"); }, }; @@ -1589,8 +2186,12 @@ const INTEGRATIONS: Partial> = { cursor, opencode, pi, - gemini, hermes, + openclaw, + factory, + devin, + antigravity, + goose, }; export function getIntegration(id: IntegrationType): Integration { diff --git a/src/hooks/policy-evaluator.ts b/src/hooks/policy-evaluator.ts index c5064d81..6e6a511b 100644 --- a/src/hooks/policy-evaluator.ts +++ b/src/hooks/policy-evaluator.ts @@ -164,8 +164,7 @@ export async function evaluatePolicies( // — that shape is only honored on tool events. The only force-retry // channel for Stop/SubagentStop is `{followup_message}` on stdout // (exit 0); Cursor auto-submits the text as the next user message - // (capped at `loop_limit`, default 5). Mirrors the Copilot Stop - // branch at line ~279 and the Gemini AfterAgent branch at line ~188. + // (capped at `loop_limit`, default 5). Mirrors the Copilot Stop branch. // Without this branch, the 5 `require-*-before-stop` builtins were // observation-only on Cursor — the deny was logged but the agent // stopped cleanly. Ref: https://cursor.com/docs/hooks @@ -202,7 +201,7 @@ export async function evaluatePolicies( // same flat shape — except Stop, where we emit the MANDATORY ACTION // wording so the shim can re-inject it as a system-prompt suffix on // the next before_agent_start (Pi cannot veto agent_end directly). - // Mirrors the Cursor/Gemini/Copilot/OpenCode Stop branches above. + // Mirrors the Cursor/Copilot/OpenCode Stop branches above. if (session?.cli === "pi") { if (eventType === "Stop") { const reasonText = `MANDATORY ACTION REQUIRED from failproofai (policy: ${policy.name}): ${reason}\n\nYou MUST complete the above action NOW. Do NOT ask the user for confirmation — execute the required action, then attempt to finish your task again.`; @@ -229,33 +228,6 @@ export async function evaluatePolicies( }; } - // Gemini CLI: flat `{decision: "deny", reason}` for non-Stop events - // (preferred per Gemini's "Golden Rule" — exit 0 with structured JSON). - // For Stop (AfterAgent), use `{decision: "block", reason}` to force-retry, - // mirroring Claude's exit-2-from-Stop "do this before stopping" semantics. - // Ref: https://geminicli.com/docs/hooks/ - if (session?.cli === "gemini") { - if (eventType === "Stop") { - const reasonText = `MANDATORY ACTION REQUIRED from failproofai (policy: ${policy.name}): ${reason}\n\nYou MUST complete the above action NOW. Do NOT ask the user for confirmation — execute the required action, then attempt to finish your task again.`; - return { - exitCode: 0, - stdout: JSON.stringify({ decision: "block", reason: reasonText }), - stderr: "", - policyName: policy.name, - reason, - decision: "deny", - }; - } - return { - exitCode: 0, - stdout: JSON.stringify({ decision: "deny", reason: blockedMessage }), - stderr: "", - policyName: policy.name, - reason, - decision: "deny", - }; - } - // Hermes: the block contract is `{"decision":"block","reason"}` on stdout; // Hermes IGNORES exit codes, so the JSON is the only channel. This one // return covers every Hermes event we install (PreToolUse / PostToolUse / @@ -275,6 +247,36 @@ export async function evaluatePolicies( }; } + // OpenClaw: the shipped openclaw-plugin parses a flat {permission, reason} + // verdict and maps it per plugin-hook — before_tool_call → {block:true, + // blockReason}; before_agent_run → {outcome:"block", reason}; + // before_agent_finalize (Stop) → {action:"revise", reason}. For Stop we + // emit the MANDATORY ACTION wording so the revise loop carries the + // directive. Observation hooks (after_tool_call / session_* / + // subagent_ended / before_compaction) ignore stdout, so the flat deny is + // logged but cannot veto — a documented limitation. Mirrors the Pi branch. + if (session?.cli === "openclaw") { + if (eventType === "Stop") { + const reasonText = `MANDATORY ACTION REQUIRED from failproofai (policy: ${policy.name}): ${reason}\n\nYou MUST complete the above action NOW. Do NOT ask the user for confirmation — execute the required action, then attempt to finish your task again.`; + return { + exitCode: 0, + stdout: JSON.stringify({ permission: "deny", reason: reasonText }), + stderr: "", + policyName: policy.name, + reason, + decision: "deny", + }; + } + return { + exitCode: 0, + stdout: JSON.stringify({ permission: "deny", reason: blockedMessage }), + stderr: "", + policyName: policy.name, + reason, + decision: "deny", + }; + } + // OpenCode: `session.idle` is a notification-only bus event — by the // time the plugin handler fires, OpenCode has already gone idle and // throwing from the handler does not force-retry. The only working @@ -283,8 +285,8 @@ export async function evaluatePolicies( // shim already routes `hookSpecificOutput.additionalContext` through // that path (see buildOpenCodePluginShim's applyDecision), so we emit // the deny reason as additionalContext instead of exit-2. Mirrors the - // Cursor `followup_message` (line ~157) and Copilot `{decision:"block"}` - // (line ~299) Stop branches. SubagentStop is widened in for forward + // Cursor `followup_message` and Copilot `{decision:"block"}` Stop + // branches. SubagentStop is widened in for forward // compat — OpenCode doesn't yet expose subagent boundaries to plugins. if (session?.cli === "opencode") { if (eventType === "Stop" || eventType === "SubagentStop") { @@ -303,6 +305,105 @@ export async function evaluatePolicies( // for tool events. } + // Factory droid: droid drives tool blocking off EXIT CODE 2 (it ignores a + // JSON `{decision:…}` on tool events — verified live against droid + // v0.171.0: `Hook returned exit code 2, throwing ToolExecutionControlError`). + // The one exception is `Stop`, where droid does NOT honor exit-2 + // force-retry; there it reads `{decision:"block", reason}` on stdout at + // exit 0 ("if decision is block, Droid does not stop"). So: Stop → JSON + // block; every other event (PreToolUse / PostToolUse / UserPromptSubmit / + // SubagentStop / …) → exit 2 + the blocked message on stderr. + if (session?.cli === "factory") { + if (eventType === "Stop") { + const reasonText = `MANDATORY ACTION REQUIRED from failproofai (policy: ${policy.name}): ${reason}\n\nYou MUST complete the above action NOW. Do NOT ask the user for confirmation — execute the required action, then attempt to finish your task again.`; + return { + exitCode: 0, + stdout: JSON.stringify({ decision: "block", reason: reasonText }), + stderr: "", + policyName: policy.name, + reason, + decision: "deny", + }; + } + return { + exitCode: 2, + stdout: "", + stderr: blockedMessage + "\n", + policyName: policy.name, + reason, + decision: "deny", + }; + } + + // Devin CLI: a pure Claude-clone that honors `{decision:"block", reason}` + // on stdout at exit 0 for EVERY event (verified live against devin + // v3000.1.27 — the block overrode `--permission-mode dangerous`). On Stop + // the reason carries the MANDATORY-ACTION force-retry wording; on other + // events it's the plain blocked message. One branch covers all events. + if (session?.cli === "devin") { + const reasonText = + eventType === "Stop" + ? `MANDATORY ACTION REQUIRED from failproofai (policy: ${policy.name}): ${reason}\n\nYou MUST complete the above action NOW. Do NOT ask the user for confirmation — execute the required action, then attempt to finish your task again.` + : blockedMessage; + return { + exitCode: 0, + stdout: JSON.stringify({ decision: "block", reason: reasonText }), + stderr: "", + policyName: policy.name, + reason, + decision: "deny", + }; + } + + // Antigravity CLI: its OWN response shapes (NOT Claude's) — verified live + // against agy v1.1.2. Tool/prompt events honor `{decision:"deny", reason}` + // on stdout at exit 0 (hard block). The Stop event has no exit-2 retry; + // instead `{decision:"continue", reason}` re-enters the loop and injects + // the reason as a system message — that is how the 5 require-*-before-stop + // builtins enforce on Antigravity. + if (session?.cli === "antigravity") { + if (eventType === "Stop") { + const reasonText = `MANDATORY ACTION REQUIRED from failproofai (policy: ${policy.name}): ${reason}\n\nYou MUST complete the above action NOW. Do NOT ask the user for confirmation — execute the required action, then attempt to finish your task again.`; + return { + exitCode: 0, + stdout: JSON.stringify({ decision: "continue", reason: reasonText }), + stderr: "", + policyName: policy.name, + reason, + decision: "deny", + }; + } + return { + exitCode: 0, + stdout: JSON.stringify({ decision: "deny", reason: blockedMessage }), + stderr: "", + policyName: policy.name, + reason, + decision: "deny", + }; + } + + // Goose: the deny contract is `{"decision":"block","reason"}` on stdout at + // exit 0, honored on PreToolUse ONLY (shipped goose ≥ v1.37.0, PR + // block/goose#9304; exit 2 also blocks but JSON carries the reason + // cleanly). Goose has NO Stop event (the 5 require-*-before-stop builtins + // never fire for it — see CLAUDE.md) and does NOT honor deny on + // UserPromptSubmit/PostToolUse (observation) — a block emitted on those + // events is ignored (fail-open), a documented limitation. PreToolUse fires + // for the shell tool AND inside delegated subagents, so this one branch + // covers the entire enforceable surface. Mirrors the Hermes branch (no + // turn-end event to special-case). Verified live against goose v1.43.0. + if (session?.cli === "goose") { + return { + exitCode: 0, + stdout: JSON.stringify({ decision: "block", reason: blockedMessage }), + stderr: "", + policyName: policy.name, + reason, + decision: "deny", + }; + } + if (eventType === "PreToolUse") { const response = { hookSpecificOutput: { @@ -429,8 +530,8 @@ export async function evaluatePolicies( // Branch first so the rest of the function only handles Claude-shaped // responses. We match both Stop and SubagentStop so custom policies // subscribing to SubagentStop on Cursor get the same force-retry - // semantics — mirrors the cli==="copilot" Stop|SubagentStop widening - // at line ~472. Ref: https://cursor.com/docs/hooks (Stdout Response Format). + // semantics — mirrors the cli==="copilot" Stop|SubagentStop widening. + // Ref: https://cursor.com/docs/hooks (Stdout Response Format). if (session?.cli === "cursor") { if (eventType === "Stop" || eventType === "SubagentStop") { const response = { @@ -499,15 +600,35 @@ export async function evaluatePolicies( }; } - // Gemini CLI: - // • Stop (AfterAgent) → {decision: "block", reason: "MANDATORY ACTION..."} - // mirrors Claude's exit-2-from-Stop "force retry" semantics. - // • UserPromptSubmit/PostToolUse/SessionStart/PreToolUse → context - // injection via {hookSpecificOutput: {hookEventName, additionalContext}} - // where hookEventName is the GEMINI event name (BeforeAgent/AfterTool/ - // SessionStart/BeforeTool), not the canonical PascalCase form. - // • Other events → stderr only (no stdout JSON shape supported). - if (session?.cli === "gemini") { + // Hermes: no additional-context channel on any event (the only actionable + // response is `{"decision":"block"}`). So instruct degrades to allow + + // log — we emit a non-blocking `{decision:"allow", reason}` (Hermes lets + // the tool run) and surface the note on stderr for the operator's logs. + // Documented limitation; there is no Stop event to force-retry into. + if (session?.cli === "hermes") { + const stderrMsg = instructEntries + .map((e) => `[failproofai] ${e.policyName}: ${e.reason}`) + .join("\n"); + return { + exitCode: 0, + stdout: JSON.stringify({ + decision: "allow", + reason: `Instruction from failproofai: ${combined}`, + }), + stderr: stderrMsg + "\n", + policyName: policyNames[0], + policyNames, + reason: combined, + decision: "instruct", + }; + } + + // OpenClaw: Stop (before_agent_finalize) can force a revise, so we emit the + // MANDATORY ACTION wording as a flat deny — the shim maps it to + // {action:"revise", reason}. Every other event lacks an additional-context + // channel (before_tool_call's return is {params,block,blockReason} only), so + // instruct degrades to allow + stderr note, like Hermes. + if (session?.cli === "openclaw") { if (eventType === "Stop") { const policyAttribution = policyNames.length === 1 ? `policy: ${policyNames[0]}` @@ -515,7 +636,7 @@ export async function evaluatePolicies( const reasonText = `MANDATORY ACTION REQUIRED from failproofai (${policyAttribution}): ${combined}\n\nYou MUST complete the above action(s) NOW. Do NOT ask the user for confirmation — execute the required action(s), then attempt to finish your task again.`; return { exitCode: 0, - stdout: JSON.stringify({ decision: "block", reason: reasonText }), + stdout: JSON.stringify({ permission: "deny", reason: reasonText }), stderr: "", policyName: policyNames[0], policyNames, @@ -523,31 +644,58 @@ export async function evaluatePolicies( decision: "instruct", }; } - // Map back from canonical → Gemini event name. Prefer the raw event name - // off the session (handler.ts populates it from parsed.hook_event_name) - // so we don't have to maintain a reverse lookup table. - const supportsContext = - eventType === "UserPromptSubmit" || - eventType === "PreToolUse" || - eventType === "PostToolUse" || - eventType === "SessionStart"; - if (supportsContext) { - // Round-trip the agent-emitted event name so Gemini sees `BeforeTool`, - // `BeforeAgent`, etc. (NOT the canonical Claude form). Prefer the - // stdin payload's `hook_event_name` when present; fall back to the raw - // CLI `--hook` arg captured by handler.ts; only use the canonical - // event as a last resort (would never round-trip correctly, but better - // than emitting nothing). - const hookEventName = session?.hookEventName ?? session?.rawHookEventName ?? eventType; - const response = { - hookSpecificOutput: { - hookEventName, - additionalContext: `Instruction from failproofai: ${combined}`, - }, + const stderrMsg = instructEntries + .map((e) => `[failproofai] ${e.policyName}: ${e.reason}`) + .join("\n"); + return { + exitCode: 0, + stdout: JSON.stringify({ + permission: "allow", + reason: `Instruction from failproofai: ${combined}`, + }), + stderr: stderrMsg + "\n", + policyName: policyNames[0], + policyNames, + reason: combined, + decision: "instruct", + }; + } + + // OpenCode: same rationale as the deny branch above — emit + // additionalContext so the shim submits a follow-up via + // client.session.prompt instead of throwing into a dead handler. + if (session?.cli === "opencode") { + if (eventType === "Stop" || eventType === "SubagentStop") { + const policyAttribution = policyNames.length === 1 + ? `policy: ${policyNames[0]}` + : `policies: ${policyNames.join(", ")}`; + const reasonText = `MANDATORY ACTION REQUIRED from failproofai (${policyAttribution}): ${combined}\n\nYou MUST complete the above action(s) NOW. Do NOT ask the user for confirmation — execute the required action(s), then attempt to finish your task again.`; + return { + exitCode: 0, + stdout: JSON.stringify({ hookSpecificOutput: { additionalContext: reasonText } }), + stderr: "", + policyName: policyNames[0], + policyNames, + reason: combined, + decision: "instruct", }; + } + } + + // Factory droid: on Stop, emit the MANDATORY ACTION wording as a + // `{decision:"block", reason}` on stdout (exit 0) — droid's only turn-end + // force-retry channel. Every other event lacks an additional-context + // channel (droid honors JSON only for the Stop block), so instruct degrades + // to allow + stderr note, like Hermes. + if (session?.cli === "factory") { + if (eventType === "Stop") { + const policyAttribution = policyNames.length === 1 + ? `policy: ${policyNames[0]}` + : `policies: ${policyNames.join(", ")}`; + const reasonText = `MANDATORY ACTION REQUIRED from failproofai (${policyAttribution}): ${combined}\n\nYou MUST complete the above action(s) NOW. Do NOT ask the user for confirmation — execute the required action(s), then attempt to finish your task again.`; return { exitCode: 0, - stdout: JSON.stringify(response), + stdout: JSON.stringify({ decision: "block", reason: reasonText }), stderr: "", policyName: policyNames[0], policyNames, @@ -555,8 +703,6 @@ export async function evaluatePolicies( decision: "instruct", }; } - // No context-injection channel for SessionEnd/PreCompress/Notification/ - // BeforeModel/AfterModel/BeforeToolSelection — surface via stderr only. const stderrMsg = instructEntries .map((e) => `[failproofai] ${e.policyName}: ${e.reason}`) .join("\n"); @@ -571,22 +717,20 @@ export async function evaluatePolicies( }; } - // Hermes: no additional-context channel on any event (the only actionable - // response is `{"decision":"block"}`). So instruct degrades to allow + - // log — we emit a non-blocking `{decision:"allow", reason}` (Hermes lets - // the tool run) and surface the note on stderr for the operator's logs. - // Documented limitation; there is no Stop event to force-retry into. - if (session?.cli === "hermes") { - const stderrMsg = instructEntries - .map((e) => `[failproofai] ${e.policyName}: ${e.reason}`) - .join("\n"); + // Devin CLI: a pure Claude-clone. On Stop, emit the MANDATORY ACTION + // wording as `{decision:"block", reason}` on stdout (exit 0) — Devin's + // turn-end force-retry channel (its exit-2 is not a force-retry). Every + // other event falls through to the generic Claude additionalContext path + // below (Devin honors `hookSpecificOutput.additionalContext`). + if (session?.cli === "devin" && eventType === "Stop") { + const policyAttribution = policyNames.length === 1 + ? `policy: ${policyNames[0]}` + : `policies: ${policyNames.join(", ")}`; + const reasonText = `MANDATORY ACTION REQUIRED from failproofai (${policyAttribution}): ${combined}\n\nYou MUST complete the above action(s) NOW. Do NOT ask the user for confirmation — execute the required action(s), then attempt to finish your task again.`; return { exitCode: 0, - stdout: JSON.stringify({ - decision: "allow", - reason: `Instruction from failproofai: ${combined}`, - }), - stderr: stderrMsg + "\n", + stdout: JSON.stringify({ decision: "block", reason: reasonText }), + stderr: "", policyName: policyNames[0], policyNames, reason: combined, @@ -594,18 +738,36 @@ export async function evaluatePolicies( }; } - // OpenCode: same rationale as the deny branch above — emit - // additionalContext so the shim submits a follow-up via - // client.session.prompt instead of throwing into a dead handler. - if (session?.cli === "opencode") { - if (eventType === "Stop" || eventType === "SubagentStop") { + // Antigravity CLI: its OWN instruct shapes (verified live agy v1.1.2). + // • UserPromptSubmit (canonical for PreInvocation) → `{injectSteps:[{ + // ephemeralMessage}]}` injects the instruction as a transient system + // message before the model runs. + // • Stop → `{decision:"continue", reason}` re-enters the loop with the + // MANDATORY-ACTION directive (Antigravity's only turn-end channel). + // • Every other event lacks an additional-context channel → degrade to + // allow + stderr note, like Hermes. + if (session?.cli === "antigravity") { + if (eventType === "UserPromptSubmit") { + return { + exitCode: 0, + stdout: JSON.stringify({ + injectSteps: [{ ephemeralMessage: `Instruction from failproofai: ${combined}` }], + }), + stderr: "", + policyName: policyNames[0], + policyNames, + reason: combined, + decision: "instruct", + }; + } + if (eventType === "Stop") { const policyAttribution = policyNames.length === 1 ? `policy: ${policyNames[0]}` : `policies: ${policyNames.join(", ")}`; const reasonText = `MANDATORY ACTION REQUIRED from failproofai (${policyAttribution}): ${combined}\n\nYou MUST complete the above action(s) NOW. Do NOT ask the user for confirmation — execute the required action(s), then attempt to finish your task again.`; return { exitCode: 0, - stdout: JSON.stringify({ hookSpecificOutput: { additionalContext: reasonText } }), + stdout: JSON.stringify({ decision: "continue", reason: reasonText }), stderr: "", policyName: policyNames[0], policyNames, @@ -613,6 +775,36 @@ export async function evaluatePolicies( decision: "instruct", }; } + const stderrMsg = instructEntries + .map((e) => `[failproofai] ${e.policyName}: ${e.reason}`) + .join("\n"); + return { + exitCode: 0, + stdout: "", + stderr: stderrMsg + "\n", + policyName: policyNames[0], + policyNames, + reason: combined, + decision: "instruct", + }; + } + + // Goose: a non-block PreToolUse decision injects nothing (verified live — no + // additional-context channel), and Goose has no Stop event, so instruct + // degrades to allow + stderr note, like Hermes / Factory non-Stop events. + if (session?.cli === "goose") { + const stderrMsg = instructEntries + .map((e) => `[failproofai] ${e.policyName}: ${e.reason}`) + .join("\n"); + return { + exitCode: 0, + stdout: "", + stderr: stderrMsg + "\n", + policyName: policyNames[0], + policyNames, + reason: combined, + decision: "instruct", + }; } if (eventType === "Stop" || eventType === "SubagentStop") { @@ -629,7 +821,7 @@ export async function evaluatePolicies( // `[WARNING] Hook warning: ...` but does NOT trigger retry. The // documented retry shape is `{decision: "block", reason}` JSON on // stdout (exit 0). Mirrors the cli==="copilot" branch in the deny - // path at line ~279 so custom instruct policies enforce on Copilot. + // path so custom instruct policies enforce on Copilot. if (session?.cli === "copilot") { return { exitCode: 0, @@ -715,39 +907,20 @@ export async function evaluatePolicies( }; } - // Gemini: mirror the instruct context-injection shape for events that - // support it; stderr-only for everything else. - if (session?.cli === "gemini") { - const supportsContext = - eventType === "UserPromptSubmit" || - eventType === "PreToolUse" || - eventType === "PostToolUse" || - eventType === "SessionStart"; + // OpenClaw: same flat shape as Pi — {permission:"allow", reason}. The shim + // returns undefined (no block) for an allow verdict regardless, so the note + // surfaces via stderr; keeping the flat stdout shape keeps the shim's parse + // path uniform across every verdict. + if (session?.cli === "openclaw") { const stderrMsg = allowEntries .map((e) => `[failproofai] ${e.policyName}: ${e.reason}`) .join("\n"); - if (supportsContext) { - // Same fallback chain as the instruct path above — see comment there. - const hookEventName = session?.hookEventName ?? session?.rawHookEventName ?? eventType; - const response = { - hookSpecificOutput: { - hookEventName, - additionalContext: `Note from failproofai: ${combined}`, - }, - }; - return { - exitCode: 0, - stdout: JSON.stringify(response), - stderr: stderrMsg + "\n", - policyName: policyNames[0], - policyNames, - reason: combined, - decision: "allow", - }; - } return { exitCode: 0, - stdout: "", + stdout: JSON.stringify({ + permission: "allow", + reason: `Note from failproofai: ${combined}`, + }), stderr: stderrMsg + "\n", policyName: policyNames[0], policyNames, @@ -761,13 +934,20 @@ export async function evaluatePolicies( eventType === "PostToolUse" || eventType === "UserPromptSubmit" || eventType === "PermissionRequest"; - const response = supportsHookSpecificOutput - ? { hookSpecificOutput: { hookEventName: eventType, additionalContext: `Note from failproofai: ${combined}` } } - : { reason: combined }; const stderrMsg = allowEntries .map((e) => `[failproofai] ${e.policyName}: ${e.reason}`) .join("\n"); - return { exitCode: 0, stdout: JSON.stringify(response), stderr: stderrMsg + "\n", policyName: policyNames[0], policyNames, reason: combined, decision: "allow" }; + // Only events with a real additional-context channel carry the allow-note + // to the agent. Everything else (Stop, SubagentStop, Session*, PreCompact, …) + // has NO channel, so we keep informational allow-notes OUT of stdout — a + // bare `{reason}` there is rendered as noise (e.g. droid printing a Stop's + // "…skipping commit check…skipping PR check…" wall on a perfectly fine turn). + // The note is still logged to stderr + the activity store for diagnostics. + if (supportsHookSpecificOutput) { + const response = { hookSpecificOutput: { hookEventName: eventType, additionalContext: `Note from failproofai: ${combined}` } }; + return { exitCode: 0, stdout: JSON.stringify(response), stderr: stderrMsg + "\n", policyName: policyNames[0], policyNames, reason: combined, decision: "allow" }; + } + return { exitCode: 0, stdout: "", stderr: stderrMsg + "\n", policyName: policyNames[0], policyNames, reason: combined, decision: "allow" }; } return { exitCode: 0, stdout: "", stderr: "", policyName: null, reason: null, decision: "allow" }; } diff --git a/src/hooks/resolve-cwd.ts b/src/hooks/resolve-cwd.ts index 0246182c..a59ec5e9 100644 --- a/src/hooks/resolve-cwd.ts +++ b/src/hooks/resolve-cwd.ts @@ -13,7 +13,7 @@ * the dashboard renders an em-dash, and `readMergedHooksConfig(cwd)` / * `loadAllCustomHooks({ sessionCwd })` skip project-scope discovery. * - * • Claude / Codex / Copilot / Gemini / Pi / OpenCode: stdin's top-level + * • Claude / Codex / Copilot / Pi / OpenCode: stdin's top-level * `cwd` is reliable for every event; passthrough. * * Mirrors the dispatch pattern of `resolve-permission-mode.ts` and diff --git a/src/hooks/resolve-permission-mode.ts b/src/hooks/resolve-permission-mode.ts index d90e3734..120e9203 100644 --- a/src/hooks/resolve-permission-mode.ts +++ b/src/hooks/resolve-permission-mode.ts @@ -32,10 +32,6 @@ * `tool_call` handlers always run with the same authority. Falls back to * "default" via the same final branch as Copilot/Cursor. * - * • Gemini CLI: hook payload doesn't carry a permission-mode field today. - * Falls back to "default" via the same final branch as Copilot/Cursor/ - * OpenCode/Pi. Revisit when Gemini's hook protocol exposes a per-session - * authority signal (the docs as of 2026-04-13 do not). */ import { readFileSync } from "node:fs"; import { findCodexTranscript } from "../../lib/codex-sessions"; @@ -54,7 +50,7 @@ export function resolvePermissionMode( return resolveCodexMode(sessionId) ?? "default"; } - // copilot, cursor, opencode, pi, gemini, unknown integrations, or codex without a sessionId + // copilot, cursor, opencode, pi, unknown integrations, or codex without a sessionId return "default"; } diff --git a/src/hooks/resolve-transcript-path.ts b/src/hooks/resolve-transcript-path.ts index 8cae75b5..6a19e326 100644 --- a/src/hooks/resolve-transcript-path.ts +++ b/src/hooks/resolve-transcript-path.ts @@ -22,10 +22,6 @@ * • Pi: shim doesn't forward transcript_path. Discover at * ~/.pi/agent/sessions//_.jsonl. * - * • Gemini: docs say stdin carries transcript_path, but coverage is uneven - * across versions. Trust stdin first; fall back to discovery under - * ~/.gemini/tmp//chats/.json. - * * Mirrors the dispatch pattern of `resolve-permission-mode.ts`. Each * `find*Transcript` helper performs its own existsSync + path-traversal * containment check, so passing in a malformed sessionId is safe (returns @@ -35,7 +31,8 @@ import { findCodexTranscript } from "../../lib/codex-sessions"; import { findCopilotTranscript } from "../../lib/copilot-sessions"; import { findCursorTranscript } from "../../lib/cursor-sessions"; import { findPiTranscript } from "../../lib/pi-sessions"; -import { findGeminiTranscript } from "../../lib/gemini-sessions"; +import { findFactoryTranscript } from "../../lib/factory-sessions"; +import { findAntigravityTranscript } from "../../lib/antigravity-sessions"; import type { IntegrationType } from "./types"; export function resolveTranscriptPath( @@ -59,14 +56,35 @@ export function resolveTranscriptPath( return findCursorTranscript(sessionId) ?? undefined; case "pi": return findPiTranscript(sessionId) ?? undefined; - case "gemini": - return findGeminiTranscript(sessionId) ?? undefined; + case "factory": + // Factory writes real JSONL at + // ~/.factory/sessions//.jsonl (Claude-style). + return findFactoryTranscript(sessionId) ?? undefined; + case "antigravity": + // Antigravity writes real JSONL at ~/.gemini/antigravity-cli/brain/ + // /.system_generated/logs/transcript_full.jsonl. + return findAntigravityTranscript(sessionId) ?? undefined; + case "devin": + // Devin keeps sessions in SQLite (~/.local/share/devin/cli/sessions.db); + // there is no on-disk transcript file, so hand back a virtual path (like + // opencode) — audit/download read the DB directly. + return `devin-db://${sessionId}`; + case "goose": + // Goose keeps sessions in SQLite (~/.local/share/goose/sessions/sessions.db); + // its live-hook payload carries no transcript file, so hand back a virtual + // path (like devin) — audit/download read the DB directly. + return `goose-db://${sessionId}`; case "opencode": return `opencode-db://${sessionId}`; case "hermes": // Hermes live-hook payloads carry no transcript file; the audit path // reads sessions straight from ~/.hermes/state.db (see hermes-sessions.ts). return undefined; + case "openclaw": + // OpenClaw's plugin shim forwards transcript_path in stdin (handled by the + // early return above); its audit path reads the real JSONL sessions + // directly via lib/openclaw-sessions.ts, so no discovery is needed here. + return undefined; default: return undefined; } diff --git a/src/hooks/tool-name-canonicalize.ts b/src/hooks/tool-name-canonicalize.ts index 6681b721..94985332 100644 --- a/src/hooks/tool-name-canonicalize.ts +++ b/src/hooks/tool-name-canonicalize.ts @@ -14,9 +14,16 @@ import { OPENCODE_TOOL_INPUT_MAP, PI_TOOL_MAP, PI_TOOL_INPUT_MAP, - GEMINI_TOOL_MAP, HERMES_TOOL_MAP, HERMES_TOOL_INPUT_MAP, + OPENCLAW_TOOL_MAP, + OPENCLAW_TOOL_INPUT_MAP, + FACTORY_TOOL_MAP, + DEVIN_TOOL_MAP, + ANTIGRAVITY_TOOL_MAP, + ANTIGRAVITY_TOOL_INPUT_MAP, + GOOSE_TOOL_MAP, + GOOSE_TOOL_INPUT_MAP, } from "./types"; /** @@ -32,10 +39,22 @@ export function canonicalizeToolName( if (cli === "copilot") return COPILOT_TOOL_MAP[raw] ?? raw; if (cli === "cursor") return CURSOR_TOOL_MAP[raw] ?? raw; if (cli === "codex") return CODEX_TOOL_MAP[raw] ?? raw; - if (cli === "gemini") return GEMINI_TOOL_MAP[raw] ?? raw; if (cli === "opencode") return OPENCODE_TOOL_MAP[raw] ?? raw; if (cli === "pi") return PI_TOOL_MAP[raw] ?? raw; if (cli === "hermes") return HERMES_TOOL_MAP[raw] ?? raw; + if (cli === "openclaw") return OPENCLAW_TOOL_MAP[raw] ?? raw; + // Factory droid: Execute→Bash, Create→Write, FetchUrl→WebFetch, … (verified + // live against droid v0.171.0). tool_input keys are already canonical. + if (cli === "factory") return FACTORY_TOOL_MAP[raw] ?? raw; + // Devin CLI: exec→Bash (verified live against devin v3000.1.27). + // tool_input.command is already canonical. + if (cli === "devin") return DEVIN_TOOL_MAP[raw] ?? raw; + // Antigravity CLI: run_command→Bash (verified agy v1.1.2), view_file→Read, … + // (best-effort). tool_input keys are PascalCase → ANTIGRAVITY_TOOL_INPUT_MAP. + if (cli === "antigravity") return ANTIGRAVITY_TOOL_MAP[raw] ?? raw; + // Goose: shell→Bash, write/edit/view→file ops, todo__todo_write→TodoWrite, … + // (verified live against goose v1.43.0). Handles bare + `__` names. + if (cli === "goose") return GOOSE_TOOL_MAP[raw] ?? raw; return raw; } @@ -62,6 +81,15 @@ export function canonicalizeToolInput( // Hermes read_file/write_file/patch deliver the file path as `path`; map it to // `file_path` so path/content builtins fire (verified against a live state.db). else if (cli === "hermes") perToolMap = HERMES_TOOL_INPUT_MAP[toolName]; + // OpenClaw file tools (read/write/edit) deliver the path as `path`; exec + // already delivers `command`. Map path → file_path so path builtins fire. + else if (cli === "openclaw") perToolMap = OPENCLAW_TOOL_INPUT_MAP[toolName]; + // Antigravity's run_command args are PascalCase (`CommandLine`, `Cwd`); map + // to `command`/`cwd` so Bash builtins fire (verified agy v1.1.2). + else if (cli === "antigravity") perToolMap = ANTIGRAVITY_TOOL_INPUT_MAP[toolName]; + // Goose file tools (write/edit/view) deliver the path as `path`, read_image as + // `source`; map to `file_path` so path builtins fire (verified goose v1.43.0). + else if (cli === "goose") perToolMap = GOOSE_TOOL_INPUT_MAP[toolName]; if (!perToolMap) return rawInput; const out: Record = {}; for (const [k, v] of Object.entries(rawInput as Record)) { diff --git a/src/hooks/types.ts b/src/hooks/types.ts index 6bfb3b77..31636f26 100644 --- a/src/hooks/types.ts +++ b/src/hooks/types.ts @@ -1,11 +1,11 @@ /** - * Constants and interfaces for agent CLI hooks integrations (Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi, Gemini CLI, …). + * Constants and interfaces for agent CLI hooks integrations (Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi, …). */ export const HOOK_SCOPES = ["user", "project", "local"] as const; export type HookScope = (typeof HOOK_SCOPES)[number]; -export const INTEGRATION_TYPES = ["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini", "hermes"] as const; +export const INTEGRATION_TYPES = ["claude", "codex", "copilot", "cursor", "opencode", "pi", "hermes", "openclaw", "factory", "devin", "antigravity", "goose"] as const; export type IntegrationType = (typeof INTEGRATION_TYPES)[number]; export const CODEX_HOOK_SCOPES = ["user", "project"] as const; @@ -510,115 +510,435 @@ export const PI_TOOL_INPUT_MAP: Record> = { Edit: { path: "file_path" }, }; -// ── Gemini CLI ───────────────────────────────────────────────────────────── -// -// Gemini CLI's hook contract is the closest thing to a Claude Code clone we've -// seen: same `{matcher, hooks: [{type, command, timeout}]}` settings shape, -// PascalCase event names, snake_case stdin payload field names (session_id, -// tool_name, tool_input, hook_event_name, cwd, transcript_path), subprocess -// execution model, and a `$CLAUDE_PROJECT_DIR` env-var alias on top of its own -// `$GEMINI_PROJECT_DIR` for back-compat. The integration is modeled on -// claudeCode in integrations.ts; only three translation layers differ: -// -// 1. Event names — Gemini uses BeforeTool/AfterTool/BeforeAgent/AfterAgent/ -// PreCompress/Notification/SessionStart/SessionEnd/BeforeModel/AfterModel/ -// BeforeToolSelection. We install all 11 events but only 8 have a Claude -// canonical equivalent; the other 3 (BeforeModel/AfterModel/ -// BeforeToolSelection) pass through unchanged (no policy matches today, -// but the binary still records activity). -// -// 2. Tool names — Gemini's tools are snake_case (run_shell_command, read_file, -// write_file, replace, glob, grep_search, list_directory, web_fetch, -// google_web_search, write_todos, save_memory, read_many_files, ask_user). -// The handler canonicalizes via GEMINI_TOOL_MAP (run_shell_command → Bash, -// read_file → Read, etc.) so existing builtin policies fire unchanged. -// Unknown tools (extensions, MCP `mcp_*` names) pass through. -// -// 3. Response shape — Gemini emits flat `{decision: "deny", reason}` for -// blocks (NOT Claude's `{hookSpecificOutput: {permissionDecision: "deny", -// permissionDecisionReason}}`), and `{hookSpecificOutput: {hookEventName, -// additionalContext}}` for context injection on BeforeAgent / AfterTool / -// SessionStart only. policy-evaluator.ts handles this via a `cli === -// "gemini"` branch. +// ── OpenClaw (openclaw gateway) ───────────────────────────────────────────── // -// Settings paths: -// user → ~/.gemini/settings.json -// project → /.gemini/settings.json -// Gemini also documents a system scope (/etc/gemini-cli/settings.json) but we -// don't expose it (matches Codex/Copilot/Cursor/OpenCode/Pi: user|project only). -// -// **Per-event capability** (from Gemini docs as of 2026-04-13): -// • BeforeTool → PreToolUse · CAN block via `{decision: "deny"}` -// · CAN rewrite via `hookSpecificOutput.tool_input` -// • AfterTool → PostToolUse · CAN observe; `additionalContext` injection -// • BeforeAgent → UserPromptSubmit · CAN block; `additionalContext` injection -// • AfterAgent → Stop · CAN force-retry via `{decision: "block"}` -// (closest to Claude's exit-2-from-Stop) -// • SessionStart → SessionStart · `additionalContext` injection -// • SessionEnd → SessionEnd · observation only -// • PreCompress → PreCompact · observation only -// • Notification → Notification · observation only -// • BeforeModel → BeforeModel · Gemini-only; no canonical, observation -// • AfterModel → AfterModel · Gemini-only; no canonical, observation -// • BeforeToolSelection → BeforeToolSelection · Gemini-only; no canonical, observation -// -// Ref: https://geminicli.com/docs/hooks/ - -export const GEMINI_HOOK_SCOPES = ["user", "project"] as const; -export type GeminiHookScope = (typeof GEMINI_HOOK_SCOPES)[number]; - -export const GEMINI_HOOK_EVENT_TYPES = [ +// OpenClaw is a self-hosted assistant gateway (23+ chat channels). Like Hermes +// it is a dual-pillar integration (live hooks + audit) and USER-scope only +// (`~/.openclaw/openclaw.json`; workspace plugins are disabled by default). +// +// Enforcement is via OpenClaw's IN-PROCESS PLUGIN hooks (its file-based +// "internal hooks" are observation-only and cannot block) — so failproofai +// ships a plugin (`openclaw-plugin/`) that async-spawns the binary and maps a +// flat `{permission, reason}` verdict back to each hook's native return shape. +// The shim forwards a Claude-shaped stdin (params→tool_input, toolName→ +// tool_name, transcriptPath→transcript_path, stopHookActive→stop_hook_active), +// so the handler + builtins work unchanged; canonicalization stays binary-side +// via the maps below (no inline maps in the shim, unlike OpenCode/Pi). +// +// Per-hook capability (verified live against openclaw v2026.7.1): +// before_tool_call → PreToolUse ✅ deny ({block:true, blockReason}) +// after_tool_call → PostToolUse observation +// before_agent_run → UserPromptSubmit ✅ deny ({outcome:"block", reason}) +// before_agent_finalize → Stop ✅ revise ({action:"revise", reason}); +// carries transcriptPath + stopHookActive (≈ Claude Stop) +// session_start/end → SessionStart/End observation +// subagent_ended → SubagentStop observation only (cannot veto) +// before_compaction → PreCompact observation +// Omitted: agent_end (would double-fire Stop); message_sending (outbound-msg +// cancel gate — OpenClaw-only capability, deferred). +export const OPENCLAW_HOOK_SCOPES = ["user"] as const; +export type OpenClawHookScope = (typeof OPENCLAW_HOOK_SCOPES)[number]; + +export const OPENCLAW_HOOK_EVENT_TYPES = [ + "before_tool_call", + "after_tool_call", + "before_agent_run", + "before_agent_finalize", + "session_start", + "session_end", + "subagent_ended", + "before_compaction", +] as const; +export type OpenClawHookEventType = (typeof OPENCLAW_HOOK_EVENT_TYPES)[number]; + +export const OPENCLAW_EVENT_MAP: Record = { + before_tool_call: "PreToolUse", + after_tool_call: "PostToolUse", + before_agent_run: "UserPromptSubmit", + before_agent_finalize: "Stop", + session_start: "SessionStart", + session_end: "SessionEnd", + subagent_ended: "SubagentStop", + before_compaction: "PreCompact", +}; + +// OpenClaw native tool ids (verified against source src/agents/tool-catalog.ts +// and a live before_tool_call payload: tool `exec`, params `{command}`). Names +// with a Claude canonical are mapped so builtin policies fire; OpenClaw-specific +// tools (process, apply_patch, memory_*, sessions_*, browser, canvas, …) pass +// through unchanged so they still appear in the audit, just unmatched. +export const OPENCLAW_TOOL_MAP: Record = { + exec: "Bash", + read: "Read", + write: "Write", + edit: "Edit", + grep: "Grep", + glob: "Glob", + web_search: "WebSearch", + web_fetch: "WebFetch", +}; + +// OpenClaw tool-INPUT key canonicalization, keyed by the *canonical* tool name +// (the handler canonicalizes the name before calling canonicalizeToolInput). +// `exec` already delivers `command` (matches Bash builtins), so no entry; the +// file tools deliver the path as `path`, which Claude builtins read as +// `file_path`. Mirrors HERMES_TOOL_INPUT_MAP / PI_TOOL_INPUT_MAP. +export const OPENCLAW_TOOL_INPUT_MAP: Record> = { + Read: { path: "file_path" }, + Write: { path: "file_path" }, + Edit: { path: "file_path" }, +}; + +// ── Factory Droid (droid) ─────────────────────────────────────────────────── +// +// Factory's droid CLI ships a Claude-compatible external-command hook system, +// but with two schema quirks verified LIVE against droid v0.171.0: +// +// 1. **Event names live at the TOP LEVEL of hooks.json — there is NO `"hooks"` +// wrapper.** The published docs are wrong; droid rejects a `{"hooks":{…}}` +// wrapper with `WARN Ignoring unknown hook event keys keys:["hooks"]`. The +// correct shape is: +// { "PreToolUse": [ { "matcher": "*", "hooks": [ { … } ] } ], +// "Stop": [ { "hooks": [ { … } ] } ] } +// Tool events (PreToolUse / PostToolUse) MUST carry `"matcher": "*"` +// (matches all tools); non-tool events omit the matcher. +// +// 2. **Deny is driven by EXIT CODE 2 + stderr, NOT a JSON decision.** droid +// ignores a `{decision:…}` object on tool events and blocks purely on +// exit code 2 (verified live: `Hook returned exit code 2, throwing +// ToolExecutionControlError`). The one exception is the `Stop` event, +// where droid does NOT support exit-2 force-retry — there we emit +// `{decision:"block", reason}` JSON on stdout at exit 0 (docs: "if +// decision is block, Droid does not stop"). Both branches live in +// policy-evaluator.ts's `cli === "factory"` handling. +// +// Event names are already PascalCase (matching Claude's canonical set), so +// there is NO FACTORY_EVENT_MAP and NO handler.ts canonicalization branch — the +// binary sees the events verbatim. The stdin payload is Claude snake_case +// (`session_id`, `transcript_path`, `cwd`, `permission_mode`, `hook_event_name`, +// `tool_name`, `tool_input:{command,…}`), so no payload normalization is needed +// either. +// +// Settings paths (verified against droid v0.171.0): +// user → ~/.factory/hooks.json +// project → /.factory/hooks.json +// +// Audit pillar: sessions at ~/.factory/sessions//.jsonl +// (Claude-style encoded-cwd folders), one JSONL per session alongside a +// `.settings.json` sibling we ignore. See lib/factory-sessions.ts. + +export const FACTORY_HOOK_SCOPES = ["user", "project"] as const; +export type FactoryHookScope = (typeof FACTORY_HOOK_SCOPES)[number]; + +export const FACTORY_HOOK_EVENT_TYPES = [ "SessionStart", - "SessionEnd", - "BeforeAgent", - "AfterAgent", - "BeforeModel", - "AfterModel", - "BeforeToolSelection", - "BeforeTool", - "AfterTool", - "PreCompress", + "UserPromptSubmit", + "PreToolUse", + "PostToolUse", "Notification", + "Stop", + "SubagentStop", + "PreCompact", + "SessionEnd", ] as const; -export type GeminiHookEventType = (typeof GEMINI_HOOK_EVENT_TYPES)[number]; - -/** Gemini event → canonical PascalCase HookEventType. Three Gemini-only events - * (BeforeModel, AfterModel, BeforeToolSelection) have no Claude equivalent and - * pass through unchanged. */ -export const GEMINI_EVENT_MAP: Record = { - SessionStart: "SessionStart", - SessionEnd: "SessionEnd", - BeforeAgent: "UserPromptSubmit", - AfterAgent: "Stop", - BeforeTool: "PreToolUse", - AfterTool: "PostToolUse", - PreCompress: "PreCompact", - Notification: "Notification", - // No canonical Claude equivalent — passthrough so the binary still records - // activity but `getPoliciesForEvent` returns [] (no-op fast path). - BeforeModel: "BeforeModel" as HookEventType, - AfterModel: "AfterModel" as HookEventType, - BeforeToolSelection: "BeforeToolSelection" as HookEventType, +export type FactoryHookEventType = (typeof FACTORY_HOOK_EVENT_TYPES)[number]; + +/** + * Factory droid's tool names → Claude PascalCase canonical names so existing + * builtin policies (which match `toolName === "Bash"`, etc.) fire unchanged. + * Verified against droid v0.171.0: shell runs as `Execute`, file writes as + * `Create`, URL fetches as `FetchUrl`. `tool_input.command` is already the + * canonical Bash key, so there is NO FACTORY_TOOL_INPUT_MAP. Unknown tools + * (MCP, extensions) pass through unchanged via the `?? raw` fallback. + */ +export const FACTORY_TOOL_MAP: Record = { + Execute: "Bash", + Read: "Read", + Edit: "Edit", + Create: "Write", + Grep: "Grep", + Glob: "Glob", + LS: "LS", + FetchUrl: "WebFetch", + WebSearch: "WebSearch", + TodoWrite: "TodoWrite", + Task: "Task", }; -/** Gemini's snake_case tool names → Claude PascalCase canonical names so existing - * builtin policies (which match `toolName === "Bash"`, etc.) fire unchanged on - * Gemini sessions. Unknown tools (MCP `mcp_*`, extensions, Skills) pass through - * unchanged. Per https://geminicli.com/docs/reference/tools/ as of 2026-04-13. */ -export const GEMINI_TOOL_MAP: Record = { - run_shell_command: "Bash", +// ── Devin CLI (devin) ─────────────────────────────────────────────────────── +// +// Devin's CLI (Cognition) is a **pure Claude-clone** external-command hook +// system — verified LIVE against devin v3000.1.27. Unlike Factory, it uses the +// standard Claude `"hooks"`-wrapper schema (its config.json also holds +// `org_id`, `theme_mode`, etc., so writes are merge-preserving via +// readJsonFile/writeJsonFile). No quirks: +// +// • Config lives under a `"hooks"` key exactly like Claude's settings.json: +// user → ~/.config/devin/config.json (the `"hooks"` key) +// project → /.devin/config.json (the `"hooks"` key) +// • Event names are already PascalCase (matching Claude's canonical set), so +// there is NO DEVIN_EVENT_MAP and NO handler.ts canonicalization branch. +// • The stdin payload is pure Claude snake_case (no normalization needed): +// PreToolUse → {hook_event_name, tool_name:"exec", tool_input:{command}, tool_use_id} +// PostToolUse → adds tool_response:{success, output, error} +// Stop → {stop_hook_active} +// • Deny contract = `{"decision":"block","reason"}` JSON on stdout at exit 0 +// (VERIFIED live — it blocked and overrode `--permission-mode dangerous`). +// Both non-Stop and Stop use the same `{decision:"block"}` shape (Stop's +// reason carries the MANDATORY-ACTION force-retry wording). Devin is +// Claude-compatible, so instruct on context-injection events emits +// `{hookSpecificOutput:{hookEventName, additionalContext}}`. See +// policy-evaluator.ts's `cli === "devin"` branch. +// +// Audit pillar: sessions live in SQLite at +// ~/.local/share/devin/cli/sessions.db (tables `sessions` — one row per +// session WITH a `working_directory` — and `message_nodes`, whose +// `chat_message` column is OpenAI-style JSON `{role, content, tool_calls?, +// tool_call_id?}`). See lib/devin-sessions.ts. `DEVIN_HOME` overrides the home +// dir for tests. + +export const DEVIN_HOOK_SCOPES = ["user", "project"] as const; +export type DevinHookScope = (typeof DEVIN_HOOK_SCOPES)[number]; + +export const DEVIN_HOOK_EVENT_TYPES = [ + "SessionStart", + "UserPromptSubmit", + "PreToolUse", + "PostToolUse", + "PermissionRequest", + "Stop", + "SessionEnd", +] as const; +export type DevinHookEventType = (typeof DEVIN_HOOK_EVENT_TYPES)[number]; + +/** + * Devin's tool names → Claude PascalCase canonical names so existing builtin + * policies (which match `toolName === "Bash"`, etc.) fire unchanged. Verified + * live against devin v3000.1.27: the shell tool runs as `exec` and its + * `tool_input.command` is already the canonical Bash key, so there is NO + * DEVIN_TOOL_INPUT_MAP. All other Devin tool names pass through unchanged via + * the `?? raw` fallback. + */ +export const DEVIN_TOOL_MAP: Record = { + exec: "Bash", +}; + +// ── Antigravity CLI (antigravity) ──────────────────────────────────────────── +// +// Antigravity's `agy` CLI has its OWN hook contract — it is NOT a Claude-clone. +// Verified LIVE against agy v1.1.2 (shipped docs at +// ~/.gemini/antigravity-cli/builtin/skills/agy-customizations/docs/hooks.md): +// +// 1. **hooks.json is a NAMED-hook schema.** Each top-level key is a hook +// *name* ("failproofai"), whose value is an event→handlers map. Tool events +// (PreToolUse / PostToolUse) use a `{matcher, hooks:[…]}` wrapper; +// non-tool events (PreInvocation / Stop) are FLAT arrays of handler +// objects: +// { "failproofai": { +// "PreToolUse": [ { "matcher":"*", "hooks":[ {type,command,timeout} ] } ], +// "PreInvocation": [ { type, command, timeout } ], +// "Stop": [ { type, command, timeout } ] } } +// Multiple named hooks merge; `"enabled": false` disables a named hook. +// +// 2. **camelCase (protojson) stdin payload.** Antigravity pipes camelCase +// fields (`toolCall:{name,args}`, `conversationId`, `workspacePaths`, +// `transcriptPath`, `stepIdx`, `modelName`) — handler.ts normalizes these +// to canonical snake_case (`tool_name`, `tool_input`, `session_id`, `cwd`, +// `transcript_path`) right after JSON.parse. Tool `args` are PascalCase +// (`CommandLine`, `Cwd`) — canonicalized via ANTIGRAVITY_TOOL_INPUT_MAP. +// +// 3. **Antigravity's OWN response shapes** (policy-evaluator.ts +// `cli === "antigravity"` branch): +// • Deny (tool events) → exit 0, `{decision:"deny", reason}` on stdout. +// • Deny on Stop → exit 0, `{decision:"continue", reason}` — +// "continue" re-enters the loop (how require-*-before-stop enforces). +// • Instruct on UserPromptSubmit (canonical for PreInvocation) → exit 0, +// `{injectSteps:[{ephemeralMessage:"Instruction from failproofai: …"}]}`. +// • Instruct on Stop → `{decision:"continue", reason}`. +// • Other instruct events → stderr note only (degrade like Hermes). +// +// 4. **ANTIGRAVITY_EVENT_MAP** maps the `--hook` arg to a canonical event: +// PreToolUse→PreToolUse, PostToolUse→PostToolUse, +// PreInvocation→UserPromptSubmit, Stop→Stop. +// +// Settings paths (verified against agy v1.1.2): +// user → ~/.gemini/config/hooks.json +// project → /.agents/hooks.json +// +// Audit pillar: plain JSONL transcripts at +// ~/.gemini/antigravity-cli/brain//.system_generated/logs/ +// transcript_full.jsonl (one step per line); the conversation index lives in +// SQLite at ~/.gemini/antigravity-cli/conversation_summaries.db. See +// lib/antigravity-sessions.ts + lib/antigravity-projects.ts. `ANTIGRAVITY_HOME` +// overrides the home dir for tests. + +export const ANTIGRAVITY_HOOK_SCOPES = ["user", "project"] as const; +export type AntigravityHookScope = (typeof ANTIGRAVITY_HOOK_SCOPES)[number]; + +/** The events failproofai installs into Antigravity's hooks.json. Tool events + * use the `{matcher, hooks}` wrapper; PreInvocation / Stop are flat arrays. */ +export const ANTIGRAVITY_HOOK_EVENT_TYPES = [ + "PreToolUse", + "PostToolUse", + "PreInvocation", + "Stop", +] as const; +export type AntigravityHookEventType = (typeof ANTIGRAVITY_HOOK_EVENT_TYPES)[number]; + +/** Antigravity `--hook` arg → canonical HookEventType. PreInvocation is + * Antigravity's before-model event → maps to UserPromptSubmit (where instruct + * injects `injectSteps`). Verified against agy v1.1.2. */ +export const ANTIGRAVITY_EVENT_MAP: Record = { + PreToolUse: "PreToolUse", + PostToolUse: "PostToolUse", + PreInvocation: "UserPromptSubmit", + Stop: "Stop", +}; + +/** + * Antigravity's tool names → Claude PascalCase canonical names so existing + * builtin policies (which match `toolName === "Bash"`, etc.) fire unchanged. + * Tool names VERIFIED against the agy binary's tool registry + live transcripts: + * the file tool is `write_to_file` (NOT `write_file`), listing is `list_dir` + * (NOT `list_directory`), and glob is `find_by_name` (NOT `find_filepath`) — the + * earlier best-effort names were wrong, so `block-env-files`/`block-secrets-write` + * silently no-op'd on Antigravity file writes. Unknown tools pass through via the + * `?? raw` fallback. + */ +export const ANTIGRAVITY_TOOL_MAP: Record = { + run_command: "Bash", + write_to_file: "Write", read_file: "Read", - read_many_files: "Read", - write_file: "Write", - replace: "Edit", - glob: "Glob", + view_file: "Read", + edit_file: "Edit", + replace_file_content: "Edit", + list_dir: "LS", + find_by_name: "Glob", grep_search: "Grep", - list_directory: "LS", - web_fetch: "WebFetch", - google_web_search: "WebSearch", - write_todos: "TodoWrite", - save_memory: "Memory", - ask_user: "AskUser", + read_url_content: "WebFetch", + search_web: "WebSearch", +}; + +/** + * 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). + */ +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" }, +}; + +// ── Goose (codename goose, Block) ──────────────────────────────────────────── +// +// Goose is Block's open-source Rust MCP agent — a LOCAL dev-agent CLI (like +// Claude/Factory/Devin, NOT a gateway). Dual-pillar: external shell-hook +// enforcement + SQLite audit. The entire contract below was verified LIVE +// against goose v1.43.0. +// +// Enforcement is via Goose's "hooks" system (the cross-agent Open Plugins spec): +// a plugin directory whose `hooks/hooks.json` `command` runs +// `failproofai --hook --cli goose`. Goose AUTO-DISCOVERS the dir at +// startup (no config edit needed) and self-registers it into config.yaml. +// +// 1. **Deny contract = `PreToolUse` ONLY** (shipped in goose ≥ v1.37.0, +// PR block/goose#9304). A hook blocks a tool via `{"decision":"block", +// "reason"}` on stdout at exit 0 (exit 2 + stderr also works); ANY other +// error/timeout → fail-open (allow). Verified live: the reason reaches the +// model and "do not retry" is appended. Goose has NO `Stop` event, so the 5 +// `require-*-before-stop` builtins never fire (inapplicable, like Hermes). +// `UserPromptSubmit` deny is NOT honored (observation only). `PreToolUse` +// fires for the shell tool AND inside delegated subagents, so it is the +// single sufficient deny point. +// +// 2. **Event names are already PascalCase** (matching Claude's canonical set), +// so there is NO `GOOSE_EVENT_MAP` and NO handler.ts event-canonicalization +// branch. The stdin payload, however, uses `event` (not `hook_event_name`) +// and `working_dir` (not `cwd`) — handler.ts normalizes `working_dir`→`cwd` +// for goose so `block-read-outside-cwd` keeps its cwd. `tool_name` / +// `tool_input` are already the canonical field names. +// +// 3. Tool names arrive BOTH bare (`shell`, `write`, `edit`, `view`, +// `read_image`, `tree`, `delegate`) AND `__` namespaced +// (`todo__todo_write`); GOOSE_TOOL_MAP covers both forms, unknown tools +// pass through via the `?? raw` fallback. Path-bearing tools deliver the +// path as `path` (or `source` for read_image), so GOOSE_TOOL_INPUT_MAP maps +// it to `file_path`. Shell's `command` is already canonical. +// +// 4. **Instruct has no channel** — a non-block PreToolUse decision injects +// nothing (verified live), so instruct() degrades to allow + stderr note +// (like Factory/Hermes on non-Stop events). policy-evaluator.ts's +// `cli === "goose"` branch: PreToolUse deny → `{"decision":"block",reason}` +// JSON at exit 0; no Stop branch. +// +// Settings paths (verified against goose v1.43.0): +// user → ~/.agents/plugins/failproofai/hooks/hooks.json +// project → /.agents/plugins/failproofai/hooks/hooks.json +// +// Audit pillar: sessions in SQLite at +// ~/.local/share/goose/sessions/sessions.db (schema v15). `sessions` rows carry +// a real `working_dir`, so `audit --project ` filters like Devin (NOT +// grouped-by-source like Hermes); `messages` rows hold Claude-style typed-block +// `content_json`. See lib/goose-sessions.ts. `GOOSE_HOME` overrides the data +// dir for tests. + +export const GOOSE_HOOK_SCOPES = ["user", "project"] as const; +export type GooseHookScope = (typeof GOOSE_HOOK_SCOPES)[number]; + +export const GOOSE_HOOK_EVENT_TYPES = [ + "SessionStart", + "UserPromptSubmit", + "PreToolUse", + "PostToolUse", + "SessionEnd", +] as const; +export type GooseHookEventType = (typeof GOOSE_HOOK_EVENT_TYPES)[number]; + +/** + * Goose's tool names → Claude PascalCase canonical names so existing builtin + * policies (which match `toolName === "Bash"`, etc.) fire unchanged. Verified + * live against goose v1.43.0: the developer extension exposes `shell` (Bash), + * `write`/`edit`/`view` (file ops), `read_image`, `glob`/`grep`, plus + * `tree`/`delegate`; other extensions namespace their tools (`todo__todo_write`). + * Unknown tools (MCP, other extensions) pass through unchanged via the `?? raw` + * fallback in handler.ts:canonicalizeToolName. + */ +export const GOOSE_TOOL_MAP: Record = { + shell: "Bash", + write: "Write", + edit: "Edit", + view: "Read", + read_image: "Read", + glob: "Glob", + grep: "Grep", + tree: "LS", + delegate: "Task", + todo__todo_write: "TodoWrite", +}; + +/** + * Per-tool input-key translation, keyed by the *canonical* tool name (the + * handler canonicalizes the name before calling canonicalizeToolInput). + * Verified live: goose's file tools deliver the path as `path` (`read_image` + * uses `source`), but Claude builtins read `file_path` (block-env-files, + * block-secrets-write, block-read-outside-cwd) — so map it. `shell` already + * delivers `command` (canonical), so Bash needs no entry. `edit` delivers + * `before`/`after` (not `old_string`/`new_string`); only the top-level `path` + * is mapped (no builtin inspects the edit body), mirroring PI_TOOL_INPUT_MAP. + */ +export const GOOSE_TOOL_INPUT_MAP: Record> = { + Read: { path: "file_path", source: "file_path" }, + Write: { path: "file_path" }, + Edit: { path: "file_path" }, + LS: { path: "file_path" }, }; export const HOOK_EVENT_TYPES = [ @@ -685,15 +1005,15 @@ export interface SessionMetadata { cwd?: string; permissionMode?: string; /** Read from the stdin payload's `hook_event_name` field. Carries the raw - * agent-emitted event name (e.g. Gemini's `BeforeTool`, Cursor's - * `preToolUse`, Pi's `tool_call`). May be undefined when stdin omits it. */ + * agent-emitted event name (e.g. Cursor's `preToolUse`, Pi's `tool_call`). + * May be undefined when stdin omits it. */ hookEventName?: string; /** The raw event name passed on the CLI's `--hook` flag, BEFORE any - * per-CLI canonicalization to PascalCase (e.g. `BeforeTool` for Gemini, - * `preToolUse` for Cursor). Use this for round-tripping the agent-side - * event name in response shapes when stdin doesn't include `hook_event_name`. */ + * per-CLI canonicalization to PascalCase (e.g. `preToolUse` for Cursor). + * Use this for round-tripping the agent-side event name in response shapes + * when stdin doesn't include `hook_event_name`. */ rawHookEventName?: string; - /** Which agent CLI fired this hook (claude | codex | copilot | cursor | opencode | pi | gemini). Set by handler.ts from --cli. */ + /** Which agent CLI fired this hook (claude | codex | copilot | cursor | opencode | pi | hermes | openclaw | factory | devin | antigravity | goose). Set by handler.ts from --cli. */ cli?: IntegrationType; }