From bc9f1f01487fc21ea74c92fe8089035d20ae5029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20J=C3=A4gle?= Date: Fri, 1 May 2026 19:48:33 +0200 Subject: [PATCH] feat(opencode-plugin): show meaningful permission patterns in web UI Replace generic patterns: ['*'] in the wrap() ask() call with tool-specific human-readable strings (e.g. 'target_phase: code', 'workflow: epcc') so the opencode web permission dialog shows useful context instead of just '*'. - Add buildPermissionPatterns(toolName, args) helper in plugin.ts - Update wrap() to use it and pass actual args as metadata - Remove redundant dead-code ask() calls from all 5 tool handlers (they were never shown since wrap() already granted always: ['*']) --- ...ix-opencode-plugin-permissions-x4bahi.json | 29 +++++ ...nt-plan-fix-opencode-plugin-permissions.md | 107 ++++++++++++++++++ packages/opencode-plugin/src/plugin.ts | 64 ++++++++++- .../src/tool-handlers/conduct-review.ts | 10 -- .../src/tool-handlers/proceed-to-phase.ts | 10 -- .../src/tool-handlers/reset-development.ts | 10 -- .../src/tool-handlers/setup-project-docs.ts | 12 +- .../src/tool-handlers/start-development.ts | 10 -- 8 files changed, 199 insertions(+), 53 deletions(-) create mode 100644 .vibe/beads-state-workflows-fix-opencode-plugin-permissions-x4bahi.json create mode 100644 .vibe/development-plan-fix-opencode-plugin-permissions.md diff --git a/.vibe/beads-state-workflows-fix-opencode-plugin-permissions-x4bahi.json b/.vibe/beads-state-workflows-fix-opencode-plugin-permissions-x4bahi.json new file mode 100644 index 00000000..1246a54a --- /dev/null +++ b/.vibe/beads-state-workflows-fix-opencode-plugin-permissions-x4bahi.json @@ -0,0 +1,29 @@ +{ + "conversationId": "workflows-fix-opencode-plugin-permissions-x4bahi", + "projectPath": "/Users/oliverjaegle/projects/privat/codemcp/workflows", + "epicId": "responsible-vibe-34", + "phaseTasks": [ + { + "phaseId": "explore", + "phaseName": "Explore", + "taskId": "responsible-vibe-34.1" + }, + { + "phaseId": "plan", + "phaseName": "Plan", + "taskId": "responsible-vibe-34.2" + }, + { + "phaseId": "code", + "phaseName": "Code", + "taskId": "responsible-vibe-34.3" + }, + { + "phaseId": "commit", + "phaseName": "Commit", + "taskId": "responsible-vibe-34.4" + } + ], + "createdAt": "2026-05-01T14:05:52.022Z", + "updatedAt": "2026-05-01T14:05:52.022Z" +} \ No newline at end of file diff --git a/.vibe/development-plan-fix-opencode-plugin-permissions.md b/.vibe/development-plan-fix-opencode-plugin-permissions.md new file mode 100644 index 00000000..c1041db6 --- /dev/null +++ b/.vibe/development-plan-fix-opencode-plugin-permissions.md @@ -0,0 +1,107 @@ +# Development Plan: workflows (fix/opencode-plugin-permissions branch) + +*Generated on 2026-05-01 by Vibe Feature MCP* +*Workflow: [epcc](https://codemcp.github.io/workflows/workflows/epcc)* + +## Goal + +Fix the opencode-plugin so that when opencode on the web asks for permissions, it shows meaningful parameter information instead of just `*`. For example, when `proceed_to_phase` is called, the user should see the target phase and reason, not just `*`. + +## Key Decisions + +### KD-1: Fix location is wrap() in plugin.ts, not individual tool handlers + +The `wrap()` function in `plugin.ts` (lines 644-667) is the ONLY `ask()` call that actually presents a dialog to the user. The individual tool handler `ask()` calls are **dead code** — they are auto-allowed because `wrap()` already granted `always: ['*']`. + +Therefore, the fix must be in `wrap()`, passing the `args` object through to build meaningful `patterns`. + +### KD-2: Individual tool handler ask() calls will be removed + +Since the handler `ask()` calls are never shown (auto-allowed by wrap's `always: ['*']`), they are misleading dead code. They will be removed to keep the code clean and avoid future confusion. + +### KD-3: patterns array format — human-readable key:value strings + +The web UI (`session-permission-dock.tsx`) only shows `props.request.patterns`. We will build a `patterns` array of human-readable strings like: +- `"workflow: epcc"` for `start_development` +- `"target_phase: code"`, `"reason: ..."` for `proceed_to_phase` +- `"target_phase: code"` for `conduct_review` +- `"delete_plan: true"`, `"reason: ..."` for `reset_development` +- `"architecture: arc42"`, `"requirements: ears"`, `"design: comprehensive"` for `setup_project_docs` + +Undefined/null/missing values are omitted. If no args produce meaningful patterns, fall back to `['*']`. + +### KD-4: metadata populated with args + +The `metadata` field in the `wrap()` `ask()` call is changed from `{}` to the actual `args` object so future consumers also get tool-specific info. + +### KD-5: buildPermissionPatterns helper colocated in plugin.ts + +The helper function is small and tightly coupled to `wrap()`, so it lives in `plugin.ts` rather than a separate utility file. + +## Notes + +### Architecture: Two-layer ask() (discovered in Explore phase) + +1. `wrap()` in `plugin.ts`: The FIRST and only effectively-shown `ask()`. Currently uses `patterns: ['*']` and `metadata: {}`. +2. Individual tool handlers: Each also calls `ask()` with tool-specific metadata, but since `always: ['*']` was already granted by `wrap()`, these second calls are auto-allowed and **never shown to the user**. + +### Web UI vs TUI difference + +- **Web UI** (`session-permission-dock.tsx`): Shows `props.request.patterns` only — we must put meaningful strings in `patterns`. +- **TUI** (`permission.tsx`): Reads `part.state.input` (raw tool call args) — already shows rich details, no change needed. + +### Patterns format per tool + +| Tool | Patterns | +|---|---| +| `start_development` | `workflow: ` | +| `proceed_to_phase` | `target_phase: `, `reason: ` (if present) | +| `conduct_review` | `target_phase: ` | +| `reset_development` | `delete_plan: ` (if true), `reason: ` (if present) | +| `setup_project_docs` | `architecture: `, `requirements: `, `design: ` | + +### Relevant files + +- `packages/opencode-plugin/src/plugin.ts` — **Primary change**: `wrap()` and new `buildPermissionPatterns()` helper +- `packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts` — Remove redundant `ask()` +- `packages/opencode-plugin/src/tool-handlers/start-development.ts` — Remove redundant `ask()` +- `packages/opencode-plugin/src/tool-handlers/conduct-review.ts` — Remove redundant `ask()` +- `packages/opencode-plugin/src/tool-handlers/reset-development.ts` — Remove redundant `ask()` +- `packages/opencode-plugin/src/tool-handlers/setup-project-docs.ts` — Remove redundant `ask()` + +## Explore + +### Tasks + +*Auto-synced — do not edit here, use `bd` CLI instead.* + +- [x] `responsible-vibe-34.1.1` Explore how opencode web UI displays permission patterns +- [x] `responsible-vibe-34.1.2` Fix patterns in all tool ask() calls to show meaningful info +- [x] `responsible-vibe-34.1.3` Fix top-level wrap() ask() call in plugin.ts + +## Plan + +### Tasks + +*Auto-synced — do not edit here, use `bd` CLI instead.* + +- [x] `responsible-vibe-34.2.1` Design buildPermissionPatterns helper function +- [x] `responsible-vibe-34.2.2` Plan removal of redundant ask() calls in tool handlers +- [x] `responsible-vibe-34.2.3` Define patterns format per tool + +## Code + +### Tasks + +*Auto-synced — do not edit here, use `bd` CLI instead.* + +- [x] `responsible-vibe-34.3.1` Add buildPermissionPatterns() helper to plugin.ts and update wrap() +- [x] `responsible-vibe-34.3.2` Remove redundant ask() calls from all 5 tool handlers +- [x] `responsible-vibe-34.3.3` Build and type-check the plugin + +## Commit + +### Tasks + +*Auto-synced — do not edit here, use `bd` CLI instead.* + diff --git a/packages/opencode-plugin/src/plugin.ts b/packages/opencode-plugin/src/plugin.ts index 04c5f323..345141ba 100644 --- a/packages/opencode-plugin/src/plugin.ts +++ b/packages/opencode-plugin/src/plugin.ts @@ -642,6 +642,63 @@ ACTION REQUIRED: Use proceed_to_phase tool to move to a phase that allows editin * an error if the agent is not allowed to use workflows. */ tool: await (async (): Promise<{ [key: string]: ToolDefinition }> => { + /** + * Build human-readable permission patterns for the web UI. + * The opencode web permission dialog only shows `patterns`, so we put + * meaningful "key: value" strings here instead of the generic '*'. + */ + const buildPermissionPatterns = ( + toolName: string, + args: Record + ): string[] => { + const entry = (key: string, value: unknown): string | null => { + if (value === undefined || value === null || value === '') + return null; + return `${key}: ${value}`; + }; + + switch (toolName) { + case 'start_development': { + const patterns = [entry('workflow', args['workflow'])].filter( + (p): p is string => p !== null + ); + return patterns.length > 0 ? patterns : ['*']; + } + case 'proceed_to_phase': { + const patterns = [ + entry('target_phase', args['target_phase']), + entry('reason', args['reason']), + ].filter((p): p is string => p !== null); + return patterns.length > 0 ? patterns : ['*']; + } + case 'conduct_review': { + const patterns = [ + entry('target_phase', args['target_phase']), + ].filter((p): p is string => p !== null); + return patterns.length > 0 ? patterns : ['*']; + } + case 'reset_development': { + const patterns = [ + args['delete_plan'] === true + ? entry('delete_plan', args['delete_plan']) + : null, + entry('reason', args['reason']), + ].filter((p): p is string => p !== null); + return patterns.length > 0 ? patterns : ['*']; + } + case 'setup_project_docs': { + const patterns = [ + entry('architecture', args['architecture']), + entry('requirements', args['requirements']), + entry('design', args['design']), + ].filter((p): p is string => p !== null); + return patterns.length > 0 ? patterns : ['*']; + } + default: + return ['*']; + } + }; + const wrap = (toolName: string, def: ToolDefinition): ToolDefinition => ({ ...def, execute: async (args, ctx) => { @@ -656,9 +713,12 @@ ACTION REQUIRED: Use proceed_to_phase tool to move to a phase that allows editin await Effect.runPromise( ctx.ask({ permission: toolName, - patterns: ['*'], + patterns: buildPermissionPatterns( + toolName, + args as Record + ), always: ['*'], - metadata: {}, + metadata: args as Record, }) ); diff --git a/packages/opencode-plugin/src/tool-handlers/conduct-review.ts b/packages/opencode-plugin/src/tool-handlers/conduct-review.ts index 5d3fbb1a..820204fa 100644 --- a/packages/opencode-plugin/src/tool-handlers/conduct-review.ts +++ b/packages/opencode-plugin/src/tool-handlers/conduct-review.ts @@ -29,16 +29,6 @@ export function createConductReviewTool( logger.debug('conduct_review called', { targetPhase: target_phase }); - // Request permission before conducting review - if (context && typeof context.ask === 'function') { - await context.ask({ - permission: 'conduct_review', - patterns: ['*'], - always: ['*'], - metadata: { target_phase }, - }); - } - try { // Delegate to ConductReviewHandler const handler = new ConductReviewHandler(); diff --git a/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts b/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts index 3a9a0b43..2e4339d6 100644 --- a/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts +++ b/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts @@ -40,16 +40,6 @@ export function createProceedToPhaseTool( logger.debug('proceed_to_phase called', { to: target_phase, reason }); - // Request permission before proceeding to new phase - if (context && typeof context.ask === 'function') { - await context.ask({ - permission: 'proceed_to_phase', - patterns: ['*'], - always: ['*'], - metadata: { target_phase, reason }, - }); - } - try { // Delegate to ProceedToPhaseHandler const handler = new ProceedToPhaseHandler(); diff --git a/packages/opencode-plugin/src/tool-handlers/reset-development.ts b/packages/opencode-plugin/src/tool-handlers/reset-development.ts index 125b97af..db169e63 100644 --- a/packages/opencode-plugin/src/tool-handlers/reset-development.ts +++ b/packages/opencode-plugin/src/tool-handlers/reset-development.ts @@ -39,16 +39,6 @@ export function createResetDevelopmentTool( logger.debug('reset_development called', { confirm, delete_plan }); - // Request permission before resetting development state (DESTRUCTIVE) - if (context && typeof context.ask === 'function') { - await context.ask({ - permission: 'reset_development', - patterns: ['*'], - always: ['*'], - metadata: { delete_plan, reason }, - }); - } - if (!confirm) { return `Reset requires confirm: true. Will delete conversation state${delete_plan ? ' and plan file' : ''}.`; } diff --git a/packages/opencode-plugin/src/tool-handlers/setup-project-docs.ts b/packages/opencode-plugin/src/tool-handlers/setup-project-docs.ts index 6a1be11f..bc4feba1 100644 --- a/packages/opencode-plugin/src/tool-handlers/setup-project-docs.ts +++ b/packages/opencode-plugin/src/tool-handlers/setup-project-docs.ts @@ -36,7 +36,7 @@ export async function createSetupProjectDocsTool( .default('freestyle') .describe('Template name, "none", or file path'), }, - execute: async (args, context) => { + execute: async (args, _context) => { const serverContext = await getServerContext(); const logger = serverContext.loggerFactory ? serverContext.loggerFactory('setup_project_docs') @@ -44,16 +44,6 @@ export async function createSetupProjectDocsTool( logger.debug('setup_project_docs called', args); - // Request permission before setting up project docs - if (context && typeof context.ask === 'function') { - await context.ask({ - permission: 'setup_project_docs', - patterns: ['*'], - always: ['*'], - metadata: args, - }); - } - try { // Delegate to SetupProjectDocsHandler const handler = new SetupProjectDocsHandler(); diff --git a/packages/opencode-plugin/src/tool-handlers/start-development.ts b/packages/opencode-plugin/src/tool-handlers/start-development.ts index 6970879c..cd93130a 100644 --- a/packages/opencode-plugin/src/tool-handlers/start-development.ts +++ b/packages/opencode-plugin/src/tool-handlers/start-development.ts @@ -50,16 +50,6 @@ export function createStartDevelopmentTool( logger.debug('start_development called', { workflow: args.workflow }); - // Request permission before starting new development workflow - if (context && typeof context.ask === 'function') { - await context.ask({ - permission: 'start_development', - patterns: ['*'], - always: ['*'], - metadata: { workflow: args.workflow }, - }); - } - try { // Delegate to StartDevelopmentHandler const handler = new StartDevelopmentHandler();