chainlesschain 0.45.63 → 0.45.65

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.
@@ -0,0 +1,372 @@
1
+ /**
2
+ * Canonical Coding Agent event protocol — CJS so both the ESM CLI runtime
3
+ * (packages/cli/src/runtime/runtime-events.js) and the Desktop Main process
4
+ * (desktop-app-vue/src/main/ai-engine/code-agent/coding-agent-events.js) can
5
+ * share a single source of truth for event types and the envelope shape.
6
+ *
7
+ * Spec: docs/design/modules/79_Coding_Agent系统.md §5
8
+ * docs/implementation-plans/CODING_AGENT_EVENT_SCHEMA.md
9
+ */
10
+
11
+ const { randomUUID } = require("crypto");
12
+
13
+ const CODING_AGENT_EVENT_VERSION = "1.0";
14
+ const CODING_AGENT_EVENT_CHANNEL = "coding-agent:event";
15
+
16
+ /**
17
+ * Unified event types — dot-case, grouped by domain.
18
+ * Renderer / CLI / Main MUST consume from this whitelist.
19
+ */
20
+ const CODING_AGENT_EVENT_TYPES = Object.freeze({
21
+ // Session lifecycle
22
+ SESSION_STARTED: "session.started",
23
+ SESSION_RESUMED: "session.resumed",
24
+ SESSION_INTERRUPTED: "session.interrupted",
25
+ SESSION_COMPLETED: "session.completed",
26
+ SESSION_CLOSED: "session.closed",
27
+
28
+ // Request lifecycle
29
+ REQUEST_ACCEPTED: "request.accepted",
30
+ REQUEST_REJECTED: "request.rejected",
31
+
32
+ // Assistant output
33
+ ASSISTANT_MESSAGE: "assistant.message",
34
+ ASSISTANT_DELTA: "assistant.delta",
35
+ ASSISTANT_THOUGHT_SUMMARY: "assistant.thought-summary",
36
+ ASSISTANT_FINAL: "assistant.final",
37
+
38
+ // Plan mode
39
+ PLAN_STARTED: "plan.started",
40
+ PLAN_UPDATED: "plan.updated",
41
+ PLAN_APPROVAL_REQUIRED: "plan.approval_required",
42
+ PLAN_APPROVED: "plan.approved",
43
+ PLAN_REJECTED: "plan.rejected",
44
+
45
+ // Tool calls
46
+ TOOL_CALL_STARTED: "tool.call.started",
47
+ TOOL_CALL_PROGRESS: "tool.call.progress",
48
+ TOOL_CALL_COMPLETED: "tool.call.completed",
49
+ TOOL_CALL_FAILED: "tool.call.failed",
50
+ TOOL_CALL_SKIPPED: "tool.call.skipped",
51
+
52
+ // Approval
53
+ APPROVAL_REQUESTED: "approval.requested",
54
+ APPROVAL_GRANTED: "approval.granted",
55
+ APPROVAL_DENIED: "approval.denied",
56
+ APPROVAL_EXPIRED: "approval.expired",
57
+
58
+ // Context compaction
59
+ CONTEXT_COMPACTION_STARTED: "context.compaction.started",
60
+ CONTEXT_COMPACTION_COMPLETED: "context.compaction.completed",
61
+
62
+ // Errors / warnings
63
+ WARNING: "warning",
64
+ ERROR: "error",
65
+
66
+ // Extension types — outside the v1.0 core but still part of the envelope
67
+ WORKTREE_LIST: "worktree.list",
68
+ WORKTREE_DIFF: "worktree.diff",
69
+ WORKTREE_MERGED: "worktree.merged",
70
+ WORKTREE_MERGE_PREVIEW: "worktree.merge-preview",
71
+ WORKTREE_AUTOMATION_APPLIED: "worktree.automation-applied",
72
+ SESSION_LIST: "session.list",
73
+ COMMAND_RESPONSE: "command.response",
74
+ SLOT_FILLING: "slot.filling",
75
+ MODEL_SWITCH: "model.switch",
76
+ HIGH_RISK_CONFIRMATION_REQUIRED: "approval.high-risk.requested",
77
+ HIGH_RISK_CONFIRMED: "approval.high-risk.granted",
78
+ SERVER_STARTING: "runtime.server.starting",
79
+ SERVER_READY: "runtime.server.ready",
80
+ SERVER_STOPPED: "runtime.server.stopped",
81
+ });
82
+
83
+ const VALID_TYPE_SET = new Set(Object.values(CODING_AGENT_EVENT_TYPES));
84
+
85
+ /**
86
+ * Map kebab-case legacy message types (the wire format the CLI WS server
87
+ * currently emits, and what `agent-core.js` yields) to the unified dot-case
88
+ * protocol. Anything not listed here gets passed through unchanged so we can
89
+ * stage the migration.
90
+ */
91
+ const LEGACY_TO_UNIFIED_TYPE = Object.freeze({
92
+ // Session lifecycle
93
+ "session-created": CODING_AGENT_EVENT_TYPES.SESSION_STARTED,
94
+ "session-resumed": CODING_AGENT_EVENT_TYPES.SESSION_RESUMED,
95
+ "session-closed": CODING_AGENT_EVENT_TYPES.SESSION_CLOSED,
96
+ "session-interrupted": CODING_AGENT_EVENT_TYPES.SESSION_INTERRUPTED,
97
+ "session-completed": CODING_AGENT_EVENT_TYPES.SESSION_COMPLETED,
98
+ "session-list-result": CODING_AGENT_EVENT_TYPES.SESSION_LIST,
99
+
100
+ // Assistant output
101
+ "response-token": CODING_AGENT_EVENT_TYPES.ASSISTANT_DELTA,
102
+ "response-delta": CODING_AGENT_EVENT_TYPES.ASSISTANT_DELTA,
103
+ "response-complete": CODING_AGENT_EVENT_TYPES.ASSISTANT_FINAL,
104
+ "response-message": CODING_AGENT_EVENT_TYPES.ASSISTANT_MESSAGE,
105
+ "thought-summary": CODING_AGENT_EVENT_TYPES.ASSISTANT_THOUGHT_SUMMARY,
106
+
107
+ // Tool calls
108
+ "tool-executing": CODING_AGENT_EVENT_TYPES.TOOL_CALL_STARTED,
109
+ "tool-progress": CODING_AGENT_EVENT_TYPES.TOOL_CALL_PROGRESS,
110
+ "tool-result": CODING_AGENT_EVENT_TYPES.TOOL_CALL_COMPLETED,
111
+ "tool-error": CODING_AGENT_EVENT_TYPES.TOOL_CALL_FAILED,
112
+ "tool-skipped": CODING_AGENT_EVENT_TYPES.TOOL_CALL_SKIPPED,
113
+ "tool-blocked": CODING_AGENT_EVENT_TYPES.TOOL_CALL_FAILED,
114
+
115
+ // Plan mode
116
+ "plan-started": CODING_AGENT_EVENT_TYPES.PLAN_STARTED,
117
+ "plan-updated": CODING_AGENT_EVENT_TYPES.PLAN_UPDATED,
118
+ "plan-ready": CODING_AGENT_EVENT_TYPES.PLAN_APPROVAL_REQUIRED,
119
+ "plan-generated": CODING_AGENT_EVENT_TYPES.PLAN_UPDATED,
120
+ "plan-approved": CODING_AGENT_EVENT_TYPES.PLAN_APPROVED,
121
+ "plan-rejected": CODING_AGENT_EVENT_TYPES.PLAN_REJECTED,
122
+
123
+ // Request lifecycle
124
+ "request-accepted": CODING_AGENT_EVENT_TYPES.REQUEST_ACCEPTED,
125
+ "request-rejected": CODING_AGENT_EVENT_TYPES.REQUEST_REJECTED,
126
+
127
+ // Approvals
128
+ "approval-requested": CODING_AGENT_EVENT_TYPES.APPROVAL_REQUESTED,
129
+ "approval-granted": CODING_AGENT_EVENT_TYPES.APPROVAL_GRANTED,
130
+ "approval-denied": CODING_AGENT_EVENT_TYPES.APPROVAL_DENIED,
131
+ "approval-expired": CODING_AGENT_EVENT_TYPES.APPROVAL_EXPIRED,
132
+ "high-risk-confirmation-required":
133
+ CODING_AGENT_EVENT_TYPES.HIGH_RISK_CONFIRMATION_REQUIRED,
134
+ "high-risk-confirmed": CODING_AGENT_EVENT_TYPES.HIGH_RISK_CONFIRMED,
135
+
136
+ // Context compaction
137
+ "compression-started": CODING_AGENT_EVENT_TYPES.CONTEXT_COMPACTION_STARTED,
138
+ "compression-applied": CODING_AGENT_EVENT_TYPES.CONTEXT_COMPACTION_COMPLETED,
139
+ "compression-stats": CODING_AGENT_EVENT_TYPES.CONTEXT_COMPACTION_COMPLETED,
140
+
141
+ // Worktree extensions
142
+ "worktree-list": CODING_AGENT_EVENT_TYPES.WORKTREE_LIST,
143
+ "worktree-diff": CODING_AGENT_EVENT_TYPES.WORKTREE_DIFF,
144
+ "worktree-merged": CODING_AGENT_EVENT_TYPES.WORKTREE_MERGED,
145
+ "worktree-merge-preview": CODING_AGENT_EVENT_TYPES.WORKTREE_MERGE_PREVIEW,
146
+ "worktree-automation-applied":
147
+ CODING_AGENT_EVENT_TYPES.WORKTREE_AUTOMATION_APPLIED,
148
+
149
+ // Misc extensions
150
+ "command-response": CODING_AGENT_EVENT_TYPES.COMMAND_RESPONSE,
151
+ "slot-filling": CODING_AGENT_EVENT_TYPES.SLOT_FILLING,
152
+ "model-switch": CODING_AGENT_EVENT_TYPES.MODEL_SWITCH,
153
+ "server-starting": CODING_AGENT_EVENT_TYPES.SERVER_STARTING,
154
+ "server-ready": CODING_AGENT_EVENT_TYPES.SERVER_READY,
155
+ "server-stopped": CODING_AGENT_EVENT_TYPES.SERVER_STOPPED,
156
+
157
+ // Errors
158
+ warning: CODING_AGENT_EVENT_TYPES.WARNING,
159
+ error: CODING_AGENT_EVENT_TYPES.ERROR,
160
+ });
161
+
162
+ /**
163
+ * Map a legacy kebab-case type into the canonical dot-case type.
164
+ * Returns the original input if no mapping exists, so unknown types fall
165
+ * through and the receiver still gets a structured envelope.
166
+ */
167
+ function mapLegacyType(type) {
168
+ if (!type) {
169
+ return null;
170
+ }
171
+ return LEGACY_TO_UNIFIED_TYPE[type] || type;
172
+ }
173
+
174
+ /**
175
+ * Per-requestId monotonically-increasing sequence tracker, scoped to a
176
+ * single agent runtime instance. Used to satisfy the protocol invariant:
177
+ *
178
+ * "Within the same requestId, sequence MUST be strictly increasing."
179
+ */
180
+ class CodingAgentSequenceTracker {
181
+ constructor() {
182
+ this._counters = new Map();
183
+ }
184
+
185
+ next(requestId) {
186
+ if (!requestId) {
187
+ return 0;
188
+ }
189
+ const current = this._counters.get(requestId) || 0;
190
+ const next = current + 1;
191
+ this._counters.set(requestId, next);
192
+ return next;
193
+ }
194
+
195
+ reset(requestId) {
196
+ if (requestId) {
197
+ this._counters.delete(requestId);
198
+ } else {
199
+ this._counters.clear();
200
+ }
201
+ }
202
+
203
+ peek(requestId) {
204
+ return this._counters.get(requestId) || 0;
205
+ }
206
+ }
207
+
208
+ const defaultSequenceTracker = new CodingAgentSequenceTracker();
209
+
210
+ /**
211
+ * Build a unified Coding Agent event envelope. This is the ONLY shape the
212
+ * CLI runtime, the Desktop Main process and the Renderer should pass around.
213
+ *
214
+ * @param {string} type One of CODING_AGENT_EVENT_TYPES (or a legacy
215
+ * kebab-case alias, which will be normalized).
216
+ * @param {object} payload Event-specific data. Must be a plain object.
217
+ * @param {object} context Envelope context: sessionId, requestId, source,
218
+ * sequence, eventId, timestamp, meta, tracker.
219
+ */
220
+ function createCodingAgentEvent(type, payload = {}, context = {}) {
221
+ if (payload && typeof payload !== "object") {
222
+ throw new TypeError(
223
+ "createCodingAgentEvent: payload must be an object",
224
+ );
225
+ }
226
+
227
+ const normalizedType = mapLegacyType(type);
228
+ const tracker = context.tracker || defaultSequenceTracker;
229
+ const sessionId =
230
+ context.sessionId || (payload && payload.sessionId) || null;
231
+ const requestId =
232
+ context.requestId ||
233
+ (payload && (payload.requestId || payload.id)) ||
234
+ null;
235
+
236
+ let sequence;
237
+ if (Number.isInteger(context.sequence)) {
238
+ sequence = context.sequence;
239
+ } else if (requestId && tracker) {
240
+ sequence = tracker.next(requestId);
241
+ } else {
242
+ sequence = null;
243
+ }
244
+
245
+ const eventId = context.eventId || randomUUID();
246
+
247
+ const meta = { ...(context.meta || {}) };
248
+ // Strip envelope fields if a caller accidentally stuffed them into meta.
249
+ delete meta.sessionId;
250
+ delete meta.requestId;
251
+ delete meta.sequence;
252
+ delete meta.source;
253
+ delete meta.eventId;
254
+
255
+ return {
256
+ version: CODING_AGENT_EVENT_VERSION,
257
+ eventId,
258
+ // Legacy alias retained for transitional consumers that still read .id.
259
+ id: eventId,
260
+ type: normalizedType,
261
+ timestamp: context.timestamp || Date.now(),
262
+ sessionId,
263
+ requestId,
264
+ // Default to "desktop-main" to preserve the existing Desktop call-site
265
+ // semantics. CLI runtime sites should pass `source: "cli-runtime"`
266
+ // explicitly when they adopt the protocol.
267
+ source: context.source || "desktop-main",
268
+ sequence,
269
+ payload: payload || {},
270
+ meta,
271
+ };
272
+ }
273
+
274
+ /**
275
+ * Wrap a legacy kebab-case message (e.g. `{ type: "session-created", ... }`
276
+ * coming over the WS wire from the CLI server) into a unified envelope.
277
+ * Used during the migration window so receivers don't have to learn two
278
+ * shapes — they can call this once at the boundary and forget the rest.
279
+ */
280
+ function wrapLegacyMessage(message, context = {}) {
281
+ if (!message || typeof message !== "object") {
282
+ throw new TypeError(
283
+ "wrapLegacyMessage: message must be a non-null object",
284
+ );
285
+ }
286
+
287
+ const { type, ...payload } = message;
288
+ return createCodingAgentEvent(type, payload, {
289
+ ...context,
290
+ requestId: context.requestId || message.id || message.requestId || null,
291
+ sessionId: context.sessionId || message.sessionId || null,
292
+ });
293
+ }
294
+
295
+ /**
296
+ * Validate an envelope. Returns `{ valid: true }` or
297
+ * `{ valid: false, errors: [...] }`. Used by tests and the Desktop bridge
298
+ * to fail fast when a producer sends a malformed event.
299
+ */
300
+ function validateCodingAgentEvent(envelope) {
301
+ const errors = [];
302
+ if (!envelope || typeof envelope !== "object") {
303
+ return { valid: false, errors: ["envelope must be an object"] };
304
+ }
305
+ if (envelope.version !== CODING_AGENT_EVENT_VERSION) {
306
+ errors.push(`version must be "${CODING_AGENT_EVENT_VERSION}"`);
307
+ }
308
+ if (!envelope.type) {
309
+ errors.push("type is required");
310
+ } else if (!VALID_TYPE_SET.has(envelope.type)) {
311
+ errors.push(`type "${envelope.type}" is not in the whitelist`);
312
+ }
313
+ if (!envelope.eventId) {
314
+ errors.push("eventId is required");
315
+ }
316
+ if (envelope.payload && typeof envelope.payload !== "object") {
317
+ errors.push("payload must be an object");
318
+ }
319
+ return { valid: errors.length === 0, errors };
320
+ }
321
+
322
+ /**
323
+ * Legacy alias enum — kept so existing Desktop code that imports
324
+ * `CodingAgentEventType.SESSION_CREATED` keeps compiling while we migrate
325
+ * call sites to `CODING_AGENT_EVENT_TYPES.SESSION_STARTED`.
326
+ */
327
+ const CodingAgentEventType = Object.freeze({
328
+ SERVER_STARTING: CODING_AGENT_EVENT_TYPES.SERVER_STARTING,
329
+ SERVER_READY: CODING_AGENT_EVENT_TYPES.SERVER_READY,
330
+ SERVER_STOPPED: CODING_AGENT_EVENT_TYPES.SERVER_STOPPED,
331
+ SESSION_CREATED: CODING_AGENT_EVENT_TYPES.SESSION_STARTED,
332
+ SESSION_RESUMED: CODING_AGENT_EVENT_TYPES.SESSION_RESUMED,
333
+ SESSION_CLOSED: CODING_AGENT_EVENT_TYPES.SESSION_CLOSED,
334
+ SESSION_LIST: CODING_AGENT_EVENT_TYPES.SESSION_LIST,
335
+ WORKTREE_LIST: CODING_AGENT_EVENT_TYPES.WORKTREE_LIST,
336
+ WORKTREE_DIFF: CODING_AGENT_EVENT_TYPES.WORKTREE_DIFF,
337
+ WORKTREE_MERGE_PREVIEW: CODING_AGENT_EVENT_TYPES.WORKTREE_MERGE_PREVIEW,
338
+ WORKTREE_MERGED: CODING_AGENT_EVENT_TYPES.WORKTREE_MERGED,
339
+ WORKTREE_AUTOMATION_APPLIED:
340
+ CODING_AGENT_EVENT_TYPES.WORKTREE_AUTOMATION_APPLIED,
341
+ MESSAGE_SENT: CODING_AGENT_EVENT_TYPES.REQUEST_ACCEPTED,
342
+ RESPONSE_COMPLETE: CODING_AGENT_EVENT_TYPES.ASSISTANT_FINAL,
343
+ TOOL_EXECUTING: CODING_AGENT_EVENT_TYPES.TOOL_CALL_STARTED,
344
+ TOOL_RESULT: CODING_AGENT_EVENT_TYPES.TOOL_CALL_COMPLETED,
345
+ TOOL_BLOCKED: CODING_AGENT_EVENT_TYPES.TOOL_CALL_FAILED,
346
+ COMMAND_RESPONSE: CODING_AGENT_EVENT_TYPES.COMMAND_RESPONSE,
347
+ PLAN_READY: CODING_AGENT_EVENT_TYPES.PLAN_APPROVAL_REQUIRED,
348
+ PLAN_GENERATED: CODING_AGENT_EVENT_TYPES.PLAN_UPDATED,
349
+ APPROVAL_REQUESTED: CODING_AGENT_EVENT_TYPES.APPROVAL_REQUESTED,
350
+ APPROVAL_GRANTED: CODING_AGENT_EVENT_TYPES.APPROVAL_GRANTED,
351
+ APPROVAL_DENIED: CODING_AGENT_EVENT_TYPES.APPROVAL_DENIED,
352
+ HIGH_RISK_CONFIRMATION_REQUIRED:
353
+ CODING_AGENT_EVENT_TYPES.HIGH_RISK_CONFIRMATION_REQUIRED,
354
+ HIGH_RISK_CONFIRMED: CODING_AGENT_EVENT_TYPES.HIGH_RISK_CONFIRMED,
355
+ SLOT_FILLING: CODING_AGENT_EVENT_TYPES.SLOT_FILLING,
356
+ MODEL_SWITCH: CODING_AGENT_EVENT_TYPES.MODEL_SWITCH,
357
+ ERROR: CODING_AGENT_EVENT_TYPES.ERROR,
358
+ });
359
+
360
+ module.exports = {
361
+ CODING_AGENT_EVENT_VERSION,
362
+ CODING_AGENT_EVENT_CHANNEL,
363
+ CODING_AGENT_EVENT_TYPES,
364
+ CodingAgentEventType, // legacy alias
365
+ LEGACY_TO_UNIFIED_TYPE,
366
+ CodingAgentSequenceTracker,
367
+ defaultSequenceTracker,
368
+ createCodingAgentEvent,
369
+ wrapLegacyMessage,
370
+ validateCodingAgentEvent,
371
+ mapLegacyType,
372
+ };
@@ -0,0 +1,294 @@
1
+ "use strict";
2
+
3
+ const DEFAULT_ALLOWED_MANAGED_TOOL_NAMES = Object.freeze([
4
+ "info_searcher",
5
+ "format_output",
6
+ "json_parser",
7
+ "yaml_parser",
8
+ "base64_handler",
9
+ ]);
10
+
11
+ const DEFAULT_ALLOWED_MCP_SERVER_NAMES = Object.freeze(["weather"]);
12
+
13
+ const MCP_SERVER_READY_STATES = Object.freeze(["connected", "ready"]);
14
+
15
+ const RISK_LEVEL_ORDER = Object.freeze({
16
+ low: 0,
17
+ medium: 1,
18
+ high: 2,
19
+ });
20
+
21
+ function normalizeCollection(values, fallback = []) {
22
+ if (values instanceof Set) {
23
+ return values;
24
+ }
25
+
26
+ if (Array.isArray(values)) {
27
+ return new Set(values);
28
+ }
29
+
30
+ return new Set(fallback);
31
+ }
32
+
33
+ function normalizeRiskLevel(value, fallback = "medium") {
34
+ if (value === "low" || value === "medium" || value === "high") {
35
+ return value;
36
+ }
37
+
38
+ if (typeof value === "number") {
39
+ if (value <= 1) {
40
+ return "low";
41
+ }
42
+ if (value === 2) {
43
+ return "medium";
44
+ }
45
+ return "high";
46
+ }
47
+
48
+ if (typeof value === "string" && /^\d+$/.test(value)) {
49
+ return normalizeRiskLevel(Number(value), fallback);
50
+ }
51
+
52
+ return fallback;
53
+ }
54
+
55
+ function normalizeBoolean(value, fallback = false) {
56
+ if (typeof value === "boolean") {
57
+ return value;
58
+ }
59
+
60
+ if (typeof value === "number") {
61
+ return value !== 0;
62
+ }
63
+
64
+ if (typeof value === "string") {
65
+ const normalized = value.trim().toLowerCase();
66
+ if (normalized === "true" || normalized === "1") {
67
+ return true;
68
+ }
69
+ if (normalized === "false" || normalized === "0") {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ return fallback;
75
+ }
76
+
77
+ function selectHigherRiskLevel(...values) {
78
+ const normalized = values
79
+ .map((value) => normalizeRiskLevel(value, null))
80
+ .filter(Boolean);
81
+
82
+ if (normalized.length === 0) {
83
+ return "medium";
84
+ }
85
+
86
+ return normalized.reduce((current, candidate) =>
87
+ RISK_LEVEL_ORDER[candidate] > RISK_LEVEL_ORDER[current] ? candidate : current,
88
+ );
89
+ }
90
+
91
+ function createTrustedMcpServerMap(registry = null) {
92
+ const trustedServers = Array.isArray(registry?.trustedServers)
93
+ ? registry.trustedServers
94
+ : Array.isArray(registry)
95
+ ? registry
96
+ : [];
97
+
98
+ return new Map(
99
+ trustedServers
100
+ .filter((server) => server?.id)
101
+ .map((server) => [
102
+ server.id,
103
+ {
104
+ ...server,
105
+ securityLevel: selectHigherRiskLevel(server.securityLevel),
106
+ requiredPermissions: Array.isArray(server.requiredPermissions)
107
+ ? [...server.requiredPermissions]
108
+ : [],
109
+ capabilities: Array.isArray(server.capabilities)
110
+ ? [...server.capabilities]
111
+ : [],
112
+ },
113
+ ]),
114
+ );
115
+ }
116
+
117
+ function resolveManagedToolPolicy(managedTool, options = {}) {
118
+ const name = String(managedTool?.name || "").trim();
119
+ const allowedManagedToolNames = normalizeCollection(
120
+ options.allowedManagedToolNames,
121
+ DEFAULT_ALLOWED_MANAGED_TOOL_NAMES,
122
+ );
123
+ const coreToolNames = normalizeCollection(options.coreToolNames);
124
+ const enabled = normalizeBoolean(managedTool?.enabled, true);
125
+ const deprecated = normalizeBoolean(managedTool?.deprecated, false);
126
+ const riskLevel = normalizeRiskLevel(managedTool?.risk_level, "medium");
127
+ const isReadOnly =
128
+ normalizeBoolean(managedTool?.is_read_only, false) || riskLevel === "low";
129
+
130
+ if (!name) {
131
+ return {
132
+ allowed: false,
133
+ decision: "deny",
134
+ reason: "Managed tool is missing a stable name.",
135
+ riskLevel,
136
+ isReadOnly,
137
+ };
138
+ }
139
+
140
+ if (coreToolNames.has(name)) {
141
+ return {
142
+ allowed: false,
143
+ decision: "deny",
144
+ reason: `Managed tool "${name}" collides with a core coding-agent tool.`,
145
+ riskLevel,
146
+ isReadOnly,
147
+ };
148
+ }
149
+
150
+ if (allowedManagedToolNames.size > 0 && !allowedManagedToolNames.has(name)) {
151
+ return {
152
+ allowed: false,
153
+ decision: "deny",
154
+ reason: `Managed tool "${name}" is not on the desktop allowlist.`,
155
+ riskLevel,
156
+ isReadOnly,
157
+ };
158
+ }
159
+
160
+ if (!enabled) {
161
+ return {
162
+ allowed: false,
163
+ decision: "deny",
164
+ reason: `Managed tool "${name}" is disabled.`,
165
+ riskLevel,
166
+ isReadOnly,
167
+ };
168
+ }
169
+
170
+ if (deprecated) {
171
+ return {
172
+ allowed: false,
173
+ decision: "deny",
174
+ reason: `Managed tool "${name}" is deprecated.`,
175
+ riskLevel,
176
+ isReadOnly,
177
+ };
178
+ }
179
+
180
+ return {
181
+ allowed: true,
182
+ decision: "allow",
183
+ reason: `Managed tool "${name}" is allowlisted for coding-agent sessions.`,
184
+ riskLevel,
185
+ isReadOnly,
186
+ };
187
+ }
188
+
189
+ function resolveMcpServerPolicy(serverName, serverState, options = {}) {
190
+ const normalizedServerName = String(serverName || "").trim();
191
+ const allowedMcpServerNames = normalizeCollection(
192
+ options.allowedMcpServerNames,
193
+ DEFAULT_ALLOWED_MCP_SERVER_NAMES,
194
+ );
195
+ const trustedMcpServers =
196
+ options.trustedMcpServers instanceof Map
197
+ ? options.trustedMcpServers
198
+ : createTrustedMcpServerMap(options.trustedMcpServers);
199
+ const allowHighRiskMcpServers = options.allowHighRiskMcpServers === true;
200
+ const trustedServer = trustedMcpServers.get(normalizedServerName) || null;
201
+ const securityLevel = trustedServer
202
+ ? selectHigherRiskLevel(
203
+ trustedServer.securityLevel,
204
+ serverState?.securityLevel,
205
+ )
206
+ : normalizeRiskLevel(serverState?.securityLevel, "high");
207
+
208
+ if (!normalizedServerName) {
209
+ return {
210
+ allowed: false,
211
+ decision: "deny",
212
+ trusted: false,
213
+ securityLevel,
214
+ reason: "MCP server is missing a stable name.",
215
+ requiredPermissions: [],
216
+ capabilities: [],
217
+ };
218
+ }
219
+
220
+ if (
221
+ allowedMcpServerNames.size > 0 &&
222
+ !allowedMcpServerNames.has(normalizedServerName)
223
+ ) {
224
+ return {
225
+ allowed: false,
226
+ decision: "deny",
227
+ trusted: !!trustedServer,
228
+ securityLevel,
229
+ reason: `MCP server "${normalizedServerName}" is not on the desktop allowlist.`,
230
+ requiredPermissions: trustedServer?.requiredPermissions || [],
231
+ capabilities: trustedServer?.capabilities || [],
232
+ };
233
+ }
234
+
235
+ if (!trustedServer) {
236
+ return {
237
+ allowed: false,
238
+ decision: "deny",
239
+ trusted: false,
240
+ securityLevel,
241
+ reason: `MCP server "${normalizedServerName}" is not in the trusted registry.`,
242
+ requiredPermissions: [],
243
+ capabilities: [],
244
+ };
245
+ }
246
+
247
+ const state = String(serverState?.state || "connected").toLowerCase();
248
+ if (!MCP_SERVER_READY_STATES.includes(state)) {
249
+ return {
250
+ allowed: false,
251
+ decision: "deny",
252
+ trusted: true,
253
+ securityLevel,
254
+ reason: `MCP server "${normalizedServerName}" is not ready.`,
255
+ requiredPermissions: trustedServer.requiredPermissions,
256
+ capabilities: trustedServer.capabilities,
257
+ };
258
+ }
259
+
260
+ if (securityLevel === "high" && !allowHighRiskMcpServers) {
261
+ return {
262
+ allowed: false,
263
+ decision: "deny",
264
+ trusted: true,
265
+ securityLevel,
266
+ reason: `MCP server "${normalizedServerName}" is high risk and requires explicit opt-in.`,
267
+ requiredPermissions: trustedServer.requiredPermissions,
268
+ capabilities: trustedServer.capabilities,
269
+ };
270
+ }
271
+
272
+ return {
273
+ allowed: true,
274
+ decision: "allow",
275
+ trusted: true,
276
+ securityLevel,
277
+ reason: `Trusted MCP server "${normalizedServerName}" is allowed for coding-agent sessions.`,
278
+ requiredPermissions: trustedServer.requiredPermissions,
279
+ capabilities: trustedServer.capabilities,
280
+ server: trustedServer,
281
+ };
282
+ }
283
+
284
+ module.exports = {
285
+ DEFAULT_ALLOWED_MANAGED_TOOL_NAMES,
286
+ DEFAULT_ALLOWED_MCP_SERVER_NAMES,
287
+ MCP_SERVER_READY_STATES,
288
+ normalizeRiskLevel,
289
+ normalizeBoolean,
290
+ selectHigherRiskLevel,
291
+ createTrustedMcpServerMap,
292
+ resolveManagedToolPolicy,
293
+ resolveMcpServerPolicy,
294
+ };