Skip to content

Commit c85b474

Browse files
committed
Add delegated Orbit inbox
Add permission bundle v2 with message scopes, delegated inbox service routes, shared DM principals, telemetry-safe route templates, migration 0025, checkbox-free consent updates, and full inbox/idempotency/revocation coverage.
1 parent b3ae8a3 commit c85b474

8 files changed

Lines changed: 674 additions & 84 deletions

File tree

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
PRAGMA defer_foreign_keys = ON;
2+
3+
DROP TRIGGER IF EXISTS mcp_delegation_codes_validate;
4+
DROP TRIGGER IF EXISTS mcp_delegation_redemptions_validate;
5+
DROP TRIGGER IF EXISTS mcp_authorization_revocations_validate;
6+
DROP TRIGGER IF EXISTS mcp_authorization_revocations_apply;
7+
8+
CREATE TABLE mcp_authorization_grants_v04 (
9+
id TEXT PRIMARY KEY,
10+
account_id TEXT NOT NULL REFERENCES accounts(id),
11+
agent_id TEXT NOT NULL REFERENCES agents(id),
12+
scopes TEXT NOT NULL CHECK (
13+
scopes IN (
14+
'feed:read',
15+
'feed:read posts:write',
16+
'feed:read replies:write',
17+
'feed:read posts:write replies:write',
18+
'feed:read posts:write replies:write messages:read messages:write'
19+
)
20+
),
21+
oauth_client_id TEXT NOT NULL CHECK (
22+
length(trim(oauth_client_id)) BETWEEN 1 AND 255
23+
),
24+
oauth_client_label TEXT NOT NULL CHECK (
25+
length(trim(oauth_client_label)) BETWEEN 1 AND 120
26+
),
27+
created_at INTEGER NOT NULL,
28+
last_used_at INTEGER,
29+
expires_at INTEGER,
30+
revoked_at INTEGER,
31+
revoked_reason TEXT,
32+
CHECK (last_used_at IS NULL OR last_used_at >= created_at),
33+
CHECK (expires_at IS NULL OR expires_at > created_at),
34+
CHECK (revoked_at IS NULL OR revoked_at >= created_at),
35+
CHECK (
36+
(revoked_at IS NULL AND revoked_reason IS NULL)
37+
OR
38+
(
39+
revoked_at IS NOT NULL
40+
AND revoked_reason IS NOT NULL
41+
AND length(trim(revoked_reason)) BETWEEN 1 AND 120
42+
)
43+
)
44+
);
45+
46+
INSERT INTO mcp_authorization_grants_v04 (
47+
id, account_id, agent_id, scopes, oauth_client_id,
48+
oauth_client_label, created_at, last_used_at, expires_at,
49+
revoked_at, revoked_reason
50+
)
51+
SELECT
52+
id, account_id, agent_id, scopes, oauth_client_id,
53+
oauth_client_label, created_at, last_used_at, expires_at,
54+
revoked_at, revoked_reason
55+
FROM mcp_authorization_grants;
56+
57+
DROP TABLE mcp_authorization_grants;
58+
ALTER TABLE mcp_authorization_grants_v04 RENAME TO mcp_authorization_grants;
59+
60+
CREATE INDEX mcp_authorization_grants_account_state_idx
61+
ON mcp_authorization_grants (account_id, revoked_at, expires_at, created_at DESC);
62+
63+
CREATE INDEX mcp_authorization_grants_agent_state_idx
64+
ON mcp_authorization_grants (agent_id, revoked_at, expires_at, created_at DESC);
65+
66+
CREATE INDEX mcp_authorization_grants_client_idx
67+
ON mcp_authorization_grants (oauth_client_id, created_at DESC);
68+
69+
CREATE TRIGGER mcp_authorization_grants_validate
70+
BEFORE INSERT ON mcp_authorization_grants
71+
BEGIN
72+
SELECT RAISE(ABORT, 'mcp_authorization_account_inactive')
73+
WHERE NOT EXISTS (
74+
SELECT 1
75+
FROM accounts account
76+
WHERE account.id = NEW.account_id
77+
AND account.status = 'active'
78+
);
79+
80+
SELECT RAISE(ABORT, 'mcp_authorization_agent_not_manageable')
81+
WHERE NOT EXISTS (
82+
SELECT 1
83+
FROM agents agent
84+
WHERE agent.id = NEW.agent_id
85+
AND (
86+
EXISTS (
87+
SELECT 1
88+
FROM agent_memberships membership
89+
WHERE membership.agent_id = agent.id
90+
AND membership.account_id = NEW.account_id
91+
AND membership.role = 'primary_sponsor'
92+
AND membership.revoked_at IS NULL
93+
)
94+
OR EXISTS (
95+
SELECT 1
96+
FROM account_roles role
97+
WHERE role.account_id = NEW.account_id
98+
AND role.role = 'platform_owner'
99+
AND role.revoked_at IS NULL
100+
)
101+
)
102+
);
103+
END;
104+
105+
CREATE TRIGGER mcp_authorization_grants_identity_immutable
106+
BEFORE UPDATE OF account_id, agent_id, scopes, oauth_client_id, oauth_client_label, created_at, expires_at
107+
ON mcp_authorization_grants
108+
BEGIN
109+
SELECT RAISE(ABORT, 'mcp_authorization_grant_identity_immutable');
110+
END;
111+
112+
CREATE TRIGGER mcp_delegation_codes_validate
113+
BEFORE INSERT ON mcp_delegation_codes
114+
BEGIN
115+
SELECT RAISE(ABORT, 'mcp_authorization_grant_unavailable')
116+
WHERE NOT EXISTS (
117+
SELECT 1
118+
FROM mcp_authorization_grants grant_row
119+
WHERE grant_row.id = NEW.grant_id
120+
AND grant_row.revoked_at IS NULL
121+
AND (grant_row.expires_at IS NULL OR grant_row.expires_at > NEW.created_at)
122+
);
123+
END;
124+
125+
CREATE TRIGGER mcp_delegation_redemptions_validate
126+
BEFORE INSERT ON mcp_delegation_redemptions
127+
BEGIN
128+
SELECT RAISE(ABORT, 'invalid_mcp_delegation_code')
129+
WHERE NOT EXISTS (
130+
SELECT 1
131+
FROM mcp_delegation_codes code
132+
JOIN mcp_authorization_grants grant_row
133+
ON grant_row.id = code.grant_id
134+
JOIN accounts account
135+
ON account.id = grant_row.account_id
136+
WHERE code.id = NEW.code_id
137+
AND code.grant_id = NEW.grant_id
138+
AND code.authorization_request_id = NEW.authorization_request_id
139+
AND code.consumed_at IS NULL
140+
AND code.expires_at > NEW.redeemed_at
141+
AND grant_row.revoked_at IS NULL
142+
AND (grant_row.expires_at IS NULL OR grant_row.expires_at > NEW.redeemed_at)
143+
AND account.status = 'active'
144+
AND (
145+
EXISTS (
146+
SELECT 1
147+
FROM agent_memberships membership
148+
WHERE membership.agent_id = grant_row.agent_id
149+
AND membership.account_id = grant_row.account_id
150+
AND membership.role = 'primary_sponsor'
151+
AND membership.revoked_at IS NULL
152+
)
153+
OR EXISTS (
154+
SELECT 1
155+
FROM account_roles role
156+
WHERE role.account_id = grant_row.account_id
157+
AND role.role = 'platform_owner'
158+
AND role.revoked_at IS NULL
159+
)
160+
)
161+
);
162+
END;
163+
164+
CREATE TRIGGER mcp_authorization_revocations_validate
165+
BEFORE INSERT ON mcp_authorization_revocations
166+
BEGIN
167+
SELECT RAISE(ABORT, 'mcp_authorization_grant_unavailable')
168+
WHERE NOT EXISTS (
169+
SELECT 1
170+
FROM mcp_authorization_grants grant_row
171+
WHERE grant_row.id = NEW.grant_id
172+
AND grant_row.revoked_at IS NULL
173+
);
174+
175+
SELECT RAISE(ABORT, 'mcp_authorization_revoke_forbidden')
176+
WHERE NOT EXISTS (
177+
SELECT 1
178+
FROM mcp_authorization_grants grant_row
179+
WHERE grant_row.id = NEW.grant_id
180+
AND (
181+
grant_row.account_id = NEW.actor_account_id
182+
OR EXISTS (
183+
SELECT 1
184+
FROM agent_memberships membership
185+
WHERE membership.agent_id = grant_row.agent_id
186+
AND membership.account_id = NEW.actor_account_id
187+
AND membership.role = 'primary_sponsor'
188+
AND membership.revoked_at IS NULL
189+
)
190+
OR EXISTS (
191+
SELECT 1
192+
FROM account_roles role
193+
WHERE role.account_id = NEW.actor_account_id
194+
AND role.role = 'platform_owner'
195+
AND role.revoked_at IS NULL
196+
)
197+
)
198+
);
199+
END;
200+
201+
CREATE TRIGGER mcp_authorization_revocations_apply
202+
AFTER INSERT ON mcp_authorization_revocations
203+
BEGIN
204+
UPDATE mcp_authorization_grants
205+
SET revoked_at = NEW.revoked_at,
206+
revoked_reason = trim(NEW.reason)
207+
WHERE id = NEW.grant_id;
208+
END;
209+
210+
PRAGMA defer_foreign_keys = OFF;

‎scripts/orbit-browser-tests.mjs‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,8 @@ if (errors.length === 0) {
205205
authorizationRequest: {
206206
id: browserMcpAuthorizationRequestId,
207207
oauthClient: { id: 'chatgpt-browser-client', label: 'ChatGPT' },
208-
scopes: ['feed:read', 'posts:write', 'replies:write'],
209-
scopeBundleVersion: 1,
208+
scopes: ['feed:read', 'posts:write', 'replies:write', 'messages:read', 'messages:write'],
209+
scopeBundleVersion: 2,
210210
issuedAt: Date.now(),
211211
expiresAt: Date.now() + 10 * 60 * 1000,
212212
},
@@ -707,7 +707,9 @@ if (errors.length === 0) {
707707
check(consentState.client?.includes('ChatGPT'), 'MCP consent istemci adını göstermiyor.');
708708
check(consentState.scope?.includes('tek izin paketi'), 'MCP consent tek paket politikasını açıklamıyor.');
709709
check(consentState.scope?.includes('yeniden onay'), 'MCP consent gelecek yeteneklerde yeniden onayı açıklamıyor.');
710-
check(consentState.scopeOptions.length === 3, 'MCP consent zorunlu kapsamları ayrı kartlarda göstermiyor.');
710+
check(consentState.scopeOptions.length === 5, 'MCP consent zorunlu kapsamları ayrı kartlarda göstermiyor.');
711+
check(consentState.scopeOptions.some((option) => option.text?.includes('messages:read')), 'MCP consent mesaj okuma kapsamını göstermiyor.');
712+
check(consentState.scopeOptions.some((option) => option.text?.includes('messages:write')), 'MCP consent mesaj yazma kapsamını göstermiyor.');
711713
check(consentState.scopeInputs === 0, 'MCP consent isteğe bağlı checkbox göstermeye devam ediyor.');
712714
check(consentState.scopeOptions.every((option) => option.text?.includes('zorunlu')), 'MCP consent tüm mevcut kapsamları zorunlu göstermiyor.');
713715
check(consentState.options.length === browserAgents.length, 'MCP consent yönetilebilir ajan listesini eksik gösteriyor.');

‎scripts/orbit-mcp-authorization-tests.ts‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ describe('Orbit MCP authorization foundation', { concurrency: false }, () => {
227227
assert.deepEqual(listed.grants.map((grant) => grant.id), [input.grantId]);
228228
});
229229

230-
test('canonical read and write scope combinations persist without widening old grants', async () => {
230+
test('legacy scope combinations persist and the current inbox bundle is canonical', async () => {
231231
const now = Date.now();
232232
const accountId = 'mcp-scope-sponsor';
233233
const agentId = 'mcp-scope-agent';
@@ -238,6 +238,7 @@ describe('Orbit MCP authorization foundation', { concurrency: false }, () => {
238238
['feed:read', 'posts:write'],
239239
['feed:read', 'replies:write'],
240240
['feed:read', 'posts:write', 'replies:write'],
241+
['feed:read', 'posts:write', 'replies:write', 'messages:read', 'messages:write'],
241242
];
242243
for (const [index, scopes] of combinations.entries()) {
243244
const input = {
@@ -250,10 +251,16 @@ describe('Orbit MCP authorization foundation', { concurrency: false }, () => {
250251

251252
const reordered = {
252253
...grantData('mcp-scope-reordered', accountId, agentId, now + 10),
253-
scopes: ['replies:write', 'feed:read', 'posts:write'],
254+
scopes: ['messages:write', 'replies:write', 'feed:read', 'messages:read', 'posts:write'],
254255
};
255256
const canonical = await callAction<{ grant: { scopes: string[] } }>('createMcpGrant', reordered, 201);
256-
assert.deepEqual(canonical.grant.scopes, ['feed:read', 'posts:write', 'replies:write']);
257+
assert.deepEqual(canonical.grant.scopes, [
258+
'feed:read',
259+
'posts:write',
260+
'replies:write',
261+
'messages:read',
262+
'messages:write',
263+
]);
257264
});
258265

259266
test('unrelated accounts and invalid scope sets are rejected', async () => {
@@ -273,6 +280,8 @@ describe('Orbit MCP authorization foundation', { concurrency: false }, () => {
273280
['posts:write'],
274281
['feed:read', 'feed:read'],
275282
['feed:read', 'records:write'],
283+
['feed:read', 'posts:write', 'replies:write', 'messages:read'],
284+
['feed:read', 'messages:write'],
276285
];
277286
for (const [index, scopes] of invalidSets.entries()) {
278287
const invalid = {

0 commit comments

Comments
 (0)