Add session permission support to background agents for denying questions
Implements question-denied session permission rules when creating child
sessions via background task delegation. This prevents subagents from
asking questions by passing explicit permission configuration during
session creation.
🤖 GENERATED WITH ASSISTANCE OF OhMyOpenCode
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { tmpdir } from "node:os"
|
||||
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
|
||||
import { BackgroundManager } from "./manager"
|
||||
|
||||
describe("BackgroundManager session permission", () => {
|
||||
test("passes explicit session permission rules to child session creation", async () => {
|
||||
// given
|
||||
const createCalls: Array<Record<string, unknown>> = []
|
||||
const client = {
|
||||
session: {
|
||||
get: async () => ({ data: { directory: "/parent" } }),
|
||||
create: async (input: Record<string, unknown>) => {
|
||||
createCalls.push(input)
|
||||
return { data: { id: "ses_child" } }
|
||||
},
|
||||
promptAsync: async () => ({}),
|
||||
abort: async () => ({}),
|
||||
},
|
||||
}
|
||||
const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput)
|
||||
|
||||
// when
|
||||
await manager.launch({
|
||||
description: "Test task",
|
||||
prompt: "Do something",
|
||||
agent: "explore",
|
||||
parentSessionID: "ses_parent",
|
||||
parentMessageID: "msg_parent",
|
||||
sessionPermission: [
|
||||
{ permission: "question", action: "deny", pattern: "*" },
|
||||
],
|
||||
})
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
manager.shutdown()
|
||||
|
||||
// then
|
||||
expect(createCalls).toHaveLength(1)
|
||||
expect(createCalls[0]?.body).toEqual({
|
||||
parentID: "ses_parent",
|
||||
title: "Test task (@explore subagent)",
|
||||
permission: [
|
||||
{ permission: "question", action: "deny", pattern: "*" },
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -272,6 +272,7 @@ export class BackgroundManager {
|
||||
body: {
|
||||
parentID: input.parentSessionID,
|
||||
title: `${input.description} (@${input.agent} subagent)`,
|
||||
...(input.sessionPermission ? { permission: input.sessionPermission } : {}),
|
||||
} as Record<string, unknown>,
|
||||
query: {
|
||||
directory: parentDirectory,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, test, expect } from "bun:test"
|
||||
import { createTask, startTask } from "./spawner"
|
||||
|
||||
describe("background-agent spawner.startTask", () => {
|
||||
test("does not override parent session permission rules when creating child session", async () => {
|
||||
test("applies explicit child session permission rules when creating child session", async () => {
|
||||
//#given
|
||||
const createCalls: any[] = []
|
||||
const parentPermission = [
|
||||
@@ -41,6 +41,9 @@ describe("background-agent spawner.startTask", () => {
|
||||
parentModel: task.parentModel,
|
||||
parentAgent: task.parentAgent,
|
||||
model: task.model,
|
||||
sessionPermission: [
|
||||
{ permission: "question", action: "deny", pattern: "*" },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -57,6 +60,8 @@ describe("background-agent spawner.startTask", () => {
|
||||
|
||||
//#then
|
||||
expect(createCalls).toHaveLength(1)
|
||||
expect(createCalls[0]?.body?.permission).toBeUndefined()
|
||||
expect(createCalls[0]?.body?.permission).toEqual([
|
||||
{ permission: "question", action: "deny", pattern: "*" },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -61,6 +61,7 @@ export async function startTask(
|
||||
const createResult = await client.session.create({
|
||||
body: {
|
||||
parentID: input.parentSessionID,
|
||||
...(input.sessionPermission ? { permission: input.sessionPermission } : {}),
|
||||
} as Record<string, unknown>,
|
||||
query: {
|
||||
directory: parentDirectory,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { FallbackEntry } from "../../shared/model-requirements"
|
||||
import type { SessionPermissionRule } from "../../shared/question-denied-session-permission"
|
||||
|
||||
export type BackgroundTaskStatus =
|
||||
| "pending"
|
||||
@@ -72,6 +73,7 @@ export interface LaunchInput {
|
||||
skills?: string[]
|
||||
skillContent?: string
|
||||
category?: string
|
||||
sessionPermission?: SessionPermissionRule[]
|
||||
}
|
||||
|
||||
export interface ResumeInput {
|
||||
|
||||
9
src/shared/question-denied-session-permission.ts
Normal file
9
src/shared/question-denied-session-permission.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export type SessionPermissionRule = {
|
||||
permission: string
|
||||
action: "allow" | "deny"
|
||||
pattern: string
|
||||
}
|
||||
|
||||
export const QUESTION_DENIED_SESSION_PERMISSION: SessionPermissionRule[] = [
|
||||
{ permission: "question", action: "deny", pattern: "*" },
|
||||
]
|
||||
@@ -155,4 +155,50 @@ describeFn("executeBackgroundTask output/session metadata compatibility", () =>
|
||||
expectFn(metadataCalls).toHaveLength(1)
|
||||
expectFn(metadataCalls[0].metadata.sessionId).toBe("ses_late_123")
|
||||
})
|
||||
|
||||
testFn("passes question-deny session permission when launching delegate task", async () => {
|
||||
//#given - delegate task background launch should deny question at session creation time
|
||||
const launchCalls: any[] = []
|
||||
const manager = {
|
||||
launch: async (input: any) => {
|
||||
launchCalls.push(input)
|
||||
return {
|
||||
id: "bg_permission",
|
||||
sessionID: "ses_permission_123",
|
||||
description: "Permission session",
|
||||
agent: "explore",
|
||||
status: "running",
|
||||
}
|
||||
},
|
||||
getTask: () => ({ sessionID: "ses_permission_123" }),
|
||||
}
|
||||
|
||||
//#when
|
||||
await executeBackgroundTask(
|
||||
{
|
||||
description: "Permission session",
|
||||
prompt: "check",
|
||||
run_in_background: true,
|
||||
load_skills: [],
|
||||
},
|
||||
{
|
||||
sessionID: "ses_parent",
|
||||
callID: "call_4",
|
||||
metadata: async () => {},
|
||||
abort: new AbortController().signal,
|
||||
},
|
||||
{ manager },
|
||||
{ sessionID: "ses_parent", messageID: "msg_4" },
|
||||
"explore",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
)
|
||||
|
||||
//#then
|
||||
expectFn(launchCalls).toHaveLength(1)
|
||||
expectFn(launchCalls[0].sessionPermission).toEqual([
|
||||
{ permission: "question", action: "deny", pattern: "*" },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@ import { storeToolMetadata } from "../../features/tool-metadata-store"
|
||||
import { formatDetailedError } from "./error-formatting"
|
||||
import { getSessionTools } from "../../shared/session-tools-store"
|
||||
import { SessionCategoryRegistry } from "../../shared/session-category-registry"
|
||||
import { QUESTION_DENIED_SESSION_PERMISSION } from "../../shared/question-denied-session-permission"
|
||||
|
||||
export async function executeBackgroundTask(
|
||||
args: DelegateTaskArgs,
|
||||
@@ -36,6 +37,7 @@ export async function executeBackgroundTask(
|
||||
skills: args.load_skills.length > 0 ? args.load_skills : undefined,
|
||||
skillContent: systemContent,
|
||||
category: args.category,
|
||||
sessionPermission: QUESTION_DENIED_SESSION_PERMISSION,
|
||||
})
|
||||
|
||||
// OpenCode TUI's `Task` tool UI calculates toolcalls by looking up
|
||||
|
||||
38
src/tools/delegate-task/sync-session-creator.test.ts
Normal file
38
src/tools/delegate-task/sync-session-creator.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
|
||||
import { createSyncSession } from "./sync-session-creator"
|
||||
|
||||
describe("createSyncSession", () => {
|
||||
test("creates child session with question permission denied", async () => {
|
||||
// given
|
||||
const createCalls: Array<Record<string, unknown>> = []
|
||||
const client = {
|
||||
session: {
|
||||
get: async () => ({ data: { directory: "/parent" } }),
|
||||
create: async (input: Record<string, unknown>) => {
|
||||
createCalls.push(input)
|
||||
return { data: { id: "ses_child" } }
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// when
|
||||
const result = await createSyncSession(client as never, {
|
||||
parentSessionID: "ses_parent",
|
||||
agentToUse: "explore",
|
||||
description: "test task",
|
||||
defaultDirectory: "/fallback",
|
||||
})
|
||||
|
||||
// then
|
||||
expect(result).toEqual({ ok: true, sessionID: "ses_child", parentDirectory: "/parent" })
|
||||
expect(createCalls).toHaveLength(1)
|
||||
expect(createCalls[0]?.body).toEqual({
|
||||
parentID: "ses_parent",
|
||||
title: "test task (@explore subagent)",
|
||||
permission: [
|
||||
{ permission: "question", action: "deny", pattern: "*" },
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { OpencodeClient } from "./types"
|
||||
import { QUESTION_DENIED_SESSION_PERMISSION } from "../../shared/question-denied-session-permission"
|
||||
|
||||
export async function createSyncSession(
|
||||
client: OpencodeClient,
|
||||
@@ -13,6 +14,7 @@ export async function createSyncSession(
|
||||
body: {
|
||||
parentID: input.parentSessionID,
|
||||
title: `${input.description} (@${input.agentToUse} subagent)`,
|
||||
permission: QUESTION_DENIED_SESSION_PERMISSION,
|
||||
} as Record<string, unknown>,
|
||||
query: {
|
||||
directory: parentDirectory,
|
||||
|
||||
74
src/tools/delegate-task/unstable-agent-permission.test.ts
Normal file
74
src/tools/delegate-task/unstable-agent-permission.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
|
||||
import { executeUnstableAgentTask } from "./unstable-agent-task"
|
||||
|
||||
describe("executeUnstableAgentTask session permission", () => {
|
||||
test("passes question-deny session permission into background launch", async () => {
|
||||
// given
|
||||
const launchCalls: Array<Record<string, unknown>> = []
|
||||
const mockManager = {
|
||||
launch: async (input: Record<string, unknown>) => {
|
||||
launchCalls.push(input)
|
||||
return {
|
||||
id: "bg_unstable_permission",
|
||||
sessionID: "ses_unstable_permission",
|
||||
description: "test task",
|
||||
agent: "sisyphus-junior",
|
||||
status: "running",
|
||||
}
|
||||
},
|
||||
getTask: () => ({
|
||||
id: "bg_unstable_permission",
|
||||
sessionID: "ses_unstable_permission",
|
||||
status: "interrupt",
|
||||
description: "test task",
|
||||
agent: "sisyphus-junior",
|
||||
error: "stop after launch",
|
||||
}),
|
||||
}
|
||||
const toolContext = {
|
||||
sessionID: "parent-session",
|
||||
messageID: "msg_parent",
|
||||
agent: "sisyphus",
|
||||
metadata: () => {},
|
||||
abort: new AbortController().signal,
|
||||
} satisfies Parameters<typeof executeUnstableAgentTask>[1]
|
||||
const executorContext = {
|
||||
manager: mockManager,
|
||||
client: {
|
||||
session: {
|
||||
status: async () => ({ data: {} }),
|
||||
messages: async () => ({ data: [] }),
|
||||
},
|
||||
},
|
||||
} as unknown as Parameters<typeof executeUnstableAgentTask>[2]
|
||||
const parentContext = {
|
||||
sessionID: "parent-session",
|
||||
messageID: "msg_parent",
|
||||
} satisfies Parameters<typeof executeUnstableAgentTask>[3]
|
||||
|
||||
// when
|
||||
await executeUnstableAgentTask(
|
||||
{
|
||||
prompt: "test prompt",
|
||||
description: "test task",
|
||||
category: "test",
|
||||
load_skills: [],
|
||||
run_in_background: false,
|
||||
},
|
||||
toolContext,
|
||||
executorContext,
|
||||
parentContext,
|
||||
"sisyphus-junior",
|
||||
undefined,
|
||||
undefined,
|
||||
"test-model",
|
||||
)
|
||||
|
||||
// then
|
||||
expect(launchCalls).toHaveLength(1)
|
||||
expect(launchCalls[0]?.sessionPermission).toEqual([
|
||||
{ permission: "question", action: "deny", pattern: "*" },
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -7,6 +7,7 @@ import { formatDuration } from "./time-formatter"
|
||||
import { formatDetailedError } from "./error-formatting"
|
||||
import { getSessionTools } from "../../shared/session-tools-store"
|
||||
import { normalizeSDKResponse } from "../../shared"
|
||||
import { QUESTION_DENIED_SESSION_PERMISSION } from "../../shared/question-denied-session-permission"
|
||||
|
||||
export async function executeUnstableAgentTask(
|
||||
args: DelegateTaskArgs,
|
||||
@@ -35,6 +36,7 @@ export async function executeUnstableAgentTask(
|
||||
skills: args.load_skills.length > 0 ? args.load_skills : undefined,
|
||||
skillContent: systemContent,
|
||||
category: args.category,
|
||||
sessionPermission: QUESTION_DENIED_SESSION_PERMISSION,
|
||||
})
|
||||
|
||||
const timing = getTimingConfig()
|
||||
|
||||
Reference in New Issue
Block a user