@sema-agent/core 7.31.0 → 7.32.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 +106 -0
- package/dist/agents/inherited-tool-pool.d.ts +28 -0
- package/dist/agents/inherited-tool-pool.js +15 -0
- package/dist/agents/list-agents-tool.d.ts +7 -0
- package/dist/agents/list-agents-tool.js +9 -2
- package/dist/agents/peer-session-drain.d.ts +2 -0
- package/dist/agents/peer-session-drain.js +1 -0
- package/dist/agents/send-message-tool.d.ts +17 -0
- package/dist/agents/send-message-tool.js +11 -4
- package/dist/agents/subagent.d.ts +10 -1
- package/dist/agents/subagent.js +24 -8
- package/dist/core/ask-origin.d.ts +15 -6
- package/dist/core/ask-origin.js +15 -9
- package/dist/core/background-shell.d.ts +45 -0
- package/dist/core/background-shell.js +13 -0
- package/dist/core/checkpoint-store.d.ts +5 -0
- package/dist/core/checkpoint-store.js +3 -2
- package/dist/core/engine-notice.d.ts +9 -0
- package/dist/core/gate-lanes.js +1 -1
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +5 -0
- package/dist/core/hooks.d.ts +2 -0
- package/dist/core/memory-engine/engine.d.ts +10 -1
- package/dist/core/memory-engine/engine.js +30 -6
- package/dist/core/memory-engine/file-backend.d.ts +15 -0
- package/dist/core/memory-engine/file-backend.js +53 -2
- package/dist/core/memory-engine/layout.d.ts +29 -4
- package/dist/core/memory-engine/layout.js +24 -4
- package/dist/core/read-root-grant-screen.d.ts +23 -0
- package/dist/core/read-root-grant-screen.js +17 -0
- package/dist/core/runner/contracts.d.ts +1 -1
- package/dist/core/runner/prepare-artifact.d.ts +12 -7
- package/dist/core/runner/prepare-artifact.js +10 -8
- package/dist/core/runner/prepare-ask-lane.d.ts +3 -0
- package/dist/core/runner/prepare-ask-lane.js +5 -1
- package/dist/core/runner/prepare-boundary-parks.js +5 -5
- package/dist/core/runner/prepare-delegation-surface.d.ts +18 -0
- package/dist/core/runner/prepare-delegation-surface.js +6 -25
- package/dist/core/runner/prepare-hands-readface.d.ts +12 -89
- package/dist/core/runner/prepare-hands-readface.js +14 -48
- package/dist/core/runner/prepare-inherited-gate.d.ts +3 -1
- package/dist/core/runner/prepare-inherited-gate.js +2 -1
- package/dist/core/runner/prepare-park-ask.js +6 -4
- package/dist/core/runner/prepare-policy-chain.js +4 -0
- package/dist/core/runner/prepare-task.js +5 -3
- package/dist/core/runner/read-root-grant.d.ts +66 -0
- package/dist/core/runner/read-root-grant.js +77 -0
- package/dist/core/runner/run-notification-lane.d.ts +1 -1
- package/dist/core/runner/run-settle-and-teardown.js +3 -9
- package/dist/core/runner/runtask.js +1 -1
- package/dist/core/runner/send-message-continuation.d.ts +45 -0
- package/dist/core/runner/send-message-continuation.js +27 -0
- package/dist/core/runner/session-read-file-states.d.ts +91 -0
- package/dist/core/runner/session-read-file-states.js +43 -0
- package/dist/core/secret-env.d.ts +1 -1
- package/dist/core/secret-env.js +7 -2
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +2 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +19 -0
- package/dist/core/task-registry-monitor.js +3 -0
- package/dist/core/task-registry-shared.d.ts +16 -3
- package/dist/core/task-registry.d.ts +65 -29
- package/dist/core/task-registry.js +78 -28
- package/dist/core/task-spec.d.ts +5 -3
- package/dist/core/tool-policy.d.ts +23 -0
- package/dist/core/tool-policy.js +15 -4
- package/dist/core/tool-spec.d.ts +2 -0
- package/dist/engine/execution-env/node-execution-env.d.ts +13 -3
- package/dist/engine/execution-env/node-execution-env.js +26 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/dist/scenarios/full-body.d.ts +5 -2
- package/dist/scenarios/full-body.js +14 -7
- package/dist/tools/fs/fs-bash-background-text.d.ts +16 -0
- package/dist/tools/fs/fs-bash-background-text.js +22 -0
- package/dist/tools/fs/fs-bash.d.ts +6 -0
- package/dist/tools/fs/fs-bash.js +9 -27
- package/dist/tools/fs/fs-read.d.ts +2 -2
- package/dist/tools/fs/fs-search-tools.d.ts +3 -2
- package/dist/tools/fs/index.d.ts +8 -1
- package/dist/tools/fs/index.js +2 -3
- package/dist/tools/fs/repo-map.d.ts +2 -1
- package/dist/tools/fs/safety.d.ts +3 -1
- package/dist/tools/fs/safety.js +2 -1
- package/dist/tools/fs/shell-read-check.d.ts +3 -0
- package/dist/tools/fs/shell-read-check.js +1 -1
- package/dist/tools/fs/shell-read-establish.d.ts +3 -1
- package/dist/tools/fs/shell-read-establish.js +2 -2
- package/dist/tools/fs/shell-read-plan.d.ts +2 -2
- package/dist/tools/fs/shell-read-plan.js +15 -10
- package/dist/tools/fs/shell-read-word.d.ts +5 -4
- package/dist/tools/fs/shell-read-word.js +3 -2
- package/dist/tools/monitor.d.ts +5 -4
- package/dist/tools/monitor.js +5 -4
- package/dist/tools/todo.d.ts +3 -3
- package/dist/tools/todo.js +5 -4
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +13 -1
- package/wire-changes/core@7.31.1.json +189 -0
- package/wire-changes/core@7.32.0.json +850 -0
package/dist/core/tool-policy.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { brandPolicyAskClass } from "./ask-class.js";
|
|
4
|
+
import { ownReadRootCandidate, readRootGrantSeat } from "./read-root-grant-screen.js";
|
|
4
5
|
import { decidersNote, settledByPerson } from "./gate-outcome.js";
|
|
5
6
|
import { sanitizeAutoModeArmingRecipe } from "./auto-mode-arming.js";
|
|
6
7
|
import { join, normalize as normalizePath, sep } from "node:path";
|
|
@@ -1160,11 +1161,13 @@ function readApproverOutcomeObject(ok, toolName, editLane) {
|
|
|
1160
1161
|
let allowed;
|
|
1161
1162
|
let suppliedEdit;
|
|
1162
1163
|
let suppliedApprover;
|
|
1164
|
+
let grant;
|
|
1163
1165
|
try {
|
|
1164
1166
|
supplied = ok.settledBy;
|
|
1165
1167
|
allowed = ok.allow;
|
|
1166
1168
|
suppliedEdit = ok.updatedInput;
|
|
1167
1169
|
suppliedApprover = ok.approver;
|
|
1170
|
+
grant = ok.readRootGrant;
|
|
1168
1171
|
}
|
|
1169
1172
|
catch (err) {
|
|
1170
1173
|
return refused(`the approver's outcome for "${toolName}" could not be read (${describeThrown(err)}) — denied fail-closed`, settledByNobody("approver_contract"));
|
|
@@ -1182,6 +1185,9 @@ function readApproverOutcomeObject(ok, toolName, editLane) {
|
|
|
1182
1185
|
if (attribution.defect !== undefined) {
|
|
1183
1186
|
return refused(`the approver for "${toolName}" reported an attribution this seam refuses: ${attribution.defect}; denied fail-closed`, settledByNobody("approver_contract"));
|
|
1184
1187
|
}
|
|
1188
|
+
if (editLane === "none" && grant !== undefined) {
|
|
1189
|
+
return refused(`the approver for "${toolName}" attached a readRootGrant on a seat that offers no directory — denied fail-closed (a grant answers an approval card's readRootCandidate)`, settledByNobody("approver_contract"));
|
|
1190
|
+
}
|
|
1185
1191
|
if (settledWord !== "human" && allowed === true) {
|
|
1186
1192
|
return refused(`the approver for "${toolName}" returned an allow settled by "${settledWord}" — only a person approves, and that word ` +
|
|
1187
1193
|
`names a wait that ended with nobody judging: it cannot be what approved a call; denied fail-closed ` +
|
|
@@ -1205,6 +1211,7 @@ function readApproverOutcomeObject(ok, toolName, editLane) {
|
|
|
1205
1211
|
kind: "deny",
|
|
1206
1212
|
word: settledWord,
|
|
1207
1213
|
note,
|
|
1214
|
+
grant,
|
|
1208
1215
|
settlement: settledWord === "timeout"
|
|
1209
1216
|
? { kind: "approval_window_expired", who: { party: "host" }, when: Date.now() }
|
|
1210
1217
|
: settledWord === "policy"
|
|
@@ -1213,7 +1220,7 @@ function readApproverOutcomeObject(ok, toolName, editLane) {
|
|
|
1213
1220
|
};
|
|
1214
1221
|
}
|
|
1215
1222
|
if (suppliedEdit === undefined)
|
|
1216
|
-
return { kind: "allow", edit: undefined, settlement: settledByPerson("human_allowed", attribution.approver) };
|
|
1223
|
+
return { kind: "allow", edit: undefined, settlement: settledByPerson("human_allowed", attribution.approver), grant };
|
|
1217
1224
|
if (editLane === "none") {
|
|
1218
1225
|
return refused(`the approver for "${toolName}" attached an edit (updatedInput) on a seat that has no lane to adjudicate one — ` +
|
|
1219
1226
|
`this seat's answer is the deployment POLICY's verdict, and an edit arriving through it would execute without the ` +
|
|
@@ -1224,7 +1231,7 @@ function readApproverOutcomeObject(ok, toolName, editLane) {
|
|
|
1224
1231
|
if (!edit.ok) {
|
|
1225
1232
|
return refused(`the approved edit for "${toolName}" is not safely clonable (${edit.reason}) — denied fail-closed`, settledByNobody("presentation_failed"));
|
|
1226
1233
|
}
|
|
1227
|
-
return { kind: "allow", edit: { value: edit.value }, settlement: settledByPerson("human_allowed", attribution.approver) };
|
|
1234
|
+
return { kind: "allow", edit: { value: edit.value }, settlement: settledByPerson("human_allowed", attribution.approver), grant };
|
|
1228
1235
|
}
|
|
1229
1236
|
async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
|
|
1230
1237
|
if (onAsk === "allow") {
|
|
@@ -1260,6 +1267,7 @@ async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
|
|
|
1260
1267
|
settlement: settledByNobody("presentation_failed"),
|
|
1261
1268
|
};
|
|
1262
1269
|
}
|
|
1270
|
+
const offered = ownReadRootCandidate(req.readRootCandidate);
|
|
1263
1271
|
let ok;
|
|
1264
1272
|
try {
|
|
1265
1273
|
const approverView = tryCloneArgs(presented.value);
|
|
@@ -1278,6 +1286,7 @@ async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
|
|
|
1278
1286
|
boundInputHash: boundInputHashOf(presented.value),
|
|
1279
1287
|
args: approverView.value,
|
|
1280
1288
|
...(bidi ? { hasBidiControls: true } : {}),
|
|
1289
|
+
...(offered !== undefined ? { readRootCandidate: { ...offered } } : {}),
|
|
1281
1290
|
}, signal));
|
|
1282
1291
|
const windowMs = req.denialLimitFallback !== undefined && req.denialLimitFallback.autoDenyAfterMs > 0 ? req.denialLimitFallback.autoDenyAfterMs : undefined;
|
|
1283
1292
|
const raced = await withTimeout(raceAskWaitAgainstSignal(wait, signal), windowMs, () => DEADLINE_ELAPSED);
|
|
@@ -1324,6 +1333,7 @@ async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
|
|
|
1324
1333
|
const reading = readApproverOutcomeObject(ok, req.toolName, "readjudicated");
|
|
1325
1334
|
if (reading.kind === "refused")
|
|
1326
1335
|
return { action: "deny", message: reading.message, decisionReason: "mode", settlement: reading.settlement };
|
|
1336
|
+
const grantSeat = readRootGrantSeat(reading.grant, offered, reading.kind === "allow");
|
|
1327
1337
|
if (reading.kind === "deny") {
|
|
1328
1338
|
return {
|
|
1329
1339
|
action: "deny",
|
|
@@ -1334,11 +1344,12 @@ async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
|
|
|
1334
1344
|
: humanRefusalMessage(req, reading.note),
|
|
1335
1345
|
decisionReason: "mode",
|
|
1336
1346
|
settlement: reading.settlement,
|
|
1347
|
+
...grantSeat,
|
|
1337
1348
|
};
|
|
1338
1349
|
}
|
|
1339
1350
|
if (reading.edit === undefined)
|
|
1340
|
-
return { action: "allow", decisionReason: "mode", presentedInput: presented.value, settlement: reading.settlement };
|
|
1341
|
-
return { action: "allow", updatedInput: reading.edit.value, decisionReason: "mode", settlement: reading.settlement };
|
|
1351
|
+
return { action: "allow", decisionReason: "mode", presentedInput: presented.value, settlement: reading.settlement, ...grantSeat };
|
|
1352
|
+
return { action: "allow", updatedInput: reading.edit.value, decisionReason: "mode", settlement: reading.settlement, ...grantSeat };
|
|
1342
1353
|
}
|
|
1343
1354
|
const okRaw = ok;
|
|
1344
1355
|
if (okRaw === true)
|
package/dist/core/tool-spec.d.ts
CHANGED
|
@@ -952,6 +952,8 @@ export interface ToolExecuteContext {
|
|
|
952
952
|
* (only `true` travels there). Undefined when the tool runs outside a Runner task.
|
|
953
953
|
*/
|
|
954
954
|
oneShot?: boolean;
|
|
955
|
+
/** Does this run mount THIS instance in `spec.tools` as handed, pre-gate (Runner-filled)? The parent bound on `SubagentToolOptions.inheritedTools`. */
|
|
956
|
+
mountsCallerTool?: (tool: ToolSpec) => boolean;
|
|
955
957
|
/** — the parent's tool-face controls ({@link TaskSpec.excludeTools} /
|
|
956
958
|
* {@link TaskSpec.deferTools}), inherited down the delegation tree like `principal` (Runner-filled,
|
|
957
959
|
* read-only): a scenario-wide roster must not be escapable by delegating to a child that remounts
|
|
@@ -2,7 +2,7 @@ import { type ExecResult, type ExecutionEnv, ExecutionError, type ExecutionEnvEx
|
|
|
2
2
|
import { type SecretEnvFinding } from "../../core/secret-env.js";
|
|
3
3
|
import type { EngineNotice } from "../../core/engine-notice.js";
|
|
4
4
|
import type { SymlinkChain } from "../harness/types.js";
|
|
5
|
-
import type { BackgroundShellCapability, BackgroundShellId, BackgroundPoll, BackgroundSpawnOptions } from "../../core/background-shell.js";
|
|
5
|
+
import type { BackgroundHardKillOutcome, BackgroundShellCapability, BackgroundShellId, BackgroundPoll, BackgroundSpawnOptions } from "../../core/background-shell.js";
|
|
6
6
|
import { BackgroundShellError } from "../../core/background-shell.js";
|
|
7
7
|
import type { SchedulerCapability, ScheduledIntent, SchedulerContext, ScheduledTaskId, ScheduledTaskSummary } from "../../core/scheduler.js";
|
|
8
8
|
import { SchedulerError } from "../../core/scheduler.js";
|
|
@@ -125,8 +125,9 @@ export declare class NodeExecutionEnv implements ExecutionEnv, BackgroundShellCa
|
|
|
125
125
|
* design/128 T1-1 — deliver-a-living-service mode (default false). When true, background processes
|
|
126
126
|
* OUTLIVE the run: `disposeBackgroundShells()` becomes a no-op (the runner's every-exit-path dispose
|
|
127
127
|
* included) and background stdio is spooled to env-owned temp files (0600, `wx`, unpredictable path)
|
|
128
|
-
* so the child never dies of EPIPE after the host exits. DEPLOYMENT-declared at env construction
|
|
129
|
-
*
|
|
128
|
+
* so the child never dies of EPIPE after the host exits. DEPLOYMENT-declared at env construction (the
|
|
129
|
+
* env lane; `TaskSpec.retainBackgroundProcesses` is the task lane, without this spooling guarantee on
|
|
130
|
+
* every platform — docs/background-process-lifetime.md). The 2h bg-timeout reaper and `killBackground` still work
|
|
130
131
|
* while the host lives; after host exit, cleanup is the deployment's responsibility (e.g. the TB
|
|
131
132
|
* container teardown). v1 covers processes spawned via `execBackground` only (a detach-adopted
|
|
132
133
|
* foreground exec keeps pipe stdio — best-effort survival). Incompatible with durable-suspend
|
|
@@ -221,6 +222,15 @@ export declare class NodeExecutionEnv implements ExecutionEnv, BackgroundShellCa
|
|
|
221
222
|
}, BackgroundShellError>>;
|
|
222
223
|
pollBackground(shellId: BackgroundShellId): Promise<Result<BackgroundPoll, BackgroundShellError>>;
|
|
223
224
|
killBackground(shellId: BackgroundShellId): Promise<Result<void, BackgroundShellError>>;
|
|
225
|
+
/** The synchronous hard kill seat ({@link BackgroundShellCapability.killBackgroundNow}). Reads process truth,
|
|
226
|
+
* never `entry.status` (other stops flip it before their attempt, which can fail or wait on a timer). Once
|
|
227
|
+
* node has observed the leader's exit its pid may name a stranger: `already_gone`, nothing signalled. Before
|
|
228
|
+
* that the leader is still our child (live or an unreaped zombie), so ONE `kill(-pid, SIGKILL)` reaches its
|
|
229
|
+
* group and nothing else — no descendant snapshot (a process that left the group is outside this seat).
|
|
230
|
+
* win32 has no synchronous hard kill: no process groups, and the tree kill's MSYS group discovery needs its
|
|
231
|
+
* deferred step, so this seat answers `no_pgid` and the caller's ordinary kill (the ladder) does the work.
|
|
232
|
+
* Only a landed signal sets the latch; a refused one is `no_pgid`. */
|
|
233
|
+
killBackgroundNow(shellId: BackgroundShellId): BackgroundHardKillOutcome;
|
|
224
234
|
/** design/128 T1-1: incrementally read a retained shell's spool files into the in-memory tails, so
|
|
225
235
|
* pollBackground serves the same cursor semantics whether stdio came over pipes or spool files.
|
|
226
236
|
* Bounded single read per stream per poll (the tail keeps only its rolling window anyway).
|
|
@@ -1339,6 +1339,32 @@ export class NodeExecutionEnv {
|
|
|
1339
1339
|
}
|
|
1340
1340
|
return ok(undefined);
|
|
1341
1341
|
}
|
|
1342
|
+
killBackgroundNow(shellId) {
|
|
1343
|
+
const entry = this.bgShells.get(shellId);
|
|
1344
|
+
if (!entry || entry.hardKilled)
|
|
1345
|
+
return "already_gone";
|
|
1346
|
+
const child = entry.child;
|
|
1347
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
1348
|
+
return "already_gone";
|
|
1349
|
+
const pid = child.pid;
|
|
1350
|
+
if (pid === undefined || pid <= 0)
|
|
1351
|
+
return "no_pgid";
|
|
1352
|
+
if (process.platform === "win32")
|
|
1353
|
+
return "no_pgid";
|
|
1354
|
+
shutdownDebug("killBackgroundNow", { shellId, pid });
|
|
1355
|
+
try {
|
|
1356
|
+
process.kill(-pid, "SIGKILL");
|
|
1357
|
+
}
|
|
1358
|
+
catch (e) {
|
|
1359
|
+
return e.code === "ESRCH" ? "already_gone" : "no_pgid";
|
|
1360
|
+
}
|
|
1361
|
+
entry.hardKilled = true;
|
|
1362
|
+
if (entry.timer)
|
|
1363
|
+
clearTimeout(entry.timer);
|
|
1364
|
+
if (entry.status === "running")
|
|
1365
|
+
entry.status = "killed";
|
|
1366
|
+
return "killed";
|
|
1367
|
+
}
|
|
1342
1368
|
syncSpool(entry) {
|
|
1343
1369
|
const spool = entry.spool;
|
|
1344
1370
|
if (!spool)
|
package/dist/index.d.ts
CHANGED
|
@@ -158,7 +158,7 @@ export { probeParkRoundTrip, durableParkGapOf, durableParkGapFor, PARK_SELFCHECK
|
|
|
158
158
|
export { type StoreDurability } from "./core/checkpoint-store.js";
|
|
159
159
|
export { type StoreFidelity } from "./core/checkpoint-store.js";
|
|
160
160
|
export { projectHumanInput, buildHumanInputEvent, type HumanInputSource, type HumanInputFrame, type HumanInputEvent, type HumanInputDelivery, } from "./core/human-input-projection.js";
|
|
161
|
-
export { TaskRegistry, defaultTaskRegistry, createTaskOutputTool, createTaskStopTool, type SemaTaskType, type SemaTaskStatus, type SemaTaskHandle, type ParkedClaimTicket, type TaskAccess, type UnifiedTaskOutput, type TaskRetrievalStatus, type StopSource, type RegisterMonitorInput, type MonitorTimers, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccessWorkflowRun, DURABLE_AGENT_HANDLE_RE, } from "./core/task-registry.js";
|
|
161
|
+
export { TaskRegistry, defaultTaskRegistry, createTaskOutputTool, createTaskStopTool, type SemaTaskType, type SemaTaskStatus, type SemaTaskHandle, type ParkedClaimTicket, type TaskAccess, type UnifiedTaskOutput, type TaskRetrievalStatus, type StopSource, type RegisterMonitorInput, type MonitorTimers, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccessWorkflowRun, DURABLE_AGENT_HANDLE_RE, type BackgroundShellHardKillReceipt, } from "./core/task-registry.js";
|
|
162
162
|
export { InMemoryMailboxStore, type MailboxStore, type MailboxMessage, type MailboxLease, MailboxStoreError, type MailboxStoreErrorCode, MAILBOX_TOMBSTONED_RECIPIENT_CODE, type MailboxAppendMessage, type MailboxPeerMeta, type MailboxPeerFromMode, type MailboxPeerRecordKind, type MailboxPeerNoticeMeta, type MailboxPeerNoticeState, MAILBOX_PEER_NOTICE_STATES, MAILBOX_PEER_FROM_MODES, MAILBOX_PEER_RECORD_KINDS, MAILBOX_INVALID_PEER_META_CODE, MAILBOX_CROSS_PROCESS_UNSAFE_CODE, readMailboxPeerMeta, mailboxCrossProcessMountVerdict, type MailboxCrossProcessVerdict, type MailboxAckResult, } from "./core/mailbox-store.js";
|
|
163
163
|
export { FileMailboxStore, type FileMailboxStoreOptions } from "./stores/file/mailbox-store.js";
|
|
164
164
|
export { createFileTaskListStore } from "./stores/file/task-list-store.js";
|
|
@@ -172,7 +172,7 @@ export { createWorktreeTools, type WorktreeToolsOptions, type ActiveWorktreeSess
|
|
|
172
172
|
export { sharpImageResizer, type McpImageResizer } from "./core/image-downsample.js";
|
|
173
173
|
export { createImageDownsampler, sharpImageDownsampler, MCP_IMAGE_MAX_BASE64, IMAGE_TARGET_RAW_SIZE, type ImageDownsampler, type ImageDimensions, } from "./core/image-downsample.js";
|
|
174
174
|
export { hasBackgroundShell, BackgroundShellError } from "./core/background-shell.js";
|
|
175
|
-
export type { BackgroundShellCapability, BackgroundShellId, BackgroundShellStatus, BackgroundShellErrorCode, BackgroundSpawnOptions, BackgroundPoll, } from "./core/background-shell.js";
|
|
175
|
+
export type { BackgroundShellCapability, BackgroundShellId, BackgroundShellStatus, BackgroundShellErrorCode, BackgroundSpawnOptions, BackgroundPoll, BackgroundHardKillOutcome, } from "./core/background-shell.js";
|
|
176
176
|
export { hasScheduler, isValidCronExpr, SchedulerError } from "./core/scheduler.js";
|
|
177
177
|
export type { SchedulerCapability, SchedulerErrorCode, ScheduledIntent, SchedulerContext, ScheduledTaskId, ScheduledTaskSummary, } from "./core/scheduler.js";
|
|
178
178
|
export { createSchedulerTools, type SchedulerToolContext, SCHEDULE_WAKEUP_TOOL_NAME, AUTONOMOUS_LOOP_SENTINEL, AUTONOMOUS_LOOP_DYNAMIC_SENTINEL, } from "./tools/scheduler-tools.js";
|
|
@@ -184,7 +184,8 @@ export { createAllowDenyPolicy, createApprovalPolicy,
|
|
|
184
184
|
* policy verdict cannot get re-adjudicated). Exported so a host can NAME it instead of deriving it through
|
|
185
185
|
* `Parameters<typeof createApprovalPolicy>[0]["approve"]`. */
|
|
186
186
|
type ApprovalSeatOutcome, COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, findUnverifiableRecursiveDelete, combinePolicies, decisionText, resolveAsk, toolPolicyNameSets, type ToolPolicyNameSets, type NamedToolPolicy, type ToolPolicyProjection, type ToolPolicyProjectionComponent, type ConstraintChainEntry, checkToolPolicyProjection, constraintChainEntryOf, constraintChainDigest, type ToolPolicy, type ToolCallRequest, type PermissionResult, type DecisionReason, screenApproverAttribution, APPROVER_ATTRIBUTION_MAX_CHARS, type OnAsk, type AskOutcome, type ResolvedAsk, type AskRequest, type AskDelegationProvenance, type AskRuleEvidence, type AskEvidenceAbsence, ASK_EVIDENCE_ABSENCE_VALUES, } from "./core/tool-policy.js";
|
|
187
|
-
export { type
|
|
187
|
+
export { type ReadRootGrantScreen } from "./core/read-root-grant-screen.js";
|
|
188
|
+
export { type AskOrigin, ASK_ORIGINS, isAskOrigin, classifierMayAnswer, ORIGIN_IMPLIES_REAL_APPROVAL, RULE_STORE_UNREADABLE_KINDS, type RuleStoreUnreadable, isRuleStoreUnreadable, PERSISTED_RULE_MANDATES, type PersistedRuleMandate, isPersistedRuleMandate } from "./core/ask-origin.js";
|
|
188
189
|
export { AUTO_MODE_DENY_SOURCES, isAutoModeDenySource, type AutoModeDenySource } from "./core/runtime-caps.js";
|
|
189
190
|
export { SETTLEMENT_KINDS, type SettlementKind, isSettlementKind, type Settlement, SETTLEMENT_IS_REFUSAL, DENIED_BY_VALUES, type DeniedBy, isDeniedBy, DENIED_BY_MAY_VETO, CLASSIFIER_DENY_CAUSES, type ClassifierDenyCause, isClassifierDenyCause, DENIED_BY_MAY_CARRY_CAUSE, type GateDisposition, type GateOutcome, screenGateOutcome } from "./core/gate-outcome.js";
|
|
190
191
|
export { type AskCarry } from "./core/hooks.js";
|
package/dist/index.js
CHANGED
|
@@ -149,7 +149,8 @@ export { createSchedulerTools, SCHEDULE_WAKEUP_TOOL_NAME, AUTONOMOUS_LOOP_SENTIN
|
|
|
149
149
|
export { resolveAutonomousLoopPrompt, AUTONOMOUS_LOOP_PREAMBLE, AUTONOMOUS_LOOP_PREAMBLE_PERSISTENT, } from "./tools/loop-tick.js";
|
|
150
150
|
export { tightenTaskSpec, TaskSpecTightenError } from "./core/tighten-task-spec.js";
|
|
151
151
|
export { createAllowDenyPolicy, createApprovalPolicy, COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, findUnverifiableRecursiveDelete, combinePolicies, decisionText, resolveAsk, toolPolicyNameSets, checkToolPolicyProjection, constraintChainEntryOf, constraintChainDigest, screenApproverAttribution, APPROVER_ATTRIBUTION_MAX_CHARS, ASK_EVIDENCE_ABSENCE_VALUES, } from "./core/tool-policy.js";
|
|
152
|
-
export {
|
|
152
|
+
export {} from "./core/read-root-grant-screen.js";
|
|
153
|
+
export { ASK_ORIGINS, isAskOrigin, classifierMayAnswer, ORIGIN_IMPLIES_REAL_APPROVAL, RULE_STORE_UNREADABLE_KINDS, isRuleStoreUnreadable, PERSISTED_RULE_MANDATES, isPersistedRuleMandate } from "./core/ask-origin.js";
|
|
153
154
|
export { AUTO_MODE_DENY_SOURCES, isAutoModeDenySource } from "./core/runtime-caps.js";
|
|
154
155
|
export { SETTLEMENT_KINDS, isSettlementKind, SETTLEMENT_IS_REFUSAL, DENIED_BY_VALUES, isDeniedBy, DENIED_BY_MAY_VETO, CLASSIFIER_DENY_CAUSES, isClassifierDenyCause, DENIED_BY_MAY_CARRY_CAUSE, screenGateOutcome } from "./core/gate-outcome.js";
|
|
155
156
|
export {} from "./core/hooks.js";
|
|
@@ -16,7 +16,10 @@ export interface CodeToolsConfig {
|
|
|
16
16
|
webFetch?: WebFetchConfig;
|
|
17
17
|
/** WebSearch backend (BRAIN/model-service leg). Absent ⇒ WebSearch is not assembled (core ships no backend). */
|
|
18
18
|
webSearch?: WebSearchConfig;
|
|
19
|
-
/** Subagent delegation. Present ⇒ assembled
|
|
19
|
+
/** Subagent delegation. Present ⇒ assembled. Without an explicit `subagent.tools` a child INHERITS this bundle's
|
|
20
|
+
* surface as the leader's own instances (one `TaskListStore`), bounded by what the spawning run mounts and trimmed
|
|
21
|
+
* by the spawn tier (`SubagentToolOptions.inheritedTools`). Tools composed BESIDE the bundle are not inherited: a
|
|
22
|
+
* host hands those per spawn through `subagent.extraTools`, behind its own gates.
|
|
20
23
|
* 合车复审修③: when this carries NO explicit `systemPrompt`, the assembler defaults it to
|
|
21
24
|
* `CODE_SYSTEM_PROMPT` — a code-scenario child holds Bash/Write side-effect tools, so it keeps the code
|
|
22
25
|
* constitution (snapshot-before-mutate / destructive-op care / git discipline) instead of falling to the
|
|
@@ -47,7 +50,7 @@ export interface CodeToolsConfig {
|
|
|
47
50
|
* design/147 §6 D2 (CC 2.1.216 shared team task list): a shared {@link TaskListStore} for the
|
|
48
51
|
* task-list family. Mount the SAME instance into each teammate's bundle to share one team task
|
|
49
52
|
* list (the `owner` claim semantics come alive); absent ⇒ the private per-run list (unchanged
|
|
50
|
-
* default). Ignored when `taskList: false`.
|
|
53
|
+
* default). Ignored when `taskList: false`. This bundle's own Agent children share it by inheritance.
|
|
51
54
|
*/
|
|
52
55
|
taskListStore?: TaskListStore;
|
|
53
56
|
}
|
|
@@ -4,21 +4,28 @@ import { createTaskListTools } from "../tools/task-list.js";
|
|
|
4
4
|
import { createSubagentTool } from "../agents/subagent.js";
|
|
5
5
|
import { CODE_SYSTEM_PROMPT } from "../prompts/default.js";
|
|
6
6
|
export function assembleCodeTools(cfg = {}) {
|
|
7
|
-
const
|
|
8
|
-
tools.push(webFetchToolSpec(cfg.webFetch ?? {}));
|
|
7
|
+
const web = [webFetchToolSpec(cfg.webFetch ?? {})];
|
|
9
8
|
if (cfg.webSearch)
|
|
10
|
-
|
|
11
|
-
if (cfg.subagent)
|
|
12
|
-
tools.push(createSubagentTool({ ...cfg.subagent, systemPrompt: cfg.subagent.systemPrompt ?? CODE_SYSTEM_PROMPT }));
|
|
9
|
+
web.push(createWebSearchTool(cfg.webSearch));
|
|
13
10
|
const stamp = (t) => ({ ...t, modelGate: "task-scaffold" });
|
|
14
11
|
const taskListOn = cfg.taskList !== false;
|
|
12
|
+
const scaffolds = [];
|
|
15
13
|
if (cfg.todoWrite !== false && !taskListOn) {
|
|
16
|
-
|
|
14
|
+
scaffolds.push(cfg.todoWrite === true ? createTodoWriteTool() : stamp(createTodoWriteTool()));
|
|
17
15
|
}
|
|
18
16
|
if (taskListOn) {
|
|
19
17
|
const family = createTaskListTools(cfg.taskListStore);
|
|
20
|
-
|
|
18
|
+
scaffolds.push(...(cfg.taskList === true ? family : family.map(stamp)));
|
|
21
19
|
}
|
|
20
|
+
const tools = [...web];
|
|
21
|
+
if (cfg.subagent) {
|
|
22
|
+
tools.push(createSubagentTool({
|
|
23
|
+
...cfg.subagent,
|
|
24
|
+
systemPrompt: cfg.subagent.systemPrompt ?? CODE_SYSTEM_PROMPT,
|
|
25
|
+
inheritedTools: cfg.subagent.inheritedTools ?? [...web, ...scaffolds],
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
tools.push(...scaffolds);
|
|
22
29
|
return tools;
|
|
23
30
|
}
|
|
24
31
|
export const CODE_ROLE = { systemPrompt: CODE_SYSTEM_PROMPT, thinking: "high" };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Bash tool's background-lifetime TEXT: the one sentence the tool description and every launch / adoption
|
|
3
|
+
* receipt render about whether a `run_in_background` process outlives the task, and the three fragments it is
|
|
4
|
+
* built from. Wording only — which rows the reaps take is the registry's (task-registry.ts); the retain answer is
|
|
5
|
+
* handed in, judged by the tool mount through THE retain predicate (background-shell.ts).
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Lifetime guidance AT THE DECISION MOMENT. The tool description alone does not steer the model:
|
|
9
|
+
* delivered services get hosted via run_in_background and reaped at THEIR OWNING RUN'S teardown — see
|
|
10
|
+
* {@link bgReapClause} for the session-scoped/session-less/isolated-owned-env split — where CC's models
|
|
11
|
+
* habitually use `nohup … &` instead, and BOTH engines kill resident bg rows at teardown. Pure prompt
|
|
12
|
+
* face: the kill semantics
|
|
13
|
+
* (CC-aligned) are untouched. ONE construction point: a ctrl+b-adopted command is the SAME lifetime class
|
|
14
|
+
* (its own leg says so in a comment), so the sentence it gets must be the same string, not a second copy.
|
|
15
|
+
*/
|
|
16
|
+
export declare function backgroundLifetimeNote(retained: boolean, sessionScoped: boolean, envIsolatedOwned: boolean, face?: "receipt" | "description"): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function bgReapClause(sessionScoped, envIsolatedOwned) {
|
|
2
|
+
if (!sessionScoped)
|
|
3
|
+
return "when this task ends";
|
|
4
|
+
return envIsolatedOwned
|
|
5
|
+
? "at session end, not when this task ends — UNLESS this task's own isolated sandbox is torn down, which reaps it right then instead"
|
|
6
|
+
: "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";
|
|
7
|
+
}
|
|
8
|
+
function bgNohupCaveat(envIsolatedOwned) {
|
|
9
|
+
return envIsolatedOwned
|
|
10
|
+
? " — 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"
|
|
11
|
+
: "";
|
|
12
|
+
}
|
|
13
|
+
function bgRetainedIsolatedNote() {
|
|
14
|
+
return " Background processes are retained here — 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.";
|
|
15
|
+
}
|
|
16
|
+
export function backgroundLifetimeNote(retained, sessionScoped, envIsolatedOwned, face = "receipt") {
|
|
17
|
+
if (retained)
|
|
18
|
+
return envIsolatedOwned ? bgRetainedIsolatedNote() : "";
|
|
19
|
+
if (face === "description")
|
|
20
|
+
return ` Background processes do NOT survive the session — they are reaped ${bgReapClause(sessionScoped, envIsolatedOwned)}. 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(envIsolatedOwned)}.`;
|
|
21
|
+
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)}.`;
|
|
22
|
+
}
|
|
@@ -167,6 +167,12 @@ export declare function createBashTool(env: ExecutionEnv, rootCanonical: string,
|
|
|
167
167
|
* hint's open tag by the mint home (see {@link import("./index.js").HandsToolkitOptions.reminderMark}).
|
|
168
168
|
* Absent ⇒ historic bare tag. */
|
|
169
169
|
reminderMark?: string;
|
|
170
|
+
/** The task lane of THE retain predicate (`TaskSpec.retainBackgroundProcesses` of the run this mount
|
|
171
|
+
* serves; see {@link import("./index.js").HandsToolkitOptions.retainBackgroundProcesses}). Read by the
|
|
172
|
+
* lifetime sentence (description + receipts) and stamped onto every background row this tool registers
|
|
173
|
+
* (passing it — true or false — states the lane on the row). Absent ⇒ the sentence reads not-retained
|
|
174
|
+
* and the rows leave the lane to the owning run's teardown. */
|
|
175
|
+
retainBackgroundProcesses?: boolean;
|
|
170
176
|
}): AgentTool;
|
|
171
177
|
/**
|
|
172
178
|
* `bash_readonly` (effect:read) — a restricted shell for the verifier read-only boundary: a single
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -5,13 +5,14 @@ import { defaultTaskRegistry } from "../../core/task-registry.js";
|
|
|
5
5
|
import { accountDroppedBytes, spoolDropNote } from "../../core/task-registry-shared.js";
|
|
6
6
|
import { RollingTailBuffer } from "../../core/exec-output-tail.js";
|
|
7
7
|
import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_MISSING_ID_MESSAGE, TASK_STOP_MISSING_ID_MESSAGE, TASK_STOP_PARAMS, resolveTaskIdArg, ENV_DIRECT_TASK_TOOL_CAPS, composeTaskOutputDescription, composeTaskOutputParams, composeTaskStopDescription, } from "../../core/task-tool-shape.js";
|
|
8
|
-
import { hasBackgroundShell } from "../../core/background-shell.js";
|
|
8
|
+
import { envDeclaresRetain, hasBackgroundShell, retainsBackgroundProcesses } from "../../core/background-shell.js";
|
|
9
9
|
import { delimitUntrusted } from "../../core/untrusted-text.js";
|
|
10
10
|
import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
|
|
11
11
|
import { imageMagicMatches, withinAnyRoot } from "./safety.js";
|
|
12
12
|
import { isDeadCwd, diagnoseDeadCwd } from "./dead-cwd.js";
|
|
13
13
|
import { boundaryIsTheSandbox, hasDestroy, isIsolated } from "../../core/remote-env.js";
|
|
14
14
|
import { ghRateLimitHint } from "./gh-rate-limit.js";
|
|
15
|
+
import { backgroundLifetimeNote } from "./fs-bash-background-text.js";
|
|
15
16
|
import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, shellRecoveryHint, CWD_SENTINEL, stripCwdSentinel, } from "./fs-shared.js";
|
|
16
17
|
import { bashClassifyDefaultAllow, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoopDetailed, classifyCompoundReadonlyDetailed } from "./bash-readonly-classifier.js";
|
|
17
18
|
import { toolFace } from "../../core/tool-catalog-entries.js";
|
|
@@ -495,21 +496,6 @@ async function runShell(env, cwd, toolName, command, timeoutSec, caps, signal, c
|
|
|
495
496
|
},
|
|
496
497
|
};
|
|
497
498
|
}
|
|
498
|
-
function bgReapClause(sessionScoped, envIsolatedOwned) {
|
|
499
|
-
if (!sessionScoped)
|
|
500
|
-
return "when this task ends";
|
|
501
|
-
return envIsolatedOwned
|
|
502
|
-
? "at session end, not when this task ends — UNLESS this task's own isolated sandbox is torn down, which reaps it right then instead"
|
|
503
|
-
: "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";
|
|
504
|
-
}
|
|
505
|
-
function bgNohupCaveat(envIsolatedOwned) {
|
|
506
|
-
return envIsolatedOwned
|
|
507
|
-
? " — 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"
|
|
508
|
-
: "";
|
|
509
|
-
}
|
|
510
|
-
function bgRetainedIsolatedNote() {
|
|
511
|
-
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.";
|
|
512
|
-
}
|
|
513
499
|
function bashDescription(coAuthor, caps, bgNotifies = false, bgRetained = false, bgSessionScoped = false, bgEnvIsolatedOwned = false) {
|
|
514
500
|
const coAuthorLines = coAuthor === false
|
|
515
501
|
? "- Follow the deployment's commit-message conventions."
|
|
@@ -520,7 +506,7 @@ function bashDescription(coAuthor, caps, bgNotifies = false, bgRetained = false,
|
|
|
520
506
|
- 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.
|
|
521
507
|
- File search by name: use the Glob tool (NOT \`find\` or \`ls\`); content search: use the Grep tool (NOT \`grep\`/\`rg\` in the shell).
|
|
522
508
|
- \`timeout\` is in milliseconds: default ${caps.defaultMs}, max ${caps.maxMs}.
|
|
523
|
-
- \`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
|
|
509
|
+
- \`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.${backgroundLifetimeNote(bgRetained, bgSessionScoped, bgEnvIsolatedOwned, "description")}
|
|
524
510
|
|
|
525
511
|
# Git
|
|
526
512
|
- Interactive flags (\`-i\`, e.g. \`git rebase -i\`, \`git add -i\`) are not supported in this environment.
|
|
@@ -557,6 +543,7 @@ async function adoptBackgroundShell(req) {
|
|
|
557
543
|
...(sessionScoped ? { sessionScoped: true } : {}),
|
|
558
544
|
...(outputFile !== undefined ? { outputFile } : {}),
|
|
559
545
|
...(onNotify !== undefined ? { onTerminal: (n) => onNotify(n, { priority: "next" }) } : {}),
|
|
546
|
+
...(wiring.retainBackgroundProcesses !== undefined ? { retainBackgroundProcesses: wiring.retainBackgroundProcesses } : {}),
|
|
560
547
|
});
|
|
561
548
|
return { ok: true, taskId, outputFile };
|
|
562
549
|
}
|
|
@@ -574,16 +561,11 @@ async function adoptBackgroundShell(req) {
|
|
|
574
561
|
return { ok: false, reason, killed: false, killMsg: kill !== undefined && !kill.ok ? kill.error.message : "killBackground threw" };
|
|
575
562
|
}
|
|
576
563
|
}
|
|
577
|
-
function backgroundLifetimeNote(caps, sessionScoped, envIsolatedOwned) {
|
|
578
|
-
if (caps.retainBackgroundProcesses === true)
|
|
579
|
-
return envIsolatedOwned ? bgRetainedIsolatedNote() : "";
|
|
580
|
-
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)}.`;
|
|
581
|
-
}
|
|
582
564
|
export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = { current: rootCanonical }, taskOpts = {}) {
|
|
583
565
|
const timeoutCaps = resolveBashTimeoutCaps(taskOpts);
|
|
584
566
|
const timeoutCapsSecView = bashTimeoutCapsSec(timeoutCaps);
|
|
585
567
|
const bgNotifies = taskOpts.taskRegistry !== undefined && taskOpts.onTaskNotification !== undefined;
|
|
586
|
-
const bgRetained = hasBackgroundShell(env) && env.
|
|
568
|
+
const bgRetained = hasBackgroundShell(env) && retainsBackgroundProcesses({ env: envDeclaresRetain(env), task: taskOpts.retainBackgroundProcesses === true });
|
|
587
569
|
const bgSessionScoped = taskOpts.sessionId !== undefined && taskOpts.taskRegistry !== undefined;
|
|
588
570
|
const bgEnvIsolatedOwned = hasDestroy(env) && isIsolated(env);
|
|
589
571
|
return defineTool({
|
|
@@ -652,7 +634,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
652
634
|
? ` Time budget: requested ${requestedTimeoutSec}s, capped at ${bgCaps.maxBgTimeoutSec}s (env ceiling: requests above ${bgCaps.maxBgTimeoutSec}s are reduced to it) — the process may run up to ${appliedTimeoutSec}s.`
|
|
653
635
|
: ` Time budget: the process may run up to ${appliedTimeoutSec}s (hard cap ${bgCaps.maxBgTimeoutSec}s).`
|
|
654
636
|
: "";
|
|
655
|
-
const lifetimeNote = backgroundLifetimeNote(
|
|
637
|
+
const lifetimeNote = backgroundLifetimeNote(bgRetained, bgSessionScoped, bgEnvIsolatedOwned);
|
|
656
638
|
const registry = taskOpts.taskRegistry;
|
|
657
639
|
if (registry !== undefined) {
|
|
658
640
|
const onNotify = taskOpts.onTaskNotification;
|
|
@@ -664,7 +646,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
664
646
|
toolUseId: ctx.toolCallId,
|
|
665
647
|
ctxTaskId: ctx.taskId,
|
|
666
648
|
ctxPrincipal: ctx.principal,
|
|
667
|
-
wiring: { sessionId: taskOpts.sessionId, taskOwner: taskOpts.taskOwner, taskScope: taskOpts.taskScope, onNotify },
|
|
649
|
+
wiring: { sessionId: taskOpts.sessionId, taskOwner: taskOpts.taskOwner, taskScope: taskOpts.taskScope, onNotify, retainBackgroundProcesses: taskOpts.retainBackgroundProcesses },
|
|
668
650
|
});
|
|
669
651
|
if (!adopted.ok) {
|
|
670
652
|
if (adopted.killed) {
|
|
@@ -736,7 +718,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
736
718
|
toolUseId: ctx.toolCallId,
|
|
737
719
|
ctxTaskId: ctx.taskId,
|
|
738
720
|
ctxPrincipal: ctx.principal,
|
|
739
|
-
wiring: { sessionId: taskOpts.sessionId, taskOwner: taskOpts.taskOwner, taskScope: taskOpts.taskScope, onNotify },
|
|
721
|
+
wiring: { sessionId: taskOpts.sessionId, taskOwner: taskOpts.taskOwner, taskScope: taskOpts.taskScope, onNotify, retainBackgroundProcesses: taskOpts.retainBackgroundProcesses },
|
|
740
722
|
});
|
|
741
723
|
if (!adopted.ok) {
|
|
742
724
|
const outTail = stdoutSoFar.slice(-1_000);
|
|
@@ -784,7 +766,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
784
766
|
? `You will be notified when it completes — do not poll.`
|
|
785
767
|
: `Poll TaskOutput until its status is no longer "running".`) +
|
|
786
768
|
adoptBudgetNote +
|
|
787
|
-
backgroundLifetimeNote(
|
|
769
|
+
backgroundLifetimeNote(bgRetained, bgSessionScoped, bgEnvIsolatedOwned) +
|
|
788
770
|
(tail ? `\n--- output so far (tail) ---\n${tail}` : ""),
|
|
789
771
|
details: { type: "bash", detached: true, task_id: taskId, ...(description !== undefined ? { description } : {}), ...(outputFile !== undefined ? { output_file: outputFile } : {}), ...(cause === "timeout" || backgroundedForQueuedMessage ? { autoBackgrounded: true } : {}) },
|
|
790
772
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
|
|
2
|
-
import { type ReadFileState } from "./safety.js";
|
|
2
|
+
import { type ReadFileState, type RootsSeat } from "./safety.js";
|
|
3
3
|
import { type PdfModelCapabilities } from "./pdf.js";
|
|
4
4
|
import { type ReadImageDownsamplerOption, type CwdRef } from "./fs-shared.js";
|
|
5
5
|
import { type ReminderDisclosureCounts } from "../../core/reminder-disclosure.js";
|
|
6
|
-
export declare function createReadFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?:
|
|
6
|
+
export declare function createReadFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: RootsSeat, imageDownsampler?: ReadImageDownsamplerOption, pdfCapabilities?: PdfModelCapabilities, bgOutputReadExemption?: (canonicalKey: string, ctx: {
|
|
7
7
|
taskId?: string;
|
|
8
8
|
principal?: string;
|
|
9
9
|
}) => boolean, readCyberReminder?: boolean, readDeny?: import("./read-deny.js").ReadDenyMatcher, readFace?: import("./containment-face.js").ContainmentFace, reminderMark?: string, reminderDisclosureCounts?: ReminderDisclosureCounts): AgentTool;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
|
|
2
|
+
import { type RootsSeat } from "./safety.js";
|
|
2
3
|
import type { ReadDenyMatcher } from "./read-deny.js";
|
|
3
4
|
import type { ContainmentFace } from "./containment-face.js";
|
|
4
5
|
import type { CwdRef } from "./fs-shared.js";
|
|
@@ -11,5 +12,5 @@ import type { CwdRef } from "./fs-shared.js";
|
|
|
11
12
|
* honesty boundary.
|
|
12
13
|
*/
|
|
13
14
|
export declare function grepDetailFields(text: string, mode: "files_with_matches" | "content" | "count", offset?: number, structuredRows?: readonly import("./search.js").GrepRow[]): Record<string, unknown>;
|
|
14
|
-
export declare function createGrepTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?:
|
|
15
|
-
export declare function createGlobTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?:
|
|
15
|
+
export declare function createGrepTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: RootsSeat, readDeny?: ReadDenyMatcher, readFace?: ContainmentFace, cwdRef?: CwdRef): AgentTool;
|
|
16
|
+
export declare function createGlobTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: RootsSeat, readDeny?: ReadDenyMatcher, readFace?: ContainmentFace, cwdRef?: CwdRef): AgentTool;
|
package/dist/tools/fs/index.d.ts
CHANGED
|
@@ -28,7 +28,8 @@ export interface HandsToolkitOptions {
|
|
|
28
28
|
/** The read-boundary whitelist: extra allowed containment roots for the READ faces only —
|
|
29
29
|
* read_file/grep/glob/repo_map and `bash_readonly` admit them; edit_file/write_file/notebook_edit
|
|
30
30
|
* and the full `bash`'s disclosure roots deliberately do NOT (widening reads must never silently
|
|
31
|
-
* widen writes).
|
|
31
|
+
* widen writes). Read per call, never copied: a caller that grows the array it passed (the Runner's
|
|
32
|
+
* in-flight read-root grant) widens the structured read tools for their later calls. Caveat, the same tradeoff `additionalRoots` states:
|
|
32
33
|
* `bash_readonly`'s COMMAND allowlist is a deployment declaration — a
|
|
33
34
|
* deployment that puts a mutating command into `bashReadonlyAllow` has declared it read-safe,
|
|
34
35
|
* and that declaration applies inside every root, these included. Keep the allowlist genuinely
|
|
@@ -69,6 +70,12 @@ export interface HandsToolkitOptions {
|
|
|
69
70
|
taskScope?: string;
|
|
70
71
|
/** See createBashTool's taskOpts.sessionId: session-resident background commands. */
|
|
71
72
|
sessionId?: string;
|
|
73
|
+
/** The task lane of THE retain predicate (`retainsBackgroundProcesses`): the serving run's
|
|
74
|
+
* `TaskSpec.retainBackgroundProcesses`. The Runner fills it from the spec of the run it builds this band
|
|
75
|
+
* for; a direct mount passes its own declaration. Read by Bash's lifetime sentence (description +
|
|
76
|
+
* receipts) and stamped onto every background row Bash registers (stated). Absent ⇒ not stated: the rows
|
|
77
|
+
* leave the lane to the owning run's teardown. */
|
|
78
|
+
retainBackgroundProcesses?: boolean;
|
|
72
79
|
/** Mount TaskOutput/TaskStop directly from this band. Runner sets false and mounts the unified dispatcher once. */
|
|
73
80
|
mountBackgroundTaskTools?: boolean;
|
|
74
81
|
/** Completion-notification sink for finished background commands (threaded to createBashTool's
|
package/dist/tools/fs/index.js
CHANGED
|
@@ -26,9 +26,7 @@ export function handsWriteHalfMounts(opts) {
|
|
|
26
26
|
}
|
|
27
27
|
export function createHandsToolkit(env, readFileState, rootCanonical, opts = {}) {
|
|
28
28
|
const { includeShell = false, readOnly = false, bashReadonlyAllow, commitCoAuthor = false, mountBackgroundTaskTools = true, additionalRoots, additionalReadRoots, } = opts;
|
|
29
|
-
const readFaceRoots = additionalReadRoots === undefined
|
|
30
|
-
? additionalRoots
|
|
31
|
-
: [...(additionalRoots ?? []), ...additionalReadRoots];
|
|
29
|
+
const readFaceRoots = additionalReadRoots === undefined ? additionalRoots : () => (additionalReadRoots.length === 0 ? additionalRoots : [...(additionalRoots ?? []), ...additionalReadRoots]);
|
|
32
30
|
const cwdRef = opts.cwdRef ?? { current: rootCanonical };
|
|
33
31
|
const bgReadRegistry = opts.taskRegistry;
|
|
34
32
|
const bgOutputReadExemption = bgReadRegistry === undefined
|
|
@@ -79,6 +77,7 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
|
|
|
79
77
|
taskOwner: opts.taskOwner,
|
|
80
78
|
taskScope: opts.taskScope,
|
|
81
79
|
...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),
|
|
80
|
+
...(opts.retainBackgroundProcesses !== undefined ? { retainBackgroundProcesses: opts.retainBackgroundProcesses } : {}),
|
|
82
81
|
onTaskNotification: opts.taskNotification,
|
|
83
82
|
detachHub: opts.detachHub,
|
|
84
83
|
...(opts.autoBackgroundOnTimeout !== undefined ? { autoBackgroundOnTimeout: opts.autoBackgroundOnTimeout } : {}),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentTool } from "../../internal/harness-types.js";
|
|
2
2
|
import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
3
|
+
import { type RootsSeat } from "./safety.js";
|
|
3
4
|
import type { ReadDenyMatcher } from "./read-deny.js";
|
|
4
5
|
import type { ContainmentFace } from "./containment-face.js";
|
|
5
6
|
/** Extract top-level symbol names (deduped, in first-seen order) from source text. A lightweight
|
|
@@ -13,4 +14,4 @@ import type { ContainmentFace } from "./containment-face.js";
|
|
|
13
14
|
* (src/core/auto-promote.ts) consumes it in ESCALATE-ONLY mode — a clean scan contributes nothing;
|
|
14
15
|
* it can only ever raise an escalation, never clear one. */
|
|
15
16
|
export declare function extractSymbols(text: string): string[];
|
|
16
|
-
export declare function createRepoMapTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?:
|
|
17
|
+
export declare function createRepoMapTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: RootsSeat, readDeny?: ReadDenyMatcher, readFace?: ContainmentFace): AgentTool;
|
|
@@ -434,6 +434,8 @@ export declare function joinForFamily(family: PathFamily, base: string, rel: str
|
|
|
434
434
|
* view's "does the TARGET" — a symlink AT a guarded spelling resolves away from it canonically,
|
|
435
435
|
* and only this view still names it. */
|
|
436
436
|
export declare function lexicalViewOf(spelled: string, base: string): string;
|
|
437
|
+
/** A face's extra containment roots: a fixed list, or a getter over a run's live set (read once per resolution). */
|
|
438
|
+
export type RootsSeat = readonly string[] | (() => readonly string[] | undefined);
|
|
437
439
|
/**
|
|
438
440
|
* Resolve a model-supplied path to a canonical key AND enforce rootPath containment. Existing paths
|
|
439
441
|
* use `canonicalPath` (resolves symlinks, so the same file cannot get two keys). New paths
|
|
@@ -441,7 +443,7 @@ export declare function lexicalViewOf(spelled: string, base: string): string;
|
|
|
441
443
|
* therefore cannot land the target outside root. Returns the canonical key, or a `path_not_in_root`
|
|
442
444
|
* violation. Defense-in-depth at the tool layer; NOT a substitute for a sandboxed `executionEnv`.
|
|
443
445
|
*/
|
|
444
|
-
export declare function resolveKey(env: ExecutionEnv, rootCanonical: string, path: string, signal?: AbortSignal, baseCwd?: string,
|
|
446
|
+
export declare function resolveKey(env: ExecutionEnv, rootCanonical: string, path: string, signal?: AbortSignal, baseCwd?: string, additionalRoots?: RootsSeat, exactFileReadExemption?: (canonicalKey: string) => boolean, readDeny?: {
|
|
445
447
|
matchTarget(canonicalKey: string, lexicalView?: string, opts?: {
|
|
446
448
|
readonly aliasResolved?: boolean;
|
|
447
449
|
}): {
|
package/dist/tools/fs/safety.js
CHANGED
|
@@ -448,11 +448,12 @@ function within(rootCanonical, p) {
|
|
|
448
448
|
return true;
|
|
449
449
|
return pN.identity.startsWith(rootN.identity.endsWith(SEP) ? rootN.identity : rootN.identity + SEP);
|
|
450
450
|
}
|
|
451
|
-
export async function resolveKey(env, rootCanonical, path, signal, baseCwd,
|
|
451
|
+
export async function resolveKey(env, rootCanonical, path, signal, baseCwd, additionalRoots, exactFileReadExemption, readDeny, containmentFace) {
|
|
452
452
|
const canon = await canonicalizeTarget(env, path, signal, pathFamilyOf({ root: rootCanonical }), baseCwd);
|
|
453
453
|
if (!canon.ok)
|
|
454
454
|
return { ok: false, violation: { code: "invalid", message: canon.message } };
|
|
455
455
|
const key = canon.key;
|
|
456
|
+
const additionalRootsCanonical = typeof additionalRoots === "function" ? additionalRoots() : additionalRoots;
|
|
456
457
|
if (isBlockedDevicePath(key)) {
|
|
457
458
|
return { ok: false, violation: { code: "invalid", message: `path "${path}" is a blocked device/special file (reading it can hang the process); refused.` } };
|
|
458
459
|
}
|
|
@@ -89,6 +89,9 @@ export interface ShellReadStationOptions {
|
|
|
89
89
|
readonly boundaryOf: (toolName: string, cwd: string | undefined) => ShellReadBoundary | undefined;
|
|
90
90
|
/** The listed read verbs (decides whether a segment is read by verb slots or word by word). */
|
|
91
91
|
readonly readers?: ReadonlySet<string>;
|
|
92
|
+
/** `true` ⇔ every root `boundaryOf` returns is already a canonical identity (the Runner's): the station never
|
|
93
|
+
* re-resolves one, so a root swapped for a link after it was declared or granted cannot bring in the link's target. */
|
|
94
|
+
readonly rootsCanonical?: boolean;
|
|
92
95
|
}
|
|
93
96
|
/** What a consumer hands {@link judgeShellRead}: the command as written, the environment whose filesystem
|
|
94
97
|
* answers where its words land (symlinks resolved, patterns expanded — a `NodeExecutionEnv` over a real
|
|
@@ -170,7 +170,7 @@ export function createShellReadStation(opts) {
|
|
|
170
170
|
const key = roots.join("\0");
|
|
171
171
|
let e = establishers.get(key);
|
|
172
172
|
if (e === undefined) {
|
|
173
|
-
e = createShellReadEstablisher(opts.env, roots);
|
|
173
|
+
e = createShellReadEstablisher(opts.env, roots, { rootsCanonical: opts.rootsCanonical === true });
|
|
174
174
|
establishers.set(key, e);
|
|
175
175
|
}
|
|
176
176
|
return e;
|
|
@@ -79,4 +79,6 @@ export declare function establishShellReadsLexically(plan: ShellReadPlan, bounda
|
|
|
79
79
|
* as declared (canonical by contract); they are resolved once more per instance because the comparison is
|
|
80
80
|
* against `realpath` output on both sides — an aliased root would otherwise refuse every read under itself.
|
|
81
81
|
*/
|
|
82
|
-
export declare function createShellReadEstablisher(env: ExecutionEnv, readRoots: readonly string[]
|
|
82
|
+
export declare function createShellReadEstablisher(env: ExecutionEnv, readRoots: readonly string[], opts?: {
|
|
83
|
+
readonly rootsCanonical?: boolean;
|
|
84
|
+
}): ShellReadEstablisher;
|