@archmax-ai/harness 0.0.0

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 (351) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/dist/agent.d.ts +174 -0
  4. package/dist/agent.js +140 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/assembly/compose.d.ts +170 -0
  7. package/dist/assembly/compose.js +391 -0
  8. package/dist/assembly/compose.js.map +1 -0
  9. package/dist/assembly/delegation-registry.d.ts +5 -0
  10. package/dist/assembly/delegation-registry.js +118 -0
  11. package/dist/assembly/delegation-registry.js.map +1 -0
  12. package/dist/assembly/index.d.ts +192 -0
  13. package/dist/assembly/index.js +265 -0
  14. package/dist/assembly/index.js.map +1 -0
  15. package/dist/assembly/plain.d.ts +7 -0
  16. package/dist/assembly/plain.js +41 -0
  17. package/dist/assembly/plain.js.map +1 -0
  18. package/dist/authoring-skill/archmax-harness/README.md +76 -0
  19. package/dist/authoring-skill/archmax-harness/SKILL.md +241 -0
  20. package/dist/authoring-skill/archmax-harness/references/backend-integration.md +626 -0
  21. package/dist/authoring-skill/archmax-harness/references/cases.md +41 -0
  22. package/dist/authoring-skill/archmax-harness/references/diagnostics.md +51 -0
  23. package/dist/authoring-skill/archmax-harness/references/hook-and-test-scripts.md +1070 -0
  24. package/dist/authoring-skill/archmax-harness/references/hooks-and-scripts.md +82 -0
  25. package/dist/authoring-skill/archmax-harness/references/wiring.md +101 -0
  26. package/dist/authoring-skill/archmax-harness/references/workflow-schema.md +1631 -0
  27. package/dist/authoring-skill/archmax-harness/references/workflow-yaml.md +241 -0
  28. package/dist/behaviour/support.d.ts +232 -0
  29. package/dist/behaviour/support.js +339 -0
  30. package/dist/behaviour/support.js.map +1 -0
  31. package/dist/cli/banner.d.ts +11 -0
  32. package/dist/cli/banner.js +29 -0
  33. package/dist/cli/banner.js.map +1 -0
  34. package/dist/cli/bootstrap.d.ts +33 -0
  35. package/dist/cli/bootstrap.js +153 -0
  36. package/dist/cli/bootstrap.js.map +1 -0
  37. package/dist/cli/command.d.ts +39 -0
  38. package/dist/cli/command.js +97 -0
  39. package/dist/cli/command.js.map +1 -0
  40. package/dist/cli/state-flow.d.ts +46 -0
  41. package/dist/cli/state-flow.js +268 -0
  42. package/dist/cli/state-flow.js.map +1 -0
  43. package/dist/cli/style.d.ts +41 -0
  44. package/dist/cli/style.js +64 -0
  45. package/dist/cli/style.js.map +1 -0
  46. package/dist/cli/turn.d.ts +41 -0
  47. package/dist/cli/turn.js +161 -0
  48. package/dist/cli/turn.js.map +1 -0
  49. package/dist/cli.d.ts +10 -0
  50. package/dist/cli.js +426 -0
  51. package/dist/cli.js.map +1 -0
  52. package/dist/core/code-description.d.ts +22 -0
  53. package/dist/core/code-description.js +38 -0
  54. package/dist/core/code-description.js.map +1 -0
  55. package/dist/core/deepagents.d.ts +183 -0
  56. package/dist/core/deepagents.js +128 -0
  57. package/dist/core/deepagents.js.map +1 -0
  58. package/dist/core/events.d.ts +350 -0
  59. package/dist/core/events.js +205 -0
  60. package/dist/core/events.js.map +1 -0
  61. package/dist/core/frontmatter.d.ts +15 -0
  62. package/dist/core/frontmatter.js +32 -0
  63. package/dist/core/frontmatter.js.map +1 -0
  64. package/dist/core/listing-redact.d.ts +37 -0
  65. package/dist/core/listing-redact.js +88 -0
  66. package/dist/core/listing-redact.js.map +1 -0
  67. package/dist/core/match.d.ts +26 -0
  68. package/dist/core/match.js +75 -0
  69. package/dist/core/match.js.map +1 -0
  70. package/dist/core/message-readers.d.ts +84 -0
  71. package/dist/core/message-readers.js +159 -0
  72. package/dist/core/message-readers.js.map +1 -0
  73. package/dist/core/messages.d.ts +49 -0
  74. package/dist/core/messages.js +85 -0
  75. package/dist/core/messages.js.map +1 -0
  76. package/dist/core/mount-prompt.d.ts +28 -0
  77. package/dist/core/mount-prompt.js +55 -0
  78. package/dist/core/mount-prompt.js.map +1 -0
  79. package/dist/core/mount-redact.d.ts +28 -0
  80. package/dist/core/mount-redact.js +32 -0
  81. package/dist/core/mount-redact.js.map +1 -0
  82. package/dist/core/mounts.d.ts +88 -0
  83. package/dist/core/mounts.js +147 -0
  84. package/dist/core/mounts.js.map +1 -0
  85. package/dist/core/path-mapping.d.ts +52 -0
  86. package/dist/core/path-mapping.js +113 -0
  87. package/dist/core/path-mapping.js.map +1 -0
  88. package/dist/core/platform-prompt.generated.d.ts +2 -0
  89. package/dist/core/platform-prompt.generated.js +4 -0
  90. package/dist/core/platform-prompt.generated.js.map +1 -0
  91. package/dist/core/prompt.d.ts +42 -0
  92. package/dist/core/prompt.js +75 -0
  93. package/dist/core/prompt.js.map +1 -0
  94. package/dist/core/session-id.d.ts +38 -0
  95. package/dist/core/session-id.js +61 -0
  96. package/dist/core/session-id.js.map +1 -0
  97. package/dist/core/session-store.d.ts +85 -0
  98. package/dist/core/session-store.js +149 -0
  99. package/dist/core/session-store.js.map +1 -0
  100. package/dist/core/session-zone.d.ts +54 -0
  101. package/dist/core/session-zone.js +82 -0
  102. package/dist/core/session-zone.js.map +1 -0
  103. package/dist/core/skill-prompt.d.ts +31 -0
  104. package/dist/core/skill-prompt.js +54 -0
  105. package/dist/core/skill-prompt.js.map +1 -0
  106. package/dist/core/skill-redact.d.ts +15 -0
  107. package/dist/core/skill-redact.js +27 -0
  108. package/dist/core/skill-redact.js.map +1 -0
  109. package/dist/core/skills.d.ts +172 -0
  110. package/dist/core/skills.js +214 -0
  111. package/dist/core/skills.js.map +1 -0
  112. package/dist/core/tool-mocks.d.ts +23 -0
  113. package/dist/core/tool-mocks.js +39 -0
  114. package/dist/core/tool-mocks.js.map +1 -0
  115. package/dist/core/tool-telemetry.d.ts +16 -0
  116. package/dist/core/tool-telemetry.js +40 -0
  117. package/dist/core/tool-telemetry.js.map +1 -0
  118. package/dist/core/usage.d.ts +114 -0
  119. package/dist/core/usage.js +226 -0
  120. package/dist/core/usage.js.map +1 -0
  121. package/dist/core/workspace-context.d.ts +136 -0
  122. package/dist/core/workspace-context.js +178 -0
  123. package/dist/core/workspace-context.js.map +1 -0
  124. package/dist/core/workspace-prompt.d.ts +7 -0
  125. package/dist/core/workspace-prompt.js +102 -0
  126. package/dist/core/workspace-prompt.js.map +1 -0
  127. package/dist/core/workspace-router.d.ts +61 -0
  128. package/dist/core/workspace-router.js +145 -0
  129. package/dist/core/workspace-router.js.map +1 -0
  130. package/dist/core/workspace.d.ts +85 -0
  131. package/dist/core/workspace.js +193 -0
  132. package/dist/core/workspace.js.map +1 -0
  133. package/dist/core/zones.d.ts +181 -0
  134. package/dist/core/zones.js +202 -0
  135. package/dist/core/zones.js.map +1 -0
  136. package/dist/env.d.ts +99 -0
  137. package/dist/env.js +271 -0
  138. package/dist/env.js.map +1 -0
  139. package/dist/index.d.ts +265 -0
  140. package/dist/index.js +197 -0
  141. package/dist/index.js.map +1 -0
  142. package/dist/kernel/kernel.d.ts +192 -0
  143. package/dist/kernel/kernel.js +751 -0
  144. package/dist/kernel/kernel.js.map +1 -0
  145. package/dist/lifecycle/hook-shape.d.ts +44 -0
  146. package/dist/lifecycle/hook-shape.js +66 -0
  147. package/dist/lifecycle/hook-shape.js.map +1 -0
  148. package/dist/lifecycle/runner.d.ts +177 -0
  149. package/dist/lifecycle/runner.js +372 -0
  150. package/dist/lifecycle/runner.js.map +1 -0
  151. package/dist/lifecycle/transcript.d.ts +27 -0
  152. package/dist/lifecycle/transcript.js +43 -0
  153. package/dist/lifecycle/transcript.js.map +1 -0
  154. package/dist/machine/allow.d.ts +65 -0
  155. package/dist/machine/allow.js +107 -0
  156. package/dist/machine/allow.js.map +1 -0
  157. package/dist/machine/delegation.d.ts +9 -0
  158. package/dist/machine/delegation.js +10 -0
  159. package/dist/machine/delegation.js.map +1 -0
  160. package/dist/machine/diagnostic.d.ts +14 -0
  161. package/dist/machine/diagnostic.js +2 -0
  162. package/dist/machine/diagnostic.js.map +1 -0
  163. package/dist/machine/lint-spec.d.ts +4 -0
  164. package/dist/machine/lint-spec.js +266 -0
  165. package/dist/machine/lint-spec.js.map +1 -0
  166. package/dist/machine/load-spec.d.ts +90 -0
  167. package/dist/machine/load-spec.js +120 -0
  168. package/dist/machine/load-spec.js.map +1 -0
  169. package/dist/machine/machine.d.ts +273 -0
  170. package/dist/machine/machine.js +551 -0
  171. package/dist/machine/machine.js.map +1 -0
  172. package/dist/machine/mount-grants.d.ts +25 -0
  173. package/dist/machine/mount-grants.js +24 -0
  174. package/dist/machine/mount-grants.js.map +1 -0
  175. package/dist/machine/slug.d.ts +14 -0
  176. package/dist/machine/slug.js +17 -0
  177. package/dist/machine/slug.js.map +1 -0
  178. package/dist/machine/spec-schema.d.ts +662 -0
  179. package/dist/machine/spec-schema.js +717 -0
  180. package/dist/machine/spec-schema.js.map +1 -0
  181. package/dist/machine/tool-names.d.ts +98 -0
  182. package/dist/machine/tool-names.js +144 -0
  183. package/dist/machine/tool-names.js.map +1 -0
  184. package/dist/machine/triggers.d.ts +114 -0
  185. package/dist/machine/triggers.js +165 -0
  186. package/dist/machine/triggers.js.map +1 -0
  187. package/dist/machine/types.d.ts +186 -0
  188. package/dist/machine/types.js +12 -0
  189. package/dist/machine/types.js.map +1 -0
  190. package/dist/machine/validate-spec.d.ts +42 -0
  191. package/dist/machine/validate-spec.js +29 -0
  192. package/dist/machine/validate-spec.js.map +1 -0
  193. package/dist/machine/variables.d.ts +196 -0
  194. package/dist/machine/variables.js +415 -0
  195. package/dist/machine/variables.js.map +1 -0
  196. package/dist/public/cli.d.ts +15 -0
  197. package/dist/public/cli.js +14 -0
  198. package/dist/public/cli.js.map +1 -0
  199. package/dist/public/messages.d.ts +33 -0
  200. package/dist/public/messages.js +33 -0
  201. package/dist/public/messages.js.map +1 -0
  202. package/dist/public/sandbox.d.ts +99 -0
  203. package/dist/public/sandbox.js +32 -0
  204. package/dist/public/sandbox.js.map +1 -0
  205. package/dist/public/spec.d.ts +147 -0
  206. package/dist/public/spec.js +149 -0
  207. package/dist/public/spec.js.map +1 -0
  208. package/dist/public/testing.d.ts +71 -0
  209. package/dist/public/testing.js +39 -0
  210. package/dist/public/testing.js.map +1 -0
  211. package/dist/rubrics/rubrics.d.ts +135 -0
  212. package/dist/rubrics/rubrics.js +145 -0
  213. package/dist/rubrics/rubrics.js.map +1 -0
  214. package/dist/runtime/contract.d.ts +73 -0
  215. package/dist/runtime/contract.js +96 -0
  216. package/dist/runtime/contract.js.map +1 -0
  217. package/dist/sandbox/assets/parts/core.js +22 -0
  218. package/dist/sandbox/assets/parts/lifecycle-hook.js +47 -0
  219. package/dist/sandbox/bridge.d.ts +16 -0
  220. package/dist/sandbox/bridge.js +95 -0
  221. package/dist/sandbox/bridge.js.map +1 -0
  222. package/dist/sandbox/executor.d.ts +46 -0
  223. package/dist/sandbox/executor.js +119 -0
  224. package/dist/sandbox/executor.js.map +1 -0
  225. package/dist/sandbox/imports.d.ts +33 -0
  226. package/dist/sandbox/imports.js +67 -0
  227. package/dist/sandbox/imports.js.map +1 -0
  228. package/dist/sandbox/prelude.d.ts +12 -0
  229. package/dist/sandbox/prelude.js +51 -0
  230. package/dist/sandbox/prelude.js.map +1 -0
  231. package/dist/sandbox/ptc-gateway.d.ts +104 -0
  232. package/dist/sandbox/ptc-gateway.js +167 -0
  233. package/dist/sandbox/ptc-gateway.js.map +1 -0
  234. package/dist/sandbox/runtime.d.ts +59 -0
  235. package/dist/sandbox/runtime.js +51 -0
  236. package/dist/sandbox/runtime.js.map +1 -0
  237. package/dist/sandbox/tools.d.ts +34 -0
  238. package/dist/sandbox/tools.js +147 -0
  239. package/dist/sandbox/tools.js.map +1 -0
  240. package/dist/sessions/operations.d.ts +79 -0
  241. package/dist/sessions/operations.js +121 -0
  242. package/dist/sessions/operations.js.map +1 -0
  243. package/dist/sessions/resolve.d.ts +97 -0
  244. package/dist/sessions/resolve.js +145 -0
  245. package/dist/sessions/resolve.js.map +1 -0
  246. package/dist/sessions/resume.d.ts +242 -0
  247. package/dist/sessions/resume.js +288 -0
  248. package/dist/sessions/resume.js.map +1 -0
  249. package/dist/sessions/scope.d.ts +99 -0
  250. package/dist/sessions/scope.js +145 -0
  251. package/dist/sessions/scope.js.map +1 -0
  252. package/dist/sessions/summary.d.ts +65 -0
  253. package/dist/sessions/summary.js +56 -0
  254. package/dist/sessions/summary.js.map +1 -0
  255. package/dist/testing/assertions.d.ts +30 -0
  256. package/dist/testing/assertions.js +231 -0
  257. package/dist/testing/assertions.js.map +1 -0
  258. package/dist/testing/case-document.d.ts +141 -0
  259. package/dist/testing/case-document.js +51 -0
  260. package/dist/testing/case-document.js.map +1 -0
  261. package/dist/testing/case-schema.d.ts +29 -0
  262. package/dist/testing/case-schema.js +519 -0
  263. package/dist/testing/case-schema.js.map +1 -0
  264. package/dist/testing/discovery.d.ts +10 -0
  265. package/dist/testing/discovery.js +24 -0
  266. package/dist/testing/discovery.js.map +1 -0
  267. package/dist/testing/driver.d.ts +78 -0
  268. package/dist/testing/driver.js +117 -0
  269. package/dist/testing/driver.js.map +1 -0
  270. package/dist/testing/grade.d.ts +116 -0
  271. package/dist/testing/grade.js +261 -0
  272. package/dist/testing/grade.js.map +1 -0
  273. package/dist/testing/interpreter.d.ts +55 -0
  274. package/dist/testing/interpreter.js +103 -0
  275. package/dist/testing/interpreter.js.map +1 -0
  276. package/dist/testing/mock-middleware.d.ts +5 -0
  277. package/dist/testing/mock-middleware.js +33 -0
  278. package/dist/testing/mock-middleware.js.map +1 -0
  279. package/dist/testing/runner.d.ts +205 -0
  280. package/dist/testing/runner.js +266 -0
  281. package/dist/testing/runner.js.map +1 -0
  282. package/dist/testing/target.d.ts +27 -0
  283. package/dist/testing/target.js +21 -0
  284. package/dist/testing/target.js.map +1 -0
  285. package/dist/validate/validate.d.ts +41 -0
  286. package/dist/validate/validate.js +669 -0
  287. package/dist/validate/validate.js.map +1 -0
  288. package/dist/workflow/agent-tools.d.ts +10 -0
  289. package/dist/workflow/agent-tools.js +10 -0
  290. package/dist/workflow/agent-tools.js.map +1 -0
  291. package/dist/workflow/checkpointer.d.ts +52 -0
  292. package/dist/workflow/checkpointer.js +200 -0
  293. package/dist/workflow/checkpointer.js.map +1 -0
  294. package/dist/workflow/control-tools.d.ts +174 -0
  295. package/dist/workflow/control-tools.js +468 -0
  296. package/dist/workflow/control-tools.js.map +1 -0
  297. package/dist/workflow/governance.d.ts +104 -0
  298. package/dist/workflow/governance.js +546 -0
  299. package/dist/workflow/governance.js.map +1 -0
  300. package/dist/workflow/middleware.d.ts +85 -0
  301. package/dist/workflow/middleware.js +283 -0
  302. package/dist/workflow/middleware.js.map +1 -0
  303. package/dist/workflow/on-error.d.ts +25 -0
  304. package/dist/workflow/on-error.js +58 -0
  305. package/dist/workflow/on-error.js.map +1 -0
  306. package/dist/workflow/parks.d.ts +46 -0
  307. package/dist/workflow/parks.js +513 -0
  308. package/dist/workflow/parks.js.map +1 -0
  309. package/dist/workflow/paths.d.ts +67 -0
  310. package/dist/workflow/paths.js +108 -0
  311. package/dist/workflow/paths.js.map +1 -0
  312. package/dist/workflow/prompt-cache.d.ts +70 -0
  313. package/dist/workflow/prompt-cache.js +103 -0
  314. package/dist/workflow/prompt-cache.js.map +1 -0
  315. package/dist/workflow/prompt-pruning.d.ts +20 -0
  316. package/dist/workflow/prompt-pruning.js +83 -0
  317. package/dist/workflow/prompt-pruning.js.map +1 -0
  318. package/dist/workflow/render-prompt.d.ts +45 -0
  319. package/dist/workflow/render-prompt.js +140 -0
  320. package/dist/workflow/render-prompt.js.map +1 -0
  321. package/dist/workflow/runtime-config.d.ts +7 -0
  322. package/dist/workflow/runtime-config.js +9 -0
  323. package/dist/workflow/runtime-config.js.map +1 -0
  324. package/dist/workflow/session-artifacts.d.ts +90 -0
  325. package/dist/workflow/session-artifacts.js +134 -0
  326. package/dist/workflow/session-artifacts.js.map +1 -0
  327. package/dist/workflow/session-view.d.ts +31 -0
  328. package/dist/workflow/session-view.js +94 -0
  329. package/dist/workflow/session-view.js.map +1 -0
  330. package/dist/workflow/snapshot.d.ts +14 -0
  331. package/dist/workflow/snapshot.js +97 -0
  332. package/dist/workflow/snapshot.js.map +1 -0
  333. package/dist/workflow/state.d.ts +347 -0
  334. package/dist/workflow/state.js +322 -0
  335. package/dist/workflow/state.js.map +1 -0
  336. package/dist/workflow/sub-workflow.d.ts +174 -0
  337. package/dist/workflow/sub-workflow.js +392 -0
  338. package/dist/workflow/sub-workflow.js.map +1 -0
  339. package/dist/workflow/tool-service.d.ts +48 -0
  340. package/dist/workflow/tool-service.js +291 -0
  341. package/dist/workflow/tool-service.js.map +1 -0
  342. package/dist/workflow/turn-boundary.d.ts +39 -0
  343. package/dist/workflow/turn-boundary.js +128 -0
  344. package/dist/workflow/turn-boundary.js.map +1 -0
  345. package/dist/workflow/turn-runner.d.ts +40 -0
  346. package/dist/workflow/turn-runner.js +122 -0
  347. package/dist/workflow/turn-runner.js.map +1 -0
  348. package/dist/workflow/workflow-tools.d.ts +46 -0
  349. package/dist/workflow/workflow-tools.js +90 -0
  350. package/dist/workflow/workflow-tools.js.map +1 -0
  351. package/package.json +119 -0
@@ -0,0 +1,751 @@
1
+ import { ADVANCE_TOOL, HARNESS_CONTROL_TOOLS, EVAL_TOOL, RUN_TOOL, SET_VARIABLES_TOOL, UNGRANTABLE_TOOLS, } from "../machine/tool-names.js";
2
+ import { FORBID_ANY_TOOL } from "../machine/spec-schema.js";
3
+ import { argsSatisfy, normalizeAllowEntry } from "../machine/allow.js";
4
+ import { unmetRequirements } from "../workflow/control-tools.js";
5
+ import { canonicalizeRelPath } from "../core/workspace.js";
6
+ import { classifyWorkspacePath, mountNameOf, SESSION_INTERNAL_DIRS, SESSION_OFFLOAD_DIRS, SESSION_OPEN_DIR, } from "../core/zones.js";
7
+ import { NO_MOUNTS } from "../core/mounts.js";
8
+ import { NO_SKILLS, skillOfPath } from "../core/skills.js";
9
+ import { authoringPlanePrefix, describeAuthoringPrefix } from "../core/mounts.js";
10
+ import { rubricTransitionOutcome } from "../rubrics/rubrics.js";
11
+ /**
12
+ * Agent tools that scripts may never invoke through programmatic tool calling
13
+ * (PTC): every harness control tool (including both sandbox entry points, so
14
+ * sandboxed code cannot re-enter the sandbox) plus a bare `eval`. Owned by
15
+ * governance so the interpreter and the lifecycle runner cannot drift.
16
+ */
17
+ export const PTC_EXCLUDED_TOOLS = new Set([
18
+ // Moving the state machine is the model's governed decision, and a script
19
+ // that could *write* variables would be editing the governance inputs of the
20
+ // state it judges (scripts read them via the `args.variables` snapshot).
21
+ ...HARNESS_CONTROL_TOOLS,
22
+ "eval",
23
+ ]);
24
+ const EVAL_BLOCK_MESSAGE = `[workflow] BLOCKED: an unnamespaced \`eval\` tool is never governed — use ` +
25
+ `\`${EVAL_TOOL}\`, the runtime's own code interpreter.`;
26
+ const TASK_BLOCK_MESSAGE = `[workflow] BLOCKED: \`task\` is the runtime's own dispatch for grading rubrics, not a tool ` +
27
+ `a state may call. A rubric grades your work; you do not call it. To delegate work to another ` +
28
+ `process, use the workflow's declared \`archmax_workflow_<slug>\` tool.`;
29
+ const ALLOW = { decision: "allow", ruleId: "allow" };
30
+ /** File-writing built-in tools whose `file_path` argument the kernel governs. */
31
+ const WRITE_TOOLS = new Set(["write_file", "edit_file"]);
32
+ /**
33
+ * File tools whose access to the always-open run areas is permitted in every
34
+ * state, independent of the state's `allow` list.
35
+ */
36
+ const SESSION_OPEN_TOOLS = new Set(["read_file", "write_file", "edit_file", "ls"]);
37
+ /** Read-shaped file tools (offload areas are readable but never writable). */
38
+ const READ_TOOLS = new Set(["read_file", "ls"]);
39
+ /** Every file tool whose path argument the zone rules classify. */
40
+ const PATH_TOOLS = new Set(["read_file", "write_file", "edit_file", "ls", "glob", "grep"]);
41
+ /** The path argument a file tool carries, whatever it is named. */
42
+ function pathArg(args) {
43
+ const target = args.file_path ?? args.path;
44
+ return typeof target === "string" ? target : null;
45
+ }
46
+ /**
47
+ * Canonicalize a tool `file_path` argument to the single workspace-relative
48
+ * form the backend resolves, so no zone decision can be dodged with a `./` or
49
+ * `..` prefix. Traversal that escapes the root keeps its leading `..`, which
50
+ * lands in no governed zone (and is separately refused by the backend).
51
+ */
52
+ function normalizeZonePath(path) {
53
+ return canonicalizeRelPath(path).path;
54
+ }
55
+ /**
56
+ * Whether a workspace-relative path targets the read-only authored zone (an
57
+ * authored mount or an authored root file), via the shared table in
58
+ * `core/zones.ts` so runtime, mount routing, and validation agree.
59
+ */
60
+ export function isReadOnlyZonePath(path, mounts) {
61
+ return classifyWorkspacePath(path, mounts) === "authored";
62
+ }
63
+ /**
64
+ * Reduce a single lifecycle-hook fact to a verdict. Fail-closed: an execution
65
+ * error is a veto. `before` hooks gate entry with `ok`/`veto` only — a `correct`
66
+ * at entry has nothing to retry, so it reduces to a veto and never consumes the
67
+ * correction budget. Within an `after` veto window any hook's `correct` (script
68
+ * or subagent judge alike) enters the same bounded correction flow, budgeted by
69
+ * the hook's `max_corrections`.
70
+ */
71
+ export function reduceHookFact(fact, opts) {
72
+ if (fact.error != null) {
73
+ return { decision: "block", ruleId: "hook.error", reason: fact.error };
74
+ }
75
+ if (fact.verdict === "ok")
76
+ return { decision: "allow", ruleId: "hook.ok" };
77
+ if (opts.phase === "before") {
78
+ const reason = fact.verdict === "correct"
79
+ ? `before hooks cannot request corrections ('correct' gates nothing at entry, treated as veto): ${fact.reason}`
80
+ : fact.reason;
81
+ return { decision: "block", ruleId: "hook.veto", reason };
82
+ }
83
+ if (opts.phase === "after" && opts.veto) {
84
+ const outcome = rubricTransitionOutcome(fact, {
85
+ maxIterations: fact.maxIterations ?? 0,
86
+ iterationsUsed: fact.iterationsUsed ?? 0,
87
+ to: opts.to,
88
+ });
89
+ if (outcome.reason) {
90
+ return {
91
+ decision: "block",
92
+ ruleId: fact.rubric ? "hook.rubric" : "hook.veto",
93
+ reason: outcome.reason,
94
+ // Only a block that actually granted a retry spends one, and only a
95
+ // `correct` refused for want of budget is unretryable.
96
+ correctionConsumed: outcome.consumesIteration,
97
+ budgetExhausted: outcome.budgetExhausted,
98
+ };
99
+ }
100
+ return { decision: "allow", ruleId: fact.rubric ? "hook.rubric" : "hook.ok" };
101
+ }
102
+ if (opts.veto) {
103
+ return {
104
+ decision: "block",
105
+ ruleId: "hook.veto",
106
+ reason: fact.reason,
107
+ correctionConsumed: false,
108
+ };
109
+ }
110
+ return { decision: "allow", ruleId: "hook.noop" };
111
+ }
112
+ /** Reduce an ordered list of hook facts, short-circuiting on the first block. */
113
+ function reduceHookFacts(facts, opts) {
114
+ for (const fact of facts) {
115
+ const verdict = reduceHookFact(fact, opts);
116
+ if (verdict.decision !== "allow")
117
+ return verdict;
118
+ }
119
+ return ALLOW;
120
+ }
121
+ /** The `not allowed in this state` block verdict shared by the tool-governance rules. */
122
+ function notAllowedVerdict(machine, state, tool, args) {
123
+ const target = typeof args.file_path === "string" ? ` on '${args.file_path}'` : "";
124
+ const toHint = typeof args.to === "string" ? ` (to='${args.to}')` : "";
125
+ // A terminal state has no outgoing transitions, so telling the agent to call
126
+ // the advance tool there invites a retry loop. Direct it to just finish.
127
+ const closingHint = machine.isTerminal(state)
128
+ ? `This state ends automatically once you finish — do not call ${ADVANCE_TOOL}.`
129
+ : `Complete the current state's work and call ${ADVANCE_TOOL} when ready to move on.`;
130
+ const reason = `[workflow] BLOCKED: '${tool}'${target}${toHint} is not allowed in state '${state}'.\n` +
131
+ `Allowed in '${state}': ${machine.describeAllowed(state)}.\n` +
132
+ closingHint;
133
+ return { decision: "block", ruleId: "tool.not-allowed", reason, warn: true };
134
+ }
135
+ /**
136
+ * Safety rule: during a reply-only turn no tool call succeeds. A parked run may
137
+ * speak, never act — no tools were disclosed, so a call here is one the model
138
+ * invented from transcript context. Evaluated ahead of every other rule because
139
+ * the control tools, `allow_always`, and the always-open scratchpad would each
140
+ * otherwise permit exactly the acting this forbids.
141
+ */
142
+ const replyOnlyRule = (action) => {
143
+ if (action.kind !== "tool-call" || action.replyOnly !== true)
144
+ return null;
145
+ return {
146
+ decision: "block",
147
+ ruleId: "tool.reply-only",
148
+ reason: `[workflow] BLOCKED: '${action.tool}' cannot run — this run is parked and a person is ` +
149
+ `deciding what happens next. Reply to them in text; you cannot act until the run resumes.`,
150
+ warn: true,
151
+ };
152
+ };
153
+ /**
154
+ * Safety rule: a tool named bare `eval` is always blocked — the harness's own
155
+ * interpreter is `archmax_eval`; an unnamespaced one is not the tool governance
156
+ * reasons about. Non-overridable — evaluated ahead of any custom rule.
157
+ */
158
+ const evalBlockRule = (action) => {
159
+ if (action.kind !== "tool-call" || action.tool !== "eval")
160
+ return null;
161
+ return { decision: "block", ruleId: "tool.eval", reason: EVAL_BLOCK_MESSAGE, warn: false };
162
+ };
163
+ /**
164
+ * Safety rule: `task` is never callable by the agent. Registered rubrics make the
165
+ * framework provide the tool so the *runtime* has a runnable to dispatch through;
166
+ * that is not a grant. Non-overridable, so no state entry, `allow_always` or
167
+ * consumer rule can re-open it — which is why disclosure withholding it and this
168
+ * rule refusing it are two independent gates on the same fact.
169
+ */
170
+ const taskBlockRule = (action) => {
171
+ if (action.kind !== "tool-call" || !UNGRANTABLE_TOOLS.has(action.tool))
172
+ return null;
173
+ return { decision: "block", ruleId: "tool.ungrantable", reason: TASK_BLOCK_MESSAGE, warn: false };
174
+ };
175
+ /**
176
+ * Safety rule: writes into the read-only authored zone are always blocked.
177
+ * Non-overridable — evaluated ahead of any custom rule.
178
+ */
179
+ const readOnlyZoneRule = (action, api) => {
180
+ if (action.kind !== "tool-call")
181
+ return null;
182
+ const { tool, args } = action;
183
+ if (WRITE_TOOLS.has(tool) &&
184
+ typeof args.file_path === "string" &&
185
+ isReadOnlyZonePath(args.file_path, api.mountPrefixes)) {
186
+ const rel = normalizeZonePath(args.file_path);
187
+ // The mount itself, so a nested key is named as authored (`catalogs/eu`)
188
+ // rather than by the segment it happens to sit under.
189
+ const mount = mountNameOf(rel, api.mountPrefixes) ?? rel.split("/")[0];
190
+ return {
191
+ decision: "block",
192
+ ruleId: "zone.read-only",
193
+ reason: `[workflow] BLOCKED: '${tool}' on '${args.file_path}' targets a read-only ` +
194
+ `mount ('${mount}'). Authored content is mounted read-only — write your files under ` +
195
+ `'${SESSION_OPEN_DIR}/…' (persisted per-session in the session store).`,
196
+ warn: true,
197
+ };
198
+ }
199
+ return null;
200
+ };
201
+ /**
202
+ * Safety rule: the harness-internal run areas (`checkpoints/`, `artifacts/`,
203
+ * `_specs/`) are not addressable by agent tools at all — not read, not written.
204
+ * They hold the runtime's own bookkeeping for the run being executed.
205
+ */
206
+ const runtimeInternalRule = (action) => {
207
+ if (action.kind !== "tool-call" || !PATH_TOOLS.has(action.tool))
208
+ return null;
209
+ const target = pathArg(action.args);
210
+ if (target == null || classifyWorkspacePath(target) !== "run-internal")
211
+ return null;
212
+ return {
213
+ decision: "block",
214
+ ruleId: "zone.runtime-internal",
215
+ reason: `[workflow] BLOCKED: '${action.tool}' on '${target}' targets a harness-internal run ` +
216
+ `area (${SESSION_INTERNAL_DIRS.map((d) => `'${d}/'`).join(", ")}). These hold the runtime's ` +
217
+ `own bookkeeping and are not part of your workspace — use '${SESSION_OPEN_DIR}/…' for ` +
218
+ `your own files.`,
219
+ warn: true,
220
+ };
221
+ };
222
+ /**
223
+ * Safety rule: the context-offload areas are runtime-owned. Deep Agents writes
224
+ * evicted content there and hands the model the path, so reads are permitted
225
+ * (see {@link runOpenAccessRule}) but an agent-initiated write is refused.
226
+ */
227
+ const runtimeManagedRule = (action) => {
228
+ if (action.kind !== "tool-call" || !WRITE_TOOLS.has(action.tool))
229
+ return null;
230
+ const filePath = action.args.file_path;
231
+ if (typeof filePath !== "string" || classifyWorkspacePath(filePath) !== "run-offload")
232
+ return null;
233
+ return {
234
+ decision: "block",
235
+ ruleId: "zone.runtime-managed",
236
+ reason: `[workflow] BLOCKED: '${action.tool}' on '${filePath}' targets a runtime-owned area ` +
237
+ `(${SESSION_OFFLOAD_DIRS.map((d) => `'${d}/'`).join(", ")}), where the runtime offloads ` +
238
+ `oversized context. You may read those files; write your own under '${SESSION_OPEN_DIR}/…'.`,
239
+ warn: true,
240
+ };
241
+ };
242
+ /**
243
+ * Safety rule: a skill bundle the acting state does not enable is unreachable.
244
+ * The enforcement half of skill governance (declared by `skills.allow` at the
245
+ * spec root and on a state): binds every path-classified tool plus
246
+ * {@link RUN_TOOL}, so a disabled capability can be neither read, listed,
247
+ * searched, written, nor executed.
248
+ *
249
+ * Non-overridable: evaluated with the safety rules, ahead of `policy`, consumer
250
+ * rules, and the per-state `allow` default, so no declaration can reach into a
251
+ * capability the state was not given; a `tools.allow` entry may only narrow
252
+ * *within* the enabled set. `lifecycle` origin is exempt (harness authority, as
253
+ * for the state allow list); `script` origin is not (model authority).
254
+ */
255
+ const skillRule = (action, api) => {
256
+ if (action.kind !== "tool-call" || action.origin === "lifecycle")
257
+ return null;
258
+ const skills = api.skills ?? NO_SKILLS;
259
+ if (skills.length === 0)
260
+ return null;
261
+ const { tool, args, state } = action;
262
+ if (!PATH_TOOLS.has(tool) && tool !== RUN_TOOL)
263
+ return null;
264
+ const target = pathArg(args);
265
+ if (target == null)
266
+ return null;
267
+ const slug = skillOfPath(target, skills);
268
+ if (slug == null)
269
+ return null;
270
+ const enabled = api.machine.enabledSkills(state, skills.map((skill) => skill.slug));
271
+ if (enabled.includes(slug))
272
+ return null;
273
+ // A slug a list *denies* is a different mistake from one nothing granted: say
274
+ // which, so an author is not sent looking for a grant they in fact wrote.
275
+ const denial = api.machine.skillDenial(state, slug);
276
+ const enabledHere = enabled.length > 0
277
+ ? `Skills enabled here: ${enabled.join(", ")}.`
278
+ : `No skill is enabled in this state.`;
279
+ if (denial) {
280
+ return {
281
+ decision: "block",
282
+ ruleId: "skill.forbidden",
283
+ reason: `[workflow] BLOCKED: '${tool}' on '${target}' belongs to the skill '${slug}', which ` +
284
+ `${denial === "workflow" ? "'skills.forbid_always' denies in every state" : `state '${state}' forbids`}. ` +
285
+ `A denial beats every grant. ${enabledHere}`,
286
+ warn: true,
287
+ };
288
+ }
289
+ return {
290
+ decision: "block",
291
+ ruleId: "skill.not-allowed",
292
+ reason: `[workflow] BLOCKED: '${tool}' on '${target}' belongs to the skill '${slug}', which is ` +
293
+ `not enabled in state '${state}'. ` +
294
+ enabledHere,
295
+ warn: true,
296
+ };
297
+ };
298
+ /**
299
+ * Safety rule: a mount the acting state does not have is unreachable.
300
+ *
301
+ * The enforcement half of mount governance (declared by `mounts.allow` at the
302
+ * spec root and on a state), and the mirror of {@link skillRule}: it binds every
303
+ * path-classified tool plus {@link RUN_TOOL}, so a mount the state cannot see
304
+ * can be neither read, listed, searched, written, nor executed from.
305
+ *
306
+ * Two kinds of mount, one rule. A **governed** mount (`MountSpec.governed`) is
307
+ * closed by default: reachable only where `enabledMounts` names it. An
308
+ * **ungoverned** mount is open in every state, so only a `forbid` takes it away.
309
+ * The refusal distinguishes the two causes, because a name a list *denies* is a
310
+ * different mistake from one nothing enabled.
311
+ *
312
+ * A mount the state *does* have is still refused a **write** when a grant
313
+ * narrowed it to `access: read` (`mount.read-only`) — the second half of a mount
314
+ * grant, beside which states may see it at all. Only a mount the host declared
315
+ * writable ever reaches that branch: the zone rules refuse a write into a
316
+ * host-read-only mount first, and say so as `zone.read-only`.
317
+ *
318
+ * Evaluated after the zone rules, so a *write* into a hidden read-only mount is
319
+ * refused as read-only — the more useful reason — whether or not the state has
320
+ * the mount. Non-overridable, and `lifecycle` origin is exempt: a hook and a
321
+ * rubric grader run on runtime authority, bound by `mounts.forbid_always` (which
322
+ * travels as an inherited rule, see {@link compileForbiddenMountRules}) and not
323
+ * by the state's own surface.
324
+ */
325
+ const mountRule = (action, api) => {
326
+ if (action.kind !== "tool-call" || action.origin === "lifecycle")
327
+ return null;
328
+ const mounts = api.mountPrefixes ?? NO_MOUNTS;
329
+ const { tool, args, state } = action;
330
+ if (!PATH_TOOLS.has(tool) && tool !== RUN_TOOL)
331
+ return null;
332
+ const target = pathArg(args);
333
+ if (target == null)
334
+ return null;
335
+ const name = mountNameOf(normalizeZonePath(target), mounts);
336
+ if (name == null)
337
+ return null;
338
+ const denial = api.machine.mountDenial(state, name);
339
+ const governed = mounts.governed.includes(name);
340
+ const enabled = api.machine.enabledMounts(state, mounts.governed);
341
+ const reachable = !denial && (!governed || enabled.includes(name));
342
+ if (reachable) {
343
+ // Reachable, but perhaps read-only *here*: a grant may narrow a writable
344
+ // mount to reads without hiding it. The zone rule has already refused a
345
+ // write into a mount the host serves read-only, so this only ever fires on
346
+ // one the host declared writable.
347
+ if (!WRITE_TOOLS.has(tool) || api.machine.mountWritable(state, name, mounts))
348
+ return null;
349
+ return {
350
+ decision: "block",
351
+ ruleId: "mount.read-only",
352
+ reason: `[workflow] BLOCKED: '${tool}' on '${target}' targets the mount '${name}', which state ` +
353
+ `'${state}' may read but not write ('access: read'). Write your files under ` +
354
+ `'${SESSION_OPEN_DIR}/…' instead.`,
355
+ warn: true,
356
+ };
357
+ }
358
+ const enabledHere = enabled.length > 0
359
+ ? `Mounts available here: ${enabled.join(", ")}.`
360
+ : `This state has no governed mount.`;
361
+ if (denial) {
362
+ return {
363
+ decision: "block",
364
+ ruleId: "mount.forbidden",
365
+ reason: `[workflow] BLOCKED: '${tool}' on '${target}' is under the mount '${name}', which ` +
366
+ `${denial === "workflow" ? "'mounts.forbid_always' denies in every state" : `state '${state}' forbids`}. ` +
367
+ `A denial beats every grant. ${enabledHere}`,
368
+ warn: true,
369
+ };
370
+ }
371
+ return {
372
+ decision: "block",
373
+ ruleId: "mount.not-allowed",
374
+ reason: `[workflow] BLOCKED: '${tool}' on '${target}' is under the mount '${name}', which state ` +
375
+ `'${state}' does not have. The path exists; this state was not given it. ` +
376
+ enabledHere,
377
+ warn: true,
378
+ };
379
+ };
380
+ /**
381
+ * An **ancestor's** `mounts.forbid_always`, as rules the descendant inherits.
382
+ * The mount twin of {@link compileForbiddenSkillRules}: a child machine resolves
383
+ * its own enabled set and cannot see a caller's denial, so the denial travels as
384
+ * a rule — any path under one of those mounts is refused, whatever the child
385
+ * granted itself, and the refusal names the workflow that denied it.
386
+ *
387
+ * A workflow-wide denial binds a hook and a rubric grader too, so this rule —
388
+ * unlike {@link mountRule} — does not exempt the `lifecycle` origin.
389
+ */
390
+ export function compileForbiddenMountRules(names, source) {
391
+ if (names.length === 0)
392
+ return [];
393
+ const denied = new Set(names);
394
+ return [
395
+ (action, api) => {
396
+ if (action.kind !== "tool-call")
397
+ return null;
398
+ const mounts = api.mountPrefixes ?? NO_MOUNTS;
399
+ const { tool, args } = action;
400
+ if (!PATH_TOOLS.has(tool) && tool !== RUN_TOOL)
401
+ return null;
402
+ const target = pathArg(args);
403
+ if (target == null)
404
+ return null;
405
+ const name = mountNameOf(normalizeZonePath(target), mounts);
406
+ if (name == null || !denied.has(name))
407
+ return null;
408
+ return {
409
+ decision: "block",
410
+ ruleId: "mount.forbidden",
411
+ reason: `[workflow] BLOCKED: '${tool}' on '${target}' is under the mount '${name}', which ` +
412
+ `workflow '${source}' denies in every state it delegates to ` +
413
+ `(mounts.forbid_always). A denial beats every grant.`,
414
+ warn: true,
415
+ };
416
+ },
417
+ ];
418
+ }
419
+ /**
420
+ * An **ancestor's** `skills.forbid_always`, as rules the descendant inherits. A
421
+ * child machine resolves its own enabled set and cannot see a caller's denial,
422
+ * so the denial travels as a rule: any path inside one of those bundles is
423
+ * refused, whatever the child granted itself.
424
+ */
425
+ export function compileForbiddenSkillRules(slugs, source) {
426
+ if (slugs.length === 0)
427
+ return [];
428
+ const denied = new Set(slugs);
429
+ return [
430
+ (action, api) => {
431
+ if (action.kind !== "tool-call")
432
+ return null;
433
+ const skills = api.skills ?? NO_SKILLS;
434
+ if (skills.length === 0)
435
+ return null;
436
+ const { tool, args } = action;
437
+ if (!PATH_TOOLS.has(tool) && tool !== RUN_TOOL)
438
+ return null;
439
+ const target = pathArg(args);
440
+ if (target == null)
441
+ return null;
442
+ const slug = skillOfPath(target, skills);
443
+ if (slug == null || !denied.has(slug))
444
+ return null;
445
+ return {
446
+ decision: "block",
447
+ ruleId: "skill.forbidden",
448
+ reason: `[workflow] BLOCKED: '${tool}' on '${target}' belongs to the skill '${slug}', which ` +
449
+ `workflow '${source}' denies in every state it delegates to ` +
450
+ `(skills.forbid_always). A denial beats every grant.`,
451
+ warn: true,
452
+ };
453
+ },
454
+ ];
455
+ }
456
+ /**
457
+ * Always-permitted run access, independent of the state's `allow` list:
458
+ * `scratchpad/…` — read, write, edit, list (run-isolated, persisted); the
459
+ * context-offload areas — read and list only (writes are blocked above).
460
+ * Evaluated after the safety, policy, and consumer rules (any of which may
461
+ * still block it), just ahead of the per-state defaults — so it loosens only
462
+ * the state `allow` list, never the governed run paths outside it.
463
+ */
464
+ const runOpenAccessRule = (action) => {
465
+ if (action.kind !== "tool-call")
466
+ return null;
467
+ const target = pathArg(action.args);
468
+ if (target == null)
469
+ return null;
470
+ const zone = classifyWorkspacePath(target);
471
+ if (zone === "run-open" && SESSION_OPEN_TOOLS.has(action.tool)) {
472
+ return { decision: "allow", ruleId: "tool.scratchpad" };
473
+ }
474
+ if (zone === "run-offload" && READ_TOOLS.has(action.tool)) {
475
+ return { decision: "allow", ruleId: "tool.offload-read" };
476
+ }
477
+ return null;
478
+ };
479
+ /**
480
+ * Default rule: the active state's `tools` governance (allow-list matching
481
+ * and the workflow-level `allow_always` grants, via {@link WorkflowMachine.checkAllowed}).
482
+ * Evaluated last, after custom and policy rules, so a custom rule may block a
483
+ * call the state would otherwise permit but cannot loosen the state's grant.
484
+ */
485
+ const stateToolsRule = (action, { machine, variables }) => {
486
+ if (action.kind !== "tool-call")
487
+ return null;
488
+ // Lifecycle hooks run on harness authority, so the state's grant does not
489
+ // bind them; every other rule in the pipeline still does.
490
+ if (action.origin === "lifecycle")
491
+ return null;
492
+ const { state, tool, args } = action;
493
+ let unresolved;
494
+ const allowed = machine.checkAllowed(state, tool, args, {
495
+ ...(variables ? { variables } : {}),
496
+ onUnresolved: (failure) => {
497
+ unresolved ??= failure;
498
+ },
499
+ });
500
+ if (allowed)
501
+ return { decision: "allow", ruleId: "tool.allowed" };
502
+ // An unresolvable reference is not a mismatch: the guard itself could not be
503
+ // evaluated, retrying cannot fix that, so the run stops here.
504
+ if (unresolved)
505
+ return unresolvedGuardVerdict(state, tool, unresolved);
506
+ return notAllowedVerdict(machine, state, tool, args);
507
+ };
508
+ /** The terminal verdict for a guard whose `${{…}}` reference could not resolve. */
509
+ function unresolvedGuardVerdict(state, tool, failure) {
510
+ return {
511
+ decision: "block",
512
+ ruleId: "tool.unresolved-variable",
513
+ terminal: true,
514
+ reason: `[workflow] FAILED: '${tool}' in state '${state}' is guarded by ` +
515
+ `'${failure.reference}', which could not be resolved — ${failure.detail}. ` +
516
+ `The run cannot continue against a guard the harness cannot evaluate.`,
517
+ warn: true,
518
+ };
519
+ }
520
+ /**
521
+ * Safety rule: `archmax_run` executes only scripts that live inside a skill bundle.
522
+ * Hook scripts (run by the lifecycle runner from the authoring plane) and runtime
523
+ * scripts (run by the agent from a skill bundle) are disjoint by construction: an
524
+ * agent cannot run its own guard, a file it wrote into the run zone, or another
525
+ * workflow's hooks. Classification uses the resolved skill registry rather than a
526
+ * literal `skills/` string, so an empty registry blocks every `archmax_run`.
527
+ *
528
+ * Non-overridable: evaluated with the safety rules, ahead of `policy`, consumer
529
+ * rules, and the per-state `allow` default, so a broad `tools.allow` entry cannot
530
+ * widen it; a state's entry may still *narrow* execution within the bundles.
531
+ */
532
+ const scriptSkillOnlyRule = (action, api) => {
533
+ if (action.kind !== "tool-call" || action.tool !== RUN_TOOL)
534
+ return null;
535
+ const target = pathArg(action.args);
536
+ if (target == null)
537
+ return null;
538
+ if (skillOfPath(target, api.skills ?? NO_SKILLS) != null)
539
+ return null;
540
+ return {
541
+ decision: "block",
542
+ ruleId: "script.skill-only",
543
+ reason: `[workflow] BLOCKED: '${RUN_TOOL}' may only execute scripts that live in a skill bundle, ` +
544
+ `and '${target}' is not in one. Scripts the agent runs belong in ` +
545
+ `'skills/<capability>/scripts/'; lifecycle hook scripts live on the authoring plane and are ` +
546
+ `run by the harness, never by the agent.`,
547
+ warn: true,
548
+ };
549
+ };
550
+ /**
551
+ * Safety rule: the authoring plane is not addressable. Every
552
+ * {@link AUTHORING_PREFIXES} prefix (`workflows/**`) has no route
553
+ * in the agent's workspace composite, so an agent read finds nothing; a
554
+ * **script** or **hook** naming one is told why, and which prefix. Applies in
555
+ * every state whatever it allows — the plane's isolation is structural, not
556
+ * granted.
557
+ */
558
+ const governancePlaneRule = (action) => {
559
+ if (action.kind !== "tool-call")
560
+ return null;
561
+ if (action.origin !== "script" && action.origin !== "lifecycle")
562
+ return null;
563
+ const target = pathArg(action.args);
564
+ if (target == null)
565
+ return null;
566
+ const { path: rel, escapes } = canonicalizeRelPath(target);
567
+ if (escapes)
568
+ return null;
569
+ const prefix = authoringPlanePrefix(rel);
570
+ if (!prefix)
571
+ return null;
572
+ return {
573
+ decision: "block",
574
+ ruleId: "zone.governance-plane",
575
+ reason: `[workflow] BLOCKED: '${action.tool}' on '${target}' addresses the authoring plane ` +
576
+ `('${prefix}/'), which holds ${describeAuthoringPrefix(prefix)}. The ` +
577
+ `harness reads it through the authoring backend; nothing running inside a run can.`,
578
+ warn: true,
579
+ };
580
+ };
581
+ /**
582
+ * Non-overridable safety rules, evaluated before any policy/consumer rule so
583
+ * none of those can loosen them.
584
+ */
585
+ const SAFETY_RULES = [
586
+ governancePlaneRule,
587
+ scriptSkillOnlyRule,
588
+ replyOnlyRule,
589
+ evalBlockRule,
590
+ taskBlockRule,
591
+ readOnlyZoneRule,
592
+ runtimeInternalRule,
593
+ runtimeManagedRule,
594
+ // After the zone rules: a *write* into a bundle is refused as a read-only
595
+ // authored path whether or not the skill is enabled — the more useful reason.
596
+ skillRule,
597
+ // And after the skill rule, for the same reason: a path inside a bundle is
598
+ // named as a skill before it is named as the mount that serves the bundle.
599
+ mountRule,
600
+ ];
601
+ /**
602
+ * Compile a `forbid_always` (root) or `forbid` (state) list into governance
603
+ * rules. One implementation for both positions: the entries share the `allow`
604
+ * grammar, so `normalizeAllowEntry` and the argument-guard matcher are the same
605
+ * ones the grant side uses, and the two differ only in what they match and how
606
+ * the refusal reads.
607
+ *
608
+ * A denial is evaluated ahead of every grant and every consumer rule, so **deny
609
+ * beats allow and no narrower level widens a denial**: nothing reaches past a
610
+ * workflow's `forbid_always`, and nothing in a state reaches past its `forbid`.
611
+ * An entry naming the tool `*` denies every tool; an entry with argument guards
612
+ * denies only the calls those guards match.
613
+ *
614
+ * Returns an empty list for an empty declaration, so the pipeline is unchanged
615
+ * for a workflow that denies nothing. Pure — the same rules run at runtime and
616
+ * in the validator.
617
+ */
618
+ export function compileForbidRules(entries, options) {
619
+ if (!entries || entries.length === 0)
620
+ return [];
621
+ const { scope, state, source } = options;
622
+ const from = source ? ` inherited from workflow '${source}'` : "";
623
+ const rules = [];
624
+ for (const entry of entries) {
625
+ const { tool, argMatchers } = normalizeAllowEntry(entry);
626
+ if (!tool)
627
+ continue; // reported by the schema
628
+ const anyTool = tool === FORBID_ANY_TOOL;
629
+ const key = scope === "workflow" ? "tools.forbid_always" : `states.${state}.tools.forbid`;
630
+ const where = scope === "workflow" ? `the workflow${from}` : `state '${state}'`;
631
+ rules.push((action, { variables }) => {
632
+ if (action.kind !== "tool-call")
633
+ return null;
634
+ if (!anyTool && action.tool !== tool)
635
+ return null;
636
+ if (scope === "state" && action.state !== state)
637
+ return null;
638
+ const guards = argMatchers ? describeGuards(argMatchers) : null;
639
+ if (argMatchers && !matchesEveryGuard(action.args, argMatchers, variables ?? {}))
640
+ return null;
641
+ const target = typeof action.args.file_path === "string" ? ` on '${action.args.file_path}'` : "";
642
+ return {
643
+ decision: "block",
644
+ ruleId: scope === "workflow" ? "tool.forbidden" : "tool.forbidden-here",
645
+ reason: `[workflow] BLOCKED: '${action.tool}'${target} is forbidden by ${where} ` +
646
+ `(${key}${guards ? `: ${guards}` : ""}). A denial beats every grant, so no allow entry ` +
647
+ `here permits it.`,
648
+ warn: true,
649
+ };
650
+ });
651
+ }
652
+ return rules;
653
+ }
654
+ /**
655
+ * Whether every one of an entry's argument guards matches the call's arguments.
656
+ *
657
+ * Resolution is the allow side's own (`argsSatisfy`), so a guard binds the same
658
+ * calls whichever list it is written in: `${{…}}` resolves against the run's
659
+ * variables and a dotted name traverses nested arguments. A denial that
660
+ * understood fewer guard forms than the grant beside it would silently match
661
+ * nothing, which is the shape of issue #156.
662
+ *
663
+ * The one asymmetry is deliberate. An allow entry whose reference cannot be
664
+ * resolved grants nothing; a forbid entry in the same position must **match**,
665
+ * so an unresolvable guard closes the call rather than opening it.
666
+ */
667
+ function matchesEveryGuard(args, argMatchers, variables) {
668
+ let unresolved = false;
669
+ const satisfied = argsSatisfy(argMatchers, args, variables, () => {
670
+ unresolved = true;
671
+ });
672
+ return satisfied || unresolved;
673
+ }
674
+ /** An entry's guards, for the refusal reason: `file_path=logs/**`. */
675
+ function describeGuards(argMatchers) {
676
+ return Object.entries(argMatchers)
677
+ .map(([name, globs]) => `${name}=${globs.join("|")}`)
678
+ .join(", ");
679
+ }
680
+ /**
681
+ * Assemble the ordered tool-governance pipeline: safety rules → the workflow's
682
+ * `tools.forbid_always` → the active state's `tools.forbid` → consumer rules →
683
+ * always-permitted run access → per-state defaults. Every declared denial
684
+ * therefore precedes every grant and every consumer rule. The same pipeline runs
685
+ * at runtime and in `validate`.
686
+ */
687
+ function toolCallPipeline(machine, customRules, state) {
688
+ return [
689
+ ...SAFETY_RULES,
690
+ ...compileForbidRules(machine.spec.tools?.forbid_always, { scope: "workflow" }),
691
+ ...compileForbidRules(machine.spec.states[state]?.tools?.forbid, { scope: "state", state }),
692
+ ...customRules,
693
+ runOpenAccessRule,
694
+ stateToolsRule,
695
+ ];
696
+ }
697
+ /** Fold the tool-call rule pipeline: first non-null verdict wins, else allow. */
698
+ function decideToolCall(machine, action, customRules, mountPrefixes, variables, skills) {
699
+ const api = { machine, mountPrefixes, variables, skills };
700
+ for (const rule of toolCallPipeline(machine, customRules, action.state)) {
701
+ const verdict = rule(action, api);
702
+ if (verdict)
703
+ return verdict;
704
+ }
705
+ return ALLOW;
706
+ }
707
+ /**
708
+ * The single governance decision function. Given the machine, a proposed action,
709
+ * and any consumer-supplied governance rules, return a typed verdict. Pure and
710
+ * synchronous, so runtime enforcement and static validation share one path.
711
+ */
712
+ export function decide(machine, action, customRules = [], mountPrefixes = NO_MOUNTS, variables = {}, skills = NO_SKILLS) {
713
+ switch (action.kind) {
714
+ case "tool-call":
715
+ return decideToolCall(machine, action, customRules, mountPrefixes, variables, skills);
716
+ case "transition": {
717
+ // The `requires` gate runs before the edge's hooks: the agent owes a value
718
+ // it can still supply, so a judge on incomplete work would be wasted.
719
+ const unmet = unmetRequirements(machine.requiredVariables(action.from), variables ?? {});
720
+ if (unmet.length > 0) {
721
+ return {
722
+ decision: "block",
723
+ ruleId: "transition.requires",
724
+ reason: `cannot leave '${action.from}' yet: ${unmet.map((n) => `'${n}'`).join(", ")} ` +
725
+ `${unmet.length === 1 ? "is" : "are"} not set. Record ` +
726
+ `${unmet.length === 1 ? "it" : "them"} with ${SET_VARIABLES_TOOL}, then advance.`,
727
+ };
728
+ }
729
+ const edge = machine.getTransition(action.from, action.to);
730
+ if (!edge) {
731
+ const valid = machine.transitionTargets(action.from);
732
+ return {
733
+ decision: "block",
734
+ ruleId: "transition.no-edge",
735
+ reason: `cannot advance from '${action.from}' to '${action.to}'. ` +
736
+ (valid.length
737
+ ? `Valid target slugs: ${valid.join(", ")}.`
738
+ : "No transitions from this state."),
739
+ };
740
+ }
741
+ return reduceHookFacts(action.hookFacts, {
742
+ phase: "after",
743
+ veto: true,
744
+ to: action.to,
745
+ });
746
+ }
747
+ case "enter-state":
748
+ return reduceHookFacts(action.hookFacts, { phase: "before", veto: false });
749
+ }
750
+ }
751
+ //# sourceMappingURL=kernel.js.map