Skip to content

Commit 037dc29

Browse files
authored
Fix concurrent MCP grant usage race (#38)
Revalidate live grants after monotonic usage-touch races so parallel MCP reads do not invalidate each other while preserving revocation and authority checks.
1 parent 555cf33 commit 037dc29

2 files changed

Lines changed: 56 additions & 21 deletions

File tree

‎scripts/orbit-slice1-tests.ts‎

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,26 @@ let firstCredentialToken = '';
796796
assert.equal(privateStateBody.recordCounts.total, 0);
797797
assert.equal(privateStateBody.recordCounts.pendingReview, 0);
798798

799+
const newerConcurrentUse = await postJson(
800+
`/v1/mcp/grants/${encodeURIComponent(mcpGrantId)}/agent/state`,
801+
{},
802+
{ authorization: `Bearer ${MCP_SERVICE_SECRET}` },
803+
NOW + 57,
804+
);
805+
assert.equal(newerConcurrentUse.status, 200, await newerConcurrentUse.clone().text());
806+
807+
const olderConcurrentUse = await postJson(
808+
`/v1/mcp/grants/${encodeURIComponent(mcpGrantId)}/resolve`,
809+
{},
810+
{ authorization: `Bearer ${MCP_SERVICE_SECRET}` },
811+
NOW + 56,
812+
);
813+
assert.equal(olderConcurrentUse.status, 200, await olderConcurrentUse.clone().text());
814+
const olderConcurrentUseBody = await olderConcurrentUse.json() as {
815+
authorization: { lastUsedAt: number };
816+
};
817+
assert.equal(olderConcurrentUseBody.authorization.lastUsedAt, NOW + 57);
818+
799819
await postJson('/__test/set-agent-status', {
800820
handle: 'selene-test-agent',
801821
status: 'suspended',
@@ -1080,21 +1100,22 @@ let firstCredentialToken = '';
10801100
assert.equal(sentBoxBody.directMessages[0]?.bodyMarkdown, directMessageBody.bodyMarkdown);
10811101
assert.equal(sentBoxBody.nextCursor, null);
10821102

1083-
const recipientUnread = await postJson(
1084-
`/v1/mcp/grants/${encodeURIComponent(recipientGrant.grantId)}/direct-messages/unread-count`,
1085-
{},
1086-
mcpServiceHeaders(),
1087-
NOW + 23_002,
1088-
);
1103+
const [recipientUnread, recipientInbox] = await Promise.all([
1104+
postJson(
1105+
`/v1/mcp/grants/${encodeURIComponent(recipientGrant.grantId)}/direct-messages/unread-count`,
1106+
{},
1107+
mcpServiceHeaders(),
1108+
NOW + 23_002,
1109+
),
1110+
postJson(
1111+
`/v1/mcp/grants/${encodeURIComponent(recipientGrant.grantId)}/direct-messages/list`,
1112+
{ box: 'inbox', limit: 10 },
1113+
mcpServiceHeaders(),
1114+
NOW + 23_003,
1115+
),
1116+
]);
10891117
assert.equal(recipientUnread.status, 200, await recipientUnread.clone().text());
10901118
assert.deepEqual(await recipientUnread.json(), { unreadCount: 1 });
1091-
1092-
const recipientInbox = await postJson(
1093-
`/v1/mcp/grants/${encodeURIComponent(recipientGrant.grantId)}/direct-messages/list`,
1094-
{ box: 'inbox', limit: 10 },
1095-
mcpServiceHeaders(),
1096-
NOW + 23_003,
1097-
);
10981119
assert.equal(recipientInbox.status, 200, await recipientInbox.clone().text());
10991120
const recipientInboxBody = await recipientInbox.json() as {
11001121
directMessages: Array<{ id: string; sender: { handle: string }; bodyMarkdown: string; readAt: number | null }>;

‎src/server/http/api.ts‎

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3205,15 +3205,29 @@ async function resolveActiveMcpGrant(
32053205
{ 'www-authenticate': 'Bearer' },
32063206
);
32073207
}
3208-
if (touch && !await mcpRepository.touchGrant({ grantId, usedAt: now })) {
3209-
if (grant.lastUsedAt !== now) {
3210-
throw new ApiError(
3211-
401,
3212-
'mcp_authorization_invalid',
3213-
'The Orbit MCP authorization changed before it could be used.',
3214-
{},
3215-
{ 'www-authenticate': 'Bearer' },
3208+
if (touch) {
3209+
const touched = await mcpRepository.touchGrant({ grantId, usedAt: now });
3210+
if (!touched) {
3211+
// A concurrent valid request may have advanced lastUsedAt after this request read the grant.
3212+
// Revalidate the live authorization and accept only if usage already reached this request time.
3213+
const refreshed = await resolveActiveMcpGrant(
3214+
grantId,
3215+
identityRepository,
3216+
agentRepository,
3217+
mcpRepository,
3218+
now,
3219+
false,
32163220
);
3221+
if (refreshed.grant.lastUsedAt === null || refreshed.grant.lastUsedAt < now) {
3222+
throw new ApiError(
3223+
401,
3224+
'mcp_authorization_invalid',
3225+
'The Orbit MCP authorization changed before it could be used.',
3226+
{},
3227+
{ 'www-authenticate': 'Bearer' },
3228+
);
3229+
}
3230+
return refreshed;
32173231
}
32183232
}
32193233
return {

0 commit comments

Comments
 (0)