From 3c88857433f76dd9240dfecb378ba83f7b7e7c25 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Thu, 22 Oct 2026 04:02:10 -0700 Subject: [PATCH 1/5] feat: hand off hosted agents to local CLI Co-authored-by: quire Agent-Model: codex/gpt-5.6-luna/xhigh Agent-Model: claude/claude-haiku-5-5/high --- backend/services/nativeRuntimeService.ts | 45 ++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/backend/services/nativeRuntimeService.ts b/backend/services/nativeRuntimeService.ts index 993667f63a..c54dbca895 100644 --- a/backend/services/nativeRuntimeService.ts +++ b/backend/services/nativeRuntimeService.ts @@ -891,6 +891,51 @@ export async function dispatchTool( return { content: { ok: true, posted: true, result } }; } + case 'commonly_offer_local_agent': { + const adapter = String(args.adapter || '') as ByoHandoffAdapter; + let owner: { username?: string } | null = null; + if (ctx.installation?.installedBy) { + try { + owner = await (require('../models/User')).findById(ctx.installation.installedBy) + .select('username').lean() as { username?: string } | null; + } catch { + // A username is only a display-name seed. The handoff remains + // actionable with the stable "my-" fallback if the + // account lookup is transiently unavailable. + } + } + const agentName = args.name + ? String(args.name) + : defaultByoHandoffName(owner?.username, adapter); + const payload = buildByoHandoffPayload({ + adapter, + os: args.os, + podId: ctx.podId, + agentName, + }); + if ('error' in payload) { + return { content: { ok: false, error: payload.error }, error: 'handoff_invalid' }; + } + const AgentMessageService = require('./agentMessageService'); + const result = await AgentMessageService.postMessage({ + agentName: ctx.agentName, + instanceId: ctx.instanceId, + podId: ctx.podId, + // The structured payload is the handoff. This fixed content is a + // compatibility value for message storage and is not rendered as + // the instructions by the card. + content: 'Connect a local agent with the command below.', + messageType: 'card', + displayName: ctx.displayName, + installationConfig: ctx.installationConfig, + metadata: { source: 'native-runtime', handoff: 'byo' }, + payload, + noOutboundRelay: Boolean(ctx.webTainted), + answeringMessageId: ctx.answeringMessageId, + }); + return { content: { ok: true, posted: true, result } }; + } + case 'commonly_create_task': { const title = toSingleLineTitle(String(args.title || '')); if (!title) { From 0a3f517cace04e0b20d63e9af1df65c0643b4b52 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:27:14 -0700 Subject: [PATCH 2/5] fix: guard kernel-authored local-agent handoff Co-authored-by: quire Agent-Model: codex/gpt-5.6-luna/xhigh Agent-Model: claude/claude-haiku-5-5/high --- .../nativeRuntimeService.byoHandoff.test.js | 72 ++++++++++++++++++- backend/services/nativeRuntimeService.ts | 50 +------------ 2 files changed, 72 insertions(+), 50 deletions(-) diff --git a/backend/__tests__/unit/services/nativeRuntimeService.byoHandoff.test.js b/backend/__tests__/unit/services/nativeRuntimeService.byoHandoff.test.js index aa23d6e825..21a951cdd3 100644 --- a/backend/__tests__/unit/services/nativeRuntimeService.byoHandoff.test.js +++ b/backend/__tests__/unit/services/nativeRuntimeService.byoHandoff.test.js @@ -4,13 +4,16 @@ * setup message. */ jest.mock('axios', () => ({ __esModule: true, default: { post: jest.fn() } })); +const mockPodFindById = jest.fn(); jest.mock('../../../models/Pod', () => ({ - findById: jest.fn(() => ({ lean: jest.fn().mockResolvedValue({ name: 'Build Pod' }) })), + findById: (...args) => mockPodFindById(...args), })); jest.mock('../../../models/AgentRun', () => ({ create: jest.fn(), countDocuments: jest.fn() })); +const mockUserFindOne = jest.fn(); +const mockUserFindById = jest.fn(); jest.mock('../../../models/User', () => ({ - findOne: jest.fn(() => ({ select: () => ({ lean: () => Promise.resolve(null) }) })), - findById: jest.fn(() => ({ select: () => ({ lean: () => Promise.resolve({ username: 'Sam Xu' }) }) })), + findOne: (...args) => mockUserFindOne(...args), + findById: (...args) => mockUserFindById(...args), })); jest.mock('../../../services/agentTypingService', () => ({ emitAgentTypingStart: jest.fn(), emitAgentTypingStop: jest.fn() })); jest.mock('../../../services/agentMessageService', () => ({ postMessage: jest.fn().mockResolvedValue({ success: true }) })); @@ -21,12 +24,27 @@ jest.mock('../../../services/messageClaimService', () => ({ jest.mock('../../../services/nativeOutsiderGrantPolicy', () => ({ podHasOutsiderWritableGrant: jest.fn().mockResolvedValue(false), })); +const mockPodIsBridged = jest.fn(); +jest.mock('../../../services/outsiderGrantPodEligibility', () => ({ + podIsBridged: (...args) => mockPodIsBridged(...args), +})); +const mockHostedBrokerToolsForRun = jest.fn(); +const mockDispatchHostedBrokerTool = jest.fn(); +jest.mock('../../../services/grantBrokerProjectionService', () => ({ + hostedBrokerToolsForRun: (...args) => mockHostedBrokerToolsForRun(...args), + dispatchHostedBrokerTool: (...args) => mockDispatchHostedBrokerTool(...args), +})); const axios = require('axios').default; const AgentRun = require('../../../models/AgentRun'); const AgentMessageService = require('../../../services/agentMessageService'); const { runAgent } = require('../../../services/nativeRuntimeService'); +const chain = (value) => ({ + select: () => ({ lean: () => Promise.resolve(value) }), + lean: () => Promise.resolve(value), +}); + const POD = '507f1f77bcf86cd799439011'; const install = { podId: POD, @@ -65,6 +83,12 @@ describe('commonly_offer_local_agent', () => { process.env.LITELLM_BASE_URL = 'http://litellm.test'; process.env.LITELLM_MASTER_KEY = 'test-key'; process.env.HOSTED_ENGINE = 'native'; + mockPodFindById.mockReturnValue(chain({ name: 'Build Pod', members: [] })); + mockUserFindOne.mockReturnValue(chain(null)); + mockUserFindById.mockReturnValue(chain({ username: 'Sam Xu' })); + mockHostedBrokerToolsForRun.mockResolvedValue({ tools: [], dispatch: new Map() }); + mockDispatchHostedBrokerTool.mockResolvedValue({ callId: 'broker-call', content: { ok: true }, outcome: 'ok' }); + mockPodIsBridged.mockResolvedValue(false); AgentRun.countDocuments.mockResolvedValue(0); AgentRun.create.mockImplementation(async (doc) => ({ _id: 'run-1', ...doc, save: jest.fn().mockResolvedValue(undefined) })); }); @@ -89,5 +113,47 @@ describe('commonly_offer_local_agent', () => { expect(posted.payload.commands.mac).not.toMatch(/cm_agent|COMMONLY_AGENT_TOKEN|token/i); expect(posted.content).not.toContain('commonly agent attach'); expect(posted.answeringMessageId).toBeUndefined(); + expect(posted.authorKind).toBe('kernel'); + }); + + test('keeps a broker result from crossing a bridge before the handoff card', async () => { + mockUserFindOne.mockReturnValue(chain({ _id: 'seat-1' })); + mockPodFindById.mockReturnValue(chain({ name: 'Build Pod', members: ['seat-1'] })); + mockHostedBrokerToolsForRun.mockResolvedValue({ + tools: [{ type: 'function', function: { name: 'github_list_issues', description: 'List issues', parameters: {} } }], + dispatch: new Map([['github_list_issues', { grantId: 'grant-1', tool: 'github.list_issues' }]]), + }); + mockPodIsBridged.mockResolvedValue(true); + axios.post + .mockResolvedValueOnce(callsTool('github_list_issues', { state: 'open' })) + .mockResolvedValueOnce(callsTool('commonly_offer_local_agent', { adapter: 'opencode', os: 'mac' })) + .mockResolvedValueOnce(says('The handoff was blocked.')); + + const result = await runAgent(install, ask); + + expect(result.status).toBe('succeeded'); + expect(mockDispatchHostedBrokerTool).toHaveBeenCalledTimes(1); + expect(mockPodIsBridged).toHaveBeenCalledWith(POD); + expect(AgentMessageService.postMessage).not.toHaveBeenCalled(); + }); + + test('does not let a client post mark itself as kernel-authored', async () => { + axios.post + .mockResolvedValueOnce(callsTool('commonly_post_message', { + content: 'ordinary post', + authorKind: 'kernel', + metadata: { authorKind: 'kernel' }, + })) + .mockResolvedValueOnce(says('Done.')); + + await runAgent({ + ...install, + config: { ...install.config, tools: ['commonly_post_message'] }, + }, ask); + + expect(AgentMessageService.postMessage).toHaveBeenCalledTimes(1); + const posted = AgentMessageService.postMessage.mock.calls[0][0]; + expect(posted.authorKind).toBeUndefined(); + expect(posted.metadata).toEqual({ source: 'native-runtime' }); }); }); diff --git a/backend/services/nativeRuntimeService.ts b/backend/services/nativeRuntimeService.ts index c54dbca895..5a05762b8b 100644 --- a/backend/services/nativeRuntimeService.ts +++ b/backend/services/nativeRuntimeService.ts @@ -596,7 +596,7 @@ const WEB_TAINTED_BRIDGED_REFUSAL = 'This pod is bridged to an external chat, so const BROKER_TAINTED_BRIDGED_REFUSAL = 'This pod is bridged to an external chat, so a turn that called a granted tool ' + 'cannot post in it.'; const POSTING_NATIVE_TOOLS: ReadonlySet = new Set([ - 'commonly_post_message', 'commonly_offer_hosted_agent', 'commonly_propose_action', + 'commonly_post_message', 'commonly_offer_hosted_agent', 'commonly_offer_local_agent', 'commonly_propose_action', ]); export const toolsForConfig = ( @@ -885,53 +885,9 @@ export async function dispatchTool( installationConfig: ctx.installationConfig, metadata: { source: 'native-runtime', handoff: 'byo' }, payload, - noOutboundRelay: Boolean(ctx.webTainted), - answeringMessageId: ctx.answeringMessageId, - }); - return { content: { ok: true, posted: true, result } }; - } - - case 'commonly_offer_local_agent': { - const adapter = String(args.adapter || '') as ByoHandoffAdapter; - let owner: { username?: string } | null = null; - if (ctx.installation?.installedBy) { - try { - owner = await (require('../models/User')).findById(ctx.installation.installedBy) - .select('username').lean() as { username?: string } | null; - } catch { - // A username is only a display-name seed. The handoff remains - // actionable with the stable "my-" fallback if the - // account lookup is transiently unavailable. - } - } - const agentName = args.name - ? String(args.name) - : defaultByoHandoffName(owner?.username, adapter); - const payload = buildByoHandoffPayload({ - adapter, - os: args.os, - podId: ctx.podId, - agentName, - }); - if ('error' in payload) { - return { content: { ok: false, error: payload.error }, error: 'handoff_invalid' }; - } - const AgentMessageService = require('./agentMessageService'); - const result = await AgentMessageService.postMessage({ - agentName: ctx.agentName, - instanceId: ctx.instanceId, - podId: ctx.podId, - // The structured payload is the handoff. This fixed content is a - // compatibility value for message storage and is not rendered as - // the instructions by the card. - content: 'Connect a local agent with the command below.', - messageType: 'card', - displayName: ctx.displayName, - installationConfig: ctx.installationConfig, - metadata: { source: 'native-runtime', handoff: 'byo' }, - payload, - noOutboundRelay: Boolean(ctx.webTainted), + noOutboundRelay: Boolean(ctx.webTainted || ctx.brokerCallSeen), answeringMessageId: ctx.answeringMessageId, + authorKind: 'kernel', }); return { content: { ok: true, posted: true, result } }; } From aad833821a9bc9e53117e41394fa474c07ec5f6b Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 24 Oct 2026 00:15:23 -0700 Subject: [PATCH 3/5] test: respect broker projection in handoff fixture Co-authored-by: quire Agent-Model: codex/gpt-5.6-luna/xhigh Agent-Model: claude/claude-haiku-5-5/high --- .../unit/services/nativeRuntimeService.byoHandoff.test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/__tests__/unit/services/nativeRuntimeService.byoHandoff.test.js b/backend/__tests__/unit/services/nativeRuntimeService.byoHandoff.test.js index 21a951cdd3..22424f8be9 100644 --- a/backend/__tests__/unit/services/nativeRuntimeService.byoHandoff.test.js +++ b/backend/__tests__/unit/services/nativeRuntimeService.byoHandoff.test.js @@ -87,7 +87,10 @@ describe('commonly_offer_local_agent', () => { mockUserFindOne.mockReturnValue(chain(null)); mockUserFindById.mockReturnValue(chain({ username: 'Sam Xu' })); mockHostedBrokerToolsForRun.mockResolvedValue({ tools: [], dispatch: new Map() }); - mockDispatchHostedBrokerTool.mockResolvedValue({ callId: 'broker-call', content: { ok: true }, outcome: 'ok' }); + mockDispatchHostedBrokerTool.mockImplementation(async ({ projection, name }) => { + if (!projection?.dispatch?.has(name)) return null; + return { callId: 'broker-call', content: { ok: true }, outcome: 'ok' }; + }); mockPodIsBridged.mockResolvedValue(false); AgentRun.countDocuments.mockResolvedValue(0); AgentRun.create.mockImplementation(async (doc) => ({ _id: 'run-1', ...doc, save: jest.fn().mockResolvedValue(undefined) })); From 150c72af8cb86ecab18589aa69391fd1fba18d9d Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:56:50 -0700 Subject: [PATCH 4/5] test: count only the granted broker tool in the bridge handoff case The runtime offers every model tool call to dispatchHostedBrokerTool; a non-broker name returns null. The bridged-handoff case asserted one call total, so the commonly_offer_local_agent call made it two and the job failed. Count the github_list_issues dispatch specifically. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: quire Agent-Model: claude/claude-haiku-5-5/high --- .../unit/services/nativeRuntimeService.byoHandoff.test.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/__tests__/unit/services/nativeRuntimeService.byoHandoff.test.js b/backend/__tests__/unit/services/nativeRuntimeService.byoHandoff.test.js index 22424f8be9..e1ab516825 100644 --- a/backend/__tests__/unit/services/nativeRuntimeService.byoHandoff.test.js +++ b/backend/__tests__/unit/services/nativeRuntimeService.byoHandoff.test.js @@ -135,7 +135,11 @@ describe('commonly_offer_local_agent', () => { const result = await runAgent(install, ask); expect(result.status).toBe('succeeded'); - expect(mockDispatchHostedBrokerTool).toHaveBeenCalledTimes(1); + // The runtime offers every model tool call to the broker; only the granted + // tool is a broker dispatch, so count that one. + const brokerDispatches = mockDispatchHostedBrokerTool.mock.calls + .filter(([call]) => call.name === 'github_list_issues'); + expect(brokerDispatches).toHaveLength(1); expect(mockPodIsBridged).toHaveBeenCalledWith(POD); expect(AgentMessageService.postMessage).not.toHaveBeenCalled(); }); From 77ec1125bc7e64bebeadc6e91f92d3acf520975f Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:33:49 -0700 Subject: [PATCH 5/5] test: cover an unbridged broker-then-handoff card as not relayed The bridged case stops the handoff before postMessage, so it cannot catch a regression that drops noOutboundRelay for an unbridged, broker-tainted pod. Assert the card carries noOutboundRelay: true when the pod is not bridged. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: quire Agent-Model: claude/claude-haiku-5-5/high --- .../nativeRuntimeService.byoHandoff.test.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/backend/__tests__/unit/services/nativeRuntimeService.byoHandoff.test.js b/backend/__tests__/unit/services/nativeRuntimeService.byoHandoff.test.js index e1ab516825..57e20660ca 100644 --- a/backend/__tests__/unit/services/nativeRuntimeService.byoHandoff.test.js +++ b/backend/__tests__/unit/services/nativeRuntimeService.byoHandoff.test.js @@ -144,6 +144,28 @@ describe('commonly_offer_local_agent', () => { expect(AgentMessageService.postMessage).not.toHaveBeenCalled(); }); + test('an unbridged broker result still marks the handoff card as not relayed', async () => { + mockUserFindOne.mockReturnValue(chain({ _id: 'seat-1' })); + mockPodFindById.mockReturnValue(chain({ name: 'Build Pod', members: ['seat-1'] })); + mockHostedBrokerToolsForRun.mockResolvedValue({ + tools: [{ type: 'function', function: { name: 'github_list_issues', description: 'List issues', parameters: {} } }], + dispatch: new Map([['github_list_issues', { grantId: 'grant-1', tool: 'github.list_issues' }]]), + }); + mockPodIsBridged.mockResolvedValue(false); + axios.post + .mockResolvedValueOnce(callsTool('github_list_issues', { state: 'open' })) + .mockResolvedValueOnce(callsTool('commonly_offer_local_agent', { adapter: 'opencode', os: 'mac' })) + .mockResolvedValueOnce(says('Run the commands in the card above.')); + + const result = await runAgent(install, ask); + + expect(result.status).toBe('succeeded'); + expect(AgentMessageService.postMessage).toHaveBeenCalledTimes(1); + const posted = AgentMessageService.postMessage.mock.calls[0][0]; + expect(posted.messageType).toBe('card'); + expect(posted.noOutboundRelay).toBe(true); + }); + test('does not let a client post mark itself as kernel-authored', async () => { axios.post .mockResolvedValueOnce(callsTool('commonly_post_message', {