@sema-agent/core 5.18.1 → 5.20.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 +144 -0
- package/dist/agents/roster-store.js +3 -0
- package/dist/brain/circuit-breaker.js +14 -3
- package/dist/brain/timeout.d.ts +1 -0
- package/dist/brain/timeout.js +11 -0
- package/dist/core/background-agent-store.d.ts +1 -0
- package/dist/core/background-agent-store.js +5 -0
- package/dist/core/fs-write-gate-policy.js +1 -1
- package/dist/core/hooks.d.ts +3 -1
- package/dist/core/hooks.js +32 -0
- package/dist/core/mailbox-store.js +2 -0
- package/dist/core/mcp.d.ts +4 -0
- package/dist/core/mcp.js +58 -11
- package/dist/core/retention-policy.d.ts +7 -0
- package/dist/core/retention-policy.js +21 -0
- package/dist/core/runner/active-skill-scope.js +1 -1
- package/dist/core/runner/prepare-task.d.ts +3 -0
- package/dist/core/runner/prepare-task.js +195 -121
- package/dist/core/runner/runtask.js +29 -4
- package/dist/core/runner/session-rule-policy.js +1 -1
- package/dist/core/sensitive-path-policy.js +1 -1
- package/dist/core/task-registry-agent.js +2 -0
- package/dist/core/tool-policy.d.ts +1 -0
- package/dist/core/tool-policy.js +21 -12
- package/dist/core/workflow-run-store.js +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/orchestration/run-spec.js +5 -1
- package/dist/orchestration/workflow.js +13 -2
- package/dist/stores/file/background-agent-store.js +2 -1
- package/dist/stores/file/mailbox-store.js +2 -0
- package/dist/stores/file/workflow-run-store.js +2 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +5 -2
- package/dist/tools/fs/bash-readonly-classifier.js +129 -17
- package/dist/tools/web.js +32 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -116,7 +116,7 @@ export { parseAllowRuleText, formatAllowRuleText, ruleAdmitsCommand, findAdmitti
|
|
|
116
116
|
export { removePersistedRule, applyTombstones, sameScope, InMemoryPermissionRuleStore, EMPTY_RULE_STORE, } from "./core/permission-rule-store.js";
|
|
117
117
|
export { prepareCardApproval, confirmRuleApproval, redeemRuleTicket, redeemRuleBatch, prepareCcImport, prepareStarterBatch, mintRuleTicket, STARTER_RULES, InMemoryRuleApprovalRecordStore, } from "./core/permission-rule-consent.js";
|
|
118
118
|
export { FilePermissionRuleStoreProvider } from "./stores/file/permission-rule-store.js";
|
|
119
|
-
export { formatHookFeedback, runToolGate, createHookEnvCapabilities, } from "./core/hooks.js";
|
|
119
|
+
export { formatHookFeedback, runToolGate, createHookEnvCapabilities, createPreToolUseConstraintPolicy, } from "./core/hooks.js";
|
|
120
120
|
export { InMemoryMemoryStore, composeMemoryBlock, composeLayeredMemoryBlock, normalizeMemorySpec, supportsConsolidation, supportsPeriodicConsolidation, firstSentence, expandLexicalTerms, lexicalSearchMatch, classifyPromotable, enforcePromotableWriteGate, guardedMemoryStore, MemoryGateError, detectSecret, enforceSecretWriteGate, enforceStructuredNoteSecretGate, } from "./core/memory.js";
|
|
121
121
|
export { MemoryEngine, FileMemoryEngineBackend, memoryBackendContract, assertMemoryBackendSearchEquivalence, buildMemoryInstruction, truncateIndex, scanEntryFiles, deriveRepoKey, deriveRepoMemoryDir, deriveControlPlaneDir, deriveRepoControlPlaneDir, resolveMemoryEngineRoot, scopeDirName, ControlPlaneCorruptError, parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_INDEX_FILENAME, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_MAX_ENTRY_DEPTH, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE, renderAnnouncements, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, migrateScope, materializeEntriesToFiles, harvestFilesToPatches, projectionsToWriteBack, screenInboundEntries, REMOTE_HARVEST_PER_FILE_BYTES, REMOTE_MASS_DELETION_FUSE_RATIO, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, reconcileMemoryEntries, nextSyncBaseline, mergeRecallHits, syncMemoryScope, } from "./core/memory-engine/index.js";
|
|
122
122
|
export { SHARED_MEMORY_READ_CAP_BYTES, SHARED_MEMORY_LIST_PAGE_SIZE, SharedMemoryStoreError, } from "./core/shared-memory/types.js";
|
|
@@ -26,7 +26,7 @@ function frozenDenyPolicy(rootDir, frozenResolved) {
|
|
|
26
26
|
if (typeof raw !== "string" || raw.length === 0)
|
|
27
27
|
return { action: "allow" };
|
|
28
28
|
const absForm = isAbsolutePathForm(raw);
|
|
29
|
-
const abs = pathKey(absForm ? raw : resolve(rootDir, raw));
|
|
29
|
+
const abs = pathKey(absForm ? raw : resolve(req.cwd || rootDir, raw));
|
|
30
30
|
if (frozen.has(abs)) {
|
|
31
31
|
return { action: "deny", message: `${raw} is part of the frozen specification surface (read-only).` };
|
|
32
32
|
}
|
|
@@ -102,6 +102,9 @@ export async function runSpec(runner, contract, opts) {
|
|
|
102
102
|
const userHooks = opts.taskSpec.hooks && deployBaseline?.hooks
|
|
103
103
|
? {
|
|
104
104
|
preToolUse: (opts.taskSpec.hooks.preToolUse ?? deployBaseline.hooks.preToolUse)?.bind(opts.taskSpec.hooks.preToolUse ? opts.taskSpec.hooks : deployBaseline.hooks),
|
|
105
|
+
...((opts.taskSpec.hooks.preToolUse ? opts.taskSpec.hooks : deployBaseline.hooks).preToolUseObservational === true
|
|
106
|
+
? { preToolUseObservational: true }
|
|
107
|
+
: {}),
|
|
105
108
|
postToolUse: (opts.taskSpec.hooks.postToolUse ?? deployBaseline.hooks.postToolUse)?.bind(opts.taskSpec.hooks.postToolUse ? opts.taskSpec.hooks : deployBaseline.hooks),
|
|
106
109
|
userPromptSubmit: (opts.taskSpec.hooks.userPromptSubmit ?? deployBaseline.hooks.userPromptSubmit)?.bind(opts.taskSpec.hooks.userPromptSubmit ? opts.taskSpec.hooks : deployBaseline.hooks),
|
|
107
110
|
stop: (opts.taskSpec.hooks.stop ?? deployBaseline.hooks.stop)?.bind(opts.taskSpec.hooks.stop ? opts.taskSpec.hooks : deployBaseline.hooks),
|
|
@@ -118,6 +121,7 @@ export async function runSpec(runner, contract, opts) {
|
|
|
118
121
|
let gateAbandoned = false;
|
|
119
122
|
const hooks = {
|
|
120
123
|
...(userHooks?.preToolUse && { preToolUse: userHooks.preToolUse.bind(userHooks) }),
|
|
124
|
+
...(userHooks?.preToolUse && userHooks.preToolUseObservational === true ? { preToolUseObservational: true } : {}),
|
|
121
125
|
...(userHooks?.postToolUse && { postToolUse: userHooks.postToolUse.bind(userHooks) }),
|
|
122
126
|
...(userHooks?.userPromptSubmit && { userPromptSubmit: userHooks.userPromptSubmit.bind(userHooks) }),
|
|
123
127
|
...(userHooks?.postToolUseFailure && { postToolUseFailure: userHooks.postToolUseFailure.bind(userHooks) }),
|
|
@@ -245,6 +245,7 @@ function normalizeConcurrency(c) {
|
|
|
245
245
|
}
|
|
246
246
|
return Math.floor(c);
|
|
247
247
|
}
|
|
248
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
248
249
|
function normalizeWorkflowHardCap(name, v) {
|
|
249
250
|
if (v === undefined)
|
|
250
251
|
return undefined;
|
|
@@ -253,12 +254,22 @@ function normalizeWorkflowHardCap(name, v) {
|
|
|
253
254
|
}
|
|
254
255
|
return v;
|
|
255
256
|
}
|
|
257
|
+
function normalizeWorkflowTimerCap(name, v) {
|
|
258
|
+
const normalized = normalizeWorkflowHardCap(name, v);
|
|
259
|
+
if (normalized !== undefined && normalized > MAX_TIMER_DELAY_MS) {
|
|
260
|
+
throw new Error(`runWorkflow: ${name} must not exceed ${MAX_TIMER_DELAY_MS}ms (~24.8 days) (got ${v}) — a host timer truncates a larger delay and fires immediately, cancelling the run at once. Omit it for no deadline.`);
|
|
261
|
+
}
|
|
262
|
+
return normalized;
|
|
263
|
+
}
|
|
256
264
|
function normalizeWorkflowStallMs(v) {
|
|
257
265
|
if (v === undefined)
|
|
258
266
|
return WORKFLOW_AGENT_STALL_MS;
|
|
259
267
|
if (!Number.isFinite(v)) {
|
|
260
268
|
throw new Error(`runWorkflow: stallMs must be a finite number of milliseconds (got ${v}); 0 or a negative value is the documented explicit "off", not NaN/Infinity`);
|
|
261
269
|
}
|
|
270
|
+
if (v > MAX_TIMER_DELAY_MS) {
|
|
271
|
+
throw new Error(`runWorkflow: stallMs must not exceed ${MAX_TIMER_DELAY_MS}ms (~24.8 days) (got ${v}) — a host timer truncates a larger delay and fires immediately, so a watchdog stretched that far would trip on every agent instead of tolerating a long one. Pass 0 to turn it off.`);
|
|
272
|
+
}
|
|
262
273
|
return v;
|
|
263
274
|
}
|
|
264
275
|
function createSemaphore(max) {
|
|
@@ -346,10 +357,10 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
346
357
|
const maxAgents = normalizeWorkflowHardCap("maxAgents", opts.maxAgents);
|
|
347
358
|
const maxLogChars = normalizeWorkflowHardCap("maxLogChars", opts.maxLogChars);
|
|
348
359
|
const maxResultChars = normalizeWorkflowHardCap("maxResultChars", opts.maxResultChars);
|
|
349
|
-
const totalTimeoutMs =
|
|
360
|
+
const totalTimeoutMs = normalizeWorkflowTimerCap("totalTimeoutMs", opts.totalTimeoutMs);
|
|
350
361
|
const stallMs = normalizeWorkflowStallMs(opts.stallMs);
|
|
351
362
|
const agentMaxRetries = normalizeWorkflowHardCap("agentMaxRetries", opts.agentMaxRetries) ?? WORKFLOW_AGENT_MAX_RETRIES;
|
|
352
|
-
const throttleBackoffMs =
|
|
363
|
+
const throttleBackoffMs = normalizeWorkflowTimerCap("throttleBackoffMs", opts.throttleBackoffMs) ?? WORKFLOW_AGENT_THROTTLE_BACKOFF_MS;
|
|
353
364
|
const timers = opts.timers ?? REAL_WORKFLOW_TIMERS;
|
|
354
365
|
const cancelController = new AbortController();
|
|
355
366
|
const timeoutController = totalTimeoutMs !== undefined ? new AbortController() : undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import { BackgroundAgentStoreError, STALE_RUNNING_REAP_ATTRIBUTION, queryBackgroundAgents, } from "../../core/background-agent-store.js";
|
|
2
|
+
import { BackgroundAgentStoreError, STALE_RUNNING_REAP_ATTRIBUTION, assertBackgroundAgentReapOptions, queryBackgroundAgents, } from "../../core/background-agent-store.js";
|
|
3
3
|
import { SharedLedgerTable } from "./shared-ledger.js";
|
|
4
4
|
const agentLedgers = new SharedLedgerTable({
|
|
5
5
|
keyOf: (r) => FileBackgroundAgentStore.key(r.handle, r.scope),
|
|
@@ -113,6 +113,7 @@ export class FileBackgroundAgentStore {
|
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
115
|
async reap(scope, now, opts) {
|
|
116
|
+
assertBackgroundAgentReapOptions(opts);
|
|
116
117
|
if (!opts || (opts.maxAgeMs === undefined && opts.keep === undefined && opts.staleRunningMaxAgeMs === undefined))
|
|
117
118
|
return 0;
|
|
118
119
|
const changed = new Set();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join, resolve, sep } from "node:path";
|
|
2
|
+
import { assertRetentionPolicy } from "../../core/retention-policy.js";
|
|
2
3
|
import { existsSync, readdirSync, realpathSync } from "node:fs";
|
|
3
4
|
import { newestSentAt, } from "../../core/mailbox-store.js";
|
|
4
5
|
import { AppendLog, atomicWriteFile, canonicalStoreKey, ensureDir, readJsonlRecords, sanitizeScope, sanitizePathComponent } from "./fs-atomic.js";
|
|
@@ -176,6 +177,7 @@ export class FileMailboxStore {
|
|
|
176
177
|
});
|
|
177
178
|
}
|
|
178
179
|
async reap(scope, now, opts) {
|
|
180
|
+
assertRetentionPolicy("MailboxStore.reap", opts);
|
|
179
181
|
if (opts?.maxAgeMs === undefined)
|
|
180
182
|
return 0;
|
|
181
183
|
let dropped = 0;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import { assertRetentionPolicy } from "../../core/retention-policy.js";
|
|
2
3
|
import { WorkflowRunStoreError, isTerminalWorkflowStatus, nextWorkflowRunOnUpdate, queryWorkflowRuns, } from "../../core/workflow-run-store.js";
|
|
3
4
|
import { SharedLedgerTable } from "./shared-ledger.js";
|
|
4
5
|
const runLedgers = new SharedLedgerTable({
|
|
@@ -64,6 +65,7 @@ export class FileWorkflowRunStore {
|
|
|
64
65
|
return queryWorkflowRuns(this.runs.values(), scope, opts);
|
|
65
66
|
}
|
|
66
67
|
async reap(scope, now, opts) {
|
|
68
|
+
assertRetentionPolicy("WorkflowRunStore.reap", opts);
|
|
67
69
|
if (opts?.maxAgeMs === undefined && opts?.keep === undefined)
|
|
68
70
|
return 0;
|
|
69
71
|
const terminal = [...this.runs.values()].filter((r) => r.scope === scope && isTerminalWorkflowStatus(r.status));
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
export declare const NOT_AUTO_ALLOWED = "\u2014 not auto-allowed";
|
|
2
2
|
export declare const BASH_READONLY_DEFAULT_ALLOW: readonly string[];
|
|
3
|
-
export
|
|
3
|
+
export interface LeadingCommandNameOptions {
|
|
4
|
+
quotedOperatorsAreText?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function parseLeadingCommandName(command: string, options?: LeadingCommandNameOptions): {
|
|
4
7
|
name: string;
|
|
5
8
|
} | {
|
|
6
9
|
reject: string;
|
|
7
10
|
};
|
|
8
|
-
export declare function coarseReadonlyCheck(command: string, allow: ReadonlySet<string
|
|
11
|
+
export declare function coarseReadonlyCheck(command: string, allow: ReadonlySet<string>, options?: LeadingCommandNameOptions): string | undefined;
|
|
9
12
|
export interface BashReadonlyRootBoundary {
|
|
10
13
|
roots: readonly string[];
|
|
11
14
|
cwd?: string;
|
|
@@ -5,11 +5,41 @@ export const BASH_READONLY_DEFAULT_ALLOW = [
|
|
|
5
5
|
"grep", "cut", "tr", "basename", "dirname", "stat", "du", "df", "which",
|
|
6
6
|
];
|
|
7
7
|
const SHELL_OPERATORS = /[;&|<>$()`\n\r\\]/;
|
|
8
|
-
|
|
8
|
+
function quoteMask(s) {
|
|
9
|
+
const quoted = new Array(s.length).fill(false);
|
|
10
|
+
let open;
|
|
11
|
+
for (let i = 0; i < s.length; i++) {
|
|
12
|
+
const ch = s[i];
|
|
13
|
+
if (open === undefined && (ch === '"' || ch === "'")) {
|
|
14
|
+
open = ch;
|
|
15
|
+
quoted[i] = true;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (open !== undefined) {
|
|
19
|
+
quoted[i] = true;
|
|
20
|
+
if (open === ch)
|
|
21
|
+
open = undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { quoted, balanced: open === undefined };
|
|
25
|
+
}
|
|
26
|
+
function hasBareShellOperator(s, quotedOperatorsAreText) {
|
|
27
|
+
if (!quotedOperatorsAreText)
|
|
28
|
+
return SHELL_OPERATORS.test(s);
|
|
29
|
+
const mask = quoteMask(s);
|
|
30
|
+
if (!mask.balanced)
|
|
31
|
+
return SHELL_OPERATORS.test(s);
|
|
32
|
+
for (let i = 0; i < s.length; i++) {
|
|
33
|
+
if (!mask.quoted[i] && SHELL_OPERATORS.test(s[i]))
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
export function parseLeadingCommandName(command, options) {
|
|
9
39
|
const trimmed = command.trim();
|
|
10
40
|
if (!trimmed)
|
|
11
41
|
return { reject: "empty command" };
|
|
12
|
-
if (
|
|
42
|
+
if (hasBareShellOperator(trimmed, options?.quotedOperatorsAreText === true)) {
|
|
13
43
|
return {
|
|
14
44
|
reject: "shell operators (pipes, redirects, ;, &&, command substitution, subshells, variable expansion) are not allowed — run a single simple command",
|
|
15
45
|
};
|
|
@@ -23,8 +53,8 @@ export function parseLeadingCommandName(command) {
|
|
|
23
53
|
return { reject: "the command name must be a bare token (no quotes, braces, globs, or ~)" };
|
|
24
54
|
return { name: first };
|
|
25
55
|
}
|
|
26
|
-
export function coarseReadonlyCheck(command, allow) {
|
|
27
|
-
const parsed = parseLeadingCommandName(command);
|
|
56
|
+
export function coarseReadonlyCheck(command, allow, options) {
|
|
57
|
+
const parsed = parseLeadingCommandName(command, options);
|
|
28
58
|
if ("reject" in parsed)
|
|
29
59
|
return parsed.reject;
|
|
30
60
|
if (!allow.has(parsed.name))
|
|
@@ -32,6 +62,35 @@ export function coarseReadonlyCheck(command, allow) {
|
|
|
32
62
|
return undefined;
|
|
33
63
|
}
|
|
34
64
|
const SHELL_SEGMENT_HARD_REJECT = /[<>$()`\n\r\\]/;
|
|
65
|
+
const EXEMPT_REDIRECTION = /^(?:2>\/dev\/null|[12]?>&[12])$/;
|
|
66
|
+
function stripExemptRedirections(command) {
|
|
67
|
+
if (!command.includes(">"))
|
|
68
|
+
return command;
|
|
69
|
+
const mask = quoteMask(command);
|
|
70
|
+
if (!mask.balanced)
|
|
71
|
+
return command;
|
|
72
|
+
let out = "";
|
|
73
|
+
let word = "";
|
|
74
|
+
let wordQuoted = false;
|
|
75
|
+
const flush = () => {
|
|
76
|
+
out += wordQuoted || !EXEMPT_REDIRECTION.test(word) ? word : "";
|
|
77
|
+
word = "";
|
|
78
|
+
wordQuoted = false;
|
|
79
|
+
};
|
|
80
|
+
for (let i = 0; i < command.length; i++) {
|
|
81
|
+
const ch = command[i];
|
|
82
|
+
if (!mask.quoted[i] && (ch === " " || ch === "\t")) {
|
|
83
|
+
flush();
|
|
84
|
+
out += ch;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (mask.quoted[i])
|
|
88
|
+
wordQuoted = true;
|
|
89
|
+
word += ch;
|
|
90
|
+
}
|
|
91
|
+
flush();
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
35
94
|
function foldQuoteRemovalToken(tok) {
|
|
36
95
|
if (!tok.includes('"') && !tok.includes("'"))
|
|
37
96
|
return tok;
|
|
@@ -77,12 +136,30 @@ function resolveOperandLexically(base, operand, homeDir) {
|
|
|
77
136
|
return normalizeAbsPathLexically(`${base.replace(/[/\\]+$/, "")}/${raw}`);
|
|
78
137
|
}
|
|
79
138
|
function tokenizeSegment(segment) {
|
|
80
|
-
const raw = segment
|
|
81
|
-
.trim()
|
|
82
|
-
.split(/\s+/)
|
|
83
|
-
.filter((t) => t.length > 0);
|
|
139
|
+
const raw = splitWordsQuoteAware(segment);
|
|
84
140
|
return { folded: raw.map(foldQuoteRemovalToken), raw };
|
|
85
141
|
}
|
|
142
|
+
function splitWordsQuoteAware(segment) {
|
|
143
|
+
const s = segment.trim();
|
|
144
|
+
const mask = quoteMask(s);
|
|
145
|
+
if (!mask.balanced)
|
|
146
|
+
return s.split(/\s+/).filter((t) => t.length > 0);
|
|
147
|
+
const out = [];
|
|
148
|
+
let cur = "";
|
|
149
|
+
for (let i = 0; i < s.length; i++) {
|
|
150
|
+
const ch = s[i];
|
|
151
|
+
if (!mask.quoted[i] && /\s/.test(ch)) {
|
|
152
|
+
if (cur.length > 0)
|
|
153
|
+
out.push(cur);
|
|
154
|
+
cur = "";
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
cur += ch;
|
|
158
|
+
}
|
|
159
|
+
if (cur.length > 0)
|
|
160
|
+
out.push(cur);
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
86
163
|
function hasUnquotedExpansionMetachar(rawToken) {
|
|
87
164
|
let open;
|
|
88
165
|
for (const ch of rawToken) {
|
|
@@ -147,6 +224,26 @@ function isCutDelimiterPayloadLongOption(tok) {
|
|
|
147
224
|
const name = longOptionNameOf(tok);
|
|
148
225
|
return name !== undefined && (isLongOptionAbbrevOf(name, "delimiter") || isLongOptionAbbrevOf(name, "output-delimiter"));
|
|
149
226
|
}
|
|
227
|
+
function isGrepClusterFileStdin(tok) {
|
|
228
|
+
if (tok.startsWith("--") || !/^-[A-Za-z0-9]/.test(tok))
|
|
229
|
+
return false;
|
|
230
|
+
for (let i = 1; i < tok.length; i++) {
|
|
231
|
+
const ch = tok[i];
|
|
232
|
+
if (ch === "e")
|
|
233
|
+
return false;
|
|
234
|
+
if (ch === "f")
|
|
235
|
+
return tok.slice(i + 1) === "-";
|
|
236
|
+
if (!/[A-Za-z0-9]/.test(ch))
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
function isCutSeparatedDelimiterOption(tok) {
|
|
242
|
+
if (tok.includes("="))
|
|
243
|
+
return false;
|
|
244
|
+
const name = longOptionNameOf(tok);
|
|
245
|
+
return name !== undefined && (isLongOptionAbbrevOf(name, "delimiter") || isLongOptionAbbrevOf(name, "output-delimiter"));
|
|
246
|
+
}
|
|
150
247
|
function isGrepFileStdinLongOption(tok) {
|
|
151
248
|
const eq = tok.indexOf("=");
|
|
152
249
|
if (eq < 0 || tok.slice(eq + 1) !== "-")
|
|
@@ -285,16 +382,22 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
285
382
|
const trimmed = command.trim();
|
|
286
383
|
if (!trimmed)
|
|
287
384
|
return { reason: "empty command" };
|
|
288
|
-
|
|
385
|
+
const redirectionStripped = stripExemptRedirections(trimmed);
|
|
386
|
+
if (SHELL_SEGMENT_HARD_REJECT.test(redirectionStripped)) {
|
|
289
387
|
return { reason: "redirection, command/variable substitution, subshells, escapes, and line breaks are not allowed" };
|
|
290
388
|
}
|
|
291
|
-
const source =
|
|
389
|
+
const source = redirectionStripped.endsWith(";") ? redirectionStripped.slice(0, -1) : redirectionStripped;
|
|
292
390
|
const segments = [];
|
|
293
391
|
const pipeFed = [false];
|
|
392
|
+
const mask = quoteMask(source);
|
|
393
|
+
const quoted = (i) => mask.balanced && mask.quoted[i] === true;
|
|
294
394
|
let cur = "";
|
|
295
395
|
for (let i = 0; i < source.length; i++) {
|
|
296
396
|
const c = source[i];
|
|
297
|
-
if (
|
|
397
|
+
if (quoted(i)) {
|
|
398
|
+
cur += c;
|
|
399
|
+
}
|
|
400
|
+
else if (c === "&") {
|
|
298
401
|
if (source[i + 1] === "&") {
|
|
299
402
|
segments.push(cur);
|
|
300
403
|
cur = "";
|
|
@@ -327,7 +430,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
327
430
|
}
|
|
328
431
|
segments.push(cur);
|
|
329
432
|
for (const segment of segments) {
|
|
330
|
-
const reason = coarseReadonlyCheck(segment, allow);
|
|
433
|
+
const reason = coarseReadonlyCheck(segment, allow, { quotedOperatorsAreText: true });
|
|
331
434
|
if (reason !== undefined)
|
|
332
435
|
return { reason };
|
|
333
436
|
}
|
|
@@ -368,21 +471,21 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
368
471
|
continue;
|
|
369
472
|
foldedSegments.push(segmentTokens);
|
|
370
473
|
const name = toks[0];
|
|
371
|
-
const nonOption = toks.slice(1).filter((t) => !t.startsWith("-"));
|
|
372
474
|
if (!(pipeFed[si] ?? false)) {
|
|
373
475
|
const floor = STDIN_FILE_FLOOR[name];
|
|
374
476
|
const restArgs = toks.slice(1);
|
|
375
477
|
const grepPatternSuppliedByFlag = name === "grep" && restArgs.some(isGrepPatternFlagToken);
|
|
376
478
|
let sawNonFlagOperand = false;
|
|
377
479
|
let hasStdinDash = false;
|
|
480
|
+
let operandCount = 0;
|
|
378
481
|
if (name !== "tr") {
|
|
379
482
|
for (let k = 0; k < restArgs.length; k++) {
|
|
380
483
|
const t = restArgs[k];
|
|
381
|
-
if (name === "cut" && (t === "-d" || t
|
|
484
|
+
if (name === "cut" && (t === "-d" || isCutSeparatedDelimiterOption(t))) {
|
|
382
485
|
k++;
|
|
383
486
|
continue;
|
|
384
487
|
}
|
|
385
|
-
if (name === "grep" && (t
|
|
488
|
+
if (name === "grep" && (isGrepClusterFileStdin(t) || isGrepFileStdinLongOption(t))) {
|
|
386
489
|
hasStdinDash = true;
|
|
387
490
|
break;
|
|
388
491
|
}
|
|
@@ -390,6 +493,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
390
493
|
continue;
|
|
391
494
|
const isGrepPatternPosition = name === "grep" && !grepPatternSuppliedByFlag && !sawNonFlagOperand;
|
|
392
495
|
sawNonFlagOperand = true;
|
|
496
|
+
operandCount++;
|
|
393
497
|
if (isGrepPatternPosition)
|
|
394
498
|
continue;
|
|
395
499
|
if (t === "-") {
|
|
@@ -401,7 +505,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
401
505
|
if (floor !== undefined && hasStdinDash) {
|
|
402
506
|
return { reason: `"${name}" reads stdin via an explicit "-" argument and would block until the tool timeout ${NOT_AUTO_ALLOWED}` };
|
|
403
507
|
}
|
|
404
|
-
if (floor !== undefined &&
|
|
508
|
+
if (floor !== undefined && operandCount < floor) {
|
|
405
509
|
return { reason: `"${name}" with no file argument reads stdin and would block until the tool timeout ${NOT_AUTO_ALLOWED}` };
|
|
406
510
|
}
|
|
407
511
|
}
|
|
@@ -409,7 +513,15 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
409
513
|
return { reason: "`tail` in follow mode never terminates " + NOT_AUTO_ALLOWED };
|
|
410
514
|
}
|
|
411
515
|
const GENERATOR_DEVICES = new Set(["/dev/zero", "/dev/random", "/dev/urandom", "/dev/full"]);
|
|
412
|
-
const
|
|
516
|
+
const deviceCandidates = [];
|
|
517
|
+
for (const t of toks.slice(1)) {
|
|
518
|
+
if (t.startsWith("-") && t !== "-") {
|
|
519
|
+
deviceCandidates.push(...attachedOptionPayloads(t));
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
deviceCandidates.push(t);
|
|
523
|
+
}
|
|
524
|
+
const deviceArgs = deviceCandidates.map(normalizeAbsPathLexically).filter(isBlockedDevicePath);
|
|
413
525
|
const rescuedByHead = headBoundIsSmall(name, toks) && deviceArgs.every((d) => GENERATOR_DEVICES.has(d));
|
|
414
526
|
if (!rescuedByHead && deviceArgs.length > 0) {
|
|
415
527
|
return { reason: "reads a device/special file that is either unbounded (/dev/zero, /dev/stdin, /proc/<pid>/fd/0, … — blocks the pipeline until the tool timeout) or process-private (/proc/<pid>/environ, /proc/<pid>/mem, …) " + NOT_AUTO_ALLOWED };
|
package/dist/tools/web.js
CHANGED
|
@@ -25,6 +25,31 @@ function resolveWebMaxBytes(value) {
|
|
|
25
25
|
}
|
|
26
26
|
return value;
|
|
27
27
|
}
|
|
28
|
+
function resolveWebTimeoutMs(value, knob, fallback) {
|
|
29
|
+
if (value === undefined)
|
|
30
|
+
return fallback;
|
|
31
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
32
|
+
const e = new Error(`${knob} must be a positive finite number of milliseconds (got ${String(value)}) — a non-finite or non-positive budget is not "no limit", it aborts every request within a millisecond`);
|
|
33
|
+
e.code = "config.web_timeout_invalid";
|
|
34
|
+
throw e;
|
|
35
|
+
}
|
|
36
|
+
if (value > 2_147_483_647) {
|
|
37
|
+
const e = new Error(`${knob} must not exceed 2147483647ms (~24.8 days) (got ${String(value)}) — a host timer truncates a larger delay and fires immediately, so widening the budget past this point aborts every request at once`);
|
|
38
|
+
e.code = "config.web_timeout_invalid";
|
|
39
|
+
throw e;
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
function resolveSearchMaxResults(value) {
|
|
44
|
+
if (value === undefined)
|
|
45
|
+
return 10;
|
|
46
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
47
|
+
const e = new Error(`WebSearch maxResults must be a whole number of 1 or more (got ${String(value)}) — a zero, negative or unevaluable cap returns an empty result set from a backend that answered`);
|
|
48
|
+
e.code = "config.web_search_max_results_invalid";
|
|
49
|
+
throw e;
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
28
53
|
export const WEBFETCH_GROUNDING_MIN_TEXT_CHARS = 200;
|
|
29
54
|
const GROUNDING_MIN_TEXT_RATIO = 0.01;
|
|
30
55
|
const GROUNDING_RATIO_MAX_TEXT_CHARS = 2_000;
|
|
@@ -230,6 +255,7 @@ async function fetchAllowlisted(doFetch, start, allowHosts, userAgent, signal, o
|
|
|
230
255
|
}
|
|
231
256
|
export function webFetchToolSpec(config = {}) {
|
|
232
257
|
const maxBytes = resolveWebMaxBytes(config.maxBytes);
|
|
258
|
+
const fetchTimeoutMs = resolveWebTimeoutMs(config.timeoutMs, "WebFetch timeoutMs", DEFAULT_FETCH_TIMEOUT_MS);
|
|
233
259
|
const doFetch = config.fetchImpl ?? globalThis.fetch;
|
|
234
260
|
const canSummarize = config.summarize !== undefined;
|
|
235
261
|
return {
|
|
@@ -276,7 +302,7 @@ export function webFetchToolSpec(config = {}) {
|
|
|
276
302
|
}
|
|
277
303
|
if (!doFetch)
|
|
278
304
|
return failure("Error (WebFetch): no fetch implementation available in this environment.");
|
|
279
|
-
const timeoutMs =
|
|
305
|
+
const timeoutMs = fetchTimeoutMs;
|
|
280
306
|
const ac = new AbortController();
|
|
281
307
|
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
282
308
|
const onOuterAbort = () => ac.abort();
|
|
@@ -794,7 +820,8 @@ function webSearchResultAllowed(url, allowed, blocked) {
|
|
|
794
820
|
return true;
|
|
795
821
|
}
|
|
796
822
|
export function createWebSearchTool(config) {
|
|
797
|
-
const max = config.maxResults
|
|
823
|
+
const max = resolveSearchMaxResults(config.maxResults);
|
|
824
|
+
const searchTimeoutMs = resolveWebTimeoutMs(config.timeoutMs, "WebSearch timeoutMs", DEFAULT_SEARCH_TIMEOUT_MS);
|
|
798
825
|
const currentMonthYear = new Date().toLocaleString("en-US", { month: "long", year: "numeric" });
|
|
799
826
|
return {
|
|
800
827
|
name: "WebSearch",
|
|
@@ -831,7 +858,7 @@ export function createWebSearchTool(config) {
|
|
|
831
858
|
if (queryChars > SEARCH_QUERY_MAX_CHARS) {
|
|
832
859
|
return errorResult(`Error (WebSearch): the query is ${queryChars} characters, over the ${SEARCH_QUERY_MAX_CHARS}-character limit. Shorten it to the terms that matter — an identical retry will fail the same way.`, failCard({ retryable: false }));
|
|
833
860
|
}
|
|
834
|
-
const timeoutMs =
|
|
861
|
+
const timeoutMs = searchTimeoutMs;
|
|
835
862
|
const ac = new AbortController();
|
|
836
863
|
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
837
864
|
const onOuterAbort = () => ac.abort();
|
|
@@ -936,7 +963,7 @@ export function createWebSearchTool(config) {
|
|
|
936
963
|
}
|
|
937
964
|
export function createSearxngSearchBackend(baseUrl, options = {}) {
|
|
938
965
|
const doFetch = options.fetchImpl ?? globalThis.fetch;
|
|
939
|
-
const timeoutMs = options.timeoutMs
|
|
966
|
+
const timeoutMs = resolveWebTimeoutMs(options.timeoutMs, "createSearxngSearchBackend timeoutMs", 10_000);
|
|
940
967
|
const parsedBase = new URL(baseUrl);
|
|
941
968
|
parsedBase.hash = "";
|
|
942
969
|
parsedBase.pathname = `${parsedBase.pathname.replace(/\/+$/, "")}/search`;
|
|
@@ -966,9 +993,9 @@ export function createSearxngSearchBackend(baseUrl, options = {}) {
|
|
|
966
993
|
};
|
|
967
994
|
}
|
|
968
995
|
export async function probeSearchBackend(search, options) {
|
|
969
|
-
const budget = options?.timeoutMs ?? 15_000;
|
|
970
996
|
let timer;
|
|
971
997
|
try {
|
|
998
|
+
const budget = resolveWebTimeoutMs(options?.timeoutMs, "probeSearchBackend timeoutMs", 15_000);
|
|
972
999
|
const results = await Promise.race([
|
|
973
1000
|
search("connectivity probe", AbortSignal.timeout(budget)),
|
|
974
1001
|
new Promise((_, reject) => {
|