@creatornader/agent-bridge 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -4
- package/README.md +567 -55
- package/ROADMAP.md +114 -0
- package/SECURITY.md +90 -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 +3555 -214
- 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 +394 -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/decisions/0003-host-adapters-and-consumer-instance-keys.md +88 -0
- package/docs/ecosystem.md +86 -0
- package/docs/postmortems/2026-07-08-wrapper-source-drift.md +82 -0
- package/docs/troubleshooting.md +89 -0
- package/llms.txt +107 -6
- 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,82 +1,997 @@
|
|
|
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");
|
|
42
937
|
}
|
|
43
|
-
const
|
|
44
|
-
return process.platform === "darwin" ?
|
|
938
|
+
const home2 = env.HOME ?? homedir2();
|
|
939
|
+
return process.platform === "darwin" ? join2(home2, "Library", "Application Support", "Claude", "claude_desktop_config.json") : join2(home2, ".config", "Claude", "claude_desktop_config.json");
|
|
45
940
|
}
|
|
46
941
|
function safeComponent(value) {
|
|
47
942
|
const readable = value.replace(/[^A-Za-z0-9._-]+/g, "_").slice(0, 48) || "client";
|
|
48
|
-
const suffix =
|
|
943
|
+
const suffix = createHash2("sha256").update(value).digest("hex").slice(0, 12);
|
|
49
944
|
return `${readable}-${suffix}`;
|
|
50
945
|
}
|
|
51
|
-
function clientBackendConfigPath(
|
|
52
|
-
const
|
|
53
|
-
return
|
|
54
|
-
|
|
946
|
+
function clientBackendConfigPath(runtime2, instance, env) {
|
|
947
|
+
const home2 = env.HOME ?? homedir2();
|
|
948
|
+
return join2(
|
|
949
|
+
home2,
|
|
55
950
|
".agent-bridge",
|
|
56
951
|
"clients",
|
|
57
|
-
`${
|
|
952
|
+
`${runtime2}-${safeComponent(instance)}.config`
|
|
58
953
|
);
|
|
59
954
|
}
|
|
60
955
|
function replacePrivateFile(path, content) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
956
|
+
const directory = dirname2(path);
|
|
957
|
+
const privateRoot = dirname2(directory);
|
|
958
|
+
mkdirSync2(privateRoot, { recursive: true, mode: 448 });
|
|
959
|
+
securePrivatePath(privateRoot, "directory");
|
|
960
|
+
mkdirSync2(directory, { recursive: true, mode: 448 });
|
|
961
|
+
securePrivatePath(directory, "directory");
|
|
962
|
+
if (existsSync2(path)) verifyPrivatePathAccess(path, "file");
|
|
963
|
+
const temporary = `${path}.${process.pid}.${randomUUID2()}.tmp`;
|
|
964
|
+
const descriptor = openSync2(temporary, "wx", 384);
|
|
965
|
+
try {
|
|
966
|
+
writeFileSync2(descriptor, content);
|
|
967
|
+
fsyncSync2(descriptor);
|
|
968
|
+
} finally {
|
|
969
|
+
closeSync2(descriptor);
|
|
970
|
+
}
|
|
971
|
+
securePrivatePath(temporary, "file");
|
|
972
|
+
renameSync2(temporary, path);
|
|
973
|
+
verifyPrivatePathAccess(path, "file");
|
|
974
|
+
if (process.platform !== "win32") {
|
|
975
|
+
const descriptor2 = openSync2(directory, "r");
|
|
976
|
+
try {
|
|
977
|
+
fsyncSync2(descriptor2);
|
|
978
|
+
} finally {
|
|
979
|
+
closeSync2(descriptor2);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
function writeBackendConfig(runtime2, identity, instance, options) {
|
|
69
984
|
const env = options.env;
|
|
70
|
-
const
|
|
71
|
-
const sharedPath = env.AGENT_BRIDGE_CONFIG ??
|
|
985
|
+
const home2 = env.HOME ?? homedir2();
|
|
986
|
+
const sharedPath = env.AGENT_BRIDGE_CONFIG ?? join2(home2, ".agent-bridge", "config");
|
|
72
987
|
const shared = readClientConfigFile(sharedPath);
|
|
73
988
|
const configuredProvider = env.AGENT_BRIDGE_PROVIDER?.trim() || shared.AGENT_BRIDGE_PROVIDER?.trim();
|
|
74
989
|
const inferredGateway = !configuredProvider && Boolean(
|
|
75
990
|
env.AGENT_BRIDGE_TOKEN?.trim() || shared.AGENT_BRIDGE_TOKEN?.trim()
|
|
76
991
|
);
|
|
77
|
-
const
|
|
992
|
+
const provider3 = configuredProvider === "legacy" || configuredProvider === "supabase" ? "legacy-supabase" : configuredProvider ?? (inferredGateway ? "gateway" : void 0);
|
|
78
993
|
const clientToken = options.token?.trim() || env.AGENT_BRIDGE_CLIENT_TOKEN?.trim();
|
|
79
|
-
if (
|
|
994
|
+
if (provider3 === "gateway" && !clientToken) {
|
|
80
995
|
throw new Error(
|
|
81
996
|
"gateway client installation requires --token or AGENT_BRIDGE_CLIENT_TOKEN"
|
|
82
997
|
);
|
|
@@ -94,121 +1009,2213 @@ function writeBackendConfig(runtime, identity, instance, options) {
|
|
|
94
1009
|
AGENT_BRIDGE_TOKEN: resolved.provider === "gateway" ? resolved.credential : void 0,
|
|
95
1010
|
AGENT_BRIDGE_KEY: resolved.provider === "legacy-supabase" ? resolved.credential : void 0,
|
|
96
1011
|
AGENT_BRIDGE_DB: resolved.provider === "local" ? resolved.databasePath : void 0,
|
|
97
|
-
AGENT_BRIDGE_EDGE_DB: resolved.provider === "gateway" ? resolved.edgeDatabasePath : void 0
|
|
1012
|
+
AGENT_BRIDGE_EDGE_DB: resolved.provider === "gateway" ? resolved.edgeDatabasePath : void 0,
|
|
1013
|
+
AGENT_BRIDGE_CREDENTIAL_ID: options.binding?.credentialId,
|
|
1014
|
+
AGENT_BRIDGE_PRINCIPAL: options.binding?.principal,
|
|
1015
|
+
AGENT_BRIDGE_CLIENT_INSTANCE: options.binding?.instance
|
|
98
1016
|
};
|
|
99
1017
|
for (const value of Object.values(values)) {
|
|
100
1018
|
if (value?.includes("\n") || value?.includes("\r")) {
|
|
101
1019
|
throw new Error("client backend config values cannot contain newlines");
|
|
102
1020
|
}
|
|
103
1021
|
}
|
|
104
|
-
const path = clientBackendConfigPath(
|
|
105
|
-
replacePrivateFile(
|
|
106
|
-
path,
|
|
107
|
-
`${Object.entries(values).filter((entry) => Boolean(entry[1])).map(([key, value]) => `${key}=${value}`).join("\n")}
|
|
108
|
-
`
|
|
109
|
-
);
|
|
110
|
-
return path;
|
|
1022
|
+
const path = clientBackendConfigPath(runtime2, instance, env);
|
|
1023
|
+
replacePrivateFile(
|
|
1024
|
+
path,
|
|
1025
|
+
`${Object.entries(values).filter((entry) => Boolean(entry[1])).map(([key, value]) => `${key}=${value}`).join("\n")}
|
|
1026
|
+
`
|
|
1027
|
+
);
|
|
1028
|
+
return path;
|
|
1029
|
+
}
|
|
1030
|
+
function writeJsonConfig(path, identity, instance, backendConfigPath, launch) {
|
|
1031
|
+
let config = {};
|
|
1032
|
+
if (existsSync2(path)) {
|
|
1033
|
+
try {
|
|
1034
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
1035
|
+
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") throw new Error();
|
|
1036
|
+
config = parsed;
|
|
1037
|
+
} catch {
|
|
1038
|
+
throw new Error(`Claude Desktop config is not valid JSON: ${path}`);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
const currentServers = config.mcpServers;
|
|
1042
|
+
const mcpServers = currentServers && !Array.isArray(currentServers) && typeof currentServers === "object" ? currentServers : {};
|
|
1043
|
+
const next = {
|
|
1044
|
+
...config,
|
|
1045
|
+
mcpServers: {
|
|
1046
|
+
...mcpServers,
|
|
1047
|
+
"agent-bridge": {
|
|
1048
|
+
command: launch.command,
|
|
1049
|
+
args: launch.args,
|
|
1050
|
+
env: {
|
|
1051
|
+
AGENT_BRIDGE_AGENT: identity,
|
|
1052
|
+
AGENT_BRIDGE_INSTANCE: instance,
|
|
1053
|
+
AGENT_BRIDGE_CONFIG: backendConfigPath
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
|
|
1059
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
1060
|
+
writeFileSync2(temporary, `${JSON.stringify(next, null, 2)}
|
|
1061
|
+
`, { mode: 384 });
|
|
1062
|
+
renameSync2(temporary, path);
|
|
1063
|
+
}
|
|
1064
|
+
function assertEnrollmentConfiguration(enrollment2, assertions) {
|
|
1065
|
+
const input = enrollment2.input;
|
|
1066
|
+
const env = assertions.env;
|
|
1067
|
+
const home2 = env.HOME ?? homedir2();
|
|
1068
|
+
const shared = readClientConfigFile(
|
|
1069
|
+
env.AGENT_BRIDGE_CONFIG ?? join2(home2, ".agent-bridge", "config")
|
|
1070
|
+
);
|
|
1071
|
+
const conflicts = [
|
|
1072
|
+
["runtime", assertions.runtime, input.runtime],
|
|
1073
|
+
["identity", assertions.identity?.trim() || void 0, input.principal],
|
|
1074
|
+
["instance", assertions.instance?.trim() || void 0, input.instance],
|
|
1075
|
+
["AGENT_BRIDGE_AGENT", env.AGENT_BRIDGE_AGENT?.trim(), input.principal],
|
|
1076
|
+
["AGENT_BRIDGE_INSTANCE", env.AGENT_BRIDGE_INSTANCE?.trim(), input.instance],
|
|
1077
|
+
["AGENT_BRIDGE_URL", env.AGENT_BRIDGE_URL?.trim(), input.gatewayUrl],
|
|
1078
|
+
["AGENT_BRIDGE_WORKSPACE", env.AGENT_BRIDGE_WORKSPACE?.trim(), input.workspaceId],
|
|
1079
|
+
["shared AGENT_BRIDGE_URL", shared.AGENT_BRIDGE_URL?.trim(), input.gatewayUrl],
|
|
1080
|
+
["shared AGENT_BRIDGE_WORKSPACE", shared.AGENT_BRIDGE_WORKSPACE?.trim(), input.workspaceId]
|
|
1081
|
+
];
|
|
1082
|
+
for (const [name, actual, expected] of conflicts) {
|
|
1083
|
+
if (actual && actual !== expected) throw new Error(name + " conflicts with the enrollment file");
|
|
1084
|
+
}
|
|
1085
|
+
for (const [name, provider3] of [
|
|
1086
|
+
["AGENT_BRIDGE_PROVIDER", env.AGENT_BRIDGE_PROVIDER?.trim()],
|
|
1087
|
+
["shared AGENT_BRIDGE_PROVIDER", shared.AGENT_BRIDGE_PROVIDER?.trim()]
|
|
1088
|
+
]) {
|
|
1089
|
+
if (provider3 && provider3 !== "gateway") throw new Error(name + " conflicts with the enrollment file");
|
|
1090
|
+
}
|
|
1091
|
+
if (assertions.token) throw new Error("--token cannot be used with --enrollment-file");
|
|
1092
|
+
if (env.AGENT_BRIDGE_CLIENT_TOKEN || env.AGENT_BRIDGE_TOKEN) {
|
|
1093
|
+
throw new Error("token environment variables cannot be used with --enrollment-file");
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
function removeConsumedEnrollment(path, result, lock2, env) {
|
|
1097
|
+
try {
|
|
1098
|
+
const deletion = deleteEnrollmentFile(path, lock2, env);
|
|
1099
|
+
if (deletion === "missing") {
|
|
1100
|
+
return {
|
|
1101
|
+
...result,
|
|
1102
|
+
installed: true,
|
|
1103
|
+
enrollmentDeleted: false,
|
|
1104
|
+
enrollmentFile: path,
|
|
1105
|
+
enrollmentStatus: "consumed-file-missing"
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
if (deletion === "deleted-durability-unknown") {
|
|
1109
|
+
return {
|
|
1110
|
+
...result,
|
|
1111
|
+
installed: true,
|
|
1112
|
+
enrollmentDeleted: true,
|
|
1113
|
+
enrollmentStatus: "consumed-deletion-durability-unknown"
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
return {
|
|
1117
|
+
...result,
|
|
1118
|
+
installed: true,
|
|
1119
|
+
enrollmentDeleted: true,
|
|
1120
|
+
enrollmentStatus: "consumed"
|
|
1121
|
+
};
|
|
1122
|
+
} catch {
|
|
1123
|
+
return {
|
|
1124
|
+
...result,
|
|
1125
|
+
installed: true,
|
|
1126
|
+
enrollmentDeleted: false,
|
|
1127
|
+
enrollmentFile: path,
|
|
1128
|
+
enrollmentStatus: "consumed-file-retained"
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
function backendMatches(path, enrollment2, credentialId, requireToken) {
|
|
1133
|
+
if (!existsSync2(path)) return false;
|
|
1134
|
+
const config = readClientConfigFile(path);
|
|
1135
|
+
return config.AGENT_BRIDGE_PROVIDER === "gateway" && config.AGENT_BRIDGE_URL === enrollment2.input.gatewayUrl && config.AGENT_BRIDGE_WORKSPACE === enrollment2.input.workspaceId && config.AGENT_BRIDGE_CREDENTIAL_ID === credentialId && config.AGENT_BRIDGE_PRINCIPAL === enrollment2.input.principal && config.AGENT_BRIDGE_CLIENT_INSTANCE === enrollment2.input.instance && (!requireToken || config.AGENT_BRIDGE_TOKEN === enrollment2.token);
|
|
1136
|
+
}
|
|
1137
|
+
function desktopRegistrationState(enrollment2, backendConfigPath, launch, env) {
|
|
1138
|
+
const path = desktopConfigPath(env);
|
|
1139
|
+
if (!existsSync2(path)) return "absent";
|
|
1140
|
+
try {
|
|
1141
|
+
const config = JSON.parse(readFileSync2(path, "utf8"));
|
|
1142
|
+
const server = config?.mcpServers?.["agent-bridge"];
|
|
1143
|
+
if (!server) return "absent";
|
|
1144
|
+
const exactEnvironment = server?.env?.AGENT_BRIDGE_AGENT === enrollment2.input.principal && server?.env?.AGENT_BRIDGE_INSTANCE === enrollment2.input.instance && server?.env?.AGENT_BRIDGE_CONFIG === backendConfigPath;
|
|
1145
|
+
if (server?.command === "agent-bridge-mcp" && (server?.args === void 0 || Array.isArray(server.args) && server.args.length === 0) && exactEnvironment) return "legacy";
|
|
1146
|
+
return server?.command === launch.command && Array.isArray(server?.args) && server.args.length === launch.args.length && server.args.every((arg, index) => arg === launch.args[index]) && exactEnvironment ? "matching" : "conflict";
|
|
1147
|
+
} catch {
|
|
1148
|
+
return "conflict";
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
function repairLegacyDesktopRegistration(state, enrollment2, backendConfigPath, launch, env) {
|
|
1152
|
+
if (state !== "legacy") return state;
|
|
1153
|
+
writeJsonConfig(
|
|
1154
|
+
desktopConfigPath(env),
|
|
1155
|
+
enrollment2.input.principal,
|
|
1156
|
+
enrollment2.input.instance,
|
|
1157
|
+
backendConfigPath,
|
|
1158
|
+
launch
|
|
1159
|
+
);
|
|
1160
|
+
const repaired = desktopRegistrationState(enrollment2, backendConfigPath, launch, env);
|
|
1161
|
+
return repaired === "legacy" ? "conflict" : repaired;
|
|
1162
|
+
}
|
|
1163
|
+
function nativeRegistrationState(enrollment2, backendConfigPath, command, scope, execute) {
|
|
1164
|
+
const executable = enrollment2.input.runtime === "codex" ? "codex" : "claude";
|
|
1165
|
+
const args = enrollment2.input.runtime === "codex" ? ["mcp", "get", "agent-bridge", "--json"] : ["mcp", "get", "agent-bridge"];
|
|
1166
|
+
const result = execute(executable, args);
|
|
1167
|
+
if (result.error) throw new Error(enrollment2.input.runtime + " MCP inspection failed");
|
|
1168
|
+
if (result.status !== 0) {
|
|
1169
|
+
const output2 = (result.stdout + "\n" + result.stderr).trim();
|
|
1170
|
+
const notFound = enrollment2.input.runtime === "codex" ? output2 === "Error: No MCP server named 'agent-bridge' found." : output2 === 'No MCP server named "agent-bridge".' || /^No MCP server named "agent-bridge"\. Configured servers: [^\r\n]+$/.test(output2);
|
|
1171
|
+
if (notFound) return "absent";
|
|
1172
|
+
throw new Error(enrollment2.input.runtime + " MCP inspection failed");
|
|
1173
|
+
}
|
|
1174
|
+
if (enrollment2.input.runtime === "codex") {
|
|
1175
|
+
try {
|
|
1176
|
+
const server = JSON.parse(result.stdout);
|
|
1177
|
+
const transport = server?.transport;
|
|
1178
|
+
const environment = transport?.env;
|
|
1179
|
+
const exactEnvironment = environment && typeof environment === "object" && !Array.isArray(environment) && Object.keys(environment).sort().join(",") === "AGENT_BRIDGE_AGENT,AGENT_BRIDGE_CONFIG,AGENT_BRIDGE_INSTANCE";
|
|
1180
|
+
return server?.name === "agent-bridge" && server?.enabled === true && transport?.type === "stdio" && transport?.command === command && Array.isArray(transport?.args) && transport.args.length === 0 && exactEnvironment && transport?.env?.AGENT_BRIDGE_AGENT === enrollment2.input.principal && transport?.env?.AGENT_BRIDGE_INSTANCE === enrollment2.input.instance && transport?.env?.AGENT_BRIDGE_CONFIG === backendConfigPath ? "matching" : "conflict";
|
|
1181
|
+
} catch {
|
|
1182
|
+
return "conflict";
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
const lines = result.stdout.replace(/\r\n/g, "\n").split("\n");
|
|
1186
|
+
const scopeLine = {
|
|
1187
|
+
local: " Scope: Local config (private to you in this project)",
|
|
1188
|
+
user: " Scope: User config (available in all your projects)",
|
|
1189
|
+
project: " Scope: Project config (shared via .mcp.json)"
|
|
1190
|
+
}[scope];
|
|
1191
|
+
const expectedPrefix = [
|
|
1192
|
+
"agent-bridge:",
|
|
1193
|
+
scopeLine
|
|
1194
|
+
];
|
|
1195
|
+
if (lines[0] !== expectedPrefix[0] || lines[1] !== expectedPrefix[1] || lines[3] !== " Type: stdio" || lines[4] !== " Command: " + command || lines[5] !== " Args:" || lines[6] !== " Environment:") return "conflict";
|
|
1196
|
+
const environmentLines = [];
|
|
1197
|
+
let index = 7;
|
|
1198
|
+
while (index < lines.length && lines[index]?.startsWith(" ")) {
|
|
1199
|
+
environmentLines.push(lines[index]);
|
|
1200
|
+
index += 1;
|
|
1201
|
+
}
|
|
1202
|
+
const expectedEnvironment = [
|
|
1203
|
+
" AGENT_BRIDGE_AGENT=" + enrollment2.input.principal,
|
|
1204
|
+
" AGENT_BRIDGE_INSTANCE=" + enrollment2.input.instance,
|
|
1205
|
+
" AGENT_BRIDGE_CONFIG=" + backendConfigPath
|
|
1206
|
+
].sort();
|
|
1207
|
+
if (environmentLines.sort().join("\n") !== expectedEnvironment.join("\n")) return "conflict";
|
|
1208
|
+
while (lines[index] === "") index += 1;
|
|
1209
|
+
if (lines[index] !== "To remove this server, run: claude mcp remove agent-bridge -s " + scope) {
|
|
1210
|
+
return "conflict";
|
|
1211
|
+
}
|
|
1212
|
+
if (lines.slice(index + 1).some((line) => line !== "")) return "conflict";
|
|
1213
|
+
return "matching";
|
|
1214
|
+
}
|
|
1215
|
+
function enrolledInstallLocked(runtimeAssertion, identityAssertion, options, execute, lock2) {
|
|
1216
|
+
const env = options.env ?? process.env;
|
|
1217
|
+
let enrollment2 = readEnrollment(options.enrollmentFile, env);
|
|
1218
|
+
assertEnrollmentConfiguration(enrollment2, {
|
|
1219
|
+
runtime: runtimeAssertion,
|
|
1220
|
+
identity: identityAssertion,
|
|
1221
|
+
instance: options.instance,
|
|
1222
|
+
token: options.token,
|
|
1223
|
+
env
|
|
1224
|
+
});
|
|
1225
|
+
const runtime2 = enrollment2.input.runtime;
|
|
1226
|
+
const identity = enrollment2.input.principal;
|
|
1227
|
+
const expectedPath = clientBackendConfigPath(runtime2, enrollment2.input.instance, env);
|
|
1228
|
+
const command = options.command?.trim() || "agent-bridge-mcp";
|
|
1229
|
+
const desktopLaunch = runtime2 === "claude-desktop" ? desktopLaunchContract(options.command, env) : void 0;
|
|
1230
|
+
const scope = options.scope ?? "user";
|
|
1231
|
+
const baseResult = {
|
|
1232
|
+
runtime: runtime2,
|
|
1233
|
+
identity,
|
|
1234
|
+
instance: enrollment2.input.instance,
|
|
1235
|
+
method: runtime2 === "claude-desktop" ? "json-config" : "native-cli",
|
|
1236
|
+
configPath: runtime2 === "claude-desktop" ? desktopConfigPath(env) : void 0,
|
|
1237
|
+
backendConfigPath: expectedPath,
|
|
1238
|
+
restartRequired: true
|
|
1239
|
+
};
|
|
1240
|
+
if (enrollment2.state === "consumed") {
|
|
1241
|
+
if (!enrollment2.result || !backendMatches(expectedPath, enrollment2, enrollment2.result.credentialId, false)) {
|
|
1242
|
+
throw new Error("consumed enrollment no longer matches its client backend");
|
|
1243
|
+
}
|
|
1244
|
+
const inspected = runtime2 === "claude-desktop" ? desktopRegistrationState(enrollment2, expectedPath, desktopLaunch, env) : nativeRegistrationState(enrollment2, expectedPath, command, scope, execute);
|
|
1245
|
+
const registration = runtime2 === "claude-desktop" ? repairLegacyDesktopRegistration(
|
|
1246
|
+
inspected,
|
|
1247
|
+
enrollment2,
|
|
1248
|
+
expectedPath,
|
|
1249
|
+
desktopLaunch,
|
|
1250
|
+
env
|
|
1251
|
+
) : inspected;
|
|
1252
|
+
if (registration !== "matching") {
|
|
1253
|
+
throw new Error("consumed enrollment no longer matches its MCP registration");
|
|
1254
|
+
}
|
|
1255
|
+
return removeConsumedEnrollment(options.enrollmentFile, baseResult, lock2, env);
|
|
1256
|
+
}
|
|
1257
|
+
if (enrollment2.state === "pending") {
|
|
1258
|
+
throw new Error("enrollment is pending; resume the owner command first");
|
|
1259
|
+
}
|
|
1260
|
+
const recovering = enrollment2.state === "consuming";
|
|
1261
|
+
if (!recovering && enrollment2.operation === "provision" && existsSync2(expectedPath)) {
|
|
1262
|
+
throw new Error("provision enrollment would replace an existing client backend file");
|
|
1263
|
+
}
|
|
1264
|
+
if (!recovering && enrollment2.operation === "provision") {
|
|
1265
|
+
const registration = runtime2 === "claude-desktop" ? desktopRegistrationState(enrollment2, expectedPath, desktopLaunch, env) : nativeRegistrationState(enrollment2, expectedPath, command, scope, execute);
|
|
1266
|
+
if (registration !== "absent") {
|
|
1267
|
+
throw new Error("provision enrollment would replace an existing MCP registration");
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
if (enrollment2.state === "ready") {
|
|
1271
|
+
enrollment2 = transitionEnrollment(options.enrollmentFile, enrollment2, "consuming", {}, env, lock2);
|
|
1272
|
+
}
|
|
1273
|
+
if (enrollment2.state !== "consuming" || !enrollment2.token) {
|
|
1274
|
+
throw new Error("enrollment is not ready for installation");
|
|
1275
|
+
}
|
|
1276
|
+
const derivedEnv = {
|
|
1277
|
+
...env,
|
|
1278
|
+
AGENT_BRIDGE_PROVIDER: "gateway",
|
|
1279
|
+
AGENT_BRIDGE_URL: enrollment2.input.gatewayUrl,
|
|
1280
|
+
AGENT_BRIDGE_WORKSPACE: enrollment2.input.workspaceId,
|
|
1281
|
+
AGENT_BRIDGE_AGENT: void 0,
|
|
1282
|
+
AGENT_BRIDGE_INSTANCE: void 0,
|
|
1283
|
+
AGENT_BRIDGE_CLIENT_TOKEN: void 0
|
|
1284
|
+
};
|
|
1285
|
+
let result;
|
|
1286
|
+
let sideEffectsCompleted = false;
|
|
1287
|
+
try {
|
|
1288
|
+
if (enrollment2.operation === "rotate") {
|
|
1289
|
+
if (!enrollment2.input.credentialId || !enrollment2.result) {
|
|
1290
|
+
throw new Error("rotation enrollment lacks credential lineage");
|
|
1291
|
+
}
|
|
1292
|
+
const predecessorMatches = backendMatches(
|
|
1293
|
+
expectedPath,
|
|
1294
|
+
enrollment2,
|
|
1295
|
+
enrollment2.input.credentialId,
|
|
1296
|
+
false
|
|
1297
|
+
);
|
|
1298
|
+
const successorMatches = recovering && backendMatches(
|
|
1299
|
+
expectedPath,
|
|
1300
|
+
enrollment2,
|
|
1301
|
+
enrollment2.result.credentialId,
|
|
1302
|
+
true
|
|
1303
|
+
);
|
|
1304
|
+
if (!predecessorMatches && !successorMatches) {
|
|
1305
|
+
throw new Error("rotation requires an exactly bound predecessor or successor backend");
|
|
1306
|
+
}
|
|
1307
|
+
const inspected = runtime2 === "claude-desktop" ? desktopRegistrationState(enrollment2, expectedPath, desktopLaunch, env) : nativeRegistrationState(enrollment2, expectedPath, command, scope, execute);
|
|
1308
|
+
const registration = runtime2 === "claude-desktop" ? repairLegacyDesktopRegistration(
|
|
1309
|
+
inspected,
|
|
1310
|
+
enrollment2,
|
|
1311
|
+
expectedPath,
|
|
1312
|
+
desktopLaunch,
|
|
1313
|
+
env
|
|
1314
|
+
) : inspected;
|
|
1315
|
+
if (registration !== "matching") {
|
|
1316
|
+
throw new Error("rotation requires the exact existing MCP registration");
|
|
1317
|
+
}
|
|
1318
|
+
if (predecessorMatches) {
|
|
1319
|
+
const previous = readFileSync2(expectedPath);
|
|
1320
|
+
try {
|
|
1321
|
+
writeBackendConfig(runtime2, identity, enrollment2.input.instance, {
|
|
1322
|
+
token: enrollment2.token,
|
|
1323
|
+
env: derivedEnv,
|
|
1324
|
+
binding: {
|
|
1325
|
+
credentialId: enrollment2.result.credentialId,
|
|
1326
|
+
principal: identity,
|
|
1327
|
+
instance: enrollment2.input.instance
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
replacePrivateFile(expectedPath, previous);
|
|
1332
|
+
throw error;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
result = baseResult;
|
|
1336
|
+
sideEffectsCompleted = true;
|
|
1337
|
+
} else {
|
|
1338
|
+
if (recovering && existsSync2(expectedPath) && !backendMatches(
|
|
1339
|
+
expectedPath,
|
|
1340
|
+
enrollment2,
|
|
1341
|
+
enrollment2.result.credentialId,
|
|
1342
|
+
true
|
|
1343
|
+
)) {
|
|
1344
|
+
throw new Error("consuming enrollment conflicts with its client backend file");
|
|
1345
|
+
}
|
|
1346
|
+
const inspected = recovering ? runtime2 === "claude-desktop" ? desktopRegistrationState(enrollment2, expectedPath, desktopLaunch, env) : nativeRegistrationState(enrollment2, expectedPath, command, scope, execute) : "absent";
|
|
1347
|
+
const registration = recovering && runtime2 === "claude-desktop" ? repairLegacyDesktopRegistration(
|
|
1348
|
+
inspected,
|
|
1349
|
+
enrollment2,
|
|
1350
|
+
expectedPath,
|
|
1351
|
+
desktopLaunch,
|
|
1352
|
+
env
|
|
1353
|
+
) : inspected;
|
|
1354
|
+
if (recovering && registration === "conflict") {
|
|
1355
|
+
throw new Error("consuming enrollment conflicts with the registered MCP server");
|
|
1356
|
+
}
|
|
1357
|
+
if (recovering && backendMatches(
|
|
1358
|
+
expectedPath,
|
|
1359
|
+
enrollment2,
|
|
1360
|
+
enrollment2.result.credentialId,
|
|
1361
|
+
true
|
|
1362
|
+
) && registration === "matching") {
|
|
1363
|
+
result = baseResult;
|
|
1364
|
+
} else {
|
|
1365
|
+
result = installClient(runtime2, identity, {
|
|
1366
|
+
command: runtime2 === "claude-desktop" ? options.command : command,
|
|
1367
|
+
scope: options.scope,
|
|
1368
|
+
instance: enrollment2.input.instance,
|
|
1369
|
+
token: enrollment2.token,
|
|
1370
|
+
backendBinding: {
|
|
1371
|
+
credentialId: enrollment2.result.credentialId,
|
|
1372
|
+
principal: identity,
|
|
1373
|
+
instance: enrollment2.input.instance
|
|
1374
|
+
},
|
|
1375
|
+
env: derivedEnv
|
|
1376
|
+
}, execute);
|
|
1377
|
+
sideEffectsCompleted = true;
|
|
1378
|
+
const verified = runtime2 === "claude-desktop" ? desktopRegistrationState(enrollment2, expectedPath, desktopLaunch, env) : nativeRegistrationState(enrollment2, expectedPath, command, scope, execute);
|
|
1379
|
+
if (verified !== "matching") throw new Error("MCP registration verification failed");
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
if (!recovering && !sideEffectsCompleted) {
|
|
1384
|
+
transitionEnrollment(options.enrollmentFile, enrollment2, "ready", {}, env, lock2);
|
|
1385
|
+
}
|
|
1386
|
+
throw error;
|
|
1387
|
+
}
|
|
1388
|
+
enrollment2 = transitionEnrollment(
|
|
1389
|
+
options.enrollmentFile,
|
|
1390
|
+
enrollment2,
|
|
1391
|
+
"consumed",
|
|
1392
|
+
{ token: null },
|
|
1393
|
+
env,
|
|
1394
|
+
lock2
|
|
1395
|
+
);
|
|
1396
|
+
return removeConsumedEnrollment(options.enrollmentFile, result, lock2, env);
|
|
1397
|
+
}
|
|
1398
|
+
function enrolledInstall(runtime2, identity, options, execute) {
|
|
1399
|
+
const env = options.env ?? process.env;
|
|
1400
|
+
if (options.recoverLock) recoverEnrollmentLock(options.enrollmentFile, env);
|
|
1401
|
+
const lock2 = acquireEnrollmentLock(options.enrollmentFile, env);
|
|
1402
|
+
let result;
|
|
1403
|
+
try {
|
|
1404
|
+
result = enrolledInstallLocked(runtime2, identity, options, execute, lock2);
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
if (!lock2.released) {
|
|
1407
|
+
try {
|
|
1408
|
+
releaseEnrollmentLock(lock2);
|
|
1409
|
+
} catch {
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
throw error;
|
|
1413
|
+
}
|
|
1414
|
+
let lockReleaseStatus;
|
|
1415
|
+
try {
|
|
1416
|
+
const release = releaseEnrollmentLock(lock2);
|
|
1417
|
+
lockReleaseStatus = release === "released" ? "released" : "durability-unknown";
|
|
1418
|
+
} catch {
|
|
1419
|
+
lockReleaseStatus = existsSync2(lock2.lockPath) ? "retained" : "durability-unknown";
|
|
1420
|
+
}
|
|
1421
|
+
return { ...result, lockReleaseStatus };
|
|
1422
|
+
}
|
|
1423
|
+
function installClient(runtime2, identity, options = {}, execute = (command, args) => spawnSync(command, args, { encoding: "utf8" })) {
|
|
1424
|
+
if (options.enrollmentFile) {
|
|
1425
|
+
return enrolledInstall(runtime2, identity, {
|
|
1426
|
+
...options,
|
|
1427
|
+
enrollmentFile: options.enrollmentFile
|
|
1428
|
+
}, execute);
|
|
1429
|
+
}
|
|
1430
|
+
if (!runtime2) throw new Error("install runtime is required");
|
|
1431
|
+
const normalizedIdentity = identity.trim();
|
|
1432
|
+
if (!normalizedIdentity || normalizedIdentity.length > 128) throw new Error("--identity is required");
|
|
1433
|
+
const instance = options.instance?.trim() || `${runtime2}-${randomUUID2()}`;
|
|
1434
|
+
if (instance.length > 128) throw new Error("--instance exceeds 128 characters");
|
|
1435
|
+
if (options.scope && !["local", "user", "project"].includes(options.scope)) {
|
|
1436
|
+
throw new Error("scope must be local, user, or project");
|
|
1437
|
+
}
|
|
1438
|
+
const command = options.command?.trim() || "agent-bridge-mcp";
|
|
1439
|
+
const env = options.env ?? process.env;
|
|
1440
|
+
const desktopLaunch = runtime2 === "claude-desktop" ? desktopLaunchContract(options.command, env) : void 0;
|
|
1441
|
+
const expectedPath = clientBackendConfigPath(runtime2, instance, env);
|
|
1442
|
+
const previous = existsSync2(expectedPath) ? readFileSync2(expectedPath) : void 0;
|
|
1443
|
+
try {
|
|
1444
|
+
const backendConfigPath = writeBackendConfig(runtime2, normalizedIdentity, instance, {
|
|
1445
|
+
token: options.token,
|
|
1446
|
+
env,
|
|
1447
|
+
binding: options.backendBinding
|
|
1448
|
+
});
|
|
1449
|
+
if (runtime2 === "claude-desktop") {
|
|
1450
|
+
const path = desktopConfigPath(env);
|
|
1451
|
+
writeJsonConfig(path, normalizedIdentity, instance, backendConfigPath, desktopLaunch);
|
|
1452
|
+
return { runtime: runtime2, identity: normalizedIdentity, instance, method: "json-config", configPath: path, backendConfigPath, restartRequired: true };
|
|
1453
|
+
}
|
|
1454
|
+
const executable = runtime2 === "codex" ? "codex" : "claude";
|
|
1455
|
+
const args = runtime2 === "codex" ? [
|
|
1456
|
+
"mcp",
|
|
1457
|
+
"add",
|
|
1458
|
+
"agent-bridge",
|
|
1459
|
+
"--env",
|
|
1460
|
+
`AGENT_BRIDGE_AGENT=${normalizedIdentity}`,
|
|
1461
|
+
"--env",
|
|
1462
|
+
`AGENT_BRIDGE_INSTANCE=${instance}`,
|
|
1463
|
+
"--env",
|
|
1464
|
+
`AGENT_BRIDGE_CONFIG=${backendConfigPath}`,
|
|
1465
|
+
"--",
|
|
1466
|
+
command
|
|
1467
|
+
] : [
|
|
1468
|
+
"mcp",
|
|
1469
|
+
"add",
|
|
1470
|
+
"--scope",
|
|
1471
|
+
options.scope ?? "user",
|
|
1472
|
+
"agent-bridge",
|
|
1473
|
+
"-e",
|
|
1474
|
+
`AGENT_BRIDGE_AGENT=${normalizedIdentity}`,
|
|
1475
|
+
"-e",
|
|
1476
|
+
`AGENT_BRIDGE_INSTANCE=${instance}`,
|
|
1477
|
+
"-e",
|
|
1478
|
+
`AGENT_BRIDGE_CONFIG=${backendConfigPath}`,
|
|
1479
|
+
"--",
|
|
1480
|
+
command
|
|
1481
|
+
];
|
|
1482
|
+
const result = execute(executable, args);
|
|
1483
|
+
if (result.error || result.status !== 0) {
|
|
1484
|
+
const detail = result.stderr?.trim() || result.error?.message || `exit ${result.status}`;
|
|
1485
|
+
throw new Error(`${runtime2} MCP installation failed: ${detail}`);
|
|
1486
|
+
}
|
|
1487
|
+
return { runtime: runtime2, identity: normalizedIdentity, instance, method: "native-cli", backendConfigPath, restartRequired: true };
|
|
1488
|
+
} catch (error) {
|
|
1489
|
+
try {
|
|
1490
|
+
if (previous) replacePrivateFile(expectedPath, previous);
|
|
1491
|
+
else rmSync2(expectedPath, { force: true });
|
|
1492
|
+
} catch (rollbackError) {
|
|
1493
|
+
const detail = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
|
|
1494
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}; client config rollback failed: ${detail}`);
|
|
1495
|
+
}
|
|
1496
|
+
throw error;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// src/owner-control.ts
|
|
1501
|
+
import { randomBytes, randomUUID as randomUUID3 } from "crypto";
|
|
1502
|
+
import pg from "pg";
|
|
1503
|
+
var OwnerDatabaseFailure = class extends Error {
|
|
1504
|
+
constructor(driverCode) {
|
|
1505
|
+
super("owner database operation failed");
|
|
1506
|
+
this.driverCode = driverCode;
|
|
1507
|
+
}
|
|
1508
|
+
};
|
|
1509
|
+
async function ownerQuery(db, sql, values) {
|
|
1510
|
+
try {
|
|
1511
|
+
return await db.query(sql, values);
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code ?? "") : "";
|
|
1514
|
+
throw new OwnerDatabaseFailure(code);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
var defaultDependencies = {
|
|
1518
|
+
connect(databaseUrl) {
|
|
1519
|
+
const pool = new pg.Pool({
|
|
1520
|
+
connectionString: databaseUrl,
|
|
1521
|
+
max: 1,
|
|
1522
|
+
connectionTimeoutMillis: 1e4,
|
|
1523
|
+
query_timeout: 3e4
|
|
1524
|
+
});
|
|
1525
|
+
return { db: pool, close: () => pool.end() };
|
|
1526
|
+
},
|
|
1527
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
1528
|
+
requestId: () => randomUUID3(),
|
|
1529
|
+
token: () => randomBytes(32).toString("base64url"),
|
|
1530
|
+
instance: (runtime2) => runtime2 + "-" + randomUUID3()
|
|
1531
|
+
};
|
|
1532
|
+
function stringOption(options, name) {
|
|
1533
|
+
const value = options[name];
|
|
1534
|
+
return typeof value === "string" ? value : void 0;
|
|
1535
|
+
}
|
|
1536
|
+
function required(options, name) {
|
|
1537
|
+
const value = stringOption(options, name);
|
|
1538
|
+
if (!value) throw new Error("--" + name + " is required");
|
|
1539
|
+
return value;
|
|
1540
|
+
}
|
|
1541
|
+
function selectWithEnvironment(options, name, env, environmentName, requiredValue) {
|
|
1542
|
+
const explicit = stringOption(options, name);
|
|
1543
|
+
const environment = env[environmentName]?.trim() || void 0;
|
|
1544
|
+
if (explicit && environment && explicit !== environment) {
|
|
1545
|
+
throw new Error("--" + name + " conflicts with " + environmentName);
|
|
1546
|
+
}
|
|
1547
|
+
const selected = explicit ?? environment;
|
|
1548
|
+
if (requiredValue && !selected) {
|
|
1549
|
+
throw new Error("--" + name + " or " + environmentName + " is required");
|
|
1550
|
+
}
|
|
1551
|
+
return selected;
|
|
1552
|
+
}
|
|
1553
|
+
function timestamp(options, name) {
|
|
1554
|
+
const value = stringOption(options, name) ?? null;
|
|
1555
|
+
return canonicalTimestamp(value, "--" + name);
|
|
1556
|
+
}
|
|
1557
|
+
function installRuntime(options) {
|
|
1558
|
+
const value = required(options, "runtime");
|
|
1559
|
+
if (value !== "codex" && value !== "claude-code" && value !== "claude-desktop") {
|
|
1560
|
+
throw new Error("--runtime must be codex, claude-code, or claude-desktop");
|
|
1561
|
+
}
|
|
1562
|
+
return value;
|
|
1563
|
+
}
|
|
1564
|
+
function scopeSet(options) {
|
|
1565
|
+
const value = required(options, "scope-set");
|
|
1566
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(value)) throw new Error("--scope-set is invalid");
|
|
1567
|
+
return value;
|
|
1568
|
+
}
|
|
1569
|
+
function uuidValue(value, label) {
|
|
1570
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) {
|
|
1571
|
+
throw new Error(label + " must be a UUID");
|
|
1572
|
+
}
|
|
1573
|
+
return value;
|
|
1574
|
+
}
|
|
1575
|
+
function uuidOption(options, name) {
|
|
1576
|
+
return uuidValue(required(options, name), "--" + name);
|
|
1577
|
+
}
|
|
1578
|
+
var OWNER_OPTIONS = {
|
|
1579
|
+
provision: /* @__PURE__ */ new Set([
|
|
1580
|
+
"workspace",
|
|
1581
|
+
"workspace-name",
|
|
1582
|
+
"identity",
|
|
1583
|
+
"runtime",
|
|
1584
|
+
"instance",
|
|
1585
|
+
"gateway-url",
|
|
1586
|
+
"scope-set",
|
|
1587
|
+
"display-name",
|
|
1588
|
+
"runtime-type",
|
|
1589
|
+
"label",
|
|
1590
|
+
"expires-at",
|
|
1591
|
+
"request-id",
|
|
1592
|
+
"enrollment-file",
|
|
1593
|
+
"recover-lock"
|
|
1594
|
+
]),
|
|
1595
|
+
rotate: /* @__PURE__ */ new Set([
|
|
1596
|
+
"workspace",
|
|
1597
|
+
"identity",
|
|
1598
|
+
"runtime",
|
|
1599
|
+
"instance",
|
|
1600
|
+
"gateway-url",
|
|
1601
|
+
"scope-set",
|
|
1602
|
+
"runtime-type",
|
|
1603
|
+
"label",
|
|
1604
|
+
"expires-at",
|
|
1605
|
+
"grace-until",
|
|
1606
|
+
"invalidate-immediately",
|
|
1607
|
+
"credential-id",
|
|
1608
|
+
"request-id",
|
|
1609
|
+
"enrollment-file",
|
|
1610
|
+
"recover-lock"
|
|
1611
|
+
]),
|
|
1612
|
+
inventory: /* @__PURE__ */ new Set(["workspace", "cursor", "limit"]),
|
|
1613
|
+
revoke: /* @__PURE__ */ new Set(["credential-id", "reason", "request-id"])
|
|
1614
|
+
};
|
|
1615
|
+
function validateOwnerOptions(subcommand, options) {
|
|
1616
|
+
if (stringOption(options, "resume")) {
|
|
1617
|
+
const override = Object.keys(options).find((key) => key !== "resume" && key !== "recover-lock");
|
|
1618
|
+
if (override) throw new Error("--" + override + " cannot be used with --resume");
|
|
1619
|
+
if (subcommand !== "provision" && subcommand !== "rotate") {
|
|
1620
|
+
throw new Error("--resume is valid only for provision or rotate");
|
|
1621
|
+
}
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
const allowed = OWNER_OPTIONS[subcommand];
|
|
1625
|
+
const invalid = Object.keys(options).find((key) => !allowed?.has(key));
|
|
1626
|
+
if (invalid) throw new Error("--" + invalid + " is not valid for owner " + subcommand);
|
|
1627
|
+
}
|
|
1628
|
+
function requestId(options, dependencies) {
|
|
1629
|
+
return stringOption(options, "request-id") ? uuidOption(options, "request-id") : dependencies.requestId();
|
|
1630
|
+
}
|
|
1631
|
+
function initialEnrollment(operation, options, env, dependencies) {
|
|
1632
|
+
const request = requestId(options, dependencies);
|
|
1633
|
+
const runtime2 = installRuntime(options);
|
|
1634
|
+
const gatewayUrl = canonicalGatewayUrl(
|
|
1635
|
+
selectWithEnvironment(options, "gateway-url", env, "AGENT_BRIDGE_URL", true),
|
|
1636
|
+
"--gateway-url"
|
|
1637
|
+
);
|
|
1638
|
+
const workspaceId = required(options, "workspace");
|
|
1639
|
+
const principal = required(options, "identity");
|
|
1640
|
+
const explicitInstance = stringOption(options, "instance");
|
|
1641
|
+
const instance = operation === "rotate" ? explicitInstance ?? (() => {
|
|
1642
|
+
throw new Error("--instance is required for rotation");
|
|
1643
|
+
})() : explicitInstance ?? dependencies.instance(runtime2);
|
|
1644
|
+
const graceUntil = timestamp(options, "grace-until");
|
|
1645
|
+
const invalidateImmediately = options["invalidate-immediately"] === true || options["invalidate-immediately"] === "true";
|
|
1646
|
+
if (operation === "rotate" && graceUntil === null === !invalidateImmediately) {
|
|
1647
|
+
throw new Error("rotation requires exactly one of --grace-until or --invalidate-immediately");
|
|
1648
|
+
}
|
|
1649
|
+
if (operation === "provision" && (graceUntil !== null || invalidateImmediately)) {
|
|
1650
|
+
throw new Error("provision does not accept rotation invalidation options");
|
|
1651
|
+
}
|
|
1652
|
+
const input = {
|
|
1653
|
+
gatewayUrl,
|
|
1654
|
+
workspaceId,
|
|
1655
|
+
principal,
|
|
1656
|
+
runtime: runtime2,
|
|
1657
|
+
instance,
|
|
1658
|
+
credentialId: operation === "rotate" ? uuidOption(options, "credential-id") : null,
|
|
1659
|
+
workspaceName: operation === "provision" ? required(options, "workspace-name") : null,
|
|
1660
|
+
displayName: stringOption(options, "display-name") ?? null,
|
|
1661
|
+
runtimeType: stringOption(options, "runtime-type") ?? runtime2,
|
|
1662
|
+
label: stringOption(options, "label") ?? null,
|
|
1663
|
+
scopeSetName: scopeSet(options),
|
|
1664
|
+
expiresAt: timestamp(options, "expires-at"),
|
|
1665
|
+
graceUntil,
|
|
1666
|
+
invalidateImmediately
|
|
1667
|
+
};
|
|
1668
|
+
if (operation === "rotate" && input.displayName !== null) {
|
|
1669
|
+
throw new Error("rotate does not accept --display-name");
|
|
1670
|
+
}
|
|
1671
|
+
const enrollment2 = {
|
|
1672
|
+
schema: "agent-bridge.enrollment",
|
|
1673
|
+
version: 1,
|
|
1674
|
+
provider: "gateway",
|
|
1675
|
+
revision: 0,
|
|
1676
|
+
state: "pending",
|
|
1677
|
+
operation,
|
|
1678
|
+
requestId: request,
|
|
1679
|
+
createdAt: dependencies.now().toISOString(),
|
|
1680
|
+
completedAt: null,
|
|
1681
|
+
input,
|
|
1682
|
+
token: dependencies.token(),
|
|
1683
|
+
result: null
|
|
1684
|
+
};
|
|
1685
|
+
const path = stringOption(options, "enrollment-file") ?? defaultEnrollmentPath(request, env);
|
|
1686
|
+
createPendingEnrollment(path, enrollment2, env);
|
|
1687
|
+
return { path, enrollment: enrollment2 };
|
|
1688
|
+
}
|
|
1689
|
+
function enrollment(operation, options, env, dependencies) {
|
|
1690
|
+
const resume = stringOption(options, "resume");
|
|
1691
|
+
if (!resume) return initialEnrollment(operation, options, env, dependencies);
|
|
1692
|
+
const loaded = readEnrollment(resume, env);
|
|
1693
|
+
if (loaded.operation !== operation) {
|
|
1694
|
+
throw new Error("enrollment operation is " + loaded.operation + ", not " + operation);
|
|
1695
|
+
}
|
|
1696
|
+
return { path: resume, enrollment: loaded };
|
|
1697
|
+
}
|
|
1698
|
+
function publicEnrollment(path, enrollment2) {
|
|
1699
|
+
if (!enrollment2.result) throw new Error("enrollment has no completed result");
|
|
1700
|
+
return {
|
|
1701
|
+
schemaVersion: 1,
|
|
1702
|
+
status: "ok",
|
|
1703
|
+
operation: enrollment2.operation,
|
|
1704
|
+
requestId: enrollment2.requestId,
|
|
1705
|
+
enrollmentFile: path,
|
|
1706
|
+
enrollmentState: enrollment2.state,
|
|
1707
|
+
...enrollment2.result
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
async function runProvision(db, path, enrollment2, completedAt, env, lock2) {
|
|
1711
|
+
if (enrollment2.state !== "pending") return publicEnrollment(path, enrollment2);
|
|
1712
|
+
const input = enrollment2.input;
|
|
1713
|
+
const response = await ownerQuery(
|
|
1714
|
+
db,
|
|
1715
|
+
"SELECT * FROM agent_bridge.control_provision($1::uuid,$2::text,$3::text,$4::text,$5::text,$6::text,$7::char(64),$8::text,$9::text,$10::timestamptz)",
|
|
1716
|
+
[
|
|
1717
|
+
enrollment2.requestId,
|
|
1718
|
+
input.workspaceId,
|
|
1719
|
+
input.workspaceName,
|
|
1720
|
+
input.principal,
|
|
1721
|
+
input.displayName,
|
|
1722
|
+
input.runtimeType,
|
|
1723
|
+
enrollmentTokenHash(enrollment2),
|
|
1724
|
+
input.label,
|
|
1725
|
+
input.scopeSetName,
|
|
1726
|
+
input.expiresAt
|
|
1727
|
+
]
|
|
1728
|
+
);
|
|
1729
|
+
const row = response.rows[0];
|
|
1730
|
+
if (!row) throw new Error("owner provision returned no result");
|
|
1731
|
+
const result = {
|
|
1732
|
+
workspaceId: row.workspace_id,
|
|
1733
|
+
principal: input.principal,
|
|
1734
|
+
agentId: row.agent_id,
|
|
1735
|
+
credentialId: row.credential_id,
|
|
1736
|
+
replayed: row.replayed
|
|
1737
|
+
};
|
|
1738
|
+
const ready = transitionEnrollment(path, enrollment2, "ready", { completedAt, result }, env, lock2);
|
|
1739
|
+
return publicEnrollment(path, ready);
|
|
1740
|
+
}
|
|
1741
|
+
async function runRotate(db, path, enrollment2, completedAt, env, lock2) {
|
|
1742
|
+
if (enrollment2.state !== "pending") return publicEnrollment(path, enrollment2);
|
|
1743
|
+
const input = enrollment2.input;
|
|
1744
|
+
const response = await ownerQuery(
|
|
1745
|
+
db,
|
|
1746
|
+
"SELECT * FROM agent_bridge.control_rotate_credential($1::uuid,$2::uuid,$3::text,$4::text,$5::char(64),$6::text,$7::text,$8::timestamptz,$9::timestamptz)",
|
|
1747
|
+
[
|
|
1748
|
+
enrollment2.requestId,
|
|
1749
|
+
input.credentialId,
|
|
1750
|
+
input.workspaceId,
|
|
1751
|
+
input.principal,
|
|
1752
|
+
enrollmentTokenHash(enrollment2),
|
|
1753
|
+
input.label,
|
|
1754
|
+
input.scopeSetName,
|
|
1755
|
+
input.expiresAt,
|
|
1756
|
+
input.graceUntil
|
|
1757
|
+
]
|
|
1758
|
+
);
|
|
1759
|
+
const row = response.rows[0];
|
|
1760
|
+
if (!row) throw new Error("owner rotation returned no result");
|
|
1761
|
+
const result = {
|
|
1762
|
+
workspaceId: row.workspace_id,
|
|
1763
|
+
principal: row.principal,
|
|
1764
|
+
agentId: null,
|
|
1765
|
+
credentialId: row.credential_id,
|
|
1766
|
+
replayed: row.replayed
|
|
1767
|
+
};
|
|
1768
|
+
const ready = transitionEnrollment(path, enrollment2, "ready", { completedAt, result }, env, lock2);
|
|
1769
|
+
return publicEnrollment(path, ready);
|
|
1770
|
+
}
|
|
1771
|
+
function decodeCursor(raw, workspace) {
|
|
1772
|
+
if (!raw) return null;
|
|
1773
|
+
let parsed;
|
|
1774
|
+
try {
|
|
1775
|
+
parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
1776
|
+
} catch {
|
|
1777
|
+
throw new Error("--cursor is not a valid owner inventory cursor");
|
|
1778
|
+
}
|
|
1779
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1780
|
+
throw new Error("--cursor is not a valid owner inventory cursor");
|
|
1781
|
+
}
|
|
1782
|
+
const cursor2 = parsed;
|
|
1783
|
+
if (cursor2.v !== 1 || cursor2.workspace !== workspace || typeof cursor2.createdAt !== "string" || typeof cursor2.credentialId !== "string") {
|
|
1784
|
+
throw new Error("--cursor does not match this inventory request");
|
|
1785
|
+
}
|
|
1786
|
+
let createdAt;
|
|
1787
|
+
try {
|
|
1788
|
+
createdAt = canonicalTimestamp(cursor2.createdAt, "inventory cursor createdAt");
|
|
1789
|
+
} catch {
|
|
1790
|
+
throw new Error("--cursor is not a valid owner inventory cursor");
|
|
1791
|
+
}
|
|
1792
|
+
if (createdAt === null) throw new Error("--cursor is not a valid owner inventory cursor");
|
|
1793
|
+
let credentialId;
|
|
1794
|
+
try {
|
|
1795
|
+
credentialId = uuidValue(cursor2.credentialId, "inventory cursor credentialId");
|
|
1796
|
+
} catch {
|
|
1797
|
+
throw new Error("--cursor is not a valid owner inventory cursor");
|
|
1798
|
+
}
|
|
1799
|
+
return { v: 1, workspace, createdAt, credentialId };
|
|
1800
|
+
}
|
|
1801
|
+
function encodeCursor(cursor2) {
|
|
1802
|
+
return Buffer.from(JSON.stringify(cursor2), "utf8").toString("base64url");
|
|
1803
|
+
}
|
|
1804
|
+
function instant(value) {
|
|
1805
|
+
if (value === null || value === void 0) return null;
|
|
1806
|
+
return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
|
|
1807
|
+
}
|
|
1808
|
+
async function runInventory(db, options) {
|
|
1809
|
+
const rawLimit = stringOption(options, "limit");
|
|
1810
|
+
const limit = rawLimit === void 0 ? 100 : Number(rawLimit);
|
|
1811
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) {
|
|
1812
|
+
throw new Error("--limit must be an integer between 1 and 1000");
|
|
1813
|
+
}
|
|
1814
|
+
const workspace = stringOption(options, "workspace") ?? null;
|
|
1815
|
+
const cursor2 = decodeCursor(stringOption(options, "cursor"), workspace);
|
|
1816
|
+
const response = await ownerQuery(
|
|
1817
|
+
db,
|
|
1818
|
+
"SELECT * FROM agent_bridge.control_credential_inventory($1::text,$2::timestamptz,$3::uuid,$4::integer)",
|
|
1819
|
+
[workspace, cursor2?.createdAt ?? null, cursor2?.credentialId ?? null, limit]
|
|
1820
|
+
);
|
|
1821
|
+
const credentials = response.rows.map((row) => ({
|
|
1822
|
+
credentialId: row.credential_id,
|
|
1823
|
+
workspaceId: row.workspace_id,
|
|
1824
|
+
principal: row.principal,
|
|
1825
|
+
label: row.label,
|
|
1826
|
+
scopes: row.scopes,
|
|
1827
|
+
scopeSetName: row.scope_set_name,
|
|
1828
|
+
expiresAt: instant(row.expires_at),
|
|
1829
|
+
revokedAt: instant(row.revoked_at),
|
|
1830
|
+
revokedBy: row.revoked_by,
|
|
1831
|
+
revocationReason: row.revocation_reason,
|
|
1832
|
+
replacesCredentialId: row.replaces_credential_id,
|
|
1833
|
+
expiryGraceUntil: instant(row.expiry_grace_until),
|
|
1834
|
+
createdAt: instant(row.created_at),
|
|
1835
|
+
lastUsedAt: instant(row.last_used_at),
|
|
1836
|
+
agentDisabledAt: instant(row.agent_disabled_at),
|
|
1837
|
+
workspaceDisabledAt: instant(row.workspace_disabled_at)
|
|
1838
|
+
}));
|
|
1839
|
+
const last = credentials.length ? credentials[credentials.length - 1] : void 0;
|
|
1840
|
+
const nextCursor = credentials.length === limit && last?.createdAt ? encodeCursor({
|
|
1841
|
+
v: 1,
|
|
1842
|
+
workspace,
|
|
1843
|
+
createdAt: last.createdAt,
|
|
1844
|
+
credentialId: String(last.credentialId)
|
|
1845
|
+
}) : null;
|
|
1846
|
+
return {
|
|
1847
|
+
schemaVersion: 1,
|
|
1848
|
+
status: "ok",
|
|
1849
|
+
operation: "inventory",
|
|
1850
|
+
items: credentials,
|
|
1851
|
+
page: { limit, nextCursor }
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
async function runRevoke(db, options, dependencies) {
|
|
1855
|
+
const selectedRequestId = requestId(options, dependencies);
|
|
1856
|
+
const credentialId = uuidOption(options, "credential-id");
|
|
1857
|
+
const reason = stringOption(options, "reason") ?? "operator_request";
|
|
1858
|
+
if (!["operator_request", "rotation", "compromise", "retired"].includes(reason)) {
|
|
1859
|
+
throw new Error("--reason must be operator_request, rotation, compromise, or retired");
|
|
1860
|
+
}
|
|
1861
|
+
const response = await ownerQuery(
|
|
1862
|
+
db,
|
|
1863
|
+
"SELECT * FROM agent_bridge.control_revoke_credential($1::uuid,$2::uuid,$3::text)",
|
|
1864
|
+
[selectedRequestId, credentialId, reason]
|
|
1865
|
+
);
|
|
1866
|
+
const row = response.rows[0];
|
|
1867
|
+
if (!row) throw new Error("owner revoke returned no result");
|
|
1868
|
+
return {
|
|
1869
|
+
schemaVersion: 1,
|
|
1870
|
+
status: "ok",
|
|
1871
|
+
operation: "revoke",
|
|
1872
|
+
requestId: selectedRequestId,
|
|
1873
|
+
credentialId,
|
|
1874
|
+
revoked: row.revoked,
|
|
1875
|
+
replayed: row.replayed
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
async function runOwnerCommand(subcommand, options, env = process.env, dependencies = defaultDependencies) {
|
|
1879
|
+
if (!subcommand || subcommand === "help") {
|
|
1880
|
+
if (Object.keys(options).length) throw new Error("owner help does not accept options");
|
|
1881
|
+
return {
|
|
1882
|
+
command: "agent-bridge owner",
|
|
1883
|
+
subcommands: ["provision", "inventory", "rotate", "revoke"],
|
|
1884
|
+
authority: "AGENT_BRIDGE_OPERATOR_DATABASE_URL"
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
if (!["provision", "inventory", "rotate", "revoke"].includes(subcommand)) {
|
|
1888
|
+
throw new Error("unsupported owner command: " + subcommand);
|
|
1889
|
+
}
|
|
1890
|
+
validateOwnerOptions(subcommand, options);
|
|
1891
|
+
if (options["recover-lock"] === true && !stringOption(options, "resume")) {
|
|
1892
|
+
throw new Error("--recover-lock requires --resume");
|
|
1893
|
+
}
|
|
1894
|
+
const databaseUrl = env.AGENT_BRIDGE_OPERATOR_DATABASE_URL?.trim();
|
|
1895
|
+
if (!databaseUrl) throw new Error("AGENT_BRIDGE_OPERATOR_DATABASE_URL is required");
|
|
1896
|
+
let connection;
|
|
1897
|
+
try {
|
|
1898
|
+
connection = dependencies.connect(databaseUrl);
|
|
1899
|
+
} catch {
|
|
1900
|
+
throw new Error("owner database connection failed");
|
|
1901
|
+
}
|
|
1902
|
+
try {
|
|
1903
|
+
if (subcommand === "inventory") return await runInventory(connection.db, options);
|
|
1904
|
+
if (subcommand === "revoke") return await runRevoke(connection.db, options, dependencies);
|
|
1905
|
+
const mutation = subcommand;
|
|
1906
|
+
const selected = enrollment(mutation, options, env, dependencies);
|
|
1907
|
+
if (options["recover-lock"] === true) recoverEnrollmentLock(selected.path, env);
|
|
1908
|
+
const lock2 = acquireEnrollmentLock(selected.path, env);
|
|
1909
|
+
try {
|
|
1910
|
+
const current = readEnrollment(selected.path, env);
|
|
1911
|
+
if (current.operation !== mutation) {
|
|
1912
|
+
throw new Error("enrollment operation is " + current.operation + ", not " + mutation);
|
|
1913
|
+
}
|
|
1914
|
+
if (subcommand === "provision") {
|
|
1915
|
+
return await runProvision(
|
|
1916
|
+
connection.db,
|
|
1917
|
+
selected.path,
|
|
1918
|
+
current,
|
|
1919
|
+
dependencies.now().toISOString(),
|
|
1920
|
+
env,
|
|
1921
|
+
lock2
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1924
|
+
return await runRotate(
|
|
1925
|
+
connection.db,
|
|
1926
|
+
selected.path,
|
|
1927
|
+
current,
|
|
1928
|
+
dependencies.now().toISOString(),
|
|
1929
|
+
env,
|
|
1930
|
+
lock2
|
|
1931
|
+
);
|
|
1932
|
+
} finally {
|
|
1933
|
+
releaseEnrollmentLock(lock2);
|
|
1934
|
+
}
|
|
1935
|
+
} catch (error) {
|
|
1936
|
+
if (!(error instanceof OwnerDatabaseFailure)) throw error;
|
|
1937
|
+
const code = error.driverCode;
|
|
1938
|
+
if (code === "42501") throw new Error("owner database authorization denied");
|
|
1939
|
+
if (code === "23505") throw new Error("owner request conflicts with existing state");
|
|
1940
|
+
if (code === "22P02" || code === "22007" || code === "22023") {
|
|
1941
|
+
throw new Error("owner database rejected invalid input");
|
|
1942
|
+
}
|
|
1943
|
+
throw new Error("owner database operation failed");
|
|
1944
|
+
} finally {
|
|
1945
|
+
try {
|
|
1946
|
+
await connection.close();
|
|
1947
|
+
} catch {
|
|
1948
|
+
throw new Error("owner database close failed");
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
// src/archive-cli.ts
|
|
1954
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
1955
|
+
import {
|
|
1956
|
+
closeSync as closeSync3,
|
|
1957
|
+
constants as constants3,
|
|
1958
|
+
existsSync as existsSync3,
|
|
1959
|
+
fstatSync as fstatSync2,
|
|
1960
|
+
fsyncSync as fsyncSync3,
|
|
1961
|
+
linkSync,
|
|
1962
|
+
lstatSync as lstatSync2,
|
|
1963
|
+
openSync as openSync3,
|
|
1964
|
+
renameSync as renameSync3,
|
|
1965
|
+
unlinkSync,
|
|
1966
|
+
writeSync
|
|
1967
|
+
} from "fs";
|
|
1968
|
+
import { homedir as homedir3 } from "os";
|
|
1969
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join3, resolve as resolve3 } from "path";
|
|
1970
|
+
import pg2 from "pg";
|
|
1971
|
+
var BOOLEAN_OPTIONS = /* @__PURE__ */ new Set(["apply", "dry-run", "force"]);
|
|
1972
|
+
var OPERATION_OPTIONS = {
|
|
1973
|
+
export: /* @__PURE__ */ new Set(["provider", "workspace", "output", "db", "config", "force", "request-id"]),
|
|
1974
|
+
verify: /* @__PURE__ */ new Set(["file"]),
|
|
1975
|
+
import: /* @__PURE__ */ new Set(["provider", "file", "db", "config", "workspace", "request-id", "apply", "dry-run"])
|
|
1976
|
+
};
|
|
1977
|
+
var ArchiveCommandError = class extends Error {
|
|
1978
|
+
constructor(code, message, details) {
|
|
1979
|
+
super(message);
|
|
1980
|
+
this.code = code;
|
|
1981
|
+
this.details = details;
|
|
1982
|
+
}
|
|
1983
|
+
};
|
|
1984
|
+
function fail(code, message, details) {
|
|
1985
|
+
throw new ArchiveCommandError(code, message, details);
|
|
1986
|
+
}
|
|
1987
|
+
function parseArchiveArgs(argv) {
|
|
1988
|
+
const operation = argv[0];
|
|
1989
|
+
if (operation !== "export" && operation !== "verify" && operation !== "import") fail("INVALID_COMMAND", "usage: agent-bridge archive <export|verify|import>");
|
|
1990
|
+
const options = {};
|
|
1991
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
1992
|
+
const argument = argv[index];
|
|
1993
|
+
if (!argument.startsWith("--") || argument === "--") fail("INVALID_ARGUMENT", `unexpected positional argument: ${argument}`);
|
|
1994
|
+
const name = argument.slice(2);
|
|
1995
|
+
if (!name || name.includes("=")) fail("INVALID_OPTION", `invalid option: ${argument}`);
|
|
1996
|
+
if (!OPERATION_OPTIONS[operation].has(name)) fail("INVALID_OPTION", `--${name} is not valid for archive ${operation}`);
|
|
1997
|
+
if (options[name] !== void 0) fail("DUPLICATE_OPTION", `--${name} may only be provided once`);
|
|
1998
|
+
if (BOOLEAN_OPTIONS.has(name)) {
|
|
1999
|
+
options[name] = true;
|
|
2000
|
+
continue;
|
|
2001
|
+
}
|
|
2002
|
+
const selected = argv[index + 1];
|
|
2003
|
+
if (!selected || selected.startsWith("--")) fail("MISSING_OPTION_VALUE", `--${name} requires a value`);
|
|
2004
|
+
options[name] = selected;
|
|
2005
|
+
index += 1;
|
|
2006
|
+
}
|
|
2007
|
+
if (operation === "import" && options.apply === true && options["dry-run"] === true) fail("INVALID_OPTION", "--apply and --dry-run are mutually exclusive");
|
|
2008
|
+
return { operation, options };
|
|
2009
|
+
}
|
|
2010
|
+
var stringOption2 = (options, name) => typeof options[name] === "string" ? options[name] : void 0;
|
|
2011
|
+
function archiveRequestId(options) {
|
|
2012
|
+
const value = stringOption2(options, "request-id") ?? randomUUID4();
|
|
2013
|
+
try {
|
|
2014
|
+
return validatePortableArchiveRequestId(value);
|
|
2015
|
+
} catch {
|
|
2016
|
+
return fail("INVALID_REQUEST_ID", "--request-id must be a UUID");
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
function required2(options, name) {
|
|
2020
|
+
const selected = stringOption2(options, name)?.trim();
|
|
2021
|
+
if (!selected) fail("MISSING_OPTION", `--${name} is required`);
|
|
2022
|
+
return selected;
|
|
2023
|
+
}
|
|
2024
|
+
function provider(options) {
|
|
2025
|
+
const selected = required2(options, "provider");
|
|
2026
|
+
if (selected !== "local" && selected !== "postgres") fail("INVALID_PROVIDER", "--provider must be local or postgres");
|
|
2027
|
+
if (selected === "postgres" && (options.db !== void 0 || options.config !== void 0)) fail("INVALID_OPTION", "PostgreSQL archives accept authority only from AGENT_BRIDGE_ARCHIVE_DATABASE_URL");
|
|
2028
|
+
return selected;
|
|
2029
|
+
}
|
|
2030
|
+
var home = (env) => env.HOME?.trim() || homedir3();
|
|
2031
|
+
function localDatabase(options, env) {
|
|
2032
|
+
const configPath = stringOption2(options, "config") ?? env.AGENT_BRIDGE_CONFIG?.trim() ?? join3(home(env), ".agent-bridge", "config");
|
|
2033
|
+
const config = readClientConfigFile(configPath);
|
|
2034
|
+
const selected = stringOption2(options, "db") ?? env.AGENT_BRIDGE_DB?.trim() ?? config.AGENT_BRIDGE_DB?.trim() ?? join3(home(env), ".agent-bridge", "bridge.sqlite3");
|
|
2035
|
+
return isAbsolute2(selected) ? selected : resolve3(selected);
|
|
2036
|
+
}
|
|
2037
|
+
function createStore(selected, options, env) {
|
|
2038
|
+
if (selected === "local") {
|
|
2039
|
+
const store = new SQLitePortableArchiveStore(localDatabase(options, env));
|
|
2040
|
+
return { store, close: async () => {
|
|
2041
|
+
store.close();
|
|
2042
|
+
} };
|
|
2043
|
+
}
|
|
2044
|
+
const databaseUrl = env.AGENT_BRIDGE_ARCHIVE_DATABASE_URL?.trim();
|
|
2045
|
+
if (!databaseUrl) fail("MISSING_AUTHORITY", "AGENT_BRIDGE_ARCHIVE_DATABASE_URL is required");
|
|
2046
|
+
const pool = new pg2.Pool({ connectionString: databaseUrl, max: 1, connectionTimeoutMillis: 1e4, query_timeout: 3e4 });
|
|
2047
|
+
return { store: new PostgresPortableArchiveStore(pool), close: () => pool.end() };
|
|
2048
|
+
}
|
|
2049
|
+
function syncDirectory2(path) {
|
|
2050
|
+
if (process.platform === "win32") return;
|
|
2051
|
+
const descriptor = openSync3(path, "r");
|
|
2052
|
+
try {
|
|
2053
|
+
fsyncSync3(descriptor);
|
|
2054
|
+
} finally {
|
|
2055
|
+
closeSync3(descriptor);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
async function publishArchive(path, force, session, workspace, options) {
|
|
2059
|
+
const exportRequestId = validatePortableArchiveRequestId(options.requestId);
|
|
2060
|
+
const target = resolve3(path);
|
|
2061
|
+
const directory = dirname3(target);
|
|
2062
|
+
const temporary = join3(directory, `.${exportRequestId}.agent-bridge-archive.tmp`);
|
|
2063
|
+
const backup = join3(directory, `.${exportRequestId}.agent-bridge-archive.backup`);
|
|
2064
|
+
const operations = {
|
|
2065
|
+
link: options.fileOperations?.link ?? linkSync,
|
|
2066
|
+
rename: options.fileOperations?.rename ?? renameSync3,
|
|
2067
|
+
unlink: options.fileOperations?.unlink ?? unlinkSync,
|
|
2068
|
+
syncDirectory: options.fileOperations?.syncDirectory ?? syncDirectory2
|
|
2069
|
+
};
|
|
2070
|
+
let descriptor;
|
|
2071
|
+
let created = false;
|
|
2072
|
+
let linked = false;
|
|
2073
|
+
let replacementVisible = false;
|
|
2074
|
+
let durablyPublished = false;
|
|
2075
|
+
let backupCreated = false;
|
|
2076
|
+
let preserveBackup = false;
|
|
2077
|
+
let targetExisted = false;
|
|
2078
|
+
const ambiguous = (message) => new ArchiveCommandError(
|
|
2079
|
+
"ARCHIVE_PUBLICATION_AMBIGUOUS",
|
|
2080
|
+
message,
|
|
2081
|
+
{
|
|
2082
|
+
requestId: exportRequestId,
|
|
2083
|
+
file: target,
|
|
2084
|
+
published: "unknown",
|
|
2085
|
+
auditStatus: "started",
|
|
2086
|
+
...backupCreated ? { backupFile: backup } : {}
|
|
2087
|
+
}
|
|
2088
|
+
);
|
|
2089
|
+
try {
|
|
2090
|
+
verifyPrivatePathAccess(directory, "directory");
|
|
2091
|
+
if (existsSync3(target)) {
|
|
2092
|
+
const stat = lstatSync2(target);
|
|
2093
|
+
if (stat.isSymbolicLink() || !stat.isFile()) fail("INVALID_OUTPUT", "archive output must be a regular file");
|
|
2094
|
+
if (!force) fail("OUTPUT_EXISTS", "archive output already exists; pass --force to replace it");
|
|
2095
|
+
targetExisted = true;
|
|
2096
|
+
}
|
|
2097
|
+
descriptor = openSync3(temporary, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL, 384);
|
|
2098
|
+
created = true;
|
|
2099
|
+
securePrivatePath(temporary, "file");
|
|
2100
|
+
const metadata = await (options.stream ?? streamPortableArchive)(session, workspace, exportRequestId, (bytes) => {
|
|
2101
|
+
let offset = 0;
|
|
2102
|
+
while (offset < bytes.length) offset += writeSync(descriptor, bytes, offset, bytes.length - offset);
|
|
2103
|
+
});
|
|
2104
|
+
fsyncSync3(descriptor);
|
|
2105
|
+
const opened = fstatSync2(descriptor);
|
|
2106
|
+
const named = lstatSync2(temporary);
|
|
2107
|
+
if (opened.dev !== named.dev || opened.ino !== named.ino || !named.isFile() || named.isSymbolicLink()) fail("INVALID_OUTPUT", "archive temporary path identity changed before publication");
|
|
2108
|
+
if (force && targetExisted) {
|
|
2109
|
+
const noFollow = typeof constants3.O_NOFOLLOW === "number" ? constants3.O_NOFOLLOW : 0;
|
|
2110
|
+
const targetDescriptor = openSync3(target, constants3.O_RDONLY | noFollow);
|
|
2111
|
+
try {
|
|
2112
|
+
const openedTarget = fstatSync2(targetDescriptor);
|
|
2113
|
+
const namedTarget = lstatSync2(target);
|
|
2114
|
+
if (!openedTarget.isFile() || namedTarget.isSymbolicLink() || openedTarget.dev !== namedTarget.dev || openedTarget.ino !== namedTarget.ino) {
|
|
2115
|
+
fail("INVALID_OUTPUT", "archive output identity changed before replacement");
|
|
2116
|
+
}
|
|
2117
|
+
verifyPrivatePathAccess(target, "file");
|
|
2118
|
+
operations.link(target, backup);
|
|
2119
|
+
backupCreated = true;
|
|
2120
|
+
const backupStat = lstatSync2(backup);
|
|
2121
|
+
if (!backupStat.isFile() || backupStat.isSymbolicLink() || openedTarget.dev !== backupStat.dev || openedTarget.ino !== backupStat.ino) {
|
|
2122
|
+
fail("INVALID_OUTPUT", "archive backup identity changed before replacement");
|
|
2123
|
+
}
|
|
2124
|
+
verifyPrivatePathAccess(backup, "file");
|
|
2125
|
+
operations.syncDirectory(directory);
|
|
2126
|
+
} finally {
|
|
2127
|
+
closeSync3(targetDescriptor);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
if (force) {
|
|
2131
|
+
operations.rename(temporary, target);
|
|
2132
|
+
created = false;
|
|
2133
|
+
replacementVisible = true;
|
|
2134
|
+
} else {
|
|
2135
|
+
try {
|
|
2136
|
+
operations.link(temporary, target);
|
|
2137
|
+
} catch (error) {
|
|
2138
|
+
if (error.code === "EEXIST") fail("OUTPUT_EXISTS", "archive output already exists; pass --force to replace it");
|
|
2139
|
+
throw error;
|
|
2140
|
+
}
|
|
2141
|
+
linked = true;
|
|
2142
|
+
operations.unlink(temporary);
|
|
2143
|
+
created = false;
|
|
2144
|
+
}
|
|
2145
|
+
verifyPrivatePathAccess(target, "file");
|
|
2146
|
+
operations.syncDirectory(directory);
|
|
2147
|
+
durablyPublished = true;
|
|
2148
|
+
if (backupCreated) {
|
|
2149
|
+
try {
|
|
2150
|
+
operations.unlink(backup);
|
|
2151
|
+
} catch {
|
|
2152
|
+
preserveBackup = true;
|
|
2153
|
+
fail("ARCHIVE_BACKUP_RETAINED", "archive was published but backup cleanup could not be proved; retry the same request ID", {
|
|
2154
|
+
requestId: exportRequestId,
|
|
2155
|
+
file: target,
|
|
2156
|
+
backupFile: backup,
|
|
2157
|
+
backupState: "retained",
|
|
2158
|
+
published: true,
|
|
2159
|
+
auditStatus: "started"
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
backupCreated = false;
|
|
2163
|
+
try {
|
|
2164
|
+
operations.syncDirectory(directory);
|
|
2165
|
+
} catch {
|
|
2166
|
+
fail("ARCHIVE_BACKUP_CLEANUP_AMBIGUOUS", "archive was published but backup deletion durability is unknown; retry the same request ID", {
|
|
2167
|
+
requestId: exportRequestId,
|
|
2168
|
+
file: target,
|
|
2169
|
+
backupFile: backup,
|
|
2170
|
+
backupState: "unknown",
|
|
2171
|
+
published: true,
|
|
2172
|
+
auditStatus: "started"
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
try {
|
|
2177
|
+
await session.complete({ ...metadata, publishedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2178
|
+
} catch {
|
|
2179
|
+
let auditStatus = "unknown";
|
|
2180
|
+
try {
|
|
2181
|
+
await session.abandon("audit_failed");
|
|
2182
|
+
auditStatus = "abandoned";
|
|
2183
|
+
} catch {
|
|
2184
|
+
}
|
|
2185
|
+
const details = { requestId: exportRequestId, file: target, published: true, auditStatus };
|
|
2186
|
+
if (auditStatus === "abandoned") fail("ARCHIVE_AUDIT_ABANDONED", "archive was published but its completion audit was abandoned", details);
|
|
2187
|
+
fail("ARCHIVE_AUDIT_AMBIGUOUS", "archive was published but its completion audit outcome is unknown", details);
|
|
2188
|
+
}
|
|
2189
|
+
return metadata;
|
|
2190
|
+
} catch (error) {
|
|
2191
|
+
if (!durablyPublished) {
|
|
2192
|
+
if (force && replacementVisible) {
|
|
2193
|
+
if (backupCreated) {
|
|
2194
|
+
try {
|
|
2195
|
+
operations.rename(backup, target);
|
|
2196
|
+
backupCreated = false;
|
|
2197
|
+
replacementVisible = false;
|
|
2198
|
+
verifyPrivatePathAccess(target, "file");
|
|
2199
|
+
operations.syncDirectory(directory);
|
|
2200
|
+
} catch {
|
|
2201
|
+
preserveBackup = true;
|
|
2202
|
+
throw ambiguous("archive replacement failed and restoration could not be proved; use owner reconciliation");
|
|
2203
|
+
}
|
|
2204
|
+
} else {
|
|
2205
|
+
try {
|
|
2206
|
+
operations.unlink(target);
|
|
2207
|
+
replacementVisible = false;
|
|
2208
|
+
operations.syncDirectory(directory);
|
|
2209
|
+
} catch {
|
|
2210
|
+
throw ambiguous("archive replacement failed and removal could not be proved; use owner reconciliation");
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
} else if (backupCreated) {
|
|
2214
|
+
try {
|
|
2215
|
+
operations.unlink(backup);
|
|
2216
|
+
backupCreated = false;
|
|
2217
|
+
operations.syncDirectory(directory);
|
|
2218
|
+
} catch {
|
|
2219
|
+
preserveBackup = true;
|
|
2220
|
+
throw ambiguous("archive backup cleanup could not be proved; use owner reconciliation");
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
if (linked && descriptor !== void 0) {
|
|
2224
|
+
try {
|
|
2225
|
+
const opened = fstatSync2(descriptor);
|
|
2226
|
+
const named = lstatSync2(target);
|
|
2227
|
+
if (opened.dev === named.dev && opened.ino === named.ino) {
|
|
2228
|
+
operations.unlink(target);
|
|
2229
|
+
linked = false;
|
|
2230
|
+
operations.syncDirectory(directory);
|
|
2231
|
+
}
|
|
2232
|
+
} catch {
|
|
2233
|
+
throw ambiguous("archive publication failed and output removal could not be proved; use owner reconciliation");
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
try {
|
|
2237
|
+
await session.abandon("publication_failed");
|
|
2238
|
+
} catch {
|
|
2239
|
+
throw new ArchiveCommandError(
|
|
2240
|
+
"ARCHIVE_AUDIT_AMBIGUOUS",
|
|
2241
|
+
"archive publication did not complete but its audit outcome is unknown",
|
|
2242
|
+
{ requestId: exportRequestId, file: target, published: false, auditStatus: "unknown" }
|
|
2243
|
+
);
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
throw error;
|
|
2247
|
+
} finally {
|
|
2248
|
+
if (descriptor !== void 0) closeSync3(descriptor);
|
|
2249
|
+
if (created) {
|
|
2250
|
+
try {
|
|
2251
|
+
operations.unlink(temporary);
|
|
2252
|
+
} catch {
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
if (backupCreated && !preserveBackup && !replacementVisible) {
|
|
2256
|
+
try {
|
|
2257
|
+
operations.unlink(backup);
|
|
2258
|
+
operations.syncDirectory(directory);
|
|
2259
|
+
} catch {
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
var success = (operation, value) => ({ schemaVersion: 1, status: "ok", operation, ...value });
|
|
2265
|
+
function recoveryArtifactPaths(target, requestId2) {
|
|
2266
|
+
const directory = dirname3(target);
|
|
2267
|
+
return [
|
|
2268
|
+
join3(directory, `.${requestId2}.agent-bridge-archive.tmp`),
|
|
2269
|
+
join3(directory, `.${requestId2}.agent-bridge-archive.backup`)
|
|
2270
|
+
];
|
|
2271
|
+
}
|
|
2272
|
+
function existingRecoveryArtifacts(target, requestId2) {
|
|
2273
|
+
return recoveryArtifactPaths(target, requestId2).filter((path) => existsSync3(path));
|
|
2274
|
+
}
|
|
2275
|
+
function openVerifiedPrivateFile(path) {
|
|
2276
|
+
verifyPrivatePathAccess(path, "file");
|
|
2277
|
+
const before = lstatSync2(path);
|
|
2278
|
+
const noFollow = typeof constants3.O_NOFOLLOW === "number" ? constants3.O_NOFOLLOW : 0;
|
|
2279
|
+
const descriptor = openSync3(path, constants3.O_RDONLY | noFollow);
|
|
2280
|
+
try {
|
|
2281
|
+
const opened = fstatSync2(descriptor);
|
|
2282
|
+
const after = lstatSync2(path);
|
|
2283
|
+
if (!opened.isFile() || before.isSymbolicLink() || after.isSymbolicLink() || opened.dev !== before.dev || opened.ino !== before.ino || opened.dev !== after.dev || opened.ino !== after.ino) {
|
|
2284
|
+
fail("INVALID_OUTPUT", "archive recovery path identity changed");
|
|
2285
|
+
}
|
|
2286
|
+
return descriptor;
|
|
2287
|
+
} catch (error) {
|
|
2288
|
+
closeSync3(descriptor);
|
|
2289
|
+
throw error;
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
function removeRecoveryArtifacts(target, requestId2) {
|
|
2293
|
+
for (const path of existingRecoveryArtifacts(target, requestId2)) {
|
|
2294
|
+
const descriptor = openVerifiedPrivateFile(path);
|
|
2295
|
+
try {
|
|
2296
|
+
const opened = fstatSync2(descriptor);
|
|
2297
|
+
const named = lstatSync2(path);
|
|
2298
|
+
if (opened.dev !== named.dev || opened.ino !== named.ino) fail("INVALID_OUTPUT", "archive recovery path identity changed");
|
|
2299
|
+
unlinkSync(path);
|
|
2300
|
+
} finally {
|
|
2301
|
+
closeSync3(descriptor);
|
|
2302
|
+
}
|
|
2303
|
+
syncDirectory2(dirname3(path));
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
function verifyCompletedExport(path, requestId2, expected) {
|
|
2307
|
+
const target = resolve3(path);
|
|
2308
|
+
const recoveryPaths2 = existingRecoveryArtifacts(target, requestId2);
|
|
2309
|
+
if (!existsSync3(target)) fail("ARCHIVE_REPLAY_FILE_MISSING", "completed export file is missing; use a new request ID", { requestId: requestId2, file: target, recoveryPaths: recoveryPaths2 });
|
|
2310
|
+
let archive;
|
|
2311
|
+
try {
|
|
2312
|
+
try {
|
|
2313
|
+
archive = new PortableArchiveFile(target);
|
|
2314
|
+
} catch {
|
|
2315
|
+
fail("ARCHIVE_REPLAY_FILE_MISMATCH", "completed export file is not a valid private archive", { requestId: requestId2, file: target, recoveryPaths: recoveryPaths2 });
|
|
2316
|
+
}
|
|
2317
|
+
let actual;
|
|
2318
|
+
try {
|
|
2319
|
+
actual = archive.verify();
|
|
2320
|
+
} catch {
|
|
2321
|
+
fail("ARCHIVE_REPLAY_FILE_MISMATCH", "completed export file is not a valid matching archive", { requestId: requestId2, file: target, recoveryPaths: recoveryPaths2 });
|
|
2322
|
+
}
|
|
2323
|
+
if (actual.exportRequestId !== requestId2 || expected.exportRequestId !== requestId2 || actual.workspace !== expected.workspace || actual.digest !== expected.digest || actual.messageCount !== expected.messageCount || actual.receiptCount !== expected.receiptCount) {
|
|
2324
|
+
fail("ARCHIVE_REPLAY_FILE_MISMATCH", "completed export file does not match the recorded audit; use a new request ID", { requestId: requestId2, file: target, recoveryPaths: recoveryPaths2 });
|
|
2325
|
+
}
|
|
2326
|
+
try {
|
|
2327
|
+
archive.proveDurable();
|
|
2328
|
+
} catch {
|
|
2329
|
+
fail("ARCHIVE_REPLAY_DURABILITY_AMBIGUOUS", "completed export file durability could not be reproved", { requestId: requestId2, file: target, published: true, auditStatus: "completed", recoveryPaths: recoveryPaths2 });
|
|
2330
|
+
}
|
|
2331
|
+
try {
|
|
2332
|
+
removeRecoveryArtifacts(target, requestId2);
|
|
2333
|
+
} catch {
|
|
2334
|
+
fail("ARCHIVE_RECOVERY_ARTIFACT_RETAINED", "completed export recovery artifacts could not be removed", { requestId: requestId2, file: target, published: true, auditStatus: "completed", recoveryPaths: existingRecoveryArtifacts(target, requestId2) });
|
|
2335
|
+
}
|
|
2336
|
+
try {
|
|
2337
|
+
archive.proveDurable();
|
|
2338
|
+
} catch {
|
|
2339
|
+
fail("ARCHIVE_REPLAY_DURABILITY_AMBIGUOUS", "completed export file durability could not be reproved after cleanup", { requestId: requestId2, file: target, published: true, auditStatus: "completed", recoveryPaths: existingRecoveryArtifacts(target, requestId2) });
|
|
2340
|
+
}
|
|
2341
|
+
return actual;
|
|
2342
|
+
} finally {
|
|
2343
|
+
archive?.close();
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
async function reconcileActiveExport(path, requestId2, workspace, session) {
|
|
2347
|
+
const target = resolve3(path);
|
|
2348
|
+
const details = { requestId: requestId2, file: target, recoveryPaths: existingRecoveryArtifacts(target, requestId2), published: "unknown", auditStatus: "started" };
|
|
2349
|
+
if (!existsSync3(target)) fail("ARCHIVE_RECONCILIATION_FILE_MISSING", "active export file is missing; use a new request ID or owner reconciliation", details);
|
|
2350
|
+
let archive;
|
|
2351
|
+
try {
|
|
2352
|
+
try {
|
|
2353
|
+
archive = new PortableArchiveFile(target);
|
|
2354
|
+
} catch {
|
|
2355
|
+
fail("ARCHIVE_RECONCILIATION_FILE_MISMATCH", "active export file is not a valid private archive", details);
|
|
2356
|
+
}
|
|
2357
|
+
let metadata;
|
|
2358
|
+
try {
|
|
2359
|
+
metadata = archive.verify();
|
|
2360
|
+
} catch {
|
|
2361
|
+
fail("ARCHIVE_RECONCILIATION_FILE_MISMATCH", "active export file is not a valid matching archive", details);
|
|
2362
|
+
}
|
|
2363
|
+
if (metadata.exportRequestId !== requestId2 || metadata.workspace !== workspace) {
|
|
2364
|
+
fail("ARCHIVE_RECONCILIATION_FILE_MISMATCH", "active export file does not match the requested workspace; use a new request ID or owner reconciliation", details);
|
|
2365
|
+
}
|
|
2366
|
+
try {
|
|
2367
|
+
archive.proveDurable();
|
|
2368
|
+
} catch {
|
|
2369
|
+
fail("ARCHIVE_RECONCILIATION_DURABILITY_AMBIGUOUS", "active export file durability could not be proved; retry the same request ID", details);
|
|
2370
|
+
}
|
|
2371
|
+
try {
|
|
2372
|
+
removeRecoveryArtifacts(target, requestId2);
|
|
2373
|
+
} catch {
|
|
2374
|
+
fail("ARCHIVE_RECOVERY_ARTIFACT_RETAINED", "active export recovery artifacts could not be removed; retry the same request ID", {
|
|
2375
|
+
...details,
|
|
2376
|
+
recoveryPaths: existingRecoveryArtifacts(target, requestId2),
|
|
2377
|
+
published: true
|
|
2378
|
+
});
|
|
2379
|
+
}
|
|
2380
|
+
try {
|
|
2381
|
+
archive.proveDurable();
|
|
2382
|
+
} catch {
|
|
2383
|
+
fail("ARCHIVE_RECONCILIATION_DURABILITY_AMBIGUOUS", "active export durability after cleanup could not be proved; retry the same request ID", { ...details, published: true });
|
|
2384
|
+
}
|
|
2385
|
+
const publishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2386
|
+
try {
|
|
2387
|
+
await session.reconcile({ ...metadata, publishedAt });
|
|
2388
|
+
} catch {
|
|
2389
|
+
fail("ARCHIVE_RECONCILIATION_AMBIGUOUS", "active export reconciliation outcome is unknown; retry the same request ID", { ...details, published: true, auditStatus: "unknown" });
|
|
2390
|
+
}
|
|
2391
|
+
return { ...metadata, publishedAt };
|
|
2392
|
+
} finally {
|
|
2393
|
+
archive?.close();
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
async function runArchiveCommand(argv, env = process.env) {
|
|
2397
|
+
const { operation, options } = parseArchiveArgs(argv);
|
|
2398
|
+
try {
|
|
2399
|
+
if (operation === "verify") {
|
|
2400
|
+
const path2 = required2(options, "file");
|
|
2401
|
+
let archive2;
|
|
2402
|
+
try {
|
|
2403
|
+
archive2 = new PortableArchiveFile(path2);
|
|
2404
|
+
const metadata = archive2.verify();
|
|
2405
|
+
return success(operation, { file: resolve3(path2), exportRequestId: metadata.exportRequestId, workspace: metadata.workspace, digest: metadata.digest, messages: metadata.messageCount, receipts: metadata.receiptCount });
|
|
2406
|
+
} finally {
|
|
2407
|
+
archive2?.close();
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
const selected = provider(options);
|
|
2411
|
+
if (operation === "export") {
|
|
2412
|
+
const workspace = required2(options, "workspace");
|
|
2413
|
+
const path2 = required2(options, "output");
|
|
2414
|
+
const requestId2 = archiveRequestId(options);
|
|
2415
|
+
const connection = createStore(selected, options, env);
|
|
2416
|
+
let session;
|
|
2417
|
+
try {
|
|
2418
|
+
const start = await connection.store.beginExport(requestId2, workspace);
|
|
2419
|
+
if (start.status === "completed") {
|
|
2420
|
+
const metadata2 = verifyCompletedExport(path2, requestId2, start.metadata);
|
|
2421
|
+
return success(operation, { provider: selected, requestId: requestId2, replayed: true, reconciled: false, file: resolve3(path2), workspace, digest: metadata2.digest, messages: metadata2.messageCount, receipts: metadata2.receiptCount, publishedAt: start.metadata.publishedAt });
|
|
2422
|
+
}
|
|
2423
|
+
session = start.session;
|
|
2424
|
+
if (start.replayed) {
|
|
2425
|
+
const metadata2 = await reconcileActiveExport(path2, requestId2, workspace, session);
|
|
2426
|
+
return success(operation, { provider: selected, requestId: requestId2, replayed: true, reconciled: true, file: resolve3(path2), workspace, digest: metadata2.digest, messages: metadata2.messageCount, receipts: metadata2.receiptCount, publishedAt: metadata2.publishedAt });
|
|
2427
|
+
}
|
|
2428
|
+
const metadata = await publishArchive(path2, options.force === true, session, workspace, { requestId: requestId2 });
|
|
2429
|
+
return success(operation, { provider: selected, requestId: requestId2, replayed: false, reconciled: false, file: resolve3(path2), workspace, digest: metadata.digest, messages: metadata.messageCount, receipts: metadata.receiptCount });
|
|
2430
|
+
} finally {
|
|
2431
|
+
await session?.close();
|
|
2432
|
+
await connection.close();
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
const path = required2(options, "file");
|
|
2436
|
+
const archive = new PortableArchiveFile(path);
|
|
2437
|
+
try {
|
|
2438
|
+
const metadata = archive.verify();
|
|
2439
|
+
const assertedWorkspace = stringOption2(options, "workspace")?.trim();
|
|
2440
|
+
if (assertedWorkspace && assertedWorkspace !== metadata.workspace) fail("WORKSPACE_MISMATCH", "--workspace does not match the archive workspace");
|
|
2441
|
+
const requestId2 = archiveRequestId(options);
|
|
2442
|
+
const connection = createStore(selected, options, env);
|
|
2443
|
+
try {
|
|
2444
|
+
const result = await connection.store.importWorkspace(requestId2, metadata, archive, { apply: options.apply === true });
|
|
2445
|
+
return success(operation, { provider: selected, file: resolve3(path), exportRequestId: metadata.exportRequestId, ...result });
|
|
2446
|
+
} finally {
|
|
2447
|
+
await connection.close();
|
|
2448
|
+
}
|
|
2449
|
+
} finally {
|
|
2450
|
+
archive.close();
|
|
2451
|
+
}
|
|
2452
|
+
} catch (error) {
|
|
2453
|
+
if (error instanceof ArchiveCommandError) throw error;
|
|
2454
|
+
if (error instanceof PrivatePathError) fail("INSECURE_PATH", "archive file or directory does not satisfy the private path policy");
|
|
2455
|
+
if (error instanceof PortableArchiveError) fail("INVALID_ARCHIVE", error.message);
|
|
2456
|
+
fail("ARCHIVE_OPERATION_FAILED", "archive operation failed");
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
// src/dr-cli.ts
|
|
2461
|
+
import { createHash as createHash3, randomUUID as randomUUID5 } from "crypto";
|
|
2462
|
+
import {
|
|
2463
|
+
closeSync as closeSync4,
|
|
2464
|
+
constants as constants4,
|
|
2465
|
+
existsSync as existsSync4,
|
|
2466
|
+
fstatSync as fstatSync3,
|
|
2467
|
+
fsyncSync as fsyncSync4,
|
|
2468
|
+
linkSync as linkSync2,
|
|
2469
|
+
lstatSync as lstatSync3,
|
|
2470
|
+
mkdirSync as mkdirSync3,
|
|
2471
|
+
openSync as openSync4,
|
|
2472
|
+
rmSync as rmSync3,
|
|
2473
|
+
unlinkSync as unlinkSync2
|
|
2474
|
+
} from "fs";
|
|
2475
|
+
import { dirname as dirname4, join as join4, resolve as resolve4 } from "path";
|
|
2476
|
+
var POSTGRES_DUMP_ENTRY = "postgres/database.dump";
|
|
2477
|
+
var POSTGRES_ROLES_ENTRY = "postgres/roles.json";
|
|
2478
|
+
var OPTIONS = {
|
|
2479
|
+
backup: /* @__PURE__ */ new Set(["provider", "source", "output", "backup-id", "timeout-ms", "tool-directory"]),
|
|
2480
|
+
verify: /* @__PURE__ */ new Set(["provider", "bundle", "tool-directory"]),
|
|
2481
|
+
restore: /* @__PURE__ */ new Set(["provider", "bundle", "target", "request-id", "timeout-ms", "tool-directory", "accept-source-sql-risk"])
|
|
2482
|
+
};
|
|
2483
|
+
var FLAGS = {
|
|
2484
|
+
backup: /* @__PURE__ */ new Set(),
|
|
2485
|
+
verify: /* @__PURE__ */ new Set(),
|
|
2486
|
+
restore: /* @__PURE__ */ new Set(["accept-source-sql-risk"])
|
|
2487
|
+
};
|
|
2488
|
+
var fail2 = (code, message, details) => {
|
|
2489
|
+
throw new NativeDrCommandError(code, message, details);
|
|
2490
|
+
};
|
|
2491
|
+
function parse2(argv) {
|
|
2492
|
+
const operation = argv[0];
|
|
2493
|
+
if (operation !== "backup" && operation !== "verify" && operation !== "restore") {
|
|
2494
|
+
fail2("INVALID_COMMAND", "usage: agent-bridge dr <backup|verify|restore>");
|
|
2495
|
+
}
|
|
2496
|
+
const selectedOperation = operation;
|
|
2497
|
+
const options = {};
|
|
2498
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
2499
|
+
const argument = argv[index];
|
|
2500
|
+
if (!argument.startsWith("--") || argument === "--") {
|
|
2501
|
+
fail2("INVALID_ARGUMENT", "DR commands do not accept positional arguments");
|
|
2502
|
+
}
|
|
2503
|
+
const name = argument.slice(2);
|
|
2504
|
+
if (!name || name.includes("=") || !OPTIONS[selectedOperation].has(name)) {
|
|
2505
|
+
fail2("INVALID_OPTION", `--${name} is not valid for dr ${selectedOperation}`);
|
|
2506
|
+
}
|
|
2507
|
+
if (options[name] !== void 0) fail2("DUPLICATE_OPTION", `--${name} may only be provided once`);
|
|
2508
|
+
if (FLAGS[selectedOperation].has(name)) {
|
|
2509
|
+
options[name] = true;
|
|
2510
|
+
continue;
|
|
2511
|
+
}
|
|
2512
|
+
const value = argv[index + 1];
|
|
2513
|
+
if (!value || value.startsWith("--")) fail2("MISSING_OPTION_VALUE", `--${name} requires a value`);
|
|
2514
|
+
options[name] = value;
|
|
2515
|
+
index += 1;
|
|
2516
|
+
}
|
|
2517
|
+
return { operation: selectedOperation, options };
|
|
2518
|
+
}
|
|
2519
|
+
function optional(options, name) {
|
|
2520
|
+
const value = options[name];
|
|
2521
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
2522
|
+
}
|
|
2523
|
+
function required3(options, name) {
|
|
2524
|
+
const value = optional(options, name);
|
|
2525
|
+
if (!value) fail2("MISSING_OPTION", `--${name} is required`);
|
|
2526
|
+
return value;
|
|
2527
|
+
}
|
|
2528
|
+
function timeout(options) {
|
|
2529
|
+
const value = optional(options, "timeout-ms");
|
|
2530
|
+
return value === void 0 ? void 0 : validateNativeDrTimeout(Number(value));
|
|
2531
|
+
}
|
|
2532
|
+
function provider2(options) {
|
|
2533
|
+
const selected = required3(options, "provider");
|
|
2534
|
+
if (selected !== "local" && selected !== "postgres") fail2("INVALID_PROVIDER", "--provider must be local or postgres");
|
|
2535
|
+
return selected;
|
|
2536
|
+
}
|
|
2537
|
+
function rejectProviderOption(options, name, selected, operation) {
|
|
2538
|
+
if (options[name] !== void 0) fail2("INVALID_OPTION", `--${name} is not valid for ${selected} DR ${operation}`);
|
|
2539
|
+
}
|
|
2540
|
+
function syncFile(path) {
|
|
2541
|
+
const before = lstatSync3(path);
|
|
2542
|
+
const noFollow = typeof constants4.O_NOFOLLOW === "number" ? constants4.O_NOFOLLOW : 0;
|
|
2543
|
+
const descriptor = openSync4(path, constants4.O_RDWR | noFollow);
|
|
2544
|
+
try {
|
|
2545
|
+
const opened = fstatSync3(descriptor);
|
|
2546
|
+
const named = lstatSync3(path);
|
|
2547
|
+
if (!opened.isFile() || before.isSymbolicLink() || named.isSymbolicLink() || !sameNode(before, opened) || !sameNode(opened, named)) {
|
|
2548
|
+
fail2("INSECURE_PATH", "DR publication path identity changed before file synchronization");
|
|
2549
|
+
}
|
|
2550
|
+
fsyncSync4(descriptor);
|
|
2551
|
+
} finally {
|
|
2552
|
+
closeSync4(descriptor);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
function syncDirectory3(path, platform = process.platform) {
|
|
2556
|
+
if (platform === "win32") return false;
|
|
2557
|
+
const descriptor = openSync4(path, constants4.O_RDONLY);
|
|
2558
|
+
try {
|
|
2559
|
+
fsyncSync4(descriptor);
|
|
2560
|
+
} finally {
|
|
2561
|
+
closeSync4(descriptor);
|
|
2562
|
+
}
|
|
2563
|
+
return true;
|
|
2564
|
+
}
|
|
2565
|
+
function fileOperations(dependencies) {
|
|
2566
|
+
return {
|
|
2567
|
+
link: dependencies.fileOperations?.link ?? linkSync2,
|
|
2568
|
+
syncDirectory: dependencies.fileOperations?.syncDirectory ?? ((path) => syncDirectory3(path, dependencies.platform ?? process.platform)),
|
|
2569
|
+
syncFile: dependencies.fileOperations?.syncFile ?? syncFile,
|
|
2570
|
+
afterPublish: dependencies.fileOperations?.afterPublish
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
function sameNode(left, right) {
|
|
2574
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
2575
|
+
}
|
|
2576
|
+
function createPrivateDirectory(path) {
|
|
2577
|
+
mkdirSync3(path, { mode: 448 });
|
|
2578
|
+
const before = lstatSync3(path);
|
|
2579
|
+
securePrivatePath(path, "directory");
|
|
2580
|
+
verifyPrivatePathAccess(path, "directory");
|
|
2581
|
+
const after = lstatSync3(path);
|
|
2582
|
+
if (!before.isDirectory() || before.isSymbolicLink() || !sameNode(before, after)) {
|
|
2583
|
+
fail2("INSECURE_PATH", "DR staging directory identity changed while securing it");
|
|
2584
|
+
}
|
|
2585
|
+
return after;
|
|
2586
|
+
}
|
|
2587
|
+
function openPrivateFile(path) {
|
|
2588
|
+
const noFollow = typeof constants4.O_NOFOLLOW === "number" ? constants4.O_NOFOLLOW : 0;
|
|
2589
|
+
const descriptor = openSync4(path, constants4.O_RDWR | constants4.O_CREAT | constants4.O_EXCL | noFollow, 384);
|
|
2590
|
+
try {
|
|
2591
|
+
const before = fstatSync3(descriptor);
|
|
2592
|
+
securePrivatePath(path, "file");
|
|
2593
|
+
verifyPrivatePathAccess(path, "file");
|
|
2594
|
+
const after = lstatSync3(path);
|
|
2595
|
+
if (!before.isFile() || after.isSymbolicLink() || !sameNode(before, after)) {
|
|
2596
|
+
fail2("INSECURE_PATH", "DR staging file identity changed while securing it");
|
|
2597
|
+
}
|
|
2598
|
+
return { descriptor, identity: after };
|
|
2599
|
+
} catch (error) {
|
|
2600
|
+
closeSync4(descriptor);
|
|
2601
|
+
throw error;
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
function artifactAnchor(descriptor, sha256) {
|
|
2605
|
+
const stat = fstatSync3(descriptor, { bigint: true });
|
|
2606
|
+
if (!stat.isFile()) fail2("ARTIFACT_ANCHOR_INVALID", "PostgreSQL DR artifact anchor is not a regular file");
|
|
2607
|
+
return {
|
|
2608
|
+
descriptor,
|
|
2609
|
+
device: stat.dev.toString(),
|
|
2610
|
+
inode: stat.ino.toString(),
|
|
2611
|
+
size: stat.size.toString(),
|
|
2612
|
+
ctimeNanoseconds: stat.ctimeNs.toString(),
|
|
2613
|
+
mtimeNanoseconds: stat.mtimeNs.toString(),
|
|
2614
|
+
sha256
|
|
2615
|
+
};
|
|
2616
|
+
}
|
|
2617
|
+
function extractPostgresBundle(bundle, stage) {
|
|
2618
|
+
const descriptors = /* @__PURE__ */ new Map();
|
|
2619
|
+
try {
|
|
2620
|
+
for (const [name, filename] of [
|
|
2621
|
+
[POSTGRES_DUMP_ENTRY, "postgres-database.dump"],
|
|
2622
|
+
[POSTGRES_ROLES_ENTRY, "postgres-roles.json"]
|
|
2623
|
+
]) {
|
|
2624
|
+
const path = join4(stage, filename);
|
|
2625
|
+
descriptors.set(name, { ...openPrivateFile(path), path });
|
|
2626
|
+
}
|
|
2627
|
+
const reader = new NativeDrBundleReader(bundle);
|
|
2628
|
+
let metadata;
|
|
2629
|
+
try {
|
|
2630
|
+
metadata = reader.inspect((entry) => descriptors.get(entry.name)?.descriptor);
|
|
2631
|
+
} finally {
|
|
2632
|
+
reader.close();
|
|
2633
|
+
}
|
|
2634
|
+
if (metadata.manifest.kind !== "postgres") {
|
|
2635
|
+
fail2("PROVIDER_MISMATCH", `DR bundle provider is ${metadata.manifest.kind}, not postgres`);
|
|
2636
|
+
}
|
|
2637
|
+
for (const value of descriptors.values()) {
|
|
2638
|
+
const opened = fstatSync3(value.descriptor);
|
|
2639
|
+
const named = lstatSync3(value.path);
|
|
2640
|
+
if (!opened.isFile() || named.isSymbolicLink() || !sameNode(opened, named)) {
|
|
2641
|
+
fail2("INSECURE_PATH", "DR extraction path identity changed while reading the bundle");
|
|
2642
|
+
}
|
|
2643
|
+
fsyncSync4(value.descriptor);
|
|
2644
|
+
securePrivatePath(value.path, "file");
|
|
2645
|
+
verifyPrivatePathAccess(value.path, "file");
|
|
2646
|
+
}
|
|
2647
|
+
const dump = descriptors.get(POSTGRES_DUMP_ENTRY);
|
|
2648
|
+
const roles = descriptors.get(POSTGRES_ROLES_ENTRY);
|
|
2649
|
+
const dumpEntry = metadata.manifest.entries.find((entry) => entry.name === POSTGRES_DUMP_ENTRY);
|
|
2650
|
+
const rolesEntry = metadata.manifest.entries.find((entry) => entry.name === POSTGRES_ROLES_ENTRY);
|
|
2651
|
+
return {
|
|
2652
|
+
metadata,
|
|
2653
|
+
dumpPath: dump.path,
|
|
2654
|
+
rolesPath: roles.path,
|
|
2655
|
+
artifactAnchors: {
|
|
2656
|
+
dump: artifactAnchor(dump.descriptor, dumpEntry.sha256),
|
|
2657
|
+
roles: artifactAnchor(roles.descriptor, rolesEntry.sha256)
|
|
2658
|
+
},
|
|
2659
|
+
descriptors
|
|
2660
|
+
};
|
|
2661
|
+
} catch (error) {
|
|
2662
|
+
for (const value of descriptors.values()) {
|
|
2663
|
+
try {
|
|
2664
|
+
closeSync4(value.descriptor);
|
|
2665
|
+
} catch {
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
throw error;
|
|
2669
|
+
}
|
|
111
2670
|
}
|
|
112
|
-
function
|
|
113
|
-
|
|
114
|
-
if (existsSync(path)) {
|
|
2671
|
+
function closeExtractedPostgresBundle(extracted) {
|
|
2672
|
+
for (const value of extracted.descriptors.values()) {
|
|
115
2673
|
try {
|
|
116
|
-
|
|
117
|
-
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") throw new Error();
|
|
118
|
-
config = parsed;
|
|
2674
|
+
closeSync4(value.descriptor);
|
|
119
2675
|
} catch {
|
|
120
|
-
throw new Error(`Claude Desktop config is not valid JSON: ${path}`);
|
|
121
2676
|
}
|
|
122
2677
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
2678
|
+
extracted.descriptors.clear();
|
|
2679
|
+
}
|
|
2680
|
+
function removeOwnedFile(path, identity) {
|
|
2681
|
+
if (!existsSync4(path)) return true;
|
|
2682
|
+
try {
|
|
2683
|
+
const current = lstatSync3(path);
|
|
2684
|
+
if (!identity || !current.isFile() || current.isSymbolicLink() || !sameNode(current, identity)) return false;
|
|
2685
|
+
unlinkSync2(path);
|
|
2686
|
+
return !existsSync4(path);
|
|
2687
|
+
} catch {
|
|
2688
|
+
return false;
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
function removeOwnedDirectory(path, identity) {
|
|
2692
|
+
if (!existsSync4(path)) return true;
|
|
2693
|
+
try {
|
|
2694
|
+
const current = lstatSync3(path);
|
|
2695
|
+
if (!identity || !current.isDirectory() || current.isSymbolicLink() || !sameNode(current, identity)) return false;
|
|
2696
|
+
rmSync3(path, { recursive: true });
|
|
2697
|
+
return !existsSync4(path);
|
|
2698
|
+
} catch {
|
|
2699
|
+
return false;
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
function recoveryPaths(paths) {
|
|
2703
|
+
return paths.filter(existsSync4);
|
|
2704
|
+
}
|
|
2705
|
+
function verifyProviderBundle(path, expected) {
|
|
2706
|
+
const expectedKind = expected === "local" ? "sqlite" : "postgres";
|
|
2707
|
+
const reader = new NativeDrBundleReader(path);
|
|
2708
|
+
try {
|
|
2709
|
+
const metadata = reader.inspect();
|
|
2710
|
+
if (metadata.manifest.kind !== expectedKind) {
|
|
2711
|
+
fail2("PROVIDER_MISMATCH", `DR bundle provider is ${metadata.manifest.kind}, not ${expected}`);
|
|
2712
|
+
}
|
|
2713
|
+
return expected === "local" ? verifyNativeDrBundle(path) : metadata;
|
|
2714
|
+
} finally {
|
|
2715
|
+
reader.close();
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
function commandErrorWithRecovery(error, identity, paths) {
|
|
2719
|
+
const details = error instanceof NativeDrCommandError || error instanceof PostgresNativeDrError ? { ...error.details } : {};
|
|
2720
|
+
const nested = Array.isArray(details.recoveryPaths) ? details.recoveryPaths.filter((path) => typeof path === "string") : [];
|
|
2721
|
+
delete details.recoveryPaths;
|
|
2722
|
+
const retained = recoveryPaths([.../* @__PURE__ */ new Set([...nested, ...paths])]);
|
|
2723
|
+
if (error instanceof NativeDrCommandError) {
|
|
2724
|
+
return new NativeDrCommandError(error.code, error.message, {
|
|
2725
|
+
...details,
|
|
2726
|
+
...identity,
|
|
2727
|
+
...retained.length ? { recoveryPaths: retained } : {}
|
|
2728
|
+
});
|
|
2729
|
+
}
|
|
2730
|
+
if (error instanceof PostgresNativeDrError) {
|
|
2731
|
+
return new NativeDrCommandError(error.code, error.message, {
|
|
2732
|
+
...details,
|
|
2733
|
+
...identity,
|
|
2734
|
+
...retained.length ? { recoveryPaths: retained } : {}
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
if (error instanceof PrivatePathError) {
|
|
2738
|
+
return new NativeDrCommandError("INSECURE_PATH", "DR path does not satisfy the private path policy", {
|
|
2739
|
+
...identity,
|
|
2740
|
+
...retained.length ? { recoveryPaths: retained } : {}
|
|
2741
|
+
});
|
|
2742
|
+
}
|
|
2743
|
+
if (error instanceof NativeDrBundleError) {
|
|
2744
|
+
return new NativeDrCommandError("INVALID_DR_BUNDLE", "DR bundle verification failed", {
|
|
2745
|
+
...identity,
|
|
2746
|
+
...retained.length ? { recoveryPaths: retained } : {}
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
2749
|
+
return new NativeDrCommandError("DR_COMMAND_FAILED", "DR command failed", {
|
|
2750
|
+
...identity,
|
|
2751
|
+
...retained.length ? { recoveryPaths: retained } : {}
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2754
|
+
async function backupPostgresBundle(outputPath, backupId, toolDirectory, dependencies) {
|
|
2755
|
+
const target = resolve4(outputPath);
|
|
2756
|
+
preparePrivateFileLocation(target);
|
|
2757
|
+
const directory = dirname4(target);
|
|
2758
|
+
const stage = join4(directory, `.${backupId}.agent-bridge-dr.postgres.stage`);
|
|
2759
|
+
const bundleStage = join4(directory, `.${backupId}.agent-bridge-dr.bundle.tmp`);
|
|
2760
|
+
const priorRecovery = recoveryPaths([bundleStage, stage]);
|
|
2761
|
+
if (priorRecovery.length) fail2("DR_RECOVERY_REQUIRED", "a prior PostgreSQL DR backup with this ID requires recovery", {
|
|
2762
|
+
backupId,
|
|
2763
|
+
output: target,
|
|
2764
|
+
outputExists: existsSync4(target),
|
|
2765
|
+
published: existsSync4(target) ? "unknown" : false,
|
|
2766
|
+
recoveryPaths: priorRecovery
|
|
2767
|
+
});
|
|
2768
|
+
if (existsSync4(target)) fail2("OUTPUT_EXISTS", "DR output already exists");
|
|
2769
|
+
let stageIdentity;
|
|
2770
|
+
let bundleIdentity;
|
|
2771
|
+
let descriptor;
|
|
2772
|
+
let published = false;
|
|
2773
|
+
let durable = false;
|
|
2774
|
+
let directoryDurability = "confirmed";
|
|
2775
|
+
const operations = fileOperations(dependencies);
|
|
2776
|
+
const platform = dependencies.platform ?? process.platform;
|
|
2777
|
+
try {
|
|
2778
|
+
stageIdentity = createPrivateDirectory(stage);
|
|
2779
|
+
operations.syncDirectory(directory);
|
|
2780
|
+
const input = await (dependencies.backupPostgres ?? backupPostgresNativeDr)({
|
|
2781
|
+
stagingDirectory: stage,
|
|
2782
|
+
backupId,
|
|
2783
|
+
environment: dependencies.environment ?? process.env,
|
|
2784
|
+
...toolDirectory ? { toolDirectory } : {}
|
|
2785
|
+
});
|
|
2786
|
+
if (input.backupId !== backupId || input.kind !== "postgres" || input.entries.length !== 2 || input.entries[0]?.name !== POSTGRES_DUMP_ENTRY || resolve4(input.entries[0].path) !== join4(stage, "postgres-database.dump") || input.entries[1]?.name !== POSTGRES_ROLES_ENTRY || resolve4(input.entries[1].path) !== join4(stage, "postgres-roles.json")) {
|
|
2787
|
+
fail2("POSTGRES_STAGE_INVALID", "PostgreSQL DR adapter returned an unexpected staging contract");
|
|
2788
|
+
}
|
|
2789
|
+
operations.syncDirectory(stage);
|
|
2790
|
+
const opened = openPrivateFile(bundleStage);
|
|
2791
|
+
descriptor = opened.descriptor;
|
|
2792
|
+
bundleIdentity = opened.identity;
|
|
2793
|
+
writeNativeDrBundle(descriptor, input, bundleStage);
|
|
2794
|
+
closeSync4(descriptor);
|
|
2795
|
+
descriptor = void 0;
|
|
2796
|
+
const verificationStage = join4(stage, "bundle-verification");
|
|
2797
|
+
createPrivateDirectory(verificationStage);
|
|
2798
|
+
const extracted = extractPostgresBundle(bundleStage, verificationStage);
|
|
2799
|
+
try {
|
|
2800
|
+
await (dependencies.verifyPostgres ?? verifyPostgresNativeDrArtifacts)({
|
|
2801
|
+
dumpPath: extracted.dumpPath,
|
|
2802
|
+
rolesPath: extracted.rolesPath,
|
|
2803
|
+
schema: extracted.metadata.manifest.schema,
|
|
2804
|
+
artifactAnchors: extracted.artifactAnchors,
|
|
2805
|
+
...toolDirectory ? { toolDirectory } : {}
|
|
2806
|
+
});
|
|
2807
|
+
} finally {
|
|
2808
|
+
closeExtractedPostgresBundle(extracted);
|
|
2809
|
+
}
|
|
2810
|
+
const beforePublication = extracted.metadata;
|
|
2811
|
+
try {
|
|
2812
|
+
operations.link(bundleStage, target);
|
|
2813
|
+
published = true;
|
|
2814
|
+
} catch (error) {
|
|
2815
|
+
if (error.code === "EEXIST") fail2("OUTPUT_EXISTS", "DR output already exists");
|
|
2816
|
+
throw error;
|
|
2817
|
+
}
|
|
2818
|
+
const publishedIdentity = lstatSync3(target);
|
|
2819
|
+
if (!bundleIdentity || !publishedIdentity.isFile() || publishedIdentity.isSymbolicLink() || !sameNode(bundleIdentity, publishedIdentity)) {
|
|
2820
|
+
fail2("DR_PUBLICATION_INVALID", "published DR output does not reference the verified staging file", {
|
|
2821
|
+
backupId,
|
|
2822
|
+
output: target,
|
|
2823
|
+
published: true,
|
|
2824
|
+
durable: false,
|
|
2825
|
+
verified: false,
|
|
2826
|
+
recoveryPaths: recoveryPaths([bundleStage, stage])
|
|
2827
|
+
});
|
|
2828
|
+
}
|
|
2829
|
+
try {
|
|
2830
|
+
operations.syncFile(target);
|
|
2831
|
+
operations.afterPublish?.(target);
|
|
2832
|
+
if (!operations.syncDirectory(directory)) {
|
|
2833
|
+
if (platform === "win32") directoryDurability = "unavailable";
|
|
2834
|
+
else {
|
|
2835
|
+
fail2("DR_PUBLICATION_AMBIGUOUS", "DR output is visible but directory durability is unproved", {
|
|
2836
|
+
backupId,
|
|
2837
|
+
output: target,
|
|
2838
|
+
published: true,
|
|
2839
|
+
durable: "unknown",
|
|
2840
|
+
platform,
|
|
2841
|
+
recoveryPaths: recoveryPaths([bundleStage, stage])
|
|
2842
|
+
});
|
|
135
2843
|
}
|
|
136
2844
|
}
|
|
2845
|
+
durable = true;
|
|
2846
|
+
} catch {
|
|
2847
|
+
fail2("DR_PUBLICATION_AMBIGUOUS", "DR output is visible but durability is unknown", {
|
|
2848
|
+
backupId,
|
|
2849
|
+
output: target,
|
|
2850
|
+
published: true,
|
|
2851
|
+
durable: "unknown",
|
|
2852
|
+
recoveryPaths: recoveryPaths([bundleStage, stage])
|
|
2853
|
+
});
|
|
137
2854
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
2855
|
+
let publishedMetadata;
|
|
2856
|
+
try {
|
|
2857
|
+
publishedMetadata = verifyProviderBundle(target, "postgres");
|
|
2858
|
+
} catch {
|
|
2859
|
+
fail2("DR_PUBLICATION_INVALID", "published DR output could not be verified", {
|
|
2860
|
+
backupId,
|
|
2861
|
+
output: target,
|
|
2862
|
+
published: true,
|
|
2863
|
+
durable: true,
|
|
2864
|
+
verified: false,
|
|
2865
|
+
recoveryPaths: recoveryPaths([bundleStage, stage])
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2868
|
+
if (!publishedMetadata) throw new NativeDrCommandError("DR_PUBLICATION_INVALID", "published DR output could not be verified");
|
|
2869
|
+
if (publishedMetadata.bundleSha256 !== beforePublication.bundleSha256) {
|
|
2870
|
+
fail2("DR_PUBLICATION_INVALID", "published DR output does not match its verified staging bundle", {
|
|
2871
|
+
backupId,
|
|
2872
|
+
output: target,
|
|
2873
|
+
published: true,
|
|
2874
|
+
durable: true,
|
|
2875
|
+
verified: false,
|
|
2876
|
+
recoveryPaths: recoveryPaths([bundleStage, stage])
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
const bundleRemoved = removeOwnedFile(bundleStage, bundleIdentity);
|
|
2880
|
+
const stageRemoved = removeOwnedDirectory(stage, stageIdentity);
|
|
2881
|
+
if (!bundleRemoved || !stageRemoved) {
|
|
2882
|
+
fail2("DR_RECOVERY_ARTIFACT_RETAINED", "DR output is valid but a recovery artifact remains", {
|
|
2883
|
+
backupId,
|
|
2884
|
+
output: target,
|
|
2885
|
+
published: true,
|
|
2886
|
+
durable: true,
|
|
2887
|
+
verified: true,
|
|
2888
|
+
recoveryPaths: recoveryPaths([bundleStage, stage])
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
try {
|
|
2892
|
+
if (!operations.syncDirectory(directory) && platform !== "win32") {
|
|
2893
|
+
fail2("DR_CLEANUP_DURABILITY_UNKNOWN", "DR output is valid but staging cleanup durability is unproved", {
|
|
2894
|
+
backupId,
|
|
2895
|
+
output: target,
|
|
2896
|
+
published: true,
|
|
2897
|
+
durable: true,
|
|
2898
|
+
verified: true,
|
|
2899
|
+
cleanupDurability: "unproved",
|
|
2900
|
+
platform
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
} catch (error) {
|
|
2904
|
+
if (error instanceof NativeDrCommandError) throw error;
|
|
2905
|
+
fail2("DR_CLEANUP_DURABILITY_UNKNOWN", "DR output is valid but staging cleanup durability is unknown", {
|
|
2906
|
+
backupId,
|
|
2907
|
+
output: target,
|
|
2908
|
+
published: true,
|
|
2909
|
+
durable: true,
|
|
2910
|
+
verified: true
|
|
2911
|
+
});
|
|
2912
|
+
}
|
|
2913
|
+
return { metadata: publishedMetadata, directoryDurability };
|
|
2914
|
+
} catch (error) {
|
|
2915
|
+
if (descriptor !== void 0) {
|
|
2916
|
+
closeSync4(descriptor);
|
|
2917
|
+
descriptor = void 0;
|
|
2918
|
+
}
|
|
2919
|
+
if (!published) {
|
|
2920
|
+
removeOwnedFile(bundleStage, bundleIdentity);
|
|
2921
|
+
removeOwnedDirectory(stage, stageIdentity);
|
|
2922
|
+
try {
|
|
2923
|
+
if (!operations.syncDirectory(directory)) {
|
|
2924
|
+
if (platform === "win32") {
|
|
2925
|
+
const primary = commandErrorWithRecovery(error, { backupId }, [bundleStage, stage]);
|
|
2926
|
+
throw new NativeDrCommandError(primary.code, primary.message, {
|
|
2927
|
+
...primary.details,
|
|
2928
|
+
cleanupDirectoryDurability: "unavailable",
|
|
2929
|
+
platform
|
|
2930
|
+
});
|
|
2931
|
+
}
|
|
2932
|
+
throw new NativeDrCommandError("DR_CLEANUP_DURABILITY_UNKNOWN", "DR backup failure cleanup durability is unproved", {
|
|
2933
|
+
backupId,
|
|
2934
|
+
cleanupDurability: "unproved",
|
|
2935
|
+
platform,
|
|
2936
|
+
recoveryPaths: recoveryPaths([bundleStage, stage])
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2939
|
+
} catch (cleanupError) {
|
|
2940
|
+
if (cleanupError instanceof NativeDrCommandError) throw cleanupError;
|
|
2941
|
+
throw new NativeDrCommandError("DR_CLEANUP_DURABILITY_UNKNOWN", "DR backup failure cleanup durability is unknown", {
|
|
2942
|
+
backupId,
|
|
2943
|
+
cleanupDurability: "unknown",
|
|
2944
|
+
platform,
|
|
2945
|
+
recoveryPaths: recoveryPaths([bundleStage, stage])
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2948
|
+
throw commandErrorWithRecovery(error, { backupId }, [bundleStage, stage]);
|
|
2949
|
+
}
|
|
2950
|
+
if (error instanceof NativeDrCommandError) throw error;
|
|
2951
|
+
throw new NativeDrCommandError("DR_PUBLICATION_AMBIGUOUS", "DR publication outcome is incomplete", {
|
|
2952
|
+
backupId,
|
|
2953
|
+
output: target,
|
|
2954
|
+
published: true,
|
|
2955
|
+
durable: durable || "unknown",
|
|
2956
|
+
recoveryPaths: recoveryPaths([bundleStage, stage])
|
|
2957
|
+
});
|
|
2958
|
+
} finally {
|
|
2959
|
+
if (descriptor !== void 0) closeSync4(descriptor);
|
|
2960
|
+
}
|
|
144
2961
|
}
|
|
145
|
-
function
|
|
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]);
|
|
210
3108
|
}
|
|
211
3109
|
}
|
|
3110
|
+
var success2 = (operation, fields) => ({
|
|
3111
|
+
schemaVersion: 1,
|
|
3112
|
+
status: "ok",
|
|
3113
|
+
operation,
|
|
3114
|
+
...fields
|
|
3115
|
+
});
|
|
3116
|
+
async function runDrCommand(argv, dependencies = {}) {
|
|
3117
|
+
const { operation, options } = parse2(argv);
|
|
3118
|
+
try {
|
|
3119
|
+
const selected = provider2(options);
|
|
3120
|
+
if (selected === "local") {
|
|
3121
|
+
rejectProviderOption(options, "tool-directory", selected, operation);
|
|
3122
|
+
rejectProviderOption(options, "accept-source-sql-risk", selected, operation);
|
|
3123
|
+
if (operation === "backup") {
|
|
3124
|
+
const source = required3(options, "source");
|
|
3125
|
+
const output2 = required3(options, "output");
|
|
3126
|
+
const backupId = optional(options, "backup-id") ? validateNativeDrId(required3(options, "backup-id")) : randomUUID5();
|
|
3127
|
+
const metadata3 = await backupLocalSqlite(source, output2, backupId, { timeoutMs: timeout(options) });
|
|
3128
|
+
return success2(operation, {
|
|
3129
|
+
provider: selected,
|
|
3130
|
+
backupId: metadata3.manifest.backupId,
|
|
3131
|
+
createdAt: metadata3.manifest.createdAt,
|
|
3132
|
+
output: resolve4(output2),
|
|
3133
|
+
bundleBytes: metadata3.bundleBytes,
|
|
3134
|
+
bundleSha256: metadata3.bundleSha256
|
|
3135
|
+
});
|
|
3136
|
+
}
|
|
3137
|
+
const bundle2 = required3(options, "bundle");
|
|
3138
|
+
if (operation === "verify") {
|
|
3139
|
+
const metadata3 = verifyProviderBundle(bundle2, selected);
|
|
3140
|
+
return success2(operation, {
|
|
3141
|
+
provider: selected,
|
|
3142
|
+
backupId: metadata3.manifest.backupId,
|
|
3143
|
+
createdAt: metadata3.manifest.createdAt,
|
|
3144
|
+
bundle: resolve4(bundle2),
|
|
3145
|
+
bundleBytes: metadata3.bundleBytes,
|
|
3146
|
+
bundleSha256: metadata3.bundleSha256
|
|
3147
|
+
});
|
|
3148
|
+
}
|
|
3149
|
+
const target = required3(options, "target");
|
|
3150
|
+
const requestId3 = optional(options, "request-id") ? validateNativeDrId(required3(options, "request-id")) : randomUUID5();
|
|
3151
|
+
const metadata2 = await restoreLocalSqlite(bundle2, target, requestId3, { timeoutMs: timeout(options) });
|
|
3152
|
+
return success2(operation, {
|
|
3153
|
+
provider: selected,
|
|
3154
|
+
requestId: metadata2.requestId,
|
|
3155
|
+
backupId: metadata2.manifest.backupId,
|
|
3156
|
+
target: resolve4(target),
|
|
3157
|
+
bundleSha256: metadata2.bundleSha256
|
|
3158
|
+
});
|
|
3159
|
+
}
|
|
3160
|
+
rejectProviderOption(options, "source", selected, operation);
|
|
3161
|
+
rejectProviderOption(options, "target", selected, operation);
|
|
3162
|
+
rejectProviderOption(options, "timeout-ms", selected, operation);
|
|
3163
|
+
const toolDirectory = optional(options, "tool-directory");
|
|
3164
|
+
if (operation === "backup") {
|
|
3165
|
+
const output2 = required3(options, "output");
|
|
3166
|
+
const backupId = validateNativeDrId(required3(options, "backup-id"));
|
|
3167
|
+
const { metadata: metadata2, directoryDurability } = await backupPostgresBundle(output2, backupId, toolDirectory, dependencies);
|
|
3168
|
+
return success2(operation, {
|
|
3169
|
+
provider: selected,
|
|
3170
|
+
backupId: metadata2.manifest.backupId,
|
|
3171
|
+
createdAt: metadata2.manifest.createdAt,
|
|
3172
|
+
output: resolve4(output2),
|
|
3173
|
+
bundleBytes: metadata2.bundleBytes,
|
|
3174
|
+
bundleSha256: metadata2.bundleSha256,
|
|
3175
|
+
directoryDurability,
|
|
3176
|
+
...directoryDurability === "unavailable" ? { platform: "win32" } : {}
|
|
3177
|
+
});
|
|
3178
|
+
}
|
|
3179
|
+
const bundle = required3(options, "bundle");
|
|
3180
|
+
if (operation === "verify") {
|
|
3181
|
+
const { metadata: metadata2, cleanupDirectoryDurability: cleanupDirectoryDurability2 } = await verifyPostgresBundle(bundle, toolDirectory, dependencies);
|
|
3182
|
+
return success2(operation, {
|
|
3183
|
+
provider: selected,
|
|
3184
|
+
backupId: metadata2.manifest.backupId,
|
|
3185
|
+
createdAt: metadata2.manifest.createdAt,
|
|
3186
|
+
bundle: resolve4(bundle),
|
|
3187
|
+
bundleBytes: metadata2.bundleBytes,
|
|
3188
|
+
bundleSha256: metadata2.bundleSha256,
|
|
3189
|
+
cleanupDirectoryDurability: cleanupDirectoryDurability2,
|
|
3190
|
+
...cleanupDirectoryDurability2 === "unavailable" ? { platform: "win32" } : {}
|
|
3191
|
+
});
|
|
3192
|
+
}
|
|
3193
|
+
if (options["accept-source-sql-risk"] !== true) {
|
|
3194
|
+
fail2("SOURCE_SQL_RISK_NOT_ACCEPTED", "PostgreSQL native restore requires --accept-source-sql-risk");
|
|
3195
|
+
}
|
|
3196
|
+
const requestId2 = validateNativeDrId(required3(options, "request-id"));
|
|
3197
|
+
const { metadata, result, cleanupDirectoryDurability } = await restorePostgresBundle(bundle, requestId2, toolDirectory, dependencies);
|
|
3198
|
+
return success2(operation, {
|
|
3199
|
+
provider: selected,
|
|
3200
|
+
requestId: requestId2,
|
|
3201
|
+
backupId: metadata.manifest.backupId,
|
|
3202
|
+
bundleSha256: metadata.bundleSha256,
|
|
3203
|
+
databaseName: result.databaseName,
|
|
3204
|
+
normalizedClaimedDeliveries: result.normalizedClaimedDeliveries,
|
|
3205
|
+
tableCounts: result.tableCounts,
|
|
3206
|
+
readiness: result.readiness,
|
|
3207
|
+
cleanupDirectoryDurability,
|
|
3208
|
+
...cleanupDirectoryDurability === "unavailable" ? { platform: "win32" } : {}
|
|
3209
|
+
});
|
|
3210
|
+
} catch (error) {
|
|
3211
|
+
if (error instanceof NativeDrCommandError) throw error;
|
|
3212
|
+
if (error instanceof PostgresNativeDrError) throw new NativeDrCommandError(error.code, error.message, error.details);
|
|
3213
|
+
if (error instanceof PrivatePathError) fail2("INSECURE_PATH", "DR path does not satisfy the private path policy");
|
|
3214
|
+
if (error instanceof NativeDrBundleError) fail2("INVALID_DR_BUNDLE", "DR bundle verification failed");
|
|
3215
|
+
fail2("DR_COMMAND_FAILED", "DR command failed");
|
|
3216
|
+
}
|
|
3217
|
+
throw new NativeDrCommandError("DR_COMMAND_FAILED", "DR command failed");
|
|
3218
|
+
}
|
|
212
3219
|
|
|
213
3220
|
// src/cli.ts
|
|
214
3221
|
var SUPPORTED_OPTIONS = /* @__PURE__ */ new Set([
|
|
@@ -226,6 +3233,10 @@ var SUPPORTED_OPTIONS = /* @__PURE__ */ new Set([
|
|
|
226
3233
|
"db",
|
|
227
3234
|
"dead",
|
|
228
3235
|
"delivery-id",
|
|
3236
|
+
"delivery-max-attempts",
|
|
3237
|
+
"delivery-mode",
|
|
3238
|
+
"delivery-policy",
|
|
3239
|
+
"disposition",
|
|
229
3240
|
"error",
|
|
230
3241
|
"expires-at",
|
|
231
3242
|
"force",
|
|
@@ -244,6 +3255,7 @@ var SUPPORTED_OPTIONS = /* @__PURE__ */ new Set([
|
|
|
244
3255
|
"max-interval-ms",
|
|
245
3256
|
"message-id",
|
|
246
3257
|
"metadata",
|
|
3258
|
+
"not-before",
|
|
247
3259
|
"payload",
|
|
248
3260
|
"payload-ciphertext",
|
|
249
3261
|
"payload-mime",
|
|
@@ -264,16 +3276,40 @@ var SUPPORTED_OPTIONS = /* @__PURE__ */ new Set([
|
|
|
264
3276
|
"unacked-by",
|
|
265
3277
|
"url",
|
|
266
3278
|
"workspace",
|
|
3279
|
+
"mailbox",
|
|
3280
|
+
"receipt-state",
|
|
267
3281
|
"identity",
|
|
268
3282
|
"command",
|
|
269
3283
|
"scope",
|
|
3284
|
+
"recipient",
|
|
3285
|
+
"retry-base-ms",
|
|
3286
|
+
"retry-jitter",
|
|
3287
|
+
"retry-max-ms",
|
|
3288
|
+
"role",
|
|
270
3289
|
"runtime",
|
|
271
3290
|
"capability",
|
|
3291
|
+
"state",
|
|
272
3292
|
"max-pages",
|
|
273
|
-
"max-push"
|
|
3293
|
+
"max-push",
|
|
3294
|
+
"latest",
|
|
3295
|
+
"apply",
|
|
3296
|
+
"display-name",
|
|
3297
|
+
"enrollment-file",
|
|
3298
|
+
"credential-id",
|
|
3299
|
+
"gateway-url",
|
|
3300
|
+
"grace-until",
|
|
3301
|
+
"invalidate-immediately",
|
|
3302
|
+
"label",
|
|
3303
|
+
"reason",
|
|
3304
|
+
"request-id",
|
|
3305
|
+
"resume",
|
|
3306
|
+
"runtime-type",
|
|
3307
|
+
"recover-lock",
|
|
3308
|
+
"scope-set",
|
|
3309
|
+
"workspace-name"
|
|
274
3310
|
]);
|
|
275
|
-
var
|
|
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,57 +3366,140 @@ function json(options, key) {
|
|
|
330
3366
|
function since(options) {
|
|
331
3367
|
const raw = one(options, "since");
|
|
332
3368
|
if (!raw) return void 0;
|
|
333
|
-
const
|
|
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}`;
|
|
3439
|
+
}
|
|
3440
|
+
function packageVersion() {
|
|
3441
|
+
const manifest = JSON.parse(readFileSync3(fileURLToPath2(new URL("../package.json", import.meta.url)), "utf8"));
|
|
3442
|
+
if (typeof manifest.version !== "string" || !manifest.version) throw new Error("package version is unavailable");
|
|
3443
|
+
return manifest.version;
|
|
357
3444
|
}
|
|
358
3445
|
function help() {
|
|
359
3446
|
process.stdout.write(`agent-bridge: provider-neutral agent messaging
|
|
360
3447
|
|
|
361
3448
|
Commands:
|
|
362
|
-
init, doctor, status, pending, migrate, sync, demo, join, presence
|
|
363
|
-
send (post), inbox (get), history, acknowledge, claim, extend, ack, nack, watch
|
|
3449
|
+
init, doctor, status, capabilities, pending, migrate, reconcile-legacy-projects, sync, demo, join, presence
|
|
3450
|
+
send (post), inbox (get), sent, history, acknowledge, claim, extend, ack, nack, watch
|
|
3451
|
+
deliveries, dead-letters, delivery-events, cancel, requeue
|
|
3452
|
+
owner <provision|inventory|rotate|revoke>
|
|
3453
|
+
archive <export|verify|import>
|
|
3454
|
+
dr <backup|verify|restore>
|
|
364
3455
|
clients install <codex|claude-code|claude-desktop> --identity <name>
|
|
3456
|
+
|
|
3457
|
+
Options:
|
|
3458
|
+
-V, --version Print the installed package version
|
|
3459
|
+
-h, --help Show this help
|
|
365
3460
|
`);
|
|
366
3461
|
}
|
|
367
3462
|
function rejectUnknownOptions(options) {
|
|
368
3463
|
const unknown = Object.keys(options).filter((key) => !SUPPORTED_OPTIONS.has(key));
|
|
369
3464
|
if (unknown.length) throw new Error(`unknown option: --${unknown[0]}`);
|
|
370
3465
|
}
|
|
3466
|
+
function rejectOptionsOutside(options, allowed, command) {
|
|
3467
|
+
const invalid = Object.keys(options).find((key) => !allowed.has(key));
|
|
3468
|
+
if (invalid) throw new Error(`--${invalid} is not valid for ${command}`);
|
|
3469
|
+
}
|
|
3470
|
+
var CLI_CONTEXT_OPTIONS = /* @__PURE__ */ new Set([
|
|
3471
|
+
"agent",
|
|
3472
|
+
"as",
|
|
3473
|
+
"config",
|
|
3474
|
+
"db",
|
|
3475
|
+
"instance",
|
|
3476
|
+
"json",
|
|
3477
|
+
"key",
|
|
3478
|
+
"provider",
|
|
3479
|
+
"token",
|
|
3480
|
+
"url",
|
|
3481
|
+
"workspace"
|
|
3482
|
+
]);
|
|
3483
|
+
function validateCanonicalCommandOptions(command, options, config) {
|
|
3484
|
+
const contract = operationForCli(command, config.provider, Object.keys(options));
|
|
3485
|
+
if (!contract?.cli) return;
|
|
3486
|
+
const allowed = /* @__PURE__ */ new Set([...CLI_CONTEXT_OPTIONS, ...contract.cli.options]);
|
|
3487
|
+
const invalid = Object.keys(options).find((key) => !allowed.has(key));
|
|
3488
|
+
if (invalid) throw new Error(`--${invalid} is not valid for ${command}`);
|
|
3489
|
+
}
|
|
371
3490
|
async function initialize(options) {
|
|
372
|
-
const
|
|
373
|
-
const configPath = one(options, "config") ?? process.env.AGENT_BRIDGE_CONFIG ??
|
|
374
|
-
if (
|
|
3491
|
+
const home2 = process.env.HOME ?? homedir4();
|
|
3492
|
+
const configPath = one(options, "config") ?? process.env.AGENT_BRIDGE_CONFIG ?? join5(home2, ".agent-bridge", "config");
|
|
3493
|
+
if (existsSync5(configPath) && !boolean(options, "force")) {
|
|
375
3494
|
throw new Error(`Config already exists at ${configPath}; pass --force to replace it`);
|
|
376
3495
|
}
|
|
377
|
-
const
|
|
3496
|
+
const provider3 = one(options, "provider") ?? process.env.AGENT_BRIDGE_PROVIDER ?? "local";
|
|
378
3497
|
if (one(options, "instance")) {
|
|
379
3498
|
throw new Error("--instance is client-scoped; set it in the active client process");
|
|
380
3499
|
}
|
|
381
3500
|
const values = {
|
|
382
|
-
AGENT_BRIDGE_PROVIDER:
|
|
383
|
-
AGENT_BRIDGE_WORKSPACE: one(options, "workspace") ?? process.env.AGENT_BRIDGE_WORKSPACE ?? (
|
|
3501
|
+
AGENT_BRIDGE_PROVIDER: provider3,
|
|
3502
|
+
AGENT_BRIDGE_WORKSPACE: one(options, "workspace") ?? process.env.AGENT_BRIDGE_WORKSPACE ?? (provider3 === "legacy-supabase" || provider3 === "legacy" || provider3 === "supabase" ? "*" : "default"),
|
|
384
3503
|
AGENT_BRIDGE_DB: one(options, "db") ?? process.env.AGENT_BRIDGE_DB,
|
|
385
3504
|
AGENT_BRIDGE_EDGE_DB: process.env.AGENT_BRIDGE_EDGE_DB,
|
|
386
3505
|
AGENT_BRIDGE_URL: one(options, "url") ?? process.env.AGENT_BRIDGE_URL,
|
|
@@ -390,49 +3509,65 @@ async function initialize(options) {
|
|
|
390
3509
|
for (const value of Object.values(values)) {
|
|
391
3510
|
if (value?.includes("\n") || value?.includes("\r")) throw new Error("config values cannot contain newlines");
|
|
392
3511
|
}
|
|
393
|
-
|
|
3512
|
+
preparePrivateOutput(configPath);
|
|
394
3513
|
const temporary = `${configPath}.${process.pid}.tmp`;
|
|
395
|
-
writeFileSync2(
|
|
396
|
-
temporary,
|
|
397
|
-
Object.entries(values).filter((entry) => Boolean(entry[1])).map(([key, value]) => `${key}=${value}`).join("\n") + "\n",
|
|
398
|
-
{ mode: 384 }
|
|
399
|
-
);
|
|
400
3514
|
let config;
|
|
401
3515
|
try {
|
|
3516
|
+
writeFileSync3(
|
|
3517
|
+
temporary,
|
|
3518
|
+
Object.entries(values).filter((entry) => Boolean(entry[1])).map(([key, value]) => `${key}=${value}`).join("\n") + "\n",
|
|
3519
|
+
{ mode: 384 }
|
|
3520
|
+
);
|
|
3521
|
+
securePrivatePath(temporary, "file");
|
|
402
3522
|
config = resolveClientConfig(
|
|
403
3523
|
{
|
|
404
|
-
HOME:
|
|
3524
|
+
HOME: home2,
|
|
405
3525
|
AGENT_BRIDGE_CONFIG: temporary,
|
|
406
3526
|
AGENT_BRIDGE_TOKEN: verificationToken
|
|
407
3527
|
},
|
|
408
3528
|
"agent-bridge-init"
|
|
409
3529
|
);
|
|
410
|
-
const
|
|
3530
|
+
const runtime2 = await createClientRuntime(config);
|
|
411
3531
|
try {
|
|
412
|
-
if (config.provider === "gateway") await
|
|
3532
|
+
if (config.provider === "gateway") await runtime2.store.verifyRemote?.();
|
|
413
3533
|
} finally {
|
|
414
|
-
await
|
|
3534
|
+
await runtime2.close();
|
|
415
3535
|
}
|
|
416
|
-
|
|
3536
|
+
renameSync4(temporary, configPath);
|
|
3537
|
+
verifyPrivatePathAccess(configPath, "file");
|
|
417
3538
|
} catch (error) {
|
|
418
|
-
|
|
3539
|
+
rmSync4(temporary, { force: true });
|
|
419
3540
|
throw error;
|
|
420
3541
|
}
|
|
421
3542
|
output({ status: "ok", config: configPath, provider: config.provider, workspace: config.principal.workspace, identity: "process-scoped", database: config.provider === "local" ? config.databasePath : null });
|
|
422
3543
|
}
|
|
423
3544
|
function configFor(options, command) {
|
|
424
3545
|
const explicit = command === "send" ? one(options, "source") ?? one(options, "agent") : command === "acknowledge" || command === "ack" && Boolean(one(options, "ids")) ? one(options, "agent") : one(options, "as");
|
|
425
|
-
const
|
|
426
|
-
|
|
3546
|
+
const environment = {
|
|
3547
|
+
...process.env,
|
|
3548
|
+
...one(options, "config") ? { AGENT_BRIDGE_CONFIG: one(options, "config") } : {},
|
|
3549
|
+
...one(options, "provider") ? { AGENT_BRIDGE_PROVIDER: one(options, "provider") } : {},
|
|
3550
|
+
...one(options, "db") ? { AGENT_BRIDGE_DB: one(options, "db") } : {},
|
|
3551
|
+
...one(options, "url") ? { AGENT_BRIDGE_URL: one(options, "url") } : {},
|
|
3552
|
+
...one(options, "key") ? { AGENT_BRIDGE_KEY: one(options, "key") } : {},
|
|
3553
|
+
...one(options, "token") ? { AGENT_BRIDGE_TOKEN: one(options, "token") } : {},
|
|
3554
|
+
...one(options, "instance") ? { AGENT_BRIDGE_INSTANCE: one(options, "instance") } : {}
|
|
3555
|
+
};
|
|
427
3556
|
const config = resolveClientConfig(environment, explicit);
|
|
428
|
-
|
|
429
|
-
if (config.provider === "gateway") {
|
|
430
|
-
throw new Error("--
|
|
3557
|
+
const assertedWorkspace = one(options, "workspace");
|
|
3558
|
+
if (config.provider === "gateway" && assertedWorkspace && assertedWorkspace !== config.principal.workspace) {
|
|
3559
|
+
throw new Error("--workspace must match the workspace bound to the gateway credential");
|
|
431
3560
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
)
|
|
3561
|
+
if (config.provider === "legacy-supabase" && assertedWorkspace && assertedWorkspace !== "*") {
|
|
3562
|
+
throw new Error("--workspace is not supported by the global legacy Supabase schema");
|
|
3563
|
+
}
|
|
3564
|
+
if (config.provider === "local" && assertedWorkspace && assertedWorkspace !== config.principal.workspace) {
|
|
3565
|
+
return resolveClientConfig(
|
|
3566
|
+
{ ...environment, AGENT_BRIDGE_WORKSPACE: assertedWorkspace },
|
|
3567
|
+
explicit
|
|
3568
|
+
);
|
|
3569
|
+
}
|
|
3570
|
+
return config;
|
|
436
3571
|
}
|
|
437
3572
|
function draft(options, positionals) {
|
|
438
3573
|
const content = one(options, "content") ?? positionals.join(" ");
|
|
@@ -443,7 +3578,12 @@ function draft(options, positionals) {
|
|
|
443
3578
|
if (one(options, "payload-ref")) envelope.payload_ref = one(options, "payload-ref");
|
|
444
3579
|
if (one(options, "payload-ciphertext")) envelope.payload_ciphertext = one(options, "payload-ciphertext");
|
|
445
3580
|
if (Object.keys(envelope).length) metadata.message_envelope = envelope;
|
|
3581
|
+
const deliveryMode = one(options, "delivery-mode");
|
|
3582
|
+
const deliveryTuning = ["delivery-max-attempts", "retry-base-ms", "retry-max-ms", "retry-jitter", "not-before"].some((key) => one(options, key) !== void 0);
|
|
3583
|
+
if (deliveryTuning && !deliveryMode) throw new Error("--delivery-mode is required with delivery policy flags");
|
|
3584
|
+
if (deliveryMode === "mailbox" && deliveryTuning) throw new Error("mailbox delivery mode does not accept retry or scheduling flags");
|
|
446
3585
|
return {
|
|
3586
|
+
project: one(options, "project"),
|
|
447
3587
|
type: one(options, "type") ?? one(options, "kind") ?? one(options, "category") ?? "operational",
|
|
448
3588
|
content,
|
|
449
3589
|
targets: [...list(options, "target"), ...list(options, "target-agent"), ...list(options, "target-agents")],
|
|
@@ -459,13 +3599,15 @@ function draft(options, positionals) {
|
|
|
459
3599
|
atribReceiptId: one(options, "atrib-receipt-id"),
|
|
460
3600
|
informedBy: list(options, "informed-by"),
|
|
461
3601
|
data: json(options, "data") ?? json(options, "payload"),
|
|
462
|
-
metadata
|
|
3602
|
+
metadata,
|
|
3603
|
+
deliveryPolicy: json(options, "delivery-policy") ?? (deliveryMode ? deliveryMode === "mailbox" ? { mode: "mailbox" } : { mode: deliveryMode, maxAttempts: integer(options, "delivery-max-attempts", 5), retryBaseDelayMs: integer(options, "retry-base-ms", 1e3), retryMaxDelayMs: integer(options, "retry-max-ms", 6e4), retryJitterRatio: Number(one(options, "retry-jitter") ?? 0.2), notBefore: one(options, "not-before") } : void 0)
|
|
463
3604
|
};
|
|
464
3605
|
}
|
|
465
3606
|
async function localDemo() {
|
|
466
|
-
const directory = mkdtempSync(
|
|
467
|
-
const databasePath =
|
|
468
|
-
|
|
3607
|
+
const directory = mkdtempSync(join5(tmpdir(), "agent-bridge-demo-"));
|
|
3608
|
+
const databasePath = join5(directory, "demo.sqlite3");
|
|
3609
|
+
securePrivatePath(directory, "directory");
|
|
3610
|
+
const base = { provider: "local", databasePath, edgeDatabasePath: join5(directory, "edge.sqlite3"), cursorPath: join5(directory, "cursor"), configPath: join5(directory, "config"), credential: void 0, url: void 0 };
|
|
469
3611
|
let sender;
|
|
470
3612
|
let worker;
|
|
471
3613
|
try {
|
|
@@ -482,34 +3624,70 @@ async function localDemo() {
|
|
|
482
3624
|
} finally {
|
|
483
3625
|
await worker?.close();
|
|
484
3626
|
await sender?.close();
|
|
485
|
-
|
|
3627
|
+
rmSync4(directory, { recursive: true, force: true });
|
|
486
3628
|
}
|
|
487
3629
|
}
|
|
488
3630
|
async function runCli(argv = process.argv.slice(2)) {
|
|
489
|
-
|
|
3631
|
+
if (argv[0] === "archive") {
|
|
3632
|
+
output(await runArchiveCommand(argv.slice(1), process.env));
|
|
3633
|
+
return;
|
|
3634
|
+
}
|
|
3635
|
+
if (argv[0] === "dr") {
|
|
3636
|
+
output(await runDrCommand(argv.slice(1)));
|
|
3637
|
+
return;
|
|
3638
|
+
}
|
|
3639
|
+
const { command: raw, options, positionals } = parse3(argv);
|
|
490
3640
|
const command = { post: "send", get: "inbox", receipt: "acknowledge" }[raw] ?? raw;
|
|
3641
|
+
if (["-V", "--version"].includes(command)) {
|
|
3642
|
+
process.stdout.write(`${packageVersion()}
|
|
3643
|
+
`);
|
|
3644
|
+
return;
|
|
3645
|
+
}
|
|
491
3646
|
if (["help", "-h", "--help"].includes(command)) {
|
|
492
3647
|
help();
|
|
493
3648
|
return;
|
|
494
3649
|
}
|
|
495
3650
|
rejectUnknownOptions(options);
|
|
3651
|
+
if (command === "owner") {
|
|
3652
|
+
if (positionals.length !== 1) {
|
|
3653
|
+
throw new Error("usage: agent-bridge owner <provision|inventory|rotate|revoke>");
|
|
3654
|
+
}
|
|
3655
|
+
const ownerOptions = {};
|
|
3656
|
+
for (const [key, value] of Object.entries(options)) {
|
|
3657
|
+
if (Array.isArray(value)) throw new Error("--" + key + " may only be provided once");
|
|
3658
|
+
ownerOptions[key] = value;
|
|
3659
|
+
}
|
|
3660
|
+
output(await runOwnerCommand(positionals[0], ownerOptions, process.env));
|
|
3661
|
+
return;
|
|
3662
|
+
}
|
|
496
3663
|
if (command === "clients") {
|
|
497
|
-
if (positionals[0] !== "install" || !positionals[1]) {
|
|
3664
|
+
if (positionals.length !== 2 || positionals[0] !== "install" || !positionals[1]) {
|
|
498
3665
|
throw new Error("usage: agent-bridge clients install <runtime> --identity <name>");
|
|
499
3666
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
3667
|
+
rejectOptionsOutside(options, /* @__PURE__ */ new Set([
|
|
3668
|
+
"identity",
|
|
3669
|
+
"command",
|
|
3670
|
+
"scope",
|
|
3671
|
+
"instance",
|
|
3672
|
+
"token",
|
|
3673
|
+
"enrollment-file",
|
|
3674
|
+
"recover-lock"
|
|
3675
|
+
]), "clients install");
|
|
3676
|
+
const runtime3 = positionals[1];
|
|
3677
|
+
if (runtime3 && !["codex", "claude-code", "claude-desktop"].includes(runtime3)) {
|
|
3678
|
+
throw new Error(`unsupported install runtime: ${runtime3}`);
|
|
503
3679
|
}
|
|
504
3680
|
const scope = one(options, "scope");
|
|
505
3681
|
if (scope && !["local", "user", "project"].includes(scope)) {
|
|
506
3682
|
throw new Error("--scope must be local, user, or project");
|
|
507
3683
|
}
|
|
508
|
-
output(installClient(
|
|
3684
|
+
output(installClient(runtime3, one(options, "identity") ?? "", {
|
|
509
3685
|
command: one(options, "command"),
|
|
510
3686
|
scope,
|
|
511
3687
|
instance: one(options, "instance"),
|
|
512
3688
|
token: one(options, "token"),
|
|
3689
|
+
enrollmentFile: one(options, "enrollment-file"),
|
|
3690
|
+
recoverLock: boolean(options, "recover-lock"),
|
|
513
3691
|
env: process.env
|
|
514
3692
|
}));
|
|
515
3693
|
return;
|
|
@@ -525,11 +3703,11 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
525
3703
|
if (command === "migrate") {
|
|
526
3704
|
const databaseUrl = process.env.AGENT_BRIDGE_DATABASE_URL;
|
|
527
3705
|
if (!databaseUrl) throw new Error("AGENT_BRIDGE_DATABASE_URL is required");
|
|
528
|
-
const pool = new
|
|
3706
|
+
const pool = new pg3.Pool({ connectionString: databaseUrl, max: 1 });
|
|
529
3707
|
try {
|
|
530
3708
|
const applied = await runMigrations(
|
|
531
3709
|
pool,
|
|
532
|
-
|
|
3710
|
+
fileURLToPath2(new URL("../sql/migrations", import.meta.url))
|
|
533
3711
|
);
|
|
534
3712
|
output({ status: "ok", applied });
|
|
535
3713
|
} finally {
|
|
@@ -537,19 +3715,99 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
537
3715
|
}
|
|
538
3716
|
return;
|
|
539
3717
|
}
|
|
3718
|
+
if (command === "reconcile-legacy-projects") {
|
|
3719
|
+
const databaseUrl = process.env.AGENT_BRIDGE_DATABASE_URL;
|
|
3720
|
+
if (!databaseUrl) throw new Error("AGENT_BRIDGE_DATABASE_URL is required");
|
|
3721
|
+
const pool = new pg3.Pool({ connectionString: databaseUrl, max: 1 });
|
|
3722
|
+
try {
|
|
3723
|
+
const directory = fileURLToPath2(new URL("../sql/migrations", import.meta.url));
|
|
3724
|
+
const migration = (await loadMigrationPlan(directory)).find((entry) => entry.version === 8);
|
|
3725
|
+
if (!migration || migration.name !== "message_projects") {
|
|
3726
|
+
throw new Error("migration 008_message_projects is missing from the migration plan");
|
|
3727
|
+
}
|
|
3728
|
+
output(await reconcileLegacyProjects(pool, {
|
|
3729
|
+
apply: boolean(options, "apply"),
|
|
3730
|
+
migrationChecksum: migration.checksum
|
|
3731
|
+
}));
|
|
3732
|
+
} finally {
|
|
3733
|
+
await pool.end();
|
|
3734
|
+
}
|
|
3735
|
+
return;
|
|
3736
|
+
}
|
|
540
3737
|
const config = configFor(options, command);
|
|
541
|
-
|
|
3738
|
+
validateCanonicalCommandOptions(raw, options, config);
|
|
3739
|
+
const assertedUnackedBy = one(options, "unacked-by");
|
|
3740
|
+
if (assertedUnackedBy !== void 0 && assertedUnackedBy !== config.principal.agent) throw new Error("--unacked-by must equal the configured principal");
|
|
3741
|
+
if (command === "capabilities") {
|
|
3742
|
+
validateRequest("capabilities", {});
|
|
3743
|
+
cliOutput("capabilities", capabilityDocument({ surface: "cli", provider: config.provider }));
|
|
3744
|
+
return;
|
|
3745
|
+
}
|
|
3746
|
+
let runtime2;
|
|
3747
|
+
try {
|
|
3748
|
+
runtime2 = await createClientRuntime(config, {
|
|
3749
|
+
autoSync: command !== "doctor" && command !== "status",
|
|
3750
|
+
initializationMode: command === "status" ? "passive" : "active"
|
|
3751
|
+
});
|
|
3752
|
+
} catch (error) {
|
|
3753
|
+
if (command !== "doctor" && command !== "status") throw error;
|
|
3754
|
+
const remoteInitialization = command === "doctor" && config.provider === "legacy-supabase";
|
|
3755
|
+
failedClientStatus(config, error, {
|
|
3756
|
+
component: remoteInitialization ? "remote" : "local-store",
|
|
3757
|
+
remoteAttempted: remoteInitialization
|
|
3758
|
+
});
|
|
3759
|
+
process.exitCode = 1;
|
|
3760
|
+
return;
|
|
3761
|
+
}
|
|
542
3762
|
try {
|
|
543
3763
|
if (command === "doctor" || command === "status") {
|
|
544
|
-
|
|
545
|
-
|
|
3764
|
+
validateRequest("client_status", {});
|
|
3765
|
+
const active = command === "doctor";
|
|
3766
|
+
try {
|
|
3767
|
+
const diagnostics = await runtime2.store.diagnostics?.(config.principal, { mode: active ? "probe" : "snapshot" }) ?? { schemaVersion: config.provider === "legacy-supabase" ? "legacy-v1" : "local-v2", deliverySupported: false, pending: null, claimed: null, retrying: null, dead: null };
|
|
3768
|
+
const remoteReachable = "remoteReachable" in diagnostics ? diagnostics.remoteReachable ?? null : active && config.provider === "legacy-supabase" ? true : null;
|
|
3769
|
+
const checks = [{
|
|
3770
|
+
name: "local-store",
|
|
3771
|
+
status: "ok",
|
|
3772
|
+
message: "local diagnostics are readable"
|
|
3773
|
+
}];
|
|
3774
|
+
if (config.provider !== "local") {
|
|
3775
|
+
const provider3 = config.provider === "gateway" ? "gateway" : "legacy provider";
|
|
3776
|
+
const remoteError2 = "remoteError" in diagnostics && typeof diagnostics.remoteError === "string" ? diagnostics.remoteError : void 0;
|
|
3777
|
+
checks.push({
|
|
3778
|
+
name: "remote",
|
|
3779
|
+
status: remoteReachable === false ? "degraded" : remoteError2 ? "failed" : remoteReachable === true ? "ok" : "unknown",
|
|
3780
|
+
message: remoteReachable === false ? `${provider3} is unreachable` : remoteError2 ? `${provider3} rejected diagnostics (${remoteError2})` : remoteReachable === true ? `${provider3} is reachable` : `${provider3} reachability has not been checked`
|
|
3781
|
+
});
|
|
3782
|
+
}
|
|
3783
|
+
const blocked = "outboxBlocked" in diagnostics ? Number(diagnostics.outboxBlocked ?? 0) : 0;
|
|
3784
|
+
checks.push({ name: "blocked-outbox", status: blocked > 0 ? "degraded" : "ok", message: blocked > 0 ? `${blocked} outbound message(s) require intervention` : "no blocked outbound messages" });
|
|
3785
|
+
const expired = Number(diagnostics.expiredLeases ?? 0);
|
|
3786
|
+
checks.push({ name: "expired-leases", status: expired > 0 ? "degraded" : "ok", message: expired > 0 ? `${expired} delivery lease(s) are expired` : "no expired delivery leases" });
|
|
3787
|
+
const dead = Number(diagnostics.dead ?? 0);
|
|
3788
|
+
checks.push({ name: "dead-deliveries", status: dead > 0 ? "degraded" : "ok", message: dead > 0 ? `${dead} dead delivery/deliveries are visible` : "no dead deliveries" });
|
|
3789
|
+
const status = checks.some((check) => check.status === "failed") ? "failed" : checks.some((check) => check.status === "degraded") ? "degraded" : checks.some((check) => check.status === "unknown") ? "unknown" : "ok";
|
|
3790
|
+
const remoteError = "remoteError" in diagnostics && typeof diagnostics.remoteError === "string";
|
|
3791
|
+
const connected = config.provider === "local" ? true : remoteReachable === true && !remoteError;
|
|
3792
|
+
const queue = { ...diagnostics };
|
|
3793
|
+
delete queue.syncLoopState;
|
|
3794
|
+
delete queue.syncLoopError;
|
|
3795
|
+
cliOutput("client_status", { status, localHealthy: true, connected, remoteReachable, provider: config.provider, workspace: config.principal.workspace, agent: config.principal.agent, instance: config.principal.instance ?? null, schemaVersion: diagnostics.schemaVersion, endpoint: config.url ?? null, database: config.provider === "local" ? config.databasePath : null, cursorPath: config.cursorPath, lastCursor: cursor(config.cursorPath) ?? null, queue, checks });
|
|
3796
|
+
if (active && status !== "ok") process.exitCode = status === "failed" ? 1 : 2;
|
|
3797
|
+
} catch (error) {
|
|
3798
|
+
failedClientStatus(config, error, { component: "local-store" });
|
|
3799
|
+
process.exitCode = 1;
|
|
3800
|
+
}
|
|
546
3801
|
return;
|
|
547
3802
|
}
|
|
548
3803
|
if (command === "pending") {
|
|
549
|
-
const diagnostics = await
|
|
550
|
-
|
|
3804
|
+
const diagnostics = await runtime2.store.diagnostics?.(config.principal, {
|
|
3805
|
+
mode: config.provider === "gateway" ? "probe" : "snapshot"
|
|
3806
|
+
});
|
|
3807
|
+
const page = await runtime2.service.history(config.principal, {
|
|
551
3808
|
limit: 1,
|
|
552
|
-
|
|
3809
|
+
project: one(options, "project"),
|
|
3810
|
+
receiptState: "unread"
|
|
553
3811
|
});
|
|
554
3812
|
const pendingDeliveries = diagnostics?.pending ?? 0;
|
|
555
3813
|
const retryingDeliveries = diagnostics?.retrying ?? 0;
|
|
@@ -557,55 +3815,91 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
557
3815
|
const deliveryAvailable = pendingDeliveries > 0 || retryingDeliveries > 0 && oldest <= Date.now();
|
|
558
3816
|
const unread = page.messages.length > 0;
|
|
559
3817
|
const available = deliveryAvailable || unread;
|
|
3818
|
+
const diagnosticsAuthoritative = !diagnostics || !("remoteReachable" in diagnostics) || diagnostics.remoteReachable === true;
|
|
3819
|
+
const pageAuthority = page;
|
|
3820
|
+
const historyAuthoritative = pageAuthority.acknowledgements !== "unknown" && pageAuthority.source !== "cache" && pageAuthority.stale !== true;
|
|
3821
|
+
const authoritative = diagnosticsAuthoritative && historyAuthoritative;
|
|
560
3822
|
output({
|
|
561
3823
|
available,
|
|
562
3824
|
unread,
|
|
563
3825
|
deliveryAvailable,
|
|
564
3826
|
pending: pendingDeliveries,
|
|
565
3827
|
retrying: retryingDeliveries,
|
|
566
|
-
authoritative
|
|
3828
|
+
authoritative,
|
|
3829
|
+
state: available ? "available" : authoritative ? "empty" : "unknown"
|
|
567
3830
|
});
|
|
568
|
-
if (!available) process.exitCode = 1;
|
|
3831
|
+
if (!available) process.exitCode = authoritative ? 1 : 2;
|
|
569
3832
|
return;
|
|
570
3833
|
}
|
|
571
3834
|
if (command === "send") {
|
|
572
|
-
|
|
3835
|
+
const message = draft(options, positionals);
|
|
3836
|
+
validateRequest("publish_message", { ...message, source: one(options, "source") });
|
|
3837
|
+
cliOutput("publish_message", await runtime2.service.publish(config.principal, message));
|
|
573
3838
|
return;
|
|
574
3839
|
}
|
|
575
3840
|
if (command === "join") {
|
|
576
|
-
|
|
3841
|
+
const input = validateRequest("heartbeat", { leaseMs: integer(options, "lease-ms", 6e4), runtimeType: one(options, "runtime"), capabilities: list(options, "capability") });
|
|
3842
|
+
cliOutput("heartbeat", await runtime2.service.heartbeat(config.principal, input));
|
|
577
3843
|
return;
|
|
578
3844
|
}
|
|
579
3845
|
if (command === "presence") {
|
|
580
|
-
|
|
3846
|
+
validateRequest("presence", {});
|
|
3847
|
+
cliOutput("presence", { agents: await runtime2.service.presence(config.principal) });
|
|
581
3848
|
return;
|
|
582
3849
|
}
|
|
583
3850
|
if (command === "sync") {
|
|
584
|
-
if (!
|
|
585
|
-
|
|
3851
|
+
if (!runtime2.store.sync) throw new Error("sync is available only with the gateway provider");
|
|
3852
|
+
const input = validateRequest("sync", {
|
|
586
3853
|
maxPush: integer(options, "max-push", integer(options, "limit", 100)),
|
|
587
3854
|
maxPages: integer(options, "max-pages", 20)
|
|
588
|
-
})
|
|
3855
|
+
});
|
|
3856
|
+
cliOutput("sync", await runtime2.store.sync(input));
|
|
589
3857
|
return;
|
|
590
3858
|
}
|
|
591
|
-
if (command === "history" || command === "inbox") {
|
|
592
|
-
const
|
|
3859
|
+
if (command === "history" || command === "inbox" || command === "sent") {
|
|
3860
|
+
const query = validateRequest("history", { cursor: one(options, "cursor"), limit: integer(options, "limit", 20), types: list(options, "type").concat(list(options, "category")), source: one(options, "source"), project: one(options, "project"), since: since(options), mailbox: command === "inbox" ? "inbox" : command === "sent" ? "sent" : one(options, "mailbox"), receiptState: one(options, "receipt-state"), unacknowledgedBy: assertedUnackedBy, threadId: one(options, "thread-id"), latest: raw === "get" || boolean(options, "latest") });
|
|
3861
|
+
const page = await runtime2.service.history(config.principal, query);
|
|
593
3862
|
if (raw === "get") {
|
|
594
|
-
|
|
595
|
-
} else
|
|
3863
|
+
cliOutput("history", page.messages.map((message) => ({ id: config.provider === "legacy-supabase" ? Number(message.sequence) : message.id, source: message.source, category: message.type, content: message.content, priority: message.priority, project: message.project ?? null, metadata: legacyContextMetadata(message), created_at: message.createdAt })), { command: raw, optionNames: Object.keys(options) });
|
|
3864
|
+
} else cliOutput("history", page);
|
|
596
3865
|
return;
|
|
597
3866
|
}
|
|
598
3867
|
if (command === "acknowledge" || command === "ack" && one(options, "ids")) {
|
|
599
3868
|
const rawIds = list(options, "ids");
|
|
600
|
-
|
|
3869
|
+
validateRequest("record_receipt", { messageIds: rawIds });
|
|
3870
|
+
const acknowledged = config.provider === "legacy-supabase" && runtime2.store.recordLegacyReceipt && rawIds.every((id) => /^\d+$/.test(id)) ? await runtime2.store.recordLegacyReceipt(rawIds, config.principal.agent) : await runtime2.service.acknowledge(
|
|
601
3871
|
config.principal,
|
|
602
3872
|
rawIds.map(legacyNumericMessageId)
|
|
603
3873
|
);
|
|
604
|
-
|
|
3874
|
+
cliOutput("record_receipt", { acknowledged, agent: config.principal.agent }, {
|
|
3875
|
+
command: raw,
|
|
3876
|
+
optionNames: Object.keys(options)
|
|
3877
|
+
});
|
|
605
3878
|
return;
|
|
606
3879
|
}
|
|
607
3880
|
if (command === "claim") {
|
|
608
|
-
|
|
3881
|
+
const input = validateRequest("claim_delivery", { leaseMs: integer(options, "lease-ms", 3e4), maxAttempts: one(options, "max-attempts") === void 0 ? void 0 : integer(options, "max-attempts", 5) });
|
|
3882
|
+
cliOutput("claim_delivery", await runtime2.service.claim(config.principal, input));
|
|
3883
|
+
return;
|
|
3884
|
+
}
|
|
3885
|
+
if (command === "deliveries" || command === "dead-letters") {
|
|
3886
|
+
const input = validateRequest("list_deliveries", { cursor: one(options, "cursor"), limit: integer(options, "limit", 50), role: one(options, "role"), messageId: one(options, "message-id"), recipient: one(options, "recipient"), states: command === "dead-letters" ? ["dead"] : list(options, "state") });
|
|
3887
|
+
cliOutput("list_deliveries", await runtime2.service.deliveries(config.principal, input));
|
|
3888
|
+
return;
|
|
3889
|
+
}
|
|
3890
|
+
if (command === "delivery-events") {
|
|
3891
|
+
const input = validateRequest("list_delivery_events", { deliveryId: one(options, "delivery-id") ?? positionals[0], cursor: one(options, "cursor"), limit: integer(options, "limit", 50) });
|
|
3892
|
+
cliOutput("list_delivery_events", await runtime2.service.deliveryEvents(config.principal, String(input.deliveryId), input));
|
|
3893
|
+
return;
|
|
3894
|
+
}
|
|
3895
|
+
if (command === "cancel") {
|
|
3896
|
+
const input = validateRequest("cancel_delivery", { deliveryId: one(options, "delivery-id") ?? positionals[0] });
|
|
3897
|
+
cliOutput("cancel_delivery", await runtime2.service.cancel(config.principal, String(input.deliveryId)));
|
|
3898
|
+
return;
|
|
3899
|
+
}
|
|
3900
|
+
if (command === "requeue") {
|
|
3901
|
+
const input = validateRequest("requeue_delivery", { deliveryId: one(options, "delivery-id") ?? positionals[0] });
|
|
3902
|
+
cliOutput("requeue_delivery", await runtime2.service.requeue(config.principal, String(input.deliveryId)));
|
|
609
3903
|
return;
|
|
610
3904
|
}
|
|
611
3905
|
const deliveryId = one(options, "delivery-id") ?? positionals[0];
|
|
@@ -614,15 +3908,19 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
614
3908
|
if (["extend", "ack", "nack"].includes(command)) throw new Error("--delivery-id and --lease-token are required");
|
|
615
3909
|
}
|
|
616
3910
|
if (command === "extend") {
|
|
617
|
-
|
|
3911
|
+
const input = validateRequest("extend_delivery", { deliveryId, leaseToken, leaseMs: integer(options, "lease-ms", 3e4) });
|
|
3912
|
+
cliOutput("extend_delivery", await runtime2.service.extend(config.principal, String(input.deliveryId), String(input.leaseToken), input.leaseMs));
|
|
618
3913
|
return;
|
|
619
3914
|
}
|
|
620
3915
|
if (command === "ack") {
|
|
621
|
-
|
|
3916
|
+
const input = validateRequest("acknowledge_delivery", { deliveryId, leaseToken });
|
|
3917
|
+
cliOutput("acknowledge_delivery", await runtime2.service.ack(config.principal, String(input.deliveryId), String(input.leaseToken)));
|
|
622
3918
|
return;
|
|
623
3919
|
}
|
|
624
3920
|
if (command === "nack") {
|
|
625
|
-
|
|
3921
|
+
const disposition = one(options, "disposition") ?? (boolean(options, "dead") ? "dead" : "retry");
|
|
3922
|
+
const input = validateRequest("negative_acknowledge_delivery", { deliveryId, leaseToken, error: one(options, "error") ?? "negative acknowledgment", disposition });
|
|
3923
|
+
cliOutput("negative_acknowledge_delivery", await runtime2.service.nack(config.principal, String(input.deliveryId), String(input.leaseToken), String(input.error), input.disposition));
|
|
626
3924
|
return;
|
|
627
3925
|
}
|
|
628
3926
|
if (command === "watch") {
|
|
@@ -630,15 +3928,21 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
630
3928
|
const baseInterval = Math.min(integer(options, "interval-ms", 1e3), 6e4);
|
|
631
3929
|
const maxInterval = Math.max(baseInterval, Math.min(integer(options, "max-interval-ms", 3e4), 3e5));
|
|
632
3930
|
let interval = baseInterval;
|
|
633
|
-
|
|
3931
|
+
const project = one(options, "project");
|
|
3932
|
+
const checkpointPath = watchCursorPath(config.cursorPath, project);
|
|
3933
|
+
let current = cursor(checkpointPath);
|
|
634
3934
|
let seen = 0;
|
|
635
3935
|
for (let i = 0; i < polls; i++) {
|
|
636
3936
|
try {
|
|
637
|
-
const page = await
|
|
3937
|
+
const page = await runtime2.service.history(config.principal, {
|
|
3938
|
+
cursor: current,
|
|
3939
|
+
limit: integer(options, "limit", 50),
|
|
3940
|
+
project
|
|
3941
|
+
});
|
|
638
3942
|
for (const message of page.messages) output(message);
|
|
639
3943
|
seen += page.messages.length;
|
|
640
3944
|
current = page.cursor;
|
|
641
|
-
saveCursor(
|
|
3945
|
+
saveCursor(checkpointPath, current);
|
|
642
3946
|
interval = page.messages.length ? baseInterval : Math.min(maxInterval, Math.max(baseInterval, interval * 2));
|
|
643
3947
|
} catch (error) {
|
|
644
3948
|
const status = error instanceof BridgeHttpError || error instanceof LegacySupabaseError ? error.status : void 0;
|
|
@@ -648,7 +3952,7 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
648
3952
|
}
|
|
649
3953
|
if (i + 1 < polls) {
|
|
650
3954
|
const jittered = Math.round(interval * (0.9 + Math.random() * 0.2));
|
|
651
|
-
await new Promise((
|
|
3955
|
+
await new Promise((resolve5) => setTimeout(resolve5, jittered));
|
|
652
3956
|
}
|
|
653
3957
|
}
|
|
654
3958
|
if (!seen && boolean(options, "json")) output({ status: "ok", messages: 0, cursor: current ?? null });
|
|
@@ -656,15 +3960,52 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
656
3960
|
}
|
|
657
3961
|
throw new Error(`Unknown command: ${raw}`);
|
|
658
3962
|
} finally {
|
|
659
|
-
await
|
|
3963
|
+
await runtime2.close();
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
function formatCliError(error, argv = process.argv.slice(2)) {
|
|
3967
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3968
|
+
if (argv[0] === "archive") {
|
|
3969
|
+
return JSON.stringify({
|
|
3970
|
+
schemaVersion: 1,
|
|
3971
|
+
status: "error",
|
|
3972
|
+
operation: argv[1] ?? null,
|
|
3973
|
+
error: {
|
|
3974
|
+
code: error instanceof ArchiveCommandError ? error.code : "ARCHIVE_COMMAND_ERROR",
|
|
3975
|
+
message: error instanceof ArchiveCommandError ? error.message : "archive command failed",
|
|
3976
|
+
...error instanceof ArchiveCommandError && error.details ? { details: error.details } : {}
|
|
3977
|
+
}
|
|
3978
|
+
}) + "\n";
|
|
660
3979
|
}
|
|
3980
|
+
if (argv[0] === "dr") {
|
|
3981
|
+
return JSON.stringify({
|
|
3982
|
+
schemaVersion: 1,
|
|
3983
|
+
status: "error",
|
|
3984
|
+
operation: argv[1] ?? null,
|
|
3985
|
+
error: {
|
|
3986
|
+
code: error instanceof NativeDrCommandError ? error.code : "DR_COMMAND_ERROR",
|
|
3987
|
+
message: error instanceof NativeDrCommandError ? error.message : "DR command failed",
|
|
3988
|
+
...error instanceof NativeDrCommandError && error.details ? { details: error.details } : {}
|
|
3989
|
+
}
|
|
3990
|
+
}) + "\n";
|
|
3991
|
+
}
|
|
3992
|
+
if (argv[0] === "owner") {
|
|
3993
|
+
return JSON.stringify({
|
|
3994
|
+
schemaVersion: 1,
|
|
3995
|
+
status: "error",
|
|
3996
|
+
operation: argv[1] ?? null,
|
|
3997
|
+
error: { code: "OWNER_COMMAND_ERROR", message }
|
|
3998
|
+
}) + "\n";
|
|
3999
|
+
}
|
|
4000
|
+
return `Error: ${message}
|
|
4001
|
+
`;
|
|
661
4002
|
}
|
|
662
4003
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) runCli().catch((error) => {
|
|
663
|
-
process.stderr.write(
|
|
664
|
-
`);
|
|
4004
|
+
process.stderr.write(formatCliError(error));
|
|
665
4005
|
process.exitCode = 1;
|
|
666
4006
|
});
|
|
667
4007
|
export {
|
|
4008
|
+
formatCliError,
|
|
668
4009
|
runCli
|
|
669
4010
|
};
|
|
670
4011
|
//# sourceMappingURL=cli.js.map
|