@sema-agent/core 7.50.2 → 7.51.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 (188) hide show
  1. package/CHANGELOG.md +263 -0
  2. package/dist/agents/child-failure.d.ts +5 -16
  3. package/dist/agents/child-failure.js +4 -24
  4. package/dist/agents/subagent-steps.js +2 -2
  5. package/dist/agents/subagent.d.ts +4 -13
  6. package/dist/agents/subagent.js +94 -204
  7. package/dist/agents/tool-filter.d.ts +2 -2
  8. package/dist/brain/anthropic.d.ts +1 -1
  9. package/dist/brain/anthropic.js +7 -5
  10. package/dist/brain/open-responses.d.ts +1 -1
  11. package/dist/brain/open-responses.js +3 -2
  12. package/dist/brain/openai.d.ts +1 -1
  13. package/dist/brain/openai.js +3 -2
  14. package/dist/brain/request-route.d.ts +2 -2
  15. package/dist/brain/route-adjudicator.d.ts +16 -7
  16. package/dist/brain/route-adjudicator.js +23 -7
  17. package/dist/brain/route-conformance.js +23 -5
  18. package/dist/brain/stream-engine.js +9 -3
  19. package/dist/core/a2a.js +6 -8
  20. package/dist/core/allow-entry.d.ts +45 -26
  21. package/dist/core/allow-entry.js +48 -24
  22. package/dist/core/arg-summary.d.ts +8 -85
  23. package/dist/core/arg-summary.js +4 -386
  24. package/dist/core/ask-origin.d.ts +35 -33
  25. package/dist/core/ask-origin.js +9 -11
  26. package/dist/core/checkpoint-requirements.d.ts +6 -0
  27. package/dist/core/checkpoint-requirements.js +1 -0
  28. package/dist/core/checkpoint-store.d.ts +15 -34
  29. package/dist/core/checkpoint-store.js +5 -4
  30. package/dist/core/classifier-unavailable-arm.d.ts +12 -8
  31. package/dist/core/classifier-unavailable-arm.js +7 -2
  32. package/dist/core/credential-vocabulary.d.ts +3 -2
  33. package/dist/core/credential-vocabulary.js +27 -18
  34. package/dist/core/delegation-frames.d.ts +40 -15
  35. package/dist/core/delegation-frames.js +34 -0
  36. package/dist/core/engine-notice.d.ts +35 -11
  37. package/dist/core/gate-fold.js +9 -7
  38. package/dist/core/gate-lanes.js +6 -4
  39. package/dist/core/gate-outcome.d.ts +81 -15
  40. package/dist/core/gate-outcome.js +168 -67
  41. package/dist/core/governance-codes.d.ts +7 -1
  42. package/dist/core/governance-codes.js +9 -0
  43. package/dist/core/hooks.d.ts +4 -4
  44. package/dist/core/hooks.js +4 -3
  45. package/dist/core/mcp-connection-pool.d.ts +2 -0
  46. package/dist/core/mcp-connection-pool.js +2 -1
  47. package/dist/core/mcp-failure.js +2 -1
  48. package/dist/core/mcp-server-spec.d.ts +27 -5
  49. package/dist/core/mcp.d.ts +13 -38
  50. package/dist/core/mcp.js +28 -23
  51. package/dist/core/media-byte-cap.d.ts +25 -0
  52. package/dist/core/media-byte-cap.js +18 -2
  53. package/dist/core/memory-engine/consolidation-driver.js +1 -1
  54. package/dist/core/mode-policy.d.ts +25 -1
  55. package/dist/core/mode-policy.js +28 -13
  56. package/dist/core/own-property-receipt.d.ts +18 -0
  57. package/dist/core/own-property-receipt.js +39 -0
  58. package/dist/core/permission-rule-model.d.ts +32 -11
  59. package/dist/core/permission-rule-model.js +12 -7
  60. package/dist/core/persisted-rule-arms.d.ts +3 -3
  61. package/dist/core/prompt-section-seat.d.ts +54 -0
  62. package/dist/core/prompt-section-seat.js +49 -0
  63. package/dist/core/protocol-http-auth.d.ts +44 -0
  64. package/dist/core/protocol-http-auth.js +45 -0
  65. package/dist/core/read-face-seats.d.ts +30 -0
  66. package/dist/core/read-face-seats.js +38 -0
  67. package/dist/core/redaction-passes.d.ts +82 -0
  68. package/dist/core/redaction-passes.js +378 -0
  69. package/dist/core/remote-env.d.ts +4 -3
  70. package/dist/core/resume-startup-cause.d.ts +56 -0
  71. package/dist/core/resume-startup-cause.js +43 -0
  72. package/dist/core/rule-denial-sentence.d.ts +2 -2
  73. package/dist/core/runner/announce-once-ledger.js +2 -2
  74. package/dist/core/runner/classifier-ladder.d.ts +17 -13
  75. package/dist/core/runner/classifier-ladder.js +12 -10
  76. package/dist/core/runner/construction-doors.js +9 -20
  77. package/dist/core/runner/contracts.d.ts +4 -4
  78. package/dist/core/runner/gate-exit.js +2 -2
  79. package/dist/core/runner/memory-consolidation-write-judge.d.ts +53 -9
  80. package/dist/core/runner/memory-consolidation-write-judge.js +39 -1
  81. package/dist/core/runner/permission-rule-lanes.d.ts +8 -13
  82. package/dist/core/runner/person-path-rows.d.ts +4 -3
  83. package/dist/core/runner/policy-throw.js +4 -4
  84. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -3
  85. package/dist/core/runner/prepare-caps-and-workflow.js +8 -6
  86. package/dist/core/runner/prepare-gate-stations.js +4 -4
  87. package/dist/core/runner/prepare-hands-readface.js +1 -19
  88. package/dist/core/runner/prepare-inherited-gate.js +10 -9
  89. package/dist/core/runner/prepare-policy-chain.d.ts +3 -5
  90. package/dist/core/runner/prepare-policy-chain.js +10 -13
  91. package/dist/core/runner/prepare-project-context.d.ts +4 -4
  92. package/dist/core/runner/prepare-project-context.js +2 -1
  93. package/dist/core/runner/prepare-prompt-assembly.js +1 -0
  94. package/dist/core/runner/prepare-prompt-inputs.d.ts +1 -1
  95. package/dist/core/runner/prepare-prompt-inputs.js +4 -4
  96. package/dist/core/runner/prepare-protocol-tools.d.ts +4 -3
  97. package/dist/core/runner/prepare-protocol-tools.js +3 -2
  98. package/dist/core/runner/prepare-task.js +6 -4
  99. package/dist/core/runner/prepare-turn-wiring.d.ts +16 -13
  100. package/dist/core/runner/prepare-turn-wiring.js +8 -7
  101. package/dist/core/runner/run-harness-handlers.js +2 -2
  102. package/dist/core/runner/run-leg.js +10 -9
  103. package/dist/core/runner/run-settle-and-teardown.js +5 -5
  104. package/dist/core/runner/run-turn-boundary.js +3 -2
  105. package/dist/core/runner/runtask.d.ts +1 -2
  106. package/dist/core/runner/runtask.js +8 -12
  107. package/dist/core/runner/session-rule-policy.d.ts +15 -11
  108. package/dist/core/runner/session-rule-policy.js +43 -33
  109. package/dist/core/runner/stream-lifecycle-verbs.js +2 -2
  110. package/dist/core/runner/stream-settle-backstop.js +2 -1
  111. package/dist/core/runner/sub-agent-deny-lists.d.ts +12 -8
  112. package/dist/core/runner/sub-agent-deny-lists.js +23 -12
  113. package/dist/core/runner/terminal-account.js +3 -3
  114. package/dist/core/runner/tool-output-projection.js +3 -8
  115. package/dist/core/runner/tool-result-media-seat.d.ts +19 -0
  116. package/dist/core/runner/tool-result-media-seat.js +13 -0
  117. package/dist/core/runner/turn-attachments.d.ts +13 -12
  118. package/dist/core/runner/turn-attachments.js +11 -7
  119. package/dist/core/runner-deps.d.ts +4 -3
  120. package/dist/core/safety-merge-corpus.js +21 -21
  121. package/dist/core/secret-env.js +1 -1
  122. package/dist/core/session-policy-store.d.ts +89 -72
  123. package/dist/core/session-policy-store.js +100 -93
  124. package/dist/core/shell-read-silence.d.ts +38 -4
  125. package/dist/core/shell-read-silence.js +102 -0
  126. package/dist/core/side-query.d.ts +7 -8
  127. package/dist/core/skill-grant-binding.d.ts +32 -11
  128. package/dist/core/skill-grant-binding.js +11 -3
  129. package/dist/core/skill-grant.d.ts +4 -3
  130. package/dist/core/skill-grant.js +5 -4
  131. package/dist/core/skills-directory.d.ts +12 -1
  132. package/dist/core/skills-directory.js +17 -8
  133. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +2 -0
  134. package/dist/core/store-contracts/checkpoint-store-contract.js +3 -0
  135. package/dist/core/swappable-deps.d.ts +15 -1
  136. package/dist/core/swappable-deps.js +25 -1
  137. package/dist/core/synthetic-read-verdict.d.ts +16 -14
  138. package/dist/core/synthetic-read-verdict.js +28 -8
  139. package/dist/core/task-notification.d.ts +13 -3
  140. package/dist/core/task-notification.js +12 -1
  141. package/dist/core/task-spec.d.ts +14 -15
  142. package/dist/core/tool-name-readings.d.ts +0 -4
  143. package/dist/core/tool-name-readings.js +0 -1
  144. package/dist/core/tool-policy.d.ts +40 -9
  145. package/dist/core/tool-policy.js +25 -7
  146. package/dist/core/types.d.ts +3 -3
  147. package/dist/core/types.js +1 -1
  148. package/dist/core/untrusted-egress.d.ts +60 -11
  149. package/dist/core/untrusted-egress.js +120 -9
  150. package/dist/core/untrusted-text.js +16 -0
  151. package/dist/core/wiring-manifest.d.ts +22 -8
  152. package/dist/core/wiring-manifest.js +12 -4
  153. package/dist/engine/harness/agent-harness.d.ts +2 -1
  154. package/dist/engine/harness/agent-harness.js +4 -1
  155. package/dist/engine/harness/types.d.ts +3 -0
  156. package/dist/engine/llm/types.d.ts +7 -9
  157. package/dist/engine/loop/agent-loop.d.ts +8 -3
  158. package/dist/engine/loop/agent-loop.js +26 -9
  159. package/dist/engine/loop/types.d.ts +10 -0
  160. package/dist/index.d.ts +17 -5
  161. package/dist/index.js +14 -5
  162. package/dist/orchestration/run-workflow-tool.js +8 -8
  163. package/dist/orchestration/workflow.js +2 -2
  164. package/dist/prompt-assembly/assemble.d.ts +3 -1
  165. package/dist/prompt-assembly/assemble.js +6 -0
  166. package/dist/prompts/cowork.d.ts +42 -0
  167. package/dist/prompts/cowork.js +529 -0
  168. package/dist/prompts/default.d.ts +14 -1
  169. package/dist/prompts/default.js +3 -0
  170. package/dist/stores/file/session-policy-store.d.ts +6 -3
  171. package/dist/stores/file/session-policy-store.js +5 -16
  172. package/dist/tools/artifact/artifact-tool.js +2 -0
  173. package/dist/tools/fs/image-admission.d.ts +57 -4
  174. package/dist/tools/fs/image-admission.js +85 -0
  175. package/dist/tools/fs/path-containment.d.ts +2 -0
  176. package/dist/tools/fs/path-containment.js +5 -0
  177. package/dist/tools/fs/shell-read-check.d.ts +31 -6
  178. package/dist/tools/fs/shell-read-check.js +2 -4
  179. package/dist/tools/fs/shell-read-environment.d.ts +10 -25
  180. package/dist/tools/fs/shell-read-environment.js +6 -18
  181. package/dist/tools/fs/shell-read-establish.d.ts +3 -2
  182. package/dist/tools/fs/shell-read-establish.js +5 -5
  183. package/dist/tools/fs/shell-read-judge.d.ts +38 -8
  184. package/dist/tools/fs/shell-read-judge.js +12 -10
  185. package/dist/tools/web.js +2 -2
  186. package/package.json +1 -1
  187. package/test/export-surface.snapshot.json +94 -10
  188. package/wire-changes/core@7.51.0.json +2421 -0
@@ -1,3 +1,105 @@
1
+ import { types } from "node:util";
2
+ import { dispositionsOfRow, SHELL_READ_FINDINGS } from "../tools/fs/shell-read-judge.js";
3
+ import { receiveContract } from "./decision-receipt.js";
4
+ import { egressThrownText } from "./untrusted-egress.js";
5
+ const DISPOSITIONS = ["refuse", "mandated", "ordinary", "vouch"];
6
+ const MEMBER_KINDS = {
7
+ disposition: "word",
8
+ finding: "word",
9
+ environmentSilent: "mark",
10
+ rowsUnread: "mark",
11
+ face: "face",
12
+ sentence: "word",
13
+ details: "record",
14
+ readRootCandidate: "record",
15
+ unestablished: "mark",
16
+ writesPlaced: "mark",
17
+ writeProtected: "mark",
18
+ targetUnresolved: "mark",
19
+ unplacedOptions: "record",
20
+ admit: "admission",
21
+ };
22
+ const VERDICT_MEMBERS = Object.keys(MEMBER_KINDS);
23
+ const DATA_DEPTH = 8;
24
+ function ownedData(value, depth, path) {
25
+ if (value === null || value === undefined || typeof value === "string" || typeof value === "number" || typeof value === "boolean")
26
+ return { ok: true, value };
27
+ if (typeof value !== "object" || depth >= DATA_DEPTH || types.isProxy(value) || path.has(value))
28
+ return { ok: false };
29
+ const list = Array.isArray(value);
30
+ const proto = Object.getPrototypeOf(value);
31
+ if (list ? proto !== Array.prototype : proto !== Object.prototype && proto !== null)
32
+ return { ok: false };
33
+ const own = Object.getOwnPropertyDescriptors(value);
34
+ path.add(value);
35
+ try {
36
+ const out = list ? [] : {};
37
+ for (const key of Object.keys(own)) {
38
+ if (list && key === "length")
39
+ continue;
40
+ const d = own[key];
41
+ if (!Object.hasOwn(d, "value") || d.enumerable !== true)
42
+ return { ok: false };
43
+ const inner = ownedData(d.value, depth + 1, path);
44
+ if (!inner.ok)
45
+ return { ok: false };
46
+ Object.defineProperty(out, key, { value: inner.value, enumerable: true, writable: true, configurable: true });
47
+ }
48
+ return { ok: true, value: out };
49
+ }
50
+ finally {
51
+ path.delete(value);
52
+ }
53
+ }
54
+ function receivedAnswer(answer, call) {
55
+ if (typeof answer !== "object" || answer === null)
56
+ return undefined;
57
+ const outer = receiveContract(answer, VERDICT_MEMBERS, "");
58
+ if (outer.kind !== "held")
59
+ return undefined;
60
+ const out = {};
61
+ for (const [member, kind] of Object.entries(MEMBER_KINDS)) {
62
+ let v = outer.held[member];
63
+ if (v === undefined)
64
+ continue;
65
+ if (kind === "word" ? typeof v !== "string" : kind === "mark" ? v !== true : kind === "face" ? v !== "write" : kind === "admission" ? typeof v !== "function" || types.isProxy(v) : false)
66
+ return undefined;
67
+ if (kind === "record") {
68
+ const data = ownedData(v, 0, new Set());
69
+ if (!data.ok || typeof data.value !== "object" || data.value === null || Array.isArray(data.value))
70
+ return undefined;
71
+ v = data.value;
72
+ }
73
+ if (kind === "admission") {
74
+ const admission = v;
75
+ v = () => {
76
+ try {
77
+ Reflect.apply(admission, undefined, []);
78
+ }
79
+ catch (e) {
80
+ call.report?.(new Error(`shell read station's admission threw for tool "${call.toolName}"${call.toolCallId !== undefined ? ` (toolCallId ${call.toolCallId})` : ""} — nothing was admitted: ${egressThrownText("same-principal", e)}`));
81
+ }
82
+ };
83
+ }
84
+ Object.defineProperty(out, member, { value: v, enumerable: true, writable: true, configurable: true });
85
+ }
86
+ return out;
87
+ }
88
+ export function readStationAnswer(answer, call) {
89
+ const verdict = receivedAnswer(answer, call);
90
+ if (verdict?.disposition === "refuse")
91
+ return { kind: "refuse", verdict };
92
+ if (verdict !== undefined && (verdict.finding === "unanswered" || (verdict.finding === "none" && (verdict.environmentSilent === true || verdict.rowsUnread === true))))
93
+ return { kind: "silence", verdict };
94
+ if (verdict !== undefined && DISPOSITIONS.includes(verdict.disposition) && dispositionsOfRow(verdict.finding).has(verdict.disposition))
95
+ return { kind: verdict.disposition, verdict };
96
+ const word = (w, set) => (w !== undefined && set.includes(w) ? `"${w}"` : "a word outside its closed set");
97
+ const sentence = verdict === undefined
98
+ ? `tool "${call.toolName}" was refused: the read station's answer is not one the engine can receive (an ordinary object holding its members as its own data properties, each of its kind), so it is not believed (denied fail-closed)`
99
+ : `tool "${call.toolName}" was refused: the read station answered ${word(verdict.disposition, DISPOSITIONS)} beside the finding ${word(verdict.finding, SHELL_READ_FINDINGS)}, an answer the read decision table never gives that finding — the station's answer contradicts its own finding and is not believed (denied fail-closed)`;
100
+ call.report?.(new Error(`shell read station answered out of its contract for tool "${call.toolName}"${call.toolCallId !== undefined ? ` (toolCallId ${call.toolCallId})` : ""} — ${sentence}`));
101
+ return { kind: "refuse", verdict: { disposition: "refuse", finding: "none", sentence }, unbelieved: true };
102
+ }
1
103
  export function silencedBy(verdict, d) {
2
104
  const environment = verdict.environmentSilent === true || (verdict.finding === "unanswered" && verdict.rowsUnread !== true);
3
105
  const rows = verdict.rowsUnread === true && d.ruleUnreadable === undefined ? { ruleUnreadable: "persisted_rows" } : {};
@@ -63,17 +63,16 @@ export interface SideQuerySpec {
63
63
  * Per-model auth — MIRRORS {@link TaskSpec.getApiKeyAndHeaders} (same signature, resolved per
64
64
  * call against the RESOLVED model, exactly like the task path's per-call hook). The brain
65
65
  * contract is the route pairing law (route-adjudicator.ts): a per-model credential (this hook,
66
- * or an auth header on `Model.headers`) always rides; the deployment credential rides only where
67
- * its pairing is verifiable-or-unpinned — a declared `config.baseUrl` with an off-root model is
68
- * refused (`route.credential_mismatch` / `route.credential_missing`), never silently followed.
66
+ * or an auth header on `Model.headers`) always rides; the deployment credential rides only to the
67
+ * deployment's own root — a model carrying a root of its own is refused
68
+ * (`route.credential_mismatch` / `route.credential_missing`), never silently followed.
69
69
  * Historically the fallback was unconditional (`options.apiKey ?? config.apiKey`, with a model's
70
70
  * own `baseUrl` outranking the brain's), so a side query routed to a model carrying its own
71
71
  * `baseUrl` + per-model key sent the GATEWAY key to the per-model (possibly external) URL — the
72
- * credential leak the pairing law now stops. Absent seat ⇒ no options are minted (byte-identical
73
- * to before): on an UNDECLARED root the construction-time credential still applies (the
74
- * quick-start posture), while a declared-root brain + off-root model hard-fails the side query
75
- * with the loud refusal (`stopReason: "error"`) instead of leaking — there is no primary model
76
- * for a side query to fall back to.
72
+ * credential leak the pairing law now stops. Absent seat ⇒ no options are minted: the
73
+ * construction-time credential applies on the deployment's own root, and a model rooted elsewhere
74
+ * hard-fails the side query with the loud refusal (`stopReason: "error"`) instead of leaking —
75
+ * there is no primary model for a side query to fall back to.
77
76
  */
78
77
  getApiKeyAndHeaders?: TaskSpec["getApiKeyAndHeaders"];
79
78
  signal?: AbortSignal;
@@ -15,30 +15,51 @@ export interface BoundSkillGrants {
15
15
  /** The skill's `disallowed-tools` entries — what activating it refuses for the rest of the run. */
16
16
  readonly disallowedTools: readonly string[];
17
17
  }
18
+ /**
19
+ * Why a card carries no binding digest — the closed set of `boundGrantAbsence`: `not_bindable` (the call is not an
20
+ * activation of the engine's `Skill` tool: it binds no grant) or `unreadable` (the card binds a grant and what it binds
21
+ * cannot be read: a stored row whose digest is not a string, or that says neither).
22
+ */
23
+ export declare const BOUND_GRANT_ABSENCES: readonly ["not_bindable", "unreadable"];
24
+ export type BoundGrantAbsence = (typeof BOUND_GRANT_ABSENCES)[number];
18
25
  /**
19
26
  * A stored value read as a {@link BoundSkillGrants}, whole or not at all: a fresh copy of the three members and nothing else,
20
27
  * or `undefined` when any member is not of its type. Every member is read ONCE, so the value screened is the value published.
21
28
  */
22
29
  export declare function readBoundSkillGrants(raw: unknown): BoundSkillGrants | undefined;
23
- /** The card members a call to the engine's `Skill` tool carries on every face its card has — empty for any other call. */
24
- export interface SkillGrantBindingMembers {
25
- /** The hash of {@link boundGrants}: the binding a parked approval of the card is judged by. */
26
- boundGrantDigest?: string;
27
- boundGrants?: BoundSkillGrants;
28
- }
30
+ /** The card members every ask carries about its binding — exactly one arm: a call to the engine's `Skill` tool carries the
31
+ * digest and the value it hashes, every other call the absence word. */
32
+ export type SkillGrantBindingMembers = {
33
+ readonly boundGrantDigest: string;
34
+ readonly boundGrants: BoundSkillGrants;
35
+ readonly boundGrantAbsence?: never;
36
+ } | {
37
+ readonly boundGrantAbsence: BoundGrantAbsence;
38
+ readonly boundGrantDigest?: never;
39
+ readonly boundGrants?: never;
40
+ };
41
+ /** The members of a card that is not a `Skill` activation: the factory's other arm, and what a tool's own consent card (never a
42
+ * `Skill` call) spreads. */
43
+ export declare const NOT_BINDABLE: Readonly<{
44
+ readonly boundGrantAbsence: "not_bindable";
45
+ }>;
29
46
  /** The leg's binding factory ({@link SkillGrantBindingMembers} for a call over `args`), the one every ask-mint site spreads. */
30
47
  export type GrantBindingOf = (call: {
31
48
  readonly toolName: string;
32
49
  }, args: unknown) => SkillGrantBindingMembers;
33
50
  /**
34
- * The binding members of a stored parked row, as the inbox projection publishes them: the digest when it is a string, and the lists
35
- * only as the value that digest hashes — {@link readBoundSkillGrants} whole, and equal to the digest by hash. A pair that
36
- * disagrees, or lists beside no digest, would show an approver one list while the approval binds another: the lists read as absent.
51
+ * The binding members of a stored parked row, as the inbox projection publishes them — exactly one of three: the digest when it
52
+ * is a string (and the lists only as the value that digest hashes — {@link readBoundSkillGrants} whole, and equal to the digest by
53
+ * hash; a pair that disagrees, or lists beside no digest, would show an approver one list while the approval binds another), else
54
+ * the row's absence word when it is a member of the set, else `unreadable` — the row does not say what it binds.
37
55
  */
38
56
  export declare function readBoundBinding(row: {
39
57
  readonly boundGrantDigest?: unknown;
40
58
  readonly boundGrants?: unknown;
41
- } | undefined): {
42
- boundGrantDigest?: string;
59
+ readonly boundGrantAbsence?: unknown;
60
+ }): {
61
+ boundGrantDigest: string;
43
62
  boundGrants?: BoundSkillGrants;
63
+ } | {
64
+ boundGrantAbsence: BoundGrantAbsence;
44
65
  };
@@ -1,4 +1,7 @@
1
1
  import { boundInputHashOf } from "./canonical-json.js";
2
+ export const BOUND_GRANT_ABSENCES = Object.freeze(["not_bindable", "unreadable"]);
3
+ const BOUND_GRANT_ABSENCE_SET = new Set(BOUND_GRANT_ABSENCES);
4
+ const isBoundGrantAbsence = (v) => BOUND_GRANT_ABSENCE_SET.has(v);
2
5
  export function readBoundSkillGrants(raw) {
3
6
  if (typeof raw !== "object" || raw === null)
4
7
  return undefined;
@@ -15,8 +18,13 @@ export function readBoundSkillGrants(raw) {
15
18
  const disallowedTools = entries(disallowed);
16
19
  return allowedTools !== undefined && disallowedTools !== undefined ? { skill, allowedTools, disallowedTools } : undefined;
17
20
  }
21
+ export const NOT_BINDABLE = Object.freeze({ boundGrantAbsence: "not_bindable" });
18
22
  export function readBoundBinding(row) {
19
- const digest = row?.boundGrantDigest;
20
- const grants = readBoundSkillGrants(row?.boundGrants);
21
- return { ...(typeof digest === "string" ? { boundGrantDigest: digest } : {}), ...(grants !== undefined && typeof digest === "string" && boundInputHashOf(grants) === digest ? { boundGrants: grants } : {}) };
23
+ const digest = row.boundGrantDigest;
24
+ if (typeof digest === "string") {
25
+ const grants = readBoundSkillGrants(row.boundGrants);
26
+ return { boundGrantDigest: digest, ...(grants !== undefined && boundInputHashOf(grants) === digest ? { boundGrants: grants } : {}) };
27
+ }
28
+ const word = row.boundGrantAbsence;
29
+ return { boundGrantAbsence: digest === undefined && isBoundGrantAbsence(word) ? word : "unreadable" };
22
30
  }
@@ -2,7 +2,7 @@ import type { PermissionResult, ToolCallRequest } from "./tool-policy.js";
2
2
  import type { PermissionMode } from "./ask-origin.js";
3
3
  import { type PermissionSeatInputs, type PermissionSeatReading } from "./mode-policy.js";
4
4
  import type { AgentTool } from "../internal/harness-types.js";
5
- import type { SkillGrantBindingMembers } from "./skill-grant-binding.js";
5
+ import { type SkillGrantBindingMembers } from "./skill-grant-binding.js";
6
6
  import { type AllowEntryGateSeats, type AllowEntryInert, type AllowEntryJudgeContext, type CompiledAllowEntry } from "./allow-entry.js";
7
7
  /** Who wrote a row: the `Skill` tool on a successful activation, prepare for a skill the person invoked by name, or a
8
8
  * delegated child's seed from its parent. */
@@ -53,8 +53,9 @@ export declare function skillGrantDigest(skill: Pick<SkillGrantSource, "name" |
53
53
  * `AskRequest` an approver is handed, an ancestor's, the parked row and (through the row) the inbox projection.
54
54
  * `boundGrants` is the skill's name and canonical lists, `boundGrantDigest` its hash — the same value, so what a person was
55
55
  * shown and what an approving resume is judged by cannot differ. The binding is a property of the CALL, whoever asked and
56
- * however the question was worded. Empty for any other call, and when no skill is loaded (no engine `Skill` tool is
57
- * mounted, so a call by that name is not an activation).
56
+ * however the question was worded. Every other call — and any call when no skill is loaded (no engine `Skill` tool is
57
+ * mounted, so a call by that name is not an activation) — carries the absence word `not_bindable` instead: one of the two,
58
+ * always, so no reader infers the binding from a tool name.
58
59
  */
59
60
  export declare function skillGrantBindingMembers(call: {
60
61
  readonly toolName: string;
@@ -3,8 +3,9 @@ import { parsePermissionRule } from "./permission-rules.js";
3
3
  import { mountedToolPermissionSeat } from "./mode-policy.js";
4
4
  import { inlineUntrusted } from "./untrusted-text.js";
5
5
  import { boundInputHashOf } from "./canonical-json.js";
6
+ import { NOT_BINDABLE } from "./skill-grant-binding.js";
6
7
  import { readToolNameList } from "./tool-name-list.js";
7
- import { allowingRows, autoStripped, compileAllowEntry, compileAllowList, inertEntryPhrase, judgeAllowEntries, nonListValueLabel, nonStringMemberLabel, renderListEntry } from "./allow-entry.js";
8
+ import { allowingRows, autoStripped, compileAllowList, readRuleEntry, inertEntryPhrase, judgeAllowEntries, nonListValueLabel, nonStringMemberLabel, renderListEntry } from "./allow-entry.js";
8
9
  export const SKILL_GRANT_ORIGINS = Object.freeze(["model", "user", "inherited"]);
9
10
  const FROZEN_SKILL_GRANT_ORIGINS = new Set(["model", "inherited"]);
10
11
  export function skillNamedBy(args, skills) {
@@ -25,13 +26,13 @@ export function skillGrantDigest(skill) {
25
26
  return skillGrantBinding(skill).boundGrantDigest;
26
27
  }
27
28
  export function skillGrantBindingMembers(call, args, skills, skillToolName) {
28
- return isSkillActivationCall(call, skills, skillToolName) ? skillGrantBinding(skillNamedBy(args, skills)) : {};
29
+ return isSkillActivationCall(call, skills, skillToolName) ? skillGrantBinding(skillNamedBy(args, skills)) : NOT_BINDABLE;
29
30
  }
30
31
  function isSkillActivationCall(call, skills, skillToolName) {
31
32
  return skills.length > 0 && wireOf("skillGrant.binding", call) === skillToolName;
32
33
  }
33
34
  export function parkedSkillApprovalRefusal(row, args, skills, skillToolName, site = "claim") {
34
- if (row.boundGrantDigest === undefined && skillGrantBindingMembers(row, args, skills, skillToolName).boundGrantDigest === undefined)
35
+ if (row.boundGrantDigest === undefined && !isSkillActivationCall(row, skills, skillToolName))
35
36
  return undefined;
36
37
  if (skillGrantDigest(skillNamedBy(args, skills)) === row.boundGrantDigest)
37
38
  return undefined;
@@ -244,7 +245,7 @@ function readCarriedRecords(raw, carrier) {
244
245
  const { skill, behavior, entry, origin } = r;
245
246
  if (typeof skill !== "string" || typeof entry !== "string" || (behavior !== "allow" && behavior !== "deny"))
246
247
  return undefined;
247
- if (typeof compileAllowEntry(entry, behavior) === "string")
248
+ if (readRuleEntry(entry, behavior).kind === "inert")
248
249
  return undefined;
249
250
  if (carrier === "capture") {
250
251
  out.push({ skill, behavior, entry });
@@ -51,7 +51,9 @@ export type SkillsDirectoryWarningCode =
51
51
  /** An attachment was left out (non-text bytes, over budget, unreadable, or resolving outside the skill). */
52
52
  | "attachment_skipped"
53
53
  /** A file or directory entry existed but could not be read/resolved. */
54
- | "read_failed";
54
+ | "read_failed"
55
+ /** A skill directory or its `SKILL.md` resolves outside {@link SkillsDirectoryOptions.containWithin} — skipped. */
56
+ | "outside_root";
55
57
  /** One non-fatal diagnostic from a load. Safe to log: no file contents, only names/reasons. */
56
58
  export interface SkillsDirectoryWarning {
57
59
  code: SkillsDirectoryWarningCode;
@@ -69,6 +71,15 @@ export interface SkillsDirectoryOptions {
69
71
  * ride a separate delivery cap at invoke time, so a generous value here does not enlarge the prompt.
70
72
  */
71
73
  maxAttachmentBytes?: number;
74
+ /**
75
+ * The directory this root's skills must physically lie inside. Declared ⇒ every skill directory, and every
76
+ * `SKILL.md`, resolves (links followed) inside it or is skipped with an `outside_root` warning; the attachments
77
+ * already lie inside their skill directory, so they are inside it too. Absent ⇒ each root entry is a mount the
78
+ * deployment declared, trusted like the root itself. A root a third party writes (a repository's skills
79
+ * directory) declares the repository root here, so a link in it cannot load a skill from elsewhere on disk.
80
+ * Must name an existing directory; anything else throws, like an unreadable root.
81
+ */
82
+ containWithin?: string;
72
83
  }
73
84
  /**
74
85
  * Load every `SKILL.md`-bearing subdirectory of `dir` into a {@link SkillSpec}, ready for
@@ -1,6 +1,7 @@
1
1
  import { readFileSync, readdirSync, statSync } from "node:fs";
2
- import { isAbsolute, join, relative } from "node:path";
2
+ import { join } from "node:path";
3
3
  import { physicalPathOf } from "./physical-path.js";
4
+ import { isPhysicallyWithin } from "../tools/fs/path-containment.js";
4
5
  import { assertNoRetiredKeys } from "./retired-keys.js";
5
6
  import { thrownText } from "./thrown-value.js";
6
7
  const SKILL_FILE = "SKILL.md";
@@ -167,12 +168,6 @@ function unquote(value) {
167
168
  const inner = value.slice(1, -1);
168
169
  return q === '"' ? inner.replace(/\\t/g, "\t").replace(/\\n/g, "\n") : inner;
169
170
  }
170
- function isInsideRoot(root, p) {
171
- if (p === root)
172
- return true;
173
- const rel = relative(root, p);
174
- return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
175
- }
176
171
  function listRelativeFiles(base, dir, prefix, out, walk) {
177
172
  let entries;
178
173
  try {
@@ -222,7 +217,7 @@ function resolveInsideSkill(abs, rel, walk) {
222
217
  walk.warn({ code: "attachment_skipped", skill: walk.skill, detail: `${rel}: could not be resolved (${errText(err)})` });
223
218
  return undefined;
224
219
  }
225
- if (!isInsideRoot(walk.realRoot, real)) {
220
+ if (!isPhysicallyWithin(walk.realRoot, real)) {
226
221
  walk.warn({
227
222
  code: "attachment_skipped",
228
223
  skill: walk.skill,
@@ -337,6 +332,18 @@ export function createSkillsFromDirectory(dir, options = {}) {
337
332
  catch (err) {
338
333
  throw new Error(`skills directory could not be read: ${dir} (${errText(err)})`);
339
334
  }
335
+ const containWithin = options.containWithin;
336
+ if (containWithin !== undefined) {
337
+ const isDir = typeof containWithin === "string" && statSync(containWithin, { throwIfNoEntry: false })?.isDirectory() === true;
338
+ if (!isDir)
339
+ throw new Error(`skills directory containment root is not a readable directory: ${String(containWithin)}`);
340
+ }
341
+ const outside = (skill, path, what) => {
342
+ if (containWithin === undefined || isPhysicallyWithin(containWithin, path))
343
+ return false;
344
+ warn({ code: "outside_root", skill, detail: `${what} resolves outside ${containWithin} — skipped` });
345
+ return true;
346
+ };
340
347
  const dirNames = [];
341
348
  for (const e of entries) {
342
349
  if (e.isDirectory()) {
@@ -357,6 +364,8 @@ export function createSkillsFromDirectory(dir, options = {}) {
357
364
  const skills = [];
358
365
  for (const name of dirNames) {
359
366
  const skillDir = join(dir, name);
367
+ if (outside(name, skillDir, "its directory") || outside(name, join(skillDir, SKILL_FILE), SKILL_FILE))
368
+ continue;
360
369
  let text;
361
370
  try {
362
371
  text = readFileSync(join(skillDir, SKILL_FILE), "utf8");
@@ -61,6 +61,7 @@ export declare const EXPECTED_LISTBYSCOPE_SUMMARIES: ({
61
61
  mandated: boolean;
62
62
  mandate: string;
63
63
  origin: string;
64
+ boundGrantAbsence: string;
64
65
  spentMicroUsd?: undefined;
65
66
  } | {
66
67
  token: string;
@@ -79,4 +80,5 @@ export declare const EXPECTED_LISTBYSCOPE_SUMMARIES: ({
79
80
  mandated?: undefined;
80
81
  mandate?: undefined;
81
82
  origin?: undefined;
83
+ boundGrantAbsence?: undefined;
82
84
  })[];
@@ -18,6 +18,7 @@ export function createCheckpointFixture(over = {}) {
18
18
  toolName: "Write",
19
19
  args: { path: "/x", content: "y" },
20
20
  boundInputHash: "h0",
21
+ boundGrantAbsence: "not_bindable",
21
22
  batchToolCallIds: ["call-3"],
22
23
  completedCallIds: [],
23
24
  },
@@ -461,6 +462,7 @@ export async function checkpointListByScopeSummaries(store) {
461
462
  origin: "policy",
462
463
  mandated: true,
463
464
  mandate: "tool_marks",
465
+ boundGrantAbsence: "not_bindable",
464
466
  toolCallId: "call-pr",
465
467
  toolName: "open_pr",
466
468
  args: { repo: "x" },
@@ -503,6 +505,7 @@ export const EXPECTED_LISTBYSCOPE_SUMMARIES = [
503
505
  mandated: true,
504
506
  mandate: "tool_marks",
505
507
  origin: "policy",
508
+ boundGrantAbsence: "not_bindable",
506
509
  },
507
510
  {
508
511
  token: "tok-resource",
@@ -38,6 +38,7 @@
38
38
  */
39
39
  import type { AssertAllKeysHandled } from "./ask-origin.js";
40
40
  import type { RunnerDeps } from "./runner-deps.js";
41
+ import { type ReadDenyDeploymentSeat } from "./read-face-seats.js";
41
42
  /**
42
43
  * The closed set of hot-swappable deployment seats.
43
44
  * - `models` — the model catalog generation (the same-name redirect, re-pricing and retirement rules
@@ -59,8 +60,12 @@ import type { RunnerDeps } from "./runner-deps.js";
59
60
  * - `contextSettings` — the per-model context-window overlay ({@link RunnerDeps.contextSettings}); screened
60
61
  * by the same value gate construction applies (`assertContextSettingsValue`). THE ONE SEAT
61
62
  * IN-FLIGHT LEGS RE-READ — see the header: it carries geometry, not route.
63
+ * - `readDenyPatterns` / `readDenyBuiltinTiers` / `readDenyBuiltinExclude` — the deployment's read-deny
64
+ * declaration (additions, built-in tier selection, built-in row removal); screened by the
65
+ * gates a prepare applies ({@link screenReadDenySeat}). A resumed leg compiles against the
66
+ * seats in force at its prepare (the checkpoint keeps additions only, stricter-wins).
62
67
  */
63
- export declare const SWAPPABLE_DEP_SEATS: readonly ["models", "tiers", "readFace", "writeFace", "contextSettings"];
68
+ export declare const SWAPPABLE_DEP_SEATS: readonly ["models", "tiers", "readFace", "writeFace", "contextSettings", "readDenyPatterns", "readDenyBuiltinTiers", "readDenyBuiltinExclude"];
64
69
  export type SwappableDepSeat = (typeof SWAPPABLE_DEP_SEATS)[number];
65
70
  /** The candidate a swap takes: the swappable seats of {@link RunnerDeps}, each optional (presence = instruction). */
66
71
  export type SwappableDeps = Pick<RunnerDeps, SwappableDepSeat>;
@@ -77,6 +82,9 @@ export declare const SWAP_SEAT_NOTICE: {
77
82
  readonly readFace: "config.read_face_swapped";
78
83
  readonly writeFace: "config.write_face_swapped";
79
84
  readonly contextSettings: "config.context_settings_swapped";
85
+ readonly readDenyPatterns: "config.read_deny_swapped";
86
+ readonly readDenyBuiltinTiers: "config.read_deny_swapped";
87
+ readonly readDenyBuiltinExclude: "config.read_deny_swapped";
80
88
  };
81
89
  /** The fence over the notice table: `never` while every seat has a row. */
82
90
  export type SwapNoticeTableCoversEverySeat = AssertAllKeysHandled<Exclude<SwappableDepSeat, keyof typeof SWAP_SEAT_NOTICE>>;
@@ -106,3 +114,9 @@ export interface ScreenedSwappableDeps {
106
114
  * `assertContainmentFaceValue`), so a value the door admits is a value every later prepare admits.
107
115
  */
108
116
  export declare function screenSwappableDeps(next: unknown): ScreenedSwappableDeps;
117
+ /**
118
+ * Announce a swap of the deployment's read-deny declaration, after the write: ONE `config.read_deny_swapped`
119
+ * line however many of its three seats the swap named (the line's contract lives at the notice catalog,
120
+ * `EngineNotice.code`).
121
+ */
122
+ export declare function announceReadDenySwap(onNotice: RunnerDeps["onNotice"], seats: readonly SwappableDepSeat[], inForce: Pick<RunnerDeps, ReadDenyDeploymentSeat>): void;
@@ -1,6 +1,9 @@
1
1
  import { assertContainmentFaceValue } from "../tools/fs/containment-face.js";
2
2
  import { assertContextSettingsValue } from "./context-settings.js";
3
- export const SWAPPABLE_DEP_SEATS = Object.freeze(["models", "tiers", "readFace", "writeFace", "contextSettings"]);
3
+ import { READ_DENY_DEPLOYMENT_SEATS, screenReadDenySeat } from "./read-face-seats.js";
4
+ import { deliverEngineNotice } from "./engine-notice.js";
5
+ import { uuidv7 } from "../internal/harness.js";
6
+ export const SWAPPABLE_DEP_SEATS = Object.freeze(["models", "tiers", "readFace", "writeFace", "contextSettings", "readDenyPatterns", "readDenyBuiltinTiers", "readDenyBuiltinExclude"]);
4
7
  const SWAPPABLE_DEP_SEAT_SET = new Set(SWAPPABLE_DEP_SEATS);
5
8
  export function isSwappableDepSeat(k) {
6
9
  return SWAPPABLE_DEP_SEAT_SET.has(k);
@@ -11,6 +14,9 @@ export const SWAP_SEAT_NOTICE = {
11
14
  readFace: "config.read_face_swapped",
12
15
  writeFace: "config.write_face_swapped",
13
16
  contextSettings: "config.context_settings_swapped",
17
+ readDenyPatterns: "config.read_deny_swapped",
18
+ readDenyBuiltinTiers: "config.read_deny_swapped",
19
+ readDenyBuiltinExclude: "config.read_deny_swapped",
14
20
  };
15
21
  export function screenSwappableDeps(next) {
16
22
  if (next === null || typeof next !== "object" || Array.isArray(next)) {
@@ -54,6 +60,11 @@ export function screenSwappableDeps(next) {
54
60
  case "contextSettings":
55
61
  candidate.contextSettings = assertContextSettingsValue(v, "swapDeps");
56
62
  break;
63
+ case "readDenyPatterns":
64
+ case "readDenyBuiltinTiers":
65
+ case "readDenyBuiltinExclude":
66
+ candidate[seat] = screenReadDenySeat(seat, v);
67
+ break;
57
68
  default: {
58
69
  const _exhaustive = seat;
59
70
  throw new Error(`swapDeps: unhandled seat ${String(_exhaustive)}`);
@@ -62,3 +73,16 @@ export function screenSwappableDeps(next) {
62
73
  }
63
74
  return { seats: present, candidate: candidate };
64
75
  }
76
+ export function announceReadDenySwap(onNotice, seats, inForce) {
77
+ const named = READ_DENY_DEPLOYMENT_SEATS.filter((s) => seats.includes(s));
78
+ if (named.length === 0)
79
+ return;
80
+ const patterns = inForce.readDenyPatterns?.length ?? null;
81
+ const tiers = inForce.readDenyBuiltinTiers !== undefined ? [...inForce.readDenyBuiltinTiers] : null;
82
+ const exclude = inForce.readDenyBuiltinExclude?.length ?? null;
83
+ deliverEngineNotice(onNotice, {
84
+ code: "config.read_deny_swapped",
85
+ message: `deployment read-deny declaration swapped (${named.join(", ")}): ${patterns ?? "no"} addition(s), built-in tiers ${tiers === null ? "(default)" : `[${tiers.join(", ")}]`}, ${exclude ?? "no"} built-in row removal(s); in-flight legs keep the deny face they prepared under, new legs compile against the new declaration`,
86
+ detail: { seats: named, patterns, tiers, exclude, occurrenceId: uuidv7() },
87
+ });
88
+ }
@@ -52,20 +52,22 @@
52
52
  * | any layer | the judgment's budget (one dependency timeout, or a | unanswerable `timed_out` |
53
53
  * | | batch's shared deadline) is spent | |
54
54
  *
55
- * NEVER AN APPROVER. A deployment policy (the caller's, an inherited parent's) is consulted with an
56
- * ALREADY-ABORTED signal and no approver seat: the `ToolPolicy.check` contract is that an aborted signal
57
- * releases a pending approval, and every engine policy factory checks it before reaching a seat (the
58
- * approval factory answers a deny attested `task_aborted`, read here as `approval_seat`; the inherited
59
- * PreToolUse constraint does the same before invoking its hook). A deny that carries no engine settlement
60
- * is a rule's deny and stands. RESIDUALS (stated, not closed): a deployment closure that reaches an
61
- * approver before it reads its signal can still be asked; a deployment wrapper that answers an aborted
62
- * signal with a deny of its own (no settlement) is read as a deny — the attachment face then withholds,
63
- * the notice face stays silent. The engine's own layers (narrowing, skill rows, the two lanes) get the
64
- * run's real signal: they are pure matchers and store reads.
55
+ * NEVER AN APPROVER. Every probe STATES that nobody can be asked about it — `approvalReach: "none"` on the request
56
+ * (`ToolCallRequest.approvalReach`) — and every policy layer is handed a LIVE signal of its own: it ends when the judge stops
57
+ * waiting for that layer (the layer answered, or its share of the judgment's budget is spent) or when the run's signal ends.
58
+ * An abort means only that: the task ended, or the judge no longer waits — never "there is no approver". Every engine
59
+ * policy factory reads the statement before reaching a seat (the approval factory answers a deny attested `no_approver`,
60
+ * read here as `approval_seat`; the inherited PreToolUse constraint does the same before invoking its hook), and the
61
+ * engine's own guards composed into a deployment's policy judge the probe as they judge a call — a lookup bounded by a
62
+ * live deadline answers. A deny that carries no engine settlement is a rule's deny and stands. RESIDUALS (stated, not
63
+ * closed): a deployment closure that ignores the statement and reaches an approver IS asked — the judge waits for it at
64
+ * most the judgment's budget (`timed_out`: the write face refuses, the notice face stays silent), then its signal ends
65
+ * so a closure that honors it releases its approval; a deployment wrapper that answers the statement with a deny of its
66
+ * own (no settlement) is read as a deny — the attachment face then withholds, the notice face stays silent.
65
67
  *
66
- * THE PROBE'S IDENTITY. Every layer sees the probed tool's name (`Read` / `Write`), the probed arguments, and a stable
67
- * synthetic call id (`<face>-visibility-probe` for a read, `<face>-write-probe` for a write; {@link syntheticReadProbeId},
68
- * {@link syntheticWriteProbeId}), so a deployment's audit can tell a probe from a model's call. The write face stamps the
68
+ * THE PROBE'S IDENTITY. Every layer sees the probed tool's name (`Read` / `Write`), the probed arguments, the statement
69
+ * above, and a stable synthetic call id (`<face>-visibility-probe` for a read, `<face>-write-probe` for a write;
70
+ * {@link syntheticReadProbeId}, {@link syntheticWriteProbeId}), so a deployment's audit can tell a probe from a model's call. The write face stamps the
69
71
  * facts the live gate stamps on a write (the roster's `Write` face, the tracked cwd and root, the advertised writable
70
72
  * directories, the data root's exemption): a containment fence and a data-root exemption judge the probe as they judge the call.
71
73
  */
@@ -120,7 +122,7 @@ export interface SyntheticReadLayers {
120
122
  };
121
123
  /** The leg's target-reading stamp: the probe's request carries a reading of its target, as the live gate's does. */
122
124
  stampCallTarget?: CallTargetStamp;
123
- /** The run's signal, for the engine's own layers. */
125
+ /** The run's signal: its end ends the judgment's every wait (each policy layer's own signal ends with it). */
124
126
  signal?: AbortSignal;
125
127
  }
126
128
  /** The arguments a synthetic `Read` carries, and the facts a request stamps. */