@sema-agent/core 7.16.0 → 7.17.1
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 +209 -0
- package/dist/agents/builtin-agents.d.ts +3 -3
- package/dist/agents/cross-session-envelope.d.ts +0 -1
- package/dist/agents/cross-session-envelope.js +0 -1
- package/dist/agents/peer-notices.d.ts +0 -9
- package/dist/agents/peer-notices.js +0 -63
- package/dist/agents/subagent.js +7 -15
- package/dist/brain/request-params.d.ts +2 -2
- package/dist/config/defaults.d.ts +0 -7
- package/dist/config/defaults.js +0 -2
- package/dist/core/artifact-host.d.ts +218 -0
- package/dist/core/artifact-host.js +92 -0
- package/dist/core/ask-origin.d.ts +29 -3
- package/dist/core/ask-origin.js +17 -9
- package/dist/core/ask-question.d.ts +25 -0
- package/dist/core/ask-question.js +4 -0
- package/dist/core/ask-unresolvable-notice.d.ts +30 -21
- package/dist/core/ask-unresolvable-notice.js +16 -8
- package/dist/core/auto-compaction.d.ts +10 -10
- package/dist/core/auto-mode-arming.d.ts +2 -2
- package/dist/core/background-agent-store.d.ts +5 -5
- package/dist/core/background-shell.d.ts +1 -1
- package/dist/core/checkpoint-execution-record.d.ts +0 -3
- package/dist/core/checkpoint-execution-record.js +0 -4
- package/dist/core/checkpoint-store.d.ts +71 -99
- package/dist/core/checkpoint-store.js +9 -16
- package/dist/core/context-edit.d.ts +36 -104
- package/dist/core/context-edit.js +37 -60
- package/dist/core/effective-path-target.d.ts +68 -8
- package/dist/core/effective-path-target.js +42 -30
- package/dist/core/engine-notice.d.ts +46 -32
- package/dist/core/env-budget.d.ts +31 -0
- package/dist/core/env-budget.js +39 -0
- package/dist/core/exec-output-tail.d.ts +5 -5
- package/dist/core/file-history-store.js +3 -6
- package/dist/core/fs-write-gate-policy.d.ts +1 -1
- package/dist/core/fs-write-gate-policy.js +3 -3
- package/dist/core/gate-lanes.js +9 -15
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hands-band.d.ts +1 -1
- package/dist/core/hooks.d.ts +18 -17
- package/dist/core/lsp-session.d.ts +1 -1
- package/dist/core/lsp.d.ts +1 -1
- package/dist/core/lsp.js +5 -4
- package/dist/core/mcp.d.ts +5 -5
- package/dist/core/mcp.js +4 -48
- package/dist/core/memory-engine/engine.js +2 -1
- package/dist/core/memory-engine/layout.d.ts +18 -6
- package/dist/core/memory-engine/layout.js +40 -21
- package/dist/core/memory-recall.d.ts +0 -8
- package/dist/core/memory.d.ts +1 -1
- package/dist/core/oracle-isolation.js +2 -2
- package/dist/core/parent-spec-seat.d.ts +66 -0
- package/dist/core/parent-spec-seat.js +47 -0
- package/dist/core/permission-rule-model.d.ts +11 -6
- package/dist/core/permission-rule-model.js +53 -37
- package/dist/core/permission-rules.js +8 -3
- package/dist/core/physical-path.d.ts +37 -0
- package/dist/core/physical-path.js +30 -0
- package/dist/core/protocol-naming.d.ts +2 -2
- package/dist/core/remote-env.d.ts +1 -1
- package/dist/core/retired-keys.d.ts +19 -0
- package/dist/core/retired-keys.js +15 -0
- package/dist/core/runner/active-skill-scope.js +3 -3
- package/dist/core/runner/assemble-result.d.ts +15 -13
- package/dist/core/runner/assemble-result.js +5 -4
- package/dist/core/runner/compaction-call-options.d.ts +25 -89
- package/dist/core/runner/contracts.d.ts +11 -10
- package/dist/core/runner/gate-exit.js +15 -1
- package/dist/core/runner/halt-attribution.d.ts +35 -0
- package/dist/core/runner/halt-attribution.js +16 -0
- package/dist/core/runner/memory-consolidation.d.ts +0 -2
- package/dist/core/runner/memory-consolidation.js +0 -1
- package/dist/core/runner/prepare-artifact.d.ts +52 -0
- package/dist/core/runner/prepare-artifact.js +63 -0
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
- package/dist/core/runner/prepare-caps-and-workflow.js +2 -3
- package/dist/core/runner/prepare-config-doors.d.ts +3 -3
- package/dist/core/runner/prepare-config-doors.js +5 -2
- package/dist/core/runner/prepare-context-lane.d.ts +2 -2
- package/dist/core/runner/prepare-context-lane.js +2 -13
- package/dist/core/runner/prepare-file-history.d.ts +1 -1
- package/dist/core/runner/prepare-file-history.js +3 -52
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -7
- package/dist/core/runner/prepare-hands-readface.js +3 -2
- package/dist/core/runner/prepare-inherited-gate.d.ts +5 -2
- package/dist/core/runner/prepare-inherited-gate.js +7 -8
- package/dist/core/runner/prepare-memory.d.ts +4 -6
- package/dist/core/runner/prepare-memory.js +4 -2
- package/dist/core/runner/prepare-policy-chain.d.ts +3 -3
- package/dist/core/runner/prepare-policy-chain.js +15 -30
- package/dist/core/runner/prepare-prompt-assembly.js +1 -1
- package/dist/core/runner/prepare-question-face.js +2 -1
- package/dist/core/runner/prepare-safety-scan.d.ts +1 -1
- package/dist/core/runner/prepare-suspend-saga.js +0 -2
- package/dist/core/runner/prepare-task.js +12 -11
- package/dist/core/runner/prepare-wiring-manifest.js +9 -7
- package/dist/core/runner/resume-admission.js +2 -1
- package/dist/core/runner/resume-internals-and-config.js +4 -0
- package/dist/core/runner/retired-runner-deps.d.ts +3 -0
- package/dist/core/runner/retired-runner-deps.js +7 -0
- package/dist/core/runner/run-harness-handlers.js +5 -3
- package/dist/core/runner/run-settle-and-teardown.d.ts +1 -1
- package/dist/core/runner/run-settle-and-teardown.js +3 -1
- package/dist/core/runner/session-file-state-replay.js +3 -3
- package/dist/core/runner/session-rule-policy.js +3 -4
- package/dist/core/runner/stream-halt-verbs.js +1 -1
- package/dist/core/runner/stream-steer-verb.js +1 -4
- package/dist/core/runner/workspace-path.js +3 -0
- package/dist/core/runner-deps.d.ts +30 -49
- package/dist/core/safe-notify.d.ts +1 -1
- package/dist/core/sensitive-path-policy.d.ts +1 -1
- package/dist/core/sensitive-path-policy.js +11 -12
- package/dist/core/session.d.ts +2 -2
- package/dist/core/skill-tool-specifier.d.ts +1 -1
- package/dist/core/skills-directory.d.ts +1 -1
- package/dist/core/skills-directory.js +4 -3
- package/dist/core/spec-contract.js +5 -4
- package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
- package/dist/core/strategy-store.d.ts +8 -11
- package/dist/core/task-event.d.ts +20 -17
- package/dist/core/task-limits.d.ts +1 -1
- package/dist/core/task-notification.d.ts +5 -5
- package/dist/core/task-registry-agent.d.ts +5 -5
- package/dist/core/task-registry-monitor.d.ts +2 -2
- package/dist/core/task-registry-shared.d.ts +32 -28
- package/dist/core/task-registry-shared.js +4 -13
- package/dist/core/task-registry.d.ts +8 -8
- package/dist/core/task-result.d.ts +37 -37
- package/dist/core/task-spec.d.ts +5 -5
- package/dist/core/task-stream.d.ts +16 -11
- package/dist/core/tool-catalog-entries.d.ts +1 -0
- package/dist/core/tool-catalog-entries.js +6 -1
- package/dist/core/tool-conformance.d.ts +0 -2
- package/dist/core/tool-conformance.js +1 -3
- package/dist/core/tool-errors.d.ts +5 -0
- package/dist/core/tool-errors.js +1 -0
- package/dist/core/tool-policy.d.ts +66 -22
- package/dist/core/tool-policy.js +31 -5
- package/dist/core/tool-result-budget.d.ts +1 -1
- package/dist/core/tool-result-store.d.ts +11 -11
- package/dist/core/tool-spec.d.ts +34 -38
- package/dist/core/tools.d.ts +2 -2
- package/dist/core/trace.d.ts +12 -15
- package/dist/core/untrusted-text.d.ts +1 -1
- package/dist/core/wiring-manifest.d.ts +4 -3
- package/dist/core/workflow-journal-store.d.ts +11 -11
- package/dist/engine/execution-env/node-execution-env.d.ts +3 -2
- package/dist/engine/execution-env/node-execution-env.js +8 -5
- package/dist/engine/harness/agent-harness.d.ts +47 -4
- package/dist/engine/harness/agent-harness.js +52 -9
- package/dist/engine/loop/agent-loop.js +15 -8
- package/dist/engine/loop/types.d.ts +5 -0
- package/dist/index.d.ts +20 -14
- package/dist/index.js +16 -10
- package/dist/internal/harness.d.ts +1 -0
- package/dist/orchestration/run-spec.d.ts +3 -0
- package/dist/orchestration/run-spec.js +3 -3
- package/dist/orchestration/run-workflow-tool.d.ts +12 -6
- package/dist/orchestration/run-workflow-tool.js +23 -5
- package/dist/orchestration/workflow-primitives.d.ts +4 -2
- package/dist/orchestration/workflow-primitives.js +1 -4
- package/dist/orchestration/workflow-script-store.js +9 -25
- package/dist/orchestration/workflow-types.d.ts +34 -4
- package/dist/orchestration/workflow-types.js +2 -0
- package/dist/orchestration/workflow.d.ts +39 -7
- package/dist/orchestration/workflow.js +237 -113
- package/dist/prompt-assembly/assemble.d.ts +2 -2
- package/dist/prompt-assembly/assemble.js +9 -9
- package/dist/prompt-assembly/composer.d.ts +6 -7
- package/dist/prompt-assembly/composer.js +10 -10
- package/dist/prompt-assembly/epoch.js +1 -1
- package/dist/prompt-assembly/event-registry.d.ts +0 -21
- package/dist/prompt-assembly/packs/sema-default.js +30 -30
- package/dist/prompt-assembly/types.d.ts +14 -14
- package/dist/prompts/coordinator.d.ts +0 -13
- package/dist/prompts/coordinator.js +0 -3
- package/dist/stores/cc/task-list-store.js +2 -10
- package/dist/stores/file/checkpoint-store.js +1 -5
- package/dist/stores/file/file-history-store.js +2 -2
- package/dist/stores/file/fs-atomic.d.ts +15 -18
- package/dist/stores/file/fs-atomic.js +4 -14
- package/dist/stores/file/mailbox-store.d.ts +7 -11
- package/dist/stores/file/mailbox-store.js +4 -11
- package/dist/stores/file/permission-rule-store.d.ts +4 -35
- package/dist/stores/file/permission-rule-store.js +4 -148
- package/dist/tools/artifact/artifact-text.d.ts +62 -0
- package/dist/tools/artifact/artifact-text.js +68 -0
- package/dist/tools/artifact/artifact-tool.d.ts +52 -0
- package/dist/tools/artifact/artifact-tool.js +351 -0
- package/dist/tools/artifact/local-stub.d.ts +26 -0
- package/dist/tools/artifact/local-stub.js +297 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +90 -8
- package/dist/tools/fs/bash-readonly-classifier.js +294 -70
- package/dist/tools/fs/fs-bash.d.ts +14 -10
- package/dist/tools/fs/fs-bash.js +50 -33
- package/dist/tools/fs/fs-search-tools.js +5 -2
- package/dist/tools/fs/fs-shared.d.ts +2 -0
- package/dist/tools/fs/fs-shared.js +3 -11
- package/dist/tools/fs/index.d.ts +4 -0
- package/dist/tools/fs/index.js +4 -1
- package/dist/tools/fs/safety.d.ts +117 -1
- package/dist/tools/fs/safety.js +131 -31
- package/dist/tools/fs/search.js +8 -6
- package/package.json +2 -1
- package/test/export-surface.snapshot.json +119 -39
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -11,7 +11,7 @@ import { isRemoteExecutionEnv, hasDestroy, isIsolated } from "../../core/remote-
|
|
|
11
11
|
import { ghRateLimitHint } from "./gh-rate-limit.js";
|
|
12
12
|
import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, BASH_READONLY_CONFINEMENT_NOTE, } from "./fs-shared.js";
|
|
13
13
|
import { PROBE_CAUSE_PATH_MAX, inlineUntrusted } from "../../core/untrusted-text.js";
|
|
14
|
-
import { BASH_CLASSIFY_DEFAULT_ALLOW, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck,
|
|
14
|
+
import { BASH_CLASSIFY_DEFAULT_ALLOW, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoopDetailed, classifyCompoundReadonlyDetailed, classifyOutOfRootReadGate, refusedCommandMayRead, classifySimpleCommandReadBoundary, NOT_AUTO_ALLOWED, } from "./bash-readonly-classifier.js";
|
|
15
15
|
import { toolFace } from "../../core/tool-catalog-entries.js";
|
|
16
16
|
const RECURSIVE_CAUSE_MAX_PATHS = 3;
|
|
17
17
|
const RECURSIVE_READ_CAUSE_CODE = "shell.recursive_read_unbounded";
|
|
@@ -36,7 +36,7 @@ export function bashReversibilityProbe(allow, boundary) {
|
|
|
36
36
|
if (backgrounded)
|
|
37
37
|
return { reversible: false, ...boundaryGate(detailed) };
|
|
38
38
|
if (detailed.reason === undefined) {
|
|
39
|
-
if (detailed
|
|
39
|
+
if (boundaryUnread(detailed)) {
|
|
40
40
|
const cause = recursiveReadCause(detailed);
|
|
41
41
|
if (cause !== undefined)
|
|
42
42
|
return { reversible: false, ...boundaryGate(detailed), cause };
|
|
@@ -44,23 +44,19 @@ export function bashReversibilityProbe(allow, boundary) {
|
|
|
44
44
|
}
|
|
45
45
|
return { reversible: true };
|
|
46
46
|
}
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
47
|
+
const loop = classifyBoundedReadonlyPollLoopDetailed(command, allowSet, resolved);
|
|
48
|
+
if (loop.reason === undefined)
|
|
49
49
|
return { reversible: true };
|
|
50
|
-
|
|
50
|
+
if (boundaryDeclared(loop) || unjudgedRead(command, allowSet, resolved, loop))
|
|
51
|
+
return { reversible: false, mandated: true };
|
|
52
|
+
return { reversible: false, ...boundaryGate(detailed) };
|
|
51
53
|
};
|
|
52
54
|
}
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
return "shape";
|
|
56
|
-
const { denyMatch: _deny, ...withoutDenyJudge } = resolved;
|
|
57
|
-
const opened = face({ ...withoutDenyJudge, face: "open" });
|
|
58
|
-
if (opened === undefined || opened !== fullRefusal)
|
|
59
|
-
return "declared";
|
|
60
|
-
return face(undefined) === opened ? "shape" : "unread";
|
|
55
|
+
function boundaryUnread(verdict) {
|
|
56
|
+
return boundaryUndecided(verdict) || boundaryUnjudged(verdict);
|
|
61
57
|
}
|
|
62
|
-
function
|
|
63
|
-
return
|
|
58
|
+
function boundaryUndecided(verdict) {
|
|
59
|
+
return verdict.undecidedPaths !== undefined && verdict.undecidedPaths.length > 0;
|
|
64
60
|
}
|
|
65
61
|
function boundaryDeclared(verdict) {
|
|
66
62
|
return denyJudgeSpoke(verdict) || verdict.outOfRootRead === true;
|
|
@@ -76,9 +72,23 @@ function isBackgroundShellCall(args) {
|
|
|
76
72
|
return args?.run_in_background === true;
|
|
77
73
|
}
|
|
78
74
|
function readBoundaryMandate(command, allowSet, resolved, verdict) {
|
|
79
|
-
if (
|
|
75
|
+
if (resolved === undefined)
|
|
76
|
+
return {};
|
|
77
|
+
if (denyJudgeSpoke(verdict) || unjudgedRead(command, allowSet, resolved, verdict))
|
|
80
78
|
return { mandated: true };
|
|
81
|
-
return
|
|
79
|
+
return classifyOutOfRootReadGate(command, allowSet, resolved).gated ? { mandated: true } : {};
|
|
80
|
+
}
|
|
81
|
+
function boundaryUnjudged(verdict) {
|
|
82
|
+
return verdict.refusedWhole === true || (verdict.unresolvedOperands !== undefined && verdict.unresolvedOperands.length > 0);
|
|
83
|
+
}
|
|
84
|
+
function unjudgedRead(command, allowSet, resolved, verdict) {
|
|
85
|
+
if (verdict.unresolvedOperands !== undefined && verdict.unresolvedOperands.length > 0)
|
|
86
|
+
return true;
|
|
87
|
+
if (verdict.refusedWhole !== true)
|
|
88
|
+
return false;
|
|
89
|
+
if (refusedCommandMayRead(command, allowSet))
|
|
90
|
+
return true;
|
|
91
|
+
return resolved !== undefined && classifyOutOfRootReadGate(command, allowSet, resolved).gated;
|
|
82
92
|
}
|
|
83
93
|
function recursiveReadCause(detailed) {
|
|
84
94
|
const recursive = detailed.recursiveReadPaths;
|
|
@@ -101,24 +111,31 @@ export function bashReadBoundaryProbe(boundary) {
|
|
|
101
111
|
const resolved = typeof boundary === "function" ? boundary() : boundary;
|
|
102
112
|
if (resolved === undefined)
|
|
103
113
|
return { reversible: true };
|
|
114
|
+
const seatVerdict = (verdict) => {
|
|
115
|
+
if (boundaryDeclared(verdict) || unjudgedRead(command, allowSet, resolved, verdict)) {
|
|
116
|
+
const cause = recursiveReadCause(verdict);
|
|
117
|
+
return { reversible: false, mandated: true, ...(cause !== undefined ? { cause } : {}) };
|
|
118
|
+
}
|
|
119
|
+
if (boundaryUndecided(verdict))
|
|
120
|
+
return { reversible: false };
|
|
121
|
+
if (verdict.reason === undefined)
|
|
122
|
+
return { reversible: true };
|
|
123
|
+
return undefined;
|
|
124
|
+
};
|
|
104
125
|
const detailed = classifyCompoundReadonlyDetailed(command, allowSet, resolved);
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
126
|
+
if (detailed.reason === undefined)
|
|
127
|
+
return seatVerdict(detailed) ?? { reversible: true };
|
|
128
|
+
const loop = classifyBoundedReadonlyPollLoopDetailed(command, allowSet, resolved);
|
|
129
|
+
if (loop.reason === undefined)
|
|
130
|
+
return seatVerdict(loop) ?? { reversible: true };
|
|
131
|
+
const compoundVerdict = seatVerdict(detailed);
|
|
132
|
+
const loopVerdict = seatVerdict(loop);
|
|
133
|
+
const mandated = [compoundVerdict, loopVerdict].find((verdict) => verdict?.mandated === true);
|
|
134
|
+
if (mandated !== undefined)
|
|
135
|
+
return mandated;
|
|
136
|
+
if (readBoundaryMandate(command, allowSet, resolved, detailed).mandated === true)
|
|
114
137
|
return { reversible: false, mandated: true };
|
|
115
|
-
|
|
116
|
-
return { reversible: false };
|
|
117
|
-
const loopRefusal = classifyBoundedReadonlyPollLoop(command, allowSet, resolved);
|
|
118
|
-
if (loopRefusal === undefined)
|
|
119
|
-
return { reversible: true };
|
|
120
|
-
const loop = pollLoopBoundaryVerdict(command, allowSet, resolved, loopRefusal);
|
|
121
|
-
return loop === "shape" ? { reversible: true } : loop === "declared" ? { reversible: false, mandated: true } : { reversible: false };
|
|
138
|
+
return compoundVerdict ?? loopVerdict ?? { reversible: true };
|
|
122
139
|
};
|
|
123
140
|
}
|
|
124
141
|
export { FULL_SHELL_CONTRACT_ID } from "../../core/tool-catalog-entries.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { defineTool, errorResult } from "../../core/tools.js";
|
|
3
|
-
import { resolveKey, violationText, violationDetails } from "./safety.js";
|
|
3
|
+
import { clipSpelling, pathFamilyOf, resolveKey, violationText, violationDetails, WIN32_NAMESPACE_REFUSAL_TEXT } from "./safety.js";
|
|
4
4
|
import { runGrepDetailed, runGlobDetailed, invalidGlobTokens } from "./search.js";
|
|
5
5
|
import { toolFace } from "../../core/tool-catalog-entries.js";
|
|
6
6
|
import { pathTargetBaseOf, splitAbsoluteGlobPattern } from "../../core/effective-path-target.js";
|
|
@@ -205,7 +205,10 @@ export function createGlobTool(env, rootCanonical, additionalRoots, readDeny, re
|
|
|
205
205
|
let path = declaredTarget === undefined ? undefined : pathTargetValue(args, declaredTarget);
|
|
206
206
|
const { max_results } = args;
|
|
207
207
|
if (path === undefined) {
|
|
208
|
-
const split = splitAbsoluteGlobPattern(pattern);
|
|
208
|
+
const split = splitAbsoluteGlobPattern(pattern, pathFamilyOf({ root: rootCanonical }));
|
|
209
|
+
if (split !== undefined && "refused" in split) {
|
|
210
|
+
return errorResult(`Error (Glob): pattern "${clipSpelling(pattern)}" ${WIN32_NAMESPACE_REFUSAL_TEXT[split.refused]}; refused (${split.refused}).`);
|
|
211
|
+
}
|
|
209
212
|
if (split) {
|
|
210
213
|
path = split.baseDir;
|
|
211
214
|
pattern = split.relativePattern;
|
|
@@ -273,6 +273,8 @@ export declare function bashTimeoutCapsSec(caps: {
|
|
|
273
273
|
defaultSec: number;
|
|
274
274
|
maxSec: number;
|
|
275
275
|
};
|
|
276
|
+
/** Read per call (a deployment steers it without a restart); the one shared resolver holds the grammar,
|
|
277
|
+
* the `[OUTPUT_BUDGET_MIN_CHARS, ceiling]` range and the loud outlet for a value it could not honor. */
|
|
276
278
|
export declare function bashMaxOutputChars(): number;
|
|
277
279
|
export declare const FILE_PATH_PARAMS: {
|
|
278
280
|
file_path: Type.TString;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
-
import { clipWithFilePointer } from "../../core/tool-errors.js";
|
|
2
|
+
import { clipWithFilePointer, OUTPUT_BUDGET_MIN_CHARS } from "../../core/tool-errors.js";
|
|
3
3
|
import { normalizeAbsPathLexically, sha256, similarNameSuggestion, OVERSIZE_READ_ESCAPE_HINT, PARTIAL_VIEW_READ_ESCAPE_HINT, } from "./safety.js";
|
|
4
4
|
import { decodeTextBytes, normalizeFileText } from "./encoding.js";
|
|
5
5
|
import { shellQuote } from "./search.js";
|
|
6
6
|
import { isNotebookPath } from "./notebook.js";
|
|
7
7
|
import { sharpImageDownsampler } from "../../core/mcp.js";
|
|
8
8
|
import { deliverEngineNotice } from "../../core/types.js";
|
|
9
|
-
import {
|
|
9
|
+
import { resolveEnvBudget } from "../../core/env-budget.js";
|
|
10
10
|
import { openSystemReminder } from "../../core/reminder-mint.js";
|
|
11
11
|
export const MAX_READ_BYTES = 256 * 1024;
|
|
12
12
|
export const SLICED_READ_MAX_BYTES = 64 * 1024 * 1024;
|
|
@@ -182,15 +182,7 @@ export function bashTimeoutCapsSec(caps) {
|
|
|
182
182
|
const BASH_DEFAULT_MAX_OUTPUT_CHARS = 30_000;
|
|
183
183
|
const BASH_MAX_OUTPUT_CHARS_CEILING = 150_000;
|
|
184
184
|
export function bashMaxOutputChars() {
|
|
185
|
-
|
|
186
|
-
if (rawEnv === undefined || rawEnv.trim() === "")
|
|
187
|
-
return BASH_DEFAULT_MAX_OUTPUT_CHARS;
|
|
188
|
-
const raw = Number(rawEnv);
|
|
189
|
-
if (!Number.isInteger(raw) || raw <= 0) {
|
|
190
|
-
announceEnvKnob(`BASH_MAX_OUTPUT_LENGTH=${rawEnv} was ignored — it is not a positive whole number of characters. Using ${BASH_DEFAULT_MAX_OUTPUT_CHARS} instead.`);
|
|
191
|
-
return BASH_DEFAULT_MAX_OUTPUT_CHARS;
|
|
192
|
-
}
|
|
193
|
-
return Math.min(raw, BASH_MAX_OUTPUT_CHARS_CEILING);
|
|
185
|
+
return resolveEnvBudget(process.env.BASH_MAX_OUTPUT_LENGTH, { name: "BASH_MAX_OUTPUT_LENGTH", unit: "characters", default: BASH_DEFAULT_MAX_OUTPUT_CHARS, min: OUTPUT_BUDGET_MIN_CHARS, max: BASH_MAX_OUTPUT_CHARS_CEILING });
|
|
194
186
|
}
|
|
195
187
|
export const FILE_PATH_PARAMS = {
|
|
196
188
|
file_path: Type.String({ description: "File path (within the configured root)." }),
|
package/dist/tools/fs/index.d.ts
CHANGED
|
@@ -179,6 +179,10 @@ export interface HandsToolkitOptions {
|
|
|
179
179
|
* is off anyway when `reminderMark` is absent). */
|
|
180
180
|
reminderDisclosureCounts?: import("../../core/reminder-disclosure.js").ReminderDisclosureCounts;
|
|
181
181
|
}
|
|
182
|
+
/** Whether the band's WRITE half (Edit / Write / NotebookEdit, and the full `bash`) mounts for these options — the ONE
|
|
183
|
+
* predicate {@link createHandsToolkit} branches on and prepare-task plans by (the memory phase must know, before the
|
|
184
|
+
* band exists, whether the instruction-taught write channel will be there). `readOnly` is the verifier boundary. */
|
|
185
|
+
export declare function handsWriteHalfMounts(opts: Pick<HandsToolkitOptions, "readOnly">): boolean;
|
|
182
186
|
/**
|
|
183
187
|
* Build the per-task hand tool band over an injected env + fresh per-task read state.
|
|
184
188
|
* `rootCanonical` is the already-canonicalized containment root (prepare-task resolves it once). Normal
|
package/dist/tools/fs/index.js
CHANGED
|
@@ -21,6 +21,9 @@ import { createReadFileTool } from "./fs-read.js";
|
|
|
21
21
|
import { createEditFileTool, createWriteFileTool, createNotebookEditTool } from "./fs-write.js";
|
|
22
22
|
import { createGrepTool, createGlobTool } from "./fs-search-tools.js";
|
|
23
23
|
import { createBashTool, createBashReadonlyTool, createEnvTaskOutputTool, createEnvTaskStopTool } from "./fs-bash.js";
|
|
24
|
+
export function handsWriteHalfMounts(opts) {
|
|
25
|
+
return opts.readOnly !== true;
|
|
26
|
+
}
|
|
24
27
|
export function createHandsToolkit(env, readFileState, rootCanonical, opts = {}) {
|
|
25
28
|
const { includeShell = false, readOnly = false, bashReadonlyAllow, commitCoAuthor = false, mountBackgroundTaskTools = true, additionalRoots, additionalReadRoots, } = opts;
|
|
26
29
|
const readFaceRoots = additionalReadRoots === undefined || additionalReadRoots.length === 0
|
|
@@ -49,7 +52,7 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
|
|
|
49
52
|
const tools = [
|
|
50
53
|
createReadFileTool(env, readFileState, rootCanonical, readOnly ? undefined : cwdRef, readFaceRoots, opts.readImageDownsampler, opts.pdfModelCapabilities, bgOutputReadExemption, opts.readCyberReminder, readDeny, readFace, opts.reminderMark, opts.reminderDisclosureCounts),
|
|
51
54
|
];
|
|
52
|
-
if (
|
|
55
|
+
if (handsWriteHalfMounts({ readOnly })) {
|
|
53
56
|
tools.push(createEditFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit, opts.onFileEdited), createWriteFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit, opts.onFileEdited), createNotebookEditTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit, opts.onFileEdited));
|
|
54
57
|
}
|
|
55
58
|
tools.push(createGrepTool(env, rootCanonical, readFaceRoots, readDeny, readFace, cwdRef), createGlobTool(env, rootCanonical, readFaceRoots, readDeny, readFace, cwdRef), createRepoMapTool(env, rootCanonical, readFaceRoots, readDeny, readFace));
|
|
@@ -258,6 +258,120 @@ export declare function isBinaryContent(sample: string): boolean;
|
|
|
258
258
|
* test with no verdict of its own.
|
|
259
259
|
*/
|
|
260
260
|
export declare function isUncPath(path: string): boolean;
|
|
261
|
+
/** The two spelling families a tree can be in. Read from the ROOT's spelling (the cwd's when a deployment
|
|
262
|
+
* supplies no root) — never from `process.platform`: the exec env may be remote and of the other family. */
|
|
263
|
+
export type PathFamily = "win" | "posix";
|
|
264
|
+
/** The family of a call's tree; `undefined` when the call declares neither root nor cwd (a treeless call
|
|
265
|
+
* gets no family invented for it — see {@link pathIdentityOf} for what it can still read). */
|
|
266
|
+
export declare function pathFamilyOf(bases: {
|
|
267
|
+
readonly root: string;
|
|
268
|
+
readonly cwd?: string | undefined;
|
|
269
|
+
}): PathFamily;
|
|
270
|
+
export declare function pathFamilyOf(bases: {
|
|
271
|
+
readonly root?: string | undefined;
|
|
272
|
+
readonly cwd: string;
|
|
273
|
+
}): PathFamily;
|
|
274
|
+
export declare function pathFamilyOf(bases: {
|
|
275
|
+
readonly root?: string | undefined;
|
|
276
|
+
readonly cwd?: string | undefined;
|
|
277
|
+
}): PathFamily | undefined;
|
|
278
|
+
/**
|
|
279
|
+
* FULLY QUALIFIED under the family. win: drive-rooted (`C:\`/`C:/`) or UNC — a `/x` / `\x` is rooted-not-
|
|
280
|
+
* qualified and joins the base's drive or share root ({@link joinForFamily}), a `C:rel` is drive-relative.
|
|
281
|
+
* posix: `/`-rooted. `undefined` (a treeless call): only the anchor-free `/`-rooted form, the one spelling
|
|
282
|
+
* that names a file without a tree.
|
|
283
|
+
*/
|
|
284
|
+
export declare function isAbsoluteForFamily(family: PathFamily | undefined, p: string): boolean;
|
|
285
|
+
/** The refusal words of {@link win32NamespaceScreen} — a closed set (docs/CLOSED-SETS.md). */
|
|
286
|
+
export declare const WIN32_NAMESPACE_REFUSALS: readonly ["device_namespace", "unmappable_namespace", "unc_on_posix_tree", "drive_relative"];
|
|
287
|
+
export type Win32NamespaceRefusal = (typeof WIN32_NAMESPACE_REFUSALS)[number];
|
|
288
|
+
/** What each refusal word says about the spelling (the message clause after `path "…" `). */
|
|
289
|
+
export declare const WIN32_NAMESPACE_REFUSAL_TEXT: {
|
|
290
|
+
readonly device_namespace: "is a Win32 device-namespace path (\\\\.\\ names a raw device object, not a file)";
|
|
291
|
+
readonly unmappable_namespace: "is a Win32 extended-length namespace path with no DOS-path equivalent (only \\\\?\\<drive>: and \\\\?\\UNC\\ forms name files)";
|
|
292
|
+
readonly unc_on_posix_tree: "is a UNC (\\\\host\\share) spelling, which names no file on a POSIX tree";
|
|
293
|
+
readonly drive_relative: "is drive-relative (a drive letter with no separator after the colon), a location no tree can locate";
|
|
294
|
+
};
|
|
295
|
+
/** The fence over the text table: `never` while every refusal word has a row. */
|
|
296
|
+
export type NamespaceRefusalTextCoversEveryWord = AssertAllKeysHandled<Exclude<Win32NamespaceRefusal, keyof typeof WIN32_NAMESPACE_REFUSAL_TEXT>>;
|
|
297
|
+
type AssertAllKeysHandled<T extends never> = T;
|
|
298
|
+
/**
|
|
299
|
+
* The Win32 namespace screen — run on every model-spelled target BEFORE the family join, at every site
|
|
300
|
+
* (the fs throat, the effective-target reader, the rule side, the glob splitter), so one spelling is
|
|
301
|
+
* refused with one word everywhere. `\\.\` ⇒ `device_namespace` (every family); `\\?\` mappable ⇒ the
|
|
302
|
+
* stripped spelling, which is screened ON (a `\\?\UNC\…` on a POSIX tree still refuses), unmappable ⇒
|
|
303
|
+
* `unmappable_namespace` (every family); a backslash UNC ⇒ passes under `win`, `unc_on_posix_tree` under
|
|
304
|
+
* `posix` (the canonicalizer's refusal boundary on POSIX trees is preserved: the family rule governs what
|
|
305
|
+
* is admitted, never what was already refused); `C:rel` under `win` ⇒ `drive_relative`. A treeless call
|
|
306
|
+
* passes a UNC spelling through — the identity then reads it as relative, as before.
|
|
307
|
+
*/
|
|
308
|
+
export declare function win32NamespaceScreen(spelled: string, family: PathFamily | undefined): {
|
|
309
|
+
readonly ok: true;
|
|
310
|
+
readonly spelled: string;
|
|
311
|
+
} | {
|
|
312
|
+
readonly ok: false;
|
|
313
|
+
readonly refuse: Win32NamespaceRefusal;
|
|
314
|
+
};
|
|
315
|
+
declare const PATH_IDENTITY: unique symbol;
|
|
316
|
+
/**
|
|
317
|
+
* The ONE comparison form of an absolute path (upstream's normal form): posix = the lexical-normal
|
|
318
|
+
* `/`-rooted path; win = `/c/rest` for a drive (letter lower-cased), `//host/share/rest` for UNC, `\`
|
|
319
|
+
* folded to `/`, `.`/`..` folded with the anchor preserved. A COMPARISON form — never a path to open, never
|
|
320
|
+
* compared against a native `C:\…` string. Branded: a spelling is not an identity until {@link pathIdentityOf}
|
|
321
|
+
* has read it.
|
|
322
|
+
*/
|
|
323
|
+
export type PathIdentity = string & {
|
|
324
|
+
readonly [PATH_IDENTITY]: true;
|
|
325
|
+
};
|
|
326
|
+
/** Why a spelling has no identity: a namespace refusal, `relative` (join it to a base first), or
|
|
327
|
+
* `unanchored` (a win `/x` with no tree root to take a drive from). */
|
|
328
|
+
export type PathIdentityRefusal = Win32NamespaceRefusal | "relative" | "unanchored";
|
|
329
|
+
export type PathIdentityReading = {
|
|
330
|
+
readonly ok: true;
|
|
331
|
+
readonly identity: PathIdentity;
|
|
332
|
+
} | {
|
|
333
|
+
readonly ok: false;
|
|
334
|
+
readonly reason: PathIdentityRefusal;
|
|
335
|
+
};
|
|
336
|
+
/**
|
|
337
|
+
* The identity of an ABSOLUTE spelling under the family ({@link PathIdentity}). `anchor` is the tree's root
|
|
338
|
+
* (or cwd) spelling: under `win` a `/`-rooted spelling whose first segment is not a drive letter
|
|
339
|
+
* (`/proj/x`) anchors on the root's drive or share, as `path.win32.resolve` anchors it on the current
|
|
340
|
+
* drive; `/c/x` and `/C:/x` are drive spellings (the cygdrive form and the rule grammar's `/`+body), and
|
|
341
|
+
* `/\\host\share\x` is the grammar's prefix before a UNC. A relative spelling is `relative` — callers join
|
|
342
|
+
* it to a base FIRST. A treeless call (`family` undefined) reads only the anchor-free `/`-rooted form, as
|
|
343
|
+
* the POSIX identity; every other spelling names nothing, which is what a missing base answered before.
|
|
344
|
+
*/
|
|
345
|
+
export declare function pathIdentityOf(spelled: string, family: PathFamily | undefined, anchor?: string): PathIdentityReading;
|
|
346
|
+
/** Is the identity a UNC one (`//host/share/…`)? Identities compare by anchor kind first: a UNC identity is
|
|
347
|
+
* never under a rooted one and vice versa (upstream's shape parity). */
|
|
348
|
+
export declare function isUncIdentity(identity: PathIdentity): boolean;
|
|
349
|
+
/** Is `s` already an identity under the family — the identity function is the identity on it? The
|
|
350
|
+
* runtime twin of the brand for values that arrive from outside the type system. */
|
|
351
|
+
export declare function isPathIdentity(s: string, family: PathFamily | undefined): s is PathIdentity;
|
|
352
|
+
/** Is the identity a DIRECTORY a rule may speak for — anything but the tree's bare root (`/`, a drive root
|
|
353
|
+
* `/c`, a share root `//host/share`), which admits nothing? */
|
|
354
|
+
export declare function isIdentityDir(identity: PathIdentity, family: PathFamily | undefined): boolean;
|
|
355
|
+
/** Does the spelling carry a `.` or `..` segment under the family's separators (a backslash is a name byte on
|
|
356
|
+
* POSIX)? A rule body with one admits nothing: folding it could make it name a second directory. */
|
|
357
|
+
export declare function carriesDotSegment(spelled: string, family: PathFamily | undefined): boolean;
|
|
358
|
+
/** A model-spelled path bounded for error text (the spelling is unbounded input). */
|
|
359
|
+
export declare function clipSpelling(spelled: string): string;
|
|
360
|
+
/** A ROOTED (not qualified) spelling handed to the SHELL on a win tree (`/d/x`, `/etc`, `\\x`) is the shell's to
|
|
361
|
+
* map — the win32 adapter runs Git Bash, where `/d/x` is drive D and `/etc` the MSYS root, neither the cwd's
|
|
362
|
+
* drive root — so no lexical reader may resolve it (fail-closed: the operand is unresolvable, the cd mints
|
|
363
|
+
* nothing). The ONE statement of that rule, for every shell-facing resolver. */
|
|
364
|
+
export declare function isShellRootedSpellingUnmapped(family: PathFamily | undefined, spelled: string): boolean;
|
|
365
|
+
/** The win family's native spelling of a UNC written with any leading-separator mix (`//host/share/x`, `\\/host/…`
|
|
366
|
+
* ⇒ `\\\\host\\share\\x`), for a lexical normalizer that recognises the backslash form only; every other spelling is
|
|
367
|
+
* returned as is. */
|
|
368
|
+
export declare function nativeUncSpellingOf(family: PathFamily | undefined, spelled: string): string;
|
|
369
|
+
/**
|
|
370
|
+
* Join a RELATIVE-OR-ROOTED spelling to a base under the family, without folding (`..` across a symlinked
|
|
371
|
+
* parent is the env's to resolve). win = `path.win32` semantics: `/x`/`\x` lands on the base's drive or
|
|
372
|
+
* share root, anything else under the base with the base's own separator. posix = `/`-join.
|
|
373
|
+
*/
|
|
374
|
+
export declare function joinForFamily(family: PathFamily, base: string, rel: string): string;
|
|
261
375
|
/** The family-aware LEXICAL view of a possibly-relative spelling against a resolution base (no I/O,
|
|
262
376
|
* `.`/`..` folded via {@link normalizeAbsPathLexically}): the SECOND deny-judgment view. It asks
|
|
263
377
|
* "does the NAME land on a guarded path", complementing the canonical
|
|
@@ -294,7 +408,7 @@ export declare function resolveKey(env: ExecutionEnv, rootCanonical: string, pat
|
|
|
294
408
|
* A single source of truth for symlink resolution: a sensitive-path policy that matched the raw path
|
|
295
409
|
* instead of the real target would compare against the spelling, not the destination.
|
|
296
410
|
*/
|
|
297
|
-
export declare function canonicalizeTarget(env: ExecutionEnv, path: string, signal
|
|
411
|
+
export declare function canonicalizeTarget(env: ExecutionEnv, path: string, signal: AbortSignal | undefined, family: PathFamily, baseCwd?: string): Promise<{
|
|
298
412
|
ok: true;
|
|
299
413
|
key: string;
|
|
300
414
|
aliasResolved?: true;
|
|
@@ -302,6 +416,7 @@ export declare function canonicalizeTarget(env: ExecutionEnv, path: string, sign
|
|
|
302
416
|
ok: false;
|
|
303
417
|
message: string;
|
|
304
418
|
unresolvedSymlink?: true;
|
|
419
|
+
refused?: Win32NamespaceRefusal;
|
|
305
420
|
}>;
|
|
306
421
|
/** Render a `FsViolation` as the model-facing tool error text (returned, never thrown — model retries). */
|
|
307
422
|
export declare function violationText(toolName: string, v: FsViolation): string;
|
|
@@ -495,3 +610,4 @@ export declare function deletionOldString(content: string, oldString: string, ne
|
|
|
495
610
|
* matters most (and these refusals return as tool results, which do not pass the loop-level budget). */
|
|
496
611
|
export declare const EDIT_ECHO_MAX_CHARS = 200;
|
|
497
612
|
export declare function checkEditMatch(content: string, oldString: string, replaceAll: boolean, truncated: boolean): FsViolation | undefined;
|
|
613
|
+
export {};
|
package/dist/tools/fs/safety.js
CHANGED
|
@@ -23,15 +23,6 @@ const SEP = "/";
|
|
|
23
23
|
export function isAbsolutePathForm(p) {
|
|
24
24
|
return p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p) || p.startsWith("\\\\");
|
|
25
25
|
}
|
|
26
|
-
function normalizeForCompare(p) {
|
|
27
|
-
let n = stripWin32ExtendedPrefix(p);
|
|
28
|
-
if (isUncPath(n) && isWinFormPath(n) && !isWin32DeviceNamespacePath(n))
|
|
29
|
-
n = foldUncLexically(n).key;
|
|
30
|
-
n = isWinFormPath(n) ? n.replace(/\\/g, "/") : n;
|
|
31
|
-
if (/^[A-Za-z]:(\/|$)/.test(n))
|
|
32
|
-
n = n[0].toLowerCase() + n.slice(1);
|
|
33
|
-
return n;
|
|
34
|
-
}
|
|
35
26
|
function nativeSepOf(p) {
|
|
36
27
|
return isWinFormPath(p) && p.includes("\\") ? "\\" : "/";
|
|
37
28
|
}
|
|
@@ -323,26 +314,137 @@ function foldUncLexically(p) {
|
|
|
323
314
|
const anchor = `\\\\${host}${share !== undefined ? `\\${share}` : ""}`;
|
|
324
315
|
return { key: out.length > 0 ? `${anchor}\\${out.join("\\")}` : anchor, climbedAboveShare: climbed };
|
|
325
316
|
}
|
|
317
|
+
export function pathFamilyOf(bases) {
|
|
318
|
+
const tree = bases.root ?? bases.cwd;
|
|
319
|
+
return tree === undefined ? undefined : isWinFormPath(tree) ? "win" : "posix";
|
|
320
|
+
}
|
|
321
|
+
function isUncSpelling(p) {
|
|
322
|
+
return /^[\\/]{2}[^\\/]+[\\/]+[^\\/]/.test(p);
|
|
323
|
+
}
|
|
324
|
+
export function isAbsoluteForFamily(family, p) {
|
|
325
|
+
return family === "win" ? /^[A-Za-z]:[\\/]/.test(p) || isUncSpelling(p) : p.startsWith("/");
|
|
326
|
+
}
|
|
327
|
+
export const WIN32_NAMESPACE_REFUSALS = ["device_namespace", "unmappable_namespace", "unc_on_posix_tree", "drive_relative"];
|
|
328
|
+
export const WIN32_NAMESPACE_REFUSAL_TEXT = {
|
|
329
|
+
device_namespace: "is a Win32 device-namespace path (\\\\.\\ names a raw device object, not a file)",
|
|
330
|
+
unmappable_namespace: "is a Win32 extended-length namespace path with no DOS-path equivalent (only \\\\?\\<drive>: and \\\\?\\UNC\\ forms name files)",
|
|
331
|
+
unc_on_posix_tree: "is a UNC (\\\\host\\share) spelling, which names no file on a POSIX tree",
|
|
332
|
+
drive_relative: "is drive-relative (a drive letter with no separator after the colon), a location no tree can locate",
|
|
333
|
+
};
|
|
334
|
+
export function win32NamespaceScreen(spelled, family) {
|
|
335
|
+
if (family === "win" && /^[\\/]{2}[?.][\\/]/.test(spelled) && !/^\\\\[?.]\\/.test(spelled))
|
|
336
|
+
spelled = spelled.replace(/\//g, "\\");
|
|
337
|
+
if (isWin32DeviceNamespacePath(spelled))
|
|
338
|
+
return { ok: false, refuse: "device_namespace" };
|
|
339
|
+
const mapped = stripWin32ExtendedPrefix(spelled);
|
|
340
|
+
if (mapped.startsWith("\\\\?\\"))
|
|
341
|
+
return { ok: false, refuse: "unmappable_namespace" };
|
|
342
|
+
if (mapped.startsWith("\\\\") && family === "posix")
|
|
343
|
+
return { ok: false, refuse: "unc_on_posix_tree" };
|
|
344
|
+
if (family === "win" && /^[A-Za-z]:(?![\\/])/.test(mapped))
|
|
345
|
+
return { ok: false, refuse: "drive_relative" };
|
|
346
|
+
return { ok: true, spelled: mapped };
|
|
347
|
+
}
|
|
348
|
+
export function pathIdentityOf(spelled, family, anchor) {
|
|
349
|
+
if (typeof spelled !== "string")
|
|
350
|
+
return { ok: false, reason: "relative" };
|
|
351
|
+
if (family !== "win") {
|
|
352
|
+
if (!spelled.startsWith("/"))
|
|
353
|
+
return { ok: false, reason: "relative" };
|
|
354
|
+
return { ok: true, identity: mintIdentity("", spelled.split("/"), false) };
|
|
355
|
+
}
|
|
356
|
+
const screened = win32NamespaceScreen(spelled, "win");
|
|
357
|
+
if (!screened.ok)
|
|
358
|
+
return { ok: false, reason: screened.refuse };
|
|
359
|
+
let s = screened.spelled;
|
|
360
|
+
if (s.startsWith("/") && isAbsoluteForFamily("win", s.slice(1)))
|
|
361
|
+
s = s.slice(1);
|
|
362
|
+
if (isUncSpelling(s)) {
|
|
363
|
+
const segs = s.slice(2).split(/[\\/]+/).filter((x) => x !== "");
|
|
364
|
+
const host = segs.shift();
|
|
365
|
+
const share = segs.shift();
|
|
366
|
+
return { ok: true, identity: mintIdentity(`//${host}/${share}`, segs, true) };
|
|
367
|
+
}
|
|
368
|
+
const drive = /^([A-Za-z]):[\\/]/.exec(s) ?? /^[\\/]([A-Za-z])(?=[\\/]|$)/.exec(s);
|
|
369
|
+
if (drive !== null)
|
|
370
|
+
return { ok: true, identity: mintIdentity(`/${drive[1].toLowerCase()}`, s.slice(2).split(/[\\/]/), false) };
|
|
371
|
+
if (s.startsWith("/") || s.startsWith("\\")) {
|
|
372
|
+
const root = anchor === undefined ? undefined : pathIdentityOf(anchor, "win");
|
|
373
|
+
if (root === undefined || !root.ok)
|
|
374
|
+
return { ok: false, reason: "unanchored" };
|
|
375
|
+
return { ok: true, identity: mintIdentity(identityAnchorOf(root.identity), s.slice(1).split(/[\\/]/), false) };
|
|
376
|
+
}
|
|
377
|
+
return { ok: false, reason: "relative" };
|
|
378
|
+
}
|
|
379
|
+
function mintIdentity(anchor, segments, stripTail) {
|
|
380
|
+
const out = [];
|
|
381
|
+
for (const seg of segments) {
|
|
382
|
+
if (seg === "" || seg === ".")
|
|
383
|
+
continue;
|
|
384
|
+
if (seg === "..") {
|
|
385
|
+
if (out.length > 0)
|
|
386
|
+
out.pop();
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const stripped = stripTail ? seg.replace(/[. ]+$/, "") : seg;
|
|
390
|
+
out.push(stripped.length > 0 ? stripped : seg);
|
|
391
|
+
}
|
|
392
|
+
return (out.length === 0 ? anchor || "/" : `${anchor}/${out.join("/")}`);
|
|
393
|
+
}
|
|
394
|
+
function identityAnchorOf(identity) {
|
|
395
|
+
return identity.startsWith("//") ? identity.split("/").slice(0, 4).join("/") : identity.slice(0, 2);
|
|
396
|
+
}
|
|
397
|
+
export function isUncIdentity(identity) {
|
|
398
|
+
return identity.startsWith("//");
|
|
399
|
+
}
|
|
400
|
+
export function isPathIdentity(s, family) {
|
|
401
|
+
const r = pathIdentityOf(s, family);
|
|
402
|
+
return r.ok && r.identity === s;
|
|
403
|
+
}
|
|
404
|
+
export function isIdentityDir(identity, family) {
|
|
405
|
+
if (family !== "win")
|
|
406
|
+
return identity !== "/";
|
|
407
|
+
return identity.length > identityAnchorOf(identity).length;
|
|
408
|
+
}
|
|
409
|
+
export function carriesDotSegment(spelled, family) {
|
|
410
|
+
return (family === "win" ? /(^|[\\/])\.{1,2}([\\/]|$)/ : /(^|\/)\.{1,2}(\/|$)/).test(spelled);
|
|
411
|
+
}
|
|
412
|
+
export function clipSpelling(spelled) {
|
|
413
|
+
return spelled.length > 512 ? `${spelled.slice(0, 512)}…` : spelled;
|
|
414
|
+
}
|
|
415
|
+
export function isShellRootedSpellingUnmapped(family, spelled) {
|
|
416
|
+
return family === "win" && (spelled.startsWith("/") || spelled.startsWith("\\"));
|
|
417
|
+
}
|
|
418
|
+
export function nativeUncSpellingOf(family, spelled) {
|
|
419
|
+
return family === "win" && isUncSpelling(spelled) ? `\\\\${spelled.slice(2).replace(/\//g, "\\")}` : spelled;
|
|
420
|
+
}
|
|
421
|
+
export function joinForFamily(family, base, rel) {
|
|
422
|
+
if (family === "posix")
|
|
423
|
+
return `${base.replace(/\/+$/, "")}/${rel}`;
|
|
424
|
+
if (rel.startsWith("/") || rel.startsWith("\\")) {
|
|
425
|
+
const root = /^[A-Za-z]:/.exec(base)?.[0] ?? /^[\\/]{2}[^\\/]+[\\/]+[^\\/]+/.exec(base)?.[0];
|
|
426
|
+
return root === undefined ? rel : root + rel;
|
|
427
|
+
}
|
|
428
|
+
return `${base.replace(/[\\/]+$/, "")}${base.includes("\\") ? "\\" : "/"}${rel}`;
|
|
429
|
+
}
|
|
326
430
|
export function lexicalViewOf(spelled, base) {
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
: `${base.replace(/[\\/]+$/, "")}${isWinFormPath(base) && base.includes("\\") ? "\\" : "/"}${spelled}`;
|
|
330
|
-
return normalizeAbsPathLexically(abs);
|
|
431
|
+
const family = pathFamilyOf({ root: base });
|
|
432
|
+
return normalizeAbsPathLexically(isAbsoluteForFamily(family, spelled) ? spelled : joinForFamily(family, base, spelled));
|
|
331
433
|
}
|
|
332
434
|
function within(rootCanonical, p) {
|
|
333
435
|
if (!rootCanonical)
|
|
334
436
|
return false;
|
|
335
|
-
const
|
|
336
|
-
const
|
|
337
|
-
|
|
437
|
+
const family = pathFamilyOf({ root: rootCanonical });
|
|
438
|
+
const rootN = pathIdentityOf(rootCanonical, family);
|
|
439
|
+
const pN = pathIdentityOf(p, family);
|
|
440
|
+
if (!rootN.ok || !pN.ok)
|
|
338
441
|
return false;
|
|
339
|
-
if (pN === rootN)
|
|
442
|
+
if (pN.identity === rootN.identity)
|
|
340
443
|
return true;
|
|
341
|
-
|
|
342
|
-
return pN.startsWith(base);
|
|
444
|
+
return pN.identity.startsWith(rootN.identity.endsWith(SEP) ? rootN.identity : rootN.identity + SEP);
|
|
343
445
|
}
|
|
344
446
|
export async function resolveKey(env, rootCanonical, path, signal, baseCwd, additionalRootsCanonical, exactFileReadExemption, readDeny, readFace) {
|
|
345
|
-
const canon = await canonicalizeTarget(env, path, signal, baseCwd);
|
|
447
|
+
const canon = await canonicalizeTarget(env, path, signal, pathFamilyOf({ root: rootCanonical }), baseCwd);
|
|
346
448
|
if (!canon.ok)
|
|
347
449
|
return { ok: false, violation: { code: "invalid", message: canon.message } };
|
|
348
450
|
const key = canon.key;
|
|
@@ -397,23 +499,21 @@ export async function resolveKey(env, rootCanonical, path, signal, baseCwd, addi
|
|
|
397
499
|
}
|
|
398
500
|
return { ok: true, key };
|
|
399
501
|
}
|
|
400
|
-
export async function canonicalizeTarget(env, path, signal, baseCwd) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const unprefixed = stripWin32ExtendedPrefix(path);
|
|
405
|
-
if (unprefixed.startsWith("\\\\?\\")) {
|
|
406
|
-
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.` };
|
|
502
|
+
export async function canonicalizeTarget(env, path, signal, family, baseCwd) {
|
|
503
|
+
const screened = win32NamespaceScreen(path, family);
|
|
504
|
+
if (!screened.ok) {
|
|
505
|
+
return { ok: false, message: `path "${clipSpelling(path)}" ${WIN32_NAMESPACE_REFUSAL_TEXT[screened.refuse]}; refused (${screened.refuse}).`, refused: screened.refuse };
|
|
407
506
|
}
|
|
408
|
-
|
|
507
|
+
const unprefixed = screened.spelled;
|
|
508
|
+
if (family === "win" && isUncSpelling(unprefixed)) {
|
|
409
509
|
const folded = foldUncLexically(unprefixed);
|
|
410
510
|
if (folded.climbedAboveShare) {
|
|
411
511
|
return { ok: false, message: `path "${path}" climbs above its UNC share root via ".." (\\\\host\\share is the root of a network path); refused.` };
|
|
412
512
|
}
|
|
413
513
|
return { ok: true, key: folded.key };
|
|
414
514
|
}
|
|
415
|
-
const spelled = unprefixed.startsWith("//") ? unprefixed.replace(/^\/+/, "/") : unprefixed;
|
|
416
|
-
const target = baseCwd && !
|
|
515
|
+
const spelled = family === "posix" && unprefixed.startsWith("//") ? unprefixed.replace(/^\/+/, "/") : unprefixed;
|
|
516
|
+
const target = baseCwd && !isAbsoluteForFamily(family, spelled) ? joinForFamily(family, baseCwd, spelled) : spelled;
|
|
417
517
|
const absR = await env.absolutePath(target, signal);
|
|
418
518
|
if (!absR.ok)
|
|
419
519
|
return { ok: false, message: `cannot resolve path "${path}": ${absR.error.message}` };
|
|
@@ -451,7 +551,7 @@ export async function canonicalizeTarget(env, path, signal, baseCwd) {
|
|
|
451
551
|
if (!link.ok) {
|
|
452
552
|
return { ok: false, message: `cannot read symlink "${path}": ${link.error.message}`, unresolvedSymlink: true };
|
|
453
553
|
}
|
|
454
|
-
const linkTarget =
|
|
554
|
+
const linkTarget = isAbsoluteForFamily(family, link.value) ? link.value : joinForFamily(family, parentDir(abs), link.value);
|
|
455
555
|
const reabs = await env.absolutePath(linkTarget, signal);
|
|
456
556
|
if (!reabs.ok) {
|
|
457
557
|
return { ok: false, message: `cannot resolve symlink target "${linkTarget}" for "${path}": ${reabs.error.message}`, unresolvedSymlink: true };
|
package/dist/tools/fs/search.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { delimitUntrusted } from "../../core/untrusted-text.js";
|
|
2
2
|
import { MAX_EXEC_OUTPUT_BYTES } from "../../core/exec-output-tail.js";
|
|
3
|
-
import { hasBinaryExtension,
|
|
3
|
+
import { hasBinaryExtension, isAbsoluteForFamily, isWinFormPath, joinForFamily, pathFamilyOf } from "./safety.js";
|
|
4
4
|
const WALK_MAX_FILES = 5000;
|
|
5
5
|
const WALK_MAX_DEPTH = 32;
|
|
6
6
|
const GREP_DEFAULT_CAP = 250;
|
|
@@ -776,7 +776,8 @@ export async function jsGrepDetailed(env, root, p, signal, guards, deny, denyOut
|
|
|
776
776
|
}
|
|
777
777
|
const ignore = await buildIgnore(env, root, signal);
|
|
778
778
|
const rootPrefix = root.replace(/[\\/]+$/, "") + (root.includes("\\") ? "\\" : "/");
|
|
779
|
-
const
|
|
779
|
+
const family = pathFamilyOf({ root });
|
|
780
|
+
const start = p.path ? (isAbsoluteForFamily(family, p.path) ? p.path : joinForFamily(family, root, p.path)) : root;
|
|
780
781
|
let walked;
|
|
781
782
|
if (p.path) {
|
|
782
783
|
const info = await env.fileInfo(start, signal);
|
|
@@ -1054,10 +1055,10 @@ export function detectRipgrep(env) {
|
|
|
1054
1055
|
async function sortRgFilesByMtime(env, root, records, signal) {
|
|
1055
1056
|
if (records.length === 0)
|
|
1056
1057
|
return [...records];
|
|
1057
|
-
const
|
|
1058
|
+
const family = pathFamilyOf({ root });
|
|
1058
1059
|
const toAbs = (p) => {
|
|
1059
1060
|
const stripped = p.startsWith("./") ? p.slice(2) : p;
|
|
1060
|
-
return
|
|
1061
|
+
return isAbsoluteForFamily(family, stripped) ? stripped : joinForFamily(family, root, stripped);
|
|
1061
1062
|
};
|
|
1062
1063
|
const paths = records.map((r) => r.path ?? r.text);
|
|
1063
1064
|
const infos = await Promise.all(paths.map((p) => env.fileInfo(toAbs(p), signal)));
|
|
@@ -1231,7 +1232,7 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
|
1231
1232
|
if (deny === undefined || deny.rgExclusionGlobs.length === 0)
|
|
1232
1233
|
return { note: "" };
|
|
1233
1234
|
if (p.path) {
|
|
1234
|
-
const startPath =
|
|
1235
|
+
const startPath = isAbsoluteForFamily(pathFamilyOf({ root }), p.path) ? p.path : joinForFamily(pathFamilyOf({ root }), root, p.path);
|
|
1235
1236
|
const info = await env.fileInfo(startPath, signal);
|
|
1236
1237
|
if (info.ok && info.value.kind === "file")
|
|
1237
1238
|
return { note: "" };
|
|
@@ -1322,7 +1323,8 @@ export async function runGlobDetailed(env, root, pattern, opts = {}, signal) {
|
|
|
1322
1323
|
const t0 = Date.now();
|
|
1323
1324
|
const baseIgnore = await buildIgnore(env, root, signal);
|
|
1324
1325
|
const rootPrefix = root.replace(/[\\/]+$/, "") + (root.includes("\\") ? "\\" : "/");
|
|
1325
|
-
const
|
|
1326
|
+
const family = pathFamilyOf({ root });
|
|
1327
|
+
const start = opts.path ? (isAbsoluteForFamily(family, opts.path) ? opts.path : joinForFamily(family, root, opts.path)) : root;
|
|
1326
1328
|
if (opts.path !== undefined) {
|
|
1327
1329
|
const probe = await env.fileInfo(start, signal);
|
|
1328
1330
|
if (!probe.ok && probe.error.code === "not_found") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/core",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.17.1",
|
|
4
4
|
"description": "Stateless, task-oriented AI agent core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
"gate:backlog-ledger": "node scripts/verify-backlog-ledger.mjs",
|
|
86
86
|
"gate:docs-tool-names": "node scripts/verify-docs-tool-names.mjs",
|
|
87
87
|
"gate:comment-ratio": "vitest run test/comment-ratio-ratchet.test.ts",
|
|
88
|
+
"gate:negative-controls": "vitest run test/gate-negative-controls.test.ts",
|
|
88
89
|
"gate:all": "node scripts/gate-all.mjs",
|
|
89
90
|
"car:gate": "node scripts/car-gate.mjs"
|
|
90
91
|
},
|