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
36 changes: 24 additions & 12 deletions packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ RULES FOR GOOD TASK DECOMPOSITION:
defaults,
PermissionNext.fromConfig({
task: "deny",
taskctl: "deny",
}),
user,
),
Expand All @@ -281,7 +282,7 @@ You will receive a task description with:
3. Write minimal code to make tests pass
4. Refactor for clarity following AGENTS.md style guide
5. Run \`bun run typecheck && bun test\` — fix all errors
6. When done: \`taskctl comment <taskId> "Implementation complete: <one-line summary of what was built>"\`
6. When done: simply complete your work - the pulse system automatically detects completion

## Rules
- ONLY implement what is explicitly in the task description
Expand All @@ -298,12 +299,7 @@ You will receive a task description with:
- **context7** — current library docs. Use resolve-library-id then query-docs before using any external library API
- **parallel-search + web_fetch MCP tools** — web search when you need current information not in the codebase

## taskctl commands available to you
- \`taskctl comment <taskId> "<message>"\` — log progress or signal completion
- \`taskctl split <taskId>\` — if task is too large, split it (creates two sub-tasks)
- \`taskctl depends <taskId> --on <otherId>\` — if you discover an undeclared dependency

You may NOT call: taskctl start, taskctl stop, taskctl verdict, taskctl override, taskctl retry, taskctl resume`,
NOTE: You do NOT have access to taskctl commands. The pipeline handles task state automatically.`,
},
"adversarial-pipeline": {
name: "adversarial-pipeline",
Expand All @@ -315,6 +311,7 @@ You may NOT call: taskctl start, taskctl stop, taskctl verdict, taskctl override
PermissionNext.fromConfig({
"*": "deny",
bash: "allow",
taskctl: "allow",
}),
user,
),
Expand All @@ -336,16 +333,31 @@ Your ONLY job is to review code changes in an assigned worktree and record a str
5. Check: Does typecheck pass? (Run \`bun run typecheck\` in the worktree)

## Recording your verdict — MANDATORY
You MUST call taskctl verdict to record your finding. Never write a text response instead.

You MUST use the \`taskctl\` MCP tool to record your verdict. This is an MCP tool in your tool list — NOT a bash command. Never write a text response instead.

Use the taskctl tool with these parameters:

**If the code is good:**
\`taskctl verdict <taskId> --verdict APPROVED\`
- command: "verdict"
- taskId: <the task ID you were given>
- verdict: "APPROVED"
- verdictSummary: "Brief summary of what you reviewed and why it passes"
- verdictIssues: []

**If there are fixable issues:**
\`taskctl verdict <taskId> --verdict ISSUES_FOUND --summary "Brief summary" --issues '[{"location":"src/foo.ts:42","severity":"HIGH","fix":"Add null check before calling user.profile"}]'\`
- command: "verdict"
- taskId: <the task ID>
- verdict: "ISSUES_FOUND"
- verdictSummary: "Brief summary"
- verdictIssues: [{"location":"src/foo.ts:42","severity":"HIGH","fix":"Add null check before calling user.profile"}]

**If there are critical/blocking issues:**
\`taskctl verdict <taskId> --verdict CRITICAL_ISSUES_FOUND --summary "Brief summary" --issues '[...]'\`
- command: "verdict"
- taskId: <the task ID>
- verdict: "CRITICAL_ISSUES_FOUND"
- verdictSummary: "Brief summary"
- verdictIssues: [{"location":"...","severity":"CRITICAL","fix":"..."}]

## Severity guide
- CRITICAL: Security vulnerability, data loss risk, or complete functional failure
Expand All @@ -354,7 +366,7 @@ You MUST call taskctl verdict to record your finding. Never write a text respons
- LOW: Style or minor improvement suggestion

## Rules
- You may ONLY call: taskctl verdict
- You may ONLY use: the taskctl MCP tool (command: "verdict")
- Do NOT spawn any agents
- Do NOT commit or push
- Be specific: every issue must have a location (file:line) and a concrete fix suggestion
Expand Down
3 changes: 1 addition & 2 deletions packages/opencode/src/tasks/pulse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -728,8 +728,7 @@ Summary: ${verdict.summary}
Issues:
${issueLines}

The codebase changes are already in this worktree. Fix the specific issues listed above, run tests, then signal completion with:
taskctl comment ${task.id} "Implementation complete: <summary of what was fixed>"`
The codebase changes are already in this worktree. Fix the specific issues listed above, run tests, and complete your work. The pulse system automatically detects completion.`

try {
await SessionPrompt.prompt({
Expand Down
99 changes: 99 additions & 0 deletions packages/opencode/test/tasks/agent-permissions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { describe, expect, test } from "bun:test"
import { Instance } from "../../src/project/instance"
import { Agent } from "../../src/agent/agent"
import { PermissionNext } from "../../src/permission/next"
import { tmpdir } from "../fixture/fixture"

// Helper to evaluate permission for a tool with wildcard pattern
function evalPerm(agent: Agent.Info | undefined, permission: string): PermissionNext.Action | undefined {
if (!agent) return undefined
return PermissionNext.evaluate(permission, "*", agent.permission).action
}

describe("agent permissions for taskctl pipeline", () => {
test("adversarial-pipeline agent has taskctl permission", async () => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const adversarial = await Agent.get("adversarial-pipeline")
expect(adversarial).toBeDefined()
expect(adversarial?.permission).toBeDefined()

// Check the raw ruleset contains taskctl allowance
const hasTaskctlAllow = adversarial!.permission.some(
(rule: any) => rule.permission === "taskctl" && rule.action === "allow"
)
expect(hasTaskctlAllow).toBe(true)

// Verify via PermissionNext.evaluate - taskctl with any pattern should be allowed
const result = PermissionNext.evaluate("taskctl", "*", adversarial!.permission)
expect(result.action).toBe("allow")
},
})
})

test("adversarial-pipeline agent has bash permission", async () => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const adversarial = await Agent.get("adversarial-pipeline")
expect(adversarial).toBeDefined()

// Verify bash is allowed
const result = PermissionNext.evaluate("bash", "*", adversarial!.permission)
expect(result.action).toBe("allow")
},
})
})

test("developer-pipeline agent explicitly denies taskctl permission", async () => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const developer = await Agent.get("developer-pipeline")
expect(developer).toBeDefined()

// Developer should explicitly deny taskctl - not just "not allow"
const result = PermissionNext.evaluate("taskctl", "*", developer!.permission)
expect(result.action).toBe("deny") // Explicit deny, not fallback "ask"
},
})
})

test("adversarial-pipeline denies everything else (minimal permissions)", async () => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const adversarial = await Agent.get("adversarial-pipeline")
expect(adversarial).toBeDefined()

// Check that common tools are denied (wildcard deny rule)
const deniedTools = ["edit", "write", "read", "task"]

for (const tool of deniedTools) {
const result = PermissionNext.evaluate(tool, "*", adversarial!.permission)
expect(result.action).toBe("deny")
}
},
})
})

test("developer-pipeline denies task tool", async () => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const developer = await Agent.get("developer-pipeline")
expect(developer).toBeDefined()

// Developer should NOT be able to spawn other agents via task tool
const result = PermissionNext.evaluate("task", "*", developer!.permission)
expect(result.action).toBe("deny")
},
})
})
})