@molecule/api-ai-tools 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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-10-20T18:19:55.686Z
6
+ Generated: 2026-10-21T20:10:49.315Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -105,6 +105,28 @@ interface CommandSegment {
105
105
  }
106
106
  ```
107
107
 
108
+ #### `ExecutedCommand`
109
+
110
+ One command a shell command actually executes, wherever the shell runs it.
111
+
112
+ ```typescript
113
+ interface ExecutedCommand {
114
+ /**
115
+ * The word in command position, its directory removed (`/usr/bin/printenv` → `printenv`) — or
116
+ * the launcher itself when it runs alone and nothing follows it (`x | env` → `env`).
117
+ */
118
+ program: string
119
+ /**
120
+ * Its arguments, as written with quotes kept — the statement's own words AFTER the program,
121
+ * including an input redirect's path (`< /proc/self/environ`), so a reader of secret files sees
122
+ * the file it reads.
123
+ */
124
+ args: readonly string[]
125
+ /** The program and its arguments as one line, what a pattern anchored at the program matches. */
126
+ text: string
127
+ }
128
+ ```
129
+
108
130
  #### `LocatedCommandSegment`
109
131
 
110
132
  One statement of a shell command, with where it sits and the variables it sets for itself.
@@ -422,6 +444,35 @@ interface DiagnosisRunContext {
422
444
  }
423
445
  ```
424
446
 
447
+ #### `DeferredToolCatalogOptions`
448
+
449
+ Options for exposing a small tool set while retaining access to other allowed tools.
450
+
451
+ ```typescript
452
+ interface DeferredToolCatalogOptions {
453
+ /** The caller's current permission/mode-filtered tools. Discovery never grants access. */
454
+ available: () => readonly AITool[]
455
+ /** Tools that should be offered without a discovery call. */
456
+ initialNames: ReadonlySet<string>
457
+ /** Previously loaded names, in load order; useful across turns and compaction. */
458
+ loadedNames?: readonly string[]
459
+ /** Persist names after a successful load. No tool content is passed to this callback. */
460
+ onLoad?: (names: string[]) => void
461
+ }
462
+ ```
463
+
464
+ #### `DeferredToolCatalog`
465
+
466
+ A provider-independent deferred catalog. The caller still executes and authorizes tools.
467
+
468
+ ```typescript
469
+ interface DeferredToolCatalog {
470
+ loader: AITool
471
+ /** Initial tools, loader, then requested tools in stable load order. */
472
+ select: () => AITool[]
473
+ }
474
+ ```
475
+
425
476
  #### `InstalledPackageFacts`
426
477
 
427
478
  What the filesystem says about a write target under `node_modules` — the half
@@ -1670,6 +1721,32 @@ function commandSegments(command: string): CommandSegment[]
1670
1721
 
1671
1722
  **Returns:** One entry per statement or pipeline stage that runs something, in order.
1672
1723
 
1724
+ #### `executedCommands(command)`
1725
+
1726
+ Every command a shell command actually executes, at any depth — the statements themselves, the
1727
+ body of a `sh -c '<command>'`, and the body of every command substitution (`$(…)`, backticks).
1728
+
1729
+ This is the reading a policy that asks "does this command run X" needs, and it is deliberately
1730
+ not a text match. The command-position parser already knows what runs and what is only quoted
1731
+ data ({@link commandSegments}), so a pattern never reads a `|` inside a quoted grep alternation
1732
+ as a pipeline: `grep -E "USER CALLS|ENV|BUTTON" app.log` prints log lines, and the `ENV` in
1733
+ its pattern started nothing (X0 xr-molecule-flash-m331 blocked exactly that call as an
1734
+ environment dump). What the shell DOES run stays visible through the same walk: a bare
1735
+ launcher (`ls | env`), a shell string (`bash -c 'env'`), a substitution
1736
+ (`echo "$(printenv)"`) and a nested one (`sh -c 'x; env'`) are all commands here.
1737
+
1738
+ `commandSegments` answers what a pipeline stage runs for a caller matching a runner; this
1739
+ answers with the launcher left standing when it runs alone, since that is what the shell
1740
+ executes then.
1741
+
1742
+ ```typescript
1743
+ function executedCommands(command: string): ExecutedCommand[]
1744
+ ```
1745
+
1746
+ - `command` — The shell command.
1747
+
1748
+ **Returns:** Each command the shell executes, in the order the shell reaches them.
1749
+
1673
1750
  #### `locatedCommandSegments(command)`
1674
1751
 
1675
1752
  Every statement of a command, with its own assignments and where it sits — assignment-only
@@ -2183,7 +2260,8 @@ function pipelineExitCodeNote(command: string, exitCode: number, output: string)
2183
2260
  Whether a command, read from its TEXT alone, starts a server that stays up in the foreground.
2184
2261
 
2185
2262
  The over-budget reader's shape: at that point the command has already run, and only its text is
2186
- left to say whether "run a smaller unit" was ever the right advice.
2263
+ left to say whether "run a smaller unit" was ever the right advice. Inline/stdin programs
2264
+ remain unclassified and use the normal bounded command runner.
2187
2265
 
2188
2266
  ```typescript
2189
2267
  function startsServerProgram(command: string): boolean
@@ -2196,7 +2274,9 @@ function startsServerProgram(command: string): boolean
2196
2274
  #### `foregroundServerCommand(command, readScript)`
2197
2275
 
2198
2276
  A FOREGROUND command that starts a server, including one that only names a script whose SOURCE
2199
- imports the app's server and calls its `create()`/`listen()`.
2277
+ imports the app's server and calls its `create()`/`listen()`. Inline/stdin programs
2278
+ (`node -e …`, `python3 -c …`) use normal bounded execution: interpreting arbitrary
2279
+ program text is outside this filename/script preflight's contract.
2200
2280
 
2201
2281
  The script read is best-effort: a path the backend will not answer for is a command this reading
2202
2282
  cannot classify, never a refusal. A backgrounded launch, a test runner and a test file are all
@@ -2491,6 +2571,24 @@ function buildAgentPrompt(ctx: PromptContext): string
2491
2571
 
2492
2572
  **Returns:** Fully assembled system prompt text for the coding agent.
2493
2573
 
2574
+ #### `createDeferredToolCatalog(options)`
2575
+
2576
+ Offer common tools and let the model load other schemas by exact name.
2577
+
2578
+ This is ordinary allowlist selection, not semantic routing or authorization.
2579
+ Pass tools already filtered by the current mode and permissions. Loading never
2580
+ executes a tool; the caller must keep its normal execution-time guards. Loaded
2581
+ schemas append in request order and stay available, avoiding an LRU that changes
2582
+ the schema prefix on every request. Only names need persistence across turns.
2583
+
2584
+ ```typescript
2585
+ function createDeferredToolCatalog(options: DeferredToolCatalogOptions): DeferredToolCatalog
2586
+ ```
2587
+
2588
+ - `options` — Allowed tools, initial selection and optional durable load state.
2589
+
2590
+ **Returns:** A loader tool and the current provider-facing selection.
2591
+
2494
2592
  #### `taskStopCommand(backend, taskId)`
2495
2593
 
2496
2594
  The command that stops a running background task, read from the sandbox's own pid files.
@@ -2948,6 +3046,24 @@ function redactSecretsInCode(s: string): string
2948
3046
 
2949
3047
  **Returns:** A redacted copy that preserves ordinary code verbatim.
2950
3048
 
3049
+ #### `addedRedactionMask(next, previous)`
3050
+
3051
+ The redaction mask `next` adds over `previous`, or null when it adds none.
3052
+
3053
+ `previous` is the file's previous content for `write_file`, and the replacement's own
3054
+ `old_string` for `edit_file` — an edit that carries an existing mask through (a file that
3055
+ already had one, read back and rewritten) adds no occurrence. A new occurrence may
3056
+ be copied output or intentional source; the caller declares the latter explicitly.
3057
+
3058
+ ```typescript
3059
+ function addedRedactionMask(next: string, previous: string): string | null
3060
+ ```
3061
+
3062
+ - `next` — The content about to be written (or the replacement's `new_string`).
3063
+ - `previous` — The content it replaces, already on disk (or the `old_string`).
3064
+
3065
+ **Returns:** The added mask as written (`'[REDACTED]'`, `"[REDACTED]"`, `=[REDACTED]`), or null.
3066
+
2951
3067
  #### `printsOnlySource(command)`
2952
3068
 
2953
3069
  Whether a shell command only PRINTS source files (`cat a.tsx`, `sed -n '1,40p' x.ts`,
@@ -2964,6 +3080,35 @@ function printsOnlySource(command: string): boolean
2964
3080
 
2965
3081
  **Returns:** True when the command prints source files, with only literal headings and filters.
2966
3082
 
3083
+ #### `isReadOnlyInspection(command)`
3084
+
3085
+ Whether a shell command only INSPECTS the workspace WITHOUT printing a file's contents:
3086
+ it prints source files (a pure literal read, {@link printsOnlySource}), lists names
3087
+ (`ls`, `find`), or counts lines (`wc`) — joined by `;`, `&&` or `|`, with literal
3088
+ `echo`/`printf` headings and `cd`.
3089
+
3090
+ Its output is code or names, not an env dump, so it gets the code-safe redaction
3091
+ ({@link redactSecretsInCode}) instead of the name-keyed env-dump passes. Without this,
3092
+ ordinary code values such as `tokenKey: 'jwtToken'` in a mixed source read and
3093
+ `ls | head` command would be masked by the environment-dump rules.
3094
+
3095
+ A RECURSIVE SEARCH IS NOT INSPECTION (`grep -rn x .`, `rg x src/`): its output is the
3096
+ content of whichever files matched, including `.env` files and JSON secret dumps, so it
3097
+ keeps the full grade. A path with no extension is not evidence that it is a directory —
3098
+ `/dev/stdin` and an ordinary `secrets` file read the same way.
3099
+
3100
+ Env dumps keep the full grade: `.env` anywhere in the command, `env`/`printenv`, a
3101
+ non-literal or redirecting form, and an interpreter one-liner (`node -e`, `python -c`)
3102
+ all return false.
3103
+
3104
+ ```typescript
3105
+ function isReadOnlyInspection(command: string): boolean
3106
+ ```
3107
+
3108
+ - `command` — The shell command.
3109
+
3110
+ **Returns:** True when the command only reads or lists workspace content.
3111
+
2967
3112
  #### `isEnvFilePath(path)`
2968
3113
 
2969
3114
  Whether a path is an env file, for which {@link redactSecrets}' full env-dump
@@ -3323,6 +3468,15 @@ function suiteRunPriorityLaunch(command: string, launch: string): { launch: stri
3323
3468
 
3324
3469
  Check if a command is blocked for security reasons. Returns error message or null if allowed.
3325
3470
 
3471
+ The first check reads the commands the shell actually RUNS — the statements, a `sh -c`
3472
+ string's body, and every command substitution ({@link executedCommands}) — so a `|` inside a
3473
+ quoted grep alternation is data, not a pipeline: `grep -E "USER CALLS|ENV|BUTTON|passed|failed"
3474
+ app.log` prints log lines, and the `ENV` in its pattern started nothing (X0
3475
+ xr-molecule-flash-m331 blocked exactly that call as an environment dump, twice). What the
3476
+ shell DOES run stays visible through the same walk: `ls | env`, `bash -c 'env'`,
3477
+ `echo "$(printenv)"` and a nested `sh -c 'x; env'` are all commands, and so is `env` itself
3478
+ when nothing follows it.
3479
+
3326
3480
  ```typescript
3327
3481
  function checkBlockedCommand(command: string): string | null
3328
3482
  ```
@@ -4253,7 +4407,10 @@ const BACKGROUNDED: RegExp
4253
4407
  A program that SERVES: the scaffold's entry (`server.ts`/`server.js`), a package manager's
4254
4408
  `start`/`dev`/`serve`/`preview` script, a Vite dev or preview server, `next start|dev`, or a
4255
4409
  Python WSGI server. Matched against a command segment's own text, and against a script's SOURCE
4256
- when the command only names the script file.
4410
+ when the command only names the script file — but NEVER against an inline program's code
4411
+ (`node -e …`, `python3 -c …`), where this text is the user's program rather than a file being
4412
+ run: `require('…/node_modules/vite')` imports a module, it does not start a dev server
4413
+ (xr-molecule-flash-m391 +13:13, refused as "node starts a server" for exactly that line).
4257
4414
 
4258
4415
  ```typescript
4259
4416
  const SERVER_PROGRAM: RegExp
@@ -4858,6 +5015,11 @@ Peer dependencies:
4858
5015
  as a broken tool and routes around, taking its verification with it. Use
4859
5016
  `guardAITool` to give tools built elsewhere the same treatment, passing
4860
5017
  `redactError` so a thrown message is masked by your own redactor.
5018
+ - **Defer occasional tools** with `createDeferredToolCatalog`: pass your current
5019
+ permission-filtered tools and a small initial name set, offer `catalog.select()`
5020
+ to the model, and execute `catalog.loader` like any other tool. Loading only
5021
+ exposes schemas, never grants permission or runs a tool. Persist its loaded
5022
+ names with `onLoad` to keep them available across turns and compaction.
4861
5023
  - **Scope the tool set per agent** with `include`/`exclude` — a read-only agent
4862
5024
  should not receive `write_file`/`exec_command`.
4863
5025
  - Outputs are size-capped (`MAX_READ_SIZE`, `MAX_OUTPUT_SIZE`, …) and searches
@@ -74,6 +74,43 @@ export declare function endsInBackground(command: string): boolean;
74
74
  * @returns One entry per statement or pipeline stage that runs something, in order.
75
75
  */
76
76
  export declare function commandSegments(command: string): CommandSegment[];
77
+ /** One command a shell command actually executes, wherever the shell runs it. */
78
+ export interface ExecutedCommand {
79
+ /**
80
+ * The word in command position, its directory removed (`/usr/bin/printenv` → `printenv`) — or
81
+ * the launcher itself when it runs alone and nothing follows it (`x | env` → `env`).
82
+ */
83
+ program: string;
84
+ /**
85
+ * Its arguments, as written with quotes kept — the statement's own words AFTER the program,
86
+ * including an input redirect's path (`< /proc/self/environ`), so a reader of secret files sees
87
+ * the file it reads.
88
+ */
89
+ args: readonly string[];
90
+ /** The program and its arguments as one line, what a pattern anchored at the program matches. */
91
+ text: string;
92
+ }
93
+ /**
94
+ * Every command a shell command actually executes, at any depth — the statements themselves, the
95
+ * body of a `sh -c '<command>'`, and the body of every command substitution (`$(…)`, backticks).
96
+ *
97
+ * This is the reading a policy that asks "does this command run X" needs, and it is deliberately
98
+ * not a text match. The command-position parser already knows what runs and what is only quoted
99
+ * data ({@link commandSegments}), so a pattern never reads a `|` inside a quoted grep alternation
100
+ * as a pipeline: `grep -E "USER CALLS|ENV|BUTTON" app.log` prints log lines, and the `ENV` in
101
+ * its pattern started nothing (X0 xr-molecule-flash-m331 blocked exactly that call as an
102
+ * environment dump). What the shell DOES run stays visible through the same walk: a bare
103
+ * launcher (`ls | env`), a shell string (`bash -c 'env'`), a substitution
104
+ * (`echo "$(printenv)"`) and a nested one (`sh -c 'x; env'`) are all commands here.
105
+ *
106
+ * `commandSegments` answers what a pipeline stage runs for a caller matching a runner; this
107
+ * answers with the launcher left standing when it runs alone, since that is what the shell
108
+ * executes then.
109
+ *
110
+ * @param command - The shell command.
111
+ * @returns Each command the shell executes, in the order the shell reaches them.
112
+ */
113
+ export declare function executedCommands(command: string): ExecutedCommand[];
77
114
  /**
78
115
  * One statement of a shell command, with where it sits and the variables it sets for itself.
79
116
  *
@@ -1 +1 @@
1
- {"version":3,"file":"command-position.d.ts","sourceRoot":"","sources":["../src/command-position.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,uFAAuF;AACvF,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,OAAO,EAAE,MAAM,CAAA;IACf,qFAAqF;IACrF,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAgSD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM3D;AAikBD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEzD;AA8GD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAIjE;AAmCD;;;;;;;;;GASG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,OAAO,EAAE,cAAc,GAAG,IAAI,CAAA;IAC9B,0FAA0F;IAC1F,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B;;;;OAIG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B;;;OAGG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;;;OAIG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB,EAAE,CAE/E;AAmDD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,GAAG,EAAE,MAAM,GAAG,IAAI,GACjB,MAAM,GAAG,IAAI,CAyDf;AA4GD;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOpE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,GAC1C;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAoD7C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAK/E"}
1
+ {"version":3,"file":"command-position.d.ts","sourceRoot":"","sources":["../src/command-position.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,uFAAuF;AACvF,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,OAAO,EAAE,MAAM,CAAA;IACf,qFAAqF;IACrF,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAgSD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM3D;AAynBD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEzD;AA8GD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAIjE;AAED,iFAAiF;AACjF,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,iGAAiG;IACjG,IAAI,EAAE,MAAM,CAAA;CACb;AAmFD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,EAAE,CAGnE;AAmCD;;;;;;;;;GASG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,OAAO,EAAE,cAAc,GAAG,IAAI,CAAA;IAC9B,0FAA0F;IAC1F,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B;;;;OAIG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B;;;OAGG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;;;OAIG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB,EAAE,CAE/E;AAmDD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,GAAG,EAAE,MAAM,GAAG,IAAI,GACjB,MAAM,GAAG,IAAI,CAyDf;AA4GD;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOpE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,GAC1C;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAoD7C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAK/E"}
@@ -694,14 +694,23 @@ function blankHeredocBodies(command) {
694
694
  * `molecule-dev/api/src/ai/supplied-tree-guard.ts` — so a script that does not classify records
695
695
  * no runner, and every run of it reads as nothing).
696
696
  *
697
+ * The walk also RECORDS the command substitutions it enters (`$(…)` and backticks), because the
698
+ * shell runs those too: a reader that only saw the outer statements could not tell that
699
+ * `echo "$(printenv)"` prints the environment ({@link executedCommands}).
700
+ *
697
701
  * @param command - The shell command.
698
702
  * @returns Each statement's text, the offset its first character sits at, and the separator that
699
703
  * ended it (`|`, `&&`, `;`, a newline, … — '' after the last one), in order, with empty ones
700
- * dropped.
704
+ * dropped — and the bodies of the command substitutions this level enters, as offsets.
701
705
  */
702
- function splitSegments(command) {
706
+ function splitCommand(command) {
703
707
  const source = blankHeredocBodies(command);
704
708
  const parts = [];
709
+ const substitutions = [];
710
+ /** Where each open `$(` body starts; -1 for a bare `(` opened inside one. */
711
+ const subOpens = [];
712
+ /** Where each open backtick body starts. */
713
+ const tickOpens = [];
705
714
  let buffer = '';
706
715
  /** The offset in the command of the current buffer's first character. */
707
716
  let bufferStart = 0;
@@ -739,6 +748,32 @@ function splitSegments(command) {
739
748
  stack.pop();
740
749
  continue;
741
750
  }
751
+ // Backticks open a command substitution wherever one runs — outside quotes, inside double
752
+ // quotes, and inside another substitution or a parameter expansion — but never inside single
753
+ // quotes (handled above). Its body is a command of its own: nothing in it splits THIS command,
754
+ // and a `'` or `"` in it is the nested command's, not this one's.
755
+ if (top !== 'tick' && char === '`') {
756
+ stack.push('tick');
757
+ tickOpens.push(i + 1);
758
+ put(char, -1);
759
+ continue;
760
+ }
761
+ if (top === 'tick') {
762
+ if (char === '\\' && next !== undefined) {
763
+ // An escaped character is literal: `\`` does not close the substitution.
764
+ put(char + next, -1);
765
+ i += 1;
766
+ continue;
767
+ }
768
+ if (char === '`') {
769
+ stack.pop();
770
+ const opened = tickOpens.pop();
771
+ if (opened !== undefined)
772
+ substitutions.push({ start: opened, end: i });
773
+ }
774
+ put(char, -1);
775
+ continue;
776
+ }
742
777
  // Comments are shell syntax only in a command context, including an unquoted `$(`.
743
778
  // Blank their bytes so quotes/operators cannot affect this walk, while every later
744
779
  // position and depth index stays aligned with the original command. Keep the newline.
@@ -753,6 +788,7 @@ function splitSegments(command) {
753
788
  // inside single quotes (handled above). The `(` is consumed here so the depth is counted once.
754
789
  if (char === '$' && next === '(') {
755
790
  stack.push('sub');
791
+ subOpens.push(i + 2);
756
792
  put('$(', -1);
757
793
  i += 1;
758
794
  continue;
@@ -802,10 +838,16 @@ function splitSegments(command) {
802
838
  // Inside a substitution or a subshell of one: its own parentheses nest, its `|`/`;`/`&&`
803
839
  // belong to the nested command, and its newlines are that command's too.
804
840
  if (top === 'sub') {
805
- if (char === '(')
841
+ if (char === '(') {
806
842
  stack.push('sub');
807
- else if (char === ')')
843
+ subOpens.push(-1);
844
+ }
845
+ else if (char === ')') {
808
846
  stack.pop();
847
+ const opened = subOpens.pop();
848
+ if (opened !== undefined && opened >= 0)
849
+ substitutions.push({ start: opened, end: i });
850
+ }
809
851
  put(char, -1);
810
852
  continue;
811
853
  }
@@ -861,7 +903,16 @@ function splitSegments(command) {
861
903
  });
862
904
  lastAfter = part.after;
863
905
  }
864
- return out;
906
+ return { parts: out, substitutions };
907
+ }
908
+ /**
909
+ * A command's statements and pipeline stages — {@link splitCommand}'s parts on their own.
910
+ *
911
+ * @param command - The shell command.
912
+ * @returns Each statement, in order, with empty ones dropped.
913
+ */
914
+ function splitSegments(command) {
915
+ return splitCommand(command).parts;
865
916
  }
866
917
  /**
867
918
  * Whether the outer shell's final statement starts a background job.
@@ -983,6 +1034,114 @@ function leadingAssignments(text) {
983
1034
  export function commandSegments(command) {
984
1035
  return locatedSegmentsOf(command, 0).flatMap((located) => located.segment === null ? [] : [located.segment]);
985
1036
  }
1037
+ /**
1038
+ * A statement's words with its redirections taken out, so the walk that finds the program does
1039
+ * not read a redirect's target as the program: `env > out.txt` RUNS `env`, and `cat > f` runs
1040
+ * `cat` with no file argument. Each word kept carries its index in `words`, because a caller
1041
+ * reading the statement's ARGUMENTS wants it as written — an input redirect's path is one.
1042
+ *
1043
+ * @param words - One statement's words, quotes kept.
1044
+ * @returns The words that are not redirections, and each one's index in `words`.
1045
+ */
1046
+ function withoutRedirects(words) {
1047
+ const kept = [];
1048
+ const index = [];
1049
+ for (let i = 0; i < words.length; i += 1) {
1050
+ const word = words[i] ?? '';
1051
+ // An operator written on its own owns the word after it (`> out.txt`, `< /proc/self/environ`).
1052
+ if (REDIRECT_OPERATOR.test(word)) {
1053
+ i += 1;
1054
+ continue;
1055
+ }
1056
+ // An operator with its target attached (`2>&1`, `>out.txt`, `&>log`) is one word.
1057
+ if (REDIRECT_WORD.test(word))
1058
+ continue;
1059
+ kept.push(word);
1060
+ index.push(i);
1061
+ }
1062
+ return { words: kept, index };
1063
+ }
1064
+ /**
1065
+ * The index of the last launcher a statement names (`x | env`, `sudo nice`), or null when it
1066
+ * names none.
1067
+ *
1068
+ * A statement that only NAMES a launcher runs that launcher: `x | env` pipes x's output into
1069
+ * `env`, which prints the environment, and the walk that reports what a command runs has to say
1070
+ * so ({@link executedCommands}).
1071
+ *
1072
+ * @param words - The statement's words, redirections already taken out.
1073
+ * @returns The launcher's index, or null.
1074
+ */
1075
+ function lastLauncherIndex(words) {
1076
+ for (let i = words.length - 1; i >= 0; i -= 1) {
1077
+ if (LAUNCHERS.has(programName(words[i] ?? '')))
1078
+ return i;
1079
+ }
1080
+ return null;
1081
+ }
1082
+ /**
1083
+ * {@link executedCommands}'s recursion: a statement's own `sh -c '<command>'`, and every command
1084
+ * substitution it carries, is a command of its own.
1085
+ *
1086
+ * @param command - The shell command, or one nested command string.
1087
+ * @param depth - How many nested command strings this one already sits inside.
1088
+ * @returns Each command the shell executes, in order.
1089
+ */
1090
+ function executedCommandsOf(command, depth) {
1091
+ if (typeof command !== 'string' || depth > MAX_NESTING)
1092
+ return [];
1093
+ const out = [];
1094
+ const { parts, substitutions } = splitCommand(command);
1095
+ for (const part of parts) {
1096
+ const words = segmentWords(part.text);
1097
+ const placed = withoutRedirects(words);
1098
+ const start = commandStart(placed.words);
1099
+ // A shell's `-c '<command>'` is the command: what the shell itself does is run it.
1100
+ if (start.nested !== null) {
1101
+ out.push(...executedCommandsOf(start.nested, depth + 1));
1102
+ continue;
1103
+ }
1104
+ const at = placed.words[start.at] === undefined ? lastLauncherIndex(placed.words) : start.at;
1105
+ if (at === null)
1106
+ continue;
1107
+ const program = start.program ?? programName(placed.words[at] ?? '');
1108
+ if (!program)
1109
+ continue;
1110
+ // Arguments from the statement as written, from the word after the program: `cat < /proc/…`
1111
+ // reads that file, and a reader of secret files has to see it.
1112
+ const args = words.slice((placed.index[at] ?? 0) + 1);
1113
+ out.push({ program, args, text: [program, ...args].join(' ') });
1114
+ }
1115
+ for (const substitution of substitutions) {
1116
+ out.push(...executedCommandsOf(command.slice(substitution.start, substitution.end), depth + 1));
1117
+ }
1118
+ return out;
1119
+ }
1120
+ /**
1121
+ * Every command a shell command actually executes, at any depth — the statements themselves, the
1122
+ * body of a `sh -c '<command>'`, and the body of every command substitution (`$(…)`, backticks).
1123
+ *
1124
+ * This is the reading a policy that asks "does this command run X" needs, and it is deliberately
1125
+ * not a text match. The command-position parser already knows what runs and what is only quoted
1126
+ * data ({@link commandSegments}), so a pattern never reads a `|` inside a quoted grep alternation
1127
+ * as a pipeline: `grep -E "USER CALLS|ENV|BUTTON" app.log` prints log lines, and the `ENV` in
1128
+ * its pattern started nothing (X0 xr-molecule-flash-m331 blocked exactly that call as an
1129
+ * environment dump). What the shell DOES run stays visible through the same walk: a bare
1130
+ * launcher (`ls | env`), a shell string (`bash -c 'env'`), a substitution
1131
+ * (`echo "$(printenv)"`) and a nested one (`sh -c 'x; env'`) are all commands here.
1132
+ *
1133
+ * `commandSegments` answers what a pipeline stage runs for a caller matching a runner; this
1134
+ * answers with the launcher left standing when it runs alone, since that is what the shell
1135
+ * executes then.
1136
+ *
1137
+ * @param command - The shell command.
1138
+ * @returns Each command the shell executes, in the order the shell reaches them.
1139
+ */
1140
+ export function executedCommands(command) {
1141
+ if (typeof command !== 'string' || !command.trim())
1142
+ return [];
1143
+ return executedCommandsOf(command, 0);
1144
+ }
986
1145
  /**
987
1146
  * Where a statement's word sits in its own raw text, as {@link segmentWords} counts the words.
988
1147
  *
@@ -1 +1 @@
1
- {"version":3,"file":"cut-run.d.ts","sourceRoot":"","sources":["../src/cut-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAMH;;;GAGG;AACH,eAAO,MAAM,aAAa,kBAAkB,CAAA;AAE5C,kFAAkF;AAClF,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,OAAO,aAAa,CAAA;AAE1D;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,eAAe,CAGhE;AAiBD;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB,wEAAwE;IACxE,QAAQ,EAAE,OAAO,CAAA;CAClB;AAkDD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CA4B9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAKD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,GACb,MAAM,GAAG,IAAI,CAIf;AAED,0BAA0B;AAC1B,MAAM,MAAM,KAAK;AACf,sEAAsE;AACpE,aAAa;AACf,oDAAoD;GAClD,iBAAiB;AACnB;;;;GAIG;GACD,cAAc,CAAA;AAElB,wDAAwD;AACxD,MAAM,WAAW,MAAM;IACrB,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAA;IACf,kFAAkF;IAClF,SAAS,EAAE,KAAK,CAAA;IAChB;;;;;;OAMG;IACH,UAAU,EAAE,OAAO,CAAA;IACnB;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB;;;OAGG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;;OAGG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,qCAAqC;IACrC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,yEAAyE;IACzE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;AAsDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,aAAa,SAAI,EACjB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CA4Ff;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAmB1D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAyClD"}
1
+ {"version":3,"file":"cut-run.d.ts","sourceRoot":"","sources":["../src/cut-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAMH;;;GAGG;AACH,eAAO,MAAM,aAAa,kBAAkB,CAAA;AAE5C,kFAAkF;AAClF,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,OAAO,aAAa,CAAA;AAE1D;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,eAAe,CAGhE;AAiBD;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB,wEAAwE;IACxE,QAAQ,EAAE,OAAO,CAAA;CAClB;AAkDD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CA4B9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAKD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,GACb,MAAM,GAAG,IAAI,CAIf;AAED,0BAA0B;AAC1B,MAAM,MAAM,KAAK;AACf,sEAAsE;AACpE,aAAa;AACf,oDAAoD;GAClD,iBAAiB;AACnB;;;;GAIG;GACD,cAAc,CAAA;AAElB,wDAAwD;AACxD,MAAM,WAAW,MAAM;IACrB,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAA;IACf,kFAAkF;IAClF,SAAS,EAAE,KAAK,CAAA;IAChB;;;;;;OAMG;IACH,UAAU,EAAE,OAAO,CAAA;IACnB;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB;;;OAGG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;;OAGG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,qCAAqC;IACrC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,yEAAyE;IACzE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;AAsDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,aAAa,SAAI,EACjB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CAgGf;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAmB1D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAyClD"}
package/dist/cut-run.js CHANGED
@@ -345,12 +345,16 @@ export function commandCutRun(command, result, elapsedMs, budgetSeconds = 0, raw
345
345
  inlineScript,
346
346
  };
347
347
  }
348
- // The runner printed a summary that does not account for the total it announced, and neither
349
- // bound above fired: the process was stopped from outside the run. A run that used the whole
350
- // budget is the backend's deadline (which sits just past it); anything else is a stop this tool
351
- // does not own. Either way the summary counts are all that ran, and a pipeline's 0 is not the
352
- // run's code — the run never reached its own verdict.
353
- if (summary !== null && summary.announced !== null) {
348
+ // A runner's summary that does not account for the total it announced, in a command that
349
+ // actually RUNS that runner, and neither bound above fired: the process was stopped from
350
+ // outside the run. The command must run the runner — a reader of a LIVE run's log prints the
351
+ // same announcement and partial tally as text it copies, and reporting that as a stopped run
352
+ // told the executor a suite it had only grepped had been killed (X0 xr-molecule-flash-m345,
353
+ // +50:00: `grep -B2 -A12 … /tmp/mol-bg-….log | head -40` came back `stoppedBy: process-stop`
354
+ // after 1 s). A run that used the whole budget is the backend's deadline (which sits just past
355
+ // it); anything else is a stop this tool does not own. Either way the summary counts are all
356
+ // that ran, and a pipeline's 0 is not the run's code — the run never reached its own verdict.
357
+ if (suite && summary !== null && summary.announced !== null) {
354
358
  const usedBudget = budgetSeconds > 0 && elapsedMs >= atBoundMs(budgetSeconds);
355
359
  return {
356
360
  seconds: usedBudget ? budgetSeconds : Math.max(1, Math.round(elapsedMs / 1000)),
package/dist/index.d.ts CHANGED
@@ -70,6 +70,11 @@
70
70
  * as a broken tool and routes around, taking its verification with it. Use
71
71
  * `guardAITool` to give tools built elsewhere the same treatment, passing
72
72
  * `redactError` so a thrown message is masked by your own redactor.
73
+ * - **Defer occasional tools** with `createDeferredToolCatalog`: pass your current
74
+ * permission-filtered tools and a small initial name set, offer `catalog.select()`
75
+ * to the model, and execute `catalog.loader` like any other tool. Loading only
76
+ * exposes schemas, never grants permission or runs a tool. Persist its loaded
77
+ * names with `onLoad` to keep them available across turns and compaction.
73
78
  * - **Scope the tool set per agent** with `include`/`exclude` — a read-only agent
74
79
  * should not receive `write_file`/`exec_command`.
75
80
  * - Outputs are size-capped (`MAX_READ_SIZE`, `MAX_OUTPUT_SIZE`, …) and searches
@@ -147,6 +152,7 @@ export * from './server-command.js';
147
152
  export * from './sleep-peek.js';
148
153
  export * from './suite-overlap.js';
149
154
  export * from './system-prompt.js';
155
+ export * from './tool-catalog.js';
150
156
  export * from './tools.js';
151
157
  export * from './trace-artifacts.js';
152
158
  export * from './types.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkIG;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,cAAc,CAAA;AAC5B,cAAc,qBAAqB,CAAA;AACnC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuIG;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,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"}
package/dist/index.js CHANGED
@@ -70,6 +70,11 @@
70
70
  * as a broken tool and routes around, taking its verification with it. Use
71
71
  * `guardAITool` to give tools built elsewhere the same treatment, passing
72
72
  * `redactError` so a thrown message is masked by your own redactor.
73
+ * - **Defer occasional tools** with `createDeferredToolCatalog`: pass your current
74
+ * permission-filtered tools and a small initial name set, offer `catalog.select()`
75
+ * to the model, and execute `catalog.loader` like any other tool. Loading only
76
+ * exposes schemas, never grants permission or runs a tool. Persist its loaded
77
+ * names with `onLoad` to keep them available across turns and compaction.
73
78
  * - **Scope the tool set per agent** with `include`/`exclude` — a read-only agent
74
79
  * should not receive `write_file`/`exec_command`.
75
80
  * - Outputs are size-capped (`MAX_READ_SIZE`, `MAX_OUTPUT_SIZE`, …) and searches
@@ -147,6 +152,7 @@ export * from './server-command.js';
147
152
  export * from './sleep-peek.js';
148
153
  export * from './suite-overlap.js';
149
154
  export * from './system-prompt.js';
155
+ export * from './tool-catalog.js';
150
156
  export * from './tools.js';
151
157
  export * from './trace-artifacts.js';
152
158
  export * from './types.js';
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAElD;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,UAAU,CAAA;CACvB;AAED;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CA8V1C,CAAA"}
1
+ {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAElD;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,UAAU,CAAA;CACvB;AAED;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAwW1C,CAAA"}
package/dist/schemas.js CHANGED
@@ -71,6 +71,10 @@ export const TOOL_SCHEMAS = {
71
71
  parameters: {
72
72
  type: 'object',
73
73
  properties: {
74
+ intentional_redaction_placeholder: {
75
+ type: 'boolean',
76
+ description: 'Set true only when intentionally authoring a literal [REDACTED] placeholder. Default false: newly copied output placeholders are refused. This never disables secret-output masking.',
77
+ },
74
78
  path: {
75
79
  type: 'string',
76
80
  description: 'Path to the file to write.',
@@ -89,6 +93,10 @@ export const TOOL_SCHEMAS = {
89
93
  parameters: {
90
94
  type: 'object',
91
95
  properties: {
96
+ intentional_redaction_placeholder: {
97
+ type: 'boolean',
98
+ description: 'Set true only when intentionally authoring a literal [REDACTED] placeholder. Default false: newly copied output placeholders are refused. This never disables secret-output masking.',
99
+ },
92
100
  path: {
93
101
  type: 'string',
94
102
  description: 'Path to the file to edit.',