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

Viewing files at commit 8135358a54bcCommit detailsView latest

apps/api/src/access-control.ts 119 lines · 4.0 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
  'openclasp_list_assurance_probes',
30
  'openclasp_get_assurance_comparisons',
31
  'openclasp_get_assurance_brief',
32
]);
33
34
const AGENT_TOOLS = new Set([
35
  'openclasp_register_agent',
36
  'openclasp_setup',
37
  'openclasp_switch_agent',
38
  'openclasp_update_profile',
39
  'openclasp_register_delegation',
40
  'openclasp_heartbeat',
41
]);
42
43
const FEEDBACK_TOOLS = new Set([
44
  'openclasp_complete_interaction',
45
  'openclasp_submit_feedback',
46
  'openclasp_complete_live_session',
47
  'openclasp_submit_completion_report',
48
  'openclasp_submit_interaction_feedback',
49
]);
50
51
export function requiredMcpToolScope(name: string): OpenClaspAuthScope {
52
  if (PROFILE_TOOLS.has(name)) return 'profile:read';
53
  if (AGENT_TOOLS.has(name)) return 'agent:manage';
54
  if (FEEDBACK_TOOLS.has(name)) return 'feedback:write';
55
  return 'interaction:write';
56
}
57
58
export async function requiredMcpRequestScopes(request: Request): Promise<OpenClaspAuthScope[]> {
59
  if (request.method !== 'POST') return ['mcp:access'];
60
  let body: unknown;
61
  try {
62
    body = await request.clone().json();
63
  } catch {
64
    return ['mcp:access'];
65
  }
66
  const messages = Array.isArray(body) ? body : [body];
67
  const scopes = new Set<OpenClaspAuthScope>(['mcp:access']);
68
  for (const message of messages) {
69
    if (!message || typeof message !== 'object') continue;
70
    const rpc = message as { method?: unknown; params?: { name?: unknown } };
71
    if (rpc.method === 'tools/call' && typeof rpc.params?.name === 'string')
72
      scopes.add(requiredMcpToolScope(rpc.params.name));
73
  }
74
  return [...scopes];
75
}
76
77
export function requiredAgentApiScopes(
78
  method: string,
79
  url: string,
80
): OpenClaspAuthScope[] | undefined {
81
  const path = new URL(url, 'https://openclasp.local').pathname;
82
  if (/^\/v0\.1\/runtime(?:\/bootstrap|\/heartbeat)?$/.test(path))
83
    return method === 'GET' ? ['profile:read'] : ['runtime:connect'];
84
  if (path === '/v0.1/runtime/profile') return ['agent:manage'];
85
  if (path === '/v0.1/feedback-requests') return ['profile:read'];
86
  if (/^\/v0\.1\/federated-interactions\/[^/]+\/(?:brief|session)$/.test(path))
87
    return ['profile:read'];
88
  if (
89
    /^\/v0\.1\/federated-interactions\/[^/]+\/(?:assurance-probes|assurance-comparisons|assurance-brief)$/.test(
90
      path,
91
    ) &&
92
    method === 'GET'
93
  )
94
    return ['profile:read'];
95
  if (/^\/v0\.1\/federated-interactions\/[^/]+\/assurance-responses$/.test(path))
96
    return ['interaction:write'];
97
  if (/^\/v0\.1\/federated-interactions\/[^/]+\/assurance-safeguards\/[^/]+\/decision$/.test(path))
98
    return ['interaction:write'];
99
  if (/^\/v0\.1\/federated-interactions\/[^/]+\/(?:completion-reports|feedback)$/.test(path))
100
    return ['feedback:write'];
101
  if (
102
    /^\/v0\.1\/federated-interactions\/[^/]+\/contract-proposals(?:\/[^/]+\/respond)?$/.test(path)
103
  )
104
    return ['interaction:write'];
105
  return undefined;
106
}
107
108
export function assertScopes(granted: readonly string[], required: readonly string[]): void {
109
  const missing = required.filter((scope) => !granted.includes(scope));
110
  if (missing.length) throw new ScopeError(missing);
111
}
112
113
export class ScopeError extends Error {
114
  readonly statusCode = 403;
115
116
  constructor(readonly missingScopes: string[]) {
117
    super(`Credential is missing required scope: ${missingScopes.join(', ')}`);
118
  }
119
}