@doingdev/opencode-claude-manager-plugin 0.1.61 → 0.1.62

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.
@@ -122,6 +122,7 @@ export class TeamOrchestrator {
122
122
  persistSession: true,
123
123
  includePartialMessages: true,
124
124
  permissionMode: 'acceptEdits',
125
+ allowedTools: workerCaps?.sessionAllowedTools,
125
126
  restrictWriteTools: input.mode === 'explore' || (workerCaps?.restrictWriteTools ?? false),
126
127
  model: input.model,
127
128
  effort: (workerCaps?.restrictWriteTools ?? false)
@@ -12,6 +12,10 @@ export function buildWorkerCapabilities(prompts) {
12
12
  plannerEligible: false,
13
13
  isRuntimeUnavailableResponse: (text) => text.trimStart().startsWith('PLAYWRIGHT_UNAVAILABLE:'),
14
14
  runtimeUnavailableTitle: '❌ Playwright unavailable',
15
+ // Pre-approve the Playwriter toolchain at the SDK level so headless sessions
16
+ // never stall waiting for interactive confirmation. Write tools remain blocked
17
+ // by restrictWriteTools and the canUseTool write-filter.
18
+ sessionAllowedTools: ['Skill', 'Bash', 'Read', 'Grep', 'Glob', 'LS', 'ListDirectory'],
15
19
  },
16
20
  };
17
21
  }
@@ -122,6 +122,7 @@ export class TeamOrchestrator {
122
122
  persistSession: true,
123
123
  includePartialMessages: true,
124
124
  permissionMode: 'acceptEdits',
125
+ allowedTools: workerCaps?.sessionAllowedTools,
125
126
  restrictWriteTools: input.mode === 'explore' || (workerCaps?.restrictWriteTools ?? false),
126
127
  model: input.model,
127
128
  effort: (workerCaps?.restrictWriteTools ?? false)
@@ -12,6 +12,10 @@ export function buildWorkerCapabilities(prompts) {
12
12
  plannerEligible: false,
13
13
  isRuntimeUnavailableResponse: (text) => text.trimStart().startsWith('PLAYWRIGHT_UNAVAILABLE:'),
14
14
  runtimeUnavailableTitle: '❌ Playwright unavailable',
15
+ // Pre-approve the Playwriter toolchain at the SDK level so headless sessions
16
+ // never stall waiting for interactive confirmation. Write tools remain blocked
17
+ // by restrictWriteTools and the canUseTool write-filter.
18
+ sessionAllowedTools: ['Skill', 'Bash', 'Read', 'Grep', 'Glob', 'LS', 'ListDirectory'],
15
19
  },
16
20
  };
17
21
  }
@@ -206,6 +206,13 @@ export interface WorkerCapabilities {
206
206
  isRuntimeUnavailableResponse?: (finalText: string) => boolean;
207
207
  /** Metadata title for the runtime-unavailable event. */
208
208
  runtimeUnavailableTitle?: string;
209
+ /**
210
+ * Explicit SDK-level pre-approval list for this worker's inner Claude Code session.
211
+ * Tools in this list bypass interactive confirmation prompts (they are still subject to
212
+ * the `canUseTool` write-restriction and approval-policy filters).
213
+ * Absent = falls back to `['Skill']` via the default adapter behaviour.
214
+ */
215
+ sessionAllowedTools?: string[];
209
216
  }
210
217
  export interface GitDiffResult {
211
218
  hasDiff: boolean;
@@ -498,6 +498,45 @@ describe('ClaudeAgentSdkAdapter', () => {
498
498
  const gitLogResult = await capturedCanUseTool('Bash', { command: 'git log --oneline -10' }, {});
499
499
  expect(gitLogResult.behavior).toBe('allow');
500
500
  });
501
+ it('allows Playwriter-style bash commands when restrictWriteTools is true', async () => {
502
+ let capturedCanUseTool;
503
+ const adapter = new ClaudeAgentSdkAdapter({
504
+ query: (params) => {
505
+ capturedCanUseTool = params.options?.canUseTool;
506
+ return createFakeQuery([
507
+ {
508
+ type: 'result',
509
+ subtype: 'success',
510
+ session_id: 'ses_playwriter',
511
+ is_error: false,
512
+ result: 'ok',
513
+ num_turns: 1,
514
+ total_cost_usd: 0,
515
+ },
516
+ ]);
517
+ },
518
+ listSessions: async () => [],
519
+ getSessionMessages: async () => [],
520
+ });
521
+ await adapter.runSession({
522
+ cwd: '/tmp/project',
523
+ prompt: 'Run Playwriter tests',
524
+ restrictWriteTools: true,
525
+ });
526
+ expect(capturedCanUseTool).toBeDefined();
527
+ // Non-destructive Playwriter commands must be allowed
528
+ const playwriterResult = await capturedCanUseTool('Bash', { command: 'playwriter session new' }, {});
529
+ expect(playwriterResult.behavior).toBe('allow');
530
+ const npxResult = await capturedCanUseTool('Bash', { command: 'npx playwriter session new' }, {});
531
+ expect(npxResult.behavior).toBe('allow');
532
+ const listResult = await capturedCanUseTool('Bash', { command: 'playwriter session list' }, {});
533
+ expect(listResult.behavior).toBe('allow');
534
+ // Destructive commands must still be denied
535
+ const rmResult = await capturedCanUseTool('Bash', { command: 'rm -rf /tmp/screenshots' }, {});
536
+ expect(rmResult.behavior).toBe('deny');
537
+ const redirectResult = await capturedCanUseTool('Bash', { command: 'echo "data" > out.txt' }, {});
538
+ expect(redirectResult.behavior).toBe('deny');
539
+ });
501
540
  it('allows write tools when restrictWriteTools is false', async () => {
502
541
  let capturedCanUseTool;
503
542
  const adapter = new ClaudeAgentSdkAdapter({
@@ -345,6 +345,59 @@ describe('TeamOrchestrator', () => {
345
345
  expect(error.message).toContain('BrowserQA is a browser QA specialist');
346
346
  expect(error.message).toContain('does not support implement mode');
347
347
  });
348
+ it('forwards sessionAllowedTools to runTask when worker has them configured', async () => {
349
+ tempRoot = await mkdtemp(join(tmpdir(), 'browserqa-allowed-'));
350
+ const runTask = vi.fn().mockResolvedValueOnce({
351
+ sessionId: 'ses_qa',
352
+ events: [],
353
+ finalText: 'Done.',
354
+ turns: 1,
355
+ totalCostUsd: 0.01,
356
+ inputTokens: 500,
357
+ outputTokens: 100,
358
+ contextWindowSize: 200_000,
359
+ });
360
+ const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Engineer prompt', 'Synthesis prompt', {
361
+ BrowserQA: {
362
+ ...BROWSER_QA_TEST_CAPS,
363
+ sessionAllowedTools: ['Skill', 'Bash', 'Read', 'Grep', 'Glob', 'LS', 'ListDirectory'],
364
+ },
365
+ });
366
+ await orchestrator.dispatchEngineer({
367
+ teamId: 'team-1',
368
+ cwd: tempRoot,
369
+ engineer: 'BrowserQA',
370
+ mode: 'explore',
371
+ message: 'Run Playwriter tests',
372
+ });
373
+ expect(runTask).toHaveBeenCalledOnce();
374
+ const taskInput = runTask.mock.calls[0]?.[0];
375
+ expect(taskInput.allowedTools).toEqual(expect.arrayContaining(['Skill', 'Bash', 'Read', 'Grep', 'Glob', 'LS', 'ListDirectory']));
376
+ });
377
+ it('passes undefined allowedTools for standard engineers without sessionAllowedTools', async () => {
378
+ tempRoot = await mkdtemp(join(tmpdir(), 'engineer-no-allowed-'));
379
+ const runTask = vi.fn().mockResolvedValueOnce({
380
+ sessionId: 'ses_tom',
381
+ events: [],
382
+ finalText: 'Done.',
383
+ turns: 1,
384
+ totalCostUsd: 0.01,
385
+ inputTokens: 500,
386
+ outputTokens: 100,
387
+ contextWindowSize: 200_000,
388
+ });
389
+ const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
390
+ await orchestrator.dispatchEngineer({
391
+ teamId: 'team-1',
392
+ cwd: tempRoot,
393
+ engineer: 'Tom',
394
+ mode: 'explore',
395
+ message: 'Investigate something',
396
+ });
397
+ expect(runTask).toHaveBeenCalledOnce();
398
+ const taskInput = runTask.mock.calls[0]?.[0];
399
+ expect(taskInput.allowedTools).toBeUndefined();
400
+ });
348
401
  it('classifyError returns modeNotSupported for implement-mode rejection', () => {
349
402
  const result = TeamOrchestrator.classifyError(new Error('BrowserQA is a browser QA specialist and does not support implement mode. ' +
350
403
  'It can only verify and explore.'));
@@ -206,6 +206,13 @@ export interface WorkerCapabilities {
206
206
  isRuntimeUnavailableResponse?: (finalText: string) => boolean;
207
207
  /** Metadata title for the runtime-unavailable event. */
208
208
  runtimeUnavailableTitle?: string;
209
+ /**
210
+ * Explicit SDK-level pre-approval list for this worker's inner Claude Code session.
211
+ * Tools in this list bypass interactive confirmation prompts (they are still subject to
212
+ * the `canUseTool` write-restriction and approval-policy filters).
213
+ * Absent = falls back to `['Skill']` via the default adapter behaviour.
214
+ */
215
+ sessionAllowedTools?: string[];
209
216
  }
210
217
  export interface GitDiffResult {
211
218
  hasDiff: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doingdev/opencode-claude-manager-plugin",
3
- "version": "0.1.61",
3
+ "version": "0.1.62",
4
4
  "description": "OpenCode plugin that orchestrates Claude Code sessions.",
5
5
  "keywords": [
6
6
  "opencode",