@sema-agent/core 2.8.0 → 2.9.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 (35) hide show
  1. package/dist/agents/send-message-tool.js +37 -29
  2. package/dist/agents/subagent.js +15 -3
  3. package/dist/brain/circuit-breaker.js +18 -8
  4. package/dist/brain/retry.d.ts +1 -0
  5. package/dist/brain/retry.js +29 -7
  6. package/dist/brain/stream-engine.d.ts +1 -0
  7. package/dist/brain/stream-engine.js +74 -12
  8. package/dist/core/auto-compaction.js +9 -1
  9. package/dist/core/background-agent-store.d.ts +2 -0
  10. package/dist/core/background-agent-store.js +20 -0
  11. package/dist/core/mcp.js +8 -5
  12. package/dist/core/runner/prepare-task.js +24 -8
  13. package/dist/core/runner/runtask.js +20 -7
  14. package/dist/core/runner/tool-disclosure.d.ts +8 -3
  15. package/dist/core/runner/tool-disclosure.js +22 -8
  16. package/dist/core/skills-directory.d.ts +1 -1
  17. package/dist/core/skills-directory.js +257 -28
  18. package/dist/core/task-registry-agent.d.ts +2 -1
  19. package/dist/core/task-registry-agent.js +47 -54
  20. package/dist/core/task-registry.d.ts +1 -0
  21. package/dist/core/task-registry.js +1 -1
  22. package/dist/core/types.d.ts +5 -1
  23. package/dist/engine/compaction/compaction.js +71 -20
  24. package/dist/internal/harness-types.d.ts +1 -1
  25. package/dist/internal/harness.d.ts +1 -1
  26. package/dist/internal/harness.js +1 -1
  27. package/dist/orchestration/run-workflow-tool.d.ts +1 -0
  28. package/dist/orchestration/run-workflow-tool.js +4 -1
  29. package/dist/orchestration/workflow.d.ts +1 -1
  30. package/dist/orchestration/workflow.js +2 -2
  31. package/dist/tools/fs/bash-readonly-classifier.js +83 -17
  32. package/dist/tools/fs/fs-bash.js +17 -11
  33. package/dist/tools/fs/fs-shared.d.ts +1 -0
  34. package/dist/tools/fs/fs-shared.js +44 -2
  35. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { uuidv7 } from "../internal/harness.js";
3
- import { canAccessAgentRecord, BackgroundAgentStoreError, } from "./background-agent-store.js";
3
+ import { canAccessAgentRecord, BackgroundAgentStoreError, REVIVED_ROW_CLEARED_FIELDS, } from "./background-agent-store.js";
4
4
  import { shutdownDebug } from "./shutdown-debug.js";
5
5
  import { delimitUntrusted } from "./untrusted-text.js";
6
6
  import { boundedRedactedSummary } from "./untrusted-egress.js";
@@ -652,6 +652,8 @@ export function settleBackgroundAgentLane(core, id, outcome) {
652
652
  const handle = core.handles.get(id);
653
653
  if (!handle || handle.type !== "background_agent")
654
654
  return undefined;
655
+ if ((outcome.cycle ?? 0) !== (handle.reviveCycle ?? 0))
656
+ return undefined;
655
657
  if (handle.status !== "running") {
656
658
  if (handle.status === "killed") {
657
659
  mintCompletionId(handle);
@@ -859,31 +861,11 @@ export function reviveBackgroundAgentLane(core, id, access, abort) {
859
861
  handle.reviveCycle = (handle.reviveCycle ?? 0) + 1;
860
862
  handle.cycleSeq = (handle.cycleSeq ?? 1) + 1;
861
863
  handle.updatedAt = Date.now();
862
- durableAgentWriteLane(handle, { status: "running" }, [
863
- "settledAt",
864
- "stoppedBy",
865
- "finalOutput",
866
- "finalOutputFull",
867
- "error",
868
- "errorCode",
869
- "errorRetryable",
870
- "errorKind",
871
- "resultIsPartial",
872
- "completionId",
873
- "summary",
874
- "recentSteps",
875
- "editedFiles",
876
- "usage",
877
- ]);
864
+ durableAgentWriteLane(handle, { status: "running" }, REVIVED_ROW_CLEARED_FIELDS);
878
865
  return { ok: true, cycle: handle.reviveCycle };
879
866
  }
880
867
  export function settleRevivedAgentLane(core, id, cycle, outcome) {
881
- const handle = core.handles.get(id);
882
- if (!handle || handle.type !== "background_agent")
883
- return undefined;
884
- if ((handle.reviveCycle ?? 0) !== cycle)
885
- return undefined;
886
- return settleBackgroundAgentLane(core, id, outcome);
868
+ return settleBackgroundAgentLane(core, id, { ...outcome, cycle });
887
869
  }
888
870
  export function unmarkRetainedContinuationLane(core, id) {
889
871
  const handle = core.handles.get(id);
@@ -1023,18 +1005,33 @@ export function notFoundRunningAgentsTail(footer) {
1023
1005
  return ((footer.named.length > 0 ? `. Running named agents: ${footer.named.join(", ")}` : "") +
1024
1006
  (footer.background.length > 0 ? `. Running background agents: ${footer.background.join(", ")}` : ""));
1025
1007
  }
1008
+ function buildAgentPollDetails(input) {
1009
+ const failed = input.status === "failed";
1010
+ return {
1011
+ task_id: input.taskId,
1012
+ type: "background_agent",
1013
+ status: input.status,
1014
+ retrieval_status: input.retrievalStatus,
1015
+ ...(input.seq !== undefined ? { seq: input.seq } : {}),
1016
+ ...(input.status === "killed" && input.stoppedBy !== undefined ? { stoppedBy: input.stoppedBy } : {}),
1017
+ ...(failed && input.error !== undefined ? { error: delimitUntrusted("agent error", boundedRedactedSummary(input.error, 300)) } : {}),
1018
+ ...(failed && input.errorCode !== undefined ? { errorCode: input.errorCode } : {}),
1019
+ ...(failed && input.errorRetryable !== undefined ? { retryable: input.errorRetryable } : {}),
1020
+ ...(input.resultIsPartial === true ? { partial_result: true } : {}),
1021
+ ...(input.completionId !== undefined ? { completionId: input.completionId } : {}),
1022
+ };
1023
+ }
1026
1024
  export function serveDurableAgentRowLane(row) {
1027
1025
  if (row.status === "parked") {
1028
1026
  return {
1029
1027
  content: delimitUntrusted(`TaskOutput ${row.handle}`, `status: parked
1030
1028
  The agent is durably suspended, waiting for an approval decision. It resumes when the pending approval is decided (durable approval inbox), or lands failed if the approval expires.`),
1031
- details: {
1032
- task_id: row.handle,
1033
- type: "background_agent",
1029
+ details: buildAgentPollDetails({
1030
+ taskId: row.handle,
1034
1031
  status: "parked",
1035
- retrieval_status: "success",
1032
+ retrievalStatus: "success",
1036
1033
  ...(row.seq !== undefined ? { seq: row.seq } : {}),
1037
- },
1034
+ }),
1038
1035
  };
1039
1036
  }
1040
1037
  const kindClause = row.status === "failed" && row.errorKind !== undefined && row.errorRetryable !== undefined
@@ -1046,18 +1043,18 @@ ${row.error ? `error: ${row.error}${kindClause}
1046
1043
  ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}`;
1047
1044
  return {
1048
1045
  content: delimitUntrusted(`TaskOutput ${row.handle}`, body),
1049
- details: {
1050
- task_id: row.handle,
1051
- type: "background_agent",
1046
+ details: buildAgentPollDetails({
1047
+ taskId: row.handle,
1052
1048
  status: row.status,
1053
- retrieval_status: "success",
1054
- ...(row.status === "killed" && row.stoppedBy !== undefined ? { stoppedBy: row.stoppedBy } : {}),
1049
+ retrievalStatus: "success",
1055
1050
  ...(row.seq !== undefined ? { seq: row.seq } : {}),
1056
- ...(row.resultIsPartial ? { partial_result: true } : {}),
1051
+ ...(row.stoppedBy !== undefined ? { stoppedBy: row.stoppedBy } : {}),
1052
+ ...(row.error !== undefined ? { error: row.error } : {}),
1053
+ ...(row.errorCode !== undefined ? { errorCode: row.errorCode } : {}),
1054
+ ...(row.errorRetryable !== undefined ? { errorRetryable: row.errorRetryable } : {}),
1055
+ ...(row.resultIsPartial === true ? { resultIsPartial: true } : {}),
1057
1056
  ...(row.completionId !== undefined ? { completionId: row.completionId } : {}),
1058
- ...(row.status === "failed" && row.errorCode !== undefined ? { errorCode: row.errorCode } : {}),
1059
- ...(row.status === "failed" && row.errorRetryable !== undefined ? { retryable: row.errorRetryable } : {}),
1060
- },
1057
+ }),
1061
1058
  ...(row.status === "failed" ? { isError: true } : {}),
1062
1059
  };
1063
1060
  }
@@ -1083,13 +1080,12 @@ export async function pollBackgroundAgentLane(handle, deadline, signal, oneShot,
1083
1080
  return {
1084
1081
  content: delimitUntrusted(`TaskOutput ${handle.id}`, `status: parked
1085
1082
  The agent is durably suspended, waiting for an approval decision. It resumes when the pending approval is decided (durable approval inbox), or lands failed if the approval expires.`),
1086
- details: {
1087
- task_id: handle.id,
1088
- type: "background_agent",
1083
+ details: buildAgentPollDetails({
1084
+ taskId: handle.id,
1089
1085
  status: "parked",
1090
- retrieval_status: "success",
1086
+ retrievalStatus: "success",
1091
1087
  ...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
1092
- },
1088
+ }),
1093
1089
  };
1094
1090
  }
1095
1091
  const fullResult = handle.result ? (handle.resultFull ?? handle.result) : undefined;
@@ -1109,21 +1105,18 @@ ${handle.error ? `error: ${handle.error}${kindClause}
1109
1105
  ${resultText}` : "(no result text)"}`;
1110
1106
  return {
1111
1107
  content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
1112
- details: {
1113
- task_id: handle.id,
1114
- type: "background_agent",
1108
+ details: buildAgentPollDetails({
1109
+ taskId: handle.id,
1115
1110
  status: handle.status,
1116
- retrieval_status: retrieval,
1111
+ retrievalStatus: retrieval,
1117
1112
  ...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
1118
- ...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
1119
- ...(handle.status === "failed" && handle.error !== undefined
1120
- ? { error: delimitUntrusted("agent error", boundedRedactedSummary(handle.error, 300)) }
1121
- : {}),
1122
- ...(handle.status === "failed" && handle.errorCode !== undefined ? { errorCode: handle.errorCode } : {}),
1123
- ...(handle.status === "failed" && handle.errorRetryable !== undefined ? { retryable: handle.errorRetryable } : {}),
1124
- ...(handle.resultIsPartial ? { partial_result: true } : {}),
1113
+ ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
1114
+ ...(handle.error !== undefined ? { error: handle.error } : {}),
1115
+ ...(handle.errorCode !== undefined ? { errorCode: handle.errorCode } : {}),
1116
+ ...(handle.errorRetryable !== undefined ? { errorRetryable: handle.errorRetryable } : {}),
1117
+ ...(handle.resultIsPartial === true ? { resultIsPartial: true } : {}),
1125
1118
  ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1126
- },
1119
+ }),
1127
1120
  ...(handle.status === "failed" ? { isError: true } : {}),
1128
1121
  };
1129
1122
  }
@@ -138,6 +138,7 @@ export declare class TaskRegistry {
138
138
  errorKind?: string;
139
139
  stoppedBy?: StopSource;
140
140
  seq?: number;
141
+ cycle?: number;
141
142
  }): "completed" | "failed" | "killed" | undefined;
142
143
  abortBackgroundAgentsForOwner(access: TaskAccess, opts?: {
143
144
  skipSessionScoped?: boolean;
@@ -518,7 +518,7 @@ export class TaskRegistry {
518
518
  this.markStopSource(handle.id, "system");
519
519
  const reapTerminalNote = handle.onReapTerminal;
520
520
  handle.abort.abort();
521
- this.settleBackgroundAgent(handle.id, { status: "killed", error: "session released" });
521
+ this.settleBackgroundAgent(handle.id, { status: "killed", error: "session released", cycle: handle.reviveCycle ?? 0 });
522
522
  if (reapTerminalNote !== undefined && handle.terminalNotified !== true) {
523
523
  handle.terminalNotified = true;
524
524
  try {
@@ -97,6 +97,7 @@ export interface ToolExecuteContext {
97
97
  clientContext?: TaskSpec["clientContext"];
98
98
  excludeTools?: readonly string[];
99
99
  deferTools?: readonly string[];
100
+ alwaysLoadTools?: readonly string[];
100
101
  promptProfile?: "simple" | "classic";
101
102
  additionalDirectories?: readonly string[];
102
103
  envFacts?: TaskSpec["envFacts"];
@@ -465,11 +466,14 @@ export interface TaskEventIdentity {
465
466
  sourceTaskId?: string;
466
467
  bgAgentId?: string;
467
468
  }
468
- export type BrainStatusPhase = "rate_limited" | "retrying" | "reconnecting" | "circuit_open";
469
+ export type BrainStatusPhase = "rate_limited" | "retrying" | "reconnecting" | "circuit_open" | "recovered" | "gave_up";
469
470
  export interface BrainStatus {
470
471
  phase: BrainStatusPhase;
471
472
  detail?: string;
472
473
  retryInSec?: number;
474
+ retryInMs?: number;
475
+ attempt?: number;
476
+ maxRetries?: number;
473
477
  }
474
478
  export interface ToolActivity {
475
479
  phase: "start" | "end";
@@ -256,7 +256,7 @@ export function findTurnStartIndex(entries, entryIndex, startIndex) {
256
256
  }
257
257
  return -1;
258
258
  }
259
- function enforceToolPairContainment(entries, startIndex, endIndex, cutIndex) {
259
+ function collectToolCallSites(entries, startIndex, endIndex) {
260
260
  const callSites = new Map();
261
261
  for (let i = startIndex; i < endIndex; i++) {
262
262
  const entry = entries[i];
@@ -275,6 +275,22 @@ function enforceToolPairContainment(entries, startIndex, endIndex, cutIndex) {
275
275
  }
276
276
  }
277
277
  }
278
+ return callSites;
279
+ }
280
+ function emittingCallSite(callSites, toolCallId, resultIndex) {
281
+ const sites = callSites.get(toolCallId);
282
+ if (sites === undefined)
283
+ return -1;
284
+ let site = -1;
285
+ for (const s of sites) {
286
+ if (s < resultIndex)
287
+ site = s;
288
+ else
289
+ break;
290
+ }
291
+ return site;
292
+ }
293
+ function enforceToolPairContainment(entries, callSites, endIndex, cutIndex) {
278
294
  if (callSites.size === 0)
279
295
  return cutIndex;
280
296
  for (let i = cutIndex; i < endIndex; i++) {
@@ -284,16 +300,7 @@ function enforceToolPairContainment(entries, startIndex, endIndex, cutIndex) {
284
300
  const msg = entry.message;
285
301
  if (msg.role !== "toolResult")
286
302
  continue;
287
- const sites = callSites.get(msg.toolCallId);
288
- if (sites === undefined)
289
- continue;
290
- let site = -1;
291
- for (const s of sites) {
292
- if (s < i)
293
- site = s;
294
- else
295
- break;
296
- }
303
+ const site = emittingCallSite(callSites, msg.toolCallId, i);
297
304
  if (site === -1 || site >= cutIndex)
298
305
  continue;
299
306
  cutIndex = site;
@@ -301,6 +308,49 @@ function enforceToolPairContainment(entries, startIndex, endIndex, cutIndex) {
301
308
  }
302
309
  return cutIndex;
303
310
  }
311
+ function foldToolPairsForward(entries, callSites, cutPoints, endIndex, cutIndex) {
312
+ for (let i = cutIndex; i < endIndex; i++) {
313
+ const entry = entries[i];
314
+ if (entry.type !== "message")
315
+ continue;
316
+ const msg = entry.message;
317
+ if (msg.role !== "toolResult")
318
+ continue;
319
+ const site = emittingCallSite(callSites, msg.toolCallId, i);
320
+ if (site === -1 || site >= cutIndex)
321
+ continue;
322
+ const next = cutPoints.find((c) => c > i);
323
+ if (next === undefined)
324
+ return -1;
325
+ cutIndex = next;
326
+ i = cutIndex - 1;
327
+ }
328
+ return cutIndex;
329
+ }
330
+ function deriveCutPoint(entries, startIndex, cutIndex) {
331
+ const cutEntry = entries[cutIndex];
332
+ const isUserMessage = cutEntry.type === "message" && cutEntry.message.role === "user";
333
+ const turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);
334
+ return {
335
+ firstKeptEntryIndex: cutIndex,
336
+ turnStartIndex,
337
+ isSplitTurn: !isUserMessage && turnStartIndex !== -1,
338
+ };
339
+ }
340
+ function leavesSummarizableHistory(entries, startIndex, cut) {
341
+ const historyEnd = cut.isSplitTurn ? cut.turnStartIndex : cut.firstKeptEntryIndex;
342
+ for (let i = startIndex; i < historyEnd; i++) {
343
+ if (getMessageFromEntryForCompaction(entries[i]) !== undefined)
344
+ return true;
345
+ }
346
+ if (!cut.isSplitTurn)
347
+ return false;
348
+ for (let i = cut.turnStartIndex; i < cut.firstKeptEntryIndex; i++) {
349
+ if (getMessageFromEntryForCompaction(entries[i]) !== undefined)
350
+ return true;
351
+ }
352
+ return false;
353
+ }
304
354
  export function findCutPoint(entries, startIndex, endIndex, keepRecentTokens, charsPerToken = DEFAULT_CHARS_PER_TOKEN) {
305
355
  const cutPoints = findValidCutPoints(entries, startIndex, endIndex);
306
356
  if (cutPoints.length === 0) {
@@ -335,15 +385,16 @@ export function findCutPoint(entries, startIndex, endIndex, keepRecentTokens, ch
335
385
  break;
336
386
  }
337
387
  }
338
- cutIndex = enforceToolPairContainment(entries, startIndex, endIndex, cutIndex);
339
- const cutEntry = entries[cutIndex];
340
- const isUserMessage = cutEntry.type === "message" && cutEntry.message.role === "user";
341
- const turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);
342
- return {
343
- firstKeptEntryIndex: cutIndex,
344
- turnStartIndex,
345
- isSplitTurn: !isUserMessage && turnStartIndex !== -1,
346
- };
388
+ const callSites = collectToolCallSites(entries, startIndex, endIndex);
389
+ const budgetCutIndex = cutIndex;
390
+ cutIndex = enforceToolPairContainment(entries, callSites, endIndex, cutIndex);
391
+ const kept = deriveCutPoint(entries, startIndex, cutIndex);
392
+ if (cutIndex !== budgetCutIndex && !leavesSummarizableHistory(entries, startIndex, kept)) {
393
+ const folded = foldToolPairsForward(entries, callSites, cutPoints, endIndex, budgetCutIndex);
394
+ if (folded !== -1)
395
+ return deriveCutPoint(entries, startIndex, folded);
396
+ }
397
+ return kept;
347
398
  }
348
399
  export const SUMMARIZATION_SYSTEM_PROMPT = `You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified.
349
400
 
@@ -1,7 +1,7 @@
1
1
  export type { CompactionPreparation, SummarizationClampDryRun } from "../engine/compaction/compaction.js";
2
2
  export type { InvokedSkillRetention } from "../engine/compaction/utils.js";
3
3
  export type { AgentCoreRuntimeDeps } from "../engine/loop/runtime-deps.js";
4
- export type { AgentMessage, AgentTool, AgentToolResult, ThinkingLevel, } from "../engine/loop/types.js";
4
+ export type { AgentMessage, AgentTool, AgentToolResult, AgentToolUpdateCallback, ThinkingLevel, ToolExecutionMode, } from "../engine/loop/types.js";
5
5
  export type { AgentHarnessEvent, CompactionSettings, ExecutionEnv, ExecutionErrorCode, FileErrorCode, FileInfo, Result, Session, SessionMetadata, SessionRepo, SessionStorage, SessionTreeEntry, Skill, } from "../engine/harness/types.js";
6
6
  export type { ExecutionEnvExecOptions } from "../engine/harness/types.js";
7
7
  export type { SessionWriteOptions, CompactionEntry } from "../engine/harness/types.js";
@@ -2,7 +2,7 @@ export * from "./harness-types.js";
2
2
  export { AgentHarness } from "../engine/harness/agent-harness.js";
3
3
  export { CompactionError, ExecutionError, FileError, ok, err } from "../engine/harness/types.js";
4
4
  export { DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, compact, computeFileLists, dryRunSummarizationClamp, estimateContextTokens, estimateTokens, prepareCompaction, shouldCompact, summaryOutputBudgetTokens, } from "../engine/compaction/compaction.js";
5
- export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, readCompactionActiveTools, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
5
+ export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, formatPersistedOutputRefs, readCompactionActiveTools, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
6
6
  export { NodeExecutionEnv } from "../engine/execution-env/node-execution-env.js";
7
7
  export { StoredSession, buildSessionContext } from "../engine/session/session.js";
8
8
  export { getEntriesToFork } from "../engine/session/repo-utils.js";
@@ -2,7 +2,7 @@ export * from "./harness-types.js";
2
2
  export { AgentHarness } from "../engine/harness/agent-harness.js";
3
3
  export { CompactionError, ExecutionError, FileError, ok, err } from "../engine/harness/types.js";
4
4
  export { DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, compact, computeFileLists, dryRunSummarizationClamp, estimateContextTokens, estimateTokens, prepareCompaction, shouldCompact, summaryOutputBudgetTokens, } from "../engine/compaction/compaction.js";
5
- export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, readCompactionActiveTools, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
5
+ export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, formatPersistedOutputRefs, readCompactionActiveTools, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
6
6
  export { NodeExecutionEnv } from "../engine/execution-env/node-execution-env.js";
7
7
  export { StoredSession, buildSessionContext } from "../engine/session/session.js";
8
8
  export { getEntriesToFork } from "../engine/session/repo-utils.js";
@@ -58,6 +58,7 @@ export interface RunWorkflowToolDeps {
58
58
  governanceBaseline: WorkflowGovernanceBaseline;
59
59
  parentExcludeTools?: readonly string[];
60
60
  parentDeferTools?: readonly string[];
61
+ parentAlwaysLoadTools?: readonly string[];
61
62
  parentPromptProfile?: "simple" | "classic";
62
63
  models?: Record<string, Model>;
63
64
  agents?: import("../core/types.js").AgentDefinition[];
@@ -154,9 +154,12 @@ export async function createRunWorkflowTool(d) {
154
154
  ...(d.parentDeferTools?.length
155
155
  ? { deferTools: [...new Set([...(base.deferTools ?? []), ...d.parentDeferTools])] }
156
156
  : {}),
157
+ ...(d.parentAlwaysLoadTools?.length
158
+ ? { alwaysLoadTools: [...new Set([...(base.alwaysLoadTools ?? []), ...d.parentAlwaysLoadTools])] }
159
+ : {}),
157
160
  });
158
161
  const withParentProfile = (base) => d.parentPromptProfile !== undefined && base.promptProfile === undefined ? { ...base, promptProfile: d.parentPromptProfile } : base;
159
- const baselineWithParentFace = (d.parentExcludeTools?.length ?? 0) > 0 || (d.parentDeferTools?.length ?? 0) > 0 || d.parentPromptProfile !== undefined
162
+ const baselineWithParentFace = (d.parentExcludeTools?.length ?? 0) > 0 || (d.parentDeferTools?.length ?? 0) > 0 || (d.parentAlwaysLoadTools?.length ?? 0) > 0 || d.parentPromptProfile !== undefined
160
163
  ? {
161
164
  ...d.governanceBaseline,
162
165
  base: withParentProfile(withParentFace(d.governanceBaseline.base)),
@@ -18,7 +18,7 @@ export declare function workflowAgentCallKey(ordinal: number, spec: TaskSpec, op
18
18
  schema?: TSchema;
19
19
  isolation?: "worktree";
20
20
  }): string;
21
- export declare function assertSupportedAgentIsolation(isolation: unknown): asserts isolation is "worktree" | undefined;
21
+ export declare function assertSupportedAgentIsolation(isolation: unknown): void;
22
22
  export interface WorkflowFanOutSlotError {
23
23
  index: number;
24
24
  kind: string;
@@ -100,7 +100,7 @@ export function workflowAgentCallKey(ordinal, spec, opts) {
100
100
  tools: spec.tools?.map((t) => t.name).slice().sort(),
101
101
  mcp: spec.mcp?.map((m) => m.name).slice().sort(),
102
102
  outputSchema: opts.schema ?? spec.outputSchema,
103
- isolation: opts.isolation,
103
+ isolation: opts.isolation ? opts.isolation : undefined,
104
104
  };
105
105
  return `${ordinal}:${boundInputHashOf(identity)}`;
106
106
  }
@@ -112,7 +112,7 @@ function resolveChildSessionIdAtSpawn(spec) {
112
112
  return randomUUID();
113
113
  }
114
114
  export function assertSupportedAgentIsolation(isolation) {
115
- if (isolation !== undefined && isolation !== "worktree") {
115
+ if (isolation && isolation !== "worktree") {
116
116
  const shown = typeof isolation === "string" ? JSON.stringify(isolation) : String(isolation);
117
117
  const e = new Error(`isolation ${shown} is not supported in workflow agents — only "worktree" (omit the option to run in the shared working tree). The agent was not started (fail-closed: an unrecognized isolation value must never silently run in the shared working tree).`);
118
118
  e.code = "isolation.invalid";
@@ -83,19 +83,80 @@ function resolveOperandLexically(base, operand, homeDir) {
83
83
  return normalizeAbsPathLexicalEitherFamily(`${base.replace(/[/\\]+$/, "")}/${raw}`);
84
84
  }
85
85
  function tokenizeSegment(segment) {
86
- return segment
86
+ const raw = segment
87
87
  .trim()
88
88
  .split(/\s+/)
89
- .filter((t) => t.length > 0)
90
- .map(foldQuoteRemovalToken);
89
+ .filter((t) => t.length > 0);
90
+ return { folded: raw.map(foldQuoteRemovalToken), raw };
91
91
  }
92
- function collectSegmentBoundaryFindings(toks, boundary) {
93
- const name = toks[0];
92
+ function hasUnquotedExpansionMetachar(rawToken) {
93
+ let open;
94
+ for (const ch of rawToken) {
95
+ if (open === undefined && (ch === '"' || ch === "'")) {
96
+ open = ch;
97
+ continue;
98
+ }
99
+ if (open === ch) {
100
+ open = undefined;
101
+ continue;
102
+ }
103
+ if (open === undefined && (ch === "{" || ch === "}" || ch === "$" || ch === "`"))
104
+ return true;
105
+ if (open === '"' && (ch === "$" || ch === "`"))
106
+ return true;
107
+ }
108
+ return false;
109
+ }
110
+ function isGrepPatternFlagToken(tok) {
111
+ if (tok.startsWith("--"))
112
+ return tok.startsWith("--regexp") || tok.startsWith("--file");
113
+ return /^-[A-Za-z]*[ef]/.test(tok);
114
+ }
115
+ function grepClusterValueOwner(tok) {
116
+ if (!/^-[A-Za-z]/.test(tok) || tok.startsWith("--"))
117
+ return undefined;
118
+ for (const ch of tok.slice(1)) {
119
+ if (ch === "e" || ch === "f")
120
+ return ch;
121
+ if (!/[A-Za-z]/.test(ch))
122
+ return undefined;
123
+ }
124
+ return undefined;
125
+ }
126
+ function attachedOptionPayloads(tok) {
127
+ const out = [];
128
+ if (tok.startsWith("--")) {
129
+ const eq = tok.indexOf("=");
130
+ if (eq > 0 && eq + 1 < tok.length)
131
+ out.push(tok.slice(eq + 1));
132
+ return out;
133
+ }
134
+ for (let k = 1; k <= tok.length; k++) {
135
+ if (!/^[A-Za-z]*$/.test(tok.slice(1, k)))
136
+ break;
137
+ const payload = tok.slice(k);
138
+ if (payload.length > 0)
139
+ out.push(payload);
140
+ }
141
+ return out;
142
+ }
143
+ function collectSegmentBoundaryFindings(tokens, boundary) {
144
+ const name = tokens.folded[0];
94
145
  if (NO_PATH_OPERAND_COMMANDS.has(name))
95
146
  return [];
96
- const args = toks.slice(1);
147
+ const args = tokens.folded.slice(1);
97
148
  const findings = [];
98
149
  const candidates = [];
150
+ for (const rawArg of tokens.raw.slice(1)) {
151
+ if (!hasUnquotedExpansionMetachar(rawArg))
152
+ continue;
153
+ return [
154
+ {
155
+ kind: "unresolvable",
156
+ reason: `"${name}" is given the argument "${rawArg}", which the shell expands (brace/variable/command expansion) before the command runs — the path it would actually read cannot be resolved statically, so it is not auto-allowed`,
157
+ },
158
+ ];
159
+ }
99
160
  if (name === "cd") {
100
161
  const target = args.find((t) => !t.startsWith("-") || t === "-");
101
162
  if (target === undefined) {
@@ -107,7 +168,7 @@ function collectSegmentBoundaryFindings(toks, boundary) {
107
168
  candidates.push(target);
108
169
  }
109
170
  else {
110
- const patternSuppliedByFlag = name === "grep" && args.some((t) => t === "-e" || t.startsWith("-e") || t === "-f" || t.startsWith("-f") || t.startsWith("--regexp") || t.startsWith("--file"));
171
+ const patternSuppliedByFlag = name === "grep" && args.some(isGrepPatternFlagToken);
111
172
  let sawOperand = false;
112
173
  for (let k = 0; k < args.length; k++) {
113
174
  const t = args[k];
@@ -115,17 +176,21 @@ function collectSegmentBoundaryFindings(toks, boundary) {
115
176
  k++;
116
177
  continue;
117
178
  }
179
+ if (name === "cut" && (/^-d./.test(t) || /^--(output-)?delimiter=/.test(t)))
180
+ continue;
181
+ if (name === "grep" && (grepClusterValueOwner(t) === "e" || /^--regexp=/.test(t)))
182
+ continue;
118
183
  if (t.startsWith("-") && t !== "-") {
119
- const eq = t.indexOf("=");
120
- const value = eq > 0 ? t.slice(eq + 1) : "";
121
- if (value.length > 0 && isAbsolutePathToken(value))
122
- candidates.push(value);
184
+ for (const payload of attachedOptionPayloads(t)) {
185
+ if (isAbsolutePathToken(payload) || isPathShapedToken(payload))
186
+ candidates.push(payload);
187
+ }
123
188
  continue;
124
189
  }
125
- if (t === "-")
126
- continue;
127
190
  const isGrepPatternSlot = name === "grep" && !patternSuppliedByFlag && !sawOperand;
128
191
  sawOperand = true;
192
+ if (t === "-")
193
+ continue;
129
194
  if (isGrepPatternSlot)
130
195
  continue;
131
196
  if (isPathShapedToken(t))
@@ -227,16 +292,17 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
227
292
  const STDIN_FILE_FLOOR = { cat: 1, grep: 2, head: 1, tail: 1, wc: 1, cut: 1, tr: Infinity };
228
293
  const foldedSegments = [];
229
294
  for (let si = 0; si < segments.length; si++) {
230
- const toks = tokenizeSegment(segments[si]);
295
+ const segmentTokens = tokenizeSegment(segments[si]);
296
+ const toks = segmentTokens.folded;
231
297
  if (toks.length === 0)
232
298
  continue;
233
- foldedSegments.push(toks);
299
+ foldedSegments.push(segmentTokens);
234
300
  const name = toks[0];
235
301
  const nonOption = toks.slice(1).filter((t) => !t.startsWith("-"));
236
302
  if (!(pipeFed[si] ?? false)) {
237
303
  const floor = STDIN_FILE_FLOOR[name];
238
304
  const restArgs = toks.slice(1);
239
- const grepPatternSuppliedByFlag = name === "grep" && restArgs.some((t) => t === "-e" || t.startsWith("-e") || t === "-f" || t.startsWith("--regexp") || t.startsWith("--file"));
305
+ const grepPatternSuppliedByFlag = name === "grep" && restArgs.some(isGrepPatternFlagToken);
240
306
  let sawNonFlagOperand = false;
241
307
  let hasStdinDash = false;
242
308
  if (name !== "tr") {
@@ -305,7 +371,7 @@ function evaluateReadBoundary(foldedSegments, boundary) {
305
371
  }
306
372
  export function classifySimpleCommandReadBoundary(command, boundary) {
307
373
  const toks = tokenizeSegment(command);
308
- if (toks.length === 0)
374
+ if (toks.folded.length === 0)
309
375
  return {};
310
376
  return evaluateReadBoundary([toks], boundary);
311
377
  }
@@ -7,7 +7,7 @@ import { delimitUntrusted } from "../../core/untrusted-text.js";
7
7
  import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
8
8
  import { imageMagicMatches, withinAnyRoot } from "./safety.js";
9
9
  import { ghRateLimitHint } from "./gh-rate-limit.js";
10
- import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, shellRecoveryHint, CWD_SENTINEL, } from "./fs-shared.js";
10
+ import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, } from "./fs-shared.js";
11
11
  import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyCompoundReadonly, classifySimpleCommandReadBoundary, } from "./bash-readonly-classifier.js";
12
12
  export function bashReversibilityProbe(allow, boundary) {
13
13
  const allowSet = new Set(allow ?? BASH_READONLY_DEFAULT_ALLOW);
@@ -632,7 +632,19 @@ export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, opt
632
632
  const timeoutCapsSecView = bashTimeoutCapsSec(timeoutCaps);
633
633
  const sample = [...allow].slice(0, 6).join(", ");
634
634
  const readRoots = [rootCanonical, ...(opts?.additionalRoots ?? [])];
635
- const mintedOutputFiles = new Set();
635
+ const overflowSpoolFence = createShellOverflowSpoolFence(env);
636
+ const readsOnlyEngineOverflowSpool = async (verdict) => {
637
+ if (verdict.outOfRootRead !== true)
638
+ return false;
639
+ const paths = verdict.outOfRootPaths ?? [];
640
+ if (paths.length === 0)
641
+ return false;
642
+ for (const p of paths) {
643
+ if (!(await overflowSpoolFence(p)))
644
+ return false;
645
+ }
646
+ return true;
647
+ };
636
648
  return defineTool({
637
649
  name: "Bash",
638
650
  contract: { contractId: "core.bash_readonly@1", implementationRevision: "1" },
@@ -651,17 +663,11 @@ export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, opt
651
663
  const reason = coarseReadonlyCheck(command, allow);
652
664
  if (reason)
653
665
  return errorResult(`Error (Bash): ${reason}`);
654
- const boundary = classifySimpleCommandReadBoundary(command, { roots: [...readRoots, ...mintedOutputFiles], cwd: rootCanonical });
655
- if (boundary.reason !== undefined) {
666
+ const boundary = classifySimpleCommandReadBoundary(command, { roots: readRoots, cwd: rootCanonical });
667
+ if (boundary.reason !== undefined && !(await readsOnlyEngineOverflowSpool(boundary))) {
656
668
  return errorResult(`Error (Bash): ${boundary.reason}. bash_readonly is confined to the workspace roots; it has no approval path, so the call is refused rather than escalated.`, { code: "readonly_out_of_root", paths: boundary.outOfRootPaths ?? [] });
657
669
  }
658
- const result = await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined, execClamp, ctx.toolCallId, true);
659
- if (typeof result !== "string") {
660
- const outputFile = result.details.output_file;
661
- if (typeof outputFile === "string")
662
- mintedOutputFiles.add(outputFile);
663
- }
664
- return result;
670
+ return await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined, execClamp, ctx.toolCallId, true);
665
671
  },
666
672
  });
667
673
  }
@@ -47,6 +47,7 @@ export declare const FILE_PATH_PARAMS: {
47
47
  };
48
48
  export declare function clipShellOutput(s: string): string;
49
49
  export declare function writeShellOverflowFile(env: ExecutionEnv, stdout: string, stderr: string): Promise<string | undefined>;
50
+ export declare function createShellOverflowSpoolFence(env: ExecutionEnv): (path: string) => Promise<boolean>;
50
51
  export declare function shellRecoveryHint(path: string, readOnly: boolean | undefined): string;
51
52
  export declare const FILE_STATE_TRAILER = " (file state is current in your context \u2014 no need to Read it back)";
52
53
  export declare const CWD_SENTINEL = "__cc_cwd_9f2c1b__";