Skip to content

Commit 32c7e07

Browse files
committed
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: ['*'])
1 parent 0ddb383 commit 32c7e07

8 files changed

Lines changed: 199 additions & 53 deletions
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"conversationId": "workflows-fix-opencode-plugin-permissions-x4bahi",
3+
"projectPath": "/Users/oliverjaegle/projects/privat/codemcp/workflows",
4+
"epicId": "responsible-vibe-34",
5+
"phaseTasks": [
6+
{
7+
"phaseId": "explore",
8+
"phaseName": "Explore",
9+
"taskId": "responsible-vibe-34.1"
10+
},
11+
{
12+
"phaseId": "plan",
13+
"phaseName": "Plan",
14+
"taskId": "responsible-vibe-34.2"
15+
},
16+
{
17+
"phaseId": "code",
18+
"phaseName": "Code",
19+
"taskId": "responsible-vibe-34.3"
20+
},
21+
{
22+
"phaseId": "commit",
23+
"phaseName": "Commit",
24+
"taskId": "responsible-vibe-34.4"
25+
}
26+
],
27+
"createdAt": "2026-05-01T14:05:52.022Z",
28+
"updatedAt": "2026-05-01T14:05:52.022Z"
29+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Development Plan: workflows (fix/opencode-plugin-permissions branch)
2+
3+
*Generated on 2026-05-01 by Vibe Feature MCP*
4+
*Workflow: [epcc](https://codemcp.github.io/workflows/workflows/epcc)*
5+
6+
## Goal
7+
8+
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 `*`.
9+
10+
## Key Decisions
11+
12+
### KD-1: Fix location is wrap() in plugin.ts, not individual tool handlers
13+
14+
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: ['*']`.
15+
16+
Therefore, the fix must be in `wrap()`, passing the `args` object through to build meaningful `patterns`.
17+
18+
### KD-2: Individual tool handler ask() calls will be removed
19+
20+
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.
21+
22+
### KD-3: patterns array format — human-readable key:value strings
23+
24+
The web UI (`session-permission-dock.tsx`) only shows `props.request.patterns`. We will build a `patterns` array of human-readable strings like:
25+
- `"workflow: epcc"` for `start_development`
26+
- `"target_phase: code"`, `"reason: ..."` for `proceed_to_phase`
27+
- `"target_phase: code"` for `conduct_review`
28+
- `"delete_plan: true"`, `"reason: ..."` for `reset_development`
29+
- `"architecture: arc42"`, `"requirements: ears"`, `"design: comprehensive"` for `setup_project_docs`
30+
31+
Undefined/null/missing values are omitted. If no args produce meaningful patterns, fall back to `['*']`.
32+
33+
### KD-4: metadata populated with args
34+
35+
The `metadata` field in the `wrap()` `ask()` call is changed from `{}` to the actual `args` object so future consumers also get tool-specific info.
36+
37+
### KD-5: buildPermissionPatterns helper colocated in plugin.ts
38+
39+
The helper function is small and tightly coupled to `wrap()`, so it lives in `plugin.ts` rather than a separate utility file.
40+
41+
## Notes
42+
43+
### Architecture: Two-layer ask() (discovered in Explore phase)
44+
45+
1. `wrap()` in `plugin.ts`: The FIRST and only effectively-shown `ask()`. Currently uses `patterns: ['*']` and `metadata: {}`.
46+
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**.
47+
48+
### Web UI vs TUI difference
49+
50+
- **Web UI** (`session-permission-dock.tsx`): Shows `props.request.patterns` only — we must put meaningful strings in `patterns`.
51+
- **TUI** (`permission.tsx`): Reads `part.state.input` (raw tool call args) — already shows rich details, no change needed.
52+
53+
### Patterns format per tool
54+
55+
| Tool | Patterns |
56+
|---|---|
57+
| `start_development` | `workflow: <value>` |
58+
| `proceed_to_phase` | `target_phase: <value>`, `reason: <value>` (if present) |
59+
| `conduct_review` | `target_phase: <value>` |
60+
| `reset_development` | `delete_plan: <value>` (if true), `reason: <value>` (if present) |
61+
| `setup_project_docs` | `architecture: <value>`, `requirements: <value>`, `design: <value>` |
62+
63+
### Relevant files
64+
65+
- `packages/opencode-plugin/src/plugin.ts`**Primary change**: `wrap()` and new `buildPermissionPatterns()` helper
66+
- `packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts` — Remove redundant `ask()`
67+
- `packages/opencode-plugin/src/tool-handlers/start-development.ts` — Remove redundant `ask()`
68+
- `packages/opencode-plugin/src/tool-handlers/conduct-review.ts` — Remove redundant `ask()`
69+
- `packages/opencode-plugin/src/tool-handlers/reset-development.ts` — Remove redundant `ask()`
70+
- `packages/opencode-plugin/src/tool-handlers/setup-project-docs.ts` — Remove redundant `ask()`
71+
72+
## Explore
73+
<!-- beads-phase-id: responsible-vibe-34.1 -->
74+
### Tasks
75+
<!-- beads-synced: 2026-05-01 -->
76+
*Auto-synced — do not edit here, use `bd` CLI instead.*
77+
78+
- [x] `responsible-vibe-34.1.1` Explore how opencode web UI displays permission patterns
79+
- [x] `responsible-vibe-34.1.2` Fix patterns in all tool ask() calls to show meaningful info
80+
- [x] `responsible-vibe-34.1.3` Fix top-level wrap() ask() call in plugin.ts
81+
82+
## Plan
83+
<!-- beads-phase-id: responsible-vibe-34.2 -->
84+
### Tasks
85+
<!-- beads-synced: 2026-05-01 -->
86+
*Auto-synced — do not edit here, use `bd` CLI instead.*
87+
88+
- [x] `responsible-vibe-34.2.1` Design buildPermissionPatterns helper function
89+
- [x] `responsible-vibe-34.2.2` Plan removal of redundant ask() calls in tool handlers
90+
- [x] `responsible-vibe-34.2.3` Define patterns format per tool
91+
92+
## Code
93+
<!-- beads-phase-id: responsible-vibe-34.3 -->
94+
### Tasks
95+
<!-- beads-synced: 2026-05-01 -->
96+
*Auto-synced — do not edit here, use `bd` CLI instead.*
97+
98+
- [x] `responsible-vibe-34.3.1` Add buildPermissionPatterns() helper to plugin.ts and update wrap()
99+
- [x] `responsible-vibe-34.3.2` Remove redundant ask() calls from all 5 tool handlers
100+
- [x] `responsible-vibe-34.3.3` Build and type-check the plugin
101+
102+
## Commit
103+
<!-- beads-phase-id: responsible-vibe-34.4 -->
104+
### Tasks
105+
<!-- beads-synced: 2026-05-01 -->
106+
*Auto-synced — do not edit here, use `bd` CLI instead.*
107+

packages/opencode-plugin/src/plugin.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,63 @@ ACTION REQUIRED: Use proceed_to_phase tool to move to a phase that allows editin
642642
* an error if the agent is not allowed to use workflows.
643643
*/
644644
tool: await (async (): Promise<{ [key: string]: ToolDefinition }> => {
645+
/**
646+
* Build human-readable permission patterns for the web UI.
647+
* The opencode web permission dialog only shows `patterns`, so we put
648+
* meaningful "key: value" strings here instead of the generic '*'.
649+
*/
650+
const buildPermissionPatterns = (
651+
toolName: string,
652+
args: Record<string, unknown>
653+
): string[] => {
654+
const entry = (key: string, value: unknown): string | null => {
655+
if (value === undefined || value === null || value === '')
656+
return null;
657+
return `${key}: ${value}`;
658+
};
659+
660+
switch (toolName) {
661+
case 'start_development': {
662+
const patterns = [entry('workflow', args['workflow'])].filter(
663+
(p): p is string => p !== null
664+
);
665+
return patterns.length > 0 ? patterns : ['*'];
666+
}
667+
case 'proceed_to_phase': {
668+
const patterns = [
669+
entry('target_phase', args['target_phase']),
670+
entry('reason', args['reason']),
671+
].filter((p): p is string => p !== null);
672+
return patterns.length > 0 ? patterns : ['*'];
673+
}
674+
case 'conduct_review': {
675+
const patterns = [
676+
entry('target_phase', args['target_phase']),
677+
].filter((p): p is string => p !== null);
678+
return patterns.length > 0 ? patterns : ['*'];
679+
}
680+
case 'reset_development': {
681+
const patterns = [
682+
args['delete_plan'] === true
683+
? entry('delete_plan', args['delete_plan'])
684+
: null,
685+
entry('reason', args['reason']),
686+
].filter((p): p is string => p !== null);
687+
return patterns.length > 0 ? patterns : ['*'];
688+
}
689+
case 'setup_project_docs': {
690+
const patterns = [
691+
entry('architecture', args['architecture']),
692+
entry('requirements', args['requirements']),
693+
entry('design', args['design']),
694+
].filter((p): p is string => p !== null);
695+
return patterns.length > 0 ? patterns : ['*'];
696+
}
697+
default:
698+
return ['*'];
699+
}
700+
};
701+
645702
const wrap = (toolName: string, def: ToolDefinition): ToolDefinition => ({
646703
...def,
647704
execute: async (args, ctx) => {
@@ -656,9 +713,12 @@ ACTION REQUIRED: Use proceed_to_phase tool to move to a phase that allows editin
656713
await Effect.runPromise(
657714
ctx.ask({
658715
permission: toolName,
659-
patterns: ['*'],
716+
patterns: buildPermissionPatterns(
717+
toolName,
718+
args as Record<string, unknown>
719+
),
660720
always: ['*'],
661-
metadata: {},
721+
metadata: args as Record<string, unknown>,
662722
})
663723
);
664724

packages/opencode-plugin/src/tool-handlers/conduct-review.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,6 @@ export function createConductReviewTool(
2929

3030
logger.debug('conduct_review called', { targetPhase: target_phase });
3131

32-
// Request permission before conducting review
33-
if (context && typeof context.ask === 'function') {
34-
await context.ask({
35-
permission: 'conduct_review',
36-
patterns: ['*'],
37-
always: ['*'],
38-
metadata: { target_phase },
39-
});
40-
}
41-
4232
try {
4333
// Delegate to ConductReviewHandler
4434
const handler = new ConductReviewHandler();

packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,6 @@ export function createProceedToPhaseTool(
4040

4141
logger.debug('proceed_to_phase called', { to: target_phase, reason });
4242

43-
// Request permission before proceeding to new phase
44-
if (context && typeof context.ask === 'function') {
45-
await context.ask({
46-
permission: 'proceed_to_phase',
47-
patterns: ['*'],
48-
always: ['*'],
49-
metadata: { target_phase, reason },
50-
});
51-
}
52-
5343
try {
5444
// Delegate to ProceedToPhaseHandler
5545
const handler = new ProceedToPhaseHandler();

packages/opencode-plugin/src/tool-handlers/reset-development.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,6 @@ export function createResetDevelopmentTool(
3939

4040
logger.debug('reset_development called', { confirm, delete_plan });
4141

42-
// Request permission before resetting development state (DESTRUCTIVE)
43-
if (context && typeof context.ask === 'function') {
44-
await context.ask({
45-
permission: 'reset_development',
46-
patterns: ['*'],
47-
always: ['*'],
48-
metadata: { delete_plan, reason },
49-
});
50-
}
51-
5242
if (!confirm) {
5343
return `Reset requires confirm: true. Will delete conversation state${delete_plan ? ' and plan file' : ''}.`;
5444
}

packages/opencode-plugin/src/tool-handlers/setup-project-docs.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,24 +36,14 @@ export async function createSetupProjectDocsTool(
3636
.default('freestyle')
3737
.describe('Template name, "none", or file path'),
3838
},
39-
execute: async (args, context) => {
39+
execute: async (args, _context) => {
4040
const serverContext = await getServerContext();
4141
const logger = serverContext.loggerFactory
4242
? serverContext.loggerFactory('setup_project_docs')
4343
: createLogger('setup_project_docs');
4444

4545
logger.debug('setup_project_docs called', args);
4646

47-
// Request permission before setting up project docs
48-
if (context && typeof context.ask === 'function') {
49-
await context.ask({
50-
permission: 'setup_project_docs',
51-
patterns: ['*'],
52-
always: ['*'],
53-
metadata: args,
54-
});
55-
}
56-
5747
try {
5848
// Delegate to SetupProjectDocsHandler
5949
const handler = new SetupProjectDocsHandler();

packages/opencode-plugin/src/tool-handlers/start-development.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,6 @@ export function createStartDevelopmentTool(
5050

5151
logger.debug('start_development called', { workflow: args.workflow });
5252

53-
// Request permission before starting new development workflow
54-
if (context && typeof context.ask === 'function') {
55-
await context.ask({
56-
permission: 'start_development',
57-
patterns: ['*'],
58-
always: ['*'],
59-
metadata: { workflow: args.workflow },
60-
});
61-
}
62-
6353
try {
6454
// Delegate to StartDevelopmentHandler
6555
const handler = new StartDevelopmentHandler();

0 commit comments

Comments
 (0)