failproofai 1.0.12-beta.0 → 1.0.12-beta.2
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/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- 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/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 +2 -2
- 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 +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- 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 +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- 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/_tree.segment.rsc +1 -1
- 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 +22 -22
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/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/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/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +1 -1
- 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 +3 -3
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- 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/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +4 -4
- 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/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/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/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/app/settings/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/settings/page.js +7 -7
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__0-ha5ydqq0mws._.js → [externals]__0vt9i6fqinjne._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__20xe05hm5u8_1._.js → [root-of-the-server]__0f_4g-vazrvxv._.js} +1 -1
- package/.next/standalone/.next/server/chunks/_0hf5q4xzyg40j._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0tovk6q0miud6._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3ycqv17cl._.js +1 -1
- package/.next/standalone/.next/server/chunks/_19011-u44143j._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68lnh2daur._.js +2 -2
- package/.next/standalone/.next/server/chunks/{_0kc-piux3hady._.js → _1y590mjzpkbos._.js} +2 -2
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4vkpxxu7._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_0_em82s16zfym._.js +5 -0
- package/.next/standalone/.next/server/chunks/src_hooks_builtin-policies_ts_1yq5ybgx1v98e._.js +3 -0
- package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3h94wnn._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_daemon-client_ts_0il_yu1zc0291._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn_4twyh._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ts0fg5yih1c1._.js → [root-of-the-server]__00l2u6j86cb8y._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08ch3staq3ejw._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bigvy9xmv2ka._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h86bl2qf0k_o._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1_xbg2kx30wp-._.js → [root-of-the-server]__0naevg4bcoh54._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pia5h3p8re4c._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pwj2h2efe5f-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-ey9fgesfbc0._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15usvs-ms-0hf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16gb3_92zvk5x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1avi2kypusyb_._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1r7drlqli-ado._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0vcyntk4snf0f._.js → [root-of-the-server]__1vofkb2oq7p7e._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0bseae9u7g98r._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0eoura3_9gjb-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0mouhaflye1ro._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1gb0ifpkowwub._.js → _0ouu886szh6yj._.js} +10 -9
- package/.next/standalone/.next/server/chunks/ssr/_0pedwblqdoh58._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1djqs9i27e4kg._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1ve8y151ua7qx._.js → _1fw0_nzkae5ao._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_20hn4gn87nsjc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo5_vm5f.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9snixb5fd-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47q0xq5w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_components_project-list_tsx_1ke6anr0vt64h._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x33jqb6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpcz0uxur._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h47jmhhd._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_00jxj8foxqz5t.js → node_modules_html-to-image_es_index_1fv03uigni8s5.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0janj0i6dqko7._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/package_json_[json]_cjs_013tntikdft59._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9ddabn3_._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0agdmptetetdt._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0mdwgjisv4fub._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4l1tzrm._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_daemon-client_ts_0k41_i7jg6ndv._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589gp4p60j._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me658zqmh_._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- 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/preview-props.json +3 -3
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
- package/.next/standalone/.next/static/chunks/{0pjxnp927y7_j.js → 0ay__-d2h1xb5.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2znt1784j9gd9.js → 0tbm3vjm62h-2.js} +1 -1
- package/.next/standalone/.next/static/chunks/1c_jf6g5wnslk.js +1 -0
- package/.next/standalone/.next/static/chunks/{3tn1bzk094e8e.js → 1my3-5p47t7r-.js} +1 -1
- package/.next/standalone/.next/static/chunks/20py8k1gd_i1m.js +1 -0
- package/.next/standalone/.next/static/chunks/21hckpmsc4g70.js +6 -0
- package/.next/standalone/.next/static/chunks/2hmex93ppj1ke.js +1 -0
- package/.next/standalone/.next/static/chunks/2o6d37j425uci.js +1 -0
- package/.next/standalone/.next/static/chunks/40oworn_-x12p.js +1 -0
- package/.next/standalone/.next/static/chunks/{2qz5piiir-qqm.js → 42xptz88mtq3x.js} +1 -1
- package/.next/standalone/app/actions/update-jev-config.ts +5 -3
- package/.next/standalone/app/settings/jev-panel.tsx +1 -1
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +32 -0
- package/.next/standalone/fp-cloud-cli/README.md +40 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +91 -12
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +78 -16
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +84 -18
- package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +124 -6
- package/.next/standalone/fp-cloud-cli/fp_cli/models.py +89 -6
- package/.next/standalone/fp-cloud-cli/fp_cli/output.py +189 -20
- package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +8 -5
- package/.next/standalone/fp-cloud-cli/skill/SKILL.md +2 -2
- package/.next/standalone/fp-cloud-cli/skill/references/commands.md +14 -7
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_jev.py +527 -0
- package/.next/standalone/fp-cloud-cli/tests/test_enforcement_logic.py +46 -0
- package/.next/standalone/fp-cloud-cli/tests/test_output.py +23 -0
- package/.next/standalone/hermes-plugin/README.md +6 -4
- package/.next/standalone/hermes-plugin/__init__.py +27 -0
- package/.next/standalone/hermes-plugin/client.py +5 -1
- package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
- package/.next/standalone/package.json +11 -10
- package/.next/standalone/sdk/python/uv.lock +31 -8
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +17 -1
- package/dist/cli.mjs +3507 -1250
- package/dist/worker.mjs +1779 -169
- package/hermes-plugin/README.md +6 -4
- package/hermes-plugin/__init__.py +27 -0
- package/hermes-plugin/client.py +5 -1
- package/hermes-plugin/plugin.yaml +1 -1
- package/package.json +11 -10
- package/src/audit/open-browser.ts +6 -7
- package/src/hooks/agent-roster.ts +180 -0
- package/src/hooks/agent-skill.ts +203 -0
- package/src/hooks/agent-targets.ts +60 -0
- package/src/hooks/cloud-connection.ts +91 -45
- package/src/hooks/cloud-enrollment-cli.ts +7 -4
- package/src/hooks/cloud-login.ts +6 -3
- package/src/hooks/cloud-managed-policies.ts +430 -20
- package/src/hooks/cloud-policy-errors.ts +481 -0
- package/src/hooks/codex-system-hooks.ts +10 -4
- package/src/hooks/config-status.ts +10 -4
- package/src/hooks/configure-wizard.ts +54 -26
- package/src/hooks/custom-hooks-loader.ts +26 -3
- package/src/hooks/daemon-client.ts +9 -4
- package/src/hooks/daemon-service.ts +2 -1
- package/src/hooks/delivery-health.ts +47 -2
- package/src/hooks/effective-reviewers.ts +36 -1
- package/src/hooks/fp-config.ts +19 -0
- package/src/hooks/fp-home.ts +6 -0
- package/src/hooks/handler.ts +255 -19
- package/src/hooks/health-probe.ts +15 -0
- package/src/hooks/hook-activity-store.ts +7 -1
- package/src/hooks/integrations.ts +36 -14
- package/src/hooks/jev-activity.ts +117 -1
- package/src/hooks/jev-cli.ts +196 -1
- package/src/hooks/jev-cloud-connection.ts +92 -0
- package/src/hooks/manager.ts +174 -34
- package/src/hooks/pack-failclosed.ts +2 -0
- package/src/hooks/pack-manifest.ts +8 -1
- package/src/hooks/policy-evaluator.ts +22 -1
- package/src/hooks/policy-registry.ts +3 -2
- package/src/hooks/policy-reviewability.ts +23 -5
- package/src/hooks/semantic/cloud-jev-health.ts +227 -0
- package/src/hooks/semantic/cloud-jev.ts +868 -0
- package/src/hooks/semantic/combine.ts +26 -1
- package/src/hooks/semantic/jev-client.ts +19 -2
- package/src/hooks/semantic/jev-config.ts +179 -30
- package/src/hooks/semantic/jev-review.ts +217 -10
- package/src/hooks/semantic/pack-policies.ts +2 -1
- package/src/hooks/semantic/types.ts +8 -0
- package/src/hooks/session-pause-cli.ts +1 -1
- package/src/hooks/tui.ts +13 -4
- package/src/hooks/uninstall-cli.ts +12 -1
- package/src/hooks/worker-server.ts +5 -1
- package/.next/standalone/.next/server/chunks/src_hooks_1aveq0uhs2t1b._.js +0 -5
- package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7cyqlb2._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo55j16x._.js +0 -5
- package/.next/standalone/.next/static/chunks/0a-ccsp3xmypz.js +0 -1
- package/.next/standalone/.next/static/chunks/13jv-xvabeb62.js +0 -6
- package/.next/standalone/.next/static/chunks/2ank2u3apy8m6.js +0 -1
- package/.next/standalone/.next/static/chunks/2meqb9kygrncb.js +0 -1
- package/.next/standalone/.next/static/chunks/2ohktp_-lkv3x.js +0 -1
- package/.next/standalone/.next/static/chunks/2py0eldhwz8ay.js +0 -1
- /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → LHCtFxVG0mdvd4lsXAOFC}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → LHCtFxVG0mdvd4lsXAOFC}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → LHCtFxVG0mdvd4lsXAOFC}/_ssgManifest.js +0 -0
package/hermes-plugin/README.md
CHANGED
|
@@ -165,23 +165,25 @@ decision, because otherwise corrupted retry state could block a turn forever.
|
|
|
165
165
|
## Local protocol
|
|
166
166
|
|
|
167
167
|
Requests and responses use the existing length-prefixed failproofaid Unix
|
|
168
|
-
socket protocol, version
|
|
168
|
+
socket protocol, version 2. A v1 daemon must be upgraded before it can safely
|
|
169
|
+
evaluate agent/profile-scoped policies.
|
|
169
170
|
|
|
170
171
|
```json
|
|
171
172
|
{
|
|
172
173
|
"type": "policyEvaluation",
|
|
173
|
-
"protocolVersion":
|
|
174
|
+
"protocolVersion": 2,
|
|
174
175
|
"integration": "hermes",
|
|
175
176
|
"event": "pre_tool_call",
|
|
176
177
|
"payload": {},
|
|
177
|
-
"cwd": "/workspace/project"
|
|
178
|
+
"cwd": "/workspace/project",
|
|
179
|
+
"agentSettingsPath": "/path/to/the/hermes/profile"
|
|
178
180
|
}
|
|
179
181
|
```
|
|
180
182
|
|
|
181
183
|
```json
|
|
182
184
|
{
|
|
183
185
|
"type": "policyResult",
|
|
184
|
-
"protocolVersion":
|
|
186
|
+
"protocolVersion": 2,
|
|
185
187
|
"decision": "instruct",
|
|
186
188
|
"policyNames": ["custom/approved-write-route"],
|
|
187
189
|
"reason": "Use the approved write route.",
|
|
@@ -102,6 +102,32 @@ def _profile_name() -> str:
|
|
|
102
102
|
return "default"
|
|
103
103
|
|
|
104
104
|
|
|
105
|
+
def _profile_settings_path(profile: str) -> str | None:
|
|
106
|
+
"""Evidence of the profile running this plugin, not a transcript guess."""
|
|
107
|
+
try:
|
|
108
|
+
# The host's own resolver first: it honours a per-task profile
|
|
109
|
+
# override as well as HERMES_HOME, so each profile of a multi-profile
|
|
110
|
+
# gateway reports its own config (and roster identity), not the
|
|
111
|
+
# process-wide one.
|
|
112
|
+
from hermes_constants import get_hermes_home
|
|
113
|
+
|
|
114
|
+
hosted = Path(get_hermes_home()).expanduser() / "config.yaml"
|
|
115
|
+
return str(hosted.absolute()) if hosted.is_file() else None
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
configured = os.environ.get("HERMES_HOME", "").strip()
|
|
119
|
+
if configured:
|
|
120
|
+
selected = Path(configured).expanduser() / "config.yaml"
|
|
121
|
+
elif profile == "default":
|
|
122
|
+
selected = Path.home() / ".hermes" / "config.yaml"
|
|
123
|
+
elif re.fullmatch(r"[A-Za-z0-9._-]{1,80}", profile):
|
|
124
|
+
selected = Path.home() / ".hermes" / "profiles" / profile / "config.yaml"
|
|
125
|
+
else:
|
|
126
|
+
return None
|
|
127
|
+
# An unresolvable profile does not acquire some other profile's ID.
|
|
128
|
+
return str(selected.absolute()) if selected.is_file() else None
|
|
129
|
+
|
|
130
|
+
|
|
105
131
|
def _string(value: object) -> str:
|
|
106
132
|
return value if isinstance(value, str) else ""
|
|
107
133
|
|
|
@@ -660,6 +686,7 @@ class FailproofAIPlugin:
|
|
|
660
686
|
event=event,
|
|
661
687
|
payload=payload,
|
|
662
688
|
cwd=cwd,
|
|
689
|
+
agent_settings_path=_profile_settings_path(self.profile),
|
|
663
690
|
connect_timeout_ms=self.connect_timeout_ms,
|
|
664
691
|
evaluation_timeout_ms=timeout_ms or self.evaluation_timeout_ms,
|
|
665
692
|
)
|
package/hermes-plugin/client.py
CHANGED
|
@@ -11,7 +11,7 @@ from dataclasses import dataclass
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any, Mapping, Sequence
|
|
13
13
|
|
|
14
|
-
PROTOCOL_VERSION =
|
|
14
|
+
PROTOCOL_VERSION = 2
|
|
15
15
|
MAX_FRAME_BYTES = 16 * 1024 * 1024
|
|
16
16
|
MAX_REQUEST_BYTES = 1024 * 1024
|
|
17
17
|
|
|
@@ -75,6 +75,7 @@ def evaluate_policy(
|
|
|
75
75
|
event: str,
|
|
76
76
|
payload: Mapping[str, Any],
|
|
77
77
|
cwd: str | None,
|
|
78
|
+
agent_settings_path: str | None = None,
|
|
78
79
|
connect_timeout_ms: int = 250,
|
|
79
80
|
evaluation_timeout_ms: int = 12_000,
|
|
80
81
|
) -> PolicyVerdict:
|
|
@@ -85,6 +86,9 @@ def evaluate_policy(
|
|
|
85
86
|
"event": event,
|
|
86
87
|
"payload": dict(payload),
|
|
87
88
|
"cwd": cwd,
|
|
89
|
+
# An unknown profile is explicit. Omitting this field would let a new
|
|
90
|
+
# daemon's warm worker attribute the call to its own Hermes home.
|
|
91
|
+
"agentSettingsPath": agent_settings_path or "",
|
|
88
92
|
}
|
|
89
93
|
try:
|
|
90
94
|
body = json.dumps(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "failproofai",
|
|
3
|
-
"version": "1.0.12-beta.
|
|
3
|
+
"version": "1.0.12-beta.2",
|
|
4
4
|
"description": "Observability and enforcement for AI agent harnesses. 40 built-in policies hooked into 12 of them — Claude Code, Codex, Cursor, Hermes, OpenClaw and more — blocking the tool call before it runs. Local dashboard included, no account needed.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"failproofai": "./dist/cli.mjs",
|
|
@@ -118,22 +118,23 @@
|
|
|
118
118
|
"postcss": "8.5.26",
|
|
119
119
|
"eslint-plugin-react-hooks": "7.0.1",
|
|
120
120
|
"vite": "8.1.6",
|
|
121
|
-
"undici": "7.
|
|
121
|
+
"undici": "7.30.1",
|
|
122
|
+
"zod": "4.6.6",
|
|
122
123
|
"brace-expansion": "5.0.12",
|
|
123
124
|
"sharp": "0.35.5",
|
|
124
125
|
"source-map-js": "1.2.2",
|
|
125
126
|
"browserslist": "4.28.8"
|
|
126
127
|
},
|
|
127
128
|
"optionalDependencies": {
|
|
128
|
-
"@failproofai/failproofaid-linux-x64": "1.0.12-beta.
|
|
129
|
-
"@failproofai/failproofaid-linux-arm64": "1.0.12-beta.
|
|
130
|
-
"@failproofai/failproofaid-darwin-x64": "1.0.12-beta.
|
|
131
|
-
"@failproofai/failproofaid-darwin-arm64": "1.0.12-beta.
|
|
129
|
+
"@failproofai/failproofaid-linux-x64": "1.0.12-beta.2",
|
|
130
|
+
"@failproofai/failproofaid-linux-arm64": "1.0.12-beta.2",
|
|
131
|
+
"@failproofai/failproofaid-darwin-x64": "1.0.12-beta.2",
|
|
132
|
+
"@failproofai/failproofaid-darwin-arm64": "1.0.12-beta.2"
|
|
132
133
|
},
|
|
133
134
|
"failproofaidBinaries": {
|
|
134
|
-
"linux-x64": "
|
|
135
|
-
"linux-arm64": "
|
|
136
|
-
"darwin-x64": "
|
|
137
|
-
"darwin-arm64": "
|
|
135
|
+
"linux-x64": "4c09117fdb80d87792d1d2b77d9359cdbd0c522b5a6222ab7b927659130ddf54",
|
|
136
|
+
"linux-arm64": "dd390d76561267fc519ceb96fb90be340044e2f564bef36a44e284f227d93f29",
|
|
137
|
+
"darwin-x64": "62381f7ffe21d02289c36c4b205166b2867dbde986444d1ea185241b94fd04ef",
|
|
138
|
+
"darwin-arm64": "9ad798817d3048a5126e80dc71883538d37c1b6b6443e9c19389dc2583c8f675"
|
|
138
139
|
}
|
|
139
140
|
}
|
|
@@ -32,17 +32,16 @@ export function canOpenBrowser(
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/** Open a URL with the platform's default handler. Never throws. */
|
|
35
|
-
export function openUrl(url: string): void {
|
|
35
|
+
export function openUrl(url: string, platform: NodeJS.Platform = process.platform): void {
|
|
36
36
|
let cmd: string;
|
|
37
37
|
let args: string[];
|
|
38
|
-
if (
|
|
38
|
+
if (platform === "darwin") {
|
|
39
39
|
cmd = "open";
|
|
40
40
|
args = [url];
|
|
41
|
-
} else if (
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
args = ["/c", "start", "", url];
|
|
41
|
+
} else if (platform === "win32") {
|
|
42
|
+
// ShellExecute via rundll32 avoids interpreting URL characters as cmd.exe syntax.
|
|
43
|
+
cmd = "rundll32.exe";
|
|
44
|
+
args = ["url.dll,FileProtocolHandler", url];
|
|
46
45
|
} else {
|
|
47
46
|
cmd = "xdg-open";
|
|
48
47
|
args = [url];
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the daemon-owned profile roster. The telemetry agent_id and transcript
|
|
3
|
+
* path are not profile evidence. A scoped Cloud assignment is withheld unless
|
|
4
|
+
* this invocation resolves to one of the daemon's recorded config paths.
|
|
5
|
+
*/
|
|
6
|
+
import { lstatSync, readFileSync, statSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { dirname, resolve } from "node:path";
|
|
9
|
+
import { agentRosterFile } from "./fp-home";
|
|
10
|
+
import { validAgentIdentity, type AgentIdentity } from "./agent-targets";
|
|
11
|
+
import type { HookScope, IntegrationType } from "./types";
|
|
12
|
+
|
|
13
|
+
/** `MAX_AGENTS` / `MAX_ROSTER_BYTES` in the daemon's `agent_roster.rs`. */
|
|
14
|
+
export const MAX_ROSTER_AGENTS = 256;
|
|
15
|
+
export const MAX_ROSTER_BYTES = 4_000_000;
|
|
16
|
+
|
|
17
|
+
const USER_SETTINGS: Partial<Record<IntegrationType, string>> = {
|
|
18
|
+
claude: ".claude/settings.json",
|
|
19
|
+
codex: ".codex/hooks.json",
|
|
20
|
+
copilot: ".copilot/hooks/failproofai.json",
|
|
21
|
+
cursor: ".cursor/hooks.json",
|
|
22
|
+
opencode: ".config/opencode/opencode.json",
|
|
23
|
+
pi: ".pi/agent/settings.json",
|
|
24
|
+
factory: ".factory/hooks.json",
|
|
25
|
+
devin: ".config/devin/config.json",
|
|
26
|
+
antigravity: ".gemini/config/hooks.json",
|
|
27
|
+
goose: ".agents/plugins/failproofai/hooks/hooks.json",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const PROJECT_SETTINGS: Partial<Record<IntegrationType, string[]>> = {
|
|
31
|
+
claude: [".claude/settings.local.json", ".claude/settings.json"],
|
|
32
|
+
codex: [".codex/hooks.json"],
|
|
33
|
+
copilot: [".github/hooks/failproofai.json"],
|
|
34
|
+
cursor: [".cursor/hooks.json"],
|
|
35
|
+
opencode: [".opencode/opencode.json"],
|
|
36
|
+
pi: [".pi/settings.json"],
|
|
37
|
+
factory: [".factory/hooks.json"],
|
|
38
|
+
devin: [".devin/config.json"],
|
|
39
|
+
antigravity: [".agents/hooks.json"],
|
|
40
|
+
goose: [".agents/plugins/failproofai/hooks/hooks.json"],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function installedHookAt(path: string, cli: IntegrationType): boolean {
|
|
44
|
+
try {
|
|
45
|
+
const stat = statSync(path);
|
|
46
|
+
if (!stat.isFile() || stat.size > 131_072) return false;
|
|
47
|
+
const text = readFileSync(path, "utf8");
|
|
48
|
+
if (text.includes("failproofai")) return true;
|
|
49
|
+
// Pi's project entry is portable and relative to `.pi/settings.json`:
|
|
50
|
+
// `../pi-extension` contains no product name. Missing it here let a
|
|
51
|
+
// simultaneously installed user hook masquerade as this project's Pi.
|
|
52
|
+
if (cli !== "pi") return false;
|
|
53
|
+
const config: unknown = JSON.parse(text);
|
|
54
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) return false;
|
|
55
|
+
const packages = (config as { packages?: unknown }).packages;
|
|
56
|
+
return Array.isArray(packages) && packages.some(
|
|
57
|
+
(entry) => typeof entry === "string" && /(?:^|\/)pi-extension\/?$/.test(entry),
|
|
58
|
+
);
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Called in the originating hook process; the worker's env may be different. */
|
|
65
|
+
export function runtimeAgentSettingsPath(
|
|
66
|
+
cli: IntegrationType,
|
|
67
|
+
cwd?: string,
|
|
68
|
+
userHome = homedir(),
|
|
69
|
+
hookScope?: HookScope,
|
|
70
|
+
): string | null {
|
|
71
|
+
if (cli === "hermes") {
|
|
72
|
+
if (hookScope && hookScope !== "user") return null;
|
|
73
|
+
const home = process.env.HERMES_HOME;
|
|
74
|
+
return resolve(home?.trim() ? home : resolve(userHome, ".hermes"), "config.yaml");
|
|
75
|
+
}
|
|
76
|
+
if (cli === "openclaw") {
|
|
77
|
+
if (hookScope && hookScope !== "user") return null;
|
|
78
|
+
const config = process.env.OPENCLAW_CONFIG_PATH;
|
|
79
|
+
if (config?.trim()) return resolve(config);
|
|
80
|
+
const state = process.env.OPENCLAW_STATE_DIR || process.env.OPENCLAW_HOME;
|
|
81
|
+
if (state?.trim()) return resolve(state, "openclaw.json");
|
|
82
|
+
// `openclaw --profile <name>` runs from `~/.openclaw-<name>`; a gateway
|
|
83
|
+
// that exports only the profile name must still resolve to that profile's
|
|
84
|
+
// config, not the default one (each profile has its own roster identity).
|
|
85
|
+
const profile = process.env.OPENCLAW_PROFILE?.trim();
|
|
86
|
+
if (profile && profile !== "default" && /^[A-Za-z0-9._-]{1,80}$/.test(profile) && !profile.startsWith(".")) {
|
|
87
|
+
return resolve(userHome, `.openclaw-${profile}`, "openclaw.json");
|
|
88
|
+
}
|
|
89
|
+
return resolve(userHome, ".openclaw", "openclaw.json");
|
|
90
|
+
}
|
|
91
|
+
const relative = USER_SETTINGS[cli];
|
|
92
|
+
if (!relative) return null;
|
|
93
|
+
const override = cli === "codex" ? process.env.CODEX_HOME
|
|
94
|
+
: cli === "claude" ? process.env.CLAUDE_CONFIG_DIR : undefined;
|
|
95
|
+
const user = resolve(override?.trim() ? override : userHome, override?.trim()
|
|
96
|
+
? cli === "codex" ? "hooks.json" : "settings.json"
|
|
97
|
+
: relative);
|
|
98
|
+
if (hookScope === "user") return user;
|
|
99
|
+
|
|
100
|
+
const scopeFiles = hookScope === "local"
|
|
101
|
+
? cli === "claude" ? [".claude/settings.local.json"] : []
|
|
102
|
+
: hookScope === "project"
|
|
103
|
+
? (PROJECT_SETTINGS[cli] ?? []).filter((name) => name !== ".claude/settings.local.json")
|
|
104
|
+
: PROJECT_SETTINGS[cli] ?? [];
|
|
105
|
+
const matches = new Set<string>();
|
|
106
|
+
if (hookScope === undefined && installedHookAt(user, cli)) matches.add(user);
|
|
107
|
+
let dir = resolve(cwd ?? process.cwd());
|
|
108
|
+
const userRoot = resolve(userHome);
|
|
109
|
+
for (let depth = 0; depth < 16 && dir !== userRoot; depth++) {
|
|
110
|
+
for (const candidate of scopeFiles) {
|
|
111
|
+
const path = resolve(dir, candidate);
|
|
112
|
+
if (installedHookAt(path, cli)) matches.add(path);
|
|
113
|
+
}
|
|
114
|
+
const parent = dirname(dir);
|
|
115
|
+
if (parent === dir) break;
|
|
116
|
+
dir = parent;
|
|
117
|
+
}
|
|
118
|
+
// No source hint can tell WHICH installed hook fired if both user and
|
|
119
|
+
// project/local scopes contain us. A guessed profile would let an exact
|
|
120
|
+
// assignment match another installation: withhold it instead.
|
|
121
|
+
if (matches.size > 1) return null;
|
|
122
|
+
if (hookScope === "project" || hookScope === "local") return matches.values().next().value ?? null;
|
|
123
|
+
return matches.values().next().value ?? user;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function readRuntimeAgentIdentity(cli: IntegrationType, settingsPath: string | null): AgentIdentity | null {
|
|
127
|
+
if (!settingsPath) return null;
|
|
128
|
+
try {
|
|
129
|
+
const path = agentRosterFile();
|
|
130
|
+
const stat = lstatSync(path);
|
|
131
|
+
if (!stat.isFile() || (stat.mode & 0o077) !== 0 || stat.size > MAX_ROSTER_BYTES) return null;
|
|
132
|
+
const roster: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
133
|
+
if (!roster || typeof roster !== "object" || Array.isArray(roster)) return null;
|
|
134
|
+
const data = roster as Record<string, unknown>;
|
|
135
|
+
if (data.schemaVersion !== 1 || !Array.isArray(data.agents) || data.agents.length > MAX_ROSTER_AGENTS) return null;
|
|
136
|
+
const absolute = resolve(settingsPath);
|
|
137
|
+
const agent = data.agents.find((entry: unknown) => {
|
|
138
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
|
|
139
|
+
const record = entry as Record<string, unknown>;
|
|
140
|
+
return record.integration === cli && record.settingsPath === absolute &&
|
|
141
|
+
validAgentIdentity({ integration: record.integration, instanceId: record.instanceId });
|
|
142
|
+
});
|
|
143
|
+
const instanceId = (agent as Record<string, unknown> | undefined)?.instanceId;
|
|
144
|
+
return validAgentIdentity({ integration: cli, instanceId })
|
|
145
|
+
? { integration: cli, instanceId: instanceId as string }
|
|
146
|
+
: null;
|
|
147
|
+
} catch {
|
|
148
|
+
// Unavailable, malformed or old roster: never infer a profile from cwd,
|
|
149
|
+
// transcript, or the display name and widen a scoped deployment.
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Display labels from the daemon's roster, by `agt_` id (`claude / .claude-work`
|
|
156
|
+
* style: integration, then the profile label). For listings only; never an
|
|
157
|
+
* identity — `readRuntimeAgentIdentity` is the only thing that decides which
|
|
158
|
+
* profile a hook is. Empty when the roster cannot be read.
|
|
159
|
+
*/
|
|
160
|
+
export function readRosterLabels(): Map<string, string> {
|
|
161
|
+
const out = new Map<string, string>();
|
|
162
|
+
try {
|
|
163
|
+
const path = agentRosterFile();
|
|
164
|
+
const stat = lstatSync(path);
|
|
165
|
+
if (!stat.isFile() || (stat.mode & 0o077) !== 0 || stat.size > MAX_ROSTER_BYTES) return out;
|
|
166
|
+
const roster: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
167
|
+
if (!roster || typeof roster !== "object" || Array.isArray(roster)) return out;
|
|
168
|
+
const data = roster as Record<string, unknown>;
|
|
169
|
+
if (data.schemaVersion !== 1 || !Array.isArray(data.agents) || data.agents.length > MAX_ROSTER_AGENTS) return out;
|
|
170
|
+
for (const entry of data.agents) {
|
|
171
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
172
|
+
const record = entry as Record<string, unknown>;
|
|
173
|
+
if (typeof record.instanceId !== "string" || typeof record.profileLabel !== "string" || record.profileLabel === "") continue;
|
|
174
|
+
out.set(record.instanceId, record.profileLabel.slice(0, 80));
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
// No roster: the bare ids are shown.
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `failproofai` skill: one small SKILL.md that `failproofai config` puts in
|
|
3
|
+
* every agent this machine has, in that agent's own user-scope skills folder.
|
|
4
|
+
*
|
|
5
|
+
* It carries no instructions of its own. It points the agent at the umbrella
|
|
6
|
+
* skill in the public FailproofAI/skills repository, which the agent reads when
|
|
7
|
+
* a FailproofAI task comes up, along with whichever focused sibling skill and
|
|
8
|
+
* `references/*.md` file the task needs. So the content is never stale on
|
|
9
|
+
* disk: changing that repository changes what every machine reads next, with
|
|
10
|
+
* no release and no re-run of setup.
|
|
11
|
+
*
|
|
12
|
+
* Every folder below was verified on 2026-10-23 against the real CLI (versions
|
|
13
|
+
* in each comment), by planting a probe skill in each candidate folder and
|
|
14
|
+
* reading what the CLI listed, or what it sent to a mock model server.
|
|
15
|
+
*
|
|
16
|
+
* Never `~/.agents/skills`, although many agents read it: Claude Code does not
|
|
17
|
+
* read it at all, Codex lists a skill found there and in `~/.codex/skills`
|
|
18
|
+
* twice, Copilot stops reading it once `COPILOT_HOME` is set, and in OpenClaw it
|
|
19
|
+
* outranks OpenClaw's own folder. Each agent's own folder works on its own.
|
|
20
|
+
*
|
|
21
|
+
* The frontmatter is shaped by the strictest of them: `name` equals the folder
|
|
22
|
+
* name (Claude Code, Cursor and Devin go by the folder, the rest by `name`, and
|
|
23
|
+
* Antigravity shows a skill without one as "SKILL"); Copilot drops a skill whose
|
|
24
|
+
* description is over 1024 characters, droid shows the model the first 200, and
|
|
25
|
+
* Hermes the first 57 — so the description is short and leads with what it is
|
|
26
|
+
* for.
|
|
27
|
+
*/
|
|
28
|
+
import { existsSync, mkdirSync, rmdirSync, rmSync } from "node:fs";
|
|
29
|
+
import { homedir } from "node:os";
|
|
30
|
+
import { join, resolve } from "node:path";
|
|
31
|
+
import { listHermesProfiles } from "../../lib/hermes-profiles";
|
|
32
|
+
import { listOpenClawProfiles } from "../../lib/openclaw-profiles";
|
|
33
|
+
import { hookLogWarn } from "./hook-logger";
|
|
34
|
+
import { writeConfigFileAtomic } from "./safe-config-write";
|
|
35
|
+
import { INTEGRATION_TYPES, type IntegrationType } from "./types";
|
|
36
|
+
|
|
37
|
+
export const AGENT_SKILL_NAME = "failproofai";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Sits beside SKILL.md in a folder failproofai wrote. A `failproofai` folder
|
|
41
|
+
* without it belongs to someone else — most often the full skill installed with
|
|
42
|
+
* `npx skills add FailproofAI/skills` — and is never overwritten or removed.
|
|
43
|
+
*/
|
|
44
|
+
export const AGENT_SKILL_MARKER = ".failproofai-managed";
|
|
45
|
+
|
|
46
|
+
const SKILLS_RAW = "https://raw.githubusercontent.com/FailproofAI/skills/main/skills";
|
|
47
|
+
|
|
48
|
+
export const AGENT_SKILL_MD = `---
|
|
49
|
+
name: ${AGENT_SKILL_NAME}
|
|
50
|
+
description: FailproofAI hooks, policies, daemon, Cloud, the fp CLI and the SDK. Use for any question about setting up, running or fixing FailproofAI.
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
# FailproofAI
|
|
54
|
+
|
|
55
|
+
This skill is a pointer. Before acting on anything FailproofAI, read the full skill, which is kept current online (use your web fetch tool, or \`curl -fsSL <url>\`):
|
|
56
|
+
|
|
57
|
+
${SKILLS_RAW}/${AGENT_SKILL_NAME}/SKILL.md
|
|
58
|
+
|
|
59
|
+
Files it names by a relative path, such as \`references/cli.md\`, are in that same folder: ${SKILLS_RAW}/${AGENT_SKILL_NAME}/<path>
|
|
60
|
+
|
|
61
|
+
For focused work, read the matching skill too. Each has the same layout, with its own \`references/\` beside it.
|
|
62
|
+
|
|
63
|
+
- Writing or testing a policy: ${SKILLS_RAW}/failproofai-policy-author/SKILL.md
|
|
64
|
+
- Publishing a policy pack: ${SKILLS_RAW}/failproofai-policy-publish/SKILL.md
|
|
65
|
+
- FailproofAI Cloud and the \`fp\` CLI: ${SKILLS_RAW}/fp-cloud-cli/SKILL.md
|
|
66
|
+
- Adding the SDK to an agent, or running an evaluator: ${SKILLS_RAW}/failproofai-sdk/SKILL.md
|
|
67
|
+
- Planning evaluations: ${SKILLS_RAW}/failproofai-eval-brainstorm/SKILL.md
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
/** An agent's own override for where it keeps its files, else the default. */
|
|
71
|
+
function envDir(name: string, fallback: string): string {
|
|
72
|
+
const value = (process.env[name] ?? "").trim();
|
|
73
|
+
return value ? resolve(value) : fallback;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The skills folders `id` reads at user scope — one, except for Hermes and
|
|
78
|
+
* OpenClaw, where every profile reads only its own.
|
|
79
|
+
*/
|
|
80
|
+
export function agentSkillRoots(id: IntegrationType): string[] {
|
|
81
|
+
const home = homedir();
|
|
82
|
+
const config = envDir("XDG_CONFIG_HOME", join(home, ".config"));
|
|
83
|
+
switch (id) {
|
|
84
|
+
// 2.1.309. CLAUDE_CONFIG_DIR replaces ~/.claude rather than adding to it.
|
|
85
|
+
case "claude":
|
|
86
|
+
return [join(envDir("CLAUDE_CONFIG_DIR", join(home, ".claude")), "skills")];
|
|
87
|
+
// 0.164.0. Its own skill installer writes here too.
|
|
88
|
+
case "codex":
|
|
89
|
+
return [join(envDir("CODEX_HOME", join(home, ".codex")), "skills")];
|
|
90
|
+
// 1.0.103.
|
|
91
|
+
case "copilot":
|
|
92
|
+
return [join(envDir("COPILOT_HOME", join(home, ".copilot")), "skills")];
|
|
93
|
+
// 2026.10.22. Always under HOME: CURSOR_CONFIG_DIR does not move it.
|
|
94
|
+
case "cursor":
|
|
95
|
+
return [join(home, ".cursor", "skills")];
|
|
96
|
+
// 1.19.4.
|
|
97
|
+
case "opencode":
|
|
98
|
+
return [join(config, "opencode", "skills")];
|
|
99
|
+
// 1.4.0 and 0.73.1. Not the pi-extension package: a `skills/` folder there
|
|
100
|
+
// makes Pi stop loading the package's index.ts, and with it enforcement.
|
|
101
|
+
case "pi":
|
|
102
|
+
return [join(envDir("PI_CODING_AGENT_DIR", join(home, ".pi", "agent")), "skills")];
|
|
103
|
+
// 0.21.3. A profile reads its own skills/ and no other profile's.
|
|
104
|
+
case "hermes":
|
|
105
|
+
return listHermesProfiles().map((p) => join(p.home, "skills"));
|
|
106
|
+
// 2026.9.2. The same profile homes its hooks are installed into.
|
|
107
|
+
case "openclaw":
|
|
108
|
+
return listOpenClawProfiles().map((p) => join(p.home, "skills"));
|
|
109
|
+
// 0.248.0. FACTORY_HOME_OVERRIDE stands in for HOME.
|
|
110
|
+
case "factory":
|
|
111
|
+
return [join(envDir("FACTORY_HOME_OVERRIDE", home), ".factory", "skills")];
|
|
112
|
+
// 3000.11.205.
|
|
113
|
+
case "devin":
|
|
114
|
+
return [join(config, "devin", "skills")];
|
|
115
|
+
// 1.4.6. The documented user folder, beside the hooks.json we write; not
|
|
116
|
+
// ~/.gemini/antigravity/skills, which it does not read.
|
|
117
|
+
case "antigravity":
|
|
118
|
+
return [join(home, ".gemini", "config", "skills")];
|
|
119
|
+
// 1.55.0. GOOSE_PATH_ROOT moves Goose's whole config tree.
|
|
120
|
+
case "goose": {
|
|
121
|
+
const root = (process.env.GOOSE_PATH_ROOT ?? "").trim();
|
|
122
|
+
return [root ? join(resolve(root), "config", "skills") : join(config, "goose", "skills")];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Whether `id` already has a `failproofai` skill from a folder it also reads,
|
|
129
|
+
* and would list ours beside it rather than pick one. Only Codex: it reads
|
|
130
|
+
* `~/.agents/skills` (always under HOME, whatever CODEX_HOME says) as well as
|
|
131
|
+
* its own folder and lists a name found in both twice (0.164.0) — and that is
|
|
132
|
+
* where `npx skills add FailproofAI/skills -g` puts the full skill for it.
|
|
133
|
+
*/
|
|
134
|
+
function listedTwice(id: IntegrationType): boolean {
|
|
135
|
+
return id === "codex" && existsSync(join(homedir(), ".agents", "skills", AGENT_SKILL_NAME, "SKILL.md"));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Write the skill for each agent in `ids`. Never fails setup: an agent whose
|
|
140
|
+
* folder cannot be written is logged and skipped. Returns the agents it was
|
|
141
|
+
* written for.
|
|
142
|
+
*/
|
|
143
|
+
export function installAgentSkill(ids: readonly IntegrationType[]): IntegrationType[] {
|
|
144
|
+
const installed: IntegrationType[] = [];
|
|
145
|
+
for (const id of ids) {
|
|
146
|
+
let wrote = false;
|
|
147
|
+
const covered = listedTwice(id);
|
|
148
|
+
for (const root of agentSkillRoots(id)) {
|
|
149
|
+
const dir = join(root, AGENT_SKILL_NAME);
|
|
150
|
+
const ours = existsSync(join(dir, AGENT_SKILL_MARKER));
|
|
151
|
+
// Covered already: a copy of ours from an earlier run is now the duplicate.
|
|
152
|
+
if (covered) {
|
|
153
|
+
if (ours) removeSkillDir(dir);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (existsSync(dir) && !ours) continue;
|
|
157
|
+
let created = false;
|
|
158
|
+
let marked = false;
|
|
159
|
+
try {
|
|
160
|
+
// Marker first: a run interrupted before it would leave a folder every
|
|
161
|
+
// later run takes for someone else's and skips.
|
|
162
|
+
mkdirSync(root, { recursive: true });
|
|
163
|
+
if (!existsSync(dir)) {
|
|
164
|
+
mkdirSync(dir);
|
|
165
|
+
created = true;
|
|
166
|
+
}
|
|
167
|
+
writeConfigFileAtomic(join(dir, AGENT_SKILL_MARKER), "", { backup: false, symlinks: "replace" });
|
|
168
|
+
marked = true;
|
|
169
|
+
writeConfigFileAtomic(join(dir, "SKILL.md"), AGENT_SKILL_MD, { backup: false, symlinks: "replace" });
|
|
170
|
+
wrote = true;
|
|
171
|
+
} catch (err) {
|
|
172
|
+
if (created && !marked) {
|
|
173
|
+
try { rmdirSync(dir); } catch { /* Preserve a directory that is no longer empty. */ }
|
|
174
|
+
}
|
|
175
|
+
hookLogWarn(`the failproofai skill was not written to ${dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (wrote) installed.push(id);
|
|
179
|
+
}
|
|
180
|
+
return installed;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Every skill folder failproofai wrote, for any agent. */
|
|
184
|
+
export function installedAgentSkillDirs(): string[] {
|
|
185
|
+
const dirs = INTEGRATION_TYPES.flatMap((id) => agentSkillRoots(id).map((root) => join(root, AGENT_SKILL_NAME)));
|
|
186
|
+
return [...new Set(dirs)].filter((dir) => existsSync(join(dir, AGENT_SKILL_MARKER)));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Remove one skill folder; never throws, so neither setup nor uninstall stops on it. */
|
|
190
|
+
function removeSkillDir(dir: string): boolean {
|
|
191
|
+
try {
|
|
192
|
+
rmSync(dir, { recursive: true, force: true });
|
|
193
|
+
return true;
|
|
194
|
+
} catch (err) {
|
|
195
|
+
hookLogWarn(`the failproofai skill was not removed from ${dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Remove every skill folder failproofai wrote. Returns the folders removed. */
|
|
201
|
+
export function removeAgentSkill(): string[] {
|
|
202
|
+
return installedAgentSkillDirs().filter(removeSkillDir);
|
|
203
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Agent identity for enforcement, separate from the project-derived telemetry agent_id. */
|
|
2
|
+
import { INTEGRATION_TYPES, type IntegrationType } from "./types";
|
|
3
|
+
|
|
4
|
+
export interface AgentIdentity {
|
|
5
|
+
integration: IntegrationType;
|
|
6
|
+
instanceId: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AgentTarget {
|
|
10
|
+
integration: IntegrationType;
|
|
11
|
+
instanceId?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const INSTANCE_ID_RE = /^agt_[0-9a-f]{16,32}$/;
|
|
15
|
+
const integrations = new Set<string>(INTEGRATION_TYPES);
|
|
16
|
+
|
|
17
|
+
export function validAgentIdentity(value: unknown): value is AgentIdentity {
|
|
18
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
19
|
+
const agent = value as Record<string, unknown>;
|
|
20
|
+
return Object.keys(agent).every((key) => key === "integration" || key === "instanceId") &&
|
|
21
|
+
typeof agent.integration === "string" && integrations.has(agent.integration) &&
|
|
22
|
+
typeof agent.instanceId === "string" && INSTANCE_ID_RE.test(agent.instanceId);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Null/absent means all; a malformed selector is never silently treated as all. */
|
|
26
|
+
export function parseAgentTargets(value: unknown, schemaVersion: number): AgentTarget[] | undefined {
|
|
27
|
+
if (value === undefined || value === null) return undefined;
|
|
28
|
+
if (schemaVersion < 3 || !Array.isArray(value) || value.length < 1 || value.length > 32) {
|
|
29
|
+
throw new Error("invalid agentTargets in cloud-managed active manifest");
|
|
30
|
+
}
|
|
31
|
+
const seen = new Set<string>();
|
|
32
|
+
return value.map((raw) => {
|
|
33
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
34
|
+
throw new Error("invalid agentTargets in cloud-managed active manifest");
|
|
35
|
+
}
|
|
36
|
+
const entry = raw as Record<string, unknown>;
|
|
37
|
+
if (Object.keys(entry).some((key) => key !== "integration" && key !== "instanceId") ||
|
|
38
|
+
typeof entry.integration !== "string" || !integrations.has(entry.integration) ||
|
|
39
|
+
(entry.instanceId !== undefined &&
|
|
40
|
+
(typeof entry.instanceId !== "string" || !INSTANCE_ID_RE.test(entry.instanceId)))) {
|
|
41
|
+
throw new Error("invalid agentTargets in cloud-managed active manifest");
|
|
42
|
+
}
|
|
43
|
+
const key = `${entry.integration}\0${entry.instanceId ?? ""}`;
|
|
44
|
+
if (seen.has(key)) throw new Error("duplicate agentTargets in cloud-managed active manifest");
|
|
45
|
+
seen.add(key);
|
|
46
|
+
return {
|
|
47
|
+
integration: entry.integration as IntegrationType,
|
|
48
|
+
...(entry.instanceId !== undefined ? { instanceId: entry.instanceId as string } : {}),
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** An unknown runtime identity cannot widen any scoped assignment. */
|
|
54
|
+
export function agentTargetsMatch(targets: readonly AgentTarget[] | undefined, agent: AgentIdentity | null): boolean {
|
|
55
|
+
if (targets === undefined) return true;
|
|
56
|
+
if (!validAgentIdentity(agent)) return false;
|
|
57
|
+
return targets.some((target) =>
|
|
58
|
+
target.integration === agent.integration &&
|
|
59
|
+
(target.instanceId === undefined || target.instanceId === agent.instanceId));
|
|
60
|
+
}
|