Skip to content

Commit ddb089a

Browse files
authored
fix(grants): the broker call refuses a seat that cannot confine it (TASK-175) (#1971)
1 parent 8faa6e3 commit ddb089a

14 files changed

Lines changed: 750 additions & 4 deletions

‎backend/__tests__/unit/routes/mcpGrants.listScope.test.js‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ jest.mock('../../../services/approvalActionService', () => ({
3131
proposeAction: jest.fn().mockResolvedValue({ ok: true, approvalId: 'approval-test' }),
3232
}));
3333
jest.mock('../../../services/dmService', () => ({ getOrCreateAgentRoom: jest.fn() }));
34+
// The seat's own declaration is judged inside the broker call (TASK-175). This
35+
// suite is about what the URL's grant exposes, so the resolution is mocked; the
36+
// resolution is witnessed on memory Mongo in
37+
// __tests__/unit/services/seatGrantConfinement.test.js.
38+
const mockSeatConfinement = jest.fn();
39+
jest.mock('../../../services/seatGrantConfinement', () => ({
40+
judgeSeatConfinement: (...args) => mockSeatConfinement(...args),
41+
}));
3442
jest.mock('../../../middleware/agentRuntimeAuth', () => (req, _res, next) => {
3543
req.agentUser = {
3644
_id: 'agent-a',
@@ -104,6 +112,7 @@ const { getToolDefinitions } = require('../../../services/toolBrokerService');
104112

105113
beforeEach(() => {
106114
jest.clearAllMocks();
115+
mockSeatConfinement.mockResolvedValue({ refusal: null, scope: 'unbound' });
107116
mockRoomGrant.findOne.mockResolvedValue(seatGrant());
108117
mockIntegration.findOne.mockResolvedValue({
109118
type: 'github-app',

‎backend/__tests__/unit/routes/mcpGrants.test.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ describe('MCP grant transport', () => {
7171
expect(mockListToolsForGrant).toHaveBeenCalledWith({
7272
grantId: 'grant-1',
7373
agentUserId: 'agent-a',
74+
// The seat's identity, so the list is judged for the same caller the call
75+
// is (TASK-175).
76+
agentName: 'openclaw',
77+
instanceId: 'aria',
7478
});
7579

7680
const called = await request(app)

‎backend/__tests__/unit/services/grantBrokerConfinement.test.js‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,19 @@ describe('grant broker confinement predicate', () => {
121121
['a padded adapter name', undefined, { adapter: ' pi ' }, 'adapter_cannot_confine'],
122122
['a padded upper-case adapter name', undefined, { adapter: 'PI ' }, 'adapter_cannot_confine'],
123123
['a normalised name that is not pi', undefined, { adapter: ' claude ' }, null],
124+
// TASK-175: the adapter is `runtime.adapter` OR `runtime.runtimeType`. A
125+
// hand-attached pi seat writes {runtimeType: 'pi', host: 'byo'} and no
126+
// adapter key (cli/src/commands/agent.js:664, :688), so reading `adapter`
127+
// alone admitted exactly the seat this rule exists to refuse.
128+
['a pi seat tagged only by runtimeType (the hand-attached shape)', undefined, { runtimeType: 'pi', host: 'byo' }, 'adapter_cannot_confine'],
129+
['a pi runtimeType in another case', undefined, { runtimeType: 'PI' }, 'adapter_cannot_confine'],
130+
['a declared adapter wins over a pi runtimeType, as it does in the daemon', undefined, { adapter: 'claude', runtimeType: 'pi', host: 'byo' }, null],
131+
['a claude-code runtimeType', undefined, { runtimeType: 'claude-code', host: 'byo' }, null],
132+
['a webhook connect-page seat', undefined, { runtimeType: 'webhook', host: 'byo' }, null],
133+
// The arm that rejects "refuse anything without an adapter": undeclared, the
134+
// daemon resolves only claude or codex (cli/src/commands/daemon.js:142), so
135+
// failing closed here would refuse working claude seats.
136+
['a row naming neither adapter nor runtimeType', undefined, { host: 'byo' }, null],
124137
])('decides %s', (_label, environment, runtime, expected) => {
125138
const refusal = grantBrokerRefusal(environment, runtime);
126139
if (expected === null) {

‎backend/__tests__/unit/services/grantBrokerProjectionService.test.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,9 @@ describe('dispatchHostedBrokerTool', () => {
208208
agentUserId: SEAT,
209209
agentName: 'kai',
210210
instanceId: 'default',
211+
// Set only here: a hosted turn has no shell, web or file tools to confine,
212+
// so the seat-confinement refusal does not apply to it (Wren, TASK-175).
213+
hosted: true,
211214
tool: 'github.list_issues',
212215
args: { grantId: 'grant-someone-else', agentUserId: OTHER_SEAT, state: 'open' },
213216
});

‎backend/__tests__/unit/services/nativeRuntimeService.hostedBroker.test.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,9 @@ describe('a hosted run in a granted pod', () => {
202202
agentUserId: SEAT,
203203
agentName: 'scout',
204204
instanceId: 'default',
205+
// A hosted turn has no shell, web or file tools, so the
206+
// seat-confinement refusal is out of scope for it (Wren, TASK-175).
207+
hosted: true,
205208
tool: 'github.list_issues',
206209
args: { state: 'open' },
207210
});

0 commit comments

Comments
 (0)