pi-adx-workflow 1.1.0 → 1.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-adx-workflow",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Declarative plan → build → verify → review workflow for pi-coding-agent, with in-house isolated sub-agents and never-stop auto-continue.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -36,6 +36,8 @@ import {
36
36
  getBuildModeTools,
37
37
  getSpecModeTools,
38
38
  planModeBashGuard,
39
+ CODE_EXEC_TOOLS,
40
+ planModeExecGuard,
39
41
  } from './modes.js'
40
42
  import { loadState, mutateState, onStateWarning, setLegacyAdoption } from './store.js'
41
43
  import { composeSpecMarkdown, defaultSpecPath, specPathError, writeSpecFile } from './spec.js'
@@ -632,20 +634,22 @@ export default function adxWorkflowExtension(pi: ExtensionAPI): void {
632
634
  }
633
635
  })
634
636
 
635
- /** Plan/spec mode: guard bash tool calls against non-read-only commands. */
637
+ /** Plan/spec mode: guard code-executing tool calls (bash, ctx_execute, ctx_batch, codemode) against non-read-only operations. */
636
638
  pi.on('tool_call', (event) => {
637
- if ((planMode || specMode) && event.toolName === 'bash') {
638
- const args = (event.input ?? {}) as Record<string, unknown>
639
- const problem = planModeBashGuard(args, (msg) =>
640
- ctx?.ui.notify(msg, 'warning'),
641
- )
642
- if (problem) {
643
- // Block the call and hand the reason back to the model as a tool result —
644
- // WITHOUT terminate: the turn continues so the model can adapt (simpler
645
- // read-only variant, another tool, or proceed with the plan). The mode
646
- // itself stays on; only this one command is refused.
647
- return { block: true, reason: problem }
648
- }
639
+ if (!(planMode || specMode)) return
640
+ const args = (event.input ?? {}) as Record<string, unknown>
641
+ let problem: string | undefined
642
+ if (event.toolName === 'bash') {
643
+ problem = planModeBashGuard(args, (msg) => ctx?.ui.notify(msg, 'warning'))
644
+ } else if (CODE_EXEC_TOOLS.has(event.toolName)) {
645
+ problem = planModeExecGuard(event.toolName, args, (msg) => ctx?.ui.notify(msg, 'warning'))
646
+ }
647
+ if (problem) {
648
+ // Block the call and hand the reason back to the model as a tool result —
649
+ // WITHOUT terminate: the turn continues so the model can adapt (simpler
650
+ // read-only variant, another tool, or proceed with the plan). The mode
651
+ // itself stays on; only this one call is refused.
652
+ return { block: true, reason: problem }
649
653
  }
650
654
  })
651
655
  // ------------------------------------------------------------------ never-stop (auto-continue)
package/src/modes.ts CHANGED
@@ -303,6 +303,77 @@ export function planModeBashGuard(
303
303
  return undefined
304
304
  }
305
305
 
306
+ /**
307
+ * Code-executing tools that bypass the bash allowlist. In plan/spec mode they
308
+ * are guarded by planModeExecGuard (bash is guarded by planModeBashGuard).
309
+ * - ctx_execute: shell/python/js/ts snippet in a subprocess (full FS access)
310
+ * - ctx_batch: several shell commands + search queries
311
+ * - codemode: JS sandbox that can invoke ANY tool, including the write tools
312
+ */
313
+ export const CODE_EXEC_TOOLS = new Set(['ctx_execute', 'codemode', 'ctx_batch'])
314
+
315
+ /** Extract the ctx_execute snippet from a tool call's args (the `code` field). */
316
+ export function ctxExecuteCodeFromArgs(args: Record<string, unknown>): string {
317
+ return typeof args?.code === 'string' ? args.code : ''
318
+ }
319
+
320
+ /** Extract the ctx_batch commands from a tool call's args (the `commands` array). */
321
+ export function ctxBatchCommandsFromArgs(args: Record<string, unknown>): string[] {
322
+ return Array.isArray(args?.commands)
323
+ ? (args.commands as unknown[]).filter((c): c is string => typeof c === 'string')
324
+ : []
325
+ }
326
+
327
+ /**
328
+ * Guard code-executing tool calls (ctx_execute / ctx_batch / codemode) while in
329
+ * plan/spec mode: block calls that can write to the workspace. Returns an
330
+ * error message to inject as the tool result, or undefined when allowed.
331
+ *
332
+ * - codemode: always blocked (its JS can invoke any tool, incl. the write tools
333
+ * that plan mode disables).
334
+ * - ctx_execute: only `language: "shell"` is allowed, and the snippet must pass
335
+ * the read-only allowlist; python/js/ts snippets get full filesystem access.
336
+ * - ctx_batch: every shell command in `commands` must pass the read-only
337
+ * allowlist.
338
+ */
339
+ export function planModeExecGuard(
340
+ toolName: string,
341
+ args: Record<string, unknown>,
342
+ notify: (msg: string) => void,
343
+ ): string | undefined {
344
+ if (toolName === 'codemode') {
345
+ const msg = `Plan mode is read-only: codemode can invoke any tool (including edit/write) and is not allowed. Use read/grep/find/ls, read-only bash, or ctx_execute with language "shell" and a read-only command instead.`
346
+ notify(msg)
347
+ return msg
348
+ }
349
+ if (toolName === 'ctx_execute') {
350
+ const language = typeof args?.language === 'string' ? args.language : ''
351
+ if (language !== 'shell') {
352
+ const msg = `Plan mode is read-only: ctx_execute with language "${language || 'unknown'}" gives full filesystem access. Use language "shell" with a read-only command (e.g. git log, ls, grep) instead.`
353
+ notify(msg)
354
+ return msg
355
+ }
356
+ const code = ctxExecuteCodeFromArgs(args)
357
+ if (code && !isSafeCommand(code)) {
358
+ const msg = `Plan mode is read-only: ctx_execute shell snippet not on the read-only allowlist: ${code.slice(0, 120)}\nPlan mode stays active — retry with a read-only command (e.g. "ls -la" or "git log --oneline -3"; destructive commands like rm/mv/cp and file redirects are refused) or proceed with the plan.`
359
+ notify(msg)
360
+ return msg
361
+ }
362
+ return undefined
363
+ }
364
+ if (toolName === 'ctx_batch') {
365
+ for (const command of ctxBatchCommandsFromArgs(args)) {
366
+ if (!isSafeCommand(command)) {
367
+ const msg = `Plan mode is read-only: ctx_batch command not on the read-only allowlist: ${command.slice(0, 120)}\nPlan mode stays active — retry with read-only commands (e.g. "ls -la" or "git log --oneline -3"; destructive commands like rm/mv/cp and file redirects are refused) or proceed with the plan.`
368
+ notify(msg)
369
+ return msg
370
+ }
371
+ }
372
+ return undefined
373
+ }
374
+ return undefined
375
+ }
376
+
306
377
  /** Convenience: apply the plan-mode tool set from an ExtensionContext's active tools. */
307
378
  export function activatePlanModeTools(activeTools: string[]): string[] {
308
379
  return getPlanModeTools(activeTools)