Skip to content

Commit f0688f3

Browse files
authored
Add delegated MCP agent state read (#33)
## Summary - add a service-authenticated `POST /v1/mcp/grants/{grantId}/agent/state` endpoint - return the authorized agent identity, policy state and private record counts - reuse active-grant, active-account and current sponsor/platform-owner checks - touch grant activity without exposing or accepting agent credential material - normalize the delegated state route in telemetry ## Security boundary - fixed to the existing `feed:read` grant - no `orb_agent_v1` token or credential metadata appears in the response - revoked, expired or no-longer-manageable grants fail with 401 ## Verification - `npm run test:d1` — 122/122 passed - `npm run build` — full D1, content, client, site and browser regression passed
1 parent 0aab03a commit f0688f3

3 files changed

Lines changed: 73 additions & 0 deletions

File tree

‎scripts/orbit-slice1-tests.ts‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,43 @@ let firstCredentialToken = '';
664664
assert.equal(resolvedBody.agent.handle, 'selene-test-agent');
665665
assert.equal(resolvedBody.agent.status, 'active');
666666

667+
const privateState = await postJson(
668+
`/v1/mcp/grants/${encodeURIComponent(mcpGrantId)}/agent/state`,
669+
{},
670+
{ authorization: `Bearer ${MCP_SERVICE_SECRET}` },
671+
NOW + 56,
672+
);
673+
assert.equal(privateState.status, 200, await privateState.clone().text());
674+
const privateStateText = await privateState.text();
675+
assert.ok(!privateStateText.includes('credential'));
676+
const privateStateBody = JSON.parse(privateStateText) as {
677+
authorization: { id: string; scopes: string[]; lastUsedAt: number };
678+
agent: {
679+
id: string;
680+
handle: string;
681+
status: string;
682+
onboardingState: string;
683+
publicationMode: string;
684+
};
685+
recordCounts: {
686+
total: number;
687+
pending: number;
688+
published: number;
689+
rejected: number;
690+
deleted: number;
691+
pendingReview: number;
692+
moderated: number;
693+
};
694+
};
695+
assert.equal(privateStateBody.authorization.id, mcpGrantId);
696+
assert.deepEqual(privateStateBody.authorization.scopes, ['feed:read']);
697+
assert.equal(privateStateBody.authorization.lastUsedAt, NOW + 56);
698+
assert.equal(privateStateBody.agent.id, sponsoredAgentId);
699+
assert.equal(privateStateBody.agent.handle, 'selene-test-agent');
700+
assert.equal(privateStateBody.agent.status, 'active');
701+
assert.equal(privateStateBody.recordCounts.total, 0);
702+
assert.equal(privateStateBody.recordCounts.pendingReview, 0);
703+
667704
const revokeNoCsrf = await postJson(
668705
`/v1/mcp/authorizations/${encodeURIComponent(mcpGrantId)}/revoke`,
669706
{},
@@ -694,6 +731,14 @@ let firstCredentialToken = '';
694731
);
695732
assert.equal(resolvedAfterRevoke.status, 401);
696733

734+
const stateAfterRevoke = await postJson(
735+
`/v1/mcp/grants/${encodeURIComponent(mcpGrantId)}/agent/state`,
736+
{},
737+
{ authorization: `Bearer ${MCP_SERVICE_SECRET}` },
738+
NOW + 59,
739+
);
740+
assert.equal(stateAfterRevoke.status, 401);
741+
697742
const secondRevoke = await postJson(
698743
`/v1/mcp/authorizations/${encodeURIComponent(mcpGrantId)}/revoke`,
699744
{},

‎src/server/http/api.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3199,6 +3199,33 @@ export async function handleApiRequest(
31993199
});
32003200
}
32013201

3202+
const mcpAgentStateMatch = /^\/v1\/mcp\/grants\/([^/]+)\/agent\/state$/u.exec(path);
3203+
if (request.method === 'POST' && mcpAgentStateMatch) {
3204+
authenticateMcpService(request, env);
3205+
const body = await readJson(request);
3206+
requireExactFields(body, [], 'invalid_mcp_agent_state_fields');
3207+
const grantId = decodeURIComponent(mcpAgentStateMatch[1]);
3208+
const resolved = await resolveActiveMcpGrant(
3209+
grantId,
3210+
repository,
3211+
agentRepository,
3212+
mcpRepository,
3213+
now,
3214+
true,
3215+
);
3216+
return json({
3217+
authorization: mcpGrantResponse(resolved.grant, now),
3218+
agent: {
3219+
id: resolved.agent.id,
3220+
handle: resolved.agent.handle,
3221+
status: resolved.agent.status,
3222+
onboardingState: resolved.agent.onboardingState,
3223+
publicationMode: resolved.agent.publicationMode,
3224+
},
3225+
recordCounts: await publicationRepository.getAgentRecordCounts(resolved.agent.id),
3226+
});
3227+
}
3228+
32023229
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);
32033230
if ((request.method === 'GET' || request.method === 'HEAD') && mediaReadMatch) {
32043231
return await serveMedia(

‎src/server/observability/telemetry.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ function routeName(path: string): string {
1414
[/^\/v1\/sessions\/[^/]+\/revoke$/u, '/v1/sessions/:session/revoke'],
1515
[/^\/v1\/mcp\/authorizations\/[^/]+\/revoke$/u, '/v1/mcp/authorizations/:grant/revoke'],
1616
[/^\/v1\/mcp\/grants\/[^/]+\/resolve$/u, '/v1/mcp/grants/:grant/resolve'],
17+
[/^\/v1\/mcp\/grants\/[^/]+\/agent\/state$/u, '/v1/mcp/grants/:grant/agent/state'],
1718
[/^\/v1\/announcements\/[^/]+\/read$/u, '/v1/announcements/:announcement/read'],
1819
[/^\/v1\/admin\/announcements\/[^/]+\/(?:publish|withdraw)$/u, '/v1/admin/announcements/:announcement/:action'],
1920
[/^\/v1\/admin\/invitations\/[^/]+\/revoke$/u, '/v1/admin/invitations/:invitation/revoke'],

0 commit comments

Comments
 (0)