@indexnetwork/protocol 6.15.0-rc.407.1 → 8.0.2-rc.418.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.
- package/CHANGELOG.md +309 -0
- package/IMPLEMENTATION.md +7 -4
- package/dist/capabilities/opportunities.facade.d.ts +4 -0
- package/dist/capabilities/opportunities.facade.js +4 -0
- package/dist/chat/chat-streaming.types.d.ts +4 -5
- package/dist/chat/chat.agent.d.ts +0 -19
- package/dist/chat/chat.agent.js +1 -70
- package/dist/chat/chat.persona.d.ts +0 -6
- package/dist/chat/chat.persona.js +0 -1
- package/dist/chat/chat.prompt.js +10 -10
- package/dist/chat/chat.prompt.modules.js +0 -104
- package/dist/chat/chat.tools.js +11 -7
- package/dist/chat/negotiator.persona.js +0 -1
- package/dist/chat/onboarding.persona.js +0 -1
- package/dist/chat/reporter.persona.d.ts +1 -1
- package/dist/chat/reporter.persona.js +1 -2
- package/dist/chat/reporter.prompt.js +5 -5
- package/dist/chat/signal.persona.js +0 -1
- package/dist/communities/application/network.tools.js +48 -27
- package/dist/contacts/application/contact.tools.js +9 -9
- package/dist/index.d.ts +9 -2
- package/dist/index.js +5 -0
- package/dist/maintenance/maintenance.graph.js +7 -0
- package/dist/mcp/mcp.authorization-policy.d.ts +250 -0
- package/dist/mcp/mcp.authorization-policy.js +522 -0
- package/dist/mcp/mcp.server.d.ts +6 -29
- package/dist/mcp/mcp.server.js +202 -228
- package/dist/negotiation/application/negotiation.agent.js +29 -5
- package/dist/negotiation/application/negotiation.graph.d.ts +2 -5
- package/dist/negotiation/application/negotiation.graph.js +53 -23
- package/dist/negotiation/domain/index.d.ts +3 -0
- package/dist/negotiation/domain/index.js +3 -0
- package/dist/negotiation/domain/negotiation.deadlock.d.ts +7 -1
- package/dist/negotiation/domain/negotiation.deadlock.js +48 -1
- package/dist/negotiation/domain/negotiation.stance.contracts.d.ts +85 -0
- package/dist/negotiation/domain/negotiation.stance.contracts.js +140 -0
- package/dist/negotiation/domain/negotiation.state.d.ts +2 -3
- package/dist/negotiation/domain/negotiation.state.js +2 -3
- package/dist/negotiation/public/index.d.ts +2 -0
- package/dist/negotiation/public/index.js +2 -0
- package/dist/opportunity/application/index.d.ts +0 -3
- package/dist/opportunity/application/index.js +0 -3
- package/dist/opportunity/application/opportunity.graph.d.ts +0 -43
- package/dist/opportunity/application/opportunity.graph.js +7 -113
- package/dist/opportunity/application/opportunity.introducer-feature.d.ts +2 -0
- package/dist/opportunity/application/opportunity.introducer-feature.js +4 -0
- package/dist/opportunity/application/opportunity.owner-approval.d.ts +118 -0
- package/dist/opportunity/application/opportunity.owner-approval.js +34 -0
- package/dist/opportunity/application/opportunity.owner-provenance.d.ts +6 -0
- package/dist/opportunity/application/opportunity.owner-provenance.js +28 -0
- package/dist/opportunity/application/opportunity.tools.d.ts +3 -3
- package/dist/opportunity/application/opportunity.tools.js +77 -870
- package/dist/opportunity/discriminator/discriminator.shadow.js +17 -10
- package/dist/opportunity/discriminator/discriminator.types.d.ts +2 -0
- package/dist/opportunity/domain/index.d.ts +0 -2
- package/dist/opportunity/domain/index.js +0 -2
- package/dist/opportunity/domain/opportunity.state.d.ts +2 -70
- package/dist/opportunity/domain/opportunity.state.js +2 -55
- package/dist/opportunity/opportunity.owner-approval.d.ts +5 -0
- package/dist/opportunity/opportunity.owner-approval.js +5 -0
- package/dist/opportunity/ports/opportunity.tools.port.d.ts +12 -1
- package/dist/opportunity/public/index.d.ts +3 -3
- package/dist/opportunity/public/index.js +2 -1
- package/dist/participant-agents/application/agent.tools.d.ts +1 -1
- package/dist/participant-agents/application/agent.tools.js +44 -6
- package/dist/participant-agents/application/index.d.ts +5 -3
- package/dist/participant-agents/application/index.js +5 -3
- package/dist/questions/application/question.tools.js +31 -2
- package/dist/runtime/foreground/composition/tool.factory.d.ts +2 -1
- package/dist/runtime/foreground/composition/tool.factory.js +9 -2
- package/dist/runtime/foreground/composition/tool.registry.d.ts +16 -2
- package/dist/runtime/foreground/composition/tool.registry.js +41 -38
- package/dist/shared/agent/activity-projection.d.ts +271 -0
- package/dist/shared/agent/activity-projection.js +248 -0
- package/dist/shared/agent/canonical-guidance.d.ts +30 -0
- package/dist/shared/agent/canonical-guidance.js +249 -0
- package/dist/shared/agent/tool.helpers.d.ts +9 -17
- package/dist/shared/agent/tool.runtime.js +1 -3
- package/dist/shared/agent/utility.tools.d.ts +11 -1
- package/dist/shared/agent/utility.tools.js +126 -90
- package/dist/shared/interfaces/auth.interface.d.ts +2 -8
- package/dist/shared/interfaces/chat-session.interface.js +5 -0
- package/dist/shared/interfaces/database.interface.d.ts +12 -5
- package/dist/shared/observability/request-context.d.ts +4 -40
- package/dist/shared/schemas/mcp-auth.schema.d.ts +87 -15
- package/dist/shared/schemas/mcp-auth.schema.js +49 -1
- package/dist/signals/application/intent.tools.js +7 -8
- package/package.json +10 -9
- package/dist/opportunity/application/opportunity.discover.d.ts +0 -215
- package/dist/opportunity/application/opportunity.discover.js +0 -793
- package/dist/opportunity/application/opportunity.discovery-continuation-finalization.d.ts +0 -34
- package/dist/opportunity/application/opportunity.discovery-continuation-finalization.js +0 -74
- package/dist/opportunity/application/opportunity.discovery-mcp-lifecycle-finalization.d.ts +0 -44
- package/dist/opportunity/application/opportunity.discovery-mcp-lifecycle-finalization.js +0 -92
- package/dist/opportunity/domain/opportunity.discovery-run-coalescing.d.ts +0 -33
- package/dist/opportunity/domain/opportunity.discovery-run-coalescing.js +0 -33
- package/dist/opportunity/domain/opportunity.discovery.contracts.d.ts +0 -45
- package/dist/opportunity/domain/opportunity.discovery.contracts.js +0 -1
- package/dist/opportunity/opportunity.discover.d.ts +0 -5
- package/dist/opportunity/opportunity.discover.js +0 -5
- package/dist/opportunity/opportunity.discovery-continuation-finalization.d.ts +0 -5
- package/dist/opportunity/opportunity.discovery-continuation-finalization.js +0 -5
- package/dist/opportunity/opportunity.discovery-mcp-lifecycle-finalization.d.ts +0 -5
- package/dist/opportunity/opportunity.discovery-mcp-lifecycle-finalization.js +0 -5
- package/dist/opportunity/opportunity.discovery-run-coalescing.d.ts +0 -5
- package/dist/opportunity/opportunity.discovery-run-coalescing.js +0 -5
- package/dist/opportunity/opportunity.discovery.contracts.d.ts +0 -5
- package/dist/opportunity/opportunity.discovery.contracts.js +0 -5
- package/dist/shared/interfaces/discovery-run.interface.d.ts +0 -70
- 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
|
+
};
|
package/dist/mcp/mcp.server.d.ts
CHANGED
|
@@ -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 {
|
|
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, '
|
|
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:
|
|
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:
|
|
114
|
-
export {};
|
|
91
|
+
export declare function createMcpServer(deps: ToolRegistryDeps, authResolver: McpAuthResolver, scopedDepsFactory: ScopedDepsFactory, policyOptions?: McpCapabilityPolicyOptions, authorizationObserver?: McpAuthorizationObserver): McpServer;
|