@indexnetwork/protocol 6.15.0-rc.407.1 → 8.0.1-rc.417.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +302 -0
  2. package/IMPLEMENTATION.md +7 -4
  3. package/dist/capabilities/opportunities.facade.d.ts +4 -0
  4. package/dist/capabilities/opportunities.facade.js +4 -0
  5. package/dist/chat/chat-streaming.types.d.ts +4 -5
  6. package/dist/chat/chat.agent.d.ts +0 -19
  7. package/dist/chat/chat.agent.js +1 -70
  8. package/dist/chat/chat.persona.d.ts +0 -6
  9. package/dist/chat/chat.persona.js +0 -1
  10. package/dist/chat/chat.prompt.js +10 -10
  11. package/dist/chat/chat.prompt.modules.js +0 -104
  12. package/dist/chat/chat.tools.js +11 -7
  13. package/dist/chat/negotiator.persona.js +0 -1
  14. package/dist/chat/onboarding.persona.js +0 -1
  15. package/dist/chat/reporter.persona.d.ts +1 -1
  16. package/dist/chat/reporter.persona.js +1 -2
  17. package/dist/chat/reporter.prompt.js +5 -5
  18. package/dist/chat/signal.persona.js +0 -1
  19. package/dist/communities/application/network.tools.js +48 -27
  20. package/dist/contacts/application/contact.tools.js +9 -9
  21. package/dist/index.d.ts +9 -2
  22. package/dist/index.js +5 -0
  23. package/dist/maintenance/maintenance.graph.js +7 -0
  24. package/dist/mcp/mcp.authorization-policy.d.ts +250 -0
  25. package/dist/mcp/mcp.authorization-policy.js +522 -0
  26. package/dist/mcp/mcp.server.d.ts +6 -29
  27. package/dist/mcp/mcp.server.js +202 -228
  28. package/dist/negotiation/application/negotiation.agent.js +29 -5
  29. package/dist/negotiation/application/negotiation.graph.d.ts +2 -5
  30. package/dist/negotiation/application/negotiation.graph.js +53 -23
  31. package/dist/negotiation/domain/index.d.ts +3 -0
  32. package/dist/negotiation/domain/index.js +3 -0
  33. package/dist/negotiation/domain/negotiation.deadlock.d.ts +7 -1
  34. package/dist/negotiation/domain/negotiation.deadlock.js +48 -1
  35. package/dist/negotiation/domain/negotiation.stance.contracts.d.ts +85 -0
  36. package/dist/negotiation/domain/negotiation.stance.contracts.js +140 -0
  37. package/dist/negotiation/domain/negotiation.state.d.ts +2 -3
  38. package/dist/negotiation/domain/negotiation.state.js +2 -3
  39. package/dist/negotiation/public/index.d.ts +2 -0
  40. package/dist/negotiation/public/index.js +2 -0
  41. package/dist/opportunity/application/index.d.ts +0 -3
  42. package/dist/opportunity/application/index.js +0 -3
  43. package/dist/opportunity/application/opportunity.graph.d.ts +0 -43
  44. package/dist/opportunity/application/opportunity.graph.js +7 -113
  45. package/dist/opportunity/application/opportunity.introducer-feature.d.ts +2 -0
  46. package/dist/opportunity/application/opportunity.introducer-feature.js +4 -0
  47. package/dist/opportunity/application/opportunity.owner-approval.d.ts +118 -0
  48. package/dist/opportunity/application/opportunity.owner-approval.js +34 -0
  49. package/dist/opportunity/application/opportunity.owner-provenance.d.ts +6 -0
  50. package/dist/opportunity/application/opportunity.owner-provenance.js +28 -0
  51. package/dist/opportunity/application/opportunity.tools.d.ts +3 -3
  52. package/dist/opportunity/application/opportunity.tools.js +77 -870
  53. package/dist/opportunity/discriminator/discriminator.shadow.js +17 -10
  54. package/dist/opportunity/discriminator/discriminator.types.d.ts +2 -0
  55. package/dist/opportunity/domain/index.d.ts +0 -2
  56. package/dist/opportunity/domain/index.js +0 -2
  57. package/dist/opportunity/domain/opportunity.state.d.ts +2 -70
  58. package/dist/opportunity/domain/opportunity.state.js +2 -55
  59. package/dist/opportunity/opportunity.owner-approval.d.ts +5 -0
  60. package/dist/opportunity/opportunity.owner-approval.js +5 -0
  61. package/dist/opportunity/ports/opportunity.tools.port.d.ts +12 -1
  62. package/dist/opportunity/public/index.d.ts +3 -3
  63. package/dist/opportunity/public/index.js +2 -1
  64. package/dist/participant-agents/application/agent.tools.d.ts +1 -1
  65. package/dist/participant-agents/application/agent.tools.js +44 -6
  66. package/dist/participant-agents/application/index.d.ts +5 -3
  67. package/dist/participant-agents/application/index.js +5 -3
  68. package/dist/questions/application/question.tools.js +31 -2
  69. package/dist/runtime/foreground/composition/tool.factory.d.ts +2 -1
  70. package/dist/runtime/foreground/composition/tool.factory.js +9 -2
  71. package/dist/runtime/foreground/composition/tool.registry.d.ts +16 -2
  72. package/dist/runtime/foreground/composition/tool.registry.js +41 -38
  73. package/dist/shared/agent/activity-projection.d.ts +271 -0
  74. package/dist/shared/agent/activity-projection.js +248 -0
  75. package/dist/shared/agent/canonical-guidance.d.ts +30 -0
  76. package/dist/shared/agent/canonical-guidance.js +249 -0
  77. package/dist/shared/agent/tool.helpers.d.ts +9 -17
  78. package/dist/shared/agent/tool.runtime.js +1 -3
  79. package/dist/shared/agent/utility.tools.d.ts +11 -1
  80. package/dist/shared/agent/utility.tools.js +126 -90
  81. package/dist/shared/interfaces/auth.interface.d.ts +2 -8
  82. package/dist/shared/interfaces/chat-session.interface.js +5 -0
  83. package/dist/shared/interfaces/database.interface.d.ts +12 -5
  84. package/dist/shared/observability/request-context.d.ts +4 -40
  85. package/dist/shared/schemas/mcp-auth.schema.d.ts +87 -15
  86. package/dist/shared/schemas/mcp-auth.schema.js +49 -1
  87. package/dist/signals/application/intent.tools.js +7 -8
  88. package/package.json +10 -9
  89. package/dist/opportunity/application/opportunity.discover.d.ts +0 -215
  90. package/dist/opportunity/application/opportunity.discover.js +0 -793
  91. package/dist/opportunity/application/opportunity.discovery-continuation-finalization.d.ts +0 -34
  92. package/dist/opportunity/application/opportunity.discovery-continuation-finalization.js +0 -74
  93. package/dist/opportunity/application/opportunity.discovery-mcp-lifecycle-finalization.d.ts +0 -44
  94. package/dist/opportunity/application/opportunity.discovery-mcp-lifecycle-finalization.js +0 -92
  95. package/dist/opportunity/domain/opportunity.discovery-run-coalescing.d.ts +0 -33
  96. package/dist/opportunity/domain/opportunity.discovery-run-coalescing.js +0 -33
  97. package/dist/opportunity/domain/opportunity.discovery.contracts.d.ts +0 -45
  98. package/dist/opportunity/domain/opportunity.discovery.contracts.js +0 -1
  99. package/dist/opportunity/opportunity.discover.d.ts +0 -5
  100. package/dist/opportunity/opportunity.discover.js +0 -5
  101. package/dist/opportunity/opportunity.discovery-continuation-finalization.d.ts +0 -5
  102. package/dist/opportunity/opportunity.discovery-continuation-finalization.js +0 -5
  103. package/dist/opportunity/opportunity.discovery-mcp-lifecycle-finalization.d.ts +0 -5
  104. package/dist/opportunity/opportunity.discovery-mcp-lifecycle-finalization.js +0 -5
  105. package/dist/opportunity/opportunity.discovery-run-coalescing.d.ts +0 -5
  106. package/dist/opportunity/opportunity.discovery-run-coalescing.js +0 -5
  107. package/dist/opportunity/opportunity.discovery.contracts.d.ts +0 -5
  108. package/dist/opportunity/opportunity.discovery.contracts.js +0 -5
  109. package/dist/shared/interfaces/discovery-run.interface.d.ts +0 -70
  110. package/dist/shared/interfaces/discovery-run.interface.js +0 -1
@@ -0,0 +1,522 @@
1
+ import { z } from 'zod';
2
+ import { McpActivityCallerSchema } from '../shared/agent/activity-projection.js';
3
+ /** Canonical MCP permission actions. */
4
+ export const MCP_PERMISSION_ACTIONS = [
5
+ 'manage:identity',
6
+ 'manage:premises',
7
+ 'manage:intents',
8
+ 'manage:networks',
9
+ 'manage:opportunities',
10
+ 'manage:negotiations',
11
+ ];
12
+ export const McpPermissionActionSchema = z.enum(MCP_PERMISSION_ACTIONS);
13
+ export const McpToolPermissionRequirementSchema = z.object({
14
+ action: McpPermissionActionSchema,
15
+ /**
16
+ * Capability reach only. Entity ownership, membership, approval, and exact
17
+ * scope checks remain in the capability handler.
18
+ */
19
+ reach: z.enum(['principal', 'network']).default('principal'),
20
+ }).strict();
21
+ const McpToolPermissionMapSchema = z.record(z.string().min(1), McpToolPermissionRequirementSchema);
22
+ /**
23
+ * Runtime-validates a capability-owned tool-to-permission mapping.
24
+ */
25
+ export function defineMcpToolPermissionMap(mapping) {
26
+ const parsed = McpToolPermissionMapSchema.parse(mapping);
27
+ return new Map(Object.entries(parsed));
28
+ }
29
+ export const McpToolAccessRuleSchema = z.object({
30
+ access: z.enum([
31
+ 'permission',
32
+ 'authenticated',
33
+ 'human_only',
34
+ 'agent_admin',
35
+ 'informational',
36
+ 'delivery_only',
37
+ 'removed',
38
+ ]),
39
+ actions: z.array(McpPermissionActionSchema).optional(),
40
+ reach: z.enum(['principal', 'network']),
41
+ }).strict().superRefine((rule, ctx) => {
42
+ if (rule.access === 'permission' && (!rule.actions || rule.actions.length === 0)) {
43
+ ctx.addIssue({
44
+ code: z.ZodIssueCode.custom,
45
+ path: ['actions'],
46
+ message: 'Permission rules require at least one action.',
47
+ });
48
+ }
49
+ if (rule.access !== 'permission' && rule.actions !== undefined) {
50
+ ctx.addIssue({
51
+ code: z.ZodIssueCode.custom,
52
+ path: ['actions'],
53
+ message: 'Only permission rules may declare actions.',
54
+ });
55
+ }
56
+ });
57
+ const McpToolAccessRuleMapSchema = z.record(z.string().min(1), McpToolAccessRuleSchema);
58
+ /** Runtime-validates a complete static MCP tool access matrix. */
59
+ export function defineMcpToolAccessRules(rules) {
60
+ const parsed = McpToolAccessRuleMapSchema.parse(rules);
61
+ return new Map(Object.entries(parsed));
62
+ }
63
+ /**
64
+ * Canonical MCP tool authorization matrix.
65
+ *
66
+ * Every tool registered by `createToolRegistry` is classified. Tool handlers
67
+ * retain domain ownership, membership, exact-scope, and approval checks.
68
+ */
69
+ export const CANONICAL_MCP_TOOL_ACCESS_RULES = defineMcpToolAccessRules({
70
+ // Participant identity/context.
71
+ read_user_contexts: { access: 'permission', actions: ['manage:identity'], reach: 'principal' },
72
+ preview_user_context: { access: 'permission', actions: ['manage:identity'], reach: 'principal' },
73
+ confirm_user_context: { access: 'permission', actions: ['manage:identity'], reach: 'principal' },
74
+ create_user_context: { access: 'permission', actions: ['manage:identity'], reach: 'principal' },
75
+ update_user_context: { access: 'permission', actions: ['manage:identity'], reach: 'principal' },
76
+ get_enrichment_run: { access: 'permission', actions: ['manage:identity'], reach: 'principal' },
77
+ cancel_enrichment_run: { access: 'permission', actions: ['manage:identity'], reach: 'principal' },
78
+ record_onboarding_privacy_consent: { access: 'human_only', reach: 'principal' },
79
+ complete_onboarding: { access: 'human_only', reach: 'principal' },
80
+ // Premises are meta-network; a network-scoped agent retains principal reach.
81
+ read_premises: { access: 'permission', actions: ['manage:premises'], reach: 'principal' },
82
+ create_premise: { access: 'permission', actions: ['manage:premises'], reach: 'principal' },
83
+ update_premise: { access: 'permission', actions: ['manage:premises'], reach: 'principal' },
84
+ retract_premise: { access: 'permission', actions: ['manage:premises'], reach: 'principal' },
85
+ // Signals.
86
+ read_intents: { access: 'authenticated', reach: 'network' },
87
+ search_intents: { access: 'authenticated', reach: 'network' },
88
+ create_intent: { access: 'permission', actions: ['manage:intents'], reach: 'network' },
89
+ update_intent: { access: 'permission', actions: ['manage:intents'], reach: 'network' },
90
+ delete_intent: { access: 'permission', actions: ['manage:intents'], reach: 'network' },
91
+ read_intent_indexes: { access: 'authenticated', reach: 'network' },
92
+ create_intent_index: { access: 'permission', actions: ['manage:intents'], reach: 'network' },
93
+ delete_intent_index: { access: 'permission', actions: ['manage:intents'], reach: 'network' },
94
+ // Question tools inherit the exact affected object's permission in-handler.
95
+ read_pending_questions: {
96
+ access: 'permission',
97
+ actions: [
98
+ 'manage:identity',
99
+ 'manage:premises',
100
+ 'manage:intents',
101
+ 'manage:opportunities',
102
+ 'manage:negotiations',
103
+ ],
104
+ reach: 'principal',
105
+ },
106
+ answer_pending_question: {
107
+ access: 'permission',
108
+ actions: [
109
+ 'manage:identity',
110
+ 'manage:premises',
111
+ 'manage:intents',
112
+ 'manage:opportunities',
113
+ 'manage:negotiations',
114
+ ],
115
+ reach: 'principal',
116
+ },
117
+ // Communities.
118
+ read_networks: { access: 'authenticated', reach: 'network' },
119
+ read_network_memberships: { access: 'authenticated', reach: 'network' },
120
+ create_network: { access: 'permission', actions: ['manage:networks'], reach: 'network' },
121
+ update_network: { access: 'permission', actions: ['manage:networks'], reach: 'network' },
122
+ delete_network: { access: 'human_only', reach: 'network' },
123
+ create_network_membership: { access: 'permission', actions: ['manage:networks'], reach: 'network' },
124
+ delete_network_membership: { access: 'permission', actions: ['manage:networks'], reach: 'network' },
125
+ // Opportunities and delivery.
126
+ list_opportunities: { access: 'authenticated', reach: 'network' },
127
+ update_opportunity: { access: 'permission', actions: ['manage:opportunities'], reach: 'network' },
128
+ confirm_opportunity_delivery: { access: 'delivery_only', reach: 'network' },
129
+ // A2A negotiations.
130
+ list_negotiations: { access: 'permission', actions: ['manage:negotiations'], reach: 'network' },
131
+ get_negotiation: { access: 'permission', actions: ['manage:negotiations'], reach: 'network' },
132
+ respond_to_negotiation: { access: 'permission', actions: ['manage:negotiations'], reach: 'network' },
133
+ // H2A chat history.
134
+ list_conversations: { access: 'human_only', reach: 'principal' },
135
+ get_conversation: { access: 'human_only', reach: 'principal' },
136
+ // Agent administration.
137
+ read_own_agent: { access: 'agent_admin', reach: 'principal' },
138
+ register_agent: { access: 'agent_admin', reach: 'principal' },
139
+ list_agents: { access: 'agent_admin', reach: 'principal' },
140
+ update_agent: { access: 'agent_admin', reach: 'principal' },
141
+ delete_agent: { access: 'agent_admin', reach: 'principal' },
142
+ grant_agent_permission: { access: 'agent_admin', reach: 'principal' },
143
+ revoke_agent_permission: { access: 'agent_admin', reach: 'principal' },
144
+ // Protocol guidance.
145
+ read_docs: { access: 'informational', reach: 'principal' },
146
+ // Aggregate activity reporting. Any activity-domain permission admits the
147
+ // call; the handler then projects each domain by the caller's exact grants
148
+ // via the centralized activity projection, and a network agent's
149
+ // network-bound aggregates are narrowed to its bound community at the
150
+ // query/adapter layer.
151
+ read_activity_summary: {
152
+ access: 'permission',
153
+ actions: [
154
+ 'manage:identity',
155
+ 'manage:premises',
156
+ 'manage:intents',
157
+ 'manage:opportunities',
158
+ 'manage:negotiations',
159
+ ],
160
+ reach: 'principal',
161
+ },
162
+ // Restricted MCP surface still registered by the shared registry. The contact
163
+ // and Gmail-import tools, scrape_url, the deprecated profile/profile-run
164
+ // aliases, and the retired report_agent_activity name are no longer
165
+ // classified here because they are omitted from the MCP registry composition
166
+ // entirely (IND-596/597/598/605) — an unregistered tool is rejected as
167
+ // unknown before any authorization work. read_activity_summary (above) is
168
+ // the only public activity-reporting name; no legacy alias is retained.
169
+ });
170
+ /** Tools visible while a session-authenticated human completes onboarding. */
171
+ export const ONBOARDING_ALLOWED = new Set([
172
+ 'register_agent',
173
+ 'read_docs',
174
+ 'record_onboarding_privacy_consent',
175
+ 'preview_user_context',
176
+ 'get_enrichment_run',
177
+ 'cancel_enrichment_run',
178
+ 'confirm_user_context',
179
+ 'create_user_context',
180
+ 'read_user_contexts',
181
+ 'complete_onboarding',
182
+ 'read_networks',
183
+ 'create_network_membership',
184
+ 'create_intent',
185
+ ]);
186
+ /** Agent administration inventory. */
187
+ export const MCP_AGENT_ADMIN_TOOLS = new Set([
188
+ 'read_own_agent',
189
+ 'register_agent',
190
+ 'list_agents',
191
+ 'update_agent',
192
+ 'delete_agent',
193
+ 'grant_agent_permission',
194
+ 'revoke_agent_permission',
195
+ ]);
196
+ /** Informational inventory retained as a convenience export. */
197
+ export const MCP_INFORMATIONAL_TOOLS = new Set([
198
+ 'read_docs',
199
+ ]);
200
+ const McpPolicyAgentPermissionSchema = z.object({
201
+ agentId: z.string().min(1),
202
+ userId: z.string().min(1),
203
+ scope: z.enum(['global', 'node', 'network']),
204
+ scopeId: z.string().min(1).nullable(),
205
+ actions: z.array(z.string()),
206
+ }).strict();
207
+ export const McpPolicyAgentSnapshotSchema = z.object({
208
+ id: z.string().min(1),
209
+ ownerId: z.string().min(1),
210
+ type: z.enum(['personal', 'external', 'system']),
211
+ status: z.enum(['active', 'inactive']),
212
+ permissions: z.array(McpPolicyAgentPermissionSchema),
213
+ }).strict();
214
+ export const McpPrincipalProfileSchema = z.enum([
215
+ 'session_human',
216
+ 'onboarding_human',
217
+ 'enrollment_key',
218
+ 'unregistered_key',
219
+ 'registered_global_agent',
220
+ 'registered_network_agent',
221
+ 'delivery_agent',
222
+ 'invalid_agent',
223
+ ]);
224
+ export const McpCapabilitySubjectSchema = z.object({
225
+ profile: McpPrincipalProfileSchema,
226
+ userId: z.string().min(1),
227
+ agentId: z.string().min(1).optional(),
228
+ agentType: z.enum(['personal', 'external', 'system']).optional(),
229
+ isOnboarding: z.boolean(),
230
+ networkScopeId: z.string().min(1).nullable(),
231
+ permissions: z.array(McpPermissionActionSchema),
232
+ }).strict();
233
+ /**
234
+ * Retired durable grant actions and the canonical capabilities each projects to
235
+ * at the permission-loading boundary. TEMPORARY rolling-data compatibility
236
+ * (IND-607): while a mixed-version fleet is live, old replicas (current `dev`)
237
+ * still WRITE `manage:profile` / `manage:contacts` rows AFTER the pre-deploy
238
+ * `0109` migration runs, and un-migrated rows may also remain. The new runtime
239
+ * must therefore interpret these residual STORED rows so no agent loses access
240
+ * during the rolling window and none is over-authorized:
241
+ *
242
+ * - `manage:profile` -> `manage:identity` + `manage:premises`
243
+ * - `manage:contacts` -> (no capability)
244
+ *
245
+ * This is NOT a public alias: the legacy names are never accepted as INPUT
246
+ * (grant validation and tool schemas are canonical-only) and never surfaced in
247
+ * documentation or `tools/list`. They are tolerated ONLY as pre-existing stored
248
+ * data, and only until the post-drain final sweep sets `retired_remaining = 0`
249
+ * and the compatibility-removal gate is met (see the IND-609 rollout doc).
250
+ */
251
+ const LEGACY_STORED_ACTION_PROJECTION = {
252
+ 'manage:profile': ['manage:identity', 'manage:premises'],
253
+ 'manage:contacts': [],
254
+ };
255
+ /**
256
+ * Central capability-loading interpretation of a stored permission row's raw
257
+ * actions. Canonical actions pass through; retired actions project per
258
+ * {@link LEGACY_STORED_ACTION_PROJECTION}; every unknown action fails closed
259
+ * (ignored). Owner/scope matching is applied by the caller BEFORE this runs, so
260
+ * this function never widens the scope a grant applies to.
261
+ */
262
+ export function projectStoredPermissionActions(actions) {
263
+ const granted = new Set();
264
+ for (const action of actions) {
265
+ const projected = LEGACY_STORED_ACTION_PROJECTION[action];
266
+ if (projected !== undefined) {
267
+ for (const capability of projected)
268
+ granted.add(capability);
269
+ continue;
270
+ }
271
+ const parsed = McpPermissionActionSchema.safeParse(action);
272
+ if (parsed.success)
273
+ granted.add(parsed.data);
274
+ // Unknown actions are ignored — fail closed.
275
+ }
276
+ return [...granted];
277
+ }
278
+ function resolveAgentPermissions(identity, agent) {
279
+ const granted = new Set();
280
+ for (const permission of agent.permissions) {
281
+ if (permission.agentId !== identity.agentId || permission.userId !== identity.userId)
282
+ continue;
283
+ const scopeApplies = permission.scope === 'global' ||
284
+ (permission.scope === 'network' &&
285
+ identity.networkScopeId !== null &&
286
+ identity.networkScopeId !== undefined &&
287
+ permission.scopeId === identity.networkScopeId);
288
+ if (!scopeApplies)
289
+ continue;
290
+ // Canonical-only going forward, plus temporary interpretation of residual
291
+ // legacy stored rows during the mixed-version rolling window.
292
+ for (const action of projectStoredPermissionActions(permission.actions)) {
293
+ granted.add(action);
294
+ }
295
+ }
296
+ return [...granted];
297
+ }
298
+ /**
299
+ * Resolves a request-local, runtime-validated principal profile.
300
+ */
301
+ export function resolveMcpCapabilitySubject(input) {
302
+ const { identity, isOnboarding } = input;
303
+ if (identity.isSessionAuth === true) {
304
+ return McpCapabilitySubjectSchema.parse({
305
+ profile: isOnboarding ? 'onboarding_human' : 'session_human',
306
+ userId: identity.userId,
307
+ isOnboarding,
308
+ networkScopeId: null,
309
+ permissions: [],
310
+ });
311
+ }
312
+ if (!identity.agentId) {
313
+ return McpCapabilitySubjectSchema.parse({
314
+ profile: identity.enrollmentCapable === true ? 'enrollment_key' : 'unregistered_key',
315
+ userId: identity.userId,
316
+ isOnboarding,
317
+ networkScopeId: null,
318
+ permissions: [],
319
+ });
320
+ }
321
+ const parsedAgent = McpPolicyAgentSnapshotSchema.safeParse(input.agent);
322
+ if (!parsedAgent.success ||
323
+ parsedAgent.data.id !== identity.agentId ||
324
+ parsedAgent.data.ownerId !== identity.userId ||
325
+ parsedAgent.data.status !== 'active') {
326
+ return McpCapabilitySubjectSchema.parse({
327
+ profile: 'invalid_agent',
328
+ userId: identity.userId,
329
+ agentId: identity.agentId,
330
+ isOnboarding,
331
+ networkScopeId: identity.networkScopeId ?? null,
332
+ permissions: [],
333
+ });
334
+ }
335
+ const agent = parsedAgent.data;
336
+ const profile = identity.isDeliveryAgent === true
337
+ ? 'delivery_agent'
338
+ : identity.networkScopeId
339
+ ? 'registered_network_agent'
340
+ : 'registered_global_agent';
341
+ return McpCapabilitySubjectSchema.parse({
342
+ profile,
343
+ userId: identity.userId,
344
+ agentId: identity.agentId,
345
+ agentType: agent.type,
346
+ isOnboarding,
347
+ networkScopeId: identity.networkScopeId ?? null,
348
+ permissions: resolveAgentPermissions(identity, agent),
349
+ });
350
+ }
351
+ /**
352
+ * Maps a resolved capability subject to the typed caller context consumed by
353
+ * the centralized activity-summary projection. Session and onboarding humans
354
+ * own the summarized data and receive the full owner view; every other
355
+ * profile is projected as an agent bounded by its granted permissions and
356
+ * (for network agents) its bound community.
357
+ */
358
+ export function resolveMcpActivityCaller(subject) {
359
+ const isHuman = subject.profile === 'session_human' || subject.profile === 'onboarding_human';
360
+ return McpActivityCallerSchema.parse({
361
+ kind: isHuman ? 'human' : 'agent',
362
+ permissions: isHuman ? [] : subject.permissions,
363
+ networkScopeId: isHuman ? null : subject.networkScopeId,
364
+ });
365
+ }
366
+ export const McpCapabilityDecisionReasonSchema = z.enum([
367
+ 'session_human',
368
+ 'onboarding',
369
+ 'enrollment',
370
+ 'authenticated',
371
+ 'agent_self_read',
372
+ 'informational',
373
+ 'permission_granted',
374
+ 'delivery',
375
+ 'onboarding_required',
376
+ 'enrollment_required',
377
+ 'unregistered_principal',
378
+ 'invalid_agent',
379
+ 'agent_admin_denied',
380
+ 'human_read_own_agent_denied',
381
+ 'human_only',
382
+ 'permission_missing',
383
+ 'delivery_required',
384
+ 'removed',
385
+ 'tool_unclassified',
386
+ ]);
387
+ /**
388
+ * Builds a safe denial event from a resolved subject and a denial decision.
389
+ * Only whitelisted, non-sensitive fields are copied across; the caller's
390
+ * granted permissions, credentials, headers, and tool arguments are never
391
+ * included.
392
+ */
393
+ export function buildMcpAuthorizationDenialEvent(input) {
394
+ const { phase, toolName, subject, decision } = input;
395
+ return {
396
+ phase,
397
+ toolName,
398
+ profile: subject.profile,
399
+ reason: decision.reason,
400
+ ...(decision.reach ? { reach: decision.reach } : {}),
401
+ ...(decision.requiredPermissions && decision.requiredPermissions.length > 0
402
+ ? { requiredPermissions: [...decision.requiredPermissions] }
403
+ : {}),
404
+ userId: subject.userId,
405
+ ...(subject.agentId ? { agentId: subject.agentId } : {}),
406
+ networkScopeId: subject.networkScopeId,
407
+ };
408
+ }
409
+ /**
410
+ * Reusable MCP capability policy. It stores static rules only; caller-specific
411
+ * subjects and decisions are never retained.
412
+ */
413
+ export class McpCapabilityPolicy {
414
+ constructor(options = {}) {
415
+ const parsedRules = McpToolAccessRuleMapSchema.parse(Object.fromEntries(options.toolRules ?? CANONICAL_MCP_TOOL_ACCESS_RULES));
416
+ this.toolRules = new Map(Object.entries(parsedRules));
417
+ }
418
+ /**
419
+ * Decides whether a resolved caller may use one classified tool.
420
+ */
421
+ authorize(subject, toolName) {
422
+ const rule = this.toolRules.get(toolName);
423
+ if (!rule)
424
+ return { allowed: false, reason: 'tool_unclassified' };
425
+ if (rule.access === 'removed') {
426
+ return { allowed: false, reason: 'removed', reach: rule.reach };
427
+ }
428
+ if (subject.profile === 'invalid_agent') {
429
+ return { allowed: false, reason: 'invalid_agent', reach: rule.reach };
430
+ }
431
+ if (subject.profile === 'unregistered_key') {
432
+ return { allowed: false, reason: 'unregistered_principal', reach: rule.reach };
433
+ }
434
+ if (subject.profile === 'enrollment_key') {
435
+ return toolName === 'register_agent'
436
+ ? { allowed: true, reason: 'enrollment', reach: rule.reach }
437
+ : { allowed: false, reason: 'enrollment_required', reach: rule.reach };
438
+ }
439
+ if (subject.isOnboarding && !ONBOARDING_ALLOWED.has(toolName)) {
440
+ return { allowed: false, reason: 'onboarding_required', reach: rule.reach };
441
+ }
442
+ if (rule.access === 'delivery_only') {
443
+ return subject.profile === 'delivery_agent'
444
+ ? { allowed: true, reason: 'delivery', reach: rule.reach }
445
+ : { allowed: false, reason: 'delivery_required', reach: rule.reach };
446
+ }
447
+ // Agent administration is split by principal kind and must be decided
448
+ // BEFORE the generic session-human blanket allow below — otherwise a
449
+ // session human would be admitted to read_own_agent (an agent-only tool)
450
+ // by the blanket allow before this rule is ever reached (IND-599).
451
+ if (rule.access === 'agent_admin') {
452
+ // read_own_agent is the ONLY agent_admin tool available to a registered
453
+ // active agent, and it returns that agent's own record (no target). Every
454
+ // other agent_admin tool is an owner/admin action reserved for humans.
455
+ if (subject.profile === 'registered_global_agent' ||
456
+ subject.profile === 'registered_network_agent' ||
457
+ subject.profile === 'delivery_agent') {
458
+ return toolName === 'read_own_agent'
459
+ ? { allowed: true, reason: 'agent_self_read', reach: rule.reach }
460
+ : { allowed: false, reason: 'agent_admin_denied', reach: rule.reach };
461
+ }
462
+ // Session/onboarding humans get every agent_admin tool EXCEPT
463
+ // read_own_agent, which is reserved for agent principals.
464
+ if (subject.profile === 'session_human' || subject.profile === 'onboarding_human') {
465
+ return toolName === 'read_own_agent'
466
+ ? { allowed: false, reason: 'human_read_own_agent_denied', reach: rule.reach }
467
+ : {
468
+ allowed: true,
469
+ reason: subject.profile === 'onboarding_human' ? 'onboarding' : 'session_human',
470
+ reach: rule.reach,
471
+ };
472
+ }
473
+ // Every other profile (enrollment/unregistered/invalid) is handled above
474
+ // and never reaches here; fail closed for completeness.
475
+ return { allowed: false, reason: 'agent_admin_denied', reach: rule.reach };
476
+ }
477
+ if (subject.profile === 'session_human' || subject.profile === 'onboarding_human') {
478
+ return {
479
+ allowed: true,
480
+ reason: subject.profile === 'onboarding_human' ? 'onboarding' : 'session_human',
481
+ reach: rule.reach,
482
+ };
483
+ }
484
+ if (rule.access === 'human_only') {
485
+ return { allowed: false, reason: 'human_only', reach: rule.reach };
486
+ }
487
+ if (rule.access === 'informational') {
488
+ return { allowed: true, reason: 'informational', reach: rule.reach };
489
+ }
490
+ if (rule.access === 'authenticated') {
491
+ return { allowed: true, reason: 'authenticated', reach: rule.reach };
492
+ }
493
+ const requiredPermissions = rule.actions ?? [];
494
+ return requiredPermissions.some((action) => subject.permissions.includes(action))
495
+ ? {
496
+ allowed: true,
497
+ reason: 'permission_granted',
498
+ reach: rule.reach,
499
+ requiredPermissions,
500
+ }
501
+ : {
502
+ allowed: false,
503
+ reason: 'permission_missing',
504
+ reach: rule.reach,
505
+ requiredPermissions,
506
+ };
507
+ }
508
+ /**
509
+ * Filters a static inventory and returns a fresh, uncached caller list.
510
+ */
511
+ visibleToolNames(subject, toolNames) {
512
+ return toolNames.filter((toolName) => this.authorize(subject, toolName).allowed);
513
+ }
514
+ /** Returns the static classification for inventory tests and host composition. */
515
+ ruleFor(toolName) {
516
+ return this.toolRules.get(toolName);
517
+ }
518
+ }
519
+ /** Canonical production options explicitly passed by the host composition. */
520
+ export const CANONICAL_MCP_CAPABILITY_POLICY_OPTIONS = {
521
+ toolRules: CANONICAL_MCP_TOOL_ACCESS_RULES,
522
+ };
@@ -8,7 +8,8 @@ import { McpServer, fromJsonSchema } from '@modelcontextprotocol/server';
8
8
  import type { JsonSchemaType } from '@modelcontextprotocol/server';
9
9
  import type { McpAuthResolver } from '../shared/interfaces/auth.interface.js';
10
10
  import type { ToolDeps, ResolvedToolContext, RawToolDefinition } from '../shared/agent/tool.helpers.js';
11
- import type { Question } from '../shared/schemas/question.schema.js';
11
+ import type { ToolRegistryDeps } from '../runtime/foreground/composition/tool.registry.js';
12
+ import type { McpAuthorizationObserver, McpCapabilityPolicyOptions } from './mcp.authorization-policy.js';
12
13
  /**
13
14
  * Static registration metadata for one MCP tool — schema conversion artifacts
14
15
  * that depend only on registry-shaping feature flags, not on per-request
@@ -23,7 +24,7 @@ type McpToolRegistrationMetadata = Pick<RawToolDefinition, 'name' | 'description
23
24
  * When a feature flag changes the tool set, the cache key changes so
24
25
  * the cached metadata set is automatically invalidated.
25
26
  */
26
- export declare function getMcpToolMetadataCacheKey(deps: Pick<ToolDeps, 'contactsEnabled' | 'chatSession' | 'agentDatabase' | 'agentDispatcher' | 'questionerEnqueue'>): string;
27
+ export declare function getMcpToolMetadataCacheKey(deps: Pick<ToolDeps, 'chatSession' | 'agentDatabase' | 'agentDispatcher' | 'questionerEnqueue'>): string;
27
28
  /**
28
29
  * Clears the metadata cache. Used in tests to ensure fresh state between cases.
29
30
  */
@@ -37,7 +38,7 @@ export declare function clearMcpToolMetadataCacheForTests(): void;
37
38
  * Does NOT store tool handlers — those remain request-scoped because they
38
39
  * capture per-request userDb/systemDb.
39
40
  */
40
- export declare function getCachedMcpToolMetadata(deps: ToolDeps): readonly McpToolRegistrationMetadata[];
41
+ export declare function getCachedMcpToolMetadata(deps: ToolRegistryDeps): readonly McpToolRegistrationMetadata[];
41
42
  /**
42
43
  * Runtime/auth failures are converted into structured MCP `isError` tool
43
44
  * results for the caller. Reporting them as application exceptions produces
@@ -53,23 +54,6 @@ export declare function sanitizeMcpResult(text: string): {
53
54
  text: string;
54
55
  isError: boolean;
55
56
  };
56
- /**
57
- * Extracts decision questions from a parsed tool-result text, if present.
58
- * Validates each entry against `QuestionSchema` and drops malformed items;
59
- * caps the array at `MAX_DECISION_QUESTIONS` (defense-in-depth — Slice 2's
60
- * generator already caps at 3, but we don't trust the cast here).
61
- *
62
- * Returns null when the text isn't JSON, has no `data.questions`, or
63
- * contains zero valid questions after validation.
64
- */
65
- export declare function extractDecisionQuestions(text: string): Question[] | null;
66
- /**
67
- * Renders the JSON-envelope text block appended to the tool result content
68
- * when decision questions are present. The leading sentinel string lets the
69
- * LLM client recognize and surface the questions in prose for clients
70
- * without elicitation support.
71
- */
72
- export declare function renderQuestionsEnvelope(questions: Question[]): string;
73
57
  /**
74
58
  * Factory for creating per-request scoped database instances.
75
59
  * Injected from the controller/handler layer to keep the protocol layer
@@ -91,13 +75,7 @@ export interface ScopedDepsFactory {
91
75
  * reach beyond its bound network).
92
76
  */
93
77
  export declare const applyNetworkScopeToContext: (context: ResolvedToolContext, networkScopeId: string | null | undefined) => void;
94
- /**
95
- * Tools allowed during onboarding — everything else is gated until
96
- * complete_onboarding is called. Includes the agent-gate-exempt tools
97
- * (register_agent, read_docs, scrape_url) because they are informational /
98
- * registration primitives needed at every lifecycle stage.
99
- */
100
- export declare const ONBOARDING_ALLOWED: ReadonlySet<string>;
78
+ export { ONBOARDING_ALLOWED } from './mcp.authorization-policy.js';
101
79
  /**
102
80
  * Builds the onboarding gate message for MCP callers. Condensed from the
103
81
  * chat orchestrator's 8-step flow (chat.prompt.ts buildOnboarding) into a
@@ -110,5 +88,4 @@ export declare const MCP_INSTRUCTIONS: string;
110
88
  */
111
89
  export declare function extractBearerToken(req: Request): string | undefined;
112
90
  export declare function parseClientSurface(raw: string | null): 'telegram' | 'web';
113
- export declare function createMcpServer(deps: ToolDeps, authResolver: McpAuthResolver, scopedDepsFactory: ScopedDepsFactory): McpServer;
114
- export {};
91
+ export declare function createMcpServer(deps: ToolRegistryDeps, authResolver: McpAuthResolver, scopedDepsFactory: ScopedDepsFactory, policyOptions?: McpCapabilityPolicyOptions, authorizationObserver?: McpAuthorizationObserver): McpServer;