@sema-agent/core 7.22.0 → 7.23.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 +168 -1
- package/dist/core/auto-compaction.d.ts +2 -2
- package/dist/core/checkpoint-store.d.ts +1 -5
- package/dist/core/context-edit.js +1 -1
- package/dist/core/engine-notice.d.ts +13 -0
- package/dist/core/gate-bounded-await.d.ts +13 -3
- package/dist/core/gate-fold.js +3 -3
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/park-selfcheck.js +1 -1
- package/dist/core/path-identity.d.ts +11 -4
- package/dist/core/path-identity.js +5 -2
- package/dist/core/read-root-candidate.js +11 -1
- package/dist/core/runner/assemble-result.d.ts +2 -7
- package/dist/core/runner/assemble-result.js +25 -8
- package/dist/core/runner/contracts.d.ts +15 -15
- package/dist/core/runner/inherited-ask-grants.d.ts +2 -7
- package/dist/core/runner/inherited-ask-grants.js +1 -0
- package/dist/core/runner/prepare-ask-lane.js +1 -0
- package/dist/core/runner/prepare-context-lane.d.ts +43 -1
- package/dist/core/runner/prepare-context-lane.js +25 -4
- package/dist/core/runner/prepare-task.js +2 -2
- package/dist/core/runner/prepare-turn-wiring.js +2 -2
- package/dist/core/runner/run-harness-handlers.js +4 -2
- package/dist/core/runner/run-recovery-lanes.js +6 -3
- package/dist/core/runner/run-telemetry-and-budget-seats.js +11 -1
- package/dist/core/runner/write-protection-station.d.ts +7 -25
- package/dist/core/runner/write-protection-station.js +18 -0
- package/dist/core/task-registry.d.ts +0 -6
- package/dist/core/task-result.d.ts +28 -10
- package/dist/core/types.d.ts +1 -1
- package/dist/core/write-protect.d.ts +64 -12
- package/dist/core/write-protect.js +88 -34
- package/dist/engine/execution-env/node-execution-env.d.ts +19 -2
- package/dist/engine/execution-env/node-execution-env.js +5 -1
- package/dist/index.d.ts +1 -1
- package/dist/tools/fs/fs-read.js +2 -1
- package/dist/tools/fs/fs-shared.d.ts +33 -7
- package/dist/tools/fs/fs-shared.js +12 -1
- package/dist/tools/fs/safety.d.ts +37 -0
- package/dist/tools/fs/safety.js +31 -3
- package/package.json +2 -2
- package/test/export-surface.snapshot.json +3 -1
- package/wire-changes/core@7.23.0.json +316 -0
|
@@ -57,8 +57,20 @@ export const WRITE_PROTECTED_DEFAULT_TABLE = freezeTable([
|
|
|
57
57
|
{ name: ".sema.json", kind: "basename" },
|
|
58
58
|
{ name: ".sema", kind: "segment" },
|
|
59
59
|
]);
|
|
60
|
-
export function
|
|
61
|
-
|
|
60
|
+
export function foldWriteProtectSegment(s) {
|
|
61
|
+
const cased = s.toLowerCase().replace(/ı/g, "i").replace(/ſ/g, "s");
|
|
62
|
+
return stripInvisibleFormat(cased) || cased;
|
|
63
|
+
}
|
|
64
|
+
function stripInvisibleFormat(s) {
|
|
65
|
+
return s.replace(/[\u200c-\u200f\u202a-\u202e\u206a-\u206f\ufeff]/g, "");
|
|
66
|
+
}
|
|
67
|
+
function stripWin32Component(raw, normalization) {
|
|
68
|
+
const base = normalization.invisibleFirst ? stripInvisibleFormat(raw) : raw;
|
|
69
|
+
const colon = normalization.stream ? base.indexOf(":") : -1;
|
|
70
|
+
return (colon < 0 ? base : base.slice(0, colon)).replace(/[. ]+$/, "");
|
|
71
|
+
}
|
|
72
|
+
function foldWriteProtectName(name) {
|
|
73
|
+
return name.split("/").map(foldWriteProtectSegment).join("/");
|
|
62
74
|
}
|
|
63
75
|
const KINDS = ["basename", "segment", "segment-run"];
|
|
64
76
|
function describeEntryValue(v) {
|
|
@@ -107,7 +119,7 @@ export function resolveWriteProtectedTable(entries) {
|
|
|
107
119
|
const seen = new Set();
|
|
108
120
|
for (const entry of entries) {
|
|
109
121
|
const row = normalizeWriteProtectedEntry(entry);
|
|
110
|
-
const key = `${row.kind}:${
|
|
122
|
+
const key = `${row.kind}:${foldWriteProtectName(row.name)}`;
|
|
111
123
|
if (seen.has(key))
|
|
112
124
|
continue;
|
|
113
125
|
seen.add(key);
|
|
@@ -115,8 +127,9 @@ export function resolveWriteProtectedTable(entries) {
|
|
|
115
127
|
}
|
|
116
128
|
return freezeTable(out);
|
|
117
129
|
}
|
|
118
|
-
function pathViewSegments(path,
|
|
130
|
+
function pathViewSegments(path, normalization, seps = BOTH_SEPARATOR_FAMILIES, anchorFloor = 0) {
|
|
119
131
|
const segs = [];
|
|
132
|
+
const win32 = normalization !== "literal";
|
|
120
133
|
for (const raw of path.split(seps)) {
|
|
121
134
|
if (raw.length === 0)
|
|
122
135
|
continue;
|
|
@@ -131,11 +144,17 @@ function pathViewSegments(path, win32, seps = BOTH_SEPARATOR_FAMILIES, anchorFlo
|
|
|
131
144
|
segs.push("..");
|
|
132
145
|
continue;
|
|
133
146
|
}
|
|
134
|
-
const stripped =
|
|
147
|
+
const stripped = normalization === "literal" ? raw : stripWin32Component(raw, normalization);
|
|
135
148
|
segs.push(stripped.length > 0 ? stripped : raw);
|
|
136
149
|
}
|
|
137
150
|
return segs;
|
|
138
151
|
}
|
|
152
|
+
const WIN32_NORMALIZATIONS = [
|
|
153
|
+
{ invisibleFirst: false, stream: false },
|
|
154
|
+
{ invisibleFirst: false, stream: true },
|
|
155
|
+
{ invisibleFirst: true, stream: false },
|
|
156
|
+
{ invisibleFirst: true, stream: true },
|
|
157
|
+
];
|
|
139
158
|
const BOTH_SEPARATOR_FAMILIES = /[\\/]/;
|
|
140
159
|
const POSIX_SEPARATOR_FAMILY = /\//;
|
|
141
160
|
function uncAnchorSegments(path) {
|
|
@@ -149,7 +168,7 @@ function relativeUnderDataRoot(path, roots) {
|
|
|
149
168
|
if (root.win ? !isWinFormPath(path) : isWinFormPath(path) || !path.startsWith("/"))
|
|
150
169
|
continue;
|
|
151
170
|
const seps = root.win ? BOTH_SEPARATOR_FAMILIES : POSIX_SEPARATOR_FAMILY;
|
|
152
|
-
const segs = pathViewSegments(path,
|
|
171
|
+
const segs = pathViewSegments(path, "literal", seps, uncAnchorSegments(path));
|
|
153
172
|
if (segs.length < root.segments.length)
|
|
154
173
|
continue;
|
|
155
174
|
if (root.segments.every((s, i) => segs[i] === s))
|
|
@@ -165,13 +184,13 @@ export function compileWriteProtection(entries, opts) {
|
|
|
165
184
|
const dataRootPaths = dataRootSpellings(opts?.dataRoot);
|
|
166
185
|
const rootViews = dataRootPaths.map((r) => {
|
|
167
186
|
const win = isWinFormPath(r);
|
|
168
|
-
return { win, segments: pathViewSegments(r,
|
|
187
|
+
return { win, segments: pathViewSegments(r, "literal", win ? BOTH_SEPARATOR_FAMILIES : POSIX_SEPARATOR_FAMILY, uncAnchorSegments(r)) };
|
|
169
188
|
});
|
|
170
189
|
const basenames = new Map();
|
|
171
190
|
const segments = new Map();
|
|
172
191
|
const runs = [];
|
|
173
192
|
for (const row of rows) {
|
|
174
|
-
const folded =
|
|
193
|
+
const folded = foldWriteProtectName(row.name);
|
|
175
194
|
if (row.kind === "basename") {
|
|
176
195
|
if (!basenames.has(folded))
|
|
177
196
|
basenames.set(folded, row.name);
|
|
@@ -187,7 +206,7 @@ export function compileWriteProtection(entries, opts) {
|
|
|
187
206
|
const judgeSegments = (raw) => {
|
|
188
207
|
if (raw.length === 0)
|
|
189
208
|
return null;
|
|
190
|
-
const segs = raw.map(
|
|
209
|
+
const segs = raw.map(foldWriteProtectSegment);
|
|
191
210
|
const base = basenames.get(segs[segs.length - 1] ?? "");
|
|
192
211
|
if (base !== undefined)
|
|
193
212
|
return { name: base, kind: "basename" };
|
|
@@ -205,9 +224,20 @@ export function compileWriteProtection(entries, opts) {
|
|
|
205
224
|
}
|
|
206
225
|
return null;
|
|
207
226
|
};
|
|
208
|
-
const judgeViews = (spelling) =>
|
|
209
|
-
judgeSegments(pathViewSegments(spelling,
|
|
210
|
-
|
|
227
|
+
const judgeViews = (spelling) => {
|
|
228
|
+
const posix = judgeSegments(pathViewSegments(spelling, "literal", POSIX_SEPARATOR_FAMILY, uncAnchorSegments(spelling)));
|
|
229
|
+
if (posix !== null)
|
|
230
|
+
return posix;
|
|
231
|
+
const bothSeparators = judgeSegments(pathViewSegments(spelling, "literal"));
|
|
232
|
+
if (bothSeparators !== null)
|
|
233
|
+
return bothSeparators;
|
|
234
|
+
for (const normalization of WIN32_NORMALIZATIONS) {
|
|
235
|
+
const hit = judgeSegments(pathViewSegments(spelling, normalization));
|
|
236
|
+
if (hit !== null)
|
|
237
|
+
return hit;
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
};
|
|
211
241
|
const judgeSpelled = (spelled) => judgeViews(relativeUnderDataRoot(spelled, rootViews) ?? spelled);
|
|
212
242
|
const judgeSpelling = (path, cwd) => {
|
|
213
243
|
const base = typeof cwd === "string" && cwd.length > 0 ? cwd : undefined;
|
|
@@ -231,31 +261,55 @@ export function compileWriteProtection(entries, opts) {
|
|
|
231
261
|
return { kind: "clear" };
|
|
232
262
|
const family = pathFamilyOf({ root: rootPath, cwd: ctx.cwd ?? env.cwd });
|
|
233
263
|
const base = ctx.base ?? ctx.cwd ?? rootPath;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
264
|
+
let noted = false;
|
|
265
|
+
const noteAbsence = (cause, unresolvedSymlink) => {
|
|
266
|
+
if (noted)
|
|
267
|
+
return;
|
|
268
|
+
noted = true;
|
|
269
|
+
opts?.onViewAbsent?.({ cause, unresolvedSymlink });
|
|
270
|
+
};
|
|
271
|
+
let canonKey;
|
|
272
|
+
try {
|
|
273
|
+
const canon = await canonicalizeTarget(env, path, ctx.signal, family, base);
|
|
274
|
+
if (canon.ok)
|
|
275
|
+
canonKey = canon.key;
|
|
276
|
+
else
|
|
277
|
+
noteAbsence(canon.message, canon.unresolvedSymlink === true);
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
noteAbsence(err instanceof Error ? err.message : String(err), false);
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
if (canonKey !== undefined) {
|
|
284
|
+
const byTarget = judgeViews(canonKey);
|
|
285
|
+
if (byTarget !== null) {
|
|
286
|
+
for (const rootKey of await canonicalDataRootKeys(env, dataRootPaths, ctx.signal)) {
|
|
287
|
+
const relative = relativeUnderRoot(canonKey, rootKey);
|
|
288
|
+
if (relative === null)
|
|
289
|
+
continue;
|
|
290
|
+
if (relative === "")
|
|
291
|
+
return { kind: "clear" };
|
|
292
|
+
const remainder = judgeViews(relative);
|
|
293
|
+
return remainder === null ? { kind: "clear" } : { kind: "hit", hit: remainder, via: "target" };
|
|
294
|
+
}
|
|
295
|
+
return { kind: "hit", hit: byTarget, via: "target" };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
for (const hop of await symlinkChainHops(env, path, ctx.signal, family, base, (cause) => noteAbsence(cause, false))) {
|
|
299
|
+
const byHop = judgeSpelled(hop);
|
|
300
|
+
if (byHop === null)
|
|
242
301
|
continue;
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
302
|
+
if (canonKey !== undefined) {
|
|
303
|
+
for (const rootKey of await canonicalDataRootKeys(env, dataRootPaths, ctx.signal)) {
|
|
304
|
+
if (relativeUnderRoot(canonKey, rootKey) !== null)
|
|
305
|
+
return { kind: "clear" };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return { kind: "hit", hit: byHop, via: "chain" };
|
|
247
309
|
}
|
|
248
|
-
return { kind: "hit", hit: byTarget, via: "target" };
|
|
249
310
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (byHop === null)
|
|
253
|
-
continue;
|
|
254
|
-
for (const rootKey of await canonicalDataRootKeys(env, dataRootPaths, ctx.signal)) {
|
|
255
|
-
if (relativeUnderRoot(canon.key, rootKey) !== null)
|
|
256
|
-
return { kind: "clear" };
|
|
257
|
-
}
|
|
258
|
-
return { kind: "hit", hit: byHop, via: "chain" };
|
|
311
|
+
catch (err) {
|
|
312
|
+
noteAbsence(err instanceof Error ? err.message : String(err), false);
|
|
259
313
|
}
|
|
260
314
|
return { kind: "clear" };
|
|
261
315
|
},
|
|
@@ -7,6 +7,21 @@ import type { SchedulerCapability, ScheduledIntent, SchedulerContext, ScheduledT
|
|
|
7
7
|
import { SchedulerError } from "../../core/scheduler.js";
|
|
8
8
|
/** Convert user-facing timeout seconds into a positive, timer-safe millisecond delay. */
|
|
9
9
|
export declare function resolveExecTimeoutMs(timeoutSeconds: unknown): number | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Anchor a RELATIVE symlink target at its link's own directory, UNFOLDED and with exactly one separator
|
|
12
|
+
* between the two. A directory that IS a root already ends in its own separator (`/`, `C:\`, `C:/`,
|
|
13
|
+
* `\\host\share\`, all of which `dirname` answers that way), and concatenating a second one mints a
|
|
14
|
+
* DOUBLE-separator spelling for a path that has one spelling — `/tmp -> private/tmp` came back as
|
|
15
|
+
* `//private/tmp`, which the judges that walk this chain then match against nothing, because a guarded row
|
|
16
|
+
* and a containment root are both written with single separators. Joining through `path.resolve`/`path.join`
|
|
17
|
+
* would fix the separator and fold `..` with it, which is the one thing this walk may not do.
|
|
18
|
+
*
|
|
19
|
+
* `separator` is the one the caller's `dirname` writes, passed rather than read from the module so the rule
|
|
20
|
+
* is a pure function of its inputs. A backslash counts as a terminator only where it IS a separator: on a
|
|
21
|
+
* POSIX tree a directory whose name ends in one (`/a/b\`) is an ordinary name, and reading it as terminated
|
|
22
|
+
* would rename `/a/b\/t` to `/a/b\t` — the same class of rewrite as folding `..` lexically.
|
|
23
|
+
*/
|
|
24
|
+
export declare function anchorAtLinkDir(linkDir: string, target: string, separator: string): string;
|
|
10
25
|
/** review finding (win32): `where bash.exe` can resolve to `System32\bash.exe` — the
|
|
11
26
|
* WSL LAUNCHER, not a native shell. Running it drops into a Linux subsystem where Windows cwd/paths
|
|
12
27
|
* do not exist; silently adopting it is exactly the "wrong shell, false green" failure D1 forbids.
|
|
@@ -171,8 +186,10 @@ export declare class NodeExecutionEnv implements ExecutionEnv, BackgroundShellCa
|
|
|
171
186
|
* target, until a hop is not a link (`EINVAL`), is absent (`ENOENT` / `ENOTDIR`), repeats, or the bound is
|
|
172
187
|
* reached. A relative target is ANCHORED at its link's directory and never folded — `..` is the filesystem's
|
|
173
188
|
* to resolve during the next `readlink`'s own lookup (a lexical fold reads `bridge/../next` as `next` and misses
|
|
174
|
-
* every link behind `bridge`).
|
|
175
|
-
*
|
|
189
|
+
* every link behind `bridge`). Anchoring puts EXACTLY ONE separator between the directory and the target
|
|
190
|
+
* ({@link anchorAtLinkDir}); a root directory already ends in its own. One syscall per hop, no `lstat`. On a
|
|
191
|
+
* win32 host a share-shaped hop is kept and ends the walk unprobed. Any other errno is an answer nobody can
|
|
192
|
+
* use (`err`), never a shortened chain.
|
|
176
193
|
*/
|
|
177
194
|
canonicalChain(path: string): Promise<Result<SymlinkChain, FileError>>;
|
|
178
195
|
exists(path: string): Promise<Result<boolean, FileError>>;
|
|
@@ -42,6 +42,10 @@ function fileKindFromStats(stats) {
|
|
|
42
42
|
return undefined;
|
|
43
43
|
}
|
|
44
44
|
const HAS_POSIX_MODE_FACE = process.platform !== "win32";
|
|
45
|
+
export function anchorAtLinkDir(linkDir, target, separator) {
|
|
46
|
+
const endsInSeparator = linkDir.endsWith(separator) || (separator === "\\" && linkDir.endsWith("/"));
|
|
47
|
+
return endsInSeparator ? `${linkDir}${target}` : `${linkDir}${separator}${target}`;
|
|
48
|
+
}
|
|
45
49
|
const SYMLINK_CHAIN_HOP_BOUND = 64;
|
|
46
50
|
function fileInfoFromStats(path, stats) {
|
|
47
51
|
const kind = fileKindFromStats(stats);
|
|
@@ -1043,7 +1047,7 @@ export class NodeExecutionEnv {
|
|
|
1043
1047
|
return ok({ hops });
|
|
1044
1048
|
return err(toFileError(error, cur));
|
|
1045
1049
|
}
|
|
1046
|
-
const next = isAbsolute(target) ? target :
|
|
1050
|
+
const next = isAbsolute(target) ? target : anchorAtLinkDir(dirname(cur), target, sep);
|
|
1047
1051
|
if (seen.has(next))
|
|
1048
1052
|
break;
|
|
1049
1053
|
hops.push(next);
|
package/dist/index.d.ts
CHANGED
|
@@ -105,7 +105,7 @@ export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, ty
|
|
|
105
105
|
export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
|
|
106
106
|
export { READ_FACE_DEFAULT_DENY_ENTRIES, READ_FACE_BUILTIN_DENY_TABLE, READ_DENY_BUILTIN_TIERS, READ_DENY_DEFAULT_TIERS, resolveReadDenyBuiltins, compileReadDeny, type ReadDenyEntry, type ReadDenyMatcher, type NormalizedReadDenyEntry, type ReadDenyBuiltinTier, type ReadDenyBuiltinRow, type ReadDenyBuiltinConfig, } from "./tools/fs/index.js";
|
|
107
107
|
export { deploymentFaceClampNotice, resolveContainmentFace, type ContainmentFace, type ContainmentFaceInputs, type ContainmentFaceKind, type ContainmentFaceLayer, } from "./tools/fs/index.js";
|
|
108
|
-
export { WRITE_PROTECTED_DEFAULT_TABLE, resolveWriteProtectedTable, compileWriteProtection, type WriteProtectedEntry, type WriteProtectedRow, type WriteProtectedKind, type WriteProtectedHit, type WriteProtectionMatcher, type WriteProtectionVerdict, type WriteProtectionJudgeContext, type WriteProtectionCompileOptions, type WriteProtectionCheck, WRITE_PROTECTION_TARGET_VIEWS, type WriteProtectionTargetView, type WriteProtectionMount, } from "./core/write-protect.js";
|
|
108
|
+
export { WRITE_PROTECTED_DEFAULT_TABLE, resolveWriteProtectedTable, compileWriteProtection, type WriteProtectedEntry, type WriteProtectedRow, type WriteProtectedKind, type WriteProtectedHit, type WriteProtectionMatcher, type WriteProtectionVerdict, type WriteProtectionViewAbsence, type WriteProtectionJudgeContext, type WriteProtectionCompileOptions, type WriteProtectionCheck, WRITE_PROTECTION_TARGET_VIEWS, type WriteProtectionTargetView, type WriteProtectionMount, } from "./core/write-protect.js";
|
|
109
109
|
export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, assertSafeToolResultRef, buildToolResultRef, toolResultContentSegment, MAX_MINTED_TOOL_RESULT_REF_CHARS, type ToolResultProvenance, assertToolResultProvenanceMatch, normalizeToolResultProvenance, toolResultProvenanceOf, ToolResultRefConflictError, TOOL_RESULT_REF_CONFLICT_CODE, type ToolResultStore, type ToolResultSlice, type ToolResultDeletionReport, } from "./core/tool-result-store.js";
|
|
110
110
|
export { InMemoryCheckpointStore, CheckpointError, mintCheckpointId, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingTokens, winnerFromOutcome, validatePendingSteer, readPendingSteerQueue, appendPendingSteer, MAX_PENDING_STEER_CHARS, MAX_PENDING_STEER_ENTRIES, PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES, PENDING_STEER_FROZEN_FIELDS, ACTOR_ASSERTION_FROZEN_FIELDS, MAX_ACTOR_FIELD_CHARS, MAX_STEER_INPUT_ID_CHARS, type ActorAssertion, type PendingSteerEntry, type PendingSteerInput, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, type RiskDescriptor, type ProbeCause, type ProbeCauseOperands, type CheckpointStore, type CheckpointSummary, type Checkpoint, type CheckpointToken, type CheckpointGate, type CheckpointState, type SerializedCheckpointState, type CheckpointFaultMode, type PendingAction, type ResumeOutcome, type ResolvedOutcome, type ReopenReason, type ResolveExpectation, type TerminalClaimIntent, type TerminalClaimOutcome, type SafetyAxis, type RealApprovalGateBit, type ResourceLedger, type ResourceLimitReason, type PlatformLimitReason, type CheckpointRow, realApprovalOrgFact, resolveCheckpointStore, checkpointStoreExecutionUndeclared, } from "./core/checkpoint-store.js";
|
|
111
111
|
export { EXECUTION_OUTCOME_RECORD_WORDS, EXECUTION_RECORD_LEAVES_ROW_UNRECORDED, type ExecutionOutcomeRecordWord, type ExecutionRecordRowFacts, type ExecutionVerdict, ExecutionOutcomeConflictError, checkpointExecutionRecorded, executionRecordDisposition, sameExecutionRecord, executionVerdict, } from "./core/checkpoint-execution-record.js";
|
package/dist/tools/fs/fs-read.js
CHANGED
|
@@ -231,7 +231,7 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
231
231
|
if (prevNb && !prevNb.isPartialView && prevNb.hash === hash && prevNb.view && prevNb.view.start === 1 && prevNb.view.end === total) {
|
|
232
232
|
return fileUnchangedResult(`[${path}: unchanged since you last read it (lines 1-${total} of ${total}); content omitted to save context]`, "unchanged-since-last-read", 1, total, total);
|
|
233
233
|
}
|
|
234
|
-
state.set(r.key, { hash, totalLines: countLines(content), truncated: false, view: { start: 1, end: total }, lastReadAt: Date.now() });
|
|
234
|
+
state.set(r.key, { hash, totalLines: countLines(content), truncated: false, view: { start: 1, end: total }, lastReadAt: Date.now(), transmittedByToolCallId: ctx.toolCallId });
|
|
235
235
|
const bodyBlocks = rendered.blocks.length > 0 ? rendered.blocks : [{ type: "text", text: "[notebook has 0 cells]" }];
|
|
236
236
|
const nbDisclosure = discloseReminderShaped({
|
|
237
237
|
segments: rendered.blocks.filter((b) => b.type === "text").map((b) => b.text),
|
|
@@ -307,6 +307,7 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
307
307
|
...(pageMarker !== undefined ? { isPartialView: true } : {}),
|
|
308
308
|
view: { start, end },
|
|
309
309
|
lastReadAt: Date.now(),
|
|
310
|
+
transmittedByToolCallId: ctx.toolCallId,
|
|
310
311
|
});
|
|
311
312
|
if (total === 0)
|
|
312
313
|
return `${openSystemReminder(reminderMark)}Warning: the file exists but the contents are empty.</system-reminder>`;
|
|
@@ -375,9 +375,7 @@ export declare const CWD_SENTINEL = "__cc_cwd_9f2c1b__";
|
|
|
375
375
|
* TWO CALLERS, ONE RULE, which is why the marker is optional. The foreground result path KNOWS the
|
|
376
376
|
* marker it minted and passes it (exact, unambiguous). A backgrounded command's spool is assembled by a
|
|
377
377
|
* different lane entirely — the shell row outlives the run that launched it and never had the marker —
|
|
378
|
-
* so it passes none and the marker is recovered per span from the sentinel plus its hex suffix.
|
|
379
|
-
* this was shared, only the foreground path stripped, and the completion notification handed a person
|
|
380
|
-
* the raw internal marker with the directory inside it.
|
|
378
|
+
* so it passes none and the marker is recovered per span from the sentinel plus its hex suffix.
|
|
381
379
|
*/
|
|
382
380
|
export declare function stripCwdSentinel(stderr: string, marker?: string): {
|
|
383
381
|
stderr: string;
|
|
@@ -504,10 +502,38 @@ export declare function inheritSessionReadFileState(target: ReadFileState, inher
|
|
|
504
502
|
* coordinate (canonicalized the same way the Read/Edit/Write tools populate it), so a plain lookup
|
|
505
503
|
* suffices; no re-hash/re-register needed since the entry's content genuinely has not changed.
|
|
506
504
|
*/
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
505
|
+
/**
|
|
506
|
+
* The SECOND way a transmission leaves the model's view, and why this function is named for context
|
|
507
|
+
* SHRINK rather than compaction: the request-layer stale-result pass replaces an old tool result's
|
|
508
|
+
* content with a cleared marker — no summary, no re-attachment, nothing changed in the session — and
|
|
509
|
+
* for a Read result the effect on the entry is identical: the body its dedup stub points at is no
|
|
510
|
+
* longer anywhere the model can see, so the entry must retire with it.
|
|
511
|
+
*
|
|
512
|
+
* Selection is by the TRANSMISSION, not by path — an entry a LATER Read re-transmitted carries that
|
|
513
|
+
* later call's id and so survives a clear of the earlier one, which a path-keyed retirement could not
|
|
514
|
+
* tell apart. KNOWN IMPRECISION: a call id is not unique over a run (the clear ledger keys occurrences
|
|
515
|
+
* by id + message timestamp + ordinal for that reason) and the finer key is not reachable at the Read
|
|
516
|
+
* mint, which does not know the timestamp of the message its result will land in. A reused id can
|
|
517
|
+
* therefore retire one entry too many, costing a re-transmission and a tightened read-before-edit gate
|
|
518
|
+
* — the same tightening the summarized arm accepts wholesale. The converse is not reachable: an entry
|
|
519
|
+
* carries the id of the last Read that transmitted it, so if that result was cleared its id is in the list.
|
|
520
|
+
*/
|
|
521
|
+
export type ReadFileStateShrink =
|
|
522
|
+
/** A landed compaction: `attachedComplete` re-entered the context whole; `preserveKeys` stayed visible in the kept tail. */
|
|
523
|
+
{
|
|
524
|
+
kind: "summarized";
|
|
525
|
+
attachedComplete: ReadonlyArray<{
|
|
526
|
+
path: string;
|
|
527
|
+
content: string;
|
|
528
|
+
}>;
|
|
529
|
+
preserveKeys?: ReadonlyArray<string>;
|
|
530
|
+
}
|
|
531
|
+
/** A request-layer stale-result clear: these tool calls' results are markers now. Nothing re-entered. */
|
|
532
|
+
| {
|
|
533
|
+
kind: "tool-results-cleared";
|
|
534
|
+
clearedToolCallIds: ReadonlyArray<string>;
|
|
535
|
+
};
|
|
536
|
+
export declare function applyContextShrinkToReadFileState(state: ReadFileState, shrink: ReadFileStateShrink): void;
|
|
511
537
|
/** CC 2.1.198 verbatim skeleton: the ENOENT error body for a
|
|
512
538
|
* missing Read/Edit target — `File does not exist. Note: your current working directory is <cwd>.`
|
|
513
539
|
* plus a `Did you mean <sibling>?` correction when the parent directory holds a near-name (case
|
|
@@ -318,7 +318,18 @@ export async function inheritSessionReadFileState(target, inherited, judgeKey) {
|
|
|
318
318
|
target.set(judged, entry);
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
|
-
export function
|
|
321
|
+
export function applyContextShrinkToReadFileState(state, shrink) {
|
|
322
|
+
if (shrink.kind === "tool-results-cleared") {
|
|
323
|
+
if (shrink.clearedToolCallIds.length === 0)
|
|
324
|
+
return;
|
|
325
|
+
const cleared = new Set(shrink.clearedToolCallIds);
|
|
326
|
+
for (const [k, v] of [...state]) {
|
|
327
|
+
if (v.transmittedByToolCallId !== undefined && cleared.has(v.transmittedByToolCallId))
|
|
328
|
+
state.delete(k);
|
|
329
|
+
}
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const { attachedComplete, preserveKeys = [] } = shrink;
|
|
322
333
|
const preserve = new Set(preserveKeys);
|
|
323
334
|
for (const [k, v] of [...state]) {
|
|
324
335
|
if (v.seededFromContext !== true && !preserve.has(k))
|
|
@@ -77,6 +77,13 @@ export interface ReadEntry {
|
|
|
77
77
|
* disk (hash mismatch) always serves real content and the dedup disarms permanently.
|
|
78
78
|
* Optional/additive: absent = normal entry. */
|
|
79
79
|
seededFromContext?: boolean;
|
|
80
|
+
/** The id of the tool call whose RESULT carried the body this entry dedups against: when the
|
|
81
|
+
* request-layer stale-result pass clears THAT transmission, the entry retires with it
|
|
82
|
+
* (`applyContextShrinkToReadFileState`, `tool-results-cleared` arm). Recorded only by the Read
|
|
83
|
+
* lanes that TRANSMIT a body — a write-back's content is the model's own arguments and a
|
|
84
|
+
* seeded/re-attached entry never rode a tool result, so neither carries this and neither is
|
|
85
|
+
* retired by a clear. Optional/additive: absent (an older checkpoint) never matches a cleared id. */
|
|
86
|
+
transmittedByToolCallId?: string;
|
|
80
87
|
}
|
|
81
88
|
/** Per-task record of which files have been read, keyed by canonical path: owned by prepare-task,
|
|
82
89
|
* closure-captured by the tools, and serialized into the durable checkpoint. */
|
|
@@ -385,6 +392,36 @@ export declare function isShellRootedSpellingUnmapped(family: PathFamily | undef
|
|
|
385
392
|
* ⇒ `\\\\host\\share\\x`), for a lexical normalizer that recognises the backslash form only; every other spelling is
|
|
386
393
|
* returned as is. */
|
|
387
394
|
export declare function nativeUncSpellingOf(family: PathFamily | undefined, spelled: string): string;
|
|
395
|
+
/**
|
|
396
|
+
* Fold a TRAILING separator run off a spelling that is about to become a KEY: `<dir>/adir`, `<dir>/adir/`
|
|
397
|
+
* and `<dir>/adir//` name one directory and must mint one key. The existing-path arm folds them already,
|
|
398
|
+
* as a side effect of asking `realpath`; the arm that mints a key for a path that does NOT exist yet
|
|
399
|
+
* rejoined the model's own tail verbatim, so the same not-yet-created directory arrived under two and
|
|
400
|
+
* three keys — and a byte-wise reader (the per-file read state, the write gate's per-key ledger) treats
|
|
401
|
+
* two keys as two files.
|
|
402
|
+
*
|
|
403
|
+
* Called on the ANSWER, at the canonicalizer's single exit, and never on a spelling about to be probed.
|
|
404
|
+
* Two things break if it moves earlier. A trailing separator is an ASSERTION that the target is a
|
|
405
|
+
* directory and the platform enforces it (`<existing regular file>/` answers `not_directory`, reported
|
|
406
|
+
* here as a refusal), so folding before the probe spends that refusal and the slash-suffixed spelling of
|
|
407
|
+
* a file resolves to the file. And the ancestor walk chooses which ancestor to canonicalize by SPLITTING
|
|
408
|
+
* the spelling it was handed: a leaf stripped of its separator first is skipped as though it had been
|
|
409
|
+
* resolved, so `<root>/link/` — a link to a directory outside the root — comes back as the in-root
|
|
410
|
+
* spelling instead of the destination. Folding the answer costs neither.
|
|
411
|
+
*
|
|
412
|
+
* A spelling whose remainder is the bare ANCHOR keeps the separator the platform writes on a root — `/`,
|
|
413
|
+
* `C:\`, `\\host\share\`. That run IS the root's own spelling rather than a tail nobody wrote, and
|
|
414
|
+
* stripping it renames the spelling to nothing, to a DRIVE-RELATIVE location, or to a share root written
|
|
415
|
+
* unlike every other root. The question is asked in the module's own vocabulary ({@link pathIdentityOf} +
|
|
416
|
+
* {@link isIdentityDir}: is anything left BELOW the anchor?), so the three families need no arm each.
|
|
417
|
+
* Interior runs are NOT touched here: the ancestor walk folds them by probing, and a run in the middle of
|
|
418
|
+
* a name is the platform's to read.
|
|
419
|
+
*
|
|
420
|
+
* Scanned backwards rather than matched: `/[\\/]+$/` against a model-supplied spelling is quadratic when a
|
|
421
|
+
* long separator run precedes a non-separator (measured: ~5s for 32k leading slashes, on the caller's
|
|
422
|
+
* thread), and a path spelling is unbounded input.
|
|
423
|
+
*/
|
|
424
|
+
export declare function foldTrailingSeparators(spelled: string, family: PathFamily): string;
|
|
388
425
|
/**
|
|
389
426
|
* Join a RELATIVE-OR-ROOTED spelling to a base under the family, without folding (`..` across a symlinked
|
|
390
427
|
* parent is the env's to resolve). win = `path.win32` semantics: `/x`/`\x` lands on the base's drive or
|
package/dist/tools/fs/safety.js
CHANGED
|
@@ -371,11 +371,17 @@ function mintIdentity(anchor, segments, stripTail) {
|
|
|
371
371
|
out.pop();
|
|
372
372
|
continue;
|
|
373
373
|
}
|
|
374
|
-
const stripped = stripTail ? seg.
|
|
374
|
+
const stripped = stripTail ? seg.slice(0, trailingDotSpaceStart(seg)) : seg;
|
|
375
375
|
out.push(stripped.length > 0 ? stripped : seg);
|
|
376
376
|
}
|
|
377
377
|
return (out.length === 0 ? anchor || "/" : `${anchor}/${out.join("/")}`);
|
|
378
378
|
}
|
|
379
|
+
function trailingDotSpaceStart(seg) {
|
|
380
|
+
let end = seg.length;
|
|
381
|
+
while (end > 0 && (seg[end - 1] === "." || seg[end - 1] === " "))
|
|
382
|
+
end -= 1;
|
|
383
|
+
return end;
|
|
384
|
+
}
|
|
379
385
|
function identityAnchorOf(identity) {
|
|
380
386
|
return identity.startsWith("//") ? identity.split("/").slice(0, 4).join("/") : identity.slice(0, 2);
|
|
381
387
|
}
|
|
@@ -403,6 +409,18 @@ export function isShellRootedSpellingUnmapped(family, spelled) {
|
|
|
403
409
|
export function nativeUncSpellingOf(family, spelled) {
|
|
404
410
|
return family === "win" && isUncSpelling(spelled) ? `\\\\${spelled.slice(2).replace(/\//g, "\\")}` : spelled;
|
|
405
411
|
}
|
|
412
|
+
export function foldTrailingSeparators(spelled, family) {
|
|
413
|
+
let end = spelled.length;
|
|
414
|
+
while (end > 0 && (spelled[end - 1] === "/" || (family === "win" && spelled[end - 1] === "\\")))
|
|
415
|
+
end -= 1;
|
|
416
|
+
if (end === spelled.length)
|
|
417
|
+
return spelled;
|
|
418
|
+
const stripped = spelled.slice(0, end);
|
|
419
|
+
const identity = pathIdentityOf(stripped, family);
|
|
420
|
+
if (!identity.ok || !isIdentityDir(identity.identity, family))
|
|
421
|
+
return spelled;
|
|
422
|
+
return stripped;
|
|
423
|
+
}
|
|
406
424
|
export function joinForFamily(family, base, rel) {
|
|
407
425
|
if (family === "posix")
|
|
408
426
|
return `${base.replace(/\/+$/, "")}/${rel}`;
|
|
@@ -500,7 +518,8 @@ export async function canonicalizeTarget(env, path, signal, family, baseCwd) {
|
|
|
500
518
|
throw e;
|
|
501
519
|
});
|
|
502
520
|
try {
|
|
503
|
-
|
|
521
|
+
const answer = await withGateTimeout(probe, DEFAULT_GATE_AWAIT_TIMEOUT_MS, signal, "pathCanonicalization");
|
|
522
|
+
return answer.ok ? { ...answer, key: foldTrailingSeparators(answer.key, family) } : answer;
|
|
504
523
|
}
|
|
505
524
|
catch (err) {
|
|
506
525
|
if (seenThrow && err === thrownByProbe)
|
|
@@ -546,7 +565,16 @@ async function probeRealTarget(env, path, target, family, signal) {
|
|
|
546
565
|
if (!link.ok) {
|
|
547
566
|
return { ok: false, message: `cannot read symlink "${path}": ${link.error.message}`, unresolvedSymlink: true };
|
|
548
567
|
}
|
|
549
|
-
const
|
|
568
|
+
const screenedTarget = win32NamespaceScreen(link.value, family);
|
|
569
|
+
if (!screenedTarget.ok) {
|
|
570
|
+
return {
|
|
571
|
+
ok: false,
|
|
572
|
+
message: `cannot resolve symlink "${path}": its target "${clipSpelling(link.value)}" ${WIN32_NAMESPACE_REFUSAL_TEXT[screenedTarget.refuse]}; refused (${screenedTarget.refuse}).`,
|
|
573
|
+
refused: screenedTarget.refuse,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
const hopSpelling = family === "win" ? screenedTarget.spelled : link.value;
|
|
577
|
+
const linkTarget = isAbsoluteForFamily(family, hopSpelling) ? hopSpelling : joinForFamily(family, parentDir(abs), hopSpelling);
|
|
550
578
|
const reabs = await env.absolutePath(linkTarget, signal);
|
|
551
579
|
if (!reabs.ok) {
|
|
552
580
|
return { ok: false, message: `cannot resolve symlink target "${linkTarget}" for "${path}": ${reabs.error.message}`, unresolvedSymlink: true };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/core",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.23.0",
|
|
4
4
|
"description": "Stateless, task-oriented AI agent core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "rm -rf dist && tsc -p tsconfig.build.json && tsc -p tsconfig.build.json --emitDeclarationOnly --removeComments false",
|
|
51
|
-
"typecheck": "tsc --noEmit",
|
|
51
|
+
"typecheck": "tsc --noEmit && echo \"typecheck OK (tsc --noEmit clean)\"",
|
|
52
52
|
"pretest": "npm run gate:debts",
|
|
53
53
|
"test": "vitest run --exclude \"**/*.local.test.ts\"",
|
|
54
54
|
"test:watch": "vitest",
|
|
@@ -1,7 +1,7 @@
|
|
|
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
3
|
"_tierComment": "#435 v1 — machine-readable layering of the public surface: stable = demonstrated by README.md / src/examples; internal = an `Internal`-marked name or a runner/engine deep-subtree declaration (the model seam src/engine/llm is excluded — it is the BYOM contract, not an engine internal); advanced = a supported export the front door does not walk you through. THIS IS AN INITIAL HEURISTIC, derived mechanically and expected to be refined ticket by ticket: no human reviewed these 1700+ entries one by one, and nothing here claims otherwise. Known bias: a short or English-word export name (ok, err, Result, Usage) can match ordinary prose in README.md and land `stable` on a coincidence. Every export MUST carry a tier — a new export with no row fails the gate in export-surface.test.ts.",
|
|
4
|
-
"count":
|
|
4
|
+
"count": 2395,
|
|
5
5
|
"exports": {
|
|
6
6
|
"A2ATaskState": "type",
|
|
7
7
|
"A2ATaskStateReversal": "type",
|
|
@@ -1615,6 +1615,7 @@
|
|
|
1615
1615
|
"WriteProtectionMount": "interface",
|
|
1616
1616
|
"WriteProtectionTargetView": "type",
|
|
1617
1617
|
"WriteProtectionVerdict": "type",
|
|
1618
|
+
"WriteProtectionViewAbsence": "interface",
|
|
1618
1619
|
"WriteReceipt": "interface",
|
|
1619
1620
|
"XARGS_READ_ONLY_TARGETS": "variable",
|
|
1620
1621
|
"ackAdoptionConfig": "function",
|
|
@@ -4011,6 +4012,7 @@
|
|
|
4011
4012
|
"WriteProtectionMount": "advanced",
|
|
4012
4013
|
"WriteProtectionTargetView": "advanced",
|
|
4013
4014
|
"WriteProtectionVerdict": "advanced",
|
|
4015
|
+
"WriteProtectionViewAbsence": "advanced",
|
|
4014
4016
|
"WriteReceipt": "internal",
|
|
4015
4017
|
"XARGS_READ_ONLY_TARGETS": "advanced",
|
|
4016
4018
|
"ackAdoptionConfig": "advanced",
|