@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.
Files changed (207) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/dist/agents/builtin-agents.d.ts +3 -3
  3. package/dist/agents/cross-session-envelope.d.ts +0 -1
  4. package/dist/agents/cross-session-envelope.js +0 -1
  5. package/dist/agents/peer-notices.d.ts +0 -9
  6. package/dist/agents/peer-notices.js +0 -63
  7. package/dist/agents/subagent.js +7 -15
  8. package/dist/brain/request-params.d.ts +2 -2
  9. package/dist/config/defaults.d.ts +0 -7
  10. package/dist/config/defaults.js +0 -2
  11. package/dist/core/artifact-host.d.ts +218 -0
  12. package/dist/core/artifact-host.js +92 -0
  13. package/dist/core/ask-origin.d.ts +29 -3
  14. package/dist/core/ask-origin.js +17 -9
  15. package/dist/core/ask-question.d.ts +25 -0
  16. package/dist/core/ask-question.js +4 -0
  17. package/dist/core/ask-unresolvable-notice.d.ts +30 -21
  18. package/dist/core/ask-unresolvable-notice.js +16 -8
  19. package/dist/core/auto-compaction.d.ts +10 -10
  20. package/dist/core/auto-mode-arming.d.ts +2 -2
  21. package/dist/core/background-agent-store.d.ts +5 -5
  22. package/dist/core/background-shell.d.ts +1 -1
  23. package/dist/core/checkpoint-execution-record.d.ts +0 -3
  24. package/dist/core/checkpoint-execution-record.js +0 -4
  25. package/dist/core/checkpoint-store.d.ts +71 -99
  26. package/dist/core/checkpoint-store.js +9 -16
  27. package/dist/core/context-edit.d.ts +36 -104
  28. package/dist/core/context-edit.js +37 -60
  29. package/dist/core/effective-path-target.d.ts +68 -8
  30. package/dist/core/effective-path-target.js +42 -30
  31. package/dist/core/engine-notice.d.ts +46 -32
  32. package/dist/core/env-budget.d.ts +31 -0
  33. package/dist/core/env-budget.js +39 -0
  34. package/dist/core/exec-output-tail.d.ts +5 -5
  35. package/dist/core/file-history-store.js +3 -6
  36. package/dist/core/fs-write-gate-policy.d.ts +1 -1
  37. package/dist/core/fs-write-gate-policy.js +3 -3
  38. package/dist/core/gate-lanes.js +9 -15
  39. package/dist/core/governance-codes.d.ts +1 -1
  40. package/dist/core/governance-codes.js +2 -0
  41. package/dist/core/hands-band.d.ts +1 -1
  42. package/dist/core/hooks.d.ts +18 -17
  43. package/dist/core/lsp-session.d.ts +1 -1
  44. package/dist/core/lsp.d.ts +1 -1
  45. package/dist/core/lsp.js +5 -4
  46. package/dist/core/mcp.d.ts +5 -5
  47. package/dist/core/mcp.js +4 -48
  48. package/dist/core/memory-engine/engine.js +2 -1
  49. package/dist/core/memory-engine/layout.d.ts +18 -6
  50. package/dist/core/memory-engine/layout.js +40 -21
  51. package/dist/core/memory-recall.d.ts +0 -8
  52. package/dist/core/memory.d.ts +1 -1
  53. package/dist/core/oracle-isolation.js +2 -2
  54. package/dist/core/parent-spec-seat.d.ts +66 -0
  55. package/dist/core/parent-spec-seat.js +47 -0
  56. package/dist/core/permission-rule-model.d.ts +11 -6
  57. package/dist/core/permission-rule-model.js +53 -37
  58. package/dist/core/permission-rules.js +8 -3
  59. package/dist/core/physical-path.d.ts +37 -0
  60. package/dist/core/physical-path.js +30 -0
  61. package/dist/core/protocol-naming.d.ts +2 -2
  62. package/dist/core/remote-env.d.ts +1 -1
  63. package/dist/core/retired-keys.d.ts +19 -0
  64. package/dist/core/retired-keys.js +15 -0
  65. package/dist/core/runner/active-skill-scope.js +3 -3
  66. package/dist/core/runner/assemble-result.d.ts +15 -13
  67. package/dist/core/runner/assemble-result.js +5 -4
  68. package/dist/core/runner/compaction-call-options.d.ts +25 -89
  69. package/dist/core/runner/contracts.d.ts +11 -10
  70. package/dist/core/runner/gate-exit.js +15 -1
  71. package/dist/core/runner/halt-attribution.d.ts +35 -0
  72. package/dist/core/runner/halt-attribution.js +16 -0
  73. package/dist/core/runner/memory-consolidation.d.ts +0 -2
  74. package/dist/core/runner/memory-consolidation.js +0 -1
  75. package/dist/core/runner/prepare-artifact.d.ts +52 -0
  76. package/dist/core/runner/prepare-artifact.js +63 -0
  77. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
  78. package/dist/core/runner/prepare-caps-and-workflow.js +2 -3
  79. package/dist/core/runner/prepare-config-doors.d.ts +3 -3
  80. package/dist/core/runner/prepare-config-doors.js +5 -2
  81. package/dist/core/runner/prepare-context-lane.d.ts +2 -2
  82. package/dist/core/runner/prepare-context-lane.js +2 -13
  83. package/dist/core/runner/prepare-file-history.d.ts +1 -1
  84. package/dist/core/runner/prepare-file-history.js +3 -52
  85. package/dist/core/runner/prepare-hands-readface.d.ts +4 -7
  86. package/dist/core/runner/prepare-hands-readface.js +3 -2
  87. package/dist/core/runner/prepare-inherited-gate.d.ts +5 -2
  88. package/dist/core/runner/prepare-inherited-gate.js +7 -8
  89. package/dist/core/runner/prepare-memory.d.ts +4 -6
  90. package/dist/core/runner/prepare-memory.js +4 -2
  91. package/dist/core/runner/prepare-policy-chain.d.ts +3 -3
  92. package/dist/core/runner/prepare-policy-chain.js +15 -30
  93. package/dist/core/runner/prepare-prompt-assembly.js +1 -1
  94. package/dist/core/runner/prepare-question-face.js +2 -1
  95. package/dist/core/runner/prepare-safety-scan.d.ts +1 -1
  96. package/dist/core/runner/prepare-suspend-saga.js +0 -2
  97. package/dist/core/runner/prepare-task.js +12 -11
  98. package/dist/core/runner/prepare-wiring-manifest.js +9 -7
  99. package/dist/core/runner/resume-admission.js +2 -1
  100. package/dist/core/runner/resume-internals-and-config.js +4 -0
  101. package/dist/core/runner/retired-runner-deps.d.ts +3 -0
  102. package/dist/core/runner/retired-runner-deps.js +7 -0
  103. package/dist/core/runner/run-harness-handlers.js +5 -3
  104. package/dist/core/runner/run-settle-and-teardown.d.ts +1 -1
  105. package/dist/core/runner/run-settle-and-teardown.js +3 -1
  106. package/dist/core/runner/session-file-state-replay.js +3 -3
  107. package/dist/core/runner/session-rule-policy.js +3 -4
  108. package/dist/core/runner/stream-halt-verbs.js +1 -1
  109. package/dist/core/runner/stream-steer-verb.js +1 -4
  110. package/dist/core/runner/workspace-path.js +3 -0
  111. package/dist/core/runner-deps.d.ts +30 -49
  112. package/dist/core/safe-notify.d.ts +1 -1
  113. package/dist/core/sensitive-path-policy.d.ts +1 -1
  114. package/dist/core/sensitive-path-policy.js +11 -12
  115. package/dist/core/session.d.ts +2 -2
  116. package/dist/core/skill-tool-specifier.d.ts +1 -1
  117. package/dist/core/skills-directory.d.ts +1 -1
  118. package/dist/core/skills-directory.js +4 -3
  119. package/dist/core/spec-contract.js +5 -4
  120. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  121. package/dist/core/strategy-store.d.ts +8 -11
  122. package/dist/core/task-event.d.ts +20 -17
  123. package/dist/core/task-limits.d.ts +1 -1
  124. package/dist/core/task-notification.d.ts +5 -5
  125. package/dist/core/task-registry-agent.d.ts +5 -5
  126. package/dist/core/task-registry-monitor.d.ts +2 -2
  127. package/dist/core/task-registry-shared.d.ts +32 -28
  128. package/dist/core/task-registry-shared.js +4 -13
  129. package/dist/core/task-registry.d.ts +8 -8
  130. package/dist/core/task-result.d.ts +37 -37
  131. package/dist/core/task-spec.d.ts +5 -5
  132. package/dist/core/task-stream.d.ts +16 -11
  133. package/dist/core/tool-catalog-entries.d.ts +1 -0
  134. package/dist/core/tool-catalog-entries.js +6 -1
  135. package/dist/core/tool-conformance.d.ts +0 -2
  136. package/dist/core/tool-conformance.js +1 -3
  137. package/dist/core/tool-errors.d.ts +5 -0
  138. package/dist/core/tool-errors.js +1 -0
  139. package/dist/core/tool-policy.d.ts +66 -22
  140. package/dist/core/tool-policy.js +31 -5
  141. package/dist/core/tool-result-budget.d.ts +1 -1
  142. package/dist/core/tool-result-store.d.ts +11 -11
  143. package/dist/core/tool-spec.d.ts +34 -38
  144. package/dist/core/tools.d.ts +2 -2
  145. package/dist/core/trace.d.ts +12 -15
  146. package/dist/core/untrusted-text.d.ts +1 -1
  147. package/dist/core/wiring-manifest.d.ts +4 -3
  148. package/dist/core/workflow-journal-store.d.ts +11 -11
  149. package/dist/engine/execution-env/node-execution-env.d.ts +3 -2
  150. package/dist/engine/execution-env/node-execution-env.js +8 -5
  151. package/dist/engine/harness/agent-harness.d.ts +47 -4
  152. package/dist/engine/harness/agent-harness.js +52 -9
  153. package/dist/engine/loop/agent-loop.js +15 -8
  154. package/dist/engine/loop/types.d.ts +5 -0
  155. package/dist/index.d.ts +20 -14
  156. package/dist/index.js +16 -10
  157. package/dist/internal/harness.d.ts +1 -0
  158. package/dist/orchestration/run-spec.d.ts +3 -0
  159. package/dist/orchestration/run-spec.js +3 -3
  160. package/dist/orchestration/run-workflow-tool.d.ts +12 -6
  161. package/dist/orchestration/run-workflow-tool.js +23 -5
  162. package/dist/orchestration/workflow-primitives.d.ts +4 -2
  163. package/dist/orchestration/workflow-primitives.js +1 -4
  164. package/dist/orchestration/workflow-script-store.js +9 -25
  165. package/dist/orchestration/workflow-types.d.ts +34 -4
  166. package/dist/orchestration/workflow-types.js +2 -0
  167. package/dist/orchestration/workflow.d.ts +39 -7
  168. package/dist/orchestration/workflow.js +237 -113
  169. package/dist/prompt-assembly/assemble.d.ts +2 -2
  170. package/dist/prompt-assembly/assemble.js +9 -9
  171. package/dist/prompt-assembly/composer.d.ts +6 -7
  172. package/dist/prompt-assembly/composer.js +10 -10
  173. package/dist/prompt-assembly/epoch.js +1 -1
  174. package/dist/prompt-assembly/event-registry.d.ts +0 -21
  175. package/dist/prompt-assembly/packs/sema-default.js +30 -30
  176. package/dist/prompt-assembly/types.d.ts +14 -14
  177. package/dist/prompts/coordinator.d.ts +0 -13
  178. package/dist/prompts/coordinator.js +0 -3
  179. package/dist/stores/cc/task-list-store.js +2 -10
  180. package/dist/stores/file/checkpoint-store.js +1 -5
  181. package/dist/stores/file/file-history-store.js +2 -2
  182. package/dist/stores/file/fs-atomic.d.ts +15 -18
  183. package/dist/stores/file/fs-atomic.js +4 -14
  184. package/dist/stores/file/mailbox-store.d.ts +7 -11
  185. package/dist/stores/file/mailbox-store.js +4 -11
  186. package/dist/stores/file/permission-rule-store.d.ts +4 -35
  187. package/dist/stores/file/permission-rule-store.js +4 -148
  188. package/dist/tools/artifact/artifact-text.d.ts +62 -0
  189. package/dist/tools/artifact/artifact-text.js +68 -0
  190. package/dist/tools/artifact/artifact-tool.d.ts +52 -0
  191. package/dist/tools/artifact/artifact-tool.js +351 -0
  192. package/dist/tools/artifact/local-stub.d.ts +26 -0
  193. package/dist/tools/artifact/local-stub.js +297 -0
  194. package/dist/tools/fs/bash-readonly-classifier.d.ts +90 -8
  195. package/dist/tools/fs/bash-readonly-classifier.js +294 -70
  196. package/dist/tools/fs/fs-bash.d.ts +14 -10
  197. package/dist/tools/fs/fs-bash.js +50 -33
  198. package/dist/tools/fs/fs-search-tools.js +5 -2
  199. package/dist/tools/fs/fs-shared.d.ts +2 -0
  200. package/dist/tools/fs/fs-shared.js +3 -11
  201. package/dist/tools/fs/index.d.ts +4 -0
  202. package/dist/tools/fs/index.js +4 -1
  203. package/dist/tools/fs/safety.d.ts +117 -1
  204. package/dist/tools/fs/safety.js +131 -31
  205. package/dist/tools/fs/search.js +8 -6
  206. package/package.json +2 -1
  207. package/test/export-surface.snapshot.json +119 -39
@@ -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, classifyBoundedReadonlyPollLoop, classifyCompoundReadonlyDetailed, classifyOutOfRootReadGate, classifySimpleCommandReadBoundary, NOT_AUTO_ALLOWED, } from "./bash-readonly-classifier.js";
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.undecidedPaths !== undefined && detailed.undecidedPaths.length > 0) {
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 loopRefusal = classifyBoundedReadonlyPollLoop(command, allowSet, resolved);
48
- if (loopRefusal === undefined)
47
+ const loop = classifyBoundedReadonlyPollLoopDetailed(command, allowSet, resolved);
48
+ if (loop.reason === undefined)
49
49
  return { reversible: true };
50
- return { reversible: false, ...(pollLoopBoundaryVerdict(command, allowSet, resolved, loopRefusal) === "declared" ? { mandated: true } : boundaryGate(detailed)) };
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 attributeRefusal(face, resolved, fullRefusal) {
54
- if (resolved === undefined)
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 pollLoopBoundaryVerdict(command, allowSet, resolved, fullRefusal) {
63
- return attributeRefusal((b) => classifyBoundedReadonlyPollLoop(command, allowSet, b), resolved, fullRefusal);
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 (denyJudgeSpoke(verdict))
75
+ if (resolved === undefined)
76
+ return {};
77
+ if (denyJudgeSpoke(verdict) || unjudgedRead(command, allowSet, resolved, verdict))
80
78
  return { mandated: true };
81
- return resolved !== undefined && classifyOutOfRootReadGate(command, allowSet, resolved).gated ? { mandated: true } : {};
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 (boundaryDeclared(detailed)) {
106
- const cause = recursiveReadCause(detailed);
107
- return { reversible: false, mandated: true, ...(cause !== undefined ? { cause } : {}) };
108
- }
109
- if (detailed.reason === undefined) {
110
- return detailed.undecidedPaths !== undefined && detailed.undecidedPaths.length > 0 ? { reversible: false } : { reversible: true };
111
- }
112
- const compound = attributeRefusal((b) => classifyCompoundReadonlyDetailed(command, allowSet, b).reason, resolved, detailed.reason);
113
- if (compound === "declared")
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
- if (compound === "unread")
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 { announceEnvKnob } from "../../core/env-knob-announce.js";
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
- const rawEnv = process.env.BASH_MAX_OUTPUT_LENGTH;
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)." }),
@@ -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
@@ -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 (!readOnly) {
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?: AbortSignal, baseCwd?: string): Promise<{
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 {};
@@ -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 abs = isAbsolutePathForm(spelled)
328
- ? spelled
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 rootN = normalizeForCompare(rootCanonical);
336
- const pN = normalizeForCompare(p);
337
- if (!rootN)
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
- const base = rootN.endsWith(SEP) ? rootN : rootN + SEP;
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
- if (isWin32DeviceNamespacePath(path)) {
402
- return { ok: false, message: `path "${path}" is a Win32 device-namespace path (\\\\.\\ names a raw device object, not a file); refused.` };
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
- if (isUncPath(unprefixed) && isWinFormPath(unprefixed)) {
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 && !isAbsolutePathForm(spelled) ? `${baseCwd.replace(/[\\/]+$/, "")}/${spelled}` : spelled;
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 = isAbsolutePathForm(link.value) ? link.value : `${parentDir(abs)}${nativeSepOf(abs)}${link.value}`;
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 };
@@ -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, isAbsolutePathForm, isWinFormPath } from "./safety.js";
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 start = p.path ? (isAbsolutePathForm(p.path) ? p.path : `${rootPrefix}${p.path}`) : root;
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 rootPrefix = root.replace(/[\\/]+$/, "") + (root.includes("\\") ? "\\" : "/");
1058
+ const family = pathFamilyOf({ root });
1058
1059
  const toAbs = (p) => {
1059
1060
  const stripped = p.startsWith("./") ? p.slice(2) : p;
1060
- return isAbsolutePathForm(stripped) ? stripped : `${rootPrefix}${stripped}`;
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 = isAbsolutePathForm(p.path) ? p.path : `${root.replace(/[\\/]+$/, "")}${root.includes("\\") ? "\\" : "/"}${p.path}`;
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 start = opts.path ? (isAbsolutePathForm(opts.path) ? opts.path : `${rootPrefix}${opts.path}`) : root;
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.16.0",
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
  },