@sema-agent/core 5.13.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 +296 -0
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +4 -0
- package/dist/agents/subagent.js +133 -41
- 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 +41 -17
- package/dist/core/checkpoint-store.js +114 -3
- 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/mcp.d.ts +7 -2
- package/dist/core/mcp.js +7 -7
- package/dist/core/memory-admission.d.ts +4 -0
- package/dist/core/memory-admission.js +3 -0
- 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 +16 -6
- package/dist/core/runner/prepare-task.js +301 -24
- package/dist/core/runner/runtask.d.ts +3 -6
- package/dist/core/runner/runtask.js +186 -36
- package/dist/core/runner/tool-output-projection.js +1 -0
- package/dist/core/session-store.d.ts +3 -0
- package/dist/core/session-store.js +4 -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/types.d.ts +32 -1
- 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 +8 -1
- 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/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 +7 -4
- package/dist/index.js +7 -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
|
@@ -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
|
+
}
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -7,10 +7,10 @@ import { hasBackgroundShell } from "../../core/background-shell.js";
|
|
|
7
7
|
import { delimitUntrusted } from "../../core/untrusted-text.js";
|
|
8
8
|
import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
|
|
9
9
|
import { imageMagicMatches, withinAnyRoot } from "./safety.js";
|
|
10
|
-
import { isRemoteExecutionEnv } from "../../core/remote-env.js";
|
|
10
|
+
import { isRemoteExecutionEnv, hasDestroy, isIsolated } from "../../core/remote-env.js";
|
|
11
11
|
import { ghRateLimitHint } from "./gh-rate-limit.js";
|
|
12
12
|
import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, } from "./fs-shared.js";
|
|
13
|
-
import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyCompoundReadonly, classifySimpleCommandReadBoundary, } from "./bash-readonly-classifier.js";
|
|
13
|
+
import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoop, classifyCompoundReadonly, classifySimpleCommandReadBoundary, } from "./bash-readonly-classifier.js";
|
|
14
14
|
export function bashReversibilityProbe(allow, boundary) {
|
|
15
15
|
const allowSet = new Set(allow ?? BASH_READONLY_DEFAULT_ALLOW);
|
|
16
16
|
return (args) => {
|
|
@@ -18,7 +18,9 @@ export function bashReversibilityProbe(allow, boundary) {
|
|
|
18
18
|
if (typeof command !== "string")
|
|
19
19
|
return { reversible: false };
|
|
20
20
|
const resolved = typeof boundary === "function" ? boundary() : boundary;
|
|
21
|
-
|
|
21
|
+
if (classifyCompoundReadonly(command, allowSet, resolved) === undefined)
|
|
22
|
+
return { reversible: true };
|
|
23
|
+
return { reversible: classifyBoundedReadonlyPollLoop(command, allowSet, resolved) === undefined };
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
26
|
const EXIT1_INTERPRETATION = {
|
|
@@ -399,7 +401,22 @@ async function runShell(env, cwd, toolName, command, timeoutSec, caps, signal, c
|
|
|
399
401
|
},
|
|
400
402
|
};
|
|
401
403
|
}
|
|
402
|
-
function
|
|
404
|
+
function bgReapClause(sessionScoped, envIsolatedOwned) {
|
|
405
|
+
if (!sessionScoped)
|
|
406
|
+
return "when this task ends";
|
|
407
|
+
return envIsolatedOwned
|
|
408
|
+
? "at session end, not when this task ends — UNLESS this task's own isolated sandbox is torn down, which reaps it right then instead"
|
|
409
|
+
: "at session end, not when this task ends: it keeps running across tasks in the same session, and a later task can still read its output or stop it";
|
|
410
|
+
}
|
|
411
|
+
function bgNohupCaveat(envIsolatedOwned) {
|
|
412
|
+
return envIsolatedOwned
|
|
413
|
+
? " — on a per-task isolated sandbox this only helps if the sandbox itself survives; if the deployment tears it down at task end, host the deliverable outside the sandbox instead"
|
|
414
|
+
: "";
|
|
415
|
+
}
|
|
416
|
+
function bgRetainedIsolatedNote() {
|
|
417
|
+
return " This environment declares retention for background processes — normally one keeps running past this task and the session. The one exception: a per-task isolated sandbox that gets torn down still reaps it right then despite the retention declaration; if the deliverable must survive that too, host it outside the sandbox instead.";
|
|
418
|
+
}
|
|
419
|
+
function bashDescription(coAuthor, caps, bgNotifies = false, bgRetained = false, bgSessionScoped = false, bgEnvIsolatedOwned = false) {
|
|
403
420
|
const coAuthorLines = coAuthor === false
|
|
404
421
|
? "- Follow the deployment's commit-message conventions."
|
|
405
422
|
: `- End git commit messages with:\nCo-Authored-By: ${coAuthor}`;
|
|
@@ -409,7 +426,7 @@ function bashDescription(coAuthor, caps, bgNotifies = false, bgRetained = false)
|
|
|
409
426
|
- IMPORTANT: Avoid using this tool to run \`cat\`, \`head\`, \`tail\`, \`sed\`, \`awk\`, or \`echo\` commands, unless explicitly instructed or after you have verified that a dedicated tool cannot accomplish your task. Instead, use the appropriate dedicated tool as this will provide a much better experience for the user.
|
|
410
427
|
- File search by name: use the Glob tool (NOT \`find\` or \`ls\`); content search: use the Grep tool (NOT \`grep\`/\`rg\` in the shell).
|
|
411
428
|
- \`timeout\` is in milliseconds: default ${caps.defaultMs}, max ${caps.maxMs}.
|
|
412
|
-
- \`run_in_background\` runs the command detached: it keeps running across turns and ${bgNotifies ? "re-invokes you when it exits" : "you read its output later with TaskOutput(task_id)"}. No \`&\` needed.${bgRetained ? "" :
|
|
429
|
+
- \`run_in_background\` runs the command detached: it keeps running across turns and ${bgNotifies ? "re-invokes you when it exits" : "you read its output later with TaskOutput(task_id)"}. No \`&\` needed.${bgRetained ? (bgEnvIsolatedOwned ? bgRetainedIsolatedNote() : "") : ` Background processes do NOT survive the session — they are reaped ${bgReapClause(bgSessionScoped, bgEnvIsolatedOwned)}. A deliverable that must stay alive afterwards (a server, a daemon) needs a self-detaching FOREGROUND start instead: run \`nohup cmd >log 2>&1 &\` as a normal foreground command (portable; setsid does not exist on macOS), or use a service manager${bgNohupCaveat(bgEnvIsolatedOwned)}.`}
|
|
413
430
|
|
|
414
431
|
# Git
|
|
415
432
|
- Interactive flags (\`-i\`, e.g. \`git rebase -i\`, \`git add -i\`) are not supported in this environment.
|
|
@@ -463,20 +480,22 @@ async function adoptBackgroundShell(req) {
|
|
|
463
480
|
return { ok: false, reason, killed: false, killMsg: kill !== undefined && !kill.ok ? kill.error.message : "killBackground threw" };
|
|
464
481
|
}
|
|
465
482
|
}
|
|
466
|
-
function backgroundLifetimeNote(caps) {
|
|
467
|
-
|
|
468
|
-
? ""
|
|
469
|
-
|
|
483
|
+
function backgroundLifetimeNote(caps, sessionScoped, envIsolatedOwned) {
|
|
484
|
+
if (caps.retainBackgroundProcesses === true)
|
|
485
|
+
return envIsolatedOwned ? bgRetainedIsolatedNote() : "";
|
|
486
|
+
return ` NOTE: background processes do NOT survive the session (reaped ${bgReapClause(sessionScoped, envIsolatedOwned)}). If this is a deliverable service that must stay alive afterwards, host it with a FOREGROUND command instead: \`nohup cmd >log 2>&1 &\` (self-detaching — survives the session)${bgNohupCaveat(envIsolatedOwned)}.`;
|
|
470
487
|
}
|
|
471
488
|
export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = { current: rootCanonical }, taskOpts = {}) {
|
|
472
489
|
const timeoutCaps = resolveBashTimeoutCaps(taskOpts);
|
|
473
490
|
const timeoutCapsSecView = bashTimeoutCapsSec(timeoutCaps);
|
|
474
491
|
const bgNotifies = taskOpts.taskRegistry !== undefined && taskOpts.onTaskNotification !== undefined;
|
|
475
492
|
const bgRetained = hasBackgroundShell(env) && env.backgroundCapabilities.retainBackgroundProcesses === true;
|
|
493
|
+
const bgSessionScoped = taskOpts.sessionId !== undefined && taskOpts.taskRegistry !== undefined;
|
|
494
|
+
const bgEnvIsolatedOwned = hasDestroy(env) && isIsolated(env);
|
|
476
495
|
return defineTool({
|
|
477
496
|
name: "Bash",
|
|
478
497
|
contract: { contractId: "core.bash@1", implementationRevision: "1" },
|
|
479
|
-
description: bashDescription(coAuthor, timeoutCaps, bgNotifies, bgRetained),
|
|
498
|
+
description: bashDescription(coAuthor, timeoutCaps, bgNotifies, bgRetained, bgSessionScoped, bgEnvIsolatedOwned),
|
|
480
499
|
parameters: Type.Object({
|
|
481
500
|
command: Type.String({ description: "The command to execute" }),
|
|
482
501
|
timeout: Type.Optional(Type.Number({ description: `Optional timeout in milliseconds (max ${timeoutCaps.maxMs}; requests above the max are capped to it)` })),
|
|
@@ -535,7 +554,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
535
554
|
? ` Time budget: requested ${requestedTimeoutSec}s, capped at ${bgCaps.maxBgTimeoutSec}s (env ceiling: requests above ${bgCaps.maxBgTimeoutSec}s are reduced to it) — auto-terminates if still running after ${appliedTimeoutSec}s.`
|
|
536
555
|
: ` Time budget: auto-terminates if still running after ${appliedTimeoutSec}s (hard cap ${bgCaps.maxBgTimeoutSec}s).`
|
|
537
556
|
: "";
|
|
538
|
-
const lifetimeNote = backgroundLifetimeNote(bgCaps);
|
|
557
|
+
const lifetimeNote = backgroundLifetimeNote(bgCaps, bgSessionScoped, bgEnvIsolatedOwned);
|
|
539
558
|
const registry = taskOpts.taskRegistry;
|
|
540
559
|
if (registry !== undefined) {
|
|
541
560
|
const onNotify = taskOpts.onTaskNotification;
|
|
@@ -649,7 +668,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
649
668
|
? `You will be notified when it completes — do not poll.`
|
|
650
669
|
: `Poll TaskOutput until its status is no longer "running".`) +
|
|
651
670
|
adoptBudgetNote +
|
|
652
|
-
backgroundLifetimeNote(adoptCaps) +
|
|
671
|
+
backgroundLifetimeNote(adoptCaps, bgSessionScoped, bgEnvIsolatedOwned) +
|
|
653
672
|
(tail ? `\n--- output so far (tail) ---\n${tail}` : ""),
|
|
654
673
|
details: { type: "bash", detached: true, task_id: taskId, ...(description !== undefined ? { description } : {}), ...(outputFile !== undefined ? { output_file: outputFile } : {}), ...(cause === "timeout" ? { autoBackgrounded: true } : {}) },
|
|
655
674
|
};
|
package/dist/tools/fs/safety.js
CHANGED
|
@@ -309,15 +309,29 @@ export async function canonicalizeTarget(env, path, signal, baseCwd) {
|
|
|
309
309
|
let abs = absR.value;
|
|
310
310
|
for (let hop = 0; hop < 16; hop++) {
|
|
311
311
|
const exists = await env.exists(abs, signal);
|
|
312
|
-
if (!
|
|
313
|
-
return { ok:
|
|
312
|
+
if (!exists.ok) {
|
|
313
|
+
return { ok: false, message: `cannot determine whether "${path}" exists: ${exists.error.message}`, unresolvedSymlink: true };
|
|
314
|
+
}
|
|
315
|
+
if (!exists.value) {
|
|
316
|
+
const newPath = await canonicalizeNewPath(env, abs, signal);
|
|
317
|
+
if (!newPath.ok) {
|
|
318
|
+
return { ok: false, message: newPath.message, unresolvedSymlink: true };
|
|
319
|
+
}
|
|
320
|
+
return { ok: true, key: newPath.key };
|
|
314
321
|
}
|
|
315
322
|
const canon = await env.canonicalPath(abs, signal);
|
|
316
323
|
if (canon.ok)
|
|
317
324
|
return { ok: true, key: canon.value };
|
|
318
325
|
const info = await env.fileInfo(abs, signal);
|
|
319
|
-
if (!
|
|
320
|
-
return { ok:
|
|
326
|
+
if (!info.ok) {
|
|
327
|
+
return { ok: false, message: `cannot determine whether "${path}" is a symlink: ${info.error.message}`, unresolvedSymlink: true };
|
|
328
|
+
}
|
|
329
|
+
if (info.value.kind !== "symlink") {
|
|
330
|
+
const viaAncestors = await canonicalizeNewPath(env, abs, signal, { skipLeaf: true });
|
|
331
|
+
if (!viaAncestors.ok) {
|
|
332
|
+
return { ok: false, message: viaAncestors.message, unresolvedSymlink: true };
|
|
333
|
+
}
|
|
334
|
+
return { ok: true, key: viaAncestors.key };
|
|
321
335
|
}
|
|
322
336
|
if (!env.readLink) {
|
|
323
337
|
return { ok: false, message: `cannot resolve symlink "${path}" (env has no readLink)`, unresolvedSymlink: true };
|
|
@@ -328,7 +342,10 @@ export async function canonicalizeTarget(env, path, signal, baseCwd) {
|
|
|
328
342
|
}
|
|
329
343
|
const linkTarget = isAbsolutePathForm(link.value) ? link.value : `${parentDir(abs)}${nativeSepOf(abs)}${link.value}`;
|
|
330
344
|
const reabs = await env.absolutePath(linkTarget, signal);
|
|
331
|
-
|
|
345
|
+
if (!reabs.ok) {
|
|
346
|
+
return { ok: false, message: `cannot resolve symlink target "${linkTarget}" for "${path}": ${reabs.error.message}`, unresolvedSymlink: true };
|
|
347
|
+
}
|
|
348
|
+
abs = reabs.value;
|
|
332
349
|
}
|
|
333
350
|
return { ok: false, message: `symlink chain for "${path}" exceeded the hop cap; target unresolved`, unresolvedSymlink: true };
|
|
334
351
|
}
|
|
@@ -339,21 +356,28 @@ function parentDir(abs) {
|
|
|
339
356
|
const head = abs.slice(0, i);
|
|
340
357
|
return /^[A-Za-z]:$/.test(head) ? head + abs[i] : head;
|
|
341
358
|
}
|
|
342
|
-
async function canonicalizeNewPath(env, abs, signal) {
|
|
359
|
+
async function canonicalizeNewPath(env, abs, signal, opts) {
|
|
343
360
|
const sep = nativeSepOf(abs);
|
|
344
361
|
const parts = isWinFormPath(abs) ? abs.split(/[\\/]/) : abs.split("/");
|
|
345
362
|
const tail = [];
|
|
363
|
+
if (opts?.skipLeaf === true && parts.length > 1)
|
|
364
|
+
tail.push(parts.pop());
|
|
346
365
|
while (parts.length > 1) {
|
|
347
366
|
const candidate = parts.join(sep) || sep;
|
|
348
367
|
const ex = await env.exists(candidate, signal);
|
|
349
|
-
if (ex.ok
|
|
368
|
+
if (!ex.ok) {
|
|
369
|
+
return { ok: false, message: `cannot determine whether ancestor "${candidate}" exists: ${ex.error.message}` };
|
|
370
|
+
}
|
|
371
|
+
if (ex.value) {
|
|
350
372
|
const canon = await env.canonicalPath(candidate, signal);
|
|
351
|
-
|
|
352
|
-
|
|
373
|
+
if (!canon.ok) {
|
|
374
|
+
return { ok: false, message: `cannot canonicalize existing ancestor "${candidate}": ${canon.error.message}` };
|
|
375
|
+
}
|
|
376
|
+
return { ok: true, key: [canon.value, ...tail.reverse()].join(nativeSepOf(canon.value)) };
|
|
353
377
|
}
|
|
354
378
|
tail.push(parts.pop());
|
|
355
379
|
}
|
|
356
|
-
return abs;
|
|
380
|
+
return { ok: true, key: abs };
|
|
357
381
|
}
|
|
358
382
|
export function violationText(toolName, v) {
|
|
359
383
|
return `Error (${toolName}): ${v.message}`;
|