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.
Files changed (214) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +2 -2
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +3 -3
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/audit/run/route.js +22 -22
  23. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  31. package/.next/standalone/.next/server/app/audit/page.js +1 -1
  32. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  34. package/.next/standalone/.next/server/app/index.html +1 -1
  35. package/.next/standalone/.next/server/app/index.rsc +3 -3
  36. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  37. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
  38. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  39. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  40. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  42. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  43. package/.next/standalone/.next/server/app/policies/page.js +4 -4
  44. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  50. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  51. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  54. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
  57. package/.next/standalone/.next/server/app/settings/page.js +7 -7
  58. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  60. package/.next/standalone/.next/server/chunks/{[externals]__0-ha5ydqq0mws._.js → [externals]__0vt9i6fqinjne._.js} +1 -1
  61. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__20xe05hm5u8_1._.js → [root-of-the-server]__0f_4g-vazrvxv._.js} +1 -1
  62. package/.next/standalone/.next/server/chunks/_0hf5q4xzyg40j._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/_0tovk6q0miud6._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/_0trp3ycqv17cl._.js +1 -1
  65. package/.next/standalone/.next/server/chunks/_19011-u44143j._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/_1ek68lnh2daur._.js +2 -2
  67. package/.next/standalone/.next/server/chunks/{_0kc-piux3hady._.js → _1y590mjzpkbos._.js} +2 -2
  68. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4vkpxxu7._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/src_hooks_0_em82s16zfym._.js +5 -0
  70. package/.next/standalone/.next/server/chunks/src_hooks_builtin-policies_ts_1yq5ybgx1v98e._.js +3 -0
  71. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3h94wnn._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/src_hooks_daemon-client_ts_0il_yu1zc0291._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn_4twyh._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ts0fg5yih1c1._.js → [root-of-the-server]__00l2u6j86cb8y._.js} +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08ch3staq3ejw._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bigvy9xmv2ka._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h86bl2qf0k_o._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1_xbg2kx30wp-._.js → [root-of-the-server]__0naevg4bcoh54._.js} +1 -1
  79. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pia5h3p8re4c._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pwj2h2efe5f-._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-ey9fgesfbc0._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15usvs-ms-0hf._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16gb3_92zvk5x._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1avi2kypusyb_._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1r7drlqli-ado._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0vcyntk4snf0f._.js → [root-of-the-server]__1vofkb2oq7p7e._.js} +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/_0bseae9u7g98r._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/_0eoura3_9gjb-._.js +1 -1
  89. package/.next/standalone/.next/server/chunks/ssr/_0mouhaflye1ro._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/{_1gb0ifpkowwub._.js → _0ouu886szh6yj._.js} +10 -9
  91. package/.next/standalone/.next/server/chunks/ssr/_0pedwblqdoh58._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/_1djqs9i27e4kg._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/{_1ve8y151ua7qx._.js → _1fw0_nzkae5ao._.js} +2 -2
  94. package/.next/standalone/.next/server/chunks/ssr/_20hn4gn87nsjc._.js +1 -1
  95. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo5_vm5f.js +2 -2
  96. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9snixb5fd-._.js +1 -1
  97. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47q0xq5w._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/app_components_project-list_tsx_1ke6anr0vt64h._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x33jqb6._.js +1 -1
  100. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpcz0uxur._.js +2 -2
  101. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h47jmhhd._.js +1 -1
  102. 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
  103. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0janj0i6dqko7._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/package_json_[json]_cjs_013tntikdft59._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9ddabn3_._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0agdmptetetdt._.js +5 -0
  107. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0mdwgjisv4fub._.js +5 -0
  108. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4l1tzrm._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/src_hooks_daemon-client_ts_0k41_i7jg6ndv._.js +1 -1
  110. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589gp4p60j._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me658zqmh_._.js +1 -1
  112. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  113. package/.next/standalone/.next/server/pages/404.html +1 -1
  114. package/.next/standalone/.next/server/pages/500.html +1 -1
  115. package/.next/standalone/.next/server/preview-props.json +3 -3
  116. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  117. package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
  118. package/.next/standalone/.next/static/chunks/{0pjxnp927y7_j.js → 0ay__-d2h1xb5.js} +1 -1
  119. package/.next/standalone/.next/static/chunks/{2znt1784j9gd9.js → 0tbm3vjm62h-2.js} +1 -1
  120. package/.next/standalone/.next/static/chunks/1c_jf6g5wnslk.js +1 -0
  121. package/.next/standalone/.next/static/chunks/{3tn1bzk094e8e.js → 1my3-5p47t7r-.js} +1 -1
  122. package/.next/standalone/.next/static/chunks/20py8k1gd_i1m.js +1 -0
  123. package/.next/standalone/.next/static/chunks/21hckpmsc4g70.js +6 -0
  124. package/.next/standalone/.next/static/chunks/2hmex93ppj1ke.js +1 -0
  125. package/.next/standalone/.next/static/chunks/2o6d37j425uci.js +1 -0
  126. package/.next/standalone/.next/static/chunks/40oworn_-x12p.js +1 -0
  127. package/.next/standalone/.next/static/chunks/{2qz5piiir-qqm.js → 42xptz88mtq3x.js} +1 -1
  128. package/.next/standalone/app/actions/update-jev-config.ts +5 -3
  129. package/.next/standalone/app/settings/jev-panel.tsx +1 -1
  130. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +32 -0
  131. package/.next/standalone/fp-cloud-cli/README.md +40 -1
  132. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +91 -12
  133. package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +78 -16
  134. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +84 -18
  135. package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +124 -6
  136. package/.next/standalone/fp-cloud-cli/fp_cli/models.py +89 -6
  137. package/.next/standalone/fp-cloud-cli/fp_cli/output.py +189 -20
  138. package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +8 -5
  139. package/.next/standalone/fp-cloud-cli/skill/SKILL.md +2 -2
  140. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +14 -7
  141. package/.next/standalone/fp-cloud-cli/tests/test_cloud_jev.py +527 -0
  142. package/.next/standalone/fp-cloud-cli/tests/test_enforcement_logic.py +46 -0
  143. package/.next/standalone/fp-cloud-cli/tests/test_output.py +23 -0
  144. package/.next/standalone/hermes-plugin/README.md +6 -4
  145. package/.next/standalone/hermes-plugin/__init__.py +27 -0
  146. package/.next/standalone/hermes-plugin/client.py +5 -1
  147. package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
  148. package/.next/standalone/package.json +11 -10
  149. package/.next/standalone/sdk/python/uv.lock +31 -8
  150. package/.next/standalone/server.js +1 -1
  151. package/bin/failproofai.mjs +17 -1
  152. package/dist/cli.mjs +3507 -1250
  153. package/dist/worker.mjs +1779 -169
  154. package/hermes-plugin/README.md +6 -4
  155. package/hermes-plugin/__init__.py +27 -0
  156. package/hermes-plugin/client.py +5 -1
  157. package/hermes-plugin/plugin.yaml +1 -1
  158. package/package.json +11 -10
  159. package/src/audit/open-browser.ts +6 -7
  160. package/src/hooks/agent-roster.ts +180 -0
  161. package/src/hooks/agent-skill.ts +203 -0
  162. package/src/hooks/agent-targets.ts +60 -0
  163. package/src/hooks/cloud-connection.ts +91 -45
  164. package/src/hooks/cloud-enrollment-cli.ts +7 -4
  165. package/src/hooks/cloud-login.ts +6 -3
  166. package/src/hooks/cloud-managed-policies.ts +430 -20
  167. package/src/hooks/cloud-policy-errors.ts +481 -0
  168. package/src/hooks/codex-system-hooks.ts +10 -4
  169. package/src/hooks/config-status.ts +10 -4
  170. package/src/hooks/configure-wizard.ts +54 -26
  171. package/src/hooks/custom-hooks-loader.ts +26 -3
  172. package/src/hooks/daemon-client.ts +9 -4
  173. package/src/hooks/daemon-service.ts +2 -1
  174. package/src/hooks/delivery-health.ts +47 -2
  175. package/src/hooks/effective-reviewers.ts +36 -1
  176. package/src/hooks/fp-config.ts +19 -0
  177. package/src/hooks/fp-home.ts +6 -0
  178. package/src/hooks/handler.ts +255 -19
  179. package/src/hooks/health-probe.ts +15 -0
  180. package/src/hooks/hook-activity-store.ts +7 -1
  181. package/src/hooks/integrations.ts +36 -14
  182. package/src/hooks/jev-activity.ts +117 -1
  183. package/src/hooks/jev-cli.ts +196 -1
  184. package/src/hooks/jev-cloud-connection.ts +92 -0
  185. package/src/hooks/manager.ts +174 -34
  186. package/src/hooks/pack-failclosed.ts +2 -0
  187. package/src/hooks/pack-manifest.ts +8 -1
  188. package/src/hooks/policy-evaluator.ts +22 -1
  189. package/src/hooks/policy-registry.ts +3 -2
  190. package/src/hooks/policy-reviewability.ts +23 -5
  191. package/src/hooks/semantic/cloud-jev-health.ts +227 -0
  192. package/src/hooks/semantic/cloud-jev.ts +868 -0
  193. package/src/hooks/semantic/combine.ts +26 -1
  194. package/src/hooks/semantic/jev-client.ts +19 -2
  195. package/src/hooks/semantic/jev-config.ts +179 -30
  196. package/src/hooks/semantic/jev-review.ts +217 -10
  197. package/src/hooks/semantic/pack-policies.ts +2 -1
  198. package/src/hooks/semantic/types.ts +8 -0
  199. package/src/hooks/session-pause-cli.ts +1 -1
  200. package/src/hooks/tui.ts +13 -4
  201. package/src/hooks/uninstall-cli.ts +12 -1
  202. package/src/hooks/worker-server.ts +5 -1
  203. package/.next/standalone/.next/server/chunks/src_hooks_1aveq0uhs2t1b._.js +0 -5
  204. package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7cyqlb2._.js +0 -3
  205. package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo55j16x._.js +0 -5
  206. package/.next/standalone/.next/static/chunks/0a-ccsp3xmypz.js +0 -1
  207. package/.next/standalone/.next/static/chunks/13jv-xvabeb62.js +0 -6
  208. package/.next/standalone/.next/static/chunks/2ank2u3apy8m6.js +0 -1
  209. package/.next/standalone/.next/static/chunks/2meqb9kygrncb.js +0 -1
  210. package/.next/standalone/.next/static/chunks/2ohktp_-lkv3x.js +0 -1
  211. package/.next/standalone/.next/static/chunks/2py0eldhwz8ay.js +0 -1
  212. /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → LHCtFxVG0mdvd4lsXAOFC}/_buildManifest.js +0 -0
  213. /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → LHCtFxVG0mdvd4lsXAOFC}/_clientMiddlewareManifest.js +0 -0
  214. /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → LHCtFxVG0mdvd4lsXAOFC}/_ssgManifest.js +0 -0
@@ -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 1.
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": 1,
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": 1,
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
  )
@@ -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 = 1
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(
@@ -1,5 +1,5 @@
1
1
  name: failproofai
2
- version: 1.0.12-beta.0
2
+ version: 1.0.12-beta.2
3
3
  description: Real-time FailproofAI policy enforcement for Hermes
4
4
  manifest_version: 2
5
5
  api_version: 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "1.0.12-beta.0",
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.29.1",
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.0",
129
- "@failproofai/failproofaid-linux-arm64": "1.0.12-beta.0",
130
- "@failproofai/failproofaid-darwin-x64": "1.0.12-beta.0",
131
- "@failproofai/failproofaid-darwin-arm64": "1.0.12-beta.0"
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": "61710dd6261ff0deb1116e390c366c26809bf04bb2e2c16f279b7d79e1819049",
135
- "linux-arm64": "5e9501522d35b32790664b4f3b1b90aa1728629f5013df94e72c2e2a11842bd2",
136
- "darwin-x64": "e9d195cf30b6bc2427b7cbe89e354cc8dab7ae02e7c45a7e44a941eda84c2674",
137
- "darwin-arm64": "d70ae3a3b8073ace0852a20671dba3d32f380b97ed4a01f5791a13d2fa12e63b"
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 (process.platform === "darwin") {
38
+ if (platform === "darwin") {
39
39
  cmd = "open";
40
40
  args = [url];
41
- } else if (process.platform === "win32") {
42
- // `start` is a cmd builtin; the empty "" is the (required) window-title arg
43
- // so a URL containing `&` or spaces isn't mis-parsed as the title.
44
- cmd = "cmd";
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
+ }