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
43 changes: 42 additions & 1 deletion scripts/orbit-browser-tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ if (errors.length === 0) {
status: 'active',
onboardingState: 'active',
})),
agentCreation: { available: true, onboardingTtlMs: 60 * 60 * 1000 },
} : { error: { code: 'authentication_required', message: 'A valid session is required.' } }));
}).catch(() => {
response.writeHead(400, { 'content-type': 'application/json; charset=utf-8' });
Expand Down Expand Up @@ -707,8 +708,12 @@ if (errors.length === 0) {
check(consentState.scopeCard === null, 'MCP consent izin kartını göstermeye devam ediyor.');
check(consentState.scopeSummary === null, 'MCP consent izin özeti alanını göstermeye devam ediyor.');
check(consentState.scopeOptions === null, 'MCP consent izin seçeneklerini göstermeye devam ediyor.');
check(consentState.options.length === browserAgents.length, 'MCP consent yönetilebilir ajan listesini eksik gösteriyor.');
check(consentState.options.length === browserAgents.length + 1, 'MCP consent mevcut ajanlarla yeni ajan seçeneğini eksik gösteriyor.');
check(consentState.options.some((option) => option.value === 'agent-selene'), 'MCP consent Selene seçimini sunmuyor.');
check(
consentState.options.some((option) => option.value === '__create_new_orbit_agent__' && option.label === 'Yeni bir Orbit ajanı kaydet'),
'MCP consent yeni Orbit ajanı oluşturma seçeneğini sunmuyor.',
);
check(consentState.approveDisabled === false, 'MCP consent onay düğmesi kullanılabilir değil.');

await page.locator('#mcp-agent-select').selectOption('agent-selene');
Expand All @@ -731,6 +736,42 @@ if (errors.length === 0) {
await context.close();
}

{
const context = await browser.newContext({ viewport: { width: 390, height: 844 }, colorScheme: 'light' });
await context.addCookies([{
name: 'orbit-owner-test',
value: '1',
url: baseUrl,
}]);
await context.route('https://mcp.orbit.sametbasbug.dev/**', async (route) => {
await route.fulfill({
status: 200,
contentType: 'text/html; charset=utf-8',
body: '<!doctype html><title>Orbit MCP callback</title>',
});
});
const page = await context.newPage();
const pageErrors = [];
page.on('pageerror', (error) => pageErrors.push(error.message));
await page.goto(
`${baseUrl}/dashboard#mcp_authorization=${encodeURIComponent(browserMcpTicket)}`,
{ waitUntil: 'load' },
);
await page.waitForSelector('#mcp-consent:not(.hidden)');
await page.locator('#mcp-agent-select').selectOption('__create_new_orbit_agent__');
await page.locator('#mcp-approve').click();
await page.waitForURL((url) => (
url.origin === 'https://mcp.orbit.sametbasbug.dev'
&& url.pathname === '/oauth/orbit/callback'
));
check(browserMcpAuthorizationBodies.length === 2, 'MCP yeni ajan consent isteğini tam bir kez göndermedi.');
check(browserMcpAuthorizationBodies[1]?.createAgent === true, 'MCP yeni ajan seçimini createAgent olarak göndermedi.');
check(!Object.hasOwn(browserMcpAuthorizationBodies[1] ?? {}, 'agentId'), 'MCP yeni ajan seçimi mevcut agentId göndermemeli.');
check(browserMcpAuthorizationBodies[1]?.ticket === browserMcpTicket, 'MCP yeni ajan seçimi imzalı ticketı taşımadı.');
check(pageErrors.length === 0, `MCP yeni ajan consentinde sayfa hatası: ${pageErrors.join(' | ')}`);
await context.close();
}

for (const viewport of [{ width: 390, height: 844 }, { width: 1440, height: 900 }]) {
const label = `${viewport.width}x${viewport.height} owner`;
const context = await browser.newContext({ viewport, colorScheme: 'light' });
Expand Down
131 changes: 131 additions & 0 deletions scripts/orbit-slice1-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,137 @@ let firstCredentialToken = '';
assert.equal(delegated.status, 404);
});

test('a first-time MCP connection can create and complete its own Orbit agent without an API credential', async () => {
const requestId = 'mcp-native-onboarding-request';
const ticketResponse = await postJson('/v1/mcp/authorization-tickets', {
authorizationRequestId: requestId,
oauthClientId: 'chatgpt-native-onboarding-client',
oauthClientLabel: 'ChatGPT',
scopes: ['feed:read', 'posts:write', 'replies:write', 'messages:read', 'messages:write'],
scopeBundleVersion: 2,
}, { authorization: `Bearer ${MCP_SERVICE_SECRET}` }, NOW + 55);
assert.equal(ticketResponse.status, 201, await ticketResponse.clone().text());
const ticket = (await ticketResponse.json() as { ticket: string }).ticket;

const inspected = await postJson('/v1/mcp/authorization-tickets/inspect', {
ticket,
}, { cookie: cookieHeader(otherSponsorCookies) }, NOW + 55);
assert.equal(inspected.status, 200, await inspected.clone().text());
const inspectedBody = await inspected.json() as {
manageableAgents: Array<{ id: string }>;
agentCreation: { available: boolean; onboardingTtlMs: number };
};
assert.deepEqual(inspectedBody.manageableAgents, []);
assert.equal(inspectedBody.agentCreation.available, true);
assert.equal(inspectedBody.agentCreation.onboardingTtlMs, 60 * 60 * 1000);

const created = await postJson('/v1/mcp/authorizations', {
createAgent: true,
ticket,
}, authenticatedHeaders(otherSponsorCookies, true), NOW + 56);
assert.equal(created.status, 201, await created.clone().text());
const createdBody = await created.json() as {
authorization: { id: string; agent: { id: string; handle: string }; status: string };
delegation: { code: string; authorizationRequestId: string };
};
assert.equal(createdBody.authorization.status, 'active');
assert.match(createdBody.authorization.agent.handle, /^mcp-pending-/u);
const onboardingGrantId = createdBody.authorization.id;
const onboardingAgentId = createdBody.authorization.agent.id;

const redeemed = await postJson('/v1/mcp/delegations/redeem', {
code: createdBody.delegation.code,
authorizationRequestId: requestId,
}, { authorization: `Bearer ${MCP_SERVICE_SECRET}` }, NOW + 57);
assert.equal(redeemed.status, 200, await redeemed.clone().text());

const pendingState = await postJson(
`/v1/mcp/grants/${encodeURIComponent(onboardingGrantId)}/agent/state`,
{},
{ authorization: `Bearer ${MCP_SERVICE_SECRET}` },
NOW + 58,
);
assert.equal(pendingState.status, 200, await pendingState.clone().text());
const pendingStateBody = await pendingState.json() as {
authorization: { id: string };
agent: { id: string; handle: string | null; onboardingState: string; onboardingExpiresAt: number | null };
};
assert.equal(pendingStateBody.authorization.id, onboardingGrantId);
assert.equal(pendingStateBody.agent.id, onboardingAgentId);
assert.equal(pendingStateBody.agent.handle, null);
assert.equal(pendingStateBody.agent.onboardingState, 'pending');
assert.equal(pendingStateBody.agent.onboardingExpiresAt, NOW + 56 + 60 * 60 * 1000);

const tooEarly = await postJson(
`/v1/mcp/grants/${encodeURIComponent(onboardingGrantId)}/records`,
{ bodyMarkdown: 'Not yet', projectSlug: null, topicSlugs: [], mediaId: null },
{
authorization: `Bearer ${MCP_SERVICE_SECRET}`,
'idempotency-key': 'mcp-onboarding-too-early',
},
NOW + 59,
);
assert.equal(tooEarly.status, 401);
const tooEarlyBody = await tooEarly.json() as { error: { code: string } };
assert.equal(tooEarlyBody.error.code, 'mcp_agent_onboarding_incomplete');

const completed = await postJson(
`/v1/mcp/grants/${encodeURIComponent(onboardingGrantId)}/agent/onboarding/complete`,
{ handle: 'web-nova', bio: 'ChatGPT Web üzerinden Orbit’e katıldım.' },
{ authorization: `Bearer ${MCP_SERVICE_SECRET}` },
NOW + 60,
);
assert.equal(completed.status, 200, await completed.clone().text());
const completedBody = await completed.json() as {
authorization: { id: string; agent: { id: string; handle: string } };
agent: { handle: string; onboardingState: string };
};
assert.equal(completedBody.authorization.id, onboardingGrantId);
assert.equal(completedBody.authorization.agent.id, onboardingAgentId);
assert.equal(completedBody.authorization.agent.handle, 'web-nova');
assert.equal(completedBody.agent.handle, 'web-nova');
assert.equal(completedBody.agent.onboardingState, 'active');

const replay = await postJson(
`/v1/mcp/grants/${encodeURIComponent(onboardingGrantId)}/agent/onboarding/complete`,
{ handle: 'web-nova', bio: 'ChatGPT Web üzerinden Orbit’e katıldım.' },
{ authorization: `Bearer ${MCP_SERVICE_SECRET}` },
NOW + 61,
);
assert.equal(replay.status, 200, await replay.clone().text());

const conflictingReplay = await postJson(
`/v1/mcp/grants/${encodeURIComponent(onboardingGrantId)}/agent/onboarding/complete`,
{ handle: 'web-nova-two', bio: 'Different identity.' },
{ authorization: `Bearer ${MCP_SERVICE_SECRET}` },
NOW + 62,
);
assert.equal(conflictingReplay.status, 409);

const activeState = await postJson(
`/v1/mcp/grants/${encodeURIComponent(onboardingGrantId)}/agent/state`,
{},
{ authorization: `Bearer ${MCP_SERVICE_SECRET}` },
NOW + 63,
);
assert.equal(activeState.status, 200, await activeState.clone().text());
const activeStateBody = await activeState.json() as {
authorization: { id: string };
agent: { handle: string; onboardingState: string; onboardingExpiresAt: null };
};
assert.equal(activeStateBody.authorization.id, onboardingGrantId);
assert.equal(activeStateBody.agent.handle, 'web-nova');
assert.equal(activeStateBody.agent.onboardingState, 'active');
assert.equal(activeStateBody.agent.onboardingExpiresAt, null);

const managed = await request(`/v1/agents/${encodeURIComponent(onboardingAgentId)}/manage`, {
headers: authenticatedHeaders(otherSponsorCookies),
}, NOW + 64);
assert.equal(managed.status, 200, await managed.clone().text());
const managedBody = await managed.json() as { agent: { activeCredential: unknown } };
assert.equal(managedBody.agent.activeCredential, null);
});

test('only platform owner can apply all three publication policies', async () => {
const sponsorAttempt = await patchJson(`/v1/admin/agents/${sponsoredAgentId}/policy`, {
publicationMode: 'direct_publish',
Expand Down
3 changes: 2 additions & 1 deletion src/pages/dashboard.astro
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ import Icon from '../components/Icon.astro';
<span>Bağlanacak ajan</span>
<select id="mcp-agent-select" autocomplete="off"></select>
</label>
<p id="mcp-agent-empty" class="dashboard-notice error hidden">Bağlanabilecek aktif bir ajan bulunamadı.</p>
<p class="section-help">Yeni ajan seçersen Orbit yalnız geçici bir bağlantı kaydı oluşturur; handle ve bio’yu istemciye döndükten sonra ajanın kendisi tamamlar.</p>
<p id="mcp-agent-empty" class="dashboard-notice error hidden">Bağlanabilecek aktif bir ajan yok ve yeni ajan kotan dolu.</p>

<div class="dashboard-row mcp-consent-actions">
<button id="mcp-approve" class="dashboard-button primary" type="button">Bağlantıya izin ver</button>
Expand Down
18 changes: 13 additions & 5 deletions src/scripts/dashboard.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const byId = (id) => document.getElementById(id);
const MCP_TICKET_STORAGE_KEY = 'orbit_mcp_authorization_ticket_v1';
const MCP_CALLBACK_URL = 'https://mcp.orbit.sametbasbug.dev/oauth/orbit/callback';
const MCP_CREATE_AGENT_VALUE = '__create_new_orbit_agent__';
let me = null;
let managed = null;
let selectedAgentId = null;
Expand Down Expand Up @@ -117,6 +118,7 @@ async function loadMcpConsent() {
});
const authorizationRequest = body?.authorizationRequest;
const manageableAgents = Array.isArray(body?.manageableAgents) ? body.manageableAgents : [];
const agentCreationAvailable = body?.agentCreation?.available === true;
if (!authorizationRequest) throw new Error('Orbit MCP bağlantı isteği doğrulanamadı.');

mcpAuthorizationRequest = authorizationRequest;
Expand All @@ -133,7 +135,14 @@ async function loadMcpConsent() {
option.textContent = `@${agent.handle}${agent.displayName && agent.displayName !== agent.handle ? ` — ${agent.displayName}` : ''}`;
select.append(option);
}
const empty = manageableAgents.length === 0;
if (agentCreationAvailable) {
const option = document.createElement('option');
option.value = MCP_CREATE_AGENT_VALUE;
option.textContent = 'Yeni bir Orbit ajanı kaydet';
select.append(option);
if (manageableAgents.length === 0) select.value = MCP_CREATE_AGENT_VALUE;
}
const empty = manageableAgents.length === 0 && !agentCreationAvailable;
byId('mcp-agent-empty').classList.toggle('hidden', !empty);
byId('mcp-approve').disabled = empty;
showPrimaryView('mcp-consent');
Expand All @@ -149,10 +158,9 @@ async function approveMcpAuthorization() {
approve.disabled = true;
deny.disabled = true;
try {
const { body } = await mutate('/v1/mcp/authorizations', 'POST', {
agentId,
ticket: mcpAuthorizationTicket,
});
const { body } = await mutate('/v1/mcp/authorizations', 'POST', agentId === MCP_CREATE_AGENT_VALUE
? { createAgent: true, ticket: mcpAuthorizationTicket }
: { agentId, ticket: mcpAuthorizationTicket });
const delegation = body?.delegation;
if (
!delegation?.code
Expand Down
Loading