chainlesschain 0.45.64 → 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.
- package/package.json +1 -1
- package/src/gateways/ws/session-protocol.js +162 -105
- package/src/gateways/ws/worktree-protocol.js +134 -141
- package/src/lib/agent-core.js +173 -13
- package/src/lib/interaction-adapter.js +45 -0
- package/src/lib/plan-mode.js +3 -1
- package/src/lib/session-manager.js +45 -8
- package/src/lib/web-ui-envelope.js +94 -0
- package/src/lib/web-ui-server.js +13 -1
- package/src/lib/ws-agent-handler.js +8 -1
- package/src/lib/ws-session-manager.js +388 -20
- package/src/runtime/agent-runtime.js +140 -37
- package/src/runtime/coding-agent-contract.js +294 -0
- package/src/runtime/coding-agent-events.cjs +372 -0
- package/src/runtime/coding-agent-managed-tool-policy.cjs +294 -0
- package/src/runtime/coding-agent-policy.cjs +354 -0
- package/src/runtime/coding-agent-shell-policy.cjs +233 -0
- package/src/runtime/contracts/session-record.js +13 -0
- package/src/runtime/index.js +14 -0
- package/src/runtime/runtime-events.js +27 -0
- package/src/tools/index.js +12 -0
- package/src/tools/legacy-agent-tools.js +12 -157
|
@@ -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
|
+
};
|