@sema-agent/core 5.29.0 → 5.31.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 (66) hide show
  1. package/CHANGELOG.md +141 -0
  2. package/dist/agents/send-message-tool.js +2 -0
  3. package/dist/agents/subagent.d.ts +2 -0
  4. package/dist/agents/subagent.js +6 -0
  5. package/dist/agents/teacher.js +2 -0
  6. package/dist/agents/verify.js +2 -0
  7. package/dist/core/auto-compaction.d.ts +5 -1
  8. package/dist/core/auto-compaction.js +10 -1
  9. package/dist/core/checkpoint-store.d.ts +51 -5
  10. package/dist/core/checkpoint-store.js +2 -1
  11. package/dist/core/hooks.d.ts +12 -1
  12. package/dist/core/hooks.js +8 -2
  13. package/dist/core/permission-rules.js +2 -2
  14. package/dist/core/runner/prepare-task.d.ts +21 -5
  15. package/dist/core/runner/prepare-task.js +115 -26
  16. package/dist/core/runner/runtask.js +29 -3
  17. package/dist/core/runner/session-rule-policy.d.ts +3 -2
  18. package/dist/core/runner/tool-output-projection.js +1 -1
  19. package/dist/core/sensitive-path-policy.js +5 -16
  20. package/dist/core/store-contracts/tool-result-store-contract.d.ts +4 -1
  21. package/dist/core/store-contracts/tool-result-store-contract.js +26 -1
  22. package/dist/core/tighten-task-spec.js +18 -0
  23. package/dist/core/tool-policy.d.ts +20 -1
  24. package/dist/core/tool-policy.js +31 -4
  25. package/dist/core/tool-result-store.d.ts +6 -4
  26. package/dist/core/tool-result-store.js +3 -1
  27. package/dist/core/types.d.ts +63 -1
  28. package/dist/engine/harness/types.d.ts +10 -0
  29. package/dist/index.d.ts +3 -1
  30. package/dist/index.js +3 -1
  31. package/dist/orchestration/run-workflow-tool.d.ts +26 -0
  32. package/dist/orchestration/run-workflow-tool.js +7 -4
  33. package/dist/orchestration/workflow-governance.d.ts +53 -3
  34. package/dist/orchestration/workflow-governance.js +162 -25
  35. package/dist/orchestration/workflow-primitives.d.ts +15 -1
  36. package/dist/orchestration/workflow-primitives.js +13 -2
  37. package/dist/prompt-assembly/epoch.js +2 -0
  38. package/dist/prompt-assembly/packs/sema-default.js +2 -2
  39. package/dist/prompt-assembly/types.d.ts +4 -0
  40. package/dist/prompts/default.d.ts +14 -9
  41. package/dist/prompts/default.js +13 -3
  42. package/dist/tools/fs/bash-readonly-classifier.d.ts +53 -4
  43. package/dist/tools/fs/bash-readonly-classifier.js +148 -16
  44. package/dist/tools/fs/fs-bash.d.ts +7 -0
  45. package/dist/tools/fs/fs-bash.js +8 -3
  46. package/dist/tools/fs/fs-pdf.d.ts +1 -1
  47. package/dist/tools/fs/fs-pdf.js +2 -2
  48. package/dist/tools/fs/fs-read.d.ts +1 -1
  49. package/dist/tools/fs/fs-read.js +11 -7
  50. package/dist/tools/fs/fs-search-tools.d.ts +4 -2
  51. package/dist/tools/fs/fs-search-tools.js +15 -8
  52. package/dist/tools/fs/fs-shared.d.ts +5 -1
  53. package/dist/tools/fs/fs-shared.js +8 -3
  54. package/dist/tools/fs/index.d.ts +18 -0
  55. package/dist/tools/fs/index.js +13 -2
  56. package/dist/tools/fs/read-deny.d.ts +110 -0
  57. package/dist/tools/fs/read-deny.js +159 -0
  58. package/dist/tools/fs/read-face.d.ts +49 -0
  59. package/dist/tools/fs/read-face.js +38 -0
  60. package/dist/tools/fs/repo-map.d.ts +3 -1
  61. package/dist/tools/fs/repo-map.js +11 -5
  62. package/dist/tools/fs/safety.d.ts +34 -11
  63. package/dist/tools/fs/safety.js +108 -8
  64. package/dist/tools/fs/search.d.ts +54 -5
  65. package/dist/tools/fs/search.js +107 -23
  66. package/package.json +1 -1
@@ -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,22 @@ 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 honestyCaveat = walkCaveat(walked, true);
811
+ const denyNote = denyWithheldNote(walked);
786
812
  let files = walked.files;
787
813
  files.sort();
788
814
  const cap = p.head_limit === 0 ? Infinity : Math.max(1, Math.floor(p.head_limit ?? GREP_DEFAULT_CAP));
@@ -937,11 +963,12 @@ export async function jsGrep(env, root, p, signal, guards) {
937
963
  if (multilineSkippedFiles > 0)
938
964
  extra.push(`${multilineSkippedFiles} file(s) skipped by multiline matching (content over ${longLineLimit} chars; the pattern's worst-case matching cost grows superlinearly with input length on the fallback engine)`);
939
965
  if (extra.length > 0) {
940
- caveat = caveat.length > 0
941
- ? caveat.replace(/\]$/, `; ${extra.join("; ")}]`)
966
+ honestyCaveat = honestyCaveat.length > 0
967
+ ? honestyCaveat.replace(/\]$/, `; ${extra.join("; ")}]`)
942
968
  : `\n…[results may be incomplete: ${extra.join("; ")}]`;
943
969
  }
944
970
  }
971
+ const caveat = honestyCaveat + denyNote;
945
972
  const paged = (arr) => (off > 0 ? arr.slice(off, off + cap) : arr);
946
973
  const offNote = off > 0 ? `\n[offset ${off}]` : "";
947
974
  if (mode === "files_with_matches") {
@@ -1016,16 +1043,18 @@ function formatRgStdout(stdout, p, caveat = "") {
1016
1043
  (off > 0 ? `\n[offset ${off}]` : "") +
1017
1044
  caveat);
1018
1045
  }
1019
- async function jsGrepFallback(env, root, p, signal, reason) {
1020
- const text = await jsGrep(env, root, p, signal);
1046
+ async function jsGrepFallback(env, root, p, signal, reason, deny) {
1047
+ const denyOut = {};
1048
+ const text = await jsGrep(env, root, p, signal, undefined, deny, denyOut);
1021
1049
  return {
1022
1050
  text: text.startsWith("Error (grep)")
1023
1051
  ? text
1024
1052
  : `${text}\n[note: ripgrep failed (${reason}); results are from a fallback scanner]`,
1025
1053
  degraded: { fallback: "js-scan", reason: `ripgrep failed (${reason})` },
1054
+ ...(denyOut.withheld !== undefined ? { withheld: denyOut.withheld } : {}),
1026
1055
  };
1027
1056
  }
1028
- export async function rgGrepDetailed(env, root, p, signal) {
1057
+ export async function rgGrepDetailed(env, root, p, signal, deny) {
1029
1058
  const mode = p.output_mode ?? "files_with_matches";
1030
1059
  const flags = ["--no-messages", "--no-require-git", "--hidden"];
1031
1060
  for (const d of VCS_DIRS)
@@ -1055,6 +1084,9 @@ export async function rgGrepDetailed(env, root, p, signal) {
1055
1084
  flags.push("--glob", shellQuote(g));
1056
1085
  if (p.type)
1057
1086
  flags.push("--type", shellQuote(p.type));
1087
+ if (deny !== undefined)
1088
+ for (const g of deny.rgExclusionGlobs)
1089
+ flags.push(g.flag, shellQuote(g.glob));
1058
1090
  const target = p.path ? shellQuote(p.path) : ".";
1059
1091
  const cmd = `rg ${flags.join(" ")} -e ${shellQuote(p.pattern)} -- ${target}`;
1060
1092
  const res = await env.exec(cmd, { cwd: root, timeout: 60, abortSignal: signal });
@@ -1070,13 +1102,26 @@ export async function rgGrepDetailed(env, root, p, signal) {
1070
1102
  degraded: { partial: true, reason: "ripgrep timed out" },
1071
1103
  };
1072
1104
  }
1073
- return jsGrepFallback(env, root, p, signal, "timed out with no output");
1105
+ return jsGrepFallback(env, root, p, signal, "timed out with no output", deny);
1074
1106
  }
1075
- return jsGrepFallback(env, root, p, signal, `could not run: ${err.code}`);
1107
+ return jsGrepFallback(env, root, p, signal, `could not run: ${err.code}`, deny);
1076
1108
  }
1077
1109
  const { exitCode, stdout } = res.value;
1078
- if (exitCode === 1)
1079
- return { text: NO_MATCHES };
1110
+ const denyDisclosure = async () => {
1111
+ if (deny === undefined || deny.rgExclusionGlobs.length === 0)
1112
+ return { note: "" };
1113
+ const withheld = await rgDenyExistenceProbe(env, root, deny, target, signal);
1114
+ if (withheld === undefined)
1115
+ return { note: "" };
1116
+ const note = withheld.kind === "existence"
1117
+ ? "\n[note: entries matching the sensitive-path read deny list exist under this scope and were excluded (readDenyPatterns)]"
1118
+ : "\n[note: entries matching the sensitive-path read deny list may have been excluded (deny-existence probe failed)]";
1119
+ return { note, withheld };
1120
+ };
1121
+ if (exitCode === 1) {
1122
+ const d = await denyDisclosure();
1123
+ return { text: NO_MATCHES + d.note, ...(d.withheld !== undefined ? { withheld: d.withheld } : {}) };
1124
+ }
1080
1125
  if (exitCode >= 2) {
1081
1126
  if (stdout.trim().length > 0) {
1082
1127
  return {
@@ -1084,15 +1129,45 @@ export async function rgGrepDetailed(env, root, p, signal) {
1084
1129
  degraded: { partial: true, reason: `ripgrep exited with code ${exitCode}` },
1085
1130
  };
1086
1131
  }
1087
- return jsGrepFallback(env, root, p, signal, `exited with code ${exitCode} and produced no output`);
1132
+ return jsGrepFallback(env, root, p, signal, `exited with code ${exitCode} and produced no output`, deny);
1088
1133
  }
1089
1134
  const orderedStdout = mode === "files_with_matches" ? await sortRgFilesByMtime(env, root, stdout, signal) : stdout;
1090
- return { text: formatRgStdout(orderedStdout, p) };
1135
+ const d = await denyDisclosure();
1136
+ return { text: formatRgStdout(orderedStdout, p) + d.note, ...(d.withheld !== undefined ? { withheld: d.withheld } : {}) };
1091
1137
  }
1092
- export async function runGrepDetailed(env, root, p, signal) {
1138
+ async function rgDenyExistenceProbe(env, root, deny, target, signal) {
1139
+ const probeFlags = ["--files", "--hidden", "--no-require-git", "--no-messages"];
1140
+ for (const d of VCS_DIRS)
1141
+ probeFlags.push("--glob", `!${d}`);
1142
+ for (const g of deny.rgProbeGlobs)
1143
+ probeFlags.push(g.flag, shellQuote(g.glob));
1144
+ try {
1145
+ const res = await env.exec(`( rg ${probeFlags.join(" ")} -- ${target}; echo "__RG_PROBE_EXIT_$?" 1>&2 ) | head -n 1`, { cwd: root, timeout: 10, abortSignal: signal });
1146
+ if (!res.ok)
1147
+ return { kind: "probe_failed" };
1148
+ const first = res.value.stdout.split("\n").find((l) => l.trim().length > 0);
1149
+ if (first !== undefined) {
1150
+ const hit = deny.matchPath(first.trim());
1151
+ return { kind: "existence", ...(hit !== null ? { patterns: [hit.pattern] } : {}) };
1152
+ }
1153
+ const marker = /__RG_PROBE_EXIT_(\d+)/.exec(res.value.stderr ?? "");
1154
+ const rgExit = marker === null ? undefined : Number(marker[1]);
1155
+ if (rgExit === 1)
1156
+ return undefined;
1157
+ if (rgExit === 0)
1158
+ return undefined;
1159
+ return { kind: "probe_failed" };
1160
+ }
1161
+ catch {
1162
+ return { kind: "probe_failed" };
1163
+ }
1164
+ }
1165
+ export async function runGrepDetailed(env, root, p, signal, deny) {
1093
1166
  if (await detectRipgrep(env))
1094
- return rgGrepDetailed(env, root, p, signal);
1095
- return { text: await jsGrep(env, root, p, signal) };
1167
+ return rgGrepDetailed(env, root, p, signal, deny);
1168
+ const denyOut = {};
1169
+ const text = await jsGrep(env, root, p, signal, undefined, deny, denyOut);
1170
+ return { text, ...(denyOut.withheld !== undefined ? { withheld: denyOut.withheld } : {}) };
1096
1171
  }
1097
1172
  export async function runGrep(env, root, p, signal) {
1098
1173
  return (await runGrepDetailed(env, root, p, signal)).text;
@@ -1180,8 +1255,8 @@ export async function runGlobDetailed(env, root, pattern, opts = {}, signal) {
1180
1255
  }
1181
1256
  return ig;
1182
1257
  };
1183
- const walked = await walk(env, root, start, ignore, signal);
1184
- const caveat = walkCaveat(walked, false);
1258
+ const walked = await walk(env, root, start, ignore, signal, opts.deny);
1259
+ const caveat = walkCaveat(walked, false) + denyWithheldNote(walked);
1185
1260
  const files = [...walked.files, ...walked.nameOnly];
1186
1261
  const rel = (abs) => (abs.startsWith(rootPrefix) ? abs.slice(rootPrefix.length) : abs);
1187
1262
  const cap = Math.max(1, Math.floor(opts.max ?? 500));
@@ -1212,5 +1287,14 @@ export async function runGlobDetailed(env, root, pattern, opts = {}, signal) {
1212
1287
  ? `\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
1288
  : "";
1214
1289
  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 };
1290
+ return {
1291
+ text,
1292
+ filenames: matched,
1293
+ numFiles: matched.length,
1294
+ truncated,
1295
+ durationMs: Date.now() - t0,
1296
+ totalMatches,
1297
+ countIsComplete,
1298
+ ...(walked.denyPruned > 0 ? { withheld: { kind: "pruned_count", count: walked.denyPruned, patterns: walked.denyPatterns } } : {}),
1299
+ };
1216
1300
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "5.29.0",
3
+ "version": "5.31.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",