Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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"
}
107 changes: 107 additions & 0 deletions .vibe/development-plan-fix-opencode-plugin-permissions.md
Original file line number Diff line number Diff line change
@@ -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: <value>` |
| `proceed_to_phase` | `target_phase: <value>`, `reason: <value>` (if present) |
| `conduct_review` | `target_phase: <value>` |
| `reset_development` | `delete_plan: <value>` (if true), `reason: <value>` (if present) |
| `setup_project_docs` | `architecture: <value>`, `requirements: <value>`, `design: <value>` |

### 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
<!-- beads-phase-id: responsible-vibe-34.1 -->
### Tasks
<!-- beads-synced: 2026-05-01 -->
*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
<!-- beads-phase-id: responsible-vibe-34.2 -->
### Tasks
<!-- beads-synced: 2026-05-01 -->
*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
<!-- beads-phase-id: responsible-vibe-34.3 -->
### Tasks
<!-- beads-synced: 2026-05-01 -->
*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
<!-- beads-phase-id: responsible-vibe-34.4 -->
### Tasks
<!-- beads-synced: 2026-05-01 -->
*Auto-synced — do not edit here, use `bd` CLI instead.*

64 changes: 62 additions & 2 deletions packages/opencode-plugin/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, unknown>
): 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) => {
Expand All @@ -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<string, unknown>
),
always: ['*'],
metadata: {},
metadata: args as Record<string, unknown>,
})
);

Expand Down
10 changes: 0 additions & 10 deletions packages/opencode-plugin/src/tool-handlers/conduct-review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
10 changes: 0 additions & 10 deletions packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
10 changes: 0 additions & 10 deletions packages/opencode-plugin/src/tool-handlers/reset-development.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' : ''}.`;
}
Expand Down
12 changes: 1 addition & 11 deletions packages/opencode-plugin/src/tool-handlers/setup-project-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,14 @@ 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')
: createLogger('setup_project_docs');

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();
Expand Down
10 changes: 0 additions & 10 deletions packages/opencode-plugin/src/tool-handlers/start-development.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading