failproofai 0.0.14-beta.0 → 0.0.14-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.codex/hooks.json +6 -6
- package/.next/standalone/.cursor/hooks.json +6 -6
- package/.next/standalone/.factory/hooks.json +9 -9
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +5 -5
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q2nbsl._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xuaoik._.js +3 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00cz9as._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__015_i4t._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03lv-pe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__042d3cd._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09q1168._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0brjczq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h12me5._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hdsupo._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qeuy9c._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s89hoe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07qfsfa._.js → [root-of-the-server]__0wp9w6i._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ynf7tx._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11rtg6s._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__14dd6h8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17ai7sy._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dky4g0._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tkjqka._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__212nf49._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_087bt9w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_1fvisnp._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_209u41o._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_0g5r64e.js → node_modules_html-to-image_es_index_12rjfea.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0-uvagv.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_03c7gi5.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_09p-8om.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0v-kfiu.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0xrgzyz.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1806lsc.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1j6dd-e.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1sa65r-.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
- package/.next/standalone/.next/static/chunks/{13f1kmjea-0md.js → 02jcotghcxrbu.js} +1 -1
- package/.next/standalone/.next/static/chunks/{17sdztgbcc0fi.js → 0d5lypu1zs21r.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2ufuwhv03yvtb.js → 0emg30_6svycu.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1374x9bv_s_vg.js → 0o9wpnze1ey8y.js} +1 -1
- package/.next/standalone/.next/static/chunks/{454dyizqik6t9.js → 1359o5785rmve.js} +1 -1
- package/.next/standalone/.next/static/chunks/{43weu8f59tlix.js → 20tuctw38jbgw.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0405htbxay9vd.js → 2ldhnaweu9ew3.js} +1 -1
- package/.next/standalone/.next/static/chunks/{003uz088wbmz7.js → 2vav_x0_336ds.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0iinhn4417nk1.js → 3-ogr79xqbw0h.js} +1 -1
- package/.next/standalone/.next/static/chunks/36j05kb2x323i.css +2 -0
- package/.next/standalone/.next/static/chunks/{35-90m9py64d4.js → 3cg7x0joi2b98.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2o3ql0n5owqsw.js → 3k87p-tijud-4.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2m_tbl39fcedn.js → 3kmaqcp7rldad.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-0vavqo2d-tlt3.js → turbopack-10hj8t5pf33tu.js} +1 -1
- package/.next/standalone/.opencode/plugins/failproofai.mjs +30 -11
- package/.next/standalone/app/components/global-error-listeners.tsx +27 -0
- package/.next/standalone/ci/cli-integration/Dockerfile +47 -0
- package/.next/standalone/ci/cli-integration/README.md +69 -0
- package/.next/standalone/ci/cli-integration/canary-policies.mjs +45 -0
- package/.next/standalone/ci/cli-integration/capture-tokens.sh +48 -0
- package/.next/standalone/ci/cli-integration/inject-tokens.sh +44 -0
- package/.next/standalone/ci/cli-integration/install-clis.sh +91 -0
- package/.next/standalone/ci/cli-integration/probe-cli.sh +208 -0
- package/.next/standalone/ci/cli-integration/report.js +74 -0
- package/.next/standalone/ci/cli-integration/run.sh +107 -0
- package/.next/standalone/components/reach-developers.tsx +1 -1
- package/.next/standalone/docker-hook-sync/Dockerfile +8 -4
- package/.next/standalone/lib/error-origin.ts +67 -0
- package/.next/standalone/lib/install-check.ts +231 -0
- package/.next/standalone/package.json +1 -3
- package/.next/standalone/server.js +1 -1
- package/README.md +1 -17
- package/bin/failproofai.mjs +11 -1
- package/dist/cli.mjs +479 -217
- package/lib/error-origin.ts +67 -0
- package/lib/install-check.ts +231 -0
- package/package.json +1 -3
- package/scripts/dev-hook.mjs +292 -0
- package/scripts/install-diagnosis.mjs +5 -8
- package/scripts/install-telemetry.mjs +12 -4
- package/scripts/translate-docs/cli.ts +47 -1
- package/scripts/translate-docs/mdx-translator.ts +133 -4
- package/scripts/translate-docs/readme-translator.ts +13 -5
- package/scripts/translate-docs/translator.ts +38 -2
- package/scripts/validate-mdx.ts +12 -4
- package/src/audit/cli.ts +41 -9
- package/src/hooks/tui.ts +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ge8e7w._.js +0 -3
- package/.next/standalone/.next/static/chunks/3r--yyjkp9_7b.css +0 -2
- package/scripts/postinstall.mjs +0 -242
- package/scripts/preuninstall.mjs +0 -131
- /package/.next/standalone/.next/static/{MNJWFb2jLRrxRKGFNK6Hy → LkN-JjmhKGmL-WklH7WVi}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{MNJWFb2jLRrxRKGFNK6Hy → LkN-JjmhKGmL-WklH7WVi}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{MNJWFb2jLRrxRKGFNK6Hy → LkN-JjmhKGmL-WklH7WVi}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# CLI integration tests
|
|
2
|
+
|
|
3
|
+
A daily **live-enforcement integration test** for failproofai. It answers one
|
|
4
|
+
question the unit/e2e suites can't: *does failproofai still enforce against every
|
|
5
|
+
supported agent CLI, at the versions users actually install today?*
|
|
6
|
+
|
|
7
|
+
Every day (`.github/workflows/cli-integration.yml`) it installs all 12 agent
|
|
8
|
+
CLIs **@latest** into an isolated Docker sandbox, drives each one against
|
|
9
|
+
failproofai's own policies (built from this repo's HEAD), and confirms the hook
|
|
10
|
+
log shows a **DENY**. A *silent-allow* — a blocked action that ran with no deny —
|
|
11
|
+
means enforcement broke against that CLI (e.g. a vendor changed their hook schema
|
|
12
|
+
out from under us). The test asserts the deny **positively**, so drift surfaces
|
|
13
|
+
as a red run + a Slack alert instead of going unnoticed until a user hits it.
|
|
14
|
+
|
|
15
|
+
## Why it's separate from `__tests__/`
|
|
16
|
+
|
|
17
|
+
It drives **real vendor CLIs against real gateway models** — it needs network,
|
|
18
|
+
Docker, credentials, and ~7-10 min, none of which belong in the fast in-process
|
|
19
|
+
vitest suites. So it's a scheduled workflow, not a PR gate.
|
|
20
|
+
|
|
21
|
+
## How a run works
|
|
22
|
+
|
|
23
|
+
1. Build failproofai under test (`dist/index.js` + `dist/cli.mjs`) from this repo.
|
|
24
|
+
2. Restore `cli-integration-state.json` from Actions cache (version-gate +
|
|
25
|
+
broke/recovered diff).
|
|
26
|
+
3. Build the sandbox image, install all 12 CLIs (`install-clis.sh`), inject the
|
|
27
|
+
OAuth credential files from secrets (`inject-tokens.sh`).
|
|
28
|
+
4. `run.sh` probes each non-gated CLI (`probe-cli.sh`), builds the report
|
|
29
|
+
(`report.js`), and POSTs it to Slack.
|
|
30
|
+
|
|
31
|
+
**Version-gate:** a CLI is re-probed only when its binary version **or**
|
|
32
|
+
failproofai's HEAD changed since its last green run — unchanged ⇒ can't have
|
|
33
|
+
drifted ⇒ skip, protecting LLM/vendor quota. `FAIL`/`INCONCLUSIVE`/`ERROR` always
|
|
34
|
+
re-probe until they recover. Dispatch with **force** to probe everything.
|
|
35
|
+
|
|
36
|
+
## Auth & secrets (the `cli-integration` Environment)
|
|
37
|
+
|
|
38
|
+
Because this repo is public, all credentials live in a scoped **GitHub
|
|
39
|
+
Environment** (`cli-integration`) — only this workflow's job can read them — and
|
|
40
|
+
the workflow triggers on `schedule`/`workflow_dispatch` **only**, so fork PRs can
|
|
41
|
+
never reach them.
|
|
42
|
+
|
|
43
|
+
| Auth | CLIs | Secret(s) |
|
|
44
|
+
|------|------|-----------|
|
|
45
|
+
| Env-var (gateway) | claude, codex, goose, opencode, pi, hermes, openclaw, factory | `CANARY_LLM_API_KEY`, `CANARY_LLM_BASE_URL` |
|
|
46
|
+
| Env-var (PAT) | copilot | `COPILOT_GITHUB_TOKEN` |
|
|
47
|
+
| Injected token file | cursor, devin, antigravity | `CURSOR_/DEVIN_/ANTIGRAVITY_TOKEN_TGZ_B64` |
|
|
48
|
+
| Delivery | — | `SLACK_WEBHOOK_URL` |
|
|
49
|
+
|
|
50
|
+
The injected-token CLIs carry OAuth session tokens captured from a logged-in
|
|
51
|
+
machine (see `capture-tokens.sh`). They authenticate on a fresh runner, but may
|
|
52
|
+
eventually expire — when that happens the test reports `⚠️ ERROR` for that CLI;
|
|
53
|
+
re-login on the capture machine and refresh the secret.
|
|
54
|
+
|
|
55
|
+
> Note: env-var / secret names keep the historical `CANARY_` prefix internally —
|
|
56
|
+
> renaming them across the harness would be churn with no user-visible benefit.
|
|
57
|
+
|
|
58
|
+
## Files
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
Dockerfile non-root sandbox base image
|
|
62
|
+
install-clis.sh install/upgrade all 12 CLIs @latest (with retries)
|
|
63
|
+
inject-tokens.sh write captured OAuth creds into the fresh volume
|
|
64
|
+
probe-cli.sh live enforcement probe for ONE CLI (the oracle)
|
|
65
|
+
canary-policies.mjs benign-marker custom policies the probe trips
|
|
66
|
+
run.sh orchestrator (gate → probe → report → Slack)
|
|
67
|
+
report.js build the Slack report + diff state (broke/recovered)
|
|
68
|
+
capture-tokens.sh (run on a logged-in machine) refresh the OAuth token secrets
|
|
69
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canary custom policies — the DEFINITE probe.
|
|
3
|
+
*
|
|
4
|
+
* The canary probes use BENIGN actions an agent never refuses: `echo`-ing a
|
|
5
|
+
* token, and reading a plain local file. Because there's nothing scary about
|
|
6
|
+
* them, the model reliably issues the tool call — eliminating the self-censorship
|
|
7
|
+
* that made sudo/.env/system-read probes come back INCONCLUSIVE.
|
|
8
|
+
*
|
|
9
|
+
* These policies intercept exactly those benign markers. A deny in the hook log
|
|
10
|
+
* is therefore positive proof that the full enforcement pipeline works against
|
|
11
|
+
* the CLI's REAL tool payload: hook fired → payload normalized (tool_name +
|
|
12
|
+
* tool_input.command / file_path — the drift-prone fields behind the Copilot
|
|
13
|
+
* 1.0.70 silent-allow) → policy evaluated → deny emitted. If normalization ever
|
|
14
|
+
* drifts, ctx.toolInput.command/file_path go empty, no deny fires, the benign
|
|
15
|
+
* command runs, and the probe reports FAIL — never a false alarm, never a miss.
|
|
16
|
+
*/
|
|
17
|
+
import { customPolicies, allow, deny } from "failproofai";
|
|
18
|
+
|
|
19
|
+
// Bash-command path: deny a benign shell command carrying the probe token.
|
|
20
|
+
customPolicies.add({
|
|
21
|
+
name: "canary-bash",
|
|
22
|
+
description: "Canary: deny the benign probe shell command (CANARY_PROBE token)",
|
|
23
|
+
match: { events: ["PreToolUse"] },
|
|
24
|
+
fn: async (ctx) => {
|
|
25
|
+
if (ctx.toolName !== "Bash") return allow();
|
|
26
|
+
const cmd = String(ctx.toolInput?.command ?? "");
|
|
27
|
+
if (cmd.includes("CANARY_PROBE")) return deny("canary-bash probe intercepted");
|
|
28
|
+
return allow();
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// File-path path: deny reading the benign marker file (via Read tool OR `cat`).
|
|
33
|
+
customPolicies.add({
|
|
34
|
+
name: "canary-read",
|
|
35
|
+
description: "Canary: deny reading the benign probe marker file (CANARY_MARKER)",
|
|
36
|
+
match: { events: ["PreToolUse"] },
|
|
37
|
+
fn: async (ctx) => {
|
|
38
|
+
const path = String(ctx.toolInput?.file_path ?? "");
|
|
39
|
+
const cmd = String(ctx.toolInput?.command ?? "");
|
|
40
|
+
if (path.includes("CANARY_MARKER") || cmd.includes("CANARY_MARKER")) {
|
|
41
|
+
return deny("canary-read probe intercepted");
|
|
42
|
+
}
|
|
43
|
+
return allow();
|
|
44
|
+
},
|
|
45
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
# capture-tokens.sh — run on the OFFICE BOX (has the logged-in canary volume).
|
|
4
|
+
# ./capture-tokens.sh <owner/repo>
|
|
5
|
+
#
|
|
6
|
+
# Captures each OAuth CLI's credential tree from the persistent HOME volume as a
|
|
7
|
+
# base64 gzip-tar rooted at $HOME, and stores it as a GitHub Actions secret on
|
|
8
|
+
# the canary repo. The CI workflow decodes it back onto the ephemeral runner's
|
|
9
|
+
# volume (see sandbox/inject-tokens.sh), so cursor/devin/antigravity authenticate
|
|
10
|
+
# in CI without an interactive login.
|
|
11
|
+
#
|
|
12
|
+
# Re-run this whenever the canary reports ERROR (not-logged-in / 401) for one of
|
|
13
|
+
# these CLIs — i.e. after you re-login on the box — to refresh the secret.
|
|
14
|
+
#
|
|
15
|
+
# NB: these are real credentials for internal@exosphere.host. They live only in
|
|
16
|
+
# GitHub *encrypted secrets*, never in the repo tree.
|
|
17
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
REPO="${1:?usage: capture-tokens.sh <owner/repo>}"
|
|
20
|
+
VOL="${CANARY_VOL:-failproofai-canary-home}"
|
|
21
|
+
IMAGE="${CANARY_IMAGE:-failproofai-canary:base}"
|
|
22
|
+
|
|
23
|
+
# secret-name → $HOME-relative path(s) to tar from the volume
|
|
24
|
+
capture() {
|
|
25
|
+
local secret="$1"; shift
|
|
26
|
+
local paths=("$@")
|
|
27
|
+
echo "── $secret ($(printf '%s ' "${paths[@]}"))"
|
|
28
|
+
# Tar the paths (rooted at $HOME) inside the container, gzip, base64 → host.
|
|
29
|
+
local b64
|
|
30
|
+
b64="$(docker run --rm -v "$VOL:/home/canary" "$IMAGE" bash -c "
|
|
31
|
+
cd \$HOME || exit 1
|
|
32
|
+
miss=1
|
|
33
|
+
for p in ${paths[*]}; do [ -e \"\$p\" ] && miss=0; done
|
|
34
|
+
[ \$miss = 1 ] && { echo 'MISSING' >&2; exit 3; }
|
|
35
|
+
tar -czf - ${paths[*]} 2>/dev/null | base64 -w0
|
|
36
|
+
")" || { echo " ⚠️ none of the paths exist in the volume — skipping (login first)"; return 0; }
|
|
37
|
+
if [ -z "$b64" ]; then echo " ⚠️ empty capture — skipping"; return 0; fi
|
|
38
|
+
printf '%s' "$b64" | gh secret set "$secret" --repo "$REPO" --body -
|
|
39
|
+
echo " ✓ set $secret (${#b64} b64 chars)"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
echo "Capturing OAuth credential trees from volume '$VOL' → secrets on $REPO"
|
|
43
|
+
capture CURSOR_TOKEN_TGZ_B64 .config/cursor
|
|
44
|
+
capture DEVIN_TOKEN_TGZ_B64 .local/share/devin/credentials.toml
|
|
45
|
+
# antigravity: capture ONLY the small OAuth token file — the full ~/.gemini tree
|
|
46
|
+
# is ~15 MB (CLI binary + brain/ + logs), far over GitHub's 48 KB secret cap.
|
|
47
|
+
capture ANTIGRAVITY_TOKEN_TGZ_B64 .gemini/antigravity-cli/antigravity-oauth-token
|
|
48
|
+
echo "done."
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
# Token injection — runs INSIDE the sandbox container, against the fresh per-run
|
|
4
|
+
# HOME volume, BEFORE the probes.
|
|
5
|
+
#
|
|
6
|
+
# On the office box the OAuth logins (cursor / devin / antigravity) live in a
|
|
7
|
+
# PERSISTENT volume. GitHub-Actions runners are ephemeral, so instead each of
|
|
8
|
+
# those credential trees is captured once (see capture-tokens.sh) into a GitHub
|
|
9
|
+
# secret as a base64 gzip-tar rooted at $HOME, decoded by the workflow into
|
|
10
|
+
# /opt/tokens/<name>.tgz, and extracted here into the fresh volume's $HOME.
|
|
11
|
+
#
|
|
12
|
+
# Extraction is rooted at $HOME and the tarballs were created with $HOME-relative
|
|
13
|
+
# paths (e.g. `.config/cursor`, `.local/share/devin/credentials.toml`), so each
|
|
14
|
+
# file lands exactly where its CLI expects it. Env-var-auth CLIs (gateway key,
|
|
15
|
+
# copilot PAT, factory BYOK) need NO file here — they authenticate from env.
|
|
16
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
set -u
|
|
18
|
+
TOK="${1:-/opt/tokens}"
|
|
19
|
+
|
|
20
|
+
if [ ! -d "$TOK" ]; then
|
|
21
|
+
echo "inject-tokens: no token dir at $TOK — nothing to inject (env-var CLIs only)"
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
shopt -s nullglob
|
|
26
|
+
found=0
|
|
27
|
+
for t in "$TOK"/*.tgz "$TOK"/*.tar.gz; do
|
|
28
|
+
found=1
|
|
29
|
+
echo "── injecting $(basename "$t") → \$HOME ──"
|
|
30
|
+
if tar -C "$HOME" -xzf "$t"; then
|
|
31
|
+
tar -tzf "$t" 2>/dev/null | sed 's/^/ /' | head -8
|
|
32
|
+
else
|
|
33
|
+
echo " WARN: extract failed for $t (skipping — that CLI will report ERROR)"
|
|
34
|
+
fi
|
|
35
|
+
done
|
|
36
|
+
[ "$found" = 0 ] && echo "inject-tokens: token dir present but empty — env-var CLIs only"
|
|
37
|
+
|
|
38
|
+
echo "── credential files now present in volume ──"
|
|
39
|
+
for f in "$HOME/.config/cursor/auth.json" \
|
|
40
|
+
"$HOME/.local/share/devin/credentials.toml" \
|
|
41
|
+
"$HOME/.gemini/antigravity-cli"; do
|
|
42
|
+
[ -e "$f" ] && echo " ✓ $f" || echo " ✗ $f (absent)"
|
|
43
|
+
done
|
|
44
|
+
exit 0
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
# Tier-0 install probe — runs INSIDE the canary sandbox container.
|
|
4
|
+
#
|
|
5
|
+
# Installs/upgrades all 12 agent CLIs to @latest into the persistent HOME volume,
|
|
6
|
+
# then records which resolve to a runnable binary + their version. This is both
|
|
7
|
+
# the daily "fresh upgrade" step AND the Tier-0 canary signal (packaging/binary
|
|
8
|
+
# breaks show up here as a FAIL).
|
|
9
|
+
#
|
|
10
|
+
# Install methods verified 2026-07-16 (see failproofai-cli-canary-design.md).
|
|
11
|
+
# Vendor installers scatter binaries across several dirs, so we search an
|
|
12
|
+
# expanded PATH and fall back to a bounded `find` under $HOME.
|
|
13
|
+
#
|
|
14
|
+
# Output: a JSON array at $CANARY_RESULTS (default ~/canary-tier0.json) + a
|
|
15
|
+
# human table on stdout. Never `set -e` — one CLI failing must not abort the rest.
|
|
16
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
set -u
|
|
18
|
+
|
|
19
|
+
RESULTS="${CANARY_RESULTS:-$HOME/canary-tier0.json}"
|
|
20
|
+
LOGDIR="${CANARY_LOGDIR:-$HOME/canary-logs}"
|
|
21
|
+
mkdir -p "$LOGDIR"
|
|
22
|
+
|
|
23
|
+
# Cover every dir the vendor installers are known to drop binaries into.
|
|
24
|
+
export PATH="$HOME/.npm-global/bin:$HOME/.local/bin:$HOME/.factory/bin:$HOME/.hermes/bin:$HOME/.cursor/bin:$HOME/.codex/bin:$HOME/bin:$PATH"
|
|
25
|
+
|
|
26
|
+
ROWS=()
|
|
27
|
+
|
|
28
|
+
probe() {
|
|
29
|
+
local id="$1" bin="$2" vflag="$3" install="$4"
|
|
30
|
+
echo "════════════════════ $id ════════════════════"
|
|
31
|
+
local t0 t1 rc=1; t0=$SECONDS
|
|
32
|
+
# Retry transient install failures. Flaky vendor CDNs / HTTP-2 stream resets are
|
|
33
|
+
# common on ephemeral CI runners (e.g. factory's `curl … | sh` dropping the
|
|
34
|
+
# download mid-stream with `curl: (18)`). Break as soon as the binary resolves;
|
|
35
|
+
# otherwise give it up to 3 attempts so one bad download isn't a false FAIL.
|
|
36
|
+
for attempt in 1 2 3; do
|
|
37
|
+
bash -c "$install" >"$LOGDIR/$id.install.log" 2>&1
|
|
38
|
+
rc=$?
|
|
39
|
+
hash -r 2>/dev/null
|
|
40
|
+
command -v "$bin" >/dev/null 2>&1 && break
|
|
41
|
+
find "$HOME" -maxdepth 5 -name "$bin" -type f -perm -u+x 2>/dev/null | grep -q . && break
|
|
42
|
+
[ "$attempt" -lt 3 ] && { echo " ↻ $id install attempt $attempt failed (rc=$rc) — retrying in 5s"; sleep 5; }
|
|
43
|
+
done
|
|
44
|
+
t1=$SECONDS
|
|
45
|
+
hash -r 2>/dev/null
|
|
46
|
+
|
|
47
|
+
# Resolve the binary: PATH first, then a bounded find under $HOME.
|
|
48
|
+
local resolved; resolved="$(command -v "$bin" 2>/dev/null || true)"
|
|
49
|
+
if [ -z "$resolved" ]; then
|
|
50
|
+
resolved="$(find "$HOME" -maxdepth 5 -name "$bin" -type f -perm -u+x 2>/dev/null | head -1)"
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
local ver="" status="FAIL"
|
|
54
|
+
if [ -n "$resolved" ]; then
|
|
55
|
+
status="OK"
|
|
56
|
+
ver="$("$resolved" $vflag 2>&1 | head -1 | tr -cd '[:alnum:] ._+:/-')"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
printf ' rc=%s time=%ss bin=%s ver=%s => %s\n' \
|
|
60
|
+
"$rc" "$((t1 - t0))" "${resolved:-<none>}" "${ver:-<none>}" "$status"
|
|
61
|
+
[ "$status" = FAIL ] && echo " ↳ last install log lines:" && tail -3 "$LOGDIR/$id.install.log" | sed 's/^/ /'
|
|
62
|
+
|
|
63
|
+
ROWS+=("$(printf '{"cli":"%s","status":"%s","binary":"%s","version":"%s","install_rc":%s,"secs":%s}' \
|
|
64
|
+
"$id" "$status" "${resolved:-}" "${ver:-}" "$rc" "$((t1 - t0))")")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# id binary --ver flag install command (@latest)
|
|
68
|
+
probe claude claude "--version" 'curl -fsSL https://claude.ai/install.sh | bash'
|
|
69
|
+
probe codex codex "--version" 'npm install -g @openai/codex@latest'
|
|
70
|
+
probe copilot copilot "--version" 'npm install -g @github/copilot@latest'
|
|
71
|
+
probe cursor cursor-agent "--version" 'curl https://cursor.com/install -fsS | bash'
|
|
72
|
+
probe opencode opencode "--version" 'npm install -g opencode-ai@latest'
|
|
73
|
+
probe pi pi "--version" 'npm install -g @mariozechner/pi-coding-agent@latest'
|
|
74
|
+
probe hermes hermes "--version" 'curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash'
|
|
75
|
+
probe openclaw openclaw "--version" 'npm install -g openclaw@latest'
|
|
76
|
+
probe factory droid "--version" 'curl -fsSL https://app.factory.ai/cli | sh'
|
|
77
|
+
probe devin devin "--version" 'curl -fsSL https://cli.devin.ai/install.sh | bash'
|
|
78
|
+
probe antigravity agy "--version" 'curl -fsSL https://antigravity.google/cli/install.sh | bash'
|
|
79
|
+
probe goose goose "--version" 'curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash'
|
|
80
|
+
|
|
81
|
+
# Emit JSON array.
|
|
82
|
+
{ printf '['; IFS=,; printf '%s' "${ROWS[*]}"; printf ']\n'; } > "$RESULTS"
|
|
83
|
+
|
|
84
|
+
echo
|
|
85
|
+
echo "════════════════════ TIER-0 SUMMARY ════════════════════"
|
|
86
|
+
ok=0; fail=0
|
|
87
|
+
for r in "${ROWS[@]}"; do
|
|
88
|
+
case "$r" in *'"status":"OK"'*) ok=$((ok+1));; *) fail=$((fail+1));; esac
|
|
89
|
+
done
|
|
90
|
+
printf 'installed OK: %s / 12 failed: %s\n' "$ok" "$fail"
|
|
91
|
+
echo "results JSON: $RESULTS"
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
# Live Tier-2 enforcement probe for ONE CLI. Runs INSIDE the sandbox container.
|
|
4
|
+
# Usage: probe-cli.sh <cli>
|
|
5
|
+
#
|
|
6
|
+
# Hooks via failproofai's OWN installer, pointed at repo main HEAD (a wrapper +
|
|
7
|
+
# FAILPROOFAI_BINARY_OVERRIDE at user scope; project-scope fallback swaps the
|
|
8
|
+
# `npx -y failproofai` command to `bun /repo/…`). Only wire()/drive() are per-CLI.
|
|
9
|
+
#
|
|
10
|
+
# Verdict (design 3-way): PASS = oracle log shows expected deny; FAIL = forbidden
|
|
11
|
+
# side-effect leaked; INCONCLUSIVE = model never attempted the tool. LOG_LEVEL=info
|
|
12
|
+
# is mandatory (oracle lines are INFO).
|
|
13
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
14
|
+
set -u
|
|
15
|
+
CLI="${1:?usage: probe-cli.sh <cli>}"
|
|
16
|
+
: "${CANARY_LLM_API_KEY:?gateway key missing}"
|
|
17
|
+
# Gateway default model. deepseek-v4-pro is cheapest AND works on both the OpenAI
|
|
18
|
+
# chat-completions and Responses paths. EXCEPTION: the claude CLI speaks Anthropic
|
|
19
|
+
# tool-use, which deepseek-via-/v1/messages doesn't emit correctly (all probes go
|
|
20
|
+
# INCONCLUSIVE), so claude pins to the cheapest Anthropic model via CANARY_CLAUDE_MODEL.
|
|
21
|
+
: "${CANARY_LLM_MODEL:=deepseek-v4-pro}"
|
|
22
|
+
: "${CANARY_CLAUDE_MODEL:=claude-haiku-4-5}"
|
|
23
|
+
# pi sends an `include` (encrypted reasoning) param deepseek rejects (400) → pin to Anthropic.
|
|
24
|
+
: "${CANARY_PI_MODEL:=claude-haiku-4-5}"
|
|
25
|
+
# Gateway base URL — overridable via env (CI supplies it as a secret). Strip any
|
|
26
|
+
# trailing slash so the `$GW/v1` joins below never produce `//v1`.
|
|
27
|
+
GW="${CANARY_LLM_BASE_URL:-https://models.aikin.club}"; GW="${GW%/}"
|
|
28
|
+
|
|
29
|
+
# The custom-policy loader writes an ESM shim NEXT TO the dist index, but /repo
|
|
30
|
+
# is mounted read-only (EROFS). So point FAILPROOFAI_DIST_PATH at a WRITABLE copy
|
|
31
|
+
# of /repo/dist (bin/failproofai.mjs only sets it when unset, so this wins). Kept
|
|
32
|
+
# fresh each run so it tracks main HEAD's built dist.
|
|
33
|
+
FP_DIST="$HOME/fp-dist"
|
|
34
|
+
# Recreate from scratch each run: the volume can persist across runs, so overlaying
|
|
35
|
+
# with `cp` would leave files a newer HEAD removed behind (mixed build). Fail loudly
|
|
36
|
+
# rather than probe against a stale/partial dist.
|
|
37
|
+
rm -rf "$FP_DIST"; mkdir -p "$FP_DIST"
|
|
38
|
+
cp -r /repo/dist/. "$FP_DIST/" || { echo "failed to prepare failproofai dist from /repo/dist" >&2; exit 1; }
|
|
39
|
+
export FAILPROOFAI_DIST_PATH="$FP_DIST" FAILPROOFAI_TELEMETRY_DISABLED=1 FAILPROOFAI_LOG_LEVEL=info
|
|
40
|
+
export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$HOME/.factory/bin:$PATH"
|
|
41
|
+
|
|
42
|
+
mkdir -p "$HOME/bin"
|
|
43
|
+
printf '#!/bin/sh\nexec bun /repo/bin/failproofai.mjs "$@"\n' > "$HOME/bin/failproofai"
|
|
44
|
+
chmod +x "$HOME/bin/failproofai"
|
|
45
|
+
export FAILPROOFAI_BINARY_OVERRIDE="$HOME/bin/failproofai"
|
|
46
|
+
|
|
47
|
+
BASE="$HOME/probe-$CLI"
|
|
48
|
+
# DEFINITE probes: BENIGN actions (echo/touch a token, read a plain file) the
|
|
49
|
+
# model never refuses → a tool call is guaranteed, so no INCONCLUSIVE from
|
|
50
|
+
# self-censorship. A custom canary policy denies exactly those benign markers,
|
|
51
|
+
# so a deny proves the enforcement pipeline works on the CLI's real payload.
|
|
52
|
+
POLICIES=(block-read-outside-cwd) # one builtin so install is non-interactive
|
|
53
|
+
# The loader writes a temp file next to the custom policy, so it must live in a
|
|
54
|
+
# WRITABLE dir (/opt/canary is read-only). Copy it into HOME.
|
|
55
|
+
CUSTOM_POLICIES="$HOME/canary-policies.mjs"
|
|
56
|
+
cp /opt/canary/canary-policies.mjs "$CUSTOM_POLICIES"
|
|
57
|
+
|
|
58
|
+
install_hooks() {
|
|
59
|
+
# Prefer user scope (writes the override → main HEAD). Fall back to project +
|
|
60
|
+
# command-swap for CLIs without a user scope. `-c` loads the custom canary policies.
|
|
61
|
+
if bun /repo/bin/failproofai.mjs policies --install "${POLICIES[@]}" --cli "$CLI" --scope user -c "$CUSTOM_POLICIES" >/dev/null 2>&1; then
|
|
62
|
+
return 0
|
|
63
|
+
fi
|
|
64
|
+
( cd "$BASE" && bun /repo/bin/failproofai.mjs policies --install "${POLICIES[@]}" --cli "$CLI" --scope project -c "$CUSTOM_POLICIES" >/dev/null 2>&1 )
|
|
65
|
+
grep -rl "npx -y failproofai" "$BASE" 2>/dev/null | while read -r f; do
|
|
66
|
+
sed -i 's#npx -y failproofai#bun /repo/bin/failproofai.mjs#g' "$f"
|
|
67
|
+
done
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
wire() { # point the CLI at the gateway via env/config only (no interactive wizard)
|
|
71
|
+
case "$CLI" in
|
|
72
|
+
claude) export ANTHROPIC_BASE_URL="$GW" ANTHROPIC_AUTH_TOKEN="$CANARY_LLM_API_KEY" ;;
|
|
73
|
+
opencode) printf '{"provider":{"gw":{"npm":"@ai-sdk/openai-compatible","options":{"baseURL":"%s/v1","apiKey":"%s"},"models":{"%s":{}}}}}' \
|
|
74
|
+
"$GW" "$CANARY_LLM_API_KEY" "$CANARY_LLM_MODEL" > "$BASE/opencode.json" ;;
|
|
75
|
+
goose) export GOOSE_PROVIDER=openai GOOSE_MODEL="$CANARY_LLM_MODEL" GOOSE_MODE=auto \
|
|
76
|
+
OPENAI_API_KEY="$CANARY_LLM_API_KEY" OPENAI_HOST="$GW" OPENAI_BASE_PATH="v1/chat/completions" ;;
|
|
77
|
+
hermes) mkdir -p "$HOME/.hermes"
|
|
78
|
+
cat >> "$HOME/.hermes/config.yaml" <<YAML
|
|
79
|
+
|
|
80
|
+
model:
|
|
81
|
+
provider: custom
|
|
82
|
+
base_url: $GW/v1
|
|
83
|
+
api_key: $CANARY_LLM_API_KEY
|
|
84
|
+
model: $CANARY_LLM_MODEL
|
|
85
|
+
max_tokens: 8192
|
|
86
|
+
custom_providers:
|
|
87
|
+
- name: gw
|
|
88
|
+
base_url: $GW/v1
|
|
89
|
+
api_key: $CANARY_LLM_API_KEY
|
|
90
|
+
model: $CANARY_LLM_MODEL
|
|
91
|
+
max_tokens: 8192
|
|
92
|
+
YAML
|
|
93
|
+
;;
|
|
94
|
+
pi) mkdir -p "$HOME/pi-gw" "$HOME/.pi/agent"
|
|
95
|
+
printf 'export default function (pi) { pi.registerProvider("openai", { baseUrl: "%s/v1" }); }\n' "$GW" > "$HOME/pi-gw/index.mjs"
|
|
96
|
+
node -e 'const fs=require("fs"),p=process.env.HOME+"/.pi/agent/settings.json";let s={};try{s=JSON.parse(fs.readFileSync(p,"utf8"))}catch{}s.packages=s.packages||[];const gw=process.env.HOME+"/pi-gw";if(!s.packages.includes(gw))s.packages.push(gw);fs.writeFileSync(p,JSON.stringify(s));'
|
|
97
|
+
# pi-extension does `node <override>`; our override is a shell wrapper it can't run.
|
|
98
|
+
# Unset so the extension self-resolves to /repo/dist/cli.mjs (node) or /repo/bin (bun) = main HEAD.
|
|
99
|
+
unset FAILPROOFAI_BINARY_OVERRIDE
|
|
100
|
+
export OPENAI_API_KEY="$CANARY_LLM_API_KEY" ;;
|
|
101
|
+
codex) : ;; # wired via -c flags in drive()
|
|
102
|
+
cursor) : ;; # uses the logged-in Cursor account (token in volume); no gateway
|
|
103
|
+
copilot) : ;; # auth via COPILOT_GITHUB_TOKEN env (personal acct, Copilot Free); no gateway
|
|
104
|
+
devin) : ;; # uses the logged-in Devin/Cognition account (token in volume); no gateway
|
|
105
|
+
antigravity) : ;; # uses the logged-in Google account (token in volume); no gateway
|
|
106
|
+
factory) mkdir -p "$HOME/.factory"
|
|
107
|
+
printf '{"custom_models":[{"model_display_name":"gw-haiku","model":"%s","base_url":"%s/v1","api_key":"%s","provider":"generic-chat-completion-api"}]}' \
|
|
108
|
+
"$CANARY_LLM_MODEL" "$GW" "$CANARY_LLM_API_KEY" > "$HOME/.factory/config.json" ;;
|
|
109
|
+
openclaw) openclaw onboard --non-interactive --accept-risk --skip-health --auth-choice custom-api-key \
|
|
110
|
+
--custom-provider-id gw --custom-base-url "$GW/v1" --custom-api-key "$CANARY_LLM_API_KEY" \
|
|
111
|
+
--custom-compatibility openai --custom-model-id "$CANARY_LLM_MODEL" --custom-text-input >/dev/null 2>&1
|
|
112
|
+
# onboard rewrites openclaw.json and drops the plugin — re-register it AFTER onboard,
|
|
113
|
+
# WITH the custom canary policies (-c) so canary-bash/canary-read stay registered.
|
|
114
|
+
bun /repo/bin/failproofai.mjs policies --install "${POLICIES[@]}" --cli openclaw --scope user \
|
|
115
|
+
-c "$CUSTOM_POLICIES" >/dev/null 2>&1
|
|
116
|
+
# open exec approval (both layers) so the agent issues tool calls headlessly
|
|
117
|
+
node -e 'const fs=require("fs"),p=process.env.HOME+"/.openclaw/openclaw.json";const c=JSON.parse(fs.readFileSync(p,"utf8"));c.tools=c.tools||{};c.tools.exec=Object.assign({},c.tools.exec,{security:"full",ask:"off",host:"gateway"});fs.writeFileSync(p,JSON.stringify(c,null,2));'
|
|
118
|
+
unset FAILPROOFAI_BINARY_OVERRIDE ;; # plugin does `node <override>`; unset → self-resolves to main HEAD
|
|
119
|
+
esac
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
drive() { # $1 = prompt ; run ONE prompt headless, executing tools without approval
|
|
123
|
+
case "$CLI" in
|
|
124
|
+
claude) ( cd "$BASE" && claude -p "$1" --model "$CANARY_CLAUDE_MODEL" --dangerously-skip-permissions 2>&1 ) ;;
|
|
125
|
+
opencode) ( cd "$BASE" && opencode run --auto -m "gw/$CANARY_LLM_MODEL" "$1" 2>&1 ) ;;
|
|
126
|
+
goose) ( cd "$BASE" && goose run --no-session -t "$1" 2>&1 ) ;;
|
|
127
|
+
hermes) ( cd "$BASE" && hermes --yolo -z "$1" 2>&1 ) ;;
|
|
128
|
+
pi) ( cd "$BASE" && pi --provider openai --model "openai/$CANARY_PI_MODEL" --api-key "$CANARY_LLM_API_KEY" -p "$1" 2>&1 ) ;;
|
|
129
|
+
codex) ( cd "$BASE" && codex exec --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust \
|
|
130
|
+
-c model_providers.gw.name="gw" -c model_providers.gw.base_url="$GW/v1" -c model_providers.gw.wire_api="responses" \
|
|
131
|
+
-c model_providers.gw.env_key="CANARY_LLM_API_KEY" -c model_provider="gw" \
|
|
132
|
+
-c model="${CANARY_CODEX_MODEL:-deepseek-v4-pro}" "$1" 2>&1 ) ;; # Claude models 400 on Bedrock metadata; deepseek is non-Bedrock
|
|
133
|
+
cursor) ( cd "$BASE" && cursor-agent -p --force "$1" 2>&1 ) ;;
|
|
134
|
+
copilot) ( cd "$BASE" && copilot -p "$1" --allow-all-tools 2>&1 ) ;;
|
|
135
|
+
devin) ( cd "$BASE" && devin -p "$1" --permission-mode dangerous --respect-workspace-trust false 2>&1 ) ;;
|
|
136
|
+
antigravity) ( cd "$BASE" && agy -p "$1" --model "${CANARY_ANTIGRAVITY_MODEL:-Gemini 3.5 Flash (Low)}" --dangerously-skip-permissions 2>&1 ) ;; # lightest model → least account-quota use
|
|
137
|
+
factory) ( cd "$BASE" && droid exec --auto high -m "custom:gw-haiku-0" "$1" 2>&1 ) ;;
|
|
138
|
+
openclaw) ( cd "$BASE" && timeout 150 openclaw agent --local --session-key "canary-$RANDOM$RANDOM" --model "gw/$CANARY_LLM_MODEL" -m "$1" 2>&1 ) ;;
|
|
139
|
+
*) echo "drive: $CLI not implemented" >&2; return 3 ;;
|
|
140
|
+
esac
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
rm -rf "$BASE"; mkdir -p "$BASE"
|
|
144
|
+
[ "$CLI" = hermes ] && rm -f "$HOME/.hermes/config.yaml" # fresh config each run (append idempotency)
|
|
145
|
+
|
|
146
|
+
# The benign marker file the read-probe asks the agent to read. Its content is a
|
|
147
|
+
# sentinel the model can't know without actually reading the file — so a leak of
|
|
148
|
+
# it into the transcript is a DEFINITE silent-allow (FAIL).
|
|
149
|
+
MARKER_CONTENT='CANARY_LEAK_SENTINEL_q7k2'
|
|
150
|
+
printf '%s\n' "$MARKER_CONTENT" > "$BASE/CANARY_MARKER.txt"
|
|
151
|
+
|
|
152
|
+
install_hooks
|
|
153
|
+
wire
|
|
154
|
+
|
|
155
|
+
denied() { grep -qE "result=deny policy=(failproofai/|custom/)?$1" "$2" 2>/dev/null; }
|
|
156
|
+
# The read probe accepts EITHER the benign canary-read deny OR block-read-outside-cwd:
|
|
157
|
+
# both are failproofai denies on the CLI's read payload, and BOTH require a populated
|
|
158
|
+
# tool_input.file_path (so a real normalization-drift still leaks the sentinel → FAIL,
|
|
159
|
+
# never a false PASS). Some CLIs (openclaw) resolve the marker's relative path into
|
|
160
|
+
# their OWN workspace dir, which sits outside the probe cwd — there block-read-outside-cwd
|
|
161
|
+
# legitimately fires before canary-read. Accepting either eliminates that false INCONCLUSIVE.
|
|
162
|
+
read_denied() { grep -qE "result=deny policy=(failproofai/|custom/)?(canary-read|block-read-outside-cwd)" "$1" 2>/dev/null; }
|
|
163
|
+
# Vendor quota / auth errors (Copilot-Free credits, antigravity Google quota,
|
|
164
|
+
# expired logins) → the CLI errors before any tool call. Report these DISTINCTLY
|
|
165
|
+
# (not as plain INCONCLUSIVE) so "can't test right now" ≠ "model just didn't try".
|
|
166
|
+
is_error() { printf '%s' "$1" | grep -qiE "quota|rate.?limit|upgrade your (subscription|plan)|too many requests|insufficient|not logged in|unauthor|forbidden|invalid.*(key|token|credential)|payment required|\\b(401|402|429)\\b"; }
|
|
167
|
+
|
|
168
|
+
ATTEMPTS=3 # retry up to N times to absorb LLM nondeterminism (flaky tool-callers)
|
|
169
|
+
|
|
170
|
+
# ── Probe A: benign shell command (canary-bash) ──────────────────────────────
|
|
171
|
+
# `touch CANARY_PROBE_ran` is maximally benign (agents run it without refusal),
|
|
172
|
+
# and the token trips canary-bash. PASS=deny logged; FAIL=file created (ran
|
|
173
|
+
# despite no deny); ERROR=vendor quota/auth failure; INCONCLUSIVE=no tool call.
|
|
174
|
+
LOGA="$BASE/log-bash"
|
|
175
|
+
BASH_PROMPT='Using your shell/bash tool, run exactly this command: touch CANARY_PROBE_ran'
|
|
176
|
+
OUTA=""
|
|
177
|
+
for _ in $(seq 1 $ATTEMPTS); do
|
|
178
|
+
export FAILPROOFAI_HOOK_LOG_FILE="$LOGA"; rm -f "$BASE/CANARY_PROBE_ran"
|
|
179
|
+
OUTA="$(drive "$BASH_PROMPT" 2>&1)"
|
|
180
|
+
denied canary-bash "$LOGA/hooks.log" && break
|
|
181
|
+
[ -f "$BASE/CANARY_PROBE_ran" ] && break
|
|
182
|
+
done
|
|
183
|
+
if denied canary-bash "$LOGA/hooks.log"; then VA=PASS
|
|
184
|
+
elif [ -f "$BASE/CANARY_PROBE_ran" ]; then VA=FAIL
|
|
185
|
+
elif is_error "$OUTA"; then VA=ERROR
|
|
186
|
+
else VA=INCONCLUSIVE; fi
|
|
187
|
+
|
|
188
|
+
# ── Probe B: benign file read (canary-read) ──────────────────────────────────
|
|
189
|
+
LOGB="$BASE/log-read"
|
|
190
|
+
READ_PROMPT='Read the file named CANARY_MARKER.txt in the current directory and print its exact contents.'
|
|
191
|
+
OUTB=""
|
|
192
|
+
for _ in $(seq 1 $ATTEMPTS); do
|
|
193
|
+
export FAILPROOFAI_HOOK_LOG_FILE="$LOGB"
|
|
194
|
+
OUTB="$(drive "$READ_PROMPT" 2>&1)"
|
|
195
|
+
read_denied "$LOGB/hooks.log" && break
|
|
196
|
+
printf '%s' "$OUTB" | grep -qF "$MARKER_CONTENT" && break
|
|
197
|
+
done
|
|
198
|
+
if read_denied "$LOGB/hooks.log"; then VB=PASS
|
|
199
|
+
elif printf '%s' "$OUTB" | grep -qF "$MARKER_CONTENT"; then VB=FAIL
|
|
200
|
+
elif is_error "$OUTB"; then VB=ERROR
|
|
201
|
+
else VB=INCONCLUSIVE; fi
|
|
202
|
+
|
|
203
|
+
echo "=== $CLI live Tier-2 verdicts (DEFINITE benign-marker probes) ==="
|
|
204
|
+
echo " Probe A (touch token → canary-bash) : $VA"
|
|
205
|
+
echo " Probe B (read marker → canary-read) : $VB"
|
|
206
|
+
echo "--- deny evidence in oracle ---"
|
|
207
|
+
grep -E "result=deny" "$LOGA/hooks.log" "$LOGB/hooks.log" 2>/dev/null | sed 's#.*/hooks.log:# #' | head -4
|
|
208
|
+
printf 'VERDICT_JSON {"cli":"%s","probes":{"bash":"%s","read":"%s"}}\n' "$CLI" "$VA" "$VB"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Build the Slack report from probe verdicts + diff against last run's state.
|
|
3
|
+
// Args: <resultsJson> <statePath> <model>
|
|
4
|
+
// Each result: {cli, probes:{...}, version?, gated?}. Prints report to stdout
|
|
5
|
+
// AND writes new state: clis[cli] = {probes, version}. Flags broke/recovered.
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const [, , resultsJson, statePath, model] = process.argv;
|
|
8
|
+
const results = JSON.parse(resultsJson);
|
|
9
|
+
|
|
10
|
+
let prev = { clis: {}, lastRun: null };
|
|
11
|
+
try { prev = JSON.parse(fs.readFileSync(statePath, "utf8")); } catch {}
|
|
12
|
+
|
|
13
|
+
const probesOf = (entry) => (entry ? (entry.probes || entry) : null); // compat old flat schema
|
|
14
|
+
const statusOf = (probes) => {
|
|
15
|
+
const v = Object.values(probes || {});
|
|
16
|
+
if (v.length === 0) return "grey";
|
|
17
|
+
if (v.includes("FAIL")) return "red";
|
|
18
|
+
if (v.includes("ERROR")) return "error"; // vendor quota/auth — can't test, not broken
|
|
19
|
+
if (v.includes("INCONCLUSIVE")) return "yellow";
|
|
20
|
+
return "green";
|
|
21
|
+
};
|
|
22
|
+
const emo = { green: "🟢", yellow: "🟡", red: "🔴", grey: "⚪", error: "⚠️" };
|
|
23
|
+
const rank = { green: 0, grey: 1, yellow: 1, error: 1, red: 2 };
|
|
24
|
+
|
|
25
|
+
const now = new Date().toISOString().replace(/\.\d+Z$/, "Z");
|
|
26
|
+
const lines = [`🧪 *failproofai CLI integration tests* · ${now} · model=${model}`];
|
|
27
|
+
let worst = "green";
|
|
28
|
+
const breaks = [], recoveries = [];
|
|
29
|
+
|
|
30
|
+
for (const r of results) {
|
|
31
|
+
const st = statusOf(r.probes);
|
|
32
|
+
const prevEntry = prev.clis && prev.clis[r.cli];
|
|
33
|
+
const prevSt = prevEntry ? statusOf(probesOf(prevEntry)) : null;
|
|
34
|
+
let mark = "";
|
|
35
|
+
if (r.gated) {
|
|
36
|
+
mark = ` 🔵 _gated: CLI v${r.version || "?"} + failproofai ${r.fpSha || "?"} unchanged, last green_`;
|
|
37
|
+
} else if (prevSt && prevSt !== "red" && st === "red") {
|
|
38
|
+
mark = " ⚠️ *BROKE*"; breaks.push(r.cli);
|
|
39
|
+
} else if (prevSt === "red" && st === "green") {
|
|
40
|
+
// Only a green result is a genuine recovery — ERROR/INCONCLUSIVE/no-verdict
|
|
41
|
+
// are "couldn't confirm", not "fixed".
|
|
42
|
+
mark = " ✅ *recovered*"; recoveries.push(r.cli);
|
|
43
|
+
}
|
|
44
|
+
if (rank[st] > rank[worst]) worst = st;
|
|
45
|
+
const probes = Object.entries(r.probes || {}).map(([k, v]) => `${k}=${v}`).join(" ") || "no verdict";
|
|
46
|
+
lines.push(`${emo[st]} *${r.cli}* ${probes}${mark}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const errored = results.filter((r) => statusOf(r.probes) === "error").map((r) => r.cli);
|
|
50
|
+
let hdr;
|
|
51
|
+
if (breaks.length) hdr = `🔴 *ENFORCEMENT BROKEN*: ${breaks.join(", ")} — investigate now`;
|
|
52
|
+
// Any CLI still red dominates the header — a recovery elsewhere must not mask it.
|
|
53
|
+
else if (worst === "red") hdr = "🔴 still broken (unchanged)";
|
|
54
|
+
else if (recoveries.length) hdr = `✅ recovered: ${recoveries.join(", ")}`;
|
|
55
|
+
else if (errored.length) hdr = `⚠️ enforcing; couldn't test ${errored.join(", ")} (quota/auth)`;
|
|
56
|
+
else if (worst === "yellow" || worst === "grey") hdr = "🟡 all enforcing where the model engaged (some inconclusive)";
|
|
57
|
+
else hdr = `🟢 all ${results.length} enforcing`;
|
|
58
|
+
lines.push("", hdr);
|
|
59
|
+
|
|
60
|
+
// Start from the previous state so CLIs omitted from a subset run (run.sh cursor
|
|
61
|
+
// devin) keep their green gating records instead of being wiped + needlessly re-probed.
|
|
62
|
+
const newState = { lastRun: new Date().toISOString(), clis: { ...(prev.clis || {}) } };
|
|
63
|
+
for (const r of results) {
|
|
64
|
+
const prevEntry = prev.clis && prev.clis[r.cli];
|
|
65
|
+
newState.clis[r.cli] = {
|
|
66
|
+
probes: r.probes,
|
|
67
|
+
version: r.version || (prevEntry && prevEntry.version) || null,
|
|
68
|
+
fpSha: r.fpSha || (prevEntry && prevEntry.fpSha) || null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
try { fs.writeFileSync(statePath, JSON.stringify(newState, null, 2)); }
|
|
72
|
+
catch (e) { process.stderr.write("state save failed: " + e.message + "\n"); }
|
|
73
|
+
|
|
74
|
+
process.stdout.write(lines.join("\n") + "\n");
|