@sema-agent/core 5.28.0 → 5.30.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 (122) hide show
  1. package/CHANGELOG.md +99 -3
  2. package/dist/agents/cascade.d.ts +1 -1
  3. package/dist/agents/observer.d.ts +3 -3
  4. package/dist/agents/peer-admission.d.ts +1 -1
  5. package/dist/agents/send-message-tool.js +2 -0
  6. package/dist/agents/subagent.d.ts +2 -0
  7. package/dist/agents/subagent.js +6 -0
  8. package/dist/agents/teacher.d.ts +1 -1
  9. package/dist/agents/teacher.js +2 -0
  10. package/dist/agents/verify.d.ts +6 -6
  11. package/dist/agents/verify.js +2 -0
  12. package/dist/bench/metrics.d.ts +1 -1
  13. package/dist/brain/retry.d.ts +1 -1
  14. package/dist/core/arg-summary.d.ts +1 -1
  15. package/dist/core/ask-class.d.ts +2 -2
  16. package/dist/core/ask-question.d.ts +1 -1
  17. package/dist/core/ask-question.js +14 -1
  18. package/dist/core/auto-compaction.d.ts +5 -1
  19. package/dist/core/auto-compaction.js +10 -1
  20. package/dist/core/background-shell.d.ts +5 -5
  21. package/dist/core/checkpoint-store.d.ts +65 -19
  22. package/dist/core/checkpoint-store.js +2 -1
  23. package/dist/core/file-snapshot-store.d.ts +1 -1
  24. package/dist/core/hooks.d.ts +15 -4
  25. package/dist/core/hooks.js +8 -2
  26. package/dist/core/human-input-projection.d.ts +2 -2
  27. package/dist/core/memory-admission.d.ts +2 -2
  28. package/dist/core/memory-engine/engine.d.ts +1 -1
  29. package/dist/core/memory-engine/file-backend.d.ts +2 -2
  30. package/dist/core/memory-engine/header-hints.d.ts +1 -1
  31. package/dist/core/memory-engine/layout.d.ts +2 -2
  32. package/dist/core/memory-recall.d.ts +1 -1
  33. package/dist/core/memory.d.ts +2 -2
  34. package/dist/core/oracle-isolation.d.ts +2 -2
  35. package/dist/core/permission-rule-consent.d.ts +1 -1
  36. package/dist/core/permission-rule-model.d.ts +1 -1
  37. package/dist/core/permission-rule-org.d.ts +1 -1
  38. package/dist/core/permission-rule-store.d.ts +1 -1
  39. package/dist/core/permission-rules.js +2 -2
  40. package/dist/core/runner/active-skill-scope.d.ts +1 -1
  41. package/dist/core/runner/memory-consolidation.d.ts +1 -1
  42. package/dist/core/runner/prepare-task.d.ts +29 -8
  43. package/dist/core/runner/prepare-task.js +118 -22
  44. package/dist/core/runner/runtask.d.ts +1 -1
  45. package/dist/core/runner/runtask.js +34 -8
  46. package/dist/core/runner/session-rule-policy.d.ts +4 -3
  47. package/dist/core/runner/teardown-bounded.d.ts +1 -1
  48. package/dist/core/runner/tool-disclosure.d.ts +1 -1
  49. package/dist/core/runner/tool-output-projection.js +1 -1
  50. package/dist/core/scheduler.d.ts +4 -4
  51. package/dist/core/sensitive-path-policy.js +5 -16
  52. package/dist/core/session-policy-store.d.ts +3 -3
  53. package/dist/core/shared-memory/normalize.d.ts +1 -1
  54. package/dist/core/skill-tool-specifier.d.ts +1 -1
  55. package/dist/core/store-contracts/tool-result-store-contract.js +23 -0
  56. package/dist/core/task-notification.d.ts +2 -2
  57. package/dist/core/task-registry-agent.d.ts +9 -9
  58. package/dist/core/task-registry-shared.d.ts +4 -4
  59. package/dist/core/tighten-task-spec.js +18 -0
  60. package/dist/core/tool-errors.d.ts +2 -2
  61. package/dist/core/tool-policy.d.ts +64 -12
  62. package/dist/core/tool-policy.js +68 -8
  63. package/dist/core/tool-result-store.d.ts +2 -2
  64. package/dist/core/tool-result-store.js +3 -1
  65. package/dist/core/trace.d.ts +1 -1
  66. package/dist/core/types.d.ts +68 -13
  67. package/dist/core/untrusted-text.d.ts +6 -0
  68. package/dist/core/untrusted-text.js +1 -0
  69. package/dist/engine/compaction/compaction.d.ts +1 -1
  70. package/dist/engine/harness/messages.d.ts +1 -1
  71. package/dist/engine/harness/types.d.ts +12 -2
  72. package/dist/engine/llm/types.d.ts +1 -1
  73. package/dist/engine/loop/types.d.ts +2 -2
  74. package/dist/engine/session/import-validate.d.ts +1 -1
  75. package/dist/engine/session/log-digest.d.ts +1 -1
  76. package/dist/fixtures/index.d.ts +18 -2
  77. package/dist/fixtures/index.js +11 -0
  78. package/dist/index.d.ts +3 -1
  79. package/dist/index.js +3 -1
  80. package/dist/orchestration/goal.d.ts +1 -1
  81. package/dist/orchestration/run-workflow-tool.d.ts +21 -0
  82. package/dist/orchestration/run-workflow-tool.js +6 -3
  83. package/dist/orchestration/workflow-primitives.d.ts +10 -1
  84. package/dist/orchestration/workflow-primitives.js +12 -1
  85. package/dist/orchestration/workflow-types.d.ts +5 -4
  86. package/dist/orchestration/workflow.js +11 -3
  87. package/dist/prompt-assembly/artifact.d.ts +1 -1
  88. package/dist/prompt-assembly/epoch.js +2 -0
  89. package/dist/prompt-assembly/packs/sema-default.js +2 -2
  90. package/dist/prompt-assembly/types.d.ts +4 -0
  91. package/dist/prompts/default.d.ts +14 -9
  92. package/dist/prompts/default.js +14 -4
  93. package/dist/scenarios/scenario-registry.d.ts +3 -3
  94. package/dist/stores/file/tool-result-store.d.ts +2 -2
  95. package/dist/tools/fs/bash-readonly-classifier.d.ts +23 -2
  96. package/dist/tools/fs/bash-readonly-classifier.js +11 -0
  97. package/dist/tools/fs/fs-bash.d.ts +8 -1
  98. package/dist/tools/fs/fs-bash.js +8 -3
  99. package/dist/tools/fs/fs-pdf.d.ts +1 -1
  100. package/dist/tools/fs/fs-pdf.js +2 -2
  101. package/dist/tools/fs/fs-read.d.ts +1 -1
  102. package/dist/tools/fs/fs-read.js +11 -7
  103. package/dist/tools/fs/fs-search-tools.d.ts +5 -3
  104. package/dist/tools/fs/fs-search-tools.js +15 -8
  105. package/dist/tools/fs/fs-shared.d.ts +6 -2
  106. package/dist/tools/fs/fs-shared.js +8 -3
  107. package/dist/tools/fs/index.d.ts +18 -0
  108. package/dist/tools/fs/index.js +13 -2
  109. package/dist/tools/fs/read-deny.d.ts +105 -0
  110. package/dist/tools/fs/read-deny.js +151 -0
  111. package/dist/tools/fs/read-face.d.ts +43 -0
  112. package/dist/tools/fs/read-face.js +38 -0
  113. package/dist/tools/fs/repo-map.d.ts +3 -1
  114. package/dist/tools/fs/repo-map.js +11 -5
  115. package/dist/tools/fs/safety.d.ts +34 -12
  116. package/dist/tools/fs/safety.js +88 -7
  117. package/dist/tools/fs/search.d.ts +54 -5
  118. package/dist/tools/fs/search.js +103 -21
  119. package/dist/tools/scheduler-tools.d.ts +1 -1
  120. package/dist/tools/task-list.d.ts +1 -1
  121. package/dist/tools/web.d.ts +3 -3
  122. package/package.json +1 -1
@@ -23,7 +23,10 @@ export function isAbsolutePathForm(p) {
23
23
  return p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p) || p.startsWith("\\\\");
24
24
  }
25
25
  function normalizeForCompare(p) {
26
- let n = isWinFormPath(p) ? p.replace(/\\/g, "/") : p;
26
+ let n = stripWin32ExtendedPrefix(p);
27
+ if (isUncPath(n) && isWinFormPath(n) && !isWin32DeviceNamespacePath(n))
28
+ n = foldUncLexically(n).key;
29
+ n = isWinFormPath(n) ? n.replace(/\\/g, "/") : n;
27
30
  if (/^[A-Za-z]:(\/|$)/.test(n))
28
31
  n = n[0].toLowerCase() + n.slice(1);
29
32
  return n;
@@ -67,6 +70,7 @@ export function foldRedundantPathSegments(p) {
67
70
  return foldSegmentsLexically(p, false);
68
71
  }
69
72
  function foldSegmentsLexically(p, foldParents) {
73
+ p = stripWin32ExtendedPrefix(p);
70
74
  const isUnc = p.startsWith("\\\\");
71
75
  const drive = /^[A-Za-z]:/.exec(p)?.[0] ?? "";
72
76
  const winForm = isUnc || drive !== "" || (!p.startsWith("/") && p.includes("\\"));
@@ -101,7 +105,7 @@ function foldSegmentsLexically(p, foldParents) {
101
105
  outSegs.push(seg);
102
106
  }
103
107
  if (outSegs.length === 0)
104
- return abs ? prefix + sep : prefix === "" ? "." : prefix;
108
+ return abs ? (isUnc ? prefix : prefix + sep) : prefix === "" ? "." : prefix;
105
109
  return prefix + (abs ? sep : "") + outSegs.join(sep);
106
110
  }
107
111
  const BINARY_EXTENSIONS = new Set([
@@ -262,6 +266,46 @@ export function isBinaryContent(sample) {
262
266
  export function isUncPath(path) {
263
267
  return path.startsWith("\\\\") || path.startsWith("//");
264
268
  }
269
+ function isWin32DeviceNamespacePath(p) {
270
+ return p.startsWith("\\\\.\\") || p.startsWith("\\\\./");
271
+ }
272
+ function stripWin32ExtendedPrefix(p) {
273
+ if (!p.startsWith("\\\\?\\"))
274
+ return p;
275
+ const rest = p.slice(4);
276
+ const unc = /^UNC[\\/]/i.exec(rest);
277
+ if (unc)
278
+ return "\\\\" + rest.slice(unc[0].length);
279
+ return rest;
280
+ }
281
+ function foldUncLexically(p) {
282
+ const body = p.slice(2);
283
+ const segs = body.split(/[\\/]+/).filter((s) => s !== "");
284
+ const host = segs.shift() ?? "";
285
+ const share = segs.shift();
286
+ const out = [];
287
+ let climbed = false;
288
+ for (const seg of segs) {
289
+ if (seg === ".")
290
+ continue;
291
+ if (seg === "..") {
292
+ if (out.length === 0)
293
+ climbed = true;
294
+ else
295
+ out.pop();
296
+ continue;
297
+ }
298
+ out.push(seg);
299
+ }
300
+ const anchor = `\\\\${host}${share !== undefined ? `\\${share}` : ""}`;
301
+ return { key: out.length > 0 ? `${anchor}\\${out.join("\\")}` : anchor, climbedAboveShare: climbed };
302
+ }
303
+ export function lexicalViewOf(spelled, base) {
304
+ const abs = isAbsolutePathForm(spelled)
305
+ ? spelled
306
+ : `${base.replace(/[\\/]+$/, "")}${isWinFormPath(base) && base.includes("\\") ? "\\" : "/"}${spelled}`;
307
+ return normalizeAbsPathLexically(abs);
308
+ }
265
309
  function within(rootCanonical, p) {
266
310
  if (!rootCanonical)
267
311
  return false;
@@ -274,7 +318,7 @@ function within(rootCanonical, p) {
274
318
  const base = rootN.endsWith(SEP) ? rootN : rootN + SEP;
275
319
  return pN.startsWith(base);
276
320
  }
277
- export async function resolveKey(env, rootCanonical, path, signal, baseCwd, additionalRootsCanonical, exactFileReadExemption) {
321
+ export async function resolveKey(env, rootCanonical, path, signal, baseCwd, additionalRootsCanonical, exactFileReadExemption, readDeny, readFace) {
278
322
  const canon = await canonicalizeTarget(env, path, signal, baseCwd);
279
323
  if (!canon.ok)
280
324
  return { ok: false, violation: { code: "invalid", message: canon.message } };
@@ -282,6 +326,32 @@ export async function resolveKey(env, rootCanonical, path, signal, baseCwd, addi
282
326
  if (isBlockedDevicePath(key)) {
283
327
  return { ok: false, violation: { code: "invalid", message: `path "${path}" is a blocked device/special file (reading it can hang the process); refused.` } };
284
328
  }
329
+ if (readDeny !== undefined) {
330
+ const hit = readDeny.matchTarget(key, lexicalViewOf(path, baseCwd ?? rootCanonical));
331
+ if (hit !== null) {
332
+ return {
333
+ ok: false,
334
+ violation: {
335
+ code: "read_path_denied",
336
+ message: `reading "${path}" ${key !== path ? `(canonical target: "${key}") ` : ""}is refused: the target matches the sensitive-path read deny list (pattern "${hit.pattern}"). This list guards credential-class paths and applies regardless of the containment roots.`,
337
+ target: key,
338
+ pattern: hit.pattern,
339
+ },
340
+ };
341
+ }
342
+ }
343
+ if (readFace === "open") {
344
+ if (isUncPath(key) && isWinFormPath(key) && !within(rootCanonical, key) && !additionalRootsCanonical?.some((r) => within(r, key))) {
345
+ return {
346
+ ok: false,
347
+ violation: {
348
+ code: "invalid",
349
+ message: `path "${path}" names a network (UNC) location outside the declared roots; refused even under the open read face — a read against an undeclared share can hang on the SMB mount timeout and offers credentials to the remote host.`,
350
+ },
351
+ };
352
+ }
353
+ return { ok: true, key };
354
+ }
285
355
  if (!within(rootCanonical, key) && !additionalRootsCanonical?.some((r) => within(r, key))) {
286
356
  if (exactFileReadExemption?.(key) === true)
287
357
  return { ok: true, key };
@@ -299,10 +369,18 @@ export async function resolveKey(env, rootCanonical, path, signal, baseCwd, addi
299
369
  return { ok: true, key };
300
370
  }
301
371
  export async function canonicalizeTarget(env, path, signal, baseCwd) {
302
- if (isUncPath(path) && isWinFormPath(path)) {
303
- return { ok: true, key: path };
372
+ if (isWin32DeviceNamespacePath(path)) {
373
+ return { ok: false, message: `path "${path}" is a Win32 device-namespace path (\\\\.\\ names a raw device object, not a file); refused.` };
374
+ }
375
+ const unprefixed = stripWin32ExtendedPrefix(path);
376
+ if (isUncPath(unprefixed) && isWinFormPath(unprefixed)) {
377
+ const folded = foldUncLexically(unprefixed);
378
+ if (folded.climbedAboveShare) {
379
+ return { ok: false, message: `path "${path}" climbs above its UNC share root via ".." (\\\\host\\share is the root of a network path); refused.` };
380
+ }
381
+ return { ok: true, key: folded.key };
304
382
  }
305
- const spelled = path.startsWith("//") ? path.replace(/^\/+/, "/") : path;
383
+ const spelled = unprefixed.startsWith("//") ? unprefixed.replace(/^\/+/, "/") : unprefixed;
306
384
  const target = baseCwd && !isAbsolutePathForm(spelled) ? `${baseCwd.replace(/[\\/]+$/, "")}/${spelled}` : spelled;
307
385
  const absR = await env.absolutePath(target, signal);
308
386
  if (!absR.ok)
@@ -384,6 +462,9 @@ export function violationText(toolName, v) {
384
462
  return `Error (${toolName}): ${v.message}`;
385
463
  }
386
464
  export function violationDetails(v) {
465
+ if (v.code === "read_path_denied" && v.target !== undefined && v.pattern !== undefined) {
466
+ return { type: "read_path_denied", code: "read_path_denied", target: v.target, pattern: v.pattern };
467
+ }
387
468
  return v.code === "path_not_in_root" && v.target !== undefined
388
469
  ? { type: "path_not_in_root", code: "path_not_in_root", target: v.target, roots: v.roots ?? [] }
389
470
  : undefined;
@@ -391,7 +472,7 @@ export function violationDetails(v) {
391
472
  export function withinAnyRoot(rootsCanonical, p) {
392
473
  return rootsCanonical.some((r) => within(r, p));
393
474
  }
394
- export const PATH_NOT_IN_ROOT_ESCAPE_HINT = "(This boundary applies to the structured file tools. If you genuinely need content outside the allowed root(s) and the deployment permits it, use the Bash tool — it is not confined by this fence, though every call remains subject to the deployment's approval policy. Or ask for the directory to be added to additionalDirectories.)";
475
+ export const PATH_NOT_IN_ROOT_ESCAPE_HINT = "(This boundary applies to the structured file tools. If you genuinely need content outside the allowed root(s), ask for the directory to be added to the deployment's additionalDirectories or additionalReadDirectories for read-only access. For completeness: the Bash tool is not confined by this fence, and every Bash call remains subject to the deployment's approval policy.)";
395
476
  export function requireRead(state, key) {
396
477
  const entry = state.get(key);
397
478
  if (entry === undefined || entry.isPartialView) {
@@ -126,12 +126,27 @@ export interface WalkResult {
126
126
  * without a second stat pass. An env whose listings carry no numeric `mtimeMs` simply leaves
127
127
  * entries out of this map (consumers fall back to name order). */
128
128
  mtimes: Map<string, number>;
129
+ /** design/199 件B — entries withheld by the sensitive-path read deny list: PRUNE-ROOT count only
130
+ * (a denied directory's descendants are not enumerated, so they cannot be counted — the note says
131
+ * so). 0 when no deny judge was supplied or nothing matched. */
132
+ denyPruned: number;
133
+ /** The deny patterns that pruned something, first-seen order (for the disclosure note). */
134
+ denyPatterns: string[];
129
135
  }
130
136
  /** F2: any-cause incompleteness — a result built on this walk must not present itself as exhaustive. */
131
137
  export declare function walkIsPartial(w: WalkResult): boolean;
132
138
  /** Bounded, ignore-aware recursive walk. Returns absolute file paths under `start`. Exported for
133
139
  * repo-map (design/72 §1) — same bounded/ignore-aware traversal as grep/glob, one source of truth. */
134
- export declare function walk(env: ExecutionEnv, root: string, start: string, ignore: (rel: string, isDir: boolean) => boolean, signal?: AbortSignal): Promise<WalkResult>;
140
+ export declare function walk(env: ExecutionEnv, root: string, start: string, ignore: (rel: string, isDir: boolean) => boolean, signal?: AbortSignal, deny?: {
141
+ matchPath(path: string): {
142
+ pattern: string;
143
+ } | null;
144
+ }): Promise<WalkResult>;
145
+ /** design/199 件B — the JS-leg withholding disclosure (§3.1: an empty result must stay
146
+ * distinguishable from searched-and-empty, so this note rides EVERY output shape when something was
147
+ * pruned). Distinct from {@link walkCaveat}: pruning is deliberate withholding, not incompleteness.
148
+ * The `[note:` prefix keeps it out of the tool layer's structured row extraction. */
149
+ export declare function denyWithheldNote(w: Pick<WalkResult, "denyPruned" | "denyPatterns">): string;
135
150
  /** Injectable execution boundaries for the JS-fallback scan (tests shrink them; callers use defaults). */
136
151
  export interface JsGrepGuards {
137
152
  /** Wall-clock budget (ms) for matching a SINGLE file before the scan stops with a diagnosable error. */
@@ -139,8 +154,37 @@ export interface JsGrepGuards {
139
154
  /** Longest line/content (chars) a gray-zone pattern may be matched against. */
140
155
  longLineLimit?: number;
141
156
  }
142
- /** JS-fallback grep: ignore-aware walk + per-line scan, honoring output_mode / context / head_limit. */
143
- export declare function jsGrep(env: ExecutionEnv, root: string, p: GrepParams, signal?: AbortSignal, guards?: JsGrepGuards): Promise<string>;
157
+ /** design/199 件B what a traversal withheld under the sensitive-path read deny list, structured
158
+ * (the prose note is the model-facing twin). Three shapes (§3.1): `pruned_count` = the JS walker
159
+ * counted its prune roots; `existence` = the rg leg's bounded probe CONFIRMED matching entries exist
160
+ * under the scope (rg's own exclusions are silent, so the count is unknowable); `probe_failed` =
161
+ * the probe itself failed — entries MAY have been withheld. */
162
+ export interface ReadDenyWithheld {
163
+ kind: "pruned_count" | "existence" | "probe_failed";
164
+ count?: number;
165
+ patterns?: readonly string[];
166
+ }
167
+ /** The deny judge seam every search leg shares (structural subset of ReadDenyMatcher). */
168
+ export interface ReadDenyJudge {
169
+ matchPath(path: string): {
170
+ pattern: string;
171
+ } | null;
172
+ rgExclusionGlobs: ReadonlyArray<{
173
+ flag: "--glob" | "--iglob";
174
+ glob: string;
175
+ }>;
176
+ rgProbeGlobs: ReadonlyArray<{
177
+ flag: "--glob" | "--iglob";
178
+ glob: string;
179
+ }>;
180
+ }
181
+ /** JS-fallback grep: ignore-aware walk + per-line scan, honoring output_mode / context / head_limit.
182
+ * design/199 件B: `deny` prunes the walk (note appended to every output shape); `denyOut` is the
183
+ * structured twin — a sink because this function's dozen error returns predate the facts (the sink
184
+ * is written the moment the walk lands, whatever the scan then returns). */
185
+ export declare function jsGrep(env: ExecutionEnv, root: string, p: GrepParams, signal?: AbortSignal, guards?: JsGrepGuards, deny?: ReadDenyJudge, denyOut?: {
186
+ withheld?: ReadDenyWithheld;
187
+ }): Promise<string>;
144
188
  /** Detect ripgrep once per env (cached). */
145
189
  export declare function detectRipgrep(env: ExecutionEnv): Promise<boolean>;
146
190
  /** How a grep result was produced when the ripgrep leg could not complete normally. Either the
@@ -160,16 +204,19 @@ export type GrepDegradation = {
160
204
  export interface GrepRunResult {
161
205
  text: string;
162
206
  degraded?: GrepDegradation;
207
+ /** design/199 件B — deny-list withholding facts (see {@link ReadDenyWithheld}); absent = nothing
208
+ * withheld / no deny judge in play. */
209
+ withheld?: ReadDenyWithheld;
163
210
  }
164
211
  /** ripgrep grep: build flags from params, run, normalize to the same output as {@link jsGrep}.
165
212
  * Result-fidelity contract: output ripgrep DID produce is never silently replaced by a fallback
166
213
  * rescan — partial results ship with a caveat; only a zero-output failure degrades to
167
214
  * {@link jsGrep}, and that degradation is declared in both text and structure. An external abort
168
215
  * propagates as the interruption it is (no fallback rerun). */
169
- export declare function rgGrepDetailed(env: ExecutionEnv, root: string, p: GrepParams, signal?: AbortSignal): Promise<GrepRunResult>;
216
+ export declare function rgGrepDetailed(env: ExecutionEnv, root: string, p: GrepParams, signal?: AbortSignal, deny?: ReadDenyJudge): Promise<GrepRunResult>;
170
217
  /** grep dispatch: ripgrep when the env has it, else the hardened JS fallback (design/64 §10.3).
171
218
  * Returns the degradation facts alongside the text so the tool layer can surface them structurally. */
172
- export declare function runGrepDetailed(env: ExecutionEnv, root: string, p: GrepParams, signal?: AbortSignal): Promise<GrepRunResult>;
219
+ export declare function runGrepDetailed(env: ExecutionEnv, root: string, p: GrepParams, signal?: AbortSignal, deny?: ReadDenyJudge): Promise<GrepRunResult>;
173
220
  /** Text-only wrapper of {@link runGrepDetailed}. */
174
221
  export declare function runGrep(env: ExecutionEnv, root: string, p: GrepParams, signal?: AbortSignal): Promise<string>;
175
222
  /** glob: find files by name pattern, with optional sub-path scoping; returns RELATIVE paths (design/64 §10.4). */
@@ -193,6 +240,7 @@ export declare function runGlob(env: ExecutionEnv, root: string, pattern: string
193
240
  export declare function runGlobDetailed(env: ExecutionEnv, root: string, pattern: string, opts?: {
194
241
  path?: string;
195
242
  max?: number;
243
+ deny?: ReadDenyJudge;
196
244
  }, signal?: AbortSignal): Promise<{
197
245
  text: string;
198
246
  filenames: string[];
@@ -202,4 +250,5 @@ export declare function runGlobDetailed(env: ExecutionEnv, root: string, pattern
202
250
  totalMatches: number;
203
251
  countIsComplete: boolean;
204
252
  error?: string;
253
+ withheld?: ReadDenyWithheld;
205
254
  }>;
@@ -218,7 +218,7 @@ export async function buildIgnore(env, root, signal) {
218
218
  export function walkIsPartial(w) {
219
219
  return w.incomplete || w.skippedUnreadable > 0 || w.aborted;
220
220
  }
221
- export async function walk(env, root, start, ignore, signal) {
221
+ export async function walk(env, root, start, ignore, signal, deny) {
222
222
  const out = [];
223
223
  const nameOnly = [];
224
224
  const mtimes = new Map();
@@ -226,6 +226,17 @@ export async function walk(env, root, start, ignore, signal) {
226
226
  let skippedLarge = 0;
227
227
  let skippedUnreadable = 0;
228
228
  let aborted = false;
229
+ let denyPruned = 0;
230
+ const denyPatterns = [];
231
+ const denyHit = (path) => {
232
+ const hit = deny?.matchPath(path);
233
+ if (!hit)
234
+ return false;
235
+ denyPruned++;
236
+ if (!denyPatterns.includes(hit.pattern))
237
+ denyPatterns.push(hit.pattern);
238
+ return true;
239
+ };
229
240
  const rootPrefix = root.replace(/[\\/]+$/, "") + (root.includes("\\") ? "\\" : "/");
230
241
  const rel = (abs) => (abs.startsWith(rootPrefix) ? abs.slice(rootPrefix.length) : abs);
231
242
  const stack = [{ dir: start, depth: 0 }];
@@ -255,6 +266,8 @@ export async function walk(env, root, start, ignore, signal) {
255
266
  if (info.kind === "directory") {
256
267
  if (ignore(rel(info.path), true) || visited.has(info.path))
257
268
  continue;
269
+ if (denyHit(info.path))
270
+ continue;
258
271
  if (depth < WALK_MAX_DEPTH)
259
272
  stack.push({ dir: info.path, depth: depth + 1 });
260
273
  else
@@ -263,6 +276,8 @@ export async function walk(env, root, start, ignore, signal) {
263
276
  else if (info.kind === "file") {
264
277
  if (ignore(rel(info.path), false))
265
278
  continue;
279
+ if (denyHit(info.path))
280
+ continue;
266
281
  if (typeof info.mtimeMs === "number")
267
282
  mtimes.set(info.path, info.mtimeMs);
268
283
  if (hasBinaryExtension(info.path)) {
@@ -290,7 +305,12 @@ export async function walk(env, root, start, ignore, signal) {
290
305
  }
291
306
  if (stack.length > 0 && out.length >= WALK_MAX_FILES)
292
307
  incomplete = true;
293
- return { files: out, nameOnly, incomplete, skippedLarge, skippedUnreadable, aborted, mtimes };
308
+ return { files: out, nameOnly, incomplete, skippedLarge, skippedUnreadable, aborted, mtimes, denyPruned, denyPatterns };
309
+ }
310
+ export function denyWithheldNote(w) {
311
+ if (w.denyPruned <= 0)
312
+ return "";
313
+ return `\n[note: ${w.denyPruned} entr${w.denyPruned === 1 ? "y" : "ies"} matching the sensitive-path read deny list ${w.denyPruned === 1 ? "was" : "were"} withheld from this traversal (pruned entries' descendants not counted; patterns: ${w.denyPatterns.join(", ")})]`;
294
314
  }
295
315
  function walkCaveat(w, forContentSearch) {
296
316
  const parts = [];
@@ -737,7 +757,7 @@ function multilineSpans(content, p, captureText = false, deadline = Infinity) {
737
757
  }
738
758
  return spans;
739
759
  }
740
- export async function jsGrep(env, root, p, signal, guards) {
760
+ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
741
761
  const analysis = analyzeRedos(p.pattern, p.ignore_case === true);
742
762
  if (analysis.risk)
743
763
  return redosRejection(analysis.risk);
@@ -773,16 +793,21 @@ export async function jsGrep(env, root, p, signal, guards) {
773
793
  skippedUnreadable: 0,
774
794
  aborted: false,
775
795
  mtimes: typeof info.value.mtimeMs === "number" ? new Map([[start, info.value.mtimeMs]]) : new Map(),
796
+ denyPruned: 0,
797
+ denyPatterns: [],
776
798
  };
777
799
  }
778
800
  else {
779
- walked = await walk(env, root, start, ignore, signal);
801
+ walked = await walk(env, root, start, ignore, signal, deny);
780
802
  }
781
803
  }
782
804
  else {
783
- walked = await walk(env, root, start, ignore, signal);
805
+ walked = await walk(env, root, start, ignore, signal, deny);
806
+ }
807
+ if (denyOut !== undefined && walked.denyPruned > 0) {
808
+ denyOut.withheld = { kind: "pruned_count", count: walked.denyPruned, patterns: walked.denyPatterns };
784
809
  }
785
- let caveat = walkCaveat(walked, true);
810
+ let caveat = walkCaveat(walked, true) + denyWithheldNote(walked);
786
811
  let files = walked.files;
787
812
  files.sort();
788
813
  const cap = p.head_limit === 0 ? Infinity : Math.max(1, Math.floor(p.head_limit ?? GREP_DEFAULT_CAP));
@@ -1016,16 +1041,18 @@ function formatRgStdout(stdout, p, caveat = "") {
1016
1041
  (off > 0 ? `\n[offset ${off}]` : "") +
1017
1042
  caveat);
1018
1043
  }
1019
- async function jsGrepFallback(env, root, p, signal, reason) {
1020
- const text = await jsGrep(env, root, p, signal);
1044
+ async function jsGrepFallback(env, root, p, signal, reason, deny) {
1045
+ const denyOut = {};
1046
+ const text = await jsGrep(env, root, p, signal, undefined, deny, denyOut);
1021
1047
  return {
1022
1048
  text: text.startsWith("Error (grep)")
1023
1049
  ? text
1024
1050
  : `${text}\n[note: ripgrep failed (${reason}); results are from a fallback scanner]`,
1025
1051
  degraded: { fallback: "js-scan", reason: `ripgrep failed (${reason})` },
1052
+ ...(denyOut.withheld !== undefined ? { withheld: denyOut.withheld } : {}),
1026
1053
  };
1027
1054
  }
1028
- export async function rgGrepDetailed(env, root, p, signal) {
1055
+ export async function rgGrepDetailed(env, root, p, signal, deny) {
1029
1056
  const mode = p.output_mode ?? "files_with_matches";
1030
1057
  const flags = ["--no-messages", "--no-require-git", "--hidden"];
1031
1058
  for (const d of VCS_DIRS)
@@ -1055,6 +1082,9 @@ export async function rgGrepDetailed(env, root, p, signal) {
1055
1082
  flags.push("--glob", shellQuote(g));
1056
1083
  if (p.type)
1057
1084
  flags.push("--type", shellQuote(p.type));
1085
+ if (deny !== undefined)
1086
+ for (const g of deny.rgExclusionGlobs)
1087
+ flags.push(g.flag, shellQuote(g.glob));
1058
1088
  const target = p.path ? shellQuote(p.path) : ".";
1059
1089
  const cmd = `rg ${flags.join(" ")} -e ${shellQuote(p.pattern)} -- ${target}`;
1060
1090
  const res = await env.exec(cmd, { cwd: root, timeout: 60, abortSignal: signal });
@@ -1070,13 +1100,26 @@ export async function rgGrepDetailed(env, root, p, signal) {
1070
1100
  degraded: { partial: true, reason: "ripgrep timed out" },
1071
1101
  };
1072
1102
  }
1073
- return jsGrepFallback(env, root, p, signal, "timed out with no output");
1103
+ return jsGrepFallback(env, root, p, signal, "timed out with no output", deny);
1074
1104
  }
1075
- return jsGrepFallback(env, root, p, signal, `could not run: ${err.code}`);
1105
+ return jsGrepFallback(env, root, p, signal, `could not run: ${err.code}`, deny);
1076
1106
  }
1077
1107
  const { exitCode, stdout } = res.value;
1078
- if (exitCode === 1)
1079
- return { text: NO_MATCHES };
1108
+ const denyDisclosure = async () => {
1109
+ if (deny === undefined || deny.rgExclusionGlobs.length === 0)
1110
+ return { note: "" };
1111
+ const withheld = await rgDenyExistenceProbe(env, root, deny, target, signal);
1112
+ if (withheld === undefined)
1113
+ return { note: "" };
1114
+ const note = withheld.kind === "existence"
1115
+ ? "\n[note: entries matching the sensitive-path read deny list exist under this scope and were excluded (readDenyPatterns)]"
1116
+ : "\n[note: entries matching the sensitive-path read deny list may have been excluded (deny-existence probe failed)]";
1117
+ return { note, withheld };
1118
+ };
1119
+ if (exitCode === 1) {
1120
+ const d = await denyDisclosure();
1121
+ return { text: NO_MATCHES + d.note, ...(d.withheld !== undefined ? { withheld: d.withheld } : {}) };
1122
+ }
1080
1123
  if (exitCode >= 2) {
1081
1124
  if (stdout.trim().length > 0) {
1082
1125
  return {
@@ -1084,15 +1127,45 @@ export async function rgGrepDetailed(env, root, p, signal) {
1084
1127
  degraded: { partial: true, reason: `ripgrep exited with code ${exitCode}` },
1085
1128
  };
1086
1129
  }
1087
- return jsGrepFallback(env, root, p, signal, `exited with code ${exitCode} and produced no output`);
1130
+ return jsGrepFallback(env, root, p, signal, `exited with code ${exitCode} and produced no output`, deny);
1088
1131
  }
1089
1132
  const orderedStdout = mode === "files_with_matches" ? await sortRgFilesByMtime(env, root, stdout, signal) : stdout;
1090
- return { text: formatRgStdout(orderedStdout, p) };
1133
+ const d = await denyDisclosure();
1134
+ return { text: formatRgStdout(orderedStdout, p) + d.note, ...(d.withheld !== undefined ? { withheld: d.withheld } : {}) };
1091
1135
  }
1092
- export async function runGrepDetailed(env, root, p, signal) {
1136
+ async function rgDenyExistenceProbe(env, root, deny, target, signal) {
1137
+ const probeFlags = ["--files", "--hidden", "--no-require-git", "--no-messages"];
1138
+ for (const d of VCS_DIRS)
1139
+ probeFlags.push("--glob", `!${d}`);
1140
+ for (const g of deny.rgProbeGlobs)
1141
+ probeFlags.push(g.flag, shellQuote(g.glob));
1142
+ try {
1143
+ const res = await env.exec(`( rg ${probeFlags.join(" ")} -- ${target}; echo "__RG_PROBE_EXIT_$?" 1>&2 ) | head -n 1`, { cwd: root, timeout: 10, abortSignal: signal });
1144
+ if (!res.ok)
1145
+ return { kind: "probe_failed" };
1146
+ const first = res.value.stdout.split("\n").find((l) => l.trim().length > 0);
1147
+ if (first !== undefined) {
1148
+ const hit = deny.matchPath(first.trim());
1149
+ return { kind: "existence", ...(hit !== null ? { patterns: [hit.pattern] } : {}) };
1150
+ }
1151
+ const marker = /__RG_PROBE_EXIT_(\d+)/.exec(res.value.stderr ?? "");
1152
+ const rgExit = marker === null ? undefined : Number(marker[1]);
1153
+ if (rgExit === 1)
1154
+ return undefined;
1155
+ if (rgExit === 0)
1156
+ return undefined;
1157
+ return { kind: "probe_failed" };
1158
+ }
1159
+ catch {
1160
+ return { kind: "probe_failed" };
1161
+ }
1162
+ }
1163
+ export async function runGrepDetailed(env, root, p, signal, deny) {
1093
1164
  if (await detectRipgrep(env))
1094
- return rgGrepDetailed(env, root, p, signal);
1095
- return { text: await jsGrep(env, root, p, signal) };
1165
+ return rgGrepDetailed(env, root, p, signal, deny);
1166
+ const denyOut = {};
1167
+ const text = await jsGrep(env, root, p, signal, undefined, deny, denyOut);
1168
+ return { text, ...(denyOut.withheld !== undefined ? { withheld: denyOut.withheld } : {}) };
1096
1169
  }
1097
1170
  export async function runGrep(env, root, p, signal) {
1098
1171
  return (await runGrepDetailed(env, root, p, signal)).text;
@@ -1180,8 +1253,8 @@ export async function runGlobDetailed(env, root, pattern, opts = {}, signal) {
1180
1253
  }
1181
1254
  return ig;
1182
1255
  };
1183
- const walked = await walk(env, root, start, ignore, signal);
1184
- const caveat = walkCaveat(walked, false);
1256
+ const walked = await walk(env, root, start, ignore, signal, opts.deny);
1257
+ const caveat = walkCaveat(walked, false) + denyWithheldNote(walked);
1185
1258
  const files = [...walked.files, ...walked.nameOnly];
1186
1259
  const rel = (abs) => (abs.startsWith(rootPrefix) ? abs.slice(rootPrefix.length) : abs);
1187
1260
  const cap = Math.max(1, Math.floor(opts.max ?? 500));
@@ -1212,5 +1285,14 @@ export async function runGlobDetailed(env, root, pattern, opts = {}, signal) {
1212
1285
  ? `\n[note: the file-walk budget (${WALK_MAX_FILES} files / ${WALK_MAX_DEPTH} directory levels) ran out before the whole tree was scanned — this empty result is NOT proof the file is absent; scope the search with \`path\`, or use a root-anchored pattern (e.g. "src/**/*.ts") so the walk is pruned toward it, then retry]`
1213
1286
  : "";
1214
1287
  const text = matched.length === 0 ? "No files matched." + ignoreNote + ignoredFileNote + budgetNote + caveat : matched.join("\n") + capNote + caveat;
1215
- return { text, filenames: matched, numFiles: matched.length, truncated, durationMs: Date.now() - t0, totalMatches, countIsComplete };
1288
+ return {
1289
+ text,
1290
+ filenames: matched,
1291
+ numFiles: matched.length,
1292
+ truncated,
1293
+ durationMs: Date.now() - t0,
1294
+ totalMatches,
1295
+ countIsComplete,
1296
+ ...(walked.denyPruned > 0 ? { withheld: { kind: "pruned_count", count: walked.denyPruned, patterns: walked.denyPatterns } } : {}),
1297
+ };
1216
1298
  }
@@ -27,7 +27,7 @@ export declare const AUTONOMOUS_LOOP_DYNAMIC_SENTINEL = "<<autonomous-loop-dynam
27
27
  /** Runner-held 上下文 + run-stop 出口,工具工厂闭包捕获(模型触不到)。 */
28
28
  export interface SchedulerToolContext {
29
29
  /**
30
- * **会话腿**的越权隔离键(Runner 填 sessionId ?? principal ?? taskId)。durable 腿不用这条链——它的键由
30
+ * **会话腿**的越界隔离键(Runner 填 sessionId ?? principal ?? taskId)。durable 腿不用这条链——它的键由
31
31
  * {@link durableScopeOf} 从 principal 独立铸(ruled 2026-08-03,见该函数注释)。
32
32
  */
33
33
  scope: string;
@@ -74,7 +74,7 @@ export interface TaskListStore {
74
74
  *
75
75
  * The in-memory store rejected such a row incidentally: its snapshot helper spreads `t.blocks`, so a
76
76
  * missing array threw "blocks is not iterable" at write. The file store had no such accident and wrote the
77
- * row to disk, where it survived until `TaskUpdate` reached `t.blocks.includes(...)` — a poisoned row that
77
+ * row to disk, where it survived until `TaskUpdate` reached `t.blocks.includes(...)` — a contaminated row that
78
78
  * passes the write and detonates at an unrelated later call, in a different session, with no trace of where
79
79
  * it came from. Two backends disagreeing about what a valid row is means a test written against one has
80
80
  * different semantics on the other.
@@ -21,7 +21,7 @@ export interface WebFetchConfig {
21
21
  *
22
22
  * Pre-2.13 review F1 (additive): the object return form lets a summarizer report that its own OUTPUT
23
23
  * was cut (`truncated: true` — e.g. the sub-model hit max_tokens) as structured fact. The tool then
24
- * renders the disclosure OUTSIDE the untrusted fence (a page must not be able to forge it) and stamps
24
+ * renders the disclosure OUTSIDE the untrusted fence (a page must not be able to self-declare it) and stamps
25
25
  * `details.truncated`. A plain-string return still works and claims a complete summary.
26
26
  *
27
27
  * ruled 2026-08-05 (grounding contract): the object form additionally carries `inputTruncated` — the
@@ -126,7 +126,7 @@ export declare const WEBFETCH_SUMMARY_INPUT_HEADROOM = 0.8;
126
126
  * reservation still gets a small excerpt rather than a computation that lands at or below zero and feeds
127
127
  * the summarizer an empty page — a call that cannot possibly answer is strictly worse than one that might.
128
128
  *
129
- * Honest scope (adversarial review round 1): this is a FLOOR, not a proof of fit. With the output
129
+ * Honest scope (falsification review round 1): this is a FLOOR, not a proof of fit. With the output
130
130
  * reservation bounded at window/4 (see `resolveSummaryInputChars`), the floor now fires only on genuinely
131
131
  * tiny windows (roughly under 3k tokens at the default chars-per-token) — a model whose declared
132
132
  * dimensions leave no room gets a small excerpt whose attempt often succeeds anyway, and a failure is
@@ -139,7 +139,7 @@ export declare const WEBFETCH_SUMMARY_MIN_CONTENT = 4000;
139
139
  * Resolve the page-content budget for {@link createWebFetchSummarizer} against `model`'s window.
140
140
  * An explicit `override` wins outright (a deployment that measured its own model beats an estimate).
141
141
  *
142
- * Fail-loud on every non-finite input, same posture as {@link resolveWebMaxBytes} (adversarial review
142
+ * Fail-loud on every non-finite input, same posture as {@link resolveWebMaxBytes} (falsification review
143
143
  * round 1): a `NaN` anywhere in the model's declared dimensions propagates through the arithmetic and out
144
144
  * through `Math.min`/`Math.max` unchanged, and a `NaN` budget makes `content.length > budget` false — so
145
145
  * the clip silently stops happening and the whole oversize page is fed to the model, which is the failure
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "5.28.0",
3
+ "version": "5.30.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",