From 0e144d77d7d6867962fafdcc75a60b1080c4bac0 Mon Sep 17 00:00:00 2001 From: sametbasbug Date: Wed, 29 Jul 2026 23:05:05 +0300 Subject: [PATCH] Add delegated MCP agent state read Expose a service-authenticated, grant-bound read for private agent state and record counts. Reuse current authorization source-truth checks, omit agent credential metadata, and fail immediately after revocation. --- scripts/orbit-slice1-tests.ts | 45 +++++++++++++++++++++++++++ src/server/http/api.ts | 27 ++++++++++++++++ src/server/observability/telemetry.ts | 1 + 3 files changed, 73 insertions(+) diff --git a/scripts/orbit-slice1-tests.ts b/scripts/orbit-slice1-tests.ts index 5c535e3..ea6c3bb 100644 --- a/scripts/orbit-slice1-tests.ts +++ b/scripts/orbit-slice1-tests.ts @@ -664,6 +664,43 @@ let firstCredentialToken = ''; assert.equal(resolvedBody.agent.handle, 'selene-test-agent'); assert.equal(resolvedBody.agent.status, 'active'); + const privateState = await postJson( + `/v1/mcp/grants/${encodeURIComponent(mcpGrantId)}/agent/state`, + {}, + { authorization: `Bearer ${MCP_SERVICE_SECRET}` }, + NOW + 56, + ); + assert.equal(privateState.status, 200, await privateState.clone().text()); + const privateStateText = await privateState.text(); + assert.ok(!privateStateText.includes('credential')); + const privateStateBody = JSON.parse(privateStateText) as { + authorization: { id: string; scopes: string[]; lastUsedAt: number }; + agent: { + id: string; + handle: string; + status: string; + onboardingState: string; + publicationMode: string; + }; + recordCounts: { + total: number; + pending: number; + published: number; + rejected: number; + deleted: number; + pendingReview: number; + moderated: number; + }; + }; + assert.equal(privateStateBody.authorization.id, mcpGrantId); + assert.deepEqual(privateStateBody.authorization.scopes, ['feed:read']); + assert.equal(privateStateBody.authorization.lastUsedAt, NOW + 56); + assert.equal(privateStateBody.agent.id, sponsoredAgentId); + assert.equal(privateStateBody.agent.handle, 'selene-test-agent'); + assert.equal(privateStateBody.agent.status, 'active'); + assert.equal(privateStateBody.recordCounts.total, 0); + assert.equal(privateStateBody.recordCounts.pendingReview, 0); + const revokeNoCsrf = await postJson( `/v1/mcp/authorizations/${encodeURIComponent(mcpGrantId)}/revoke`, {}, @@ -694,6 +731,14 @@ let firstCredentialToken = ''; ); assert.equal(resolvedAfterRevoke.status, 401); + const stateAfterRevoke = await postJson( + `/v1/mcp/grants/${encodeURIComponent(mcpGrantId)}/agent/state`, + {}, + { authorization: `Bearer ${MCP_SERVICE_SECRET}` }, + NOW + 59, + ); + assert.equal(stateAfterRevoke.status, 401); + const secondRevoke = await postJson( `/v1/mcp/authorizations/${encodeURIComponent(mcpGrantId)}/revoke`, {}, diff --git a/src/server/http/api.ts b/src/server/http/api.ts index 9c9362e..a2b19ec 100644 --- a/src/server/http/api.ts +++ b/src/server/http/api.ts @@ -3199,6 +3199,33 @@ export async function handleApiRequest( }); } + const mcpAgentStateMatch = /^\/v1\/mcp\/grants\/([^/]+)\/agent\/state$/u.exec(path); + if (request.method === 'POST' && mcpAgentStateMatch) { + authenticateMcpService(request, env); + const body = await readJson(request); + requireExactFields(body, [], 'invalid_mcp_agent_state_fields'); + const grantId = decodeURIComponent(mcpAgentStateMatch[1]); + const resolved = await resolveActiveMcpGrant( + grantId, + repository, + agentRepository, + mcpRepository, + now, + true, + ); + return json({ + authorization: mcpGrantResponse(resolved.grant, now), + agent: { + id: resolved.agent.id, + handle: resolved.agent.handle, + status: resolved.agent.status, + onboardingState: resolved.agent.onboardingState, + publicationMode: resolved.agent.publicationMode, + }, + recordCounts: await publicationRepository.getAgentRecordCounts(resolved.agent.id), + }); + } + const mediaReadMatch = /^\/v1\/media\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/u.exec(path); if ((request.method === 'GET' || request.method === 'HEAD') && mediaReadMatch) { return await serveMedia( diff --git a/src/server/observability/telemetry.ts b/src/server/observability/telemetry.ts index 7078646..94d9e5f 100644 --- a/src/server/observability/telemetry.ts +++ b/src/server/observability/telemetry.ts @@ -14,6 +14,7 @@ function routeName(path: string): string { [/^\/v1\/sessions\/[^/]+\/revoke$/u, '/v1/sessions/:session/revoke'], [/^\/v1\/mcp\/authorizations\/[^/]+\/revoke$/u, '/v1/mcp/authorizations/:grant/revoke'], [/^\/v1\/mcp\/grants\/[^/]+\/resolve$/u, '/v1/mcp/grants/:grant/resolve'], + [/^\/v1\/mcp\/grants\/[^/]+\/agent\/state$/u, '/v1/mcp/grants/:grant/agent/state'], [/^\/v1\/announcements\/[^/]+\/read$/u, '/v1/announcements/:announcement/read'], [/^\/v1\/admin\/announcements\/[^/]+\/(?:publish|withdraw)$/u, '/v1/admin/announcements/:announcement/:action'], [/^\/v1\/admin\/invitations\/[^/]+\/revoke$/u, '/v1/admin/invitations/:invitation/revoke'],