@molecule/api-ai-tools 1.5.7 → 1.5.9

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-15T21:29:34.478Z
6
+ Generated: 2026-10-16T00:26:34.364Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -58,6 +58,62 @@ npm install @molecule/api-ai-tools @molecule/api-ai
58
58
 
59
59
  ### Interfaces
60
60
 
61
+ #### `NormalizableSchema`
62
+
63
+ The JSON-Schema-ish shape every tool in this package declares.
64
+
65
+ ```typescript
66
+ interface NormalizableSchema {
67
+ name: string
68
+ parameters?: {
69
+ type?: string
70
+ properties?: Record<string, { type?: string; description?: string }>
71
+ required?: string[]
72
+ }
73
+ }
74
+ ```
75
+
76
+ #### `NormalizedInput`
77
+
78
+ The result of normalizing one tool call's input.
79
+
80
+ ```typescript
81
+ interface NormalizedInput {
82
+ /** The input to hand the handler, with aliases moved onto the declared names. */
83
+ input: Record<string, unknown>
84
+ /** Declared-required parameters still absent after aliasing. */
85
+ missing: string[]
86
+ /** `alias→declared` pairs that were moved, for logging. */
87
+ aliased: string[]
88
+ }
89
+ ```
90
+
91
+ #### `GuardOptions`
92
+
93
+ Options shared by {@link guardToolExecute} and {@link guardAITool}.
94
+
95
+ ```typescript
96
+ interface GuardOptions {
97
+ /**
98
+ * Redactor applied to a THROWN error's message before it is returned to the
99
+ * model. Defaults to this package's `redactSecrets`.
100
+ */
101
+ redactError?: (message: string) => string
102
+ }
103
+ ```
104
+
105
+ #### `ToolSchema`
106
+
107
+ JSON-schema-backed definition for a single agent tool.
108
+
109
+ ```typescript
110
+ interface ToolSchema {
111
+ name: string
112
+ description: string
113
+ parameters: JSONSchema
114
+ }
115
+ ```
116
+
61
117
  #### `ExecutionBackend`
62
118
 
63
119
  Abstraction over the execution environment.
@@ -110,117 +166,6 @@ interface ExecutionBackend {
110
166
  }
111
167
  ```
112
168
 
113
- #### `FileChangeEvent`
114
-
115
- Payload emitted when a file is created, modified, or deleted structurally.
116
-
117
- ```typescript
118
- interface FileChangeEvent {
119
- type: 'created' | 'modified' | 'deleted'
120
- path: string
121
- }
122
- ```
123
-
124
- #### `FileDiffEvent`
125
-
126
- Payload emitted when a tracked file changes contents.
127
-
128
- ```typescript
129
- interface FileDiffEvent {
130
- path: string
131
- oldContent: string | null
132
- newContent: string
133
- }
134
- ```
135
-
136
- #### `GuardOptions`
137
-
138
- Options shared by {@link guardToolExecute} and {@link guardAITool}.
139
-
140
- ```typescript
141
- interface GuardOptions {
142
- /**
143
- * Redactor applied to a THROWN error's message before it is returned to the
144
- * model. Defaults to this package's `redactSecrets`.
145
- */
146
- redactError?: (message: string) => string
147
- }
148
- ```
149
-
150
- #### `NormalizableSchema`
151
-
152
- The JSON-Schema-ish shape every tool in this package declares.
153
-
154
- ```typescript
155
- interface NormalizableSchema {
156
- name: string
157
- parameters?: {
158
- type?: string
159
- properties?: Record<string, { type?: string; description?: string }>
160
- required?: string[]
161
- }
162
- }
163
- ```
164
-
165
- #### `NormalizedInput`
166
-
167
- The result of normalizing one tool call's input.
168
-
169
- ```typescript
170
- interface NormalizedInput {
171
- /** The input to hand the handler, with aliases moved onto the declared names. */
172
- input: Record<string, unknown>
173
- /** Declared-required parameters still absent after aliasing. */
174
- missing: string[]
175
- /** `alias→declared` pairs that were moved, for logging. */
176
- aliased: string[]
177
- }
178
- ```
179
-
180
- #### `PromptContext`
181
-
182
- Context for building a composable system prompt.
183
-
184
- ```typescript
185
- interface PromptContext {
186
- /** Agent identity (e.g. 'Synthase', 'Polish Agent'). */
187
- agentName: string
188
-
189
- /** Project root path. */
190
- projectRoot: string
191
-
192
- /** Names of available tools (for the tool listing section). */
193
- tools: string[]
194
-
195
- /** Project-specific rules (AGENTS.md or CLAUDE.md content). */
196
- projectDocs?: string
197
-
198
- /** Additional skill/reference content to inject. */
199
- skills?: string[]
200
-
201
- /** Discovered skills to list in the prompt (use load_skill to read on demand). */
202
- discoveredSkills?: SkillEntry[]
203
-
204
- /** Custom sections to append to the prompt. */
205
- customSections?: string[]
206
- }
207
- ```
208
-
209
- #### `SkillEntry`
210
-
211
- Metadata for a discovered skill (used in PromptContext).
212
-
213
- ```typescript
214
- interface SkillEntry {
215
- /** Skill name. */
216
- name: string
217
- /** Short description. */
218
- description: string
219
- /** Relative path to the SKILL.md file. */
220
- path: string
221
- }
222
- ```
223
-
224
169
  #### `ToolBuildConfig`
225
170
 
226
171
  Configuration for building the tool set.
@@ -308,15 +253,70 @@ interface ToolBuildConfig {
308
253
  }
309
254
  ```
310
255
 
311
- #### `ToolSchema`
256
+ #### `FileDiffEvent`
312
257
 
313
- JSON-schema-backed definition for a single agent tool.
258
+ Payload emitted when a tracked file changes contents.
314
259
 
315
260
  ```typescript
316
- interface ToolSchema {
261
+ interface FileDiffEvent {
262
+ path: string
263
+ oldContent: string | null
264
+ newContent: string
265
+ }
266
+ ```
267
+
268
+ #### `FileChangeEvent`
269
+
270
+ Payload emitted when a file is created, modified, or deleted structurally.
271
+
272
+ ```typescript
273
+ interface FileChangeEvent {
274
+ type: 'created' | 'modified' | 'deleted'
275
+ path: string
276
+ }
277
+ ```
278
+
279
+ #### `SkillEntry`
280
+
281
+ Metadata for a discovered skill (used in PromptContext).
282
+
283
+ ```typescript
284
+ interface SkillEntry {
285
+ /** Skill name. */
317
286
  name: string
287
+ /** Short description. */
318
288
  description: string
319
- parameters: JSONSchema
289
+ /** Relative path to the SKILL.md file. */
290
+ path: string
291
+ }
292
+ ```
293
+
294
+ #### `PromptContext`
295
+
296
+ Context for building a composable system prompt.
297
+
298
+ ```typescript
299
+ interface PromptContext {
300
+ /** Agent identity (e.g. 'Synthase', 'Polish Agent'). */
301
+ agentName: string
302
+
303
+ /** Project root path. */
304
+ projectRoot: string
305
+
306
+ /** Names of available tools (for the tool listing section). */
307
+ tools: string[]
308
+
309
+ /** Project-specific rules (AGENTS.md or CLAUDE.md content). */
310
+ projectDocs?: string
311
+
312
+ /** Additional skill/reference content to inject. */
313
+ skills?: string[]
314
+
315
+ /** Discovered skills to list in the prompt (use load_skill to read on demand). */
316
+ discoveredSkills?: SkillEntry[]
317
+
318
+ /** Custom sections to append to the prompt. */
319
+ customSections?: string[]
320
320
  }
321
321
  ```
322
322
 
@@ -338,112 +338,117 @@ A tool call the handler refuses with a message meant for the model as is —
338
338
  thrown from deep inside a handler (a path check), returned by the guard as the
339
339
  call's `error` without the generic "check your parameters" wrapping.
340
340
 
341
- ### Functions
342
-
343
- #### `buildAgentPrompt(ctx)`
341
+ ```typescript
342
+ class ToolInputError extends Error {}
343
+ ```
344
344
 
345
- Build a coding-focused system prompt from composable sections.
345
+ ### Functions
346
346
 
347
- Returns a string that includes:
347
+ #### `createLocalBackend(projectRoot)`
348
348
 
349
- - Agent identity
350
- - Available tools listing
351
- - Coding best practices
352
- - Tool argument formatting guidance
353
- - Project docs (if provided)
354
- - Discovered skills listing (if provided)
355
- - Inline skills (if provided)
356
- - Custom sections (if provided)
349
+ Create an ExecutionBackend that operates on the local filesystem.
357
350
 
358
351
  ```typescript
359
- function buildAgentPrompt(ctx: PromptContext): string
352
+ function createLocalBackend(projectRoot: string): ExecutionBackend
360
353
  ```
361
354
 
362
- - `ctx` — Prompt construction inputs (tools, docs, skills, etc.).
355
+ - `projectRoot` — Absolute path to the project root directory
363
356
 
364
- **Returns:** Fully assembled system prompt text for the coding agent.
357
+ **Returns:** A backend wired to `fs/promises` and guarded subprocess calls.
365
358
 
366
- #### `buildTools(backend, config)`
359
+ #### `createSandboxBackend(sandbox, projectRoot)`
367
360
 
368
- Build a complete set of AI agent tools bound to an execution backend.
361
+ Create an ExecutionBackend that delegates to a Docker sandbox instance.
362
+ The sandbox.exec method is inherently safe — it runs inside an isolated Docker container.
369
363
 
370
364
  ```typescript
371
- function buildTools(backend: ExecutionBackend, config?: ToolBuildConfig): AITool[]
365
+ function createSandboxBackend(sandbox: SandboxLike, projectRoot?: string): ExecutionBackend
372
366
  ```
373
367
 
374
- - `backend` — The execution environment (sandbox or local filesystem)
375
- - `config` — Optional configuration for security, callbacks, and tool selection
368
+ - `sandbox` — A running Sandbox instance from `@molecule/api-code-sandbox`
369
+ - `projectRoot` — Root directory inside the sandbox (default: '/workspace')
376
370
 
377
- **Returns:** Array of AITool objects ready to pass to an AI provider
371
+ **Returns:** A backend that proxies all filesystem calls into the sandbox.
378
372
 
379
- #### `checkBlockedCommand(command)`
373
+ #### `normalizeToolInput(schema, raw)`
380
374
 
381
- Check if a command is blocked for security reasons. Returns error message or null if allowed.
375
+ Move known aliases onto the declared parameter names and report what is still
376
+ missing. Unknown keys are left in place — handlers ignore them, and dropping
377
+ them would hide a model's intent from anything reading the call log.
382
378
 
383
379
  ```typescript
384
- function checkBlockedCommand(command: string): string | null
380
+ function normalizeToolInput(schema: NormalizableSchema, raw: unknown): NormalizedInput
385
381
  ```
386
382
 
387
- - `command` — Shell command string proposed for execution.
383
+ - `schema` — The tool's declared schema.
384
+ - `raw` — Whatever the model sent as the tool input.
388
385
 
389
- **Returns:** A human-readable block reason, or `null` when the command is allowed.
386
+ **Returns:** The normalized input plus the missing-required and aliased lists.
390
387
 
391
- #### `createLocalBackend(projectRoot)`
388
+ #### `missingParamError(schema, input, missing)`
392
389
 
393
- Create an ExecutionBackend that operates on the local filesystem.
390
+ The error a tool returns when a required parameter is still absent after
391
+ aliasing. Model-facing, so it names the right parameter, what was actually
392
+ sent, and the shape of a correct call — everything needed to fix it in one
393
+ turn instead of concluding the tool is broken.
394
394
 
395
395
  ```typescript
396
- function createLocalBackend(projectRoot: string): ExecutionBackend
396
+ function missingParamError(
397
+ schema: NormalizableSchema,
398
+ input: Record<string, unknown>,
399
+ missing: string[],
400
+ ): string
397
401
  ```
398
402
 
399
- - `projectRoot` — Absolute path to the project root directory
400
-
401
- **Returns:** A backend wired to `fs/promises` and guarded subprocess calls.
402
-
403
- #### `createSandboxBackend(sandbox, projectRoot)`
404
-
405
- Create an ExecutionBackend that delegates to a Docker sandbox instance.
406
- The sandbox.exec method is inherently safe — it runs inside an isolated Docker container.
407
-
408
- ```typescript
409
- function createSandboxBackend(sandbox: SandboxLike, projectRoot?: string): ExecutionBackend
410
- ```
403
+ - `schema` — The tool's declared schema.
404
+ - `input` — The normalized input (used to list the keys that were sent).
405
+ - `missing` — The required parameters still absent.
411
406
 
412
- - `sandbox` — A running Sandbox instance from `@molecule/api-code-sandbox`
413
- - `projectRoot` — Root directory inside the sandbox (default: '/workspace')
407
+ **Returns:** A single-sentence-per-clause error string.
414
408
 
415
- **Returns:** A backend that proxies all filesystem calls into the sandbox.
409
+ #### `guardToolExecute(schema, impl)`
416
410
 
417
- #### `declaredBudgetSeconds(command, timeoutParam)`
411
+ Wrap a tool handler so every call is normalized first and a handler that
412
+ THROWS returns something the model can act on.
418
413
 
419
- The time budget a COMMAND declares for itself — a `timeout <seconds>` prefix
420
- anywhere in it, or the tool call's own `timeout` (ms) parameter, whichever is
421
- larger. Zero when it declares none.
414
+ The guard deliberately does NOT pre-empt the handler when a required
415
+ parameter looks absent: several handlers accept a documented older shape
416
+ (`edit_file`'s top-level `old_string`/`new_string`) or answer a missing
417
+ parameter with far better guidance than a generic message could (`edit_file`
418
+ telling the model to re-read the file). It steps in only where the handler
419
+ would otherwise crash — and when it does, a still-missing required parameter
420
+ is the accurate diagnosis for that crash, so it is reported as one.
422
421
 
423
422
  ```typescript
424
- function declaredBudgetSeconds(command: string, timeoutParam: unknown): number
423
+ function guardToolExecute(
424
+ schema: NormalizableSchema,
425
+ impl: (input: Record<string, unknown>, ...rest: never[]) => Promise<unknown>,
426
+ options?: GuardOptions,
427
+ ): (input: unknown, ...rest: never[]) => Promise<unknown>
425
428
  ```
426
429
 
427
- - `command` — The shell command.
428
- - `timeoutParam` — The tool call's `timeout` input, in milliseconds.
430
+ - `schema` — The tool's declared schema.
431
+ - `impl` — The raw handler.
429
432
 
430
- **Returns:** The declared budget in seconds, or 0.
433
+ **Returns:** A handler with the same signature, guarded.
431
434
 
432
- #### `directoryReadHint(message, path)`
435
+ #### `guardAITool(tool)`
433
436
 
434
- Detect a "read/edit targeted a directory, not a file" failure from a backend
435
- error message (local fs `EISDIR` or the sandbox's `cat: X: Is a directory`),
436
- and return an actionable message steering the model to `list_files`. Returns
437
- null when the error is not a directory error.
437
+ Apply {@link guardToolExecute} to a ready-made tool object (the
438
+ `@molecule/api-ai` `AITool` shape: `{ name, description, parameters,
439
+ execute }`), returning a copy whose `execute` is guarded.
440
+
441
+ Use it at the point a consumer hands its full tool list to the model, so
442
+ tools built outside this package get the same treatment as the ones built
443
+ inside it.
438
444
 
439
445
  ```typescript
440
- function directoryReadHint(message: string, path: string): string | null
446
+ function guardAITool(tool: T, options?: GuardOptions): T
441
447
  ```
442
448
 
443
- - `message` — The backend error message.
444
- - `path` — The resolved path that was targeted.
449
+ - `tool` — The tool to guard.
445
450
 
446
- **Returns:** An actionable directory-error string, or null.
451
+ **Returns:** A copy with a guarded `execute`.
447
452
 
448
453
  #### `discoverSkills(backend)`
449
454
 
@@ -460,139 +465,141 @@ function discoverSkills(backend: ExecutionBackend): Promise<SkillEntry[]>
460
465
 
461
466
  **Returns:** Array of discovered skills with name, description, and path
462
467
 
463
- #### `guardAITool(tool)`
468
+ #### `buildAgentPrompt(ctx)`
464
469
 
465
- Apply {@link guardToolExecute} to a ready-made tool object (the
466
- `@molecule/api-ai` `AITool` shape: `{ name, description, parameters,
467
- execute }`), returning a copy whose `execute` is guarded.
470
+ Build a coding-focused system prompt from composable sections.
468
471
 
469
- Use it at the point a consumer hands its full tool list to the model, so
470
- tools built outside this package get the same treatment as the ones built
471
- inside it.
472
+ Returns a string that includes:
473
+
474
+ - Agent identity
475
+ - Available tools listing
476
+ - Coding best practices
477
+ - Tool argument formatting guidance
478
+ - Project docs (if provided)
479
+ - Discovered skills listing (if provided)
480
+ - Inline skills (if provided)
481
+ - Custom sections (if provided)
472
482
 
473
483
  ```typescript
474
- function guardAITool(tool: T, options?: GuardOptions): T
484
+ function buildAgentPrompt(ctx: PromptContext): string
475
485
  ```
476
486
 
477
- - `tool` — The tool to guard.
487
+ - `ctx` — Prompt construction inputs (tools, docs, skills, etc.).
478
488
 
479
- **Returns:** A copy with a guarded `execute`.
489
+ **Returns:** Fully assembled system prompt text for the coding agent.
480
490
 
481
- #### `guardToolExecute(schema, impl)`
491
+ #### `buildTools(backend, config)`
482
492
 
483
- Wrap a tool handler so every call is normalized first and a handler that
484
- THROWS returns something the model can act on.
493
+ Build a complete set of AI agent tools bound to an execution backend.
485
494
 
486
- The guard deliberately does NOT pre-empt the handler when a required
487
- parameter looks absent: several handlers accept a documented older shape
488
- (`edit_file`'s top-level `old_string`/`new_string`) or answer a missing
489
- parameter with far better guidance than a generic message could (`edit_file`
490
- telling the model to re-read the file). It steps in only where the handler
491
- would otherwise crash — and when it does, a still-missing required parameter
492
- is the accurate diagnosis for that crash, so it is reported as one.
495
+ ```typescript
496
+ function buildTools(backend: ExecutionBackend, config?: ToolBuildConfig): AITool[]
497
+ ```
498
+
499
+ - `backend` — The execution environment (sandbox or local filesystem)
500
+ - `config` — Optional configuration for security, callbacks, and tool selection
501
+
502
+ **Returns:** Array of AITool objects ready to pass to an AI provider
503
+
504
+ #### `shellQuote(s)`
505
+
506
+ Shell-safe quoting using single quotes. Unlike JSON.stringify (double quotes),
507
+ single-quoted strings prevent command substitution ($(), backticks) and variable expansion.
493
508
 
494
509
  ```typescript
495
- function guardToolExecute(
496
- schema: NormalizableSchema,
497
- impl: (input: Record<string, unknown>, ...rest: never[]) => Promise<unknown>,
498
- options?: GuardOptions,
499
- ): (input: unknown, ...rest: never[]) => Promise<unknown>
510
+ function shellQuote(s: string): string
500
511
  ```
501
512
 
502
- - `schema` — The tool's declared schema.
503
- - `impl` — The raw handler.
513
+ - `s` — Raw string to wrap for POSIX shell single-quoted context.
504
514
 
505
- **Returns:** A handler with the same signature, guarded.
515
+ **Returns:** A single-quoted shell literal representing `s`.
506
516
 
507
- #### `isEnvFilePath(path)`
517
+ #### `stripControlChars(s)`
508
518
 
509
- Whether a path is an env file, for which {@link redactSecrets}' full env-dump
510
- treatment is appropriate rather than {@link redactSecretsInCode}. Matches
511
- `.env`, `.env.<suffix>`, and `<name>.env`.
519
+ Strip C0 control chars (except tab, newline, CR) that break PostgreSQL JSONB
520
+ and can cause rendering issues.
512
521
 
513
522
  ```typescript
514
- function isEnvFilePath(path: string): boolean
523
+ function stripControlChars(s: string): string
515
524
  ```
516
525
 
517
- - `path` — A workspace-relative or absolute file path.
526
+ - `s` — Arbitrary text that may contain disallowed control characters.
518
527
 
519
- **Returns:** `true` when the file is an env file.
528
+ **Returns:** A copy of `s` with unsafe control characters removed.
520
529
 
521
- #### `isOutsideRoot(path, projectRoot)`
530
+ #### `redactSecrets(s)`
522
531
 
523
- Whether a tool path points outside the project root once normalized (an
524
- absolute path elsewhere, or a `..` that climbs out). {@link resolvePath}
525
- clamps such a path to the root; a file tool must refuse it instead of
526
- answering about the root — a `list_files /tmp/x` used to return the
527
- workspace listing as if it were `/tmp/x` (X1 runs sonnet-m17 and sol-m05,
528
- 2026-10-14).
532
+ Redact values of common secret/credential patterns in text output.
533
+
534
+ ENV-DUMP GRADE — includes the JSON `KEY: 'value'` passes, which key off the
535
+ NAME beside the value and therefore cannot tell a credential from ordinary
536
+ code that happens to use a keyword-ish identifier. Use this for `.env` reads
537
+ and command output (where an env dump is the actual threat); use
538
+ {@link redactSecretsInCode} for source-file content.
529
539
 
530
540
  ```typescript
531
- function isOutsideRoot(path: string, projectRoot: string): boolean
541
+ function redactSecrets(s: string): string
532
542
  ```
533
543
 
534
- - `path` — The path the tool was given.
535
- - `projectRoot` — The project root.
544
+ - `s` — Log or command output that may contain `.env`-style secrets.
536
545
 
537
- **Returns:** True when the path leaves the root.
546
+ **Returns:** A redacted copy safe to surface to end users or models.
538
547
 
539
- #### `isValidGlob(pattern)`
548
+ #### `redactSecretsInCode(s)`
540
549
 
541
- Validate that a glob/include pattern is safe (no shell metacharacters that could
542
- inject). Allows alphanumeric, `* ? . _ - /` and the bracket/paren glob chars `[] ()`.
550
+ CODE-SAFE redaction — the env-assignment pass of {@link redactSecrets} WITHOUT
551
+ the JSON `KEY: 'value'` passes.
543
552
 
544
- The brackets/parens matter for real frameworks: Next.js App Router names route
545
- directories `[id]`, `[...slug]`, `(group)`, `[[...optional]]`, so without them the
546
- executor cannot `find_files`/`search_files` its own routes on any Next.js project — a
547
- hard block observed on live imports. They are injection-safe here because every caller
548
- passes the pattern through `shellQuote` before it reaches `find -name`/`grep --include`,
549
- where inside single quotes `[]()` are literal (a subshell `(...)` only starts UNquoted);
550
- to the glob engine `[abc]` is a normal character class. The genuinely dangerous
551
- metacharacters (`; | & $ \` > < \n` space) remain disallowed.
553
+ Those passes match on the NAME next to a quoted value, so over source code they
554
+ replace legitimate content at enormous scale: `forgotPasswordEndpoint:
555
+ '/users/forgot-password'`, `apiKeys: 'API keys'`, and every localized "Show
556
+ password" string all became `'[REDACTED]'`. Because the agent writes back the
557
+ content it reads, that token then lands in the user's project — measured at
558
+ 10,952 of 27,919 flagship template files before this split.
559
+
560
+ No value heuristic can fix that: a legitimate `password = 'TestPass123!'` in a
561
+ test helper is indistinguishable from a real credential by shape. So the
562
+ name-keyed passes simply do not run over code. Credentials in source are still
563
+ caught by the env-assignment pass here, and consumers layer VALUE-SHAPE
564
+ detection (vendor prefixes, PEM blocks, credentials in a URL authority) on top —
565
+ which is what actually catches a secret sitting under an innocuous name.
552
566
 
553
567
  ```typescript
554
- function isValidGlob(pattern: string): boolean
568
+ function redactSecretsInCode(s: string): string
555
569
  ```
556
570
 
557
- - `pattern` — User-supplied glob fragment for search/list operations.
571
+ - `s` — Source-file content or other code-shaped text.
558
572
 
559
- **Returns:** `true` when the pattern contains only allowed characters.
573
+ **Returns:** A redacted copy that preserves ordinary code verbatim.
560
574
 
561
- #### `missingParamError(schema, input, missing)`
575
+ #### `isEnvFilePath(path)`
562
576
 
563
- The error a tool returns when a required parameter is still absent after
564
- aliasing. Model-facing, so it names the right parameter, what was actually
565
- sent, and the shape of a correct call — everything needed to fix it in one
566
- turn instead of concluding the tool is broken.
577
+ Whether a path is an env file, for which {@link redactSecrets}' full env-dump
578
+ treatment is appropriate rather than {@link redactSecretsInCode}. Matches
579
+ `.env`, `.env.<suffix>`, and `<name>.env`.
567
580
 
568
581
  ```typescript
569
- function missingParamError(
570
- schema: NormalizableSchema,
571
- input: Record<string, unknown>,
572
- missing: string[],
573
- ): string
582
+ function isEnvFilePath(path: string): boolean
574
583
  ```
575
584
 
576
- - `schema` — The tool's declared schema.
577
- - `input` — The normalized input (used to list the keys that were sent).
578
- - `missing` — The required parameters still absent.
585
+ - `path` — A workspace-relative or absolute file path.
579
586
 
580
- **Returns:** A single-sentence-per-clause error string.
587
+ **Returns:** `true` when the file is an env file.
581
588
 
582
- #### `normalizeToolInput(schema, raw)`
589
+ #### `declaredBudgetSeconds(command, timeoutParam)`
583
590
 
584
- Move known aliases onto the declared parameter names and report what is still
585
- missing. Unknown keys are left in place — handlers ignore them, and dropping
586
- them would hide a model's intent from anything reading the call log.
591
+ The time budget a COMMAND declares for itself — a `timeout <seconds>` prefix
592
+ anywhere in it, or the tool call's own `timeout` (ms) parameter, whichever is
593
+ larger. Zero when it declares none.
587
594
 
588
595
  ```typescript
589
- function normalizeToolInput(schema: NormalizableSchema, raw: unknown): NormalizedInput
596
+ function declaredBudgetSeconds(command: string, timeoutParam: unknown): number
590
597
  ```
591
598
 
592
- - `schema` — The tool's declared schema.
593
- - `raw` — Whatever the model sent as the tool input.
599
+ - `command` — The shell command.
600
+ - `timeoutParam` — The tool call's `timeout` input, in milliseconds.
594
601
 
595
- **Returns:** The normalized input plus the missing-required and aliased lists.
602
+ **Returns:** The declared budget in seconds, or 0.
596
603
 
597
604
  #### `outputIsWithheldUntilExit(command)`
598
605
 
@@ -614,126 +621,142 @@ function outputIsWithheldUntilExit(command: string): boolean
614
621
 
615
622
  **Returns:** True when a kill would return no useful output.
616
623
 
617
- #### `parseCheckCommand(path)`
624
+ #### `outputRedirectTarget(command)`
618
625
 
619
- The shell command that parses one file and exits non-zero with the error
620
- when it does not parse: `node --check` for JavaScript, `JSON.parse` for
621
- JSON, esbuild's transform (present wherever Vite is) for TypeScript.
626
+ The file a command sends its own output to, when it redirects it (`> out.txt`,
627
+ `>> log`, `&> run.log`, `| tee [-a] file`), or null. A background command that does
628
+ this leaves its task log empty, so the log is the wrong place to look and an empty
629
+ log proves nothing (X1 flash-m29).
622
630
 
623
631
  ```typescript
624
- function parseCheckCommand(path: string): string | null
632
+ function outputRedirectTarget(command: string): string | null
625
633
  ```
626
634
 
627
- - `path` — The file's absolute path.
635
+ - `command` — The shell command.
628
636
 
629
- **Returns:** The command, or null when the file type has no cheap parser.
637
+ **Returns:** The target as written, or null (`/dev/null` and `&1`/`&2` are not files).
630
638
 
631
- #### `parseCheckProblem(result)`
639
+ #### `streamingBackgroundCommand(command)`
632
640
 
633
- What a failed parse check says to the executor, or null when the check
634
- passed or could not run (no parser available is not a syntax error).
641
+ Rewrite a command about to run in the background so its log grows while it
642
+ runs. Two shapes left the log empty until exit, and an empty log is what
643
+ `wait_for_task` reads as "stuck", so working suites were reported stuck and
644
+ killed (X1 run flash-m15 lost 20 of its 60 minutes this way, 2026-10-15):
645
+
646
+ - a trailing `| tail -N`, which holds everything back until the command ends.
647
+ It is dropped: `wait_for_task` already returns the end of the log.
648
+ - `playwright test` with no `--reporter`, which runs the config's reporter —
649
+ often `html`, which prints nothing per test. `--reporter=line` is added
650
+ when the playwright run is the command's last step and nothing is piped or
651
+ redirected from it ( aside); the results are the same, printed as they happen.
635
652
 
636
653
  ```typescript
637
- function parseCheckProblem(result: {
638
- stdout: string
639
- stderr: string
640
- exitCode: number
641
- }): string | null
654
+ function streamingBackgroundCommand(command: string): { command: string; changes: string[] }
642
655
  ```
643
656
 
644
- - `result` — The check command's result.
657
+ - `command` — The command as the executor sent it.
645
658
 
646
- **Returns:** The message for the tool result, or null.
659
+ **Returns:** The command to run and one sentence per change made (empty when none).
647
660
 
648
- #### `pathArgError(path, tool)`
661
+ #### `checkBlockedCommand(command)`
649
662
 
650
- Validate a file tool's `path` argument is a non-empty string. A weak model
651
- sometimes omits it or passes a non-string, which would otherwise crash
652
- `resolvePath` (`path.replace` on undefined) with the cryptic, unactionable
653
- "Cannot read properties of undefined (reading 'replace')" — wasting executor
654
- turns. Returns an actionable message, or null when the path is usable.
663
+ Check if a command is blocked for security reasons. Returns error message or null if allowed.
655
664
 
656
665
  ```typescript
657
- function pathArgError(path: unknown, tool: string): string | null
666
+ function checkBlockedCommand(command: string): string | null
658
667
  ```
659
668
 
660
- - `path` — The raw `path` argument from the tool input.
661
- - `tool` — The tool name, for the error message (e.g. 'read_file').
669
+ - `command` — Shell command string proposed for execution.
662
670
 
663
- **Returns:** An actionable error string, or null when `path` is a non-empty string.
671
+ **Returns:** A human-readable block reason, or `null` when the command is allowed.
664
672
 
665
- #### `redactSecrets(s)`
673
+ #### `resolvePath(path, projectRoot)`
666
674
 
667
- Redact values of common secret/credential patterns in text output.
675
+ Normalize a path to be absolute within the project root.
676
+ Empty string and '/' both resolve to projectRoot.
677
+ Rejects paths that escape via traversal or absolute paths outside root.
668
678
 
669
- ENV-DUMP GRADE — includes the JSON `KEY: 'value'` passes, which key off the
670
- NAME beside the value and therefore cannot tell a credential from ordinary
671
- code that happens to use a keyword-ish identifier. Use this for `.env` reads
672
- and command output (where an env dump is the actual threat); use
673
- {@link redactSecretsInCode} for source-file content.
679
+ ```typescript
680
+ function resolvePath(path: string, projectRoot: string): string
681
+ ```
682
+
683
+ - `path` — Relative or absolute path inside the workspace.
684
+ - `projectRoot` — Absolute filesystem root for the active project.
685
+
686
+ **Returns:** A normalized absolute path confined to `projectRoot`.
687
+
688
+ #### `isOutsideRoot(path, projectRoot)`
689
+
690
+ Whether a tool path points outside the project root once normalized (an
691
+ absolute path elsewhere, or a `..` that climbs out). {@link resolvePath}
692
+ clamps such a path to the root; a file tool must refuse it instead of
693
+ answering about the root — a `list_files /tmp/x` used to return the
694
+ workspace listing as if it were `/tmp/x` (X1 runs sonnet-m17 and sol-m05,
695
+ 2026-10-14).
674
696
 
675
697
  ```typescript
676
- function redactSecrets(s: string): string
698
+ function isOutsideRoot(path: string, projectRoot: string): boolean
677
699
  ```
678
700
 
679
- - `s` — Log or command output that may contain `.env`-style secrets.
680
-
681
- **Returns:** A redacted copy safe to surface to end users or models.
701
+ - `path` — The path the tool was given.
702
+ - `projectRoot` — The project root.
682
703
 
683
- #### `redactSecretsInCode(s)`
704
+ **Returns:** True when the path leaves the root.
684
705
 
685
- CODE-SAFE redaction — the env-assignment pass of {@link redactSecrets} WITHOUT
686
- the JSON `KEY: 'value'` passes.
706
+ #### `isValidGlob(pattern)`
687
707
 
688
- Those passes match on the NAME next to a quoted value, so over source code they
689
- replace legitimate content at enormous scale: `forgotPasswordEndpoint:
690
- '/users/forgot-password'`, `apiKeys: 'API keys'`, and every localized "Show
691
- password" string all became `'[REDACTED]'`. Because the agent writes back the
692
- content it reads, that token then lands in the user's project — measured at
693
- 10,952 of 27,919 flagship template files before this split.
708
+ Validate that a glob/include pattern is safe (no shell metacharacters that could
709
+ inject). Allows alphanumeric, `* ? . _ - /` and the bracket/paren glob chars `[] ()`.
694
710
 
695
- No value heuristic can fix that: a legitimate `password = 'TestPass123!'` in a
696
- test helper is indistinguishable from a real credential by shape. So the
697
- name-keyed passes simply do not run over code. Credentials in source are still
698
- caught by the env-assignment pass here, and consumers layer VALUE-SHAPE
699
- detection (vendor prefixes, PEM blocks, credentials in a URL authority) on top —
700
- which is what actually catches a secret sitting under an innocuous name.
711
+ The brackets/parens matter for real frameworks: Next.js App Router names route
712
+ directories `[id]`, `[...slug]`, `(group)`, `[[...optional]]`, so without them the
713
+ executor cannot `find_files`/`search_files` its own routes on any Next.js project — a
714
+ hard block observed on live imports. They are injection-safe here because every caller
715
+ passes the pattern through `shellQuote` before it reaches `find -name`/`grep --include`,
716
+ where inside single quotes `[]()` are literal (a subshell `(...)` only starts UNquoted);
717
+ to the glob engine `[abc]` is a normal character class. The genuinely dangerous
718
+ metacharacters (`; | & $ \` > < \n` space) remain disallowed.
701
719
 
702
720
  ```typescript
703
- function redactSecretsInCode(s: string): string
721
+ function isValidGlob(pattern: string): boolean
704
722
  ```
705
723
 
706
- - `s` — Source-file content or other code-shaped text.
724
+ - `pattern` — User-supplied glob fragment for search/list operations.
707
725
 
708
- **Returns:** A redacted copy that preserves ordinary code verbatim.
726
+ **Returns:** `true` when the pattern contains only allowed characters.
709
727
 
710
- #### `resolvePath(path, projectRoot)`
728
+ #### `pathArgError(path, tool)`
711
729
 
712
- Normalize a path to be absolute within the project root.
713
- Empty string and '/' both resolve to projectRoot.
714
- Rejects paths that escape via traversal or absolute paths outside root.
730
+ Validate a file tool's `path` argument is a non-empty string. A weak model
731
+ sometimes omits it or passes a non-string, which would otherwise crash
732
+ `resolvePath` (`path.replace` on undefined) with the cryptic, unactionable
733
+ "Cannot read properties of undefined (reading 'replace')" — wasting executor
734
+ turns. Returns an actionable message, or null when the path is usable.
715
735
 
716
736
  ```typescript
717
- function resolvePath(path: string, projectRoot: string): string
737
+ function pathArgError(path: unknown, tool: string): string | null
718
738
  ```
719
739
 
720
- - `path` — Relative or absolute path inside the workspace.
721
- - `projectRoot` — Absolute filesystem root for the active project.
740
+ - `path` — The raw `path` argument from the tool input.
741
+ - `tool` — The tool name, for the error message (e.g. 'read_file').
722
742
 
723
- **Returns:** A normalized absolute path confined to `projectRoot`.
743
+ **Returns:** An actionable error string, or null when `path` is a non-empty string.
724
744
 
725
- #### `shellQuote(s)`
745
+ #### `directoryReadHint(message, path)`
726
746
 
727
- Shell-safe quoting using single quotes. Unlike JSON.stringify (double quotes),
728
- single-quoted strings prevent command substitution ($(), backticks) and variable expansion.
747
+ Detect a "read/edit targeted a directory, not a file" failure from a backend
748
+ error message (local fs `EISDIR` or the sandbox's `cat: X: Is a directory`),
749
+ and return an actionable message steering the model to `list_files`. Returns
750
+ null when the error is not a directory error.
729
751
 
730
752
  ```typescript
731
- function shellQuote(s: string): string
753
+ function directoryReadHint(message: string, path: string): string | null
732
754
  ```
733
755
 
734
- - `s` — Raw string to wrap for POSIX shell single-quoted context.
756
+ - `message` — The backend error message.
757
+ - `path` — The resolved path that was targeted.
735
758
 
736
- **Returns:** A single-quoted shell literal representing `s`.
759
+ **Returns:** An actionable directory-error string, or null.
737
760
 
738
761
  #### `sliceLines(content, window)`
739
762
 
@@ -757,41 +780,6 @@ function sliceLines(
757
780
 
758
781
  **Returns:** The window plus its position, or null when neither was given.
759
782
 
760
- #### `streamingBackgroundCommand(command)`
761
-
762
- Rewrite a command about to run in the background so its log grows while it
763
- runs. Two shapes left the log empty until exit, and an empty log is what
764
- `wait_for_task` reads as "stuck", so working suites were reported stuck and
765
- killed (X1 run flash-m15 lost 20 of its 60 minutes this way, 2026-10-15):
766
-
767
- - a trailing `| tail -N`, which holds everything back until the command ends.
768
- It is dropped: `wait_for_task` already returns the end of the log.
769
- - `playwright test` with no `--reporter`, which runs the config's reporter —
770
- often `html`, which prints nothing per test. `--reporter=line` is added
771
- when the playwright run is the command's last step and nothing is piped or
772
- redirected from it ( aside); the results are the same, printed as they happen.
773
-
774
- ```typescript
775
- function streamingBackgroundCommand(command: string): { command: string; changes: string[] }
776
- ```
777
-
778
- - `command` — The command as the executor sent it.
779
-
780
- **Returns:** The command to run and one sentence per change made (empty when none).
781
-
782
- #### `stripControlChars(s)`
783
-
784
- Strip C0 control chars (except tab, newline, CR) that break PostgreSQL JSONB
785
- and can cause rendering issues.
786
-
787
- ```typescript
788
- function stripControlChars(s: string): string
789
- ```
790
-
791
- - `s` — Arbitrary text that may contain disallowed control characters.
792
-
793
- **Returns:** A copy of `s` with unsafe control characters removed.
794
-
795
783
  #### `truncate(s, maxLength)`
796
784
 
797
785
  Truncate a string to a max length with a truncation notice.
@@ -855,57 +843,61 @@ function whitespaceTolerantReplace(
855
843
 
856
844
  **Returns:** The new content if a unique fuzzy run matched, else null.
857
845
 
858
- ### Constants
846
+ #### `parseCheckCommand(path)`
859
847
 
860
- #### `DEFAULT_SEARCH_EXCLUDED_DIRS`
848
+ The shell command that parses one file and exits non-zero with the error
849
+ when it does not parse: `node --check` for JavaScript, `JSON.parse` for
850
+ JSON, esbuild's transform (present wherever Vite is) for TypeScript.
861
851
 
862
- Default directory names `search_files`/`find_files` skip — VS Code's
863
- `search.exclude` + `files.exclude` defaults (node_modules, bower_components,
864
- VCS dirs) plus the platform's vendored/build dirs. Overridable per consumer
865
- via `ToolBuildConfig.searchExcludedDirs` (a per-project, user-editable
866
- setting in molecule.dev — keep the APP-SIDE copy in
867
- `@molecule/app-ide-react`'s search types in sync with this list).
852
+ ```typescript
853
+ function parseCheckCommand(path: string): string | null
854
+ ```
855
+
856
+ - `path` — The file's absolute path.
857
+
858
+ **Returns:** The command, or null when the file type has no cheap parser.
859
+
860
+ #### `parseCheckProblem(result)`
861
+
862
+ What a failed parse check says to the executor, or null when the check
863
+ passed or could not run (no parser available is not a syntax error).
868
864
 
869
865
  ```typescript
870
- const DEFAULT_SEARCH_EXCLUDED_DIRS: readonly [
871
- 'node_modules',
872
- 'bower_components',
873
- '.git',
874
- '.svn',
875
- '.hg',
876
- 'CVS',
877
- 'dist',
878
- '.next',
879
- '.vite',
880
- 'molecule',
881
- ]
866
+ function parseCheckProblem(result: {
867
+ stdout: string
868
+ stderr: string
869
+ exitCode: number
870
+ }): string | null
882
871
  ```
883
872
 
884
- #### `MAX_BATCH_READ_BYTES`
873
+ - `result` — The check command's result.
885
874
 
886
- Total content bytes one batched `read_file` call returns, across all files.
887
- The per-file {@link MAX_READ_SIZE} still applies to each one.
875
+ **Returns:** The message for the tool result, or null.
876
+
877
+ ### Constants
878
+
879
+ #### `TOOL_SCHEMAS`
880
+
881
+ Canonical tool schemas shared by the agent runtime and documentation.
888
882
 
889
883
  ```typescript
890
- const MAX_BATCH_READ_BYTES: number
884
+ const TOOL_SCHEMAS: Record<string, ToolSchema>
891
885
  ```
892
886
 
893
- #### `MAX_BATCH_READ_FILES`
887
+ #### `MAX_READ_SIZE`
894
888
 
895
- Most files one batched `read_file` call returns. The cap exists so a survey
896
- of a large tree cannot swallow the turn's context in one result; the tool
897
- reports where it stopped so the model continues from there.
889
+ Max file size for read_file (5MB).
898
890
 
899
891
  ```typescript
900
- const MAX_BATCH_READ_FILES: 25
892
+ const MAX_READ_SIZE: number
901
893
  ```
902
894
 
903
- #### `MAX_FIND_RESULTS`
895
+ #### `MAX_WRITE_SIZE`
904
896
 
905
- Max find results.
897
+ Max content size for write_file (10MB).
906
898
 
907
899
  ```typescript
908
- const MAX_FIND_RESULTS: 100
900
+ const MAX_WRITE_SIZE: number
909
901
  ```
910
902
 
911
903
  #### `MAX_OUTPUT_SIZE`
@@ -916,6 +908,35 @@ Max command output size (100KB per stream).
916
908
  const MAX_OUTPUT_SIZE: number
917
909
  ```
918
910
 
911
+ #### `MAX_SEARCH_CONTEXT_LINES`
912
+
913
+ Most lines of surrounding context `search_files` will return per match.
914
+ Bounded so a broad pattern with a generous context cannot pull a whole tree
915
+ into one reply.
916
+
917
+ ```typescript
918
+ const MAX_SEARCH_CONTEXT_LINES: 40
919
+ ```
920
+
921
+ #### `MAX_BATCH_READ_FILES`
922
+
923
+ Most files one batched `read_file` call returns. The cap exists so a survey
924
+ of a large tree cannot swallow the turn's context in one result; the tool
925
+ reports where it stopped so the model continues from there.
926
+
927
+ ```typescript
928
+ const MAX_BATCH_READ_FILES: 25
929
+ ```
930
+
931
+ #### `MAX_BATCH_READ_BYTES`
932
+
933
+ Total content bytes one batched `read_file` call returns, across all files.
934
+ The per-file {@link MAX_READ_SIZE} still applies to each one.
935
+
936
+ ```typescript
937
+ const MAX_BATCH_READ_BYTES: number
938
+ ```
939
+
919
940
  #### `MAX_READ_RETURN_CHARS`
920
941
 
921
942
  The most one read_file call returns for ONE file when no window was asked
@@ -933,24 +954,6 @@ needs.
933
954
  const MAX_READ_RETURN_CHARS: number
934
955
  ```
935
956
 
936
- #### `MAX_READ_SIZE`
937
-
938
- Max file size for read_file (5MB).
939
-
940
- ```typescript
941
- const MAX_READ_SIZE: number
942
- ```
943
-
944
- #### `MAX_SEARCH_CONTEXT_LINES`
945
-
946
- Most lines of surrounding context `search_files` will return per match.
947
- Bounded so a broad pattern with a generous context cannot pull a whole tree
948
- into one reply.
949
-
950
- ```typescript
951
- const MAX_SEARCH_CONTEXT_LINES: 40
952
- ```
953
-
954
957
  #### `MAX_SEARCH_RESULTS`
955
958
 
956
959
  Max search results.
@@ -959,28 +962,44 @@ Max search results.
959
962
  const MAX_SEARCH_RESULTS: 50
960
963
  ```
961
964
 
962
- #### `MAX_WRITE_SIZE`
965
+ #### `MAX_FIND_RESULTS`
963
966
 
964
- Max content size for write_file (10MB).
967
+ Max find results.
965
968
 
966
969
  ```typescript
967
- const MAX_WRITE_SIZE: number
970
+ const MAX_FIND_RESULTS: 100
968
971
  ```
969
972
 
970
- #### `PARSE_CHECKABLE`
973
+ #### `DEFAULT_SEARCH_EXCLUDED_DIRS`
971
974
 
972
- Files a parse check can vouch for cheaply right after a write.
975
+ Default directory names `search_files`/`find_files` skip — VS Code's
976
+ `search.exclude` + `files.exclude` defaults (node_modules, bower_components,
977
+ VCS dirs) plus the platform's vendored/build dirs. Overridable per consumer
978
+ via `ToolBuildConfig.searchExcludedDirs` (a per-project, user-editable
979
+ setting in molecule.dev — keep the APP-SIDE copy in
980
+ `@molecule/app-ide-react`'s search types in sync with this list).
973
981
 
974
982
  ```typescript
975
- const PARSE_CHECKABLE: RegExp
983
+ const DEFAULT_SEARCH_EXCLUDED_DIRS: readonly [
984
+ 'node_modules',
985
+ 'bower_components',
986
+ '.git',
987
+ '.svn',
988
+ '.hg',
989
+ 'CVS',
990
+ 'dist',
991
+ '.next',
992
+ '.vite',
993
+ 'molecule',
994
+ ]
976
995
  ```
977
996
 
978
- #### `TOOL_SCHEMAS`
997
+ #### `PARSE_CHECKABLE`
979
998
 
980
- Canonical tool schemas shared by the agent runtime and documentation.
999
+ Files a parse check can vouch for cheaply right after a write.
981
1000
 
982
1001
  ```typescript
983
- const TOOL_SCHEMAS: Record<string, ToolSchema>
1002
+ const PARSE_CHECKABLE: RegExp
984
1003
  ```
985
1004
 
986
1005
  ## Injection Notes