@sema-agent/core 5.30.0 → 5.32.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.
@@ -2,6 +2,12 @@
2
2
  * `"open"` = the containment step is skipped (canonicalization, deny set, UNC out-of-set refusal
3
3
  * and the type gates all still run). */
4
4
  export type ReadFace = "open" | "roots";
5
+ /** Loud value gate (#123): a `readFace` seat carries exactly "open" | "roots" — anything else
6
+ * (case variants, truthy garbage) refuses at wiring time, never folds to a default. Exported
7
+ * module-internally (not on the package surface) so prepare's unconditional config-guard leg can
8
+ * screen the seats on runs that never mount hands — the resolver only runs beside a mount, and a
9
+ * garbage value must not become silently legal on the hands-less path (5.30 re-review). */
10
+ export declare function assertReadFaceValue(v: unknown, seat: string): ReadFace | undefined;
5
11
  /** Inputs to {@link resolveReadFace} — all structural/declaration facts, never permission modes
6
12
  * (a deliberate axis separation: this shape carries facts, not verdicts). */
7
13
  export interface ReadFaceInputs {
@@ -23,6 +29,13 @@ export interface ReadFaceInputs {
23
29
  * "open"… no — see the resolution order: an EXPLICIT open still wins (row 4 is a default, not a
24
30
  * clamp); what it changes is that NOTHING implicit opens a bash-less mount. */
25
31
  fullShellReachable: boolean;
32
+ /** #237 — observation hook for the ONE silent branch of the order: row 1's deployment-seat clamp
33
+ * (readOnly mount + deps "open" + NO task seat). Fired exactly when the deps declaration was the
34
+ * deciding seat and the mount overrode it — not when a task-level "roots" pinned the same value
35
+ * anyway (the mount changed nothing there, and announcing it would claim a false cause), and
36
+ * never on the throw rows (those are already loud). The resolver stays the single owner of the
37
+ * clamp predicate; stations own delivery/dedup ({@link deploymentReadFaceClampNotice}). */
38
+ onDeploymentClamp?: () => void;
26
39
  }
27
40
  /**
28
41
  * The ONE resolution order (§2.2), first hit wins. prepare-task AND createHandsToolkit both call
@@ -41,3 +54,17 @@ export interface ReadFaceInputs {
41
54
  * 5. default: ROOTS (D-1b — the engine never opens implicitly).
42
55
  */
43
56
  export declare function resolveReadFace(i: ReadFaceInputs): ReadFace;
57
+ /**
58
+ * #237 — the ONE text of the deployment-clamp announcement (`config.read_face_deployment_clamped`).
59
+ * Both stations deliver THIS object through their own `deliverEngineNotice` call (prepare-task with
60
+ * the `RunnerDeps.onNotice` seat, once per notice sink — the sink, not the deps object, because the
61
+ * Runner rebuilds its deps; `createHandsToolkit` with its band-local `HandsToolkitOptions.onNotice`
62
+ * seat — toolkit creation is itself the one-per-mount boot moment, and an absent seat prints the
63
+ * same line via `console.warn`). Built here, import-free, so the message can never fork between
64
+ * the stations.
65
+ */
66
+ export declare function deploymentReadFaceClampNotice(): {
67
+ code: string;
68
+ message: string;
69
+ detail: Record<string, unknown>;
70
+ };
@@ -1,4 +1,4 @@
1
- function assertReadFaceValue(v, seat) {
1
+ export function assertReadFaceValue(v, seat) {
2
2
  if (v === undefined)
3
3
  return undefined;
4
4
  if (v === "open" || v === "roots")
@@ -16,6 +16,8 @@ export function resolveReadFace(i) {
16
16
  e.code = "config.read_face_readonly_conflict";
17
17
  throw e;
18
18
  }
19
+ if (deps === "open" && spec === undefined)
20
+ i.onDeploymentClamp?.();
19
21
  return "roots";
20
22
  }
21
23
  if (i.orgGoverned) {
@@ -36,3 +38,10 @@ export function resolveReadFace(i) {
36
38
  return "roots";
37
39
  return "roots";
38
40
  }
41
+ export function deploymentReadFaceClampNotice() {
42
+ return {
43
+ code: "config.read_face_deployment_clamped",
44
+ message: `readFace (deployment seat): the deployment-wide "open" declaration is not in force on this read-only (verifier) mount — clamped to "roots" (stricter-wins: the mount's containment is load-bearing and never openable). Other mounts still honor "open".`,
45
+ detail: { seat: "readFace (deployment seat)", declared: "open", inForce: "roots", cause: "read_only_mount" },
46
+ };
47
+ }
@@ -270,6 +270,7 @@ export declare function lexicalViewOf(spelled: string, base: string): string;
270
270
  export declare function resolveKey(env: ExecutionEnv, rootCanonical: string, path: string, signal?: AbortSignal, baseCwd?: string, additionalRootsCanonical?: readonly string[], exactFileReadExemption?: (canonicalKey: string) => boolean, readDeny?: {
271
271
  matchTarget(canonicalKey: string, lexicalView?: string): {
272
272
  pattern: string;
273
+ matchedView?: string;
273
274
  } | null;
274
275
  }, readFace?: "open" | "roots"): Promise<{
275
276
  ok: true;
@@ -49,7 +49,7 @@ const PROC_SENSITIVE_SUFFIXES = ["/environ", "/cmdline", "/auxv", "/maps", "/mem
49
49
  function isProcSensitiveFile(key) {
50
50
  return key.startsWith("/proc/") && PROC_SENSITIVE_SUFFIXES.some((suf) => key.endsWith(suf));
51
51
  }
52
- const WIN_RESERVED_RE = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\.[^\\/]*)?$/i;
52
+ const WIN_RESERVED_RE = /^(CON|PRN|AUX|NUL|COM[0-9¹²³]|LPT[0-9¹²³]|CONIN\$|CONOUT\$)(\.[^\\/]*)?$/i;
53
53
  export function isWinFormPath(p) {
54
54
  return /^[A-Za-z]:[\\/]/.test(p) || p.startsWith("\\\\") || (!p.startsWith("/") && p.includes("\\"));
55
55
  }
@@ -273,10 +273,21 @@ function stripWin32ExtendedPrefix(p) {
273
273
  if (!p.startsWith("\\\\?\\"))
274
274
  return p;
275
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;
276
+ const unc = /^UNC\\/i.exec(rest);
277
+ const body = unc !== null ? rest.slice(unc[0].length) : /^[A-Za-z]:\\/.test(rest) ? rest.slice(3) : undefined;
278
+ if (body === undefined)
279
+ return p;
280
+ if (body !== "") {
281
+ for (const seg of body.split("\\")) {
282
+ if (seg === "" || seg === "." || seg === "..")
283
+ return p;
284
+ if (seg.endsWith(".") || seg.endsWith(" ") || seg.includes("/"))
285
+ return p;
286
+ if (WIN_RESERVED_RE.test(seg))
287
+ return p;
288
+ }
289
+ }
290
+ return unc ? "\\\\" + rest.slice(unc[0].length) : rest;
280
291
  }
281
292
  function foldUncLexically(p) {
282
293
  const body = p.slice(2);
@@ -295,7 +306,8 @@ function foldUncLexically(p) {
295
306
  out.pop();
296
307
  continue;
297
308
  }
298
- out.push(seg);
309
+ const stripped = seg.replace(/[. ]+$/, "");
310
+ out.push(stripped.length > 0 ? stripped : seg);
299
311
  }
300
312
  const anchor = `\\\\${host}${share !== undefined ? `\\${share}` : ""}`;
301
313
  return { key: out.length > 0 ? `${anchor}\\${out.join("\\")}` : anchor, climbedAboveShare: climbed };
@@ -329,11 +341,15 @@ export async function resolveKey(env, rootCanonical, path, signal, baseCwd, addi
329
341
  if (readDeny !== undefined) {
330
342
  const hit = readDeny.matchTarget(key, lexicalViewOf(path, baseCwd ?? rootCanonical));
331
343
  if (hit !== null) {
344
+ const lexicalHit = hit.matchedView !== undefined && hit.matchedView !== key;
345
+ const judged = lexicalHit
346
+ ? `the requested path's spelling ("${hit.matchedView}") matches`
347
+ : `the target matches`;
332
348
  return {
333
349
  ok: false,
334
350
  violation: {
335
351
  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.`,
352
+ message: `reading "${path}" ${key !== path ? `(canonical target: "${key}") ` : ""}is refused: ${judged} the sensitive-path read deny list (pattern "${hit.pattern}"). This list guards credential-class paths and applies regardless of the containment roots.`,
337
353
  target: key,
338
354
  pattern: hit.pattern,
339
355
  },
@@ -373,6 +389,9 @@ export async function canonicalizeTarget(env, path, signal, baseCwd) {
373
389
  return { ok: false, message: `path "${path}" is a Win32 device-namespace path (\\\\.\\ names a raw device object, not a file); refused.` };
374
390
  }
375
391
  const unprefixed = stripWin32ExtendedPrefix(path);
392
+ if (unprefixed.startsWith("\\\\?\\")) {
393
+ return { ok: false, message: `path "${path}" is a Win32 extended-length namespace path with no DOS-path equivalent (only \\\\?\\<drive>: and \\\\?\\UNC\\ forms name files); refused.` };
394
+ }
376
395
  if (isUncPath(unprefixed) && isWinFormPath(unprefixed)) {
377
396
  const folded = foldUncLexically(unprefixed);
378
397
  if (folded.climbedAboveShare) {
@@ -807,7 +807,8 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
807
807
  if (denyOut !== undefined && walked.denyPruned > 0) {
808
808
  denyOut.withheld = { kind: "pruned_count", count: walked.denyPruned, patterns: walked.denyPatterns };
809
809
  }
810
- let caveat = walkCaveat(walked, true) + denyWithheldNote(walked);
810
+ let honestyCaveat = walkCaveat(walked, true);
811
+ const denyNote = denyWithheldNote(walked);
811
812
  let files = walked.files;
812
813
  files.sort();
813
814
  const cap = p.head_limit === 0 ? Infinity : Math.max(1, Math.floor(p.head_limit ?? GREP_DEFAULT_CAP));
@@ -962,11 +963,12 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
962
963
  if (multilineSkippedFiles > 0)
963
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)`);
964
965
  if (extra.length > 0) {
965
- caveat = caveat.length > 0
966
- ? caveat.replace(/\]$/, `; ${extra.join("; ")}]`)
966
+ honestyCaveat = honestyCaveat.length > 0
967
+ ? honestyCaveat.replace(/\]$/, `; ${extra.join("; ")}]`)
967
968
  : `\n…[results may be incomplete: ${extra.join("; ")}]`;
968
969
  }
969
970
  }
971
+ const caveat = honestyCaveat + denyNote;
970
972
  const paged = (arr) => (off > 0 ? arr.slice(off, off + cap) : arr);
971
973
  const offNote = off > 0 ? `\n[offset ${off}]` : "";
972
974
  if (mode === "files_with_matches") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "5.30.0",
3
+ "version": "5.32.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",