From 984464470c697f416799afb7154bd2745ee1f686 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 24 Mar 2026 23:54:29 +0900 Subject: [PATCH] fix(task): preserve restricted agent tools in sync continuation Restore sync continuation to apply agent tool restrictions after permissive defaults so resumed explore and librarian sessions cannot regain nested delegation. Add regression tests for resumed restricted agents while keeping plan-family continuation behavior intact. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../delegate-task/sync-continuation.test.ts | 208 ++++++++++++++++++ src/tools/delegate-task/sync-continuation.ts | 2 +- 2 files changed, 209 insertions(+), 1 deletion(-) diff --git a/src/tools/delegate-task/sync-continuation.test.ts b/src/tools/delegate-task/sync-continuation.test.ts index 32c3d50d7..104d7e84b 100644 --- a/src/tools/delegate-task/sync-continuation.test.ts +++ b/src/tools/delegate-task/sync-continuation.test.ts @@ -464,4 +464,212 @@ describe("executeSyncContinuation - toast cleanup error paths", () => { expect(result).toContain("session_id: ses_test_12345678") expect(result).not.toContain("subagent:") }) + + test("preserves restricted tool permissions for resumed explore sessions", async () => { + //#given - a resumed explore session should not regain delegation tools + const promptAsyncCalls: Array<{ path: { id: string }; body: Record }> = [] + const mockClient = { + session: { + messages: async () => ({ + data: [ + { info: { id: "msg_001", role: "user", time: { created: 1000 } } }, + { + info: { + id: "msg_002", + role: "assistant", + time: { created: 2000 }, + finish: "end_turn", + agent: "explore", + }, + parts: [{ type: "text", text: "Response" }], + }, + ], + }), + promptAsync: async (input: { path: { id: string }; body: Record }) => { + promptAsyncCalls.push(input) + return {} + }, + status: async () => ({ + data: { ses_test: { type: "idle" } }, + }), + }, + } + + const { executeSyncContinuation } = require("./sync-continuation") + + const deps = { + pollSyncSession: async () => null, + fetchSyncResult: async () => ({ ok: true as const, textContent: "Result" }), + } + + const mockCtx = { + sessionID: "parent-session", + callID: "call-123", + metadata: () => {}, + } + + const mockExecutorCtx = { + client: mockClient, + syncPollTimeoutMs: 100, + } + + const args = { + session_id: "ses_test_12345678", + prompt: "continue working", + description: "resume explore task", + load_skills: [], + run_in_background: false, + } + + //#when + await executeSyncContinuation(args, mockCtx, mockExecutorCtx, deps) + + //#then + expect(promptAsyncCalls).toHaveLength(1) + expect(promptAsyncCalls[0]?.body.tools).toEqual({ + task: false, + call_omo_agent: false, + question: false, + write: false, + edit: false, + }) + }) + + test("preserves restricted tool permissions for resumed librarian sessions", async () => { + //#given - a resumed librarian session should stay read-only for delegation tools + const promptAsyncCalls: Array<{ path: { id: string }; body: Record }> = [] + const mockClient = { + session: { + messages: async () => ({ + data: [ + { info: { id: "msg_001", role: "user", time: { created: 1000 } } }, + { + info: { + id: "msg_002", + role: "assistant", + time: { created: 2000 }, + finish: "end_turn", + agent: "librarian", + }, + parts: [{ type: "text", text: "Response" }], + }, + ], + }), + promptAsync: async (input: { path: { id: string }; body: Record }) => { + promptAsyncCalls.push(input) + return {} + }, + status: async () => ({ + data: { ses_test: { type: "idle" } }, + }), + }, + } + + const { executeSyncContinuation } = require("./sync-continuation") + + const deps = { + pollSyncSession: async () => null, + fetchSyncResult: async () => ({ ok: true as const, textContent: "Result" }), + } + + const mockCtx = { + sessionID: "parent-session", + callID: "call-123", + metadata: () => {}, + } + + const mockExecutorCtx = { + client: mockClient, + syncPollTimeoutMs: 100, + } + + const args = { + session_id: "ses_test_12345678", + prompt: "continue researching", + description: "resume librarian task", + load_skills: [], + run_in_background: false, + } + + //#when + await executeSyncContinuation(args, mockCtx, mockExecutorCtx, deps) + + //#then + expect(promptAsyncCalls).toHaveLength(1) + expect(promptAsyncCalls[0]?.body.tools).toEqual({ + task: false, + call_omo_agent: false, + question: false, + write: false, + edit: false, + }) + }) + + test("keeps plan-family task delegation available during sync continuation", async () => { + //#given - a resumed plan-family session should keep its intended task capability + const promptAsyncCalls: Array<{ path: { id: string }; body: Record }> = [] + const mockClient = { + session: { + messages: async () => ({ + data: [ + { info: { id: "msg_001", role: "user", time: { created: 1000 } } }, + { + info: { + id: "msg_002", + role: "assistant", + time: { created: 2000 }, + finish: "end_turn", + agent: "prometheus", + }, + parts: [{ type: "text", text: "Response" }], + }, + ], + }), + promptAsync: async (input: { path: { id: string }; body: Record }) => { + promptAsyncCalls.push(input) + return {} + }, + status: async () => ({ + data: { ses_test: { type: "idle" } }, + }), + }, + } + + const { executeSyncContinuation } = require("./sync-continuation") + + const deps = { + pollSyncSession: async () => null, + fetchSyncResult: async () => ({ ok: true as const, textContent: "Result" }), + } + + const mockCtx = { + sessionID: "parent-session", + callID: "call-123", + metadata: () => {}, + } + + const mockExecutorCtx = { + client: mockClient, + syncPollTimeoutMs: 100, + } + + const args = { + session_id: "ses_test_12345678", + prompt: "continue planning", + description: "resume plan task", + load_skills: [], + run_in_background: false, + } + + //#when + await executeSyncContinuation(args, mockCtx, mockExecutorCtx, deps) + + //#then + expect(promptAsyncCalls).toHaveLength(1) + expect(promptAsyncCalls[0]?.body.tools).toEqual({ + task: true, + call_omo_agent: true, + question: false, + }) + }) }) diff --git a/src/tools/delegate-task/sync-continuation.ts b/src/tools/delegate-task/sync-continuation.ts index d24ba4574..82618a289 100644 --- a/src/tools/delegate-task/sync-continuation.ts +++ b/src/tools/delegate-task/sync-continuation.ts @@ -85,10 +85,10 @@ export async function executeSyncContinuation( const allowTask = isPlanFamily(resumeAgent) const effectivePrompt = buildTaskPrompt(args.prompt, resumeAgent) const tools = { - ...(resumeAgent ? getAgentToolRestrictions(resumeAgent) : {}), task: allowTask, call_omo_agent: true, question: false, + ...(resumeAgent ? getAgentToolRestrictions(resumeAgent) : {}), } setSessionTools(args.session_id!, tools)