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
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) }));
Expand All @@ -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,
Expand Down Expand Up @@ -65,6 +83,15 @@ 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.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) }));
});
Expand All @@ -89,5 +116,73 @@ 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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
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');
// 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();
});

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', {
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' });
});
});
5 changes: 3 additions & 2 deletions backend/services/nativeRuntimeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> = 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 = (
Expand Down Expand Up @@ -885,8 +885,9 @@ export async function dispatchTool(
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 } };
}
Expand Down
Loading