@sema-agent/core 7.17.4 → 7.17.6

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 (40) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/core/memory-engine/delegation-settlement.d.ts +27 -9
  3. package/dist/core/memory-engine/delegation-settlement.js +155 -46
  4. package/dist/core/memory-engine/engine.js +12 -6
  5. package/dist/core/remote-env.d.ts +23 -1
  6. package/dist/core/remote-env.js +4 -1
  7. package/dist/core/runner/prepare-hands-readface.js +10 -8
  8. package/dist/core/tool-catalog-entries.js +1 -1
  9. package/dist/tools/fs/bash-readonly-classifier.d.ts +46 -5
  10. package/dist/tools/fs/bash-readonly-classifier.js +572 -80
  11. package/dist/tools/fs/fs-bash.d.ts +28 -0
  12. package/dist/tools/fs/fs-bash.js +135 -27
  13. package/dist/tools/fs/index.d.ts +13 -0
  14. package/dist/tools/fs/index.js +1 -0
  15. package/package.json +3 -2
  16. package/wire-changes/core@7.10.0.json +739 -0
  17. package/wire-changes/core@7.11.0.json +685 -0
  18. package/wire-changes/core@7.11.1.json +112 -0
  19. package/wire-changes/core@7.11.2.json +73 -0
  20. package/wire-changes/core@7.12.0.json +356 -0
  21. package/wire-changes/core@7.13.0.json +199 -0
  22. package/wire-changes/core@7.14.0.json +578 -0
  23. package/wire-changes/core@7.15.0.json +174 -0
  24. package/wire-changes/core@7.16.0.json +132 -0
  25. package/wire-changes/core@7.17.0.json +1621 -0
  26. package/wire-changes/core@7.17.1.json +139 -0
  27. package/wire-changes/core@7.17.2.json +65 -0
  28. package/wire-changes/core@7.17.3.json +44 -0
  29. package/wire-changes/core@7.17.4.json +52 -0
  30. package/wire-changes/core@7.17.5.json +67 -0
  31. package/wire-changes/core@7.5.0.json +1063 -0
  32. package/wire-changes/core@7.5.1.json +252 -0
  33. package/wire-changes/core@7.5.2.json +418 -0
  34. package/wire-changes/core@7.6.0.json +1243 -0
  35. package/wire-changes/core@7.6.1.json +1130 -0
  36. package/wire-changes/core@7.7.0.json +509 -0
  37. package/wire-changes/core@7.8.0.json +1293 -0
  38. package/wire-changes/core@7.9.0.json +691 -0
  39. package/wire-changes/core@7.9.1.json +518 -0
  40. package/wire-changes/core@7.9.2.json +46 -0
@@ -40,5 +40,8 @@ export function isSuspendable(env) {
40
40
  return isRemoteExecutionEnv(env) && env.capabilities.suspendable && missingRestoreSurface(env).length === 0;
41
41
  }
42
42
  export function isIsolated(env) {
43
- return isRemoteExecutionEnv(env) ? env.capabilities.isolation : false;
43
+ return isRemoteExecutionEnv(env) ? env.capabilities.isolation === true : false;
44
+ }
45
+ export function boundaryIsTheSandbox(env) {
46
+ return isIsolated(env);
44
47
  }
@@ -1,6 +1,7 @@
1
1
  import { emitTrace } from "../trace.js";
2
2
  import { StubExecutionEnv } from "../stub-env.js";
3
3
  import { hasBackgroundShell } from "../background-shell.js";
4
+ import { boundaryIsTheSandbox } from "../remote-env.js";
4
5
  import { defaultTaskRegistry } from "../task-registry.js";
5
6
  import { bashReadBoundaryProbe, bashReversibilityProbe, compileReadDeny, createHandsToolkit, deploymentReadFaceClampNotice, pdfModelCapabilitiesOf, resolveReadDenyBuiltins, inheritSessionReadFileState, resolveReadFace, seedReadFileStateFromContext, seedReadFileStateFromTranscript, } from "../../tools/fs/index.js";
6
7
  import { resolveKey, sha256 } from "../../tools/fs/safety.js";
@@ -293,6 +294,13 @@ export async function prepareHandsMount(input) {
293
294
  sessionSeats.reanchor(sessionId, readFileState, ownCommittedTailEntryId);
294
295
  }
295
296
  : undefined;
297
+ const shellReadBoundary = () => ({
298
+ roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical],
299
+ ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}),
300
+ ...(executionEnv.homeDir !== undefined ? { homeDir: executionEnv.homeDir } : {}),
301
+ denyMatch: (p) => readDenyMatcher?.matchPath(p)?.pattern ?? null,
302
+ ...(resolvedReadFace !== undefined ? { face: resolvedReadFace } : {}),
303
+ });
296
304
  const band = createHandsToolkit(executionEnv, readFileState, rootCanonical, {
297
305
  ...(additionalRootsCanonical.length > 0 ? { additionalRoots: additionalRootsCanonical } : {}),
298
306
  ...(additionalReadRootsCanonical.length > 0 ? { additionalReadRoots: additionalReadRootsCanonical } : {}),
@@ -306,6 +314,7 @@ export async function prepareHandsMount(input) {
306
314
  ...(input.onFileEdited !== undefined ? { onFileEdited: input.onFileEdited } : {}),
307
315
  includeShell: handsIncludeShell,
308
316
  readOnly: handsReadOnly,
317
+ shellReadBoundary,
309
318
  ...(handsCwdRef ? { cwdRef: handsCwdRef } : {}),
310
319
  taskRegistry: defaultTaskRegistry,
311
320
  taskOwner: hostTaskId,
@@ -346,18 +355,11 @@ export async function prepareHandsMount(input) {
346
355
  if (!(executionEnv instanceof StubExecutionEnv) && spec.handsReadOnly !== true) {
347
356
  if (shellGate === "off") {
348
357
  deps.onError?.(new Error(`shell gate doctrine is "off" while a real writable shell (Bash) is mounted — the read boundary is ` +
349
- `still judged on every command (a deny-listed or out-of-root read asks; the ask is mandated), but no ` +
358
+ `still judged on every command (a deny-listed or out-of-root read asks; the ask is mandated${boundaryIsTheSandbox(executionEnv) ? "" : "; a read that only leaves the roots after symlink resolution is refused before it runs"}), but no ` +
350
359
  `residual shell-risk classifier is installed: writes, egress and unknown commands are adjudicated by ` +
351
360
  `the ordinary policy/hook chain only. Set spec.shellGate to "classify" or "always" if this deployment ` +
352
361
  `expects doctrine-gated shell behavior.`), { phase: "config", sessionId, classification: "shell-gate-off" });
353
362
  }
354
- const shellReadBoundary = () => ({
355
- roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical],
356
- ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}),
357
- ...(executionEnv.homeDir !== undefined ? { homeDir: executionEnv.homeDir } : {}),
358
- denyMatch: (p) => readDenyMatcher?.matchPath(p)?.pattern ?? null,
359
- ...(resolvedReadFace !== undefined ? { face: resolvedReadFace } : {}),
360
- });
361
363
  const installShellSeat = (name) => {
362
364
  const tierBefore = irreversibilityTier.get(name);
363
365
  const seatOwned = !egressTools.has(name) && tierBefore !== "always" && tierBefore !== "maybe";
@@ -56,7 +56,7 @@ export const TOOL_CATALOG_ENTRIES = [
56
56
  },
57
57
  {
58
58
  id: "bash", name: "Bash", source: "builtin", definedIn: "src/tools/fs/fs-bash.ts", mountedBy: ["hands"],
59
- cards: ["bash", "bash_invalid_timeout"],
59
+ cards: ["bash", "readonly_out_of_root", "bash_invalid_timeout"],
60
60
  face: {
61
61
  family: "shell", effect: "write", contentOrigin: "execution", contract: c(FULL_SHELL_CONTRACT_ID),
62
62
  ruleFace: SHELL_RULE_FACE,
@@ -140,10 +140,22 @@ export declare function coarseReadonlyCheck(command: string, allow: ReadonlySet<
140
140
  export declare function carriesShellRedirection(text: string): boolean;
141
141
  /** What {@link splitShellCompoundSegments} produces: the connector-delimited segments, plus which of
142
142
  * them a PIPE fed (the read-only classifier's stdin arm needs that distinction; a rule lane does not). */
143
+ /** The connector bash ran a segment through — `"start"` for the first one. A closed set, because the read
144
+ * boundary distinguishes all four: `&&` is the only one under which the previous segment is known to have
145
+ * exited ZERO, `||` is the only one under which the previous segment may have been SKIPPED ENTIRELY, `|`
146
+ * puts the segment in a subshell whose directory changes never reach the parent, and `;` is plain sequence. */
147
+ export type ShellSegmentConnector = "start" | ";" | "&&" | "||" | "|";
143
148
  export interface ShellCompoundSegments {
144
149
  segments: string[];
145
- /** Parallel to {@link segments}: was this segment preceded by `|` (rather than `;`/`&&`/`||`/nothing)? */
150
+ /** Parallel to {@link segments}: was this segment preceded by `|` (rather than `;`/`&&`/`||`/nothing)?
151
+ * A VIEW of {@link connectors}, derived at the one place both are minted — two rows that could drift are
152
+ * two rows that will. */
146
153
  pipeFed: boolean[];
154
+ /** Parallel to {@link segments}: the connector bash reached this segment through. Read by the read
155
+ * boundary, where it is the difference between a directory change the shell itself proved happened and
156
+ * one this walk cannot verify. A consumer that does not read it gets the unverified reading, which is
157
+ * the fail-closed one. */
158
+ connectors: ShellSegmentConnector[];
147
159
  }
148
160
  /**
149
161
  * Split a compound command into the segments bash would run, or refuse the whole string.
@@ -228,10 +240,10 @@ export interface BashReadonlyRootBoundary {
228
240
  * allowlists classifies by its ordinary form and can auto-run a traversal unjudged);
229
241
  * ② LEXICAL only — an in-root symlink whose target is a
230
242
  * guarded path reads as its innocent spelling here, exactly as it does for the containment half
231
- * (the enforcing/canonicalizing recheck is the bash_readonly leg's job via checkedPaths; the
243
+ * (the enforcing/canonicalizing recheck is both shell faces' execute leg, via checkedPaths; the
232
244
  * classify auto-allow lane has no I/O seat by contract). The structured read faces judge BOTH
233
- * views (canonical + lexical) — this seat is the shell lane's honest-friction floor, not its
234
- * security boundary. The `bash_readonly` face deliberately does NOT wire this seat (its
245
+ * views (canonical + lexical); what this seat is and is not is stated once, at
246
+ * `shell.read_boundary.friction_floor`. The `bash_readonly` face deliberately does NOT wire this seat (its
235
247
  * roots containment + command allowlist double gate is the deployment's own read-safety
236
248
  * declaration — v1 ruling). */
237
249
  denyMatch?: (resolvedPath: string) => string | null;
@@ -315,6 +327,20 @@ export interface CompoundReadonlyVerdict {
315
327
  * and miss the other.
316
328
  */
317
329
  undecidedPaths?: readonly string[];
330
+ /** The KERNEL spellings of the {@link checkedPaths} / {@link undecidedPaths} entries whose operand
331
+ * traverses `..`: the same absolute path, `..` UNCOLLAPSED ({@link OperandSpellings.spelled}). A fold
332
+ * names a different file whenever a parent step follows a symlink — `<root>/link/../x` is
333
+ * `dirname(realpath(link))/x` to the kernel, `<root>/x` to the text. Absent when nothing traverses
334
+ * `..`, and never a replacement: a resolving caller judges these BESIDE the folded entries, list for
335
+ * list. Containment, deny judgment and every `reason` here stay decided on the FOLDED form — this file
336
+ * is zero-I/O, and only a caller holding the filesystem can tell the two apart. */
337
+ checkedSpellings?: readonly string[];
338
+ /** The kernel spellings of {@link undecidedPaths} — see {@link checkedSpellings}. */
339
+ undecidedSpellings?: readonly string[];
340
+ /** The kernel spellings of {@link outOfRootPaths} — see {@link checkedSpellings}. A caller GRANTING
341
+ * anything off a resolved out-of-root path judges this beside it: the two can disagree about whether
342
+ * the target is back inside. */
343
+ outOfRootSpellings?: readonly string[];
318
344
  /**
319
345
  * Operands (written spellings) the walk could NOT resolve at all — a `~user` form, a `~` under no declared
320
346
  * home, an argument the shell rewrites (brace/variable/command expansion), a `cd` target it cannot follow
@@ -389,6 +415,20 @@ export declare function formatOutOfRootReadApprovalOption(directory: string): st
389
415
  * has no filesystem and stays lexical, and a REMOTE env keeps the lexical behaviour (its
390
416
  * `canonicalPath` is an RPC per candidate). */
391
417
  export declare function resolveOperandLexically(base: string | undefined, operand: string, homeDir: string | undefined): string | undefined;
418
+ /** One operand's two absolute spellings. `folded` has its `..` collapsed in the TEXT and is the
419
+ * containment answer — every root comparison, deny judgment and verdict field here is built from it.
420
+ * `spelled` keeps those segments, the walk the KERNEL performs: each prefix resolves (symlinks included)
421
+ * BEFORE a parent step applies, so `<root>/link/../x` names `dirname(realpath(link))/x`. Present ONLY
422
+ * when the operand traverses `..`; a filesystem-resolving caller judges it BESIDE the folded form. */
423
+ export interface OperandSpellings {
424
+ /** The `..`-collapsed absolute path — what containment is decided on. */
425
+ folded: string;
426
+ /** The uncollapsed absolute path, when the operand traverses `..`. */
427
+ spelled?: string;
428
+ }
429
+ /** Resolve one operand into {@link OperandSpellings}, or `undefined` on the same unresolvable operands
430
+ * {@link resolveOperandLexically} reports (it is this function's folded half). */
431
+ export declare function resolveOperandSpellings(base: string | undefined, operand: string, homeDir: string | undefined): OperandSpellings | undefined;
392
432
  /** Whether a command the grammar gate REFUSED WHOLE may READ a file the boundary never judged. The answer is the
393
433
  * complement of a WHITELIST: the command is exempt only when every part of it is a shape the scan fully understands
394
434
  * and none of those shapes reads a file outside the walk's reach — everything else is read evidence, so a token the
@@ -459,7 +499,8 @@ opts?: {
459
499
  * so this face bounds where a command may NAME a file, not where the kernel will ultimately take it.
460
500
  * A deployment that needs filesystem-true containment resolves the operands itself, and this
461
501
  * verdict hands it the list to resolve ({@link CompoundReadonlyVerdict.checkedPaths}, on both arms).
462
- * `createBashReadonlyTool` is that caller for the `bash_readonly` leg and does it on a local env.
502
+ * The engine's own shell legs are that caller on a local env — the read-only face before it runs an
503
+ * allowlisted command, the full face after its gate allowed one.
463
504
  */
464
505
  export declare function classifySimpleCommandReadBoundary(command: string, boundary: BashReadonlyRootBoundary): CompoundReadonlyVerdict;
465
506
  /**