@sema-agent/core 2.8.0 → 2.10.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/dist/agents/send-message-tool.js +37 -29
- package/dist/agents/subagent.js +91 -4
- package/dist/brain/circuit-breaker.js +18 -8
- package/dist/brain/retry.d.ts +1 -0
- package/dist/brain/retry.js +29 -7
- package/dist/brain/stream-engine.d.ts +1 -0
- package/dist/brain/stream-engine.js +74 -12
- package/dist/config/defaults.d.ts +1 -0
- package/dist/config/defaults.js +1 -0
- package/dist/core/auto-compaction.js +9 -1
- package/dist/core/background-agent-store.d.ts +2 -0
- package/dist/core/background-agent-store.js +20 -0
- package/dist/core/mcp.js +8 -5
- package/dist/core/runner/assemble-result.d.ts +1 -0
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/prepare-task.d.ts +2 -1
- package/dist/core/runner/prepare-task.js +61 -20
- package/dist/core/runner/runtask.js +38 -12
- package/dist/core/runner/tool-disclosure.d.ts +8 -3
- package/dist/core/runner/tool-disclosure.js +39 -10
- package/dist/core/skills-directory.d.ts +1 -1
- package/dist/core/skills-directory.js +257 -28
- package/dist/core/task-registry-agent.d.ts +2 -1
- package/dist/core/task-registry-agent.js +50 -54
- package/dist/core/task-registry.d.ts +1 -0
- package/dist/core/task-registry.js +1 -1
- package/dist/core/types.d.ts +9 -1
- package/dist/engine/compaction/compaction.js +71 -20
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/internal/harness.d.ts +1 -1
- package/dist/internal/harness.js +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +1 -0
- package/dist/orchestration/run-workflow-tool.js +4 -1
- package/dist/orchestration/workflow.d.ts +1 -1
- package/dist/orchestration/workflow.js +20 -12
- package/dist/tools/fs/bash-readonly-classifier.js +83 -17
- package/dist/tools/fs/fs-bash.js +17 -11
- package/dist/tools/fs/fs-shared.d.ts +1 -0
- package/dist/tools/fs/fs-shared.js +44 -2
- package/dist/tools/web.d.ts +15 -0
- package/dist/tools/web.js +42 -0
- package/package.json +1 -1
|
@@ -256,7 +256,7 @@ export function findTurnStartIndex(entries, entryIndex, startIndex) {
|
|
|
256
256
|
}
|
|
257
257
|
return -1;
|
|
258
258
|
}
|
|
259
|
-
function
|
|
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
|
|
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
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { createSkillsFromDirectory, type SkillsDirectoryOptions, type SkillsDire
|
|
|
7
7
|
export { REPORT_FINDINGS_TOOL_NAME, type ReportedFinding } from "./core/runner/synthetic-tools.js";
|
|
8
8
|
export { formatToolError, formatZodValidationError, formatValidationPath, truncateError, errorClassOf } from "./core/tool-errors.js";
|
|
9
9
|
export type { WorkerErrorClass } from "./core/tool-errors.js";
|
|
10
|
-
export { createWebFetchTool, webFetchToolSpec, htmlToText, type WebFetchConfig, createWebSearchTool, type WebSearchConfig, createWebFetchSummarizer, WEBFETCH_SUMMARY_MAX_CONTENT, WEBFETCH_SUMMARY_GUIDELINES, } from "./tools/web.js";
|
|
10
|
+
export { createWebFetchTool, webFetchToolSpec, htmlToText, type WebFetchConfig, createWebSearchTool, type WebSearchConfig, createSearxngSearchBackend, probeSearchBackend, type SearxngBackendOptions, createWebFetchSummarizer, WEBFETCH_SUMMARY_MAX_CONTENT, WEBFETCH_SUMMARY_GUIDELINES, } from "./tools/web.js";
|
|
11
11
|
export { createTodoWriteTool } from "./tools/todo.js";
|
|
12
12
|
export { createTaskListTools, createMemoryTaskListStore, assertJsonMetadata, type TaskListItem, type TaskListStore } from "./tools/task-list.js";
|
|
13
13
|
export { assembleCodeTools, type CodeToolsConfig, CODE_ROLE } from "./scenarios/full-body.js";
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { SKILL_TOOL_NAME } from "./core/runner/synthetic-tools.js";
|
|
|
5
5
|
export { createSkillsFromDirectory, } from "./core/skills-directory.js";
|
|
6
6
|
export { REPORT_FINDINGS_TOOL_NAME } from "./core/runner/synthetic-tools.js";
|
|
7
7
|
export { formatToolError, formatZodValidationError, formatValidationPath, truncateError, errorClassOf } from "./core/tool-errors.js";
|
|
8
|
-
export { createWebFetchTool, webFetchToolSpec, htmlToText, createWebSearchTool, createWebFetchSummarizer, WEBFETCH_SUMMARY_MAX_CONTENT, WEBFETCH_SUMMARY_GUIDELINES, } from "./tools/web.js";
|
|
8
|
+
export { createWebFetchTool, webFetchToolSpec, htmlToText, createWebSearchTool, createSearxngSearchBackend, probeSearchBackend, createWebFetchSummarizer, WEBFETCH_SUMMARY_MAX_CONTENT, WEBFETCH_SUMMARY_GUIDELINES, } from "./tools/web.js";
|
|
9
9
|
export { createTodoWriteTool } from "./tools/todo.js";
|
|
10
10
|
export { createTaskListTools, createMemoryTaskListStore, assertJsonMetadata } from "./tools/task-list.js";
|
|
11
11
|
export { assembleCodeTools, CODE_ROLE } from "./scenarios/full-body.js";
|
|
@@ -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";
|
package/dist/internal/harness.js
CHANGED
|
@@ -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):
|
|
21
|
+
export declare function assertSupportedAgentIsolation(isolation: unknown): void;
|
|
22
22
|
export interface WorkflowFanOutSlotError {
|
|
23
23
|
index: number;
|
|
24
24
|
kind: string;
|
|
@@ -9,6 +9,7 @@ import { combinePolicies, createAllowDenyPolicy } from "../core/tool-policy.js";
|
|
|
9
9
|
import { callKeyOrdinal, oversizeJournalResult, journalOversizeTombstone, JOURNAL_OVERSIZE_ERROR_CODE, MAX_JOURNAL_RESULT_BYTES } from "../core/workflow-journal-store.js";
|
|
10
10
|
import { isWorkflowRunActive, closeWorkflowChannel, markWorkflowActive, publishWorkflowEvent } from "./workflow-observe.js";
|
|
11
11
|
import { isDurablePause, mapNestedSuspend } from "../agents/suspend-guard.js";
|
|
12
|
+
import { RUNNING_AGENT_OBSERVE_EVERY_BEATS } from "../config/defaults.js";
|
|
12
13
|
import { boundInputHashOf } from "../core/canonical-json.js";
|
|
13
14
|
import { boundedRedactedSummary } from "../core/untrusted-egress.js";
|
|
14
15
|
import { delimitUntrusted } from "../core/untrusted-text.js";
|
|
@@ -20,7 +21,6 @@ const MAX_TRANSCRIPT_CHARS = 4000;
|
|
|
20
21
|
const WORKFLOW_RESULT_MAX = 4000;
|
|
21
22
|
const WORKFLOW_RESULT_FULL_MAX = 200_000;
|
|
22
23
|
const MAX_ACTIVITY = 30;
|
|
23
|
-
const RUNNING_AGENT_PERSIST_EVERY_BEATS = 4;
|
|
24
24
|
function workflowModelLabel(spec) {
|
|
25
25
|
const model = spec.model;
|
|
26
26
|
if (model === undefined)
|
|
@@ -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
|
|
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";
|
|
@@ -453,16 +453,17 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
453
453
|
};
|
|
454
454
|
const waIdOf = (callKey) => `wa${createHash("sha256").update(`${runId}:${callKey}`).digest("hex").slice(0, 16)}`;
|
|
455
455
|
const bceLive = new Map();
|
|
456
|
-
const bceSpawn = (callKey, label, agentType, replayed) => {
|
|
456
|
+
const bceSpawn = (callKey, label, agentType, replayed, sessionId) => {
|
|
457
457
|
if (!bceSink)
|
|
458
458
|
return;
|
|
459
459
|
const id = waIdOf(callKey);
|
|
460
|
-
bceLive.set(id, { callKey, label, ...(agentType !== undefined ? { agentType } : {}) });
|
|
460
|
+
bceLive.set(id, { callKey, label, ...(agentType !== undefined ? { agentType } : {}), ...(sessionId !== undefined ? { sessionId } : {}) });
|
|
461
461
|
bceEmit({
|
|
462
462
|
kind: "spawn",
|
|
463
463
|
taskId: id,
|
|
464
464
|
sessionScoped: false,
|
|
465
465
|
owner: runId,
|
|
466
|
+
...(sessionId !== undefined ? { sessionId, transcriptId: sessionId } : {}),
|
|
466
467
|
...(scope !== undefined ? { scope } : {}),
|
|
467
468
|
description: replayed ? `${label} (replayed)` : label,
|
|
468
469
|
agentType: agentType ?? "workflow-agent",
|
|
@@ -474,6 +475,11 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
474
475
|
startedAt: Date.now(),
|
|
475
476
|
});
|
|
476
477
|
};
|
|
478
|
+
const bceBindSession = (callKey, sessionId) => {
|
|
479
|
+
const row = bceLive.get(waIdOf(callKey));
|
|
480
|
+
if (row !== undefined)
|
|
481
|
+
row.sessionId = sessionId;
|
|
482
|
+
};
|
|
477
483
|
const bceTick = (callKey, e) => {
|
|
478
484
|
if (!bceSink)
|
|
479
485
|
return;
|
|
@@ -489,6 +495,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
489
495
|
workflowRunId: runId,
|
|
490
496
|
...(scope !== undefined ? { scope } : {}),
|
|
491
497
|
...(row.agentType !== undefined ? { agentType: row.agentType } : { agentType: "workflow-agent" }),
|
|
498
|
+
...(row.sessionId !== undefined ? { sessionId: row.sessionId, transcriptId: row.sessionId } : {}),
|
|
492
499
|
name: e.name ?? row.label,
|
|
493
500
|
progressTaskId: e.taskId,
|
|
494
501
|
...(e.parentTaskId !== undefined ? { progressParentTaskId: e.parentTaskId } : {}),
|
|
@@ -635,7 +642,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
635
642
|
rec.toolCalls = (rec.toolCalls ?? 0) + 1;
|
|
636
643
|
rec.activity = tail;
|
|
637
644
|
beatCount += 1;
|
|
638
|
-
if (beatCount === 1 || beatCount %
|
|
645
|
+
if (beatCount === 1 || beatCount % RUNNING_AGENT_OBSERVE_EVERY_BEATS === 0) {
|
|
639
646
|
void persist("update");
|
|
640
647
|
}
|
|
641
648
|
};
|
|
@@ -768,7 +775,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
768
775
|
agentPhaseOf.set(replayRec, phaseInstance);
|
|
769
776
|
emit({ type: "agent_start", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), callKey, prompt, ...(model !== undefined ? { model } : {}), replayed: true, ts: at });
|
|
770
777
|
emit({ type: "agent_end", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), status: replayRec.status, output: cachedOutput, ...(rs.toolCalls !== undefined ? { toolCalls: rs.toolCalls } : {}), replayed: true, ts: at });
|
|
771
|
-
bceSpawn(callKey, label, agentOpts.agentType, true);
|
|
778
|
+
bceSpawn(callKey, label, agentOpts.agentType, true, r.sessionId || undefined);
|
|
772
779
|
bceTerminal(callKey, replayRec.status === "completed" ? "completed" : "failed", cachedOutput, r.sessionId || undefined, replayRec.stats);
|
|
773
780
|
accumulateStats(r, false);
|
|
774
781
|
void persist("update");
|
|
@@ -810,7 +817,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
810
817
|
if (finalized)
|
|
811
818
|
throw new Error("workflow run already finalized — ctx.agent cannot spawn after the run ended");
|
|
812
819
|
rec.startedAt = now();
|
|
813
|
-
|
|
820
|
+
const bornChildSessionId = resolveChildSessionIdAtSpawn(spec);
|
|
821
|
+
bceSpawn(callKey, label, agentOpts.agentType, false, bornChildSessionId);
|
|
814
822
|
const typedSpec0 = applyWorkflowAgentType(spec, agentOpts.agentType, agentRegistry);
|
|
815
823
|
const typedSpec = typedSpec0.model === undefined && inheritedModelSnap !== undefined ? { ...typedSpec0, model: inheritedModelSnap } : typedSpec0;
|
|
816
824
|
const framedSpec = withWorkflowChildPersona(typedSpec, agentOpts.schema ?? typedSpec.outputSchema);
|
|
@@ -852,10 +860,11 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
852
860
|
const onCallerAbort = () => attemptCtl.abort(new Error("workflow aborted"));
|
|
853
861
|
effectiveSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
854
862
|
armWatchdog();
|
|
855
|
-
const attemptSessionId = resolveChildSessionIdAtSpawn(runSpec);
|
|
863
|
+
const attemptSessionId = attempts === 1 ? bornChildSessionId : resolveChildSessionIdAtSpawn(runSpec);
|
|
856
864
|
const attemptSpec = attemptSessionId !== undefined ? { ...runSpec, sessionId: attemptSessionId, signal: attemptCtl.signal } : { ...runSpec, signal: attemptCtl.signal };
|
|
857
865
|
if (attemptSessionId !== undefined && !finalized) {
|
|
858
866
|
rec.sessionId = attemptSessionId;
|
|
867
|
+
bceBindSession(callKey, attemptSessionId);
|
|
859
868
|
void persist("update");
|
|
860
869
|
}
|
|
861
870
|
const attemptInternals = {
|
|
@@ -1143,9 +1152,9 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1143
1152
|
throw new Error(finalized ? "workflow run already finalized — ctx.agentStream cannot spawn after the run ended" : "workflow aborted");
|
|
1144
1153
|
}
|
|
1145
1154
|
rec.startedAt = now();
|
|
1146
|
-
|
|
1155
|
+
const childSessionId = resolveChildSessionIdAtSpawn(spec);
|
|
1156
|
+
bceSpawn(callKey, label, agentOpts.agentType, false, childSessionId);
|
|
1147
1157
|
let stream;
|
|
1148
|
-
let childSessionId;
|
|
1149
1158
|
try {
|
|
1150
1159
|
const typedSpec0 = applyWorkflowAgentType(spec, agentOpts.agentType, agentRegistry);
|
|
1151
1160
|
const typedSpec = typedSpec0.model === undefined && inheritedModelSnap !== undefined ? { ...typedSpec0, model: inheritedModelSnap } : typedSpec0;
|
|
@@ -1154,7 +1163,6 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1154
1163
|
const baseRunSpec = agentOpts.schema
|
|
1155
1164
|
? { ...framedSpec, ...authInherit, signal: effectiveSignal, outputSchema: agentOpts.schema }
|
|
1156
1165
|
: { ...framedSpec, ...authInherit, signal: effectiveSignal };
|
|
1157
|
-
childSessionId = resolveChildSessionIdAtSpawn(baseRunSpec);
|
|
1158
1166
|
const runSpec = childSessionId !== undefined ? { ...baseRunSpec, sessionId: childSessionId } : baseRunSpec;
|
|
1159
1167
|
const enrichedForwardS = opts.onForwardEvent !== undefined
|
|
1160
1168
|
? (e) => {
|
|
@@ -83,19 +83,80 @@ function resolveOperandLexically(base, operand, homeDir) {
|
|
|
83
83
|
return normalizeAbsPathLexicalEitherFamily(`${base.replace(/[/\\]+$/, "")}/${raw}`);
|
|
84
84
|
}
|
|
85
85
|
function tokenizeSegment(segment) {
|
|
86
|
-
|
|
86
|
+
const raw = segment
|
|
87
87
|
.trim()
|
|
88
88
|
.split(/\s+/)
|
|
89
|
-
.filter((t) => t.length > 0)
|
|
90
|
-
|
|
89
|
+
.filter((t) => t.length > 0);
|
|
90
|
+
return { folded: raw.map(foldQuoteRemovalToken), raw };
|
|
91
91
|
}
|
|
92
|
-
function
|
|
93
|
-
|
|
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 =
|
|
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(
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
295
|
+
const segmentTokens = tokenizeSegment(segments[si]);
|
|
296
|
+
const toks = segmentTokens.folded;
|
|
231
297
|
if (toks.length === 0)
|
|
232
298
|
continue;
|
|
233
|
-
foldedSegments.push(
|
|
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(
|
|
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
|
}
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
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__";
|
|
@@ -52,7 +52,10 @@ export const BASH_MAX_TIMEOUT_SEC = 600;
|
|
|
52
52
|
export const BASH_DEFAULT_TIMEOUT_MS = BASH_DEFAULT_TIMEOUT_SEC * 1000;
|
|
53
53
|
export const BASH_MAX_TIMEOUT_MS = BASH_MAX_TIMEOUT_SEC * 1000;
|
|
54
54
|
function validTimeoutMs(n) {
|
|
55
|
-
|
|
55
|
+
if (n === undefined || !Number.isFinite(n))
|
|
56
|
+
return undefined;
|
|
57
|
+
const floored = Math.floor(n);
|
|
58
|
+
return floored >= 1 ? floored : undefined;
|
|
56
59
|
}
|
|
57
60
|
export function resolveBashTimeoutCaps(opts) {
|
|
58
61
|
const defaultMs = validTimeoutMs(opts?.bashDefaultTimeoutMs) ??
|
|
@@ -80,7 +83,7 @@ export function clipShellOutput(s) {
|
|
|
80
83
|
return clipWithFilePointer(s, bashMaxOutputChars());
|
|
81
84
|
}
|
|
82
85
|
export async function writeShellOverflowFile(env, stdout, stderr) {
|
|
83
|
-
const tf = await env.createTempFile({ prefix:
|
|
86
|
+
const tf = await env.createTempFile({ prefix: SHELL_OVERFLOW_FILE_PREFIX, suffix: SHELL_OVERFLOW_FILE_SUFFIX });
|
|
84
87
|
if (!tf.ok)
|
|
85
88
|
return undefined;
|
|
86
89
|
const body = stderr.length > 0 ? `${stdout}${stdout.length > 0 && !stdout.endsWith("\n") ? "\n" : ""}--- stderr ---\n${stderr}` : stdout;
|
|
@@ -90,6 +93,45 @@ export async function writeShellOverflowFile(env, stdout, stderr) {
|
|
|
90
93
|
const canon = await env.canonicalPath(tf.value);
|
|
91
94
|
return canon.ok ? canon.value : tf.value;
|
|
92
95
|
}
|
|
96
|
+
const SHELL_OVERFLOW_FILE_PREFIX = "bash-output-";
|
|
97
|
+
const SHELL_OVERFLOW_FILE_SUFFIX = ".log";
|
|
98
|
+
function toPosixPathKey(p) {
|
|
99
|
+
return p.replace(/\\/g, "/").replace(/(.)\/+$/, "$1");
|
|
100
|
+
}
|
|
101
|
+
function isShellOverflowFileName(base) {
|
|
102
|
+
if (!base.startsWith(SHELL_OVERFLOW_FILE_PREFIX) || !base.endsWith(SHELL_OVERFLOW_FILE_SUFFIX))
|
|
103
|
+
return false;
|
|
104
|
+
const middle = base.slice(SHELL_OVERFLOW_FILE_PREFIX.length, base.length - SHELL_OVERFLOW_FILE_SUFFIX.length);
|
|
105
|
+
return /^[0-9A-Za-z][0-9A-Za-z._-]*$/.test(middle);
|
|
106
|
+
}
|
|
107
|
+
export function createShellOverflowSpoolFence(env) {
|
|
108
|
+
let tempArea;
|
|
109
|
+
const learnTempArea = async () => {
|
|
110
|
+
const probe = await env.createTempDir();
|
|
111
|
+
if (!probe.ok)
|
|
112
|
+
return undefined;
|
|
113
|
+
const canon = await env.canonicalPath(probe.value);
|
|
114
|
+
const resolved = toPosixPathKey(canon.ok ? canon.value : probe.value);
|
|
115
|
+
await env.remove(probe.value, { recursive: true, force: true });
|
|
116
|
+
const cut = resolved.lastIndexOf("/");
|
|
117
|
+
return cut > 0 ? resolved.slice(0, cut) : undefined;
|
|
118
|
+
};
|
|
119
|
+
return async (path) => {
|
|
120
|
+
const target = toPosixPathKey(path);
|
|
121
|
+
const cut = target.lastIndexOf("/");
|
|
122
|
+
if (cut <= 0 || !isShellOverflowFileName(target.slice(cut + 1)))
|
|
123
|
+
return false;
|
|
124
|
+
tempArea ??= learnTempArea();
|
|
125
|
+
const area = await tempArea;
|
|
126
|
+
if (area === undefined)
|
|
127
|
+
return false;
|
|
128
|
+
const parent = target.slice(0, cut);
|
|
129
|
+
if (parent === area)
|
|
130
|
+
return true;
|
|
131
|
+
const up = parent.lastIndexOf("/");
|
|
132
|
+
return up > 0 && parent.slice(0, up) === area;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
93
135
|
export function shellRecoveryHint(path, readOnly) {
|
|
94
136
|
const quoted = shellQuote(path);
|
|
95
137
|
const example = readOnly ? `tail -c 50000 ${quoted}` : `sed -n 'START,ENDp' ${quoted}`;
|
package/dist/tools/web.d.ts
CHANGED
|
@@ -29,3 +29,18 @@ export interface WebSearchConfig {
|
|
|
29
29
|
}
|
|
30
30
|
export declare function clipCodePoints(s: string, max: number): string;
|
|
31
31
|
export declare function createWebSearchTool(config: WebSearchConfig): ToolSpec;
|
|
32
|
+
export interface SearxngBackendOptions {
|
|
33
|
+
fetchImpl?: typeof fetch;
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
extraParams?: Record<string, string>;
|
|
36
|
+
}
|
|
37
|
+
export declare function createSearxngSearchBackend(baseUrl: string, options?: SearxngBackendOptions): WebSearchConfig["search"];
|
|
38
|
+
export declare function probeSearchBackend(search: WebSearchConfig["search"], options?: {
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
}): Promise<{
|
|
41
|
+
ok: true;
|
|
42
|
+
results: number;
|
|
43
|
+
} | {
|
|
44
|
+
ok: false;
|
|
45
|
+
error: string;
|
|
46
|
+
}>;
|