@sema-agent/core 7.31.1 → 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.
Files changed (86) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/agents/inherited-tool-pool.d.ts +28 -0
  3. package/dist/agents/inherited-tool-pool.js +15 -0
  4. package/dist/agents/list-agents-tool.d.ts +7 -0
  5. package/dist/agents/list-agents-tool.js +9 -2
  6. package/dist/agents/peer-session-drain.d.ts +2 -0
  7. package/dist/agents/peer-session-drain.js +1 -0
  8. package/dist/agents/send-message-tool.d.ts +17 -0
  9. package/dist/agents/send-message-tool.js +11 -4
  10. package/dist/agents/subagent.d.ts +10 -1
  11. package/dist/agents/subagent.js +24 -8
  12. package/dist/core/ask-origin.d.ts +15 -6
  13. package/dist/core/ask-origin.js +15 -9
  14. package/dist/core/background-shell.d.ts +45 -0
  15. package/dist/core/background-shell.js +13 -0
  16. package/dist/core/checkpoint-store.d.ts +5 -0
  17. package/dist/core/checkpoint-store.js +3 -2
  18. package/dist/core/engine-notice.d.ts +9 -0
  19. package/dist/core/gate-lanes.js +1 -1
  20. package/dist/core/governance-codes.d.ts +1 -1
  21. package/dist/core/governance-codes.js +4 -0
  22. package/dist/core/hooks.d.ts +2 -0
  23. package/dist/core/read-root-grant-screen.d.ts +23 -0
  24. package/dist/core/read-root-grant-screen.js +17 -0
  25. package/dist/core/runner/contracts.d.ts +1 -1
  26. package/dist/core/runner/prepare-artifact.d.ts +12 -7
  27. package/dist/core/runner/prepare-artifact.js +10 -8
  28. package/dist/core/runner/prepare-ask-lane.d.ts +3 -0
  29. package/dist/core/runner/prepare-ask-lane.js +5 -1
  30. package/dist/core/runner/prepare-boundary-parks.js +5 -5
  31. package/dist/core/runner/prepare-delegation-surface.d.ts +18 -0
  32. package/dist/core/runner/prepare-delegation-surface.js +6 -25
  33. package/dist/core/runner/prepare-hands-readface.d.ts +12 -89
  34. package/dist/core/runner/prepare-hands-readface.js +14 -48
  35. package/dist/core/runner/prepare-inherited-gate.d.ts +3 -1
  36. package/dist/core/runner/prepare-inherited-gate.js +2 -1
  37. package/dist/core/runner/prepare-park-ask.js +6 -4
  38. package/dist/core/runner/prepare-policy-chain.js +4 -0
  39. package/dist/core/runner/prepare-task.js +5 -3
  40. package/dist/core/runner/read-root-grant.d.ts +66 -0
  41. package/dist/core/runner/read-root-grant.js +77 -0
  42. package/dist/core/runner/run-notification-lane.d.ts +1 -1
  43. package/dist/core/runner/run-settle-and-teardown.js +3 -9
  44. package/dist/core/runner/runtask.js +1 -1
  45. package/dist/core/runner/send-message-continuation.d.ts +45 -0
  46. package/dist/core/runner/send-message-continuation.js +27 -0
  47. package/dist/core/runner/session-read-file-states.d.ts +91 -0
  48. package/dist/core/runner/session-read-file-states.js +43 -0
  49. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +2 -0
  50. package/dist/core/store-contracts/checkpoint-store-contract.js +19 -0
  51. package/dist/core/task-registry-monitor.js +3 -0
  52. package/dist/core/task-registry-shared.d.ts +16 -3
  53. package/dist/core/task-registry.d.ts +65 -29
  54. package/dist/core/task-registry.js +78 -28
  55. package/dist/core/task-spec.d.ts +5 -3
  56. package/dist/core/tool-policy.d.ts +23 -0
  57. package/dist/core/tool-policy.js +15 -4
  58. package/dist/core/tool-spec.d.ts +2 -0
  59. package/dist/engine/execution-env/node-execution-env.d.ts +13 -3
  60. package/dist/engine/execution-env/node-execution-env.js +26 -0
  61. package/dist/index.d.ts +4 -3
  62. package/dist/index.js +2 -1
  63. package/dist/scenarios/full-body.d.ts +5 -2
  64. package/dist/scenarios/full-body.js +14 -7
  65. package/dist/tools/fs/fs-bash-background-text.d.ts +16 -0
  66. package/dist/tools/fs/fs-bash-background-text.js +22 -0
  67. package/dist/tools/fs/fs-bash.d.ts +6 -0
  68. package/dist/tools/fs/fs-bash.js +9 -27
  69. package/dist/tools/fs/fs-read.d.ts +2 -2
  70. package/dist/tools/fs/fs-search-tools.d.ts +3 -2
  71. package/dist/tools/fs/index.d.ts +8 -1
  72. package/dist/tools/fs/index.js +2 -3
  73. package/dist/tools/fs/repo-map.d.ts +2 -1
  74. package/dist/tools/fs/safety.d.ts +3 -1
  75. package/dist/tools/fs/safety.js +2 -1
  76. package/dist/tools/fs/shell-read-check.d.ts +3 -0
  77. package/dist/tools/fs/shell-read-check.js +1 -1
  78. package/dist/tools/fs/shell-read-establish.d.ts +3 -1
  79. package/dist/tools/fs/shell-read-establish.js +2 -2
  80. package/dist/tools/monitor.d.ts +5 -4
  81. package/dist/tools/monitor.js +5 -4
  82. package/dist/tools/todo.d.ts +3 -3
  83. package/dist/tools/todo.js +5 -4
  84. package/package.json +1 -1
  85. package/test/export-surface.snapshot.json +13 -1
  86. package/wire-changes/core@7.32.0.json +850 -0
@@ -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,
@@ -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)
@@ -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
- * no TaskSpec/model-facing switch exists. The 2h bg-timeout reaper and `killBackground` still work
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 AskOrigin, ASK_ORIGINS, isAskOrigin, classifierMayAnswer, ORIGIN_IMPLIES_REAL_APPROVAL, RULE_STORE_UNREADABLE_KINDS, type RuleStoreUnreadable, isRuleStoreUnreadable } from "./core/ask-origin.js";
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 { ASK_ORIGINS, isAskOrigin, classifierMayAnswer, ORIGIN_IMPLIES_REAL_APPROVAL, RULE_STORE_UNREADABLE_KINDS, isRuleStoreUnreadable } from "./core/ask-origin.js";
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; child rosters are ⊆ parent via `resolveToolSubset` inside the tool.
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 tools = [];
8
- tools.push(webFetchToolSpec(cfg.webFetch ?? {}));
7
+ const web = [webFetchToolSpec(cfg.webFetch ?? {})];
9
8
  if (cfg.webSearch)
10
- tools.push(createWebSearchTool(cfg.webSearch));
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
- tools.push(cfg.todoWrite === true ? createTodoWriteTool() : stamp(createTodoWriteTool()));
14
+ scaffolds.push(cfg.todoWrite === true ? createTodoWriteTool() : stamp(createTodoWriteTool()));
17
15
  }
18
16
  if (taskListOn) {
19
17
  const family = createTaskListTools(cfg.taskListStore);
20
- tools.push(...(cfg.taskList === true ? family : family.map(stamp)));
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
@@ -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 ? (bgEnvIsolatedOwned ? bgRetainedIsolatedNote() : "") : ` Background processes do NOT survive the session — they are reaped ${bgReapClause(bgSessionScoped, bgEnvIsolatedOwned)}. A deliverable that must stay alive afterwards (a server, a daemon) needs a self-detaching FOREGROUND start instead: run \`nohup cmd >log 2>&1 &\` as a normal foreground command (portable; setsid does not exist on macOS), or use a service manager${bgNohupCaveat(bgEnvIsolatedOwned)}.`}
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.backgroundCapabilities.retainBackgroundProcesses === true;
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(bgCaps, bgSessionScoped, bgEnvIsolatedOwned);
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(adoptCaps, bgSessionScoped, bgEnvIsolatedOwned) +
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?: readonly string[], imageDownsampler?: ReadImageDownsamplerOption, pdfCapabilities?: PdfModelCapabilities, bgOutputReadExemption?: (canonicalKey: string, ctx: {
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?: readonly string[], readDeny?: ReadDenyMatcher, readFace?: ContainmentFace, cwdRef?: CwdRef): AgentTool;
15
- export declare function createGlobTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[], readDeny?: ReadDenyMatcher, readFace?: ContainmentFace, cwdRef?: CwdRef): AgentTool;
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;
@@ -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). Caveat, the same tradeoff `additionalRoots` states:
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
@@ -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 || additionalReadRoots.length === 0
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?: readonly string[], readDeny?: ReadDenyMatcher, readFace?: ContainmentFace): AgentTool;
17
+ export declare function createRepoMapTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: RootsSeat, readDeny?: ReadDenyMatcher, readFace?: ContainmentFace): AgentTool;