@creatornader/agent-bridge 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -1
- package/README.md +467 -29
- package/ROADMAP.md +104 -0
- package/SECURITY.md +73 -0
- package/SKILL.md +68 -2
- package/bin/agent-bridge +2 -2
- package/clients/claude-desktop.json +3 -1
- package/dist/archive.d.ts +163 -0
- package/dist/archive.js +50 -0
- package/dist/archive.js.map +1 -0
- package/dist/bridge-domain-CBX3IVqC.d.ts +153 -0
- package/dist/{bridge-service-CfqMcMAn.d.ts → bridge-service-hX1ZQp4W.d.ts} +15 -5
- package/dist/bridge-store-DPVSCwfs.d.ts +102 -0
- package/dist/bridge.d.ts +76 -17
- package/dist/bridge.js +52 -24
- package/dist/chunk-2DFNOLMO.js +618 -0
- package/dist/chunk-2DFNOLMO.js.map +1 -0
- package/dist/{chunk-KVR4JBRF.js → chunk-2EVLG3YA.js} +2 -2
- package/dist/{chunk-KVR4JBRF.js.map → chunk-2EVLG3YA.js.map} +1 -1
- package/dist/chunk-45BZGB77.js +709 -0
- package/dist/chunk-45BZGB77.js.map +1 -0
- package/dist/chunk-4VB44KJU.js +230 -0
- package/dist/chunk-4VB44KJU.js.map +1 -0
- package/dist/chunk-5GMB3A77.js +1013 -0
- package/dist/chunk-5GMB3A77.js.map +1 -0
- package/dist/chunk-A6W4QALF.js +1258 -0
- package/dist/chunk-A6W4QALF.js.map +1 -0
- package/dist/chunk-BZLMF5O5.js +328 -0
- package/dist/chunk-BZLMF5O5.js.map +1 -0
- package/dist/{chunk-AXL2I7FH.js → chunk-CEXBMOI5.js} +240 -79
- package/dist/chunk-CEXBMOI5.js.map +1 -0
- package/dist/chunk-GADH244D.js +527 -0
- package/dist/chunk-GADH244D.js.map +1 -0
- package/dist/chunk-MBPZXTJA.js +2428 -0
- package/dist/chunk-MBPZXTJA.js.map +1 -0
- package/dist/chunk-NCO7RLBM.js +146 -0
- package/dist/chunk-NCO7RLBM.js.map +1 -0
- package/dist/chunk-Q6PPMXJC.js +1050 -0
- package/dist/chunk-Q6PPMXJC.js.map +1 -0
- package/dist/{chunk-4KAOBC5X.js → chunk-SPSMZOCJ.js} +190 -165
- package/dist/chunk-SPSMZOCJ.js.map +1 -0
- package/dist/chunk-UAHMP6GC.js +54 -0
- package/dist/chunk-UAHMP6GC.js.map +1 -0
- package/dist/{chunk-YD6YJATN.js → chunk-VOQIDTCJ.js} +4 -2
- package/dist/chunk-VOQIDTCJ.js.map +1 -0
- package/dist/chunk-WAI2IYIQ.js +554 -0
- package/dist/chunk-WAI2IYIQ.js.map +1 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +3577 -250
- package/dist/cli.js.map +1 -1
- package/dist/client-config.d.ts +1 -1
- package/dist/client-config.js +1 -1
- package/dist/client-runtime.d.ts +10 -6
- package/dist/client-runtime.js +9 -6
- package/dist/contracts/generate.d.ts +2 -0
- package/dist/contracts/generate.js +247 -0
- package/dist/contracts/generate.js.map +1 -0
- package/dist/dr.d.ts +324 -0
- package/dist/dr.js +124 -0
- package/dist/dr.js.map +1 -0
- package/dist/gateway-CggQ8vTC.d.ts +78 -0
- package/dist/gateway-main.js +190 -8
- package/dist/gateway-main.js.map +1 -1
- package/dist/gateway.d.ts +4 -3
- package/dist/gateway.js +4 -3
- package/dist/index.js +11 -8
- package/dist/index.js.map +1 -1
- package/dist/postgres-bridge-store-Bfd3hRId.d.ts +49 -0
- package/dist/server.d.ts +3 -2
- package/dist/server.js +13 -8
- package/dist/sqlite.d.ts +94 -12
- package/dist/sqlite.js +7 -4
- package/docs/architecture-v2.md +375 -0
- package/docs/decisions/0001-protocol-layers-and-acknowledgment-semantics.md +85 -0
- package/docs/decisions/0002-canonical-operation-contract-registry.md +46 -0
- package/docs/ecosystem.md +54 -0
- package/docs/postmortems/2026-07-08-wrapper-source-drift.md +82 -0
- package/docs/troubleshooting.md +89 -0
- package/llms.txt +96 -4
- package/openapi/agent-bridge-v2.openapi.json +9610 -0
- package/package.json +24 -4
- package/schemas/agent-bridge-v2.capabilities.json +1829 -0
- package/schemas/agent-bridge-v2.mcp.json +2074 -0
- package/schemas/agent-bridge-v2.schema.json +4560 -0
- package/sql/migrations/008_message_projects.sql +36 -0
- package/sql/migrations/009_mailbox_query_indexes.sql +28 -0
- package/sql/migrations/010_publisher_delivery_policy.sql +210 -0
- package/sql/migrations/011_credential_security.sql +875 -0
- package/sql/migrations/012_request_authority.sql +177 -0
- package/sql/migrations/013_row_isolation.sql +495 -0
- package/sql/migrations/014_owner_control_plane.sql +1641 -0
- package/sql/migrations/015_portable_archives.sql +1813 -0
- package/sql/migrations/016_native_dr_fence.sql +1005 -0
- package/bin/CLAUDE.md +0 -11
- package/dist/bridge-domain-BcnBeHOn.d.ts +0 -97
- package/dist/bridge-store-BuGjxTn4.d.ts +0 -61
- package/dist/chunk-4KAOBC5X.js.map +0 -1
- package/dist/chunk-6MAZ6YLZ.js +0 -146
- package/dist/chunk-6MAZ6YLZ.js.map +0 -1
- package/dist/chunk-AXL2I7FH.js.map +0 -1
- package/dist/chunk-BV6W4AJK.js +0 -211
- package/dist/chunk-BV6W4AJK.js.map +0 -1
- package/dist/chunk-CKSKUBDQ.js +0 -153
- package/dist/chunk-CKSKUBDQ.js.map +0 -1
- package/dist/chunk-JOUBDRYX.js +0 -272
- package/dist/chunk-JOUBDRYX.js.map +0 -1
- package/dist/chunk-JTC5YURG.js +0 -49
- package/dist/chunk-JTC5YURG.js.map +0 -1
- package/dist/chunk-O26W5QHS.js +0 -409
- package/dist/chunk-O26W5QHS.js.map +0 -1
- package/dist/chunk-WEULPMUY.js +0 -988
- package/dist/chunk-WEULPMUY.js.map +0 -1
- package/dist/chunk-YD6YJATN.js.map +0 -1
- package/dist/gateway-BULozv3K.d.ts +0 -54
package/dist/cli.js
CHANGED
|
@@ -1,213 +1,3220 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
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-
|
|
13
|
+
} from "./chunk-2EVLG3YA.js";
|
|
9
14
|
import {
|
|
10
15
|
createClientRuntime
|
|
11
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-UAHMP6GC.js";
|
|
12
17
|
import {
|
|
13
18
|
BridgeHttpError,
|
|
14
19
|
LegacySupabaseError,
|
|
15
20
|
legacyContextMetadata,
|
|
16
21
|
legacyNumericMessageId
|
|
17
|
-
} from "./chunk-
|
|
18
|
-
import "./chunk-
|
|
19
|
-
import
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
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
|
|
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 {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
936
|
+
return join2(appData, "Claude", "claude_desktop_config.json");
|
|
937
|
+
}
|
|
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");
|
|
940
|
+
}
|
|
941
|
+
function safeComponent(value) {
|
|
942
|
+
const readable = value.replace(/[^A-Za-z0-9._-]+/g, "_").slice(0, 48) || "client";
|
|
943
|
+
const suffix = createHash2("sha256").update(value).digest("hex").slice(0, 12);
|
|
944
|
+
return `${readable}-${suffix}`;
|
|
945
|
+
}
|
|
946
|
+
function clientBackendConfigPath(runtime2, instance, env) {
|
|
947
|
+
const home2 = env.HOME ?? homedir2();
|
|
948
|
+
return join2(
|
|
949
|
+
home2,
|
|
950
|
+
".agent-bridge",
|
|
951
|
+
"clients",
|
|
952
|
+
`${runtime2}-${safeComponent(instance)}.config`
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
function replacePrivateFile(path, content) {
|
|
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) {
|
|
984
|
+
const env = options.env;
|
|
985
|
+
const home2 = env.HOME ?? homedir2();
|
|
986
|
+
const sharedPath = env.AGENT_BRIDGE_CONFIG ?? join2(home2, ".agent-bridge", "config");
|
|
987
|
+
const shared = readClientConfigFile(sharedPath);
|
|
988
|
+
const configuredProvider = env.AGENT_BRIDGE_PROVIDER?.trim() || shared.AGENT_BRIDGE_PROVIDER?.trim();
|
|
989
|
+
const inferredGateway = !configuredProvider && Boolean(
|
|
990
|
+
env.AGENT_BRIDGE_TOKEN?.trim() || shared.AGENT_BRIDGE_TOKEN?.trim()
|
|
991
|
+
);
|
|
992
|
+
const provider3 = configuredProvider === "legacy" || configuredProvider === "supabase" ? "legacy-supabase" : configuredProvider ?? (inferredGateway ? "gateway" : void 0);
|
|
993
|
+
const clientToken = options.token?.trim() || env.AGENT_BRIDGE_CLIENT_TOKEN?.trim();
|
|
994
|
+
if (provider3 === "gateway" && !clientToken) {
|
|
995
|
+
throw new Error(
|
|
996
|
+
"gateway client installation requires --token or AGENT_BRIDGE_CLIENT_TOKEN"
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
const resolved = resolveClientConfig({
|
|
1000
|
+
...env,
|
|
1001
|
+
AGENT_BRIDGE_AGENT: void 0,
|
|
1002
|
+
AGENT_BRIDGE_INSTANCE: void 0,
|
|
1003
|
+
AGENT_BRIDGE_TOKEN: clientToken ?? env.AGENT_BRIDGE_TOKEN
|
|
1004
|
+
}, identity);
|
|
1005
|
+
const values = {
|
|
1006
|
+
AGENT_BRIDGE_PROVIDER: resolved.provider,
|
|
1007
|
+
AGENT_BRIDGE_WORKSPACE: resolved.principal.workspace,
|
|
1008
|
+
AGENT_BRIDGE_URL: resolved.url,
|
|
1009
|
+
AGENT_BRIDGE_TOKEN: resolved.provider === "gateway" ? resolved.credential : void 0,
|
|
1010
|
+
AGENT_BRIDGE_KEY: resolved.provider === "legacy-supabase" ? resolved.credential : void 0,
|
|
1011
|
+
AGENT_BRIDGE_DB: resolved.provider === "local" ? resolved.databasePath : 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
|
|
1016
|
+
};
|
|
1017
|
+
for (const value of Object.values(values)) {
|
|
1018
|
+
if (value?.includes("\n") || value?.includes("\r")) {
|
|
1019
|
+
throw new Error("client backend config values cannot contain newlines");
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
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
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
function closeExtractedPostgresBundle(extracted) {
|
|
2672
|
+
for (const value of extracted.descriptors.values()) {
|
|
2673
|
+
try {
|
|
2674
|
+
closeSync4(value.descriptor);
|
|
2675
|
+
} catch {
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
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;
|
|
42
2700
|
}
|
|
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");
|
|
45
2701
|
}
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
const suffix = createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
49
|
-
return `${readable}-${suffix}`;
|
|
2702
|
+
function recoveryPaths(paths) {
|
|
2703
|
+
return paths.filter(existsSync4);
|
|
50
2704
|
}
|
|
51
|
-
function
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
+
}
|
|
59
2717
|
}
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const sharedPath = env.AGENT_BRIDGE_CONFIG ?? join(home, ".agent-bridge", "config");
|
|
72
|
-
const shared = readClientConfigFile(sharedPath);
|
|
73
|
-
const configuredProvider = env.AGENT_BRIDGE_PROVIDER?.trim() || shared.AGENT_BRIDGE_PROVIDER?.trim();
|
|
74
|
-
const inferredGateway = !configuredProvider && Boolean(
|
|
75
|
-
env.AGENT_BRIDGE_TOKEN?.trim() || shared.AGENT_BRIDGE_TOKEN?.trim()
|
|
76
|
-
);
|
|
77
|
-
const provider = configuredProvider === "legacy" || configuredProvider === "supabase" ? "legacy-supabase" : configuredProvider ?? (inferredGateway ? "gateway" : void 0);
|
|
78
|
-
const clientToken = options.token?.trim() || env.AGENT_BRIDGE_CLIENT_TOKEN?.trim();
|
|
79
|
-
if (provider === "gateway" && !clientToken) {
|
|
80
|
-
throw new Error(
|
|
81
|
-
"gateway client installation requires --token or AGENT_BRIDGE_CLIENT_TOKEN"
|
|
82
|
-
);
|
|
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
|
+
});
|
|
83
2729
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const values = {
|
|
91
|
-
AGENT_BRIDGE_PROVIDER: resolved.provider,
|
|
92
|
-
AGENT_BRIDGE_WORKSPACE: resolved.principal.workspace,
|
|
93
|
-
AGENT_BRIDGE_URL: resolved.url,
|
|
94
|
-
AGENT_BRIDGE_TOKEN: resolved.provider === "gateway" ? resolved.credential : void 0,
|
|
95
|
-
AGENT_BRIDGE_KEY: resolved.provider === "legacy-supabase" ? resolved.credential : void 0,
|
|
96
|
-
AGENT_BRIDGE_DB: resolved.provider === "local" ? resolved.databasePath : void 0,
|
|
97
|
-
AGENT_BRIDGE_EDGE_DB: resolved.provider === "gateway" ? resolved.edgeDatabasePath : void 0
|
|
98
|
-
};
|
|
99
|
-
for (const value of Object.values(values)) {
|
|
100
|
-
if (value?.includes("\n") || value?.includes("\r")) {
|
|
101
|
-
throw new Error("client backend config values cannot contain newlines");
|
|
102
|
-
}
|
|
2730
|
+
if (error instanceof PostgresNativeDrError) {
|
|
2731
|
+
return new NativeDrCommandError(error.code, error.message, {
|
|
2732
|
+
...details,
|
|
2733
|
+
...identity,
|
|
2734
|
+
...retained.length ? { recoveryPaths: retained } : {}
|
|
2735
|
+
});
|
|
103
2736
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
+
});
|
|
111
2753
|
}
|
|
112
|
-
function
|
|
113
|
-
|
|
114
|
-
|
|
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);
|
|
115
2799
|
try {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
+
});
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
durable = true;
|
|
119
2846
|
} catch {
|
|
120
|
-
|
|
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
|
+
});
|
|
121
2854
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
+
});
|
|
135
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
|
+
});
|
|
136
2947
|
}
|
|
2948
|
+
throw commandErrorWithRecovery(error, { backupId }, [bundleStage, stage]);
|
|
137
2949
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
const
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
168
|
-
|
|
169
|
-
"
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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 {
|
|
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
|
-
|
|
204
|
-
|
|
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]);
|
|
3108
|
+
}
|
|
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");
|
|
210
3216
|
}
|
|
3217
|
+
throw new NativeDrCommandError("DR_COMMAND_FAILED", "DR command failed");
|
|
211
3218
|
}
|
|
212
3219
|
|
|
213
3220
|
// src/cli.ts
|
|
@@ -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
|
|
276
|
-
function
|
|
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 (!
|
|
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,37 +3366,87 @@ 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
|
|
334
|
-
if (!
|
|
335
|
-
const amount = Number(
|
|
336
|
-
const milliseconds = amount * (
|
|
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
|
|
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
|
-
|
|
3422
|
+
preparePrivateOutput(path);
|
|
353
3423
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
354
|
-
|
|
3424
|
+
try {
|
|
3425
|
+
writeFileSync3(temporary, `${value}
|
|
355
3426
|
`, { mode: 384 });
|
|
356
|
-
|
|
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}`;
|
|
357
3439
|
}
|
|
358
3440
|
function help() {
|
|
359
3441
|
process.stdout.write(`agent-bridge: provider-neutral agent messaging
|
|
360
3442
|
|
|
361
3443
|
Commands:
|
|
362
|
-
init, doctor, status, pending, migrate, sync, demo, join, presence
|
|
363
|
-
send (post), inbox (get), history, acknowledge, claim, extend, ack, nack, watch
|
|
3444
|
+
init, doctor, status, capabilities, pending, migrate, reconcile-legacy-projects, sync, demo, join, presence
|
|
3445
|
+
send (post), inbox (get), sent, history, acknowledge, claim, extend, ack, nack, watch
|
|
3446
|
+
deliveries, dead-letters, delivery-events, cancel, requeue
|
|
3447
|
+
owner <provision|inventory|rotate|revoke>
|
|
3448
|
+
archive <export|verify|import>
|
|
3449
|
+
dr <backup|verify|restore>
|
|
364
3450
|
clients install <codex|claude-code|claude-desktop> --identity <name>
|
|
365
3451
|
`);
|
|
366
3452
|
}
|
|
@@ -368,19 +3454,43 @@ function rejectUnknownOptions(options) {
|
|
|
368
3454
|
const unknown = Object.keys(options).filter((key) => !SUPPORTED_OPTIONS.has(key));
|
|
369
3455
|
if (unknown.length) throw new Error(`unknown option: --${unknown[0]}`);
|
|
370
3456
|
}
|
|
3457
|
+
function rejectOptionsOutside(options, allowed, command) {
|
|
3458
|
+
const invalid = Object.keys(options).find((key) => !allowed.has(key));
|
|
3459
|
+
if (invalid) throw new Error(`--${invalid} is not valid for ${command}`);
|
|
3460
|
+
}
|
|
3461
|
+
var CLI_CONTEXT_OPTIONS = /* @__PURE__ */ new Set([
|
|
3462
|
+
"agent",
|
|
3463
|
+
"as",
|
|
3464
|
+
"config",
|
|
3465
|
+
"db",
|
|
3466
|
+
"instance",
|
|
3467
|
+
"json",
|
|
3468
|
+
"key",
|
|
3469
|
+
"provider",
|
|
3470
|
+
"token",
|
|
3471
|
+
"url",
|
|
3472
|
+
"workspace"
|
|
3473
|
+
]);
|
|
3474
|
+
function validateCanonicalCommandOptions(command, options, config) {
|
|
3475
|
+
const contract = operationForCli(command, config.provider, Object.keys(options));
|
|
3476
|
+
if (!contract?.cli) return;
|
|
3477
|
+
const allowed = /* @__PURE__ */ new Set([...CLI_CONTEXT_OPTIONS, ...contract.cli.options]);
|
|
3478
|
+
const invalid = Object.keys(options).find((key) => !allowed.has(key));
|
|
3479
|
+
if (invalid) throw new Error(`--${invalid} is not valid for ${command}`);
|
|
3480
|
+
}
|
|
371
3481
|
async function initialize(options) {
|
|
372
|
-
const
|
|
373
|
-
const configPath = one(options, "config") ?? process.env.AGENT_BRIDGE_CONFIG ??
|
|
374
|
-
if (
|
|
3482
|
+
const home2 = process.env.HOME ?? homedir4();
|
|
3483
|
+
const configPath = one(options, "config") ?? process.env.AGENT_BRIDGE_CONFIG ?? join5(home2, ".agent-bridge", "config");
|
|
3484
|
+
if (existsSync5(configPath) && !boolean(options, "force")) {
|
|
375
3485
|
throw new Error(`Config already exists at ${configPath}; pass --force to replace it`);
|
|
376
3486
|
}
|
|
377
|
-
const
|
|
3487
|
+
const provider3 = one(options, "provider") ?? process.env.AGENT_BRIDGE_PROVIDER ?? "local";
|
|
378
3488
|
if (one(options, "instance")) {
|
|
379
3489
|
throw new Error("--instance is client-scoped; set it in the active client process");
|
|
380
3490
|
}
|
|
381
3491
|
const values = {
|
|
382
|
-
AGENT_BRIDGE_PROVIDER:
|
|
383
|
-
AGENT_BRIDGE_WORKSPACE: one(options, "workspace") ?? process.env.AGENT_BRIDGE_WORKSPACE ?? (
|
|
3492
|
+
AGENT_BRIDGE_PROVIDER: provider3,
|
|
3493
|
+
AGENT_BRIDGE_WORKSPACE: one(options, "workspace") ?? process.env.AGENT_BRIDGE_WORKSPACE ?? (provider3 === "legacy-supabase" || provider3 === "legacy" || provider3 === "supabase" ? "*" : "default"),
|
|
384
3494
|
AGENT_BRIDGE_DB: one(options, "db") ?? process.env.AGENT_BRIDGE_DB,
|
|
385
3495
|
AGENT_BRIDGE_EDGE_DB: process.env.AGENT_BRIDGE_EDGE_DB,
|
|
386
3496
|
AGENT_BRIDGE_URL: one(options, "url") ?? process.env.AGENT_BRIDGE_URL,
|
|
@@ -390,49 +3500,65 @@ async function initialize(options) {
|
|
|
390
3500
|
for (const value of Object.values(values)) {
|
|
391
3501
|
if (value?.includes("\n") || value?.includes("\r")) throw new Error("config values cannot contain newlines");
|
|
392
3502
|
}
|
|
393
|
-
|
|
3503
|
+
preparePrivateOutput(configPath);
|
|
394
3504
|
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
3505
|
let config;
|
|
401
3506
|
try {
|
|
3507
|
+
writeFileSync3(
|
|
3508
|
+
temporary,
|
|
3509
|
+
Object.entries(values).filter((entry) => Boolean(entry[1])).map(([key, value]) => `${key}=${value}`).join("\n") + "\n",
|
|
3510
|
+
{ mode: 384 }
|
|
3511
|
+
);
|
|
3512
|
+
securePrivatePath(temporary, "file");
|
|
402
3513
|
config = resolveClientConfig(
|
|
403
3514
|
{
|
|
404
|
-
HOME:
|
|
3515
|
+
HOME: home2,
|
|
405
3516
|
AGENT_BRIDGE_CONFIG: temporary,
|
|
406
3517
|
AGENT_BRIDGE_TOKEN: verificationToken
|
|
407
3518
|
},
|
|
408
3519
|
"agent-bridge-init"
|
|
409
3520
|
);
|
|
410
|
-
const
|
|
3521
|
+
const runtime2 = await createClientRuntime(config);
|
|
411
3522
|
try {
|
|
412
|
-
if (config.provider === "gateway") await
|
|
3523
|
+
if (config.provider === "gateway") await runtime2.store.verifyRemote?.();
|
|
413
3524
|
} finally {
|
|
414
|
-
await
|
|
3525
|
+
await runtime2.close();
|
|
415
3526
|
}
|
|
416
|
-
|
|
3527
|
+
renameSync4(temporary, configPath);
|
|
3528
|
+
verifyPrivatePathAccess(configPath, "file");
|
|
417
3529
|
} catch (error) {
|
|
418
|
-
|
|
3530
|
+
rmSync4(temporary, { force: true });
|
|
419
3531
|
throw error;
|
|
420
3532
|
}
|
|
421
3533
|
output({ status: "ok", config: configPath, provider: config.provider, workspace: config.principal.workspace, identity: "process-scoped", database: config.provider === "local" ? config.databasePath : null });
|
|
422
3534
|
}
|
|
423
3535
|
function configFor(options, command) {
|
|
424
3536
|
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
|
|
426
|
-
|
|
3537
|
+
const environment = {
|
|
3538
|
+
...process.env,
|
|
3539
|
+
...one(options, "config") ? { AGENT_BRIDGE_CONFIG: one(options, "config") } : {},
|
|
3540
|
+
...one(options, "provider") ? { AGENT_BRIDGE_PROVIDER: one(options, "provider") } : {},
|
|
3541
|
+
...one(options, "db") ? { AGENT_BRIDGE_DB: one(options, "db") } : {},
|
|
3542
|
+
...one(options, "url") ? { AGENT_BRIDGE_URL: one(options, "url") } : {},
|
|
3543
|
+
...one(options, "key") ? { AGENT_BRIDGE_KEY: one(options, "key") } : {},
|
|
3544
|
+
...one(options, "token") ? { AGENT_BRIDGE_TOKEN: one(options, "token") } : {},
|
|
3545
|
+
...one(options, "instance") ? { AGENT_BRIDGE_INSTANCE: one(options, "instance") } : {}
|
|
3546
|
+
};
|
|
427
3547
|
const config = resolveClientConfig(environment, explicit);
|
|
428
|
-
|
|
429
|
-
if (config.provider === "gateway") {
|
|
430
|
-
throw new Error("--
|
|
3548
|
+
const assertedWorkspace = one(options, "workspace");
|
|
3549
|
+
if (config.provider === "gateway" && assertedWorkspace && assertedWorkspace !== config.principal.workspace) {
|
|
3550
|
+
throw new Error("--workspace must match the workspace bound to the gateway credential");
|
|
431
3551
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
)
|
|
3552
|
+
if (config.provider === "legacy-supabase" && assertedWorkspace && assertedWorkspace !== "*") {
|
|
3553
|
+
throw new Error("--workspace is not supported by the global legacy Supabase schema");
|
|
3554
|
+
}
|
|
3555
|
+
if (config.provider === "local" && assertedWorkspace && assertedWorkspace !== config.principal.workspace) {
|
|
3556
|
+
return resolveClientConfig(
|
|
3557
|
+
{ ...environment, AGENT_BRIDGE_WORKSPACE: assertedWorkspace },
|
|
3558
|
+
explicit
|
|
3559
|
+
);
|
|
3560
|
+
}
|
|
3561
|
+
return config;
|
|
436
3562
|
}
|
|
437
3563
|
function draft(options, positionals) {
|
|
438
3564
|
const content = one(options, "content") ?? positionals.join(" ");
|
|
@@ -443,7 +3569,12 @@ function draft(options, positionals) {
|
|
|
443
3569
|
if (one(options, "payload-ref")) envelope.payload_ref = one(options, "payload-ref");
|
|
444
3570
|
if (one(options, "payload-ciphertext")) envelope.payload_ciphertext = one(options, "payload-ciphertext");
|
|
445
3571
|
if (Object.keys(envelope).length) metadata.message_envelope = envelope;
|
|
3572
|
+
const deliveryMode = one(options, "delivery-mode");
|
|
3573
|
+
const deliveryTuning = ["delivery-max-attempts", "retry-base-ms", "retry-max-ms", "retry-jitter", "not-before"].some((key) => one(options, key) !== void 0);
|
|
3574
|
+
if (deliveryTuning && !deliveryMode) throw new Error("--delivery-mode is required with delivery policy flags");
|
|
3575
|
+
if (deliveryMode === "mailbox" && deliveryTuning) throw new Error("mailbox delivery mode does not accept retry or scheduling flags");
|
|
446
3576
|
return {
|
|
3577
|
+
project: one(options, "project"),
|
|
447
3578
|
type: one(options, "type") ?? one(options, "kind") ?? one(options, "category") ?? "operational",
|
|
448
3579
|
content,
|
|
449
3580
|
targets: [...list(options, "target"), ...list(options, "target-agent"), ...list(options, "target-agents")],
|
|
@@ -459,13 +3590,15 @@ function draft(options, positionals) {
|
|
|
459
3590
|
atribReceiptId: one(options, "atrib-receipt-id"),
|
|
460
3591
|
informedBy: list(options, "informed-by"),
|
|
461
3592
|
data: json(options, "data") ?? json(options, "payload"),
|
|
462
|
-
metadata
|
|
3593
|
+
metadata,
|
|
3594
|
+
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
3595
|
};
|
|
464
3596
|
}
|
|
465
3597
|
async function localDemo() {
|
|
466
|
-
const directory = mkdtempSync(
|
|
467
|
-
const databasePath =
|
|
468
|
-
|
|
3598
|
+
const directory = mkdtempSync(join5(tmpdir(), "agent-bridge-demo-"));
|
|
3599
|
+
const databasePath = join5(directory, "demo.sqlite3");
|
|
3600
|
+
securePrivatePath(directory, "directory");
|
|
3601
|
+
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
3602
|
let sender;
|
|
470
3603
|
let worker;
|
|
471
3604
|
try {
|
|
@@ -482,34 +3615,65 @@ async function localDemo() {
|
|
|
482
3615
|
} finally {
|
|
483
3616
|
await worker?.close();
|
|
484
3617
|
await sender?.close();
|
|
485
|
-
|
|
3618
|
+
rmSync4(directory, { recursive: true, force: true });
|
|
486
3619
|
}
|
|
487
3620
|
}
|
|
488
3621
|
async function runCli(argv = process.argv.slice(2)) {
|
|
489
|
-
|
|
3622
|
+
if (argv[0] === "archive") {
|
|
3623
|
+
output(await runArchiveCommand(argv.slice(1), process.env));
|
|
3624
|
+
return;
|
|
3625
|
+
}
|
|
3626
|
+
if (argv[0] === "dr") {
|
|
3627
|
+
output(await runDrCommand(argv.slice(1)));
|
|
3628
|
+
return;
|
|
3629
|
+
}
|
|
3630
|
+
const { command: raw, options, positionals } = parse3(argv);
|
|
490
3631
|
const command = { post: "send", get: "inbox", receipt: "acknowledge" }[raw] ?? raw;
|
|
491
3632
|
if (["help", "-h", "--help"].includes(command)) {
|
|
492
3633
|
help();
|
|
493
3634
|
return;
|
|
494
3635
|
}
|
|
495
3636
|
rejectUnknownOptions(options);
|
|
3637
|
+
if (command === "owner") {
|
|
3638
|
+
if (positionals.length !== 1) {
|
|
3639
|
+
throw new Error("usage: agent-bridge owner <provision|inventory|rotate|revoke>");
|
|
3640
|
+
}
|
|
3641
|
+
const ownerOptions = {};
|
|
3642
|
+
for (const [key, value] of Object.entries(options)) {
|
|
3643
|
+
if (Array.isArray(value)) throw new Error("--" + key + " may only be provided once");
|
|
3644
|
+
ownerOptions[key] = value;
|
|
3645
|
+
}
|
|
3646
|
+
output(await runOwnerCommand(positionals[0], ownerOptions, process.env));
|
|
3647
|
+
return;
|
|
3648
|
+
}
|
|
496
3649
|
if (command === "clients") {
|
|
497
|
-
if (positionals[0] !== "install" || !positionals[1]) {
|
|
3650
|
+
if (positionals.length !== 2 || positionals[0] !== "install" || !positionals[1]) {
|
|
498
3651
|
throw new Error("usage: agent-bridge clients install <runtime> --identity <name>");
|
|
499
3652
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
3653
|
+
rejectOptionsOutside(options, /* @__PURE__ */ new Set([
|
|
3654
|
+
"identity",
|
|
3655
|
+
"command",
|
|
3656
|
+
"scope",
|
|
3657
|
+
"instance",
|
|
3658
|
+
"token",
|
|
3659
|
+
"enrollment-file",
|
|
3660
|
+
"recover-lock"
|
|
3661
|
+
]), "clients install");
|
|
3662
|
+
const runtime3 = positionals[1];
|
|
3663
|
+
if (runtime3 && !["codex", "claude-code", "claude-desktop"].includes(runtime3)) {
|
|
3664
|
+
throw new Error(`unsupported install runtime: ${runtime3}`);
|
|
503
3665
|
}
|
|
504
3666
|
const scope = one(options, "scope");
|
|
505
3667
|
if (scope && !["local", "user", "project"].includes(scope)) {
|
|
506
3668
|
throw new Error("--scope must be local, user, or project");
|
|
507
3669
|
}
|
|
508
|
-
output(installClient(
|
|
3670
|
+
output(installClient(runtime3, one(options, "identity") ?? "", {
|
|
509
3671
|
command: one(options, "command"),
|
|
510
3672
|
scope,
|
|
511
3673
|
instance: one(options, "instance"),
|
|
512
3674
|
token: one(options, "token"),
|
|
3675
|
+
enrollmentFile: one(options, "enrollment-file"),
|
|
3676
|
+
recoverLock: boolean(options, "recover-lock"),
|
|
513
3677
|
env: process.env
|
|
514
3678
|
}));
|
|
515
3679
|
return;
|
|
@@ -525,11 +3689,11 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
525
3689
|
if (command === "migrate") {
|
|
526
3690
|
const databaseUrl = process.env.AGENT_BRIDGE_DATABASE_URL;
|
|
527
3691
|
if (!databaseUrl) throw new Error("AGENT_BRIDGE_DATABASE_URL is required");
|
|
528
|
-
const pool = new
|
|
3692
|
+
const pool = new pg3.Pool({ connectionString: databaseUrl, max: 1 });
|
|
529
3693
|
try {
|
|
530
3694
|
const applied = await runMigrations(
|
|
531
3695
|
pool,
|
|
532
|
-
|
|
3696
|
+
fileURLToPath2(new URL("../sql/migrations", import.meta.url))
|
|
533
3697
|
);
|
|
534
3698
|
output({ status: "ok", applied });
|
|
535
3699
|
} finally {
|
|
@@ -537,19 +3701,99 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
537
3701
|
}
|
|
538
3702
|
return;
|
|
539
3703
|
}
|
|
3704
|
+
if (command === "reconcile-legacy-projects") {
|
|
3705
|
+
const databaseUrl = process.env.AGENT_BRIDGE_DATABASE_URL;
|
|
3706
|
+
if (!databaseUrl) throw new Error("AGENT_BRIDGE_DATABASE_URL is required");
|
|
3707
|
+
const pool = new pg3.Pool({ connectionString: databaseUrl, max: 1 });
|
|
3708
|
+
try {
|
|
3709
|
+
const directory = fileURLToPath2(new URL("../sql/migrations", import.meta.url));
|
|
3710
|
+
const migration = (await loadMigrationPlan(directory)).find((entry) => entry.version === 8);
|
|
3711
|
+
if (!migration || migration.name !== "message_projects") {
|
|
3712
|
+
throw new Error("migration 008_message_projects is missing from the migration plan");
|
|
3713
|
+
}
|
|
3714
|
+
output(await reconcileLegacyProjects(pool, {
|
|
3715
|
+
apply: boolean(options, "apply"),
|
|
3716
|
+
migrationChecksum: migration.checksum
|
|
3717
|
+
}));
|
|
3718
|
+
} finally {
|
|
3719
|
+
await pool.end();
|
|
3720
|
+
}
|
|
3721
|
+
return;
|
|
3722
|
+
}
|
|
540
3723
|
const config = configFor(options, command);
|
|
541
|
-
|
|
3724
|
+
validateCanonicalCommandOptions(raw, options, config);
|
|
3725
|
+
const assertedUnackedBy = one(options, "unacked-by");
|
|
3726
|
+
if (assertedUnackedBy !== void 0 && assertedUnackedBy !== config.principal.agent) throw new Error("--unacked-by must equal the configured principal");
|
|
3727
|
+
if (command === "capabilities") {
|
|
3728
|
+
validateRequest("capabilities", {});
|
|
3729
|
+
cliOutput("capabilities", capabilityDocument({ surface: "cli", provider: config.provider }));
|
|
3730
|
+
return;
|
|
3731
|
+
}
|
|
3732
|
+
let runtime2;
|
|
3733
|
+
try {
|
|
3734
|
+
runtime2 = await createClientRuntime(config, {
|
|
3735
|
+
autoSync: command !== "doctor" && command !== "status",
|
|
3736
|
+
initializationMode: command === "status" ? "passive" : "active"
|
|
3737
|
+
});
|
|
3738
|
+
} catch (error) {
|
|
3739
|
+
if (command !== "doctor" && command !== "status") throw error;
|
|
3740
|
+
const remoteInitialization = command === "doctor" && config.provider === "legacy-supabase";
|
|
3741
|
+
failedClientStatus(config, error, {
|
|
3742
|
+
component: remoteInitialization ? "remote" : "local-store",
|
|
3743
|
+
remoteAttempted: remoteInitialization
|
|
3744
|
+
});
|
|
3745
|
+
process.exitCode = 1;
|
|
3746
|
+
return;
|
|
3747
|
+
}
|
|
542
3748
|
try {
|
|
543
3749
|
if (command === "doctor" || command === "status") {
|
|
544
|
-
|
|
545
|
-
|
|
3750
|
+
validateRequest("client_status", {});
|
|
3751
|
+
const active = command === "doctor";
|
|
3752
|
+
try {
|
|
3753
|
+
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 };
|
|
3754
|
+
const remoteReachable = "remoteReachable" in diagnostics ? diagnostics.remoteReachable ?? null : active && config.provider === "legacy-supabase" ? true : null;
|
|
3755
|
+
const checks = [{
|
|
3756
|
+
name: "local-store",
|
|
3757
|
+
status: "ok",
|
|
3758
|
+
message: "local diagnostics are readable"
|
|
3759
|
+
}];
|
|
3760
|
+
if (config.provider !== "local") {
|
|
3761
|
+
const provider3 = config.provider === "gateway" ? "gateway" : "legacy provider";
|
|
3762
|
+
const remoteError2 = "remoteError" in diagnostics && typeof diagnostics.remoteError === "string" ? diagnostics.remoteError : void 0;
|
|
3763
|
+
checks.push({
|
|
3764
|
+
name: "remote",
|
|
3765
|
+
status: remoteReachable === false ? "degraded" : remoteError2 ? "failed" : remoteReachable === true ? "ok" : "unknown",
|
|
3766
|
+
message: remoteReachable === false ? `${provider3} is unreachable` : remoteError2 ? `${provider3} rejected diagnostics (${remoteError2})` : remoteReachable === true ? `${provider3} is reachable` : `${provider3} reachability has not been checked`
|
|
3767
|
+
});
|
|
3768
|
+
}
|
|
3769
|
+
const blocked = "outboxBlocked" in diagnostics ? Number(diagnostics.outboxBlocked ?? 0) : 0;
|
|
3770
|
+
checks.push({ name: "blocked-outbox", status: blocked > 0 ? "degraded" : "ok", message: blocked > 0 ? `${blocked} outbound message(s) require intervention` : "no blocked outbound messages" });
|
|
3771
|
+
const expired = Number(diagnostics.expiredLeases ?? 0);
|
|
3772
|
+
checks.push({ name: "expired-leases", status: expired > 0 ? "degraded" : "ok", message: expired > 0 ? `${expired} delivery lease(s) are expired` : "no expired delivery leases" });
|
|
3773
|
+
const dead = Number(diagnostics.dead ?? 0);
|
|
3774
|
+
checks.push({ name: "dead-deliveries", status: dead > 0 ? "degraded" : "ok", message: dead > 0 ? `${dead} dead delivery/deliveries are visible` : "no dead deliveries" });
|
|
3775
|
+
const status = checks.some((check) => check.status === "failed") ? "failed" : checks.some((check) => check.status === "degraded") ? "degraded" : checks.some((check) => check.status === "unknown") ? "unknown" : "ok";
|
|
3776
|
+
const remoteError = "remoteError" in diagnostics && typeof diagnostics.remoteError === "string";
|
|
3777
|
+
const connected = config.provider === "local" ? true : remoteReachable === true && !remoteError;
|
|
3778
|
+
const queue = { ...diagnostics };
|
|
3779
|
+
delete queue.syncLoopState;
|
|
3780
|
+
delete queue.syncLoopError;
|
|
3781
|
+
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 });
|
|
3782
|
+
if (active && status !== "ok") process.exitCode = status === "failed" ? 1 : 2;
|
|
3783
|
+
} catch (error) {
|
|
3784
|
+
failedClientStatus(config, error, { component: "local-store" });
|
|
3785
|
+
process.exitCode = 1;
|
|
3786
|
+
}
|
|
546
3787
|
return;
|
|
547
3788
|
}
|
|
548
3789
|
if (command === "pending") {
|
|
549
|
-
const diagnostics = await
|
|
550
|
-
|
|
3790
|
+
const diagnostics = await runtime2.store.diagnostics?.(config.principal, {
|
|
3791
|
+
mode: config.provider === "gateway" ? "probe" : "snapshot"
|
|
3792
|
+
});
|
|
3793
|
+
const page = await runtime2.service.history(config.principal, {
|
|
551
3794
|
limit: 1,
|
|
552
|
-
|
|
3795
|
+
project: one(options, "project"),
|
|
3796
|
+
receiptState: "unread"
|
|
553
3797
|
});
|
|
554
3798
|
const pendingDeliveries = diagnostics?.pending ?? 0;
|
|
555
3799
|
const retryingDeliveries = diagnostics?.retrying ?? 0;
|
|
@@ -557,55 +3801,91 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
557
3801
|
const deliveryAvailable = pendingDeliveries > 0 || retryingDeliveries > 0 && oldest <= Date.now();
|
|
558
3802
|
const unread = page.messages.length > 0;
|
|
559
3803
|
const available = deliveryAvailable || unread;
|
|
3804
|
+
const diagnosticsAuthoritative = !diagnostics || !("remoteReachable" in diagnostics) || diagnostics.remoteReachable === true;
|
|
3805
|
+
const pageAuthority = page;
|
|
3806
|
+
const historyAuthoritative = pageAuthority.acknowledgements !== "unknown" && pageAuthority.source !== "cache" && pageAuthority.stale !== true;
|
|
3807
|
+
const authoritative = diagnosticsAuthoritative && historyAuthoritative;
|
|
560
3808
|
output({
|
|
561
3809
|
available,
|
|
562
3810
|
unread,
|
|
563
3811
|
deliveryAvailable,
|
|
564
3812
|
pending: pendingDeliveries,
|
|
565
3813
|
retrying: retryingDeliveries,
|
|
566
|
-
authoritative
|
|
3814
|
+
authoritative,
|
|
3815
|
+
state: available ? "available" : authoritative ? "empty" : "unknown"
|
|
567
3816
|
});
|
|
568
|
-
if (!available) process.exitCode = 1;
|
|
3817
|
+
if (!available) process.exitCode = authoritative ? 1 : 2;
|
|
569
3818
|
return;
|
|
570
3819
|
}
|
|
571
3820
|
if (command === "send") {
|
|
572
|
-
|
|
3821
|
+
const message = draft(options, positionals);
|
|
3822
|
+
validateRequest("publish_message", { ...message, source: one(options, "source") });
|
|
3823
|
+
cliOutput("publish_message", await runtime2.service.publish(config.principal, message));
|
|
573
3824
|
return;
|
|
574
3825
|
}
|
|
575
3826
|
if (command === "join") {
|
|
576
|
-
|
|
3827
|
+
const input = validateRequest("heartbeat", { leaseMs: integer(options, "lease-ms", 6e4), runtimeType: one(options, "runtime"), capabilities: list(options, "capability") });
|
|
3828
|
+
cliOutput("heartbeat", await runtime2.service.heartbeat(config.principal, input));
|
|
577
3829
|
return;
|
|
578
3830
|
}
|
|
579
3831
|
if (command === "presence") {
|
|
580
|
-
|
|
3832
|
+
validateRequest("presence", {});
|
|
3833
|
+
cliOutput("presence", { agents: await runtime2.service.presence(config.principal) });
|
|
581
3834
|
return;
|
|
582
3835
|
}
|
|
583
3836
|
if (command === "sync") {
|
|
584
|
-
if (!
|
|
585
|
-
|
|
3837
|
+
if (!runtime2.store.sync) throw new Error("sync is available only with the gateway provider");
|
|
3838
|
+
const input = validateRequest("sync", {
|
|
586
3839
|
maxPush: integer(options, "max-push", integer(options, "limit", 100)),
|
|
587
3840
|
maxPages: integer(options, "max-pages", 20)
|
|
588
|
-
})
|
|
3841
|
+
});
|
|
3842
|
+
cliOutput("sync", await runtime2.store.sync(input));
|
|
589
3843
|
return;
|
|
590
3844
|
}
|
|
591
|
-
if (command === "history" || command === "inbox") {
|
|
592
|
-
const
|
|
3845
|
+
if (command === "history" || command === "inbox" || command === "sent") {
|
|
3846
|
+
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") });
|
|
3847
|
+
const page = await runtime2.service.history(config.principal, query);
|
|
593
3848
|
if (raw === "get") {
|
|
594
|
-
|
|
595
|
-
} else
|
|
3849
|
+
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) });
|
|
3850
|
+
} else cliOutput("history", page);
|
|
596
3851
|
return;
|
|
597
3852
|
}
|
|
598
3853
|
if (command === "acknowledge" || command === "ack" && one(options, "ids")) {
|
|
599
3854
|
const rawIds = list(options, "ids");
|
|
600
|
-
|
|
3855
|
+
validateRequest("record_receipt", { messageIds: rawIds });
|
|
3856
|
+
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
3857
|
config.principal,
|
|
602
3858
|
rawIds.map(legacyNumericMessageId)
|
|
603
3859
|
);
|
|
604
|
-
|
|
3860
|
+
cliOutput("record_receipt", { acknowledged, agent: config.principal.agent }, {
|
|
3861
|
+
command: raw,
|
|
3862
|
+
optionNames: Object.keys(options)
|
|
3863
|
+
});
|
|
605
3864
|
return;
|
|
606
3865
|
}
|
|
607
3866
|
if (command === "claim") {
|
|
608
|
-
|
|
3867
|
+
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) });
|
|
3868
|
+
cliOutput("claim_delivery", await runtime2.service.claim(config.principal, input));
|
|
3869
|
+
return;
|
|
3870
|
+
}
|
|
3871
|
+
if (command === "deliveries" || command === "dead-letters") {
|
|
3872
|
+
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") });
|
|
3873
|
+
cliOutput("list_deliveries", await runtime2.service.deliveries(config.principal, input));
|
|
3874
|
+
return;
|
|
3875
|
+
}
|
|
3876
|
+
if (command === "delivery-events") {
|
|
3877
|
+
const input = validateRequest("list_delivery_events", { deliveryId: one(options, "delivery-id") ?? positionals[0], cursor: one(options, "cursor"), limit: integer(options, "limit", 50) });
|
|
3878
|
+
cliOutput("list_delivery_events", await runtime2.service.deliveryEvents(config.principal, String(input.deliveryId), input));
|
|
3879
|
+
return;
|
|
3880
|
+
}
|
|
3881
|
+
if (command === "cancel") {
|
|
3882
|
+
const input = validateRequest("cancel_delivery", { deliveryId: one(options, "delivery-id") ?? positionals[0] });
|
|
3883
|
+
cliOutput("cancel_delivery", await runtime2.service.cancel(config.principal, String(input.deliveryId)));
|
|
3884
|
+
return;
|
|
3885
|
+
}
|
|
3886
|
+
if (command === "requeue") {
|
|
3887
|
+
const input = validateRequest("requeue_delivery", { deliveryId: one(options, "delivery-id") ?? positionals[0] });
|
|
3888
|
+
cliOutput("requeue_delivery", await runtime2.service.requeue(config.principal, String(input.deliveryId)));
|
|
609
3889
|
return;
|
|
610
3890
|
}
|
|
611
3891
|
const deliveryId = one(options, "delivery-id") ?? positionals[0];
|
|
@@ -614,15 +3894,19 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
614
3894
|
if (["extend", "ack", "nack"].includes(command)) throw new Error("--delivery-id and --lease-token are required");
|
|
615
3895
|
}
|
|
616
3896
|
if (command === "extend") {
|
|
617
|
-
|
|
3897
|
+
const input = validateRequest("extend_delivery", { deliveryId, leaseToken, leaseMs: integer(options, "lease-ms", 3e4) });
|
|
3898
|
+
cliOutput("extend_delivery", await runtime2.service.extend(config.principal, String(input.deliveryId), String(input.leaseToken), input.leaseMs));
|
|
618
3899
|
return;
|
|
619
3900
|
}
|
|
620
3901
|
if (command === "ack") {
|
|
621
|
-
|
|
3902
|
+
const input = validateRequest("acknowledge_delivery", { deliveryId, leaseToken });
|
|
3903
|
+
cliOutput("acknowledge_delivery", await runtime2.service.ack(config.principal, String(input.deliveryId), String(input.leaseToken)));
|
|
622
3904
|
return;
|
|
623
3905
|
}
|
|
624
3906
|
if (command === "nack") {
|
|
625
|
-
|
|
3907
|
+
const disposition = one(options, "disposition") ?? (boolean(options, "dead") ? "dead" : "retry");
|
|
3908
|
+
const input = validateRequest("negative_acknowledge_delivery", { deliveryId, leaseToken, error: one(options, "error") ?? "negative acknowledgment", disposition });
|
|
3909
|
+
cliOutput("negative_acknowledge_delivery", await runtime2.service.nack(config.principal, String(input.deliveryId), String(input.leaseToken), String(input.error), input.disposition));
|
|
626
3910
|
return;
|
|
627
3911
|
}
|
|
628
3912
|
if (command === "watch") {
|
|
@@ -630,15 +3914,21 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
630
3914
|
const baseInterval = Math.min(integer(options, "interval-ms", 1e3), 6e4);
|
|
631
3915
|
const maxInterval = Math.max(baseInterval, Math.min(integer(options, "max-interval-ms", 3e4), 3e5));
|
|
632
3916
|
let interval = baseInterval;
|
|
633
|
-
|
|
3917
|
+
const project = one(options, "project");
|
|
3918
|
+
const checkpointPath = watchCursorPath(config.cursorPath, project);
|
|
3919
|
+
let current = cursor(checkpointPath);
|
|
634
3920
|
let seen = 0;
|
|
635
3921
|
for (let i = 0; i < polls; i++) {
|
|
636
3922
|
try {
|
|
637
|
-
const page = await
|
|
3923
|
+
const page = await runtime2.service.history(config.principal, {
|
|
3924
|
+
cursor: current,
|
|
3925
|
+
limit: integer(options, "limit", 50),
|
|
3926
|
+
project
|
|
3927
|
+
});
|
|
638
3928
|
for (const message of page.messages) output(message);
|
|
639
3929
|
seen += page.messages.length;
|
|
640
3930
|
current = page.cursor;
|
|
641
|
-
saveCursor(
|
|
3931
|
+
saveCursor(checkpointPath, current);
|
|
642
3932
|
interval = page.messages.length ? baseInterval : Math.min(maxInterval, Math.max(baseInterval, interval * 2));
|
|
643
3933
|
} catch (error) {
|
|
644
3934
|
const status = error instanceof BridgeHttpError || error instanceof LegacySupabaseError ? error.status : void 0;
|
|
@@ -648,7 +3938,7 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
648
3938
|
}
|
|
649
3939
|
if (i + 1 < polls) {
|
|
650
3940
|
const jittered = Math.round(interval * (0.9 + Math.random() * 0.2));
|
|
651
|
-
await new Promise((
|
|
3941
|
+
await new Promise((resolve5) => setTimeout(resolve5, jittered));
|
|
652
3942
|
}
|
|
653
3943
|
}
|
|
654
3944
|
if (!seen && boolean(options, "json")) output({ status: "ok", messages: 0, cursor: current ?? null });
|
|
@@ -656,15 +3946,52 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
656
3946
|
}
|
|
657
3947
|
throw new Error(`Unknown command: ${raw}`);
|
|
658
3948
|
} finally {
|
|
659
|
-
await
|
|
3949
|
+
await runtime2.close();
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
function formatCliError(error, argv = process.argv.slice(2)) {
|
|
3953
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3954
|
+
if (argv[0] === "archive") {
|
|
3955
|
+
return JSON.stringify({
|
|
3956
|
+
schemaVersion: 1,
|
|
3957
|
+
status: "error",
|
|
3958
|
+
operation: argv[1] ?? null,
|
|
3959
|
+
error: {
|
|
3960
|
+
code: error instanceof ArchiveCommandError ? error.code : "ARCHIVE_COMMAND_ERROR",
|
|
3961
|
+
message: error instanceof ArchiveCommandError ? error.message : "archive command failed",
|
|
3962
|
+
...error instanceof ArchiveCommandError && error.details ? { details: error.details } : {}
|
|
3963
|
+
}
|
|
3964
|
+
}) + "\n";
|
|
660
3965
|
}
|
|
3966
|
+
if (argv[0] === "dr") {
|
|
3967
|
+
return JSON.stringify({
|
|
3968
|
+
schemaVersion: 1,
|
|
3969
|
+
status: "error",
|
|
3970
|
+
operation: argv[1] ?? null,
|
|
3971
|
+
error: {
|
|
3972
|
+
code: error instanceof NativeDrCommandError ? error.code : "DR_COMMAND_ERROR",
|
|
3973
|
+
message: error instanceof NativeDrCommandError ? error.message : "DR command failed",
|
|
3974
|
+
...error instanceof NativeDrCommandError && error.details ? { details: error.details } : {}
|
|
3975
|
+
}
|
|
3976
|
+
}) + "\n";
|
|
3977
|
+
}
|
|
3978
|
+
if (argv[0] === "owner") {
|
|
3979
|
+
return JSON.stringify({
|
|
3980
|
+
schemaVersion: 1,
|
|
3981
|
+
status: "error",
|
|
3982
|
+
operation: argv[1] ?? null,
|
|
3983
|
+
error: { code: "OWNER_COMMAND_ERROR", message }
|
|
3984
|
+
}) + "\n";
|
|
3985
|
+
}
|
|
3986
|
+
return `Error: ${message}
|
|
3987
|
+
`;
|
|
661
3988
|
}
|
|
662
3989
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) runCli().catch((error) => {
|
|
663
|
-
process.stderr.write(
|
|
664
|
-
`);
|
|
3990
|
+
process.stderr.write(formatCliError(error));
|
|
665
3991
|
process.exitCode = 1;
|
|
666
3992
|
});
|
|
667
3993
|
export {
|
|
3994
|
+
formatCliError,
|
|
668
3995
|
runCli
|
|
669
3996
|
};
|
|
670
3997
|
//# sourceMappingURL=cli.js.map
|