@sema-agent/core 7.31.1 → 7.32.1
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 +75 -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 +61 -0
- package/dist/core/background-shell.js +14 -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 +4 -0
- package/dist/core/hooks.d.ts +2 -0
- 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 -11
- 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/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 +17 -4
- package/dist/core/task-registry.d.ts +68 -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/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.32.0.json +850 -0
- package/wire-changes/core@7.32.1.json +78 -0
package/dist/core/task-spec.d.ts
CHANGED
|
@@ -1138,9 +1138,11 @@ export interface TaskSpec {
|
|
|
1138
1138
|
* retention there would be illusory, so the teardown keeps its honest killed receipts instead.
|
|
1139
1139
|
* - win32: best-effort "no active kill" only; without detached process groups, process-tree survival
|
|
1140
1140
|
* past the host is a platform boundary (some children may still exit with it).
|
|
1141
|
-
* -
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
1141
|
+
* - Automatic reaps leave a retained process alone: the run-teardown sweep (settle + dispose), the
|
|
1142
|
+
* session-release reap, and the exit-boundary hard face under `includeRetained: false`. What still ends
|
|
1143
|
+
* it: the TaskStop tool, its per-shell background timeout (while the host runs), env destruction, and the
|
|
1144
|
+
* hard face under `includeRetained: true`. The row carries the lane it was launched under (a later run's
|
|
1145
|
+
* declaration does not re-label a session-resident row) — see docs/background-process-lifetime.md.
|
|
1144
1146
|
* - NOT inherited by delegated child tasks: each child's own spec decides for the child's own
|
|
1145
1147
|
* background processes.
|
|
1146
1148
|
*/
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AskOrigin } from "./ask-origin.js";
|
|
2
|
+
import { type ReadRootGrantScreen } from "./read-root-grant-screen.js";
|
|
2
3
|
import { type Settlement } from "./gate-outcome.js";
|
|
3
4
|
/** A tool call presented to a policy before it executes. */
|
|
4
5
|
/** design/388 B6 — the per-call face (see {@link ToolCallRequest.face}). */
|
|
@@ -332,6 +333,9 @@ export type PermissionResult = {
|
|
|
332
333
|
* set, and a policy self-declaring it only opts its own ask OUT of automatic answering. Carried
|
|
333
334
|
* onto the approval request as {@link AskRequest.mandated}. */
|
|
334
335
|
mandated?: true;
|
|
336
|
+
/** The TABLE WORD the {@link mandated} bit stands on — see {@link AskRequest.mandate}. ENGINE-STAMPED at
|
|
337
|
+
* the same origin stamp as the bit; a policy's self-declared value is dropped there. */
|
|
338
|
+
mandate?: import("./ask-origin.js").PersistedRuleMandate;
|
|
335
339
|
/** The probe named a directory that would clear this ask
|
|
336
340
|
* ({@link import("./types.js").ReversibilityVerdict.readRootCandidate}). Display metadata like the two
|
|
337
341
|
* `probe*` above; ENGINE-STAMPED and VALIDATED at the tighten, and deliberately NOT merged onto an ask
|
|
@@ -790,6 +794,7 @@ export type ApprovalSeatOutcome = boolean | (Extract<AskOutcome, {
|
|
|
790
794
|
allow: boolean;
|
|
791
795
|
}> & {
|
|
792
796
|
updatedInput?: never;
|
|
797
|
+
readRootGrant?: never;
|
|
793
798
|
});
|
|
794
799
|
/**
|
|
795
800
|
* Human-in-the-loop approval for selected tools. Tools in `requireApproval` call `approve(req)` and
|
|
@@ -1294,6 +1299,12 @@ export interface AskRequest {
|
|
|
1294
1299
|
* gate, never caller/worker-settable; tightening-only.
|
|
1295
1300
|
*/
|
|
1296
1301
|
readonly mandated?: true;
|
|
1302
|
+
/** WHICH word of the closed {@link import("./ask-origin.js").PERSISTED_RULE_MANDATES} set {@link mandated} stands
|
|
1303
|
+
* on — present ⇔ that table word is the bit's WHOLE reason (`probe_mandate` = a read outside the session's
|
|
1304
|
+
* directories). ABSENT beside `mandated: true` = another door holds too (governance, a hook, an ancestor's
|
|
1305
|
+
* approver, a policy's own claim) — absence never names a word. Stamped by the gate beside
|
|
1306
|
+
* the bit, copied by the four mint stations; the park row carries the same word. Never adjudication input. */
|
|
1307
|
+
readonly mandate?: import("./ask-origin.js").PersistedRuleMandate;
|
|
1297
1308
|
/** Present ⇔ this ask is the classifier DENIAL-LIMIT fallback (see the
|
|
1298
1309
|
* {@link PermissionResult} ask-arm member of the same name): the counts that tripped the bound and
|
|
1299
1310
|
* this ask's own auto-deny window. Two readers: a card renders it as a countdown; {@link resolveAsk}
|
|
@@ -1488,6 +1499,13 @@ export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal)
|
|
|
1488
1499
|
* that were quietly dropped would leave a settlement looking unattributed rather than misreported. It is
|
|
1489
1500
|
* carried on the arms where the CALLER settled something (its allow, its human/timeout deny) and never
|
|
1490
1501
|
* on an end the engine produced (an abort, a throw, an unclonable edit): nobody approved those.
|
|
1502
|
+
*
|
|
1503
|
+
* `readRootGrant` — "yes, and add this directory": the card's {@link AskRequest.readRootCandidate} `dir`, sent back
|
|
1504
|
+
* byte for byte (`shell.read_boundary.grant_candidate_is_the_grant`). Beside `allow: true` on an ask carrying that
|
|
1505
|
+
* candidate, THIS run's read directories include it from then on (read faces only; `covers: "exact"` = that one
|
|
1506
|
+
* path) — announced `approval.read_root_granted`. Anything else changes nothing and is announced
|
|
1507
|
+
* `approval.read_root_grant_rejected`; the verdict it rode stands. A seat with no card (a policy's `approve`)
|
|
1508
|
+
* refuses it. Keeping the directory beyond this run is the deployment's (`TaskSpec.additionalReadDirectories`).
|
|
1491
1509
|
*/
|
|
1492
1510
|
export type AskOutcome = boolean | "unavailable" | {
|
|
1493
1511
|
allow: boolean;
|
|
@@ -1495,6 +1513,7 @@ export type AskOutcome = boolean | "unavailable" | {
|
|
|
1495
1513
|
settledBy?: "human" | "timeout" | "policy";
|
|
1496
1514
|
reason?: string;
|
|
1497
1515
|
approver?: string;
|
|
1516
|
+
readRootGrant?: string;
|
|
1498
1517
|
};
|
|
1499
1518
|
/**
|
|
1500
1519
|
* ruled 2026-08-04 — forward an approver into a delegated child, stamping every ask it raises with the
|
|
@@ -1649,6 +1668,8 @@ export type ResolvedAsk = (Extract<PermissionResult, {
|
|
|
1649
1668
|
* (string modes present nothing). */
|
|
1650
1669
|
presentedInput?: unknown;
|
|
1651
1670
|
approverUnavailable?: never;
|
|
1671
|
+
/** The answer's `readRootGrant` ({@link AskOutcome}), screened against the request's candidate. */
|
|
1672
|
+
readRootGrant?: ReadRootGrantScreen;
|
|
1652
1673
|
}) | (Extract<PermissionResult, {
|
|
1653
1674
|
action: "deny";
|
|
1654
1675
|
}> & {
|
|
@@ -1657,6 +1678,8 @@ export type ResolvedAsk = (Extract<PermissionResult, {
|
|
|
1657
1678
|
settlement: Settlement;
|
|
1658
1679
|
presentedInput?: unknown;
|
|
1659
1680
|
approverUnavailable?: true;
|
|
1681
|
+
/** A grant riding a refusal: always `rejected` (`on_refusal`). */
|
|
1682
|
+
readRootGrant?: ReadRootGrantScreen;
|
|
1660
1683
|
});
|
|
1661
1684
|
/**
|
|
1662
1685
|
* Does any string reachable in `value` carry a {@link BIDI_CONTROL_RE} member? Bounded, cycle-safe,
|
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 } : {}),
|