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
42 changes: 42 additions & 0 deletions migrations/0023_mcp_authorization_source_truth.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
PRAGMA foreign_keys = ON;

DROP TRIGGER IF EXISTS mcp_delegation_redemptions_validate;

CREATE TRIGGER mcp_delegation_redemptions_validate
BEFORE INSERT ON mcp_delegation_redemptions
BEGIN
SELECT RAISE(ABORT, 'invalid_mcp_delegation_code')
WHERE NOT EXISTS (
SELECT 1
FROM mcp_delegation_codes code
JOIN mcp_authorization_grants grant_row
ON grant_row.id = code.grant_id
JOIN accounts account
ON account.id = grant_row.account_id
WHERE code.id = NEW.code_id
AND code.grant_id = NEW.grant_id
AND code.authorization_request_id = NEW.authorization_request_id
AND code.consumed_at IS NULL
AND code.expires_at > NEW.redeemed_at
AND grant_row.revoked_at IS NULL
AND (grant_row.expires_at IS NULL OR grant_row.expires_at > NEW.redeemed_at)
AND account.status = 'active'
AND (
EXISTS (
SELECT 1
FROM agent_memberships membership
WHERE membership.agent_id = grant_row.agent_id
AND membership.account_id = grant_row.account_id
AND membership.role = 'primary_sponsor'
AND membership.revoked_at IS NULL
)
OR EXISTS (
SELECT 1
FROM account_roles role
WHERE role.account_id = grant_row.account_id
AND role.role = 'platform_owner'
AND role.revoked_at IS NULL
)
)
);
END;
16 changes: 16 additions & 0 deletions scripts/orbit-d1-test-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,22 @@ async function handleAction(body: ActionRequest, env: Environment): Promise<Resp
return json({ redemption, revocation, audits: audits.results });
}

case 'revokeMcpMembership': {
await env.DB.prepare(`
UPDATE agent_memberships
SET revoked_at = ?
WHERE account_id = ?
AND agent_id = ?
AND role = 'primary_sponsor'
AND revoked_at IS NULL
`).bind(
numberValue(data, 'now'),
stringValue(data, 'accountId'),
stringValue(data, 'agentId'),
).run();
return json({ ok: true });
}

case 'exportBackup': {
return json(await createDynamicBackup(
env.DB,
Expand Down
30 changes: 30 additions & 0 deletions scripts/orbit-mcp-authorization-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,36 @@ describe('Orbit MCP authorization foundation', { concurrency: false }, () => {
);
});

test('losing current agent management invalidates touch and redemption', async () => {
const now = Date.now();
const accountId = 'mcp-source-truth-sponsor';
const agentId = 'mcp-source-truth-agent';
await callAction('seedMcpAgent', { accountId, agentId, now });
const input = grantData('mcp-source-truth', accountId, agentId, now + 1);
await callAction('createMcpGrant', input, 201);
await callAction('revokeMcpMembership', {
accountId,
agentId,
now: now + 2,
});

const touched = await callAction<{ touched: boolean }>(
'touchMcpGrant',
{ grantId: input.grantId, now: now + 3 },
);
assert.equal(touched.touched, false);

const redemption = await callAction<{ error: string }>('redeemMcpCode', {
codeId: input.codeId,
grantId: input.grantId,
authorizationRequestId: input.authorizationRequestId,
auditEventId: 'mcp-source-truth-redeem-audit',
requestId: 'mcp-source-truth-redeem-request',
now: now + 4,
}, 409);
assert.match(redemption.error, /invalid_mcp_delegation_code/u);
});

test('durable grants and revocations enter backups without transient codes', async () => {
const backup = await callAction<{
schemaVersion: number;
Expand Down
Loading