@creatornader/agent-bridge 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +86 -4
  2. package/README.md +567 -55
  3. package/ROADMAP.md +114 -0
  4. package/SECURITY.md +90 -0
  5. package/SKILL.md +68 -2
  6. package/bin/agent-bridge +2 -2
  7. package/clients/claude-desktop.json +3 -1
  8. package/dist/archive.d.ts +163 -0
  9. package/dist/archive.js +50 -0
  10. package/dist/archive.js.map +1 -0
  11. package/dist/bridge-domain-CBX3IVqC.d.ts +153 -0
  12. package/dist/{bridge-service-CfqMcMAn.d.ts → bridge-service-hX1ZQp4W.d.ts} +15 -5
  13. package/dist/bridge-store-DPVSCwfs.d.ts +102 -0
  14. package/dist/bridge.d.ts +76 -17
  15. package/dist/bridge.js +52 -24
  16. package/dist/chunk-2DFNOLMO.js +618 -0
  17. package/dist/chunk-2DFNOLMO.js.map +1 -0
  18. package/dist/{chunk-KVR4JBRF.js → chunk-2EVLG3YA.js} +2 -2
  19. package/dist/{chunk-KVR4JBRF.js.map → chunk-2EVLG3YA.js.map} +1 -1
  20. package/dist/chunk-45BZGB77.js +709 -0
  21. package/dist/chunk-45BZGB77.js.map +1 -0
  22. package/dist/chunk-4VB44KJU.js +230 -0
  23. package/dist/chunk-4VB44KJU.js.map +1 -0
  24. package/dist/chunk-5GMB3A77.js +1013 -0
  25. package/dist/chunk-5GMB3A77.js.map +1 -0
  26. package/dist/chunk-A6W4QALF.js +1258 -0
  27. package/dist/chunk-A6W4QALF.js.map +1 -0
  28. package/dist/chunk-BZLMF5O5.js +328 -0
  29. package/dist/chunk-BZLMF5O5.js.map +1 -0
  30. package/dist/{chunk-AXL2I7FH.js → chunk-CEXBMOI5.js} +240 -79
  31. package/dist/chunk-CEXBMOI5.js.map +1 -0
  32. package/dist/chunk-GADH244D.js +527 -0
  33. package/dist/chunk-GADH244D.js.map +1 -0
  34. package/dist/chunk-MBPZXTJA.js +2428 -0
  35. package/dist/chunk-MBPZXTJA.js.map +1 -0
  36. package/dist/chunk-NCO7RLBM.js +146 -0
  37. package/dist/chunk-NCO7RLBM.js.map +1 -0
  38. package/dist/chunk-Q6PPMXJC.js +1050 -0
  39. package/dist/chunk-Q6PPMXJC.js.map +1 -0
  40. package/dist/{chunk-4KAOBC5X.js → chunk-SPSMZOCJ.js} +190 -165
  41. package/dist/chunk-SPSMZOCJ.js.map +1 -0
  42. package/dist/chunk-UAHMP6GC.js +54 -0
  43. package/dist/chunk-UAHMP6GC.js.map +1 -0
  44. package/dist/{chunk-YD6YJATN.js → chunk-VOQIDTCJ.js} +4 -2
  45. package/dist/chunk-VOQIDTCJ.js.map +1 -0
  46. package/dist/chunk-WAI2IYIQ.js +554 -0
  47. package/dist/chunk-WAI2IYIQ.js.map +1 -0
  48. package/dist/cli.d.ts +2 -1
  49. package/dist/cli.js +3555 -214
  50. package/dist/cli.js.map +1 -1
  51. package/dist/client-config.d.ts +1 -1
  52. package/dist/client-config.js +1 -1
  53. package/dist/client-runtime.d.ts +10 -6
  54. package/dist/client-runtime.js +9 -6
  55. package/dist/contracts/generate.d.ts +2 -0
  56. package/dist/contracts/generate.js +247 -0
  57. package/dist/contracts/generate.js.map +1 -0
  58. package/dist/dr.d.ts +324 -0
  59. package/dist/dr.js +124 -0
  60. package/dist/dr.js.map +1 -0
  61. package/dist/gateway-CggQ8vTC.d.ts +78 -0
  62. package/dist/gateway-main.js +190 -8
  63. package/dist/gateway-main.js.map +1 -1
  64. package/dist/gateway.d.ts +4 -3
  65. package/dist/gateway.js +4 -3
  66. package/dist/index.js +11 -8
  67. package/dist/index.js.map +1 -1
  68. package/dist/postgres-bridge-store-Bfd3hRId.d.ts +49 -0
  69. package/dist/server.d.ts +3 -2
  70. package/dist/server.js +13 -8
  71. package/dist/sqlite.d.ts +94 -12
  72. package/dist/sqlite.js +7 -4
  73. package/docs/architecture-v2.md +394 -0
  74. package/docs/decisions/0001-protocol-layers-and-acknowledgment-semantics.md +85 -0
  75. package/docs/decisions/0002-canonical-operation-contract-registry.md +46 -0
  76. package/docs/decisions/0003-host-adapters-and-consumer-instance-keys.md +88 -0
  77. package/docs/ecosystem.md +86 -0
  78. package/docs/postmortems/2026-07-08-wrapper-source-drift.md +82 -0
  79. package/docs/troubleshooting.md +89 -0
  80. package/llms.txt +107 -6
  81. package/openapi/agent-bridge-v2.openapi.json +9610 -0
  82. package/package.json +24 -4
  83. package/schemas/agent-bridge-v2.capabilities.json +1829 -0
  84. package/schemas/agent-bridge-v2.mcp.json +2074 -0
  85. package/schemas/agent-bridge-v2.schema.json +4560 -0
  86. package/sql/migrations/008_message_projects.sql +36 -0
  87. package/sql/migrations/009_mailbox_query_indexes.sql +28 -0
  88. package/sql/migrations/010_publisher_delivery_policy.sql +210 -0
  89. package/sql/migrations/011_credential_security.sql +875 -0
  90. package/sql/migrations/012_request_authority.sql +177 -0
  91. package/sql/migrations/013_row_isolation.sql +495 -0
  92. package/sql/migrations/014_owner_control_plane.sql +1641 -0
  93. package/sql/migrations/015_portable_archives.sql +1813 -0
  94. package/sql/migrations/016_native_dr_fence.sql +1005 -0
  95. package/bin/CLAUDE.md +0 -11
  96. package/dist/bridge-domain-BcnBeHOn.d.ts +0 -97
  97. package/dist/bridge-store-BuGjxTn4.d.ts +0 -61
  98. package/dist/chunk-4KAOBC5X.js.map +0 -1
  99. package/dist/chunk-6MAZ6YLZ.js +0 -146
  100. package/dist/chunk-6MAZ6YLZ.js.map +0 -1
  101. package/dist/chunk-AXL2I7FH.js.map +0 -1
  102. package/dist/chunk-BV6W4AJK.js +0 -211
  103. package/dist/chunk-BV6W4AJK.js.map +0 -1
  104. package/dist/chunk-CKSKUBDQ.js +0 -153
  105. package/dist/chunk-CKSKUBDQ.js.map +0 -1
  106. package/dist/chunk-JOUBDRYX.js +0 -272
  107. package/dist/chunk-JOUBDRYX.js.map +0 -1
  108. package/dist/chunk-JTC5YURG.js +0 -49
  109. package/dist/chunk-JTC5YURG.js.map +0 -1
  110. package/dist/chunk-O26W5QHS.js +0 -409
  111. package/dist/chunk-O26W5QHS.js.map +0 -1
  112. package/dist/chunk-WEULPMUY.js +0 -988
  113. package/dist/chunk-WEULPMUY.js.map +0 -1
  114. package/dist/chunk-YD6YJATN.js.map +0 -1
  115. package/dist/gateway-BULozv3K.d.ts +0 -54
package/dist/cli.js CHANGED
@@ -1,82 +1,997 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- runMigrations
4
- } from "./chunk-6MAZ6YLZ.js";
3
+ PortableArchiveError,
4
+ PortableArchiveFile,
5
+ PostgresPortableArchiveStore,
6
+ SQLitePortableArchiveStore,
7
+ streamPortableArchive,
8
+ validatePortableArchiveRequestId
9
+ } from "./chunk-A6W4QALF.js";
5
10
  import {
6
11
  readClientConfigFile,
7
12
  resolveClientConfig
8
- } from "./chunk-KVR4JBRF.js";
13
+ } from "./chunk-2EVLG3YA.js";
9
14
  import {
10
15
  createClientRuntime
11
- } from "./chunk-JTC5YURG.js";
16
+ } from "./chunk-UAHMP6GC.js";
12
17
  import {
13
18
  BridgeHttpError,
14
19
  LegacySupabaseError,
15
20
  legacyContextMetadata,
16
21
  legacyNumericMessageId
17
- } from "./chunk-AXL2I7FH.js";
18
- import "./chunk-WEULPMUY.js";
19
- import "./chunk-YD6YJATN.js";
20
- import "./chunk-CKSKUBDQ.js";
21
- import "./chunk-BV6W4AJK.js";
22
+ } from "./chunk-CEXBMOI5.js";
23
+ import "./chunk-Q6PPMXJC.js";
24
+ import {
25
+ NativeDrBundleError,
26
+ NativeDrBundleReader,
27
+ NativeDrCommandError,
28
+ PostgresNativeDrError,
29
+ backupLocalSqlite,
30
+ backupPostgresNativeDr,
31
+ restoreLocalSqlite,
32
+ restorePostgresNativeDr,
33
+ validateNativeDrId,
34
+ validateNativeDrTimeout,
35
+ verifyNativeDrBundle,
36
+ verifyPostgresNativeDrArtifacts,
37
+ writeNativeDrBundle
38
+ } from "./chunk-MBPZXTJA.js";
39
+ import "./chunk-5GMB3A77.js";
40
+ import {
41
+ PrivatePathError,
42
+ preparePrivateFileLocation,
43
+ securePrivatePath,
44
+ verifyPrivatePathAccess
45
+ } from "./chunk-NCO7RLBM.js";
46
+ import {
47
+ loadMigrationPlan,
48
+ runMigrations
49
+ } from "./chunk-45BZGB77.js";
50
+ import "./chunk-VOQIDTCJ.js";
51
+ import "./chunk-4VB44KJU.js";
52
+ import {
53
+ capabilityDocument,
54
+ operationForCli,
55
+ parseCliResponse,
56
+ validateRequest
57
+ } from "./chunk-GADH244D.js";
58
+ import "./chunk-BZLMF5O5.js";
22
59
 
23
60
  // src/cli.ts
24
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
25
- import { dirname as dirname2, join as join2 } from "path";
26
- import { homedir as homedir2, tmpdir } from "os";
61
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
62
+ import { createHash as createHash4 } from "crypto";
63
+ import { basename, dirname as dirname5, join as join5 } from "path";
64
+ import { homedir as homedir4, tmpdir } from "os";
27
65
  import { mkdtempSync } from "fs";
28
- import { fileURLToPath, pathToFileURL } from "url";
29
- import pg from "pg";
66
+ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
67
+ import pg3 from "pg";
68
+
69
+ // src/legacy-project-reconciliation.ts
70
+ var lock = `LOCK TABLE public.shared_context IN SHARE MODE;
71
+ LOCK TABLE agent_bridge.messages IN ACCESS EXCLUSIVE MODE;
72
+ LOCK TABLE agent_bridge.receipts IN SHARE ROW EXCLUSIVE MODE;
73
+ LOCK TABLE agent_bridge.deliveries IN SHARE MODE`;
74
+ function preflight(migrationChecksum) {
75
+ return `DO $reconcile$ BEGIN
76
+ IF to_regclass('public.shared_context') IS NULL THEN
77
+ RAISE EXCEPTION 'public.shared_context does not exist';
78
+ END IF;
79
+ IF NOT EXISTS (
80
+ SELECT 1 FROM agent_bridge.schema_migrations
81
+ WHERE version=8
82
+ AND name='message_projects'
83
+ AND checksum='${migrationChecksum}'
84
+ ) THEN
85
+ RAISE EXCEPTION 'migration 008_message_projects does not match the expected migration plan';
86
+ END IF;
87
+ IF NOT EXISTS (
88
+ SELECT 1 FROM information_schema.columns
89
+ WHERE table_schema='agent_bridge'
90
+ AND table_name='messages'
91
+ AND column_name='project'
92
+ AND data_type='text'
93
+ ) THEN
94
+ RAISE EXCEPTION 'agent_bridge.messages.project is missing or has the wrong type';
95
+ END IF;
96
+ IF NOT EXISTS (
97
+ SELECT 1 FROM pg_constraint
98
+ WHERE conrelid='agent_bridge.messages'::regclass
99
+ AND conname='messages_project_label'
100
+ AND contype='c'
101
+ AND convalidated
102
+ ) THEN
103
+ RAISE EXCEPTION 'messages_project_label constraint is missing or invalid';
104
+ END IF;
105
+ IF NOT EXISTS (
106
+ SELECT 1 FROM pg_index index_state
107
+ JOIN pg_class index_class ON index_class.oid=index_state.indexrelid
108
+ WHERE index_state.indrelid='agent_bridge.messages'::regclass
109
+ AND index_class.relname='messages_project'
110
+ AND index_state.indisvalid
111
+ AND index_state.indisready
112
+ ) THEN
113
+ RAISE EXCEPTION 'messages_project index is missing or invalid';
114
+ END IF;
115
+ IF NOT EXISTS (
116
+ SELECT 1 FROM agent_bridge.workspaces WHERE id='agent-bridge'
117
+ ) THEN
118
+ RAISE EXCEPTION 'canonical workspace agent-bridge must be provisioned before reconciliation';
119
+ END IF;
120
+ IF EXISTS (
121
+ SELECT 1 FROM public.shared_context
122
+ WHERE project IS NOT NULL
123
+ AND (project<>btrim(project) OR char_length(project) NOT BETWEEN 1 AND 128)
124
+ ) THEN
125
+ RAISE EXCEPTION 'legacy shared_context contains invalid project labels';
126
+ END IF;
127
+ IF EXISTS (
128
+ SELECT agent_bridge.legacy_message_uuid(id, metadata)
129
+ FROM public.shared_context GROUP BY 1 HAVING count(*)>1
130
+ ) THEN
131
+ RAISE EXCEPTION 'legacy shared_context contains duplicate message IDs';
132
+ END IF;
133
+ IF (
134
+ SELECT count(*) FROM public.shared_context context
135
+ JOIN agent_bridge.messages message
136
+ ON message.id=agent_bridge.legacy_message_uuid(context.id, context.metadata)
137
+ ) <> (SELECT count(*) FROM public.shared_context) THEN
138
+ RAISE EXCEPTION 'legacy shared_context and v2 message counts do not match';
139
+ END IF;
140
+ IF EXISTS (
141
+ SELECT 1 FROM public.shared_context context
142
+ JOIN agent_bridge.messages message
143
+ ON message.id=agent_bridge.legacy_message_uuid(context.id, context.metadata)
144
+ WHERE NOT (
145
+ (message.workspace=coalesce(context.project,'legacy') AND message.project IS NULL)
146
+ OR (message.workspace='agent-bridge' AND message.project IS NOT DISTINCT FROM context.project)
147
+ )
148
+ ) THEN
149
+ RAISE EXCEPTION 'legacy message has a conflicting workspace or project';
150
+ END IF;
151
+ IF EXISTS (
152
+ SELECT 1 FROM public.shared_context context
153
+ JOIN agent_bridge.messages message
154
+ ON message.id=agent_bridge.legacy_message_uuid(context.id, context.metadata)
155
+ WHERE message.source IS DISTINCT FROM context.source
156
+ OR message.type IS DISTINCT FROM coalesce(
157
+ context.metadata #>> '{message_envelope,kind}', context.category
158
+ )
159
+ OR message.content IS DISTINCT FROM context.content
160
+ OR message.content_type IS DISTINCT FROM coalesce(
161
+ context.metadata #>> '{message_envelope,payload_mime}', 'text/plain'
162
+ )
163
+ OR message.data IS DISTINCT FROM context.metadata #> '{message_envelope,payload}'
164
+ OR message.targets IS DISTINCT FROM CASE
165
+ WHEN jsonb_typeof(context.metadata #> '{message_envelope,target_agents}')='array'
166
+ THEN context.metadata #> '{message_envelope,target_agents}'
167
+ ELSE '[]'::jsonb
168
+ END
169
+ OR message.thread_id IS DISTINCT FROM context.metadata #>> '{message_envelope,thread_id}'
170
+ OR message.reply_to_id IS DISTINCT FROM context.metadata #>> '{message_envelope,reply_to_id}'
171
+ OR message.correlation_id IS DISTINCT FROM context.metadata #>> '{message_envelope,correlation_id}'
172
+ OR message.causation_id IS DISTINCT FROM context.metadata #>> '{message_envelope,causation_id}'
173
+ OR message.priority IS DISTINCT FROM CASE
174
+ WHEN context.priority IN ('info','high','urgent') THEN context.priority ELSE 'info'
175
+ END
176
+ OR message.expires_at IS DISTINCT FROM agent_bridge.safe_timestamptz(
177
+ context.metadata #>> '{message_envelope,expires_at}'
178
+ )
179
+ OR message.idempotency_key IS DISTINCT FROM context.metadata #>> '{message_envelope,idempotency_key}'
180
+ OR message.atrib_receipt_id IS DISTINCT FROM coalesce(
181
+ context.atrib_receipt_id,
182
+ context.metadata #>> '{message_envelope,atrib_receipt_id}'
183
+ )
184
+ OR message.informed_by IS DISTINCT FROM context.metadata #> '{message_envelope,informed_by}'
185
+ OR message.metadata IS DISTINCT FROM context.metadata
186
+ OR message.created_at IS DISTINCT FROM context.created_at
187
+ ) THEN
188
+ RAISE EXCEPTION 'legacy message content differs from migration 006 import';
189
+ END IF;
190
+ IF EXISTS (
191
+ SELECT 1 FROM public.shared_context context
192
+ JOIN agent_bridge.messages message
193
+ ON message.id=agent_bridge.legacy_message_uuid(context.id, context.metadata)
194
+ JOIN agent_bridge.deliveries delivery
195
+ ON delivery.workspace=message.workspace AND delivery.message_id=message.id
196
+ ) THEN
197
+ RAISE EXCEPTION 'legacy messages with deliveries cannot be reconciled';
198
+ END IF;
199
+ IF EXISTS (
200
+ WITH candidates AS (
201
+ SELECT message.id, message.source, message.idempotency_key
202
+ FROM public.shared_context context
203
+ JOIN agent_bridge.messages message
204
+ ON message.id=agent_bridge.legacy_message_uuid(context.id, context.metadata)
205
+ WHERE message.idempotency_key IS NOT NULL
206
+ ), canonical AS (
207
+ SELECT candidate.id, candidate.source, candidate.idempotency_key FROM candidates candidate
208
+ UNION ALL
209
+ SELECT existing.id, existing.source, existing.idempotency_key
210
+ FROM agent_bridge.messages existing
211
+ WHERE existing.workspace='agent-bridge'
212
+ AND existing.idempotency_key IS NOT NULL
213
+ AND NOT EXISTS (SELECT 1 FROM candidates candidate WHERE candidate.id=existing.id)
214
+ )
215
+ SELECT source, idempotency_key FROM canonical
216
+ GROUP BY source, idempotency_key HAVING count(*)>1
217
+ ) THEN
218
+ RAISE EXCEPTION 'legacy idempotency key collides in canonical workspace';
219
+ END IF;
220
+ END $reconcile$`;
221
+ }
222
+ var counts = `SELECT
223
+ count(*)::int AS messages,
224
+ count(*) FILTER (
225
+ WHERE message.workspace<>'agent-bridge'
226
+ OR message.project IS DISTINCT FROM context.project
227
+ )::int AS changed,
228
+ coalesce(sum((
229
+ SELECT count(*) FROM agent_bridge.receipts receipt
230
+ WHERE receipt.workspace=message.workspace AND receipt.message_id=message.id
231
+ )),0)::int AS receipts
232
+ FROM public.shared_context context
233
+ JOIN agent_bridge.messages message
234
+ ON message.id=agent_bridge.legacy_message_uuid(context.id, context.metadata)`;
235
+ function report(mode, row) {
236
+ return {
237
+ mode,
238
+ workspace: "agent-bridge",
239
+ messages: Number(row?.messages ?? 0),
240
+ receipts: Number(row?.receipts ?? 0),
241
+ deliveries: 0,
242
+ changed: Number(row?.changed ?? 0)
243
+ };
244
+ }
245
+ async function reconcileLegacyProjects(db, options) {
246
+ if (!/^[0-9a-f]{64}$/.test(options.migrationChecksum)) {
247
+ throw new Error("migrationChecksum must be 64 lowercase hexadecimal characters");
248
+ }
249
+ const apply = options.apply === true;
250
+ const verify = preflight(options.migrationChecksum);
251
+ await db.query("BEGIN");
252
+ try {
253
+ await db.query("SELECT pg_advisory_xact_lock(1646705660)");
254
+ await db.query(verify);
255
+ if (apply) {
256
+ await db.query(lock);
257
+ await db.query(verify);
258
+ }
259
+ const before = await db.query(counts);
260
+ const summary = report(apply ? "apply" : "dry-run", before.rows[0]);
261
+ if (!apply) {
262
+ await db.query("ROLLBACK");
263
+ return summary;
264
+ }
265
+ const totals = await db.query(
266
+ `SELECT
267
+ (SELECT count(*)::int FROM agent_bridge.messages) AS messages,
268
+ (SELECT count(*)::int FROM agent_bridge.receipts) AS receipts`
269
+ );
270
+ await db.query(`CREATE TEMP TABLE reconcile_receipts ON COMMIT DROP AS
271
+ SELECT receipt.* FROM agent_bridge.receipts receipt
272
+ JOIN public.shared_context context
273
+ ON receipt.message_id=agent_bridge.legacy_message_uuid(context.id,context.metadata)
274
+ AND receipt.workspace=coalesce(context.project,'legacy')`);
275
+ await db.query(`DELETE FROM agent_bridge.receipts receipt
276
+ USING reconcile_receipts saved
277
+ WHERE receipt.workspace=saved.workspace
278
+ AND receipt.message_id=saved.message_id
279
+ AND receipt.principal=saved.principal`);
280
+ await db.query("ALTER TABLE agent_bridge.messages DISABLE TRIGGER messages_immutable");
281
+ await db.query(`UPDATE agent_bridge.messages message
282
+ SET workspace='agent-bridge', project=context.project
283
+ FROM public.shared_context context
284
+ WHERE message.id=agent_bridge.legacy_message_uuid(context.id,context.metadata)
285
+ AND (message.workspace<>'agent-bridge' OR message.project IS DISTINCT FROM context.project)`);
286
+ await db.query("ALTER TABLE agent_bridge.messages ENABLE TRIGGER messages_immutable");
287
+ await db.query(`INSERT INTO agent_bridge.receipts (workspace,message_id,principal,read_at)
288
+ SELECT 'agent-bridge',message_id,principal,read_at FROM reconcile_receipts`);
289
+ const verified = await db.query(`SELECT
290
+ (SELECT count(*)::int FROM agent_bridge.messages) AS messages,
291
+ (SELECT count(*)::int FROM agent_bridge.receipts) AS receipts,
292
+ (SELECT count(*)::int FROM public.shared_context context
293
+ JOIN agent_bridge.messages message
294
+ ON message.id=agent_bridge.legacy_message_uuid(context.id,context.metadata)
295
+ WHERE message.workspace<>'agent-bridge'
296
+ OR message.project IS DISTINCT FROM context.project) AS mismatches,
297
+ (SELECT count(*)::int FROM public.shared_context context
298
+ JOIN agent_bridge.deliveries delivery
299
+ ON delivery.message_id=agent_bridge.legacy_message_uuid(context.id,context.metadata)) AS deliveries`);
300
+ const expected = totals.rows[0];
301
+ const actual = verified.rows[0];
302
+ if (!expected || !actual || Number(actual.messages) !== Number(expected.messages) || Number(actual.receipts) !== Number(expected.receipts) || Number(actual.mismatches) !== 0 || Number(actual.deliveries) !== 0) {
303
+ throw new Error("legacy project reconciliation verification failed");
304
+ }
305
+ await db.query("COMMIT");
306
+ return summary;
307
+ } catch (error) {
308
+ await db.query("ROLLBACK").catch(() => void 0);
309
+ throw error;
310
+ }
311
+ }
30
312
 
31
313
  // src/client-installer.ts
32
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
33
- import { createHash, randomUUID } from "crypto";
34
- import { homedir } from "os";
35
- import { dirname, join } from "path";
314
+ import {
315
+ accessSync,
316
+ closeSync as closeSync2,
317
+ constants as constants2,
318
+ existsSync as existsSync2,
319
+ fsyncSync as fsyncSync2,
320
+ mkdirSync as mkdirSync2,
321
+ openSync as openSync2,
322
+ readFileSync as readFileSync2,
323
+ renameSync as renameSync2,
324
+ rmSync as rmSync2,
325
+ statSync,
326
+ writeFileSync as writeFileSync2
327
+ } from "fs";
328
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
329
+ import { homedir as homedir2 } from "os";
330
+ import { delimiter, dirname as dirname2, extname, isAbsolute, join as join2, resolve as resolve2 } from "path";
36
331
  import { spawnSync } from "child_process";
332
+ import { fileURLToPath } from "url";
333
+
334
+ // src/enrollment-file.ts
335
+ import {
336
+ closeSync,
337
+ constants,
338
+ existsSync,
339
+ fstatSync,
340
+ fsyncSync,
341
+ lstatSync,
342
+ mkdirSync,
343
+ openSync,
344
+ readFileSync,
345
+ realpathSync,
346
+ renameSync,
347
+ rmSync,
348
+ writeFileSync
349
+ } from "fs";
350
+ import { createHash, randomUUID } from "crypto";
351
+ import { homedir, hostname } from "os";
352
+ import { dirname, join, parse, relative, resolve, sep } from "path";
353
+ var ENROLLMENT_SCHEMA = "agent-bridge.enrollment";
354
+ var ENROLLMENT_VERSION = 1;
355
+ function object(value, label) {
356
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
357
+ throw new Error(label + " must be an object");
358
+ }
359
+ return value;
360
+ }
361
+ function text(value, label, maximum = 128) {
362
+ if (typeof value !== "string" || !value || value !== value.trim() || value.length > maximum || /[\u0000-\u001f\u007f]/.test(value)) {
363
+ throw new Error(label + " is invalid");
364
+ }
365
+ return value;
366
+ }
367
+ function nullableText(value, label) {
368
+ return value === null ? null : text(value, label);
369
+ }
370
+ var RFC3339 = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?(Z|([+-])(\d{2}):(\d{2}))$/;
371
+ function canonicalTimestamp(value, label) {
372
+ if (value === null) return null;
373
+ const selected = text(value, label, 64);
374
+ const match = RFC3339.exec(selected);
375
+ if (!match) throw new Error(label + " must be RFC3339 with a timezone");
376
+ const [
377
+ ,
378
+ yearText,
379
+ monthText,
380
+ dayText,
381
+ hourText,
382
+ minuteText,
383
+ secondText,
384
+ fraction = "",
385
+ zone,
386
+ sign,
387
+ offsetHourText = "0",
388
+ offsetMinuteText = "0"
389
+ ] = match;
390
+ const year = Number(yearText);
391
+ const month = Number(monthText);
392
+ const day = Number(dayText);
393
+ const hour = Number(hourText);
394
+ const minute = Number(minuteText);
395
+ const second = Number(secondText);
396
+ const millisecond = Number(fraction.padEnd(3, "0"));
397
+ const offsetHour = Number(offsetHourText);
398
+ const offsetMinute = Number(offsetMinuteText);
399
+ if (hour > 23 || minute > 59 || second > 59 || offsetHour > 23 || offsetMinute > 59) {
400
+ throw new Error(label + " is invalid");
401
+ }
402
+ const wall = /* @__PURE__ */ new Date(0);
403
+ wall.setUTCFullYear(year, month - 1, day);
404
+ wall.setUTCHours(hour, minute, second, millisecond);
405
+ if (wall.getUTCFullYear() !== year || wall.getUTCMonth() !== month - 1 || wall.getUTCDate() !== day || wall.getUTCHours() !== hour || wall.getUTCMinutes() !== minute || wall.getUTCSeconds() !== second || wall.getUTCMilliseconds() !== millisecond) {
406
+ throw new Error(label + " is invalid");
407
+ }
408
+ const direction = zone === "Z" ? 0 : sign === "+" ? 1 : -1;
409
+ const instant2 = new Date(wall.getTime() - direction * (offsetHour * 60 + offsetMinute) * 6e4);
410
+ if (!Number.isFinite(instant2.getTime())) throw new Error(label + " is invalid");
411
+ return instant2.toISOString();
412
+ }
413
+ function canonicalGatewayUrl(value, label) {
414
+ const selected = text(value, label, 2048);
415
+ let parsed;
416
+ try {
417
+ parsed = new URL(selected);
418
+ } catch {
419
+ throw new Error(label + " is invalid");
420
+ }
421
+ if (parsed.username || parsed.password) throw new Error(label + " cannot contain credentials");
422
+ if (parsed.hash) throw new Error(label + " cannot contain a fragment");
423
+ const loopback = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]";
424
+ if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && loopback)) {
425
+ throw new Error(label + " must use HTTPS, or HTTP on an explicit loopback host");
426
+ }
427
+ return parsed.toString();
428
+ }
429
+ function uuid(value, label) {
430
+ const selected = text(value, label, 36);
431
+ 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)) {
432
+ throw new Error(label + " is invalid");
433
+ }
434
+ return selected;
435
+ }
436
+ function runtime(value) {
437
+ if (value !== "codex" && value !== "claude-code" && value !== "claude-desktop") {
438
+ throw new Error("enrollment runtime is invalid");
439
+ }
440
+ return value;
441
+ }
442
+ function validateInput(value, operation) {
443
+ const input = object(value, "enrollment input");
444
+ const gatewayUrl = canonicalGatewayUrl(input.gatewayUrl, "enrollment gatewayUrl");
445
+ const credentialId = input.credentialId === null ? null : uuid(input.credentialId, "enrollment credentialId");
446
+ const workspaceName = nullableText(input.workspaceName, "enrollment workspaceName");
447
+ const graceUntil = canonicalTimestamp(input.graceUntil, "enrollment graceUntil");
448
+ const invalidateImmediately = input.invalidateImmediately;
449
+ if (typeof invalidateImmediately !== "boolean") throw new Error("enrollment invalidateImmediately is invalid");
450
+ if (operation === "provision" && (credentialId !== null || workspaceName === null || graceUntil !== null || invalidateImmediately)) {
451
+ throw new Error("provision enrollment input is invalid");
452
+ }
453
+ if (operation === "rotate" && (credentialId === null || workspaceName !== null || graceUntil === null === !invalidateImmediately)) {
454
+ throw new Error("rotation must choose graceUntil or invalidateImmediately");
455
+ }
456
+ return {
457
+ gatewayUrl,
458
+ workspaceId: text(input.workspaceId, "enrollment workspaceId"),
459
+ principal: text(input.principal, "enrollment principal"),
460
+ runtime: runtime(input.runtime),
461
+ instance: text(input.instance, "enrollment instance"),
462
+ credentialId,
463
+ workspaceName,
464
+ displayName: nullableText(input.displayName, "enrollment displayName"),
465
+ runtimeType: text(input.runtimeType, "enrollment runtimeType"),
466
+ label: nullableText(input.label, "enrollment label"),
467
+ scopeSetName: text(input.scopeSetName, "enrollment scopeSetName"),
468
+ expiresAt: canonicalTimestamp(input.expiresAt, "enrollment expiresAt"),
469
+ graceUntil,
470
+ invalidateImmediately
471
+ };
472
+ }
473
+ function validateResult(value, input) {
474
+ if (value === null) return null;
475
+ const result = object(value, "enrollment result");
476
+ if (typeof result.replayed !== "boolean") throw new Error("enrollment result replayed is invalid");
477
+ const validated = {
478
+ workspaceId: text(result.workspaceId, "enrollment result workspaceId"),
479
+ principal: text(result.principal, "enrollment result principal"),
480
+ agentId: result.agentId === null ? null : uuid(result.agentId, "enrollment result agentId"),
481
+ credentialId: uuid(result.credentialId, "enrollment result credentialId"),
482
+ replayed: result.replayed
483
+ };
484
+ if (validated.workspaceId !== input.workspaceId || validated.principal !== input.principal) {
485
+ throw new Error("enrollment result does not match its workspace and principal");
486
+ }
487
+ return validated;
488
+ }
489
+ function enrollmentTokenHash(enrollment2) {
490
+ if (!enrollment2.token) throw new Error("enrollment no longer contains a credential");
491
+ return createHash("sha256").update(enrollment2.token, "utf8").digest("hex");
492
+ }
493
+ function validateEnrollmentFile(value) {
494
+ const file = object(value, "enrollment file");
495
+ if (file.schema !== ENROLLMENT_SCHEMA || file.version !== ENROLLMENT_VERSION) {
496
+ throw new Error("unsupported enrollment schema or version");
497
+ }
498
+ if (file.provider !== "gateway") throw new Error("enrollment provider must be gateway");
499
+ if (!["pending", "ready", "consuming", "consumed"].includes(String(file.state))) {
500
+ throw new Error("enrollment state is invalid");
501
+ }
502
+ if (file.operation !== "provision" && file.operation !== "rotate") {
503
+ throw new Error("enrollment operation is invalid");
504
+ }
505
+ const state = file.state;
506
+ if (!Number.isSafeInteger(file.revision) || Number(file.revision) < 0) {
507
+ throw new Error("enrollment revision is invalid");
508
+ }
509
+ const input = validateInput(file.input, file.operation);
510
+ const token = file.token === null ? null : text(file.token, "enrollment token", 4096);
511
+ const result = validateResult(file.result, input);
512
+ const completedAt = canonicalTimestamp(file.completedAt, "enrollment completedAt");
513
+ if (state === "pending" && (result !== null || completedAt !== null || token === null)) {
514
+ throw new Error("pending enrollment state is invalid");
515
+ }
516
+ if ((state === "ready" || state === "consuming") && (result === null || completedAt === null || token === null)) {
517
+ throw new Error(state + " enrollment state is invalid");
518
+ }
519
+ if (state === "consumed" && (result === null || completedAt === null || token !== null)) {
520
+ throw new Error("consumed enrollment state is invalid");
521
+ }
522
+ return {
523
+ schema: ENROLLMENT_SCHEMA,
524
+ version: ENROLLMENT_VERSION,
525
+ provider: "gateway",
526
+ revision: Number(file.revision),
527
+ state,
528
+ operation: file.operation,
529
+ requestId: uuid(file.requestId, "enrollment requestId"),
530
+ createdAt: (() => {
531
+ const createdAt = canonicalTimestamp(file.createdAt, "enrollment createdAt");
532
+ if (createdAt === null) throw new Error("enrollment createdAt is invalid");
533
+ return createdAt;
534
+ })(),
535
+ completedAt,
536
+ input,
537
+ token,
538
+ result
539
+ };
540
+ }
541
+ function syncDirectory(path) {
542
+ if (process.platform === "win32") return;
543
+ const descriptor = openSync(path, "r");
544
+ try {
545
+ fsyncSync(descriptor);
546
+ } finally {
547
+ closeSync(descriptor);
548
+ }
549
+ }
550
+ var durabilityDefaults = { syncDirectory };
551
+ function prepareDirectory(path) {
552
+ mkdirSync(path, { recursive: true, mode: 448 });
553
+ securePrivatePath(path, "directory");
554
+ }
555
+ function enrollmentRoot(env = process.env) {
556
+ if (env.AGENT_BRIDGE_ENROLLMENT_DIR) return resolve(env.AGENT_BRIDGE_ENROLLMENT_DIR);
557
+ const home2 = realpathSync(env.HOME ?? homedir());
558
+ return join(home2, ".agent-bridge", "enrollments");
559
+ }
560
+ function rejectSymlinkComponents(path) {
561
+ const root = parse(path).root;
562
+ let current = root;
563
+ for (const component of path.slice(root.length).split(sep).filter(Boolean)) {
564
+ current = join(current, component);
565
+ if (!existsSync(current)) break;
566
+ if (lstatSync(current).isSymbolicLink()) {
567
+ throw new Error("enrollment paths cannot contain symbolic links");
568
+ }
569
+ }
570
+ }
571
+ function verifyPrivateDirectory(path) {
572
+ rejectSymlinkComponents(path);
573
+ const details = lstatSync(path);
574
+ if (!details.isDirectory() || details.isSymbolicLink()) {
575
+ throw new Error("enrollment directory must be a real directory");
576
+ }
577
+ verifyPrivatePathAccess(path, "directory");
578
+ return { device: details.dev, inode: details.ino };
579
+ }
580
+ function sameDirectory(path, expected) {
581
+ const actual = verifyPrivateDirectory(path);
582
+ if (actual.device !== expected.device || actual.inode !== expected.inode) {
583
+ throw new Error("enrollment directory changed during the operation");
584
+ }
585
+ }
586
+ function verifyPrivatePath(root, directory) {
587
+ verifyPrivateDirectory(root);
588
+ const inside = relative(root, directory);
589
+ if (!inside) return;
590
+ let current = root;
591
+ for (const component of inside.split(sep).filter(Boolean)) {
592
+ current = join(current, component);
593
+ verifyPrivateDirectory(current);
594
+ }
595
+ }
596
+ function prepareEnrollmentRoot(env) {
597
+ const root = enrollmentRoot(env);
598
+ rejectSymlinkComponents(dirname(root));
599
+ prepareDirectory(root);
600
+ verifyPrivateDirectory(root);
601
+ return root;
602
+ }
603
+ function assertEnrollmentPath(path, env = process.env) {
604
+ const root = enrollmentRoot(env);
605
+ const selected = resolve(path);
606
+ const inside = relative(root, selected);
607
+ if (!inside || inside.startsWith(".." + sep) || inside === ".." || inside.includes(sep + ".." + sep)) {
608
+ throw new Error("enrollment file must be inside the configured enrollment directory");
609
+ }
610
+ rejectSymlinkComponents(root);
611
+ rejectSymlinkComponents(dirname(selected));
612
+ if (existsSync(root) && existsSync(dirname(selected))) {
613
+ verifyPrivatePath(root, dirname(selected));
614
+ }
615
+ return selected;
616
+ }
617
+ function serialized(value) {
618
+ return JSON.stringify(validateEnrollmentFile(value), null, 2) + "\n";
619
+ }
620
+ function defaultEnrollmentPath(requestId2, env = process.env) {
621
+ return join(enrollmentRoot(env), requestId2 + ".json");
622
+ }
623
+ function createPendingEnrollment(path, enrollment2, env = process.env) {
624
+ if (enrollment2.state !== "pending") throw new Error("new enrollment must be pending");
625
+ if (enrollment2.revision !== 0) throw new Error("new enrollment revision must be zero");
626
+ path = assertEnrollmentPath(path, env);
627
+ prepareEnrollmentRoot(env);
628
+ path = assertEnrollmentPath(path, env);
629
+ const directory = dirname(path);
630
+ prepareDirectory(directory);
631
+ verifyPrivatePath(enrollmentRoot(env), directory);
632
+ const directoryIdentity = verifyPrivateDirectory(directory);
633
+ const descriptor = openSync(path, "wx", 384);
634
+ try {
635
+ writeFileSync(descriptor, serialized(enrollment2), "utf8");
636
+ fsyncSync(descriptor);
637
+ closeSync(descriptor);
638
+ securePrivatePath(path, "file");
639
+ sameDirectory(directory, directoryIdentity);
640
+ syncDirectory(directory);
641
+ } catch (error) {
642
+ try {
643
+ closeSync(descriptor);
644
+ } catch {
645
+ }
646
+ rmSync(path, { force: true });
647
+ throw error;
648
+ }
649
+ }
650
+ function readEnrollmentUnlocked(path, env) {
651
+ path = assertEnrollmentPath(path, env);
652
+ const directory = dirname(path);
653
+ verifyPrivatePath(enrollmentRoot(env), directory);
654
+ const directoryIdentity = verifyPrivateDirectory(directory);
655
+ verifyPrivatePathAccess(path, "file");
656
+ const descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
657
+ try {
658
+ const details = fstatSync(descriptor);
659
+ if (!details.isFile()) throw new Error("enrollment path must be a regular file, not a symbolic link");
660
+ if (process.platform !== "win32") {
661
+ if ((details.mode & 63) !== 0) throw new Error("enrollment file permissions must be 0600");
662
+ if (typeof process.getuid === "function" && details.uid !== process.getuid()) {
663
+ throw new Error("enrollment file must be owned by the current user");
664
+ }
665
+ }
666
+ let parsed;
667
+ try {
668
+ parsed = JSON.parse(readFileSync(descriptor, "utf8"));
669
+ } catch {
670
+ throw new Error("enrollment file is not valid JSON");
671
+ }
672
+ sameDirectory(directory, directoryIdentity);
673
+ return validateEnrollmentFile(parsed);
674
+ } finally {
675
+ closeSync(descriptor);
676
+ }
677
+ }
678
+ function readEnrollment(path, env = process.env) {
679
+ return readEnrollmentUnlocked(path, env);
680
+ }
681
+ function assertActiveLock(lock2, enrollmentPath) {
682
+ if (lock2.released || lock2.enrollmentPath !== enrollmentPath) {
683
+ throw new Error("enrollment lock does not cover this file");
684
+ }
685
+ const open = fstatSync(lock2.descriptor);
686
+ const current = lstatSync(lock2.lockPath);
687
+ if (open.dev !== lock2.device || open.ino !== lock2.inode || current.dev !== lock2.device || current.ino !== lock2.inode || current.isSymbolicLink()) {
688
+ throw new Error("enrollment lock changed during the operation");
689
+ }
690
+ }
691
+ function acquireEnrollmentLock(path, env = process.env) {
692
+ prepareEnrollmentRoot(env);
693
+ path = assertEnrollmentPath(path, env);
694
+ const directory = dirname(path);
695
+ prepareDirectory(directory);
696
+ verifyPrivatePath(enrollmentRoot(env), directory);
697
+ const directoryIdentity = verifyPrivateDirectory(directory);
698
+ const lockPath = path + ".lock";
699
+ const descriptor = openSync(lockPath, "wx", 384);
700
+ const nonce = randomUUID();
701
+ try {
702
+ writeFileSync(descriptor, JSON.stringify({
703
+ schema: "agent-bridge.enrollment-lock",
704
+ version: 1,
705
+ enrollmentPath: path,
706
+ pid: process.pid,
707
+ host: hostname(),
708
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
709
+ nonce
710
+ }) + "\n", "utf8");
711
+ fsyncSync(descriptor);
712
+ securePrivatePath(lockPath, "file");
713
+ sameDirectory(directory, directoryIdentity);
714
+ syncDirectory(directory);
715
+ const details = fstatSync(descriptor);
716
+ return {
717
+ enrollmentPath: path,
718
+ lockPath,
719
+ descriptor,
720
+ device: details.dev,
721
+ inode: details.ino,
722
+ nonce,
723
+ released: false
724
+ };
725
+ } catch (error) {
726
+ try {
727
+ closeSync(descriptor);
728
+ } catch {
729
+ }
730
+ rmSync(lockPath, { force: true });
731
+ throw error;
732
+ }
733
+ }
734
+ function releaseEnrollmentLock(lock2, dependencies = durabilityDefaults) {
735
+ assertActiveLock(lock2, lock2.enrollmentPath);
736
+ const directory = dirname(lock2.lockPath);
737
+ const directoryIdentity = verifyPrivateDirectory(directory);
738
+ closeSync(lock2.descriptor);
739
+ const current = lstatSync(lock2.lockPath);
740
+ if (current.dev !== lock2.device || current.ino !== lock2.inode) {
741
+ throw new Error("enrollment lock changed before release");
742
+ }
743
+ rmSync(lock2.lockPath);
744
+ lock2.released = true;
745
+ try {
746
+ sameDirectory(directory, directoryIdentity);
747
+ dependencies.syncDirectory(directory);
748
+ } catch {
749
+ return "durability-unknown";
750
+ }
751
+ return "released";
752
+ }
753
+ function recoverEnrollmentLock(path, env = process.env, now = Date.now()) {
754
+ path = assertEnrollmentPath(path, env);
755
+ const lockPath = path + ".lock";
756
+ verifyPrivatePathAccess(lockPath, "file");
757
+ const descriptor = openSync(lockPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
758
+ try {
759
+ const details = fstatSync(descriptor);
760
+ if (!details.isFile()) throw new Error("enrollment lock is not a regular file");
761
+ if (process.platform !== "win32") {
762
+ if ((details.mode & 63) !== 0) throw new Error("enrollment lock permissions must be 0600");
763
+ if (typeof process.getuid === "function" && details.uid !== process.getuid()) {
764
+ throw new Error("enrollment lock must be owned by the current user");
765
+ }
766
+ }
767
+ let metadata;
768
+ try {
769
+ metadata = object(JSON.parse(readFileSync(descriptor, "utf8")), "enrollment lock");
770
+ } catch {
771
+ throw new Error("enrollment lock metadata is invalid");
772
+ }
773
+ 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)) {
774
+ throw new Error("enrollment lock metadata is invalid");
775
+ }
776
+ const age = now - new Date(metadata.createdAt).getTime();
777
+ if (age < 6e4) throw new Error("enrollment lock is too recent to recover");
778
+ try {
779
+ process.kill(metadata.pid, 0);
780
+ throw new Error("enrollment lock owner is still running");
781
+ } catch (error) {
782
+ if (error instanceof Error && error.message === "enrollment lock owner is still running") throw error;
783
+ if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ESRCH") {
784
+ throw new Error("cannot prove that the enrollment lock owner has stopped");
785
+ }
786
+ }
787
+ const current = lstatSync(lockPath);
788
+ if (current.dev !== details.dev || current.ino !== details.ino) {
789
+ throw new Error("enrollment lock changed during recovery");
790
+ }
791
+ } finally {
792
+ closeSync(descriptor);
793
+ }
794
+ rmSync(lockPath);
795
+ syncDirectory(dirname(lockPath));
796
+ }
797
+ function deleteEnrollmentFile(path, lock2, env = process.env, dependencies = durabilityDefaults) {
798
+ path = assertEnrollmentPath(path, env);
799
+ assertActiveLock(lock2, path);
800
+ let current;
801
+ try {
802
+ current = readEnrollmentUnlocked(path, env);
803
+ } catch (error) {
804
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
805
+ return "missing";
806
+ }
807
+ throw error;
808
+ }
809
+ if (current.state !== "consumed" || current.token !== null) {
810
+ throw new Error("only a consumed enrollment can be deleted");
811
+ }
812
+ const directory = dirname(path);
813
+ const directoryIdentity = verifyPrivateDirectory(directory);
814
+ try {
815
+ rmSync(path);
816
+ } catch (error) {
817
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
818
+ return "missing";
819
+ }
820
+ throw error;
821
+ }
822
+ try {
823
+ sameDirectory(directory, directoryIdentity);
824
+ dependencies.syncDirectory(directory);
825
+ } catch {
826
+ return "deleted-durability-unknown";
827
+ }
828
+ return "deleted-and-durable";
829
+ }
830
+ function transitionEnrollment(path, current, state, changes = {}, env = process.env, heldLock) {
831
+ path = assertEnrollmentPath(path, env);
832
+ const lock2 = heldLock ?? acquireEnrollmentLock(path, env);
833
+ try {
834
+ assertActiveLock(lock2, path);
835
+ const onDisk = readEnrollmentUnlocked(path, env);
836
+ if (onDisk.revision !== current.revision || onDisk.state !== current.state || onDisk.requestId !== current.requestId || onDisk.operation !== current.operation || onDisk.token !== current.token) {
837
+ throw new Error("stale enrollment transition refused");
838
+ }
839
+ const transition = current.state + "->" + state;
840
+ if (!(/* @__PURE__ */ new Set([
841
+ "pending->ready",
842
+ "ready->consuming",
843
+ "consuming->ready",
844
+ "consuming->consumed"
845
+ ])).has(transition)) {
846
+ throw new Error("illegal enrollment state transition: " + transition);
847
+ }
848
+ const next = validateEnrollmentFile({
849
+ ...onDisk,
850
+ ...changes,
851
+ state,
852
+ revision: onDisk.revision + 1
853
+ });
854
+ const directory = dirname(path);
855
+ const directoryIdentity = verifyPrivateDirectory(directory);
856
+ const temporary = path + "." + process.pid + "." + randomUUID() + ".tmp";
857
+ const descriptor = openSync(temporary, "wx", 384);
858
+ try {
859
+ writeFileSync(descriptor, serialized(next), "utf8");
860
+ fsyncSync(descriptor);
861
+ closeSync(descriptor);
862
+ securePrivatePath(temporary, "file");
863
+ sameDirectory(directory, directoryIdentity);
864
+ renameSync(temporary, path);
865
+ verifyPrivatePathAccess(path, "file");
866
+ sameDirectory(directory, directoryIdentity);
867
+ syncDirectory(directory);
868
+ return next;
869
+ } catch (error) {
870
+ try {
871
+ closeSync(descriptor);
872
+ } catch {
873
+ }
874
+ rmSync(temporary, { force: true });
875
+ throw error;
876
+ }
877
+ } finally {
878
+ if (!heldLock) releaseEnrollmentLock(lock2);
879
+ }
880
+ }
881
+
882
+ // src/client-installer.ts
883
+ function assertExecutable(path) {
884
+ if (!existsSync2(path)) {
885
+ throw new Error(`Claude Desktop MCP executable does not exist: ${path}`);
886
+ }
887
+ if (!statSync(path).isFile()) {
888
+ throw new Error(`Claude Desktop MCP executable is not a file: ${path}`);
889
+ }
890
+ try {
891
+ accessSync(path, process.platform === "win32" ? constants2.F_OK : constants2.X_OK);
892
+ } catch {
893
+ throw new Error(`Claude Desktop MCP executable is not executable: ${path}`);
894
+ }
895
+ return path;
896
+ }
897
+ function resolveExecutable(command, env) {
898
+ if (isAbsolute(command) || command.includes("/") || command.includes("\\")) {
899
+ return assertExecutable(resolve2(command));
900
+ }
901
+ const searchPath = env.PATH ?? process.env.PATH ?? "";
902
+ const extensions = process.platform === "win32" && !extname(command) ? (env.PATHEXT ?? process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
903
+ for (const directory of searchPath.split(delimiter).filter(Boolean)) {
904
+ for (const extension of extensions) {
905
+ const candidate = join2(directory, command + extension);
906
+ if (!existsSync2(candidate)) continue;
907
+ return assertExecutable(resolve2(candidate));
908
+ }
909
+ }
910
+ throw new Error(`Claude Desktop MCP executable was not found on PATH: ${command}`);
911
+ }
912
+ function defaultServerEntry() {
913
+ const moduleDirectory = dirname2(fileURLToPath(import.meta.url));
914
+ const candidates = [
915
+ join2(moduleDirectory, "index.js"),
916
+ join2(moduleDirectory, "..", "dist", "index.js")
917
+ ];
918
+ const entry = candidates.find((candidate) => existsSync2(candidate) && statSync(candidate).isFile());
919
+ if (!entry) {
920
+ throw new Error(`Claude Desktop MCP server entry does not exist: ${candidates[0]}`);
921
+ }
922
+ return resolve2(entry);
923
+ }
924
+ function desktopLaunchContract(requestedCommand, env) {
925
+ const command = requestedCommand?.trim();
926
+ if (command) return { command: resolveExecutable(command, env), args: [] };
927
+ return {
928
+ command: assertExecutable(resolve2(process.execPath)),
929
+ args: [defaultServerEntry()]
930
+ };
931
+ }
37
932
  function desktopConfigPath(env) {
38
933
  if (process.platform === "win32") {
39
934
  const appData = env.APPDATA;
40
935
  if (!appData) throw new Error("APPDATA is required for Claude Desktop installation");
41
- return join(appData, "Claude", "claude_desktop_config.json");
936
+ return join2(appData, "Claude", "claude_desktop_config.json");
42
937
  }
43
- const home = env.HOME ?? homedir();
44
- return process.platform === "darwin" ? join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json") : join(home, ".config", "Claude", "claude_desktop_config.json");
938
+ const home2 = env.HOME ?? homedir2();
939
+ return process.platform === "darwin" ? join2(home2, "Library", "Application Support", "Claude", "claude_desktop_config.json") : join2(home2, ".config", "Claude", "claude_desktop_config.json");
45
940
  }
46
941
  function safeComponent(value) {
47
942
  const readable = value.replace(/[^A-Za-z0-9._-]+/g, "_").slice(0, 48) || "client";
48
- const suffix = createHash("sha256").update(value).digest("hex").slice(0, 12);
943
+ const suffix = createHash2("sha256").update(value).digest("hex").slice(0, 12);
49
944
  return `${readable}-${suffix}`;
50
945
  }
51
- function clientBackendConfigPath(runtime, instance, env) {
52
- const home = env.HOME ?? homedir();
53
- return join(
54
- home,
946
+ function clientBackendConfigPath(runtime2, instance, env) {
947
+ const home2 = env.HOME ?? homedir2();
948
+ return join2(
949
+ home2,
55
950
  ".agent-bridge",
56
951
  "clients",
57
- `${runtime}-${safeComponent(instance)}.config`
952
+ `${runtime2}-${safeComponent(instance)}.config`
58
953
  );
59
954
  }
60
955
  function replacePrivateFile(path, content) {
61
- mkdirSync(dirname(path), { recursive: true, mode: 448 });
62
- if (process.platform !== "win32") chmodSync(dirname(path), 448);
63
- const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
64
- writeFileSync(temporary, content, { mode: 384 });
65
- renameSync(temporary, path);
66
- if (process.platform !== "win32") chmodSync(path, 384);
67
- }
68
- function writeBackendConfig(runtime, identity, instance, options) {
956
+ const directory = dirname2(path);
957
+ const privateRoot = dirname2(directory);
958
+ mkdirSync2(privateRoot, { recursive: true, mode: 448 });
959
+ securePrivatePath(privateRoot, "directory");
960
+ mkdirSync2(directory, { recursive: true, mode: 448 });
961
+ securePrivatePath(directory, "directory");
962
+ if (existsSync2(path)) verifyPrivatePathAccess(path, "file");
963
+ const temporary = `${path}.${process.pid}.${randomUUID2()}.tmp`;
964
+ const descriptor = openSync2(temporary, "wx", 384);
965
+ try {
966
+ writeFileSync2(descriptor, content);
967
+ fsyncSync2(descriptor);
968
+ } finally {
969
+ closeSync2(descriptor);
970
+ }
971
+ securePrivatePath(temporary, "file");
972
+ renameSync2(temporary, path);
973
+ verifyPrivatePathAccess(path, "file");
974
+ if (process.platform !== "win32") {
975
+ const descriptor2 = openSync2(directory, "r");
976
+ try {
977
+ fsyncSync2(descriptor2);
978
+ } finally {
979
+ closeSync2(descriptor2);
980
+ }
981
+ }
982
+ }
983
+ function writeBackendConfig(runtime2, identity, instance, options) {
69
984
  const env = options.env;
70
- const home = env.HOME ?? homedir();
71
- const sharedPath = env.AGENT_BRIDGE_CONFIG ?? join(home, ".agent-bridge", "config");
985
+ const home2 = env.HOME ?? homedir2();
986
+ const sharedPath = env.AGENT_BRIDGE_CONFIG ?? join2(home2, ".agent-bridge", "config");
72
987
  const shared = readClientConfigFile(sharedPath);
73
988
  const configuredProvider = env.AGENT_BRIDGE_PROVIDER?.trim() || shared.AGENT_BRIDGE_PROVIDER?.trim();
74
989
  const inferredGateway = !configuredProvider && Boolean(
75
990
  env.AGENT_BRIDGE_TOKEN?.trim() || shared.AGENT_BRIDGE_TOKEN?.trim()
76
991
  );
77
- const provider = configuredProvider === "legacy" || configuredProvider === "supabase" ? "legacy-supabase" : configuredProvider ?? (inferredGateway ? "gateway" : void 0);
992
+ const provider3 = configuredProvider === "legacy" || configuredProvider === "supabase" ? "legacy-supabase" : configuredProvider ?? (inferredGateway ? "gateway" : void 0);
78
993
  const clientToken = options.token?.trim() || env.AGENT_BRIDGE_CLIENT_TOKEN?.trim();
79
- if (provider === "gateway" && !clientToken) {
994
+ if (provider3 === "gateway" && !clientToken) {
80
995
  throw new Error(
81
996
  "gateway client installation requires --token or AGENT_BRIDGE_CLIENT_TOKEN"
82
997
  );
@@ -94,121 +1009,2213 @@ function writeBackendConfig(runtime, identity, instance, options) {
94
1009
  AGENT_BRIDGE_TOKEN: resolved.provider === "gateway" ? resolved.credential : void 0,
95
1010
  AGENT_BRIDGE_KEY: resolved.provider === "legacy-supabase" ? resolved.credential : void 0,
96
1011
  AGENT_BRIDGE_DB: resolved.provider === "local" ? resolved.databasePath : void 0,
97
- AGENT_BRIDGE_EDGE_DB: resolved.provider === "gateway" ? resolved.edgeDatabasePath : void 0
1012
+ AGENT_BRIDGE_EDGE_DB: resolved.provider === "gateway" ? resolved.edgeDatabasePath : void 0,
1013
+ AGENT_BRIDGE_CREDENTIAL_ID: options.binding?.credentialId,
1014
+ AGENT_BRIDGE_PRINCIPAL: options.binding?.principal,
1015
+ AGENT_BRIDGE_CLIENT_INSTANCE: options.binding?.instance
98
1016
  };
99
1017
  for (const value of Object.values(values)) {
100
1018
  if (value?.includes("\n") || value?.includes("\r")) {
101
1019
  throw new Error("client backend config values cannot contain newlines");
102
1020
  }
103
1021
  }
104
- const path = clientBackendConfigPath(runtime, instance, env);
105
- replacePrivateFile(
106
- path,
107
- `${Object.entries(values).filter((entry) => Boolean(entry[1])).map(([key, value]) => `${key}=${value}`).join("\n")}
108
- `
109
- );
110
- return path;
1022
+ const path = clientBackendConfigPath(runtime2, instance, env);
1023
+ replacePrivateFile(
1024
+ path,
1025
+ `${Object.entries(values).filter((entry) => Boolean(entry[1])).map(([key, value]) => `${key}=${value}`).join("\n")}
1026
+ `
1027
+ );
1028
+ return path;
1029
+ }
1030
+ function writeJsonConfig(path, identity, instance, backendConfigPath, launch) {
1031
+ let config = {};
1032
+ if (existsSync2(path)) {
1033
+ try {
1034
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
1035
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") throw new Error();
1036
+ config = parsed;
1037
+ } catch {
1038
+ throw new Error(`Claude Desktop config is not valid JSON: ${path}`);
1039
+ }
1040
+ }
1041
+ const currentServers = config.mcpServers;
1042
+ const mcpServers = currentServers && !Array.isArray(currentServers) && typeof currentServers === "object" ? currentServers : {};
1043
+ const next = {
1044
+ ...config,
1045
+ mcpServers: {
1046
+ ...mcpServers,
1047
+ "agent-bridge": {
1048
+ command: launch.command,
1049
+ args: launch.args,
1050
+ env: {
1051
+ AGENT_BRIDGE_AGENT: identity,
1052
+ AGENT_BRIDGE_INSTANCE: instance,
1053
+ AGENT_BRIDGE_CONFIG: backendConfigPath
1054
+ }
1055
+ }
1056
+ }
1057
+ };
1058
+ mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
1059
+ const temporary = `${path}.${process.pid}.tmp`;
1060
+ writeFileSync2(temporary, `${JSON.stringify(next, null, 2)}
1061
+ `, { mode: 384 });
1062
+ renameSync2(temporary, path);
1063
+ }
1064
+ function assertEnrollmentConfiguration(enrollment2, assertions) {
1065
+ const input = enrollment2.input;
1066
+ const env = assertions.env;
1067
+ const home2 = env.HOME ?? homedir2();
1068
+ const shared = readClientConfigFile(
1069
+ env.AGENT_BRIDGE_CONFIG ?? join2(home2, ".agent-bridge", "config")
1070
+ );
1071
+ const conflicts = [
1072
+ ["runtime", assertions.runtime, input.runtime],
1073
+ ["identity", assertions.identity?.trim() || void 0, input.principal],
1074
+ ["instance", assertions.instance?.trim() || void 0, input.instance],
1075
+ ["AGENT_BRIDGE_AGENT", env.AGENT_BRIDGE_AGENT?.trim(), input.principal],
1076
+ ["AGENT_BRIDGE_INSTANCE", env.AGENT_BRIDGE_INSTANCE?.trim(), input.instance],
1077
+ ["AGENT_BRIDGE_URL", env.AGENT_BRIDGE_URL?.trim(), input.gatewayUrl],
1078
+ ["AGENT_BRIDGE_WORKSPACE", env.AGENT_BRIDGE_WORKSPACE?.trim(), input.workspaceId],
1079
+ ["shared AGENT_BRIDGE_URL", shared.AGENT_BRIDGE_URL?.trim(), input.gatewayUrl],
1080
+ ["shared AGENT_BRIDGE_WORKSPACE", shared.AGENT_BRIDGE_WORKSPACE?.trim(), input.workspaceId]
1081
+ ];
1082
+ for (const [name, actual, expected] of conflicts) {
1083
+ if (actual && actual !== expected) throw new Error(name + " conflicts with the enrollment file");
1084
+ }
1085
+ for (const [name, provider3] of [
1086
+ ["AGENT_BRIDGE_PROVIDER", env.AGENT_BRIDGE_PROVIDER?.trim()],
1087
+ ["shared AGENT_BRIDGE_PROVIDER", shared.AGENT_BRIDGE_PROVIDER?.trim()]
1088
+ ]) {
1089
+ if (provider3 && provider3 !== "gateway") throw new Error(name + " conflicts with the enrollment file");
1090
+ }
1091
+ if (assertions.token) throw new Error("--token cannot be used with --enrollment-file");
1092
+ if (env.AGENT_BRIDGE_CLIENT_TOKEN || env.AGENT_BRIDGE_TOKEN) {
1093
+ throw new Error("token environment variables cannot be used with --enrollment-file");
1094
+ }
1095
+ }
1096
+ function removeConsumedEnrollment(path, result, lock2, env) {
1097
+ try {
1098
+ const deletion = deleteEnrollmentFile(path, lock2, env);
1099
+ if (deletion === "missing") {
1100
+ return {
1101
+ ...result,
1102
+ installed: true,
1103
+ enrollmentDeleted: false,
1104
+ enrollmentFile: path,
1105
+ enrollmentStatus: "consumed-file-missing"
1106
+ };
1107
+ }
1108
+ if (deletion === "deleted-durability-unknown") {
1109
+ return {
1110
+ ...result,
1111
+ installed: true,
1112
+ enrollmentDeleted: true,
1113
+ enrollmentStatus: "consumed-deletion-durability-unknown"
1114
+ };
1115
+ }
1116
+ return {
1117
+ ...result,
1118
+ installed: true,
1119
+ enrollmentDeleted: true,
1120
+ enrollmentStatus: "consumed"
1121
+ };
1122
+ } catch {
1123
+ return {
1124
+ ...result,
1125
+ installed: true,
1126
+ enrollmentDeleted: false,
1127
+ enrollmentFile: path,
1128
+ enrollmentStatus: "consumed-file-retained"
1129
+ };
1130
+ }
1131
+ }
1132
+ function backendMatches(path, enrollment2, credentialId, requireToken) {
1133
+ if (!existsSync2(path)) return false;
1134
+ const config = readClientConfigFile(path);
1135
+ return config.AGENT_BRIDGE_PROVIDER === "gateway" && config.AGENT_BRIDGE_URL === enrollment2.input.gatewayUrl && config.AGENT_BRIDGE_WORKSPACE === enrollment2.input.workspaceId && config.AGENT_BRIDGE_CREDENTIAL_ID === credentialId && config.AGENT_BRIDGE_PRINCIPAL === enrollment2.input.principal && config.AGENT_BRIDGE_CLIENT_INSTANCE === enrollment2.input.instance && (!requireToken || config.AGENT_BRIDGE_TOKEN === enrollment2.token);
1136
+ }
1137
+ function desktopRegistrationState(enrollment2, backendConfigPath, launch, env) {
1138
+ const path = desktopConfigPath(env);
1139
+ if (!existsSync2(path)) return "absent";
1140
+ try {
1141
+ const config = JSON.parse(readFileSync2(path, "utf8"));
1142
+ const server = config?.mcpServers?.["agent-bridge"];
1143
+ if (!server) return "absent";
1144
+ const exactEnvironment = server?.env?.AGENT_BRIDGE_AGENT === enrollment2.input.principal && server?.env?.AGENT_BRIDGE_INSTANCE === enrollment2.input.instance && server?.env?.AGENT_BRIDGE_CONFIG === backendConfigPath;
1145
+ if (server?.command === "agent-bridge-mcp" && (server?.args === void 0 || Array.isArray(server.args) && server.args.length === 0) && exactEnvironment) return "legacy";
1146
+ 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";
1147
+ } catch {
1148
+ return "conflict";
1149
+ }
1150
+ }
1151
+ function repairLegacyDesktopRegistration(state, enrollment2, backendConfigPath, launch, env) {
1152
+ if (state !== "legacy") return state;
1153
+ writeJsonConfig(
1154
+ desktopConfigPath(env),
1155
+ enrollment2.input.principal,
1156
+ enrollment2.input.instance,
1157
+ backendConfigPath,
1158
+ launch
1159
+ );
1160
+ const repaired = desktopRegistrationState(enrollment2, backendConfigPath, launch, env);
1161
+ return repaired === "legacy" ? "conflict" : repaired;
1162
+ }
1163
+ function nativeRegistrationState(enrollment2, backendConfigPath, command, scope, execute) {
1164
+ const executable = enrollment2.input.runtime === "codex" ? "codex" : "claude";
1165
+ const args = enrollment2.input.runtime === "codex" ? ["mcp", "get", "agent-bridge", "--json"] : ["mcp", "get", "agent-bridge"];
1166
+ const result = execute(executable, args);
1167
+ if (result.error) throw new Error(enrollment2.input.runtime + " MCP inspection failed");
1168
+ if (result.status !== 0) {
1169
+ const output2 = (result.stdout + "\n" + result.stderr).trim();
1170
+ const notFound = enrollment2.input.runtime === "codex" ? output2 === "Error: No MCP server named 'agent-bridge' found." : output2 === 'No MCP server named "agent-bridge".' || /^No MCP server named "agent-bridge"\. Configured servers: [^\r\n]+$/.test(output2);
1171
+ if (notFound) return "absent";
1172
+ throw new Error(enrollment2.input.runtime + " MCP inspection failed");
1173
+ }
1174
+ if (enrollment2.input.runtime === "codex") {
1175
+ try {
1176
+ const server = JSON.parse(result.stdout);
1177
+ const transport = server?.transport;
1178
+ const environment = transport?.env;
1179
+ const exactEnvironment = environment && typeof environment === "object" && !Array.isArray(environment) && Object.keys(environment).sort().join(",") === "AGENT_BRIDGE_AGENT,AGENT_BRIDGE_CONFIG,AGENT_BRIDGE_INSTANCE";
1180
+ 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 === enrollment2.input.principal && transport?.env?.AGENT_BRIDGE_INSTANCE === enrollment2.input.instance && transport?.env?.AGENT_BRIDGE_CONFIG === backendConfigPath ? "matching" : "conflict";
1181
+ } catch {
1182
+ return "conflict";
1183
+ }
1184
+ }
1185
+ const lines = result.stdout.replace(/\r\n/g, "\n").split("\n");
1186
+ const scopeLine = {
1187
+ local: " Scope: Local config (private to you in this project)",
1188
+ user: " Scope: User config (available in all your projects)",
1189
+ project: " Scope: Project config (shared via .mcp.json)"
1190
+ }[scope];
1191
+ const expectedPrefix = [
1192
+ "agent-bridge:",
1193
+ scopeLine
1194
+ ];
1195
+ 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";
1196
+ const environmentLines = [];
1197
+ let index = 7;
1198
+ while (index < lines.length && lines[index]?.startsWith(" ")) {
1199
+ environmentLines.push(lines[index]);
1200
+ index += 1;
1201
+ }
1202
+ const expectedEnvironment = [
1203
+ " AGENT_BRIDGE_AGENT=" + enrollment2.input.principal,
1204
+ " AGENT_BRIDGE_INSTANCE=" + enrollment2.input.instance,
1205
+ " AGENT_BRIDGE_CONFIG=" + backendConfigPath
1206
+ ].sort();
1207
+ if (environmentLines.sort().join("\n") !== expectedEnvironment.join("\n")) return "conflict";
1208
+ while (lines[index] === "") index += 1;
1209
+ if (lines[index] !== "To remove this server, run: claude mcp remove agent-bridge -s " + scope) {
1210
+ return "conflict";
1211
+ }
1212
+ if (lines.slice(index + 1).some((line) => line !== "")) return "conflict";
1213
+ return "matching";
1214
+ }
1215
+ function enrolledInstallLocked(runtimeAssertion, identityAssertion, options, execute, lock2) {
1216
+ const env = options.env ?? process.env;
1217
+ let enrollment2 = readEnrollment(options.enrollmentFile, env);
1218
+ assertEnrollmentConfiguration(enrollment2, {
1219
+ runtime: runtimeAssertion,
1220
+ identity: identityAssertion,
1221
+ instance: options.instance,
1222
+ token: options.token,
1223
+ env
1224
+ });
1225
+ const runtime2 = enrollment2.input.runtime;
1226
+ const identity = enrollment2.input.principal;
1227
+ const expectedPath = clientBackendConfigPath(runtime2, enrollment2.input.instance, env);
1228
+ const command = options.command?.trim() || "agent-bridge-mcp";
1229
+ const desktopLaunch = runtime2 === "claude-desktop" ? desktopLaunchContract(options.command, env) : void 0;
1230
+ const scope = options.scope ?? "user";
1231
+ const baseResult = {
1232
+ runtime: runtime2,
1233
+ identity,
1234
+ instance: enrollment2.input.instance,
1235
+ method: runtime2 === "claude-desktop" ? "json-config" : "native-cli",
1236
+ configPath: runtime2 === "claude-desktop" ? desktopConfigPath(env) : void 0,
1237
+ backendConfigPath: expectedPath,
1238
+ restartRequired: true
1239
+ };
1240
+ if (enrollment2.state === "consumed") {
1241
+ if (!enrollment2.result || !backendMatches(expectedPath, enrollment2, enrollment2.result.credentialId, false)) {
1242
+ throw new Error("consumed enrollment no longer matches its client backend");
1243
+ }
1244
+ const inspected = runtime2 === "claude-desktop" ? desktopRegistrationState(enrollment2, expectedPath, desktopLaunch, env) : nativeRegistrationState(enrollment2, expectedPath, command, scope, execute);
1245
+ const registration = runtime2 === "claude-desktop" ? repairLegacyDesktopRegistration(
1246
+ inspected,
1247
+ enrollment2,
1248
+ expectedPath,
1249
+ desktopLaunch,
1250
+ env
1251
+ ) : inspected;
1252
+ if (registration !== "matching") {
1253
+ throw new Error("consumed enrollment no longer matches its MCP registration");
1254
+ }
1255
+ return removeConsumedEnrollment(options.enrollmentFile, baseResult, lock2, env);
1256
+ }
1257
+ if (enrollment2.state === "pending") {
1258
+ throw new Error("enrollment is pending; resume the owner command first");
1259
+ }
1260
+ const recovering = enrollment2.state === "consuming";
1261
+ if (!recovering && enrollment2.operation === "provision" && existsSync2(expectedPath)) {
1262
+ throw new Error("provision enrollment would replace an existing client backend file");
1263
+ }
1264
+ if (!recovering && enrollment2.operation === "provision") {
1265
+ const registration = runtime2 === "claude-desktop" ? desktopRegistrationState(enrollment2, expectedPath, desktopLaunch, env) : nativeRegistrationState(enrollment2, expectedPath, command, scope, execute);
1266
+ if (registration !== "absent") {
1267
+ throw new Error("provision enrollment would replace an existing MCP registration");
1268
+ }
1269
+ }
1270
+ if (enrollment2.state === "ready") {
1271
+ enrollment2 = transitionEnrollment(options.enrollmentFile, enrollment2, "consuming", {}, env, lock2);
1272
+ }
1273
+ if (enrollment2.state !== "consuming" || !enrollment2.token) {
1274
+ throw new Error("enrollment is not ready for installation");
1275
+ }
1276
+ const derivedEnv = {
1277
+ ...env,
1278
+ AGENT_BRIDGE_PROVIDER: "gateway",
1279
+ AGENT_BRIDGE_URL: enrollment2.input.gatewayUrl,
1280
+ AGENT_BRIDGE_WORKSPACE: enrollment2.input.workspaceId,
1281
+ AGENT_BRIDGE_AGENT: void 0,
1282
+ AGENT_BRIDGE_INSTANCE: void 0,
1283
+ AGENT_BRIDGE_CLIENT_TOKEN: void 0
1284
+ };
1285
+ let result;
1286
+ let sideEffectsCompleted = false;
1287
+ try {
1288
+ if (enrollment2.operation === "rotate") {
1289
+ if (!enrollment2.input.credentialId || !enrollment2.result) {
1290
+ throw new Error("rotation enrollment lacks credential lineage");
1291
+ }
1292
+ const predecessorMatches = backendMatches(
1293
+ expectedPath,
1294
+ enrollment2,
1295
+ enrollment2.input.credentialId,
1296
+ false
1297
+ );
1298
+ const successorMatches = recovering && backendMatches(
1299
+ expectedPath,
1300
+ enrollment2,
1301
+ enrollment2.result.credentialId,
1302
+ true
1303
+ );
1304
+ if (!predecessorMatches && !successorMatches) {
1305
+ throw new Error("rotation requires an exactly bound predecessor or successor backend");
1306
+ }
1307
+ const inspected = runtime2 === "claude-desktop" ? desktopRegistrationState(enrollment2, expectedPath, desktopLaunch, env) : nativeRegistrationState(enrollment2, expectedPath, command, scope, execute);
1308
+ const registration = runtime2 === "claude-desktop" ? repairLegacyDesktopRegistration(
1309
+ inspected,
1310
+ enrollment2,
1311
+ expectedPath,
1312
+ desktopLaunch,
1313
+ env
1314
+ ) : inspected;
1315
+ if (registration !== "matching") {
1316
+ throw new Error("rotation requires the exact existing MCP registration");
1317
+ }
1318
+ if (predecessorMatches) {
1319
+ const previous = readFileSync2(expectedPath);
1320
+ try {
1321
+ writeBackendConfig(runtime2, identity, enrollment2.input.instance, {
1322
+ token: enrollment2.token,
1323
+ env: derivedEnv,
1324
+ binding: {
1325
+ credentialId: enrollment2.result.credentialId,
1326
+ principal: identity,
1327
+ instance: enrollment2.input.instance
1328
+ }
1329
+ });
1330
+ } catch (error) {
1331
+ replacePrivateFile(expectedPath, previous);
1332
+ throw error;
1333
+ }
1334
+ }
1335
+ result = baseResult;
1336
+ sideEffectsCompleted = true;
1337
+ } else {
1338
+ if (recovering && existsSync2(expectedPath) && !backendMatches(
1339
+ expectedPath,
1340
+ enrollment2,
1341
+ enrollment2.result.credentialId,
1342
+ true
1343
+ )) {
1344
+ throw new Error("consuming enrollment conflicts with its client backend file");
1345
+ }
1346
+ const inspected = recovering ? runtime2 === "claude-desktop" ? desktopRegistrationState(enrollment2, expectedPath, desktopLaunch, env) : nativeRegistrationState(enrollment2, expectedPath, command, scope, execute) : "absent";
1347
+ const registration = recovering && runtime2 === "claude-desktop" ? repairLegacyDesktopRegistration(
1348
+ inspected,
1349
+ enrollment2,
1350
+ expectedPath,
1351
+ desktopLaunch,
1352
+ env
1353
+ ) : inspected;
1354
+ if (recovering && registration === "conflict") {
1355
+ throw new Error("consuming enrollment conflicts with the registered MCP server");
1356
+ }
1357
+ if (recovering && backendMatches(
1358
+ expectedPath,
1359
+ enrollment2,
1360
+ enrollment2.result.credentialId,
1361
+ true
1362
+ ) && registration === "matching") {
1363
+ result = baseResult;
1364
+ } else {
1365
+ result = installClient(runtime2, identity, {
1366
+ command: runtime2 === "claude-desktop" ? options.command : command,
1367
+ scope: options.scope,
1368
+ instance: enrollment2.input.instance,
1369
+ token: enrollment2.token,
1370
+ backendBinding: {
1371
+ credentialId: enrollment2.result.credentialId,
1372
+ principal: identity,
1373
+ instance: enrollment2.input.instance
1374
+ },
1375
+ env: derivedEnv
1376
+ }, execute);
1377
+ sideEffectsCompleted = true;
1378
+ const verified = runtime2 === "claude-desktop" ? desktopRegistrationState(enrollment2, expectedPath, desktopLaunch, env) : nativeRegistrationState(enrollment2, expectedPath, command, scope, execute);
1379
+ if (verified !== "matching") throw new Error("MCP registration verification failed");
1380
+ }
1381
+ }
1382
+ } catch (error) {
1383
+ if (!recovering && !sideEffectsCompleted) {
1384
+ transitionEnrollment(options.enrollmentFile, enrollment2, "ready", {}, env, lock2);
1385
+ }
1386
+ throw error;
1387
+ }
1388
+ enrollment2 = transitionEnrollment(
1389
+ options.enrollmentFile,
1390
+ enrollment2,
1391
+ "consumed",
1392
+ { token: null },
1393
+ env,
1394
+ lock2
1395
+ );
1396
+ return removeConsumedEnrollment(options.enrollmentFile, result, lock2, env);
1397
+ }
1398
+ function enrolledInstall(runtime2, identity, options, execute) {
1399
+ const env = options.env ?? process.env;
1400
+ if (options.recoverLock) recoverEnrollmentLock(options.enrollmentFile, env);
1401
+ const lock2 = acquireEnrollmentLock(options.enrollmentFile, env);
1402
+ let result;
1403
+ try {
1404
+ result = enrolledInstallLocked(runtime2, identity, options, execute, lock2);
1405
+ } catch (error) {
1406
+ if (!lock2.released) {
1407
+ try {
1408
+ releaseEnrollmentLock(lock2);
1409
+ } catch {
1410
+ }
1411
+ }
1412
+ throw error;
1413
+ }
1414
+ let lockReleaseStatus;
1415
+ try {
1416
+ const release = releaseEnrollmentLock(lock2);
1417
+ lockReleaseStatus = release === "released" ? "released" : "durability-unknown";
1418
+ } catch {
1419
+ lockReleaseStatus = existsSync2(lock2.lockPath) ? "retained" : "durability-unknown";
1420
+ }
1421
+ return { ...result, lockReleaseStatus };
1422
+ }
1423
+ function installClient(runtime2, identity, options = {}, execute = (command, args) => spawnSync(command, args, { encoding: "utf8" })) {
1424
+ if (options.enrollmentFile) {
1425
+ return enrolledInstall(runtime2, identity, {
1426
+ ...options,
1427
+ enrollmentFile: options.enrollmentFile
1428
+ }, execute);
1429
+ }
1430
+ if (!runtime2) throw new Error("install runtime is required");
1431
+ const normalizedIdentity = identity.trim();
1432
+ if (!normalizedIdentity || normalizedIdentity.length > 128) throw new Error("--identity is required");
1433
+ const instance = options.instance?.trim() || `${runtime2}-${randomUUID2()}`;
1434
+ if (instance.length > 128) throw new Error("--instance exceeds 128 characters");
1435
+ if (options.scope && !["local", "user", "project"].includes(options.scope)) {
1436
+ throw new Error("scope must be local, user, or project");
1437
+ }
1438
+ const command = options.command?.trim() || "agent-bridge-mcp";
1439
+ const env = options.env ?? process.env;
1440
+ const desktopLaunch = runtime2 === "claude-desktop" ? desktopLaunchContract(options.command, env) : void 0;
1441
+ const expectedPath = clientBackendConfigPath(runtime2, instance, env);
1442
+ const previous = existsSync2(expectedPath) ? readFileSync2(expectedPath) : void 0;
1443
+ try {
1444
+ const backendConfigPath = writeBackendConfig(runtime2, normalizedIdentity, instance, {
1445
+ token: options.token,
1446
+ env,
1447
+ binding: options.backendBinding
1448
+ });
1449
+ if (runtime2 === "claude-desktop") {
1450
+ const path = desktopConfigPath(env);
1451
+ writeJsonConfig(path, normalizedIdentity, instance, backendConfigPath, desktopLaunch);
1452
+ return { runtime: runtime2, identity: normalizedIdentity, instance, method: "json-config", configPath: path, backendConfigPath, restartRequired: true };
1453
+ }
1454
+ const executable = runtime2 === "codex" ? "codex" : "claude";
1455
+ const args = runtime2 === "codex" ? [
1456
+ "mcp",
1457
+ "add",
1458
+ "agent-bridge",
1459
+ "--env",
1460
+ `AGENT_BRIDGE_AGENT=${normalizedIdentity}`,
1461
+ "--env",
1462
+ `AGENT_BRIDGE_INSTANCE=${instance}`,
1463
+ "--env",
1464
+ `AGENT_BRIDGE_CONFIG=${backendConfigPath}`,
1465
+ "--",
1466
+ command
1467
+ ] : [
1468
+ "mcp",
1469
+ "add",
1470
+ "--scope",
1471
+ options.scope ?? "user",
1472
+ "agent-bridge",
1473
+ "-e",
1474
+ `AGENT_BRIDGE_AGENT=${normalizedIdentity}`,
1475
+ "-e",
1476
+ `AGENT_BRIDGE_INSTANCE=${instance}`,
1477
+ "-e",
1478
+ `AGENT_BRIDGE_CONFIG=${backendConfigPath}`,
1479
+ "--",
1480
+ command
1481
+ ];
1482
+ const result = execute(executable, args);
1483
+ if (result.error || result.status !== 0) {
1484
+ const detail = result.stderr?.trim() || result.error?.message || `exit ${result.status}`;
1485
+ throw new Error(`${runtime2} MCP installation failed: ${detail}`);
1486
+ }
1487
+ return { runtime: runtime2, identity: normalizedIdentity, instance, method: "native-cli", backendConfigPath, restartRequired: true };
1488
+ } catch (error) {
1489
+ try {
1490
+ if (previous) replacePrivateFile(expectedPath, previous);
1491
+ else rmSync2(expectedPath, { force: true });
1492
+ } catch (rollbackError) {
1493
+ const detail = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
1494
+ throw new Error(`${error instanceof Error ? error.message : String(error)}; client config rollback failed: ${detail}`);
1495
+ }
1496
+ throw error;
1497
+ }
1498
+ }
1499
+
1500
+ // src/owner-control.ts
1501
+ import { randomBytes, randomUUID as randomUUID3 } from "crypto";
1502
+ import pg from "pg";
1503
+ var OwnerDatabaseFailure = class extends Error {
1504
+ constructor(driverCode) {
1505
+ super("owner database operation failed");
1506
+ this.driverCode = driverCode;
1507
+ }
1508
+ };
1509
+ async function ownerQuery(db, sql, values) {
1510
+ try {
1511
+ return await db.query(sql, values);
1512
+ } catch (error) {
1513
+ const code = error && typeof error === "object" && "code" in error ? String(error.code ?? "") : "";
1514
+ throw new OwnerDatabaseFailure(code);
1515
+ }
1516
+ }
1517
+ var defaultDependencies = {
1518
+ connect(databaseUrl) {
1519
+ const pool = new pg.Pool({
1520
+ connectionString: databaseUrl,
1521
+ max: 1,
1522
+ connectionTimeoutMillis: 1e4,
1523
+ query_timeout: 3e4
1524
+ });
1525
+ return { db: pool, close: () => pool.end() };
1526
+ },
1527
+ now: () => /* @__PURE__ */ new Date(),
1528
+ requestId: () => randomUUID3(),
1529
+ token: () => randomBytes(32).toString("base64url"),
1530
+ instance: (runtime2) => runtime2 + "-" + randomUUID3()
1531
+ };
1532
+ function stringOption(options, name) {
1533
+ const value = options[name];
1534
+ return typeof value === "string" ? value : void 0;
1535
+ }
1536
+ function required(options, name) {
1537
+ const value = stringOption(options, name);
1538
+ if (!value) throw new Error("--" + name + " is required");
1539
+ return value;
1540
+ }
1541
+ function selectWithEnvironment(options, name, env, environmentName, requiredValue) {
1542
+ const explicit = stringOption(options, name);
1543
+ const environment = env[environmentName]?.trim() || void 0;
1544
+ if (explicit && environment && explicit !== environment) {
1545
+ throw new Error("--" + name + " conflicts with " + environmentName);
1546
+ }
1547
+ const selected = explicit ?? environment;
1548
+ if (requiredValue && !selected) {
1549
+ throw new Error("--" + name + " or " + environmentName + " is required");
1550
+ }
1551
+ return selected;
1552
+ }
1553
+ function timestamp(options, name) {
1554
+ const value = stringOption(options, name) ?? null;
1555
+ return canonicalTimestamp(value, "--" + name);
1556
+ }
1557
+ function installRuntime(options) {
1558
+ const value = required(options, "runtime");
1559
+ if (value !== "codex" && value !== "claude-code" && value !== "claude-desktop") {
1560
+ throw new Error("--runtime must be codex, claude-code, or claude-desktop");
1561
+ }
1562
+ return value;
1563
+ }
1564
+ function scopeSet(options) {
1565
+ const value = required(options, "scope-set");
1566
+ if (!/^[a-z][a-z0-9-]{0,63}$/.test(value)) throw new Error("--scope-set is invalid");
1567
+ return value;
1568
+ }
1569
+ function uuidValue(value, label) {
1570
+ 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)) {
1571
+ throw new Error(label + " must be a UUID");
1572
+ }
1573
+ return value;
1574
+ }
1575
+ function uuidOption(options, name) {
1576
+ return uuidValue(required(options, name), "--" + name);
1577
+ }
1578
+ var OWNER_OPTIONS = {
1579
+ provision: /* @__PURE__ */ new Set([
1580
+ "workspace",
1581
+ "workspace-name",
1582
+ "identity",
1583
+ "runtime",
1584
+ "instance",
1585
+ "gateway-url",
1586
+ "scope-set",
1587
+ "display-name",
1588
+ "runtime-type",
1589
+ "label",
1590
+ "expires-at",
1591
+ "request-id",
1592
+ "enrollment-file",
1593
+ "recover-lock"
1594
+ ]),
1595
+ rotate: /* @__PURE__ */ new Set([
1596
+ "workspace",
1597
+ "identity",
1598
+ "runtime",
1599
+ "instance",
1600
+ "gateway-url",
1601
+ "scope-set",
1602
+ "runtime-type",
1603
+ "label",
1604
+ "expires-at",
1605
+ "grace-until",
1606
+ "invalidate-immediately",
1607
+ "credential-id",
1608
+ "request-id",
1609
+ "enrollment-file",
1610
+ "recover-lock"
1611
+ ]),
1612
+ inventory: /* @__PURE__ */ new Set(["workspace", "cursor", "limit"]),
1613
+ revoke: /* @__PURE__ */ new Set(["credential-id", "reason", "request-id"])
1614
+ };
1615
+ function validateOwnerOptions(subcommand, options) {
1616
+ if (stringOption(options, "resume")) {
1617
+ const override = Object.keys(options).find((key) => key !== "resume" && key !== "recover-lock");
1618
+ if (override) throw new Error("--" + override + " cannot be used with --resume");
1619
+ if (subcommand !== "provision" && subcommand !== "rotate") {
1620
+ throw new Error("--resume is valid only for provision or rotate");
1621
+ }
1622
+ return;
1623
+ }
1624
+ const allowed = OWNER_OPTIONS[subcommand];
1625
+ const invalid = Object.keys(options).find((key) => !allowed?.has(key));
1626
+ if (invalid) throw new Error("--" + invalid + " is not valid for owner " + subcommand);
1627
+ }
1628
+ function requestId(options, dependencies) {
1629
+ return stringOption(options, "request-id") ? uuidOption(options, "request-id") : dependencies.requestId();
1630
+ }
1631
+ function initialEnrollment(operation, options, env, dependencies) {
1632
+ const request = requestId(options, dependencies);
1633
+ const runtime2 = installRuntime(options);
1634
+ const gatewayUrl = canonicalGatewayUrl(
1635
+ selectWithEnvironment(options, "gateway-url", env, "AGENT_BRIDGE_URL", true),
1636
+ "--gateway-url"
1637
+ );
1638
+ const workspaceId = required(options, "workspace");
1639
+ const principal = required(options, "identity");
1640
+ const explicitInstance = stringOption(options, "instance");
1641
+ const instance = operation === "rotate" ? explicitInstance ?? (() => {
1642
+ throw new Error("--instance is required for rotation");
1643
+ })() : explicitInstance ?? dependencies.instance(runtime2);
1644
+ const graceUntil = timestamp(options, "grace-until");
1645
+ const invalidateImmediately = options["invalidate-immediately"] === true || options["invalidate-immediately"] === "true";
1646
+ if (operation === "rotate" && graceUntil === null === !invalidateImmediately) {
1647
+ throw new Error("rotation requires exactly one of --grace-until or --invalidate-immediately");
1648
+ }
1649
+ if (operation === "provision" && (graceUntil !== null || invalidateImmediately)) {
1650
+ throw new Error("provision does not accept rotation invalidation options");
1651
+ }
1652
+ const input = {
1653
+ gatewayUrl,
1654
+ workspaceId,
1655
+ principal,
1656
+ runtime: runtime2,
1657
+ instance,
1658
+ credentialId: operation === "rotate" ? uuidOption(options, "credential-id") : null,
1659
+ workspaceName: operation === "provision" ? required(options, "workspace-name") : null,
1660
+ displayName: stringOption(options, "display-name") ?? null,
1661
+ runtimeType: stringOption(options, "runtime-type") ?? runtime2,
1662
+ label: stringOption(options, "label") ?? null,
1663
+ scopeSetName: scopeSet(options),
1664
+ expiresAt: timestamp(options, "expires-at"),
1665
+ graceUntil,
1666
+ invalidateImmediately
1667
+ };
1668
+ if (operation === "rotate" && input.displayName !== null) {
1669
+ throw new Error("rotate does not accept --display-name");
1670
+ }
1671
+ const enrollment2 = {
1672
+ schema: "agent-bridge.enrollment",
1673
+ version: 1,
1674
+ provider: "gateway",
1675
+ revision: 0,
1676
+ state: "pending",
1677
+ operation,
1678
+ requestId: request,
1679
+ createdAt: dependencies.now().toISOString(),
1680
+ completedAt: null,
1681
+ input,
1682
+ token: dependencies.token(),
1683
+ result: null
1684
+ };
1685
+ const path = stringOption(options, "enrollment-file") ?? defaultEnrollmentPath(request, env);
1686
+ createPendingEnrollment(path, enrollment2, env);
1687
+ return { path, enrollment: enrollment2 };
1688
+ }
1689
+ function enrollment(operation, options, env, dependencies) {
1690
+ const resume = stringOption(options, "resume");
1691
+ if (!resume) return initialEnrollment(operation, options, env, dependencies);
1692
+ const loaded = readEnrollment(resume, env);
1693
+ if (loaded.operation !== operation) {
1694
+ throw new Error("enrollment operation is " + loaded.operation + ", not " + operation);
1695
+ }
1696
+ return { path: resume, enrollment: loaded };
1697
+ }
1698
+ function publicEnrollment(path, enrollment2) {
1699
+ if (!enrollment2.result) throw new Error("enrollment has no completed result");
1700
+ return {
1701
+ schemaVersion: 1,
1702
+ status: "ok",
1703
+ operation: enrollment2.operation,
1704
+ requestId: enrollment2.requestId,
1705
+ enrollmentFile: path,
1706
+ enrollmentState: enrollment2.state,
1707
+ ...enrollment2.result
1708
+ };
1709
+ }
1710
+ async function runProvision(db, path, enrollment2, completedAt, env, lock2) {
1711
+ if (enrollment2.state !== "pending") return publicEnrollment(path, enrollment2);
1712
+ const input = enrollment2.input;
1713
+ const response = await ownerQuery(
1714
+ db,
1715
+ "SELECT * FROM agent_bridge.control_provision($1::uuid,$2::text,$3::text,$4::text,$5::text,$6::text,$7::char(64),$8::text,$9::text,$10::timestamptz)",
1716
+ [
1717
+ enrollment2.requestId,
1718
+ input.workspaceId,
1719
+ input.workspaceName,
1720
+ input.principal,
1721
+ input.displayName,
1722
+ input.runtimeType,
1723
+ enrollmentTokenHash(enrollment2),
1724
+ input.label,
1725
+ input.scopeSetName,
1726
+ input.expiresAt
1727
+ ]
1728
+ );
1729
+ const row = response.rows[0];
1730
+ if (!row) throw new Error("owner provision returned no result");
1731
+ const result = {
1732
+ workspaceId: row.workspace_id,
1733
+ principal: input.principal,
1734
+ agentId: row.agent_id,
1735
+ credentialId: row.credential_id,
1736
+ replayed: row.replayed
1737
+ };
1738
+ const ready = transitionEnrollment(path, enrollment2, "ready", { completedAt, result }, env, lock2);
1739
+ return publicEnrollment(path, ready);
1740
+ }
1741
+ async function runRotate(db, path, enrollment2, completedAt, env, lock2) {
1742
+ if (enrollment2.state !== "pending") return publicEnrollment(path, enrollment2);
1743
+ const input = enrollment2.input;
1744
+ const response = await ownerQuery(
1745
+ db,
1746
+ "SELECT * FROM agent_bridge.control_rotate_credential($1::uuid,$2::uuid,$3::text,$4::text,$5::char(64),$6::text,$7::text,$8::timestamptz,$9::timestamptz)",
1747
+ [
1748
+ enrollment2.requestId,
1749
+ input.credentialId,
1750
+ input.workspaceId,
1751
+ input.principal,
1752
+ enrollmentTokenHash(enrollment2),
1753
+ input.label,
1754
+ input.scopeSetName,
1755
+ input.expiresAt,
1756
+ input.graceUntil
1757
+ ]
1758
+ );
1759
+ const row = response.rows[0];
1760
+ if (!row) throw new Error("owner rotation returned no result");
1761
+ const result = {
1762
+ workspaceId: row.workspace_id,
1763
+ principal: row.principal,
1764
+ agentId: null,
1765
+ credentialId: row.credential_id,
1766
+ replayed: row.replayed
1767
+ };
1768
+ const ready = transitionEnrollment(path, enrollment2, "ready", { completedAt, result }, env, lock2);
1769
+ return publicEnrollment(path, ready);
1770
+ }
1771
+ function decodeCursor(raw, workspace) {
1772
+ if (!raw) return null;
1773
+ let parsed;
1774
+ try {
1775
+ parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
1776
+ } catch {
1777
+ throw new Error("--cursor is not a valid owner inventory cursor");
1778
+ }
1779
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1780
+ throw new Error("--cursor is not a valid owner inventory cursor");
1781
+ }
1782
+ const cursor2 = parsed;
1783
+ if (cursor2.v !== 1 || cursor2.workspace !== workspace || typeof cursor2.createdAt !== "string" || typeof cursor2.credentialId !== "string") {
1784
+ throw new Error("--cursor does not match this inventory request");
1785
+ }
1786
+ let createdAt;
1787
+ try {
1788
+ createdAt = canonicalTimestamp(cursor2.createdAt, "inventory cursor createdAt");
1789
+ } catch {
1790
+ throw new Error("--cursor is not a valid owner inventory cursor");
1791
+ }
1792
+ if (createdAt === null) throw new Error("--cursor is not a valid owner inventory cursor");
1793
+ let credentialId;
1794
+ try {
1795
+ credentialId = uuidValue(cursor2.credentialId, "inventory cursor credentialId");
1796
+ } catch {
1797
+ throw new Error("--cursor is not a valid owner inventory cursor");
1798
+ }
1799
+ return { v: 1, workspace, createdAt, credentialId };
1800
+ }
1801
+ function encodeCursor(cursor2) {
1802
+ return Buffer.from(JSON.stringify(cursor2), "utf8").toString("base64url");
1803
+ }
1804
+ function instant(value) {
1805
+ if (value === null || value === void 0) return null;
1806
+ return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
1807
+ }
1808
+ async function runInventory(db, options) {
1809
+ const rawLimit = stringOption(options, "limit");
1810
+ const limit = rawLimit === void 0 ? 100 : Number(rawLimit);
1811
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) {
1812
+ throw new Error("--limit must be an integer between 1 and 1000");
1813
+ }
1814
+ const workspace = stringOption(options, "workspace") ?? null;
1815
+ const cursor2 = decodeCursor(stringOption(options, "cursor"), workspace);
1816
+ const response = await ownerQuery(
1817
+ db,
1818
+ "SELECT * FROM agent_bridge.control_credential_inventory($1::text,$2::timestamptz,$3::uuid,$4::integer)",
1819
+ [workspace, cursor2?.createdAt ?? null, cursor2?.credentialId ?? null, limit]
1820
+ );
1821
+ const credentials = response.rows.map((row) => ({
1822
+ credentialId: row.credential_id,
1823
+ workspaceId: row.workspace_id,
1824
+ principal: row.principal,
1825
+ label: row.label,
1826
+ scopes: row.scopes,
1827
+ scopeSetName: row.scope_set_name,
1828
+ expiresAt: instant(row.expires_at),
1829
+ revokedAt: instant(row.revoked_at),
1830
+ revokedBy: row.revoked_by,
1831
+ revocationReason: row.revocation_reason,
1832
+ replacesCredentialId: row.replaces_credential_id,
1833
+ expiryGraceUntil: instant(row.expiry_grace_until),
1834
+ createdAt: instant(row.created_at),
1835
+ lastUsedAt: instant(row.last_used_at),
1836
+ agentDisabledAt: instant(row.agent_disabled_at),
1837
+ workspaceDisabledAt: instant(row.workspace_disabled_at)
1838
+ }));
1839
+ const last = credentials.length ? credentials[credentials.length - 1] : void 0;
1840
+ const nextCursor = credentials.length === limit && last?.createdAt ? encodeCursor({
1841
+ v: 1,
1842
+ workspace,
1843
+ createdAt: last.createdAt,
1844
+ credentialId: String(last.credentialId)
1845
+ }) : null;
1846
+ return {
1847
+ schemaVersion: 1,
1848
+ status: "ok",
1849
+ operation: "inventory",
1850
+ items: credentials,
1851
+ page: { limit, nextCursor }
1852
+ };
1853
+ }
1854
+ async function runRevoke(db, options, dependencies) {
1855
+ const selectedRequestId = requestId(options, dependencies);
1856
+ const credentialId = uuidOption(options, "credential-id");
1857
+ const reason = stringOption(options, "reason") ?? "operator_request";
1858
+ if (!["operator_request", "rotation", "compromise", "retired"].includes(reason)) {
1859
+ throw new Error("--reason must be operator_request, rotation, compromise, or retired");
1860
+ }
1861
+ const response = await ownerQuery(
1862
+ db,
1863
+ "SELECT * FROM agent_bridge.control_revoke_credential($1::uuid,$2::uuid,$3::text)",
1864
+ [selectedRequestId, credentialId, reason]
1865
+ );
1866
+ const row = response.rows[0];
1867
+ if (!row) throw new Error("owner revoke returned no result");
1868
+ return {
1869
+ schemaVersion: 1,
1870
+ status: "ok",
1871
+ operation: "revoke",
1872
+ requestId: selectedRequestId,
1873
+ credentialId,
1874
+ revoked: row.revoked,
1875
+ replayed: row.replayed
1876
+ };
1877
+ }
1878
+ async function runOwnerCommand(subcommand, options, env = process.env, dependencies = defaultDependencies) {
1879
+ if (!subcommand || subcommand === "help") {
1880
+ if (Object.keys(options).length) throw new Error("owner help does not accept options");
1881
+ return {
1882
+ command: "agent-bridge owner",
1883
+ subcommands: ["provision", "inventory", "rotate", "revoke"],
1884
+ authority: "AGENT_BRIDGE_OPERATOR_DATABASE_URL"
1885
+ };
1886
+ }
1887
+ if (!["provision", "inventory", "rotate", "revoke"].includes(subcommand)) {
1888
+ throw new Error("unsupported owner command: " + subcommand);
1889
+ }
1890
+ validateOwnerOptions(subcommand, options);
1891
+ if (options["recover-lock"] === true && !stringOption(options, "resume")) {
1892
+ throw new Error("--recover-lock requires --resume");
1893
+ }
1894
+ const databaseUrl = env.AGENT_BRIDGE_OPERATOR_DATABASE_URL?.trim();
1895
+ if (!databaseUrl) throw new Error("AGENT_BRIDGE_OPERATOR_DATABASE_URL is required");
1896
+ let connection;
1897
+ try {
1898
+ connection = dependencies.connect(databaseUrl);
1899
+ } catch {
1900
+ throw new Error("owner database connection failed");
1901
+ }
1902
+ try {
1903
+ if (subcommand === "inventory") return await runInventory(connection.db, options);
1904
+ if (subcommand === "revoke") return await runRevoke(connection.db, options, dependencies);
1905
+ const mutation = subcommand;
1906
+ const selected = enrollment(mutation, options, env, dependencies);
1907
+ if (options["recover-lock"] === true) recoverEnrollmentLock(selected.path, env);
1908
+ const lock2 = acquireEnrollmentLock(selected.path, env);
1909
+ try {
1910
+ const current = readEnrollment(selected.path, env);
1911
+ if (current.operation !== mutation) {
1912
+ throw new Error("enrollment operation is " + current.operation + ", not " + mutation);
1913
+ }
1914
+ if (subcommand === "provision") {
1915
+ return await runProvision(
1916
+ connection.db,
1917
+ selected.path,
1918
+ current,
1919
+ dependencies.now().toISOString(),
1920
+ env,
1921
+ lock2
1922
+ );
1923
+ }
1924
+ return await runRotate(
1925
+ connection.db,
1926
+ selected.path,
1927
+ current,
1928
+ dependencies.now().toISOString(),
1929
+ env,
1930
+ lock2
1931
+ );
1932
+ } finally {
1933
+ releaseEnrollmentLock(lock2);
1934
+ }
1935
+ } catch (error) {
1936
+ if (!(error instanceof OwnerDatabaseFailure)) throw error;
1937
+ const code = error.driverCode;
1938
+ if (code === "42501") throw new Error("owner database authorization denied");
1939
+ if (code === "23505") throw new Error("owner request conflicts with existing state");
1940
+ if (code === "22P02" || code === "22007" || code === "22023") {
1941
+ throw new Error("owner database rejected invalid input");
1942
+ }
1943
+ throw new Error("owner database operation failed");
1944
+ } finally {
1945
+ try {
1946
+ await connection.close();
1947
+ } catch {
1948
+ throw new Error("owner database close failed");
1949
+ }
1950
+ }
1951
+ }
1952
+
1953
+ // src/archive-cli.ts
1954
+ import { randomUUID as randomUUID4 } from "crypto";
1955
+ import {
1956
+ closeSync as closeSync3,
1957
+ constants as constants3,
1958
+ existsSync as existsSync3,
1959
+ fstatSync as fstatSync2,
1960
+ fsyncSync as fsyncSync3,
1961
+ linkSync,
1962
+ lstatSync as lstatSync2,
1963
+ openSync as openSync3,
1964
+ renameSync as renameSync3,
1965
+ unlinkSync,
1966
+ writeSync
1967
+ } from "fs";
1968
+ import { homedir as homedir3 } from "os";
1969
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join3, resolve as resolve3 } from "path";
1970
+ import pg2 from "pg";
1971
+ var BOOLEAN_OPTIONS = /* @__PURE__ */ new Set(["apply", "dry-run", "force"]);
1972
+ var OPERATION_OPTIONS = {
1973
+ export: /* @__PURE__ */ new Set(["provider", "workspace", "output", "db", "config", "force", "request-id"]),
1974
+ verify: /* @__PURE__ */ new Set(["file"]),
1975
+ import: /* @__PURE__ */ new Set(["provider", "file", "db", "config", "workspace", "request-id", "apply", "dry-run"])
1976
+ };
1977
+ var ArchiveCommandError = class extends Error {
1978
+ constructor(code, message, details) {
1979
+ super(message);
1980
+ this.code = code;
1981
+ this.details = details;
1982
+ }
1983
+ };
1984
+ function fail(code, message, details) {
1985
+ throw new ArchiveCommandError(code, message, details);
1986
+ }
1987
+ function parseArchiveArgs(argv) {
1988
+ const operation = argv[0];
1989
+ if (operation !== "export" && operation !== "verify" && operation !== "import") fail("INVALID_COMMAND", "usage: agent-bridge archive <export|verify|import>");
1990
+ const options = {};
1991
+ for (let index = 1; index < argv.length; index += 1) {
1992
+ const argument = argv[index];
1993
+ if (!argument.startsWith("--") || argument === "--") fail("INVALID_ARGUMENT", `unexpected positional argument: ${argument}`);
1994
+ const name = argument.slice(2);
1995
+ if (!name || name.includes("=")) fail("INVALID_OPTION", `invalid option: ${argument}`);
1996
+ if (!OPERATION_OPTIONS[operation].has(name)) fail("INVALID_OPTION", `--${name} is not valid for archive ${operation}`);
1997
+ if (options[name] !== void 0) fail("DUPLICATE_OPTION", `--${name} may only be provided once`);
1998
+ if (BOOLEAN_OPTIONS.has(name)) {
1999
+ options[name] = true;
2000
+ continue;
2001
+ }
2002
+ const selected = argv[index + 1];
2003
+ if (!selected || selected.startsWith("--")) fail("MISSING_OPTION_VALUE", `--${name} requires a value`);
2004
+ options[name] = selected;
2005
+ index += 1;
2006
+ }
2007
+ if (operation === "import" && options.apply === true && options["dry-run"] === true) fail("INVALID_OPTION", "--apply and --dry-run are mutually exclusive");
2008
+ return { operation, options };
2009
+ }
2010
+ var stringOption2 = (options, name) => typeof options[name] === "string" ? options[name] : void 0;
2011
+ function archiveRequestId(options) {
2012
+ const value = stringOption2(options, "request-id") ?? randomUUID4();
2013
+ try {
2014
+ return validatePortableArchiveRequestId(value);
2015
+ } catch {
2016
+ return fail("INVALID_REQUEST_ID", "--request-id must be a UUID");
2017
+ }
2018
+ }
2019
+ function required2(options, name) {
2020
+ const selected = stringOption2(options, name)?.trim();
2021
+ if (!selected) fail("MISSING_OPTION", `--${name} is required`);
2022
+ return selected;
2023
+ }
2024
+ function provider(options) {
2025
+ const selected = required2(options, "provider");
2026
+ if (selected !== "local" && selected !== "postgres") fail("INVALID_PROVIDER", "--provider must be local or postgres");
2027
+ if (selected === "postgres" && (options.db !== void 0 || options.config !== void 0)) fail("INVALID_OPTION", "PostgreSQL archives accept authority only from AGENT_BRIDGE_ARCHIVE_DATABASE_URL");
2028
+ return selected;
2029
+ }
2030
+ var home = (env) => env.HOME?.trim() || homedir3();
2031
+ function localDatabase(options, env) {
2032
+ const configPath = stringOption2(options, "config") ?? env.AGENT_BRIDGE_CONFIG?.trim() ?? join3(home(env), ".agent-bridge", "config");
2033
+ const config = readClientConfigFile(configPath);
2034
+ const selected = stringOption2(options, "db") ?? env.AGENT_BRIDGE_DB?.trim() ?? config.AGENT_BRIDGE_DB?.trim() ?? join3(home(env), ".agent-bridge", "bridge.sqlite3");
2035
+ return isAbsolute2(selected) ? selected : resolve3(selected);
2036
+ }
2037
+ function createStore(selected, options, env) {
2038
+ if (selected === "local") {
2039
+ const store = new SQLitePortableArchiveStore(localDatabase(options, env));
2040
+ return { store, close: async () => {
2041
+ store.close();
2042
+ } };
2043
+ }
2044
+ const databaseUrl = env.AGENT_BRIDGE_ARCHIVE_DATABASE_URL?.trim();
2045
+ if (!databaseUrl) fail("MISSING_AUTHORITY", "AGENT_BRIDGE_ARCHIVE_DATABASE_URL is required");
2046
+ const pool = new pg2.Pool({ connectionString: databaseUrl, max: 1, connectionTimeoutMillis: 1e4, query_timeout: 3e4 });
2047
+ return { store: new PostgresPortableArchiveStore(pool), close: () => pool.end() };
2048
+ }
2049
+ function syncDirectory2(path) {
2050
+ if (process.platform === "win32") return;
2051
+ const descriptor = openSync3(path, "r");
2052
+ try {
2053
+ fsyncSync3(descriptor);
2054
+ } finally {
2055
+ closeSync3(descriptor);
2056
+ }
2057
+ }
2058
+ async function publishArchive(path, force, session, workspace, options) {
2059
+ const exportRequestId = validatePortableArchiveRequestId(options.requestId);
2060
+ const target = resolve3(path);
2061
+ const directory = dirname3(target);
2062
+ const temporary = join3(directory, `.${exportRequestId}.agent-bridge-archive.tmp`);
2063
+ const backup = join3(directory, `.${exportRequestId}.agent-bridge-archive.backup`);
2064
+ const operations = {
2065
+ link: options.fileOperations?.link ?? linkSync,
2066
+ rename: options.fileOperations?.rename ?? renameSync3,
2067
+ unlink: options.fileOperations?.unlink ?? unlinkSync,
2068
+ syncDirectory: options.fileOperations?.syncDirectory ?? syncDirectory2
2069
+ };
2070
+ let descriptor;
2071
+ let created = false;
2072
+ let linked = false;
2073
+ let replacementVisible = false;
2074
+ let durablyPublished = false;
2075
+ let backupCreated = false;
2076
+ let preserveBackup = false;
2077
+ let targetExisted = false;
2078
+ const ambiguous = (message) => new ArchiveCommandError(
2079
+ "ARCHIVE_PUBLICATION_AMBIGUOUS",
2080
+ message,
2081
+ {
2082
+ requestId: exportRequestId,
2083
+ file: target,
2084
+ published: "unknown",
2085
+ auditStatus: "started",
2086
+ ...backupCreated ? { backupFile: backup } : {}
2087
+ }
2088
+ );
2089
+ try {
2090
+ verifyPrivatePathAccess(directory, "directory");
2091
+ if (existsSync3(target)) {
2092
+ const stat = lstatSync2(target);
2093
+ if (stat.isSymbolicLink() || !stat.isFile()) fail("INVALID_OUTPUT", "archive output must be a regular file");
2094
+ if (!force) fail("OUTPUT_EXISTS", "archive output already exists; pass --force to replace it");
2095
+ targetExisted = true;
2096
+ }
2097
+ descriptor = openSync3(temporary, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL, 384);
2098
+ created = true;
2099
+ securePrivatePath(temporary, "file");
2100
+ const metadata = await (options.stream ?? streamPortableArchive)(session, workspace, exportRequestId, (bytes) => {
2101
+ let offset = 0;
2102
+ while (offset < bytes.length) offset += writeSync(descriptor, bytes, offset, bytes.length - offset);
2103
+ });
2104
+ fsyncSync3(descriptor);
2105
+ const opened = fstatSync2(descriptor);
2106
+ const named = lstatSync2(temporary);
2107
+ if (opened.dev !== named.dev || opened.ino !== named.ino || !named.isFile() || named.isSymbolicLink()) fail("INVALID_OUTPUT", "archive temporary path identity changed before publication");
2108
+ if (force && targetExisted) {
2109
+ const noFollow = typeof constants3.O_NOFOLLOW === "number" ? constants3.O_NOFOLLOW : 0;
2110
+ const targetDescriptor = openSync3(target, constants3.O_RDONLY | noFollow);
2111
+ try {
2112
+ const openedTarget = fstatSync2(targetDescriptor);
2113
+ const namedTarget = lstatSync2(target);
2114
+ if (!openedTarget.isFile() || namedTarget.isSymbolicLink() || openedTarget.dev !== namedTarget.dev || openedTarget.ino !== namedTarget.ino) {
2115
+ fail("INVALID_OUTPUT", "archive output identity changed before replacement");
2116
+ }
2117
+ verifyPrivatePathAccess(target, "file");
2118
+ operations.link(target, backup);
2119
+ backupCreated = true;
2120
+ const backupStat = lstatSync2(backup);
2121
+ if (!backupStat.isFile() || backupStat.isSymbolicLink() || openedTarget.dev !== backupStat.dev || openedTarget.ino !== backupStat.ino) {
2122
+ fail("INVALID_OUTPUT", "archive backup identity changed before replacement");
2123
+ }
2124
+ verifyPrivatePathAccess(backup, "file");
2125
+ operations.syncDirectory(directory);
2126
+ } finally {
2127
+ closeSync3(targetDescriptor);
2128
+ }
2129
+ }
2130
+ if (force) {
2131
+ operations.rename(temporary, target);
2132
+ created = false;
2133
+ replacementVisible = true;
2134
+ } else {
2135
+ try {
2136
+ operations.link(temporary, target);
2137
+ } catch (error) {
2138
+ if (error.code === "EEXIST") fail("OUTPUT_EXISTS", "archive output already exists; pass --force to replace it");
2139
+ throw error;
2140
+ }
2141
+ linked = true;
2142
+ operations.unlink(temporary);
2143
+ created = false;
2144
+ }
2145
+ verifyPrivatePathAccess(target, "file");
2146
+ operations.syncDirectory(directory);
2147
+ durablyPublished = true;
2148
+ if (backupCreated) {
2149
+ try {
2150
+ operations.unlink(backup);
2151
+ } catch {
2152
+ preserveBackup = true;
2153
+ fail("ARCHIVE_BACKUP_RETAINED", "archive was published but backup cleanup could not be proved; retry the same request ID", {
2154
+ requestId: exportRequestId,
2155
+ file: target,
2156
+ backupFile: backup,
2157
+ backupState: "retained",
2158
+ published: true,
2159
+ auditStatus: "started"
2160
+ });
2161
+ }
2162
+ backupCreated = false;
2163
+ try {
2164
+ operations.syncDirectory(directory);
2165
+ } catch {
2166
+ fail("ARCHIVE_BACKUP_CLEANUP_AMBIGUOUS", "archive was published but backup deletion durability is unknown; retry the same request ID", {
2167
+ requestId: exportRequestId,
2168
+ file: target,
2169
+ backupFile: backup,
2170
+ backupState: "unknown",
2171
+ published: true,
2172
+ auditStatus: "started"
2173
+ });
2174
+ }
2175
+ }
2176
+ try {
2177
+ await session.complete({ ...metadata, publishedAt: (/* @__PURE__ */ new Date()).toISOString() });
2178
+ } catch {
2179
+ let auditStatus = "unknown";
2180
+ try {
2181
+ await session.abandon("audit_failed");
2182
+ auditStatus = "abandoned";
2183
+ } catch {
2184
+ }
2185
+ const details = { requestId: exportRequestId, file: target, published: true, auditStatus };
2186
+ if (auditStatus === "abandoned") fail("ARCHIVE_AUDIT_ABANDONED", "archive was published but its completion audit was abandoned", details);
2187
+ fail("ARCHIVE_AUDIT_AMBIGUOUS", "archive was published but its completion audit outcome is unknown", details);
2188
+ }
2189
+ return metadata;
2190
+ } catch (error) {
2191
+ if (!durablyPublished) {
2192
+ if (force && replacementVisible) {
2193
+ if (backupCreated) {
2194
+ try {
2195
+ operations.rename(backup, target);
2196
+ backupCreated = false;
2197
+ replacementVisible = false;
2198
+ verifyPrivatePathAccess(target, "file");
2199
+ operations.syncDirectory(directory);
2200
+ } catch {
2201
+ preserveBackup = true;
2202
+ throw ambiguous("archive replacement failed and restoration could not be proved; use owner reconciliation");
2203
+ }
2204
+ } else {
2205
+ try {
2206
+ operations.unlink(target);
2207
+ replacementVisible = false;
2208
+ operations.syncDirectory(directory);
2209
+ } catch {
2210
+ throw ambiguous("archive replacement failed and removal could not be proved; use owner reconciliation");
2211
+ }
2212
+ }
2213
+ } else if (backupCreated) {
2214
+ try {
2215
+ operations.unlink(backup);
2216
+ backupCreated = false;
2217
+ operations.syncDirectory(directory);
2218
+ } catch {
2219
+ preserveBackup = true;
2220
+ throw ambiguous("archive backup cleanup could not be proved; use owner reconciliation");
2221
+ }
2222
+ }
2223
+ if (linked && descriptor !== void 0) {
2224
+ try {
2225
+ const opened = fstatSync2(descriptor);
2226
+ const named = lstatSync2(target);
2227
+ if (opened.dev === named.dev && opened.ino === named.ino) {
2228
+ operations.unlink(target);
2229
+ linked = false;
2230
+ operations.syncDirectory(directory);
2231
+ }
2232
+ } catch {
2233
+ throw ambiguous("archive publication failed and output removal could not be proved; use owner reconciliation");
2234
+ }
2235
+ }
2236
+ try {
2237
+ await session.abandon("publication_failed");
2238
+ } catch {
2239
+ throw new ArchiveCommandError(
2240
+ "ARCHIVE_AUDIT_AMBIGUOUS",
2241
+ "archive publication did not complete but its audit outcome is unknown",
2242
+ { requestId: exportRequestId, file: target, published: false, auditStatus: "unknown" }
2243
+ );
2244
+ }
2245
+ }
2246
+ throw error;
2247
+ } finally {
2248
+ if (descriptor !== void 0) closeSync3(descriptor);
2249
+ if (created) {
2250
+ try {
2251
+ operations.unlink(temporary);
2252
+ } catch {
2253
+ }
2254
+ }
2255
+ if (backupCreated && !preserveBackup && !replacementVisible) {
2256
+ try {
2257
+ operations.unlink(backup);
2258
+ operations.syncDirectory(directory);
2259
+ } catch {
2260
+ }
2261
+ }
2262
+ }
2263
+ }
2264
+ var success = (operation, value) => ({ schemaVersion: 1, status: "ok", operation, ...value });
2265
+ function recoveryArtifactPaths(target, requestId2) {
2266
+ const directory = dirname3(target);
2267
+ return [
2268
+ join3(directory, `.${requestId2}.agent-bridge-archive.tmp`),
2269
+ join3(directory, `.${requestId2}.agent-bridge-archive.backup`)
2270
+ ];
2271
+ }
2272
+ function existingRecoveryArtifacts(target, requestId2) {
2273
+ return recoveryArtifactPaths(target, requestId2).filter((path) => existsSync3(path));
2274
+ }
2275
+ function openVerifiedPrivateFile(path) {
2276
+ verifyPrivatePathAccess(path, "file");
2277
+ const before = lstatSync2(path);
2278
+ const noFollow = typeof constants3.O_NOFOLLOW === "number" ? constants3.O_NOFOLLOW : 0;
2279
+ const descriptor = openSync3(path, constants3.O_RDONLY | noFollow);
2280
+ try {
2281
+ const opened = fstatSync2(descriptor);
2282
+ const after = lstatSync2(path);
2283
+ if (!opened.isFile() || before.isSymbolicLink() || after.isSymbolicLink() || opened.dev !== before.dev || opened.ino !== before.ino || opened.dev !== after.dev || opened.ino !== after.ino) {
2284
+ fail("INVALID_OUTPUT", "archive recovery path identity changed");
2285
+ }
2286
+ return descriptor;
2287
+ } catch (error) {
2288
+ closeSync3(descriptor);
2289
+ throw error;
2290
+ }
2291
+ }
2292
+ function removeRecoveryArtifacts(target, requestId2) {
2293
+ for (const path of existingRecoveryArtifacts(target, requestId2)) {
2294
+ const descriptor = openVerifiedPrivateFile(path);
2295
+ try {
2296
+ const opened = fstatSync2(descriptor);
2297
+ const named = lstatSync2(path);
2298
+ if (opened.dev !== named.dev || opened.ino !== named.ino) fail("INVALID_OUTPUT", "archive recovery path identity changed");
2299
+ unlinkSync(path);
2300
+ } finally {
2301
+ closeSync3(descriptor);
2302
+ }
2303
+ syncDirectory2(dirname3(path));
2304
+ }
2305
+ }
2306
+ function verifyCompletedExport(path, requestId2, expected) {
2307
+ const target = resolve3(path);
2308
+ const recoveryPaths2 = existingRecoveryArtifacts(target, requestId2);
2309
+ if (!existsSync3(target)) fail("ARCHIVE_REPLAY_FILE_MISSING", "completed export file is missing; use a new request ID", { requestId: requestId2, file: target, recoveryPaths: recoveryPaths2 });
2310
+ let archive;
2311
+ try {
2312
+ try {
2313
+ archive = new PortableArchiveFile(target);
2314
+ } catch {
2315
+ fail("ARCHIVE_REPLAY_FILE_MISMATCH", "completed export file is not a valid private archive", { requestId: requestId2, file: target, recoveryPaths: recoveryPaths2 });
2316
+ }
2317
+ let actual;
2318
+ try {
2319
+ actual = archive.verify();
2320
+ } catch {
2321
+ fail("ARCHIVE_REPLAY_FILE_MISMATCH", "completed export file is not a valid matching archive", { requestId: requestId2, file: target, recoveryPaths: recoveryPaths2 });
2322
+ }
2323
+ if (actual.exportRequestId !== requestId2 || expected.exportRequestId !== requestId2 || actual.workspace !== expected.workspace || actual.digest !== expected.digest || actual.messageCount !== expected.messageCount || actual.receiptCount !== expected.receiptCount) {
2324
+ fail("ARCHIVE_REPLAY_FILE_MISMATCH", "completed export file does not match the recorded audit; use a new request ID", { requestId: requestId2, file: target, recoveryPaths: recoveryPaths2 });
2325
+ }
2326
+ try {
2327
+ archive.proveDurable();
2328
+ } catch {
2329
+ fail("ARCHIVE_REPLAY_DURABILITY_AMBIGUOUS", "completed export file durability could not be reproved", { requestId: requestId2, file: target, published: true, auditStatus: "completed", recoveryPaths: recoveryPaths2 });
2330
+ }
2331
+ try {
2332
+ removeRecoveryArtifacts(target, requestId2);
2333
+ } catch {
2334
+ fail("ARCHIVE_RECOVERY_ARTIFACT_RETAINED", "completed export recovery artifacts could not be removed", { requestId: requestId2, file: target, published: true, auditStatus: "completed", recoveryPaths: existingRecoveryArtifacts(target, requestId2) });
2335
+ }
2336
+ try {
2337
+ archive.proveDurable();
2338
+ } catch {
2339
+ fail("ARCHIVE_REPLAY_DURABILITY_AMBIGUOUS", "completed export file durability could not be reproved after cleanup", { requestId: requestId2, file: target, published: true, auditStatus: "completed", recoveryPaths: existingRecoveryArtifacts(target, requestId2) });
2340
+ }
2341
+ return actual;
2342
+ } finally {
2343
+ archive?.close();
2344
+ }
2345
+ }
2346
+ async function reconcileActiveExport(path, requestId2, workspace, session) {
2347
+ const target = resolve3(path);
2348
+ const details = { requestId: requestId2, file: target, recoveryPaths: existingRecoveryArtifacts(target, requestId2), published: "unknown", auditStatus: "started" };
2349
+ if (!existsSync3(target)) fail("ARCHIVE_RECONCILIATION_FILE_MISSING", "active export file is missing; use a new request ID or owner reconciliation", details);
2350
+ let archive;
2351
+ try {
2352
+ try {
2353
+ archive = new PortableArchiveFile(target);
2354
+ } catch {
2355
+ fail("ARCHIVE_RECONCILIATION_FILE_MISMATCH", "active export file is not a valid private archive", details);
2356
+ }
2357
+ let metadata;
2358
+ try {
2359
+ metadata = archive.verify();
2360
+ } catch {
2361
+ fail("ARCHIVE_RECONCILIATION_FILE_MISMATCH", "active export file is not a valid matching archive", details);
2362
+ }
2363
+ if (metadata.exportRequestId !== requestId2 || metadata.workspace !== workspace) {
2364
+ fail("ARCHIVE_RECONCILIATION_FILE_MISMATCH", "active export file does not match the requested workspace; use a new request ID or owner reconciliation", details);
2365
+ }
2366
+ try {
2367
+ archive.proveDurable();
2368
+ } catch {
2369
+ fail("ARCHIVE_RECONCILIATION_DURABILITY_AMBIGUOUS", "active export file durability could not be proved; retry the same request ID", details);
2370
+ }
2371
+ try {
2372
+ removeRecoveryArtifacts(target, requestId2);
2373
+ } catch {
2374
+ fail("ARCHIVE_RECOVERY_ARTIFACT_RETAINED", "active export recovery artifacts could not be removed; retry the same request ID", {
2375
+ ...details,
2376
+ recoveryPaths: existingRecoveryArtifacts(target, requestId2),
2377
+ published: true
2378
+ });
2379
+ }
2380
+ try {
2381
+ archive.proveDurable();
2382
+ } catch {
2383
+ fail("ARCHIVE_RECONCILIATION_DURABILITY_AMBIGUOUS", "active export durability after cleanup could not be proved; retry the same request ID", { ...details, published: true });
2384
+ }
2385
+ const publishedAt = (/* @__PURE__ */ new Date()).toISOString();
2386
+ try {
2387
+ await session.reconcile({ ...metadata, publishedAt });
2388
+ } catch {
2389
+ fail("ARCHIVE_RECONCILIATION_AMBIGUOUS", "active export reconciliation outcome is unknown; retry the same request ID", { ...details, published: true, auditStatus: "unknown" });
2390
+ }
2391
+ return { ...metadata, publishedAt };
2392
+ } finally {
2393
+ archive?.close();
2394
+ }
2395
+ }
2396
+ async function runArchiveCommand(argv, env = process.env) {
2397
+ const { operation, options } = parseArchiveArgs(argv);
2398
+ try {
2399
+ if (operation === "verify") {
2400
+ const path2 = required2(options, "file");
2401
+ let archive2;
2402
+ try {
2403
+ archive2 = new PortableArchiveFile(path2);
2404
+ const metadata = archive2.verify();
2405
+ return success(operation, { file: resolve3(path2), exportRequestId: metadata.exportRequestId, workspace: metadata.workspace, digest: metadata.digest, messages: metadata.messageCount, receipts: metadata.receiptCount });
2406
+ } finally {
2407
+ archive2?.close();
2408
+ }
2409
+ }
2410
+ const selected = provider(options);
2411
+ if (operation === "export") {
2412
+ const workspace = required2(options, "workspace");
2413
+ const path2 = required2(options, "output");
2414
+ const requestId2 = archiveRequestId(options);
2415
+ const connection = createStore(selected, options, env);
2416
+ let session;
2417
+ try {
2418
+ const start = await connection.store.beginExport(requestId2, workspace);
2419
+ if (start.status === "completed") {
2420
+ const metadata2 = verifyCompletedExport(path2, requestId2, start.metadata);
2421
+ return success(operation, { provider: selected, requestId: requestId2, replayed: true, reconciled: false, file: resolve3(path2), workspace, digest: metadata2.digest, messages: metadata2.messageCount, receipts: metadata2.receiptCount, publishedAt: start.metadata.publishedAt });
2422
+ }
2423
+ session = start.session;
2424
+ if (start.replayed) {
2425
+ const metadata2 = await reconcileActiveExport(path2, requestId2, workspace, session);
2426
+ return success(operation, { provider: selected, requestId: requestId2, replayed: true, reconciled: true, file: resolve3(path2), workspace, digest: metadata2.digest, messages: metadata2.messageCount, receipts: metadata2.receiptCount, publishedAt: metadata2.publishedAt });
2427
+ }
2428
+ const metadata = await publishArchive(path2, options.force === true, session, workspace, { requestId: requestId2 });
2429
+ return success(operation, { provider: selected, requestId: requestId2, replayed: false, reconciled: false, file: resolve3(path2), workspace, digest: metadata.digest, messages: metadata.messageCount, receipts: metadata.receiptCount });
2430
+ } finally {
2431
+ await session?.close();
2432
+ await connection.close();
2433
+ }
2434
+ }
2435
+ const path = required2(options, "file");
2436
+ const archive = new PortableArchiveFile(path);
2437
+ try {
2438
+ const metadata = archive.verify();
2439
+ const assertedWorkspace = stringOption2(options, "workspace")?.trim();
2440
+ if (assertedWorkspace && assertedWorkspace !== metadata.workspace) fail("WORKSPACE_MISMATCH", "--workspace does not match the archive workspace");
2441
+ const requestId2 = archiveRequestId(options);
2442
+ const connection = createStore(selected, options, env);
2443
+ try {
2444
+ const result = await connection.store.importWorkspace(requestId2, metadata, archive, { apply: options.apply === true });
2445
+ return success(operation, { provider: selected, file: resolve3(path), exportRequestId: metadata.exportRequestId, ...result });
2446
+ } finally {
2447
+ await connection.close();
2448
+ }
2449
+ } finally {
2450
+ archive.close();
2451
+ }
2452
+ } catch (error) {
2453
+ if (error instanceof ArchiveCommandError) throw error;
2454
+ if (error instanceof PrivatePathError) fail("INSECURE_PATH", "archive file or directory does not satisfy the private path policy");
2455
+ if (error instanceof PortableArchiveError) fail("INVALID_ARCHIVE", error.message);
2456
+ fail("ARCHIVE_OPERATION_FAILED", "archive operation failed");
2457
+ }
2458
+ }
2459
+
2460
+ // src/dr-cli.ts
2461
+ import { createHash as createHash3, randomUUID as randomUUID5 } from "crypto";
2462
+ import {
2463
+ closeSync as closeSync4,
2464
+ constants as constants4,
2465
+ existsSync as existsSync4,
2466
+ fstatSync as fstatSync3,
2467
+ fsyncSync as fsyncSync4,
2468
+ linkSync as linkSync2,
2469
+ lstatSync as lstatSync3,
2470
+ mkdirSync as mkdirSync3,
2471
+ openSync as openSync4,
2472
+ rmSync as rmSync3,
2473
+ unlinkSync as unlinkSync2
2474
+ } from "fs";
2475
+ import { dirname as dirname4, join as join4, resolve as resolve4 } from "path";
2476
+ var POSTGRES_DUMP_ENTRY = "postgres/database.dump";
2477
+ var POSTGRES_ROLES_ENTRY = "postgres/roles.json";
2478
+ var OPTIONS = {
2479
+ backup: /* @__PURE__ */ new Set(["provider", "source", "output", "backup-id", "timeout-ms", "tool-directory"]),
2480
+ verify: /* @__PURE__ */ new Set(["provider", "bundle", "tool-directory"]),
2481
+ restore: /* @__PURE__ */ new Set(["provider", "bundle", "target", "request-id", "timeout-ms", "tool-directory", "accept-source-sql-risk"])
2482
+ };
2483
+ var FLAGS = {
2484
+ backup: /* @__PURE__ */ new Set(),
2485
+ verify: /* @__PURE__ */ new Set(),
2486
+ restore: /* @__PURE__ */ new Set(["accept-source-sql-risk"])
2487
+ };
2488
+ var fail2 = (code, message, details) => {
2489
+ throw new NativeDrCommandError(code, message, details);
2490
+ };
2491
+ function parse2(argv) {
2492
+ const operation = argv[0];
2493
+ if (operation !== "backup" && operation !== "verify" && operation !== "restore") {
2494
+ fail2("INVALID_COMMAND", "usage: agent-bridge dr <backup|verify|restore>");
2495
+ }
2496
+ const selectedOperation = operation;
2497
+ const options = {};
2498
+ for (let index = 1; index < argv.length; index += 1) {
2499
+ const argument = argv[index];
2500
+ if (!argument.startsWith("--") || argument === "--") {
2501
+ fail2("INVALID_ARGUMENT", "DR commands do not accept positional arguments");
2502
+ }
2503
+ const name = argument.slice(2);
2504
+ if (!name || name.includes("=") || !OPTIONS[selectedOperation].has(name)) {
2505
+ fail2("INVALID_OPTION", `--${name} is not valid for dr ${selectedOperation}`);
2506
+ }
2507
+ if (options[name] !== void 0) fail2("DUPLICATE_OPTION", `--${name} may only be provided once`);
2508
+ if (FLAGS[selectedOperation].has(name)) {
2509
+ options[name] = true;
2510
+ continue;
2511
+ }
2512
+ const value = argv[index + 1];
2513
+ if (!value || value.startsWith("--")) fail2("MISSING_OPTION_VALUE", `--${name} requires a value`);
2514
+ options[name] = value;
2515
+ index += 1;
2516
+ }
2517
+ return { operation: selectedOperation, options };
2518
+ }
2519
+ function optional(options, name) {
2520
+ const value = options[name];
2521
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
2522
+ }
2523
+ function required3(options, name) {
2524
+ const value = optional(options, name);
2525
+ if (!value) fail2("MISSING_OPTION", `--${name} is required`);
2526
+ return value;
2527
+ }
2528
+ function timeout(options) {
2529
+ const value = optional(options, "timeout-ms");
2530
+ return value === void 0 ? void 0 : validateNativeDrTimeout(Number(value));
2531
+ }
2532
+ function provider2(options) {
2533
+ const selected = required3(options, "provider");
2534
+ if (selected !== "local" && selected !== "postgres") fail2("INVALID_PROVIDER", "--provider must be local or postgres");
2535
+ return selected;
2536
+ }
2537
+ function rejectProviderOption(options, name, selected, operation) {
2538
+ if (options[name] !== void 0) fail2("INVALID_OPTION", `--${name} is not valid for ${selected} DR ${operation}`);
2539
+ }
2540
+ function syncFile(path) {
2541
+ const before = lstatSync3(path);
2542
+ const noFollow = typeof constants4.O_NOFOLLOW === "number" ? constants4.O_NOFOLLOW : 0;
2543
+ const descriptor = openSync4(path, constants4.O_RDWR | noFollow);
2544
+ try {
2545
+ const opened = fstatSync3(descriptor);
2546
+ const named = lstatSync3(path);
2547
+ if (!opened.isFile() || before.isSymbolicLink() || named.isSymbolicLink() || !sameNode(before, opened) || !sameNode(opened, named)) {
2548
+ fail2("INSECURE_PATH", "DR publication path identity changed before file synchronization");
2549
+ }
2550
+ fsyncSync4(descriptor);
2551
+ } finally {
2552
+ closeSync4(descriptor);
2553
+ }
2554
+ }
2555
+ function syncDirectory3(path, platform = process.platform) {
2556
+ if (platform === "win32") return false;
2557
+ const descriptor = openSync4(path, constants4.O_RDONLY);
2558
+ try {
2559
+ fsyncSync4(descriptor);
2560
+ } finally {
2561
+ closeSync4(descriptor);
2562
+ }
2563
+ return true;
2564
+ }
2565
+ function fileOperations(dependencies) {
2566
+ return {
2567
+ link: dependencies.fileOperations?.link ?? linkSync2,
2568
+ syncDirectory: dependencies.fileOperations?.syncDirectory ?? ((path) => syncDirectory3(path, dependencies.platform ?? process.platform)),
2569
+ syncFile: dependencies.fileOperations?.syncFile ?? syncFile,
2570
+ afterPublish: dependencies.fileOperations?.afterPublish
2571
+ };
2572
+ }
2573
+ function sameNode(left, right) {
2574
+ return left.dev === right.dev && left.ino === right.ino;
2575
+ }
2576
+ function createPrivateDirectory(path) {
2577
+ mkdirSync3(path, { mode: 448 });
2578
+ const before = lstatSync3(path);
2579
+ securePrivatePath(path, "directory");
2580
+ verifyPrivatePathAccess(path, "directory");
2581
+ const after = lstatSync3(path);
2582
+ if (!before.isDirectory() || before.isSymbolicLink() || !sameNode(before, after)) {
2583
+ fail2("INSECURE_PATH", "DR staging directory identity changed while securing it");
2584
+ }
2585
+ return after;
2586
+ }
2587
+ function openPrivateFile(path) {
2588
+ const noFollow = typeof constants4.O_NOFOLLOW === "number" ? constants4.O_NOFOLLOW : 0;
2589
+ const descriptor = openSync4(path, constants4.O_RDWR | constants4.O_CREAT | constants4.O_EXCL | noFollow, 384);
2590
+ try {
2591
+ const before = fstatSync3(descriptor);
2592
+ securePrivatePath(path, "file");
2593
+ verifyPrivatePathAccess(path, "file");
2594
+ const after = lstatSync3(path);
2595
+ if (!before.isFile() || after.isSymbolicLink() || !sameNode(before, after)) {
2596
+ fail2("INSECURE_PATH", "DR staging file identity changed while securing it");
2597
+ }
2598
+ return { descriptor, identity: after };
2599
+ } catch (error) {
2600
+ closeSync4(descriptor);
2601
+ throw error;
2602
+ }
2603
+ }
2604
+ function artifactAnchor(descriptor, sha256) {
2605
+ const stat = fstatSync3(descriptor, { bigint: true });
2606
+ if (!stat.isFile()) fail2("ARTIFACT_ANCHOR_INVALID", "PostgreSQL DR artifact anchor is not a regular file");
2607
+ return {
2608
+ descriptor,
2609
+ device: stat.dev.toString(),
2610
+ inode: stat.ino.toString(),
2611
+ size: stat.size.toString(),
2612
+ ctimeNanoseconds: stat.ctimeNs.toString(),
2613
+ mtimeNanoseconds: stat.mtimeNs.toString(),
2614
+ sha256
2615
+ };
2616
+ }
2617
+ function extractPostgresBundle(bundle, stage) {
2618
+ const descriptors = /* @__PURE__ */ new Map();
2619
+ try {
2620
+ for (const [name, filename] of [
2621
+ [POSTGRES_DUMP_ENTRY, "postgres-database.dump"],
2622
+ [POSTGRES_ROLES_ENTRY, "postgres-roles.json"]
2623
+ ]) {
2624
+ const path = join4(stage, filename);
2625
+ descriptors.set(name, { ...openPrivateFile(path), path });
2626
+ }
2627
+ const reader = new NativeDrBundleReader(bundle);
2628
+ let metadata;
2629
+ try {
2630
+ metadata = reader.inspect((entry) => descriptors.get(entry.name)?.descriptor);
2631
+ } finally {
2632
+ reader.close();
2633
+ }
2634
+ if (metadata.manifest.kind !== "postgres") {
2635
+ fail2("PROVIDER_MISMATCH", `DR bundle provider is ${metadata.manifest.kind}, not postgres`);
2636
+ }
2637
+ for (const value of descriptors.values()) {
2638
+ const opened = fstatSync3(value.descriptor);
2639
+ const named = lstatSync3(value.path);
2640
+ if (!opened.isFile() || named.isSymbolicLink() || !sameNode(opened, named)) {
2641
+ fail2("INSECURE_PATH", "DR extraction path identity changed while reading the bundle");
2642
+ }
2643
+ fsyncSync4(value.descriptor);
2644
+ securePrivatePath(value.path, "file");
2645
+ verifyPrivatePathAccess(value.path, "file");
2646
+ }
2647
+ const dump = descriptors.get(POSTGRES_DUMP_ENTRY);
2648
+ const roles = descriptors.get(POSTGRES_ROLES_ENTRY);
2649
+ const dumpEntry = metadata.manifest.entries.find((entry) => entry.name === POSTGRES_DUMP_ENTRY);
2650
+ const rolesEntry = metadata.manifest.entries.find((entry) => entry.name === POSTGRES_ROLES_ENTRY);
2651
+ return {
2652
+ metadata,
2653
+ dumpPath: dump.path,
2654
+ rolesPath: roles.path,
2655
+ artifactAnchors: {
2656
+ dump: artifactAnchor(dump.descriptor, dumpEntry.sha256),
2657
+ roles: artifactAnchor(roles.descriptor, rolesEntry.sha256)
2658
+ },
2659
+ descriptors
2660
+ };
2661
+ } catch (error) {
2662
+ for (const value of descriptors.values()) {
2663
+ try {
2664
+ closeSync4(value.descriptor);
2665
+ } catch {
2666
+ }
2667
+ }
2668
+ throw error;
2669
+ }
111
2670
  }
112
- function writeJsonConfig(path, identity, instance, backendConfigPath, command) {
113
- let config = {};
114
- if (existsSync(path)) {
2671
+ function closeExtractedPostgresBundle(extracted) {
2672
+ for (const value of extracted.descriptors.values()) {
115
2673
  try {
116
- const parsed = JSON.parse(readFileSync(path, "utf8"));
117
- if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") throw new Error();
118
- config = parsed;
2674
+ closeSync4(value.descriptor);
119
2675
  } catch {
120
- throw new Error(`Claude Desktop config is not valid JSON: ${path}`);
121
2676
  }
122
2677
  }
123
- const currentServers = config.mcpServers;
124
- const mcpServers = currentServers && !Array.isArray(currentServers) && typeof currentServers === "object" ? currentServers : {};
125
- const next = {
126
- ...config,
127
- mcpServers: {
128
- ...mcpServers,
129
- "agent-bridge": {
130
- command,
131
- env: {
132
- AGENT_BRIDGE_AGENT: identity,
133
- AGENT_BRIDGE_INSTANCE: instance,
134
- AGENT_BRIDGE_CONFIG: backendConfigPath
2678
+ extracted.descriptors.clear();
2679
+ }
2680
+ function removeOwnedFile(path, identity) {
2681
+ if (!existsSync4(path)) return true;
2682
+ try {
2683
+ const current = lstatSync3(path);
2684
+ if (!identity || !current.isFile() || current.isSymbolicLink() || !sameNode(current, identity)) return false;
2685
+ unlinkSync2(path);
2686
+ return !existsSync4(path);
2687
+ } catch {
2688
+ return false;
2689
+ }
2690
+ }
2691
+ function removeOwnedDirectory(path, identity) {
2692
+ if (!existsSync4(path)) return true;
2693
+ try {
2694
+ const current = lstatSync3(path);
2695
+ if (!identity || !current.isDirectory() || current.isSymbolicLink() || !sameNode(current, identity)) return false;
2696
+ rmSync3(path, { recursive: true });
2697
+ return !existsSync4(path);
2698
+ } catch {
2699
+ return false;
2700
+ }
2701
+ }
2702
+ function recoveryPaths(paths) {
2703
+ return paths.filter(existsSync4);
2704
+ }
2705
+ function verifyProviderBundle(path, expected) {
2706
+ const expectedKind = expected === "local" ? "sqlite" : "postgres";
2707
+ const reader = new NativeDrBundleReader(path);
2708
+ try {
2709
+ const metadata = reader.inspect();
2710
+ if (metadata.manifest.kind !== expectedKind) {
2711
+ fail2("PROVIDER_MISMATCH", `DR bundle provider is ${metadata.manifest.kind}, not ${expected}`);
2712
+ }
2713
+ return expected === "local" ? verifyNativeDrBundle(path) : metadata;
2714
+ } finally {
2715
+ reader.close();
2716
+ }
2717
+ }
2718
+ function commandErrorWithRecovery(error, identity, paths) {
2719
+ const details = error instanceof NativeDrCommandError || error instanceof PostgresNativeDrError ? { ...error.details } : {};
2720
+ const nested = Array.isArray(details.recoveryPaths) ? details.recoveryPaths.filter((path) => typeof path === "string") : [];
2721
+ delete details.recoveryPaths;
2722
+ const retained = recoveryPaths([.../* @__PURE__ */ new Set([...nested, ...paths])]);
2723
+ if (error instanceof NativeDrCommandError) {
2724
+ return new NativeDrCommandError(error.code, error.message, {
2725
+ ...details,
2726
+ ...identity,
2727
+ ...retained.length ? { recoveryPaths: retained } : {}
2728
+ });
2729
+ }
2730
+ if (error instanceof PostgresNativeDrError) {
2731
+ return new NativeDrCommandError(error.code, error.message, {
2732
+ ...details,
2733
+ ...identity,
2734
+ ...retained.length ? { recoveryPaths: retained } : {}
2735
+ });
2736
+ }
2737
+ if (error instanceof PrivatePathError) {
2738
+ return new NativeDrCommandError("INSECURE_PATH", "DR path does not satisfy the private path policy", {
2739
+ ...identity,
2740
+ ...retained.length ? { recoveryPaths: retained } : {}
2741
+ });
2742
+ }
2743
+ if (error instanceof NativeDrBundleError) {
2744
+ return new NativeDrCommandError("INVALID_DR_BUNDLE", "DR bundle verification failed", {
2745
+ ...identity,
2746
+ ...retained.length ? { recoveryPaths: retained } : {}
2747
+ });
2748
+ }
2749
+ return new NativeDrCommandError("DR_COMMAND_FAILED", "DR command failed", {
2750
+ ...identity,
2751
+ ...retained.length ? { recoveryPaths: retained } : {}
2752
+ });
2753
+ }
2754
+ async function backupPostgresBundle(outputPath, backupId, toolDirectory, dependencies) {
2755
+ const target = resolve4(outputPath);
2756
+ preparePrivateFileLocation(target);
2757
+ const directory = dirname4(target);
2758
+ const stage = join4(directory, `.${backupId}.agent-bridge-dr.postgres.stage`);
2759
+ const bundleStage = join4(directory, `.${backupId}.agent-bridge-dr.bundle.tmp`);
2760
+ const priorRecovery = recoveryPaths([bundleStage, stage]);
2761
+ if (priorRecovery.length) fail2("DR_RECOVERY_REQUIRED", "a prior PostgreSQL DR backup with this ID requires recovery", {
2762
+ backupId,
2763
+ output: target,
2764
+ outputExists: existsSync4(target),
2765
+ published: existsSync4(target) ? "unknown" : false,
2766
+ recoveryPaths: priorRecovery
2767
+ });
2768
+ if (existsSync4(target)) fail2("OUTPUT_EXISTS", "DR output already exists");
2769
+ let stageIdentity;
2770
+ let bundleIdentity;
2771
+ let descriptor;
2772
+ let published = false;
2773
+ let durable = false;
2774
+ let directoryDurability = "confirmed";
2775
+ const operations = fileOperations(dependencies);
2776
+ const platform = dependencies.platform ?? process.platform;
2777
+ try {
2778
+ stageIdentity = createPrivateDirectory(stage);
2779
+ operations.syncDirectory(directory);
2780
+ const input = await (dependencies.backupPostgres ?? backupPostgresNativeDr)({
2781
+ stagingDirectory: stage,
2782
+ backupId,
2783
+ environment: dependencies.environment ?? process.env,
2784
+ ...toolDirectory ? { toolDirectory } : {}
2785
+ });
2786
+ if (input.backupId !== backupId || input.kind !== "postgres" || input.entries.length !== 2 || input.entries[0]?.name !== POSTGRES_DUMP_ENTRY || resolve4(input.entries[0].path) !== join4(stage, "postgres-database.dump") || input.entries[1]?.name !== POSTGRES_ROLES_ENTRY || resolve4(input.entries[1].path) !== join4(stage, "postgres-roles.json")) {
2787
+ fail2("POSTGRES_STAGE_INVALID", "PostgreSQL DR adapter returned an unexpected staging contract");
2788
+ }
2789
+ operations.syncDirectory(stage);
2790
+ const opened = openPrivateFile(bundleStage);
2791
+ descriptor = opened.descriptor;
2792
+ bundleIdentity = opened.identity;
2793
+ writeNativeDrBundle(descriptor, input, bundleStage);
2794
+ closeSync4(descriptor);
2795
+ descriptor = void 0;
2796
+ const verificationStage = join4(stage, "bundle-verification");
2797
+ createPrivateDirectory(verificationStage);
2798
+ const extracted = extractPostgresBundle(bundleStage, verificationStage);
2799
+ try {
2800
+ await (dependencies.verifyPostgres ?? verifyPostgresNativeDrArtifacts)({
2801
+ dumpPath: extracted.dumpPath,
2802
+ rolesPath: extracted.rolesPath,
2803
+ schema: extracted.metadata.manifest.schema,
2804
+ artifactAnchors: extracted.artifactAnchors,
2805
+ ...toolDirectory ? { toolDirectory } : {}
2806
+ });
2807
+ } finally {
2808
+ closeExtractedPostgresBundle(extracted);
2809
+ }
2810
+ const beforePublication = extracted.metadata;
2811
+ try {
2812
+ operations.link(bundleStage, target);
2813
+ published = true;
2814
+ } catch (error) {
2815
+ if (error.code === "EEXIST") fail2("OUTPUT_EXISTS", "DR output already exists");
2816
+ throw error;
2817
+ }
2818
+ const publishedIdentity = lstatSync3(target);
2819
+ if (!bundleIdentity || !publishedIdentity.isFile() || publishedIdentity.isSymbolicLink() || !sameNode(bundleIdentity, publishedIdentity)) {
2820
+ fail2("DR_PUBLICATION_INVALID", "published DR output does not reference the verified staging file", {
2821
+ backupId,
2822
+ output: target,
2823
+ published: true,
2824
+ durable: false,
2825
+ verified: false,
2826
+ recoveryPaths: recoveryPaths([bundleStage, stage])
2827
+ });
2828
+ }
2829
+ try {
2830
+ operations.syncFile(target);
2831
+ operations.afterPublish?.(target);
2832
+ if (!operations.syncDirectory(directory)) {
2833
+ if (platform === "win32") directoryDurability = "unavailable";
2834
+ else {
2835
+ fail2("DR_PUBLICATION_AMBIGUOUS", "DR output is visible but directory durability is unproved", {
2836
+ backupId,
2837
+ output: target,
2838
+ published: true,
2839
+ durable: "unknown",
2840
+ platform,
2841
+ recoveryPaths: recoveryPaths([bundleStage, stage])
2842
+ });
135
2843
  }
136
2844
  }
2845
+ durable = true;
2846
+ } catch {
2847
+ fail2("DR_PUBLICATION_AMBIGUOUS", "DR output is visible but durability is unknown", {
2848
+ backupId,
2849
+ output: target,
2850
+ published: true,
2851
+ durable: "unknown",
2852
+ recoveryPaths: recoveryPaths([bundleStage, stage])
2853
+ });
137
2854
  }
138
- };
139
- mkdirSync(dirname(path), { recursive: true, mode: 448 });
140
- const temporary = `${path}.${process.pid}.tmp`;
141
- writeFileSync(temporary, `${JSON.stringify(next, null, 2)}
142
- `, { mode: 384 });
143
- renameSync(temporary, path);
2855
+ let publishedMetadata;
2856
+ try {
2857
+ publishedMetadata = verifyProviderBundle(target, "postgres");
2858
+ } catch {
2859
+ fail2("DR_PUBLICATION_INVALID", "published DR output could not be verified", {
2860
+ backupId,
2861
+ output: target,
2862
+ published: true,
2863
+ durable: true,
2864
+ verified: false,
2865
+ recoveryPaths: recoveryPaths([bundleStage, stage])
2866
+ });
2867
+ }
2868
+ if (!publishedMetadata) throw new NativeDrCommandError("DR_PUBLICATION_INVALID", "published DR output could not be verified");
2869
+ if (publishedMetadata.bundleSha256 !== beforePublication.bundleSha256) {
2870
+ fail2("DR_PUBLICATION_INVALID", "published DR output does not match its verified staging bundle", {
2871
+ backupId,
2872
+ output: target,
2873
+ published: true,
2874
+ durable: true,
2875
+ verified: false,
2876
+ recoveryPaths: recoveryPaths([bundleStage, stage])
2877
+ });
2878
+ }
2879
+ const bundleRemoved = removeOwnedFile(bundleStage, bundleIdentity);
2880
+ const stageRemoved = removeOwnedDirectory(stage, stageIdentity);
2881
+ if (!bundleRemoved || !stageRemoved) {
2882
+ fail2("DR_RECOVERY_ARTIFACT_RETAINED", "DR output is valid but a recovery artifact remains", {
2883
+ backupId,
2884
+ output: target,
2885
+ published: true,
2886
+ durable: true,
2887
+ verified: true,
2888
+ recoveryPaths: recoveryPaths([bundleStage, stage])
2889
+ });
2890
+ }
2891
+ try {
2892
+ if (!operations.syncDirectory(directory) && platform !== "win32") {
2893
+ fail2("DR_CLEANUP_DURABILITY_UNKNOWN", "DR output is valid but staging cleanup durability is unproved", {
2894
+ backupId,
2895
+ output: target,
2896
+ published: true,
2897
+ durable: true,
2898
+ verified: true,
2899
+ cleanupDurability: "unproved",
2900
+ platform
2901
+ });
2902
+ }
2903
+ } catch (error) {
2904
+ if (error instanceof NativeDrCommandError) throw error;
2905
+ fail2("DR_CLEANUP_DURABILITY_UNKNOWN", "DR output is valid but staging cleanup durability is unknown", {
2906
+ backupId,
2907
+ output: target,
2908
+ published: true,
2909
+ durable: true,
2910
+ verified: true
2911
+ });
2912
+ }
2913
+ return { metadata: publishedMetadata, directoryDurability };
2914
+ } catch (error) {
2915
+ if (descriptor !== void 0) {
2916
+ closeSync4(descriptor);
2917
+ descriptor = void 0;
2918
+ }
2919
+ if (!published) {
2920
+ removeOwnedFile(bundleStage, bundleIdentity);
2921
+ removeOwnedDirectory(stage, stageIdentity);
2922
+ try {
2923
+ if (!operations.syncDirectory(directory)) {
2924
+ if (platform === "win32") {
2925
+ const primary = commandErrorWithRecovery(error, { backupId }, [bundleStage, stage]);
2926
+ throw new NativeDrCommandError(primary.code, primary.message, {
2927
+ ...primary.details,
2928
+ cleanupDirectoryDurability: "unavailable",
2929
+ platform
2930
+ });
2931
+ }
2932
+ throw new NativeDrCommandError("DR_CLEANUP_DURABILITY_UNKNOWN", "DR backup failure cleanup durability is unproved", {
2933
+ backupId,
2934
+ cleanupDurability: "unproved",
2935
+ platform,
2936
+ recoveryPaths: recoveryPaths([bundleStage, stage])
2937
+ });
2938
+ }
2939
+ } catch (cleanupError) {
2940
+ if (cleanupError instanceof NativeDrCommandError) throw cleanupError;
2941
+ throw new NativeDrCommandError("DR_CLEANUP_DURABILITY_UNKNOWN", "DR backup failure cleanup durability is unknown", {
2942
+ backupId,
2943
+ cleanupDurability: "unknown",
2944
+ platform,
2945
+ recoveryPaths: recoveryPaths([bundleStage, stage])
2946
+ });
2947
+ }
2948
+ throw commandErrorWithRecovery(error, { backupId }, [bundleStage, stage]);
2949
+ }
2950
+ if (error instanceof NativeDrCommandError) throw error;
2951
+ throw new NativeDrCommandError("DR_PUBLICATION_AMBIGUOUS", "DR publication outcome is incomplete", {
2952
+ backupId,
2953
+ output: target,
2954
+ published: true,
2955
+ durable: durable || "unknown",
2956
+ recoveryPaths: recoveryPaths([bundleStage, stage])
2957
+ });
2958
+ } finally {
2959
+ if (descriptor !== void 0) closeSync4(descriptor);
2960
+ }
144
2961
  }
145
- function installClient(runtime, identity, options = {}, execute = (command, args) => spawnSync(command, args, { encoding: "utf8" })) {
146
- const normalizedIdentity = identity.trim();
147
- if (!normalizedIdentity || normalizedIdentity.length > 128) throw new Error("--identity is required");
148
- const instance = options.instance?.trim() || `${runtime}-${randomUUID()}`;
149
- if (instance.length > 128) throw new Error("--instance exceeds 128 characters");
150
- if (options.scope && !["local", "user", "project"].includes(options.scope)) {
151
- throw new Error("scope must be local, user, or project");
2962
+ async function restorePostgresBundle(bundlePath, requestId2, toolDirectory, dependencies) {
2963
+ const bundle = resolve4(bundlePath);
2964
+ const directory = dirname4(bundle);
2965
+ const stage = join4(directory, `.${requestId2}.agent-bridge-dr.postgres-restore.stage`);
2966
+ if (existsSync4(stage)) fail2("DR_RECOVERY_REQUIRED", "a prior PostgreSQL DR restore with this ID requires recovery", {
2967
+ requestId: requestId2,
2968
+ bundle,
2969
+ recoveryPaths: [stage]
2970
+ });
2971
+ let stageIdentity;
2972
+ let targetRestored = false;
2973
+ const platform = dependencies.platform ?? process.platform;
2974
+ const operations = fileOperations(dependencies);
2975
+ let extracted;
2976
+ try {
2977
+ stageIdentity = createPrivateDirectory(stage);
2978
+ operations.syncDirectory(directory);
2979
+ extracted = extractPostgresBundle(bundle, stage);
2980
+ operations.syncDirectory(stage);
2981
+ const result = await (dependencies.restorePostgres ?? restorePostgresNativeDr)({
2982
+ dumpPath: extracted.dumpPath,
2983
+ rolesPath: extracted.rolesPath,
2984
+ schema: extracted.metadata.manifest.schema,
2985
+ artifactAnchors: extracted.artifactAnchors,
2986
+ acceptSourceSqlRisk: true,
2987
+ environment: dependencies.environment ?? process.env,
2988
+ ...toolDirectory ? { toolDirectory } : {}
2989
+ });
2990
+ targetRestored = true;
2991
+ const metadata = extracted.metadata;
2992
+ closeExtractedPostgresBundle(extracted);
2993
+ extracted = void 0;
2994
+ if (!removeOwnedDirectory(stage, stageIdentity)) {
2995
+ fail2("DR_RECOVERY_ARTIFACT_RETAINED", "PostgreSQL restore completed but a recovery artifact remains", {
2996
+ requestId: requestId2,
2997
+ recoveryPaths: recoveryPaths([stage])
2998
+ });
2999
+ }
3000
+ try {
3001
+ if (!operations.syncDirectory(directory) && platform !== "win32") {
3002
+ fail2("DR_CLEANUP_DURABILITY_UNKNOWN", "PostgreSQL restore completed but cleanup durability is unproved", {
3003
+ requestId: requestId2,
3004
+ targetRestored: true,
3005
+ cleanupDurability: "unproved",
3006
+ platform
3007
+ });
3008
+ }
3009
+ } catch (error) {
3010
+ if (error instanceof NativeDrCommandError) throw error;
3011
+ fail2("DR_CLEANUP_DURABILITY_UNKNOWN", "PostgreSQL restore completed but cleanup durability is unknown", {
3012
+ requestId: requestId2,
3013
+ targetRestored: true,
3014
+ cleanupDurability: "unknown",
3015
+ platform
3016
+ });
3017
+ }
3018
+ return { metadata, result, cleanupDirectoryDurability: platform === "win32" ? "unavailable" : "confirmed" };
3019
+ } catch (error) {
3020
+ if (extracted) {
3021
+ closeExtractedPostgresBundle(extracted);
3022
+ extracted = void 0;
3023
+ }
3024
+ const targetMutated = error instanceof PostgresNativeDrError && (error.details.targetMutated === true || error.details.restoreCompleted === true);
3025
+ if (targetRestored || targetMutated) throw commandErrorWithRecovery(error, { requestId: requestId2 }, [stage]);
3026
+ removeOwnedDirectory(stage, stageIdentity);
3027
+ try {
3028
+ if (!operations.syncDirectory(directory)) {
3029
+ if (platform === "win32") {
3030
+ const primary = commandErrorWithRecovery(error, { requestId: requestId2 }, [stage]);
3031
+ throw new NativeDrCommandError(primary.code, primary.message, {
3032
+ ...primary.details,
3033
+ cleanupDirectoryDurability: "unavailable",
3034
+ platform
3035
+ });
3036
+ }
3037
+ throw new NativeDrCommandError("DR_CLEANUP_DURABILITY_UNKNOWN", "PostgreSQL restore failure cleanup durability is unproved", {
3038
+ requestId: requestId2,
3039
+ cleanupDurability: "unproved",
3040
+ platform,
3041
+ recoveryPaths: recoveryPaths([stage])
3042
+ });
3043
+ }
3044
+ } catch (cleanupError) {
3045
+ if (cleanupError instanceof NativeDrCommandError) throw cleanupError;
3046
+ throw new NativeDrCommandError("DR_CLEANUP_DURABILITY_UNKNOWN", "PostgreSQL restore failure cleanup durability is unknown", {
3047
+ requestId: requestId2,
3048
+ cleanupDurability: "unknown",
3049
+ platform,
3050
+ recoveryPaths: recoveryPaths([stage])
3051
+ });
3052
+ }
3053
+ throw commandErrorWithRecovery(error, { requestId: requestId2 }, [stage]);
152
3054
  }
153
- const command = options.command?.trim() || "agent-bridge-mcp";
154
- const env = options.env ?? process.env;
155
- const expectedPath = clientBackendConfigPath(runtime, instance, env);
156
- const previous = existsSync(expectedPath) ? readFileSync(expectedPath) : void 0;
3055
+ }
3056
+ async function verifyPostgresBundle(bundlePath, toolDirectory, dependencies) {
3057
+ const bundle = resolve4(bundlePath);
3058
+ const directory = dirname4(bundle);
3059
+ const bundlePathId = createHash3("sha256").update(bundle).digest("hex").slice(0, 32);
3060
+ const stage = join4(directory, `.${bundlePathId}.agent-bridge-dr.postgres-verify.stage`);
3061
+ if (existsSync4(stage)) fail2("DR_RECOVERY_REQUIRED", "a prior PostgreSQL DR verification requires recovery", {
3062
+ bundle,
3063
+ recoveryPaths: [stage]
3064
+ });
3065
+ const platform = dependencies.platform ?? process.platform;
3066
+ const operations = fileOperations(dependencies);
3067
+ let stageIdentity;
3068
+ let extracted;
3069
+ let verificationCompleted = false;
157
3070
  try {
158
- const backendConfigPath = writeBackendConfig(runtime, normalizedIdentity, instance, {
159
- token: options.token,
160
- env
3071
+ stageIdentity = createPrivateDirectory(stage);
3072
+ extracted = extractPostgresBundle(bundle, stage);
3073
+ await (dependencies.verifyPostgres ?? verifyPostgresNativeDrArtifacts)({
3074
+ dumpPath: extracted.dumpPath,
3075
+ rolesPath: extracted.rolesPath,
3076
+ schema: extracted.metadata.manifest.schema,
3077
+ artifactAnchors: extracted.artifactAnchors,
3078
+ ...toolDirectory ? { toolDirectory } : {}
161
3079
  });
162
- if (runtime === "claude-desktop") {
163
- const path = desktopConfigPath(env);
164
- writeJsonConfig(path, normalizedIdentity, instance, backendConfigPath, command);
165
- return { runtime, identity: normalizedIdentity, instance, method: "json-config", configPath: path, backendConfigPath, restartRequired: true };
3080
+ verificationCompleted = true;
3081
+ const metadata = extracted.metadata;
3082
+ closeExtractedPostgresBundle(extracted);
3083
+ extracted = void 0;
3084
+ if (!removeOwnedDirectory(stage, stageIdentity)) {
3085
+ fail2("DR_RECOVERY_ARTIFACT_RETAINED", "PostgreSQL verification completed but a recovery artifact remains", {
3086
+ backupId: metadata.manifest.backupId,
3087
+ recoveryPaths: recoveryPaths([stage])
3088
+ });
166
3089
  }
167
- const executable = runtime === "codex" ? "codex" : "claude";
168
- const args = runtime === "codex" ? [
169
- "mcp",
170
- "add",
171
- "agent-bridge",
172
- "--env",
173
- `AGENT_BRIDGE_AGENT=${normalizedIdentity}`,
174
- "--env",
175
- `AGENT_BRIDGE_INSTANCE=${instance}`,
176
- "--env",
177
- `AGENT_BRIDGE_CONFIG=${backendConfigPath}`,
178
- "--",
179
- command
180
- ] : [
181
- "mcp",
182
- "add",
183
- "--scope",
184
- options.scope ?? "user",
185
- "agent-bridge",
186
- "-e",
187
- `AGENT_BRIDGE_AGENT=${normalizedIdentity}`,
188
- "-e",
189
- `AGENT_BRIDGE_INSTANCE=${instance}`,
190
- "-e",
191
- `AGENT_BRIDGE_CONFIG=${backendConfigPath}`,
192
- "--",
193
- command
194
- ];
195
- const result = execute(executable, args);
196
- if (result.error || result.status !== 0) {
197
- const detail = result.stderr?.trim() || result.error?.message || `exit ${result.status}`;
198
- throw new Error(`${runtime} MCP installation failed: ${detail}`);
3090
+ const durable = operations.syncDirectory(directory);
3091
+ if (!durable && platform !== "win32") {
3092
+ fail2("DR_CLEANUP_DURABILITY_UNKNOWN", "PostgreSQL verification cleanup durability is unproved", {
3093
+ backupId: metadata.manifest.backupId,
3094
+ cleanupDurability: "unproved",
3095
+ platform
3096
+ });
199
3097
  }
200
- return { runtime, identity: normalizedIdentity, instance, method: "native-cli", backendConfigPath, restartRequired: true };
3098
+ return { metadata, cleanupDirectoryDurability: durable ? "confirmed" : "unavailable" };
201
3099
  } catch (error) {
3100
+ if (extracted) closeExtractedPostgresBundle(extracted);
3101
+ if (verificationCompleted) throw commandErrorWithRecovery(error, {}, [stage]);
3102
+ removeOwnedDirectory(stage, stageIdentity);
202
3103
  try {
203
- if (previous) replacePrivateFile(expectedPath, previous);
204
- else rmSync(expectedPath, { force: true });
205
- } catch (rollbackError) {
206
- const detail = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
207
- throw new Error(`${error instanceof Error ? error.message : String(error)}; client config rollback failed: ${detail}`);
3104
+ operations.syncDirectory(directory);
3105
+ } catch {
208
3106
  }
209
- throw error;
3107
+ throw commandErrorWithRecovery(error, {}, [stage]);
210
3108
  }
211
3109
  }
3110
+ var success2 = (operation, fields) => ({
3111
+ schemaVersion: 1,
3112
+ status: "ok",
3113
+ operation,
3114
+ ...fields
3115
+ });
3116
+ async function runDrCommand(argv, dependencies = {}) {
3117
+ const { operation, options } = parse2(argv);
3118
+ try {
3119
+ const selected = provider2(options);
3120
+ if (selected === "local") {
3121
+ rejectProviderOption(options, "tool-directory", selected, operation);
3122
+ rejectProviderOption(options, "accept-source-sql-risk", selected, operation);
3123
+ if (operation === "backup") {
3124
+ const source = required3(options, "source");
3125
+ const output2 = required3(options, "output");
3126
+ const backupId = optional(options, "backup-id") ? validateNativeDrId(required3(options, "backup-id")) : randomUUID5();
3127
+ const metadata3 = await backupLocalSqlite(source, output2, backupId, { timeoutMs: timeout(options) });
3128
+ return success2(operation, {
3129
+ provider: selected,
3130
+ backupId: metadata3.manifest.backupId,
3131
+ createdAt: metadata3.manifest.createdAt,
3132
+ output: resolve4(output2),
3133
+ bundleBytes: metadata3.bundleBytes,
3134
+ bundleSha256: metadata3.bundleSha256
3135
+ });
3136
+ }
3137
+ const bundle2 = required3(options, "bundle");
3138
+ if (operation === "verify") {
3139
+ const metadata3 = verifyProviderBundle(bundle2, selected);
3140
+ return success2(operation, {
3141
+ provider: selected,
3142
+ backupId: metadata3.manifest.backupId,
3143
+ createdAt: metadata3.manifest.createdAt,
3144
+ bundle: resolve4(bundle2),
3145
+ bundleBytes: metadata3.bundleBytes,
3146
+ bundleSha256: metadata3.bundleSha256
3147
+ });
3148
+ }
3149
+ const target = required3(options, "target");
3150
+ const requestId3 = optional(options, "request-id") ? validateNativeDrId(required3(options, "request-id")) : randomUUID5();
3151
+ const metadata2 = await restoreLocalSqlite(bundle2, target, requestId3, { timeoutMs: timeout(options) });
3152
+ return success2(operation, {
3153
+ provider: selected,
3154
+ requestId: metadata2.requestId,
3155
+ backupId: metadata2.manifest.backupId,
3156
+ target: resolve4(target),
3157
+ bundleSha256: metadata2.bundleSha256
3158
+ });
3159
+ }
3160
+ rejectProviderOption(options, "source", selected, operation);
3161
+ rejectProviderOption(options, "target", selected, operation);
3162
+ rejectProviderOption(options, "timeout-ms", selected, operation);
3163
+ const toolDirectory = optional(options, "tool-directory");
3164
+ if (operation === "backup") {
3165
+ const output2 = required3(options, "output");
3166
+ const backupId = validateNativeDrId(required3(options, "backup-id"));
3167
+ const { metadata: metadata2, directoryDurability } = await backupPostgresBundle(output2, backupId, toolDirectory, dependencies);
3168
+ return success2(operation, {
3169
+ provider: selected,
3170
+ backupId: metadata2.manifest.backupId,
3171
+ createdAt: metadata2.manifest.createdAt,
3172
+ output: resolve4(output2),
3173
+ bundleBytes: metadata2.bundleBytes,
3174
+ bundleSha256: metadata2.bundleSha256,
3175
+ directoryDurability,
3176
+ ...directoryDurability === "unavailable" ? { platform: "win32" } : {}
3177
+ });
3178
+ }
3179
+ const bundle = required3(options, "bundle");
3180
+ if (operation === "verify") {
3181
+ const { metadata: metadata2, cleanupDirectoryDurability: cleanupDirectoryDurability2 } = await verifyPostgresBundle(bundle, toolDirectory, dependencies);
3182
+ return success2(operation, {
3183
+ provider: selected,
3184
+ backupId: metadata2.manifest.backupId,
3185
+ createdAt: metadata2.manifest.createdAt,
3186
+ bundle: resolve4(bundle),
3187
+ bundleBytes: metadata2.bundleBytes,
3188
+ bundleSha256: metadata2.bundleSha256,
3189
+ cleanupDirectoryDurability: cleanupDirectoryDurability2,
3190
+ ...cleanupDirectoryDurability2 === "unavailable" ? { platform: "win32" } : {}
3191
+ });
3192
+ }
3193
+ if (options["accept-source-sql-risk"] !== true) {
3194
+ fail2("SOURCE_SQL_RISK_NOT_ACCEPTED", "PostgreSQL native restore requires --accept-source-sql-risk");
3195
+ }
3196
+ const requestId2 = validateNativeDrId(required3(options, "request-id"));
3197
+ const { metadata, result, cleanupDirectoryDurability } = await restorePostgresBundle(bundle, requestId2, toolDirectory, dependencies);
3198
+ return success2(operation, {
3199
+ provider: selected,
3200
+ requestId: requestId2,
3201
+ backupId: metadata.manifest.backupId,
3202
+ bundleSha256: metadata.bundleSha256,
3203
+ databaseName: result.databaseName,
3204
+ normalizedClaimedDeliveries: result.normalizedClaimedDeliveries,
3205
+ tableCounts: result.tableCounts,
3206
+ readiness: result.readiness,
3207
+ cleanupDirectoryDurability,
3208
+ ...cleanupDirectoryDurability === "unavailable" ? { platform: "win32" } : {}
3209
+ });
3210
+ } catch (error) {
3211
+ if (error instanceof NativeDrCommandError) throw error;
3212
+ if (error instanceof PostgresNativeDrError) throw new NativeDrCommandError(error.code, error.message, error.details);
3213
+ if (error instanceof PrivatePathError) fail2("INSECURE_PATH", "DR path does not satisfy the private path policy");
3214
+ if (error instanceof NativeDrBundleError) fail2("INVALID_DR_BUNDLE", "DR bundle verification failed");
3215
+ fail2("DR_COMMAND_FAILED", "DR command failed");
3216
+ }
3217
+ throw new NativeDrCommandError("DR_COMMAND_FAILED", "DR command failed");
3218
+ }
212
3219
 
213
3220
  // src/cli.ts
214
3221
  var SUPPORTED_OPTIONS = /* @__PURE__ */ new Set([
@@ -226,6 +3233,10 @@ var SUPPORTED_OPTIONS = /* @__PURE__ */ new Set([
226
3233
  "db",
227
3234
  "dead",
228
3235
  "delivery-id",
3236
+ "delivery-max-attempts",
3237
+ "delivery-mode",
3238
+ "delivery-policy",
3239
+ "disposition",
229
3240
  "error",
230
3241
  "expires-at",
231
3242
  "force",
@@ -244,6 +3255,7 @@ var SUPPORTED_OPTIONS = /* @__PURE__ */ new Set([
244
3255
  "max-interval-ms",
245
3256
  "message-id",
246
3257
  "metadata",
3258
+ "not-before",
247
3259
  "payload",
248
3260
  "payload-ciphertext",
249
3261
  "payload-mime",
@@ -264,16 +3276,40 @@ var SUPPORTED_OPTIONS = /* @__PURE__ */ new Set([
264
3276
  "unacked-by",
265
3277
  "url",
266
3278
  "workspace",
3279
+ "mailbox",
3280
+ "receipt-state",
267
3281
  "identity",
268
3282
  "command",
269
3283
  "scope",
3284
+ "recipient",
3285
+ "retry-base-ms",
3286
+ "retry-jitter",
3287
+ "retry-max-ms",
3288
+ "role",
270
3289
  "runtime",
271
3290
  "capability",
3291
+ "state",
272
3292
  "max-pages",
273
- "max-push"
3293
+ "max-push",
3294
+ "latest",
3295
+ "apply",
3296
+ "display-name",
3297
+ "enrollment-file",
3298
+ "credential-id",
3299
+ "gateway-url",
3300
+ "grace-until",
3301
+ "invalidate-immediately",
3302
+ "label",
3303
+ "reason",
3304
+ "request-id",
3305
+ "resume",
3306
+ "runtime-type",
3307
+ "recover-lock",
3308
+ "scope-set",
3309
+ "workspace-name"
274
3310
  ]);
275
- var BOOLEAN_OPTIONS = /* @__PURE__ */ new Set(["dead", "force", "json"]);
276
- function parse(argv) {
3311
+ var BOOLEAN_OPTIONS2 = /* @__PURE__ */ new Set(["apply", "dead", "force", "invalidate-immediately", "json", "latest", "recover-lock"]);
3312
+ function parse3(argv) {
277
3313
  const command = argv[0] ?? "help";
278
3314
  const options = {};
279
3315
  const positionals = [];
@@ -286,7 +3322,7 @@ function parse(argv) {
286
3322
  const key = value.slice(2);
287
3323
  const next = argv[i + 1];
288
3324
  if (!next || next.startsWith("--")) {
289
- if (!BOOLEAN_OPTIONS.has(key)) throw new Error(`--${key} requires a value`);
3325
+ if (!BOOLEAN_OPTIONS2.has(key)) throw new Error(`--${key} requires a value`);
290
3326
  options[key] = true;
291
3327
  } else {
292
3328
  i++;
@@ -330,57 +3366,140 @@ function json(options, key) {
330
3366
  function since(options) {
331
3367
  const raw = one(options, "since");
332
3368
  if (!raw) return void 0;
333
- const relative = raw.match(/^(\d+)(h|d)$/);
334
- if (!relative) return raw;
335
- const amount = Number(relative[1]);
336
- const milliseconds = amount * (relative[2] === "h" ? 36e5 : 864e5);
3369
+ const relative2 = raw.match(/^(\d+)(h|d)$/);
3370
+ if (!relative2) return raw;
3371
+ const amount = Number(relative2[1]);
3372
+ const milliseconds = amount * (relative2[2] === "h" ? 36e5 : 864e5);
337
3373
  return new Date(Date.now() - milliseconds).toISOString();
338
3374
  }
339
3375
  function output(value) {
340
3376
  process.stdout.write(`${JSON.stringify(value, null, 2)}
341
3377
  `);
342
3378
  }
3379
+ function cliOutput(operationId, value, invocation) {
3380
+ output(parseCliResponse(operationId, value, invocation));
3381
+ }
3382
+ function failedClientStatus(config, error, failure) {
3383
+ const schemaVersion = config.provider === "legacy-supabase" ? "legacy-v1" : config.provider === "gateway" ? "postgres-v2" : "local-v2";
3384
+ const remote = config.provider === "local" || !failure.remoteAttempted ? null : false;
3385
+ const localHealthy = failure.component !== "local-store";
3386
+ const code = error && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : "diagnostics_failed";
3387
+ cliOutput("client_status", {
3388
+ status: "failed",
3389
+ localHealthy,
3390
+ connected: false,
3391
+ remoteReachable: remote,
3392
+ provider: config.provider,
3393
+ workspace: config.principal.workspace,
3394
+ agent: config.principal.agent,
3395
+ instance: config.principal.instance ?? null,
3396
+ schemaVersion,
3397
+ endpoint: config.url ?? null,
3398
+ database: config.provider === "local" ? config.databasePath : null,
3399
+ cursorPath: config.cursorPath,
3400
+ lastCursor: cursor(config.cursorPath) ?? null,
3401
+ queue: { schemaVersion, deliverySupported: false, pending: null, claimed: null, retrying: null, dead: null },
3402
+ checks: [{ name: failure.component, status: "failed", message: `${failure.component} diagnostics failed (${code})` }]
3403
+ });
3404
+ }
343
3405
  function cursor(path) {
344
3406
  try {
345
- return readFileSync2(path, "utf8").trim() || void 0;
3407
+ return readFileSync3(path, "utf8").trim() || void 0;
346
3408
  } catch {
347
3409
  return void 0;
348
3410
  }
349
3411
  }
3412
+ function preparePrivateOutput(path) {
3413
+ const directory = dirname5(path);
3414
+ const existed = existsSync5(directory);
3415
+ mkdirSync4(directory, { recursive: true, mode: 448 });
3416
+ if (!existed || basename(directory) === ".agent-bridge") securePrivatePath(directory, "directory");
3417
+ else verifyPrivatePathAccess(directory, "directory");
3418
+ if (existsSync5(path)) verifyPrivatePathAccess(path, "file");
3419
+ }
350
3420
  function saveCursor(path, value) {
351
3421
  if (!value) return;
352
- mkdirSync2(dirname2(path), { recursive: true });
3422
+ preparePrivateOutput(path);
353
3423
  const temporary = `${path}.${process.pid}.tmp`;
354
- writeFileSync2(temporary, `${value}
3424
+ try {
3425
+ writeFileSync3(temporary, `${value}
355
3426
  `, { mode: 384 });
356
- renameSync2(temporary, path);
3427
+ securePrivatePath(temporary, "file");
3428
+ renameSync4(temporary, path);
3429
+ verifyPrivatePathAccess(path, "file");
3430
+ } catch (error) {
3431
+ rmSync4(temporary, { force: true });
3432
+ throw error;
3433
+ }
3434
+ }
3435
+ function watchCursorPath(path, project) {
3436
+ if (!project) return path;
3437
+ const scope = createHash4("sha256").update(project).digest("hex").slice(0, 16);
3438
+ return `${path}.project-${scope}`;
3439
+ }
3440
+ function packageVersion() {
3441
+ const manifest = JSON.parse(readFileSync3(fileURLToPath2(new URL("../package.json", import.meta.url)), "utf8"));
3442
+ if (typeof manifest.version !== "string" || !manifest.version) throw new Error("package version is unavailable");
3443
+ return manifest.version;
357
3444
  }
358
3445
  function help() {
359
3446
  process.stdout.write(`agent-bridge: provider-neutral agent messaging
360
3447
 
361
3448
  Commands:
362
- init, doctor, status, pending, migrate, sync, demo, join, presence
363
- send (post), inbox (get), history, acknowledge, claim, extend, ack, nack, watch
3449
+ init, doctor, status, capabilities, pending, migrate, reconcile-legacy-projects, sync, demo, join, presence
3450
+ send (post), inbox (get), sent, history, acknowledge, claim, extend, ack, nack, watch
3451
+ deliveries, dead-letters, delivery-events, cancel, requeue
3452
+ owner <provision|inventory|rotate|revoke>
3453
+ archive <export|verify|import>
3454
+ dr <backup|verify|restore>
364
3455
  clients install <codex|claude-code|claude-desktop> --identity <name>
3456
+
3457
+ Options:
3458
+ -V, --version Print the installed package version
3459
+ -h, --help Show this help
365
3460
  `);
366
3461
  }
367
3462
  function rejectUnknownOptions(options) {
368
3463
  const unknown = Object.keys(options).filter((key) => !SUPPORTED_OPTIONS.has(key));
369
3464
  if (unknown.length) throw new Error(`unknown option: --${unknown[0]}`);
370
3465
  }
3466
+ function rejectOptionsOutside(options, allowed, command) {
3467
+ const invalid = Object.keys(options).find((key) => !allowed.has(key));
3468
+ if (invalid) throw new Error(`--${invalid} is not valid for ${command}`);
3469
+ }
3470
+ var CLI_CONTEXT_OPTIONS = /* @__PURE__ */ new Set([
3471
+ "agent",
3472
+ "as",
3473
+ "config",
3474
+ "db",
3475
+ "instance",
3476
+ "json",
3477
+ "key",
3478
+ "provider",
3479
+ "token",
3480
+ "url",
3481
+ "workspace"
3482
+ ]);
3483
+ function validateCanonicalCommandOptions(command, options, config) {
3484
+ const contract = operationForCli(command, config.provider, Object.keys(options));
3485
+ if (!contract?.cli) return;
3486
+ const allowed = /* @__PURE__ */ new Set([...CLI_CONTEXT_OPTIONS, ...contract.cli.options]);
3487
+ const invalid = Object.keys(options).find((key) => !allowed.has(key));
3488
+ if (invalid) throw new Error(`--${invalid} is not valid for ${command}`);
3489
+ }
371
3490
  async function initialize(options) {
372
- const home = process.env.HOME ?? homedir2();
373
- const configPath = one(options, "config") ?? process.env.AGENT_BRIDGE_CONFIG ?? join2(home, ".agent-bridge", "config");
374
- if (existsSync2(configPath) && !boolean(options, "force")) {
3491
+ const home2 = process.env.HOME ?? homedir4();
3492
+ const configPath = one(options, "config") ?? process.env.AGENT_BRIDGE_CONFIG ?? join5(home2, ".agent-bridge", "config");
3493
+ if (existsSync5(configPath) && !boolean(options, "force")) {
375
3494
  throw new Error(`Config already exists at ${configPath}; pass --force to replace it`);
376
3495
  }
377
- const provider = one(options, "provider") ?? process.env.AGENT_BRIDGE_PROVIDER ?? "local";
3496
+ const provider3 = one(options, "provider") ?? process.env.AGENT_BRIDGE_PROVIDER ?? "local";
378
3497
  if (one(options, "instance")) {
379
3498
  throw new Error("--instance is client-scoped; set it in the active client process");
380
3499
  }
381
3500
  const values = {
382
- AGENT_BRIDGE_PROVIDER: provider,
383
- AGENT_BRIDGE_WORKSPACE: one(options, "workspace") ?? process.env.AGENT_BRIDGE_WORKSPACE ?? (provider === "legacy-supabase" || provider === "legacy" || provider === "supabase" ? "*" : "default"),
3501
+ AGENT_BRIDGE_PROVIDER: provider3,
3502
+ AGENT_BRIDGE_WORKSPACE: one(options, "workspace") ?? process.env.AGENT_BRIDGE_WORKSPACE ?? (provider3 === "legacy-supabase" || provider3 === "legacy" || provider3 === "supabase" ? "*" : "default"),
384
3503
  AGENT_BRIDGE_DB: one(options, "db") ?? process.env.AGENT_BRIDGE_DB,
385
3504
  AGENT_BRIDGE_EDGE_DB: process.env.AGENT_BRIDGE_EDGE_DB,
386
3505
  AGENT_BRIDGE_URL: one(options, "url") ?? process.env.AGENT_BRIDGE_URL,
@@ -390,49 +3509,65 @@ async function initialize(options) {
390
3509
  for (const value of Object.values(values)) {
391
3510
  if (value?.includes("\n") || value?.includes("\r")) throw new Error("config values cannot contain newlines");
392
3511
  }
393
- mkdirSync2(dirname2(configPath), { recursive: true });
3512
+ preparePrivateOutput(configPath);
394
3513
  const temporary = `${configPath}.${process.pid}.tmp`;
395
- writeFileSync2(
396
- temporary,
397
- Object.entries(values).filter((entry) => Boolean(entry[1])).map(([key, value]) => `${key}=${value}`).join("\n") + "\n",
398
- { mode: 384 }
399
- );
400
3514
  let config;
401
3515
  try {
3516
+ writeFileSync3(
3517
+ temporary,
3518
+ Object.entries(values).filter((entry) => Boolean(entry[1])).map(([key, value]) => `${key}=${value}`).join("\n") + "\n",
3519
+ { mode: 384 }
3520
+ );
3521
+ securePrivatePath(temporary, "file");
402
3522
  config = resolveClientConfig(
403
3523
  {
404
- HOME: home,
3524
+ HOME: home2,
405
3525
  AGENT_BRIDGE_CONFIG: temporary,
406
3526
  AGENT_BRIDGE_TOKEN: verificationToken
407
3527
  },
408
3528
  "agent-bridge-init"
409
3529
  );
410
- const runtime = await createClientRuntime(config);
3530
+ const runtime2 = await createClientRuntime(config);
411
3531
  try {
412
- if (config.provider === "gateway") await runtime.store.verifyRemote?.();
3532
+ if (config.provider === "gateway") await runtime2.store.verifyRemote?.();
413
3533
  } finally {
414
- await runtime.close();
3534
+ await runtime2.close();
415
3535
  }
416
- renameSync2(temporary, configPath);
3536
+ renameSync4(temporary, configPath);
3537
+ verifyPrivatePathAccess(configPath, "file");
417
3538
  } catch (error) {
418
- rmSync2(temporary, { force: true });
3539
+ rmSync4(temporary, { force: true });
419
3540
  throw error;
420
3541
  }
421
3542
  output({ status: "ok", config: configPath, provider: config.provider, workspace: config.principal.workspace, identity: "process-scoped", database: config.provider === "local" ? config.databasePath : null });
422
3543
  }
423
3544
  function configFor(options, command) {
424
3545
  const explicit = command === "send" ? one(options, "source") ?? one(options, "agent") : command === "acknowledge" || command === "ack" && Boolean(one(options, "ids")) ? one(options, "agent") : one(options, "as");
425
- const project = one(options, "project");
426
- const environment = one(options, "instance") ? { ...process.env, AGENT_BRIDGE_INSTANCE: one(options, "instance") } : process.env;
3546
+ const environment = {
3547
+ ...process.env,
3548
+ ...one(options, "config") ? { AGENT_BRIDGE_CONFIG: one(options, "config") } : {},
3549
+ ...one(options, "provider") ? { AGENT_BRIDGE_PROVIDER: one(options, "provider") } : {},
3550
+ ...one(options, "db") ? { AGENT_BRIDGE_DB: one(options, "db") } : {},
3551
+ ...one(options, "url") ? { AGENT_BRIDGE_URL: one(options, "url") } : {},
3552
+ ...one(options, "key") ? { AGENT_BRIDGE_KEY: one(options, "key") } : {},
3553
+ ...one(options, "token") ? { AGENT_BRIDGE_TOKEN: one(options, "token") } : {},
3554
+ ...one(options, "instance") ? { AGENT_BRIDGE_INSTANCE: one(options, "instance") } : {}
3555
+ };
427
3556
  const config = resolveClientConfig(environment, explicit);
428
- if (!project || project === config.principal.workspace) return config;
429
- if (config.provider === "gateway") {
430
- throw new Error("--project cannot override the workspace bound to a gateway credential");
3557
+ const assertedWorkspace = one(options, "workspace");
3558
+ if (config.provider === "gateway" && assertedWorkspace && assertedWorkspace !== config.principal.workspace) {
3559
+ throw new Error("--workspace must match the workspace bound to the gateway credential");
431
3560
  }
432
- return resolveClientConfig(
433
- { ...environment, AGENT_BRIDGE_WORKSPACE: project },
434
- explicit
435
- );
3561
+ if (config.provider === "legacy-supabase" && assertedWorkspace && assertedWorkspace !== "*") {
3562
+ throw new Error("--workspace is not supported by the global legacy Supabase schema");
3563
+ }
3564
+ if (config.provider === "local" && assertedWorkspace && assertedWorkspace !== config.principal.workspace) {
3565
+ return resolveClientConfig(
3566
+ { ...environment, AGENT_BRIDGE_WORKSPACE: assertedWorkspace },
3567
+ explicit
3568
+ );
3569
+ }
3570
+ return config;
436
3571
  }
437
3572
  function draft(options, positionals) {
438
3573
  const content = one(options, "content") ?? positionals.join(" ");
@@ -443,7 +3578,12 @@ function draft(options, positionals) {
443
3578
  if (one(options, "payload-ref")) envelope.payload_ref = one(options, "payload-ref");
444
3579
  if (one(options, "payload-ciphertext")) envelope.payload_ciphertext = one(options, "payload-ciphertext");
445
3580
  if (Object.keys(envelope).length) metadata.message_envelope = envelope;
3581
+ const deliveryMode = one(options, "delivery-mode");
3582
+ const deliveryTuning = ["delivery-max-attempts", "retry-base-ms", "retry-max-ms", "retry-jitter", "not-before"].some((key) => one(options, key) !== void 0);
3583
+ if (deliveryTuning && !deliveryMode) throw new Error("--delivery-mode is required with delivery policy flags");
3584
+ if (deliveryMode === "mailbox" && deliveryTuning) throw new Error("mailbox delivery mode does not accept retry or scheduling flags");
446
3585
  return {
3586
+ project: one(options, "project"),
447
3587
  type: one(options, "type") ?? one(options, "kind") ?? one(options, "category") ?? "operational",
448
3588
  content,
449
3589
  targets: [...list(options, "target"), ...list(options, "target-agent"), ...list(options, "target-agents")],
@@ -459,13 +3599,15 @@ function draft(options, positionals) {
459
3599
  atribReceiptId: one(options, "atrib-receipt-id"),
460
3600
  informedBy: list(options, "informed-by"),
461
3601
  data: json(options, "data") ?? json(options, "payload"),
462
- metadata
3602
+ metadata,
3603
+ deliveryPolicy: json(options, "delivery-policy") ?? (deliveryMode ? deliveryMode === "mailbox" ? { mode: "mailbox" } : { mode: deliveryMode, maxAttempts: integer(options, "delivery-max-attempts", 5), retryBaseDelayMs: integer(options, "retry-base-ms", 1e3), retryMaxDelayMs: integer(options, "retry-max-ms", 6e4), retryJitterRatio: Number(one(options, "retry-jitter") ?? 0.2), notBefore: one(options, "not-before") } : void 0)
463
3604
  };
464
3605
  }
465
3606
  async function localDemo() {
466
- const directory = mkdtempSync(join2(tmpdir(), "agent-bridge-demo-"));
467
- const databasePath = join2(directory, "demo.sqlite3");
468
- const base = { provider: "local", databasePath, edgeDatabasePath: join2(directory, "edge.sqlite3"), cursorPath: join2(directory, "cursor"), configPath: join2(directory, "config"), credential: void 0, url: void 0 };
3607
+ const directory = mkdtempSync(join5(tmpdir(), "agent-bridge-demo-"));
3608
+ const databasePath = join5(directory, "demo.sqlite3");
3609
+ securePrivatePath(directory, "directory");
3610
+ const base = { provider: "local", databasePath, edgeDatabasePath: join5(directory, "edge.sqlite3"), cursorPath: join5(directory, "cursor"), configPath: join5(directory, "config"), credential: void 0, url: void 0 };
469
3611
  let sender;
470
3612
  let worker;
471
3613
  try {
@@ -482,34 +3624,70 @@ async function localDemo() {
482
3624
  } finally {
483
3625
  await worker?.close();
484
3626
  await sender?.close();
485
- rmSync2(directory, { recursive: true, force: true });
3627
+ rmSync4(directory, { recursive: true, force: true });
486
3628
  }
487
3629
  }
488
3630
  async function runCli(argv = process.argv.slice(2)) {
489
- const { command: raw, options, positionals } = parse(argv);
3631
+ if (argv[0] === "archive") {
3632
+ output(await runArchiveCommand(argv.slice(1), process.env));
3633
+ return;
3634
+ }
3635
+ if (argv[0] === "dr") {
3636
+ output(await runDrCommand(argv.slice(1)));
3637
+ return;
3638
+ }
3639
+ const { command: raw, options, positionals } = parse3(argv);
490
3640
  const command = { post: "send", get: "inbox", receipt: "acknowledge" }[raw] ?? raw;
3641
+ if (["-V", "--version"].includes(command)) {
3642
+ process.stdout.write(`${packageVersion()}
3643
+ `);
3644
+ return;
3645
+ }
491
3646
  if (["help", "-h", "--help"].includes(command)) {
492
3647
  help();
493
3648
  return;
494
3649
  }
495
3650
  rejectUnknownOptions(options);
3651
+ if (command === "owner") {
3652
+ if (positionals.length !== 1) {
3653
+ throw new Error("usage: agent-bridge owner <provision|inventory|rotate|revoke>");
3654
+ }
3655
+ const ownerOptions = {};
3656
+ for (const [key, value] of Object.entries(options)) {
3657
+ if (Array.isArray(value)) throw new Error("--" + key + " may only be provided once");
3658
+ ownerOptions[key] = value;
3659
+ }
3660
+ output(await runOwnerCommand(positionals[0], ownerOptions, process.env));
3661
+ return;
3662
+ }
496
3663
  if (command === "clients") {
497
- if (positionals[0] !== "install" || !positionals[1]) {
3664
+ if (positionals.length !== 2 || positionals[0] !== "install" || !positionals[1]) {
498
3665
  throw new Error("usage: agent-bridge clients install <runtime> --identity <name>");
499
3666
  }
500
- const runtime2 = positionals[1];
501
- if (!["codex", "claude-code", "claude-desktop"].includes(runtime2)) {
502
- throw new Error(`unsupported install runtime: ${runtime2}`);
3667
+ rejectOptionsOutside(options, /* @__PURE__ */ new Set([
3668
+ "identity",
3669
+ "command",
3670
+ "scope",
3671
+ "instance",
3672
+ "token",
3673
+ "enrollment-file",
3674
+ "recover-lock"
3675
+ ]), "clients install");
3676
+ const runtime3 = positionals[1];
3677
+ if (runtime3 && !["codex", "claude-code", "claude-desktop"].includes(runtime3)) {
3678
+ throw new Error(`unsupported install runtime: ${runtime3}`);
503
3679
  }
504
3680
  const scope = one(options, "scope");
505
3681
  if (scope && !["local", "user", "project"].includes(scope)) {
506
3682
  throw new Error("--scope must be local, user, or project");
507
3683
  }
508
- output(installClient(runtime2, one(options, "identity") ?? "", {
3684
+ output(installClient(runtime3, one(options, "identity") ?? "", {
509
3685
  command: one(options, "command"),
510
3686
  scope,
511
3687
  instance: one(options, "instance"),
512
3688
  token: one(options, "token"),
3689
+ enrollmentFile: one(options, "enrollment-file"),
3690
+ recoverLock: boolean(options, "recover-lock"),
513
3691
  env: process.env
514
3692
  }));
515
3693
  return;
@@ -525,11 +3703,11 @@ async function runCli(argv = process.argv.slice(2)) {
525
3703
  if (command === "migrate") {
526
3704
  const databaseUrl = process.env.AGENT_BRIDGE_DATABASE_URL;
527
3705
  if (!databaseUrl) throw new Error("AGENT_BRIDGE_DATABASE_URL is required");
528
- const pool = new pg.Pool({ connectionString: databaseUrl, max: 1 });
3706
+ const pool = new pg3.Pool({ connectionString: databaseUrl, max: 1 });
529
3707
  try {
530
3708
  const applied = await runMigrations(
531
3709
  pool,
532
- fileURLToPath(new URL("../sql/migrations", import.meta.url))
3710
+ fileURLToPath2(new URL("../sql/migrations", import.meta.url))
533
3711
  );
534
3712
  output({ status: "ok", applied });
535
3713
  } finally {
@@ -537,19 +3715,99 @@ async function runCli(argv = process.argv.slice(2)) {
537
3715
  }
538
3716
  return;
539
3717
  }
3718
+ if (command === "reconcile-legacy-projects") {
3719
+ const databaseUrl = process.env.AGENT_BRIDGE_DATABASE_URL;
3720
+ if (!databaseUrl) throw new Error("AGENT_BRIDGE_DATABASE_URL is required");
3721
+ const pool = new pg3.Pool({ connectionString: databaseUrl, max: 1 });
3722
+ try {
3723
+ const directory = fileURLToPath2(new URL("../sql/migrations", import.meta.url));
3724
+ const migration = (await loadMigrationPlan(directory)).find((entry) => entry.version === 8);
3725
+ if (!migration || migration.name !== "message_projects") {
3726
+ throw new Error("migration 008_message_projects is missing from the migration plan");
3727
+ }
3728
+ output(await reconcileLegacyProjects(pool, {
3729
+ apply: boolean(options, "apply"),
3730
+ migrationChecksum: migration.checksum
3731
+ }));
3732
+ } finally {
3733
+ await pool.end();
3734
+ }
3735
+ return;
3736
+ }
540
3737
  const config = configFor(options, command);
541
- const runtime = await createClientRuntime(config);
3738
+ validateCanonicalCommandOptions(raw, options, config);
3739
+ const assertedUnackedBy = one(options, "unacked-by");
3740
+ if (assertedUnackedBy !== void 0 && assertedUnackedBy !== config.principal.agent) throw new Error("--unacked-by must equal the configured principal");
3741
+ if (command === "capabilities") {
3742
+ validateRequest("capabilities", {});
3743
+ cliOutput("capabilities", capabilityDocument({ surface: "cli", provider: config.provider }));
3744
+ return;
3745
+ }
3746
+ let runtime2;
3747
+ try {
3748
+ runtime2 = await createClientRuntime(config, {
3749
+ autoSync: command !== "doctor" && command !== "status",
3750
+ initializationMode: command === "status" ? "passive" : "active"
3751
+ });
3752
+ } catch (error) {
3753
+ if (command !== "doctor" && command !== "status") throw error;
3754
+ const remoteInitialization = command === "doctor" && config.provider === "legacy-supabase";
3755
+ failedClientStatus(config, error, {
3756
+ component: remoteInitialization ? "remote" : "local-store",
3757
+ remoteAttempted: remoteInitialization
3758
+ });
3759
+ process.exitCode = 1;
3760
+ return;
3761
+ }
542
3762
  try {
543
3763
  if (command === "doctor" || command === "status") {
544
- const diagnostics = await runtime.store.diagnostics?.(config.principal) ?? { schemaVersion: config.provider === "legacy-supabase" ? "legacy-v1" : "local-v2", deliverySupported: false, pending: null, claimed: null, retrying: null, dead: null };
545
- output({ status: "ok", connected: true, provider: config.provider, workspace: config.principal.workspace, agent: config.principal.agent, instance: config.principal.instance ?? null, schemaVersion: diagnostics.schemaVersion, endpoint: config.url ?? null, database: config.provider === "local" ? config.databasePath : null, cursorPath: config.cursorPath, lastCursor: cursor(config.cursorPath) ?? null, queue: diagnostics });
3764
+ validateRequest("client_status", {});
3765
+ const active = command === "doctor";
3766
+ try {
3767
+ const diagnostics = await runtime2.store.diagnostics?.(config.principal, { mode: active ? "probe" : "snapshot" }) ?? { schemaVersion: config.provider === "legacy-supabase" ? "legacy-v1" : "local-v2", deliverySupported: false, pending: null, claimed: null, retrying: null, dead: null };
3768
+ const remoteReachable = "remoteReachable" in diagnostics ? diagnostics.remoteReachable ?? null : active && config.provider === "legacy-supabase" ? true : null;
3769
+ const checks = [{
3770
+ name: "local-store",
3771
+ status: "ok",
3772
+ message: "local diagnostics are readable"
3773
+ }];
3774
+ if (config.provider !== "local") {
3775
+ const provider3 = config.provider === "gateway" ? "gateway" : "legacy provider";
3776
+ const remoteError2 = "remoteError" in diagnostics && typeof diagnostics.remoteError === "string" ? diagnostics.remoteError : void 0;
3777
+ checks.push({
3778
+ name: "remote",
3779
+ status: remoteReachable === false ? "degraded" : remoteError2 ? "failed" : remoteReachable === true ? "ok" : "unknown",
3780
+ message: remoteReachable === false ? `${provider3} is unreachable` : remoteError2 ? `${provider3} rejected diagnostics (${remoteError2})` : remoteReachable === true ? `${provider3} is reachable` : `${provider3} reachability has not been checked`
3781
+ });
3782
+ }
3783
+ const blocked = "outboxBlocked" in diagnostics ? Number(diagnostics.outboxBlocked ?? 0) : 0;
3784
+ checks.push({ name: "blocked-outbox", status: blocked > 0 ? "degraded" : "ok", message: blocked > 0 ? `${blocked} outbound message(s) require intervention` : "no blocked outbound messages" });
3785
+ const expired = Number(diagnostics.expiredLeases ?? 0);
3786
+ checks.push({ name: "expired-leases", status: expired > 0 ? "degraded" : "ok", message: expired > 0 ? `${expired} delivery lease(s) are expired` : "no expired delivery leases" });
3787
+ const dead = Number(diagnostics.dead ?? 0);
3788
+ checks.push({ name: "dead-deliveries", status: dead > 0 ? "degraded" : "ok", message: dead > 0 ? `${dead} dead delivery/deliveries are visible` : "no dead deliveries" });
3789
+ const status = checks.some((check) => check.status === "failed") ? "failed" : checks.some((check) => check.status === "degraded") ? "degraded" : checks.some((check) => check.status === "unknown") ? "unknown" : "ok";
3790
+ const remoteError = "remoteError" in diagnostics && typeof diagnostics.remoteError === "string";
3791
+ const connected = config.provider === "local" ? true : remoteReachable === true && !remoteError;
3792
+ const queue = { ...diagnostics };
3793
+ delete queue.syncLoopState;
3794
+ delete queue.syncLoopError;
3795
+ cliOutput("client_status", { status, localHealthy: true, connected, remoteReachable, provider: config.provider, workspace: config.principal.workspace, agent: config.principal.agent, instance: config.principal.instance ?? null, schemaVersion: diagnostics.schemaVersion, endpoint: config.url ?? null, database: config.provider === "local" ? config.databasePath : null, cursorPath: config.cursorPath, lastCursor: cursor(config.cursorPath) ?? null, queue, checks });
3796
+ if (active && status !== "ok") process.exitCode = status === "failed" ? 1 : 2;
3797
+ } catch (error) {
3798
+ failedClientStatus(config, error, { component: "local-store" });
3799
+ process.exitCode = 1;
3800
+ }
546
3801
  return;
547
3802
  }
548
3803
  if (command === "pending") {
549
- const diagnostics = await runtime.store.diagnostics?.(config.principal);
550
- const page = await runtime.service.history(config.principal, {
3804
+ const diagnostics = await runtime2.store.diagnostics?.(config.principal, {
3805
+ mode: config.provider === "gateway" ? "probe" : "snapshot"
3806
+ });
3807
+ const page = await runtime2.service.history(config.principal, {
551
3808
  limit: 1,
552
- unacknowledgedBy: config.principal.agent
3809
+ project: one(options, "project"),
3810
+ receiptState: "unread"
553
3811
  });
554
3812
  const pendingDeliveries = diagnostics?.pending ?? 0;
555
3813
  const retryingDeliveries = diagnostics?.retrying ?? 0;
@@ -557,55 +3815,91 @@ async function runCli(argv = process.argv.slice(2)) {
557
3815
  const deliveryAvailable = pendingDeliveries > 0 || retryingDeliveries > 0 && oldest <= Date.now();
558
3816
  const unread = page.messages.length > 0;
559
3817
  const available = deliveryAvailable || unread;
3818
+ const diagnosticsAuthoritative = !diagnostics || !("remoteReachable" in diagnostics) || diagnostics.remoteReachable === true;
3819
+ const pageAuthority = page;
3820
+ const historyAuthoritative = pageAuthority.acknowledgements !== "unknown" && pageAuthority.source !== "cache" && pageAuthority.stale !== true;
3821
+ const authoritative = diagnosticsAuthoritative && historyAuthoritative;
560
3822
  output({
561
3823
  available,
562
3824
  unread,
563
3825
  deliveryAvailable,
564
3826
  pending: pendingDeliveries,
565
3827
  retrying: retryingDeliveries,
566
- authoritative: !diagnostics || !("remoteReachable" in diagnostics) || diagnostics.remoteReachable !== false
3828
+ authoritative,
3829
+ state: available ? "available" : authoritative ? "empty" : "unknown"
567
3830
  });
568
- if (!available) process.exitCode = 1;
3831
+ if (!available) process.exitCode = authoritative ? 1 : 2;
569
3832
  return;
570
3833
  }
571
3834
  if (command === "send") {
572
- output(await runtime.service.publish(config.principal, draft(options, positionals)));
3835
+ const message = draft(options, positionals);
3836
+ validateRequest("publish_message", { ...message, source: one(options, "source") });
3837
+ cliOutput("publish_message", await runtime2.service.publish(config.principal, message));
573
3838
  return;
574
3839
  }
575
3840
  if (command === "join") {
576
- output(await runtime.service.heartbeat(config.principal, { leaseMs: integer(options, "lease-ms", 6e4), runtimeType: one(options, "runtime"), capabilities: list(options, "capability") }));
3841
+ const input = validateRequest("heartbeat", { leaseMs: integer(options, "lease-ms", 6e4), runtimeType: one(options, "runtime"), capabilities: list(options, "capability") });
3842
+ cliOutput("heartbeat", await runtime2.service.heartbeat(config.principal, input));
577
3843
  return;
578
3844
  }
579
3845
  if (command === "presence") {
580
- output({ agents: await runtime.service.presence(config.principal) });
3846
+ validateRequest("presence", {});
3847
+ cliOutput("presence", { agents: await runtime2.service.presence(config.principal) });
581
3848
  return;
582
3849
  }
583
3850
  if (command === "sync") {
584
- if (!runtime.store.sync) throw new Error("sync is available only with the gateway provider");
585
- output(await runtime.store.sync({
3851
+ if (!runtime2.store.sync) throw new Error("sync is available only with the gateway provider");
3852
+ const input = validateRequest("sync", {
586
3853
  maxPush: integer(options, "max-push", integer(options, "limit", 100)),
587
3854
  maxPages: integer(options, "max-pages", 20)
588
- }));
3855
+ });
3856
+ cliOutput("sync", await runtime2.store.sync(input));
589
3857
  return;
590
3858
  }
591
- if (command === "history" || command === "inbox") {
592
- const page = await runtime.service.history(config.principal, { cursor: one(options, "cursor"), limit: integer(options, "limit", 20), types: list(options, "type").concat(list(options, "category")), source: one(options, "source"), since: since(options), unacknowledgedBy: one(options, "unacked-by"), threadId: one(options, "thread-id"), latest: raw === "get" });
3859
+ if (command === "history" || command === "inbox" || command === "sent") {
3860
+ const query = validateRequest("history", { cursor: one(options, "cursor"), limit: integer(options, "limit", 20), types: list(options, "type").concat(list(options, "category")), source: one(options, "source"), project: one(options, "project"), since: since(options), mailbox: command === "inbox" ? "inbox" : command === "sent" ? "sent" : one(options, "mailbox"), receiptState: one(options, "receipt-state"), unacknowledgedBy: assertedUnackedBy, threadId: one(options, "thread-id"), latest: raw === "get" || boolean(options, "latest") });
3861
+ const page = await runtime2.service.history(config.principal, query);
593
3862
  if (raw === "get") {
594
- output(page.messages.map((message) => ({ id: config.provider === "legacy-supabase" ? Number(message.sequence) : message.id, source: message.source, category: message.type, content: message.content, priority: message.priority, project: message.workspace === "*" ? null : message.workspace, metadata: legacyContextMetadata(message), created_at: message.createdAt })));
595
- } else output(page);
3863
+ cliOutput("history", page.messages.map((message) => ({ id: config.provider === "legacy-supabase" ? Number(message.sequence) : message.id, source: message.source, category: message.type, content: message.content, priority: message.priority, project: message.project ?? null, metadata: legacyContextMetadata(message), created_at: message.createdAt })), { command: raw, optionNames: Object.keys(options) });
3864
+ } else cliOutput("history", page);
596
3865
  return;
597
3866
  }
598
3867
  if (command === "acknowledge" || command === "ack" && one(options, "ids")) {
599
3868
  const rawIds = list(options, "ids");
600
- const acknowledged = config.provider === "legacy-supabase" && runtime.store.recordLegacyReceipt && rawIds.every((id) => /^\d+$/.test(id)) ? await runtime.store.recordLegacyReceipt(rawIds, config.principal.agent) : await runtime.service.acknowledge(
3869
+ validateRequest("record_receipt", { messageIds: rawIds });
3870
+ const acknowledged = config.provider === "legacy-supabase" && runtime2.store.recordLegacyReceipt && rawIds.every((id) => /^\d+$/.test(id)) ? await runtime2.store.recordLegacyReceipt(rawIds, config.principal.agent) : await runtime2.service.acknowledge(
601
3871
  config.principal,
602
3872
  rawIds.map(legacyNumericMessageId)
603
3873
  );
604
- output({ acknowledged, agent: config.principal.agent });
3874
+ cliOutput("record_receipt", { acknowledged, agent: config.principal.agent }, {
3875
+ command: raw,
3876
+ optionNames: Object.keys(options)
3877
+ });
605
3878
  return;
606
3879
  }
607
3880
  if (command === "claim") {
608
- output(await runtime.service.claim(config.principal, { leaseMs: integer(options, "lease-ms", 3e4), maxAttempts: integer(options, "max-attempts", 5) }));
3881
+ const input = validateRequest("claim_delivery", { leaseMs: integer(options, "lease-ms", 3e4), maxAttempts: one(options, "max-attempts") === void 0 ? void 0 : integer(options, "max-attempts", 5) });
3882
+ cliOutput("claim_delivery", await runtime2.service.claim(config.principal, input));
3883
+ return;
3884
+ }
3885
+ if (command === "deliveries" || command === "dead-letters") {
3886
+ const input = validateRequest("list_deliveries", { cursor: one(options, "cursor"), limit: integer(options, "limit", 50), role: one(options, "role"), messageId: one(options, "message-id"), recipient: one(options, "recipient"), states: command === "dead-letters" ? ["dead"] : list(options, "state") });
3887
+ cliOutput("list_deliveries", await runtime2.service.deliveries(config.principal, input));
3888
+ return;
3889
+ }
3890
+ if (command === "delivery-events") {
3891
+ const input = validateRequest("list_delivery_events", { deliveryId: one(options, "delivery-id") ?? positionals[0], cursor: one(options, "cursor"), limit: integer(options, "limit", 50) });
3892
+ cliOutput("list_delivery_events", await runtime2.service.deliveryEvents(config.principal, String(input.deliveryId), input));
3893
+ return;
3894
+ }
3895
+ if (command === "cancel") {
3896
+ const input = validateRequest("cancel_delivery", { deliveryId: one(options, "delivery-id") ?? positionals[0] });
3897
+ cliOutput("cancel_delivery", await runtime2.service.cancel(config.principal, String(input.deliveryId)));
3898
+ return;
3899
+ }
3900
+ if (command === "requeue") {
3901
+ const input = validateRequest("requeue_delivery", { deliveryId: one(options, "delivery-id") ?? positionals[0] });
3902
+ cliOutput("requeue_delivery", await runtime2.service.requeue(config.principal, String(input.deliveryId)));
609
3903
  return;
610
3904
  }
611
3905
  const deliveryId = one(options, "delivery-id") ?? positionals[0];
@@ -614,15 +3908,19 @@ async function runCli(argv = process.argv.slice(2)) {
614
3908
  if (["extend", "ack", "nack"].includes(command)) throw new Error("--delivery-id and --lease-token are required");
615
3909
  }
616
3910
  if (command === "extend") {
617
- output(await runtime.service.extend(config.principal, deliveryId, leaseToken, integer(options, "lease-ms", 3e4)));
3911
+ const input = validateRequest("extend_delivery", { deliveryId, leaseToken, leaseMs: integer(options, "lease-ms", 3e4) });
3912
+ cliOutput("extend_delivery", await runtime2.service.extend(config.principal, String(input.deliveryId), String(input.leaseToken), input.leaseMs));
618
3913
  return;
619
3914
  }
620
3915
  if (command === "ack") {
621
- output(await runtime.service.ack(config.principal, deliveryId, leaseToken));
3916
+ const input = validateRequest("acknowledge_delivery", { deliveryId, leaseToken });
3917
+ cliOutput("acknowledge_delivery", await runtime2.service.ack(config.principal, String(input.deliveryId), String(input.leaseToken)));
622
3918
  return;
623
3919
  }
624
3920
  if (command === "nack") {
625
- output(await runtime.service.nack(config.principal, deliveryId, leaseToken, one(options, "error") ?? "negative acknowledgment", boolean(options, "dead")));
3921
+ const disposition = one(options, "disposition") ?? (boolean(options, "dead") ? "dead" : "retry");
3922
+ const input = validateRequest("negative_acknowledge_delivery", { deliveryId, leaseToken, error: one(options, "error") ?? "negative acknowledgment", disposition });
3923
+ cliOutput("negative_acknowledge_delivery", await runtime2.service.nack(config.principal, String(input.deliveryId), String(input.leaseToken), String(input.error), input.disposition));
626
3924
  return;
627
3925
  }
628
3926
  if (command === "watch") {
@@ -630,15 +3928,21 @@ async function runCli(argv = process.argv.slice(2)) {
630
3928
  const baseInterval = Math.min(integer(options, "interval-ms", 1e3), 6e4);
631
3929
  const maxInterval = Math.max(baseInterval, Math.min(integer(options, "max-interval-ms", 3e4), 3e5));
632
3930
  let interval = baseInterval;
633
- let current = cursor(config.cursorPath);
3931
+ const project = one(options, "project");
3932
+ const checkpointPath = watchCursorPath(config.cursorPath, project);
3933
+ let current = cursor(checkpointPath);
634
3934
  let seen = 0;
635
3935
  for (let i = 0; i < polls; i++) {
636
3936
  try {
637
- const page = await runtime.service.history(config.principal, { cursor: current, limit: integer(options, "limit", 50) });
3937
+ const page = await runtime2.service.history(config.principal, {
3938
+ cursor: current,
3939
+ limit: integer(options, "limit", 50),
3940
+ project
3941
+ });
638
3942
  for (const message of page.messages) output(message);
639
3943
  seen += page.messages.length;
640
3944
  current = page.cursor;
641
- saveCursor(config.cursorPath, current);
3945
+ saveCursor(checkpointPath, current);
642
3946
  interval = page.messages.length ? baseInterval : Math.min(maxInterval, Math.max(baseInterval, interval * 2));
643
3947
  } catch (error) {
644
3948
  const status = error instanceof BridgeHttpError || error instanceof LegacySupabaseError ? error.status : void 0;
@@ -648,7 +3952,7 @@ async function runCli(argv = process.argv.slice(2)) {
648
3952
  }
649
3953
  if (i + 1 < polls) {
650
3954
  const jittered = Math.round(interval * (0.9 + Math.random() * 0.2));
651
- await new Promise((resolve) => setTimeout(resolve, jittered));
3955
+ await new Promise((resolve5) => setTimeout(resolve5, jittered));
652
3956
  }
653
3957
  }
654
3958
  if (!seen && boolean(options, "json")) output({ status: "ok", messages: 0, cursor: current ?? null });
@@ -656,15 +3960,52 @@ async function runCli(argv = process.argv.slice(2)) {
656
3960
  }
657
3961
  throw new Error(`Unknown command: ${raw}`);
658
3962
  } finally {
659
- await runtime.close();
3963
+ await runtime2.close();
3964
+ }
3965
+ }
3966
+ function formatCliError(error, argv = process.argv.slice(2)) {
3967
+ const message = error instanceof Error ? error.message : String(error);
3968
+ if (argv[0] === "archive") {
3969
+ return JSON.stringify({
3970
+ schemaVersion: 1,
3971
+ status: "error",
3972
+ operation: argv[1] ?? null,
3973
+ error: {
3974
+ code: error instanceof ArchiveCommandError ? error.code : "ARCHIVE_COMMAND_ERROR",
3975
+ message: error instanceof ArchiveCommandError ? error.message : "archive command failed",
3976
+ ...error instanceof ArchiveCommandError && error.details ? { details: error.details } : {}
3977
+ }
3978
+ }) + "\n";
660
3979
  }
3980
+ if (argv[0] === "dr") {
3981
+ return JSON.stringify({
3982
+ schemaVersion: 1,
3983
+ status: "error",
3984
+ operation: argv[1] ?? null,
3985
+ error: {
3986
+ code: error instanceof NativeDrCommandError ? error.code : "DR_COMMAND_ERROR",
3987
+ message: error instanceof NativeDrCommandError ? error.message : "DR command failed",
3988
+ ...error instanceof NativeDrCommandError && error.details ? { details: error.details } : {}
3989
+ }
3990
+ }) + "\n";
3991
+ }
3992
+ if (argv[0] === "owner") {
3993
+ return JSON.stringify({
3994
+ schemaVersion: 1,
3995
+ status: "error",
3996
+ operation: argv[1] ?? null,
3997
+ error: { code: "OWNER_COMMAND_ERROR", message }
3998
+ }) + "\n";
3999
+ }
4000
+ return `Error: ${message}
4001
+ `;
661
4002
  }
662
4003
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) runCli().catch((error) => {
663
- process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
664
- `);
4004
+ process.stderr.write(formatCliError(error));
665
4005
  process.exitCode = 1;
666
4006
  });
667
4007
  export {
4008
+ formatCliError,
668
4009
  runCli
669
4010
  };
670
4011
  //# sourceMappingURL=cli.js.map