@creatornader/agent-bridge 0.7.2 → 0.7.4

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,2044 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ readClientConfigFile,
4
+ resolveClientConfig
5
+ } from "./chunk-UKFRCP6X.js";
6
+ import {
7
+ securePrivatePath,
8
+ verifyPrivatePathAccess
9
+ } from "./chunk-GTK4KOED.js";
10
+
11
+ // src/client-installer.ts
12
+ import {
13
+ accessSync,
14
+ closeSync as closeSync2,
15
+ constants as constants2,
16
+ existsSync as existsSync2,
17
+ fsyncSync as fsyncSync2,
18
+ mkdirSync as mkdirSync2,
19
+ openSync as openSync2,
20
+ readFileSync as readFileSync2,
21
+ renameSync as renameSync2,
22
+ rmSync as rmSync2,
23
+ statSync,
24
+ writeFileSync as writeFileSync2
25
+ } from "fs";
26
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
27
+ import { homedir as homedir2 } from "os";
28
+ import { delimiter, dirname as dirname2, extname, isAbsolute, join as join2, resolve as resolve2 } from "path";
29
+ import { spawnSync } from "child_process";
30
+ import { fileURLToPath } from "url";
31
+
32
+ // src/enrollment-file.ts
33
+ import {
34
+ closeSync,
35
+ constants,
36
+ existsSync,
37
+ fstatSync,
38
+ fsyncSync,
39
+ lstatSync,
40
+ mkdirSync,
41
+ openSync,
42
+ readFileSync,
43
+ realpathSync,
44
+ renameSync,
45
+ rmSync,
46
+ writeFileSync
47
+ } from "fs";
48
+ import { createHash, randomUUID } from "crypto";
49
+ import { homedir, hostname } from "os";
50
+ import { dirname, join, parse, relative, resolve, sep } from "path";
51
+ var ENROLLMENT_SCHEMA = "agent-bridge.enrollment";
52
+ var ENROLLMENT_VERSION = 1;
53
+ function object(value, label) {
54
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
55
+ throw new Error(label + " must be an object");
56
+ }
57
+ return value;
58
+ }
59
+ function text(value, label, maximum = 128) {
60
+ if (typeof value !== "string" || !value || value !== value.trim() || value.length > maximum || /[\u0000-\u001f\u007f]/.test(value)) {
61
+ throw new Error(label + " is invalid");
62
+ }
63
+ return value;
64
+ }
65
+ function nullableText(value, label) {
66
+ return value === null ? null : text(value, label);
67
+ }
68
+ var RFC3339 = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?(Z|([+-])(\d{2}):(\d{2}))$/;
69
+ function canonicalTimestamp(value, label) {
70
+ if (value === null) return null;
71
+ const selected = text(value, label, 64);
72
+ const match = RFC3339.exec(selected);
73
+ if (!match) throw new Error(label + " must be RFC3339 with a timezone");
74
+ const [
75
+ ,
76
+ yearText,
77
+ monthText,
78
+ dayText,
79
+ hourText,
80
+ minuteText,
81
+ secondText,
82
+ fraction = "",
83
+ zone,
84
+ sign,
85
+ offsetHourText = "0",
86
+ offsetMinuteText = "0"
87
+ ] = match;
88
+ const year = Number(yearText);
89
+ const month = Number(monthText);
90
+ const day = Number(dayText);
91
+ const hour = Number(hourText);
92
+ const minute = Number(minuteText);
93
+ const second = Number(secondText);
94
+ const millisecond = Number(fraction.padEnd(3, "0"));
95
+ const offsetHour = Number(offsetHourText);
96
+ const offsetMinute = Number(offsetMinuteText);
97
+ if (hour > 23 || minute > 59 || second > 59 || offsetHour > 23 || offsetMinute > 59) {
98
+ throw new Error(label + " is invalid");
99
+ }
100
+ const wall = /* @__PURE__ */ new Date(0);
101
+ wall.setUTCFullYear(year, month - 1, day);
102
+ wall.setUTCHours(hour, minute, second, millisecond);
103
+ if (wall.getUTCFullYear() !== year || wall.getUTCMonth() !== month - 1 || wall.getUTCDate() !== day || wall.getUTCHours() !== hour || wall.getUTCMinutes() !== minute || wall.getUTCSeconds() !== second || wall.getUTCMilliseconds() !== millisecond) {
104
+ throw new Error(label + " is invalid");
105
+ }
106
+ const direction = zone === "Z" ? 0 : sign === "+" ? 1 : -1;
107
+ const instant = new Date(wall.getTime() - direction * (offsetHour * 60 + offsetMinute) * 6e4);
108
+ if (!Number.isFinite(instant.getTime())) throw new Error(label + " is invalid");
109
+ return instant.toISOString();
110
+ }
111
+ function canonicalGatewayUrl(value, label) {
112
+ const selected = text(value, label, 2048);
113
+ let parsed;
114
+ try {
115
+ parsed = new URL(selected);
116
+ } catch {
117
+ throw new Error(label + " is invalid");
118
+ }
119
+ if (parsed.username || parsed.password) throw new Error(label + " cannot contain credentials");
120
+ if (parsed.hash) throw new Error(label + " cannot contain a fragment");
121
+ const loopback = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]";
122
+ if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && loopback)) {
123
+ throw new Error(label + " must use HTTPS, or HTTP on an explicit loopback host");
124
+ }
125
+ return parsed.toString();
126
+ }
127
+ function uuid(value, label) {
128
+ const selected = text(value, label, 36);
129
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(selected)) {
130
+ throw new Error(label + " is invalid");
131
+ }
132
+ return selected;
133
+ }
134
+ function runtime(value) {
135
+ if (value !== "codex" && value !== "claude-code" && value !== "claude-desktop") {
136
+ throw new Error("enrollment runtime is invalid");
137
+ }
138
+ return value;
139
+ }
140
+ function validateInput(value, operation) {
141
+ const input = object(value, "enrollment input");
142
+ const gatewayUrl = canonicalGatewayUrl(input.gatewayUrl, "enrollment gatewayUrl");
143
+ const credentialId = input.credentialId === null ? null : uuid(input.credentialId, "enrollment credentialId");
144
+ const workspaceName = nullableText(input.workspaceName, "enrollment workspaceName");
145
+ const graceUntil = canonicalTimestamp(input.graceUntil, "enrollment graceUntil");
146
+ const invalidateImmediately = input.invalidateImmediately;
147
+ if (typeof invalidateImmediately !== "boolean") throw new Error("enrollment invalidateImmediately is invalid");
148
+ if (operation === "provision" && (credentialId !== null || workspaceName === null || graceUntil !== null || invalidateImmediately)) {
149
+ throw new Error("provision enrollment input is invalid");
150
+ }
151
+ if (operation === "rotate" && (credentialId === null || workspaceName !== null || graceUntil === null === !invalidateImmediately)) {
152
+ throw new Error("rotation must choose graceUntil or invalidateImmediately");
153
+ }
154
+ return {
155
+ gatewayUrl,
156
+ workspaceId: text(input.workspaceId, "enrollment workspaceId"),
157
+ principal: text(input.principal, "enrollment principal"),
158
+ runtime: runtime(input.runtime),
159
+ instance: text(input.instance, "enrollment instance"),
160
+ credentialId,
161
+ workspaceName,
162
+ displayName: nullableText(input.displayName, "enrollment displayName"),
163
+ runtimeType: text(input.runtimeType, "enrollment runtimeType"),
164
+ label: nullableText(input.label, "enrollment label"),
165
+ scopeSetName: text(input.scopeSetName, "enrollment scopeSetName"),
166
+ expiresAt: canonicalTimestamp(input.expiresAt, "enrollment expiresAt"),
167
+ graceUntil,
168
+ invalidateImmediately
169
+ };
170
+ }
171
+ function validateResult(value, input) {
172
+ if (value === null) return null;
173
+ const result = object(value, "enrollment result");
174
+ if (typeof result.replayed !== "boolean") throw new Error("enrollment result replayed is invalid");
175
+ const validated = {
176
+ workspaceId: text(result.workspaceId, "enrollment result workspaceId"),
177
+ principal: text(result.principal, "enrollment result principal"),
178
+ agentId: result.agentId === null ? null : uuid(result.agentId, "enrollment result agentId"),
179
+ credentialId: uuid(result.credentialId, "enrollment result credentialId"),
180
+ replayed: result.replayed
181
+ };
182
+ if (validated.workspaceId !== input.workspaceId || validated.principal !== input.principal) {
183
+ throw new Error("enrollment result does not match its workspace and principal");
184
+ }
185
+ return validated;
186
+ }
187
+ function enrollmentTokenHash(enrollment) {
188
+ if (!enrollment.token) throw new Error("enrollment no longer contains a credential");
189
+ return createHash("sha256").update(enrollment.token, "utf8").digest("hex");
190
+ }
191
+ function validateEnrollmentFile(value) {
192
+ const file = object(value, "enrollment file");
193
+ if (file.schema !== ENROLLMENT_SCHEMA || file.version !== ENROLLMENT_VERSION) {
194
+ throw new Error("unsupported enrollment schema or version");
195
+ }
196
+ if (file.provider !== "gateway") throw new Error("enrollment provider must be gateway");
197
+ if (!["pending", "ready", "consuming", "consumed"].includes(String(file.state))) {
198
+ throw new Error("enrollment state is invalid");
199
+ }
200
+ if (file.operation !== "provision" && file.operation !== "rotate") {
201
+ throw new Error("enrollment operation is invalid");
202
+ }
203
+ const state = file.state;
204
+ if (!Number.isSafeInteger(file.revision) || Number(file.revision) < 0) {
205
+ throw new Error("enrollment revision is invalid");
206
+ }
207
+ const input = validateInput(file.input, file.operation);
208
+ const token = file.token === null ? null : text(file.token, "enrollment token", 4096);
209
+ const result = validateResult(file.result, input);
210
+ const completedAt = canonicalTimestamp(file.completedAt, "enrollment completedAt");
211
+ if (state === "pending" && (result !== null || completedAt !== null || token === null)) {
212
+ throw new Error("pending enrollment state is invalid");
213
+ }
214
+ if ((state === "ready" || state === "consuming") && (result === null || completedAt === null || token === null)) {
215
+ throw new Error(state + " enrollment state is invalid");
216
+ }
217
+ if (state === "consumed" && (result === null || completedAt === null || token !== null)) {
218
+ throw new Error("consumed enrollment state is invalid");
219
+ }
220
+ return {
221
+ schema: ENROLLMENT_SCHEMA,
222
+ version: ENROLLMENT_VERSION,
223
+ provider: "gateway",
224
+ revision: Number(file.revision),
225
+ state,
226
+ operation: file.operation,
227
+ requestId: uuid(file.requestId, "enrollment requestId"),
228
+ createdAt: (() => {
229
+ const createdAt = canonicalTimestamp(file.createdAt, "enrollment createdAt");
230
+ if (createdAt === null) throw new Error("enrollment createdAt is invalid");
231
+ return createdAt;
232
+ })(),
233
+ completedAt,
234
+ input,
235
+ token,
236
+ result
237
+ };
238
+ }
239
+ function syncDirectory(path) {
240
+ if (process.platform === "win32") return;
241
+ const descriptor = openSync(path, "r");
242
+ try {
243
+ fsyncSync(descriptor);
244
+ } finally {
245
+ closeSync(descriptor);
246
+ }
247
+ }
248
+ var durabilityDefaults = { syncDirectory };
249
+ function prepareDirectory(path) {
250
+ mkdirSync(path, { recursive: true, mode: 448 });
251
+ securePrivatePath(path, "directory");
252
+ }
253
+ function enrollmentRoot(env = process.env) {
254
+ if (env.AGENT_BRIDGE_ENROLLMENT_DIR) return resolve(env.AGENT_BRIDGE_ENROLLMENT_DIR);
255
+ const home = realpathSync(env.HOME ?? homedir());
256
+ return join(home, ".agent-bridge", "enrollments");
257
+ }
258
+ function rejectSymlinkComponents(path) {
259
+ const root = parse(path).root;
260
+ let current = root;
261
+ for (const component of path.slice(root.length).split(sep).filter(Boolean)) {
262
+ current = join(current, component);
263
+ if (!existsSync(current)) break;
264
+ if (lstatSync(current).isSymbolicLink()) {
265
+ throw new Error("enrollment paths cannot contain symbolic links");
266
+ }
267
+ }
268
+ }
269
+ function verifyPrivateDirectory(path) {
270
+ rejectSymlinkComponents(path);
271
+ const details = lstatSync(path);
272
+ if (!details.isDirectory() || details.isSymbolicLink()) {
273
+ throw new Error("enrollment directory must be a real directory");
274
+ }
275
+ verifyPrivatePathAccess(path, "directory");
276
+ return { device: details.dev, inode: details.ino };
277
+ }
278
+ function sameDirectory(path, expected) {
279
+ const actual = verifyPrivateDirectory(path);
280
+ if (actual.device !== expected.device || actual.inode !== expected.inode) {
281
+ throw new Error("enrollment directory changed during the operation");
282
+ }
283
+ }
284
+ function verifyPrivatePath(root, directory) {
285
+ verifyPrivateDirectory(root);
286
+ const inside = relative(root, directory);
287
+ if (!inside) return;
288
+ let current = root;
289
+ for (const component of inside.split(sep).filter(Boolean)) {
290
+ current = join(current, component);
291
+ verifyPrivateDirectory(current);
292
+ }
293
+ }
294
+ function prepareEnrollmentRoot(env) {
295
+ const root = enrollmentRoot(env);
296
+ rejectSymlinkComponents(dirname(root));
297
+ prepareDirectory(root);
298
+ verifyPrivateDirectory(root);
299
+ return root;
300
+ }
301
+ function assertEnrollmentPath(path, env = process.env) {
302
+ const root = enrollmentRoot(env);
303
+ const selected = resolve(path);
304
+ const inside = relative(root, selected);
305
+ if (!inside || inside.startsWith(".." + sep) || inside === ".." || inside.includes(sep + ".." + sep)) {
306
+ throw new Error("enrollment file must be inside the configured enrollment directory");
307
+ }
308
+ rejectSymlinkComponents(root);
309
+ rejectSymlinkComponents(dirname(selected));
310
+ if (existsSync(root) && existsSync(dirname(selected))) {
311
+ verifyPrivatePath(root, dirname(selected));
312
+ }
313
+ return selected;
314
+ }
315
+ function serialized(value) {
316
+ return JSON.stringify(validateEnrollmentFile(value), null, 2) + "\n";
317
+ }
318
+ function defaultEnrollmentPath(requestId, env = process.env) {
319
+ return join(enrollmentRoot(env), requestId + ".json");
320
+ }
321
+ function createPendingEnrollment(path, enrollment, env = process.env) {
322
+ if (enrollment.state !== "pending") throw new Error("new enrollment must be pending");
323
+ if (enrollment.revision !== 0) throw new Error("new enrollment revision must be zero");
324
+ path = assertEnrollmentPath(path, env);
325
+ prepareEnrollmentRoot(env);
326
+ path = assertEnrollmentPath(path, env);
327
+ const directory = dirname(path);
328
+ prepareDirectory(directory);
329
+ verifyPrivatePath(enrollmentRoot(env), directory);
330
+ const directoryIdentity = verifyPrivateDirectory(directory);
331
+ const descriptor = openSync(path, "wx", 384);
332
+ try {
333
+ writeFileSync(descriptor, serialized(enrollment), "utf8");
334
+ fsyncSync(descriptor);
335
+ closeSync(descriptor);
336
+ securePrivatePath(path, "file");
337
+ sameDirectory(directory, directoryIdentity);
338
+ syncDirectory(directory);
339
+ } catch (error) {
340
+ try {
341
+ closeSync(descriptor);
342
+ } catch {
343
+ }
344
+ rmSync(path, { force: true });
345
+ throw error;
346
+ }
347
+ }
348
+ function readEnrollmentUnlocked(path, env) {
349
+ path = assertEnrollmentPath(path, env);
350
+ const directory = dirname(path);
351
+ verifyPrivatePath(enrollmentRoot(env), directory);
352
+ const directoryIdentity = verifyPrivateDirectory(directory);
353
+ verifyPrivatePathAccess(path, "file");
354
+ const descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
355
+ try {
356
+ const details = fstatSync(descriptor);
357
+ if (!details.isFile()) throw new Error("enrollment path must be a regular file, not a symbolic link");
358
+ if (process.platform !== "win32") {
359
+ if ((details.mode & 63) !== 0) throw new Error("enrollment file permissions must be 0600");
360
+ if (typeof process.getuid === "function" && details.uid !== process.getuid()) {
361
+ throw new Error("enrollment file must be owned by the current user");
362
+ }
363
+ }
364
+ let parsed;
365
+ try {
366
+ parsed = JSON.parse(readFileSync(descriptor, "utf8"));
367
+ } catch {
368
+ throw new Error("enrollment file is not valid JSON");
369
+ }
370
+ sameDirectory(directory, directoryIdentity);
371
+ return validateEnrollmentFile(parsed);
372
+ } finally {
373
+ closeSync(descriptor);
374
+ }
375
+ }
376
+ function readEnrollment(path, env = process.env) {
377
+ return readEnrollmentUnlocked(path, env);
378
+ }
379
+ function assertActiveLock(lock, enrollmentPath) {
380
+ if (lock.released || lock.enrollmentPath !== enrollmentPath) {
381
+ throw new Error("enrollment lock does not cover this file");
382
+ }
383
+ const open = fstatSync(lock.descriptor);
384
+ const current = lstatSync(lock.lockPath);
385
+ if (open.dev !== lock.device || open.ino !== lock.inode || current.dev !== lock.device || current.ino !== lock.inode || current.isSymbolicLink()) {
386
+ throw new Error("enrollment lock changed during the operation");
387
+ }
388
+ }
389
+ function acquireEnrollmentLock(path, env = process.env) {
390
+ prepareEnrollmentRoot(env);
391
+ path = assertEnrollmentPath(path, env);
392
+ const directory = dirname(path);
393
+ prepareDirectory(directory);
394
+ verifyPrivatePath(enrollmentRoot(env), directory);
395
+ const directoryIdentity = verifyPrivateDirectory(directory);
396
+ const lockPath = path + ".lock";
397
+ const descriptor = openSync(lockPath, "wx", 384);
398
+ const nonce = randomUUID();
399
+ try {
400
+ writeFileSync(descriptor, JSON.stringify({
401
+ schema: "agent-bridge.enrollment-lock",
402
+ version: 1,
403
+ enrollmentPath: path,
404
+ pid: process.pid,
405
+ host: hostname(),
406
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
407
+ nonce
408
+ }) + "\n", "utf8");
409
+ fsyncSync(descriptor);
410
+ securePrivatePath(lockPath, "file");
411
+ sameDirectory(directory, directoryIdentity);
412
+ syncDirectory(directory);
413
+ const details = fstatSync(descriptor);
414
+ return {
415
+ enrollmentPath: path,
416
+ lockPath,
417
+ descriptor,
418
+ device: details.dev,
419
+ inode: details.ino,
420
+ nonce,
421
+ released: false
422
+ };
423
+ } catch (error) {
424
+ try {
425
+ closeSync(descriptor);
426
+ } catch {
427
+ }
428
+ rmSync(lockPath, { force: true });
429
+ throw error;
430
+ }
431
+ }
432
+ function releaseEnrollmentLock(lock, dependencies = durabilityDefaults) {
433
+ assertActiveLock(lock, lock.enrollmentPath);
434
+ const directory = dirname(lock.lockPath);
435
+ const directoryIdentity = verifyPrivateDirectory(directory);
436
+ closeSync(lock.descriptor);
437
+ const current = lstatSync(lock.lockPath);
438
+ if (current.dev !== lock.device || current.ino !== lock.inode) {
439
+ throw new Error("enrollment lock changed before release");
440
+ }
441
+ rmSync(lock.lockPath);
442
+ lock.released = true;
443
+ try {
444
+ sameDirectory(directory, directoryIdentity);
445
+ dependencies.syncDirectory(directory);
446
+ } catch {
447
+ return "durability-unknown";
448
+ }
449
+ return "released";
450
+ }
451
+ function recoverEnrollmentLock(path, env = process.env, now = Date.now()) {
452
+ path = assertEnrollmentPath(path, env);
453
+ const lockPath = path + ".lock";
454
+ verifyPrivatePathAccess(lockPath, "file");
455
+ const descriptor = openSync(lockPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
456
+ try {
457
+ const details = fstatSync(descriptor);
458
+ if (!details.isFile()) throw new Error("enrollment lock is not a regular file");
459
+ if (process.platform !== "win32") {
460
+ if ((details.mode & 63) !== 0) throw new Error("enrollment lock permissions must be 0600");
461
+ if (typeof process.getuid === "function" && details.uid !== process.getuid()) {
462
+ throw new Error("enrollment lock must be owned by the current user");
463
+ }
464
+ }
465
+ let metadata;
466
+ try {
467
+ metadata = object(JSON.parse(readFileSync(descriptor, "utf8")), "enrollment lock");
468
+ } catch {
469
+ throw new Error("enrollment lock metadata is invalid");
470
+ }
471
+ if (metadata.schema !== "agent-bridge.enrollment-lock" || metadata.version !== 1 || metadata.enrollmentPath !== path || metadata.host !== hostname() || typeof metadata.pid !== "number" || !Number.isSafeInteger(metadata.pid) || typeof metadata.createdAt !== "string" || !RFC3339.test(metadata.createdAt)) {
472
+ throw new Error("enrollment lock metadata is invalid");
473
+ }
474
+ const age = now - new Date(metadata.createdAt).getTime();
475
+ if (age < 6e4) throw new Error("enrollment lock is too recent to recover");
476
+ try {
477
+ process.kill(metadata.pid, 0);
478
+ throw new Error("enrollment lock owner is still running");
479
+ } catch (error) {
480
+ if (error instanceof Error && error.message === "enrollment lock owner is still running") throw error;
481
+ if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ESRCH") {
482
+ throw new Error("cannot prove that the enrollment lock owner has stopped");
483
+ }
484
+ }
485
+ const current = lstatSync(lockPath);
486
+ if (current.dev !== details.dev || current.ino !== details.ino) {
487
+ throw new Error("enrollment lock changed during recovery");
488
+ }
489
+ } finally {
490
+ closeSync(descriptor);
491
+ }
492
+ rmSync(lockPath);
493
+ syncDirectory(dirname(lockPath));
494
+ }
495
+ function deleteEnrollmentFile(path, lock, env = process.env, dependencies = durabilityDefaults) {
496
+ path = assertEnrollmentPath(path, env);
497
+ assertActiveLock(lock, path);
498
+ let current;
499
+ try {
500
+ current = readEnrollmentUnlocked(path, env);
501
+ } catch (error) {
502
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
503
+ return "missing";
504
+ }
505
+ throw error;
506
+ }
507
+ if (current.state !== "consumed" || current.token !== null) {
508
+ throw new Error("only a consumed enrollment can be deleted");
509
+ }
510
+ const directory = dirname(path);
511
+ const directoryIdentity = verifyPrivateDirectory(directory);
512
+ try {
513
+ rmSync(path);
514
+ } catch (error) {
515
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
516
+ return "missing";
517
+ }
518
+ throw error;
519
+ }
520
+ try {
521
+ sameDirectory(directory, directoryIdentity);
522
+ dependencies.syncDirectory(directory);
523
+ } catch {
524
+ return "deleted-durability-unknown";
525
+ }
526
+ return "deleted-and-durable";
527
+ }
528
+ function transitionEnrollment(path, current, state, changes = {}, env = process.env, heldLock) {
529
+ path = assertEnrollmentPath(path, env);
530
+ const lock = heldLock ?? acquireEnrollmentLock(path, env);
531
+ try {
532
+ assertActiveLock(lock, path);
533
+ const onDisk = readEnrollmentUnlocked(path, env);
534
+ if (onDisk.revision !== current.revision || onDisk.state !== current.state || onDisk.requestId !== current.requestId || onDisk.operation !== current.operation || onDisk.token !== current.token) {
535
+ throw new Error("stale enrollment transition refused");
536
+ }
537
+ const transition = current.state + "->" + state;
538
+ if (!(/* @__PURE__ */ new Set([
539
+ "pending->ready",
540
+ "ready->consuming",
541
+ "consuming->ready",
542
+ "consuming->consumed"
543
+ ])).has(transition)) {
544
+ throw new Error("illegal enrollment state transition: " + transition);
545
+ }
546
+ const next = validateEnrollmentFile({
547
+ ...onDisk,
548
+ ...changes,
549
+ state,
550
+ revision: onDisk.revision + 1
551
+ });
552
+ const directory = dirname(path);
553
+ const directoryIdentity = verifyPrivateDirectory(directory);
554
+ const temporary = path + "." + process.pid + "." + randomUUID() + ".tmp";
555
+ const descriptor = openSync(temporary, "wx", 384);
556
+ try {
557
+ writeFileSync(descriptor, serialized(next), "utf8");
558
+ fsyncSync(descriptor);
559
+ closeSync(descriptor);
560
+ securePrivatePath(temporary, "file");
561
+ sameDirectory(directory, directoryIdentity);
562
+ renameSync(temporary, path);
563
+ verifyPrivatePathAccess(path, "file");
564
+ sameDirectory(directory, directoryIdentity);
565
+ syncDirectory(directory);
566
+ return next;
567
+ } catch (error) {
568
+ try {
569
+ closeSync(descriptor);
570
+ } catch {
571
+ }
572
+ rmSync(temporary, { force: true });
573
+ throw error;
574
+ }
575
+ } finally {
576
+ if (!heldLock) releaseEnrollmentLock(lock);
577
+ }
578
+ }
579
+
580
+ // src/client-installer.ts
581
+ function assertExecutable(path) {
582
+ if (!existsSync2(path)) {
583
+ throw new Error(`Claude Desktop MCP executable does not exist: ${path}`);
584
+ }
585
+ if (!statSync(path).isFile()) {
586
+ throw new Error(`Claude Desktop MCP executable is not a file: ${path}`);
587
+ }
588
+ try {
589
+ accessSync(path, process.platform === "win32" ? constants2.F_OK : constants2.X_OK);
590
+ } catch {
591
+ throw new Error(`Claude Desktop MCP executable is not executable: ${path}`);
592
+ }
593
+ return path;
594
+ }
595
+ function resolveExecutable(command, env) {
596
+ if (isAbsolute(command) || command.includes("/") || command.includes("\\")) {
597
+ return assertExecutable(resolve2(command));
598
+ }
599
+ const searchPath = env.PATH ?? process.env.PATH ?? "";
600
+ const extensions = process.platform === "win32" && !extname(command) ? (env.PATHEXT ?? process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
601
+ for (const directory of searchPath.split(delimiter).filter(Boolean)) {
602
+ for (const extension of extensions) {
603
+ const candidate = join2(directory, command + extension);
604
+ if (!existsSync2(candidate)) continue;
605
+ return assertExecutable(resolve2(candidate));
606
+ }
607
+ }
608
+ throw new Error(`Claude Desktop MCP executable was not found on PATH: ${command}`);
609
+ }
610
+ function defaultServerEntry() {
611
+ const moduleDirectory = dirname2(fileURLToPath(import.meta.url));
612
+ const candidates = [
613
+ join2(moduleDirectory, "index.js"),
614
+ join2(moduleDirectory, "..", "dist", "index.js")
615
+ ];
616
+ const entry = candidates.find((candidate) => existsSync2(candidate) && statSync(candidate).isFile());
617
+ if (!entry) {
618
+ throw new Error(`Claude Desktop MCP server entry does not exist: ${candidates[0]}`);
619
+ }
620
+ return resolve2(entry);
621
+ }
622
+ function resolveDesktopLaunchContract(requestedCommand, env) {
623
+ const command = requestedCommand?.trim();
624
+ if (command) return { command: resolveExecutable(command, env), args: [] };
625
+ return {
626
+ command: assertExecutable(resolve2(process.execPath)),
627
+ args: [defaultServerEntry()]
628
+ };
629
+ }
630
+ function desktopConfigPath(env) {
631
+ if (process.platform === "win32") {
632
+ const appData = env.APPDATA;
633
+ if (!appData) throw new Error("APPDATA is required for Claude Desktop installation");
634
+ return join2(appData, "Claude", "claude_desktop_config.json");
635
+ }
636
+ const home = env.HOME ?? homedir2();
637
+ return process.platform === "darwin" ? join2(home, "Library", "Application Support", "Claude", "claude_desktop_config.json") : join2(home, ".config", "Claude", "claude_desktop_config.json");
638
+ }
639
+ function safeComponent(value) {
640
+ const readable = value.replace(/[^A-Za-z0-9._-]+/g, "_").slice(0, 48) || "client";
641
+ const suffix = createHash2("sha256").update(value).digest("hex").slice(0, 12);
642
+ return `${readable}-${suffix}`;
643
+ }
644
+ function clientBackendConfigPath(runtime2, instance, env) {
645
+ const home = env.HOME ?? homedir2();
646
+ return join2(
647
+ home,
648
+ ".agent-bridge",
649
+ "clients",
650
+ `${runtime2}-${safeComponent(instance)}.config`
651
+ );
652
+ }
653
+ function replacePrivateFile(path, content) {
654
+ const directory = dirname2(path);
655
+ const privateRoot = dirname2(directory);
656
+ mkdirSync2(privateRoot, { recursive: true, mode: 448 });
657
+ securePrivatePath(privateRoot, "directory");
658
+ mkdirSync2(directory, { recursive: true, mode: 448 });
659
+ securePrivatePath(directory, "directory");
660
+ if (existsSync2(path)) verifyPrivatePathAccess(path, "file");
661
+ const temporary = `${path}.${process.pid}.${randomUUID2()}.tmp`;
662
+ const descriptor = openSync2(temporary, "wx", 384);
663
+ try {
664
+ writeFileSync2(descriptor, content);
665
+ fsyncSync2(descriptor);
666
+ } finally {
667
+ closeSync2(descriptor);
668
+ }
669
+ securePrivatePath(temporary, "file");
670
+ renameSync2(temporary, path);
671
+ verifyPrivatePathAccess(path, "file");
672
+ if (process.platform !== "win32") {
673
+ const descriptor2 = openSync2(directory, "r");
674
+ try {
675
+ fsyncSync2(descriptor2);
676
+ } finally {
677
+ closeSync2(descriptor2);
678
+ }
679
+ }
680
+ }
681
+ function writeClientBackendConfig(runtime2, identity, instance, options) {
682
+ const env = options.env;
683
+ const home = env.HOME ?? homedir2();
684
+ const sharedPath = env.AGENT_BRIDGE_CONFIG ?? join2(home, ".agent-bridge", "config");
685
+ const shared = readClientConfigFile(sharedPath);
686
+ const stagedGateway = options.gatewayUrl !== void 0 || options.workspace !== void 0 || options.edgeDatabasePath !== void 0;
687
+ if (stagedGateway && (!options.gatewayUrl || !options.workspace || !options.edgeDatabasePath)) {
688
+ throw new Error("staged gateway backend requires URL, workspace, and edge database path");
689
+ }
690
+ const configuredProvider = stagedGateway ? "gateway" : env.AGENT_BRIDGE_PROVIDER?.trim() || shared.AGENT_BRIDGE_PROVIDER?.trim();
691
+ const inferredGateway = !configuredProvider && Boolean(
692
+ env.AGENT_BRIDGE_TOKEN?.trim() || shared.AGENT_BRIDGE_TOKEN?.trim()
693
+ );
694
+ const provider = configuredProvider ?? (inferredGateway ? "gateway" : void 0);
695
+ const clientToken = options.token?.trim() || env.AGENT_BRIDGE_CLIENT_TOKEN?.trim();
696
+ if (provider === "gateway" && !clientToken) {
697
+ throw new Error(
698
+ "gateway client installation requires --token or AGENT_BRIDGE_CLIENT_TOKEN"
699
+ );
700
+ }
701
+ const resolved = resolveClientConfig(stagedGateway ? {
702
+ ...env,
703
+ AGENT_BRIDGE_CONFIG: void 0,
704
+ AGENT_BRIDGE_PROVIDER: "gateway",
705
+ AGENT_BRIDGE_URL: options.gatewayUrl,
706
+ AGENT_BRIDGE_WORKSPACE: options.workspace,
707
+ AGENT_BRIDGE_EDGE_DB: options.edgeDatabasePath,
708
+ AGENT_BRIDGE_AGENT: void 0,
709
+ AGENT_BRIDGE_INSTANCE: void 0,
710
+ AGENT_BRIDGE_TOKEN: clientToken
711
+ } : {
712
+ ...env,
713
+ AGENT_BRIDGE_AGENT: void 0,
714
+ AGENT_BRIDGE_INSTANCE: void 0,
715
+ AGENT_BRIDGE_TOKEN: clientToken ?? env.AGENT_BRIDGE_TOKEN
716
+ }, identity);
717
+ const values = {
718
+ AGENT_BRIDGE_PROVIDER: resolved.provider,
719
+ AGENT_BRIDGE_WORKSPACE: resolved.principal.workspace,
720
+ AGENT_BRIDGE_URL: resolved.url,
721
+ AGENT_BRIDGE_TOKEN: resolved.provider === "gateway" ? resolved.credential : void 0,
722
+ AGENT_BRIDGE_DB: resolved.provider === "local" ? resolved.databasePath : void 0,
723
+ AGENT_BRIDGE_EDGE_DB: resolved.provider === "gateway" ? resolved.edgeDatabasePath : void 0,
724
+ AGENT_BRIDGE_CREDENTIAL_ID: options.binding?.credentialId,
725
+ AGENT_BRIDGE_PRINCIPAL: options.binding?.principal,
726
+ AGENT_BRIDGE_CLIENT_INSTANCE: options.binding?.instance
727
+ };
728
+ for (const value of Object.values(values)) {
729
+ if (value?.includes("\n") || value?.includes("\r")) {
730
+ throw new Error("client backend config values cannot contain newlines");
731
+ }
732
+ }
733
+ const path = options.destinationPath ?? clientBackendConfigPath(runtime2, instance, env);
734
+ replacePrivateFile(
735
+ path,
736
+ `${Object.entries(values).filter((entry) => Boolean(entry[1])).map(([key, value]) => `${key}=${value}`).join("\n")}
737
+ `
738
+ );
739
+ return path;
740
+ }
741
+ function migrationOperationId(value) {
742
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) {
743
+ throw new Error("client migration operation ID is invalid");
744
+ }
745
+ return value.toLowerCase();
746
+ }
747
+ function stagedGatewayBackendPaths(operationId, env = process.env) {
748
+ const home = env.HOME ?? homedir2();
749
+ const directory = join2(home, ".agent-bridge", "client-migrations", migrationOperationId(operationId));
750
+ return {
751
+ directory,
752
+ backendConfigPath: join2(directory, "target.config"),
753
+ edgeDatabasePath: join2(directory, "target.edge.sqlite3")
754
+ };
755
+ }
756
+ function writeStagedGatewayBackendConfig(runtime2, identity, instance, operationId, options) {
757
+ const env = options.env ?? process.env;
758
+ const paths = stagedGatewayBackendPaths(operationId, env);
759
+ writeClientBackendConfig(runtime2, identity, instance, {
760
+ token: options.token,
761
+ env,
762
+ binding: {
763
+ credentialId: options.credentialId,
764
+ principal: options.principal,
765
+ instance
766
+ },
767
+ destinationPath: paths.backendConfigPath,
768
+ gatewayUrl: options.gatewayUrl,
769
+ workspace: options.workspace,
770
+ edgeDatabasePath: paths.edgeDatabasePath
771
+ });
772
+ return paths;
773
+ }
774
+ function writeBackendConfig(runtime2, identity, instance, options) {
775
+ return writeClientBackendConfig(runtime2, identity, instance, options);
776
+ }
777
+ function writeJsonConfig(path, identity, instance, backendConfigPath, launch) {
778
+ let config = {};
779
+ if (existsSync2(path)) {
780
+ try {
781
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
782
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") throw new Error();
783
+ config = parsed;
784
+ } catch {
785
+ throw new Error(`Claude Desktop config is not valid JSON: ${path}`);
786
+ }
787
+ }
788
+ const currentServers = config.mcpServers;
789
+ const mcpServers = currentServers && !Array.isArray(currentServers) && typeof currentServers === "object" ? currentServers : {};
790
+ const next = {
791
+ ...config,
792
+ mcpServers: {
793
+ ...mcpServers,
794
+ "agent-bridge": {
795
+ command: launch.command,
796
+ args: launch.args,
797
+ env: {
798
+ AGENT_BRIDGE_AGENT: identity,
799
+ AGENT_BRIDGE_INSTANCE: instance,
800
+ AGENT_BRIDGE_CONFIG: backendConfigPath
801
+ }
802
+ }
803
+ }
804
+ };
805
+ mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
806
+ const temporary = `${path}.${process.pid}.tmp`;
807
+ writeFileSync2(temporary, `${JSON.stringify(next, null, 2)}
808
+ `, { mode: 384 });
809
+ renameSync2(temporary, path);
810
+ }
811
+ function assertEnrollmentConfiguration(enrollment, assertions) {
812
+ const input = enrollment.input;
813
+ const env = assertions.env;
814
+ const home = env.HOME ?? homedir2();
815
+ const shared = readClientConfigFile(
816
+ env.AGENT_BRIDGE_CONFIG ?? join2(home, ".agent-bridge", "config")
817
+ );
818
+ const conflicts = [
819
+ ["runtime", assertions.runtime, input.runtime],
820
+ ["identity", assertions.identity?.trim() || void 0, input.principal],
821
+ ["instance", assertions.instance?.trim() || void 0, input.instance],
822
+ ["AGENT_BRIDGE_AGENT", env.AGENT_BRIDGE_AGENT?.trim(), input.principal],
823
+ ["AGENT_BRIDGE_INSTANCE", env.AGENT_BRIDGE_INSTANCE?.trim(), input.instance],
824
+ ["AGENT_BRIDGE_URL", env.AGENT_BRIDGE_URL?.trim(), input.gatewayUrl],
825
+ ["AGENT_BRIDGE_WORKSPACE", env.AGENT_BRIDGE_WORKSPACE?.trim(), input.workspaceId],
826
+ ["shared AGENT_BRIDGE_URL", shared.AGENT_BRIDGE_URL?.trim(), input.gatewayUrl],
827
+ ["shared AGENT_BRIDGE_WORKSPACE", shared.AGENT_BRIDGE_WORKSPACE?.trim(), input.workspaceId]
828
+ ];
829
+ for (const [name, actual, expected] of conflicts) {
830
+ if (actual && actual !== expected) throw new Error(name + " conflicts with the enrollment file");
831
+ }
832
+ for (const [name, provider] of [
833
+ ["AGENT_BRIDGE_PROVIDER", env.AGENT_BRIDGE_PROVIDER?.trim()],
834
+ ["shared AGENT_BRIDGE_PROVIDER", shared.AGENT_BRIDGE_PROVIDER?.trim()]
835
+ ]) {
836
+ if (provider && provider !== "gateway") throw new Error(name + " conflicts with the enrollment file");
837
+ }
838
+ if (assertions.token) throw new Error("--token cannot be used with --enrollment-file");
839
+ if (env.AGENT_BRIDGE_CLIENT_TOKEN || env.AGENT_BRIDGE_TOKEN) {
840
+ throw new Error("token environment variables cannot be used with --enrollment-file");
841
+ }
842
+ }
843
+ function removeConsumedEnrollment(path, result, lock, env) {
844
+ try {
845
+ const deletion = deleteEnrollmentFile(path, lock, env);
846
+ if (deletion === "missing") {
847
+ return {
848
+ ...result,
849
+ installed: true,
850
+ enrollmentDeleted: false,
851
+ enrollmentFile: path,
852
+ enrollmentStatus: "consumed-file-missing"
853
+ };
854
+ }
855
+ if (deletion === "deleted-durability-unknown") {
856
+ return {
857
+ ...result,
858
+ installed: true,
859
+ enrollmentDeleted: true,
860
+ enrollmentStatus: "consumed-deletion-durability-unknown"
861
+ };
862
+ }
863
+ return {
864
+ ...result,
865
+ installed: true,
866
+ enrollmentDeleted: true,
867
+ enrollmentStatus: "consumed"
868
+ };
869
+ } catch {
870
+ return {
871
+ ...result,
872
+ installed: true,
873
+ enrollmentDeleted: false,
874
+ enrollmentFile: path,
875
+ enrollmentStatus: "consumed-file-retained"
876
+ };
877
+ }
878
+ }
879
+ function backendMatches(path, enrollment, credentialId, requireToken) {
880
+ if (!existsSync2(path)) return false;
881
+ const config = readClientConfigFile(path);
882
+ return config.AGENT_BRIDGE_PROVIDER === "gateway" && config.AGENT_BRIDGE_URL === enrollment.input.gatewayUrl && config.AGENT_BRIDGE_WORKSPACE === enrollment.input.workspaceId && config.AGENT_BRIDGE_CREDENTIAL_ID === credentialId && config.AGENT_BRIDGE_PRINCIPAL === enrollment.input.principal && config.AGENT_BRIDGE_CLIENT_INSTANCE === enrollment.input.instance && (!requireToken || config.AGENT_BRIDGE_TOKEN === enrollment.token);
883
+ }
884
+ function desktopRegistrationState(enrollment, backendConfigPath, launch, env) {
885
+ const path = desktopConfigPath(env);
886
+ if (!existsSync2(path)) return "absent";
887
+ try {
888
+ const config = JSON.parse(readFileSync2(path, "utf8"));
889
+ const server = config?.mcpServers?.["agent-bridge"];
890
+ if (!server) return "absent";
891
+ const environment = server?.env;
892
+ const exactEnvironment = environment && typeof environment === "object" && !Array.isArray(environment) && Object.keys(environment).sort().join(",") === "AGENT_BRIDGE_AGENT,AGENT_BRIDGE_CONFIG,AGENT_BRIDGE_INSTANCE" && environment.AGENT_BRIDGE_AGENT === enrollment.input.principal && environment.AGENT_BRIDGE_INSTANCE === enrollment.input.instance && environment.AGENT_BRIDGE_CONFIG === backendConfigPath;
893
+ if (server?.command === "agent-bridge-mcp" && (server?.args === void 0 || Array.isArray(server.args) && server.args.length === 0) && exactEnvironment) return "legacy";
894
+ return server?.command === launch.command && Array.isArray(server?.args) && server.args.length === launch.args.length && server.args.every((arg, index) => arg === launch.args[index]) && exactEnvironment ? "matching" : "conflict";
895
+ } catch {
896
+ return "conflict";
897
+ }
898
+ }
899
+ function repairLegacyDesktopRegistration(state, enrollment, backendConfigPath, launch, env) {
900
+ if (state !== "legacy") return state;
901
+ writeJsonConfig(
902
+ desktopConfigPath(env),
903
+ enrollment.input.principal,
904
+ enrollment.input.instance,
905
+ backendConfigPath,
906
+ launch
907
+ );
908
+ const repaired = desktopRegistrationState(enrollment, backendConfigPath, launch, env);
909
+ return repaired === "legacy" ? "conflict" : repaired;
910
+ }
911
+ function nativeRegistrationState(enrollment, backendConfigPath, command, scope, execute) {
912
+ const executable = enrollment.input.runtime === "codex" ? "codex" : "claude";
913
+ const args = enrollment.input.runtime === "codex" ? ["mcp", "get", "agent-bridge", "--json"] : ["mcp", "get", "agent-bridge"];
914
+ const result = execute(executable, args);
915
+ if (result.error) throw new Error(enrollment.input.runtime + " MCP inspection failed");
916
+ if (result.status !== 0) {
917
+ const output = (result.stdout + "\n" + result.stderr).trim();
918
+ const notFound = enrollment.input.runtime === "codex" ? output === "Error: No MCP server named 'agent-bridge' found." : output === 'No MCP server named "agent-bridge".' || /^No MCP server named "agent-bridge"\. Configured servers: [^\r\n]+$/.test(output);
919
+ if (notFound) return "absent";
920
+ throw new Error(enrollment.input.runtime + " MCP inspection failed");
921
+ }
922
+ if (enrollment.input.runtime === "codex") {
923
+ try {
924
+ const server = JSON.parse(result.stdout);
925
+ const transport = server?.transport;
926
+ const environment = transport?.env;
927
+ const exactEnvironment = environment && typeof environment === "object" && !Array.isArray(environment) && Object.keys(environment).sort().join(",") === "AGENT_BRIDGE_AGENT,AGENT_BRIDGE_CONFIG,AGENT_BRIDGE_INSTANCE";
928
+ return server?.name === "agent-bridge" && server?.enabled === true && transport?.type === "stdio" && transport?.command === command && Array.isArray(transport?.args) && transport.args.length === 0 && exactEnvironment && transport?.env?.AGENT_BRIDGE_AGENT === enrollment.input.principal && transport?.env?.AGENT_BRIDGE_INSTANCE === enrollment.input.instance && transport?.env?.AGENT_BRIDGE_CONFIG === backendConfigPath ? "matching" : "conflict";
929
+ } catch {
930
+ return "conflict";
931
+ }
932
+ }
933
+ const lines = result.stdout.replace(/\r\n/g, "\n").split("\n");
934
+ const scopeLine = {
935
+ local: " Scope: Local config (private to you in this project)",
936
+ user: " Scope: User config (available in all your projects)",
937
+ project: " Scope: Project config (shared via .mcp.json)"
938
+ }[scope];
939
+ const expectedPrefix = [
940
+ "agent-bridge:",
941
+ scopeLine
942
+ ];
943
+ if (lines[0] !== expectedPrefix[0] || lines[1] !== expectedPrefix[1] || lines[3] !== " Type: stdio" || lines[4] !== " Command: " + command || lines[5] !== " Args:" || lines[6] !== " Environment:") return "conflict";
944
+ const environmentLines = [];
945
+ let index = 7;
946
+ while (index < lines.length && lines[index]?.startsWith(" ")) {
947
+ environmentLines.push(lines[index]);
948
+ index += 1;
949
+ }
950
+ const expectedEnvironment = [
951
+ " AGENT_BRIDGE_AGENT=" + enrollment.input.principal,
952
+ " AGENT_BRIDGE_INSTANCE=" + enrollment.input.instance,
953
+ " AGENT_BRIDGE_CONFIG=" + backendConfigPath
954
+ ].sort();
955
+ if (environmentLines.sort().join("\n") !== expectedEnvironment.join("\n")) return "conflict";
956
+ while (lines[index] === "") index += 1;
957
+ if (lines[index] !== "To remove this server, run: claude mcp remove agent-bridge -s " + scope) {
958
+ return "conflict";
959
+ }
960
+ if (lines.slice(index + 1).some((line) => line !== "")) return "conflict";
961
+ return "matching";
962
+ }
963
+ function enrolledInstallLocked(runtimeAssertion, identityAssertion, options, execute, lock) {
964
+ const env = options.env ?? process.env;
965
+ let enrollment = readEnrollment(options.enrollmentFile, env);
966
+ assertEnrollmentConfiguration(enrollment, {
967
+ runtime: runtimeAssertion,
968
+ identity: identityAssertion,
969
+ instance: options.instance,
970
+ token: options.token,
971
+ env
972
+ });
973
+ const runtime2 = enrollment.input.runtime;
974
+ const identity = enrollment.input.principal;
975
+ const expectedPath = clientBackendConfigPath(runtime2, enrollment.input.instance, env);
976
+ const command = options.command?.trim() || "agent-bridge-mcp";
977
+ const desktopLaunch = runtime2 === "claude-desktop" ? resolveDesktopLaunchContract(options.command, env) : void 0;
978
+ const scope = options.scope ?? "user";
979
+ const baseResult = {
980
+ runtime: runtime2,
981
+ identity,
982
+ instance: enrollment.input.instance,
983
+ method: runtime2 === "claude-desktop" ? "json-config" : "native-cli",
984
+ configPath: runtime2 === "claude-desktop" ? desktopConfigPath(env) : void 0,
985
+ backendConfigPath: expectedPath,
986
+ restartRequired: true
987
+ };
988
+ if (enrollment.state === "consumed") {
989
+ if (!enrollment.result || !backendMatches(expectedPath, enrollment, enrollment.result.credentialId, false)) {
990
+ throw new Error("consumed enrollment no longer matches its client backend");
991
+ }
992
+ const inspected = runtime2 === "claude-desktop" ? desktopRegistrationState(enrollment, expectedPath, desktopLaunch, env) : nativeRegistrationState(enrollment, expectedPath, command, scope, execute);
993
+ const registration = runtime2 === "claude-desktop" ? repairLegacyDesktopRegistration(
994
+ inspected,
995
+ enrollment,
996
+ expectedPath,
997
+ desktopLaunch,
998
+ env
999
+ ) : inspected;
1000
+ if (registration !== "matching") {
1001
+ throw new Error("consumed enrollment no longer matches its MCP registration");
1002
+ }
1003
+ return removeConsumedEnrollment(options.enrollmentFile, baseResult, lock, env);
1004
+ }
1005
+ if (enrollment.state === "pending") {
1006
+ throw new Error("enrollment is pending; resume the owner command first");
1007
+ }
1008
+ const recovering = enrollment.state === "consuming";
1009
+ if (!recovering && enrollment.operation === "provision" && existsSync2(expectedPath)) {
1010
+ throw new Error("provision enrollment would replace an existing client backend file");
1011
+ }
1012
+ if (!recovering && enrollment.operation === "provision") {
1013
+ const registration = runtime2 === "claude-desktop" ? desktopRegistrationState(enrollment, expectedPath, desktopLaunch, env) : nativeRegistrationState(enrollment, expectedPath, command, scope, execute);
1014
+ if (registration !== "absent") {
1015
+ throw new Error("provision enrollment would replace an existing MCP registration");
1016
+ }
1017
+ }
1018
+ if (enrollment.state === "ready") {
1019
+ enrollment = transitionEnrollment(options.enrollmentFile, enrollment, "consuming", {}, env, lock);
1020
+ }
1021
+ if (enrollment.state !== "consuming" || !enrollment.token) {
1022
+ throw new Error("enrollment is not ready for installation");
1023
+ }
1024
+ const derivedEnv = {
1025
+ ...env,
1026
+ AGENT_BRIDGE_PROVIDER: "gateway",
1027
+ AGENT_BRIDGE_URL: enrollment.input.gatewayUrl,
1028
+ AGENT_BRIDGE_WORKSPACE: enrollment.input.workspaceId,
1029
+ AGENT_BRIDGE_AGENT: void 0,
1030
+ AGENT_BRIDGE_INSTANCE: void 0,
1031
+ AGENT_BRIDGE_CLIENT_TOKEN: void 0
1032
+ };
1033
+ let result;
1034
+ let sideEffectsCompleted = false;
1035
+ try {
1036
+ if (enrollment.operation === "rotate") {
1037
+ if (!enrollment.input.credentialId || !enrollment.result) {
1038
+ throw new Error("rotation enrollment lacks credential lineage");
1039
+ }
1040
+ const predecessorMatches = backendMatches(
1041
+ expectedPath,
1042
+ enrollment,
1043
+ enrollment.input.credentialId,
1044
+ false
1045
+ );
1046
+ const successorMatches = recovering && backendMatches(
1047
+ expectedPath,
1048
+ enrollment,
1049
+ enrollment.result.credentialId,
1050
+ true
1051
+ );
1052
+ if (!predecessorMatches && !successorMatches) {
1053
+ throw new Error("rotation requires an exactly bound predecessor or successor backend");
1054
+ }
1055
+ const inspected = runtime2 === "claude-desktop" ? desktopRegistrationState(enrollment, expectedPath, desktopLaunch, env) : nativeRegistrationState(enrollment, expectedPath, command, scope, execute);
1056
+ const registration = runtime2 === "claude-desktop" ? repairLegacyDesktopRegistration(
1057
+ inspected,
1058
+ enrollment,
1059
+ expectedPath,
1060
+ desktopLaunch,
1061
+ env
1062
+ ) : inspected;
1063
+ if (registration !== "matching") {
1064
+ throw new Error("rotation requires the exact existing MCP registration");
1065
+ }
1066
+ if (predecessorMatches) {
1067
+ const previous = readFileSync2(expectedPath);
1068
+ try {
1069
+ writeBackendConfig(runtime2, identity, enrollment.input.instance, {
1070
+ token: enrollment.token,
1071
+ env: derivedEnv,
1072
+ binding: {
1073
+ credentialId: enrollment.result.credentialId,
1074
+ principal: identity,
1075
+ instance: enrollment.input.instance
1076
+ }
1077
+ });
1078
+ } catch (error) {
1079
+ replacePrivateFile(expectedPath, previous);
1080
+ throw error;
1081
+ }
1082
+ }
1083
+ result = baseResult;
1084
+ sideEffectsCompleted = true;
1085
+ } else {
1086
+ if (recovering && existsSync2(expectedPath) && !backendMatches(
1087
+ expectedPath,
1088
+ enrollment,
1089
+ enrollment.result.credentialId,
1090
+ true
1091
+ )) {
1092
+ throw new Error("consuming enrollment conflicts with its client backend file");
1093
+ }
1094
+ const inspected = recovering ? runtime2 === "claude-desktop" ? desktopRegistrationState(enrollment, expectedPath, desktopLaunch, env) : nativeRegistrationState(enrollment, expectedPath, command, scope, execute) : "absent";
1095
+ const registration = recovering && runtime2 === "claude-desktop" ? repairLegacyDesktopRegistration(
1096
+ inspected,
1097
+ enrollment,
1098
+ expectedPath,
1099
+ desktopLaunch,
1100
+ env
1101
+ ) : inspected;
1102
+ if (recovering && registration === "conflict") {
1103
+ throw new Error("consuming enrollment conflicts with the registered MCP server");
1104
+ }
1105
+ if (recovering && backendMatches(
1106
+ expectedPath,
1107
+ enrollment,
1108
+ enrollment.result.credentialId,
1109
+ true
1110
+ ) && registration === "matching") {
1111
+ result = baseResult;
1112
+ } else {
1113
+ result = installClient(runtime2, identity, {
1114
+ command: runtime2 === "claude-desktop" ? options.command : command,
1115
+ scope: options.scope,
1116
+ instance: enrollment.input.instance,
1117
+ token: enrollment.token,
1118
+ backendBinding: {
1119
+ credentialId: enrollment.result.credentialId,
1120
+ principal: identity,
1121
+ instance: enrollment.input.instance
1122
+ },
1123
+ env: derivedEnv
1124
+ }, execute);
1125
+ sideEffectsCompleted = true;
1126
+ const verified = runtime2 === "claude-desktop" ? desktopRegistrationState(enrollment, expectedPath, desktopLaunch, env) : nativeRegistrationState(enrollment, expectedPath, command, scope, execute);
1127
+ if (verified !== "matching") throw new Error("MCP registration verification failed");
1128
+ }
1129
+ }
1130
+ } catch (error) {
1131
+ if (!recovering && !sideEffectsCompleted) {
1132
+ transitionEnrollment(options.enrollmentFile, enrollment, "ready", {}, env, lock);
1133
+ }
1134
+ throw error;
1135
+ }
1136
+ enrollment = transitionEnrollment(
1137
+ options.enrollmentFile,
1138
+ enrollment,
1139
+ "consumed",
1140
+ { token: null },
1141
+ env,
1142
+ lock
1143
+ );
1144
+ return removeConsumedEnrollment(options.enrollmentFile, result, lock, env);
1145
+ }
1146
+ function enrolledInstall(runtime2, identity, options, execute) {
1147
+ const env = options.env ?? process.env;
1148
+ if (options.recoverLock) recoverEnrollmentLock(options.enrollmentFile, env);
1149
+ const lock = acquireEnrollmentLock(options.enrollmentFile, env);
1150
+ let result;
1151
+ try {
1152
+ result = enrolledInstallLocked(runtime2, identity, options, execute, lock);
1153
+ } catch (error) {
1154
+ if (!lock.released) {
1155
+ try {
1156
+ releaseEnrollmentLock(lock);
1157
+ } catch {
1158
+ }
1159
+ }
1160
+ throw error;
1161
+ }
1162
+ let lockReleaseStatus;
1163
+ try {
1164
+ const release = releaseEnrollmentLock(lock);
1165
+ lockReleaseStatus = release === "released" ? "released" : "durability-unknown";
1166
+ } catch {
1167
+ lockReleaseStatus = existsSync2(lock.lockPath) ? "retained" : "durability-unknown";
1168
+ }
1169
+ return { ...result, lockReleaseStatus };
1170
+ }
1171
+ function installClient(runtime2, identity, options = {}, execute = (command, args) => spawnSync(command, args, { encoding: "utf8" })) {
1172
+ if (options.enrollmentFile) {
1173
+ return enrolledInstall(runtime2, identity, {
1174
+ ...options,
1175
+ enrollmentFile: options.enrollmentFile
1176
+ }, execute);
1177
+ }
1178
+ if (!runtime2) throw new Error("install runtime is required");
1179
+ const normalizedIdentity = identity.trim();
1180
+ if (!normalizedIdentity || normalizedIdentity.length > 128) throw new Error("--identity is required");
1181
+ const instance = options.instance?.trim() || `${runtime2}-${randomUUID2()}`;
1182
+ if (instance.length > 128) throw new Error("--instance exceeds 128 characters");
1183
+ if (options.scope && !["local", "user", "project"].includes(options.scope)) {
1184
+ throw new Error("scope must be local, user, or project");
1185
+ }
1186
+ const command = options.command?.trim() || "agent-bridge-mcp";
1187
+ const env = options.env ?? process.env;
1188
+ const desktopLaunch = runtime2 === "claude-desktop" ? resolveDesktopLaunchContract(options.command, env) : void 0;
1189
+ const expectedPath = clientBackendConfigPath(runtime2, instance, env);
1190
+ const previous = existsSync2(expectedPath) ? readFileSync2(expectedPath) : void 0;
1191
+ try {
1192
+ const backendConfigPath = writeBackendConfig(runtime2, normalizedIdentity, instance, {
1193
+ token: options.token,
1194
+ env,
1195
+ binding: options.backendBinding
1196
+ });
1197
+ if (runtime2 === "claude-desktop") {
1198
+ const path = desktopConfigPath(env);
1199
+ writeJsonConfig(path, normalizedIdentity, instance, backendConfigPath, desktopLaunch);
1200
+ return { runtime: runtime2, identity: normalizedIdentity, instance, method: "json-config", configPath: path, backendConfigPath, restartRequired: true };
1201
+ }
1202
+ const executable = runtime2 === "codex" ? "codex" : "claude";
1203
+ const args = runtime2 === "codex" ? [
1204
+ "mcp",
1205
+ "add",
1206
+ "agent-bridge",
1207
+ "--env",
1208
+ `AGENT_BRIDGE_AGENT=${normalizedIdentity}`,
1209
+ "--env",
1210
+ `AGENT_BRIDGE_INSTANCE=${instance}`,
1211
+ "--env",
1212
+ `AGENT_BRIDGE_CONFIG=${backendConfigPath}`,
1213
+ "--",
1214
+ command
1215
+ ] : [
1216
+ "mcp",
1217
+ "add",
1218
+ "--scope",
1219
+ options.scope ?? "user",
1220
+ "agent-bridge",
1221
+ "-e",
1222
+ `AGENT_BRIDGE_AGENT=${normalizedIdentity}`,
1223
+ "-e",
1224
+ `AGENT_BRIDGE_INSTANCE=${instance}`,
1225
+ "-e",
1226
+ `AGENT_BRIDGE_CONFIG=${backendConfigPath}`,
1227
+ "--",
1228
+ command
1229
+ ];
1230
+ const result = execute(executable, args);
1231
+ if (result.error || result.status !== 0) {
1232
+ const detail = result.stderr?.trim() || result.error?.message || `exit ${result.status}`;
1233
+ throw new Error(`${runtime2} MCP installation failed: ${detail}`);
1234
+ }
1235
+ return { runtime: runtime2, identity: normalizedIdentity, instance, method: "native-cli", backendConfigPath, restartRequired: true };
1236
+ } catch (error) {
1237
+ try {
1238
+ if (previous) replacePrivateFile(expectedPath, previous);
1239
+ else rmSync2(expectedPath, { force: true });
1240
+ } catch (rollbackError) {
1241
+ const detail = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
1242
+ throw new Error(`${error instanceof Error ? error.message : String(error)}; client config rollback failed: ${detail}`);
1243
+ }
1244
+ throw error;
1245
+ }
1246
+ }
1247
+
1248
+ // src/client-lifecycle.ts
1249
+ import {
1250
+ accessSync as accessSync2,
1251
+ closeSync as closeSync3,
1252
+ constants as constants3,
1253
+ existsSync as existsSync3,
1254
+ fstatSync as fstatSync2,
1255
+ fsyncSync as fsyncSync3,
1256
+ lstatSync as lstatSync2,
1257
+ mkdirSync as mkdirSync3,
1258
+ openSync as openSync3,
1259
+ readFileSync as readFileSync3,
1260
+ renameSync as renameSync3,
1261
+ statSync as statSync2,
1262
+ writeFileSync as writeFileSync3
1263
+ } from "fs";
1264
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
1265
+ import { homedir as homedir3 } from "os";
1266
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join3, parse as parse2, resolve as resolve3, sep as sep2 } from "path";
1267
+ import { spawnSync as spawnSync2 } from "child_process";
1268
+ import { isDeepStrictEqual } from "util";
1269
+ var defaultExecutor = (command, args, context) => spawnSync2(command, args, {
1270
+ encoding: "utf8",
1271
+ cwd: context?.cwd,
1272
+ env: context?.env
1273
+ });
1274
+ function validateLoopbackMcpUrl(value, field = "--mcp-url") {
1275
+ const text2 = strictText(value, field, 2048);
1276
+ let parsed;
1277
+ try {
1278
+ parsed = new URL(text2);
1279
+ } catch {
1280
+ throw new Error(`${field} must be a loopback HTTP MCP URL`);
1281
+ }
1282
+ if (parsed.protocol !== "http:" || parsed.username || parsed.password || parsed.search || parsed.hash || !["127.0.0.1", "localhost", "[::1]"].includes(parsed.hostname) || !parsed.port || parsed.pathname !== "/mcp") {
1283
+ throw new Error(`${field} must be a loopback HTTP MCP URL ending in /mcp`);
1284
+ }
1285
+ return parsed.toString();
1286
+ }
1287
+ function managedClientTarget(metadata) {
1288
+ return metadata.version === 2 ? metadata.target : {
1289
+ version: 1,
1290
+ transport: "stdio",
1291
+ command: metadata.launch.command,
1292
+ args: [...metadata.launch.args],
1293
+ scope: metadata.launch.scope
1294
+ };
1295
+ }
1296
+ function targetLaunchProjection(target) {
1297
+ return target.transport === "stdio" ? { command: target.command, args: [...target.args], scope: target.scope } : { command: target.url, args: [], scope: target.scope };
1298
+ }
1299
+ function safeComponent2(value) {
1300
+ const readable = value.replace(/[^A-Za-z0-9._-]+/g, "_").slice(0, 48) || "client";
1301
+ const suffix = createHash3("sha256").update(value).digest("hex").slice(0, 12);
1302
+ return `${readable}-${suffix}`;
1303
+ }
1304
+ function managedClientMetadataPath(runtime2, instance, env = process.env) {
1305
+ return join3(env.HOME ?? homedir3(), ".agent-bridge", "clients", `${runtime2}-${safeComponent2(instance)}.managed.json`);
1306
+ }
1307
+ function desktopConfigPath2(env) {
1308
+ if (process.platform === "win32") {
1309
+ if (!env.APPDATA) throw new Error("APPDATA is required for Claude Desktop inspection");
1310
+ return join3(env.APPDATA, "Claude", "claude_desktop_config.json");
1311
+ }
1312
+ const home = env.HOME ?? homedir3();
1313
+ return process.platform === "darwin" ? join3(home, "Library", "Application Support", "Claude", "claude_desktop_config.json") : join3(home, ".config", "Claude", "claude_desktop_config.json");
1314
+ }
1315
+ function inspectedDesktopConfigPath(options, env) {
1316
+ return resolve3(options.configPath?.trim() || desktopConfigPath2(env));
1317
+ }
1318
+ function clientRegistrationLocator(runtime2, scope, options, env) {
1319
+ if (runtime2 === "claude-desktop") {
1320
+ return { kind: "claude-desktop-config", configPath: inspectedDesktopConfigPath(options, env) };
1321
+ }
1322
+ if (runtime2 === "claude-code") {
1323
+ const claudeScope = scope;
1324
+ return {
1325
+ kind: "claude-code-scope",
1326
+ scope: claudeScope,
1327
+ contextPath: claudeScope === "user" ? null : resolve3(process.cwd())
1328
+ };
1329
+ }
1330
+ const codexHome = env.CODEX_HOME?.trim() || join3(env.HOME ?? homedir3(), ".codex");
1331
+ return { kind: "codex-profile", configPath: resolve3(codexHome, "config.toml") };
1332
+ }
1333
+ function expectedManagedClientMetadata(runtime2, identity, options, env) {
1334
+ const scope = runtime2 === "claude-code" ? options.scope ?? "user" : null;
1335
+ const launch = runtime2 === "claude-desktop" ? resolveDesktopLaunchContract(options.command, env) : { command: safeNativeExecutableContract(options.command === void 0 ? "agent-bridge-mcp" : options.command, "--command"), args: [] };
1336
+ return {
1337
+ schema: "agent-bridge.client-management",
1338
+ version: 1,
1339
+ runtime: runtime2,
1340
+ identity,
1341
+ instance: options.instance.trim(),
1342
+ backendConfigPath: resolve3(options.backendConfigPath.trim()),
1343
+ launch: { ...launch, scope },
1344
+ locator: clientRegistrationLocator(runtime2, scope, options, env)
1345
+ };
1346
+ }
1347
+ function exactManagedTarget(actual, expected) {
1348
+ return actual != null && actual.schema === expected.schema && actual.runtime === expected.runtime && actual.identity === expected.identity && actual.instance === expected.instance && actual.backendConfigPath === expected.backendConfigPath && isDeepStrictEqual(actual.locator, expected.locator);
1349
+ }
1350
+ var REGISTRATION_ENV_KEYS = [
1351
+ "AGENT_BRIDGE_AGENT",
1352
+ "AGENT_BRIDGE_CONFIG",
1353
+ "AGENT_BRIDGE_INSTANCE"
1354
+ ];
1355
+ function registrationTarget(metadata) {
1356
+ return {
1357
+ runtime: metadata.runtime,
1358
+ identity: metadata.identity,
1359
+ instance: metadata.instance,
1360
+ backendConfigPath: metadata.backendConfigPath,
1361
+ launch: { command: metadata.launch.command, args: [...metadata.launch.args], scope: metadata.launch.scope },
1362
+ locator: metadata.locator
1363
+ };
1364
+ }
1365
+ function safeNativeExecutableContract(value, field) {
1366
+ const command = strictText(value, field, 4096);
1367
+ if (/[?=#]/.test(command) || /:\/\//.test(command) || !isAbsolute2(command) && /\s/.test(command)) {
1368
+ throw new Error(`${field} is invalid`);
1369
+ }
1370
+ return command;
1371
+ }
1372
+ function newNativeExecutableContract(value, field) {
1373
+ const command = safeNativeExecutableContract(value, field);
1374
+ if (!isAbsolute2(command)) return command;
1375
+ if (resolve3(command) !== command || !existsSync3(command) || !statSync2(command).isFile()) {
1376
+ throw new Error(`${field} is not an existing executable`);
1377
+ }
1378
+ try {
1379
+ accessSync2(command, process.platform === "win32" ? constants3.F_OK : constants3.X_OK);
1380
+ } catch {
1381
+ throw new Error(`${field} is not an existing executable`);
1382
+ }
1383
+ return command;
1384
+ }
1385
+ function safeEnvironment(value) {
1386
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1387
+ throw new Error("managed registration environment is invalid");
1388
+ }
1389
+ const environment = {};
1390
+ for (const [key, item] of Object.entries(value)) {
1391
+ if (!REGISTRATION_ENV_KEYS.includes(key)) {
1392
+ throw new Error("managed registration contains an unsupported environment key");
1393
+ }
1394
+ if (key === "AGENT_BRIDGE_CONFIG") environment.AGENT_BRIDGE_CONFIG = strictAbsolutePath(item, "registration config");
1395
+ else environment[key] = strictText(item, `registration ${key}`, 128);
1396
+ }
1397
+ return environment;
1398
+ }
1399
+ function exactRegistration(expected, command, args, environment) {
1400
+ const target = managedClientTarget(expected);
1401
+ if (target.transport === "streamable-http") {
1402
+ return command === target.url && args.length === 0 && Object.keys(environment).length === 0;
1403
+ }
1404
+ return command === expected.launch.command && isDeepStrictEqual(args, expected.launch.args) && isDeepStrictEqual(environment, {
1405
+ AGENT_BRIDGE_AGENT: expected.identity,
1406
+ AGENT_BRIDGE_INSTANCE: expected.instance,
1407
+ AGENT_BRIDGE_CONFIG: expected.backendConfigPath
1408
+ });
1409
+ }
1410
+ function safeArgs(value, expected, alternate = []) {
1411
+ if (!Array.isArray(value) || value.length > 16) throw new Error("managed registration arguments are invalid");
1412
+ const args = value.map((item) => strictText(item, "registration argument", 1024));
1413
+ if (!isDeepStrictEqual(args, expected) && !isDeepStrictEqual(args, alternate)) {
1414
+ throw new Error("managed registration arguments cannot be represented safely");
1415
+ }
1416
+ return args;
1417
+ }
1418
+ function sensitiveArgument(args, index) {
1419
+ const arg = args[index];
1420
+ const previous = index > 0 ? args[index - 1] : "";
1421
+ const sensitiveFlag = /^--?(?:token|key|secret|password|authorization|credential)(?:=.*)?$/i;
1422
+ return sensitiveFlag.test(arg) || sensitiveFlag.test(previous) || /\bBearer\s+\S+/i.test(arg) || /^https?:\/\/\S*[?#]\S+/i.test(arg);
1423
+ }
1424
+ function safeDesktopArgs(value, expected, alternate = []) {
1425
+ if (!Array.isArray(value) || value.length > 16) throw new Error("managed registration arguments are invalid");
1426
+ const args = value.map((item) => strictText(item, "registration argument", 1024));
1427
+ if (isDeepStrictEqual(args, expected) || isDeepStrictEqual(args, alternate)) return args;
1428
+ if (args.some((_, index) => sensitiveArgument(args, index))) {
1429
+ throw new Error("managed registration arguments cannot be represented safely");
1430
+ }
1431
+ return args;
1432
+ }
1433
+ function sensitiveEnvironmentEntry(key, value) {
1434
+ return /(?:TOKEN|KEY|SECRET|PASSWORD|AUTHORIZATION|CREDENTIAL)/i.test(key) || /\bBearer\s+\S+/i.test(value) || /^https?:\/\/\S*[?#]\S+/i.test(value);
1435
+ }
1436
+ function safeDesktopEnvironment(value) {
1437
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1438
+ throw new Error("managed registration environment is invalid");
1439
+ }
1440
+ const environment = {};
1441
+ const extraEntries = [];
1442
+ for (const [key, item] of Object.entries(value)) {
1443
+ const text2 = strictText(item, `registration ${key}`, 4096);
1444
+ if (!REGISTRATION_ENV_KEYS.includes(key)) {
1445
+ if (sensitiveEnvironmentEntry(key, text2)) {
1446
+ throw new Error("managed registration contains an unsupported environment key");
1447
+ }
1448
+ extraEntries.push(`${JSON.stringify(key)}=${JSON.stringify(text2)}`);
1449
+ continue;
1450
+ }
1451
+ if (key === "AGENT_BRIDGE_CONFIG") environment.AGENT_BRIDGE_CONFIG = strictAbsolutePath(text2, "registration config");
1452
+ else environment[key] = strictText(text2, `registration ${key}`, 128);
1453
+ }
1454
+ return {
1455
+ environment,
1456
+ extra: extraEntries.length > 0,
1457
+ extraSha256: extraEntries.length ? createHash3("sha256").update(extraEntries.sort().join("\n")).digest("hex") : void 0
1458
+ };
1459
+ }
1460
+ function commandContext(metadata, env) {
1461
+ if (metadata.runtime === "codex") {
1462
+ const locator2 = metadata.locator;
1463
+ return { env: { ...env, CODEX_HOME: dirname3(locator2.configPath) } };
1464
+ }
1465
+ const locator = metadata.locator;
1466
+ return { cwd: locator.contextPath ?? void 0, env: { ...env } };
1467
+ }
1468
+ function observeCodex(expected, execute, env, alternateArgs) {
1469
+ const result = execute("codex", ["mcp", "get", "agent-bridge", "--json"], {
1470
+ ...commandContext(expected, env)
1471
+ });
1472
+ if (result.error) throw new Error("codex MCP inspection failed");
1473
+ if (result.status !== 0) {
1474
+ const output = `${result.stdout}
1475
+ ${result.stderr}`.trim();
1476
+ if (output === "Error: No MCP server named 'agent-bridge' found.") {
1477
+ return { state: "absent", target: registrationTarget(expected), observed: { state: "absent" } };
1478
+ }
1479
+ throw new Error("codex MCP inspection failed");
1480
+ }
1481
+ try {
1482
+ const server = JSON.parse(result.stdout);
1483
+ const transport = server?.transport;
1484
+ const serverFields = /* @__PURE__ */ new Set([
1485
+ "name",
1486
+ "enabled",
1487
+ "disabled_reason",
1488
+ "transport",
1489
+ "enabled_tools",
1490
+ "disabled_tools",
1491
+ "startup_timeout_sec",
1492
+ "tool_timeout_sec",
1493
+ "status"
1494
+ ]);
1495
+ const target = managedClientTarget(expected);
1496
+ const transportFields = transport?.type === "streamable_http" ? /* @__PURE__ */ new Set(["type", "url", "bearer_token_env_var", "http_headers", "env_http_headers"]) : /* @__PURE__ */ new Set(["type", "command", "args", "env", "env_vars", "cwd"]);
1497
+ const hasNonNullSetting = (field) => field in server && server[field] !== null;
1498
+ if (!server || typeof server !== "object" || Array.isArray(server) || server.name !== "agent-bridge" || server.enabled !== true || !transport || typeof transport !== "object" || Array.isArray(transport)) {
1499
+ throw new Error("Codex MCP registration is not a supported contract");
1500
+ }
1501
+ if (Object.keys(server).some((field) => !serverFields.has(field)) || Object.keys(transport).some((field) => !transportFields.has(field)) || hasNonNullSetting("enabled_tools") || hasNonNullSetting("disabled_tools")) {
1502
+ throw new Error("Codex MCP registration contains an unsupported execution setting");
1503
+ }
1504
+ if (transport.type === "streamable_http") {
1505
+ if (transport.type !== "streamable_http" || transport.bearer_token_env_var !== null || transport.http_headers !== null || transport.env_http_headers !== null) {
1506
+ throw new Error("Codex MCP registration contains an unsupported HTTP setting");
1507
+ }
1508
+ const url = validateLoopbackMcpUrl(transport.url, "Codex MCP URL");
1509
+ return {
1510
+ state: exactRegistration(expected, url, [], {}) ? "exact" : "inexact",
1511
+ target: registrationTarget(expected),
1512
+ observed: { state: "present", command: url, args: [], env: {} }
1513
+ };
1514
+ }
1515
+ if (transport.type !== "stdio") {
1516
+ throw new Error("Codex MCP registration is not a supported stdio contract");
1517
+ }
1518
+ if ("cwd" in transport && transport.cwd !== null || "env_vars" in transport && (!Array.isArray(transport.env_vars) || transport.env_vars.length !== 0)) {
1519
+ throw new Error("Codex MCP registration contains an unsupported execution setting");
1520
+ }
1521
+ const command = safeNativeExecutableContract(transport.command, "Codex MCP command");
1522
+ const args = safeArgs(transport.args, expected.launch.args, alternateArgs);
1523
+ const environment = safeEnvironment(transport.env);
1524
+ return {
1525
+ state: exactRegistration(expected, command, args, environment) ? "exact" : "inexact",
1526
+ target: registrationTarget(expected),
1527
+ observed: { state: "present", command, args, env: environment }
1528
+ };
1529
+ } catch {
1530
+ throw new Error("Codex MCP registration cannot be represented safely");
1531
+ }
1532
+ }
1533
+ function observeClaudeCode(expected, execute, env, alternateArgs) {
1534
+ const locator = expected.locator;
1535
+ const result = execute("claude", ["mcp", "get", "agent-bridge"], {
1536
+ ...commandContext(expected, env)
1537
+ });
1538
+ if (result.error) throw new Error("claude-code MCP inspection failed");
1539
+ if (result.status !== 0) {
1540
+ const output = `${result.stdout}
1541
+ ${result.stderr}`.trim();
1542
+ if (output === 'No MCP server named "agent-bridge".' || /^No MCP server named "agent-bridge"\. Configured servers: [^\r\n]+$/.test(output)) {
1543
+ return { state: "absent", target: registrationTarget(expected), observed: { state: "absent" } };
1544
+ }
1545
+ throw new Error("claude-code MCP inspection failed");
1546
+ }
1547
+ const scope = {
1548
+ local: " Scope: Local config (private to you in this project)",
1549
+ user: " Scope: User config (available in all your projects)",
1550
+ project: " Scope: Project config (shared via .mcp.json)"
1551
+ }[expected.launch.scope];
1552
+ const lines = result.stdout.replace(/\r\n/g, "\n").trimEnd().split("\n");
1553
+ if (lines[0] !== "agent-bridge:" || lines[1] !== scope) {
1554
+ throw new Error("Claude Code MCP registration scope is not the recorded scope");
1555
+ }
1556
+ const target = managedClientTarget(expected);
1557
+ const httpTypeIndex = lines.indexOf(" Type: http", 2);
1558
+ if (httpTypeIndex >= 0) {
1559
+ const urlLine = lines[httpTypeIndex + 1];
1560
+ if (!urlLine?.startsWith(" URL: ")) {
1561
+ throw new Error("Claude Code MCP registration cannot be represented safely");
1562
+ }
1563
+ const url = validateLoopbackMcpUrl(urlLine.slice(" URL: ".length), "Claude Code MCP URL");
1564
+ let index2 = httpTypeIndex + 2;
1565
+ while (lines[index2] === "") index2 += 1;
1566
+ if (lines[index2] !== `To remove this server, run: claude mcp remove agent-bridge -s ${expected.launch.scope}`) {
1567
+ throw new Error("Claude Code MCP registration cannot be represented safely");
1568
+ }
1569
+ if (lines.slice(index2 + 1).some((line) => line !== "")) {
1570
+ throw new Error("Claude Code MCP registration cannot be represented safely");
1571
+ }
1572
+ return {
1573
+ state: target.transport === "streamable-http" && target.url === url ? "exact" : "inexact",
1574
+ target: registrationTarget(expected),
1575
+ observed: { state: "present", command: url, args: [], env: {} }
1576
+ };
1577
+ }
1578
+ const typeIndex = lines.indexOf(" Type: stdio", 2);
1579
+ if (typeIndex < 0 || lines[typeIndex + 1] !== ` Command: ${expected.launch.command}` || lines[typeIndex + 2] !== " Args:" || lines[typeIndex + 3] !== " Environment:") {
1580
+ if (typeIndex < 0 || lines[typeIndex + 2] !== " Args:" || lines[typeIndex + 3] !== " Environment:") {
1581
+ throw new Error("Claude Code MCP registration cannot be represented safely");
1582
+ }
1583
+ }
1584
+ const commandLine = lines[typeIndex + 1];
1585
+ if (!commandLine?.startsWith(" Command: ")) throw new Error("Claude Code MCP registration cannot be represented safely");
1586
+ const command = safeNativeExecutableContract(commandLine.slice(" Command: ".length), "Claude Code MCP command");
1587
+ const environmentLines = [];
1588
+ let index = typeIndex + 4;
1589
+ while (index < lines.length && lines[index]?.startsWith(" ")) {
1590
+ environmentLines.push(lines[index]);
1591
+ index += 1;
1592
+ }
1593
+ const values = {};
1594
+ for (const line of environmentLines) {
1595
+ const match = /^ ([A-Z0-9_]+)=(.*)$/.exec(line);
1596
+ if (!match || Object.prototype.hasOwnProperty.call(values, match[1])) {
1597
+ throw new Error("Claude Code MCP environment is invalid");
1598
+ }
1599
+ values[match[1]] = match[2];
1600
+ }
1601
+ const environment = safeEnvironment(values);
1602
+ while (lines[index] === "") index += 1;
1603
+ if (lines[index] !== `To remove this server, run: claude mcp remove agent-bridge -s ${expected.launch.scope}`) {
1604
+ throw new Error("Claude Code MCP registration cannot be represented safely");
1605
+ }
1606
+ if (lines.slice(index + 1).some((line) => line !== "")) {
1607
+ throw new Error("Claude Code MCP registration cannot be represented safely");
1608
+ }
1609
+ const args = safeArgs([], expected.launch.args, alternateArgs);
1610
+ return {
1611
+ state: exactRegistration(expected, command, args, environment) ? "exact" : "inexact",
1612
+ target: registrationTarget(expected),
1613
+ observed: { state: "present", command, args, env: environment }
1614
+ };
1615
+ }
1616
+ function assertNoLinkedPathAncestors(path) {
1617
+ const root = parse2(path).root;
1618
+ let current = root;
1619
+ for (const part of path.slice(root.length).split(sep2).filter(Boolean)) {
1620
+ current = join3(current, part);
1621
+ try {
1622
+ if (lstatSync2(current).isSymbolicLink()) throw new Error("Claude Desktop config path contains a link");
1623
+ } catch (error) {
1624
+ if (error instanceof Error && error.message === "Claude Desktop config path contains a link") throw error;
1625
+ if (error.code === "ENOENT") break;
1626
+ throw new Error("Claude Desktop config path is unavailable");
1627
+ }
1628
+ }
1629
+ }
1630
+ function observeDesktop(expected, path, alternateArgs) {
1631
+ assertNoLinkedPathAncestors(path);
1632
+ if (!existsSync3(path)) return { state: "absent", target: registrationTarget(expected), observed: { state: "absent" } };
1633
+ try {
1634
+ const before = lstatSync2(path);
1635
+ if (before.isSymbolicLink() || !before.isFile() || before.size > 4 * 1024 * 1024) {
1636
+ throw new Error("Claude Desktop config is not a safe regular JSON file");
1637
+ }
1638
+ const descriptor = openSync3(path, constants3.O_RDONLY | (constants3.O_NOFOLLOW ?? 0));
1639
+ let parsed;
1640
+ try {
1641
+ const opened = fstatSync2(descriptor);
1642
+ if (!opened.isFile() || opened.size > 4 * 1024 * 1024 || opened.dev !== before.dev || opened.ino !== before.ino) {
1643
+ throw new Error("Claude Desktop config changed during access");
1644
+ }
1645
+ parsed = JSON.parse(readFileSync3(descriptor, "utf8"));
1646
+ const after = lstatSync2(path);
1647
+ if (after.isSymbolicLink() || after.dev !== opened.dev || after.ino !== opened.ino) {
1648
+ throw new Error("Claude Desktop config changed during access");
1649
+ }
1650
+ } finally {
1651
+ closeSync3(descriptor);
1652
+ }
1653
+ const servers = parsed?.mcpServers;
1654
+ if (servers === void 0) return { state: "absent", target: registrationTarget(expected), observed: { state: "absent" } };
1655
+ if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
1656
+ throw new Error("Claude Desktop mcpServers is invalid");
1657
+ }
1658
+ const server = servers["agent-bridge"];
1659
+ if (server === void 0) return { state: "absent", target: registrationTarget(expected), observed: { state: "absent" } };
1660
+ if (!server || typeof server !== "object" || Array.isArray(server) || Object.keys(server).sort().join(",") !== "args,command,env") {
1661
+ throw new Error("Claude Desktop MCP registration cannot be represented safely");
1662
+ }
1663
+ const entry = server;
1664
+ const command = safeNativeExecutableContract(entry.command, "Claude Desktop MCP command");
1665
+ const args = safeDesktopArgs(entry.args, expected.launch.args, alternateArgs);
1666
+ const { environment, extra, extraSha256 } = safeDesktopEnvironment(entry.env);
1667
+ return {
1668
+ state: !extra && exactRegistration(expected, command, args, environment) ? "exact" : "inexact",
1669
+ target: registrationTarget(expected),
1670
+ observed: { state: "present", command, args, env: environment, ...extraSha256 ? { extraSha256 } : {} }
1671
+ };
1672
+ } catch {
1673
+ throw new Error("Claude Desktop MCP registration cannot be represented safely");
1674
+ }
1675
+ }
1676
+ function observeManagedRegistration(metadata, execute = (command, args, context) => spawnSync2(command, args, {
1677
+ encoding: "utf8",
1678
+ cwd: context?.cwd,
1679
+ env: context?.env
1680
+ }), env = process.env, alternateArgs = []) {
1681
+ return metadata.runtime === "codex" ? observeCodex(metadata, execute, env, alternateArgs) : metadata.runtime === "claude-code" ? observeClaudeCode(metadata, execute, env, alternateArgs) : observeDesktop(
1682
+ metadata,
1683
+ metadata.locator.configPath,
1684
+ alternateArgs
1685
+ );
1686
+ }
1687
+ function inspectManagedRegistration(metadata, execute = (command, args, context) => spawnSync2(command, args, {
1688
+ encoding: "utf8",
1689
+ cwd: context?.cwd,
1690
+ env: context?.env
1691
+ }), env = process.env) {
1692
+ try {
1693
+ return observeManagedRegistration(metadata, execute, env).state;
1694
+ } catch {
1695
+ return "inexact";
1696
+ }
1697
+ }
1698
+ var MAX_MANAGED_METADATA_BYTES = 64 * 1024;
1699
+ function strictText(value, field, maximum = 1024) {
1700
+ if (typeof value !== "string" || !value || value.length > maximum || value.trim() !== value || /[\u0000-\u001f\u007f]/.test(value)) {
1701
+ throw new Error(`managed metadata ${field} is invalid`);
1702
+ }
1703
+ return value;
1704
+ }
1705
+ function strictAbsolutePath(value, field) {
1706
+ const path = strictText(value, field, 4096);
1707
+ if (!isAbsolute2(path) || resolve3(path) !== path) throw new Error(`managed metadata ${field} is invalid`);
1708
+ return path;
1709
+ }
1710
+ function strictLocator(runtime2, value) {
1711
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1712
+ throw new Error("managed metadata locator is invalid");
1713
+ }
1714
+ const locator = value;
1715
+ if (runtime2 === "codex") {
1716
+ if (Object.keys(locator).sort().join(",") !== "configPath,kind" || locator.kind !== "codex-profile") throw new Error("managed metadata locator is invalid");
1717
+ const configPath = strictAbsolutePath(locator.configPath, "locator configPath");
1718
+ if (!configPath.endsWith("/config.toml") && !configPath.endsWith("\\config.toml")) {
1719
+ throw new Error("managed metadata locator is invalid");
1720
+ }
1721
+ return { kind: "codex-profile", configPath };
1722
+ }
1723
+ if (runtime2 === "claude-code") {
1724
+ if (Object.keys(locator).sort().join(",") !== "contextPath,kind,scope" || locator.kind !== "claude-code-scope" || !["local", "user", "project"].includes(locator.scope)) {
1725
+ throw new Error("managed metadata locator is invalid");
1726
+ }
1727
+ const scope = locator.scope;
1728
+ if (scope === "user" && locator.contextPath !== null || scope !== "user" && typeof locator.contextPath !== "string") {
1729
+ throw new Error("managed metadata locator is invalid");
1730
+ }
1731
+ return {
1732
+ kind: "claude-code-scope",
1733
+ scope,
1734
+ contextPath: scope === "user" ? null : strictAbsolutePath(locator.contextPath, "locator contextPath")
1735
+ };
1736
+ }
1737
+ if (Object.keys(locator).sort().join(",") !== "configPath,kind" || locator.kind !== "claude-desktop-config") throw new Error("managed metadata locator is invalid");
1738
+ return { kind: "claude-desktop-config", configPath: strictAbsolutePath(locator.configPath, "locator configPath") };
1739
+ }
1740
+ function strictManagedMetadata(value, runtime2, instance) {
1741
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("managed metadata is invalid");
1742
+ const metadata = value;
1743
+ const version = metadata.version;
1744
+ const expectedKeys = version === 2 ? "backendConfigPath,identity,instance,launch,locator,runtime,schema,target,version" : "backendConfigPath,identity,instance,launch,locator,runtime,schema,version";
1745
+ if (Object.keys(metadata).sort().join(",") !== expectedKeys || metadata.schema !== "agent-bridge.client-management" || version !== 1 && version !== 2 || metadata.runtime !== runtime2) throw new Error("managed metadata is invalid");
1746
+ const identity = strictText(metadata.identity, "identity", 128);
1747
+ const recordedInstance = strictText(metadata.instance, "instance", 128);
1748
+ if (recordedInstance !== instance) throw new Error("managed metadata instance does not match its locator");
1749
+ const backendConfigPath = strictAbsolutePath(metadata.backendConfigPath, "backendConfigPath");
1750
+ if (!metadata.launch || typeof metadata.launch !== "object" || Array.isArray(metadata.launch)) {
1751
+ throw new Error("managed metadata launch is invalid");
1752
+ }
1753
+ const launch = metadata.launch;
1754
+ if (Object.keys(launch).sort().join(",") !== "args,command,scope" || !Array.isArray(launch.args) || launch.args.length > 16 || launch.args.some((arg) => {
1755
+ try {
1756
+ strictText(arg, "launch argument", 1024);
1757
+ return false;
1758
+ } catch {
1759
+ return true;
1760
+ }
1761
+ }) || ![null, "local", "user", "project"].includes(launch.scope)) {
1762
+ throw new Error("managed metadata launch is invalid");
1763
+ }
1764
+ const locator = strictLocator(runtime2, metadata.locator);
1765
+ const scope = launch.scope;
1766
+ if (runtime2 !== "claude-code" && scope !== null || runtime2 === "claude-code" && scope !== locator.scope || (runtime2 === "codex" || runtime2 === "claude-code") && launch.args.length !== 0 || runtime2 === "claude-desktop" && !isAbsolute2(strictText(launch.command, "launch command", 4096)) || (runtime2 === "codex" || runtime2 === "claude-code") && !(version === 2 && metadata.target?.transport === "streamable-http") && (() => {
1767
+ try {
1768
+ safeNativeExecutableContract(launch.command, "launch command");
1769
+ return false;
1770
+ } catch {
1771
+ return true;
1772
+ }
1773
+ })()) {
1774
+ throw new Error("managed metadata launch is invalid");
1775
+ }
1776
+ let target;
1777
+ if (version === 2) {
1778
+ if (!metadata.target || typeof metadata.target !== "object" || Array.isArray(metadata.target)) {
1779
+ throw new Error("managed metadata target is invalid");
1780
+ }
1781
+ const candidate = metadata.target;
1782
+ const targetScope = candidate.scope;
1783
+ if (candidate.version !== 1 || ![null, "local", "user", "project"].includes(targetScope)) {
1784
+ throw new Error("managed metadata target is invalid");
1785
+ }
1786
+ if (candidate.transport === "streamable-http") {
1787
+ if (Object.keys(candidate).sort().join(",") !== "scope,transport,url,version" || runtime2 === "claude-desktop" || targetScope !== scope) {
1788
+ throw new Error("managed metadata target is invalid");
1789
+ }
1790
+ target = {
1791
+ version: 1,
1792
+ transport: "streamable-http",
1793
+ url: validateLoopbackMcpUrl(candidate.url, "managed metadata target URL"),
1794
+ scope: targetScope
1795
+ };
1796
+ } else if (candidate.transport === "stdio") {
1797
+ if (Object.keys(candidate).sort().join(",") !== "args,command,scope,transport,version" || !Array.isArray(candidate.args) || candidate.args.length > 16 || candidate.args.some((arg) => {
1798
+ try {
1799
+ strictText(arg, "target argument", 1024);
1800
+ return false;
1801
+ } catch {
1802
+ return true;
1803
+ }
1804
+ })) {
1805
+ throw new Error("managed metadata target is invalid");
1806
+ }
1807
+ target = {
1808
+ version: 1,
1809
+ transport: "stdio",
1810
+ command: strictText(candidate.command, "target command", 4096),
1811
+ args: [...candidate.args],
1812
+ scope: targetScope
1813
+ };
1814
+ if (runtime2 === "claude-desktop") {
1815
+ if (!isAbsolute2(target.command)) throw new Error("managed metadata target is invalid");
1816
+ } else {
1817
+ safeNativeExecutableContract(target.command, "managed metadata target command");
1818
+ }
1819
+ } else {
1820
+ throw new Error("managed metadata target is invalid");
1821
+ }
1822
+ if (!isDeepStrictEqual(targetLaunchProjection(target), {
1823
+ command: strictText(launch.command, "launch command", 4096),
1824
+ args: [...launch.args],
1825
+ scope
1826
+ })) {
1827
+ throw new Error("managed metadata target does not match its launch projection");
1828
+ }
1829
+ }
1830
+ return {
1831
+ schema: "agent-bridge.client-management",
1832
+ version,
1833
+ runtime: runtime2,
1834
+ identity,
1835
+ instance: recordedInstance,
1836
+ backendConfigPath,
1837
+ launch: { command: strictText(launch.command, "launch command", 4096), args: [...launch.args], scope },
1838
+ ...target ? { target } : {},
1839
+ locator
1840
+ };
1841
+ }
1842
+ function loadManagedClientMetadata(runtime2, instance, env = process.env) {
1843
+ const normalizedInstance = strictText(instance, "instance", 128);
1844
+ const path = managedClientMetadataPath(runtime2, normalizedInstance, env);
1845
+ verifyPrivatePathAccess(dirname3(dirname3(path)), "directory");
1846
+ verifyPrivatePathAccess(dirname3(path), "directory");
1847
+ const before = lstatSync2(path);
1848
+ if (before.isSymbolicLink() || !before.isFile() || before.size > MAX_MANAGED_METADATA_BYTES) {
1849
+ throw new Error("managed metadata is invalid");
1850
+ }
1851
+ verifyPrivatePathAccess(path, "file");
1852
+ const descriptor = openSync3(path, constants3.O_RDONLY | (constants3.O_NOFOLLOW ?? 0));
1853
+ try {
1854
+ const opened = fstatSync2(descriptor);
1855
+ if (!opened.isFile() || opened.size > MAX_MANAGED_METADATA_BYTES || opened.dev !== before.dev || opened.ino !== before.ino) {
1856
+ throw new Error("managed metadata changed during access");
1857
+ }
1858
+ const parsed = JSON.parse(readFileSync3(descriptor, "utf8"));
1859
+ const after = lstatSync2(path);
1860
+ if (after.isSymbolicLink() || after.dev !== opened.dev || after.ino !== opened.ino) {
1861
+ throw new Error("managed metadata changed during access");
1862
+ }
1863
+ return strictManagedMetadata(parsed, runtime2, normalizedInstance);
1864
+ } finally {
1865
+ closeSync3(descriptor);
1866
+ }
1867
+ }
1868
+ function readMetadata(path, runtime2, instance) {
1869
+ if (!existsSync3(path)) return void 0;
1870
+ try {
1871
+ return loadManagedClientMetadata(runtime2, instance, { HOME: dirname3(dirname3(dirname3(path))) });
1872
+ } catch {
1873
+ return null;
1874
+ }
1875
+ }
1876
+ function writeManagedClientMetadata(path, metadata) {
1877
+ const directory = dirname3(path);
1878
+ const privateRoot = dirname3(directory);
1879
+ mkdirSync3(privateRoot, { recursive: true, mode: 448 });
1880
+ securePrivatePath(privateRoot, "directory");
1881
+ mkdirSync3(directory, { recursive: true, mode: 448 });
1882
+ securePrivatePath(directory, "directory");
1883
+ const directoryBefore = lstatSync2(directory);
1884
+ if (directoryBefore.isSymbolicLink() || !directoryBefore.isDirectory()) {
1885
+ throw new Error("managed metadata directory is invalid");
1886
+ }
1887
+ const temporary = `${path}.${process.pid}.${randomUUID3()}.tmp`;
1888
+ const descriptor = openSync3(temporary, "wx", 384);
1889
+ try {
1890
+ const opened = fstatSync2(descriptor);
1891
+ securePrivatePath(temporary, "file");
1892
+ const secured = lstatSync2(temporary);
1893
+ if (!opened.isFile() || secured.isSymbolicLink() || !secured.isFile() || secured.dev !== opened.dev || secured.ino !== opened.ino) {
1894
+ throw new Error("managed metadata temporary file changed during access");
1895
+ }
1896
+ writeFileSync3(descriptor, `${JSON.stringify(metadata, null, 2)}
1897
+ `);
1898
+ fsyncSync3(descriptor);
1899
+ } finally {
1900
+ closeSync3(descriptor);
1901
+ }
1902
+ const beforeRename = lstatSync2(directory);
1903
+ const temporaryBeforeRename = lstatSync2(temporary);
1904
+ if (beforeRename.isSymbolicLink() || beforeRename.dev !== directoryBefore.dev || beforeRename.ino !== directoryBefore.ino || temporaryBeforeRename.isSymbolicLink() || !temporaryBeforeRename.isFile()) {
1905
+ throw new Error("managed metadata directory changed before publication");
1906
+ }
1907
+ verifyPrivatePathAccess(temporary, "file");
1908
+ renameSync3(temporary, path);
1909
+ const afterRename = lstatSync2(directory);
1910
+ if (afterRename.isSymbolicLink() || afterRename.dev !== directoryBefore.dev || afterRename.ino !== directoryBefore.ino) {
1911
+ throw new Error("managed metadata directory changed during publication");
1912
+ }
1913
+ verifyPrivatePathAccess(path, "file");
1914
+ if (process.platform !== "win32") {
1915
+ const directoryDescriptor = openSync3(directory, "r");
1916
+ try {
1917
+ fsyncSync3(directoryDescriptor);
1918
+ } finally {
1919
+ closeSync3(directoryDescriptor);
1920
+ }
1921
+ }
1922
+ }
1923
+ function validate(runtime2, identity, options) {
1924
+ if (!identity.trim() || identity.trim().length > 128) throw new Error("--identity is required");
1925
+ if (!options.instance.trim() || options.instance.trim().length > 128) throw new Error("--instance is required");
1926
+ if (!options.backendConfigPath.trim()) throw new Error("--backend-config is required");
1927
+ if (runtime2 === "claude-code" && options.scope && !["local", "user", "project"].includes(options.scope)) {
1928
+ throw new Error("scope must be local, user, or project");
1929
+ }
1930
+ }
1931
+ function backendState(path) {
1932
+ if (!existsSync3(path)) return "absent";
1933
+ try {
1934
+ verifyPrivatePathAccess(dirname3(path), "directory");
1935
+ verifyPrivatePathAccess(path, "file");
1936
+ return statSync2(path).isFile() ? "private-file" : "invalid";
1937
+ } catch {
1938
+ return "invalid";
1939
+ }
1940
+ }
1941
+ function inspectClient(runtime2, identity, options, execute = defaultExecutor) {
1942
+ validate(runtime2, identity, options);
1943
+ const env = options.env ?? process.env;
1944
+ const normalizedIdentity = identity.trim();
1945
+ const expected = expectedManagedClientMetadata(runtime2, normalizedIdentity, options, env);
1946
+ const path = managedClientMetadataPath(runtime2, expected.instance, env);
1947
+ const metadata = readMetadata(path, runtime2, expected.instance);
1948
+ const registration = inspectManagedRegistration(metadata ?? expected, execute, env);
1949
+ const backend = backendState(expected.backendConfigPath);
1950
+ let state;
1951
+ let reason;
1952
+ if (registration === "absent" && metadata === void 0 && backend === "absent") {
1953
+ state = "absent";
1954
+ reason = "registration is absent";
1955
+ } else if (registration === "exact" && backend === "private-file" && metadata === void 0) {
1956
+ state = "unmanaged";
1957
+ reason = "exact registration has no managed metadata";
1958
+ } else if (registration === "exact" && backend === "private-file" && exactManagedTarget(metadata, expected)) {
1959
+ state = "managed";
1960
+ reason = "registration and managed metadata are exact";
1961
+ } else {
1962
+ state = "drifted";
1963
+ reason = backend !== "private-file" ? "backend path or its immediate parent is absent or not owner-private" : "registration or managed metadata differs from the requested contract";
1964
+ }
1965
+ return {
1966
+ schemaVersion: 1,
1967
+ runtime: runtime2,
1968
+ identity: normalizedIdentity,
1969
+ instance: expected.instance,
1970
+ backendConfigPath: expected.backendConfigPath,
1971
+ registrationLocator: expected.locator,
1972
+ metadataPath: path,
1973
+ state,
1974
+ managed: state === "managed",
1975
+ exact: registration === "exact" && backend === "private-file",
1976
+ reason
1977
+ };
1978
+ }
1979
+ function adoptClient(runtime2, identity, options, execute = defaultExecutor) {
1980
+ const inspection = inspectClient(runtime2, identity, options, execute);
1981
+ if (inspection.state === "managed") {
1982
+ return { schemaVersion: 1, action: "none", applied: false, before: "managed", after: "managed", metadataPath: inspection.metadataPath, inspection };
1983
+ }
1984
+ if (inspection.state !== "unmanaged") {
1985
+ throw new Error("only an exact unmanaged registration can be adopted");
1986
+ }
1987
+ if (options.apply) {
1988
+ writeManagedClientMetadata(
1989
+ inspection.metadataPath,
1990
+ expectedManagedClientMetadata(runtime2, inspection.identity, options, options.env ?? process.env)
1991
+ );
1992
+ const postcondition = inspectClient(runtime2, identity, options, execute);
1993
+ if (postcondition.state !== "managed") {
1994
+ throw new Error("client registration changed while adoption was applied");
1995
+ }
1996
+ return {
1997
+ schemaVersion: 1,
1998
+ action: "adopt",
1999
+ applied: true,
2000
+ before: "unmanaged",
2001
+ after: "managed",
2002
+ metadataPath: inspection.metadataPath,
2003
+ inspection: postcondition
2004
+ };
2005
+ }
2006
+ return {
2007
+ schemaVersion: 1,
2008
+ action: "adopt",
2009
+ applied: false,
2010
+ before: "unmanaged",
2011
+ after: "managed",
2012
+ metadataPath: inspection.metadataPath,
2013
+ inspection
2014
+ };
2015
+ }
2016
+
2017
+ export {
2018
+ canonicalTimestamp,
2019
+ canonicalGatewayUrl,
2020
+ enrollmentTokenHash,
2021
+ defaultEnrollmentPath,
2022
+ createPendingEnrollment,
2023
+ readEnrollment,
2024
+ acquireEnrollmentLock,
2025
+ releaseEnrollmentLock,
2026
+ recoverEnrollmentLock,
2027
+ deleteEnrollmentFile,
2028
+ transitionEnrollment,
2029
+ resolveDesktopLaunchContract,
2030
+ stagedGatewayBackendPaths,
2031
+ writeStagedGatewayBackendConfig,
2032
+ installClient,
2033
+ validateLoopbackMcpUrl,
2034
+ managedClientTarget,
2035
+ managedClientMetadataPath,
2036
+ newNativeExecutableContract,
2037
+ assertNoLinkedPathAncestors,
2038
+ observeManagedRegistration,
2039
+ loadManagedClientMetadata,
2040
+ writeManagedClientMetadata,
2041
+ inspectClient,
2042
+ adoptClient
2043
+ };
2044
+ //# sourceMappingURL=chunk-JKKIGIO5.js.map