@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.
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/dist/agent.d.ts +174 -0
- package/dist/agent.js +140 -0
- package/dist/agent.js.map +1 -0
- package/dist/assembly/compose.d.ts +170 -0
- package/dist/assembly/compose.js +391 -0
- package/dist/assembly/compose.js.map +1 -0
- package/dist/assembly/delegation-registry.d.ts +5 -0
- package/dist/assembly/delegation-registry.js +118 -0
- package/dist/assembly/delegation-registry.js.map +1 -0
- package/dist/assembly/index.d.ts +192 -0
- package/dist/assembly/index.js +265 -0
- package/dist/assembly/index.js.map +1 -0
- package/dist/assembly/plain.d.ts +7 -0
- package/dist/assembly/plain.js +41 -0
- package/dist/assembly/plain.js.map +1 -0
- package/dist/authoring-skill/archmax-harness/README.md +76 -0
- package/dist/authoring-skill/archmax-harness/SKILL.md +241 -0
- package/dist/authoring-skill/archmax-harness/references/backend-integration.md +626 -0
- package/dist/authoring-skill/archmax-harness/references/cases.md +41 -0
- package/dist/authoring-skill/archmax-harness/references/diagnostics.md +51 -0
- package/dist/authoring-skill/archmax-harness/references/hook-and-test-scripts.md +1070 -0
- package/dist/authoring-skill/archmax-harness/references/hooks-and-scripts.md +82 -0
- package/dist/authoring-skill/archmax-harness/references/wiring.md +101 -0
- package/dist/authoring-skill/archmax-harness/references/workflow-schema.md +1631 -0
- package/dist/authoring-skill/archmax-harness/references/workflow-yaml.md +241 -0
- package/dist/behaviour/support.d.ts +232 -0
- package/dist/behaviour/support.js +339 -0
- package/dist/behaviour/support.js.map +1 -0
- package/dist/cli/banner.d.ts +11 -0
- package/dist/cli/banner.js +29 -0
- package/dist/cli/banner.js.map +1 -0
- package/dist/cli/bootstrap.d.ts +33 -0
- package/dist/cli/bootstrap.js +153 -0
- package/dist/cli/bootstrap.js.map +1 -0
- package/dist/cli/command.d.ts +39 -0
- package/dist/cli/command.js +97 -0
- package/dist/cli/command.js.map +1 -0
- package/dist/cli/state-flow.d.ts +46 -0
- package/dist/cli/state-flow.js +268 -0
- package/dist/cli/state-flow.js.map +1 -0
- package/dist/cli/style.d.ts +41 -0
- package/dist/cli/style.js +64 -0
- package/dist/cli/style.js.map +1 -0
- package/dist/cli/turn.d.ts +41 -0
- package/dist/cli/turn.js +161 -0
- package/dist/cli/turn.js.map +1 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +426 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/code-description.d.ts +22 -0
- package/dist/core/code-description.js +38 -0
- package/dist/core/code-description.js.map +1 -0
- package/dist/core/deepagents.d.ts +183 -0
- package/dist/core/deepagents.js +128 -0
- package/dist/core/deepagents.js.map +1 -0
- package/dist/core/events.d.ts +350 -0
- package/dist/core/events.js +205 -0
- package/dist/core/events.js.map +1 -0
- package/dist/core/frontmatter.d.ts +15 -0
- package/dist/core/frontmatter.js +32 -0
- package/dist/core/frontmatter.js.map +1 -0
- package/dist/core/listing-redact.d.ts +37 -0
- package/dist/core/listing-redact.js +88 -0
- package/dist/core/listing-redact.js.map +1 -0
- package/dist/core/match.d.ts +26 -0
- package/dist/core/match.js +75 -0
- package/dist/core/match.js.map +1 -0
- package/dist/core/message-readers.d.ts +84 -0
- package/dist/core/message-readers.js +159 -0
- package/dist/core/message-readers.js.map +1 -0
- package/dist/core/messages.d.ts +49 -0
- package/dist/core/messages.js +85 -0
- package/dist/core/messages.js.map +1 -0
- package/dist/core/mount-prompt.d.ts +28 -0
- package/dist/core/mount-prompt.js +55 -0
- package/dist/core/mount-prompt.js.map +1 -0
- package/dist/core/mount-redact.d.ts +28 -0
- package/dist/core/mount-redact.js +32 -0
- package/dist/core/mount-redact.js.map +1 -0
- package/dist/core/mounts.d.ts +88 -0
- package/dist/core/mounts.js +147 -0
- package/dist/core/mounts.js.map +1 -0
- package/dist/core/path-mapping.d.ts +52 -0
- package/dist/core/path-mapping.js +113 -0
- package/dist/core/path-mapping.js.map +1 -0
- package/dist/core/platform-prompt.generated.d.ts +2 -0
- package/dist/core/platform-prompt.generated.js +4 -0
- package/dist/core/platform-prompt.generated.js.map +1 -0
- package/dist/core/prompt.d.ts +42 -0
- package/dist/core/prompt.js +75 -0
- package/dist/core/prompt.js.map +1 -0
- package/dist/core/session-id.d.ts +38 -0
- package/dist/core/session-id.js +61 -0
- package/dist/core/session-id.js.map +1 -0
- package/dist/core/session-store.d.ts +85 -0
- package/dist/core/session-store.js +149 -0
- package/dist/core/session-store.js.map +1 -0
- package/dist/core/session-zone.d.ts +54 -0
- package/dist/core/session-zone.js +82 -0
- package/dist/core/session-zone.js.map +1 -0
- package/dist/core/skill-prompt.d.ts +31 -0
- package/dist/core/skill-prompt.js +54 -0
- package/dist/core/skill-prompt.js.map +1 -0
- package/dist/core/skill-redact.d.ts +15 -0
- package/dist/core/skill-redact.js +27 -0
- package/dist/core/skill-redact.js.map +1 -0
- package/dist/core/skills.d.ts +172 -0
- package/dist/core/skills.js +214 -0
- package/dist/core/skills.js.map +1 -0
- package/dist/core/tool-mocks.d.ts +23 -0
- package/dist/core/tool-mocks.js +39 -0
- package/dist/core/tool-mocks.js.map +1 -0
- package/dist/core/tool-telemetry.d.ts +16 -0
- package/dist/core/tool-telemetry.js +40 -0
- package/dist/core/tool-telemetry.js.map +1 -0
- package/dist/core/usage.d.ts +114 -0
- package/dist/core/usage.js +226 -0
- package/dist/core/usage.js.map +1 -0
- package/dist/core/workspace-context.d.ts +136 -0
- package/dist/core/workspace-context.js +178 -0
- package/dist/core/workspace-context.js.map +1 -0
- package/dist/core/workspace-prompt.d.ts +7 -0
- package/dist/core/workspace-prompt.js +102 -0
- package/dist/core/workspace-prompt.js.map +1 -0
- package/dist/core/workspace-router.d.ts +61 -0
- package/dist/core/workspace-router.js +145 -0
- package/dist/core/workspace-router.js.map +1 -0
- package/dist/core/workspace.d.ts +85 -0
- package/dist/core/workspace.js +193 -0
- package/dist/core/workspace.js.map +1 -0
- package/dist/core/zones.d.ts +181 -0
- package/dist/core/zones.js +202 -0
- package/dist/core/zones.js.map +1 -0
- package/dist/env.d.ts +99 -0
- package/dist/env.js +271 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +265 -0
- package/dist/index.js +197 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/kernel.d.ts +192 -0
- package/dist/kernel/kernel.js +751 -0
- package/dist/kernel/kernel.js.map +1 -0
- package/dist/lifecycle/hook-shape.d.ts +44 -0
- package/dist/lifecycle/hook-shape.js +66 -0
- package/dist/lifecycle/hook-shape.js.map +1 -0
- package/dist/lifecycle/runner.d.ts +177 -0
- package/dist/lifecycle/runner.js +372 -0
- package/dist/lifecycle/runner.js.map +1 -0
- package/dist/lifecycle/transcript.d.ts +27 -0
- package/dist/lifecycle/transcript.js +43 -0
- package/dist/lifecycle/transcript.js.map +1 -0
- package/dist/machine/allow.d.ts +65 -0
- package/dist/machine/allow.js +107 -0
- package/dist/machine/allow.js.map +1 -0
- package/dist/machine/delegation.d.ts +9 -0
- package/dist/machine/delegation.js +10 -0
- package/dist/machine/delegation.js.map +1 -0
- package/dist/machine/diagnostic.d.ts +14 -0
- package/dist/machine/diagnostic.js +2 -0
- package/dist/machine/diagnostic.js.map +1 -0
- package/dist/machine/lint-spec.d.ts +4 -0
- package/dist/machine/lint-spec.js +266 -0
- package/dist/machine/lint-spec.js.map +1 -0
- package/dist/machine/load-spec.d.ts +90 -0
- package/dist/machine/load-spec.js +120 -0
- package/dist/machine/load-spec.js.map +1 -0
- package/dist/machine/machine.d.ts +273 -0
- package/dist/machine/machine.js +551 -0
- package/dist/machine/machine.js.map +1 -0
- package/dist/machine/mount-grants.d.ts +25 -0
- package/dist/machine/mount-grants.js +24 -0
- package/dist/machine/mount-grants.js.map +1 -0
- package/dist/machine/slug.d.ts +14 -0
- package/dist/machine/slug.js +17 -0
- package/dist/machine/slug.js.map +1 -0
- package/dist/machine/spec-schema.d.ts +662 -0
- package/dist/machine/spec-schema.js +717 -0
- package/dist/machine/spec-schema.js.map +1 -0
- package/dist/machine/tool-names.d.ts +98 -0
- package/dist/machine/tool-names.js +144 -0
- package/dist/machine/tool-names.js.map +1 -0
- package/dist/machine/triggers.d.ts +114 -0
- package/dist/machine/triggers.js +165 -0
- package/dist/machine/triggers.js.map +1 -0
- package/dist/machine/types.d.ts +186 -0
- package/dist/machine/types.js +12 -0
- package/dist/machine/types.js.map +1 -0
- package/dist/machine/validate-spec.d.ts +42 -0
- package/dist/machine/validate-spec.js +29 -0
- package/dist/machine/validate-spec.js.map +1 -0
- package/dist/machine/variables.d.ts +196 -0
- package/dist/machine/variables.js +415 -0
- package/dist/machine/variables.js.map +1 -0
- package/dist/public/cli.d.ts +15 -0
- package/dist/public/cli.js +14 -0
- package/dist/public/cli.js.map +1 -0
- package/dist/public/messages.d.ts +33 -0
- package/dist/public/messages.js +33 -0
- package/dist/public/messages.js.map +1 -0
- package/dist/public/sandbox.d.ts +99 -0
- package/dist/public/sandbox.js +32 -0
- package/dist/public/sandbox.js.map +1 -0
- package/dist/public/spec.d.ts +147 -0
- package/dist/public/spec.js +149 -0
- package/dist/public/spec.js.map +1 -0
- package/dist/public/testing.d.ts +71 -0
- package/dist/public/testing.js +39 -0
- package/dist/public/testing.js.map +1 -0
- package/dist/rubrics/rubrics.d.ts +135 -0
- package/dist/rubrics/rubrics.js +145 -0
- package/dist/rubrics/rubrics.js.map +1 -0
- package/dist/runtime/contract.d.ts +73 -0
- package/dist/runtime/contract.js +96 -0
- package/dist/runtime/contract.js.map +1 -0
- package/dist/sandbox/assets/parts/core.js +22 -0
- package/dist/sandbox/assets/parts/lifecycle-hook.js +47 -0
- package/dist/sandbox/bridge.d.ts +16 -0
- package/dist/sandbox/bridge.js +95 -0
- package/dist/sandbox/bridge.js.map +1 -0
- package/dist/sandbox/executor.d.ts +46 -0
- package/dist/sandbox/executor.js +119 -0
- package/dist/sandbox/executor.js.map +1 -0
- package/dist/sandbox/imports.d.ts +33 -0
- package/dist/sandbox/imports.js +67 -0
- package/dist/sandbox/imports.js.map +1 -0
- package/dist/sandbox/prelude.d.ts +12 -0
- package/dist/sandbox/prelude.js +51 -0
- package/dist/sandbox/prelude.js.map +1 -0
- package/dist/sandbox/ptc-gateway.d.ts +104 -0
- package/dist/sandbox/ptc-gateway.js +167 -0
- package/dist/sandbox/ptc-gateway.js.map +1 -0
- package/dist/sandbox/runtime.d.ts +59 -0
- package/dist/sandbox/runtime.js +51 -0
- package/dist/sandbox/runtime.js.map +1 -0
- package/dist/sandbox/tools.d.ts +34 -0
- package/dist/sandbox/tools.js +147 -0
- package/dist/sandbox/tools.js.map +1 -0
- package/dist/sessions/operations.d.ts +79 -0
- package/dist/sessions/operations.js +121 -0
- package/dist/sessions/operations.js.map +1 -0
- package/dist/sessions/resolve.d.ts +97 -0
- package/dist/sessions/resolve.js +145 -0
- package/dist/sessions/resolve.js.map +1 -0
- package/dist/sessions/resume.d.ts +242 -0
- package/dist/sessions/resume.js +288 -0
- package/dist/sessions/resume.js.map +1 -0
- package/dist/sessions/scope.d.ts +99 -0
- package/dist/sessions/scope.js +145 -0
- package/dist/sessions/scope.js.map +1 -0
- package/dist/sessions/summary.d.ts +65 -0
- package/dist/sessions/summary.js +56 -0
- package/dist/sessions/summary.js.map +1 -0
- package/dist/testing/assertions.d.ts +30 -0
- package/dist/testing/assertions.js +231 -0
- package/dist/testing/assertions.js.map +1 -0
- package/dist/testing/case-document.d.ts +141 -0
- package/dist/testing/case-document.js +51 -0
- package/dist/testing/case-document.js.map +1 -0
- package/dist/testing/case-schema.d.ts +29 -0
- package/dist/testing/case-schema.js +519 -0
- package/dist/testing/case-schema.js.map +1 -0
- package/dist/testing/discovery.d.ts +10 -0
- package/dist/testing/discovery.js +24 -0
- package/dist/testing/discovery.js.map +1 -0
- package/dist/testing/driver.d.ts +78 -0
- package/dist/testing/driver.js +117 -0
- package/dist/testing/driver.js.map +1 -0
- package/dist/testing/grade.d.ts +116 -0
- package/dist/testing/grade.js +261 -0
- package/dist/testing/grade.js.map +1 -0
- package/dist/testing/interpreter.d.ts +55 -0
- package/dist/testing/interpreter.js +103 -0
- package/dist/testing/interpreter.js.map +1 -0
- package/dist/testing/mock-middleware.d.ts +5 -0
- package/dist/testing/mock-middleware.js +33 -0
- package/dist/testing/mock-middleware.js.map +1 -0
- package/dist/testing/runner.d.ts +205 -0
- package/dist/testing/runner.js +266 -0
- package/dist/testing/runner.js.map +1 -0
- package/dist/testing/target.d.ts +27 -0
- package/dist/testing/target.js +21 -0
- package/dist/testing/target.js.map +1 -0
- package/dist/validate/validate.d.ts +41 -0
- package/dist/validate/validate.js +669 -0
- package/dist/validate/validate.js.map +1 -0
- package/dist/workflow/agent-tools.d.ts +10 -0
- package/dist/workflow/agent-tools.js +10 -0
- package/dist/workflow/agent-tools.js.map +1 -0
- package/dist/workflow/checkpointer.d.ts +52 -0
- package/dist/workflow/checkpointer.js +200 -0
- package/dist/workflow/checkpointer.js.map +1 -0
- package/dist/workflow/control-tools.d.ts +174 -0
- package/dist/workflow/control-tools.js +468 -0
- package/dist/workflow/control-tools.js.map +1 -0
- package/dist/workflow/governance.d.ts +104 -0
- package/dist/workflow/governance.js +546 -0
- package/dist/workflow/governance.js.map +1 -0
- package/dist/workflow/middleware.d.ts +85 -0
- package/dist/workflow/middleware.js +283 -0
- package/dist/workflow/middleware.js.map +1 -0
- package/dist/workflow/on-error.d.ts +25 -0
- package/dist/workflow/on-error.js +58 -0
- package/dist/workflow/on-error.js.map +1 -0
- package/dist/workflow/parks.d.ts +46 -0
- package/dist/workflow/parks.js +513 -0
- package/dist/workflow/parks.js.map +1 -0
- package/dist/workflow/paths.d.ts +67 -0
- package/dist/workflow/paths.js +108 -0
- package/dist/workflow/paths.js.map +1 -0
- package/dist/workflow/prompt-cache.d.ts +70 -0
- package/dist/workflow/prompt-cache.js +103 -0
- package/dist/workflow/prompt-cache.js.map +1 -0
- package/dist/workflow/prompt-pruning.d.ts +20 -0
- package/dist/workflow/prompt-pruning.js +83 -0
- package/dist/workflow/prompt-pruning.js.map +1 -0
- package/dist/workflow/render-prompt.d.ts +45 -0
- package/dist/workflow/render-prompt.js +140 -0
- package/dist/workflow/render-prompt.js.map +1 -0
- package/dist/workflow/runtime-config.d.ts +7 -0
- package/dist/workflow/runtime-config.js +9 -0
- package/dist/workflow/runtime-config.js.map +1 -0
- package/dist/workflow/session-artifacts.d.ts +90 -0
- package/dist/workflow/session-artifacts.js +134 -0
- package/dist/workflow/session-artifacts.js.map +1 -0
- package/dist/workflow/session-view.d.ts +31 -0
- package/dist/workflow/session-view.js +94 -0
- package/dist/workflow/session-view.js.map +1 -0
- package/dist/workflow/snapshot.d.ts +14 -0
- package/dist/workflow/snapshot.js +97 -0
- package/dist/workflow/snapshot.js.map +1 -0
- package/dist/workflow/state.d.ts +347 -0
- package/dist/workflow/state.js +322 -0
- package/dist/workflow/state.js.map +1 -0
- package/dist/workflow/sub-workflow.d.ts +174 -0
- package/dist/workflow/sub-workflow.js +392 -0
- package/dist/workflow/sub-workflow.js.map +1 -0
- package/dist/workflow/tool-service.d.ts +48 -0
- package/dist/workflow/tool-service.js +291 -0
- package/dist/workflow/tool-service.js.map +1 -0
- package/dist/workflow/turn-boundary.d.ts +39 -0
- package/dist/workflow/turn-boundary.js +128 -0
- package/dist/workflow/turn-boundary.js.map +1 -0
- package/dist/workflow/turn-runner.d.ts +40 -0
- package/dist/workflow/turn-runner.js +122 -0
- package/dist/workflow/turn-runner.js.map +1 -0
- package/dist/workflow/workflow-tools.d.ts +46 -0
- package/dist/workflow/workflow-tools.js +90 -0
- package/dist/workflow/workflow-tools.js.map +1 -0
- 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
|