@sema-agent/core 5.37.0 → 5.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/dist/agents/send-message-tool.d.ts +8 -0
  3. package/dist/agents/send-message-tool.js +8 -0
  4. package/dist/agents/subagent.js +6 -0
  5. package/dist/agents/teacher.js +12 -3
  6. package/dist/agents/team.d.ts +7 -1
  7. package/dist/agents/team.js +11 -9
  8. package/dist/agents/verify.js +12 -3
  9. package/dist/core/auto-mode-prompt-assets.d.ts +5 -3
  10. package/dist/core/auto-mode-prompt-assets.js +1 -1
  11. package/dist/core/checkpoint-store.d.ts +26 -1
  12. package/dist/core/hooks.d.ts +152 -2
  13. package/dist/core/hooks.js +65 -7
  14. package/dist/core/mailbox-store.d.ts +39 -0
  15. package/dist/core/mailbox-store.js +9 -0
  16. package/dist/core/permission-rule-consent.d.ts +27 -4
  17. package/dist/core/permission-rule-consent.js +29 -4
  18. package/dist/core/permission-rule-model.d.ts +7 -1
  19. package/dist/core/runner/prepare-config-doors.d.ts +17 -0
  20. package/dist/core/runner/prepare-config-doors.js +33 -2
  21. package/dist/core/runner/prepare-task.d.ts +17 -2
  22. package/dist/core/runner/prepare-task.js +135 -44
  23. package/dist/core/runner/runtask.js +46 -11
  24. package/dist/core/sensitive-path-policy.js +3 -3
  25. package/dist/core/store-contracts/mailbox-store-contract.d.ts +29 -1
  26. package/dist/core/store-contracts/mailbox-store-contract.js +78 -0
  27. package/dist/core/tool-model-gate.d.ts +125 -0
  28. package/dist/core/tool-model-gate.js +303 -0
  29. package/dist/core/tool-policy.d.ts +1 -1
  30. package/dist/core/types.d.ts +210 -1
  31. package/dist/core/types.js +21 -0
  32. package/dist/core/untrusted-text.d.ts +1 -1
  33. package/dist/core/write-protect.d.ts +93 -0
  34. package/dist/core/write-protect.js +194 -0
  35. package/dist/index.d.ts +7 -5
  36. package/dist/index.js +5 -3
  37. package/dist/orchestration/builtin-workflows.d.ts +68 -6
  38. package/dist/orchestration/builtin-workflows.js +26 -9
  39. package/dist/orchestration/governance-baseline-validity.d.ts +44 -0
  40. package/dist/orchestration/governance-baseline-validity.js +55 -0
  41. package/dist/orchestration/run-workflow-tool.d.ts +10 -1
  42. package/dist/orchestration/run-workflow-tool.js +99 -31
  43. package/dist/orchestration/workflow-script-runner.js +9 -4
  44. package/dist/orchestration/workflow-script-store.d.ts +8 -3
  45. package/dist/prompts/coordinator.d.ts +4 -1
  46. package/dist/prompts/coordinator.js +8 -0
  47. package/dist/prompts/default.d.ts +14 -4
  48. package/dist/prompts/default.js +2 -1
  49. package/dist/scenarios/full-body.d.ts +5 -0
  50. package/dist/scenarios/full-body.js +8 -4
  51. package/dist/tools/fs/fs-shared.d.ts +3 -2
  52. package/dist/tools/fs/fs-shared.js +19 -9
  53. package/dist/tools/fs/read-deny.d.ts +15 -5
  54. package/dist/tools/fs/read-deny.js +33 -12
  55. package/dist/tools/fs/safety.d.ts +4 -1
  56. package/dist/tools/fs/safety.js +4 -2
  57. package/package.json +1 -1
  58. package/test/export-surface.snapshot.json +24 -1
@@ -4,6 +4,7 @@ import { resolveCheckpointStore } from "../checkpoint-store.js";
4
4
  import { preflightLockedConfig } from "../locked-config.js";
5
5
  import { assertRetentionCapability } from "../retention.js";
6
6
  import { resolveModel, resolveTaskModel, roleModelIfSet } from "../roles.js";
7
+ import { applyToolModelGate, assertRestoreGatedToolsValue, modelIdTail } from "../tool-model-gate.js";
7
8
  import { resolveUsageWindows } from "../usage-window-store.js";
8
9
  import { deriveAskEffective, resolveAskSeamForm, resolveQuestionSeam } from "../wiring-manifest.js";
9
10
  const TASK_LIMIT_KEY_DICT = {
@@ -73,7 +74,7 @@ export function resolveTaskLimits(limits) {
73
74
  return limits;
74
75
  }
75
76
  export function isFableFamilyModelId(id) {
76
- const tail = id.toLowerCase().split("/").pop() ?? "";
77
+ const tail = modelIdTail(id);
77
78
  return /^claude-fable-\d/.test(tail) || /^claude-mythos-5(?!\d)/.test(tail);
78
79
  }
79
80
  export function resolveModelPromptTraits(model, spec, internals) {
@@ -85,10 +86,16 @@ export function resolveModelPromptTraits(model, spec, internals) {
85
86
  export function prepareConfigDoors(input) {
86
87
  const { deps, sessions, resume, internals } = input;
87
88
  let spec = input.spec;
89
+ assertRestoreGatedToolsValue(spec.restoreGatedTools);
88
90
  const toolFaceSnapshot = {
89
91
  exclude: spec.excludeTools ? Object.freeze([...spec.excludeTools]) : undefined,
90
92
  defer: spec.deferTools ? Object.freeze([...spec.deferTools]) : undefined,
91
93
  alwaysLoad: spec.alwaysLoadTools ? Object.freeze([...spec.alwaysLoadTools]) : undefined,
94
+ restoreGated: spec.restoreGatedTools === true
95
+ ? true
96
+ : spec.restoreGatedTools !== undefined
97
+ ? Object.freeze([...spec.restoreGatedTools])
98
+ : undefined,
92
99
  };
93
100
  const promptProfile = resolveModelPromptTraits({ id: "" }, spec, internals).promptProfile;
94
101
  if (spec.resumeAt !== undefined) {
@@ -223,11 +230,34 @@ export function prepareConfigDoors(input) {
223
230
  throw e;
224
231
  }
225
232
  const perTask = spec.agents;
226
- spec = { ...spec, tools: pool.map((t) => (typeof t.withAgents === "function" ? t.withAgents(perTask) : t)) };
233
+ spec = {
234
+ ...spec,
235
+ tools: pool.map((t) => {
236
+ if (typeof t.withAgents !== "function")
237
+ return t;
238
+ const rebuilt = t.withAgents(perTask);
239
+ return t.modelGate !== undefined && rebuilt.modelGate === undefined ? { ...rebuilt, modelGate: t.modelGate } : rebuilt;
240
+ }),
241
+ };
227
242
  }
228
243
  const resolvedRole = resolveTaskModel(spec, deps);
229
244
  const model = resolvedRole.model;
230
245
  const fableMitigations = resolveModelPromptTraits(model, spec, internals).fableMitigations;
246
+ const gateDecision = applyToolModelGate({
247
+ tools: spec.tools,
248
+ modelId: model.id,
249
+ depsSeat: deps.toolModelGate,
250
+ restoreGated: toolFaceSnapshot.restoreGated,
251
+ envRaw: process.env.SEMA_TOOL_MODEL_GATE,
252
+ });
253
+ if (gateDecision.survivors !== undefined) {
254
+ spec = { ...spec, tools: gateDecision.survivors };
255
+ }
256
+ const modelGate = Object.freeze({
257
+ removedByClass: gateDecision.removedByClass,
258
+ unknownClasses: gateDecision.unknownClasses,
259
+ discardedEnvRaw: gateDecision.discardedEnvRaw,
260
+ });
231
261
  const thinking = spec.thinking ?? resolvedRole.thinking ?? model.defaultThinking;
232
262
  const compModel = spec.compactionModel
233
263
  ? resolveModel(spec.compactionModel, deps.models)
@@ -243,6 +273,7 @@ export function prepareConfigDoors(input) {
243
273
  thinking,
244
274
  compModel,
245
275
  fableMitigations,
276
+ modelGate,
246
277
  usageWindows,
247
278
  brainCallGuardrailRef,
248
279
  brainCallGuardrailMs,
@@ -11,7 +11,7 @@ import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
11
11
  import type { OnAsk, ToolCallRequest, ToolPolicy } from "../tool-policy.js";
12
12
  import { type ActiveSkillFrame } from "./active-skill-scope.js";
13
13
  import type { SessionPermissionRules } from "../session-policy-store.js";
14
- import { type Hooks, type OrgGateVerdict } from "../hooks.js";
14
+ import { type Hooks, type HookInvocationIdentity, type OrgGateVerdict } from "../hooks.js";
15
15
  import type { RecoveredOrphan } from "../session-reconcile.js";
16
16
  import { CacheBreakDetector, type ToolFingerprintInput } from "../cache-break-detector.js";
17
17
  import { type BrainCallGuardrailRef } from "../../brain/timeout.js";
@@ -29,8 +29,14 @@ import { type WiringManifest } from "../wiring-manifest.js";
29
29
  import type { ActiveWorktreeSession, AgentMessage, AgentTool, ExecutionEnv } from "../../internal/harness.js";
30
30
  import type { NestedUsageAccum, RunnerDeps, TaskEvent, TaskResult, TaskSpec, ToolActivity, ToolEffect } from "../types.js";
31
31
  import type { RepairBundle } from "../../agents/repair-loop.js";
32
- /** Test seam (mirrors `__resetBashTimeoutAnnouncements`): never called by production code. */
32
+ /** Test seam (mirrors `__resetToolModelGateAnnouncements`): never called by production code.
33
+ * Re-arms BOTH arms (a WeakMap has no clear — it is re-minted). */
33
34
  export declare function __resetMaterializeEnvAnnouncements(): void;
35
+ /** Test seam (mirrors `__resetMaterializeEnvAnnouncements`): never called by production code.
36
+ * Re-arms BOTH arms (a WeakMap has no clear — it is re-minted). Deliberately UNLIKE the read-face
37
+ * seam below (console latch only): tests here legitimately reuse ONE sink across prepares to pin
38
+ * the per-sink dedup itself, so the seam must be able to re-arm a still-referenced sink. */
39
+ export declare function __resetToolModelGateAnnouncements(): void;
34
40
  /** Test seam (mirrors `__resetMalformedNoticeSeatAnnouncement`): never called by production code.
35
41
  * Deliberately asymmetric — it resets only the console latch: the WeakSet arm needs no seam
36
42
  * because a test resets it by minting a fresh sink function (identity IS the ledger key), while
@@ -279,6 +285,15 @@ export interface Prepared {
279
285
  * model/tool interaction. Host/operator plane — never enters model context.
280
286
  */
281
287
  wiringManifest: WiringManifest;
288
+ /**
289
+ * #281 件A — this leg's frozen identity envelope, minted ONCE in prepare beside the wiring
290
+ * manifest (same leg derivation, one mint — {@link mintHookInvocationIdentity}). Every hook
291
+ * station runtask drives (stop/stopFailure/userPromptSubmit/postToolBatch, the compaction
292
+ * wrapper) and the 件B delegation-lifecycle observer read THIS object; prepare's own stations
293
+ * (the tool gate, the post-tool contexts) close over the same const. Always present — a prepared
294
+ * leg always knows its identity.
295
+ */
296
+ hookIdentity: HookInvocationIdentity;
282
297
  promptManifest: {
283
298
  constitution: "core" | "replaced" | "provider-assembled";
284
299
  blocks: Array<{
@@ -28,8 +28,9 @@ import { CHANGED_FILES_MTIME_EPS_MS, fenceMcpServerInstructions, renderAgentList
28
28
  import { inlineUntrusted } from "../untrusted-text.js";
29
29
  import { policyAskClassOf } from "../ask-class.js";
30
30
  import { emitTrace } from "../trace.js";
31
- import { createSessionRulePolicy } from "./session-rule-policy.js";
32
- import { cloneObserverInput, createHookEnvCapabilities, createPreToolUseConstraintPolicy, formatHookFeedback, persistedRuleMandateOf, runToolGate } from "../hooks.js";
31
+ import { createSessionRulePolicy, PATH_CONFINABLE_WRITE_TOOLS } from "./session-rule-policy.js";
32
+ import { cloneObserverInput, createHookEnvCapabilities, createPreToolUseConstraintPolicy, formatHookFeedback, mintHookInvocationIdentity, persistedRuleMandateOf, runToolGate } from "../hooks.js";
33
+ import { createWriteProtectionCheck } from "../write-protect.js";
33
34
  import { orgRuleVerdictFor } from "../permission-rule-org.js";
34
35
  import { CacheBreakDetector, toolsToFingerprintInputs } from "../cache-break-detector.js";
35
36
  import { reservedCollisions, reservedFor } from "../../brain/request-params.js";
@@ -92,13 +93,104 @@ import { countElicitOptIns, deriveWiringManifest, resolveAskSeamForm, resolveDec
92
93
  import { durableParkGapFor } from "../park-selfcheck.js";
93
94
  import { GLOBAL_USAGE_KEY, usageRetryAfterMs } from "../usage-window-store.js";
94
95
  import { deliverEngineNotice } from "../types.js";
95
- const announcedMaterializeEnv = new Set();
96
+ let announcedMaterializeEnvBySink = new WeakMap();
97
+ const announcedMaterializeEnvConsole = new Set();
98
+ function materializeEnvLedger(onNotice) {
99
+ if (typeof onNotice !== "function")
100
+ return announcedMaterializeEnvConsole;
101
+ let lines = announcedMaterializeEnvBySink.get(onNotice);
102
+ if (lines === undefined) {
103
+ lines = new Set();
104
+ announcedMaterializeEnvBySink.set(onNotice, lines);
105
+ }
106
+ return lines;
107
+ }
96
108
  export function __resetMaterializeEnvAnnouncements() {
97
- announcedMaterializeEnv.clear();
109
+ announcedMaterializeEnvBySink = new WeakMap();
110
+ announcedMaterializeEnvConsole.clear();
98
111
  }
99
112
  function emitMaterializeEnvNotice(onNotice, message, detail) {
113
+ const ledger = materializeEnvLedger(onNotice);
114
+ if (ledger.has(message))
115
+ return;
116
+ ledger.add(message);
100
117
  deliverEngineNotice(onNotice, { code: "config.materialize_env_discarded", message, detail });
101
118
  }
119
+ function warnCompactionWindowHazard(tracer, spec, model, compModel, hostTaskId) {
120
+ if (compModel === undefined)
121
+ return;
122
+ const compWindow = compModel.contextTokens ?? compModel.contextWindow;
123
+ const mainWindow = model.autoCompactTokens ?? model.contextTokens ?? model.contextWindow;
124
+ if (compWindow > 0 && mainWindow > 0 && compWindow < mainWindow) {
125
+ const merged = { ...DEFAULT_COMPACTION_SETTINGS, ...spec.compaction };
126
+ const sanitized = sanitizeCompactionSettings(merged, mainWindow);
127
+ const tolerance = sanitized.clampTolerance ?? DEFAULT_CLAMP_TOLERANCE;
128
+ const headroom = Math.max(0, compWindow - Math.max(Math.floor(0.8 * summaryOutputBudgetTokens(compModel, sanitized)), 2048) - 512);
129
+ emitTrace(tracer, () => ({
130
+ kind: "compaction.window_config_warning",
131
+ version: 1,
132
+ taskId: hostTaskId,
133
+ compactionModelWindow: compWindow,
134
+ mainModelWindow: mainWindow,
135
+ ...(tolerance < 1 ? { fallbackAt: Math.floor(headroom / (1 - tolerance)) } : {}),
136
+ ts: Date.now(),
137
+ }));
138
+ }
139
+ }
140
+ let announcedToolModelGateBySink = new WeakMap();
141
+ const announcedToolModelGateConsole = new Set();
142
+ function toolModelGateLedger(onNotice) {
143
+ if (typeof onNotice !== "function")
144
+ return announcedToolModelGateConsole;
145
+ let lines = announcedToolModelGateBySink.get(onNotice);
146
+ if (lines === undefined) {
147
+ lines = new Set();
148
+ announcedToolModelGateBySink.set(onNotice, lines);
149
+ }
150
+ return lines;
151
+ }
152
+ export function __resetToolModelGateAnnouncements() {
153
+ announcedToolModelGateBySink = new WeakMap();
154
+ announcedToolModelGateConsole.clear();
155
+ }
156
+ function announceToolModelGate(onNotice, modelId, gate) {
157
+ const ledger = toolModelGateLedger(onNotice);
158
+ for (const [gateClass, removed] of gate.removedByClass) {
159
+ const line = `Model gate: default-mounted tool(s) ${removed.map((n) => JSON.stringify(n)).join(", ")} (class ${JSON.stringify(gateClass)}) ` +
160
+ `were not mounted for model ${JSON.stringify(modelId)} — the gate table marks this model as managing multi-step work without the scaffold. ` +
161
+ `Explicitly composed tools are exempt; restore via TaskSpec.restoreGatedTools, SEMA_TOOL_MODEL_GATE=off, or RunnerDeps.toolModelGate: false.`;
162
+ if (!ledger.has(line)) {
163
+ ledger.add(line);
164
+ deliverEngineNotice(onNotice, {
165
+ code: "config.tool_model_gate_removed",
166
+ message: line,
167
+ detail: {
168
+ modelId,
169
+ gateClass,
170
+ removed: [...removed],
171
+ restore: { spec: "TaskSpec.restoreGatedTools", env: "SEMA_TOOL_MODEL_GATE=off", deps: "RunnerDeps.toolModelGate: false" },
172
+ },
173
+ });
174
+ }
175
+ }
176
+ for (const gateClass of gate.unknownClasses) {
177
+ const line = `ToolSpec.modelGate names gate class ${JSON.stringify(gateClass)}, which no row of the merged gate table defines — the tag is inert ` +
178
+ `(fail-open: the tool stays mounted). Fix the tag, or define the class via RunnerDeps.toolModelGate.classes.`;
179
+ if (!ledger.has(line)) {
180
+ ledger.add(line);
181
+ deliverEngineNotice(onNotice, { code: "config.tool_model_gate_unknown_class", message: line, detail: { gateClass } });
182
+ }
183
+ }
184
+ if (gate.discardedEnvRaw !== undefined) {
185
+ const line = `SEMA_TOOL_MODEL_GATE=${JSON.stringify(gate.discardedEnvRaw)} is not in the closed set (on|1|true|off|0|false, case-insensitive) — ` +
186
+ `not in force on this task (nothing the model gate would remove), but a task where the gate WOULD trim a default-mounted tool ` +
187
+ `will refuse to prepare under it (config.tool_model_gate_env_invalid). Fix or unset the flag.`;
188
+ if (!ledger.has(line)) {
189
+ ledger.add(line);
190
+ deliverEngineNotice(onNotice, { code: "config.tool_model_gate_env_invalid", message: line, detail: { raw: gate.discardedEnvRaw } });
191
+ }
192
+ }
193
+ }
102
194
  const readFaceClampAnnouncedSinks = new WeakSet();
103
195
  let readFaceClampConsoleAnnounced = false;
104
196
  export function __resetReadFaceClampAnnouncement() {
@@ -153,8 +245,9 @@ export { rebaseWorkspacePath, rebaseWorkspacePathAcross } from "./prepare-worksp
153
245
  function hasConversationContent(branch) {
154
246
  return branch.some((e) => e.type === "message" || e.type === "custom_message" || e.type === "compaction");
155
247
  }
156
- function isDelegatedNonForkChild(internals) {
157
- return internals?.isDelegatedChild === true && internals?.insideFork !== true;
248
+ function effectiveDelegationFacts(internals, seedIsDelegatedChild) {
249
+ const isDelegatedChild = internals?.isDelegatedChild !== undefined ? internals.isDelegatedChild === true : seedIsDelegatedChild === true;
250
+ return { isDelegatedChild, isNonForkChild: isDelegatedChild && internals?.insideFork !== true };
158
251
  }
159
252
  export function batchContextAt(messages, currentId) {
160
253
  let batch = [];
@@ -238,6 +331,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
238
331
  const doors = prepareConfigDoors({ spec, deps, sessions, resume, internals });
239
332
  spec = doors.spec;
240
333
  const { toolFaceSnapshot, promptProfile, lockedPreflight, resolvedInteractionPosture, resolvedRole, model, thinking, compModel, fableMitigations, usageWindows, brainCallGuardrailRef, brainCallGuardrailMs } = doors;
334
+ announceToolModelGate(deps.onNotice, model.id, doors.modelGate);
241
335
  const { toolEffects, egressTools, irreversibleTools, irreversibilityTier, axisExplicitNegatives, reversibilityProbes, ownToolNames } = prepareSafetyScan({ spec, deps });
242
336
  let shellGatedBash = false;
243
337
  let shellGatedMonitor = false;
@@ -249,25 +343,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
249
343
  const { acquired, session, conflictRef, wakeRecovered, resumeAtBeforeParentId } = await prepareAcquireReconcile({ sessions, spec, resume, toolEffects, ...(() => { const g = durableParkGapFor(deps, spec); return g !== undefined ? { durableParkGap: g } : {}; })() });
250
344
  const sessionId = acquired.sessionId;
251
345
  const hostTaskId = spec.taskId ?? sessionId;
252
- if (compModel !== undefined) {
253
- const compWindow = compModel.contextTokens ?? compModel.contextWindow;
254
- const mainWindow = model.autoCompactTokens ?? model.contextTokens ?? model.contextWindow;
255
- if (compWindow > 0 && mainWindow > 0 && compWindow < mainWindow) {
256
- const merged = { ...DEFAULT_COMPACTION_SETTINGS, ...spec.compaction };
257
- const sanitized = sanitizeCompactionSettings(merged, mainWindow);
258
- const tolerance = sanitized.clampTolerance ?? DEFAULT_CLAMP_TOLERANCE;
259
- const headroom = Math.max(0, compWindow - Math.max(Math.floor(0.8 * summaryOutputBudgetTokens(compModel, sanitized)), 2048) - 512);
260
- emitTrace(deps.tracer, () => ({
261
- kind: "compaction.window_config_warning",
262
- version: 1,
263
- taskId: hostTaskId,
264
- compactionModelWindow: compWindow,
265
- mainModelWindow: mainWindow,
266
- ...(tolerance < 1 ? { fallbackAt: Math.floor(headroom / (1 - tolerance)) } : {}),
267
- ts: Date.now(),
268
- }));
269
- }
270
- }
346
+ const delegation = effectiveDelegationFacts(internals, resume?.seed.isDelegatedChild);
347
+ warnCompactionWindowHazard(deps.tracer, spec, model, compModel, hostTaskId);
271
348
  const taskScope = internals?.registryScope ?? spec.principal ?? "default";
272
349
  internals?.peerSelfRef?.addAxis("s", sessionId);
273
350
  internals?.peerSelfRef?.addAxis("t", hostTaskId);
@@ -705,6 +782,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
705
782
  excludeTools: toolFaceSnapshot.exclude,
706
783
  deferTools: toolFaceSnapshot.defer,
707
784
  alwaysLoadTools: toolFaceSnapshot.alwaysLoad,
785
+ ...(toolFaceSnapshot.restoreGated !== undefined ? { restoreGatedTools: toolFaceSnapshot.restoreGated } : {}),
708
786
  promptProfile,
709
787
  ...(spec.additionalDirectories !== undefined ? { additionalDirectories: Object.freeze([...spec.additionalDirectories]) } : {}),
710
788
  ...(spec.additionalReadDirectories !== undefined ? { additionalReadDirectories: Object.freeze([...spec.additionalReadDirectories]) } : {}),
@@ -876,6 +954,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
876
954
  parentExcludeTools: toolFaceSnapshot.exclude,
877
955
  parentDeferTools: toolFaceSnapshot.defer,
878
956
  parentAlwaysLoadTools: toolFaceSnapshot.alwaysLoad,
957
+ ...(toolFaceSnapshot.restoreGated !== undefined ? { parentRestoreGatedTools: toolFaceSnapshot.restoreGated } : {}),
879
958
  parentPromptProfile: promptProfile,
880
959
  models: deps.models,
881
960
  agents: deps.agents,
@@ -1335,6 +1414,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1335
1414
  toolEffects.set("TaskStop", "write");
1336
1415
  tools.push(firstPartyOffload(createTaskOutputTool({ registry: defaultTaskRegistry, owner: hostTaskId, scope: taskScope, sessionId, workflowStore: deps.workflowRunStore, agentStore: deps.backgroundAgentStore, notificationWired: internals?.onTaskNotification !== undefined, oneShot: spec.oneShot, toolResultStore: offloadStore })), firstPartyOffload(createTaskStopTool({ registry: defaultTaskRegistry, owner: hostTaskId, scope: taskScope, sessionId, workflowStore: deps.workflowRunStore, agentStore: deps.backgroundAgentStore })));
1337
1416
  if (runnerSelf && !(spec.tools ?? []).some((t) => t.name === SEND_MESSAGE_TOOL_NAME)) {
1417
+ toolEffects.set(SEND_MESSAGE_TOOL_NAME, "write");
1418
+ axisExplicitNegatives.set(SEND_MESSAGE_TOOL_NAME, { ...axisExplicitNegatives.get(SEND_MESSAGE_TOOL_NAME), egress: false });
1338
1419
  const delegationForRevive = (spec.tools ?? []).find((t) => t.agentListing !== undefined);
1339
1420
  const reviveSpawn = delegationForRevive !== undefined && deps.backgroundAgentStore !== undefined && deps.mailboxStore !== undefined
1340
1421
  ? async (req) => {
@@ -1581,7 +1662,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1581
1662
  orgMemoryDenied: complianceDenies.has("org_memory_mount"),
1582
1663
  complianceDegraded,
1583
1664
  parentAdmittedOrgScopes: foldAdmissionFreeze({
1584
- delegated: internals?.isDelegatedChild === true ||
1665
+ delegated: delegation.isDelegatedChild ||
1585
1666
  internals?.inheritedGate !== undefined ||
1586
1667
  (resume?.seed.inheritedGate !== undefined &&
1587
1668
  (resume.seed.inheritedGate.ancestorRules !== undefined ||
@@ -1650,7 +1731,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1650
1731
  loaded = await Promise.resolve(deps.loadProjectMemory({
1651
1732
  cwd: taskRootFinal,
1652
1733
  handsEnabled,
1653
- isSubagent: isDelegatedNonForkChild(internals),
1734
+ isSubagent: delegation.isNonForkChild,
1654
1735
  ...(internals?.agentName ? { agentName: internals.agentName } : {}),
1655
1736
  sessionId,
1656
1737
  phase: projectMemoryPhase,
@@ -1775,7 +1856,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1775
1856
  awarenessEnabled: thinking !== undefined && ULTRA_REASONING_TIERS.has(thinking),
1776
1857
  worktreeIsolated: internals?.isolation === "worktree" && ownedEnv !== undefined,
1777
1858
  withinTaskCompactionEnabled: (spec.compaction?.enabled ?? true) && (spec.compaction?.withinTask ?? true),
1778
- isSubagent: isDelegatedNonForkChild(internals),
1859
+ isSubagent: delegation.isNonForkChild,
1779
1860
  };
1780
1861
  const userSystemPrompt = spec.systemPrompt ?? resolvedRole.systemPrompt ?? internals?.defaultSystemPrompt;
1781
1862
  const userAppendSystemPrompt = spec.appendSystemPrompt;
@@ -2209,10 +2290,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2209
2290
  const raw = process.env.SEMA_TOOL_MATERIALIZE_STRATEGY;
2210
2291
  if (raw !== undefined && raw !== "swap" && raw !== "static" && deferred.size === 0) {
2211
2292
  const line = `SEMA_TOOL_MATERIALIZE_STRATEGY=${JSON.stringify(raw)} is not "swap" or "static" — inert on this task (no deferred tools), but a deferring task WITHOUT an explicit spec strategy will refuse to prepare under it (an explicit legal spec outranks and discards it, loudly). Fix or unset the flag.`;
2212
- if (!announcedMaterializeEnv.has(line)) {
2213
- announcedMaterializeEnv.add(line);
2214
- emitMaterializeEnvNotice(deps.onNotice, line, { raw });
2215
- }
2293
+ emitMaterializeEnvNotice(deps.onNotice, line, { raw });
2216
2294
  }
2217
2295
  }
2218
2296
  if (deferred.size > 0) {
@@ -2231,10 +2309,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2231
2309
  }
2232
2310
  if (envStrategyInvalid) {
2233
2311
  const line = `SEMA_TOOL_MATERIALIZE_STRATEGY=${JSON.stringify(rawEnvStrategy)} was ignored — not "swap" or "static", and the task spec pins toolMaterializeStrategy=${JSON.stringify(spec.toolMaterializeStrategy)} which outranks it. Fix or unset the env flag.`;
2234
- if (!announcedMaterializeEnv.has(line)) {
2235
- announcedMaterializeEnv.add(line);
2236
- emitMaterializeEnvNotice(deps.onNotice, line, { raw: rawEnvStrategy, specStrategy: spec.toolMaterializeStrategy });
2237
- }
2312
+ emitMaterializeEnvNotice(deps.onNotice, line, { raw: rawEnvStrategy, specStrategy: spec.toolMaterializeStrategy });
2238
2313
  }
2239
2314
  const envStrategy = envStrategyInvalid ? undefined : rawEnvStrategy;
2240
2315
  const requestedStrategy = spec.toolMaterializeStrategy ?? envStrategy ?? "swap";
@@ -2741,7 +2816,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2741
2816
  list.push({ cls, layer });
2742
2817
  foldAskClasses.set(toolCallId, list);
2743
2818
  };
2744
- const sandboxBoundaryCapable = (toolName) => egressTools.has(toolName) || toolName.includes("__") || ownToolNames.has(toolName);
2819
+ const sandboxBoundaryCapable = (toolName) => egressTools.has(toolName) || toolName.includes("__") || ownToolNames.has(toolName) || toolName === SEND_MESSAGE_TOOL_NAME;
2745
2820
  const emitSandboxAdmitted = (info) => {
2746
2821
  emitTrace(deps.tracer, () => ({
2747
2822
  kind: "permission.sandbox_admitted",
@@ -2785,7 +2860,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2785
2860
  ...((internals?.explicitAgentName ?? internals?.agentName) !== undefined
2786
2861
  ? { sourceAgentName: internals?.explicitAgentName ?? internals?.agentName }
2787
2862
  : {}),
2788
- ...(internals?.isDelegatedChild === true ? { isDelegatedChild: true } : {}),
2863
+ ...(delegation.isDelegatedChild ? { isDelegatedChild: true } : {}),
2789
2864
  });
2790
2865
  const riskAxesOf = (toolName) => {
2791
2866
  const tier = irreversibilityTier.get(toolName);
@@ -3347,6 +3422,15 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3347
3422
  retentionPolicyWired: deps.retentionPolicy !== undefined,
3348
3423
  });
3349
3424
  const parkLaneArmed = wiringManifest.parkLane.effective === true;
3425
+ const hookIdentity = mintHookInvocationIdentity({
3426
+ sessionId,
3427
+ taskId: spec.taskId ?? sessionId,
3428
+ legKind: wiringManifest.leg.kind,
3429
+ isDelegatedChild: delegation.isDelegatedChild,
3430
+ ...(internals?.insideFork === true ? { insideFork: true } : {}),
3431
+ ...(internals?.agentName !== undefined ? { agentName: internals.agentName } : {}),
3432
+ ...(internals?.parentToolCallId !== undefined ? { parentToolCallId: internals.parentToolCallId } : {}),
3433
+ });
3350
3434
  const hookContextConsumerWired = hooks?.preToolUse !== undefined || hooks?.postToolUse !== undefined || hooks?.postToolUseFailure !== undefined;
3351
3435
  const hookEnvFace = hookContextConsumerWired && (ownedEnv ?? deps.executionEnv) != null ? createHookEnvCapabilities(executionEnv) : undefined;
3352
3436
  if (effectivePolicy || hooks?.preToolUse || egressTools.size > 0 || irreversibleTools.size > 0 || resourceSuspendEligible || platformSuspendArmed || spec.enablePlanMode === true) {
@@ -3556,6 +3640,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3556
3640
  : undefined,
3557
3641
  gitAnnouncement: gitStatusRef.announced !== undefined ? { ...gitStatusRef.announced } : undefined,
3558
3642
  delegationProvenance: internals?.delegationProvenance !== undefined ? { ...internals.delegationProvenance.ref.current } : undefined,
3643
+ isDelegatedChild: hookIdentity.isDelegatedChild ? true : undefined,
3559
3644
  });
3560
3645
  const commitSuspendSaga = async (token, cp, remoteEnv, remoteHandle) => {
3561
3646
  if (!checkpointStore)
@@ -4138,13 +4223,16 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4138
4223
  if (eff !== undefined && !toolEffects.has(t.name))
4139
4224
  toolEffects.set(t.name, eff);
4140
4225
  }
4226
+ const writeProtectionCheck = createWriteProtectionCheck(deps.writeProtectedPaths);
4227
+ const writeProtectionArmed = writeProtectionCheck !== undefined && tools.some((t) => PATH_CONFINABLE_WRITE_TOOLS.has(t.name));
4141
4228
  toolCallGateArmedRef.armed =
4142
4229
  effectivePolicy !== undefined ||
4143
4230
  hooks?.preToolUse !== undefined ||
4144
4231
  egressTools.size > 0 ||
4145
4232
  irreversibleTools.size > 0 ||
4146
4233
  spec.enablePlanMode === true ||
4147
- complianceDenies.has("web_fetch");
4234
+ complianceDenies.has("web_fetch") ||
4235
+ writeProtectionArmed;
4148
4236
  if (toolCallGateArmedRef.armed) {
4149
4237
  harness.on("tool_call", async (e) => {
4150
4238
  blockedToolCalls.delete(e.toolCallId);
@@ -4155,7 +4243,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4155
4243
  if (blockedTracked)
4156
4244
  blockedToolCalls.add(e.toolCallId);
4157
4245
  if (notifyPermissionDenied) {
4158
- await notifyPermissionDenied({ toolName: e.toolName, input: cloneObserverInput(e.input), toolCallId: e.toolCallId, reason: complianceDeny, source: "safety" });
4246
+ await notifyPermissionDenied({ toolName: e.toolName, input: cloneObserverInput(e.input), toolCallId: e.toolCallId, reason: complianceDeny, source: "safety", identity: hookIdentity });
4159
4247
  }
4160
4248
  return { block: true, reason: formatHookFeedback(complianceDeny), preToolContext: [] };
4161
4249
  }
@@ -4166,7 +4254,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4166
4254
  const planDenyReason = `Plan mode is active — "${e.toolName}" is a write/mutating tool and is read-only-blocked. ` +
4167
4255
  `Research with read-only tools, then call ${PRESENT_PLAN_TOOL_NAME} with your plan to get it approved before acting.`;
4168
4256
  if (notifyPermissionDenied) {
4169
- await notifyPermissionDenied({ toolName: e.toolName, input: cloneObserverInput(e.input), toolCallId: e.toolCallId, reason: planDenyReason, source: "planMode" });
4257
+ await notifyPermissionDenied({ toolName: e.toolName, input: cloneObserverInput(e.input), toolCallId: e.toolCallId, reason: planDenyReason, source: "planMode", identity: hookIdentity });
4170
4258
  }
4171
4259
  return {
4172
4260
  block: true,
@@ -4179,6 +4267,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4179
4267
  result = await runToolGate({
4180
4268
  onNotifyError: (f) => emitTrace(deps.tracer, () => ({ kind: "observer.notify_failed", version: 1, taskId: spec.taskId ?? sessionId, site: f.site, message: f.error.message, ts: Date.now() })),
4181
4269
  event: e,
4270
+ identity: hookIdentity,
4182
4271
  preToolUse: ownGatePreToolUse,
4183
4272
  ...(hookEnvFace !== undefined ? { hookEnv: hookEnvFace } : {}),
4184
4273
  adjudicate,
@@ -4186,6 +4275,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4186
4275
  suspendAsk,
4187
4276
  resolveContentAsk,
4188
4277
  egress: egressTools.has(e.toolName),
4278
+ peerMessage: e.toolName === SEND_MESSAGE_TOOL_NAME,
4279
+ ...(writeProtectionCheck !== undefined ? { writeProtectionCheck } : {}),
4189
4280
  irreversibility: irreversibilityTier.get(e.toolName),
4190
4281
  reversibilityProbe: reversibilityProbes.get(e.toolName),
4191
4282
  abortSignal: abortController.signal,
@@ -4291,7 +4382,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4291
4382
  isInterrupt: abortController.signal.aborted || spec.signal?.aborted === true,
4292
4383
  content: e.content.map((c) => ({ ...c })),
4293
4384
  details: clonedDetails,
4294
- }, { toolCallId: e.toolCallId, toolName: e.toolName, ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}) });
4385
+ }, { toolCallId: e.toolCallId, toolName: e.toolName, ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity });
4295
4386
  if (patch?.additionalContext) {
4296
4387
  content = [...content, { type: "text", text: formatHookFeedback(patch.additionalContext) }];
4297
4388
  changed = true;
@@ -4299,7 +4390,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4299
4390
  }
4300
4391
  }
4301
4392
  else if (hooks?.postToolUse) {
4302
- const patch = await hooks.postToolUse(e.toolName, e.input, { content: e.content, details: e.details, isError: e.isError }, { toolCallId: e.toolCallId, toolName: e.toolName, ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}) });
4393
+ const patch = await hooks.postToolUse(e.toolName, e.input, { content: e.content, details: e.details, isError: e.isError }, { toolCallId: e.toolCallId, toolName: e.toolName, ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity });
4303
4394
  if (patch?.updatedOutput) {
4304
4395
  content = patch.updatedOutput;
4305
4396
  changed = true;
@@ -4586,7 +4677,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4586
4677
  const effectiveReadFaceObserved = carrierReadFace();
4587
4678
  const effectiveReadDenyObserved = readDenyAdditionsNormalized.length > 0 ? readDenyAdditionsNormalized.map((e) => ({ ...e })) : undefined;
4588
4679
  const preparedHolder = {};
4589
- const buildPrepared = () => ({ harness, session, sessionId, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettlement, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
4680
+ const buildPrepared = () => ({ harness, session, sessionId, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettlement, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, hookIdentity, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
4590
4681
  const prepared = buildPrepared();
4591
4682
  preparedHolder.current = prepared;
4592
4683
  return prepared;