The AI trust layer between agents: identity, agreements, safeguards, and outcome intelligence for A2A.

Viewing files at commit 922a6b480fb8Commit detailsView latest

apps/api/src/access-control.ts 105 lines · 3.5 KB · typescript
1
import {
2
  DEFAULT_MCP_AUTH_SCOPES,
3
  OPENCLASP_AUTH_SCOPES,
4
  type OpenClaspAuthScope,
5
} from '../../../packages/protocol/src/index.js';
6
7
export { DEFAULT_MCP_AUTH_SCOPES, OPENCLASP_AUTH_SCOPES, type OpenClaspAuthScope };
8
9
const PROFILE_TOOLS = new Set([
10
  'openclasp_get_profile',
11
  'openclasp_assess_counterparty',
12
  'openclasp_check_claim',
13
  'openclasp_validate_commitment',
14
  'openclasp_suggest_resolution',
15
  'openclasp_verify_receipt',
16
  'openclasp_get_identity',
17
  'openclasp_connection_status',
18
  'openclasp_find_agent',
19
  'openclasp_search_agents',
20
  'openclasp_list_invitations',
21
  'openclasp_get_shared_interaction',
22
  'openclasp_get_live_session',
23
  'openclasp_list_threads',
24
  'openclasp_get_thread',
25
  'openclasp_list_feedback_requests',
26
  'openclasp_resolve_agent',
27
  'openclasp_get_contextual_intelligence',
28
  'openclasp_recommend_agents',
29
]);
30
31
const AGENT_TOOLS = new Set([
32
  'openclasp_register_agent',
33
  'openclasp_setup',
34
  'openclasp_switch_agent',
35
  'openclasp_update_profile',
36
  'openclasp_register_delegation',
37
  'openclasp_heartbeat',
38
]);
39
40
const FEEDBACK_TOOLS = new Set([
41
  'openclasp_complete_interaction',
42
  'openclasp_submit_feedback',
43
  'openclasp_complete_live_session',
44
  'openclasp_submit_completion_report',
45
  'openclasp_submit_interaction_feedback',
46
]);
47
48
export function requiredMcpToolScope(name: string): OpenClaspAuthScope {
49
  if (PROFILE_TOOLS.has(name)) return 'profile:read';
50
  if (AGENT_TOOLS.has(name)) return 'agent:manage';
51
  if (FEEDBACK_TOOLS.has(name)) return 'feedback:write';
52
  return 'interaction:write';
53
}
54
55
export async function requiredMcpRequestScopes(request: Request): Promise<OpenClaspAuthScope[]> {
56
  if (request.method !== 'POST') return ['mcp:access'];
57
  let body: unknown;
58
  try {
59
    body = await request.clone().json();
60
  } catch {
61
    return ['mcp:access'];
62
  }
63
  const messages = Array.isArray(body) ? body : [body];
64
  const scopes = new Set<OpenClaspAuthScope>(['mcp:access']);
65
  for (const message of messages) {
66
    if (!message || typeof message !== 'object') continue;
67
    const rpc = message as { method?: unknown; params?: { name?: unknown } };
68
    if (rpc.method === 'tools/call' && typeof rpc.params?.name === 'string')
69
      scopes.add(requiredMcpToolScope(rpc.params.name));
70
  }
71
  return [...scopes];
72
}
73
74
export function requiredAgentApiScopes(
75
  method: string,
76
  url: string,
77
): OpenClaspAuthScope[] | undefined {
78
  const path = new URL(url, 'https://openclasp.local').pathname;
79
  if (/^\/v0\.1\/runtime(?:\/bootstrap|\/heartbeat)?$/.test(path))
80
    return method === 'GET' ? ['profile:read'] : ['runtime:connect'];
81
  if (path === '/v0.1/runtime/profile') return ['agent:manage'];
82
  if (path === '/v0.1/feedback-requests') return ['profile:read'];
83
  if (/^\/v0\.1\/federated-interactions\/[^/]+\/(?:brief|session)$/.test(path))
84
    return ['profile:read'];
85
  if (/^\/v0\.1\/federated-interactions\/[^/]+\/(?:completion-reports|feedback)$/.test(path))
86
    return ['feedback:write'];
87
  if (
88
    /^\/v0\.1\/federated-interactions\/[^/]+\/contract-proposals(?:\/[^/]+\/respond)?$/.test(path)
89
  )
90
    return ['interaction:write'];
91
  return undefined;
92
}
93
94
export function assertScopes(granted: readonly string[], required: readonly string[]): void {
95
  const missing = required.filter((scope) => !granted.includes(scope));
96
  if (missing.length) throw new ScopeError(missing);
97
}
98
99
export class ScopeError extends Error {
100
  readonly statusCode = 403;
101
102
  constructor(readonly missingScopes: string[]) {
103
    super(`Credential is missing required scope: ${missingScopes.join(', ')}`);
104
  }
105
}