rovloom 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +9 -1
  2. package/dist/src/admin/index.js +1 -1
  3. package/dist/src/{chunk-emcefrc1.js → chunk-0e9gg4sg.js} +3 -4
  4. package/dist/src/chunk-1xer1ent.js +102 -0
  5. package/dist/src/{chunk-863tk45b.js → chunk-41fny9qm.js} +5 -5
  6. package/dist/src/{chunk-zjzrcc59.js → chunk-59h200cr.js} +139 -7
  7. package/dist/src/chunk-5w5srj3b.js +93 -0
  8. package/dist/src/{chunk-d4ejrtjq.js → chunk-6n6b1xpy.js} +1 -1
  9. package/dist/src/{chunk-ympzv86c.js → chunk-6w4g9fw8.js} +16 -4
  10. package/dist/src/{chunk-smpevv7n.js → chunk-73aq2qkz.js} +9 -16
  11. package/dist/src/{chunk-0ake7e4r.js → chunk-8c9yc0ge.js} +9 -17
  12. package/dist/src/{chunk-57ayxqnb.js → chunk-8fy8w7ds.js} +2 -23
  13. package/dist/src/{chunk-64ftwe9b.js → chunk-8ykrqhxh.js} +8 -2
  14. package/dist/src/{chunk-2tn0r64b.js → chunk-9h9x79bx.js} +5 -5
  15. package/dist/src/{chunk-nyn75b8v.js → chunk-av31bx1e.js} +31 -2
  16. package/dist/src/{chunk-0xgpxdgc.js → chunk-bbdtsg4b.js} +7 -5
  17. package/dist/src/{chunk-q5pgbwb3.js → chunk-e7jgpxkf.js} +2 -2
  18. package/dist/src/{chunk-zj48jvfe.js → chunk-efty77sb.js} +3 -40
  19. package/dist/src/{chunk-abd4envz.js → chunk-enj7s8cv.js} +71 -78
  20. package/dist/src/{chunk-4etdq0cp.js → chunk-ftpy8xx3.js} +313 -28
  21. package/dist/src/{chunk-zdxww7ms.js → chunk-h3xyq7zk.js} +1 -1
  22. package/dist/src/{chunk-ttsrctxg.js → chunk-hxmq28pg.js} +9 -7
  23. package/dist/src/{chunk-r0nfthsf.js → chunk-j59ac0xs.js} +2 -2
  24. package/dist/src/{chunk-5s2emvww.js → chunk-j6svdqr8.js} +10 -302
  25. package/dist/src/{chunk-9y36gntb.js → chunk-jkzfck98.js} +10 -5
  26. package/dist/src/{chunk-sj24fpz3.js → chunk-m76cmkfy.js} +28 -30
  27. package/dist/src/{chunk-5n8csngz.js → chunk-m96mbzcw.js} +816 -305
  28. package/dist/src/{chunk-xhq21g24.js → chunk-m9c1pnv9.js} +3 -3
  29. package/dist/src/{chunk-fnakrk0a.js → chunk-n3z3jaxy.js} +39 -36
  30. package/dist/src/{chunk-e7x51y0t.js → chunk-ndj0ay2p.js} +19 -18
  31. package/dist/src/chunk-pgs9jp6t.js +320 -0
  32. package/dist/src/chunk-pksaqt9a.js +25 -0
  33. package/dist/src/{chunk-ebm3kvnq.js → chunk-pnep7yhj.js} +31 -100
  34. package/dist/src/chunk-qn5xae7g.js +66 -0
  35. package/dist/src/{chunk-0pewfj89.js → chunk-qp8tgms2.js} +1 -1
  36. package/dist/src/{chunk-7k9g0x5e.js → chunk-rmgw8h27.js} +10 -8
  37. package/dist/src/{chunk-gbsbwssz.js → chunk-sf4ztxba.js} +3 -3
  38. package/dist/src/{chunk-7q96e161.js → chunk-shez7hkc.js} +6 -12
  39. package/dist/src/chunk-sqz614bh.js +30 -0
  40. package/dist/src/{chunk-09f9q26d.js → chunk-vdwzb63d.js} +2 -2
  41. package/dist/src/chunk-xtvgm6kf.js +48 -0
  42. package/dist/src/chunk-y8zm75jt.js +157 -0
  43. package/dist/src/chunk-yasqw2vj.js +756 -0
  44. package/dist/src/{chunk-ybte82d3.js → chunk-z0qmfqvd.js} +4 -3
  45. package/dist/src/chunk-zm705snh.js +53 -0
  46. package/dist/src/cli/index.js +39 -30
  47. package/dist/src/cli/main.js +14 -9
  48. package/dist/src/connector/herdr/installation.js +4 -4
  49. package/dist/src/connector/herdr/provider.js +9 -9
  50. package/dist/src/connector/herdr/runtime.js +7 -7
  51. package/dist/src/connector/luvus/index.js +7 -6
  52. package/dist/src/mcp/index.js +16 -14
  53. package/dist/src/runtime/index.js +16 -15
  54. package/dist/src/service/index.js +3 -4
  55. package/dist/src/store.js +4 -4
  56. package/dist/src/transport/index.js +9 -9
  57. package/dist/src/webui/index.js +975 -834
  58. package/package.json +1 -1
  59. package/dist/src/chunk-5j0gj6w2.js +0 -7
  60. package/dist/src/chunk-9yg6x3gt.js +0 -24
  61. package/dist/src/chunk-erdq0rn8.js +0 -1300
  62. package/dist/src/chunk-n073xnr1.js +0 -1117
  63. package/dist/src/chunk-rwpynx2j.js +0 -51
  64. package/dist/src/chunk-vwcntax3.js +0 -228
@@ -1,1117 +0,0 @@
1
- // @bun
2
- import {
3
- parseNativeJson2
4
- } from "./chunk-7crhmqn2.js";
5
- import {
6
- object2,
7
- fields2,
8
- id2,
9
- sha2,
10
- boundedJson2,
11
- hash2
12
- } from "./chunk-qt9m2kg2.js";
13
- import {
14
- HERDR_WORKFLOW_METHODS
15
- } from "./chunk-9tt13dm0.js";
16
- import {
17
- LUVUS_OPERATION_METHODS2
18
- } from "./chunk-zj48jvfe.js";
19
- import {
20
- compileHerdrSchemas2
21
- } from "./chunk-5h82060k.js";
22
- import {
23
- createHerdrCodec2
24
- } from "./chunk-zb641qa3.js";
25
- import {
26
- parseHerdrProviderRefConfig2,
27
- createHerdrGatewayResources2
28
- } from "./chunk-2tn0r64b.js";
29
- import {
30
- checkedRemoteLuvusConfig,
31
- createPinnedRemoteLuvus
32
- } from "./chunk-5s2emvww.js";
33
- import {
34
- writeAdminFiles2,
35
- managementSourceExists2
36
- } from "./chunk-fnakrk0a.js";
37
- import {
38
- setupFileStamp,
39
- writeSetupFile,
40
- readSetupJson,
41
- writeSetupJson,
42
- REGISTRY_MAX_BYTES,
43
- parseRegistry,
44
- isAdminPlaceholder
45
- } from "./chunk-ebm3kvnq.js";
46
- import {
47
- readRuntimeConfig2
48
- } from "./chunk-bspzg8er.js";
49
- import {
50
- runtimeReference,
51
- readRuntimeFile,
52
- readRuntimeJson
53
- } from "./chunk-z7kkfrgz.js";
54
- import {
55
- luvusMode,
56
- CliError2
57
- } from "./chunk-hpzr94hm.js";
58
- import {
59
- canonicalLoopbackHost
60
- } from "./chunk-6n9s7nfe.js";
61
- import {
62
- PairingAdmin2,
63
- createPairingDelivery2
64
- } from "./chunk-vwcntax3.js";
65
-
66
- // src/cli/pairing-http.ts
67
- import { createHash, timingSafeEqual } from "crypto";
68
-
69
- // src/cli/setup-policy.ts
70
- function setupSessions(input) {
71
- if (input === undefined) {
72
- return Object.freeze({});
73
- }
74
- const row = input;
75
- if (row === null || typeof row !== "object" || Array.isArray(row) || Object.keys(row).some((key) => key !== "luvus") || row.luvus !== undefined && row.luvus !== "default" && row.luvus !== "rovloom") {
76
- throw new CliError2("configuration_required");
77
- }
78
- return Object.freeze(row.luvus === undefined ? {} : { luvus: row.luvus });
79
- }
80
- function setupTarget(machine, kind = "luvus", sessions = {}) {
81
- return {
82
- machine_id: machine,
83
- provider_id: kind,
84
- profile: "session",
85
- session: kind === "luvus" ? sessions.luvus ?? "rovloom" : "default"
86
- };
87
- }
88
- function setupProviderKinds(input) {
89
- if (!Array.isArray(input) || input.some((kind) => kind !== "luvus" && kind !== "herdr") || !["luvus", "herdr", "luvus,herdr"].includes(input.join(","))) {
90
- throw new CliError2("configuration_required");
91
- }
92
- return Object.freeze([...input]);
93
- }
94
- function selectedSetupPolicies(plan, machine, kinds, sessions = {}) {
95
- const grants = setupProviderKinds(kinds).map((kind) => ({
96
- target: setupTarget(machine, kind, sessions),
97
- methods: [...kind === "luvus" ? LUVUS_OPERATION_METHODS2 : HERDR_WORKFLOW_METHODS],
98
- actions: ["dispatch", "read", "cancel"]
99
- }));
100
- return {
101
- principal_id: plan.actor,
102
- policies: {
103
- caller: { revision: "setup_caller_" + (plan.lineage ?? plan.attempt_id), grants },
104
- local: { revision: "setup_local_" + (plan.lineage ?? plan.attempt_id), grants: structuredClone(grants) }
105
- }
106
- };
107
- }
108
- function setupReference(attempt, kind = "luvus") {
109
- return kind + "_" + attempt;
110
- }
111
- function setupSessionsFor(config) {
112
- return Object.freeze({ luvus: luvusMode(config) === "attach" ? "default" : "rovloom" });
113
- }
114
-
115
- // src/cli/pairing-http.ts
116
- function setupReply(value, status = 200) {
117
- return new Response(JSON.stringify(value), {
118
- status,
119
- headers: {
120
- "content-type": "application/json",
121
- "cache-control": "no-store",
122
- "x-content-type-options": "nosniff"
123
- }
124
- });
125
- }
126
- async function requestJson(request, maximum, stopped) {
127
- const signal = AbortSignal.any([request.signal, stopped, AbortSignal.timeout(5000)]);
128
- const reader = request.body?.getReader();
129
- if (!reader) {
130
- throw new Error("setup_request_invalid");
131
- }
132
- const chunks = [];
133
- let bytes = 0;
134
- try {
135
- while (true) {
136
- let abort;
137
- const cancelled = new Promise((_resolve, reject) => {
138
- abort = () => reject(new Error("setup_request_closed"));
139
- signal.addEventListener("abort", abort, { once: true });
140
- });
141
- let result;
142
- try {
143
- if (signal.aborted) {
144
- throw new Error("setup_request_closed");
145
- }
146
- result = await Promise.race([reader.read(), cancelled]);
147
- } finally {
148
- signal.removeEventListener("abort", abort);
149
- }
150
- if (result.done) {
151
- break;
152
- }
153
- bytes += result.value.length;
154
- if (bytes > maximum || signal.aborted) {
155
- throw new Error("setup_request_invalid");
156
- }
157
- chunks.push(result.value);
158
- }
159
- const parsed = parseNativeJson2(Buffer.concat(chunks, bytes), { object_bytes: maximum });
160
- if (!parsed.ok) {
161
- throw new Error("setup_request_invalid");
162
- }
163
- return object2(parsed.value);
164
- } finally {
165
- reader.cancel().catch(() => {});
166
- reader.releaseLock();
167
- }
168
- }
169
- function device(value) {
170
- const input = object2(value);
171
- fields2(input, ["machine_id", "recovery_generation", "credential"]);
172
- id2(input.machine_id);
173
- id2(input.recovery_generation);
174
- if (typeof input.credential !== "string" || !/^[A-Za-z0-9_-]{32,256}$/.test(input.credential)) {
175
- throw new Error("setup_request_invalid");
176
- }
177
- return input;
178
- }
179
-
180
- class GatewayPairingHttp {
181
- state;
182
- approval;
183
- native;
184
- repaired;
185
- #exchanges = new Map;
186
- #requests = new Set;
187
- #stop = new AbortController;
188
- #closed = false;
189
- constructor(state, approval, native, repaired = () => {}) {
190
- this.state = state;
191
- this.approval = approval;
192
- this.native = native;
193
- this.repaired = repaired;
194
- }
195
- async authenticated(credential, run) {
196
- const host = this.state.lifecycle.identityHost;
197
- const active = host.authenticate(credential.machine_id, credential.recovery_generation, credential.credential);
198
- try {
199
- host.assert(active.identity, active.lease);
200
- const value = await run();
201
- host.assert(active.identity, active.lease);
202
- return value;
203
- } finally {
204
- host.disconnect(active.identity);
205
- }
206
- }
207
- handle(request) {
208
- const url = new URL(request.url);
209
- if (!["/pairing/redeem", "/pairing/ack", "/pairing/status", "/pairing/native"].includes(url.pathname)) {
210
- return;
211
- }
212
- if (this.#closed || request.method !== "POST" || request.headers.has("origin") || url.search || url.hash || url.username || url.password || request.headers.get("content-type")?.split(";")[0]?.trim() !== "application/json") {
213
- return Promise.resolve(setupReply({ error: "setup_request_refused" }, 403));
214
- }
215
- if (this.#requests.size >= 16) {
216
- return Promise.resolve(setupReply({ error: "setup_busy" }, 503));
217
- }
218
- const task = this.respond(request, url.pathname);
219
- this.#requests.add(task);
220
- task.then(() => this.#requests.delete(task), () => this.#requests.delete(task));
221
- return task;
222
- }
223
- async respond(request, path) {
224
- try {
225
- const input = await requestJson(request, path === "/pairing/native" ? 1048576 : 16384, this.#stop.signal);
226
- if (this.#closed) {
227
- throw new Error("setup_closed");
228
- }
229
- if (path === "/pairing/redeem") {
230
- return await this.redeem(input);
231
- }
232
- const credential = device(input.authentication);
233
- id2(input.attempt_id);
234
- if (this.approval.read(input.attempt_id).phase === "ready") {
235
- throw new Error("setup_closed");
236
- }
237
- return await this.authenticated(credential, async () => {
238
- const plan = this.approval.read(input.attempt_id);
239
- if (plan.machine_id !== credential.machine_id || plan.generation !== credential.recovery_generation) {
240
- throw new Error("setup_target_mismatch");
241
- }
242
- if (path === "/pairing/ack") {
243
- fields2(input, ["attempt_id", "authentication", "acknowledgement"]);
244
- return await this.acknowledge(input.attempt_id, credential, input.acknowledgement);
245
- }
246
- if (path === "/pairing/status") {
247
- fields2(input, ["attempt_id", "authentication"], ["providers", "sessions"]);
248
- const providers = setupProviderKinds(input.providers ?? ["luvus"]);
249
- const sessions = setupSessions(input.sessions);
250
- if (plan.providers && boundedJson2(plan.providers) !== boundedJson2(providers)) {
251
- throw new Error("setup_provider_selection_changed");
252
- }
253
- return setupReply({
254
- attempt_id: plan.attempt_id,
255
- machine_id: plan.machine_id,
256
- recovery_generation: plan.generation,
257
- phase: plan.phase,
258
- ...plan.lineage ? { lineage: plan.lineage } : {},
259
- authorization: selectedSetupPolicies(plan, credential.machine_id, providers, sessions)
260
- });
261
- }
262
- fields2(input, ["attempt_id", "authentication", "installation"]);
263
- if (!["paired", "native_approved", "configured"].includes(plan.phase)) {
264
- throw new Error("setup_not_paired");
265
- }
266
- return setupReply(await this.native({
267
- plan,
268
- credential,
269
- input: input.installation,
270
- signal: AbortSignal.any([request.signal, this.#stop.signal])
271
- }));
272
- });
273
- } catch {
274
- return setupReply({ error: "setup_not_completed" }, 409);
275
- }
276
- }
277
- async redeem(input) {
278
- fields2(input, ["attempt_id", "machine_id", "enrollment"]);
279
- id2(input.attempt_id);
280
- id2(input.machine_id);
281
- if (this.#exchanges.size >= 4 || this.#exchanges.has(input.attempt_id)) {
282
- throw new Error("setup_exchange_unavailable");
283
- }
284
- const plan = this.approval.claim(input.attempt_id, input.machine_id, input.enrollment);
285
- let deliver;
286
- let refuse;
287
- const response = new Promise((resolve, reject) => {
288
- deliver = resolve;
289
- refuse = reject;
290
- });
291
- let acknowledge;
292
- let rejectAck;
293
- const ack = new Promise((resolve, reject) => {
294
- acknowledge = resolve;
295
- rejectAck = reject;
296
- });
297
- ack.catch(() => {});
298
- const custody = createPairingDelivery2({
299
- backend: this.state.store,
300
- identities: this.state.credentials,
301
- authorizeChannel: (context) => {
302
- if (this.#closed || context.operation !== "enroll" || context.machine_id !== plan.machine_id) {
303
- throw new Error("setup_channel_refused");
304
- }
305
- return {
306
- deliver: async (credential, signal, delivery) => {
307
- if (this.#closed || signal.aborted || credential.machine_id !== plan.machine_id || credential.recovery_generation !== plan.generation) {
308
- throw new Error("setup_channel_refused");
309
- }
310
- exchange.context = delivery;
311
- exchange.credential_sha256 = createHash("sha256").update(credential.credential).digest();
312
- exchange.acknowledgement = acknowledge;
313
- deliver(setupReply({
314
- attempt_id: plan.attempt_id,
315
- ...delivery,
316
- ...credential,
317
- state: "persist_and_acknowledge",
318
- expires_at: plan.expires_at
319
- }));
320
- return await ack;
321
- },
322
- close: () => {
323
- rejectAck(new Error("setup_delivery_closed"));
324
- }
325
- };
326
- }
327
- });
328
- const task = custody.redeem({ registration_id: plan.machine_id, enrollment: input.enrollment }).then((receipt) => {
329
- this.approval.finishDelivery(plan.attempt_id, true);
330
- return receipt;
331
- }, (error) => {
332
- try {
333
- this.approval.finishDelivery(plan.attempt_id, false);
334
- } catch {}
335
- refuse(new Error("setup_delivery_unknown"));
336
- throw error;
337
- }).finally(() => {
338
- custody.close();
339
- exchange.credential_sha256?.fill(0);
340
- exchange.credential_sha256 = undefined;
341
- exchange.acknowledgement = undefined;
342
- });
343
- const exchange = {
344
- plan,
345
- task,
346
- close: () => custody.close()
347
- };
348
- this.#exchanges.set(plan.attempt_id, exchange);
349
- task.catch(() => {});
350
- return await response;
351
- }
352
- async acknowledge(attempt, credential, value) {
353
- const ack = object2(value);
354
- fields2(ack, ["delivery_id", "revision", "machine_id", "recovery_generation"]);
355
- const exchange = this.#exchanges.get(attempt);
356
- if (!exchange?.context || !exchange.acknowledgement || !exchange.credential_sha256 || ack.delivery_id !== exchange.context.delivery_id || ack.revision !== exchange.context.revision || ack.machine_id !== credential.machine_id || ack.recovery_generation !== credential.recovery_generation || !timingSafeEqual(createHash("sha256").update(credential.credential).digest(), exchange.credential_sha256)) {
357
- throw new Error("setup_ack_refused");
358
- }
359
- exchange.acknowledgement(ack);
360
- const receipt = await exchange.task;
361
- if (this.approval.read(attempt).repair) {
362
- this.repaired();
363
- }
364
- return setupReply({ ...receipt, attempt_id: attempt });
365
- }
366
- async close() {
367
- this.#closed = true;
368
- this.#stop.abort();
369
- for (const exchange of this.#exchanges.values()) {
370
- exchange.close();
371
- }
372
- await Promise.allSettled([...this.#exchanges.values()].map((value) => value.task));
373
- await Promise.allSettled(this.#requests);
374
- this.#exchanges.clear();
375
- }
376
- }
377
-
378
- // src/cli/setup-manifest.ts
379
- function label(value) {
380
- if (value === null) {
381
- return "unavailable";
382
- }
383
- if (!value.length || value.length > 128 || [...value].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)) {
384
- throw new CliError2("configuration_required");
385
- }
386
- return value;
387
- }
388
- async function reviewSetupManifest(input, plan, machine, directory, signal) {
389
- const row = object2(input);
390
- let entries;
391
- if (Object.hasOwn(row, "installations")) {
392
- fields2(row, ["installations"]);
393
- if (!Array.isArray(row.installations)) {
394
- throw new CliError2("configuration_required");
395
- }
396
- entries = row.installations;
397
- } else {
398
- fields2(row, ["reference", "value"]);
399
- entries = [row];
400
- }
401
- const kinds = setupProviderKinds(entries.map((entry) => object2(object2(entry).value).kind));
402
- if (plan.providers && boundedJson2(plan.providers) !== boundedJson2(kinds)) {
403
- throw new CliError2("configuration_required");
404
- }
405
- const luvusIndex = kinds.indexOf("luvus");
406
- const sessions = setupSessions(luvusIndex < 0 ? undefined : { luvus: object2(object2(object2(entries[luvusIndex]).value).target).session });
407
- const authorization = selectedSetupPolicies(plan, machine, kinds, sessions);
408
- const reviewed = [];
409
- for (let index = 0;index < entries.length; index++) {
410
- const kind = kinds[index];
411
- const entry = object2(entries[index]);
412
- fields2(entry, ["reference", "value"]);
413
- const reference = setupReference(plan.attempt_id, kind);
414
- const target = setupTarget(machine, kind, sessions);
415
- if (entry.reference !== reference) {
416
- throw new CliError2("configuration_required");
417
- }
418
- if (kind === "luvus") {
419
- const remote = checkedRemoteLuvusConfig(reference, entry.value);
420
- const expectedGrant = authorization.policies.caller.grants.find((grant) => boundedJson2(grant.target) === boundedJson2(target));
421
- if (boundedJson2(remote.target) !== boundedJson2(target) || remote.binding.binding_revision !== 1 || !remote.binding.enabled || remote.peer_policy_revision !== authorization.policies.local.revision || boundedJson2(remote.raw_grant) !== boundedJson2({ principal_id: authorization.principal_id, methods: expectedGrant.methods })) {
422
- throw new CliError2("configuration_required");
423
- }
424
- const verified = await createPinnedRemoteLuvus(remote);
425
- verified.close();
426
- reviewed.push({
427
- kind,
428
- reference,
429
- target,
430
- binding: remote.binding,
431
- value: remote,
432
- label: label(remote.contract.binary_version),
433
- binary_sha256: remote.contract.binary_sha256
434
- });
435
- } else {
436
- const remote = object2(entry.value);
437
- fields2(remote, ["kind", "config", "schema_bundle"]);
438
- const config = object2(remote.config);
439
- fields2(config, ["version", "target", "binding_revision", "config_ref", "expected_source", "peer_policy_revision"]);
440
- if (remote.kind !== "herdr" || typeof remote.schema_bundle !== "string" || Buffer.byteLength(remote.schema_bundle) > 524288) {
441
- throw new CliError2("configuration_required");
442
- }
443
- const path = runtimeReference(directory, "herdr-schema_" + plan.attempt_id);
444
- const parsed = parseHerdrProviderRefConfig2({ ...config, schema_path: path });
445
- const bytes = Buffer.from(remote.schema_bundle, "utf8");
446
- if (parsed.config_ref !== reference || parsed.binding_revision !== 1 || boundedJson2(parsed.target) !== boundedJson2(target) || parsed.peer_policy_revision !== authorization.policies.local.revision || hash2(bytes) !== parsed.expected_source.schema_sha256) {
447
- throw new CliError2("configuration_required");
448
- }
449
- const compiled = await compileHerdrSchemas2(bytes, signal);
450
- createHerdrCodec2(parsed.expected_source, compiled);
451
- const binding = {
452
- machine_id: machine,
453
- provider_id: kind,
454
- kind,
455
- config_ref: reference,
456
- binding_revision: 1,
457
- enabled: true
458
- };
459
- reviewed.push({
460
- kind,
461
- reference,
462
- target,
463
- binding,
464
- value: { kind, config: parsed },
465
- label: label(parsed.expected_source.binary_version),
466
- binary_sha256: parsed.expected_source.binary_sha256,
467
- schema: { path, bytes }
468
- });
469
- }
470
- }
471
- return { kinds, reviewed, authorization, fingerprint: hash2(boundedJson2(input, 1048576)) };
472
- }
473
- async function publishSetupSchemas(entries, signal) {
474
- for (const entry of entries) {
475
- if (!entry.schema) {
476
- continue;
477
- }
478
- const { path, bytes } = entry.schema;
479
- let previous;
480
- try {
481
- previous = readRuntimeFile(path, 524288);
482
- } catch (error) {
483
- if (!(error instanceof CliError2 && error.code === "backend_unavailable")) {
484
- throw error;
485
- }
486
- }
487
- if (previous === undefined) {
488
- writeSetupFile(path, bytes);
489
- } else if (!previous.equals(bytes)) {
490
- throw new CliError2("configuration_required");
491
- }
492
- const value = entry.value;
493
- const verified = await createHerdrGatewayResources2({ config: value.config, signal });
494
- await verified.release();
495
- }
496
- }
497
-
498
- // src/cli/setup-gateway.ts
499
- import { createHash as createHash3, randomBytes } from "crypto";
500
-
501
- // src/cli/setup-approval.ts
502
- import { createHash as createHash2, randomUUID, timingSafeEqual as timingSafeEqual2 } from "crypto";
503
- var phases = new Set([
504
- "registering",
505
- "reserved",
506
- "issued",
507
- "redeeming",
508
- "paired",
509
- "native_approved",
510
- "configured",
511
- "ready",
512
- "unknown"
513
- ]);
514
- function plan(value) {
515
- try {
516
- const row = object2(value);
517
- fields2(row, ["schema_version", "attempt_id", "actor", "uid", "generation", "name", "created_at", "expires_at", "phase"], ["machine_id", "enrollment_sha256", "native_fingerprint", "providers", "repair", "lineage"]);
518
- for (const key of ["attempt_id", "actor", "generation", "name", ...row.machine_id ? ["machine_id"] : []]) {
519
- id2(row[key]);
520
- }
521
- if (row.schema_version !== 1 || !Number.isSafeInteger(row.uid) || Number(row.uid) < 0 || !Number.isSafeInteger(row.created_at) || !Number.isSafeInteger(row.expires_at) || Number(row.expires_at) <= Number(row.created_at) || !phases.has(String(row.phase))) {
522
- throw new CliError2("invalid_config");
523
- }
524
- if (row.enrollment_sha256 !== undefined) {
525
- sha2(row.enrollment_sha256);
526
- }
527
- if (row.native_fingerprint !== undefined) {
528
- sha2(row.native_fingerprint);
529
- }
530
- if (row.providers !== undefined) {
531
- setupProviderKinds(row.providers);
532
- }
533
- if (row.repair !== undefined && (row.repair !== true || row.providers === undefined)) {
534
- throw new CliError2("invalid_config");
535
- }
536
- if (row.lineage !== undefined) {
537
- id2(row.lineage);
538
- }
539
- if (!["registering", "reserved"].includes(String(row.phase)) && (!row.machine_id || !row.enrollment_sha256)) {
540
- throw new CliError2("invalid_config");
541
- }
542
- return Object.freeze({
543
- ...row,
544
- ...row.providers ? { providers: setupProviderKinds(row.providers) } : {}
545
- });
546
- } catch {
547
- throw new CliError2("invalid_config");
548
- }
549
- }
550
-
551
- class SetupApproval {
552
- terminal;
553
- directory;
554
- #providers = new Map;
555
- #resets = new Map;
556
- #state;
557
- constructor(terminal, directory) {
558
- this.terminal = terminal;
559
- this.directory = directory;
560
- }
561
- attach(state) {
562
- this.terminal.assertOwner();
563
- this.#state = state;
564
- }
565
- detach(state) {
566
- if (this.#state === state) {
567
- this.#state = undefined;
568
- }
569
- }
570
- state() {
571
- this.terminal.assertOwner();
572
- if (!this.#state) {
573
- throw new CliError2("backend_unavailable", 3);
574
- }
575
- return this.#state;
576
- }
577
- path(attempt) {
578
- id2(attempt);
579
- return runtimeReference(this.directory, "setup_" + attempt);
580
- }
581
- read(attempt) {
582
- const state = this.state();
583
- const result = plan(readSetupJson(this.path(attempt)));
584
- if (result.attempt_id !== attempt || result.actor !== this.terminal.actor || result.uid !== this.terminal.uid || result.generation !== state.store.generation()) {
585
- throw new CliError2("configuration_required");
586
- }
587
- return result;
588
- }
589
- active() {
590
- const raw = readSetupJson(runtimeReference(this.directory, "setup-active"));
591
- if (raw === undefined) {
592
- return;
593
- }
594
- const value = object2(raw);
595
- fields2(value, ["attempt_id"]);
596
- id2(value.attempt_id);
597
- return this.read(value.attempt_id);
598
- }
599
- save(value, replace) {
600
- const checked = plan(value);
601
- writeSetupJson(this.path(checked.attempt_id), checked, replace);
602
- return checked;
603
- }
604
- async issue() {
605
- const state = this.state();
606
- const previous = this.active();
607
- if (previous && previous.phase !== "ready") {
608
- if (!["issued", "paired", "native_approved"].includes(previous.phase) || previous.phase === "issued" && Date.now() >= previous.expires_at) {
609
- throw new CliError2("configuration_required");
610
- }
611
- if (!await this.terminal.confirm("Resume the existing device setup? / \u7EE7\u7EED\u539F\u8BBE\u5907\u8BBE\u7F6E\uFF1F")) {
612
- throw new CliError2("configuration_required");
613
- }
614
- return { plan: previous };
615
- }
616
- if (!await this.terminal.confirm("Pair one Connector with this Gateway? / \u5141\u8BB8\u914D\u5BF9\u4E00\u4E2A Connector\uFF1F")) {
617
- return { declined: true };
618
- }
619
- const now = Date.now();
620
- const attempt = randomUUID();
621
- const lineage = previous?.phase === "ready" ? previous.lineage ?? previous.attempt_id : undefined;
622
- let current = this.save({
623
- schema_version: 1,
624
- attempt_id: attempt,
625
- actor: this.terminal.actor,
626
- uid: this.terminal.uid,
627
- generation: state.store.generation(),
628
- ...lineage ? { lineage } : {},
629
- name: "connector-" + attempt.slice(0, 8),
630
- created_at: now,
631
- expires_at: now + 300000,
632
- phase: "registering"
633
- }, false);
634
- writeSetupJson(runtimeReference(this.directory, "setup-active"), { attempt_id: attempt }, previous !== undefined);
635
- let registration = false;
636
- let issuance = false;
637
- const admin = new PairingAdmin2(state.store, {
638
- authorize: (action, machine) => {
639
- this.state();
640
- if (this.#state !== state || Date.now() >= current.expires_at) {
641
- throw new CliError2("configuration_required");
642
- }
643
- if (action === "register" && machine === undefined && !registration && current.phase === "registering") {
644
- registration = true;
645
- return;
646
- }
647
- if (action === "enroll" && machine === current.machine_id && !issuance && current.phase === "reserved") {
648
- issuance = true;
649
- return;
650
- }
651
- throw new CliError2("configuration_required");
652
- }
653
- });
654
- const machine = admin.register(current.name);
655
- current = this.save({ ...current, phase: "reserved", machine_id: machine.machine_id }, true);
656
- const enrollment = admin.issue(machine.machine_id, Math.max(1, current.expires_at - Date.now()));
657
- current = this.save({ ...current, phase: "issued", enrollment_sha256: createHash2("sha256").update(enrollment).digest("hex") }, true);
658
- return { plan: current, code: machine.machine_id + "." + current.attempt_id + "." + enrollment };
659
- }
660
- async issueRepair() {
661
- const state = this.state();
662
- const previous = this.active();
663
- const retry = previous?.repair === true && ["issued", "redeeming", "unknown"].includes(previous.phase);
664
- if (!previous?.machine_id || !previous.providers || previous.phase !== "ready" && !retry) {
665
- throw new CliError2("configuration_required");
666
- }
667
- const machine = previous.machine_id;
668
- const device = state.store.machine(machine);
669
- if (device.status === "revoked") {
670
- throw new CliError2("configuration_required");
671
- }
672
- if (!await this.terminal.confirm("Re-pair the existing Connector device " + device.name + " (" + machine + ")? The credential it holds stops working now and in-flight calls become unknown. / \u91CD\u65B0\u914D\u5BF9\u5DF2\u6709\u8BBE\u5907 " + device.name + "\uFF08" + machine + "\uFF09\uFF1F\u8BE5\u8BBE\u5907\u5F53\u524D\u51ED\u636E\u7ACB\u5373\u5931\u6548\uFF0C\u8FDB\u884C\u4E2D\u7684\u8C03\u7528\u7ED3\u679C\u53D8\u4E3A\u672A\u77E5\u3002")) {
673
- throw new CliError2("configuration_required");
674
- }
675
- for (let attempt = 0;; attempt++) {
676
- const epoch = state.store.deviceEpoch(machine);
677
- const generation = state.store.generation();
678
- this.#resets.set(machine, {
679
- confirmation: randomUUID(),
680
- expires: Date.now() + 60000,
681
- fence: boundedJson2({ recovery_generation: generation, connection_epoch: epoch })
682
- });
683
- try {
684
- state.store.resetDeviceEnrollment(machine, generation, epoch);
685
- state.channels.blockAndClose(machine);
686
- break;
687
- } catch (error) {
688
- this.#resets.delete(machine);
689
- if (attempt >= 2) {
690
- throw error;
691
- }
692
- }
693
- }
694
- const now = Date.now();
695
- const attempt = randomUUID();
696
- let current = this.save({
697
- schema_version: 1,
698
- attempt_id: attempt,
699
- actor: this.terminal.actor,
700
- uid: this.terminal.uid,
701
- generation: state.store.generation(),
702
- name: previous.name,
703
- created_at: now,
704
- expires_at: now + 300000,
705
- phase: "reserved",
706
- machine_id: machine,
707
- providers: previous.providers,
708
- repair: true,
709
- ...previous.lineage ? { lineage: previous.lineage } : {}
710
- }, false);
711
- let issuance = false;
712
- const admin = new PairingAdmin2(state.store, {
713
- authorize: (action, target) => {
714
- this.state();
715
- if (this.#state !== state || Date.now() >= current.expires_at || action !== "enroll" || target !== machine || issuance) {
716
- throw new CliError2("configuration_required");
717
- }
718
- issuance = true;
719
- }
720
- });
721
- const enrollment = admin.issue(machine, Math.max(1, current.expires_at - Date.now()));
722
- current = this.save({ ...current, phase: "issued", enrollment_sha256: createHash2("sha256").update(enrollment).digest("hex") }, true);
723
- writeSetupJson(runtimeReference(this.directory, "setup-active"), { attempt_id: attempt }, true);
724
- return { plan: current, code: machine + "." + current.attempt_id + "." + enrollment };
725
- }
726
- claim(attempt, machine, enrollment) {
727
- const current = this.read(attempt);
728
- if (current.phase !== "issued" || current.machine_id !== machine || Date.now() >= current.expires_at || typeof enrollment !== "string" || !/^[A-Za-z0-9_-]{32,256}$/.test(enrollment) || !timingSafeEqual2(createHash2("sha256").update(enrollment).digest(), Buffer.from(current.enrollment_sha256, "hex"))) {
729
- throw new CliError2("configuration_required");
730
- }
731
- return this.save({ ...current, phase: "redeeming" }, true);
732
- }
733
- finishDelivery(attempt, delivered) {
734
- const current = this.read(attempt);
735
- if (current.phase !== "redeeming") {
736
- throw new CliError2("configuration_required");
737
- }
738
- return this.save({ ...current, phase: delivered ? current.repair ? "configured" : "paired" : "unknown" }, true);
739
- }
740
- async approveNative(attempt, provider, fingerprint, description, signal = this.terminal.io.signal) {
741
- const state = this.state();
742
- const current = this.read(attempt);
743
- const providers = setupProviderKinds(typeof provider === "string" ? [provider] : provider);
744
- sha2(fingerprint);
745
- if (!["paired", "native_approved"].includes(current.phase)) {
746
- throw new CliError2("configuration_required");
747
- }
748
- if (current.phase === "native_approved" && current.native_fingerprint !== fingerprint) {
749
- throw new CliError2("configuration_required");
750
- }
751
- if (current.providers && boundedJson2(current.providers) !== boundedJson2(providers)) {
752
- throw new CliError2("configuration_required");
753
- }
754
- if (!await this.terminal.confirm(description, signal)) {
755
- throw new CliError2("configuration_required");
756
- }
757
- const checked = this.read(attempt);
758
- if (checked.phase !== current.phase || checked.native_fingerprint !== current.native_fingerprint || this.#state !== state) {
759
- throw new CliError2("configuration_required");
760
- }
761
- for (const kind of providers) {
762
- this.#providers.set(current.machine_id + "/" + kind, {
763
- confirmation: randomUUID(),
764
- expires: Date.now() + 300000,
765
- generation: current.generation
766
- });
767
- }
768
- return this.save({ ...current, phase: "native_approved", native_fingerprint: fingerprint, providers }, true);
769
- }
770
- authorizeManagement(request) {
771
- const state = this.state();
772
- if (request.action === "device_enrollment_reset") {
773
- const reset = this.#resets.get(request.target);
774
- if (!reset || reset.fence !== request.fence || Date.now() >= reset.expires) {
775
- throw new CliError2("configuration_required");
776
- }
777
- this.#resets.delete(request.target);
778
- return { actor: this.terminal.actor, confirmation_id: reset.confirmation };
779
- }
780
- const permit = this.#providers.get(request.target);
781
- if (request.action !== "providers_manage" || request.fence !== "new" || !permit || permit.generation !== state.store.generation() || Date.now() >= permit.expires) {
782
- throw new CliError2("configuration_required");
783
- }
784
- this.#providers.delete(request.target);
785
- return { actor: this.terminal.actor, confirmation_id: permit.confirmation };
786
- }
787
- finishNative(attempt, fingerprint) {
788
- const current = this.read(attempt);
789
- if (current.phase !== "native_approved" || current.native_fingerprint !== fingerprint) {
790
- throw new CliError2("configuration_required");
791
- }
792
- return this.save({ ...current, phase: "configured" }, true);
793
- }
794
- finishReady(attempt) {
795
- const current = this.read(attempt);
796
- if (current.phase !== "configured") {
797
- throw new CliError2("configuration_required");
798
- }
799
- this.save({ ...current, phase: "ready" }, true);
800
- }
801
- }
802
-
803
- // src/cli/setup-gateway.ts
804
- function publish(path, value, maximum = 16384) {
805
- const current = readSetupJson(path, maximum);
806
- if (current === undefined) {
807
- writeSetupJson(path, value, false, maximum);
808
- } else if (boundedJson2(current, maximum) !== boundedJson2(value, maximum)) {
809
- throw new CliError2("configuration_required");
810
- }
811
- }
812
- function unionGrants(existing, added) {
813
- const merged = [...existing];
814
- for (const grant of added) {
815
- const found = merged.find((entry) => boundedJson2(entry.target) === boundedJson2(grant.target));
816
- if (!found) {
817
- merged.push(grant);
818
- } else if (boundedJson2(found) !== boundedJson2(grant)) {
819
- throw new CliError2("configuration_required");
820
- }
821
- }
822
- return merged;
823
- }
824
- function publishFirstCallers(path, resource, authorization, caller) {
825
- const previous = readSetupJson(path, REGISTRY_MAX_BYTES);
826
- const created = {
827
- schema_version: 1,
828
- kind: "mcp-caller-source",
829
- resource,
830
- local_policy: authorization.policies.local,
831
- callers: [caller]
832
- };
833
- if (previous === undefined) {
834
- parseRegistry(JSON.parse(boundedJson2(created, REGISTRY_MAX_BYTES)));
835
- writeSetupFile(path, boundedJson2(created, REGISTRY_MAX_BYTES), false);
836
- return;
837
- }
838
- const registry = parseRegistry(previous, resource);
839
- const kept = registry.callers.filter((entry) => isAdminPlaceholder(entry) || boundedJson2(entry, REGISTRY_MAX_BYTES) === boundedJson2(caller, REGISTRY_MAX_BYTES));
840
- if (kept.length !== registry.callers.length) {
841
- throw new CliError2("configuration_required");
842
- }
843
- const callers = [...kept];
844
- if (!callers.some((entry) => boundedJson2(entry, REGISTRY_MAX_BYTES) === boundedJson2(caller, REGISTRY_MAX_BYTES))) {
845
- callers.push(caller);
846
- }
847
- const merged = { ...created, callers };
848
- parseRegistry(JSON.parse(boundedJson2(merged, REGISTRY_MAX_BYTES)), resource);
849
- const stamp = setupFileStamp(path);
850
- writeSetupFile(path, boundedJson2(merged, REGISTRY_MAX_BYTES), true, stamp);
851
- }
852
-
853
- class GatewaySetup {
854
- config;
855
- terminal;
856
- options;
857
- approval;
858
- #begun = false;
859
- #active;
860
- #timer;
861
- #reload;
862
- reconfigure = new Promise(() => {});
863
- constructor(config, terminal, options = {}) {
864
- this.config = config;
865
- this.terminal = terminal;
866
- this.options = options;
867
- this.approval = new SetupApproval(terminal, config.state_dir);
868
- }
869
- attach(state) {
870
- this.approval.attach(state);
871
- this.reconfigure = new Promise((resolve) => {
872
- this.#reload = resolve;
873
- });
874
- const http = new GatewayPairingHttp(state, this.approval, (request) => this.native(state, request), () => setImmediate(() => this.#reload?.()));
875
- this.#active = http;
876
- return http;
877
- }
878
- async listening(gateway, mcpUrl) {
879
- if (this.options.repair && !this.#begun) {
880
- this.#begun = true;
881
- const active = this.approval.active();
882
- if (active?.repair && active.phase === "configured") {
883
- this.terminal.show(`Resuming the device re-pairing; waiting for the Connector to reconnect. / \u7EE7\u7EED\u539F\u91CD\u65B0\u914D\u5BF9\uFF0C\u7B49\u5F85 Connector \u91CD\u8FDE\u3002
884
- `);
885
- } else {
886
- const issued = await this.approval.issueRepair();
887
- this.terminal.show("Gateway MCP: " + mcpUrl + `
888
- Connector endpoint: ` + gateway.transport.url.replace(/\/connector$/, "") + `
889
- One-time re-pairing code for the existing device (5 minutes; keep private) / \u5DF2\u6709\u8BBE\u5907\u7684\u4E00\u6B21\u6027\u91CD\u65B0\u914D\u5BF9\u7801\uFF085\u5206\u949F\uFF0C\u8BF7\u4FDD\u5BC6\uFF09:
890
- ` + issued.code + `
891
- Stop that Connector, then run rovloom connector --repair on its machine and enter the code. The old credential no longer works.
892
- \u5728\u8BE5\u8BBE\u5907\u4E0A\u505C\u6B62 Connector\uFF0C\u8FD0\u884C rovloom connector --repair \u5E76\u8F93\u5165\u6B64\u7801\uFF1B\u65E7\u51ED\u636E\u5DF2\u5931\u6548\u3002
893
- `);
894
- }
895
- }
896
- const current = this.approval.active();
897
- if (!this.#begun && (current?.phase === "ready" ? this.options.addDevice === true : current?.phase !== "configured")) {
898
- this.#begun = true;
899
- this.terminal.show("Gateway MCP: " + mcpUrl + `
900
- Connector endpoint: ` + gateway.transport.url.replace(/\/connector$/, "") + `
901
- `);
902
- const issued = await this.approval.issue();
903
- if (issued.declined) {
904
- const page = this.config.management_enabled ? "http://127.0.0.1:" + this.config.management_port + "/" : "";
905
- this.terminal.show(`Gateway stays unpaired and running; no enrollment or credential is issued. / Gateway \u4FDD\u6301\u672A\u914D\u5BF9\u5E76\u7EE7\u7EED\u8FD0\u884C\uFF0C\u4E0D\u7B7E\u53D1\u51ED\u636E\u3002
906
- ` + (page ? "Finish first pairing from the management page: create the administrator with `rovloom admin create`, sign in at " + page + ", create a pairing application, then run `rovloom connector --claim` on the device. / \u5728\u7BA1\u7406\u9875\u5B8C\u6210\u9996\u6B21\u914D\u5BF9\uFF1A\u5148\u7528 `rovloom admin create` \u521B\u5EFA\u7BA1\u7406\u5458\uFF0C\u767B\u5F55 " + page + " \u65B0\u5EFA\u914D\u5BF9\u7533\u8BF7\uFF0C\u518D\u5728\u8BBE\u5907\u4E0A\u8FD0\u884C `rovloom connector --claim`\u3002\n" : `Management is disabled; enable it or rerun to pair from this terminal. / \u7BA1\u7406\u9875\u672A\u542F\u7528\uFF1B\u8BF7\u542F\u7528\u540E\u91CD\u8BD5\u6216\u5728\u672C\u7EC8\u7AEF\u5B8C\u6210\u914D\u5BF9\u3002
907
- `));
908
- this.terminal.finishQuestions();
909
- return;
910
- }
911
- if (issued.code) {
912
- this.terminal.show(`One-time pairing code (5 minutes; keep private) / \u4E00\u6B21\u6027\u914D\u5BF9\u7801\uFF085\u5206\u949F\uFF0C\u8BF7\u4FDD\u5BC6\uFF09:
913
- ` + issued.code + `
914
- Run rovloom connector in another local terminal and enter the Gateway address and code.
915
- \u5728 Connector \u7684\u672C\u5730\u7EC8\u7AEF\u8F93\u5165 Gateway \u5730\u5740\u4E0E\u8BE5\u7801\uFF1B\u968F\u540E\u56DE\u5230\u6B64\u7EC8\u7AEF\u6279\u51C6\u5B9E\u6D4B\u5B89\u88C5\u3002
916
- `);
917
- } else {
918
- this.terminal.show(`Resuming the existing setup; no new enrollment or credential is issued. / \u7EE7\u7EED\u539F\u8BBE\u7F6E\uFF0C\u4E0D\u91CD\u65B0\u7B7E\u53D1\u3002
919
- `);
920
- }
921
- }
922
- const plan = this.approval.active();
923
- if (plan?.phase === "ready") {
924
- this.terminal.finishQuestions();
925
- return;
926
- }
927
- if (plan?.phase === "configured") {
928
- this.terminal.finishQuestions();
929
- let previous = "";
930
- this.#timer = setInterval(() => {
931
- try {
932
- const device = gateway.transport.status().devices.find((entry) => entry.machine_id === plan.machine_id);
933
- const current = device?.authenticated ? "connector_authenticated native_pending" : "gateway_waiting_for_connector";
934
- if (current !== previous) {
935
- previous = current;
936
- this.terminal.show(current + `
937
- `);
938
- }
939
- if (device?.authenticated && device.ready && (plan.providers ?? ["luvus"]).every((kind) => device.providers.some((entry) => entry.provider_id === kind && entry.ready))) {
940
- this.approval.finishReady(plan.attempt_id);
941
- this.terminal.show(`connector_ready / Connector \u5DF2\u901A\u8FC7\u771F\u5B9E\u8BA4\u8BC1\u4E14\u5168\u90E8\u6240\u9009\u539F\u751F provider \u5C31\u7EEA\u3002
942
- `);
943
- clearInterval(this.#timer);
944
- this.#timer = undefined;
945
- }
946
- } catch {
947
- this.terminal.io.stderr.write(`setup_observation_unavailable
948
- `);
949
- clearInterval(this.#timer);
950
- this.#timer = undefined;
951
- }
952
- }, 250);
953
- }
954
- }
955
- async native(state, request) {
956
- const { reviewed, kinds, authorization, fingerprint } = await reviewSetupManifest(request.input, request.plan, request.credential.machine_id, this.config.state_dir, request.signal);
957
- if (request.plan.phase === "configured") {
958
- if (request.plan.native_fingerprint !== fingerprint) {
959
- throw new CliError2("configuration_required");
960
- }
961
- return { state: "configured", fingerprint };
962
- }
963
- if (request.signal.aborted) {
964
- throw new CliError2("configuration_required");
965
- }
966
- const providers = readRuntimeConfig2(this.config).providers;
967
- const additional = request.plan.lineage !== undefined;
968
- if (providers.some((entry) => {
969
- const expected = reviewed.find((value) => value.reference === entry.reference);
970
- return expected ? boundedJson2(entry.value, 1048576) !== boundedJson2(expected.value, 1048576) : !additional;
971
- })) {
972
- throw new CliError2("configuration_required");
973
- }
974
- await this.approval.approveNative(request.plan.attempt_id, kinds, fingerprint, "Allow this paired device's measured native installations (" + reviewed.map((entry) => entry.kind + " " + entry.label + ", Target " + boundedJson2(entry.target) + ", binary SHA-256 " + entry.binary_sha256).join("; ") + `) for reviewed workspace/Agent operations and one local MCP caller? Terminal execution is controlled by the Connector machine owner's local configuration. / \u6279\u51C6\u8FD9\u4E9B\u5B9E\u6D4B\u539F\u751F\u6765\u6E90\u3001\u5404\u81EA\u7A97\u53E3/Agent \u64CD\u4F5C\u53CA\u4E00\u4E2A\u672C\u673A MCP caller\uFF08\u7EC8\u7AEF\u6267\u884C\u7531 Connector \u672C\u673A\u6240\u6709\u8005\u914D\u7F6E\u51B3\u5B9A\uFF09\uFF1F`, request.signal);
975
- this.terminal.assertOwner();
976
- if (request.signal.aborted) {
977
- throw new CliError2("configuration_required");
978
- }
979
- await publishSetupSchemas(reviewed, request.signal);
980
- const hostname = canonicalLoopbackHost(this.config.mcp_hostname);
981
- const resource = new URL("http://" + hostname + ":" + this.config.mcp_port + "/mcp").href;
982
- const clientPath = runtimeReference(this.config.state_dir, "mcp-client");
983
- const original = readSetupJson(clientPath);
984
- if (additional && original === undefined) {
985
- throw new CliError2("configuration_required");
986
- }
987
- const client = original === undefined ? {
988
- schema_version: 1,
989
- kind: "mcp-client-credential",
990
- attempt_id: request.plan.attempt_id,
991
- resource,
992
- principal_id: authorization.principal_id,
993
- token: randomBytes(32).toString("base64url"),
994
- issued_at: Date.now(),
995
- expires_at: Date.now() + 30 * 86400000
996
- } : object2(original);
997
- fields2(client, [
998
- "schema_version",
999
- "kind",
1000
- "attempt_id",
1001
- "resource",
1002
- "principal_id",
1003
- "token",
1004
- "issued_at",
1005
- "expires_at"
1006
- ]);
1007
- if (client.schema_version !== 1 || client.kind !== "mcp-client-credential" || client.attempt_id !== (request.plan.lineage ?? request.plan.attempt_id) || client.resource !== resource || client.principal_id !== authorization.principal_id || typeof client.token !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(client.token) || !Number.isSafeInteger(client.issued_at) || !Number.isSafeInteger(client.expires_at) || Number(client.expires_at) <= Date.now()) {
1008
- throw new CliError2("configuration_required");
1009
- }
1010
- publish(clientPath, client);
1011
- const callersPath = runtimeReference(this.config.state_dir, "mcp-callers");
1012
- const providersPath = runtimeReference(this.config.state_dir, "providers");
1013
- const reviewedEntries = reviewed.map((entry) => ({
1014
- reference: entry.reference,
1015
- value: entry.value
1016
- }));
1017
- if (!additional) {
1018
- publishFirstCallers(callersPath, resource, authorization, {
1019
- principal: { id: authorization.principal_id, policy: authorization.policies.caller },
1020
- token_sha256: createHash3("sha256").update(client.token).digest("hex"),
1021
- issued_at: client.issued_at,
1022
- expires_at: client.expires_at
1023
- });
1024
- publish(providersPath, { schema_version: 1, role: "gateway", providers: reviewedEntries }, 1048576);
1025
- } else {
1026
- mergeSetupCallers(callersPath, resource, authorization);
1027
- mergeSetupProviders(providersPath, reviewedEntries);
1028
- }
1029
- for (const entry of reviewed) {
1030
- const binding = state.store.provider(entry.target.machine_id, entry.target.provider_id);
1031
- if (!binding) {
1032
- state.store.defineProvider(entry.binding);
1033
- } else if (boundedJson2(binding) !== boundedJson2(entry.binding)) {
1034
- throw new CliError2("configuration_required");
1035
- }
1036
- }
1037
- this.approval.finishNative(request.plan.attempt_id, fingerprint);
1038
- let adminNote = "";
1039
- if (this.config.management_enabled && !managementSourceExists2(this.config)) {
1040
- const page = "http://127.0.0.1:" + this.config.management_port + "/";
1041
- try {
1042
- const admin = writeAdminFiles2(this.config, "admin", Date.now());
1043
- adminNote = `
1044
- Management page / \u7BA1\u7406\u9875: ` + page + `
1045
- Management user / \u7BA1\u7406\u5458: admin` + `
1046
- Management password (private) / \u7BA1\u7406\u5458\u5BC6\u7801: ` + admin.secret + `
1047
- (Shown once; run rovloom admin create to re-issue. / \u53EA\u663E\u793A\u4E00\u6B21\uFF1B\u4E22\u5931\u540E\u8FD0\u884C rovloom admin create \u91CD\u65B0\u7B7E\u53D1\u3002)`;
1048
- } catch {
1049
- adminNote = `
1050
- Management administrator was not created; run rovloom admin create. / \u672A\u80FD\u521B\u5EFA\u7BA1\u7406\u9875\u7BA1\u7406\u5458\uFF0C\u8BF7\u7A0D\u540E\u8FD0\u884C rovloom admin create\u3002`;
1051
- }
1052
- }
1053
- this.terminal.show(`Native configuration saved; Gateway will reopen its real providers. / \u539F\u751F\u914D\u7F6E\u5DF2\u4FDD\u5B58\uFF0CGateway \u5C06\u91CD\u5F00\u5B9E\u9645 provider\u3002
1054
- MCP credential file / MCP \u51ED\u636E\u6587\u4EF6: ` + clientPath + `
1055
- MCP URL: ` + (this.config.mcp_public_url ?? resource) + `
1056
- Targets: ` + boundedJson2(reviewed.map((entry) => entry.target)) + `
1057
- Binding revision: 1` + (additional ? `
1058
- Existing MCP Bearer now also covers this device (not shown again; tokens made with rovloom token create --like need re-creating). / \u73B0\u6709 MCP Bearer \u5DF2\u8986\u76D6\u6B64\u8BBE\u5907\uFF08\u4E0D\u518D\u663E\u793A\uFF1B\u7528 token create --like \u514B\u9686\u7684\u4EE4\u724C\u9700\u8981\u91CD\u65B0\u514B\u9686\uFF09\u3002` : `
1059
- Bearer (private): ` + client.token) + adminNote + `
1060
- `);
1061
- setImmediate(() => this.#reload?.());
1062
- return { state: "configured", fingerprint };
1063
- }
1064
- async detach(state) {
1065
- if (this.#timer) {
1066
- clearInterval(this.#timer);
1067
- this.#timer = undefined;
1068
- }
1069
- const http = this.#active;
1070
- this.#active = undefined;
1071
- await http?.close();
1072
- this.approval.detach(state);
1073
- }
1074
- }
1075
- function mergeSetupCallers(path, resource, authorization) {
1076
- const stamp = setupFileStamp(path);
1077
- const registry = structuredClone(parseRegistry(readRuntimeJson(path, REGISTRY_MAX_BYTES), resource));
1078
- if (registry.local_policy.revision !== authorization.policies.local.revision) {
1079
- throw new CliError2("configuration_required");
1080
- }
1081
- registry.local_policy.grants = unionGrants(registry.local_policy.grants, authorization.policies.local.grants);
1082
- for (const entry of registry.callers) {
1083
- if (entry.principal.id === authorization.principal_id) {
1084
- if (entry.principal.policy.revision !== authorization.policies.caller.revision) {
1085
- throw new CliError2("configuration_required");
1086
- }
1087
- entry.principal.policy.grants = unionGrants(entry.principal.policy.grants, authorization.policies.caller.grants);
1088
- }
1089
- }
1090
- parseRegistry(JSON.parse(boundedJson2(registry, REGISTRY_MAX_BYTES)), resource);
1091
- writeSetupFile(path, boundedJson2(registry, REGISTRY_MAX_BYTES), true, stamp);
1092
- }
1093
- function mergeSetupProviders(path, entries) {
1094
- const stamp = setupFileStamp(path);
1095
- const document = object2(readRuntimeJson(path, 1048576));
1096
- fields2(document, ["schema_version", "role", "providers"]);
1097
- if (document.schema_version !== 1 || document.role !== "gateway" || !Array.isArray(document.providers)) {
1098
- throw new CliError2("configuration_required");
1099
- }
1100
- const merged = [...document.providers];
1101
- for (const entry of entries) {
1102
- const found = merged.find((value) => value.reference === entry.reference);
1103
- if (!found) {
1104
- merged.push(entry);
1105
- } else if (boundedJson2(found.value, 1048576) !== boundedJson2(entry.value, 1048576)) {
1106
- throw new CliError2("configuration_required");
1107
- }
1108
- }
1109
- if (merged.length > 32) {
1110
- throw new CliError2("configuration_required");
1111
- }
1112
- writeSetupFile(path, boundedJson2({ ...document, providers: merged }, 1048576), true, stamp);
1113
- }
1114
-
1115
- export { setupSessions, setupTarget, setupProviderKinds, selectedSetupPolicies, setupReference, setupSessionsFor, setupReply, requestJson, device, reviewSetupManifest, publishSetupSchemas, publish, publishFirstCallers, GatewaySetup, mergeSetupCallers, mergeSetupProviders };
1116
-
1117
- //# debugId=3920190CFC3283A264756E2164756E21