@molecule/api-ai-tools 1.16.6 → 1.16.8

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/README.md CHANGED
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
3
  Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
4
  Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
5
  To change this document, edit the module-level JSDoc in src/index.ts.
6
- Generated: 2026-11-05T22:20:21.693Z
6
+ Generated: 2026-11-06T03:40:01.617Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -80,6 +80,36 @@ interface ForegroundBrowserProbe extends BrowserProbeLeak {
80
80
  }
81
81
  ```
82
82
 
83
+ #### `CommandPlanStep`
84
+
85
+ One sequential command, with boundaries emitted by its launcher around execution.
86
+
87
+ ```typescript
88
+ interface CommandPlanStep {
89
+ /** Launcher's stable step identity. */
90
+ id: string
91
+ /** The logical command actually executed, including launch rewrites. */
92
+ command: string
93
+ /** Printed on its own line immediately before this step runs. */
94
+ startMarker: string
95
+ /** Printed on its own line after this step settles. */
96
+ endMarker: string
97
+ }
98
+ ```
99
+
100
+ #### `CommandPlan`
101
+
102
+ A wrapper's ordered commands; metadata describes execution, never proof or completion.
103
+
104
+ ```typescript
105
+ interface CommandPlan {
106
+ /** Per-launch boundary identity, preventing ordinary suite output from matching a boundary. */
107
+ id: string
108
+ /** Steps execute in this order. */
109
+ steps: CommandPlanStep[]
110
+ }
111
+ ```
112
+
83
113
  #### `CommandSegment`
84
114
 
85
115
  One statement or pipeline stage of a shell command: what it runs, and with what.
@@ -1188,6 +1218,30 @@ interface ExecutionBackend {
1188
1218
  /** Write content to a file. Creates parent directories as needed. */
1189
1219
  writeFile(path: string, content: string): Promise<void>
1190
1220
 
1221
+ /**
1222
+ * Optional single-call path check and bounded text read for write/edit tools.
1223
+ * `realPath` must be resolved before reading; never read outside `symlinkRoot`.
1224
+ * Null `content` means missing; omit it for a native read (large/unreadable text).
1225
+ * Returned text must match readFile's decoding and honor any host file-method wrappers.
1226
+ * Implementations must use this backend's `run` so host command wrappers still apply.
1227
+ */
1228
+ readFileForUpdate?(
1229
+ path: string,
1230
+ symlinkRoot?: string,
1231
+ ): Promise<{ realPath: string; content?: string | null }>
1232
+
1233
+ /**
1234
+ * Optional write followed by the supplied parse command in one transport call.
1235
+ * Null means unsupported BEFORE a successful write; the caller uses writeFile/run.
1236
+ * Never retry a transport error after a write may have landed. After-write and
1237
+ * file-change hooks use the ordinary path so their ordering before parsing stays intact.
1238
+ */
1239
+ writeFileAndCheck?(
1240
+ path: string,
1241
+ content: string,
1242
+ command: string,
1243
+ ): Promise<{ stdout: string; stderr: string; exitCode: number } | null>
1244
+
1191
1245
  /** Delete a file. */
1192
1246
  deleteFile(path: string): Promise<void>
1193
1247
 
@@ -1710,8 +1764,8 @@ function createLocalBackend(projectRoot: string): ExecutionBackend
1710
1764
 
1711
1765
  #### `createSandboxBackend(sandbox, projectRoot)`
1712
1766
 
1713
- Create an ExecutionBackend that delegates to a Docker sandbox instance.
1714
- The sandbox.exec method is inherently safe — it runs inside an isolated Docker container.
1767
+ Create an ExecutionBackend that delegates to an isolated sandbox instance.
1768
+ The sandbox.exec method runs inside the provider's isolated environment.
1715
1769
 
1716
1770
  ```typescript
1717
1771
  function createSandboxBackend(sandbox: SandboxLike, projectRoot?: string): ExecutionBackend
@@ -1774,6 +1828,94 @@ function browserProbeOverBudgetAdvice(probe: ForegroundBrowserProbe): string
1774
1828
 
1775
1829
  **Returns:** The sentence the cut result carries.
1776
1830
 
1831
+ #### `createCommandPlan(steps)`
1832
+
1833
+ Give already-prepared logical commands unique log boundaries.
1834
+
1835
+ ```typescript
1836
+ function createCommandPlan(steps: { id: string; command: string }[]): CommandPlan
1837
+ ```
1838
+
1839
+ - `steps` — Commands in execution order.
1840
+
1841
+ **Returns:** Metadata for the wrapper and the launcher that executes its steps.
1842
+
1843
+ #### `commandWithPlan(command, plan)`
1844
+
1845
+ Preserve a wrapper's typed logical plan in its durable shell script, including across restarts.
1846
+ Shell comments keep this independent of the wrapper's executable, arguments and payload format.
1847
+
1848
+ ```typescript
1849
+ function commandWithPlan(command: string, plan: CommandPlan): string
1850
+ ```
1851
+
1852
+ - `command` — Executed wrapper.
1853
+ - `plan` — Commands the launcher will actually execute and delimit.
1854
+
1855
+ **Returns:** A shell command with a passive metadata header.
1856
+
1857
+ #### `commandPlan(command)`
1858
+
1859
+ Read the shared descriptor, never a launcher's private wrapper format.
1860
+
1861
+ ```typescript
1862
+ function commandPlan(command: string): CommandPlan | null
1863
+ ```
1864
+
1865
+ - `command` — Command or recovered task script.
1866
+
1867
+ **Returns:** A valid logical plan, or null for an ordinary command.
1868
+
1869
+ #### `logicalCommandText(command)`
1870
+
1871
+ Commands a wrapper will run, for static classification; live diagnostics use commandStepView.
1872
+
1873
+ ```typescript
1874
+ function logicalCommandText(command: string): string
1875
+ ```
1876
+
1877
+ - `command` — Executed command.
1878
+
1879
+ **Returns:** Logical commands in order, or the direct command unchanged.
1880
+
1881
+ #### `commandStepView(command, output)`
1882
+
1883
+ Select only the step that owns the latest log boundary. Completed steps cannot supply a live
1884
+ failure, target, retry policy, file selection or rerun. Before the first boundary and between
1885
+ steps the live command is unknown; no diagnostic is permitted.
1886
+
1887
+ ```typescript
1888
+ function commandStepView(
1889
+ command: string,
1890
+ output: string,
1891
+ ): {
1892
+ command: string
1893
+ output: string
1894
+ complete: boolean
1895
+ /** Unique execution step, including when adjacent steps run identical commands. */
1896
+ stepKey?: string
1897
+ }
1898
+ ```
1899
+
1900
+ - `command` — Wrapper or direct command.
1901
+ - `output` — Whole log, or the boundary-preserving bounded read.
1902
+
1903
+ **Returns:** Active command and its output, plus whether the entire step's output is available.
1904
+
1905
+ #### `commandPlanLogRead(plan, limit)`
1906
+
1907
+ AWK program for a bounded read that retains the active boundary even after a long step and
1908
+ discards prior steps. A truncated step cannot establish a universal failure across its history.
1909
+
1910
+ ```typescript
1911
+ function commandPlanLogRead(plan: CommandPlan, limit: number): string
1912
+ ```
1913
+
1914
+ - `plan` — Logical plan whose boundaries the launcher emits.
1915
+ - `limit` — Maximum output characters retained for diagnosis.
1916
+
1917
+ **Returns:** An AWK program; callers shell-quote it like every other argument.
1918
+
1777
1919
  #### `durationSeconds(word)`
1778
1920
 
1779
1921
  The seconds a duration word stands for.
@@ -4432,7 +4574,7 @@ function failureBlocks(s: string): string[]
4432
4574
 
4433
4575
  **Returns:** Each block, in order (empty when there are none).
4434
4576
 
4435
- #### `runningOutputSliceCommand(log, own)`
4577
+ #### `runningOutputSliceCommand(log, own, command)`
4436
4578
 
4437
4579
  The shell command whose stdout is the interesting part of a log that is still being written:
4438
4580
  the failure lines so far (capped at {@link RUNNING_FAILURE_LINES}), then the end of the log.
@@ -4449,6 +4591,7 @@ function runningOutputSliceCommand(log: string, own?: string): string
4449
4591
 
4450
4592
  - `log` — The task log path.
4451
4593
  - `own` — A file the command redirects its own output to, if any.
4594
+ - `command` — Executed command, whose logical plan supplies live step boundaries if present.
4452
4595
 
4453
4596
  **Returns:** The shell command, safe to hand to the execution backend as-is.
4454
4597
 
@@ -4464,7 +4607,7 @@ a duration — three bodies do not fit, and the repeat is invisible. This read i
4464
4607
  anyone; it is parsed.
4465
4608
 
4466
4609
  ```typescript
4467
- function runningCauseCommand(log: string, own?: string): string
4610
+ function runningCauseCommand(log: string, own?: string, command?: string): string
4468
4611
  ```
4469
4612
 
4470
4613
  - `log` — The task log path.
@@ -5522,6 +5665,14 @@ Bytes of a running log a repeating-cause look reads back.
5522
5665
  const REPEATING_CAUSE_TAIL_BYTES: 40000
5523
5666
  ```
5524
5667
 
5668
+ #### `COMMAND_PLAN_LOG_CHARS`
5669
+
5670
+ Whole-step history retained for wrapped plans; ordinary diagnostic logs fit inside this bound.
5671
+
5672
+ ```typescript
5673
+ const COMMAND_PLAN_LOG_CHARS: 4000000
5674
+ ```
5675
+
5525
5676
  #### `RUNNING_LOG_READ_BYTES`
5526
5677
 
5527
5678
  Bytes of a running log a diagnostic note reads back: one {@link REPEATING_CAUSE_TAIL_BYTES} window.
@@ -5654,6 +5805,12 @@ Peer dependencies:
5654
5805
 
5655
5806
  - `@molecule/api-ai`
5656
5807
 
5808
+ - **Opaque command plans:** prepare each logical step with `streamingBackgroundCommand`,
5809
+ call `createCommandPlan`, and attach it to the executed wrapper with `commandWithPlan`.
5810
+ The launcher prints each step's `startMarker` and `endMarker` on their own lines around
5811
+ execution. `commandStepView` reads the active step for diagnostics; `logicalCommandText`
5812
+ exposes the planned commands for static classification. Preserve the descriptor when
5813
+ wrapping again. The metadata is advisory and never establishes a completed test result.
5657
5814
  - **`yieldOnFirstFailureFor`** can yield one still-running deployed task's concrete diagnostic; callers opt out after a prior wait result or unknown history.
5658
5815
  - **`wait_for_task` state probes report current progress without guessing log growth.**
5659
5816
  A call shorter than one normal two-second poll omits both growth and no-growth
@@ -5723,6 +5880,11 @@ Peer dependencies:
5723
5880
  to the model, and execute `catalog.loader` like any other tool. Loading only
5724
5881
  exposes schemas, never grants permission or runs a tool. Persist its loaded
5725
5882
  names with `onLoad` to keep them available across turns and compaction.
5883
+ - **Sandbox writes batch transport work.** The optional backend operations
5884
+ `readFileForUpdate` and `writeFileAndCheck` combine path safety with the old-text
5885
+ read, then the write with its parse check. Diff tracking and redaction checks
5886
+ stay in the tools. Format and change hooks retain their ordering before parsing;
5887
+ large files and backends without these operations use the native file APIs.
5726
5888
  - **Scope the tool set per agent** with `include`/`exclude` — a read-only agent
5727
5889
  should not receive `write_file`/`exec_command`.
5728
5890
  - Outputs are size-capped (`MAX_READ_SIZE`, `MAX_OUTPUT_SIZE`, …) and searches
@@ -0,0 +1,11 @@
1
+ /** Bounded file/command batching for sandbox transports; native file APIs remain the fallback. */
2
+ import type { ExecutionBackend } from '../types.js';
3
+ /**
4
+ * Batch sandbox filesystem work through the backend's current command wrapper.
5
+ * Keeping `this.run` preserves a host's environment and module-resolution prelude.
6
+ *
7
+ * @param native - Original file delegates; host overrides must not be bypassed.
8
+ * @returns Optional backend operations that retain native fallbacks for large files.
9
+ */
10
+ export declare function sandboxFileOperations(native: Pick<ExecutionBackend, 'readFile' | 'writeFile'>): Pick<ExecutionBackend, 'readFileForUpdate' | 'writeFileAndCheck'>;
11
+ //# sourceMappingURL=sandbox-file-operations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox-file-operations.d.ts","sourceRoot":"","sources":["../../src/backends/sandbox-file-operations.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAElG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAenD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,IAAI,CAAC,gBAAgB,EAAE,UAAU,GAAG,WAAW,CAAC,GACvD,IAAI,CAAC,gBAAgB,EAAE,mBAAmB,GAAG,mBAAmB,CAAC,CA2FnE"}
@@ -0,0 +1,100 @@
1
+ /** Bounded file/command batching for sandbox transports; native file APIs remain the fallback. */
2
+ import { shellQuote } from '../utilities.js';
3
+ // envd may truncate command output; Docker also caps a shell argument at 128 KiB.
4
+ // Keep both envelopes small, and accept a read only after its complete JSON arrives.
5
+ const MAX_ENVELOPE = 24_000;
6
+ const WRITE_MARK = 'MOL_FILE_WRITTEN\n';
7
+ const NO_WRITE_MARK = 'MOL_FILE_NOT_WRITTEN\n';
8
+ // Native text APIs may strip an initial UTF-8 BOM (Response.text), whereas
9
+ // command envelopes preserve it. Keep that provider-specific decoding on the native path.
10
+ const READ_SCRIPT = `const fs=require('fs'),cp=require('child_process');const p=process.argv[1],root=process.argv[2];let realPath='';if(root){const r=cp.spawnSync('readlink',['-f',p],{encoding:'utf8'});realPath=(r.status===0?r.stdout:p).trim()}let out={realPath};if(!root||!realPath||realPath===root||realPath.startsWith(root==='/'?'/':root+'/')){try{if(fs.statSync(p).size<=24000){const content=fs.readFileSync(p,'utf8');if(content.charCodeAt(0)!==65279&&Buffer.byteLength(JSON.stringify({realPath,content}))<=24000)out={realPath,content}}}catch(e){if(e.code==='ENOENT'||e.code==='ENOTDIR')out.content=null;/* other errors use the native API */}}process.stdout.write(JSON.stringify(out))`;
11
+ const WRITE_SCRIPT = `const fs=require('fs'),cp=require('child_process'),path=require('path');const r=JSON.parse(Buffer.from(process.argv[1],'base64').toString('utf8'));try{fs.mkdirSync(path.dirname(r.path),{recursive:true});fs.writeFileSync(r.path,r.content,'utf8')}catch(e){process.stdout.write('MOL_FILE_NOT_WRITTEN\\n');process.exit(1)}process.stdout.write('MOL_FILE_WRITTEN\\n');const c=cp.spawnSync('sh',['-c',r.command],{cwd:path.dirname(r.path),encoding:'utf8',timeout:15000,maxBuffer:10*1024*1024});process.stdout.write(JSON.stringify({stdout:c.stdout||'',stderr:c.stderr||'',exitCode:c.status??1}))`;
12
+ /**
13
+ * Batch sandbox filesystem work through the backend's current command wrapper.
14
+ * Keeping `this.run` preserves a host's environment and module-resolution prelude.
15
+ *
16
+ * @param native - Original file delegates; host overrides must not be bypassed.
17
+ * @returns Optional backend operations that retain native fallbacks for large files.
18
+ */
19
+ export function sandboxFileOperations(native) {
20
+ let supported = false;
21
+ const { readFile, writeFile } = native;
22
+ return {
23
+ async readFileForUpdate(path, symlinkRoot) {
24
+ const q = shellQuote(path);
25
+ // The native API may resolve relative paths against a different base than exec.
26
+ if (!path.startsWith('/') || this.readFile !== readFile) {
27
+ if (!symlinkRoot)
28
+ return { realPath: '' };
29
+ const checked = await this.run(`readlink -f ${q} 2>/dev/null || echo ${q}`, {
30
+ timeout: 5000,
31
+ });
32
+ return { realPath: checked.stdout.trim() };
33
+ }
34
+ const result = await this.run(`node -e ${shellQuote(READ_SCRIPT)} ${q} ${shellQuote(symlinkRoot ?? '')}`, {
35
+ timeout: 5000,
36
+ });
37
+ try {
38
+ const body = JSON.parse(result.stdout);
39
+ if (body &&
40
+ typeof body === 'object' &&
41
+ 'realPath' in body &&
42
+ typeof body.realPath === 'string') {
43
+ supported = true;
44
+ if ('content' in body && (typeof body.content === 'string' || body.content === null))
45
+ return { realPath: body.realPath, content: body.content };
46
+ return { realPath: body.realPath };
47
+ }
48
+ }
49
+ catch (_error) {
50
+ // Missing Node or truncated/malformed stdout cannot supply file contents.
51
+ }
52
+ supported = false;
53
+ if (!symlinkRoot)
54
+ return { realPath: '' };
55
+ // Native path verification still decides safety when batching is unavailable.
56
+ const checked = await this.run(`readlink -f ${q} 2>/dev/null || echo ${q}`, { timeout: 5000 });
57
+ return { realPath: checked.stdout.trim() };
58
+ },
59
+ async writeFileAndCheck(path, content, command) {
60
+ if (!path.startsWith('/') || !supported || this.writeFile !== writeFile)
61
+ return null;
62
+ const request = Buffer.from(JSON.stringify({ path, content, command })).toString('base64');
63
+ if (request.length > MAX_ENVELOPE)
64
+ return null;
65
+ const result = await this.run(`node -e ${shellQuote(WRITE_SCRIPT)} ${shellQuote(request)}`, {
66
+ // The target's parent may be created by this very write. The child parse
67
+ // command enters it only after mkdir/write have succeeded.
68
+ cwd: this.projectRoot,
69
+ timeout: 20_000,
70
+ });
71
+ if (!result.stdout.startsWith(WRITE_MARK)) {
72
+ // No successful write acknowledged (e.g. no Node or a filesystem error).
73
+ // The native API retains its provider-specific error and directory behavior.
74
+ if (result.stdout.startsWith(NO_WRITE_MARK) ||
75
+ (result.exitCode === 127 && /node.*not found/.test(result.stderr)))
76
+ return null;
77
+ throw new Error('Sandbox file write returned no completion acknowledgement; not retrying an uncertain write');
78
+ }
79
+ try {
80
+ const check = JSON.parse(result.stdout.slice(WRITE_MARK.length));
81
+ if (typeof check.stdout === 'string' &&
82
+ typeof check.stderr === 'string' &&
83
+ typeof check.exitCode === 'number')
84
+ return check;
85
+ }
86
+ catch (_error) {
87
+ // The write succeeded, but its check output was truncated. Recheck only;
88
+ // never replay an acknowledged write or a failed transport.
89
+ }
90
+ return this.run(command, {
91
+ cwd: path.slice(0, path.lastIndexOf('/')) || '/',
92
+ timeout: 15_000,
93
+ }).catch((_error) => {
94
+ // Like the ordinary post-write check, an unavailable parser adds no syntax note.
95
+ // The write was acknowledged above, so only the best-effort check failed here.
96
+ return { stdout: '', stderr: '', exitCode: 0 };
97
+ });
98
+ },
99
+ };
100
+ }
@@ -14,12 +14,12 @@ interface SandboxLike {
14
14
  name: string;
15
15
  type: 'file' | 'directory';
16
16
  }>>;
17
- /** Sandbox command execution — already sandboxed within Docker. */
17
+ /** Sandbox command execution — already isolated by the provider. */
18
18
  [key: string]: any;
19
19
  }
20
20
  /**
21
- * Create an ExecutionBackend that delegates to a Docker sandbox instance.
22
- * The sandbox.exec method is inherently safe — it runs inside an isolated Docker container.
21
+ * Create an ExecutionBackend that delegates to an isolated sandbox instance.
22
+ * The sandbox.exec method runs inside the provider's isolated environment.
23
23
  *
24
24
  * @param sandbox - A running Sandbox instance from `@molecule/api-code-sandbox`
25
25
  * @param projectRoot - Root directory inside the sandbox (default: '/workspace')
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../src/backends/sandbox.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAGnD,sFAAsF;AACtF,UAAU,WAAW;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IACvC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvD,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAA;KAAE,CAAC,CAAC,CAAA;IACnF,mEAAmE;IAEnE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,WAAW,EACpB,WAAW,SAAe,GACzB,gBAAgB,CAmDlB"}
1
+ {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../src/backends/sandbox.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAInD,sFAAsF;AACtF,UAAU,WAAW;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IACvC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvD,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAA;KAAE,CAAC,CAAC,CAAA;IACnF,oEAAoE;IAEpE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,WAAW,EACpB,WAAW,SAAe,GACzB,gBAAgB,CAuDlB"}
@@ -5,16 +5,17 @@
5
5
  * @module
6
6
  */
7
7
  import { shellQuote } from '../utilities.js';
8
+ import { sandboxFileOperations } from './sandbox-file-operations.js';
8
9
  /**
9
- * Create an ExecutionBackend that delegates to a Docker sandbox instance.
10
- * The sandbox.exec method is inherently safe — it runs inside an isolated Docker container.
10
+ * Create an ExecutionBackend that delegates to an isolated sandbox instance.
11
+ * The sandbox.exec method runs inside the provider's isolated environment.
11
12
  *
12
13
  * @param sandbox - A running Sandbox instance from `@molecule/api-code-sandbox`
13
14
  * @param projectRoot - Root directory inside the sandbox (default: '/workspace')
14
15
  * @returns A backend that proxies all filesystem calls into the sandbox.
15
16
  */
16
17
  export function createSandboxBackend(sandbox, projectRoot = '/workspace') {
17
- return {
18
+ const backend = {
18
19
  projectRoot,
19
20
  readFile(path) {
20
21
  return sandbox.readFile(path);
@@ -54,9 +55,10 @@ export function createSandboxBackend(sandbox, projectRoot = '/workspace') {
54
55
  const fullCommand = budgetSeconds
55
56
  ? `o=$(mktemp); e=$(mktemp); timeout -k 5 ${budgetSeconds} bash -c ${shellQuote(anchored)} > "$o" 2> "$e" < /dev/null; c=$?; cat "$o"; cat "$e" >&2; rm -f "$o" "$e"; exit $c`
56
57
  : anchored;
57
- // sandbox.exec is the Sandbox interface method — runs inside Docker, inherently sandboxed
58
+ // sandbox.exec is the Sandbox interface method — runs inside the provider's isolated environment
58
59
  const result = await sandbox.exec(fullCommand, { timeout: opts?.timeout });
59
60
  return { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode };
60
61
  },
61
62
  };
63
+ return Object.assign(backend, typeof sandbox.exec === 'function' ? sandboxFileOperations(backend) : {});
62
64
  }
@@ -0,0 +1,71 @@
1
+ /** One sequential command, with boundaries emitted by its launcher around execution. */
2
+ export interface CommandPlanStep {
3
+ /** Launcher's stable step identity. */
4
+ id: string;
5
+ /** The logical command actually executed, including launch rewrites. */
6
+ command: string;
7
+ /** Printed on its own line immediately before this step runs. */
8
+ startMarker: string;
9
+ /** Printed on its own line after this step settles. */
10
+ endMarker: string;
11
+ }
12
+ /** A wrapper's ordered commands; metadata describes execution, never proof or completion. */
13
+ export interface CommandPlan {
14
+ /** Per-launch boundary identity, preventing ordinary suite output from matching a boundary. */
15
+ id: string;
16
+ /** Steps execute in this order. */
17
+ steps: CommandPlanStep[];
18
+ }
19
+ /**
20
+ * Give already-prepared logical commands unique log boundaries.
21
+ * @param steps - Commands in execution order.
22
+ * @returns Metadata for the wrapper and the launcher that executes its steps.
23
+ */
24
+ export declare function createCommandPlan(steps: {
25
+ id: string;
26
+ command: string;
27
+ }[]): CommandPlan;
28
+ /**
29
+ * Preserve a wrapper's typed logical plan in its durable shell script, including across restarts.
30
+ * Shell comments keep this independent of the wrapper's executable, arguments and payload format.
31
+ * @param command - Executed wrapper.
32
+ * @param plan - Commands the launcher will actually execute and delimit.
33
+ * @returns A shell command with a passive metadata header.
34
+ */
35
+ export declare function commandWithPlan(command: string, plan: CommandPlan): string;
36
+ /**
37
+ * Read the shared descriptor, never a launcher's private wrapper format.
38
+ * @param command - Command or recovered task script.
39
+ * @returns A valid logical plan, or null for an ordinary command.
40
+ */
41
+ export declare function commandPlan(command: string): CommandPlan | null;
42
+ /**
43
+ * Commands a wrapper will run, for static classification; live diagnostics use commandStepView.
44
+ * @param command - Executed command.
45
+ * @returns Logical commands in order, or the direct command unchanged.
46
+ */
47
+ export declare function logicalCommandText(command: string): string;
48
+ /**
49
+ * Select only the step that owns the latest log boundary. Completed steps cannot supply a live
50
+ * failure, target, retry policy, file selection or rerun. Before the first boundary and between
51
+ * steps the live command is unknown; no diagnostic is permitted.
52
+ * @param command - Wrapper or direct command.
53
+ * @param output - Whole log, or the boundary-preserving bounded read.
54
+ * @returns Active command and its output, plus whether the entire step's output is available.
55
+ */
56
+ export declare function commandStepView(command: string, output: string): {
57
+ command: string;
58
+ output: string;
59
+ complete: boolean;
60
+ /** Unique execution step, including when adjacent steps run identical commands. */
61
+ stepKey?: string;
62
+ };
63
+ /**
64
+ * AWK program for a bounded read that retains the active boundary even after a long step and
65
+ * discards prior steps. A truncated step cannot establish a universal failure across its history.
66
+ * @param plan - Logical plan whose boundaries the launcher emits.
67
+ * @param limit - Maximum output characters retained for diagnosis.
68
+ * @returns An AWK program; callers shell-quote it like every other argument.
69
+ */
70
+ export declare function commandPlanLogRead(plan: CommandPlan, limit: number): string;
71
+ //# sourceMappingURL=command-plan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command-plan.d.ts","sourceRoot":"","sources":["../src/command-plan.ts"],"names":[],"mappings":"AAIA,wFAAwF;AACxF,MAAM,WAAW,eAAe;IAC9B,uCAAuC;IACvC,EAAE,EAAE,MAAM,CAAA;IACV,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAA;IACf,iEAAiE;IACjE,WAAW,EAAE,MAAM,CAAA;IACnB,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,6FAA6F;AAC7F,MAAM,WAAW,WAAW;IAC1B,+FAA+F;IAC/F,EAAE,EAAE,MAAM,CAAA;IACV,mCAAmC;IACnC,KAAK,EAAE,eAAe,EAAE,CAAA;CACzB;AAKD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,WAAW,CAUvF;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,CAE1E;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CA4B/D;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAM1D;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,GACb;IACD,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,OAAO,CAAA;IACjB,mFAAmF;IACnF,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAsBA;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAQ3E"}
@@ -0,0 +1,116 @@
1
+ /** Logical commands and ordered log boundaries for launchers that execute opaque wrappers. */
2
+ import { randomUUID } from 'node:crypto';
3
+ import { gunzipSync, gzipSync } from 'node:zlib';
4
+ const HEADER = '# mol-command-plan-v1 ';
5
+ const TRUNCATED = '__MOL_COMMAND_LOG_TRUNCATED__';
6
+ /**
7
+ * Give already-prepared logical commands unique log boundaries.
8
+ * @param steps - Commands in execution order.
9
+ * @returns Metadata for the wrapper and the launcher that executes its steps.
10
+ */
11
+ export function createCommandPlan(steps) {
12
+ const id = randomUUID();
13
+ return {
14
+ id,
15
+ steps: steps.map((step, index) => ({
16
+ ...step,
17
+ startMarker: `__MOL_COMMAND_STEP_${id}_${index}_START__`,
18
+ endMarker: `__MOL_COMMAND_STEP_${id}_${index}_END__`,
19
+ })),
20
+ };
21
+ }
22
+ /**
23
+ * Preserve a wrapper's typed logical plan in its durable shell script, including across restarts.
24
+ * Shell comments keep this independent of the wrapper's executable, arguments and payload format.
25
+ * @param command - Executed wrapper.
26
+ * @param plan - Commands the launcher will actually execute and delimit.
27
+ * @returns A shell command with a passive metadata header.
28
+ */
29
+ export function commandWithPlan(command, plan) {
30
+ return `${HEADER}${gzipSync(JSON.stringify(plan)).toString('base64')}\n${command}`;
31
+ }
32
+ /**
33
+ * Read the shared descriptor, never a launcher's private wrapper format.
34
+ * @param command - Command or recovered task script.
35
+ * @returns A valid logical plan, or null for an ordinary command.
36
+ */
37
+ export function commandPlan(command) {
38
+ const header = command.split('\n').find((line) => line.startsWith(HEADER));
39
+ if (!header || header.length > 1024 * 1024)
40
+ return null;
41
+ try {
42
+ const plan = JSON.parse(gunzipSync(Buffer.from(header.slice(HEADER.length), 'base64'), {
43
+ maxOutputLength: 1024 * 1024,
44
+ }).toString('utf8'));
45
+ if (!plan ||
46
+ !/^[a-f0-9-]{36}$/.test(plan.id) ||
47
+ !Array.isArray(plan.steps) ||
48
+ plan.steps.length === 0 ||
49
+ !plan.steps.every((step, index) => typeof step?.id === 'string' &&
50
+ typeof step.command === 'string' &&
51
+ step.startMarker === `__MOL_COMMAND_STEP_${plan.id}_${index}_START__` &&
52
+ step.endMarker === `__MOL_COMMAND_STEP_${plan.id}_${index}_END__`))
53
+ return null;
54
+ return plan;
55
+ }
56
+ catch (_error) {
57
+ // Metadata is advisory. An absent or malformed descriptor supplies no logical commands.
58
+ return null;
59
+ }
60
+ }
61
+ /**
62
+ * Commands a wrapper will run, for static classification; live diagnostics use commandStepView.
63
+ * @param command - Executed command.
64
+ * @returns Logical commands in order, or the direct command unchanged.
65
+ */
66
+ export function logicalCommandText(command) {
67
+ return (commandPlan(command)
68
+ ?.steps.map((step) => step.command)
69
+ .join(';\n') ?? command);
70
+ }
71
+ /**
72
+ * Select only the step that owns the latest log boundary. Completed steps cannot supply a live
73
+ * failure, target, retry policy, file selection or rerun. Before the first boundary and between
74
+ * steps the live command is unknown; no diagnostic is permitted.
75
+ * @param command - Wrapper or direct command.
76
+ * @param output - Whole log, or the boundary-preserving bounded read.
77
+ * @returns Active command and its output, plus whether the entire step's output is available.
78
+ */
79
+ export function commandStepView(command, output) {
80
+ const plan = commandPlan(command);
81
+ if (!plan)
82
+ return { command, output, complete: true };
83
+ let at = -1;
84
+ let selected;
85
+ let offset = 0;
86
+ for (const line of output.split('\n')) {
87
+ for (const step of plan.steps) {
88
+ if (line === step.startMarker || line === step.endMarker) {
89
+ at = offset + line.length + 1;
90
+ selected = line === step.startMarker ? step : undefined;
91
+ }
92
+ }
93
+ offset += line.length + 1;
94
+ }
95
+ const text = selected && at >= 0 ? output.slice(at) : '';
96
+ return {
97
+ stepKey: selected?.startMarker,
98
+ command: selected?.command ?? '',
99
+ output: text.replace(`${TRUNCATED}\n`, ''),
100
+ complete: !text.startsWith(TRUNCATED),
101
+ };
102
+ }
103
+ /**
104
+ * AWK program for a bounded read that retains the active boundary even after a long step and
105
+ * discards prior steps. A truncated step cannot establish a universal failure across its history.
106
+ * @param plan - Logical plan whose boundaries the launcher emits.
107
+ * @param limit - Maximum output characters retained for diagnosis.
108
+ * @returns An AWK program; callers shell-quote it like every other argument.
109
+ */
110
+ export function commandPlanLogRead(plan, limit) {
111
+ const markers = plan.steps.flatMap((step) => [step.startMarker, step.endMarker]);
112
+ return (`BEGIN { ${markers.map((marker) => `marks["${marker}"]=1`).join('; ')} }\n` +
113
+ '$0 in marks { marker=$0; text=""; cut=0; next }\n' +
114
+ `{ text=text $0 "\\n"; if(length(text)>${limit}) { text=substr(text,length(text)-${limit}+1); cut=1 } }\n` +
115
+ `END { if(marker!="") { print marker; if(cut) print "${TRUNCATED}"; printf "%s",text } }`);
116
+ }
package/dist/index.d.ts CHANGED
@@ -12,6 +12,12 @@
12
12
  * (identity, tool listing, project docs, discovered skills).
13
13
  *
14
14
  * @remarks
15
+ * - **Opaque command plans:** prepare each logical step with `streamingBackgroundCommand`,
16
+ * call `createCommandPlan`, and attach it to the executed wrapper with `commandWithPlan`.
17
+ * The launcher prints each step's `startMarker` and `endMarker` on their own lines around
18
+ * execution. `commandStepView` reads the active step for diagnostics; `logicalCommandText`
19
+ * exposes the planned commands for static classification. Preserve the descriptor when
20
+ * wrapping again. The metadata is advisory and never establishes a completed test result.
15
21
  * - **`yieldOnFirstFailureFor`** can yield one still-running deployed task's concrete diagnostic; callers opt out after a prior wait result or unknown history.
16
22
  * - **`wait_for_task` state probes report current progress without guessing log growth.**
17
23
  * A call shorter than one normal two-second poll omits both growth and no-growth
@@ -81,6 +87,11 @@
81
87
  * to the model, and execute `catalog.loader` like any other tool. Loading only
82
88
  * exposes schemas, never grants permission or runs a tool. Persist its loaded
83
89
  * names with `onLoad` to keep them available across turns and compaction.
90
+ * - **Sandbox writes batch transport work.** The optional backend operations
91
+ * `readFileForUpdate` and `writeFileAndCheck` combine path safety with the old-text
92
+ * read, then the write with its parse check. Diff tracking and redaction checks
93
+ * stay in the tools. Format and change hooks retain their ordering before parsing;
94
+ * large files and backends without these operations use the native file APIs.
84
95
  * - **Scope the tool set per agent** with `include`/`exclude` — a read-only agent
85
96
  * should not receive `write_file`/`exec_command`.
86
97
  * - Outputs are size-capped (`MAX_READ_SIZE`, `MAX_OUTPUT_SIZE`, …) and searches
@@ -144,6 +155,7 @@ export * from './backends/local.js';
144
155
  export * from './backends/sandbox.js';
145
156
  export * from './browser-guard.js';
146
157
  export * from './browser-probe.js';
158
+ export * from './command-plan.js';
147
159
  export * from './command-position.js';
148
160
  export * from './cut-run.js';
149
161
  export * from './failure-kind.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6IG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,yBAAyB,CAAA;AACvC,cAAc,wBAAwB,CAAA;AACtC,cAAc,cAAc,CAAA;AAC5B,cAAc,qBAAqB,CAAA;AACnC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,mBAAmB,CAAA;AACjC,cAAc,YAAY,CAAA;AAC1B,cAAc,sBAAsB,CAAA;AACpC,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwJG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,mBAAmB,CAAA;AACjC,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,yBAAyB,CAAA;AACvC,cAAc,wBAAwB,CAAA;AACtC,cAAc,cAAc,CAAA;AAC5B,cAAc,qBAAqB,CAAA;AACnC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,mBAAmB,CAAA;AACjC,cAAc,YAAY,CAAA;AAC1B,cAAc,sBAAsB,CAAA;AACpC,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}