@sema-agent/core 7.44.0 → 7.45.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 (75) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/dist/agents/subagent.js +33 -14
  3. package/dist/agents/tool-filter.d.ts +65 -4
  4. package/dist/agents/tool-filter.js +127 -4
  5. package/dist/core/agent-definition.d.ts +2 -2
  6. package/dist/core/allow-entry.d.ts +25 -1
  7. package/dist/core/allow-entry.js +29 -4
  8. package/dist/core/ask-origin.d.ts +23 -4
  9. package/dist/core/ask-origin.js +10 -4
  10. package/dist/core/checkpoint-store.d.ts +2 -2
  11. package/dist/core/decision-receipt.js +1 -1
  12. package/dist/core/effective-path-target.d.ts +7 -6
  13. package/dist/core/effective-path-target.js +3 -7
  14. package/dist/core/engine-notice.d.ts +20 -3
  15. package/dist/core/engine-notice.js +12 -6
  16. package/dist/core/gate-lanes.js +2 -0
  17. package/dist/core/gate-outcome.d.ts +5 -0
  18. package/dist/core/gate-outcome.js +2 -0
  19. package/dist/core/hooks.d.ts +2 -0
  20. package/dist/core/memory-engine/content-origin.d.ts +0 -34
  21. package/dist/core/memory-engine/content-origin.js +9 -4
  22. package/dist/core/memory-engine/layout.d.ts +5 -1
  23. package/dist/core/memory-engine/layout.js +1 -0
  24. package/dist/core/permission-rules.d.ts +4 -0
  25. package/dist/core/permission-rules.js +5 -2
  26. package/dist/core/real-approval.d.ts +9 -6
  27. package/dist/core/real-approval.js +5 -5
  28. package/dist/core/runner/child-tree.d.ts +13 -0
  29. package/dist/core/runner/child-tree.js +3 -0
  30. package/dist/core/runner/contracts.d.ts +8 -2
  31. package/dist/core/runner/prepare-ask-lane.d.ts +2 -0
  32. package/dist/core/runner/prepare-ask-lane.js +2 -2
  33. package/dist/core/runner/prepare-config-doors.js +3 -1
  34. package/dist/core/runner/prepare-execution-env.js +2 -1
  35. package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
  36. package/dist/core/runner/prepare-gate-stations.js +2 -1
  37. package/dist/core/runner/prepare-listings.d.ts +4 -4
  38. package/dist/core/runner/prepare-listings.js +2 -22
  39. package/dist/core/runner/prepare-park-ask.d.ts +2 -0
  40. package/dist/core/runner/prepare-park-ask.js +2 -2
  41. package/dist/core/runner/prepare-policy-chain.d.ts +2 -0
  42. package/dist/core/runner/prepare-policy-chain.js +4 -2
  43. package/dist/core/runner/prepare-task.js +10 -6
  44. package/dist/core/runner/resume-policy-outcome.js +2 -1
  45. package/dist/core/runner/run-leg.js +1 -1
  46. package/dist/core/runner/run-notification-lane.js +2 -2
  47. package/dist/core/runner/runtask.d.ts +2 -0
  48. package/dist/core/runner/runtask.js +2 -0
  49. package/dist/core/runner/stream-steer-verb.js +2 -2
  50. package/dist/core/runner/sub-agent-deny-lists.d.ts +32 -0
  51. package/dist/core/runner/sub-agent-deny-lists.js +121 -0
  52. package/dist/core/runner/synthetic-tools.d.ts +1 -1
  53. package/dist/core/runner/synthetic-tools.js +3 -2
  54. package/dist/core/session-policy-store.d.ts +6 -2
  55. package/dist/core/session-policy-store.js +2 -1
  56. package/dist/core/skill-grant.d.ts +10 -2
  57. package/dist/core/skill-grant.js +20 -4
  58. package/dist/core/task-notification.d.ts +12 -1
  59. package/dist/core/task-notification.js +13 -1
  60. package/dist/core/task-spec.d.ts +5 -3
  61. package/dist/core/tool-policy.d.ts +15 -4
  62. package/dist/core/tool-policy.js +9 -13
  63. package/dist/core/tool-spec.d.ts +20 -2
  64. package/dist/core/types.d.ts +1 -1
  65. package/dist/core/types.js +1 -1
  66. package/dist/index.d.ts +5 -3
  67. package/dist/index.js +5 -3
  68. package/dist/orchestration/workflow.js +2 -1
  69. package/dist/tools/fs/removal-targets.d.ts +82 -0
  70. package/dist/tools/fs/removal-targets.js +236 -0
  71. package/dist/tools/fs/shell-read-write-targets.d.ts +27 -0
  72. package/dist/tools/fs/shell-read-write-targets.js +1 -1
  73. package/package.json +1 -1
  74. package/test/export-surface.snapshot.json +33 -1
  75. package/wire-changes/core@7.45.0.json +1084 -0
@@ -125,6 +125,11 @@ export type SettlementParty = (typeof SETTLEMENT_PARTIES)[number];
125
125
  export type SettlementPartiesMatchTheRecord = AssertAllKeysHandled<Exclude<Settlement["who"]["party"], SettlementParty> | Exclude<SettlementParty, Settlement["who"]["party"]>>;
126
126
  /** Membership test for {@link SettlementParty}. */
127
127
  export declare function isSettlementParty(v: unknown): v is SettlementParty;
128
+ /** Who REPORTED a durable decide (`hostDecision.decidedBy`): a person, the host's own SLA sweep at the row's deadline, or the
129
+ * deployment's policy — the closed set; a host's decide door mirrors the words it accepts from this one. */
130
+ export declare const HOST_DECIDERS: readonly ["person", "sla_timeout", "policy"];
131
+ export type HostDecider = (typeof HOST_DECIDERS)[number];
132
+ export declare const isHostDecider: (word: unknown) => word is HostDecider;
128
133
  /** The DECIDER arms of {@link Settlement} — a person's and the deployment policy's, the two parties that
129
134
  * answer an ask either way. Named so a site that both composes one and quotes its note can read the seat
130
135
  * off the record instead of re-deriving it. */
@@ -25,6 +25,8 @@ const SETTLEMENT_PARTY_SET = new Set(SETTLEMENT_PARTIES);
25
25
  export function isSettlementParty(v) {
26
26
  return SETTLEMENT_PARTY_SET.has(v);
27
27
  }
28
+ export const HOST_DECIDERS = Object.freeze(["person", "sla_timeout", "policy"]);
29
+ export const isHostDecider = (word) => HOST_DECIDERS.some((w) => w === word);
28
30
  export function decidersNote(text) {
29
31
  return text === "" ? undefined : text;
30
32
  }
@@ -1466,6 +1466,8 @@ export interface ToolGateInput extends PermissionSeatInputs, SkillGateSeats, All
1466
1466
  permissionMode?: () => import("./ask-origin.js").PermissionMode;
1467
1467
  /** The leg's approval-seat form (`WiringManifest.ask.form`): the deny exit's way-out clause names a declaration, never a missing approver. */
1468
1468
  askSeatForm?: import("./wiring-manifest.js").AskSeamForm;
1469
+ /** The read face the leg resolved, read per call into the class derivation (`roots` withholds the classifier from a `probe` question); absent ⇒ none resolved. */
1470
+ readFace?: () => import("../tools/fs/containment-face.js").ContainmentFace | undefined;
1469
1471
  /** WHO put the tool's egress / irreversibility marks on the seat ({@link import("./ask-origin.js").ToolMarkSource});
1470
1472
  * absent or empty with a mark present reads as the deployment's own (fail-closed: an operator's mandate). */
1471
1473
  toolMarkSources?: ReadonlySet<import("./ask-origin.js").ToolMarkSource>;
@@ -1,37 +1,3 @@
1
- /**
2
- * design/178 §3 — the tool content-origin classification (three tiers by CAPABILITY, fail-closed).
3
- *
4
- * The question this face answers per mounted tool: can invoking it bring content from OUTSIDE the
5
- * deployment's trust boundary into the session — and therefore, via the automatic harvest path,
6
- * into long-term memory? Three tiers:
7
- *
8
- * - "external" — network-retrieval families and external protocol channels. Invocation marks the
9
- * session's memory EXTERNALLY EXPOSED (one-way, durable; what the mark does at
10
- * harvest is the provenance mode's question — design/336: tag-admit with an origin
11
- * marker under "carry", full quarantine under "off").
12
- * - "execution" — general execution (shell/exec). A NAMED residual channel: it can reach external
13
- * content indirectly, but the two industry anchors this tier is calibrated against
14
- * gate on network/protocol families only, and excluding every shell session would
15
- * disable memory for the main coding scenario. Not polluting by default; the
16
- * `execIsExternalContent` strict knob upgrades it to external.
17
- * - "local" — purely local reads/computation.
18
- *
19
- * WHO CLASSIFIES (single-source rule): core built-ins declare `contentOrigin` on their own ToolSpec
20
- * (the classification lives with the tool definition); protocol tools (MCP/A2A) are structurally
21
- * external (the channel itself crosses the boundary); a HOST tool declares its own — and when it
22
- * does not, the default is EXTERNAL (fail-closed: unknown = external), overridable only through the
23
- * explicit `TaskSpec.memory.trustedTools` allowlist. A core-mounted built-in without a declaration
24
- * defaults local: core owns those definitions, and the fail-closed default exists for tools core
25
- * CANNOT vouch for, not for its own. A declaration is honored only when it SPELLS a member of the
26
- * closed vocabulary — an unreadable one is not a declaration.
27
- *
28
- * The three tier names are BOUNDARY-RELATIVE, not statements about topology: `"local"` means "does
29
- * not bring content from outside the deployment's trust boundary", which a unix socket to another
30
- * process the deployment itself runs satisfies and a loopback HTTP call to somebody else's service
31
- * does not. Distance is not the axis; whose content it is, is.
32
- *
33
- * There is deliberately no "gate inactive" state: every mounted tool gets a class.
34
- */
35
1
  import type { ToolContentOrigin } from "../types.js";
36
2
  export interface ClassifyToolContentOriginInput {
37
3
  /** The tool's own `contentOrigin` declaration (ToolSpec/AgentTool carry) — wins when present AND
@@ -1,3 +1,4 @@
1
+ import { allowedToolNames, readDeniedEntry, readToolNameList } from "../../agents/tool-filter.js";
1
2
  const TOOL_CONTENT_ORIGIN_SET = { external: true, execution: true, local: true };
2
3
  export const TOOL_CONTENT_ORIGINS = Object.keys(TOOL_CONTENT_ORIGIN_SET);
3
4
  export function isToolContentOrigin(value) {
@@ -27,10 +28,14 @@ export function delegationCallIsExternal(input) {
27
28
  if (face === undefined)
28
29
  return true;
29
30
  const selectedFor = (f) => {
30
- const d = f.denyTools ?? [];
31
- const a = f.allowTools;
32
- const isNarrowed = a !== undefined && !a.includes("*");
33
- return input.pool.filter((t) => (!isNarrowed || a.some((x) => selectedBy(x, t))) && !d.some((x) => selectedBy(x, t)));
31
+ const deniedList = readToolNameList(f.denyTools);
32
+ const d = deniedList.kind === "entries" ? deniedList.entries : [];
33
+ const a = allowedToolNames(f.allowTools);
34
+ const denies = (t) => d.some((x) => {
35
+ const read = readDeniedEntry(x);
36
+ return read.kind === "name" && selectedBy(read.name, t);
37
+ });
38
+ return input.pool.filter((t) => (a === undefined || [...a].some((x) => selectedBy(x, t))) && !denies(t));
34
39
  };
35
40
  if (selectedFor(face).some((t) => input.isPolluting(t)))
36
41
  return true;
@@ -259,6 +259,10 @@ export declare function adoptRootScopeLayout(rootPath: string, controlDir: strin
259
259
  export declare const canonicalize: typeof physicalPathOfExisting;
260
260
  /** True iff `child` (canonicalized) is `root` or inside it. */
261
261
  export declare function isContainedIn(root: string, child: string): boolean;
262
+ /** The codes the store's physical form refuses an act with — the whole family, one closed set: {@link TARGET_OUTSIDE_STORE} and
263
+ * {@link SCOPE_HOME_NOT_DIRECTORY}. Each is a plain `Error` carrying `code` and a `detail` ({@link StoreFormRefusalDetail}). */
264
+ export declare const STORE_FORM_REFUSAL_CODES: readonly ["memory.target_outside_store", "memory.scope_home_not_directory"];
265
+ export type StoreFormRefusalCode = (typeof STORE_FORM_REFUSAL_CODES)[number];
262
266
  /** The code an act on a store path is refused with when it would land outside the store ({@link landsOutsideStore}). */
263
267
  export declare const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
264
268
  /** The code a scope home that is not a real directory is refused with ({@link assertHomePhysicalForm}). */
@@ -283,7 +287,7 @@ export type StoreFormRefusalDetail = {
283
287
  * the engine's own account is intact, the layout it was handed is not the layout it needs) carrying a
284
288
  * `code` and a `detail`. */
285
289
  export type StoreFormRefusal = Error & {
286
- code: typeof TARGET_OUTSIDE_STORE | typeof SCOPE_HOME_NOT_DIRECTORY;
290
+ code: StoreFormRefusalCode;
287
291
  detail: StoreFormRefusalDetail;
288
292
  };
289
293
  /** Seat 1 — an act the store's physical form refuses: `act` names what was refused (no path), `path` is the
@@ -731,6 +731,7 @@ export function isContainedIn(root, child) {
731
731
  const c = canonicalize(child);
732
732
  return c === r || c.startsWith(`${r}${sep}`);
733
733
  }
734
+ export const STORE_FORM_REFUSAL_CODES = Object.freeze(["memory.target_outside_store", "memory.scope_home_not_directory"]);
734
735
  export const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
735
736
  export const SCOPE_HOME_NOT_DIRECTORY = "memory.scope_home_not_directory";
736
737
  export const SCOPE_HOME_FORMS = Object.freeze(["symlink", "file", "other"]);
@@ -149,6 +149,10 @@ export interface PermissionRulePolicyOptions {
149
149
  * own; #644). Facts of the DEPLOYMENT and the task, never of the rule — a pattern is stored as spelled.
150
150
  * A rule whose base is absent (or not an absolute path) here is refused at compile time
151
151
  * (`unsupported.path_base`, naming the base — {@link ruleBasesNeeded} is the same classification).
152
+ * A call's own target resolves against these bases as given; when they hold neither `root` nor `cwd`,
153
+ * against the request's (`ToolCallRequest.root` / `.cwd`, stamped by the Runner) — never a mix of the two
154
+ * sources. A target neither supplies the base for is unreadable: every path rule able to reach the call
155
+ * answers it with the fail-closed ask (`ruleUnreadable`).
152
156
  */
153
157
  pathBases?: PathRuleBases;
154
158
  }
@@ -366,10 +366,13 @@ export function createPermissionRulePolicy(rules, opts) {
366
366
  ...(callFace !== undefined ? faceRules[behavior].filter((r) => r.faces.includes(callFace)) : []),
367
367
  ];
368
368
  const commandParam = catalogRuleFaceOf("Bash")?.primaryParams[0] ?? "command";
369
+ const policyNamesTree = pathBases.root !== undefined || pathBases.cwd !== undefined;
370
+ const callBasesOf = (req) => policyNamesTree ? pathBases : { ...pathBases, ...(typeof req.root === "string" ? { root: req.root } : {}), ...(typeof req.cwd === "string" ? { cwd: req.cwd } : {}) };
369
371
  const contentHit = (lane, req) => {
370
372
  if (lane.length === 0)
371
373
  return undefined;
372
- const target = effectivePathTargetOf(req, protectivePathTargetOf(req), pathBases);
374
+ const bases = callBasesOf(req);
375
+ const target = effectivePathTargetOf(req, protectivePathTargetOf(req), bases);
373
376
  const command = isPlainObjectArgs(req.args) ? req.args[commandParam] : undefined;
374
377
  let unreadable;
375
378
  for (const r of lane) {
@@ -386,7 +389,7 @@ export function createPermissionRulePolicy(rules, opts) {
386
389
  unreadable ??= { rule: r, unreadable: target.reason };
387
390
  continue;
388
391
  }
389
- const outcome = pathRuleReachOf(r.parsed, target.identity, pathBases);
392
+ const outcome = pathRuleReachOf(r.parsed, target.identity, bases);
390
393
  if (outcome.reach === "reached")
391
394
  return { rule: r };
392
395
  if (outcome.reach === "unreadable")
@@ -6,6 +6,7 @@
6
6
  * may still refuse it.
7
7
  */
8
8
  import type { Checkpoint } from "./checkpoint-store.js";
9
+ import type { HostDecider } from "./gate-outcome.js";
9
10
  import type { OrgGateVerdict } from "./hooks.js";
10
11
  import type { AskRequest, PermissionResult, ResolvedAsk } from "./tool-policy.js";
11
12
  /** The synchronous seat's refusal of a configuration's allow: one settlement word for both forms
@@ -16,10 +17,12 @@ export declare function unjudgedAllowRefused(req: AskRequest, by: "blanket" | "p
16
17
  /** A durable row demands a person when its gate carries the real-approval bit (the gate's own, which every resume
17
18
  * check reads; the pending action's `requiresRealApproval` is its display twin). */
18
19
  export declare function rowDemandsAPerson(cp: Checkpoint): boolean;
19
- /** A question a durable decide is held against, in each form a station holds one: the parked row, a policy's
20
- * re-check of an approver's edit, and the organization's CURRENT verdict about the call. */
21
- export type HeldQuestion = Checkpoint | PermissionResult | OrgGateVerdict;
22
- /** Whether a durable decide made by `decidedBy` cannot answer the held question: a question demanding a person is not
20
+ /** A question a decide is held against, in each form a station holds one: the parked row, a policy's re-check of an
21
+ * approver's edit, the organization's CURRENT verdict about the call, and the request an approver was handed (the card a
22
+ * live door answers). */
23
+ export type HeldQuestion = Checkpoint | PermissionResult | OrgGateVerdict | AskRequest;
24
+ /** Whether a decide made by `decidedBy` cannot answer the held question: a question demanding a person is not
23
25
  * the policy's to answer. One predicate for every station that holds a decide against such a question — the resume
24
- * entry (the row, an edit's re-check, the org's current verdict) and the execution belt (the last two, re-read). */
25
- export declare function policyCannotAnswer(question: HeldQuestion, decidedBy: "person" | "sla_timeout" | "policy"): boolean;
26
+ * entry (the row, an edit's re-check, the org's current verdict), the execution belt (the last two, re-read) and a live
27
+ * door answering the request an approver was handed. */
28
+ export declare function policyCannotAnswer(question: HeldQuestion, decidedBy: HostDecider): boolean;
@@ -16,11 +16,11 @@ export function rowDemandsAPerson(cp) {
16
16
  return cp.gate.kind === "irreversible_ask" && cp.gate.realApproval !== undefined;
17
17
  }
18
18
  function demandsAPerson(question) {
19
- if ("gate" in question)
20
- return rowDemandsAPerson(question);
21
- if ("action" in question)
22
- return question.action === "ask" && question.requiresRealApproval === true;
23
- return question.status === "available" && question.verdict?.behavior === "ask";
19
+ const gate = Reflect.get(question, "gate");
20
+ const verdict = Reflect.get(question, "verdict");
21
+ const row = typeof gate === "object" && gate !== null && rowDemandsAPerson(question);
22
+ const org = Reflect.get(question, "status") === "available" && typeof verdict === "object" && verdict !== null && Reflect.get(verdict, "behavior") === "ask";
23
+ return row || Reflect.get(question, "requiresRealApproval") === true || org;
24
24
  }
25
25
  export function policyCannotAnswer(question, decidedBy) {
26
26
  return decidedBy === "policy" && demandsAPerson(question);
@@ -0,0 +1,13 @@
1
+ import type { RunnerDeps } from "../types.js";
2
+ /**
3
+ * Whether this deployment can place a child agent in a tree OTHER than the caller's — the one fact behind
4
+ * every request of that shape: an explicit `cwd`, a workflow agent's `isolation: "worktree"`, and the
5
+ * delegation tool's per-call or per-definition `isolation: "worktree"`.
6
+ *
7
+ * Only an `executionEnvFactory` can: it builds one execution env per task, so a child can be rooted
8
+ * wherever the request says. A static `executionEnv` is ONE shared env with ONE cwd — every child that
9
+ * runs on it runs in the caller's root, whatever tree it was asked to work in. A request the deployment
10
+ * cannot honor is refused before the child starts (and, for a worktree, before one is minted); each site
11
+ * keeps its own code and sentence for the refusal, but none decides the condition itself.
12
+ */
13
+ export declare function deploymentCanRerootChild(deps: Pick<RunnerDeps, "executionEnvFactory">): boolean;
@@ -0,0 +1,3 @@
1
+ export function deploymentCanRerootChild(deps) {
2
+ return deps.executionEnvFactory !== undefined;
3
+ }
@@ -31,7 +31,7 @@ import type { MaterializedA2a } from "../a2a.js";
31
31
  import type { CompactionForkContext, CompactionPhaseDurations, MaybeCompactOptions, RapidRefillState } from "../auto-compaction.js";
32
32
  import type { AutoModeArmingRecipe } from "../auto-mode-arming.js";
33
33
  import type { AutoModeDecider, AutoModeDenialTracker, DenialLimitFallbackFace } from "../auto-mode.js";
34
- import type { SettlementKind } from "../gate-outcome.js";
34
+ import type { HostDecider, SettlementKind } from "../gate-outcome.js";
35
35
  import type { CacheBreakDetector, ToolFingerprintInput } from "../cache-break-detector.js";
36
36
  import type { Checkpoint, CheckpointState, CheckpointStore, CheckpointToken, PendingSteerEntry, PlatformLimitReason, ReopenReason, ResourceLedger, ResourceLimitReason, ResumeOutcome, SerializedCheckpointState } from "../checkpoint-store.js";
37
37
  import type { ExecutionOutcomeRecordWord } from "../checkpoint-execution-record.js";
@@ -391,6 +391,8 @@ export interface Prepared {
391
391
  /** The per-task logical cwd ref when a real shell is mounted (else undefined). The Runner
392
392
  * reads `cwdRef.current` after each tool to detect a `cd` move and emit `workspace_changed`. */
393
393
  cwdRef?: CwdRef;
394
+ /** The root the hand tools resolve root-relative spellings against (present with hands; read-only hands included). */
395
+ handsRoot?: string;
394
396
  /** The run's TOOL-PROGRESS lane seat, late-bound for the same reason `harnessRef` is (the tool band is
395
397
  * built during prepare; the lane needs `pushContent` and `ident`, which exist only after it returns).
396
398
  * The driver binds `current` right after `pushContent` exists. Always present on a completed prepare —
@@ -442,7 +444,7 @@ export interface Prepared {
442
444
  toolCallId: string;
443
445
  toolName: string;
444
446
  args: unknown;
445
- decidedBy: "person" | "sla_timeout" | "policy";
447
+ decidedBy: HostDecider;
446
448
  }) => Promise<{
447
449
  readonly refused: string;
448
450
  } | {
@@ -2748,6 +2750,10 @@ export interface RunnerSelfSeat {
2748
2750
  toolPolicy?: RunnerDeps["toolPolicy"];
2749
2751
  hooks?: RunnerDeps["hooks"];
2750
2752
  };
2753
+ /** Whether a child task THIS runner runs can be placed in a tree other than the caller's (an explicit cwd, a worktree):
2754
+ * true only when its deps carry an `executionEnvFactory` (live read — `swapDeps` replaces the deps). The delegation tool asks
2755
+ * the runner that will run the child, not the one that ran the parent leg. Absent on a hand-built seat ⇒ read as "cannot say" ⇒ no; a hand-built seat answers for the runner its `runTask` forwards to. */
2756
+ canRerootChild?(): boolean;
2751
2757
  /** Fire `RunnerDeps.onTaskOutcome` through the swallow-guarded chokepoint (thin compositions emit at their terminal state). */
2752
2758
  emitTaskOutcome(outcome: TaskOutcome): void;
2753
2759
  /** Run a task to completion. `internals` is the TRUSTED run-scoped channel (never a TaskSpec field). */
@@ -28,6 +28,8 @@ export interface PrepareAskLaneInput {
28
28
  /** borrowed-readonly — the hand's live tracked cwd, or undefined (hands-less). Read per call at RUN time (`current`),
29
29
  * never captured: the fs hand tools resolve their relative paths against this very ref. Not written here. */
30
30
  handsCwdRef: CwdRef | undefined;
31
+ /** borrowed-readonly — the root the hand tools resolve root-relative spellings against, or undefined (hands-less). */
32
+ handsRoot: string | undefined;
31
33
  /** borrowed-readonly — the run's advertised-writable-directory seat, or undefined (hands-less / nothing advertised). `roots` is read at RUN time, never captured: it is the fence's own admitted list and the write gate's exemption domain must be that same list. Not written here. */
32
34
  advertisedWritableDirs: Pick<AdvertisedWritableDirs, "roots"> | undefined;
33
35
  /** borrowed-readonly — the shared mount roster; read at RUN time by the approval-preview projection (alias-aware
@@ -59,14 +59,14 @@ function resolveApprovalPreview(tools, toolName, args) {
59
59
  }
60
60
  }
61
61
  export function prepareAskLane(input) {
62
- const { gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, advertisedWritableDirs, tools, inheritedUnavailableAsks, inheritedAskGrants, onAsk, humanReviewRef, now, ruleOffersOf, grantBindingOf, askSourceIdentity, riskAxesOf, autoModeDenialTracking, spec, deps, sessionId, runId, hooks, hookTimeoutMs, notifyOwnHookCrash, grantReadRoot } = input;
62
+ const { gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, handsRoot, advertisedWritableDirs, tools, inheritedUnavailableAsks, inheritedAskGrants, onAsk, humanReviewRef, now, ruleOffersOf, grantBindingOf, askSourceIdentity, riskAxesOf, autoModeDenialTracking, spec, deps, sessionId, runId, hooks, hookTimeoutMs, notifyOwnHookCrash, grantReadRoot } = input;
63
63
  if (!gateMachineryActive)
64
64
  return { askLane: undefined };
65
65
  const composedCallSignal = (callSignal) => composeCallSignal(abortController.signal, callSignal);
66
66
  const adjudicate = effectivePolicy
67
67
  ? (req, callSignal) => {
68
68
  const signal = composedCallSignal(callSignal);
69
- return raceAbort(Promise.resolve(effectivePolicy.check({ ...req, budget: budgetSnapshot, ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}), ...advertisedWritableRootsSeat(advertisedWritableDirs) }, signal)), signal, () => ({
69
+ return raceAbort(Promise.resolve(effectivePolicy.check({ ...req, budget: budgetSnapshot, ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}), ...(handsRoot !== undefined ? { root: handsRoot } : {}), ...advertisedWritableRootsSeat(advertisedWritableDirs) }, signal)), signal, () => ({
70
70
  action: "deny",
71
71
  message: "policy check aborted (task timed out or cancelled)",
72
72
  }));
@@ -14,6 +14,7 @@ import { applyToolModelGate, assertRestoreGatedToolsValue, modelIdTail } from ".
14
14
  import { resolveUsageWindows } from "../usage-window-store.js";
15
15
  import { deriveAskEffective, resolveAskSeat, resolveQuestionSeam } from "../wiring-manifest.js";
16
16
  import { assertPeerPresenceValue } from "../../agents/peer-directory.js";
17
+ import { snapshotDefinitionLists } from "../../agents/tool-filter.js";
17
18
  import { TOTAL_TOKENS_REMINDER_MODES } from "./turn-attachments.js";
18
19
  const TASK_LIMIT_KEY_DICT = {
19
20
  maxTokens: true,
@@ -359,9 +360,10 @@ export function prepareConfigDoors(input) {
359
360
  e.code = "config.agents.no_delegation_tool";
360
361
  throw e;
361
362
  }
362
- const perTask = spec.agents;
363
+ const perTask = spec.agents.map(snapshotDefinitionLists);
363
364
  spec = {
364
365
  ...spec,
366
+ agents: perTask,
365
367
  tools: pool.map((t) => {
366
368
  if (typeof t.withAgents !== "function")
367
369
  return t;
@@ -1,12 +1,13 @@
1
1
  import { StubExecutionEnv } from "../stub-env.js";
2
2
  import { hasDestroy } from "../remote-env.js";
3
3
  import { announceMalformedCapabilities } from "./env-capability-census.js";
4
+ import { deploymentCanRerootChild } from "./child-tree.js";
4
5
  import { settleTeardownLeg } from "./teardown-bounded.js";
5
6
  export async function prepareExecutionEnv(input) {
6
7
  const { deps, spec, internals, resume, placementRootResolved, sessionId, forgetOnThrow, rollback } = input;
7
8
  let ownedEnv;
8
9
  let ownedEnvObservedAt;
9
- if (internals?.requestedCwd !== undefined && deps.executionEnvFactory === undefined) {
10
+ if (internals?.requestedCwd !== undefined && !deploymentCanRerootChild(deps)) {
10
11
  await settleTeardownLeg(() => forgetOnThrow(), "forgetOnThrow (cwd-unsupported leg)", (err) => deps.onError?.(err, { phase: "config", sessionId }));
11
12
  const e = new Error(`Agent cwd "${internals.requestedCwd}" cannot take effect: this deployment has no executionEnvFactory (a static execution environment cannot be re-rooted per agent). Drop the cwd parameter or deploy a factory.`);
12
13
  e.code = "config.cwd_unsupported";
@@ -37,6 +37,7 @@ import { type WiringManifest } from "../wiring-manifest.js";
37
37
  import type { WriteProtectionCheck } from "../write-protect.js";
38
38
  import type { ShellReadCheck } from "../../tools/fs/shell-read-check.js";
39
39
  import type { CwdRef } from "../../tools/fs/fs-shared.js";
40
+ import type { ContainmentFace } from "../../tools/fs/containment-face.js";
40
41
  import type { AskLane, GateSeatInputsFor, IrreversibilityTier, ParkAsk, Prepared, ReversibilityProbes, StopForDenialLimit } from "./contracts.js";
41
42
  import type { PermissionRuleLanes } from "./permission-rule-lanes.js";
42
43
  import { type ToolRosterDeltaSeat } from "../tool-roster.js";
@@ -167,6 +168,8 @@ export interface PrepareGateStationsInput {
167
168
  /** borrowed-readonly — the leg's shell READ STATION (`ToolGateInput.shellReadCheck`), mounted by the hands phase, or
168
169
  * undefined with no real shell: no station, nothing judged. */
169
170
  shellReadCheck: ShellReadCheck | undefined;
171
+ /** borrowed-readonly — the leg's resolved read face (`undefined` with no hands), read per call into the class derivation. */
172
+ readFace: () => ContainmentFace | undefined;
170
173
  /** borrowed-readonly — the per-tool irreversibility tier handed to the gate per call. */
171
174
  irreversibilityTier: IrreversibilityTier;
172
175
  /** borrowed-readonly — the per-tool reversibility probes handed to the gate per call. */
@@ -46,7 +46,7 @@ export function maybeHumanRejectionHalt(input) {
46
46
  };
47
47
  }
48
48
  export function prepareGateStations(input) {
49
- const { askLane, parkAsk, tools, toolRosterDeltas, toolEffects, deps, toolCallGateArmedRef, effectivePolicy, hooks, egressTools, irreversibleTools, spec, complianceDenies, harness, blockedToolCalls, inheritedAskGrants, inheritedUnavailableAsks, foldAskClasses, ancestorSandboxAdmissions, preToolContexts, gateOutcomes, batchHaltRef, blockedTracked, hookIdentity, reminderMark, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, hookEnvFace, writeProtectionCheck, shellReadCheck, irreversibilityTier, reversibilityProbes, abortController, shellGatedTools, autoModeDecider, autoModeDenialTracking, askRoute, permissionMode, toolMarkSources, a2aIsolatePeers, ancestorArbitrations, runId, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash, frameSettlement, seatInputsFor } = input;
49
+ const { askLane, parkAsk, tools, toolRosterDeltas, toolEffects, deps, toolCallGateArmedRef, effectivePolicy, hooks, egressTools, irreversibleTools, spec, complianceDenies, harness, blockedToolCalls, inheritedAskGrants, inheritedUnavailableAsks, foldAskClasses, ancestorSandboxAdmissions, preToolContexts, gateOutcomes, batchHaltRef, blockedTracked, hookIdentity, reminderMark, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, hookEnvFace, writeProtectionCheck, shellReadCheck, readFace, irreversibilityTier, reversibilityProbes, abortController, shellGatedTools, autoModeDecider, autoModeDenialTracking, askRoute, permissionMode, toolMarkSources, a2aIsolatePeers, ancestorArbitrations, runId, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash, frameSettlement, seatInputsFor } = input;
50
50
  const gatePreToolUse = ownGatePreToolUse !== undefined ? (name, args, ctx) => ownGatePreToolUse(name, args, { ...ctx, permissionMode: permissionMode.observed }) : undefined;
51
51
  const releasesAnnounced = new Map();
52
52
  const onModeReleased = (info) => {
@@ -130,6 +130,7 @@ export function prepareGateStations(input) {
130
130
  peerMessage: heldBy("gate.peerMessage", PEER_MESSAGE_NAMES, call),
131
131
  ...(writeProtectionCheck !== undefined ? { writeProtectionCheck } : {}),
132
132
  ...(shellReadCheck !== undefined ? { shellReadCheck } : {}),
133
+ readFace,
133
134
  irreversibility: tierSeat?.value,
134
135
  reversibilityProbe: tierSeat?.read(reversibilityProbes),
135
136
  abortSignal: abortController.signal,
@@ -1,6 +1,6 @@
1
1
  /**
2
- * PrepareTask's LISTINGS phase: the per-task agent definitions' tool-name audit
3
- * (deny entries fail loud, allow entries advise), the agent-listing announce face read off the
2
+ * PrepareTask's LISTINGS phase: the sub-agent tool lists' audit (`sub-agent-deny-lists.ts`),
3
+ * the agent-listing announce face read off the
4
4
  * delegation tool's roster (with its deferred / excluded / removal-to-zero arms), the two listing
5
5
  * opt-out advisories, the announced-listings mirror seeded from the once-per-session ledger, the
6
6
  * deferred roster's ToolSearch ride, and the date_change seam. The body is the driver's stretch moved
@@ -10,8 +10,8 @@
10
10
  * the disclosure mount before it and the context-lane seats after it (a stretch
11
11
  * with no await is extracted as a sync function — an `async` wrapper would open a microtask window).
12
12
  *
13
- * Throws pass through unchanged: `config.agent.unknown_tool` (a deny entry naming a tool this
14
- * deployment does not mount — a typo there would silently guard nothing).
13
+ * Throws pass through unchanged: `config.agent.unknown_tool` (a denied-tools entry that names no
14
+ * tool — one judgment for every list a deployment writes, in `sub-agent-deny-lists.ts`).
15
15
  */
16
16
  import type { AgentTool } from "../../internal/harness.js";
17
17
  import type { TaskSpec } from "../types.js";
@@ -1,30 +1,10 @@
1
1
  import { formatLocalDate } from "../../prompts/default.js";
2
+ import { auditSubAgentToolLists } from "./sub-agent-deny-lists.js";
2
3
  import { SKILL_TOOL_NAME } from "./synthetic-tools.js";
3
4
  import { renderAgentListingDelta } from "./turn-attachments.js";
4
5
  export function prepareListings(input) {
5
6
  const { spec, tools, onceLedger, sessionId, toolFaceSnapshot, activeTools, resume, skillsListing, listingBudget, listingRideRef, renderWithDate, envFacts, tzValid, userTz, dateAnchor } = input;
6
- if (spec.agents !== undefined && spec.agents.length > 0) {
7
- const known = new Set(tools.flatMap((t) => [t.name, ...(t.aliases ?? [])]));
8
- for (const def of spec.agents) {
9
- const unknownAllow = (def.allowTools ?? []).filter((n) => n !== "*" && !known.has(n));
10
- if (unknownAllow.length > 0) {
11
- try {
12
- onceLedger.onError(new Error(`TaskSpec.agents: agent "${def.name}" allows tool(s) ${unknownAllow.join(", ")} not present in this task's assembled roster — likely a typo (the entry would be item-filtered at spawn; the agent stays usable). Advisory only: the delegation pool can differ from this roster, so a tool mounted only on the delegation tool or produced by per-spawn extraTools makes this spurious.`), { phase: "config", sessionId });
13
- }
14
- catch {
15
- }
16
- }
17
- for (const n of def.denyTools ?? []) {
18
- if (n === "*")
19
- continue;
20
- if (!known.has(n)) {
21
- const e = new Error(`TaskSpec.agents: agent "${def.name}" declares tool "${n}" in its denied tools, but no such tool exists in this deployment — fix the agent's tools list or mount the tool.`);
22
- e.code = "config.agent.unknown_tool";
23
- throw e;
24
- }
25
- }
26
- }
27
- }
7
+ auditSubAgentToolLists({ spec, tools, onceLedger, sessionId });
28
8
  const rosterBearingSpec = (spec.tools ?? []).find((t) => t.agentListing !== undefined && t.agentListing.length > 0);
29
9
  const rosterFaceDeferred = rosterBearingSpec !== undefined &&
30
10
  !(toolFaceSnapshot.exclude?.includes(rosterBearingSpec.name) ?? false) &&
@@ -94,6 +94,8 @@ export interface PrepareParkAskInput {
94
94
  /** borrowed-readonly — the hand's live tracked cwd, or undefined; `current` is read at park time for the re-adjudication.
95
95
  * Not written here. */
96
96
  handsCwdRef: CwdRef | undefined;
97
+ /** borrowed-readonly — the root the live gate stamps beside the cwd ({@link import("./prepare-ask-lane.js").PrepareAskLaneInput.handsRoot}). */
98
+ handsRoot: string | undefined;
97
99
  /** borrowed-readonly — the SAME seat the live gate stamps from ({@link import("./prepare-ask-lane.js").PrepareAskLaneInput.advertisedWritableDirs}): a parked call's projection re-adjudication asks one question about one call, so it must ask it against the same admitted list. Read at park time, not written here. */
98
100
  advertisedWritableDirs: Pick<AdvertisedWritableDirs, "roots"> | undefined;
99
101
  /** borrowed-readonly — the run's offload store, or undefined (a volatile store holding results refuses the park). */
@@ -27,7 +27,7 @@ const wordlessDemand = (toolName) => {
27
27
  throw new Error(`a real-approval demand for "${toolName}" reached the park with no origin word — the origin stamp precedes every park`);
28
28
  };
29
29
  export function prepareParkAsk(input) {
30
- const { askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, advertisedWritableDirs, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedTools, irreversibilityTier, toolMarkSources, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, grantBindingOf, now, humanReviewRef, abortController, harness, pausedRef } = input;
30
+ const { askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, handsRoot, advertisedWritableDirs, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedTools, irreversibilityTier, toolMarkSources, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, grantBindingOf, now, humanReviewRef, abortController, harness, pausedRef } = input;
31
31
  if (askLane === undefined || saga === undefined)
32
32
  return { parkAsk: undefined };
33
33
  const { composedCallSignal, approvalPreviewOf } = askLane;
@@ -165,7 +165,7 @@ export function prepareParkAsk(input) {
165
165
  toolCallId: req.toolCallId,
166
166
  budget: budgetSnapshot,
167
167
  ...callFaceSeat(req.face),
168
- ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}),
168
+ ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}), ...(handsRoot !== undefined ? { root: handsRoot } : {}),
169
169
  ...advertisedWritableRootsSeat(advertisedWritableDirs),
170
170
  }, cutSignal)), cutSignal);
171
171
  if (reprojectedRaced.tag === "aborted")
@@ -140,6 +140,8 @@ export interface PreparePolicyChainInput {
140
140
  /** borrowed-readonly — LATE-BOUND: this task's own classifier instance (the pass-through test's decider term), read at chain
141
141
  * assembly (the arming runs after this phase, and again on a steer into `auto`). */
142
142
  autoModeDecider: () => AutoModeDecider | undefined;
143
+ /** borrowed-readonly — the leg's resolved read face (`undefined` with no hands), read per call into the inherited stations' class derivation. */
144
+ readFace: () => import("../../tools/fs/containment-face.js").ContainmentFace | undefined;
143
145
  /** borrowed-readonly — the leg's live permission-mode seat: the mode gate policy layer and the inherited stations' arbitration
144
146
  * read `current` per call. */
145
147
  permissionMode: PermissionModeController;
@@ -24,7 +24,7 @@ import { createLiveSessionRules, createSessionRulePolicy } from "./session-rule-
24
24
  import { thrownText } from "../thrown-value.js";
25
25
  import { heldBy, heldValues, seatOf, wireOf } from "../tool-name-readings.js";
26
26
  export async function preparePolicyChain(input) {
27
- const { spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, additionalRootsCanonical, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, shellGatedTools, toolMarkSources, envHandToolNames, handsCwdRef, readDenyMatcher, announceRuleStoreSkipped, questionToolMounted, runtimeCaps, inheritedAncestorRules, ancestorSessionRulesRef, inheritedParentConstraints, seedInheritedGate, skillGrants, skillSpecs, allowedToolTables, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider, permissionMode, scratchpadDir, toolRosterDeltas, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef } = input;
27
+ const { spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, additionalRootsCanonical, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, shellGatedTools, toolMarkSources, envHandToolNames, handsCwdRef, readDenyMatcher, announceRuleStoreSkipped, questionToolMounted, runtimeCaps, inheritedAncestorRules, ancestorSessionRulesRef, inheritedParentConstraints, seedInheritedGate, skillGrants, skillSpecs, allowedToolTables, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider, readFace, permissionMode, scratchpadDir, toolRosterDeltas, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef } = input;
28
28
  const ancestorArbitrations = new Map();
29
29
  const arbitrateInherited = (ask, toolCallId, judged) => {
30
30
  const verdict = arbitrateByMode(permissionMode.current, judged.classification, { origin: judged.origin, ...askMandateMember(ancestorFacts(judged.ask.action === "ask" ? judged.ask : undefined)) });
@@ -305,11 +305,13 @@ export async function preparePolicyChain(input) {
305
305
  const grantBindingOf = (call, args) => skillGrantBindingMembers(call, args, skillSpecs, SKILL_TOOL_NAME);
306
306
  const inheritedStationFacts = (call) => {
307
307
  const seat = seatMarksOf(call);
308
+ const face = readFace();
308
309
  return {
309
310
  ...INHERITED_STATION_FACTS,
310
311
  mandateMarks: { egress: seat.egress, shellGated: false, ...(seat.tier === "always" && !seat.shellGated ? { irreversibility: "always" } : {}) },
311
312
  ...(seat.sources.size > 0 ? { toolMarkSources: seat.sources } : {}),
312
313
  ...(deps.a2a?.isolatePeers === true ? { a2aIsolatePeers: true } : {}),
314
+ ...(face !== undefined ? { readFace: face } : {}),
313
315
  };
314
316
  };
315
317
  const inheritedAskEvidence = inheritedAskRuleEvidence(deps.permissionRuleStore);
@@ -616,7 +618,7 @@ export async function preparePolicyChain(input) {
616
618
  ...instrumentedChain,
617
619
  ...(rewriteCapableLayers ? narrowingLayers : []),
618
620
  ];
619
- const integrityGuardLayers = [createTranscriptIntegrityPolicy(), createUnverifiableDeletePolicy()];
621
+ const integrityGuardLayers = [createTranscriptIntegrityPolicy(), createUnverifiableDeletePolicy({ removalSeat: { env: executionEnv, additionalRoots: () => additionalRootsCanonical } })];
620
622
  const guardedPolicyLayers = rewriteCapableLayers
621
623
  ? [...integrityGuardLayers, ...policyLayers, modeGateLayer, ...integrityGuardLayers]
622
624
  : [...integrityGuardLayers, ...policyLayers, modeGateLayer];