@engineeros/connector 0.8.8 → 0.9.0

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,643 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { createReadStream, createWriteStream } from "node:fs";
4
+ import {
5
+ access,
6
+ chmod,
7
+ mkdir,
8
+ readFile,
9
+ rename,
10
+ rm,
11
+ stat,
12
+ writeFile,
13
+ } from "node:fs/promises";
14
+ import os from "node:os";
15
+ import path from "node:path";
16
+ import { Readable } from "node:stream";
17
+ import { pipeline } from "node:stream/promises";
18
+
19
+ export const ACP_REGISTRY_URL =
20
+ "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
21
+
22
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1_000;
23
+ const PRIORITY_AGENT_IDS = ["codex-acp", "claude-acp", "gemini"];
24
+ const AGENT_ALIASES = {
25
+ codex: "codex-acp",
26
+ claude: "claude-acp",
27
+ };
28
+ const SAFE_MODES = {
29
+ "codex-acp": { "read-only": "read-only", "workspace-write": "agent" },
30
+ "claude-acp": {
31
+ "read-only": "plan",
32
+ "workspace-write": "acceptEdits",
33
+ },
34
+ };
35
+
36
+ let defaultRegistryPromise;
37
+ const commandChecks = new Map();
38
+
39
+ export async function registeredAgents(options = {}) {
40
+ const registry = await loadRegistry(options);
41
+ return registry.agents
42
+ .map((agent) => ({
43
+ ...agent,
44
+ modes: { ...(SAFE_MODES[agent.id] || {}) },
45
+ distribution_type: distributionType(agent),
46
+ }))
47
+ .sort(compareAgents);
48
+ }
49
+
50
+ export async function registeredAgent(agentId, options = {}) {
51
+ const normalized = normalizeLookup(agentId);
52
+ const resolvedId = AGENT_ALIASES[normalized] || normalized;
53
+ const agents = await registeredAgents(options);
54
+ return (
55
+ agents.find(
56
+ (agent) =>
57
+ agent.id.toLowerCase() === resolvedId ||
58
+ agent.name.toLowerCase() === normalized,
59
+ ) ?? null
60
+ );
61
+ }
62
+
63
+ export async function registeredAgentConfig(agentId, options = {}) {
64
+ const agent = await requireAgent(agentId, options);
65
+ const launch = resolveLaunch(agent, options);
66
+ return {
67
+ agent_id: agent.id,
68
+ agent_protocol: "acp",
69
+ agent_command: launch.command,
70
+ agent_args: launch.args,
71
+ agent_env: launch.env,
72
+ agent_name: agent.name,
73
+ agent_version: agent.version,
74
+ agent_distribution: launch.type,
75
+ agent_modes: { ...agent.modes },
76
+ };
77
+ }
78
+
79
+ export async function agentInstallCommand(agentId, options = {}) {
80
+ const agent = await requireAgent(agentId, options);
81
+ const launch = resolveLaunch(agent, options);
82
+ if (launch.type === "npx") {
83
+ return [
84
+ platformCommand("npm", options.platform),
85
+ ["exec", "--yes", `--package=${launch.package}`, "--", "node", "-e", ""],
86
+ ];
87
+ }
88
+ if (launch.type === "uvx") {
89
+ return [
90
+ platformCommand("uv", options.platform),
91
+ ["tool", "install", launch.package],
92
+ ];
93
+ }
94
+ return null;
95
+ }
96
+
97
+ export async function agentCheckCommand(agentId, options = {}) {
98
+ const agent = await requireAgent(agentId, options);
99
+ const launch = resolveLaunch(agent, options);
100
+ if (launch.type === "binary") return null;
101
+ return [launch.command, ["--version"]];
102
+ }
103
+
104
+ export async function inspectRegisteredAgent(agentId, options = {}) {
105
+ const agent = await requireAgent(agentId, options);
106
+ const launch = resolveLaunch(agent, options);
107
+ const ready =
108
+ launch.type === "binary"
109
+ ? await fileExists(launch.command)
110
+ : await commandAvailable(launch.command, options);
111
+ if (!ready) {
112
+ throw new Error(
113
+ `${agent.name} is not ready. Run ` +
114
+ `\`engineeros-connector agent ${agent.id} install\`, then retry.`,
115
+ );
116
+ }
117
+ return {
118
+ protocol: "acp",
119
+ id: agent.id,
120
+ name: agent.name,
121
+ version: agent.version,
122
+ distribution: launch.type,
123
+ };
124
+ }
125
+
126
+ export async function installRegisteredAgent(agentId, options = {}) {
127
+ const agent = await requireAgent(agentId, options);
128
+ const launch = resolveLaunch(agent, options);
129
+ if (launch.type === "binary") {
130
+ await installBinary(agent, launch, options);
131
+ } else {
132
+ const install = await agentInstallCommand(agent.id, options);
133
+ const result = await (options.runCommand || runCommand)(
134
+ install[0],
135
+ install[1],
136
+ {
137
+ ...options,
138
+ inherit: true,
139
+ },
140
+ );
141
+ if (result.code !== 0) {
142
+ throw new Error(
143
+ `Could not prepare ${agent.name}. ${String(result.output || "").trim()}`.trim(),
144
+ );
145
+ }
146
+ }
147
+ return inspectRegisteredAgent(agent.id, options);
148
+ }
149
+
150
+ export function parseAgentRegistry(value) {
151
+ if (!value || typeof value !== "object" || !Array.isArray(value.agents)) {
152
+ throw new Error(
153
+ "The ACP registry response does not contain an agent list.",
154
+ );
155
+ }
156
+ const agents = value.agents.map((agent, index) =>
157
+ validateAgent(agent, index),
158
+ );
159
+ return {
160
+ version: String(value.version || "unknown"),
161
+ agents,
162
+ };
163
+ }
164
+
165
+ export function currentPlatformKey(
166
+ platform = process.platform,
167
+ arch = process.arch,
168
+ ) {
169
+ const platformName =
170
+ platform === "win32"
171
+ ? "windows"
172
+ : platform === "darwin" || platform === "linux"
173
+ ? platform
174
+ : null;
175
+ const architecture =
176
+ arch === "arm64" ? "aarch64" : arch === "x64" ? "x86_64" : null;
177
+ if (!platformName || !architecture) {
178
+ throw new Error(
179
+ `ACP registry binaries do not support ${platform}/${arch}. Use --agent-command for a local ACP agent.`,
180
+ );
181
+ }
182
+ return `${platformName}-${architecture}`;
183
+ }
184
+
185
+ async function requireAgent(agentId, options) {
186
+ const agent = await registeredAgent(agentId, options);
187
+ if (agent) return agent;
188
+ throw new Error(
189
+ `Unknown coding agent '${agentId}'. Run \`engineeros-connector agents\` to see the current official ACP catalog.`,
190
+ );
191
+ }
192
+
193
+ async function loadRegistry(options) {
194
+ if (options.registry) return parseAgentRegistry(options.registry);
195
+ if (Object.keys(options).length === 0 && defaultRegistryPromise) {
196
+ return defaultRegistryPromise;
197
+ }
198
+ const loading = fetchRegistry(options);
199
+ if (Object.keys(options).length === 0) defaultRegistryPromise = loading;
200
+ try {
201
+ return await loading;
202
+ } catch (error) {
203
+ if (Object.keys(options).length === 0) defaultRegistryPromise = undefined;
204
+ throw error;
205
+ }
206
+ }
207
+
208
+ async function fetchRegistry(options) {
209
+ const cachePath =
210
+ options.cachePath ||
211
+ path.join(os.homedir(), ".engineeros", "cache", "acp-registry.json");
212
+ const cached = await readRegistryCache(cachePath);
213
+ if (cached && Date.now() - cached.fetched_at < CACHE_TTL_MS) {
214
+ return cached.registry;
215
+ }
216
+ try {
217
+ const fetcher = options.fetch || globalThis.fetch;
218
+ const response = await fetcher(options.registryUrl || ACP_REGISTRY_URL, {
219
+ signal: options.signal || AbortSignal.timeout(10_000),
220
+ });
221
+ if (!response.ok) {
222
+ throw new Error(`${response.status} ${response.statusText}`.trim());
223
+ }
224
+ const registry = parseAgentRegistry(await response.json());
225
+ await writeRegistryCache(cachePath, registry);
226
+ return registry;
227
+ } catch (error) {
228
+ if (cached) return cached.registry;
229
+ throw new Error(
230
+ `The official ACP agent catalog is unavailable. Check the network connection and retry. ${error instanceof Error ? error.message : String(error)}`,
231
+ );
232
+ }
233
+ }
234
+
235
+ async function readRegistryCache(cachePath) {
236
+ try {
237
+ const parsed = JSON.parse(await readFile(cachePath, "utf8"));
238
+ return {
239
+ fetched_at: Number(parsed.fetched_at || 0),
240
+ registry: parseAgentRegistry(parsed.registry),
241
+ };
242
+ } catch {
243
+ return null;
244
+ }
245
+ }
246
+
247
+ async function writeRegistryCache(cachePath, registry) {
248
+ try {
249
+ await mkdir(path.dirname(cachePath), { recursive: true });
250
+ await writeFile(
251
+ cachePath,
252
+ `${JSON.stringify({ fetched_at: Date.now(), registry }, null, 2)}\n`,
253
+ "utf8",
254
+ );
255
+ } catch {
256
+ // The live registry remains usable when a locked-down machine disallows caching.
257
+ }
258
+ }
259
+
260
+ function validateAgent(agent, index) {
261
+ if (!agent || typeof agent !== "object") {
262
+ throw new Error(`ACP registry agent ${index + 1} is invalid.`);
263
+ }
264
+ const id = String(agent.id || "").trim();
265
+ const name = String(agent.name || "").trim();
266
+ const version = String(agent.version || "").trim();
267
+ if (
268
+ !/^[a-z0-9][a-z0-9-]*$/.test(id) ||
269
+ !name ||
270
+ !version ||
271
+ /[\0\r\n]/.test(version)
272
+ ) {
273
+ throw new Error(
274
+ `ACP registry agent ${index + 1} has invalid identity fields.`,
275
+ );
276
+ }
277
+ if (!agent.distribution || typeof agent.distribution !== "object") {
278
+ throw new Error(`ACP registry agent '${id}' has no distribution.`);
279
+ }
280
+ return {
281
+ id,
282
+ name,
283
+ version,
284
+ description: String(agent.description || "").trim(),
285
+ repository: agent.repository ? String(agent.repository) : undefined,
286
+ website: agent.website ? String(agent.website) : undefined,
287
+ icon: agent.icon ? String(agent.icon) : undefined,
288
+ distribution: agent.distribution,
289
+ };
290
+ }
291
+
292
+ function distributionType(agent) {
293
+ if (agent.distribution.npx) return "npx";
294
+ if (agent.distribution.uvx) return "uvx";
295
+ if (agent.distribution.binary) return "binary";
296
+ return "unsupported";
297
+ }
298
+
299
+ function resolveLaunch(agent, options) {
300
+ if (agent.distribution.npx) {
301
+ const distribution = validatePackageDistribution(
302
+ agent,
303
+ "npx",
304
+ agent.distribution.npx,
305
+ );
306
+ return {
307
+ type: "npx",
308
+ package: distribution.package,
309
+ command: platformCommand("npx", options.platform),
310
+ args: ["--yes", distribution.package, ...(distribution.args || [])],
311
+ env: { ...(distribution.env || {}) },
312
+ };
313
+ }
314
+ if (agent.distribution.uvx) {
315
+ const distribution = validatePackageDistribution(
316
+ agent,
317
+ "uvx",
318
+ agent.distribution.uvx,
319
+ );
320
+ return {
321
+ type: "uvx",
322
+ package: distribution.package,
323
+ command: platformCommand("uvx", options.platform),
324
+ args: [distribution.package, ...(distribution.args || [])],
325
+ env: { ...(distribution.env || {}) },
326
+ };
327
+ }
328
+ const platformKey = currentPlatformKey(options.platform, options.arch);
329
+ const target = agent.distribution.binary?.[platformKey];
330
+ if (!target) {
331
+ throw new Error(
332
+ `${agent.name} does not publish an ACP binary for ${platformKey}. Use --agent-command if it is already installed another way.`,
333
+ );
334
+ }
335
+ const agentHome =
336
+ options.agentHome || path.join(os.homedir(), ".engineeros", "agents");
337
+ const installDir = path.join(
338
+ agentHome,
339
+ agent.id,
340
+ managedSegment(agent.version),
341
+ platformKey,
342
+ );
343
+ return {
344
+ type: "binary",
345
+ command: resolveManagedCommand(installDir, target.cmd),
346
+ args: stringArray(target.args, `${agent.id} binary args`),
347
+ env: stringRecord(target.env, `${agent.id} binary environment`),
348
+ archive: validatedArchiveUrl(target.archive, agent.name),
349
+ sha256: target.sha256 ? String(target.sha256).toLowerCase() : null,
350
+ installDir,
351
+ target,
352
+ };
353
+ }
354
+
355
+ function validatePackageDistribution(agent, type, distribution) {
356
+ const packageName = String(distribution.package || "").trim();
357
+ if (!packageName || !/^[a-zA-Z0-9@][a-zA-Z0-9@/._+=:-]*$/.test(packageName)) {
358
+ throw new Error(`${agent.name} has an invalid ${type} package.`);
359
+ }
360
+ return {
361
+ package: packageName,
362
+ args: stringArray(distribution.args, `${agent.id} ${type} args`),
363
+ env: stringRecord(distribution.env, `${agent.id} ${type} environment`),
364
+ };
365
+ }
366
+
367
+ function stringArray(value, label) {
368
+ if (value === undefined) return [];
369
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
370
+ throw new Error(`The ACP registry contains invalid ${label}.`);
371
+ }
372
+ return [...value];
373
+ }
374
+
375
+ function stringRecord(value, label) {
376
+ if (value === undefined) return {};
377
+ if (
378
+ !value ||
379
+ typeof value !== "object" ||
380
+ Array.isArray(value) ||
381
+ Object.values(value).some((item) => typeof item !== "string")
382
+ ) {
383
+ throw new Error(`The ACP registry contains an invalid ${label}.`);
384
+ }
385
+ return { ...value };
386
+ }
387
+
388
+ function compareAgents(left, right) {
389
+ const leftPriority = PRIORITY_AGENT_IDS.indexOf(left.id);
390
+ const rightPriority = PRIORITY_AGENT_IDS.indexOf(right.id);
391
+ if (leftPriority >= 0 || rightPriority >= 0) {
392
+ if (leftPriority < 0) return 1;
393
+ if (rightPriority < 0) return -1;
394
+ return leftPriority - rightPriority;
395
+ }
396
+ return left.name.localeCompare(right.name);
397
+ }
398
+
399
+ function normalizeLookup(value) {
400
+ return String(value || "")
401
+ .trim()
402
+ .toLowerCase();
403
+ }
404
+
405
+ function managedSegment(value) {
406
+ const segment = String(value).replace(/[^a-zA-Z0-9._+-]/g, "_");
407
+ return segment === "." || segment === ".." ? `_${segment}` : segment;
408
+ }
409
+
410
+ function validatedArchiveUrl(value, agentName) {
411
+ try {
412
+ const url = new URL(String(value || ""));
413
+ if (url.protocol !== "https:") throw new Error("not HTTPS");
414
+ return url.toString();
415
+ } catch {
416
+ throw new Error(
417
+ `${agentName} has an invalid binary download URL in the ACP registry.`,
418
+ );
419
+ }
420
+ }
421
+
422
+ function platformCommand(command, platform = process.platform) {
423
+ return platform === "win32" ? `${command}.cmd` : command;
424
+ }
425
+
426
+ async function commandAvailable(command, options) {
427
+ const key = `${options.platform || process.platform}:${command}`;
428
+ if (!options.runCommand && commandChecks.has(key))
429
+ return commandChecks.get(key);
430
+ const checking = (async () => {
431
+ const checker =
432
+ (options.platform || process.platform) === "win32"
433
+ ? "where.exe"
434
+ : "which";
435
+ const result = await (options.runCommand || runCommand)(
436
+ checker,
437
+ [command],
438
+ {
439
+ ...options,
440
+ inherit: false,
441
+ },
442
+ );
443
+ return result.code === 0;
444
+ })();
445
+ if (!options.runCommand) commandChecks.set(key, checking);
446
+ return checking;
447
+ }
448
+
449
+ async function installBinary(agent, launch, options) {
450
+ if (await fileExists(launch.command)) return;
451
+ if (!launch.archive) {
452
+ throw new Error(
453
+ `${agent.name} has no binary download URL in the ACP registry.`,
454
+ );
455
+ }
456
+ const parent = path.dirname(launch.installDir);
457
+ const stage = path.join(
458
+ parent,
459
+ `.${path.basename(launch.installDir)}-${randomUUID()}`,
460
+ );
461
+ const downloadPath = path.join(stage, archiveFilename(launch.archive));
462
+ await mkdir(stage, { recursive: true });
463
+ try {
464
+ const response = await (options.fetch || globalThis.fetch)(launch.archive, {
465
+ signal: options.signal || AbortSignal.timeout(120_000),
466
+ });
467
+ if (!response.ok || !response.body) {
468
+ throw new Error(
469
+ `Download failed (${response.status} ${response.statusText}).`.trim(),
470
+ );
471
+ }
472
+ await pipeline(
473
+ Readable.fromWeb(response.body),
474
+ createWriteStream(downloadPath),
475
+ );
476
+ if (launch.sha256) await verifySha256(downloadPath, launch.sha256);
477
+ const archiveKind = archiveType(downloadPath);
478
+ if (archiveKind === "binary") {
479
+ const stagedCommand = resolveManagedCommand(stage, launch.target.cmd);
480
+ await mkdir(path.dirname(stagedCommand), { recursive: true });
481
+ await rename(downloadPath, stagedCommand);
482
+ } else {
483
+ await extractArchive(downloadPath, stage, archiveKind, options);
484
+ await rm(downloadPath, { force: true });
485
+ }
486
+ const stagedCommand = resolveManagedCommand(stage, launch.target.cmd);
487
+ if (!(await fileExists(stagedCommand))) {
488
+ throw new Error(
489
+ `The ${agent.name} archive did not contain ${launch.target.cmd}.`,
490
+ );
491
+ }
492
+ if ((options.platform || process.platform) !== "win32") {
493
+ await chmod(stagedCommand, 0o755);
494
+ }
495
+ await mkdir(parent, { recursive: true });
496
+ await rm(launch.installDir, { recursive: true, force: true });
497
+ await rename(stage, launch.installDir);
498
+ } catch (error) {
499
+ await rm(stage, { recursive: true, force: true });
500
+ throw new Error(
501
+ `Could not install ${agent.name}: ${error instanceof Error ? error.message : String(error)}`,
502
+ );
503
+ }
504
+ }
505
+
506
+ function archiveFilename(url) {
507
+ try {
508
+ const filename = path
509
+ .basename(new URL(url).pathname)
510
+ .replace(/[^a-zA-Z0-9._+-]/g, "_");
511
+ return filename && filename !== "." && filename !== ".."
512
+ ? filename
513
+ : "agent-binary";
514
+ } catch {
515
+ return "agent-binary";
516
+ }
517
+ }
518
+
519
+ function archiveType(filename) {
520
+ const lower = filename.toLowerCase();
521
+ if (lower.endsWith(".zip")) return "zip";
522
+ if (
523
+ lower.endsWith(".tar.gz") ||
524
+ lower.endsWith(".tgz") ||
525
+ lower.endsWith(".tar.bz2") ||
526
+ lower.endsWith(".tbz2")
527
+ ) {
528
+ return "tar";
529
+ }
530
+ return "binary";
531
+ }
532
+
533
+ async function extractArchive(archive, destination, kind, options) {
534
+ const platform = options.platform || process.platform;
535
+ const command = kind === "zip" && platform === "linux" ? "unzip" : "tar";
536
+ const listArgs =
537
+ kind === "zip" && platform === "linux"
538
+ ? ["-Z1", archive]
539
+ : ["-tf", archive];
540
+ const args =
541
+ kind === "zip" && platform === "linux"
542
+ ? ["-q", archive, "-d", destination]
543
+ : ["-xf", archive, "-C", destination];
544
+ const runner = options.runCommand || runCommand;
545
+ const listing = await runner(command, listArgs, {
546
+ ...options,
547
+ inherit: false,
548
+ });
549
+ if (listing.code !== 0) {
550
+ throw new Error(
551
+ `Archive inspection failed. Install '${command}' and retry. ${String(listing.output || "").trim()}`.trim(),
552
+ );
553
+ }
554
+ validateArchiveEntries(listing.output);
555
+ const result = await runner(command, args, {
556
+ ...options,
557
+ inherit: true,
558
+ });
559
+ if (result.code !== 0) {
560
+ throw new Error(
561
+ `Archive extraction failed. Install '${command}' and retry. ${String(result.output || "").trim()}`.trim(),
562
+ );
563
+ }
564
+ }
565
+
566
+ function validateArchiveEntries(output) {
567
+ for (const entry of String(output || "").split(/\r?\n/)) {
568
+ const normalized = entry.trim().replace(/\\/g, "/");
569
+ if (!normalized) continue;
570
+ const segments = normalized.split("/");
571
+ if (
572
+ normalized.startsWith("/") ||
573
+ /^[a-zA-Z]:/.test(normalized) ||
574
+ segments.includes("..")
575
+ ) {
576
+ throw new Error(`The agent archive contains an unsafe path: ${entry}.`);
577
+ }
578
+ }
579
+ }
580
+
581
+ function resolveManagedCommand(root, command) {
582
+ const relative = String(command || "")
583
+ .replace(/^[.][\\/]/, "")
584
+ .replace(/[\\/]+/g, path.sep);
585
+ const resolvedRoot = path.resolve(root);
586
+ const resolved = path.resolve(resolvedRoot, relative);
587
+ if (
588
+ !relative ||
589
+ (resolved !== resolvedRoot &&
590
+ !resolved.startsWith(`${resolvedRoot}${path.sep}`))
591
+ ) {
592
+ throw new Error("The ACP registry contains an unsafe binary command path.");
593
+ }
594
+ return resolved;
595
+ }
596
+
597
+ async function verifySha256(filename, expected) {
598
+ if (!/^[a-f0-9]{64}$/.test(expected)) {
599
+ throw new Error("The ACP registry contains an invalid SHA-256 checksum.");
600
+ }
601
+ const hash = createHash("sha256");
602
+ for await (const chunk of createReadStream(filename)) hash.update(chunk);
603
+ const actual = hash.digest("hex");
604
+ if (actual !== expected) {
605
+ throw new Error(
606
+ `Binary checksum mismatch (expected ${expected}, received ${actual}).`,
607
+ );
608
+ }
609
+ }
610
+
611
+ async function fileExists(filename) {
612
+ try {
613
+ await access(filename);
614
+ const details = await stat(filename);
615
+ return details.isFile();
616
+ } catch {
617
+ return false;
618
+ }
619
+ }
620
+
621
+ function runCommand(command, args, options = {}) {
622
+ return new Promise((resolve) => {
623
+ let output = "";
624
+ const child = spawn(command, args, {
625
+ cwd: options.cwd ?? process.cwd(),
626
+ env: { ...process.env, ...(options.env || {}) },
627
+ shell:
628
+ (options.platform || process.platform) === "win32" &&
629
+ /\.(cmd|bat)$/i.test(command),
630
+ stdio: options.inherit ? "inherit" : ["ignore", "pipe", "pipe"],
631
+ });
632
+ if (!options.inherit) {
633
+ child.stdout.setEncoding("utf8");
634
+ child.stderr.setEncoding("utf8");
635
+ child.stdout.on("data", (chunk) => (output += chunk));
636
+ child.stderr.on("data", (chunk) => (output += chunk));
637
+ }
638
+ child.once("error", (error) =>
639
+ resolve({ code: -1, output: error.message }),
640
+ );
641
+ child.once("close", (code) => resolve({ code: code ?? -1, output }));
642
+ });
643
+ }
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { agentHarnessCapabilities } from "./agent-harness.mjs";
2
3
 
3
4
  export function advertisedCapabilities(config, codingAgent) {
4
5
  const configuredModels = String(
@@ -15,6 +16,7 @@ export function advertisedCapabilities(config, codingAgent) {
15
16
  workspace_name: path.basename(config.workspace),
16
17
  agent_name: codingAgent.name,
17
18
  agent_version: codingAgent.version,
19
+ ...agentHarnessCapabilities(),
18
20
  execution_profiles: {
19
21
  model_selection: codingAgent.protocol === "codex",
20
22
  models: codingAgent.protocol === "codex" ? configuredModels : [],
package/src/config.mjs CHANGED
@@ -46,11 +46,18 @@ export function mergePairedConfig(
46
46
  ) {
47
47
  const preserved =
48
48
  existingConfig?.connector_id === connectorId ? existingConfig : {};
49
+ const sameAgent =
50
+ preserved.agent_protocol === requestedConfig.agent_protocol &&
51
+ (preserved.agent_id || preserved.agent_command || null) ===
52
+ (requestedConfig.agent_id || requestedConfig.agent_command || null) &&
53
+ (preserved.agent_version || null) ===
54
+ (requestedConfig.agent_version || null);
49
55
  return {
50
56
  ...preserved,
51
57
  ...requestedConfig,
52
58
  connector_id: connectorId,
53
59
  token,
60
+ sessions: sameAgent ? preserved.sessions || {} : {},
54
61
  };
55
62
  }
56
63