diff --git a/src/cli/run/session-resolver.test.ts b/src/cli/run/session-resolver.test.ts
index ca7d9f65..a9775bb4 100644
--- a/src/cli/run/session-resolver.test.ts
+++ b/src/cli/run/session-resolver.test.ts
@@ -1,6 +1,8 @@
-import { describe, it, expect, beforeEach, mock, spyOn } from "bun:test"
-import { resolveSession } from "./session-resolver"
-import type { OpencodeClient } from "./types"
+///
+
+import { beforeEach, describe, expect, it, mock, spyOn } from "bun:test";
+import { resolveSession } from "./session-resolver";
+import type { OpencodeClient } from "./types";
const createMockClient = (overrides: {
getResult?: { error?: unknown; data?: { id: string } }
@@ -58,7 +60,9 @@ describe("resolveSession", () => {
const result = resolveSession({ client: mockClient, sessionId })
// then
- await expect(result).rejects.toThrow(`Session not found: ${sessionId}`)
+ await Promise.resolve(
+ expect(result).rejects.toThrow(`Session not found: ${sessionId}`)
+ )
expect(mockClient.session.get).toHaveBeenCalledWith({
path: { id: sessionId },
})
@@ -77,7 +81,12 @@ describe("resolveSession", () => {
// then
expect(result).toBe("new-session-id")
expect(mockClient.session.create).toHaveBeenCalledWith({
- body: { title: "oh-my-opencode run" },
+ body: {
+ title: "oh-my-opencode run",
+ permission: [
+ { permission: "question", action: "deny", pattern: "*" },
+ ],
+ },
})
expect(mockClient.session.get).not.toHaveBeenCalled()
})
@@ -98,7 +107,12 @@ describe("resolveSession", () => {
expect(result).toBe("retried-session-id")
expect(mockClient.session.create).toHaveBeenCalledTimes(2)
expect(mockClient.session.create).toHaveBeenCalledWith({
- body: { title: "oh-my-opencode run" },
+ body: {
+ title: "oh-my-opencode run",
+ permission: [
+ { permission: "question", action: "deny", pattern: "*" },
+ ],
+ },
})
})
@@ -116,7 +130,9 @@ describe("resolveSession", () => {
const result = resolveSession({ client: mockClient })
// then
- await expect(result).rejects.toThrow("Failed to create session after all retries")
+ await Promise.resolve(
+ expect(result).rejects.toThrow("Failed to create session after all retries")
+ )
expect(mockClient.session.create).toHaveBeenCalledTimes(3)
})
@@ -134,7 +150,9 @@ describe("resolveSession", () => {
const result = resolveSession({ client: mockClient })
// then
- await expect(result).rejects.toThrow("Failed to create session after all retries")
+ await Promise.resolve(
+ expect(result).rejects.toThrow("Failed to create session after all retries")
+ )
expect(mockClient.session.create).toHaveBeenCalledTimes(3)
})
})
diff --git a/src/cli/run/session-resolver.ts b/src/cli/run/session-resolver.ts
index 31bd5a2c..1ec07199 100644
--- a/src/cli/run/session-resolver.ts
+++ b/src/cli/run/session-resolver.ts
@@ -19,14 +19,18 @@ export async function resolveSession(options: {
return sessionId
}
- let lastError: unknown
for (let attempt = 1; attempt <= SESSION_CREATE_MAX_RETRIES; attempt++) {
const res = await client.session.create({
- body: { title: "oh-my-opencode run" },
+ body: {
+ title: "oh-my-opencode run",
+ // In CLI run mode there's no TUI to answer questions.
+ permission: [
+ { permission: "question", action: "deny" as const, pattern: "*" },
+ ],
+ } as any,
})
if (res.error) {
- lastError = res.error
console.error(
pc.yellow(`Session create attempt ${attempt}/${SESSION_CREATE_MAX_RETRIES} failed:`)
)
@@ -44,9 +48,6 @@ export async function resolveSession(options: {
return res.data.id
}
- lastError = new Error(
- `Unexpected response: ${JSON.stringify(res, null, 2)}`
- )
console.error(
pc.yellow(
`Session create attempt ${attempt}/${SESSION_CREATE_MAX_RETRIES}: No session ID returned`
diff --git a/src/features/background-agent/manager.test.ts b/src/features/background-agent/manager.test.ts
index c4db9056..d67ae2ad 100644
--- a/src/features/background-agent/manager.test.ts
+++ b/src/features/background-agent/manager.test.ts
@@ -1412,14 +1412,14 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => {
let manager: BackgroundManager
let mockClient: ReturnType
- function createMockClient() {
- return {
- session: {
- create: async () => ({ data: { id: `ses_${crypto.randomUUID()}` } }),
- get: async () => ({ data: { directory: "/test/dir" } }),
- prompt: async () => ({}),
- promptAsync: async () => ({}),
- messages: async () => ({ data: [] }),
+ function createMockClient() {
+ return {
+ session: {
+ create: async (_args?: any) => ({ data: { id: `ses_${crypto.randomUUID()}` } }),
+ get: async () => ({ data: { directory: "/test/dir" } }),
+ prompt: async () => ({}),
+ promptAsync: async () => ({}),
+ messages: async () => ({ data: [] }),
todo: async () => ({ data: [] }),
status: async () => ({ data: {} }),
abort: async () => ({}),
@@ -1520,6 +1520,55 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => {
})
describe("task transitions pending→running when slot available", () => {
+ test("should inherit parent session permission rules (and force deny question)", async () => {
+ // given
+ const createCalls: any[] = []
+ const parentPermission = [
+ { permission: "question", action: "allow" as const, pattern: "*" },
+ { permission: "plan_enter", action: "deny" as const, pattern: "*" },
+ ]
+
+ const customClient = {
+ session: {
+ create: async (args?: any) => {
+ createCalls.push(args)
+ return { data: { id: `ses_${crypto.randomUUID()}` } }
+ },
+ get: async () => ({ data: { directory: "/test/dir", permission: parentPermission } }),
+ prompt: async () => ({}),
+ promptAsync: async () => ({}),
+ messages: async () => ({ data: [] }),
+ todo: async () => ({ data: [] }),
+ status: async () => ({ data: {} }),
+ abort: async () => ({}),
+ },
+ }
+ manager.shutdown()
+ manager = new BackgroundManager({ client: customClient, directory: tmpdir() } as unknown as PluginInput, {
+ defaultConcurrency: 5,
+ })
+
+ const input = {
+ description: "Test task",
+ prompt: "Do something",
+ agent: "test-agent",
+ parentSessionID: "parent-session",
+ parentMessageID: "parent-message",
+ }
+
+ // when
+ await manager.launch(input)
+ await new Promise(resolve => setTimeout(resolve, 50))
+
+ // then
+ expect(createCalls).toHaveLength(1)
+ const permission = createCalls[0]?.body?.permission
+ expect(permission).toEqual([
+ { permission: "plan_enter", action: "deny", pattern: "*" },
+ { permission: "question", action: "deny", pattern: "*" },
+ ])
+ })
+
test("should transition first task to running immediately", async () => {
// given
const config = { defaultConcurrency: 5 }
diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts
index 0604c876..cfe8808a 100644
--- a/src/features/background-agent/manager.ts
+++ b/src/features/background-agent/manager.ts
@@ -236,13 +236,17 @@ export class BackgroundManager {
const parentDirectory = parentSession?.data?.directory ?? this.directory
log(`[background-agent] Parent dir: ${parentSession?.data?.directory}, using: ${parentDirectory}`)
+ const inheritedPermission = (parentSession as any)?.data?.permission
+ const permissionRules = Array.isArray(inheritedPermission)
+ ? inheritedPermission.filter((r: any) => r?.permission !== "question")
+ : []
+ permissionRules.push({ permission: "question", action: "deny" as const, pattern: "*" })
+
const createResult = await this.client.session.create({
body: {
parentID: input.parentSessionID,
title: `${input.description} (@${input.agent} subagent)`,
- permission: [
- { permission: "question", action: "deny" as const, pattern: "*" },
- ],
+ permission: permissionRules,
} as any,
query: {
directory: parentDirectory,
diff --git a/src/features/background-agent/spawner.test.ts b/src/features/background-agent/spawner.test.ts
new file mode 100644
index 00000000..334f3762
--- /dev/null
+++ b/src/features/background-agent/spawner.test.ts
@@ -0,0 +1,65 @@
+import { describe, test, expect } from "bun:test"
+
+import { createTask, startTask } from "./spawner"
+
+describe("background-agent spawner.startTask", () => {
+ test("should inherit parent session permission rules (and force deny question)", async () => {
+ //#given
+ const createCalls: any[] = []
+ const parentPermission = [
+ { permission: "question", action: "allow" as const, pattern: "*" },
+ { permission: "plan_enter", action: "deny" as const, pattern: "*" },
+ ]
+
+ const client = {
+ session: {
+ get: async () => ({ data: { directory: "/parent/dir", permission: parentPermission } }),
+ create: async (args?: any) => {
+ createCalls.push(args)
+ return { data: { id: "ses_child" } }
+ },
+ promptAsync: async () => ({}),
+ },
+ }
+
+ const task = createTask({
+ description: "Test task",
+ prompt: "Do work",
+ agent: "explore",
+ parentSessionID: "ses_parent",
+ parentMessageID: "msg_parent",
+ })
+
+ const item = {
+ task,
+ input: {
+ description: task.description,
+ prompt: task.prompt,
+ agent: task.agent,
+ parentSessionID: task.parentSessionID,
+ parentMessageID: task.parentMessageID,
+ parentModel: task.parentModel,
+ parentAgent: task.parentAgent,
+ model: task.model,
+ },
+ }
+
+ const ctx = {
+ client,
+ directory: "/fallback",
+ concurrencyManager: { release: () => {} },
+ tmuxEnabled: false,
+ onTaskError: () => {},
+ }
+
+ //#when
+ await startTask(item as any, ctx as any)
+
+ //#then
+ expect(createCalls).toHaveLength(1)
+ expect(createCalls[0]?.body?.permission).toEqual([
+ { permission: "plan_enter", action: "deny", pattern: "*" },
+ { permission: "question", action: "deny", pattern: "*" },
+ ])
+ })
+})
diff --git a/src/features/background-agent/spawner.ts b/src/features/background-agent/spawner.ts
index 477aafc1..1b6773fb 100644
--- a/src/features/background-agent/spawner.ts
+++ b/src/features/background-agent/spawner.ts
@@ -58,13 +58,17 @@ export async function startTask(
const parentDirectory = parentSession?.data?.directory ?? directory
log(`[background-agent] Parent dir: ${parentSession?.data?.directory}, using: ${parentDirectory}`)
+ const inheritedPermission = (parentSession as any)?.data?.permission
+ const permissionRules = Array.isArray(inheritedPermission)
+ ? inheritedPermission.filter((r: any) => r?.permission !== "question")
+ : []
+ permissionRules.push({ permission: "question", action: "deny" as const, pattern: "*" })
+
const createResult = await client.session.create({
body: {
parentID: input.parentSessionID,
title: `Background: ${input.description}`,
- permission: [
- { permission: "question", action: "deny" as const, pattern: "*" },
- ],
+ permission: permissionRules,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any,
query: {