@sema-agent/server 7.131.0 → 7.132.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/MIGRATION.md +309 -0
- package/README.md +5 -1
- package/README.zh-CN.md +3 -1
- package/USAGE.md +163 -54
- package/dist/approval-card.d.ts +3 -1
- package/dist/approval-card.js +2 -3
- package/dist/audit.d.ts +7 -10
- package/dist/auth-bridge.d.ts +4 -0
- package/dist/auth-bridge.js +10 -11
- package/dist/boot/deployment-mods.d.ts +31 -0
- package/dist/boot/deployment-mods.js +26 -0
- package/dist/boot/execution-env.d.ts +1 -1
- package/dist/boot/execution-env.js +19 -6
- package/dist/boot/memory-boundary.d.ts +2 -2
- package/dist/boot/read-deny-anchor-audit.d.ts +17 -0
- package/dist/boot/read-deny-anchor-audit.js +36 -0
- package/dist/boot/resolve-spec.d.ts +1 -5
- package/dist/boot/resolve-spec.js +19 -47
- package/dist/boot/runner-deps.d.ts +19 -8
- package/dist/boot/runner-deps.js +14 -2
- package/dist/boot/session-faces.d.ts +1 -7
- package/dist/boot/stage-03-store-backend.d.ts +1 -1
- package/dist/boot/stage-03-store-backend.js +2 -2
- package/dist/boot/stage-04-stores.d.ts +1 -1
- package/dist/boot/stage-05-execution-env.d.ts +4 -4
- package/dist/boot/stage-05-execution-env.js +7 -10
- package/dist/boot/stage-06-runners.d.ts +2 -2
- package/dist/boot/stage-06-runners.js +5 -4
- package/dist/boot/stage-07-capability-layer.d.ts +2 -2
- package/dist/boot/stage-07-capability-layer.js +3 -3
- package/dist/boot/stage-08-reapers.d.ts +2 -2
- package/dist/boot/stage-09-leader.d.ts +3 -9
- package/dist/boot/stage-10-http-server.d.ts +3 -9
- package/dist/boot/stage-10-http-server.js +6 -7
- package/dist/boot/stores.d.ts +1 -1
- package/dist/boot/stores.js +5 -6
- package/dist/boot/web-admin.d.ts +0 -3
- package/dist/boot/web-admin.js +35 -35
- package/dist/brain.js +17 -22
- package/dist/budget.js +5 -4
- package/dist/caller-text-echo.d.ts +20 -0
- package/dist/caller-text-echo.js +24 -0
- package/dist/capabilities/brand-identity.d.ts +26 -0
- package/dist/capabilities/brand-identity.js +27 -0
- package/dist/capabilities/center-plugins.js +1 -1
- package/dist/capabilities/center-prompts.d.ts +32 -59
- package/dist/capabilities/center-prompts.js +18 -32
- package/dist/capabilities/engine-capabilities.js +2 -1
- package/dist/capabilities/hands-lane.d.ts +9 -6
- package/dist/capabilities/hands-lane.js +1 -1
- package/dist/capabilities/mcp-probe.d.ts +14 -1
- package/dist/capabilities/mcp-probe.js +22 -133
- package/dist/capabilities/scenarios.d.ts +34 -40
- package/dist/capabilities/scenarios.js +16 -33
- package/dist/capabilities/skills.d.ts +19 -4
- package/dist/capabilities/skills.js +30 -10
- package/dist/config-catalog.js +6 -6
- package/dist/config-lkg.d.ts +9 -9
- package/dist/config-lkg.js +20 -4
- package/dist/config-provider.d.ts +3 -1
- package/dist/config-provider.js +12 -4
- package/dist/config-types.d.ts +7 -9
- package/dist/config.js +17 -4
- package/dist/device-ws-protocol.d.ts +1 -1
- package/dist/device-ws-protocol.js +1 -0
- package/dist/env-name-allowlist-knobs.d.ts +12 -2
- package/dist/env-name-allowlist-knobs.js +5 -4
- package/dist/exec-bounds.d.ts +15 -3
- package/dist/exec-bounds.js +4 -0
- package/dist/execution-lane-caps.d.ts +9 -2
- package/dist/execution-lane-caps.js +11 -0
- package/dist/hooks/classic-stations.d.ts +86 -0
- package/dist/hooks/classic-stations.js +27 -0
- package/dist/hooks/hook-llm.d.ts +8 -5
- package/dist/hooks/hook-llm.js +78 -35
- package/dist/hooks/hook-runner.d.ts +232 -80
- package/dist/hooks/hook-runner.js +218 -312
- package/dist/http/admission.d.ts +8 -2
- package/dist/http/admission.js +101 -57
- package/dist/http/approval-sweeps.js +57 -16
- package/dist/http/checkpoint-ctx.d.ts +25 -8
- package/dist/http/checkpoint-ctx.js +11 -7
- package/dist/http/claimed-run.d.ts +13 -0
- package/dist/http/claimed-run.js +15 -0
- package/dist/http/device-placement.js +5 -10
- package/dist/http/dispatch.d.ts +0 -4
- package/dist/http/dispatch.js +5 -9
- package/dist/http/durable-run-leg.d.ts +2 -1
- package/dist/http/durable-run-leg.js +5 -3
- package/dist/http/health-self-report.d.ts +7 -5
- package/dist/http/health-self-report.js +4 -18
- package/dist/http/principal-gate.d.ts +23 -6
- package/dist/http/principal-gate.js +24 -5
- package/dist/http/refusal-body.d.ts +21 -0
- package/dist/http/refusal-body.js +4 -0
- package/dist/http/request-guards.d.ts +27 -0
- package/dist/http/request-guards.js +24 -3
- package/dist/http/resume-legs.d.ts +25 -0
- package/dist/http/resume-legs.js +43 -22
- package/dist/http/routes/capabilities.js +1 -1
- package/dist/http/routes/diagnostics.js +1 -1
- package/dist/http/routes/rules.js +2 -2
- package/dist/http/routes/runs.js +7 -7
- package/dist/http/routes/session-sync.js +4 -2
- package/dist/http/routes/sessions.js +1 -1
- package/dist/http/routes/shared-memory.js +2 -13
- package/dist/http/routes/side-query.js +2 -3
- package/dist/http/routes/tasks.js +19 -9
- package/dist/http/send.d.ts +9 -7
- package/dist/http/send.js +7 -2
- package/dist/http/server.d.ts +21 -11
- package/dist/http/server.js +28 -2
- package/dist/http/submit-ctx.d.ts +25 -0
- package/dist/http/submit-ctx.js +39 -0
- package/dist/http/wire-types.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lane-home-probe.d.ts +43 -0
- package/dist/lane-home-probe.js +35 -0
- package/dist/limit-refusal.d.ts +47 -0
- package/dist/limit-refusal.js +12 -0
- package/dist/main.js +1 -1
- package/dist/mcp-assertion-header.d.ts +1 -9
- package/dist/mcp-assertion-header.js +0 -6
- package/dist/mcp-tool-faces.d.ts +5 -0
- package/dist/mcp-tool-faces.js +5 -1
- package/dist/memory-scope.d.ts +1 -1
- package/dist/model-provider.d.ts +39 -12
- package/dist/model-provider.js +19 -2
- package/dist/model-route-endpoint.d.ts +1 -1
- package/dist/model-route-endpoint.js +5 -5
- package/dist/observability/cost-taxonomy.d.ts +7 -7
- package/dist/observability/fail-open.d.ts +42 -6
- package/dist/observability/fail-open.js +42 -6
- package/dist/observability/metrics.js +3 -1
- package/dist/observability/prompt-manifest.d.ts +47 -82
- package/dist/observability/prompt-manifest.js +119 -17
- package/dist/observability/run-terminal.js +14 -6
- package/dist/observability/secret-env-scrub.d.ts +13 -1
- package/dist/observability/secret-env-scrub.js +13 -1
- package/dist/observability/tool-trace.d.ts +16 -21
- package/dist/observability/tool-trace.js +51 -42
- package/dist/per-task-image.d.ts +7 -1
- package/dist/per-task-image.js +3 -3
- package/dist/plain-string-record.d.ts +25 -0
- package/dist/plain-string-record.js +32 -0
- package/dist/plugins/blob-backend.d.ts +68 -13
- package/dist/plugins/blob-backend.js +53 -10
- package/dist/plugins/checkpoint-store-sql.d.ts +14 -4
- package/dist/plugins/checkpoint-store-sql.js +39 -26
- package/dist/plugins/k8s-bg-scripts.d.ts +11 -0
- package/dist/plugins/k8s-bg-scripts.js +22 -0
- package/dist/plugins/local-checkpoint-store.js +5 -2
- package/dist/plugins/local-session-store.js +1 -1
- package/dist/plugins/one-time-migrations.d.ts +137 -90
- package/dist/plugins/one-time-migrations.js +356 -207
- package/dist/plugins/permission-rule-store-sql.d.ts +13 -7
- package/dist/plugins/permission-rule-store-sql.js +4 -18
- package/dist/plugins/pg-session-storage.d.ts +4 -5
- package/dist/plugins/pg-session-storage.js +7 -17
- package/dist/plugins/remote-env-adb.d.ts +6 -0
- package/dist/plugins/remote-env-adb.js +9 -4
- package/dist/plugins/remote-env-device.js +3 -0
- package/dist/plugins/remote-env-e2b.d.ts +1 -1
- package/dist/plugins/remote-env-e2b.js +21 -9
- package/dist/plugins/remote-env-host.d.ts +9 -9
- package/dist/plugins/remote-env-host.js +10 -32
- package/dist/plugins/remote-env-k8s.d.ts +95 -26
- package/dist/plugins/remote-env-k8s.js +265 -90
- package/dist/plugins/remote-env-local-docker.d.ts +20 -1
- package/dist/plugins/remote-env-local-docker.js +77 -9
- package/dist/plugins/remote-env-ssh.d.ts +15 -0
- package/dist/plugins/remote-env-ssh.js +109 -3
- package/dist/plugins/remote-shell.d.ts +0 -1
- package/dist/plugins/remote-shell.js +1 -2
- package/dist/plugins/retention-store-sql.js +6 -2
- package/dist/plugins/schema-consistency.d.ts +54 -0
- package/dist/plugins/schema-consistency.js +52 -0
- package/dist/plugins/session-epoch-anchor.d.ts +13 -0
- package/dist/plugins/session-epoch-anchor.js +23 -0
- package/dist/plugins/shared-memory-store-sql.d.ts +9 -3
- package/dist/plugins/shared-memory-store-sql.js +5 -16
- package/dist/plugins/sql-driver.d.ts +141 -0
- package/dist/plugins/sql-driver.js +115 -1
- package/dist/plugins/sql-errors.d.ts +41 -0
- package/dist/plugins/sql-errors.js +36 -0
- package/dist/plugins/store-backend.js +2 -1
- package/dist/plugins/tidb-session-storage.d.ts +7 -11
- package/dist/plugins/tidb-session-storage.js +6 -16
- package/dist/plugins/tidb-session-store.js +1 -1
- package/dist/prompts-domain-validate.d.ts +17 -61
- package/dist/prompts-domain-validate.js +9 -119
- package/dist/request-key-closure.d.ts +2 -2
- package/dist/request-key-closure.js +2 -4
- package/dist/rule-entry-list.d.ts +12 -9
- package/dist/rule-entry-list.js +37 -21
- package/dist/run-local.js +19 -19
- package/dist/runs.d.ts +6 -0
- package/dist/runs.js +16 -0
- package/dist/server-secret-env.d.ts +44 -18
- package/dist/server-secret-env.js +49 -8
- package/dist/session-incarnation.d.ts +15 -5
- package/dist/session-incarnation.js +16 -9
- package/dist/session-titler.js +12 -2
- package/dist/spec-fields.d.ts +28 -16
- package/dist/spec-fields.js +46 -53
- package/dist/sql-engine-posture.d.ts +29 -4
- package/dist/sql-engine-posture.js +36 -1
- package/dist/stored-request-tool-axes-m19.d.ts +19 -4
- package/dist/stored-request-tool-axes-m19.js +30 -2
- package/dist/task-a2a.d.ts +9 -1
- package/dist/task-a2a.js +73 -47
- package/dist/task-mcp.d.ts +46 -58
- package/dist/task-mcp.js +99 -65
- package/dist/task-settings.d.ts +7 -21
- package/dist/task-settings.js +7 -36
- package/dist/text-bidi.d.ts +3 -46
- package/dist/text-bidi.js +3 -3
- package/dist/tool-approval.d.ts +1 -1
- package/dist/trace/core-keyset-guard.d.ts +24 -4
- package/dist/trace/engine-notice-wire.d.ts +11 -3
- package/dist/trace/engine-notice-wire.js +8 -1
- package/dist/trace/ledger-events.d.ts +3 -1
- package/dist/trace/ledger-sink.js +4 -1
- package/dist/trace/model-segment.js +1 -1
- package/dist/trace/project.d.ts +19 -6
- package/dist/trace/project.js +57 -22
- package/dist/trace/wire-projection-faces.d.ts +79 -1
- package/dist/trace/wire-projection-faces.js +22 -0
- package/dist/web-admin-client/candidate-gate.d.ts +6 -6
- package/dist/web-admin-client/candidate-gate.js +3 -3
- package/dist/web-admin-client/http-client.js +0 -6
- package/dist/web-admin-client/read-warnings.d.ts +13 -0
- package/dist/web-admin-client/read-warnings.js +10 -0
- package/dist/web-admin-client/skills-mcp.d.ts +3 -0
- package/dist/web-admin-client/skills-mcp.js +9 -6
- package/dist/web-admin-client/types.d.ts +2 -3
- package/dist/workspace-cache.d.ts +35 -0
- package/dist/workspace-cache.js +36 -0
- package/dist/workspace-space.d.ts +3 -2
- package/dist/workspace-space.js +2 -0
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +8 -1
- package/package.json +4 -4
- package/scripts/drain-check-local.mjs +34 -1
- package/scripts/drain-check.mysql.sql +30 -2
- package/scripts/drain-check.pg.sql +29 -2
- package/dist/capabilities/memory-notice.d.ts +0 -138
- package/dist/capabilities/memory-notice.js +0 -64
- package/dist/capabilities/prompt.d.ts +0 -17
- package/dist/capabilities/prompt.js +0 -6
- package/dist/mcp-content-origin.d.ts +0 -34
- package/dist/mcp-content-origin.js +0 -10
- package/dist/tool-axis-words.d.ts +0 -27
- package/dist/tool-axis-words.js +0 -9
- package/dist/wire-name-bound.d.ts +0 -14
- package/dist/wire-name-bound.js +0 -13
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { composeHostSubprocessEnv, hostSubprocessEnv } from "../server-secret-env.js";
|
|
2
3
|
import vm from "node:vm";
|
|
3
4
|
import { hostShell, resolveHostShell, adoptHostForegroundChild } from "../plugins/host-platform.js";
|
|
4
5
|
import { classifyHooksConfig, DEFAULT_HOOK_TIMEOUT_SECONDS, HOOK_ENTRY_TYPES, HOOK_EVENTS, HOOK_EVENT_OWNER, HOOK_EVENT_MATCHER_FIELD, hookMatcherForm, hookMatcherSelects, interpolateHookEnvVars, } from "@sema-agent/settings-schema/hooks";
|
|
5
6
|
import { formatConfigPath, outgoingPath } from "@sema-agent/settings-schema";
|
|
6
|
-
import {
|
|
7
|
+
import { ENGINE_OWNED_HOOK_EVENTS } from "@sema-agent/settings-schema/hooks";
|
|
8
|
+
import { describeToolCatalog } from "@sema-agent/core";
|
|
9
|
+
import { hangClassic } from "./classic-stations.js";
|
|
7
10
|
import { boundedNoticeText } from "../fleet/fleet-bus.js";
|
|
8
11
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
12
|
+
import { LIMIT_REASONS } from "../limit-refusal.js";
|
|
13
|
+
import { echoCallerText } from "../caller-text-echo.js";
|
|
9
14
|
import { redactSecrets, buildDisplaySafeUrl } from "../trace/redact.js";
|
|
10
15
|
import { ccPromptSystemFor, wrapCondition, parseCcVerdict, CC_EVALUATOR_MAX_OUTPUT_TOKENS } from "./cc-stop-prompt.js";
|
|
11
16
|
import { ccAgentHookSystemFor } from "./cc-agent-hook-prompt.js";
|
|
@@ -26,26 +31,35 @@ const HOOK_ENV_ALLOWLIST = [
|
|
|
26
31
|
"PATH", "HOME", "SHELL", "USER", "LOGNAME", "TERM", "TZ",
|
|
27
32
|
"LANG", "LC_ALL", "LC_CTYPE", "LC_MESSAGES", "TMPDIR",
|
|
28
33
|
];
|
|
29
|
-
function hookEnv(ctx) {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
function hookEnv(ctx, spawning) {
|
|
35
|
+
const base = spawning
|
|
36
|
+
? hostSubprocessEnv({ site: "server.hooks.spawn-env", policy: [...HOOK_ENV_ALLOWLIST], allow: [] })
|
|
37
|
+
: composeHostSubprocessEnv({ policy: [...HOOK_ENV_ALLOWLIST], allow: [] });
|
|
38
|
+
return { ...base, CLAUDE_PROJECT_DIR: ctx.cwd, ...ctx.shellEnv };
|
|
39
|
+
}
|
|
40
|
+
export const SETTINGS_HOOK_STATIONS = {
|
|
41
|
+
PreToolUse: ["classic.PreToolUse"],
|
|
42
|
+
PostToolUse: ["classic.PostToolUse"],
|
|
43
|
+
PostToolUseFailure: ["classic.PostToolUseFailure"],
|
|
44
|
+
PostToolBatch: ["classic.PostToolBatch"],
|
|
45
|
+
UserPromptSubmit: ["classic.UserPromptSubmit"],
|
|
46
|
+
Stop: ["classic.Stop", "classic.SubagentStop"],
|
|
47
|
+
StopFailure: ["classic.StopFailure"],
|
|
48
|
+
PreCompact: ["classic.PreCompact"],
|
|
49
|
+
PostCompact: ["classic.PostCompact"],
|
|
50
|
+
};
|
|
51
|
+
function hangTranslator(on, event, translators) {
|
|
52
|
+
const translate = translators[event];
|
|
53
|
+
if (translate === undefined)
|
|
54
|
+
return;
|
|
55
|
+
for (const station of SETTINGS_HOOK_STATIONS[event])
|
|
56
|
+
hangClassic(on, station, translate);
|
|
57
|
+
}
|
|
58
|
+
export const SETTINGS_HOOKS_REGISTRATION = "settings.hooks";
|
|
59
|
+
export const DEFAULT_SETTINGS_HOOKS_BUDGET_MS = MAX_HOOK_TIMEOUT_SECONDS * 1000;
|
|
60
|
+
export function settingsHooksBudgetMs(config) {
|
|
61
|
+
return config.hooksTimeoutMs ?? DEFAULT_SETTINGS_HOOKS_BUDGET_MS;
|
|
37
62
|
}
|
|
38
|
-
const LIT_EVENTS = [
|
|
39
|
-
"PreToolUse",
|
|
40
|
-
"PostToolUse",
|
|
41
|
-
"PostToolUseFailure",
|
|
42
|
-
"PostToolBatch",
|
|
43
|
-
"UserPromptSubmit",
|
|
44
|
-
"Stop",
|
|
45
|
-
"StopFailure",
|
|
46
|
-
"PreCompact",
|
|
47
|
-
"PostCompact",
|
|
48
|
-
];
|
|
49
63
|
const BRAIN_TO_TURN_ERROR = {
|
|
50
64
|
auth: "authentication_failed",
|
|
51
65
|
rate_limit: "rate_limit",
|
|
@@ -76,7 +90,7 @@ function isRunnableHookEntry(entry) {
|
|
|
76
90
|
return isSupportedHookEntryType(entry.type);
|
|
77
91
|
}
|
|
78
92
|
export const MAX_MATCHER_EVAL_CHARS = MAX_HOOK_COMMAND_CHARS;
|
|
79
|
-
export const
|
|
93
|
+
export const ADMISSION_MATCHER_BUDGET = { budgetMs: 1_000, totalMs: 5_000 };
|
|
80
94
|
export const MATCHER_ECHO_CHARS = 200;
|
|
81
95
|
const MATCHER_UNJUDGED_TAG = "server.hooks.matcher-unjudged";
|
|
82
96
|
function matcherReadsWireToolName(event) {
|
|
@@ -130,7 +144,7 @@ const MATCHER_EVALUATION = {
|
|
|
130
144
|
regex: { regex: "budgeted", literal: "not_applicable" },
|
|
131
145
|
invalid: { regex: "unreadable", literal: "not_applicable" },
|
|
132
146
|
};
|
|
133
|
-
function judgeMatcher(matcher, depth,
|
|
147
|
+
function judgeMatcher(matcher, depth, ledger, judgesAlias) {
|
|
134
148
|
if (matcher.length > MAX_MATCHER_EVAL_CHARS)
|
|
135
149
|
return { kind: "unjudged", reason: "oversize" };
|
|
136
150
|
const evaluate = MATCHER_EVALUATION[hookMatcherForm(matcher)][depth];
|
|
@@ -141,14 +155,34 @@ function judgeMatcher(matcher, depth, budgetMs) {
|
|
|
141
155
|
return { kind: "invalid_regex" };
|
|
142
156
|
case "inline":
|
|
143
157
|
case "budgeted":
|
|
158
|
+
if (!judgesAlias)
|
|
159
|
+
return undefined;
|
|
144
160
|
break;
|
|
145
161
|
default: {
|
|
146
162
|
const unreachable = evaluate;
|
|
147
163
|
return unreachable;
|
|
148
164
|
}
|
|
149
165
|
}
|
|
150
|
-
const selects = (name) =>
|
|
151
|
-
|
|
166
|
+
const selects = (name) => {
|
|
167
|
+
if (evaluate === "inline")
|
|
168
|
+
return hookMatcherSelects(matcher, name);
|
|
169
|
+
const startedAt = ledger.now();
|
|
170
|
+
const answer = matcherSelectsWithin(matcher, name, ledger.budgetMs);
|
|
171
|
+
ledger.spentMs += ledger.now() - startedAt;
|
|
172
|
+
if (answer === "over_budget" || answer === "invalid")
|
|
173
|
+
return answer;
|
|
174
|
+
return ledger.spentMs > ledger.totalMs ? "budget_exceeded" : answer;
|
|
175
|
+
};
|
|
176
|
+
const unanswered = (verdict) => {
|
|
177
|
+
switch (verdict) {
|
|
178
|
+
case "over_budget":
|
|
179
|
+
return { kind: "over_budget" };
|
|
180
|
+
case "budget_exceeded":
|
|
181
|
+
return { kind: "budget_exceeded" };
|
|
182
|
+
case "invalid":
|
|
183
|
+
return { kind: "invalid_regex" };
|
|
184
|
+
}
|
|
185
|
+
};
|
|
152
186
|
for (const [alias, canonical] of engineToolAliases()) {
|
|
153
187
|
const selectsAlias = selects(alias);
|
|
154
188
|
if (typeof selectsAlias === "string")
|
|
@@ -163,50 +197,102 @@ function judgeMatcher(matcher, depth, budgetMs) {
|
|
|
163
197
|
}
|
|
164
198
|
return undefined;
|
|
165
199
|
}
|
|
166
|
-
|
|
200
|
+
const HOOKS_CAPS = {
|
|
201
|
+
matcher_groups: { measure: "entries", cap: MAX_HOOK_MATCHER_GROUPS, sentence: (event, count) => `settings.hooks.${event}: ${count} matcher groups exceeds cap ${MAX_HOOK_MATCHER_GROUPS}` },
|
|
202
|
+
event_entries: { measure: "entries", cap: MAX_HOOK_ENTRIES_PER_EVENT, sentence: (event, count) => `settings.hooks.${event}: ${count} entries exceeds cap ${MAX_HOOK_ENTRIES_PER_EVENT}` },
|
|
203
|
+
command: { measure: "characters", cap: MAX_HOOK_COMMAND_CHARS, sentence: (event) => `settings.hooks.${event}: command exceeds ${MAX_HOOK_COMMAND_CHARS} chars` },
|
|
204
|
+
url: { measure: "characters", cap: MAX_HOOK_COMMAND_CHARS, sentence: (event) => `settings.hooks.${event}: url exceeds ${MAX_HOOK_COMMAND_CHARS} chars` },
|
|
205
|
+
headers: { measure: "entries", cap: MAX_HOOK_HTTP_HEADERS, sentence: (event, count) => `settings.hooks.${event}: ${count} headers exceeds cap ${MAX_HOOK_HTTP_HEADERS}` },
|
|
206
|
+
header_chars: { measure: "characters", cap: MAX_HOOK_COMMAND_CHARS, sentence: (event) => `settings.hooks.${event}: headers exceed ${MAX_HOOK_COMMAND_CHARS} chars total` },
|
|
207
|
+
allowed_env_vars: { measure: "entries", cap: MAX_HOOK_HTTP_HEADERS, sentence: (event) => `settings.hooks.${event}: allowedEnvVars exceeds cap ${MAX_HOOK_HTTP_HEADERS}` },
|
|
208
|
+
};
|
|
209
|
+
function hooksCapIssue(limit, event, path, count) {
|
|
210
|
+
const c = HOOKS_CAPS[limit];
|
|
211
|
+
return { reason: LIMIT_REASONS[c.measure], limit, event, path, count, cap: c.cap };
|
|
212
|
+
}
|
|
213
|
+
const matcherPath = (event, groupIndex) => ["hooks", event, groupIndex, "matcher"];
|
|
214
|
+
export function buildHooksRefusal(issue) {
|
|
215
|
+
const field = "settings.hooks";
|
|
167
216
|
switch (issue.reason) {
|
|
168
217
|
case "dead_alias":
|
|
169
218
|
return {
|
|
170
219
|
message: `settings.hooks.${issue.event}: matcher "${issue.alias}" is an alias; tool-event matchers match the tool's wire name (PostToolUse / PostToolUseFailure since 7.107.0) — use "${issue.canonical}"`,
|
|
171
|
-
field
|
|
172
|
-
detail: { reason: issue.reason, event: issue.event, alias: issue.alias, canonical: issue.canonical },
|
|
220
|
+
field,
|
|
221
|
+
detail: { reason: issue.reason, event: issue.event, alias: issue.alias, canonical: issue.canonical, path: matcherPath(issue.event, issue.groupIndex) },
|
|
173
222
|
};
|
|
174
223
|
case "over_budget":
|
|
175
224
|
return {
|
|
176
225
|
message: `settings.hooks.${issue.event}[${issue.groupIndex}].matcher: evaluating the pattern did not finish within ${issue.budgetMs} ms (catastrophic regex backtracking) — rewrite it as a tool-name list or a pattern without nested repetition`,
|
|
177
|
-
field
|
|
178
|
-
detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex, budgetMs: issue.budgetMs },
|
|
226
|
+
field,
|
|
227
|
+
detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex, budgetMs: issue.budgetMs, path: matcherPath(issue.event, issue.groupIndex) },
|
|
228
|
+
};
|
|
229
|
+
case "budget_exceeded":
|
|
230
|
+
return {
|
|
231
|
+
message: `settings.hooks.${issue.event}[${issue.groupIndex}].matcher: evaluating this configuration's regular-expression matchers took longer than ${issue.totalMs} ms in total (very slow patterns) — write tool names as a list such as "Read|Edit", or simplify the patterns`,
|
|
232
|
+
field,
|
|
233
|
+
detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex, totalMs: issue.totalMs, path: matcherPath(issue.event, issue.groupIndex) },
|
|
179
234
|
};
|
|
180
235
|
case "invalid_regex":
|
|
181
236
|
return {
|
|
182
237
|
message: `settings.hooks.${issue.event}[${issue.groupIndex}].matcher: ${JSON.stringify(issue.matcher)} is not a valid regular expression (${issue.compileError}) — fix the pattern, or write the tool names as a list such as "Read|Edit"`,
|
|
183
|
-
field
|
|
184
|
-
detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex },
|
|
238
|
+
field,
|
|
239
|
+
detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex, path: matcherPath(issue.event, issue.groupIndex) },
|
|
240
|
+
};
|
|
241
|
+
case "unsupported_entry_type":
|
|
242
|
+
return {
|
|
243
|
+
message: `${hooksCoordinate(issue.path)}: this server does not run hook entries of type ${issue.type}; supported: ${SUPPORTED_HOOK_ENTRY_TYPES.join(" / ")}`,
|
|
244
|
+
field,
|
|
245
|
+
detail: { reason: issue.reason, path: hooksOutgoingPath(issue.path), type: issue.type },
|
|
185
246
|
};
|
|
247
|
+
case "over_cap":
|
|
248
|
+
case "too_long": {
|
|
249
|
+
const cap = issue;
|
|
250
|
+
return { message: HOOKS_CAPS[cap.limit].sentence(cap.event, cap.count), field, detail: { reason: cap.reason, path: hooksOutgoingPath(cap.path), count: cap.count, cap: cap.cap } };
|
|
251
|
+
}
|
|
252
|
+
case "unnamed_refusal":
|
|
253
|
+
return { message: `${hooksCoordinate(issue.path)}: the hooks reader refused this configuration without naming a piece`, field, detail: { reason: issue.reason, path: hooksOutgoingPath(issue.path) } };
|
|
186
254
|
default: {
|
|
187
|
-
const
|
|
188
|
-
|
|
255
|
+
const piece = issue;
|
|
256
|
+
const key = piece.key !== undefined ? hooksOutgoingKey(piece.key) : undefined;
|
|
257
|
+
return {
|
|
258
|
+
message: hooksPieceRefusal(piece),
|
|
259
|
+
field,
|
|
260
|
+
detail: { reason: piece.reason, path: hooksOutgoingPath(piece.path), ...(key !== undefined ? { key } : {}) },
|
|
261
|
+
};
|
|
189
262
|
}
|
|
190
263
|
}
|
|
191
264
|
}
|
|
192
|
-
export function
|
|
193
|
-
const
|
|
265
|
+
export function hooksMatcherIssue(config, depth, budget = {}) {
|
|
266
|
+
const ledger = {
|
|
267
|
+
budgetMs: budget.budgetMs ?? ADMISSION_MATCHER_BUDGET.budgetMs,
|
|
268
|
+
totalMs: budget.totalMs ?? ADMISSION_MATCHER_BUDGET.totalMs,
|
|
269
|
+
now: budget.now ?? (() => performance.now()),
|
|
270
|
+
spentMs: 0,
|
|
271
|
+
};
|
|
272
|
+
const budgetMs = ledger.budgetMs;
|
|
194
273
|
for (const [event, groups] of Object.entries(config)) {
|
|
195
|
-
|
|
196
|
-
continue;
|
|
274
|
+
const judgesAlias = matcherReadsWireToolName(event);
|
|
197
275
|
for (const [groupIndex, group] of (groups ?? []).entries()) {
|
|
198
276
|
if (group.matcher === undefined)
|
|
199
277
|
continue;
|
|
200
|
-
const verdict = judgeMatcher(group.matcher, depth,
|
|
278
|
+
const verdict = judgeMatcher(group.matcher, depth, ledger, judgesAlias);
|
|
201
279
|
if (verdict === undefined)
|
|
202
280
|
continue;
|
|
203
281
|
switch (verdict.kind) {
|
|
204
282
|
case "dead":
|
|
205
|
-
return { reason: "dead_alias", event, alias: verdict.alias, canonical: verdict.canonical };
|
|
283
|
+
return { reason: "dead_alias", event, groupIndex, alias: verdict.alias, canonical: verdict.canonical };
|
|
206
284
|
case "over_budget":
|
|
207
285
|
return { reason: "over_budget", event, groupIndex, budgetMs };
|
|
286
|
+
case "budget_exceeded":
|
|
287
|
+
return { reason: "budget_exceeded", event, groupIndex, totalMs: ledger.totalMs };
|
|
208
288
|
case "invalid_regex":
|
|
209
|
-
return {
|
|
289
|
+
return {
|
|
290
|
+
reason: "invalid_regex",
|
|
291
|
+
event,
|
|
292
|
+
groupIndex,
|
|
293
|
+
matcher: echoCallerText(group.matcher, { maxChars: MATCHER_ECHO_CHARS }),
|
|
294
|
+
compileError: echoCallerText(matcherCompileSentence(group.matcher), { maxChars: MATCHER_ECHO_CHARS }),
|
|
295
|
+
};
|
|
210
296
|
case "unjudged":
|
|
211
297
|
recordFailOpen(MATCHER_UNJUDGED_TAG, `event=${event} reason=${verdict.reason} matcherChars=${group.matcher.length}`);
|
|
212
298
|
break;
|
|
@@ -232,74 +318,81 @@ const HOOKS_PIECE_MEANING = {
|
|
|
232
318
|
"guard-matcher-invalid": "a PreToolUse / PermissionRequest matcher group is not valid",
|
|
233
319
|
"guard-entry-invalid": "a PreToolUse / PermissionRequest hook entry is not valid",
|
|
234
320
|
};
|
|
321
|
+
export function hooksOutgoingPath(path) {
|
|
322
|
+
return outgoingPath(path).map((seg) => (typeof seg === "string" ? echoCallerText(seg) : seg));
|
|
323
|
+
}
|
|
235
324
|
export function hooksCoordinate(path) {
|
|
236
|
-
return `settings.${formatConfigPath(
|
|
325
|
+
return `settings.${formatConfigPath(hooksOutgoingPath(path))}`;
|
|
326
|
+
}
|
|
327
|
+
function hooksOutgoingKey(key) {
|
|
328
|
+
return echoCallerText(String(outgoingPath([key])[0] ?? ""));
|
|
329
|
+
}
|
|
330
|
+
function hooksPieceIssue(piece) {
|
|
331
|
+
return piece.field !== undefined ? { reason: piece.code, path: piece.path, key: piece.field } : { reason: piece.code, path: piece.path };
|
|
237
332
|
}
|
|
238
333
|
function hooksPieceRefusal(piece) {
|
|
239
|
-
const field = piece.
|
|
240
|
-
return `${hooksCoordinate(piece.path)}: ${piece.
|
|
334
|
+
const field = piece.key !== undefined ? ` (field: ${hooksOutgoingKey(piece.key)})` : "";
|
|
335
|
+
return `${hooksCoordinate(piece.path)}: ${piece.reason} — ${HOOKS_PIECE_MEANING[piece.reason]}${field}`;
|
|
241
336
|
}
|
|
242
|
-
function
|
|
243
|
-
|
|
337
|
+
function hooksRefused(issue, pieces) {
|
|
338
|
+
const error = buildHooksRefusal(issue).message;
|
|
339
|
+
return pieces === undefined ? { issue, error } : { issue, error, pieces };
|
|
244
340
|
}
|
|
245
341
|
export function parseHooksConfig(raw) {
|
|
246
342
|
const classified = classifyHooksConfig({ hooks: raw });
|
|
247
343
|
if (classified.outcome !== "loaded") {
|
|
248
344
|
const first = classified.outcome === "rejected" ? classified.rejections[0] : classified.dropped[0];
|
|
249
345
|
const pieces = classified.outcome === "rejected" ? classified.rejections : classified.dropped;
|
|
250
|
-
return
|
|
346
|
+
return hooksRefused(first !== undefined ? hooksPieceIssue(first) : { reason: "unnamed_refusal", path: ["hooks"] }, pieces);
|
|
251
347
|
}
|
|
252
348
|
const loaded = classified.hooks;
|
|
253
349
|
if (loaded === undefined) {
|
|
254
|
-
return
|
|
350
|
+
return hooksRefused({ reason: "hooks-not-object", path: ["hooks"] });
|
|
255
351
|
}
|
|
256
352
|
const config = {};
|
|
257
353
|
for (const key of Object.keys(loaded)) {
|
|
258
354
|
if (!isHookEventName(key)) {
|
|
259
|
-
return
|
|
355
|
+
return hooksRefused({ reason: "unknown-event", path: ["hooks", key] });
|
|
260
356
|
}
|
|
261
357
|
const event = key;
|
|
262
358
|
const groups = loaded[event];
|
|
263
359
|
if (!groups)
|
|
264
360
|
continue;
|
|
265
|
-
if (groups.length > MAX_HOOK_MATCHER_GROUPS)
|
|
266
|
-
return
|
|
267
|
-
}
|
|
361
|
+
if (groups.length > MAX_HOOK_MATCHER_GROUPS)
|
|
362
|
+
return hooksRefused(hooksCapIssue("matcher_groups", event, ["hooks", event], groups.length));
|
|
268
363
|
let entries = 0;
|
|
364
|
+
let firstPastCap;
|
|
269
365
|
const kept = [];
|
|
270
366
|
for (const [groupIndex, group] of groups.entries()) {
|
|
271
367
|
const hooks = [];
|
|
272
368
|
for (const [entryIndex, entry] of group.hooks.entries()) {
|
|
273
369
|
entries++;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (entry
|
|
278
|
-
return {
|
|
279
|
-
|
|
370
|
+
const at = ["hooks", event, groupIndex, "hooks", entryIndex];
|
|
371
|
+
if (entries === MAX_HOOK_ENTRIES_PER_EVENT + 1)
|
|
372
|
+
firstPastCap = at;
|
|
373
|
+
if (!isRunnableHookEntry(entry))
|
|
374
|
+
return hooksRefused({ reason: "unsupported_entry_type", path: [...at, "type"], type: entry.type });
|
|
375
|
+
if (entry.type === "command" && entry.command.length > MAX_HOOK_COMMAND_CHARS)
|
|
376
|
+
return hooksRefused(hooksCapIssue("command", event, [...at, "command"], entry.command.length));
|
|
280
377
|
if (entry.type === "http") {
|
|
281
|
-
if (entry.url.length > MAX_HOOK_COMMAND_CHARS)
|
|
282
|
-
return
|
|
283
|
-
}
|
|
378
|
+
if (entry.url.length > MAX_HOOK_COMMAND_CHARS)
|
|
379
|
+
return hooksRefused(hooksCapIssue("url", event, [...at, "url"], entry.url.length));
|
|
284
380
|
const headerEntries = Object.entries(entry.headers ?? {});
|
|
285
|
-
if (headerEntries.length > MAX_HOOK_HTTP_HEADERS)
|
|
286
|
-
return
|
|
287
|
-
}
|
|
381
|
+
if (headerEntries.length > MAX_HOOK_HTTP_HEADERS)
|
|
382
|
+
return hooksRefused(hooksCapIssue("headers", event, [...at, "headers"], headerEntries.length));
|
|
288
383
|
const headerChars = headerEntries.reduce((n, [k, v]) => n + k.length + v.length, 0);
|
|
289
|
-
if (headerChars > MAX_HOOK_COMMAND_CHARS)
|
|
290
|
-
return
|
|
291
|
-
|
|
292
|
-
if (
|
|
293
|
-
return
|
|
294
|
-
}
|
|
384
|
+
if (headerChars > MAX_HOOK_COMMAND_CHARS)
|
|
385
|
+
return hooksRefused(hooksCapIssue("header_chars", event, [...at, "headers"], headerChars));
|
|
386
|
+
const envVars = entry.allowedEnvVars?.length ?? 0;
|
|
387
|
+
if (envVars > MAX_HOOK_HTTP_HEADERS)
|
|
388
|
+
return hooksRefused(hooksCapIssue("allowed_env_vars", event, [...at, "allowedEnvVars"], envVars));
|
|
295
389
|
}
|
|
296
390
|
hooks.push(entry);
|
|
297
391
|
}
|
|
298
392
|
kept.push(group.matcher !== undefined ? { matcher: group.matcher, hooks } : { hooks });
|
|
299
393
|
}
|
|
300
|
-
if (
|
|
301
|
-
return
|
|
302
|
-
}
|
|
394
|
+
if (firstPastCap !== undefined)
|
|
395
|
+
return hooksRefused(hooksCapIssue("event_entries", event, firstPastCap, entries));
|
|
303
396
|
config[event] = kept;
|
|
304
397
|
}
|
|
305
398
|
return { config };
|
|
@@ -390,61 +483,40 @@ function noteHookFailure(ctx, args) {
|
|
|
390
483
|
});
|
|
391
484
|
}
|
|
392
485
|
export function buildHookContext(parts) {
|
|
393
|
-
const present = parts.filter((p) => p.length > 0)
|
|
486
|
+
const present = parts.filter((p) => p.length > 0);
|
|
394
487
|
if (present.length === 0)
|
|
395
488
|
return undefined;
|
|
396
489
|
const kept = [];
|
|
397
490
|
let used = 0;
|
|
398
491
|
let truncating = false;
|
|
399
|
-
let messages = 0;
|
|
400
492
|
let omitted = 0;
|
|
401
493
|
let droppedChars = 0;
|
|
402
494
|
for (const part of present) {
|
|
403
|
-
messages += part.messages;
|
|
404
|
-
omitted += part.omitted;
|
|
405
|
-
droppedChars += part.dropped;
|
|
406
495
|
if (!truncating) {
|
|
407
496
|
const sep = kept.length > 0 ? 1 : 0;
|
|
408
497
|
const room = MAX_HOOK_CONTEXT_TOTAL_CHARS - used - sep;
|
|
409
|
-
if (room >= part.
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
used += sep + part.body.length;
|
|
413
|
-
}
|
|
498
|
+
if (room >= part.length) {
|
|
499
|
+
kept.push(part);
|
|
500
|
+
used += sep + part.length;
|
|
414
501
|
continue;
|
|
415
502
|
}
|
|
416
503
|
truncating = true;
|
|
417
|
-
if (
|
|
418
|
-
kept.push(part.
|
|
504
|
+
if (room >= MIN_HOOK_CONTEXT_PARTIAL_CHARS) {
|
|
505
|
+
kept.push(part.slice(0, room));
|
|
419
506
|
used += sep + room;
|
|
420
|
-
droppedChars += part.
|
|
507
|
+
droppedChars += part.length - room;
|
|
421
508
|
continue;
|
|
422
509
|
}
|
|
423
510
|
}
|
|
424
|
-
omitted +=
|
|
425
|
-
droppedChars += part.
|
|
511
|
+
omitted += 1;
|
|
512
|
+
droppedChars += part.length;
|
|
426
513
|
}
|
|
427
514
|
const body = kept.join("\n");
|
|
428
515
|
if (droppedChars === 0)
|
|
429
516
|
return body;
|
|
430
|
-
const marker = `[hook context truncated at ${MAX_HOOK_CONTEXT_TOTAL_CHARS} chars: ${omitted} of ${
|
|
517
|
+
const marker = `[hook context truncated at ${MAX_HOOK_CONTEXT_TOTAL_CHARS} chars: ${omitted} of ${present.length} hook messages omitted, ${droppedChars} characters dropped]`;
|
|
431
518
|
return kept.length > 0 ? `${body}\n${marker}` : marker;
|
|
432
519
|
}
|
|
433
|
-
const HOOK_CONTEXT_MARKER_RE = new RegExp(`\\n\\[hook context truncated at ${MAX_HOOK_CONTEXT_TOTAL_CHARS} chars: (\\d+) of (\\d+) hook messages omitted, (\\d+) characters dropped\\]$`);
|
|
434
|
-
const MAX_HOOK_CONTEXT_PARSED_COUNT = 2 ** 42;
|
|
435
|
-
function readHookContextPart(part) {
|
|
436
|
-
const m = HOOK_CONTEXT_MARKER_RE.exec(part);
|
|
437
|
-
if (!m)
|
|
438
|
-
return { body: part, messages: 1, omitted: 0, dropped: 0 };
|
|
439
|
-
const omitted = Number(m[1]);
|
|
440
|
-
const messages = Number(m[2]);
|
|
441
|
-
const dropped = Number(m[3]);
|
|
442
|
-
const sane = (n) => Number.isSafeInteger(n) && n <= MAX_HOOK_CONTEXT_PARSED_COUNT;
|
|
443
|
-
if (!sane(omitted) || !sane(messages) || !sane(dropped) || messages < 1 || omitted > messages || dropped < 1) {
|
|
444
|
-
return { body: part, messages: 1, omitted: 0, dropped: 0 };
|
|
445
|
-
}
|
|
446
|
-
return { body: part.slice(0, part.length - m[0].length), messages, omitted, dropped };
|
|
447
|
-
}
|
|
448
520
|
function buildHookContextField(parts) {
|
|
449
521
|
const text = buildHookContext(parts);
|
|
450
522
|
return text === undefined ? {} : { additionalContext: text };
|
|
@@ -463,7 +535,7 @@ async function runCommandHook(entry, payload, ctx) {
|
|
|
463
535
|
const sh = hostShell();
|
|
464
536
|
child = spawn(sh.shell, [...sh.args, entry.command], {
|
|
465
537
|
cwd: ctx.cwd,
|
|
466
|
-
env: hookEnv(ctx),
|
|
538
|
+
env: hookEnv(ctx, true),
|
|
467
539
|
stdio: ["pipe", "pipe", "pipe"],
|
|
468
540
|
detached: true,
|
|
469
541
|
});
|
|
@@ -526,7 +598,7 @@ async function fireAsyncCommandHook(entry, payload, ctx, event) {
|
|
|
526
598
|
const sh = hostShell();
|
|
527
599
|
const child = spawn(sh.shell, [...sh.args, entry.command], {
|
|
528
600
|
cwd: ctx.cwd,
|
|
529
|
-
env: hookEnv(ctx),
|
|
601
|
+
env: hookEnv(ctx, true),
|
|
530
602
|
stdio: ["pipe", "ignore", rewake ? "pipe" : "ignore"],
|
|
531
603
|
detached: true,
|
|
532
604
|
});
|
|
@@ -659,7 +731,7 @@ async function runLlmHook(entry, payload, ctx, extra) {
|
|
|
659
731
|
const MAX_BLANKED_REFERENCES_LOGGED = 16;
|
|
660
732
|
async function runHttpHook(entry, payload, ctx, event) {
|
|
661
733
|
const timeoutMs = Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
|
|
662
|
-
const interpolated = interpolateHookEnvVars(entry.headers ?? {}, entry.allowedEnvVars ?? [], hookEnv(ctx));
|
|
734
|
+
const interpolated = interpolateHookEnvVars(entry.headers ?? {}, entry.allowedEnvVars ?? [], hookEnv(ctx, false));
|
|
663
735
|
if (interpolated.blanked.length > 0) {
|
|
664
736
|
ctx.logger.warn("hook_http_header_env_blanked", {
|
|
665
737
|
event,
|
|
@@ -796,7 +868,7 @@ async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceF
|
|
|
796
868
|
gi++;
|
|
797
869
|
const m = hookMatcherMatches(group.matcher, matchValue);
|
|
798
870
|
if (m === "invalid") {
|
|
799
|
-
ctx.logger.warn("hook_matcher_invalid", { event, matcher:
|
|
871
|
+
ctx.logger.warn("hook_matcher_invalid", { event, matcher: echoCallerText(group.matcher ?? "", { maxChars: MATCHER_ECHO_CHARS }) });
|
|
800
872
|
continue;
|
|
801
873
|
}
|
|
802
874
|
if (!m)
|
|
@@ -898,12 +970,8 @@ async function runObserveOnlyEvent(event, groups, matchValue, payload, ctx, once
|
|
|
898
970
|
}
|
|
899
971
|
}
|
|
900
972
|
}
|
|
901
|
-
export function
|
|
973
|
+
export function createSettingsHooksRegistration(config, ctx, budgetMs) {
|
|
902
974
|
const configured = Object.keys(config).filter((e) => (config[e]?.length ?? 0) > 0);
|
|
903
|
-
const dark = configured.filter((e) => HOOK_EVENT_OWNER[e] === "engine" && !LIT_EVENTS.includes(e));
|
|
904
|
-
if (dark.length > 0) {
|
|
905
|
-
ctx.logger.warn("hook_events_not_fired", { events: dark, phase: 2 });
|
|
906
|
-
}
|
|
907
975
|
const shellOwned = configured.filter((e) => HOOK_EVENT_OWNER[e] === "shell");
|
|
908
976
|
if (shellOwned.length > 0) {
|
|
909
977
|
ctx.logger.debug("hook_events_shell_owned", { events: shellOwned });
|
|
@@ -925,9 +993,11 @@ export function createTaskHooks(config, ctx) {
|
|
|
925
993
|
return undefined;
|
|
926
994
|
}
|
|
927
995
|
const onceFired = new Set();
|
|
928
|
-
const
|
|
996
|
+
const translators = {};
|
|
929
997
|
if (pre) {
|
|
930
|
-
|
|
998
|
+
translators.PreToolUse = async (e) => {
|
|
999
|
+
const toolName = e.tool;
|
|
1000
|
+
const input = e.input;
|
|
931
1001
|
let currentInput = input;
|
|
932
1002
|
let rewritten = false;
|
|
933
1003
|
let asked;
|
|
@@ -940,18 +1010,18 @@ export function createTaskHooks(config, ctx) {
|
|
|
940
1010
|
gi++;
|
|
941
1011
|
const m = hookMatcherMatches(group.matcher, toolName);
|
|
942
1012
|
if (m === "invalid") {
|
|
943
|
-
ctx.logger.warn("hook_matcher_invalid", { event: "PreToolUse", matcher:
|
|
1013
|
+
ctx.logger.warn("hook_matcher_invalid", { event: "PreToolUse", matcher: echoCallerText(group.matcher ?? "", { maxChars: MATCHER_ECHO_CHARS }) });
|
|
944
1014
|
continue;
|
|
945
1015
|
}
|
|
946
1016
|
if (!m)
|
|
947
1017
|
continue;
|
|
948
1018
|
const groupInput = currentInput;
|
|
949
1019
|
const payload = {
|
|
950
|
-
...basePayload(ctx,
|
|
1020
|
+
...basePayload(ctx, e.permissionMode),
|
|
951
1021
|
hook_event_name: "PreToolUse",
|
|
952
1022
|
tool_name: toolName,
|
|
953
1023
|
tool_input: groupInput,
|
|
954
|
-
tool_use_id:
|
|
1024
|
+
tool_use_id: e.tool_use_id,
|
|
955
1025
|
};
|
|
956
1026
|
const singles = await runMatchingEntries("PreToolUse", [{ ...group, matcher: undefined }], toolName, payload, ctx, onceFired, deadlineAt, `PreToolUse:${gi}`);
|
|
957
1027
|
for (const { entry, run } of singles) {
|
|
@@ -1034,21 +1104,23 @@ export function createTaskHooks(config, ctx) {
|
|
|
1034
1104
|
return carried;
|
|
1035
1105
|
return undefined;
|
|
1036
1106
|
};
|
|
1037
|
-
hooks.preToolUse = preToolUseHook;
|
|
1038
1107
|
}
|
|
1039
1108
|
if (post) {
|
|
1040
|
-
|
|
1109
|
+
translators.PostToolUse = async (e) => {
|
|
1110
|
+
const toolName = e.tool;
|
|
1111
|
+
const input = e.input;
|
|
1112
|
+
const output = e.output;
|
|
1041
1113
|
const contexts = [];
|
|
1042
1114
|
const responseText = output.content
|
|
1043
1115
|
.map((c) => (c && typeof c === "object" && typeof c.text === "string" ? c.text : ""))
|
|
1044
1116
|
.join("");
|
|
1045
1117
|
const payload = {
|
|
1046
|
-
...basePayload(ctx,
|
|
1118
|
+
...basePayload(ctx, e.permissionMode),
|
|
1047
1119
|
hook_event_name: "PostToolUse",
|
|
1048
1120
|
tool_name: toolName,
|
|
1049
1121
|
tool_input: input,
|
|
1050
1122
|
tool_response: { isError: output.isError, text: clip(responseText, MAX_TOOL_RESPONSE_CHARS) },
|
|
1051
|
-
tool_use_id:
|
|
1123
|
+
tool_use_id: e.tool_use_id,
|
|
1052
1124
|
};
|
|
1053
1125
|
const singles = await runMatchingEntries("PostToolUse", post, toolName, payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
|
|
1054
1126
|
for (const { entry, run } of singles) {
|
|
@@ -1086,17 +1158,18 @@ export function createTaskHooks(config, ctx) {
|
|
|
1086
1158
|
const additionalContext = buildHookContext(contexts);
|
|
1087
1159
|
return additionalContext === undefined ? undefined : { additionalContext };
|
|
1088
1160
|
};
|
|
1089
|
-
hooks.postToolUse = postToolUseHook;
|
|
1090
1161
|
}
|
|
1091
1162
|
if (postFailure) {
|
|
1092
|
-
|
|
1163
|
+
translators.PostToolUseFailure = async (e) => {
|
|
1164
|
+
const toolName = e.tool;
|
|
1165
|
+
const f = e.failure;
|
|
1093
1166
|
const contexts = [];
|
|
1094
1167
|
const payload = {
|
|
1095
|
-
...basePayload(ctx,
|
|
1168
|
+
...basePayload(ctx, e.permissionMode),
|
|
1096
1169
|
hook_event_name: "PostToolUseFailure",
|
|
1097
1170
|
tool_name: toolName,
|
|
1098
|
-
tool_input: input,
|
|
1099
|
-
tool_use_id:
|
|
1171
|
+
tool_input: e.input,
|
|
1172
|
+
tool_use_id: e.tool_use_id,
|
|
1100
1173
|
error: clip(f.error, MAX_TOOL_RESPONSE_CHARS),
|
|
1101
1174
|
is_interrupt: f.isInterrupt,
|
|
1102
1175
|
};
|
|
@@ -1132,15 +1205,14 @@ export function createTaskHooks(config, ctx) {
|
|
|
1132
1205
|
const additionalContext = buildHookContext(contexts);
|
|
1133
1206
|
return additionalContext === undefined ? undefined : { additionalContext };
|
|
1134
1207
|
};
|
|
1135
|
-
hooks.postToolUseFailure = postToolUseFailureHook;
|
|
1136
1208
|
}
|
|
1137
1209
|
if (postBatch) {
|
|
1138
|
-
|
|
1210
|
+
translators.PostToolBatch = async (e) => {
|
|
1139
1211
|
const contexts = [];
|
|
1140
1212
|
const payload = {
|
|
1141
|
-
...basePayload(ctx,
|
|
1213
|
+
...basePayload(ctx, e.permissionMode),
|
|
1142
1214
|
hook_event_name: "PostToolBatch",
|
|
1143
|
-
tool_calls:
|
|
1215
|
+
tool_calls: e.batch.map((c) => ({
|
|
1144
1216
|
tool_name: c.toolName,
|
|
1145
1217
|
tool_input: boundedInputValue(c.input),
|
|
1146
1218
|
tool_use_id: c.toolCallId,
|
|
@@ -1175,12 +1247,11 @@ export function createTaskHooks(config, ctx) {
|
|
|
1175
1247
|
const additionalContext = buildHookContext(contexts);
|
|
1176
1248
|
return additionalContext === undefined ? undefined : { additionalContext };
|
|
1177
1249
|
};
|
|
1178
|
-
hooks.postToolBatch = postToolBatchHook;
|
|
1179
1250
|
}
|
|
1180
1251
|
if (promptSubmit) {
|
|
1181
|
-
|
|
1252
|
+
translators.UserPromptSubmit = async (e) => {
|
|
1182
1253
|
const contexts = [];
|
|
1183
|
-
const payload = { ...basePayload(ctx,
|
|
1254
|
+
const payload = { ...basePayload(ctx, e.permissionMode), hook_event_name: "UserPromptSubmit", prompt: clip(e.prompt, MAX_TOOL_INPUT_CHARS) };
|
|
1184
1255
|
const singles = await runMatchingEntries("UserPromptSubmit", ignoreMatchers(promptSubmit), "", payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
|
|
1185
1256
|
for (const { entry, run } of singles) {
|
|
1186
1257
|
if (run.code === 2) {
|
|
@@ -1215,10 +1286,9 @@ export function createTaskHooks(config, ctx) {
|
|
|
1215
1286
|
const additionalContext = buildHookContext(contexts);
|
|
1216
1287
|
return additionalContext === undefined ? undefined : { additionalContext };
|
|
1217
1288
|
};
|
|
1218
|
-
hooks.userPromptSubmit = userPromptSubmitHook;
|
|
1219
1289
|
}
|
|
1220
1290
|
if (stopEntries) {
|
|
1221
|
-
|
|
1291
|
+
translators.Stop = async (sctx) => {
|
|
1222
1292
|
const payload = { ...basePayload(ctx, sctx.permissionMode), hook_event_name: "Stop", stop_hook_active: sctx.stopHookActive };
|
|
1223
1293
|
let llmExtra;
|
|
1224
1294
|
const wantsPrompt = stopEntries.some((g) => g.hooks.some((h) => h.type === "prompt"));
|
|
@@ -1307,10 +1377,9 @@ export function createTaskHooks(config, ctx) {
|
|
|
1307
1377
|
...buildHookContextField(contexts),
|
|
1308
1378
|
};
|
|
1309
1379
|
};
|
|
1310
|
-
hooks.stop = stopHook;
|
|
1311
1380
|
}
|
|
1312
1381
|
if (stopFailureEntries) {
|
|
1313
|
-
|
|
1382
|
+
translators.StopFailure = async (fctx) => {
|
|
1314
1383
|
const turnError = toAssistantTurnError(fctx.errorKind);
|
|
1315
1384
|
const payload = {
|
|
1316
1385
|
...basePayload(ctx, undefined),
|
|
@@ -1320,10 +1389,9 @@ export function createTaskHooks(config, ctx) {
|
|
|
1320
1389
|
};
|
|
1321
1390
|
await runObserveOnlyEvent("StopFailure", stopFailureEntries, turnError, payload, ctx, onceFired);
|
|
1322
1391
|
};
|
|
1323
|
-
hooks.stopFailure = stopFailureHook;
|
|
1324
1392
|
}
|
|
1325
1393
|
if (preCompactEntries) {
|
|
1326
|
-
|
|
1394
|
+
translators.PreCompact = async (pctx) => {
|
|
1327
1395
|
const payload = {
|
|
1328
1396
|
...basePayload(ctx, undefined),
|
|
1329
1397
|
hook_event_name: "PreCompact",
|
|
@@ -1354,10 +1422,9 @@ export function createTaskHooks(config, ctx) {
|
|
|
1354
1422
|
}
|
|
1355
1423
|
return undefined;
|
|
1356
1424
|
};
|
|
1357
|
-
hooks.preCompact = preCompactHook;
|
|
1358
1425
|
}
|
|
1359
1426
|
if (postCompactEntries) {
|
|
1360
|
-
|
|
1427
|
+
translators.PostCompact = async (cctx) => {
|
|
1361
1428
|
const payload = {
|
|
1362
1429
|
...basePayload(ctx, undefined),
|
|
1363
1430
|
hook_event_name: "PostCompact",
|
|
@@ -1366,177 +1433,16 @@ export function createTaskHooks(config, ctx) {
|
|
|
1366
1433
|
};
|
|
1367
1434
|
await runObserveOnlyEvent("PostCompact", postCompactEntries, cctx.trigger, payload, ctx, onceFired);
|
|
1368
1435
|
};
|
|
1369
|
-
hooks.postCompact = postCompactHook;
|
|
1370
|
-
}
|
|
1371
|
-
return hooks;
|
|
1372
|
-
}
|
|
1373
|
-
const HOOK_SLOT_FOLD = {
|
|
1374
|
-
preToolUse: "custom",
|
|
1375
|
-
preToolUseObservational: "custom",
|
|
1376
|
-
postToolUse: "custom",
|
|
1377
|
-
postToolUseFailure: "custom",
|
|
1378
|
-
postToolBatch: "custom",
|
|
1379
|
-
userPromptSubmit: "custom",
|
|
1380
|
-
stop: "custom",
|
|
1381
|
-
preCompact: "custom",
|
|
1382
|
-
timeoutMs: "scalar",
|
|
1383
|
-
postCompact: "observe-void",
|
|
1384
|
-
stopFailure: "observe-void",
|
|
1385
|
-
permissionDenied: "observe-void",
|
|
1386
|
-
};
|
|
1387
|
-
const OBSERVE_VOID_HOOK_SLOTS = Object.keys(HOOK_SLOT_FOLD).filter((k) => HOOK_SLOT_FOLD[k] === "observe-void");
|
|
1388
|
-
export function composeHooks(deployment, task) {
|
|
1389
|
-
if (!deployment)
|
|
1390
|
-
return task;
|
|
1391
|
-
const d = deployment;
|
|
1392
|
-
const t = task;
|
|
1393
|
-
const out = {};
|
|
1394
|
-
const timeoutMs = d.timeoutMs ?? t.timeoutMs;
|
|
1395
|
-
if (timeoutMs !== undefined)
|
|
1396
|
-
out.timeoutMs = timeoutMs;
|
|
1397
|
-
if (d.preToolUse || t.preToolUse) {
|
|
1398
|
-
const bothObservational = (d.preToolUse === undefined || d.preToolUseObservational === true) &&
|
|
1399
|
-
(t.preToolUse === undefined || t.preToolUseObservational === true);
|
|
1400
|
-
if (bothObservational)
|
|
1401
|
-
out.preToolUseObservational = true;
|
|
1402
|
-
out.preToolUse = async (toolName, input, ctx) => {
|
|
1403
|
-
let current = input;
|
|
1404
|
-
const contexts = [];
|
|
1405
|
-
let asked;
|
|
1406
|
-
let allowedFor;
|
|
1407
|
-
for (const slot of [d.preToolUse?.bind(d), t.preToolUse?.bind(t)]) {
|
|
1408
|
-
if (!slot)
|
|
1409
|
-
continue;
|
|
1410
|
-
const r = receiveHookResult(await slot(toolName, current, ctx));
|
|
1411
|
-
if (r === undefined || r === null || r === false)
|
|
1412
|
-
continue;
|
|
1413
|
-
if (isEngineRefusal(r))
|
|
1414
|
-
return r;
|
|
1415
|
-
if (r.additionalContext)
|
|
1416
|
-
contexts.push(r.additionalContext);
|
|
1417
|
-
const action = r.action;
|
|
1418
|
-
const updatedInput = r.updatedInput;
|
|
1419
|
-
switch (action) {
|
|
1420
|
-
case "deny":
|
|
1421
|
-
return { ...r, ...buildHookContextField(contexts) };
|
|
1422
|
-
case "ask":
|
|
1423
|
-
asked = r;
|
|
1424
|
-
break;
|
|
1425
|
-
case "allow":
|
|
1426
|
-
if (updatedInput !== undefined)
|
|
1427
|
-
current = updatedInput;
|
|
1428
|
-
allowedFor = { input: current };
|
|
1429
|
-
break;
|
|
1430
|
-
case undefined:
|
|
1431
|
-
if (updatedInput !== undefined)
|
|
1432
|
-
current = updatedInput;
|
|
1433
|
-
break;
|
|
1434
|
-
default: {
|
|
1435
|
-
const unread = action;
|
|
1436
|
-
void unread;
|
|
1437
|
-
return r;
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
if (asked) {
|
|
1442
|
-
const askedUpdated = asked.updatedInput;
|
|
1443
|
-
return {
|
|
1444
|
-
...asked,
|
|
1445
|
-
...(askedUpdated === undefined && current !== input ? { updatedInput: current } : {}),
|
|
1446
|
-
...buildHookContextField(contexts),
|
|
1447
|
-
};
|
|
1448
|
-
}
|
|
1449
|
-
const carried = { ...(current !== input ? { updatedInput: current } : {}), ...buildHookContextField(contexts) };
|
|
1450
|
-
if (allowedFor !== undefined && allowedFor.input === current)
|
|
1451
|
-
return { action: "allow", ...carried };
|
|
1452
|
-
if (current !== input || contexts.length > 0)
|
|
1453
|
-
return carried;
|
|
1454
|
-
return undefined;
|
|
1455
|
-
};
|
|
1456
|
-
}
|
|
1457
|
-
if (d.postToolUse || t.postToolUse) {
|
|
1458
|
-
out.postToolUse = async (toolName, input, output, ctx) => {
|
|
1459
|
-
const rd = d.postToolUse ? await d.postToolUse(toolName, input, output, ctx) : undefined;
|
|
1460
|
-
const rt = t.postToolUse ? await t.postToolUse(toolName, input, output, ctx) : undefined;
|
|
1461
|
-
if (!rd && !rt)
|
|
1462
|
-
return undefined;
|
|
1463
|
-
const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
|
|
1464
|
-
const updatedOutput = rt?.updatedOutput ?? rd?.updatedOutput;
|
|
1465
|
-
return {
|
|
1466
|
-
...(updatedOutput ? { updatedOutput } : {}),
|
|
1467
|
-
...buildHookContextField(contexts),
|
|
1468
|
-
};
|
|
1469
|
-
};
|
|
1470
|
-
}
|
|
1471
|
-
if (d.postToolUseFailure || t.postToolUseFailure) {
|
|
1472
|
-
out.postToolUseFailure = async (toolName, input, failure, ctx) => {
|
|
1473
|
-
const rd = d.postToolUseFailure ? await d.postToolUseFailure(toolName, input, failure, ctx) : undefined;
|
|
1474
|
-
const rt = t.postToolUseFailure ? await t.postToolUseFailure(toolName, input, failure, ctx) : undefined;
|
|
1475
|
-
const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
|
|
1476
|
-
const additionalContext = buildHookContext(contexts);
|
|
1477
|
-
return additionalContext === undefined ? undefined : { additionalContext };
|
|
1478
|
-
};
|
|
1479
1436
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
out.userPromptSubmit = async (prompt, uctx) => {
|
|
1491
|
-
const rd = d.userPromptSubmit ? await d.userPromptSubmit(prompt, uctx) : undefined;
|
|
1492
|
-
if (rd?.block)
|
|
1493
|
-
return { ...rd, ...buildHookContextField(rd.additionalContext ? [rd.additionalContext] : []) };
|
|
1494
|
-
const rt = t.userPromptSubmit ? await t.userPromptSubmit(prompt, uctx) : undefined;
|
|
1495
|
-
if (!rd && !rt)
|
|
1496
|
-
return undefined;
|
|
1497
|
-
const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
|
|
1498
|
-
return { ...(rt?.block ? { block: rt.block } : {}), ...buildHookContextField(contexts) };
|
|
1499
|
-
};
|
|
1500
|
-
}
|
|
1501
|
-
if (d.stop || t.stop) {
|
|
1502
|
-
out.stop = async (sctx) => {
|
|
1503
|
-
const rd = d.stop ? await d.stop(sctx) : undefined;
|
|
1504
|
-
if (rd?.block)
|
|
1505
|
-
return { ...rd, ...buildHookContextField(rd.additionalContext ? [rd.additionalContext] : []) };
|
|
1506
|
-
const rt = t.stop ? await t.stop(sctx) : undefined;
|
|
1507
|
-
if (!rd && !rt)
|
|
1508
|
-
return undefined;
|
|
1509
|
-
const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
|
|
1510
|
-
return { ...(rt?.block ? { block: rt.block } : {}), ...buildHookContextField(contexts) };
|
|
1511
|
-
};
|
|
1512
|
-
}
|
|
1513
|
-
if (d.preCompact || t.preCompact) {
|
|
1514
|
-
out.preCompact = async (pctx) => {
|
|
1515
|
-
const rd = d.preCompact ? await d.preCompact(pctx) : undefined;
|
|
1516
|
-
const rt = t.preCompact ? await t.preCompact(pctx) : undefined;
|
|
1517
|
-
if (!rd && !rt)
|
|
1518
|
-
return undefined;
|
|
1519
|
-
const block = rd?.block ?? rt?.block;
|
|
1520
|
-
const instructions = [rd?.additionalInstructions, rt?.additionalInstructions].filter((c) => typeof c === "string" && c.length > 0);
|
|
1521
|
-
return {
|
|
1522
|
-
...(block !== undefined ? { block } : {}),
|
|
1523
|
-
...(instructions.length ? { additionalInstructions: instructions.join("\n") } : {}),
|
|
1524
|
-
};
|
|
1525
|
-
};
|
|
1526
|
-
}
|
|
1527
|
-
for (const slot of OBSERVE_VOID_HOOK_SLOTS) {
|
|
1528
|
-
const ds = d[slot];
|
|
1529
|
-
const ts = t[slot];
|
|
1530
|
-
if (ds && ts) {
|
|
1531
|
-
out[slot] = async (...args) => {
|
|
1532
|
-
await ds(...args);
|
|
1533
|
-
await ts(...args);
|
|
1534
|
-
};
|
|
1535
|
-
}
|
|
1536
|
-
else if (ds || ts) {
|
|
1537
|
-
out[slot] = (ds ?? ts);
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
return out;
|
|
1437
|
+
const events = Object.keys(translators);
|
|
1438
|
+
return {
|
|
1439
|
+
name: SETTINGS_HOOKS_REGISTRATION,
|
|
1440
|
+
uses: { events: events.flatMap((event) => SETTINGS_HOOK_STATIONS[event]), calls: [] },
|
|
1441
|
+
budgetMs,
|
|
1442
|
+
register(on) {
|
|
1443
|
+
for (const event of events)
|
|
1444
|
+
hangTranslator(on, event, translators);
|
|
1445
|
+
},
|
|
1446
|
+
};
|
|
1541
1447
|
}
|
|
1542
1448
|
//# sourceMappingURL=hook-runner.js.map
|