@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.
- package/CHANGELOG.md +111 -0
- package/dist/core/runner/prepare-task.d.ts +2 -0
- package/dist/core/runner/prepare-task.js +32 -10
- package/dist/core/store-contracts/tool-result-store-contract.d.ts +4 -1
- package/dist/core/store-contracts/tool-result-store-contract.js +5 -3
- package/dist/core/tool-result-store.d.ts +6 -4
- package/dist/core/types.d.ts +17 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +5 -0
- package/dist/orchestration/run-workflow-tool.js +1 -1
- package/dist/orchestration/workflow-governance.d.ts +53 -3
- package/dist/orchestration/workflow-governance.js +162 -25
- package/dist/orchestration/workflow-primitives.d.ts +5 -0
- package/dist/orchestration/workflow-primitives.js +1 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +32 -4
- package/dist/tools/fs/bash-readonly-classifier.js +137 -16
- package/dist/tools/fs/index.d.ts +7 -3
- package/dist/tools/fs/index.js +3 -1
- package/dist/tools/fs/read-deny.d.ts +5 -0
- package/dist/tools/fs/read-deny.js +9 -1
- package/dist/tools/fs/read-face.d.ts +27 -0
- package/dist/tools/fs/read-face.js +10 -1
- package/dist/tools/fs/safety.d.ts +1 -0
- package/dist/tools/fs/safety.js +26 -7
- package/dist/tools/fs/search.js +5 -3
- package/package.json +1 -1
|
@@ -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;
|
package/dist/tools/fs/safety.js
CHANGED
|
@@ -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[
|
|
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
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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:
|
|
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) {
|
package/dist/tools/fs/search.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
966
|
-
?
|
|
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") {
|