@sema-agent/core 5.39.0 → 5.40.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 (32) hide show
  1. package/CHANGELOG.md +99 -11
  2. package/dist/core/checkpoint-store.d.ts +12 -0
  3. package/dist/core/governance-codes.js +2 -0
  4. package/dist/core/hooks.js +9 -1
  5. package/dist/core/memory-engine/engine.d.ts +27 -0
  6. package/dist/core/memory-engine/engine.js +103 -1
  7. package/dist/core/memory-engine/export-bundle.d.ts +192 -0
  8. package/dist/core/memory-engine/export-bundle.js +306 -0
  9. package/dist/core/memory-engine/file-backend.d.ts +178 -1
  10. package/dist/core/memory-engine/file-backend.js +637 -6
  11. package/dist/core/memory-engine/index.d.ts +2 -1
  12. package/dist/core/memory-engine/index.js +1 -0
  13. package/dist/core/memory-engine/layout.d.ts +89 -1
  14. package/dist/core/memory-engine/layout.js +131 -1
  15. package/dist/core/memory-engine/memory-backend-contract.d.ts +1 -1
  16. package/dist/core/memory-engine/memory-backend-contract.js +52 -0
  17. package/dist/core/memory-engine/tools.js +8 -1
  18. package/dist/core/permission-rule-consent.js +14 -2
  19. package/dist/core/runner/prepare-task.js +13 -0
  20. package/dist/core/runner/runtask.js +5 -0
  21. package/dist/core/runner/synthetic-tools.js +3 -1
  22. package/dist/core/runner/tool-disclosure.js +2 -1
  23. package/dist/core/write-protect.d.ts +0 -20
  24. package/dist/core/write-protect.js +4 -3
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/tools/fs/safety.d.ts +1 -1
  28. package/dist/tools/fs/safety.js +1 -1
  29. package/dist/tools/fs/search.d.ts +33 -0
  30. package/dist/tools/fs/search.js +72 -0
  31. package/package.json +1 -1
  32. package/test/export-surface.snapshot.json +9 -1
@@ -344,7 +344,7 @@ export declare function withinAnyRoot(rootsCanonical: readonly string[], p: stri
344
344
  * just let the model discover it by trial), but it is a statement, not a recommendation, and it
345
345
  * comes last. Every shell call still passes the deployment's approval policy.
346
346
  */
347
- export declare 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 \u2014 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.)";
347
+ export declare 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 \u2014 or additionalReadDirectories for read-only access; a deployment that wants reads open everywhere can declare readFace: \"open\" instead of listing directories. For completeness: the Bash tool is not confined by this fence, and every Bash call remains subject to the deployment's approval policy.)";
348
348
  /** inv 1 (read-before-edit): a file must have been read this task before it can be edited/overwritten.
349
349
  * Message is CC 2.1.198 live-verbatim (all-tools-live-probe 2026-07-08 §2.1/§3.1/§5.1 — one message for
350
350
  * Edit/Write/NotebookEdit: "before writing to it", not the old sema "before editing").
@@ -493,7 +493,7 @@ export function violationDetails(v) {
493
493
  export function withinAnyRoot(rootsCanonical, p) {
494
494
  return rootsCanonical.some((r) => within(r, p));
495
495
  }
496
- 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.)";
496
+ 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; a deployment that wants reads open everywhere can declare readFace: \"open\" instead of listing directories. For completeness: the Bash tool is not confined by this fence, and every Bash call remains subject to the deployment's approval policy.)";
497
497
  export function requireRead(state, key) {
498
498
  const entry = state.get(key);
499
499
  if (entry === undefined || entry.isPartialView) {
@@ -208,6 +208,39 @@ export interface GrepRunResult {
208
208
  * withheld / no deny judge in play. */
209
209
  withheld?: ReadDenyWithheld;
210
210
  }
211
+ /**
212
+ * backlog #303 (rescan-hardened form) — the rg leg's deny TRIPWIRE. rg's exclusion globs are spelled
213
+ * from the pattern text and cannot express the win32 component-alias family (`.aws.` / `.aws ` —
214
+ * trailing dots/spaces the win32 namespace strips), so a directory planted under an alias spelling
215
+ * slips past the globs while the shared `matchPath` (two-view, read-deny.ts) correctly names it —
216
+ * the JS leg pruned it, the rg leg returned its contents, and the same tree answered differently
217
+ * depending on whether ripgrep was installed.
218
+ *
219
+ * The first fix FILTERED rg's text output line-by-line. The rescan killed that shape twice over:
220
+ * ripgrep prints filenames verbatim, so a path component containing a NEWLINE splits one record
221
+ * across physical lines whose fragments carry no `:line:` boundary — the filter kept them and the
222
+ * deny-listed content passed (a protection hole, not a precision residual); and pruning every line
223
+ * of a partial run fabricated a `No matches.` row inside the structured card. Text-splitting rg
224
+ * records is unsalvageable without structured output (`--json`, backlog #306), so this function no
225
+ * longer edits anything: it JUDGES. Any deny hit — or any line the format cannot account for —
226
+ * trips, and the caller abandons the rg run for the JS scanner, whose walk prunes with the
227
+ * authoritative judge and needs no path parsing at all. rg stays the fast path for the common case
228
+ * (no guarded entries in the result); the moment a guarded spelling is involved, the engine that
229
+ * cannot mis-parse it owns the answer.
230
+ *
231
+ * Trip conditions by mode — `files_with_matches`: every line IS a path, judged whole; `count`:
232
+ * `path:N` (a line without the numeric tail is a split record — ambiguous, trip); `content`:
233
+ * judged at EVERY `:digits:` boundary and every `-digits-` boundary (a candidate cut inside match
234
+ * text can over-trip — safe: the fallback re-derives the exact answer), and a non-separator line
235
+ * with NO boundary at all is a split record — trip. `--` separators and empty lines pass.
236
+ */
237
+ export declare function rgOutputDenyTripwire(stdout: string, mode: "content" | "files_with_matches" | "count", judge: Pick<ReadDenyJudge, "matchPath">): {
238
+ trip: false;
239
+ } | {
240
+ trip: true;
241
+ reason: "deny-hit" | "ambiguous-record";
242
+ pattern?: string;
243
+ };
211
244
  /** ripgrep grep: build flags from params, run, normalize to the same output as {@link jsGrep}.
212
245
  * Result-fidelity contract: output ripgrep DID produce is never silently replaced by a fallback
213
246
  * rescan — partial results ship with a caveat; only a zero-output failure degrades to
@@ -1043,6 +1043,63 @@ function formatRgStdout(stdout, p, caveat = "") {
1043
1043
  (off > 0 ? `\n[offset ${off}]` : "") +
1044
1044
  caveat);
1045
1045
  }
1046
+ export function rgOutputDenyTripwire(stdout, mode, judge) {
1047
+ if (stdout.length === 0)
1048
+ return { trip: false };
1049
+ const judgeBoundaries = (line, boundary) => {
1050
+ let sawBoundary = false;
1051
+ for (let m = boundary.exec(line); m !== null; m = boundary.exec(line)) {
1052
+ sawBoundary = true;
1053
+ const prefix = line.slice(0, m.index);
1054
+ if (prefix.length === 0)
1055
+ continue;
1056
+ const hit = judge.matchPath(prefix);
1057
+ if (hit !== null)
1058
+ return hit.pattern;
1059
+ }
1060
+ return sawBoundary ? null : "";
1061
+ };
1062
+ for (const line of stdout.split("\n")) {
1063
+ if (line.length === 0 || line === "--")
1064
+ continue;
1065
+ if (mode === "files_with_matches") {
1066
+ const h = judge.matchPath(line);
1067
+ if (h !== null)
1068
+ return { trip: true, reason: "deny-hit", pattern: h.pattern };
1069
+ }
1070
+ else if (mode === "count") {
1071
+ const m = /^(.*):\d+$/.exec(line);
1072
+ if (m === null)
1073
+ return { trip: true, reason: "ambiguous-record" };
1074
+ const h = judge.matchPath(m[1]);
1075
+ if (h !== null)
1076
+ return { trip: true, reason: "deny-hit", pattern: h.pattern };
1077
+ }
1078
+ else {
1079
+ const colon = judgeBoundaries(line, /:\d+:/g);
1080
+ if (colon !== null && colon !== "")
1081
+ return { trip: true, reason: "deny-hit", pattern: colon };
1082
+ const dash = judgeBoundaries(line, /-\d+-/g);
1083
+ if (dash !== null && dash !== "")
1084
+ return { trip: true, reason: "deny-hit", pattern: dash };
1085
+ if (colon === "" && dash === "")
1086
+ return { trip: true, reason: "ambiguous-record" };
1087
+ }
1088
+ }
1089
+ return { trip: false };
1090
+ }
1091
+ async function jsGrepDenyTripFallback(env, root, p, signal, trip, deny) {
1092
+ const denyOut = {};
1093
+ const text = await jsGrep(env, root, p, signal, undefined, deny, denyOut);
1094
+ const why = trip.reason === "deny-hit" ? "its output involved sensitive-path deny-listed entries the exclusion globs cannot express" : "its output contained a record the line format cannot account for";
1095
+ return {
1096
+ text: text.startsWith("Error (grep)")
1097
+ ? text
1098
+ : `${text}\n[note: the ripgrep pass was abandoned (${why}); results are from the fallback scanner, which prunes with the authoritative deny judge]`,
1099
+ degraded: { fallback: "js-scan", reason: `ripgrep output tripped the deny tripwire (${trip.reason})` },
1100
+ ...(denyOut.withheld !== undefined ? { withheld: denyOut.withheld } : {}),
1101
+ };
1102
+ }
1046
1103
  async function jsGrepFallback(env, root, p, signal, reason, deny) {
1047
1104
  const denyOut = {};
1048
1105
  const text = await jsGrep(env, root, p, signal, undefined, deny, denyOut);
@@ -1097,6 +1154,11 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
1097
1154
  if (err.code === "timeout") {
1098
1155
  const partial = err.partialStdout ?? "";
1099
1156
  if (partial.trim().length > 0) {
1157
+ if (deny !== undefined) {
1158
+ const trip = rgOutputDenyTripwire(partial, mode, deny);
1159
+ if (trip.trip)
1160
+ return jsGrepDenyTripFallback(env, root, p, signal, trip, deny);
1161
+ }
1100
1162
  return {
1101
1163
  text: `${delimitUntrusted("ripgrep partial output", formatRgStdout(partial, p))}\n…[ripgrep timed out after producing partial output — results may be incomplete]`,
1102
1164
  degraded: { partial: true, reason: "ripgrep timed out" },
@@ -1124,6 +1186,11 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
1124
1186
  }
1125
1187
  if (exitCode >= 2) {
1126
1188
  if (stdout.trim().length > 0) {
1189
+ if (deny !== undefined) {
1190
+ const trip = rgOutputDenyTripwire(stdout, mode, deny);
1191
+ if (trip.trip)
1192
+ return jsGrepDenyTripFallback(env, root, p, signal, trip, deny);
1193
+ }
1127
1194
  return {
1128
1195
  text: `${delimitUntrusted("ripgrep partial output", formatRgStdout(stdout, p))}\n…[ripgrep exited with an error after producing partial output — results may be incomplete]`,
1129
1196
  degraded: { partial: true, reason: `ripgrep exited with code ${exitCode}` },
@@ -1131,6 +1198,11 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
1131
1198
  }
1132
1199
  return jsGrepFallback(env, root, p, signal, `exited with code ${exitCode} and produced no output`, deny);
1133
1200
  }
1201
+ if (deny !== undefined) {
1202
+ const trip = rgOutputDenyTripwire(stdout, mode, deny);
1203
+ if (trip.trip)
1204
+ return jsGrepDenyTripFallback(env, root, p, signal, trip, deny);
1205
+ }
1134
1206
  const orderedStdout = mode === "files_with_matches" ? await sortRgFilesByMtime(env, root, stdout, signal) : stdout;
1135
1207
  const d = await denyDisclosure();
1136
1208
  return { text: formatRgStdout(orderedStdout, p) + d.note, ...(d.withheld !== undefined ? { withheld: d.withheld } : {}) };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "5.39.0",
3
+ "version": "5.40.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_comment": "design/87 L3 — frozen public export surface of src/index.ts (name -> kind). DO NOT edit by hand to silence a red test. A removed/changed entry = a SemVer-BREAKING change; bump MAJOR and update this fixture in the SAME commit (design/87 §4.2 / §5.2). Regenerate via REGEN in test/export-surface.test.ts.",
3
- "count": 1597,
3
+ "count": 1605,
4
4
  "exports": {
5
5
  "A2ATaskState": "type",
6
6
  "A2ATaskStateReversal": "type",
@@ -119,6 +119,9 @@
119
119
  "BudgetAxis": "type",
120
120
  "BuildAutoModePromptOptions": "interface",
121
121
  "BuiltinWorkflowDefinition": "interface",
122
+ "BundleChallengeRow": "interface",
123
+ "BundleLineageRow": "interface",
124
+ "BundlePollutedSession": "interface",
122
125
  "CC_MODEL_TIER_ALIASES": "variable",
123
126
  "CHALLENGED_HISTORY_FILE": "variable",
124
127
  "CHALLENGES_FILE": "variable",
@@ -467,14 +470,18 @@
467
470
  "MemoryAnnouncement": "interface",
468
471
  "MemoryBackend": "interface",
469
472
  "MemoryBackendContractHooks": "interface",
473
+ "MemoryBundleImportPlan": "interface",
470
474
  "MemoryEngine": "class",
471
475
  "MemoryEngineOptions": "interface",
472
476
  "MemoryEntry": "interface",
473
477
  "MemoryEntryFrontmatter": "interface",
474
478
  "MemoryEntryHeader": "interface",
475
479
  "MemoryErasureAttestation": "interface",
480
+ "MemoryExportBundle": "interface",
481
+ "MemoryExportSnapshot": "interface",
476
482
  "MemoryGateError": "class",
477
483
  "MemoryGetDetails": "interface",
484
+ "MemoryImportReport": "interface",
478
485
  "MemoryInjection": "interface",
479
486
  "MemoryListDetails": "type",
480
487
  "MemoryNoteHeader": "interface",
@@ -1180,6 +1187,7 @@
1180
1187
  "computeCatalogDigest": "function",
1181
1188
  "computeCostMicroUsd": "function",
1182
1189
  "computeEntryRev": "function",
1190
+ "computeMemoryBundleHash": "function",
1183
1191
  "computeShapeDigest": "function",
1184
1192
  "confirmRuleApproval": "function",
1185
1193
  "consolidateScope": "function",