@humwick/runner 0.0.0-stage → 0.130.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +167 -0
- package/agent-bin/agent-relay-browser +10 -0
- package/agent-bin/humwick +10 -0
- package/hooks/fff-search-redirect/fff-search-redirect.cjs +161 -0
- package/hooks/fff-search-redirect/hooks.json +13 -0
- package/hooks/rg-r-guard/hooks.json +18 -0
- package/hooks/rg-r-guard/rg-r-guard.cjs +144 -0
- package/hooks/web-tools-redirect/hooks.json +13 -0
- package/hooks/web-tools-redirect/web-tools-redirect.cjs +84 -0
- package/node_modules/@humwick/offload/LICENSE +167 -0
- package/node_modules/@humwick/offload/README.md +57 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts +18 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/async-runner.js +158 -0
- package/node_modules/@humwick/offload/dist/async-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts +25 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.js +80 -0
- package/node_modules/@humwick/offload/dist/envelope.js.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +10 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +59 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +1 -0
- package/node_modules/@humwick/offload/dist/index.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/index.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/index.js +5 -0
- package/node_modules/@humwick/offload/dist/index.js.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +62 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js +295 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/types.d.ts +111 -0
- package/node_modules/@humwick/offload/dist/types.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/types.js +176 -0
- package/node_modules/@humwick/offload/dist/types.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts +23 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js +435 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +2 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js +27 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.js +1 -0
- package/node_modules/@humwick/offload/dist/worker.js.map +1 -0
- package/node_modules/@humwick/offload/package.json +42 -0
- package/node_modules/@humwick/offload/src/async-runner.ts +170 -0
- package/node_modules/@humwick/offload/src/envelope.ts +92 -0
- package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +49 -0
- package/node_modules/@humwick/offload/src/index.ts +4 -0
- package/node_modules/@humwick/offload/src/subprocess-runner.ts +334 -0
- package/node_modules/@humwick/offload/src/types.ts +235 -0
- package/node_modules/@humwick/offload/src/worker-pool.ts +463 -0
- package/node_modules/@humwick/offload/src/worker-runtime.ts +32 -0
- package/node_modules/@humwick/offload/src/worker.ts +5 -0
- package/package.json +47 -4
- package/plugins/browser/skills/playwright-cli/SKILL.md +53 -0
- package/plugins/claude/.claude-plugin/plugin.json +8 -0
- package/plugins/claude/claude-statusline-probe.cjs +144 -0
- package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +914 -0
- package/plugins/claude/hooks/gate-run-guard.sh +9 -0
- package/plugins/claude/hooks/gate-run-guard.ts +237 -0
- package/plugins/claude/hooks/hooks.json +158 -0
- package/plugins/claude/hooks/kill-guard.provisioned.mjs +7027 -0
- package/plugins/claude/hooks/kill-guard.sh +341 -0
- package/plugins/claude/hooks/kill-guard.ts +337 -0
- package/plugins/claude/hooks/permission-request.sh +22 -0
- package/plugins/claude/hooks/post-compact.sh +10 -0
- package/plugins/claude/hooks/post-tool-use.sh +8 -0
- package/plugins/claude/hooks/pre-compact.sh +12 -0
- package/plugins/claude/hooks/read-only-bash-guard.sh +587 -0
- package/plugins/claude/hooks/relay-status.sh +373 -0
- package/plugins/claude/hooks/session-end.sh +26 -0
- package/plugins/claude/hooks/session-start.sh +31 -0
- package/plugins/claude/hooks/shell-trust.ts +28 -0
- package/plugins/claude/hooks/stop-failure.sh +47 -0
- package/plugins/claude/hooks/stop.sh +48 -0
- package/plugins/claude/hooks/subagent-guard.cjs +26 -0
- package/plugins/claude/hooks/subagent-skill-policy.cjs +160 -0
- package/plugins/claude/hooks/subagent-start.sh +13 -0
- package/plugins/claude/hooks/subagent-stop.sh +19 -0
- package/plugins/claude/hooks/user-prompt-submit.sh +18 -0
- package/plugins/claude/hooks/wait-loop-guard.ts +120 -0
- package/plugins/claude/monitors/monitors.json +7 -0
- package/plugins/claude/monitors/relay-monitor.provisioned.mjs +786 -0
- package/plugins/claude/monitors/relay-monitor.ts +58 -0
- package/plugins/claude/skills/disconnect/SKILL.md +16 -0
- package/plugins/claude/skills/label/SKILL.md +23 -0
- package/plugins/claude/skills/message/SKILL.md +40 -0
- package/plugins/claude/skills/pair/SKILL.md +30 -0
- package/plugins/claude/skills/react/SKILL.md +18 -0
- package/plugins/claude/skills/read-message/SKILL.md +24 -0
- package/plugins/claude/skills/reply/SKILL.md +44 -0
- package/plugins/claude/skills/send-claimable/SKILL.md +39 -0
- package/plugins/claude/skills/slice-first/SKILL.md +32 -0
- package/plugins/claude/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/claude/skills/status/SKILL.md +16 -0
- package/plugins/claude/skills/tags/SKILL.md +25 -0
- package/plugins/claude/skills/task/SKILL.md +28 -0
- package/plugins/claude-output-style/.claude-plugin/plugin.json +5 -0
- package/plugins/claude-output-style/output-styles/agent-relay.md +5 -0
- package/plugins/codex/skills/disconnect/SKILL.md +15 -0
- package/plugins/codex/skills/guide/SKILL.md +15 -0
- package/plugins/codex/skills/label/SKILL.md +22 -0
- package/plugins/codex/skills/message/SKILL.md +40 -0
- package/plugins/codex/skills/pair/SKILL.md +29 -0
- package/plugins/codex/skills/react/SKILL.md +17 -0
- package/plugins/codex/skills/read-message/SKILL.md +23 -0
- package/plugins/codex/skills/reply/SKILL.md +44 -0
- package/plugins/codex/skills/send-claimable/SKILL.md +39 -0
- package/plugins/codex/skills/slice-first/SKILL.md +32 -0
- package/plugins/codex/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/codex/skills/status/SKILL.md +15 -0
- package/plugins/codex/skills/tags/SKILL.md +24 -0
- package/plugins/codex/skills/task/SKILL.md +28 -0
- package/src/adapter.ts +1061 -0
- package/src/adapters/__fixtures__/codex-app-server.ts +47 -0
- package/src/adapters/claude-api.ts +243 -0
- package/src/adapters/claude-context-command.ts +16 -0
- package/src/adapters/claude-credentials.ts +22 -0
- package/src/adapters/claude-delivery-lanes.ts +28 -0
- package/src/adapters/claude-delivery.ts +166 -0
- package/src/adapters/claude-effort.ts +94 -0
- package/src/adapters/claude-exit.ts +290 -0
- package/src/adapters/claude-headless-mirror.ts +88 -0
- package/src/adapters/claude-headless-refusal.ts +51 -0
- package/src/adapters/claude-headless.ts +617 -0
- package/src/adapters/claude-monitor-delivery-plan.ts +317 -0
- package/src/adapters/claude-permission.ts +243 -0
- package/src/adapters/claude-prompt-gates.ts +269 -0
- package/src/adapters/claude-prompt-hook.ts +145 -0
- package/src/adapters/claude-session-capture.ts +293 -0
- package/src/adapters/claude-session-probe.ts +414 -0
- package/src/adapters/claude-shutdown.ts +190 -0
- package/src/adapters/claude-status-detectors.ts +426 -0
- package/src/adapters/claude-subagents.ts +93 -0
- package/src/adapters/claude-submit-confirmation.ts +287 -0
- package/src/adapters/claude-tmux.ts +548 -0
- package/src/adapters/claude-token-usage-capture.ts +253 -0
- package/src/adapters/claude-tool-policy.ts +165 -0
- package/src/adapters/claude-transcript-tail.ts +193 -0
- package/src/adapters/claude-transcript.ts +812 -0
- package/src/adapters/claude.ts +1097 -0
- package/src/adapters/codex-agent-message-capture.ts +189 -0
- package/src/adapters/codex-app-server.ts +70 -0
- package/src/adapters/codex-client.ts +362 -0
- package/src/adapters/codex-effective-config.ts +87 -0
- package/src/adapters/codex-launch-config.ts +153 -0
- package/src/adapters/codex-rollout-context.ts +272 -0
- package/src/adapters/codex-skill-discovery.ts +18 -0
- package/src/adapters/codex-token-usage-capture.ts +163 -0
- package/src/adapters/codex-tool-output.ts +53 -0
- package/src/adapters/codex-turn-outcome.ts +27 -0
- package/src/adapters/codex-version.ts +64 -0
- package/src/adapters/codex.ts +1442 -0
- package/src/adapters/incremental-file.ts +110 -0
- package/src/adapters/opencode-api.ts +71 -0
- package/src/adapters/opencode-context.ts +277 -0
- package/src/adapters/opencode-model-registry.ts +126 -0
- package/src/adapters/opencode-provider.ts +169 -0
- package/src/adapters/opencode-server-retry.ts +50 -0
- package/src/adapters/opencode-soft-steer.ts +302 -0
- package/src/adapters/opencode-spawn.ts +145 -0
- package/src/adapters/opencode-subscribe.ts +62 -0
- package/src/adapters/opencode-token-usage-capture.ts +187 -0
- package/src/adapters/opencode-tool-events.ts +302 -0
- package/src/adapters/opencode-tools-config.ts +17 -0
- package/src/adapters/opencode.ts +2264 -0
- package/src/adapters/project-shell.ts +80 -0
- package/src/adapters/provider-launch-config.ts +21 -0
- package/src/adapters/provider-quota-harvest.ts +127 -0
- package/src/adapters/service-access.ts +153 -0
- package/src/adapters/tmux-offload.ts +207 -0
- package/src/adapters/tool-summary.ts +214 -0
- package/src/agent-redact.ts +75 -0
- package/src/agent-runner/commands/dispatch.ts +185 -0
- package/src/agent-runner/commands/lifecycle.ts +84 -0
- package/src/agent-runner/commands/prompt.ts +284 -0
- package/src/agent-runner/credentials.ts +208 -0
- package/src/agent-runner/http-liveness.ts +85 -0
- package/src/agent-runner/lifecycle-milestones.ts +207 -0
- package/src/agent-runner/lifecycle-phase.ts +80 -0
- package/src/agent-runner/registration.ts +163 -0
- package/src/agent-runner/runner-context.ts +158 -0
- package/src/agent-runner/session-mirror.ts +278 -0
- package/src/agent-runner/task-claims.ts +116 -0
- package/src/agent-runner/token-usage-coverage.ts +54 -0
- package/src/agent-runner/turn-report.ts +269 -0
- package/src/api-budget-accounting.ts +91 -0
- package/src/area-memory.ts +255 -0
- package/src/artifact-path-materialize.ts +52 -0
- package/src/async-guard.ts +35 -0
- package/src/attachment-cache-sweep.ts +31 -0
- package/src/attachment-cache.ts +195 -0
- package/src/background-script-reconcile.ts +106 -0
- package/src/bounded-log.ts +59 -0
- package/src/bounded-spawn-sync.ts +18 -0
- package/src/busy-reconciler.ts +282 -0
- package/src/claim-tracker.ts +140 -0
- package/src/claude-settings.ts +167 -0
- package/src/claude-workspace-trust.ts +289 -0
- package/src/config.ts +547 -0
- package/src/context-boundary.ts +101 -0
- package/src/context-command.ts +151 -0
- package/src/context-probe-state.ts +6 -0
- package/src/continuation-archive.ts +179 -0
- package/src/control-server.ts +730 -0
- package/src/delivery-policy.ts +9 -0
- package/src/delivery-result.ts +24 -0
- package/src/effort-command.ts +125 -0
- package/src/empty-turn.ts +816 -0
- package/src/free-port.ts +25 -0
- package/src/fs-offload.ts +26 -0
- package/src/gate-run-config.ts +35 -0
- package/src/gate-run-lease-probe.ts +38 -0
- package/src/host-asset-plan.ts +450 -0
- package/src/host-claude-config-worker.ts +34 -0
- package/src/host-sources.ts +40 -0
- package/src/inbox-carrier.ts +7 -0
- package/src/index.ts +386 -0
- package/src/launch-assembly.ts +955 -0
- package/src/liveness.ts +105 -0
- package/src/logger.ts +108 -0
- package/src/mcp-outbox.ts +204 -0
- package/src/mcp-preflight.ts +86 -0
- package/src/mcp-source-selection.ts +54 -0
- package/src/mcp-tool-deferral.ts +35 -0
- package/src/mcp-vault-env.ts +25 -0
- package/src/message-body-config/config.ts +16 -0
- package/src/message-body-config.ts +1 -0
- package/src/mid-turn-delivery.ts +43 -0
- package/src/native-memory-lever.ts +329 -0
- package/src/native-self-resume.ts +244 -0
- package/src/outbox.ts +453 -0
- package/src/park-post.ts +17 -0
- package/src/pending-permission-store.ts +148 -0
- package/src/process-meta.ts +9 -0
- package/src/profile-home.ts +419 -0
- package/src/profile-projection.ts +411 -0
- package/src/project-hooks.ts +65 -0
- package/src/project-instructions.ts +119 -0
- package/src/provider-double.ts +59 -0
- package/src/provider-hold.ts +228 -0
- package/src/provider-home-marker.ts +3 -0
- package/src/provider-home-tombstone.ts +4 -0
- package/src/provider-input-queue.ts +134 -0
- package/src/provider-integration.ts +93 -0
- package/src/provider-processes.ts +17 -0
- package/src/provider-submit-failure.ts +15 -0
- package/src/providers.ts +35 -0
- package/src/provisioning.ts +703 -0
- package/src/question-decision.ts +18 -0
- package/src/quota.ts +22 -0
- package/src/rate-limit.ts +351 -0
- package/src/refused-prompt-store.ts +36 -0
- package/src/relay-injection-events.ts +161 -0
- package/src/relay-instructions.ts +107 -0
- package/src/relay-mcp-proxy.long-turn-e2e.ts +183 -0
- package/src/relay-mcp-proxy.ts +704 -0
- package/src/relay-mcp.ts +583 -0
- package/src/reply-obligation-cache.ts +211 -0
- package/src/response-capture-report.ts +111 -0
- package/src/response-routing.ts +6 -0
- package/src/runner-core.ts +2955 -0
- package/src/runner-exit.ts +12 -0
- package/src/runner-helpers.ts +582 -0
- package/src/runner-insights.ts +105 -0
- package/src/runner-mcp-proxy.ts +46 -0
- package/src/runner-spawn-config.ts +68 -0
- package/src/runner-stale-scratch-sweep.ts +21 -0
- package/src/runner.ts +9 -0
- package/src/runtime-token-file.ts +45 -0
- package/src/runtime-token-remint.ts +56 -0
- package/src/service-access.ts +88 -0
- package/src/session-destroy.ts +27 -0
- package/src/session-insights.ts +118 -0
- package/src/session-scratch.ts +296 -0
- package/src/spawn-input.ts +147 -0
- package/src/startup-config-fetch.ts +48 -0
- package/src/subreaper.ts +113 -0
- package/src/template-resolver.ts +29 -0
- package/src/test-run-dedup.ts +435 -0
- package/src/transcript-context.ts +99 -0
- package/src/transport-errors.ts +48 -0
- package/src/turn-opener.ts +55 -0
- package/src/turn-reconcile-event.ts +88 -0
- package/src/vault-proxy-hop.ts +99 -0
- package/src/version.ts +44 -0
- package/src/web-tool-call-proxy.ts +128 -0
- package/src/web-tools.ts +59 -0
- package/README.md +0 -3
|
@@ -0,0 +1,914 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// runner/plugins/claude/hooks/gate-run-guard.ts
|
|
3
|
+
import { resolve as resolve4 } from "path";
|
|
4
|
+
|
|
5
|
+
// sdk/src/gate-run-classify.ts
|
|
6
|
+
var PACKAGE_MANAGERS = new Set(["bun", "bunx", "npm", "pnpm", "yarn"]);
|
|
7
|
+
var SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
|
|
8
|
+
var GATE_ENTRYPOINT_SCRIPTS = [
|
|
9
|
+
"scripts/test-shard.ts",
|
|
10
|
+
"scripts/test-affected.ts",
|
|
11
|
+
"scripts/delivery-gate.ts"
|
|
12
|
+
];
|
|
13
|
+
var GATE_ENTRYPOINT_SCRIPT_SET = new Set(GATE_ENTRYPOINT_SCRIPTS);
|
|
14
|
+
function isTestScriptName(name, binary) {
|
|
15
|
+
if (name === "test" || name.startsWith("test:"))
|
|
16
|
+
return true;
|
|
17
|
+
if (binary !== "bun")
|
|
18
|
+
return false;
|
|
19
|
+
return GATE_ENTRYPOINT_SCRIPT_SET.has(name.startsWith("./") ? name.slice(2) : name);
|
|
20
|
+
}
|
|
21
|
+
function isTestRunCommand(args) {
|
|
22
|
+
const tokens = stripToCommandPosition(firstSegment(args));
|
|
23
|
+
if (tokens.length === 0)
|
|
24
|
+
return false;
|
|
25
|
+
const binary = (tokens[0] ?? "").split("/").pop()?.toLowerCase() ?? "";
|
|
26
|
+
if (!PACKAGE_MANAGERS.has(binary))
|
|
27
|
+
return false;
|
|
28
|
+
let index = 1;
|
|
29
|
+
index = skipBunFlags(tokens, index);
|
|
30
|
+
const verb = tokens[index];
|
|
31
|
+
if (!verb)
|
|
32
|
+
return false;
|
|
33
|
+
if (verb === "test")
|
|
34
|
+
return true;
|
|
35
|
+
if (verb !== "run")
|
|
36
|
+
return false;
|
|
37
|
+
index += 1;
|
|
38
|
+
index = skipBunFlags(tokens, index);
|
|
39
|
+
const script = tokens[index];
|
|
40
|
+
return script !== undefined && isTestScriptName(script, binary);
|
|
41
|
+
}
|
|
42
|
+
function skipBunFlags(tokens, index) {
|
|
43
|
+
while (index < tokens.length && tokens[index].startsWith("-")) {
|
|
44
|
+
const flag = tokens[index].split("=", 2)[0];
|
|
45
|
+
const takesOperand = flag === "--cwd" || flag === "--config";
|
|
46
|
+
const inline = tokens[index].includes("=");
|
|
47
|
+
index += 1;
|
|
48
|
+
if (takesOperand && !inline && index < tokens.length)
|
|
49
|
+
index += 1;
|
|
50
|
+
}
|
|
51
|
+
return index;
|
|
52
|
+
}
|
|
53
|
+
function lexCommandLine(value) {
|
|
54
|
+
const segments = [];
|
|
55
|
+
let words = [];
|
|
56
|
+
let word = "";
|
|
57
|
+
let hasWord = false;
|
|
58
|
+
let dropNextWord = false;
|
|
59
|
+
const endWord = () => {
|
|
60
|
+
if (!hasWord)
|
|
61
|
+
return;
|
|
62
|
+
if (dropNextWord)
|
|
63
|
+
dropNextWord = false;
|
|
64
|
+
else
|
|
65
|
+
words.push(word);
|
|
66
|
+
word = "";
|
|
67
|
+
hasWord = false;
|
|
68
|
+
};
|
|
69
|
+
const endSegment = () => {
|
|
70
|
+
endWord();
|
|
71
|
+
if (words.length > 0)
|
|
72
|
+
segments.push(words);
|
|
73
|
+
words = [];
|
|
74
|
+
dropNextWord = false;
|
|
75
|
+
};
|
|
76
|
+
let index = 0;
|
|
77
|
+
while (index < value.length) {
|
|
78
|
+
const char = value[index];
|
|
79
|
+
if (char === "\\" && index + 1 < value.length) {
|
|
80
|
+
word += value[index + 1];
|
|
81
|
+
hasWord = true;
|
|
82
|
+
index += 2;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (char === "'" || char === '"') {
|
|
86
|
+
hasWord = true;
|
|
87
|
+
index += 1;
|
|
88
|
+
while (index < value.length && value[index] !== char) {
|
|
89
|
+
if (char === '"' && value[index] === "\\" && index + 1 < value.length) {
|
|
90
|
+
word += value[index + 1];
|
|
91
|
+
index += 2;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
word += value[index];
|
|
95
|
+
index += 1;
|
|
96
|
+
}
|
|
97
|
+
index += 1;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (char === " " || char === "\t" || char === `
|
|
101
|
+
` || char === "\r") {
|
|
102
|
+
endWord();
|
|
103
|
+
index += 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (char === "&" && value[index + 1] === "&" || char === "|" && value[index + 1] === "|") {
|
|
107
|
+
endSegment();
|
|
108
|
+
index += 2;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (char === ";" || char === "|") {
|
|
112
|
+
endSegment();
|
|
113
|
+
index += 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (char === "&" && value[index + 1] === ">") {
|
|
117
|
+
endWord();
|
|
118
|
+
index += 2;
|
|
119
|
+
if (value[index] === ">")
|
|
120
|
+
index += 1;
|
|
121
|
+
dropNextWord = true;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (char === "&") {
|
|
125
|
+
endSegment();
|
|
126
|
+
index += 1;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (char === ">" || char === "<") {
|
|
130
|
+
if (hasWord && /^\d+$/.test(word)) {
|
|
131
|
+
word = "";
|
|
132
|
+
hasWord = false;
|
|
133
|
+
} else
|
|
134
|
+
endWord();
|
|
135
|
+
index += 1;
|
|
136
|
+
if (value[index] === ">" || value[index] === "<")
|
|
137
|
+
index += 1;
|
|
138
|
+
if (value[index] === "&") {
|
|
139
|
+
index += 1;
|
|
140
|
+
while (index < value.length && /[\d-]/.test(value[index]))
|
|
141
|
+
index += 1;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
dropNextWord = true;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
word += char;
|
|
148
|
+
hasWord = true;
|
|
149
|
+
index += 1;
|
|
150
|
+
}
|
|
151
|
+
endSegment();
|
|
152
|
+
return segments;
|
|
153
|
+
}
|
|
154
|
+
function firstSegment(value) {
|
|
155
|
+
return lexCommandLine(value)[0] ?? [];
|
|
156
|
+
}
|
|
157
|
+
var ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
158
|
+
var TRANSPARENT_PREFIXES = new Set(["env", "exec", "nice", "nohup", "time", "command", "stdbuf"]);
|
|
159
|
+
var PREFIX_OPTIONS_WITH_OPERAND = {
|
|
160
|
+
env: new Set(["-u", "--unset", "-C", "--chdir", "-a", "--argv0", "--block-signal", "--default-signal"]),
|
|
161
|
+
nice: new Set(["-n", "--adjustment"]),
|
|
162
|
+
stdbuf: new Set(["-i", "-o", "-e"]),
|
|
163
|
+
exec: new Set(["-a"])
|
|
164
|
+
};
|
|
165
|
+
function prefixOperands(prefix) {
|
|
166
|
+
return PREFIX_OPTIONS_WITH_OPERAND[prefix];
|
|
167
|
+
}
|
|
168
|
+
function stripToCommandPosition(tokens) {
|
|
169
|
+
let index = 0;
|
|
170
|
+
while (index < tokens.length) {
|
|
171
|
+
const token = tokens[index];
|
|
172
|
+
if (ENV_ASSIGNMENT.test(token)) {
|
|
173
|
+
index += 1;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (!TRANSPARENT_PREFIXES.has(token))
|
|
177
|
+
break;
|
|
178
|
+
const operands = prefixOperands(token);
|
|
179
|
+
index += 1;
|
|
180
|
+
while (index < tokens.length && tokens[index].startsWith("-")) {
|
|
181
|
+
const option = tokens[index];
|
|
182
|
+
index += 1;
|
|
183
|
+
const [flag, inlineValue] = option.split("=", 2);
|
|
184
|
+
if (inlineValue === undefined && operands?.has(flag) && index < tokens.length)
|
|
185
|
+
index += 1;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return tokens.slice(index);
|
|
189
|
+
}
|
|
190
|
+
function sameBinary(a, b) {
|
|
191
|
+
return a === b || (a.split("/").pop() ?? a) === (b.split("/").pop() ?? b);
|
|
192
|
+
}
|
|
193
|
+
function runsCommand(segment, configured) {
|
|
194
|
+
const candidate = stripToCommandPosition(segment);
|
|
195
|
+
const wanted = stripToCommandPosition(configured);
|
|
196
|
+
if (wanted.length === 0 || wanted.length > candidate.length)
|
|
197
|
+
return false;
|
|
198
|
+
if (!sameBinary(candidate[0], wanted[0]))
|
|
199
|
+
return false;
|
|
200
|
+
for (let offset = 1;offset < wanted.length; offset += 1) {
|
|
201
|
+
if (candidate[offset] !== wanted[offset])
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
function unwrapShellCommand(command) {
|
|
207
|
+
const tokens = stripToCommandPosition(firstSegment(command));
|
|
208
|
+
if (tokens.length < 3)
|
|
209
|
+
return;
|
|
210
|
+
const binary = (tokens[0] ?? "").split("/").pop()?.toLowerCase() ?? "";
|
|
211
|
+
if (!SHELLS.has(binary))
|
|
212
|
+
return;
|
|
213
|
+
let index = 1;
|
|
214
|
+
while (index < tokens.length && tokens[index].startsWith("-") && !/^-[a-z]*c$/.test(tokens[index]))
|
|
215
|
+
index += 1;
|
|
216
|
+
if (index >= tokens.length || !/^-[a-z]*c$/.test(tokens[index]))
|
|
217
|
+
return;
|
|
218
|
+
const inner = tokens.slice(index + 1).join(" ").trim();
|
|
219
|
+
return inner ? inner : undefined;
|
|
220
|
+
}
|
|
221
|
+
function unwrapGroupCommand(command) {
|
|
222
|
+
const trimmed = command.trim().replace(/;\s*$/, "").trim();
|
|
223
|
+
if (trimmed.startsWith("((") || trimmed.endsWith("))"))
|
|
224
|
+
return;
|
|
225
|
+
const open = trimmed[0];
|
|
226
|
+
const close = open === "(" ? ")" : open === "{" ? "}" : undefined;
|
|
227
|
+
if (!close || !trimmed.endsWith(close))
|
|
228
|
+
return;
|
|
229
|
+
const inner = trimmed.slice(1, -1).trim().replace(/;\s*$/, "").trim();
|
|
230
|
+
if (open === "{" && !/^\s/.test(trimmed.slice(1)))
|
|
231
|
+
return;
|
|
232
|
+
return inner ? inner : undefined;
|
|
233
|
+
}
|
|
234
|
+
var MAX_SHELL_UNWRAP_DEPTH = 2;
|
|
235
|
+
function classifyGateRun(command, commands = {}) {
|
|
236
|
+
const configured = [];
|
|
237
|
+
if (commands.iterate)
|
|
238
|
+
configured.push({ category: "iterate", tokens: firstSegment(commands.iterate) });
|
|
239
|
+
if (commands.delivery)
|
|
240
|
+
configured.push({ category: "delivery", tokens: firstSegment(commands.delivery) });
|
|
241
|
+
for (const gate of commands.gates ?? []) {
|
|
242
|
+
if (gate)
|
|
243
|
+
configured.push({ category: "gate", tokens: firstSegment(gate) });
|
|
244
|
+
}
|
|
245
|
+
let candidate = command;
|
|
246
|
+
for (let depth = 0;depth <= MAX_SHELL_UNWRAP_DEPTH && candidate !== undefined; depth += 1) {
|
|
247
|
+
const segments = lexCommandLine(candidate);
|
|
248
|
+
for (const entry of configured) {
|
|
249
|
+
for (const segment of segments) {
|
|
250
|
+
if (runsCommand(segment, entry.tokens))
|
|
251
|
+
return entry.category;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const segment of segments) {
|
|
255
|
+
if (isTestRunCommand(stripToCommandPosition(segment).join(" ")))
|
|
256
|
+
return "heuristic";
|
|
257
|
+
}
|
|
258
|
+
candidate = unwrapShellCommand(candidate) ?? unwrapGroupCommand(candidate);
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// sdk/src/land-gates.ts
|
|
264
|
+
import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve3 } from "path";
|
|
265
|
+
|
|
266
|
+
// sdk/src/types/guards.ts
|
|
267
|
+
function isRecord(value) {
|
|
268
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// sdk/src/types/land-gates.ts
|
|
272
|
+
var LAND_GATE_SCOPES = ["full", "partial"];
|
|
273
|
+
|
|
274
|
+
// sdk/src/validation-core.ts
|
|
275
|
+
import { isAbsolute, relative, resolve } from "path";
|
|
276
|
+
|
|
277
|
+
class ValidationError extends Error {
|
|
278
|
+
}
|
|
279
|
+
function cleanString(value, field, opts = {}) {
|
|
280
|
+
if (value === undefined || value === null) {
|
|
281
|
+
if (opts.required)
|
|
282
|
+
throw new ValidationError(`${field} required`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (typeof value !== "string")
|
|
286
|
+
throw new ValidationError(`${field} must be a string`);
|
|
287
|
+
if (opts.rejectControlBytes)
|
|
288
|
+
assertNoControlBytes(value, field);
|
|
289
|
+
const trimmed = value.trim();
|
|
290
|
+
if (opts.required && !trimmed)
|
|
291
|
+
throw new ValidationError(`${field} required`);
|
|
292
|
+
if (opts.max && trimmed.length > opts.max) {
|
|
293
|
+
throw new ValidationError(`${field} must be ${opts.max} characters or fewer`);
|
|
294
|
+
}
|
|
295
|
+
return trimmed || undefined;
|
|
296
|
+
}
|
|
297
|
+
function parseJson(raw, fallback) {
|
|
298
|
+
if (typeof raw !== "string" || !raw)
|
|
299
|
+
return fallback;
|
|
300
|
+
try {
|
|
301
|
+
return JSON.parse(raw);
|
|
302
|
+
} catch {
|
|
303
|
+
return fallback;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function isPathWithinBase(path, baseDir) {
|
|
307
|
+
if (!path || !baseDir)
|
|
308
|
+
return false;
|
|
309
|
+
const rel = relative(resolve(baseDir), resolve(path));
|
|
310
|
+
return rel === "" || !!rel && !rel.startsWith("..") && !isAbsolute(rel);
|
|
311
|
+
}
|
|
312
|
+
var CONTROL_BYTES = /[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
313
|
+
function stripControlBytes(text) {
|
|
314
|
+
return text.replace(CONTROL_BYTES, "");
|
|
315
|
+
}
|
|
316
|
+
function assertNoControlBytes(text, field) {
|
|
317
|
+
if (stripControlBytes(text) !== text)
|
|
318
|
+
throw new ValidationError(`${field} must not contain control bytes (only newline and tab are allowed)`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// sdk/src/repo-config-file.ts
|
|
322
|
+
import { dirname, resolve as resolve2 } from "path";
|
|
323
|
+
import { readFileSync } from "fs";
|
|
324
|
+
function absent(err) {
|
|
325
|
+
const code = err.code;
|
|
326
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
327
|
+
return;
|
|
328
|
+
throw err;
|
|
329
|
+
}
|
|
330
|
+
function* ancestors(startDir) {
|
|
331
|
+
for (let current = resolve2(startDir);; current = dirname(current)) {
|
|
332
|
+
yield current;
|
|
333
|
+
if (dirname(current) === current)
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function parsed(file, raw, repoRoot, clean) {
|
|
338
|
+
const value = parseJson(raw, null);
|
|
339
|
+
if (value === null)
|
|
340
|
+
throw new ValidationError(`${file}: invalid JSON`);
|
|
341
|
+
return { repoRoot, config: clean(value, repoRoot) };
|
|
342
|
+
}
|
|
343
|
+
function findRepoConfigFile(startDir, file, clean) {
|
|
344
|
+
for (const dir of ancestors(startDir)) {
|
|
345
|
+
let raw;
|
|
346
|
+
try {
|
|
347
|
+
raw = readFileSync(resolve2(dir, file), "utf8");
|
|
348
|
+
} catch (err) {
|
|
349
|
+
raw = absent(err);
|
|
350
|
+
}
|
|
351
|
+
if (raw !== undefined)
|
|
352
|
+
return parsed(file, raw, dir, clean);
|
|
353
|
+
}
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// sdk/src/land-gates.ts
|
|
358
|
+
var LAND_GATES_FILE = ".agent-relay/land-gates.json";
|
|
359
|
+
var DEFAULT_LAND_GATE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
360
|
+
var LAND_GATE_TIMEOUT_MARGIN_MS = 5 * 60 * 1000;
|
|
361
|
+
function cleanLandGate(value, index, worktreePath) {
|
|
362
|
+
if (!isRecord(value))
|
|
363
|
+
throw new ValidationError(`gates[${index}] must be an object`);
|
|
364
|
+
const name = cleanString(value.name, `gates[${index}].name`, { required: true, max: 120 });
|
|
365
|
+
if (!name)
|
|
366
|
+
throw new ValidationError(`gates[${index}].name required`);
|
|
367
|
+
const command = cleanString(value.command, `gates[${index}].command`, { required: true, max: 4096 });
|
|
368
|
+
if (!command)
|
|
369
|
+
throw new ValidationError(`gates[${index}].command required`);
|
|
370
|
+
let cwd;
|
|
371
|
+
if (value.cwd !== undefined && value.cwd !== null) {
|
|
372
|
+
const rawCwd = cleanString(value.cwd, `gates[${index}].cwd`, { max: 1000 });
|
|
373
|
+
if (rawCwd) {
|
|
374
|
+
if (worktreePath) {
|
|
375
|
+
const resolved = resolve3(worktreePath, rawCwd);
|
|
376
|
+
if (!isPathWithinBase(resolved, worktreePath)) {
|
|
377
|
+
throw new ValidationError(`gates[${index}].cwd must be within the worktree`);
|
|
378
|
+
}
|
|
379
|
+
cwd = relative2(worktreePath, resolved) || ".";
|
|
380
|
+
} else {
|
|
381
|
+
if (isAbsolute2(rawCwd))
|
|
382
|
+
throw new ValidationError(`gates[${index}].cwd must be a relative path when no worktreePath is provided`);
|
|
383
|
+
cwd = rawCwd;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (value.timeoutMs !== undefined && value.timeoutMs !== null) {
|
|
388
|
+
if (typeof value.timeoutMs !== "number" || !Number.isSafeInteger(value.timeoutMs) || value.timeoutMs <= 0) {
|
|
389
|
+
throw new ValidationError(`gates[${index}].timeoutMs must be a positive integer`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (value.optional !== undefined && value.optional !== null) {
|
|
393
|
+
if (typeof value.optional !== "boolean") {
|
|
394
|
+
throw new ValidationError(`gates[${index}].optional must be a boolean`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
name,
|
|
399
|
+
command,
|
|
400
|
+
...cwd !== undefined ? { cwd } : {},
|
|
401
|
+
...typeof value.timeoutMs === "number" ? { timeoutMs: value.timeoutMs } : {},
|
|
402
|
+
...typeof value.optional === "boolean" ? { optional: value.optional } : {}
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function cleanLandGatesConfig(value, worktreePath) {
|
|
406
|
+
if (!isRecord(value))
|
|
407
|
+
throw new ValidationError("land-gates config must be an object");
|
|
408
|
+
const { gates } = value;
|
|
409
|
+
if (!Array.isArray(gates))
|
|
410
|
+
throw new ValidationError("gates must be an array");
|
|
411
|
+
if (gates.length > 50)
|
|
412
|
+
throw new ValidationError("gates: at most 50 gates allowed");
|
|
413
|
+
return gates.map((g, i) => cleanLandGate(g, i, worktreePath));
|
|
414
|
+
}
|
|
415
|
+
function cleanLandGatesConfigFull(value, worktreePath) {
|
|
416
|
+
const gates = cleanLandGatesConfig(value, worktreePath);
|
|
417
|
+
const iterate = cleanString(value.iterate, "iterate", { max: 4096 });
|
|
418
|
+
const deliveryPolicy = cleanDeliveryGatePolicy(value);
|
|
419
|
+
const placement = cleanLandPlacement(value);
|
|
420
|
+
const requirements = cleanLandGateRequirements(value);
|
|
421
|
+
const cd = value.cd === true;
|
|
422
|
+
const scope = cleanLandGateScope(value);
|
|
423
|
+
return { gates, ...deliveryPolicy, ...iterate ? { iterate } : {}, ...cd ? { cd } : {}, ...placement ? { placement } : {}, ...requirements ? { requirements } : {}, ...scope };
|
|
424
|
+
}
|
|
425
|
+
function cleanDeliveryGatePolicy(value) {
|
|
426
|
+
if (!isRecord(value))
|
|
427
|
+
throw new ValidationError("land-gates config must be an object");
|
|
428
|
+
const delivery = cleanString(value.delivery, "delivery", { max: 4096 });
|
|
429
|
+
if (value.deliveryRequire !== undefined && value.deliveryRequire !== "captured") {
|
|
430
|
+
throw new ValidationError('deliveryRequire must be "captured" when present');
|
|
431
|
+
}
|
|
432
|
+
if (value.deliveryRequire && !delivery)
|
|
433
|
+
throw new ValidationError('deliveryRequire "captured" requires delivery command');
|
|
434
|
+
return { ...delivery ? { delivery } : {}, ...value.deliveryRequire ? { deliveryRequire: "captured" } : {} };
|
|
435
|
+
}
|
|
436
|
+
function cleanLandGateScope(value) {
|
|
437
|
+
const policy = value.mainFailurePolicy;
|
|
438
|
+
if (policy !== undefined && policy !== "stop" && policy !== "advisory") {
|
|
439
|
+
throw new ValidationError("mainFailurePolicy must be one of stop, advisory");
|
|
440
|
+
}
|
|
441
|
+
const scope = value.gateScope;
|
|
442
|
+
if (scope !== undefined && !LAND_GATE_SCOPES.includes(scope)) {
|
|
443
|
+
throw new ValidationError(`gateScope must be one of ${LAND_GATE_SCOPES.join(", ")}`);
|
|
444
|
+
}
|
|
445
|
+
let mainVerdict;
|
|
446
|
+
if (value.mainVerdict !== undefined) {
|
|
447
|
+
if (!isRecord(value.mainVerdict))
|
|
448
|
+
throw new ValidationError("mainVerdict must be an object");
|
|
449
|
+
const watch = cleanString(value.mainVerdict.watch, "mainVerdict.watch", { required: true, max: 64 });
|
|
450
|
+
if (!watch)
|
|
451
|
+
throw new ValidationError("mainVerdict.watch required");
|
|
452
|
+
mainVerdict = { watch };
|
|
453
|
+
}
|
|
454
|
+
if (scope === "partial" && !mainVerdict)
|
|
455
|
+
throw new ValidationError('gateScope "partial" requires mainVerdict.watch');
|
|
456
|
+
return { ...policy ? { mainFailurePolicy: policy } : {}, ...scope ? { gateScope: scope } : {}, ...mainVerdict ? { mainVerdict } : {} };
|
|
457
|
+
}
|
|
458
|
+
function cleanLandGateRequirements(value) {
|
|
459
|
+
if (!isRecord(value))
|
|
460
|
+
throw new ValidationError("land-gates config must be an object");
|
|
461
|
+
if (value.requirements === undefined)
|
|
462
|
+
return;
|
|
463
|
+
if (!isRecord(value.requirements))
|
|
464
|
+
throw new ValidationError("requirements must be an object");
|
|
465
|
+
const requirements = {};
|
|
466
|
+
for (const key of ["platforms", "capabilities"]) {
|
|
467
|
+
const items = value.requirements[key];
|
|
468
|
+
if (items === undefined)
|
|
469
|
+
continue;
|
|
470
|
+
if (!Array.isArray(items) || items.length === 0 || items.length > 50 || items.some((item) => typeof item !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9:_-]{0,119}$/.test(item))) {
|
|
471
|
+
throw new ValidationError(`requirements.${key} must be a nonempty array of capability names`);
|
|
472
|
+
}
|
|
473
|
+
requirements[key] = [...new Set(items)];
|
|
474
|
+
}
|
|
475
|
+
return requirements;
|
|
476
|
+
}
|
|
477
|
+
function cleanLandPlacement(value) {
|
|
478
|
+
if (!isRecord(value))
|
|
479
|
+
throw new ValidationError("land-gates config must be an object");
|
|
480
|
+
if (value.placement === undefined)
|
|
481
|
+
return;
|
|
482
|
+
if (!isRecord(value.placement))
|
|
483
|
+
throw new ValidationError("placement must be an object");
|
|
484
|
+
const platforms = value.placement.platforms;
|
|
485
|
+
if (platforms === undefined)
|
|
486
|
+
return {};
|
|
487
|
+
if (!Array.isArray(platforms) || !platforms.length || platforms.length > 20) {
|
|
488
|
+
throw new ValidationError("placement.platforms must be a nonempty array of at most 20 platforms");
|
|
489
|
+
}
|
|
490
|
+
return { platforms: [...new Set(platforms.map((item) => {
|
|
491
|
+
if (item !== "linux" && item !== "darwin" && item !== "win32") {
|
|
492
|
+
throw new ValidationError(`placement.platforms: unknown platform '${String(item)}'; expected linux, darwin, win32`);
|
|
493
|
+
}
|
|
494
|
+
return item;
|
|
495
|
+
}))] };
|
|
496
|
+
}
|
|
497
|
+
function resolveIterateCommand(config) {
|
|
498
|
+
if (config.iterate)
|
|
499
|
+
return config.iterate;
|
|
500
|
+
return config.gates.find((g) => g.name === "test")?.command;
|
|
501
|
+
}
|
|
502
|
+
function findRepoLandGatesConfig(startDir) {
|
|
503
|
+
return findRepoConfigFile(startDir, LAND_GATES_FILE, cleanLandGatesConfigFull);
|
|
504
|
+
}
|
|
505
|
+
var MERGE_NON_GATE_PHASE_OVERHEAD_MS = 21 * 60000;
|
|
506
|
+
var DEFAULT_GATE_RUN_BUDGET_MS = 35 * 60000;
|
|
507
|
+
var HEAVY_GATE_QUEUE_STALL_MS = 2 * 60 * 60000;
|
|
508
|
+
var HEAVY_GATE_RUN_CAP_MS = 90 * 60000;
|
|
509
|
+
var MERGE_TOTAL_SAFETY_MARGIN_MS = 10 * 60000;
|
|
510
|
+
var HUB_MERGE_TTL_REPORT_HEADROOM_MS = 5 * 60000;
|
|
511
|
+
function gateCommandsForCwd(cwd) {
|
|
512
|
+
try {
|
|
513
|
+
const found = findRepoLandGatesConfig(cwd);
|
|
514
|
+
if (!found)
|
|
515
|
+
return {};
|
|
516
|
+
return {
|
|
517
|
+
iterate: resolveIterateCommand(found.config),
|
|
518
|
+
delivery: found.config.delivery,
|
|
519
|
+
gates: found.config.gates.map((gate) => gate.command)
|
|
520
|
+
};
|
|
521
|
+
} catch {
|
|
522
|
+
return {};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// runner/plugins/claude/hooks/shell-trust.ts
|
|
527
|
+
var SYNTAX = String.raw`<<|\\$|(^|\s)#|\$\(|` + "`" + String.raw`|\(\)|^\s*[{}]|\{\s*$`;
|
|
528
|
+
var CONTROL_KEYWORDS = ["if", "then", "elif", "else", "fi", "for", "while", "until", "case", "esac", "select", "function"];
|
|
529
|
+
var patterns = new Map;
|
|
530
|
+
function unsurePattern(keywords) {
|
|
531
|
+
const key = keywords.join("|");
|
|
532
|
+
let pattern = patterns.get(key);
|
|
533
|
+
if (!pattern) {
|
|
534
|
+
pattern = new RegExp(`${SYNTAX}|(^|[\\s;&|(])(${key})\\b`, "m");
|
|
535
|
+
patterns.set(key, pattern);
|
|
536
|
+
}
|
|
537
|
+
return pattern;
|
|
538
|
+
}
|
|
539
|
+
var countOf = (line, char) => line.split(char).length - 1;
|
|
540
|
+
function trustedFlatCommand(command, keywords = CONTROL_KEYWORDS) {
|
|
541
|
+
if (unsurePattern(keywords).test(command))
|
|
542
|
+
return null;
|
|
543
|
+
const lines = command.split(/\r?\n/);
|
|
544
|
+
if (lines.some((line) => countOf(line, "'") % 2 === 1 || countOf(line, '"') % 2 === 1))
|
|
545
|
+
return null;
|
|
546
|
+
return lines.filter((line) => line.trim()).join("; ");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// runner/plugins/claude/hooks/wait-loop-guard.ts
|
|
550
|
+
var MIN_BARE_SLEEP_SECONDS = 30;
|
|
551
|
+
var MIN_LOOP_SLEEP_SECONDS = 5;
|
|
552
|
+
var MAX_UNWRAP_DEPTH = 2;
|
|
553
|
+
var SETUP = new Set(["cd", "pushd", "export", "echo", "printf", "date", "true", ":"]);
|
|
554
|
+
var BODY_FILLER = new Set(["echo", "printf", "date", "true", ":"]);
|
|
555
|
+
var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
556
|
+
var DURATION = /^(\d+(?:\.\d+)?)([smhd]?)$/;
|
|
557
|
+
var UNIT_SECONDS = { "": 1, s: 1, m: 60, h: 3600, d: 86400 };
|
|
558
|
+
var UNSURE_OPERATORS = /\|\||</;
|
|
559
|
+
var SHELL_BINARIES = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
|
|
560
|
+
var EXECUTING_FLAGS = /^-[leuxi]+$/;
|
|
561
|
+
var EXECUTING_C_FLAGS = /^-[leuxi]*c$/;
|
|
562
|
+
var READERS = new Set(["read", "mapfile", "readarray"]);
|
|
563
|
+
var BACKGROUND_AMP = /(?<![&>])&(?![&>])/;
|
|
564
|
+
var LOOP_KEYWORDS = CONTROL_KEYWORDS.filter((word) => word !== "while" && word !== "until");
|
|
565
|
+
var STRUCTURE_WORDS = new Set([...CONTROL_KEYWORDS, "do", "done", "{", "}", "(", ")", "[["]);
|
|
566
|
+
function sleepSeconds(words) {
|
|
567
|
+
if (words[0] !== "sleep" || words.length < 2)
|
|
568
|
+
return null;
|
|
569
|
+
let total = 0;
|
|
570
|
+
for (const arg of words.slice(1)) {
|
|
571
|
+
const match = DURATION.exec(arg);
|
|
572
|
+
if (!match)
|
|
573
|
+
return null;
|
|
574
|
+
total += Number(match[1]) * UNIT_SECONDS[match[2]];
|
|
575
|
+
}
|
|
576
|
+
return total;
|
|
577
|
+
}
|
|
578
|
+
var isSetup = (words) => SETUP.has(words[0]) || words.every((word) => ASSIGNMENT.test(word));
|
|
579
|
+
var isPlainRest = (words) => !STRUCTURE_WORDS.has(words[0]);
|
|
580
|
+
function isForegroundWait(segments) {
|
|
581
|
+
let i = 0;
|
|
582
|
+
while (i < segments.length && isSetup(segments[i]))
|
|
583
|
+
i += 1;
|
|
584
|
+
const head = segments[i];
|
|
585
|
+
if (!head)
|
|
586
|
+
return false;
|
|
587
|
+
let next;
|
|
588
|
+
if (head[0] === "sleep") {
|
|
589
|
+
const seconds = sleepSeconds(head);
|
|
590
|
+
if (seconds === null || seconds < MIN_BARE_SLEEP_SECONDS)
|
|
591
|
+
return false;
|
|
592
|
+
next = i + 1;
|
|
593
|
+
} else if (head[0] === "until" || head[0] === "while") {
|
|
594
|
+
const checkStart = head.slice(1);
|
|
595
|
+
const checkCommand = checkStart.find((word) => !ASSIGNMENT.test(word));
|
|
596
|
+
if (!checkCommand || READERS.has(checkCommand))
|
|
597
|
+
return false;
|
|
598
|
+
let j = i + 1;
|
|
599
|
+
while (j < segments.length && segments[j][0] !== "do") {
|
|
600
|
+
if (STRUCTURE_WORDS.has(segments[j][0]))
|
|
601
|
+
return false;
|
|
602
|
+
j += 1;
|
|
603
|
+
}
|
|
604
|
+
if (j >= segments.length)
|
|
605
|
+
return false;
|
|
606
|
+
let sleeps = false;
|
|
607
|
+
let first = true;
|
|
608
|
+
for (;j < segments.length; j += 1) {
|
|
609
|
+
const words = first ? segments[j].slice(1) : segments[j];
|
|
610
|
+
first = false;
|
|
611
|
+
if (words.length === 1 && words[0] === "done")
|
|
612
|
+
break;
|
|
613
|
+
if (words.length === 0)
|
|
614
|
+
continue;
|
|
615
|
+
const seconds = sleepSeconds(words);
|
|
616
|
+
if (seconds !== null) {
|
|
617
|
+
if (seconds === 0)
|
|
618
|
+
return false;
|
|
619
|
+
if (seconds >= MIN_LOOP_SLEEP_SECONDS)
|
|
620
|
+
sleeps = true;
|
|
621
|
+
} else if (!BODY_FILLER.has(words[0])) {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (j >= segments.length || !sleeps)
|
|
626
|
+
return false;
|
|
627
|
+
next = j + 1;
|
|
628
|
+
} else {
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
return segments.slice(next).every(isPlainRest);
|
|
632
|
+
}
|
|
633
|
+
function executingShellScript(candidate) {
|
|
634
|
+
const segments = lexCommandLine(candidate);
|
|
635
|
+
const words = segments[0] ?? [];
|
|
636
|
+
if (!SHELL_BINARIES.has((words[0] ?? "").split("/").pop().toLowerCase()))
|
|
637
|
+
return;
|
|
638
|
+
let i = 1;
|
|
639
|
+
while (i < words.length && EXECUTING_FLAGS.test(words[i]))
|
|
640
|
+
i += 1;
|
|
641
|
+
if (words.length !== i + 2 || !EXECUTING_C_FLAGS.test(words[i]))
|
|
642
|
+
return;
|
|
643
|
+
return words[i + 1];
|
|
644
|
+
}
|
|
645
|
+
var TIMEOUT_PREFIX = /^\s*timeout\s+(?:-\S+\s+)*\S+\s+/;
|
|
646
|
+
function waitLoopDenial(command) {
|
|
647
|
+
const flat = trustedFlatCommand(command, LOOP_KEYWORDS);
|
|
648
|
+
if (flat === null || BACKGROUND_AMP.test(flat) || UNSURE_OPERATORS.test(flat))
|
|
649
|
+
return null;
|
|
650
|
+
let candidate = flat;
|
|
651
|
+
for (let depth = 0;depth <= MAX_UNWRAP_DEPTH && candidate !== undefined; depth += 1) {
|
|
652
|
+
candidate = candidate.replace(TIMEOUT_PREFIX, "");
|
|
653
|
+
if (isForegroundWait(lexCommandLine(candidate))) {
|
|
654
|
+
return "Do not hold the turn on a foreground wait. Run it with the relay_run MCP tool (detach: true, waitMs: 0) as ONE poll loop with a deadline " + "(e.g. `end=$((SECONDS+1800)); until <check>; do [ $SECONDS -ge $end ] && exit 1; sleep 30; done`), then end your turn with no text; " + `Relay wakes you once when it finishes. Command: ${command}`;
|
|
655
|
+
}
|
|
656
|
+
candidate = executingShellScript(candidate) ?? unwrapGroupCommand(candidate);
|
|
657
|
+
}
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// runner/plugins/claude/hooks/gate-run-guard.ts
|
|
662
|
+
var MAX_COMMAND_CHARS = 64 * 1024;
|
|
663
|
+
var MAX_UNWRAP_DEPTH2 = 2;
|
|
664
|
+
var SETUP_COMMANDS = new Set(["cd", "pushd", "export", "set", "source", ".", "echo", "printf", "true"]);
|
|
665
|
+
var ASSIGNMENT2 = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
666
|
+
var SHELL_BINARIES2 = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
|
|
667
|
+
var SHELL_NON_EXEC_LONG = new Set(["--help", "--version", "--dump-po-strings", "--dump-strings"]);
|
|
668
|
+
var ENV_NON_EXEC_LONG = new Set(["--help", "--version"]);
|
|
669
|
+
var shellQuote = (word) => /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`;
|
|
670
|
+
function normalizeGate(gate) {
|
|
671
|
+
if (!gate)
|
|
672
|
+
return gate;
|
|
673
|
+
const segments = lexCommandLine(gate).filter((words) => !SETUP_COMMANDS.has(words[0] ?? "") && !words.every((word) => ASSIGNMENT2.test(word)));
|
|
674
|
+
return segments[0]?.map(shellQuote).join(" ");
|
|
675
|
+
}
|
|
676
|
+
var repoRootOf = (dir) => findRepoLandGatesConfig(dir)?.repoRoot;
|
|
677
|
+
function leavesProject(command, cwd) {
|
|
678
|
+
const home = repoRootOf(cwd);
|
|
679
|
+
let candidate = command;
|
|
680
|
+
for (let depth = 0;depth <= MAX_UNWRAP_DEPTH2 && candidate !== undefined; depth += 1) {
|
|
681
|
+
let dir = cwd;
|
|
682
|
+
for (const words of lexCommandLine(candidate)) {
|
|
683
|
+
if (words[0] !== "cd" && words[0] !== "pushd")
|
|
684
|
+
continue;
|
|
685
|
+
const target = words.slice(1).find((word) => !word.startsWith("-") || word === "-");
|
|
686
|
+
if (!target || target === "-" || /^~|\$|`/.test(target))
|
|
687
|
+
return true;
|
|
688
|
+
dir = resolve4(dir, target);
|
|
689
|
+
if (repoRootOf(dir) !== home)
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
candidate = unwrapShellCommand(candidate) ?? unwrapGroupCommand(candidate);
|
|
693
|
+
}
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
var baseBinary = (word) => (word.split("/").pop() ?? word).toLowerCase();
|
|
697
|
+
function wrapperHasNonExecutingOption(candidate) {
|
|
698
|
+
const words = lexCommandLine(candidate)[0] ?? [];
|
|
699
|
+
let index = 0;
|
|
700
|
+
while (index < words.length && ASSIGNMENT2.test(words[index]))
|
|
701
|
+
index += 1;
|
|
702
|
+
const binary = baseBinary(words[index] ?? "");
|
|
703
|
+
if (SHELL_BINARIES2.has(binary)) {
|
|
704
|
+
for (let i = index + 1;i < words.length; i += 1) {
|
|
705
|
+
const flag = words[i];
|
|
706
|
+
if (!flag.startsWith("-") || flag === "--")
|
|
707
|
+
break;
|
|
708
|
+
if (flag.startsWith("--")) {
|
|
709
|
+
if (SHELL_NON_EXEC_LONG.has(flag.split("=", 2)[0]))
|
|
710
|
+
return true;
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
if (flag.includes("n"))
|
|
714
|
+
return true;
|
|
715
|
+
if (/^-[a-z]*c$/.test(flag))
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
if (binary === "env") {
|
|
721
|
+
for (let i = index + 1;i < words.length; i += 1) {
|
|
722
|
+
const flag = words[i];
|
|
723
|
+
if (!flag.startsWith("-") || flag === "--")
|
|
724
|
+
break;
|
|
725
|
+
if (flag.startsWith("--") && ENV_NON_EXEC_LONG.has(flag.split("=", 2)[0]))
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
function shellHasExtraOperands(candidate) {
|
|
732
|
+
const match = /^\s*(?:\S+\/)?(sh|bash|zsh|dash|ksh)\s+([\s\S]*)$/i.exec(candidate);
|
|
733
|
+
if (!match)
|
|
734
|
+
return false;
|
|
735
|
+
let rest = match[2];
|
|
736
|
+
for (let guard = 0;guard < 16; guard += 1) {
|
|
737
|
+
rest = rest.replace(/^\s+/, "");
|
|
738
|
+
const flag = /^\S+/.exec(rest)?.[0];
|
|
739
|
+
if (!flag || !flag.startsWith("-"))
|
|
740
|
+
return false;
|
|
741
|
+
rest = rest.slice(flag.length);
|
|
742
|
+
if (/^-[a-z]*c$/.test(flag))
|
|
743
|
+
break;
|
|
744
|
+
if (!rest.trim())
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
rest = rest.replace(/^\s+/, "");
|
|
748
|
+
const quote = rest[0];
|
|
749
|
+
if (quote !== "'" && quote !== '"')
|
|
750
|
+
return false;
|
|
751
|
+
let i = 1;
|
|
752
|
+
while (i < rest.length && rest[i] !== quote)
|
|
753
|
+
i += 1;
|
|
754
|
+
if (i >= rest.length)
|
|
755
|
+
return false;
|
|
756
|
+
const after = rest.slice(i + 1).replace(/^\s+/, "");
|
|
757
|
+
if (!after)
|
|
758
|
+
return false;
|
|
759
|
+
return !/^(?:\d*\s*[><|;&#])/.test(after);
|
|
760
|
+
}
|
|
761
|
+
function layerHasShortCircuit(candidate) {
|
|
762
|
+
const parts = [];
|
|
763
|
+
let current = "";
|
|
764
|
+
let inSingle = false;
|
|
765
|
+
let inDouble = false;
|
|
766
|
+
let pendingOp = null;
|
|
767
|
+
const push = () => {
|
|
768
|
+
parts.push({ text: current, op: pendingOp });
|
|
769
|
+
pendingOp = null;
|
|
770
|
+
current = "";
|
|
771
|
+
};
|
|
772
|
+
for (let i = 0;i < candidate.length; i += 1) {
|
|
773
|
+
const char = candidate[i];
|
|
774
|
+
if (inSingle) {
|
|
775
|
+
current += char;
|
|
776
|
+
if (char === "'")
|
|
777
|
+
inSingle = false;
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
if (inDouble) {
|
|
781
|
+
current += char;
|
|
782
|
+
if (char === "\\") {
|
|
783
|
+
i += 1;
|
|
784
|
+
if (i < candidate.length)
|
|
785
|
+
current += candidate[i];
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
if (char === '"')
|
|
789
|
+
inDouble = false;
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
if (char === "\\") {
|
|
793
|
+
current += char;
|
|
794
|
+
i += 1;
|
|
795
|
+
if (i < candidate.length)
|
|
796
|
+
current += candidate[i];
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
if (char === "'") {
|
|
800
|
+
inSingle = true;
|
|
801
|
+
current += char;
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
if (char === '"') {
|
|
805
|
+
inDouble = true;
|
|
806
|
+
current += char;
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
if (char === "&" && candidate[i + 1] === "&") {
|
|
810
|
+
push();
|
|
811
|
+
pendingOp = "&&";
|
|
812
|
+
i += 1;
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
if (char === "|" && candidate[i + 1] === "|") {
|
|
816
|
+
push();
|
|
817
|
+
pendingOp = "||";
|
|
818
|
+
i += 1;
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
current += char;
|
|
822
|
+
}
|
|
823
|
+
push();
|
|
824
|
+
const decidingBinary = (text) => {
|
|
825
|
+
const words = lexCommandLine(text).at(-1) ?? [];
|
|
826
|
+
return words.find((word) => !ASSIGNMENT2.test(word));
|
|
827
|
+
};
|
|
828
|
+
for (let j = 1;j < parts.length; j += 1) {
|
|
829
|
+
const prev = decidingBinary(parts[j - 1].text);
|
|
830
|
+
if (parts[j].op === "&&" && prev === "false")
|
|
831
|
+
return true;
|
|
832
|
+
if (parts[j].op === "||" && prev === "true")
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
function dropShellOptionOperands(command) {
|
|
838
|
+
const match = /^\s*(?:\S+\/)?(?:sh|bash|zsh|dash|ksh)\s+([\s\S]*)$/i.exec(command);
|
|
839
|
+
if (!match)
|
|
840
|
+
return command;
|
|
841
|
+
const regionStart = command.length - match[1].length;
|
|
842
|
+
let rest = match[1];
|
|
843
|
+
let kept = "";
|
|
844
|
+
for (let guard = 0;guard < 16; guard += 1) {
|
|
845
|
+
const trimmed = rest.replace(/^\s+/, "");
|
|
846
|
+
const word = /^\S+/.exec(trimmed)?.[0];
|
|
847
|
+
if (!word || word[0] === "'" || word[0] === '"')
|
|
848
|
+
return command;
|
|
849
|
+
if (/^[-+][oO]$/.test(word)) {
|
|
850
|
+
const operand = /^[^\s&|;#]+/.exec(trimmed.slice(word.length).replace(/^\s+/, ""))?.[0];
|
|
851
|
+
if (!operand || operand[0] === "'" || operand[0] === '"')
|
|
852
|
+
return command;
|
|
853
|
+
rest = trimmed.slice(word.length).replace(/^\s+/, "").slice(operand.length);
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
kept += rest.slice(0, rest.length - trimmed.length + word.length);
|
|
857
|
+
rest = trimmed.slice(word.length);
|
|
858
|
+
if (/^-[a-z]*c$/.test(word))
|
|
859
|
+
break;
|
|
860
|
+
if (!word.startsWith("-") && !word.startsWith("+"))
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
return command.slice(0, regionStart) + kept + rest;
|
|
864
|
+
}
|
|
865
|
+
function failOpenForContrivedForm(flat) {
|
|
866
|
+
let candidate = flat;
|
|
867
|
+
for (let depth = 0;depth <= MAX_UNWRAP_DEPTH2 && candidate !== undefined; depth += 1) {
|
|
868
|
+
if (wrapperHasNonExecutingOption(candidate))
|
|
869
|
+
return true;
|
|
870
|
+
if (shellHasExtraOperands(candidate))
|
|
871
|
+
return true;
|
|
872
|
+
if (layerHasShortCircuit(candidate))
|
|
873
|
+
return true;
|
|
874
|
+
candidate = unwrapShellCommand(candidate) ?? unwrapGroupCommand(candidate);
|
|
875
|
+
}
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
function gateDenial(command, flat, cwd) {
|
|
879
|
+
const configured = gateCommandsForCwd(cwd);
|
|
880
|
+
const commands = {
|
|
881
|
+
iterate: normalizeGate(configured.iterate),
|
|
882
|
+
delivery: normalizeGate(configured.delivery),
|
|
883
|
+
gates: (configured.gates ?? []).map(normalizeGate).filter((gate) => Boolean(gate))
|
|
884
|
+
};
|
|
885
|
+
if (!commands.iterate && !commands.delivery && !commands.gates?.length)
|
|
886
|
+
return null;
|
|
887
|
+
const normalized = dropShellOptionOperands(flat);
|
|
888
|
+
if (failOpenForContrivedForm(normalized))
|
|
889
|
+
return null;
|
|
890
|
+
const category = classifyGateRun(normalized, commands);
|
|
891
|
+
if (!category || category === "heuristic")
|
|
892
|
+
return null;
|
|
893
|
+
if (leavesProject(normalized, cwd))
|
|
894
|
+
return null;
|
|
895
|
+
return `This is a project gate: run it with the relay_run MCP tool (waitMs: 0), then end your turn; Relay wakes you once when it finishes. Command: ${command}`;
|
|
896
|
+
}
|
|
897
|
+
function decide(payload) {
|
|
898
|
+
const input = payload;
|
|
899
|
+
if (input?.tool_name !== "Bash" && input?.tool_name !== "Monitor")
|
|
900
|
+
return null;
|
|
901
|
+
const command = input.tool_input?.command;
|
|
902
|
+
if (typeof command !== "string" || !command || command.length > MAX_COMMAND_CHARS)
|
|
903
|
+
return null;
|
|
904
|
+
const cwd = typeof input.cwd === "string" && input.cwd ? input.cwd : process.cwd();
|
|
905
|
+
const flat = trustedFlatCommand(command);
|
|
906
|
+
const gate = flat === null ? null : gateDenial(command, flat, cwd);
|
|
907
|
+
return gate ?? (input.tool_input?.run_in_background === true ? null : waitLoopDenial(command));
|
|
908
|
+
}
|
|
909
|
+
try {
|
|
910
|
+
const reason = decide(JSON.parse(await Bun.stdin.text()));
|
|
911
|
+
if (reason) {
|
|
912
|
+
console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } }));
|
|
913
|
+
}
|
|
914
|
+
} catch {}
|