Skip to content

Commit 360a738

Browse files
authored
Make Orbit MCP authorization evergreen (#39)
Remove permission-list consent UI and make active Orbit MCP grants evergreen while preserving live revocation and identity checks.
1 parent 037dc29 commit 360a738

6 files changed

Lines changed: 58 additions & 111 deletions

File tree

‎scripts/orbit-browser-tests.mjs‎

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -692,11 +692,9 @@ if (errors.length === 0) {
692692
const consentState = await page.evaluate(() => ({
693693
hash: location.hash,
694694
client: document.querySelector('#mcp-client-summary')?.textContent?.trim(),
695-
scope: document.querySelector('#mcp-scope-summary')?.textContent?.trim(),
696-
scopeOptions: [...document.querySelectorAll('#mcp-scope-options .mcp-scope-option')].map((option) => ({
697-
text: option.textContent?.trim(),
698-
})),
699-
scopeInputs: document.querySelectorAll('#mcp-scope-options input').length,
695+
scopeCard: document.querySelector('.mcp-scope-card'),
696+
scopeSummary: document.querySelector('#mcp-scope-summary'),
697+
scopeOptions: document.querySelector('#mcp-scope-options'),
700698
options: [...document.querySelectorAll('#mcp-agent-select option')].map((option) => ({
701699
value: option.value,
702700
label: option.textContent?.trim(),
@@ -705,13 +703,10 @@ if (errors.length === 0) {
705703
}));
706704
check(consentState.hash === '', 'MCP owner consentinde ticket URL fragmentından temizlenmedi.');
707705
check(consentState.client?.includes('ChatGPT'), 'MCP consent istemci adını göstermiyor.');
708-
check(consentState.scope?.includes('tek izin paketi'), 'MCP consent tek paket politikasını açıklamıyor.');
709-
check(consentState.scope?.includes('yeniden onay'), 'MCP consent gelecek yeteneklerde yeniden onayı açıklamıyor.');
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.');
713-
check(consentState.scopeInputs === 0, 'MCP consent isteğe bağlı checkbox göstermeye devam ediyor.');
714-
check(consentState.scopeOptions.every((option) => option.text?.includes('zorunlu')), 'MCP consent tüm mevcut kapsamları zorunlu göstermiyor.');
706+
check(consentState.client?.includes('yeniden onay gerektirmeden'), 'MCP consent bağlantının gelecek özelliklerde kalıcı olduğunu açıklamıyor.');
707+
check(consentState.scopeCard === null, 'MCP consent izin kartını göstermeye devam ediyor.');
708+
check(consentState.scopeSummary === null, 'MCP consent izin özeti alanını göstermeye devam ediyor.');
709+
check(consentState.scopeOptions === null, 'MCP consent izin seçeneklerini göstermeye devam ediyor.');
715710
check(consentState.options.length === browserAgents.length, 'MCP consent yönetilebilir ajan listesini eksik gösteriyor.');
716711
check(consentState.options.some((option) => option.value === 'agent-selene'), 'MCP consent Selene seçimini sunmuyor.');
717712
check(consentState.approveDisabled === false, 'MCP consent onay düğmesi kullanılabilir değil.');

‎scripts/orbit-slice1-test-worker.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,13 @@ async function testRoute(request: Request, env: TestEnv): Promise<Response | nul
257257
return Response.json({ ok: true });
258258
}
259259

260+
if (url.pathname === '/__test/set-mcp-grant-scopes') {
261+
await env.DB.prepare(`UPDATE mcp_authorization_grants SET scopes = ? WHERE id = ?`)
262+
.bind(String(body.scopes), String(body.grantId))
263+
.run();
264+
return Response.json({ ok: true });
265+
}
266+
260267
if (url.pathname === '/__test/set-agent-status') {
261268
await env.DB.batch([
262269
env.DB.prepare(`UPDATE agents SET status = ? WHERE handle_normalized = ?`)

‎scripts/orbit-slice1-tests.ts‎

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

799+
await postJson('/__test/set-mcp-grant-scopes', {
800+
grantId: mcpGrantId,
801+
scopes: 'feed:read',
802+
}, {}, NOW + 56);
803+
const evergreenInbox = await postJson(
804+
`/v1/mcp/grants/${encodeURIComponent(mcpGrantId)}/direct-messages/unread-count`,
805+
{},
806+
{ authorization: `Bearer ${MCP_SERVICE_SECRET}` },
807+
NOW + 56,
808+
);
809+
assert.equal(evergreenInbox.status, 200, await evergreenInbox.clone().text());
810+
assert.deepEqual(await evergreenInbox.json(), { unreadCount: 0 });
811+
812+
const evergreenState = await postJson(
813+
`/v1/mcp/grants/${encodeURIComponent(mcpGrantId)}/agent/state`,
814+
{},
815+
{ authorization: `Bearer ${MCP_SERVICE_SECRET}` },
816+
NOW + 56,
817+
);
818+
assert.equal(evergreenState.status, 200, await evergreenState.clone().text());
819+
const evergreenStateBody = await evergreenState.json() as {
820+
authorization: { authorizationMode: string; scopes: string[]; upgradeRequired: boolean };
821+
};
822+
assert.equal(evergreenStateBody.authorization.authorizationMode, 'full_access');
823+
assert.deepEqual(
824+
evergreenStateBody.authorization.scopes,
825+
['feed:read', 'posts:write', 'replies:write', 'messages:read', 'messages:write'],
826+
);
827+
assert.equal(evergreenStateBody.authorization.upgradeRequired, false);
828+
799829
const newerConcurrentUse = await postJson(
800830
`/v1/mcp/grants/${encodeURIComponent(mcpGrantId)}/agent/state`,
801831
{},

‎src/pages/dashboard.astro‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ import Icon from '../components/Icon.astro';
5151
<div class="context-sticky">
5252
<p class="section-label">Bağlantı izni</p>
5353
<h1 id="mcp-consent-title">Ajanını istemciye bağla.</h1>
54-
<p>Bağlantı yalnız seçtiğin ajan ve bu sürümün açıkça gösterilen izin paketi için geçerlidir.</p>
54+
<p>Bağlantı yalnız seçtiğin ajan için geçerlidir ve istediğin an iptal edilebilir.</p>
5555
</div>
5656
</aside>
5757

@@ -60,12 +60,6 @@ import Icon from '../components/Icon.astro';
6060
<h2>Bağlantıyı onayla</h2>
6161
<p id="mcp-client-summary" class="mcp-consent-summary">İstemci bilgisi yükleniyor.</p>
6262

63-
<div class="mcp-scope-card">
64-
<strong>İzin kapsamları</strong>
65-
<span id="mcp-scope-summary">İstenen kapsamlar yükleniyor.</span>
66-
<div id="mcp-scope-options" class="mcp-scope-options" aria-label="Orbit MCP izin kapsamları"></div>
67-
</div>
68-
6963
<label class="dashboard-field" for="mcp-agent-select">
7064
<span>Bağlanacak ajan</span>
7165
<select id="mcp-agent-select" autocomplete="off"></select>

‎src/scripts/dashboard.js‎

Lines changed: 3 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,6 @@
11
const byId = (id) => document.getElementById(id);
22
const MCP_TICKET_STORAGE_KEY = 'orbit_mcp_authorization_ticket_v1';
33
const MCP_CALLBACK_URL = 'https://mcp.orbit.sametbasbug.dev/oauth/orbit/callback';
4-
const MCP_SCOPE_ORDER = ['feed:read', 'posts:write', 'replies:write', 'messages:read', 'messages:write'];
5-
const MCP_SCOPE_BUNDLE_VERSION = 2;
6-
const MCP_SCOPE_DETAILS = {
7-
'feed:read': {
8-
title: 'Ajan durumunu ve özel kayıt sayılarını oku',
9-
description: 'Bağlı ajanın durumunu, yayın modunu ve özel kayıt sayılarını görüntüler.',
10-
required: true,
11-
},
12-
'posts:write': {
13-
title: 'Yeni gönderi yayımla',
14-
description: 'Bağlı ajan adına metin tabanlı kök gönderi oluşturur. Görsel, düzenleme ve silme yetkisi vermez.',
15-
required: true,
16-
},
17-
'replies:write': {
18-
title: 'Gönderi ve yanıtlara cevap ver',
19-
description: 'Bağlı ajan adına görünür bir gönderiye veya yanıta metin tabanlı cevap oluşturur.',
20-
required: true,
21-
},
22-
'messages:read': {
23-
title: 'Özel mesaj kutusunu oku',
24-
description: 'Bağlı ajanın gelen ve gönderilen özel mesajlarını, okunmamış sayısını ve okundu durumlarını görüntüler.',
25-
required: true,
26-
},
27-
'messages:write': {
28-
title: 'Özel mesaj gönder ve okundu işaretle',
29-
description: 'Bağlı ajan adına tek bir Orbit ajanına metin tabanlı özel mesaj gönderir ve alınan mesajları okundu işaretler.',
30-
required: true,
31-
},
32-
};
33-
344
let me = null;
355
let managed = null;
366
let selectedAgentId = null;
@@ -140,60 +110,20 @@ function mcpCallback(parameters) {
140110
return url.toString();
141111
}
142112

143-
function canonicalMcpScopes(value) {
144-
if (!Array.isArray(value) || value.length === 0 || value.length > MCP_SCOPE_ORDER.length) return null;
145-
if (new Set(value).size !== value.length || !value.includes('feed:read')) return null;
146-
if (value.some((scope) => !(scope in MCP_SCOPE_DETAILS))) return null;
147-
const ordered = MCP_SCOPE_ORDER.filter((scope) => value.includes(scope));
148-
return ordered.every((scope, index) => value[index] === scope) ? ordered : null;
149-
}
150-
151-
function renderMcpScopeOptions(scopes) {
152-
const host = byId('mcp-scope-options');
153-
host.replaceChildren();
154-
for (const scope of scopes) {
155-
const details = MCP_SCOPE_DETAILS[scope];
156-
const item = document.createElement('div');
157-
item.className = 'mcp-scope-option';
158-
const copy = document.createElement('span');
159-
copy.className = 'mcp-scope-option-copy';
160-
const title = document.createElement('strong');
161-
title.textContent = details.title;
162-
const description = document.createElement('span');
163-
description.textContent = details.description;
164-
const code = document.createElement('span');
165-
code.className = 'mcp-scope-option-code';
166-
code.textContent = `${scope} · zorunlu`;
167-
copy.append(title, description, code);
168-
item.append(copy);
169-
host.append(item);
170-
}
171-
}
172-
173113
async function loadMcpConsent() {
174114
if (!mcpAuthorizationTicket) return false;
175115
const { body } = await mutate('/v1/mcp/authorization-tickets/inspect', 'POST', {
176116
ticket: mcpAuthorizationTicket,
177117
});
178118
const authorizationRequest = body?.authorizationRequest;
179119
const manageableAgents = Array.isArray(body?.manageableAgents) ? body.manageableAgents : [];
180-
const requestedScopes = canonicalMcpScopes(authorizationRequest?.scopes);
181-
if (
182-
!authorizationRequest
183-
|| !requestedScopes
184-
|| requestedScopes.length !== MCP_SCOPE_ORDER.length
185-
|| authorizationRequest.scopeBundleVersion !== MCP_SCOPE_BUNDLE_VERSION
186-
) {
187-
throw new Error('Orbit MCP bağlantı isteği güncel izin paketiyle eşleşmiyor.');
188-
}
120+
if (!authorizationRequest) throw new Error('Orbit MCP bağlantı isteği doğrulanamadı.');
189121

190-
mcpAuthorizationRequest = { ...authorizationRequest, scopes: requestedScopes };
122+
mcpAuthorizationRequest = authorizationRequest;
191123
const expires = new Date(authorizationRequest.expiresAt).toLocaleTimeString('tr-TR', {
192124
hour: '2-digit', minute: '2-digit',
193125
});
194-
byId('mcp-client-summary').textContent = `${authorizationRequest.oauthClient.label} adlı istemci, seçtiğin Orbit ajanı için aşağıdaki yetkileri istiyor. İstek ${expires} saatine kadar geçerli.`;
195-
byId('mcp-scope-summary').textContent = 'Bu sürümdeki tüm Orbit yetenekleri tek izin paketi olarak verilir. Yeni bir yetenek eklendiğinde mevcut bağlantı sessizce genişlemez; yeniden onay istenir. Profil, silme, medya ve moderasyon yetkisi verilmez.';
196-
renderMcpScopeOptions(requestedScopes);
126+
byId('mcp-client-summary').textContent = `${authorizationRequest.oauthClient.label} adlı istemciyi seçtiğin Orbit ajanına bağlıyorsun. Bağlantı aktif kaldığı sürece Orbit MCP’ye sonradan eklenen ajan özellikleri de yeniden onay gerektirmeden kullanılabilir. İstek ${expires} saatine kadar geçerli.`;
197127

198128
const select = byId('mcp-agent-select');
199129
select.replaceChildren();

‎src/server/http/api.ts‎

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,8 @@ import {
2222
verifyMcpAuthorizationTicket,
2323
} from '../identity/mcp-authorization-ticket';
2424
import {
25+
CURRENT_MCP_AUTHORIZATION_SCOPE_BUNDLE,
2526
MCP_AUTHORIZATION_SCOPE_BUNDLE_VERSION,
26-
isCurrentMcpAuthorizationScopeBundle,
27-
mcpAuthorizationScopeBundleVersion,
2827
normalizeCurrentMcpAuthorizationScopeBundle,
2928
type McpAuthorizationScope,
3029
} from '../identity/mcp-authorization-scopes';
@@ -3116,16 +3115,11 @@ function currentMcpAuthorizationScopeBundle(value: unknown): McpAuthorizationSco
31163115
}
31173116

31183117
function requireMcpAuthorizationScope(
3119-
grant: McpAuthorizationGrantView,
3120-
scope: McpAuthorizationScope,
3118+
_grant: McpAuthorizationGrantView,
3119+
_scope: McpAuthorizationScope,
31213120
): void {
3122-
if (grant.scopes.includes(scope)) return;
3123-
throw new ApiError(
3124-
403,
3125-
'mcp_authorization_scope_denied',
3126-
`${scope} scope is required for this Orbit MCP operation.`,
3127-
{ requiredScope: scope, grantedScopes: grant.scopes },
3128-
);
3121+
// v0.4.2 grants are evergreen: one live agent connection authorizes the current MCP surface.
3122+
// Stored scopes remain historical/audit data and no longer gate newly introduced capabilities.
31293123
}
31303124

31313125
function mcpGrantStatus(
@@ -3145,10 +3139,11 @@ function mcpGrantResponse(grant: McpAuthorizationGrantView, now: number) {
31453139
id: grant.agentId,
31463140
handle: grant.handle,
31473141
},
3148-
scopes: grant.scopes,
3149-
scopeBundleVersion: mcpAuthorizationScopeBundleVersion(grant.scopes),
3142+
authorizationMode: 'full_access',
3143+
scopes: [...CURRENT_MCP_AUTHORIZATION_SCOPE_BUNDLE],
3144+
scopeBundleVersion: MCP_AUTHORIZATION_SCOPE_BUNDLE_VERSION,
31503145
currentScopeBundleVersion: MCP_AUTHORIZATION_SCOPE_BUNDLE_VERSION,
3151-
upgradeRequired: !isCurrentMcpAuthorizationScopeBundle(grant.scopes),
3146+
upgradeRequired: false,
31523147
oauthClient: {
31533148
id: grant.oauthClientId,
31543149
label: grant.oauthClientLabel,
@@ -3175,11 +3170,7 @@ async function resolveActiveMcpGrant(
31753170
agent: ManagedAgentView;
31763171
}> {
31773172
const grant = await mcpRepository.getGrant(grantId);
3178-
if (
3179-
!grant
3180-
|| mcpGrantStatus(grant, now) !== 'active'
3181-
|| !isCurrentMcpAuthorizationScopeBundle(grant.scopes)
3182-
) {
3173+
if (!grant || mcpGrantStatus(grant, now) !== 'active') {
31833174
throw new ApiError(
31843175
401,
31853176
'mcp_authorization_invalid',

0 commit comments

Comments
 (0)