@sema-agent/core 5.12.0 → 5.14.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/CHANGELOG.md +320 -0
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +4 -0
- package/dist/agents/subagent.js +137 -43
- package/dist/brain/anthropic.js +33 -10
- package/dist/brain/context-overflow.d.ts +20 -0
- package/dist/brain/context-overflow.js +58 -0
- package/dist/brain/open-responses.js +24 -10
- package/dist/brain/openai.js +29 -11
- package/dist/brain/request-params.d.ts +2 -0
- package/dist/brain/request-params.js +16 -0
- package/dist/brain/stream-engine.d.ts +9 -1
- package/dist/brain/stream-engine.js +256 -27
- package/dist/brain/timeout.d.ts +1 -0
- package/dist/brain/timeout.js +1 -0
- package/dist/core/a2a.d.ts +2 -2
- package/dist/core/a2a.js +3 -3
- package/dist/core/ask-question.d.ts +47 -2
- package/dist/core/ask-question.js +209 -28
- package/dist/core/background-agent-store.d.ts +2 -0
- package/dist/core/checkpoint-store.d.ts +49 -18
- package/dist/core/checkpoint-store.js +117 -4
- package/dist/core/compliance.d.ts +11 -0
- package/dist/core/compliance.js +34 -0
- package/dist/core/governance-codes.d.ts +12 -0
- package/dist/core/governance-codes.js +24 -0
- package/dist/core/hooks.d.ts +24 -2
- package/dist/core/hooks.js +97 -10
- package/dist/core/human-input-projection.d.ts +12 -0
- package/dist/core/human-input-projection.js +27 -0
- package/dist/core/locked-config.d.ts +27 -0
- package/dist/core/locked-config.js +42 -0
- package/dist/core/mcp.d.ts +7 -2
- package/dist/core/mcp.js +7 -7
- package/dist/core/memory-admission.d.ts +51 -0
- package/dist/core/memory-admission.js +159 -0
- package/dist/core/memory.d.ts +2 -0
- package/dist/core/memory.js +3 -2
- package/dist/core/retention.d.ts +36 -0
- package/dist/core/retention.js +31 -0
- package/dist/core/runner/assemble-result.d.ts +1 -0
- package/dist/core/runner/assemble-result.js +2 -2
- package/dist/core/runner/prepare-memory.d.ts +9 -0
- package/dist/core/runner/prepare-memory.js +28 -2
- package/dist/core/runner/prepare-task.d.ts +18 -6
- package/dist/core/runner/prepare-task.js +428 -38
- package/dist/core/runner/runtask.d.ts +3 -6
- package/dist/core/runner/runtask.js +258 -77
- package/dist/core/runner/tool-output-projection.js +1 -0
- package/dist/core/session-store.d.ts +4 -0
- package/dist/core/session-store.js +5 -0
- package/dist/core/session.d.ts +1 -0
- package/dist/core/store-contracts/background-agent-store-contract.js +19 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +62 -3
- package/dist/core/task-notification.d.ts +2 -0
- package/dist/core/task-notification.js +5 -3
- package/dist/core/task-registry-agent.d.ts +1 -0
- package/dist/core/task-registry-agent.js +6 -0
- package/dist/core/task-registry.d.ts +1 -0
- package/dist/core/task-registry.js +4 -1
- package/dist/core/tool-policy.d.ts +5 -0
- package/dist/core/tool-policy.js +2 -1
- package/dist/core/tool-result-store.d.ts +2 -0
- package/dist/core/tool-result-store.js +1 -0
- package/dist/core/types.d.ts +38 -2
- package/dist/core/wiring-manifest.d.ts +97 -0
- package/dist/core/wiring-manifest.js +186 -0
- package/dist/engine/compaction/compaction.js +2 -2
- package/dist/engine/harness/agent-harness.d.ts +2 -1
- package/dist/engine/harness/agent-harness.js +19 -2
- package/dist/engine/harness/types.d.ts +3 -1
- package/dist/engine/llm/types.d.ts +7 -0
- package/dist/engine/llm/types.js +8 -1
- package/dist/engine/llm/validation.js +11 -1
- package/dist/engine/session/import-validate.d.ts +6 -1
- package/dist/engine/session/import-validate.js +29 -6
- package/dist/engine/session/memory-repo.d.ts +3 -1
- package/dist/engine/session/memory-repo.js +2 -2
- package/dist/index.d.ts +12 -4
- package/dist/index.js +12 -4
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/internal/llm.d.ts +2 -2
- package/dist/internal/llm.js +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +4 -0
- package/dist/orchestration/run-workflow-tool.js +3 -0
- package/dist/orchestration/workflow-types.d.ts +8 -0
- package/dist/orchestration/workflow-types.js +14 -0
- package/dist/orchestration/workflow.d.ts +4 -0
- package/dist/orchestration/workflow.js +134 -5
- package/dist/prompts/default.js +1 -1
- package/dist/stores/file/checkpoint-store.d.ts +3 -5
- package/dist/stores/file/checkpoint-store.js +31 -2
- package/dist/stores/file/index.js +1 -1
- package/dist/stores/file/session-store.d.ts +3 -1
- package/dist/stores/file/session-store.js +2 -2
- package/dist/stores/file/shared-ledger.js +8 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +3 -0
- package/dist/tools/fs/bash-readonly-classifier.js +94 -0
- package/dist/tools/fs/fs-bash.js +31 -12
- package/dist/tools/fs/safety.js +34 -10
- package/package.json +1 -1
|
@@ -81,6 +81,7 @@ export interface RunWorkflowToolDeps {
|
|
|
81
81
|
sourceTaskId?: string;
|
|
82
82
|
principal?: string;
|
|
83
83
|
oneShot?: boolean;
|
|
84
|
+
parentInteractionPosture?: "interactive" | "headless";
|
|
84
85
|
workflowDepth?: number;
|
|
85
86
|
parentCwd?: string;
|
|
86
87
|
parentThinking?: () => import("../core/types.js").TaskSpec["thinking"];
|
|
@@ -88,5 +89,8 @@ export interface RunWorkflowToolDeps {
|
|
|
88
89
|
parentGetApiKeyAndHeaders?: import("../core/types.js").TaskSpec["getApiKeyAndHeaders"];
|
|
89
90
|
forwardEvent?: (event: import("../core/types.js").TaskEvent) => void;
|
|
90
91
|
inheritedGateForChildren?: () => import("../core/runner/prepare-task.js").InheritedGate;
|
|
92
|
+
autoModeReview?: () => {
|
|
93
|
+
decider: import("../core/auto-mode.js").AutoModeDecider;
|
|
94
|
+
} | undefined;
|
|
91
95
|
}
|
|
92
96
|
export declare function createRunWorkflowTool(d: RunWorkflowToolDeps): Promise<AgentTool>;
|
|
@@ -388,6 +388,7 @@ export async function createRunWorkflowTool(d) {
|
|
|
388
388
|
}
|
|
389
389
|
let handle;
|
|
390
390
|
const workflowTaskId = d.taskRegistry?.mintTaskId("workflow");
|
|
391
|
+
const autoModeReview = ctx.autoModeReview ?? d.autoModeReview?.();
|
|
391
392
|
try {
|
|
392
393
|
handle = startWorkflow(d.runner, scriptFn, {
|
|
393
394
|
store: d.store,
|
|
@@ -413,6 +414,7 @@ export async function createRunWorkflowTool(d) {
|
|
|
413
414
|
...(d.parentCwd !== undefined ? { parentCwd: d.parentCwd } : {}),
|
|
414
415
|
parentToolCallId: ctx.toolCallId,
|
|
415
416
|
...(sourceTaskId !== undefined ? { parentTaskId: sourceTaskId } : {}),
|
|
417
|
+
...((ctx.interactionPosture ?? d.parentInteractionPosture) !== undefined ? { interactionPosture: (ctx.interactionPosture ?? d.parentInteractionPosture) } : {}),
|
|
416
418
|
...(d.parentModel !== undefined ? { defaultModel: d.parentModel } : {}),
|
|
417
419
|
...(d.parentGetApiKeyAndHeaders !== undefined ? { defaultGetApiKeyAndHeaders: d.parentGetApiKeyAndHeaders } : {}),
|
|
418
420
|
...(ctx.centerArtifactDigest !== undefined ? { parentCenterArtifactDigest: ctx.centerArtifactDigest } : {}),
|
|
@@ -421,6 +423,7 @@ export async function createRunWorkflowTool(d) {
|
|
|
421
423
|
...((ctx.inheritedGateForChildren ?? d.inheritedGateForChildren) !== undefined
|
|
422
424
|
? { inheritedGate: (ctx.inheritedGateForChildren ?? d.inheritedGateForChildren)() }
|
|
423
425
|
: {}),
|
|
426
|
+
...(autoModeReview !== undefined ? { autoModeReview } : {}),
|
|
424
427
|
}, { workflowDepth: d.workflowDepth });
|
|
425
428
|
}
|
|
426
429
|
catch (err) {
|
|
@@ -187,6 +187,14 @@ export declare class WorkflowAgentStalledError extends Error {
|
|
|
187
187
|
readonly code = "workflow.agent_stalled";
|
|
188
188
|
constructor(attempts: number, stallMs: number, lastResult?: TaskResult | undefined);
|
|
189
189
|
}
|
|
190
|
+
export declare const WORKFLOW_SPAWN_BLOCKED_ERROR_CODE = "autoMode.spawn_blocked";
|
|
191
|
+
export declare class WorkflowAgentBlockedError extends Error {
|
|
192
|
+
readonly label: string;
|
|
193
|
+
readonly category: string;
|
|
194
|
+
readonly reason: string;
|
|
195
|
+
readonly code = "workflow.agent_blocked";
|
|
196
|
+
constructor(label: string, category: string, reason: string);
|
|
197
|
+
}
|
|
190
198
|
export declare class WorkflowMaxAgentsError extends Error {
|
|
191
199
|
readonly max: number;
|
|
192
200
|
readonly code = "workflow.max_agents";
|
|
@@ -41,6 +41,20 @@ export class WorkflowAgentStalledError extends Error {
|
|
|
41
41
|
this.name = "WorkflowAgentStalledError";
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
export const WORKFLOW_SPAWN_BLOCKED_ERROR_CODE = "autoMode.spawn_blocked";
|
|
45
|
+
export class WorkflowAgentBlockedError extends Error {
|
|
46
|
+
label;
|
|
47
|
+
category;
|
|
48
|
+
reason;
|
|
49
|
+
code = "workflow.agent_blocked";
|
|
50
|
+
constructor(label, category, reason) {
|
|
51
|
+
super(`workflow agent "${label}" was not started: blocked by the pre-spawn review${reason ? `: ${reason}` : ""}`);
|
|
52
|
+
this.label = label;
|
|
53
|
+
this.category = category;
|
|
54
|
+
this.reason = reason;
|
|
55
|
+
this.name = "WorkflowAgentBlockedError";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
44
58
|
export class WorkflowMaxAgentsError extends Error {
|
|
45
59
|
max;
|
|
46
60
|
code = "workflow.max_agents";
|
|
@@ -96,12 +96,16 @@ export interface RunWorkflowOptions {
|
|
|
96
96
|
parentCwd?: string;
|
|
97
97
|
parentToolCallId?: string;
|
|
98
98
|
parentTaskId?: string;
|
|
99
|
+
interactionPosture?: "interactive" | "headless";
|
|
99
100
|
defaultModel?: () => import("../internal/llm.js").Model | undefined;
|
|
100
101
|
defaultGetApiKeyAndHeaders?: TaskSpec["getApiKeyAndHeaders"];
|
|
101
102
|
parentCenterArtifactDigest?: string;
|
|
102
103
|
parentCenterSourceRevision?: string;
|
|
103
104
|
onForwardEvent?: (event: TaskEvent) => void;
|
|
104
105
|
inheritedGate?: import("../core/runner/prepare-task.js").InheritedGate;
|
|
106
|
+
autoModeReview?: {
|
|
107
|
+
decider: import("../core/auto-mode.js").AutoModeDecider;
|
|
108
|
+
};
|
|
105
109
|
phases?: ReadonlyArray<{
|
|
106
110
|
title: string;
|
|
107
111
|
detail?: string;
|
|
@@ -16,12 +16,43 @@ import { boundedRedactedSummary } from "../core/untrusted-egress.js";
|
|
|
16
16
|
import { delimitUntrusted } from "../core/untrusted-text.js";
|
|
17
17
|
import { OUTPUT_TOOL_NAME } from "../core/runner/synthetic-tools.js";
|
|
18
18
|
import { compileOutputSchema } from "../core/runner/strict-output-schema.js";
|
|
19
|
-
import { WorkflowBudgetExceededError, WorkflowNestingError, WorkflowAgentSchemaError, WorkflowAgentStalledError, WorkflowMaxAgentsError } from "./workflow-types.js";
|
|
19
|
+
import { WorkflowBudgetExceededError, WorkflowNestingError, WorkflowAgentBlockedError, WorkflowAgentSchemaError, WorkflowAgentStalledError, WorkflowMaxAgentsError, WORKFLOW_SPAWN_BLOCKED_ERROR_CODE } from "./workflow-types.js";
|
|
20
20
|
export * from "./workflow-types.js";
|
|
21
21
|
const MAX_TRANSCRIPT_CHARS = 4000;
|
|
22
22
|
const WORKFLOW_RESULT_MAX = 4000;
|
|
23
23
|
const WORKFLOW_RESULT_FULL_MAX = 200_000;
|
|
24
24
|
const MAX_ACTIVITY = 30;
|
|
25
|
+
const MAX_REVIEWED_SCHEMA_BYTES = 4096;
|
|
26
|
+
const MAX_REVIEW_REASON_CHARS = 500;
|
|
27
|
+
const MAX_REVIEWED_PROMPT_CHARS = 2_000;
|
|
28
|
+
const MAX_SCHEMA_WALK_DEPTH = 32;
|
|
29
|
+
function isUsableSchema(v) {
|
|
30
|
+
if (v === true)
|
|
31
|
+
return true;
|
|
32
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
33
|
+
}
|
|
34
|
+
function isSingleFaced(v, depth = 0) {
|
|
35
|
+
if (typeof v !== "object" || v === null)
|
|
36
|
+
return true;
|
|
37
|
+
if (depth > MAX_SCHEMA_WALK_DEPTH)
|
|
38
|
+
return false;
|
|
39
|
+
if (typeof v.toJSON === "function")
|
|
40
|
+
return false;
|
|
41
|
+
for (const key of Object.keys(v)) {
|
|
42
|
+
if (typeof Object.getOwnPropertyDescriptor(v, key)?.get === "function")
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
for (const child of Object.values(v)) {
|
|
46
|
+
if (!isSingleFaced(child, depth + 1))
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
function forReview(text) {
|
|
52
|
+
if (text.length <= MAX_REVIEWED_PROMPT_CHARS)
|
|
53
|
+
return text;
|
|
54
|
+
return `${text.slice(0, MAX_REVIEWED_PROMPT_CHARS)}\n[…TRUNCATED FOR REVIEW: ${text.length - MAX_REVIEWED_PROMPT_CHARS} further characters follow that the child WILL receive and this review did NOT see]`;
|
|
55
|
+
}
|
|
25
56
|
function workflowModelLabel(spec) {
|
|
26
57
|
const model = spec.model;
|
|
27
58
|
if (model === undefined)
|
|
@@ -473,6 +504,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
473
504
|
const sem = createSemaphore(concurrency);
|
|
474
505
|
const spawnAttribution = {
|
|
475
506
|
isDelegatedChild: true,
|
|
507
|
+
...(opts.interactionPosture !== undefined ? { parentInteractionPosture: opts.interactionPosture } : {}),
|
|
476
508
|
...(opts.parentToolCallId !== undefined ? { parentToolCallId: opts.parentToolCallId } : {}),
|
|
477
509
|
...(opts.parentTaskId !== undefined ? { parentTaskId: opts.parentTaskId } : {}),
|
|
478
510
|
...(opts.parentCenterArtifactDigest !== undefined ? { parentCenterArtifactDigest: opts.parentCenterArtifactDigest } : {}),
|
|
@@ -782,6 +814,83 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
782
814
|
const model = workflowModelLabel(specForIdentity);
|
|
783
815
|
return { label, phase, phaseInstance, groupId, inheritedModelSnap, callKey, prompt, model };
|
|
784
816
|
};
|
|
817
|
+
const reviewSpawnBeforeLaunch = async (lane, label, callKey, runSpec, effectiveSignal) => {
|
|
818
|
+
const review = opts.autoModeReview;
|
|
819
|
+
if (review === undefined)
|
|
820
|
+
return runSpec;
|
|
821
|
+
const schema = runSpec.outputSchema;
|
|
822
|
+
let schemaText;
|
|
823
|
+
let specToLaunch = runSpec;
|
|
824
|
+
if (schema !== undefined) {
|
|
825
|
+
try {
|
|
826
|
+
schemaText = JSON.stringify(schema);
|
|
827
|
+
}
|
|
828
|
+
catch {
|
|
829
|
+
schemaText = undefined;
|
|
830
|
+
}
|
|
831
|
+
if (schemaText === undefined) {
|
|
832
|
+
throw new WorkflowAgentBlockedError(label, "", "the child's output schema could not be serialized for review");
|
|
833
|
+
}
|
|
834
|
+
const schemaBytes = Buffer.byteLength(schemaText, "utf8");
|
|
835
|
+
if (schemaBytes > MAX_REVIEWED_SCHEMA_BYTES) {
|
|
836
|
+
throw new WorkflowAgentBlockedError(label, "", `output schema too large to classify safely (${schemaBytes} bytes, cap ${MAX_REVIEWED_SCHEMA_BYTES})`);
|
|
837
|
+
}
|
|
838
|
+
if (!isSingleFaced(schema)) {
|
|
839
|
+
throw new WorkflowAgentBlockedError(label, "", "the child's output schema cannot be read as one unambiguous value (it overrides its own serialization, or nests deeper than the review can walk)");
|
|
840
|
+
}
|
|
841
|
+
const normalized = JSON.parse(schemaText);
|
|
842
|
+
if (!isUsableSchema(normalized)) {
|
|
843
|
+
throw new WorkflowAgentBlockedError(label, "", "the child's output schema does not normalize to a JSON Schema object");
|
|
844
|
+
}
|
|
845
|
+
specToLaunch = { ...runSpec, outputSchema: normalized };
|
|
846
|
+
}
|
|
847
|
+
const toolNames = (runSpec.tools ?? []).map((t) => t.name);
|
|
848
|
+
const toolsNote = `${toolNames.length > 0 ? toolNames.join(", ") : "(none explicitly listed)"}` +
|
|
849
|
+
` — this is the EXPLICIT roster only. If this deployment gave the child a real execution environment,` +
|
|
850
|
+
` its own prepare additionally mounts the standard file/shell toolkit (Read/Edit/Write/Bash/Grep/Glob)` +
|
|
851
|
+
` on top of this list. Treat the absence of a tool here as unknown, not as denied.`;
|
|
852
|
+
const remoteSources = [
|
|
853
|
+
...(runSpec.mcp ?? []).map((m) => `mcp:${m.name}`),
|
|
854
|
+
...(runSpec.a2a ?? []).map((p) => `a2a:${p.name}`),
|
|
855
|
+
];
|
|
856
|
+
const shownSources = remoteSources.slice(0, 20).map((s) => s.slice(0, 80));
|
|
857
|
+
const remoteNote = remoteSources.length > 0
|
|
858
|
+
? `\nThis child also mounts tools from ${remoteSources.length} external capability source(s) — ${shownSources.join(", ")}${remoteSources.length > shownSources.length ? `, +${remoteSources.length - shownSources.length} more` : ""}. Each mounts its own tools under a "<source>__*" namespace; those tool names are not resolved yet and are NOT in the roster above. Treat this child as able to reach outside this process.`
|
|
859
|
+
: "";
|
|
860
|
+
const objective = forReview(runSpec.objective ?? "");
|
|
861
|
+
const systemPrompt = runSpec.systemPrompt !== undefined ? forReview(runSpec.systemPrompt) : undefined;
|
|
862
|
+
const imageCount = runSpec.images?.length ?? 0;
|
|
863
|
+
const imagesNote = imageCount > 0
|
|
864
|
+
? `\n${imageCount} image attachment(s) ride with this child. Their CONTENT was NOT inspected by this review — the classifier leg is text-only. Treat any instruction that defers to attached or embedded content as unreviewed.`
|
|
865
|
+
: "";
|
|
866
|
+
const labelForReview = label.slice(0, 80);
|
|
867
|
+
const verdict = await review.decider
|
|
868
|
+
.decide({
|
|
869
|
+
req: {
|
|
870
|
+
toolName: "Workflow(agent)",
|
|
871
|
+
args: {
|
|
872
|
+
objective,
|
|
873
|
+
tools: toolNames,
|
|
874
|
+
...(systemPrompt !== undefined ? { systemPrompt } : {}),
|
|
875
|
+
...(schemaText !== undefined ? { outputSchema: schemaText } : {}),
|
|
876
|
+
...(imageCount > 0 ? { uninspectedImageAttachments: imageCount } : {}),
|
|
877
|
+
...(remoteSources.length > 0 ? { externalCapabilitySources: shownSources } : {}),
|
|
878
|
+
},
|
|
879
|
+
toolCallId: `${runId}:${callKey}`,
|
|
880
|
+
},
|
|
881
|
+
askMessage: `Reviewing a workflow-spawned sub-agent ("${labelForReview}", ${lane}) about to be started. ` +
|
|
882
|
+
`Objective: ${objective}\nTools available to it: ${toolsNote}${remoteNote}${imagesNote}`,
|
|
883
|
+
}, effectiveSignal)
|
|
884
|
+
.catch(() => ({ kind: "unavailable", cause: "error" }));
|
|
885
|
+
if (effectiveSignal?.aborted)
|
|
886
|
+
throw new Error("workflow aborted");
|
|
887
|
+
if (finalized)
|
|
888
|
+
throw new Error(`workflow run already finalized — ${lane} cannot spawn after the run ended`);
|
|
889
|
+
if (verdict.kind === "block") {
|
|
890
|
+
throw new WorkflowAgentBlockedError(label, verdict.category, boundedRedactedSummary(verdict.reason, MAX_REVIEW_REASON_CHARS));
|
|
891
|
+
}
|
|
892
|
+
return specToLaunch;
|
|
893
|
+
};
|
|
785
894
|
const createUsageBeat = (rec) => {
|
|
786
895
|
let beatTokens = 0;
|
|
787
896
|
let beatTurns = 0;
|
|
@@ -931,15 +1040,16 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
931
1040
|
const typedSpec = typedSpec0.model === undefined && inheritedModelSnap !== undefined ? { ...typedSpec0, model: inheritedModelSnap } : typedSpec0;
|
|
932
1041
|
const framedSpec = withWorkflowChildPersona(typedSpec, agentOpts.schema ?? typedSpec.outputSchema);
|
|
933
1042
|
const authInherit = framedSpec.getApiKeyAndHeaders === undefined && opts.defaultGetApiKeyAndHeaders !== undefined ? { getApiKeyAndHeaders: opts.defaultGetApiKeyAndHeaders } : {};
|
|
934
|
-
const
|
|
1043
|
+
const preReviewSpec = agentOpts.schema
|
|
935
1044
|
? { ...framedSpec, ...authInherit, signal: effectiveSignal, outputSchema: agentOpts.schema }
|
|
936
1045
|
: { ...framedSpec, ...authInherit, signal: effectiveSignal };
|
|
1046
|
+
const runSpec = await reviewSpawnBeforeLaunch("ctx.agent", label, callKey, preReviewSpec, effectiveSignal);
|
|
937
1047
|
const enrichedForward = opts.onForwardEvent !== undefined
|
|
938
1048
|
? (e) => {
|
|
939
1049
|
opts.onForwardEvent(e.type === "task_progress" ? { ...e, workflowRunId: runId, workflowAgentLabel: label } : e);
|
|
940
1050
|
}
|
|
941
1051
|
: undefined;
|
|
942
|
-
const baseInternals = { ...(agentOpts.isolation ? { isolation: agentOpts.isolation } : {}), ...(opts.parentCwd !== undefined ? { parentCwd: opts.parentCwd } : {}), ...spawnAttribution, ...(enrichedForward !== undefined ? { onForwardEvent: enrichedForward } : {}), agentName: label, onWorkspaceResolved: createWorkspaceObserver(rec) };
|
|
1052
|
+
const baseInternals = { ...(agentOpts.isolation ? { isolation: agentOpts.isolation } : {}), ...(opts.parentCwd !== undefined ? { parentCwd: opts.parentCwd } : {}), ...spawnAttribution, ...(enrichedForward !== undefined ? { onForwardEvent: enrichedForward } : {}), delegationTaskType: "workflow", agentName: label, onWorkspaceResolved: createWorkspaceObserver(rec) };
|
|
943
1053
|
let attempts = 0;
|
|
944
1054
|
let throttleRetried = false;
|
|
945
1055
|
let lastAttemptReason;
|
|
@@ -1128,6 +1238,10 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1128
1238
|
rec.output = boundedRedactedSummary(salvaged.structuredOutput ?? salvaged.result, MAX_TRANSCRIPT_CHARS) || (rec.errorMessage ?? "");
|
|
1129
1239
|
rec.stats = { tokens: salvaged.stats.tokens ?? 0, turns: salvaged.stats.turns ?? 0, costMicroUsd: salvaged.stats.costMicroUsd };
|
|
1130
1240
|
}
|
|
1241
|
+
if (err instanceof WorkflowAgentBlockedError) {
|
|
1242
|
+
rec.errorCode = WORKFLOW_SPAWN_BLOCKED_ERROR_CODE;
|
|
1243
|
+
rec.errorMessage = boundedRedactedSummary(err.message, MAX_TRANSCRIPT_CHARS);
|
|
1244
|
+
}
|
|
1131
1245
|
if (err instanceof WorkflowAgentStalledError && err.attempts > 1) {
|
|
1132
1246
|
rec.attempts = err.attempts;
|
|
1133
1247
|
rec.lastAttemptReason = "stalled";
|
|
@@ -1139,6 +1253,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1139
1253
|
sessionId: "",
|
|
1140
1254
|
status: "failed",
|
|
1141
1255
|
result: boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500),
|
|
1256
|
+
...(err instanceof WorkflowAgentBlockedError ? { errorCode: WORKFLOW_SPAWN_BLOCKED_ERROR_CODE } : {}),
|
|
1142
1257
|
stats: { turns: 0, tokens: 0, costMicroUsd: 0 },
|
|
1143
1258
|
}, label).catch(() => undefined);
|
|
1144
1259
|
bceTerminal(callKey, "failed", rec.output ?? (err instanceof Error ? err.message : String(err)), rec.sessionId, rec.stats);
|
|
@@ -1184,7 +1299,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1184
1299
|
if (!finalized) {
|
|
1185
1300
|
rec.status = "failed";
|
|
1186
1301
|
rec.endedAt = now();
|
|
1187
|
-
emit({ type: "agent_end", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), status: "failed", ts: rec.endedAt });
|
|
1302
|
+
emit({ type: "agent_end", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), status: "failed", ...(rec.errorCode !== undefined ? { errorCode: rec.errorCode } : {}), ts: rec.endedAt });
|
|
1188
1303
|
void persist("update");
|
|
1189
1304
|
}
|
|
1190
1305
|
};
|
|
@@ -1205,7 +1320,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1205
1320
|
const baseRunSpec = agentOpts.schema
|
|
1206
1321
|
? { ...framedSpec, ...authInherit, signal: effectiveSignal, outputSchema: agentOpts.schema }
|
|
1207
1322
|
: { ...framedSpec, ...authInherit, signal: effectiveSignal };
|
|
1208
|
-
const
|
|
1323
|
+
const preReviewSpec = childSessionId !== undefined ? { ...baseRunSpec, sessionId: childSessionId } : baseRunSpec;
|
|
1324
|
+
const runSpec = await reviewSpawnBeforeLaunch("ctx.agentStream", label, callKey, preReviewSpec, effectiveSignal);
|
|
1209
1325
|
const enrichedForwardS = opts.onForwardEvent !== undefined
|
|
1210
1326
|
? (e) => {
|
|
1211
1327
|
opts.onForwardEvent(e.type === "task_progress" ? { ...e, workflowRunId: runId, workflowAgentLabel: label } : e);
|
|
@@ -1216,6 +1332,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1216
1332
|
...(opts.parentCwd !== undefined ? { parentCwd: opts.parentCwd } : {}),
|
|
1217
1333
|
...spawnAttribution,
|
|
1218
1334
|
...(enrichedForwardS !== undefined ? { onForwardEvent: enrichedForwardS } : {}),
|
|
1335
|
+
delegationTaskType: "workflow",
|
|
1219
1336
|
agentName: label,
|
|
1220
1337
|
onActivity,
|
|
1221
1338
|
onWorkspaceResolved: createWorkspaceObserver(rec),
|
|
@@ -1236,6 +1353,18 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1236
1353
|
stream = workflowDepthStore.run({ depth: depth + 1 }, () => runner.runTaskStream(runSpec, undefined, runInternals));
|
|
1237
1354
|
}
|
|
1238
1355
|
catch (err) {
|
|
1356
|
+
if (!finalized && err instanceof WorkflowAgentBlockedError) {
|
|
1357
|
+
rec.errorCode = WORKFLOW_SPAWN_BLOCKED_ERROR_CODE;
|
|
1358
|
+
rec.errorMessage = boundedRedactedSummary(err.message, MAX_TRANSCRIPT_CHARS);
|
|
1359
|
+
void journalAppend(callKey, {
|
|
1360
|
+
taskId: callKey,
|
|
1361
|
+
sessionId: "",
|
|
1362
|
+
status: "failed",
|
|
1363
|
+
result: boundedRedactedSummary(err.message, 500),
|
|
1364
|
+
errorCode: WORKFLOW_SPAWN_BLOCKED_ERROR_CODE,
|
|
1365
|
+
stats: { turns: 0, tokens: 0, costMicroUsd: 0 },
|
|
1366
|
+
}).catch(() => undefined);
|
|
1367
|
+
}
|
|
1239
1368
|
recordFailed();
|
|
1240
1369
|
releaseOnce();
|
|
1241
1370
|
bceTerminal(callKey, "failed", err instanceof Error ? err.message : String(err), undefined, undefined);
|
package/dist/prompts/default.js
CHANGED
|
@@ -183,7 +183,7 @@ Use this directory for ALL temporary file needs:
|
|
|
183
183
|
|
|
184
184
|
Only use \`/tmp\` if the user explicitly requests it.
|
|
185
185
|
|
|
186
|
-
The scratchpad directory is session-specific, isolated from the user's project, and can generally be used without permission prompts.`;
|
|
186
|
+
The scratchpad directory is session-specific, isolated from the user's project, and can generally be used without permission prompts. Treat it as ephemeral — it may not survive a long suspension or a resume on a different worker; keep durable outputs in the working directory.`;
|
|
187
187
|
}
|
|
188
188
|
export const GIT_STATUS_MAX_CHARS = 2000;
|
|
189
189
|
export function buildGitSnapshot(p) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { type Checkpoint, type CheckpointFaultMode, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type ReopenReason, type ResolveExpectation, type ResumeOutcome } from "../../core/checkpoint-store.js";
|
|
1
|
+
import { type Checkpoint, type PendingSteerInput, type CheckpointFaultMode, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type ReopenReason, type ResolveExpectation, type ResumeOutcome } from "../../core/checkpoint-store.js";
|
|
2
2
|
export interface FileCheckpointStoreOptions {
|
|
3
3
|
fsync?: boolean;
|
|
4
4
|
compactEvery?: number;
|
|
5
5
|
}
|
|
6
6
|
export declare class FileCheckpointStore implements CheckpointStore {
|
|
7
|
+
readonly durability: "durable";
|
|
7
8
|
private readonly fsyncEnabled;
|
|
8
9
|
private readonly compactEvery;
|
|
9
10
|
private readonly ledger;
|
|
@@ -17,10 +18,7 @@ export declare class FileCheckpointStore implements CheckpointStore {
|
|
|
17
18
|
get(token: CheckpointToken): Promise<Checkpoint | null>;
|
|
18
19
|
resolve(token: CheckpointToken, scope: string, outcome: ResumeOutcome, expect?: ResolveExpectation): Promise<boolean>;
|
|
19
20
|
reopen(token: CheckpointToken, scope: string, reason: ReopenReason): Promise<boolean>;
|
|
20
|
-
setPendingSteer(token: CheckpointToken, scope: string, steer:
|
|
21
|
-
text: string;
|
|
22
|
-
trusted: boolean;
|
|
23
|
-
}): Promise<boolean>;
|
|
21
|
+
setPendingSteer(token: CheckpointToken, scope: string, steer: PendingSteerInput): Promise<boolean>;
|
|
24
22
|
expire(token: CheckpointToken, scope: string): Promise<boolean>;
|
|
25
23
|
reap(scope: string, cutoff: number): Promise<number>;
|
|
26
24
|
listByScope(scope: string): Promise<CheckpointSummary[]>;
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import { CheckpointError, checkpointOccMatches, checkpointRowMatches, summarizeCheckpoint, validatePendingSteer, winnerFromOutcome, } from "../../core/checkpoint-store.js";
|
|
2
|
+
import { appendPendingSteer, CheckpointError, checkpointOccMatches, checkpointRowMatches, summarizeCheckpoint, validatePendingSteer, winnerFromOutcome, } from "../../core/checkpoint-store.js";
|
|
3
3
|
import { SharedLedgerTable } from "./shared-ledger.js";
|
|
4
|
+
const CHECKPOINT_LEDGER_EVENT_REGISTRY = {
|
|
5
|
+
put: true,
|
|
6
|
+
resolve: true,
|
|
7
|
+
reopen: true,
|
|
8
|
+
expire: true,
|
|
9
|
+
steer: true,
|
|
10
|
+
steer_append: true,
|
|
11
|
+
};
|
|
12
|
+
const KNOWN_CHECKPOINT_LEDGER_EVENTS = new Set(Object.keys(CHECKPOINT_LEDGER_EVENT_REGISTRY));
|
|
4
13
|
function applyCheckpointEvent(cps, ev) {
|
|
14
|
+
if (!KNOWN_CHECKPOINT_LEDGER_EVENTS.has(ev.t)) {
|
|
15
|
+
throw new CheckpointError("checkpoint.unsupported_version", `file checkpoint ledger: unrecognised event kind "${String(ev.t)}" — ` +
|
|
16
|
+
`it was written by a newer worker; refusing the replay rather than serving an incomplete authority`);
|
|
17
|
+
}
|
|
5
18
|
if (ev.t === "put") {
|
|
6
19
|
cps.set(ev.token, ev.cp);
|
|
7
20
|
return;
|
|
@@ -28,6 +41,17 @@ function applyCheckpointEvent(cps, ev) {
|
|
|
28
41
|
case "steer":
|
|
29
42
|
cp.state.pendingSteer = ev.steer;
|
|
30
43
|
break;
|
|
44
|
+
case "steer_append": {
|
|
45
|
+
const queue = cp.state.pendingSteerQueue ?? [];
|
|
46
|
+
if (!queue.some((e) => e.inputId === ev.entry.inputId))
|
|
47
|
+
cp.state.pendingSteerQueue = [...queue, ev.entry];
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
default: {
|
|
51
|
+
const _exhaustive = ev;
|
|
52
|
+
void _exhaustive;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
31
55
|
}
|
|
32
56
|
}
|
|
33
57
|
const checkpointLedgers = new SharedLedgerTable({
|
|
@@ -35,6 +59,7 @@ const checkpointLedgers = new SharedLedgerTable({
|
|
|
35
59
|
apply: applyCheckpointEvent,
|
|
36
60
|
});
|
|
37
61
|
export class FileCheckpointStore {
|
|
62
|
+
durability = "durable";
|
|
38
63
|
fsyncEnabled;
|
|
39
64
|
compactEvery;
|
|
40
65
|
ledger;
|
|
@@ -114,7 +139,11 @@ export class FileCheckpointStore {
|
|
|
114
139
|
if (!checkpointRowMatches(cp, scope, "pending")) {
|
|
115
140
|
return false;
|
|
116
141
|
}
|
|
117
|
-
|
|
142
|
+
const next = appendPendingSteer(cp.state, clean);
|
|
143
|
+
const appended = next[next.length - 1];
|
|
144
|
+
if (appended === undefined || appended.inputId !== clean.inputId)
|
|
145
|
+
return true;
|
|
146
|
+
this.commit({ t: "steer_append", token, entry: appended }, this.fsyncEnabled);
|
|
118
147
|
return true;
|
|
119
148
|
});
|
|
120
149
|
}
|
|
@@ -45,7 +45,7 @@ export class FileStorageBackend {
|
|
|
45
45
|
const corruptRead = opts.onCorruptRead !== undefined ? { onCorruptRead: opts.onCorruptRead } : undefined;
|
|
46
46
|
const repo = new FileSessionRepo(this.root, corruptRead);
|
|
47
47
|
this.fileSessions = repo;
|
|
48
|
-
this.ttl = new TtlSessionStore({ repo, evict: opts.evict ?? "forget" });
|
|
48
|
+
this.ttl = new TtlSessionStore({ repo, evict: opts.evict ?? "forget", durability: "durable" });
|
|
49
49
|
this.sessionStore = this.ttl;
|
|
50
50
|
this.fileCheckpoints = new FileCheckpointStore(this.root, opts.checkpoint);
|
|
51
51
|
this.checkpointStore = this.fileCheckpoints;
|
|
@@ -24,5 +24,7 @@ export declare class FileSessionRepo implements SessionRepo {
|
|
|
24
24
|
delete(metadata: SessionMetadata): Promise<void>;
|
|
25
25
|
fork(sourceMetadata: SessionMetadata, options?: SessionForkOptions): Promise<Session>;
|
|
26
26
|
exportEntries(sessionId: string): Promise<SessionTreeEntry[]>;
|
|
27
|
-
importEntries(sessionId: string, owner: string | undefined, entries: SessionTreeEntry[]
|
|
27
|
+
importEntries(sessionId: string, owner: string | undefined, entries: SessionTreeEntry[], options?: {
|
|
28
|
+
preserveActorAssertions?: boolean;
|
|
29
|
+
}): Promise<void>;
|
|
28
30
|
}
|
|
@@ -191,9 +191,9 @@ export class FileSessionRepo {
|
|
|
191
191
|
async exportEntries(sessionId) {
|
|
192
192
|
return this.read(sessionId).entries;
|
|
193
193
|
}
|
|
194
|
-
async importEntries(sessionId, owner, entries) {
|
|
194
|
+
async importEntries(sessionId, owner, entries, options) {
|
|
195
195
|
void owner;
|
|
196
|
-
const validated = validateEntriesForImport(entries);
|
|
196
|
+
const validated = validateEntriesForImport(entries, options);
|
|
197
197
|
evictSharedSessionStorage(canonicalStoreKey(this.pathFor(sessionId)));
|
|
198
198
|
const createdAt = new Date().toISOString();
|
|
199
199
|
const lines = [JSON.stringify({ kind: "meta", id: sessionId, createdAt })];
|
|
@@ -88,7 +88,14 @@ export class SharedLedgerTable {
|
|
|
88
88
|
}
|
|
89
89
|
const core = new LedgerCore(key, paths, this.model, this.live);
|
|
90
90
|
this.live.set(key, core);
|
|
91
|
-
|
|
91
|
+
try {
|
|
92
|
+
core.bootstrap();
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
if (this.live.get(key) === core)
|
|
96
|
+
this.live.delete(key);
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
92
99
|
return core;
|
|
93
100
|
}
|
|
94
101
|
}
|
|
@@ -21,3 +21,6 @@ export declare function formatOutOfRootReadApprovalOption(directory: string): st
|
|
|
21
21
|
export declare function classifyCompoundReadonlyDetailed(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): CompoundReadonlyVerdict;
|
|
22
22
|
export declare function classifySimpleCommandReadBoundary(command: string, boundary: BashReadonlyRootBoundary): CompoundReadonlyVerdict;
|
|
23
23
|
export declare function classifyCompoundReadonly(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): string | undefined;
|
|
24
|
+
export declare const POLL_LOOP_MAX_BEATS = 120;
|
|
25
|
+
export declare const POLL_LOOP_MAX_SLEEP_SECONDS = 600;
|
|
26
|
+
export declare function classifyBoundedReadonlyPollLoop(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): string | undefined;
|
|
@@ -462,3 +462,97 @@ export function classifySimpleCommandReadBoundary(command, boundary) {
|
|
|
462
462
|
export function classifyCompoundReadonly(command, allow, boundary) {
|
|
463
463
|
return classifyCompoundReadonlyDetailed(command, allow, boundary).reason;
|
|
464
464
|
}
|
|
465
|
+
export const POLL_LOOP_MAX_BEATS = 120;
|
|
466
|
+
export const POLL_LOOP_MAX_SLEEP_SECONDS = 600;
|
|
467
|
+
const POLL_BODY_HARD_REJECT = /[<>$()`|&{}\n\r\\]/;
|
|
468
|
+
const NON_ASCII_WHITESPACE = /[^\S \t]/;
|
|
469
|
+
const POLL_LOOP_SHAPE = /^for[ \t]+([A-Za-z_][A-Za-z0-9_]*)[ \t]+in[ \t]+(.+?)[ \t]*;[ \t]*do[ \t]+(.+?)[ \t]*;[ \t]*done[ \t]*;?$/;
|
|
470
|
+
function pollLoopQuotesBalanced(s) {
|
|
471
|
+
let open;
|
|
472
|
+
for (const ch of s) {
|
|
473
|
+
if (open === undefined && (ch === '"' || ch === "'"))
|
|
474
|
+
open = ch;
|
|
475
|
+
else if (open === ch)
|
|
476
|
+
open = undefined;
|
|
477
|
+
}
|
|
478
|
+
return open === undefined;
|
|
479
|
+
}
|
|
480
|
+
function pollLoopBeatsOf(iterable) {
|
|
481
|
+
const seq = /^\$\(seq[ \t]+(\d{1,7})[ \t]+(\d{1,7})\)$/.exec(iterable);
|
|
482
|
+
const brace = seq === null ? /^\{(\d{1,7})\.\.(\d{1,7})\}$/.exec(iterable) : null;
|
|
483
|
+
const range = seq ?? brace;
|
|
484
|
+
if (range !== null) {
|
|
485
|
+
const lo = Number(range[1]);
|
|
486
|
+
const hi = Number(range[2]);
|
|
487
|
+
return hi >= lo ? hi - lo + 1 : undefined;
|
|
488
|
+
}
|
|
489
|
+
const items = iterable.split(/[ \t]+/);
|
|
490
|
+
return items.every((t) => /^[A-Za-z0-9_.-]{1,64}$/.test(t)) ? items.length : undefined;
|
|
491
|
+
}
|
|
492
|
+
function pollLoopSleepReason(segment) {
|
|
493
|
+
const toks = segment.split(/[ \t]+/);
|
|
494
|
+
if (toks.length !== 2)
|
|
495
|
+
return "`sleep` in a poll loop must take exactly one literal numeric argument";
|
|
496
|
+
const v = toks[1];
|
|
497
|
+
if (!/^\d+(\.\d+)?$/.test(v) || v.length > 8 || Number(v) > POLL_LOOP_MAX_SLEEP_SECONDS) {
|
|
498
|
+
return `\`sleep ${v}\` is not a literal duration within the ${POLL_LOOP_MAX_SLEEP_SECONDS}s per-beat cap — not auto-allowed`;
|
|
499
|
+
}
|
|
500
|
+
return undefined;
|
|
501
|
+
}
|
|
502
|
+
export function classifyBoundedReadonlyPollLoop(command, allow, boundary) {
|
|
503
|
+
if (NON_ASCII_WHITESPACE.test(command)) {
|
|
504
|
+
return "the command contains a non-ASCII or control whitespace character (only space and tab are allowed) — its word boundaries cannot be read the way bash would split them, so it is not auto-allowed";
|
|
505
|
+
}
|
|
506
|
+
const trimmed = command.trim();
|
|
507
|
+
if (!trimmed)
|
|
508
|
+
return "empty command";
|
|
509
|
+
if (!pollLoopQuotesBalanced(trimmed)) {
|
|
510
|
+
return "unbalanced quote — the command's structure cannot be read reliably, so it is not auto-allowed";
|
|
511
|
+
}
|
|
512
|
+
const m = POLL_LOOP_SHAPE.exec(trimmed);
|
|
513
|
+
if (m === null) {
|
|
514
|
+
return "not a bounded read-only poll loop (`for <v> in <literal bound>; do <read commands>; done` is the only accepted control structure)";
|
|
515
|
+
}
|
|
516
|
+
const varName = m[1];
|
|
517
|
+
const iterable = m[2];
|
|
518
|
+
const body = m[3];
|
|
519
|
+
if (!/^[a-z]$/.test(varName)) {
|
|
520
|
+
return `the loop variable "${varName}" must be a single lowercase letter — a name like PATH/IFS/LD_PRELOAD would change how the body's commands resolve`;
|
|
521
|
+
}
|
|
522
|
+
const beats = pollLoopBeatsOf(iterable);
|
|
523
|
+
if (beats === undefined) {
|
|
524
|
+
return `the loop bound "${iterable}" is not a recognized literal — only \`$(seq <int> <int>)\`, \`{<int>..<int>}\` (ascending) or a literal word list is accepted`;
|
|
525
|
+
}
|
|
526
|
+
if (beats > POLL_LOOP_MAX_BEATS) {
|
|
527
|
+
return `the loop runs ${beats} iterations, above the ${POLL_LOOP_MAX_BEATS}-iteration cap for auto-allow`;
|
|
528
|
+
}
|
|
529
|
+
if (POLL_BODY_HARD_REJECT.test(body)) {
|
|
530
|
+
return "the loop body may not contain redirection, pipes, backgrounding, substitution, subshells, braces, escapes, or line breaks";
|
|
531
|
+
}
|
|
532
|
+
const readSegments = [];
|
|
533
|
+
for (const rawSegment of body.split(";")) {
|
|
534
|
+
const segment = rawSegment.trim();
|
|
535
|
+
if (segment.length === 0)
|
|
536
|
+
return "empty command in the loop body";
|
|
537
|
+
const parsed = parseLeadingCommandName(segment);
|
|
538
|
+
if ("reject" in parsed)
|
|
539
|
+
return parsed.reject;
|
|
540
|
+
if (parsed.name === "sleep") {
|
|
541
|
+
const sleepReason = pollLoopSleepReason(segment);
|
|
542
|
+
if (sleepReason !== undefined)
|
|
543
|
+
return sleepReason;
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
readSegments.push(segment);
|
|
547
|
+
}
|
|
548
|
+
if (readSegments.length === 0) {
|
|
549
|
+
return "the loop body has no read command — a sleep-only loop observes nothing and is not auto-allowed";
|
|
550
|
+
}
|
|
551
|
+
const verdict = classifyCompoundReadonlyDetailed(readSegments.join("; "), allow, boundary);
|
|
552
|
+
if (verdict.reason !== undefined)
|
|
553
|
+
return verdict.reason;
|
|
554
|
+
if (verdict.undecidedPaths !== undefined) {
|
|
555
|
+
return `the loop body carries an unexpanded glob (${verdict.undecidedPaths.join(", ")}) — what a REPEATED read touches is decided at run time, so it is not auto-allowed`;
|
|
556
|
+
}
|
|
557
|
+
return undefined;
|
|
558
|
+
}
|