@synap-core/eve 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +251 -0
  2. package/dist/add-R6RGUYER.js +22 -0
  3. package/dist/add-R6RGUYER.js.map +1 -0
  4. package/dist/auth-7FFZNARZ-4OIRGZ7R.js +31 -0
  5. package/dist/auth-7FFZNARZ-4OIRGZ7R.js.map +1 -0
  6. package/dist/chunk-22RIHU4J.js +975 -0
  7. package/dist/chunk-22RIHU4J.js.map +1 -0
  8. package/dist/chunk-23XQDB7K.js +1773 -0
  9. package/dist/chunk-23XQDB7K.js.map +1 -0
  10. package/dist/chunk-5TBV2VHZ.js +6067 -0
  11. package/dist/chunk-5TBV2VHZ.js.map +1 -0
  12. package/dist/chunk-D7P3IYYR.js +44 -0
  13. package/dist/chunk-D7P3IYYR.js.map +1 -0
  14. package/dist/chunk-EHPC7CKE.js +757 -0
  15. package/dist/chunk-EHPC7CKE.js.map +1 -0
  16. package/dist/chunk-FAE5UZFE.js +17 -0
  17. package/dist/chunk-FAE5UZFE.js.map +1 -0
  18. package/dist/chunk-HRF6TNXX.js +53 -0
  19. package/dist/chunk-HRF6TNXX.js.map +1 -0
  20. package/dist/chunk-HVJEW7TU.js +495 -0
  21. package/dist/chunk-HVJEW7TU.js.map +1 -0
  22. package/dist/chunk-JO5H5G6U.js +1529 -0
  23. package/dist/chunk-JO5H5G6U.js.map +1 -0
  24. package/dist/chunk-MUEQNBE6.js +1140 -0
  25. package/dist/chunk-MUEQNBE6.js.map +1 -0
  26. package/dist/chunk-O5WTCVMZ.js +3391 -0
  27. package/dist/chunk-O5WTCVMZ.js.map +1 -0
  28. package/dist/chunk-ROKIQIEX.js +65 -0
  29. package/dist/chunk-ROKIQIEX.js.map +1 -0
  30. package/dist/chunk-T7GCAJK5.js +4895 -0
  31. package/dist/chunk-T7GCAJK5.js.map +1 -0
  32. package/dist/chunk-UOQWJ3S4.js +138 -0
  33. package/dist/chunk-UOQWJ3S4.js.map +1 -0
  34. package/dist/chunk-YQWV5W4U.js +3479 -0
  35. package/dist/chunk-YQWV5W4U.js.map +1 -0
  36. package/dist/chunk-ZPDNZRVW.js +272 -0
  37. package/dist/chunk-ZPDNZRVW.js.map +1 -0
  38. package/dist/config-store-KTOWIKAS-XVZHNPKW.js +11 -0
  39. package/dist/config-store-KTOWIKAS-XVZHNPKW.js.map +1 -0
  40. package/dist/dist-2VRFSXMO.js +40 -0
  41. package/dist/dist-2VRFSXMO.js.map +1 -0
  42. package/dist/dist-C2QB4IKT.js +340 -0
  43. package/dist/dist-C2QB4IKT.js.map +1 -0
  44. package/dist/dist-DEIUVZ57.js +299 -0
  45. package/dist/dist-DEIUVZ57.js.map +1 -0
  46. package/dist/dist-DSKHGECB.js +36 -0
  47. package/dist/dist-DSKHGECB.js.map +1 -0
  48. package/dist/dist-GN2DN7CI.js +95 -0
  49. package/dist/dist-GN2DN7CI.js.map +1 -0
  50. package/dist/dist-TZL4XW2R.js +58 -0
  51. package/dist/dist-TZL4XW2R.js.map +1 -0
  52. package/dist/dist-ZP2APOYV.js +53 -0
  53. package/dist/dist-ZP2APOYV.js.map +1 -0
  54. package/dist/index.js +15083 -0
  55. package/dist/index.js.map +1 -0
  56. package/dist/operational-5RT6AZJI-ZEMPD32Q.js +12 -0
  57. package/dist/operational-5RT6AZJI-ZEMPD32Q.js.map +1 -0
  58. package/dist/reconcile-A4I3Z7VX-GQMEN4GC.js +12 -0
  59. package/dist/reconcile-A4I3Z7VX-GQMEN4GC.js.map +1 -0
  60. package/dist/rsshub-GOJBTNOD-UNNVKBFL.js +18 -0
  61. package/dist/rsshub-GOJBTNOD-UNNVKBFL.js.map +1 -0
  62. package/dist/synap-image-install-X36REYQ5-UGO4MAOP.js +17 -0
  63. package/dist/synap-image-install-X36REYQ5-UGO4MAOP.js.map +1 -0
  64. package/package.json +44 -0
@@ -0,0 +1,1529 @@
1
+ import { createRequire as __eveCreateRequire } from 'module'; const require = __eveCreateRequire(import.meta.url);
2
+ import {
3
+ LEGACY_CODER_ENGINE_SLUGS,
4
+ agentsToProvision
5
+ } from "./chunk-5TBV2VHZ.js";
6
+ import {
7
+ POD_DEPLOY_DIR_CANDIDATES,
8
+ SYNAP_BACKEND_CONTAINERS,
9
+ findPodDeployDir,
10
+ resolveSynapUrlOnHost,
11
+ restartBackendContainer
12
+ } from "./chunk-O5WTCVMZ.js";
13
+ import {
14
+ readAgentKey,
15
+ readEveSecrets,
16
+ writeAgentKey,
17
+ writeCodeEngine,
18
+ writeEveSecrets
19
+ } from "./chunk-T7GCAJK5.js";
20
+
21
+ // ../@eve/lifecycle/dist/chunk-6JFSP4GT.js
22
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
23
+ import { join as join2 } from "path";
24
+ import { exec as execCallback } from "child_process";
25
+ import { promisify } from "util";
26
+ import { randomBytes } from "crypto";
27
+ import {
28
+ readFileSync,
29
+ writeFileSync,
30
+ renameSync,
31
+ existsSync,
32
+ mkdirSync,
33
+ unlinkSync
34
+ } from "fs";
35
+ import { join, dirname } from "path";
36
+ import { randomUUID } from "crypto";
37
+ var ENV_FILENAME = ".env";
38
+ function envPath(deployDir) {
39
+ return join(deployDir, ENV_FILENAME);
40
+ }
41
+ function readEnvVar(deployDir, key) {
42
+ const path = envPath(deployDir);
43
+ if (!existsSync(path)) return null;
44
+ const text = readFileSync(path, "utf-8");
45
+ let last = null;
46
+ for (const line of text.split(/\r?\n/)) {
47
+ const trimmed = line.trimStart();
48
+ if (!trimmed || trimmed.startsWith("#")) continue;
49
+ const eq = line.indexOf("=");
50
+ if (eq < 0) continue;
51
+ if (line.slice(0, eq).trim() === key) {
52
+ last = line.slice(eq + 1);
53
+ }
54
+ }
55
+ return last;
56
+ }
57
+ function readEnvFile(deployDir) {
58
+ const path = envPath(deployDir);
59
+ const out = /* @__PURE__ */ new Map();
60
+ if (!existsSync(path)) return out;
61
+ const text = readFileSync(path, "utf-8");
62
+ for (const line of text.split(/\r?\n/)) {
63
+ const trimmed = line.trimStart();
64
+ if (!trimmed || trimmed.startsWith("#")) continue;
65
+ const eq = line.indexOf("=");
66
+ if (eq < 0) continue;
67
+ const k = line.slice(0, eq).trim();
68
+ if (!k) continue;
69
+ out.set(k, line.slice(eq + 1));
70
+ }
71
+ return out;
72
+ }
73
+ function writeEnvVar(deployDir, key, value) {
74
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
75
+ throw new Error(`writeEnvVar: invalid env var name ${JSON.stringify(key)}`);
76
+ }
77
+ const path = envPath(deployDir);
78
+ if (!existsSync(dirname(path))) {
79
+ mkdirSync(dirname(path), { recursive: true });
80
+ }
81
+ const existed = existsSync(path);
82
+ const original = existed ? readFileSync(path, "utf-8") : "";
83
+ const hadTrailingNewline = original.endsWith("\n");
84
+ let previous = null;
85
+ let found = false;
86
+ const sourceText = hadTrailingNewline ? original.slice(0, -1) : original;
87
+ const lines = sourceText === "" ? [] : sourceText.split(/\r?\n/);
88
+ const out = [];
89
+ for (const line of lines) {
90
+ const trimmed = line.trimStart();
91
+ if (!trimmed || trimmed.startsWith("#")) {
92
+ out.push(line);
93
+ continue;
94
+ }
95
+ const eq = line.indexOf("=");
96
+ if (eq < 0) {
97
+ out.push(line);
98
+ continue;
99
+ }
100
+ const k = line.slice(0, eq).trim();
101
+ if (k !== key) {
102
+ out.push(line);
103
+ continue;
104
+ }
105
+ const val = line.slice(eq + 1);
106
+ if (!found) {
107
+ previous = val;
108
+ found = true;
109
+ if (value !== null) {
110
+ out.push(`${key}=${value}`);
111
+ }
112
+ }
113
+ }
114
+ if (!found && value !== null) {
115
+ out.push(`${key}=${value}`);
116
+ }
117
+ let next = out.join("\n");
118
+ if (next.length > 0) next += "\n";
119
+ if (existed && next === original) {
120
+ return { changed: false, previous };
121
+ }
122
+ const tmp = `${path}.tmp-${process.pid}`;
123
+ try {
124
+ writeFileSync(tmp, next, { mode: 384 });
125
+ renameSync(tmp, path);
126
+ } catch (err) {
127
+ try {
128
+ unlinkSync(tmp);
129
+ } catch {
130
+ }
131
+ throw err;
132
+ }
133
+ return { changed: true, previous };
134
+ }
135
+ var OPENAPI_TIMEOUT_MS = 4e3;
136
+ var AUTH_TIMEOUT_MS = 6e3;
137
+ var ENTITY_POST_TIMEOUT_MS = 6e3;
138
+ var SSE_TIMEOUT_MS = 35e3;
139
+ var ME_TIMEOUT_MS = 6e3;
140
+ var DELETE_TIMEOUT_MS = 4e3;
141
+ var AUTH_NAME = "Synap auth";
142
+ var DISCOVERY_NAME = "Synap public discovery";
143
+ var IDEMPOTENCY_NAME = "Synap idempotency replay";
144
+ var EVENTS_NAME = "Synap event stream";
145
+ var SUB_TOKENS_NAME = "Synap sub-token mode";
146
+ async function runHubProtocolProbes(opts) {
147
+ const synapUrl = opts.synapUrl.replace(/\/+$/, "");
148
+ const apiKey = opts.apiKey;
149
+ const signal = opts.signal;
150
+ const runner = opts.runner ?? new FetchRunner();
151
+ const onRunnerNote = opts.onRunnerNote;
152
+ const auth = await probeSynapAuth(runner, synapUrl, apiKey, signal, onRunnerNote);
153
+ if (auth.status !== "pass") {
154
+ const skipMsg = auth.status === "skip" ? "Skipped \u2014 Synap URL or API key missing" : "Skipped \u2014 auth probe didn't pass";
155
+ return [
156
+ auth,
157
+ skipDiag("hub-protocol-idempotency", IDEMPOTENCY_NAME, skipMsg),
158
+ skipDiag("hub-protocol-events", EVENTS_NAME, skipMsg),
159
+ skipDiag("hub-protocol-sub-tokens", SUB_TOKENS_NAME, skipMsg)
160
+ ];
161
+ }
162
+ const [idem, sse, subTokens] = await Promise.all([
163
+ probeIdempotency(runner, synapUrl, apiKey, signal),
164
+ probeEventStream(runner, synapUrl, apiKey, signal),
165
+ probeSubTokens(runner, synapUrl, apiKey, signal)
166
+ ]);
167
+ return [auth, idem, sse, subTokens];
168
+ }
169
+ async function probeSynapAuth(runner, synapUrl, apiKey, outer, onRunnerNote) {
170
+ const start = Date.now();
171
+ if (!synapUrl) {
172
+ return {
173
+ id: "hub-protocol-openapi",
174
+ name: AUTH_NAME,
175
+ status: "skip",
176
+ message: "Synap apiUrl not configured in secrets",
177
+ fix: "Re-run setup or open the AI page \u2192 Save",
178
+ durationMs: Date.now() - start
179
+ };
180
+ }
181
+ if (!apiKey) {
182
+ return {
183
+ id: "hub-protocol-openapi",
184
+ name: AUTH_NAME,
185
+ status: "skip",
186
+ message: "Synap API key missing \u2014 auth probe needs Bearer auth",
187
+ fix: "Re-install Synap (`eve add synap`) to provision an API key",
188
+ durationMs: Date.now() - start
189
+ };
190
+ }
191
+ void onRunnerNote;
192
+ const result = await getAuthStatus({
193
+ synapUrl,
194
+ apiKey,
195
+ runner,
196
+ signal: outer,
197
+ timeoutMs: AUTH_TIMEOUT_MS
198
+ });
199
+ if (result.ok) {
200
+ return {
201
+ id: "hub-protocol-openapi",
202
+ name: AUTH_NAME,
203
+ status: "pass",
204
+ message: authPassMessage(result.status),
205
+ durationMs: Date.now() - start
206
+ };
207
+ }
208
+ return failureToDiagnostic(result.failure, synapUrl, Date.now() - start);
209
+ }
210
+ function authPassMessage(s) {
211
+ const userTag = s.userId.slice(0, 8);
212
+ const emailPart = s.userEmail ?? "no email";
213
+ const scopes = s.scopes.length > 0 ? `[${s.scopes.join(", ")}]` : "[no scopes]";
214
+ return `Auth OK \u2014 user ${userTag} (${emailPart}), scopes ${scopes}, key age ${s.ageDays}d`;
215
+ }
216
+ function failureToDiagnostic(failure, synapUrl, durationMs) {
217
+ switch (failure.reason) {
218
+ case "key_revoked":
219
+ return {
220
+ id: "hub-protocol-openapi",
221
+ name: AUTH_NAME,
222
+ status: "fail",
223
+ message: failure.message || "API key was revoked or never registered",
224
+ fix: "eve auth renew",
225
+ durationMs
226
+ };
227
+ case "expired":
228
+ return {
229
+ id: "hub-protocol-openapi",
230
+ name: AUTH_NAME,
231
+ status: "fail",
232
+ message: failure.message || "API key expired",
233
+ fix: "eve auth renew",
234
+ durationMs
235
+ };
236
+ case "invalid_format":
237
+ return {
238
+ id: "hub-protocol-openapi",
239
+ name: AUTH_NAME,
240
+ status: "fail",
241
+ message: failure.message || "API key format is malformed",
242
+ fix: "Re-run `eve install` or `eve auth renew`",
243
+ durationMs
244
+ };
245
+ case "no_auth":
246
+ return {
247
+ id: "hub-protocol-openapi",
248
+ name: AUTH_NAME,
249
+ status: "fail",
250
+ message: "No bearer key sent \u2014 internal probe bug or missing agents.eve.hubApiKey",
251
+ fix: "Run: eve auth provision (or check ~/.eve/secrets/secrets.json has agents.eve.hubApiKey set)",
252
+ durationMs
253
+ };
254
+ case "missing_scope":
255
+ return {
256
+ id: "hub-protocol-openapi",
257
+ name: AUTH_NAME,
258
+ status: "fail",
259
+ message: failure.message || `Key lacks required scope '${failure.missingScope ?? "?"}'`,
260
+ fix: failure.missingScope ? `eve auth grant ${failure.missingScope}` : "Re-mint the agent key with the required scopes (eve auth renew)",
261
+ durationMs
262
+ };
263
+ case "backend_unhealthy":
264
+ return {
265
+ id: "hub-protocol-openapi",
266
+ name: AUTH_NAME,
267
+ status: "fail",
268
+ message: failure.message || `Backend returned ${failure.httpStatus}`,
269
+ fix: "docker logs synap-backend-backend-1 --tail 50",
270
+ durationMs
271
+ };
272
+ case "transport":
273
+ return {
274
+ id: "hub-protocol-openapi",
275
+ name: AUTH_NAME,
276
+ status: "fail",
277
+ message: `Cannot reach pod at ${synapUrl} (${failure.message})`,
278
+ fix: "Run `eve doctor` for a deeper view (check Synap container is running)",
279
+ durationMs
280
+ };
281
+ case "unknown":
282
+ default:
283
+ return {
284
+ id: "hub-protocol-openapi",
285
+ name: AUTH_NAME,
286
+ status: "fail",
287
+ message: failure.message || `Auth check failed (${failure.httpStatus})`,
288
+ durationMs
289
+ };
290
+ }
291
+ }
292
+ async function probeSynapDiscovery(runner, synapUrl, outer) {
293
+ const start = Date.now();
294
+ if (!synapUrl) {
295
+ return {
296
+ id: "hub-protocol-discovery",
297
+ name: DISCOVERY_NAME,
298
+ status: "skip",
299
+ message: "Synap apiUrl not configured",
300
+ durationMs: Date.now() - start
301
+ };
302
+ }
303
+ const url = `${synapUrl.replace(/\/+$/, "")}/api/hub/openapi.json`;
304
+ const res = await runner.httpGet(
305
+ url,
306
+ { Accept: "application/json" },
307
+ { signal: outer, timeoutMs: OPENAPI_TIMEOUT_MS }
308
+ );
309
+ if (res.status === 0) {
310
+ return {
311
+ id: "hub-protocol-discovery",
312
+ name: DISCOVERY_NAME,
313
+ status: "skip",
314
+ message: `Could not reach ${url} (${res.error ?? "network error"})`,
315
+ durationMs: Date.now() - start
316
+ };
317
+ }
318
+ if (res.status === 200) {
319
+ let version = "?";
320
+ try {
321
+ const parsed = JSON.parse(res.body);
322
+ if (typeof parsed.openapi === "string") version = parsed.openapi;
323
+ } catch {
324
+ }
325
+ return {
326
+ id: "hub-protocol-discovery",
327
+ name: DISCOVERY_NAME,
328
+ status: "pass",
329
+ message: `Public openapi.json reachable (OpenAPI ${version})`,
330
+ durationMs: Date.now() - start
331
+ };
332
+ }
333
+ if (res.status === 401 || res.status === 403) {
334
+ return {
335
+ id: "hub-protocol-discovery",
336
+ name: DISCOVERY_NAME,
337
+ status: "skip",
338
+ message: `Backend still requires auth on /api/hub/openapi.json (${res.status}) \u2014 predates the public-discovery release`,
339
+ durationMs: Date.now() - start
340
+ };
341
+ }
342
+ if (res.status === 404) {
343
+ return {
344
+ id: "hub-protocol-discovery",
345
+ name: DISCOVERY_NAME,
346
+ status: "skip",
347
+ message: "Older backend \u2014 /api/hub/openapi.json not exposed",
348
+ durationMs: Date.now() - start
349
+ };
350
+ }
351
+ return {
352
+ id: "hub-protocol-discovery",
353
+ name: DISCOVERY_NAME,
354
+ status: "skip",
355
+ message: `Unexpected status ${res.status} from ${url}`,
356
+ durationMs: Date.now() - start
357
+ };
358
+ }
359
+ async function probeIdempotency(runner, synapUrl, apiKey, outer) {
360
+ const start = Date.now();
361
+ if (!synapUrl || !apiKey) {
362
+ return skipDiag("hub-protocol-idempotency", IDEMPOTENCY_NAME, "Skipped \u2014 Synap URL or API key missing");
363
+ }
364
+ const url = `${synapUrl}/api/hub/entities`;
365
+ const idempotencyKey = randomUUID();
366
+ const body = {
367
+ profileSlug: "note",
368
+ title: "eve-doctor-idempotency-smoke",
369
+ source: "openwebui-pipeline"
370
+ };
371
+ const bodyJson = JSON.stringify(body);
372
+ const headers = {
373
+ Authorization: `Bearer ${apiKey}`,
374
+ "Content-Type": "application/json",
375
+ Accept: "application/json",
376
+ "Idempotency-Key": idempotencyKey
377
+ };
378
+ const post = () => runner.httpPost(url, headers, bodyJson, {
379
+ signal: outer,
380
+ timeoutMs: ENTITY_POST_TIMEOUT_MS
381
+ });
382
+ const first = await post();
383
+ if (first.status === 0) {
384
+ return {
385
+ id: "hub-protocol-idempotency",
386
+ name: IDEMPOTENCY_NAME,
387
+ status: "fail",
388
+ message: `Cannot reach ${url} (${first.error ?? "network error"})`,
389
+ fix: "Check the Synap container is running and the URL is reachable",
390
+ durationMs: Date.now() - start
391
+ };
392
+ }
393
+ if (first.status === 401 || first.status === 403) {
394
+ return {
395
+ id: "hub-protocol-idempotency",
396
+ name: IDEMPOTENCY_NAME,
397
+ status: "fail",
398
+ message: `API key rejected \u2014 ${first.status} from POST /api/hub/entities`,
399
+ fix: "Re-run setup to mint a fresh agent API key",
400
+ durationMs: Date.now() - start
401
+ };
402
+ }
403
+ if (first.status === 404) {
404
+ return {
405
+ id: "hub-protocol-idempotency",
406
+ name: IDEMPOTENCY_NAME,
407
+ status: "fail",
408
+ message: "POST /api/hub/entities not available \u2014 backend version too old",
409
+ fix: "Update Synap (`eve update synap`)",
410
+ durationMs: Date.now() - start
411
+ };
412
+ }
413
+ if (first.status < 200 || first.status >= 300) {
414
+ return {
415
+ id: "hub-protocol-idempotency",
416
+ name: IDEMPOTENCY_NAME,
417
+ status: "fail",
418
+ message: `First POST returned ${first.status}: ${truncate(first.body)}`,
419
+ durationMs: Date.now() - start
420
+ };
421
+ }
422
+ const second = await post();
423
+ if (second.status === 0) {
424
+ return {
425
+ id: "hub-protocol-idempotency",
426
+ name: IDEMPOTENCY_NAME,
427
+ status: "fail",
428
+ message: `Second POST failed (${second.error ?? "network error"}) \u2014 couldn't verify replay`,
429
+ durationMs: Date.now() - start
430
+ };
431
+ }
432
+ if (second.status < 200 || second.status >= 300) {
433
+ return {
434
+ id: "hub-protocol-idempotency",
435
+ name: IDEMPOTENCY_NAME,
436
+ status: "fail",
437
+ message: `Second POST returned ${second.status}: ${truncate(second.body)}`,
438
+ durationMs: Date.now() - start
439
+ };
440
+ }
441
+ const entityId = extractEntityId(first.body);
442
+ if (entityId) {
443
+ void deleteEntityBestEffort(synapUrl, apiKey, entityId, runner);
444
+ }
445
+ const replayHeader = second.headers["x-idempotent-replay"];
446
+ if (replayHeader !== "true") {
447
+ return {
448
+ id: "hub-protocol-idempotency",
449
+ name: IDEMPOTENCY_NAME,
450
+ status: "fail",
451
+ message: "Backend doesn't echo X-Idempotent-Replay \u2014 idempotency middleware not mounted",
452
+ fix: "Update Synap (`eve update synap`) \u2014 needs 2026-05+ Hub Protocol",
453
+ durationMs: Date.now() - start
454
+ };
455
+ }
456
+ if (first.body !== second.body) {
457
+ return {
458
+ id: "hub-protocol-idempotency",
459
+ name: IDEMPOTENCY_NAME,
460
+ status: "fail",
461
+ message: "Backend created a duplicate entity (idempotency middleware not honouring the key)",
462
+ fix: "Update Synap (`eve update synap`) \u2014 needs 2026-05+ Hub Protocol",
463
+ durationMs: Date.now() - start
464
+ };
465
+ }
466
+ return {
467
+ id: "hub-protocol-idempotency",
468
+ name: IDEMPOTENCY_NAME,
469
+ status: "pass",
470
+ message: "Replay confirmed \u2014 same Idempotency-Key returns the cached response",
471
+ durationMs: Date.now() - start
472
+ };
473
+ }
474
+ async function probeEventStream(runner, synapUrl, apiKey, outer) {
475
+ const start = Date.now();
476
+ if (!synapUrl || !apiKey) {
477
+ return skipDiag("hub-protocol-events", EVENTS_NAME, "Skipped \u2014 Synap URL or API key missing");
478
+ }
479
+ const url = `${synapUrl}/api/hub/events/stream`;
480
+ const headers = { Authorization: `Bearer ${apiKey}`, Accept: "text/event-stream" };
481
+ const stream = await runner.httpStream(url, headers, { signal: outer, timeoutMs: SSE_TIMEOUT_MS });
482
+ if (!stream.ok) {
483
+ if (stream.error === "stream-not-supported") {
484
+ return skipDiag(
485
+ "hub-protocol-events",
486
+ EVENTS_NAME,
487
+ "Skipped \u2014 SSE probing not supported by current runner (e.g. docker-exec wget without unbuffered piping)"
488
+ );
489
+ }
490
+ if (stream.status === 401 || stream.status === 403) {
491
+ return {
492
+ id: "hub-protocol-events",
493
+ name: EVENTS_NAME,
494
+ status: "fail",
495
+ message: `API key rejected on /api/hub/events/stream (${stream.status})`,
496
+ fix: "Re-run setup to mint a fresh agent API key",
497
+ durationMs: Date.now() - start
498
+ };
499
+ }
500
+ if (stream.status === 404) {
501
+ return {
502
+ id: "hub-protocol-events",
503
+ name: EVENTS_NAME,
504
+ status: "fail",
505
+ message: "GET /api/hub/events/stream not available \u2014 backend version too old",
506
+ fix: "Update Synap (`eve update synap`)",
507
+ durationMs: Date.now() - start
508
+ };
509
+ }
510
+ if (stream.status === 0) {
511
+ const aborted = stream.error?.toLowerCase().includes("abort") || stream.error?.toLowerCase().includes("timeout");
512
+ return {
513
+ id: "hub-protocol-events",
514
+ name: EVENTS_NAME,
515
+ status: "fail",
516
+ message: aborted ? `No SSE response within ${SSE_TIMEOUT_MS / 1e3}s \u2014 proxy may be killing the stream` : `Cannot open event stream at ${url} (${stream.error ?? "network error"})`,
517
+ fix: aborted ? "Check reverse-proxy buffering: nginx needs `proxy_buffering off` for SSE" : "Check the Synap container is running",
518
+ durationMs: Date.now() - start
519
+ };
520
+ }
521
+ return {
522
+ id: "hub-protocol-events",
523
+ name: EVENTS_NAME,
524
+ status: "fail",
525
+ message: `Event stream returned ${stream.status} (${stream.error ?? "no body"})`,
526
+ durationMs: Date.now() - start
527
+ };
528
+ }
529
+ let received = false;
530
+ let receivedKind = "";
531
+ let buffer = "";
532
+ let readError = null;
533
+ try {
534
+ if (stream.chunks) {
535
+ for await (const chunk of stream.chunks) {
536
+ buffer += chunk;
537
+ const lines = buffer.split("\n");
538
+ buffer = lines.pop() ?? "";
539
+ for (const line of lines) {
540
+ const trimmed = line.trim();
541
+ if (trimmed.startsWith("event:")) {
542
+ const kind = trimmed.slice(6).trim();
543
+ if (kind === "heartbeat" || kind === "event") {
544
+ received = true;
545
+ receivedKind = kind;
546
+ break;
547
+ }
548
+ }
549
+ }
550
+ if (received) break;
551
+ }
552
+ }
553
+ } catch (err) {
554
+ if (!(err instanceof Error && err.name === "AbortError")) {
555
+ readError = err instanceof Error ? err : new Error(String(err));
556
+ }
557
+ } finally {
558
+ await stream.close();
559
+ }
560
+ if (readError) {
561
+ return {
562
+ id: "hub-protocol-events",
563
+ name: EVENTS_NAME,
564
+ status: "fail",
565
+ message: `Stream read error: ${readError.message}`,
566
+ durationMs: Date.now() - start
567
+ };
568
+ }
569
+ if (!received) {
570
+ return {
571
+ id: "hub-protocol-events",
572
+ name: EVENTS_NAME,
573
+ status: "fail",
574
+ message: `No heartbeat received within ${SSE_TIMEOUT_MS / 1e3}s \u2014 stream may be proxied without flush`,
575
+ fix: "Check reverse-proxy: SSE needs `proxy_buffering off` and no response gzip",
576
+ durationMs: Date.now() - start
577
+ };
578
+ }
579
+ return {
580
+ id: "hub-protocol-events",
581
+ name: EVENTS_NAME,
582
+ status: "pass",
583
+ message: `Stream open \u2014 received \`${receivedKind}\` frame within ${SSE_TIMEOUT_MS / 1e3}s`,
584
+ durationMs: Date.now() - start
585
+ };
586
+ }
587
+ async function probeSubTokens(runner, synapUrl, apiKey, outer) {
588
+ const start = Date.now();
589
+ if (!synapUrl || !apiKey) {
590
+ return skipDiag("hub-protocol-sub-tokens", SUB_TOKENS_NAME, "Skipped \u2014 Synap URL or API key missing");
591
+ }
592
+ const url = `${synapUrl}/api/hub/users/me`;
593
+ const externalUserId = `eve-doctor-smoke-${Date.now()}`;
594
+ const fetchMe = async (extraHeaders) => {
595
+ const res = await runner.httpGet(
596
+ url,
597
+ {
598
+ Authorization: `Bearer ${apiKey}`,
599
+ Accept: "application/json",
600
+ ...extraHeaders
601
+ },
602
+ { signal: outer, timeoutMs: ME_TIMEOUT_MS }
603
+ );
604
+ if (res.status === 0) {
605
+ return {
606
+ ok: false,
607
+ status: 0,
608
+ message: res.error ?? "network error"
609
+ };
610
+ }
611
+ if (res.status < 200 || res.status >= 300) {
612
+ return { ok: false, status: res.status, message: truncate(res.body) };
613
+ }
614
+ let id = null;
615
+ try {
616
+ const parsed = JSON.parse(res.body);
617
+ const candidate = parsed.id ?? parsed.user?.id;
618
+ if (typeof candidate === "string") id = candidate;
619
+ } catch {
620
+ return { ok: false, status: res.status, message: "response was not JSON" };
621
+ }
622
+ return { ok: true, userId: id, status: res.status };
623
+ };
624
+ const parent = await fetchMe({});
625
+ if (!parent.ok) {
626
+ if (parent.status === 401 || parent.status === 403) {
627
+ return {
628
+ id: "hub-protocol-sub-tokens",
629
+ name: SUB_TOKENS_NAME,
630
+ status: "fail",
631
+ message: `API key rejected on /api/hub/users/me (${parent.status})`,
632
+ fix: "Re-run setup to mint a fresh agent API key",
633
+ durationMs: Date.now() - start
634
+ };
635
+ }
636
+ if (parent.status === 404) {
637
+ return {
638
+ id: "hub-protocol-sub-tokens",
639
+ name: SUB_TOKENS_NAME,
640
+ status: "fail",
641
+ message: "GET /api/hub/users/me not available \u2014 backend version too old",
642
+ fix: "Update Synap (`eve update synap`)",
643
+ durationMs: Date.now() - start
644
+ };
645
+ }
646
+ return {
647
+ id: "hub-protocol-sub-tokens",
648
+ name: SUB_TOKENS_NAME,
649
+ status: "fail",
650
+ message: `Couldn't fetch parent user (${parent.status || "network error"}): ${parent.message}`,
651
+ durationMs: Date.now() - start
652
+ };
653
+ }
654
+ if (!parent.userId) {
655
+ return {
656
+ id: "hub-protocol-sub-tokens",
657
+ name: SUB_TOKENS_NAME,
658
+ status: "fail",
659
+ message: "Backend returned /users/me without an id field",
660
+ durationMs: Date.now() - start
661
+ };
662
+ }
663
+ const sub = await fetchMe({ "X-External-User-Id": externalUserId });
664
+ if (!sub.ok) {
665
+ return {
666
+ id: "hub-protocol-sub-tokens",
667
+ name: SUB_TOKENS_NAME,
668
+ status: "fail",
669
+ message: `Couldn't fetch user with X-External-User-Id (${sub.status || "network error"}): ${sub.message}`,
670
+ durationMs: Date.now() - start
671
+ };
672
+ }
673
+ if (!sub.userId) {
674
+ return {
675
+ id: "hub-protocol-sub-tokens",
676
+ name: SUB_TOKENS_NAME,
677
+ status: "fail",
678
+ message: "Backend returned /users/me without an id field on sub-token call",
679
+ durationMs: Date.now() - start
680
+ };
681
+ }
682
+ if (parent.userId === sub.userId) {
683
+ return {
684
+ id: "hub-protocol-sub-tokens",
685
+ name: SUB_TOKENS_NAME,
686
+ status: "skip",
687
+ message: "Multi-user mode: OFF \u2014 X-External-User-Id ignored (HUB_PROTOCOL_SUB_TOKENS=false)",
688
+ durationMs: Date.now() - start
689
+ };
690
+ }
691
+ return {
692
+ id: "hub-protocol-sub-tokens",
693
+ name: SUB_TOKENS_NAME,
694
+ status: "pass",
695
+ message: `Multi-user mode: ON \u2014 sub-token resolved to a separate user id (parent=${shortId(parent.userId)}, sub=${shortId(sub.userId)})`,
696
+ durationMs: Date.now() - start
697
+ };
698
+ }
699
+ function skipDiag(id, name, message) {
700
+ return { id, name, status: "skip", message, durationMs: 0 };
701
+ }
702
+ function truncate(text, max = 160) {
703
+ const oneline = text.replace(/\s+/g, " ").trim();
704
+ return oneline.length > max ? `${oneline.slice(0, max)}\u2026` : oneline;
705
+ }
706
+ function shortId(id) {
707
+ return id.length > 8 ? `\u2026${id.slice(-8)}` : id;
708
+ }
709
+ function extractEntityId(text) {
710
+ try {
711
+ const parsed = JSON.parse(text);
712
+ const direct = parsed.id;
713
+ if (typeof direct === "string") return direct;
714
+ const nested = parsed.entity?.id;
715
+ if (typeof nested === "string") return nested;
716
+ return null;
717
+ } catch {
718
+ return null;
719
+ }
720
+ }
721
+ async function deleteEntityBestEffort(synapUrl, apiKey, entityId, runner) {
722
+ const r = runner ?? new FetchRunner();
723
+ try {
724
+ await r.httpDelete(
725
+ `${synapUrl}/api/hub/entities/${encodeURIComponent(entityId)}`,
726
+ { Authorization: `Bearer ${apiKey}` },
727
+ { timeoutMs: DELETE_TIMEOUT_MS }
728
+ );
729
+ } catch {
730
+ }
731
+ }
732
+ function combineSignals(outer, timeoutMs) {
733
+ const ac = new AbortController();
734
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
735
+ let onAbort = null;
736
+ if (outer) {
737
+ if (outer.aborted) {
738
+ ac.abort();
739
+ } else {
740
+ onAbort = () => ac.abort();
741
+ outer.addEventListener("abort", onAbort);
742
+ }
743
+ }
744
+ return {
745
+ signal: ac.signal,
746
+ cleanup: () => {
747
+ clearTimeout(timer);
748
+ if (onAbort && outer) outer.removeEventListener("abort", onAbort);
749
+ }
750
+ };
751
+ }
752
+ function headersToRecord(h) {
753
+ const out = {};
754
+ h.forEach((value, key) => {
755
+ out[key.toLowerCase()] = value;
756
+ });
757
+ return out;
758
+ }
759
+ function isFetchTransportError(err) {
760
+ if (!(err instanceof Error)) return false;
761
+ const cause = err.cause;
762
+ const code = cause?.code;
763
+ if (code === "ECONNREFUSED" || code === "EHOSTUNREACH" || code === "ENOTFOUND" || code === "EAI_AGAIN") {
764
+ return true;
765
+ }
766
+ const msg = err.message || "";
767
+ return /\bECONNREFUSED\b|\bEHOSTUNREACH\b|\bENOTFOUND\b/.test(msg);
768
+ }
769
+ var FetchRunner = class {
770
+ name = "fetch";
771
+ async httpGet(url, headers, opts = {}) {
772
+ return this.request("GET", url, headers, void 0, opts);
773
+ }
774
+ async httpPost(url, headers, body, opts = {}) {
775
+ return this.request("POST", url, headers, body, opts);
776
+ }
777
+ async httpDelete(url, headers, opts = {}) {
778
+ return this.request("DELETE", url, headers, void 0, opts);
779
+ }
780
+ async request(method, url, headers, body, opts) {
781
+ const ac = combineSignals(opts.signal, opts.timeoutMs ?? 6e3);
782
+ try {
783
+ const res = await fetch(url, {
784
+ method,
785
+ headers,
786
+ body,
787
+ signal: ac.signal
788
+ });
789
+ const text = await res.text();
790
+ return {
791
+ status: res.status,
792
+ body: text,
793
+ headers: headersToRecord(res.headers)
794
+ };
795
+ } catch (err) {
796
+ return {
797
+ status: 0,
798
+ body: "",
799
+ headers: {},
800
+ error: err instanceof Error ? err.message : "network error"
801
+ };
802
+ } finally {
803
+ ac.cleanup();
804
+ }
805
+ }
806
+ async httpStream(url, headers, opts = {}) {
807
+ const ac = combineSignals(opts.signal, opts.timeoutMs ?? 35e3);
808
+ let res;
809
+ try {
810
+ res = await fetch(url, { method: "GET", headers, signal: ac.signal });
811
+ } catch (err) {
812
+ ac.cleanup();
813
+ return {
814
+ ok: false,
815
+ status: 0,
816
+ headers: {},
817
+ close: async () => {
818
+ },
819
+ error: err instanceof Error ? err.message : "network error"
820
+ };
821
+ }
822
+ if (!res.ok || !res.body) {
823
+ try {
824
+ await res.body?.cancel();
825
+ } catch {
826
+ }
827
+ ac.cleanup();
828
+ return {
829
+ ok: false,
830
+ status: res.status,
831
+ headers: headersToRecord(res.headers),
832
+ close: async () => {
833
+ },
834
+ error: res.body ? void 0 : "no body"
835
+ };
836
+ }
837
+ const reader = res.body.getReader();
838
+ const decoder = new TextDecoder();
839
+ async function* iterate() {
840
+ while (true) {
841
+ const { done, value } = await reader.read();
842
+ if (done) return;
843
+ if (value) yield decoder.decode(value, { stream: true });
844
+ }
845
+ }
846
+ const close = async () => {
847
+ try {
848
+ await reader.cancel();
849
+ } catch {
850
+ }
851
+ ac.cleanup();
852
+ };
853
+ return {
854
+ ok: true,
855
+ status: res.status,
856
+ headers: headersToRecord(res.headers),
857
+ chunks: iterate(),
858
+ close
859
+ };
860
+ }
861
+ };
862
+ var execAsync = promisify(execCallback);
863
+ var EVE_AGENT_TYPE = "eve";
864
+ var EVE_EXTRA_SCOPES = ["providers.write"];
865
+ var DEFAULT_TIMEOUT_MS = 6e3;
866
+ async function getAuthStatus(opts) {
867
+ const synapUrl = opts.synapUrl.replace(/\/+$/, "");
868
+ if (!synapUrl) {
869
+ return {
870
+ ok: false,
871
+ failure: {
872
+ reason: "no_auth",
873
+ httpStatus: 0,
874
+ message: "synapUrl not configured"
875
+ }
876
+ };
877
+ }
878
+ if (!opts.apiKey) {
879
+ return {
880
+ ok: false,
881
+ failure: {
882
+ reason: "no_auth",
883
+ httpStatus: 0,
884
+ message: "apiKey not configured"
885
+ }
886
+ };
887
+ }
888
+ const runner = opts.runner ?? new FetchRunner();
889
+ const url = `${synapUrl}/api/hub/auth/status`;
890
+ const headers = {
891
+ Authorization: `Bearer ${opts.apiKey}`,
892
+ Accept: "application/json"
893
+ };
894
+ const res = await runner.httpGet(url, headers, {
895
+ signal: opts.signal,
896
+ timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
897
+ });
898
+ if (res.status === 0) {
899
+ return {
900
+ ok: false,
901
+ failure: {
902
+ reason: "transport",
903
+ httpStatus: 0,
904
+ message: res.error ?? "network error"
905
+ }
906
+ };
907
+ }
908
+ if (res.status >= 500) {
909
+ return {
910
+ ok: false,
911
+ failure: {
912
+ reason: "backend_unhealthy",
913
+ httpStatus: res.status,
914
+ message: `Backend returned ${res.status}`,
915
+ raw: tryJson(res.body)
916
+ }
917
+ };
918
+ }
919
+ if (res.status === 401 || res.status === 403) {
920
+ return parse401(res.status, res.body);
921
+ }
922
+ if (res.status === 404) {
923
+ return {
924
+ ok: false,
925
+ failure: {
926
+ reason: "backend_unhealthy",
927
+ httpStatus: 404,
928
+ message: "GET /api/hub/auth/status not available \u2014 backend version too old",
929
+ raw: tryJson(res.body)
930
+ }
931
+ };
932
+ }
933
+ if (res.status < 200 || res.status >= 300) {
934
+ return {
935
+ ok: false,
936
+ failure: {
937
+ reason: "unknown",
938
+ httpStatus: res.status,
939
+ message: `Backend returned ${res.status}`,
940
+ raw: tryJson(res.body)
941
+ }
942
+ };
943
+ }
944
+ const parsed = tryJson(res.body);
945
+ if (!parsed || typeof parsed !== "object") {
946
+ return {
947
+ ok: false,
948
+ failure: {
949
+ reason: "unknown",
950
+ httpStatus: res.status,
951
+ message: "auth/status returned non-JSON body"
952
+ }
953
+ };
954
+ }
955
+ const body = parsed;
956
+ const keyId = stringOr(body.keyId, "");
957
+ const userId = stringOr(body.userId, "");
958
+ const createdAtStr = stringOr(body.createdAt, "");
959
+ if (!keyId || !userId || !createdAtStr) {
960
+ return {
961
+ ok: false,
962
+ failure: {
963
+ reason: "unknown",
964
+ httpStatus: res.status,
965
+ message: "auth/status payload missing required fields (keyId/userId/createdAt)",
966
+ raw: parsed
967
+ }
968
+ };
969
+ }
970
+ const ageDays = computeAgeDays(createdAtStr);
971
+ const status = {
972
+ keyId,
973
+ keyIdPrefix: stringOr(body.keyIdPrefix, keyId.slice(0, 8)),
974
+ userId,
975
+ userEmail: stringOrNull(body.userEmail),
976
+ userName: stringOrNull(body.userName),
977
+ name: stringOrNull(body.name),
978
+ scopes: stringArray(body.scopes),
979
+ createdAt: createdAtStr,
980
+ expiresAt: stringOrNull(body.expiresAt),
981
+ lastUsedAt: stringOrNull(body.lastUsedAt),
982
+ ageDays,
983
+ parentKeyId: stringOrNull(body.parentKeyId),
984
+ isActive: true,
985
+ raw: parsed
986
+ };
987
+ return { ok: true, status };
988
+ }
989
+ async function isKeyValid(opts) {
990
+ const r = await getAuthStatus(opts);
991
+ return r.ok;
992
+ }
993
+ var RENEW_TIMEOUT_MS = 1e4;
994
+ async function renewAgentKey(opts = {}) {
995
+ const agentType = opts.agentType ?? "eve";
996
+ const result = await provisionAgent({
997
+ agentType,
998
+ deployDir: opts.deployDir,
999
+ reason: opts.reason ?? "renew",
1000
+ runner: opts.runner,
1001
+ timeoutMs: opts.timeoutMs,
1002
+ provisioningToken: opts.provisioningToken,
1003
+ idempotent: opts.idempotent
1004
+ });
1005
+ if (result.provisioned) {
1006
+ return {
1007
+ renewed: true,
1008
+ apiKey: result.record.hubApiKey,
1009
+ keyIdPrefix: result.keyIdPrefix,
1010
+ agentType,
1011
+ wasAlreadyPresent: result.wasAlreadyPresent
1012
+ };
1013
+ }
1014
+ return { renewed: false, reason: result.reason, agentType };
1015
+ }
1016
+ async function provisionAgent(opts) {
1017
+ const agentType = opts.agentType;
1018
+ if (!agentType || agentType.trim().length === 0) {
1019
+ return { provisioned: false, agentType: "", reason: "agentType is required" };
1020
+ }
1021
+ const cwd = opts.deployDir ?? process.env.EVE_HOME ?? process.cwd();
1022
+ const secrets = await readEveSecrets(cwd);
1023
+ const synapUrl = (opts.synapUrl ?? await resolveSynapUrlOnHost(secrets)).trim();
1024
+ if (!synapUrl) {
1025
+ const hasSecrets = !!secrets;
1026
+ return {
1027
+ provisioned: false,
1028
+ agentType,
1029
+ reason: hasSecrets ? "synap pod URL unresolved \u2014 set domain.primary or synap.apiUrl in ~/.eve/secrets.json" : "~/.eve/secrets.json not found \u2014 run `eve setup` first to initialise Eve on this server"
1030
+ };
1031
+ }
1032
+ let tokenLookup = opts.provisioningToken ? { token: opts.provisioningToken, source: "explicit", diagnosticReason: "" } : await resolveProvisioningTokenWithDiagnostics();
1033
+ if (!tokenLookup.token && !opts.provisioningToken) {
1034
+ try {
1035
+ const ensured = await ensurePodProvisioningToken();
1036
+ tokenLookup = {
1037
+ token: ensured.token,
1038
+ source: ensured.source === "generated" ? "file" : "file",
1039
+ diagnosticReason: ""
1040
+ };
1041
+ if (ensured.source === "generated" && ensured.backendRestarted) {
1042
+ await waitForBackend(synapUrl, opts.runner ?? new FetchRunner());
1043
+ }
1044
+ } catch (err) {
1045
+ return {
1046
+ provisioned: false,
1047
+ agentType,
1048
+ reason: tokenLookup.diagnosticReason || `Could not auto-generate PROVISIONING_TOKEN: ${err instanceof Error ? err.message : String(err)}`
1049
+ };
1050
+ }
1051
+ }
1052
+ if (!tokenLookup.token) {
1053
+ return {
1054
+ provisioned: false,
1055
+ agentType,
1056
+ reason: tokenLookup.diagnosticReason
1057
+ };
1058
+ }
1059
+ const provisioningToken = tokenLookup.token;
1060
+ const runner = opts.runner ?? new FetchRunner();
1061
+ const url = `${synapUrl.replace(/\/+$/, "")}/api/hub/setup/agent`;
1062
+ const previousPrefix = (secrets?.agents?.[agentType]?.hubApiKey ?? secrets?.synap?.apiKey ?? "").slice(0, 8);
1063
+ const body = JSON.stringify({
1064
+ agentType,
1065
+ // Scopes BEYOND the pod's shared agent bundle, granted per-agent.
1066
+ //
1067
+ // Only `eve` asks, and only for `providers.write`. Writing an ai_providers
1068
+ // row sets the URL the pod sends every prompt to, which is why the pod keeps
1069
+ // that scope out of the default bundle — so requesting it for every agent
1070
+ // here would undo the narrowing rather than satisfy it. `eve` needs it
1071
+ // because it is the operator tool that registers providers (e.g. the
1072
+ // FreeLLMAPI gateway) on the user's behalf.
1073
+ //
1074
+ // The pod refuses this from a weak surface key; we authenticate with the
1075
+ // PROVISIONING_TOKEN, which is operator-grade. An older pod without the
1076
+ // scope ignores the unknown field — the key simply lacks it, and
1077
+ // `upsertPodProvider` then reports the 403 in plain words.
1078
+ ...agentType === EVE_AGENT_TYPE ? { extraScopes: EVE_EXTRA_SCOPES } : {},
1079
+ // Diagnostic hints — backend ignores unknown fields. Useful in pod
1080
+ // logs to attribute key churn ("renew", "migrate", "install") and
1081
+ // to chain new keys back to the previous one if the backend tracks
1082
+ // parentKeyId.
1083
+ name: `eve-${agentType}`,
1084
+ parentKeyIdPrefix: previousPrefix || void 0,
1085
+ reason: opts.reason ?? "provision",
1086
+ idempotent: opts.idempotent ?? false
1087
+ });
1088
+ const headers = {
1089
+ Authorization: `Bearer ${provisioningToken}`,
1090
+ "Content-Type": "application/json",
1091
+ Accept: "application/json"
1092
+ };
1093
+ const res = await runner.httpPost(url, headers, body, {
1094
+ timeoutMs: opts.timeoutMs ?? RENEW_TIMEOUT_MS
1095
+ });
1096
+ if (res.status === 0) {
1097
+ return {
1098
+ provisioned: false,
1099
+ agentType,
1100
+ reason: `Cannot reach ${url} (${res.error ?? "network error"})`
1101
+ };
1102
+ }
1103
+ if (res.status === 202) {
1104
+ const parsed2 = tryJson(res.body);
1105
+ const adminUrl = typeof parsed2 === "object" && parsed2 && typeof parsed2.adminUrl === "string" ? parsed2.adminUrl : null;
1106
+ return {
1107
+ provisioned: false,
1108
+ agentType,
1109
+ reason: `Backend deferred the request (issuer pending approval). Visit ${adminUrl ?? `${synapUrl}/admin/trusted-issuers`} to approve, then retry.`
1110
+ };
1111
+ }
1112
+ if (res.status === 401 || res.status === 403) {
1113
+ return {
1114
+ provisioned: false,
1115
+ agentType,
1116
+ reason: `PROVISIONING_TOKEN rejected (${res.status}) \u2014 token expired, wrong, or backend doesn't accept it. Re-check deploy/.env or run \`eve install\`.`
1117
+ };
1118
+ }
1119
+ if (res.status === 404) {
1120
+ const parsed404 = tryJson(res.body);
1121
+ const isBackend404 = parsed404 !== null && typeof parsed404 === "object" && ("error" in parsed404 || "message" in parsed404);
1122
+ if (isBackend404) {
1123
+ const setupUrl = synapUrl.replace(/127\.0\.0\.1:\d+/, "pod.<your-domain>").replace(/:\d+$/, "");
1124
+ return {
1125
+ provisioned: false,
1126
+ agentType,
1127
+ reason: `No admin account on this pod \u2014 the backend returned 404 because there is no workspace to create the agent in. Create your first admin at ${setupUrl}/setup (or run \`eve auth provision --email <you@example.com>\` to be prompted inline), then retry.`
1128
+ };
1129
+ }
1130
+ return {
1131
+ provisioned: false,
1132
+ agentType,
1133
+ reason: `POST ${url} returned 404 (route not matched). Check Traefik routes with \`eve domain check\` \u2014 or the backend image may be outdated (run \`eve update synap\`). Raw: ${res.body.slice(0, 120)}`
1134
+ };
1135
+ }
1136
+ if (res.status < 200 || res.status >= 300) {
1137
+ return {
1138
+ provisioned: false,
1139
+ agentType,
1140
+ reason: `Backend returned ${res.status}: ${res.body.slice(0, 160)}`
1141
+ };
1142
+ }
1143
+ const parsed = tryJson(res.body);
1144
+ if (!parsed || typeof parsed !== "object") {
1145
+ return {
1146
+ provisioned: false,
1147
+ agentType,
1148
+ reason: "Mint response was not JSON"
1149
+ };
1150
+ }
1151
+ const obj = parsed;
1152
+ if (obj.alreadyValid === true) {
1153
+ const serverWorkspaceId = stringOr(obj.workspaceId, "");
1154
+ try {
1155
+ const existing = await readAgentKey(agentType, cwd);
1156
+ if (existing?.hubApiKey) {
1157
+ const updated = {
1158
+ ...existing,
1159
+ ...serverWorkspaceId ? { workspaceId: serverWorkspaceId } : {}
1160
+ };
1161
+ await writeAgentKey(agentType, updated, cwd);
1162
+ return {
1163
+ provisioned: true,
1164
+ agentType,
1165
+ record: updated,
1166
+ keyIdPrefix: (updated.keyId ?? updated.hubApiKey).slice(0, 8),
1167
+ wasAlreadyPresent: true
1168
+ };
1169
+ }
1170
+ } catch {
1171
+ }
1172
+ }
1173
+ const hubApiKey = stringOr(obj.hubApiKey, stringOr(obj.apiKey, ""));
1174
+ if (!hubApiKey) {
1175
+ return {
1176
+ provisioned: false,
1177
+ agentType,
1178
+ reason: "Mint response did not include hubApiKey"
1179
+ };
1180
+ }
1181
+ const agentUserId = stringOr(obj.agentUserId, "");
1182
+ let workspaceId = stringOr(obj.workspaceId, "");
1183
+ if (!workspaceId) {
1184
+ try {
1185
+ const existing = await readAgentKey(agentType, cwd);
1186
+ if (existing?.workspaceId) workspaceId = existing.workspaceId;
1187
+ } catch {
1188
+ }
1189
+ }
1190
+ const keyId = stringOr(obj.keyId, "");
1191
+ const keyIdPrefix = stringOr(
1192
+ obj.keyIdPrefix,
1193
+ keyId.slice(0, 8) || hubApiKey.slice(0, 8)
1194
+ );
1195
+ if (!agentUserId) {
1196
+ return {
1197
+ provisioned: false,
1198
+ agentType,
1199
+ reason: "Mint response missing required field: agentUserId"
1200
+ };
1201
+ }
1202
+ const record = {
1203
+ hubApiKey,
1204
+ agentUserId,
1205
+ workspaceId,
1206
+ keyId: keyId || void 0,
1207
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1208
+ };
1209
+ try {
1210
+ await writeAgentKey(agentType, record, cwd);
1211
+ } catch (err) {
1212
+ return {
1213
+ provisioned: false,
1214
+ agentType,
1215
+ reason: `Mint succeeded but secrets.json write failed: ${err instanceof Error ? err.message : String(err)}`
1216
+ };
1217
+ }
1218
+ try {
1219
+ await writeEveSecrets({ pod: { bootstrapToken: provisioningToken } }, cwd);
1220
+ } catch {
1221
+ }
1222
+ return { provisioned: true, agentType, record, keyIdPrefix };
1223
+ }
1224
+ async function provisionAllAgents(opts) {
1225
+ const cwd = opts.deployDir ?? process.env.EVE_HOME ?? process.cwd();
1226
+ const skipIfPresent = opts.skipIfPresent ?? true;
1227
+ const agents = agentsToProvision(opts.installedComponentIds);
1228
+ const results = [];
1229
+ for (const agent of agents) {
1230
+ if (skipIfPresent) {
1231
+ const existing = await readAgentKey(agent.agentType, cwd);
1232
+ if (existing && existing.hubApiKey) {
1233
+ results.push({
1234
+ provisioned: true,
1235
+ agentType: agent.agentType,
1236
+ record: existing,
1237
+ keyIdPrefix: (existing.keyId ?? existing.hubApiKey).slice(0, 8),
1238
+ wasAlreadyPresent: true
1239
+ });
1240
+ continue;
1241
+ }
1242
+ }
1243
+ const result = await provisionAgent({
1244
+ agentType: agent.agentType,
1245
+ deployDir: cwd,
1246
+ reason: opts.reason ?? "provision-all",
1247
+ runner: opts.runner,
1248
+ synapUrl: opts.synapUrl,
1249
+ provisioningToken: opts.provisioningToken
1250
+ });
1251
+ results.push(result);
1252
+ }
1253
+ return results;
1254
+ }
1255
+ async function migrateLegacyToAgents(opts) {
1256
+ const cwd = opts.deployDir ?? process.env.EVE_HOME ?? process.cwd();
1257
+ const secretsBefore = await readEveSecrets(cwd);
1258
+ const collapse = await collapseLegacyCoderAgents(secretsBefore, cwd);
1259
+ const eveExisting = await readAgentKey("eve", cwd);
1260
+ const needsPerAgent = !eveExisting || !eveExisting.hubApiKey;
1261
+ if (!needsPerAgent && !collapse.collapsed) {
1262
+ return {
1263
+ migrated: false,
1264
+ results: [],
1265
+ reason: "already migrated \u2014 eve agent key present and no legacy coder slugs"
1266
+ };
1267
+ }
1268
+ const results = await provisionAllAgents({
1269
+ installedComponentIds: opts.installedComponentIds,
1270
+ deployDir: cwd,
1271
+ reason: collapse.collapsed ? "migrate-legacy-coder-collapse" : "migrate-legacy",
1272
+ runner: opts.runner,
1273
+ skipIfPresent: !needsPerAgent
1274
+ });
1275
+ const anyOk = results.some((r) => r.provisioned);
1276
+ return {
1277
+ migrated: anyOk || collapse.collapsed,
1278
+ results,
1279
+ reason: anyOk ? void 0 : collapse.collapsed ? "coder collapse applied; new agent provisioning failed \u2014 see per-agent reasons" : "no agent could be provisioned \u2014 see per-agent reasons",
1280
+ collapsedToEngine: collapse.engine,
1281
+ strippedLegacySlugs: collapse.stripped
1282
+ };
1283
+ }
1284
+ async function collapseLegacyCoderAgents(secrets, cwd) {
1285
+ if (!secrets) return { collapsed: false, stripped: [] };
1286
+ const agents = secrets.agents ?? {};
1287
+ const presentLegacy = LEGACY_CODER_ENGINE_SLUGS.filter(
1288
+ (slug) => agents[slug] !== void 0
1289
+ );
1290
+ if (presentLegacy.length === 0) {
1291
+ return { collapsed: false, stripped: [] };
1292
+ }
1293
+ const existingChoice = secrets.builder?.codeEngine;
1294
+ const pickedEngine = existingChoice && LEGACY_CODER_ENGINE_SLUGS.includes(existingChoice) ? existingChoice : presentLegacy[0];
1295
+ await writeCodeEngine(pickedEngine, cwd);
1296
+ const nextAgents = {};
1297
+ for (const [slug, rec] of Object.entries(agents)) {
1298
+ if (LEGACY_CODER_ENGINE_SLUGS.includes(slug)) continue;
1299
+ nextAgents[slug] = rec;
1300
+ }
1301
+ await writeEveSecrets({ agents: nextAgents }, cwd);
1302
+ return { collapsed: true, engine: pickedEngine, stripped: presentLegacy };
1303
+ }
1304
+ async function resolveProvisioningToken() {
1305
+ return (await resolveProvisioningTokenWithDiagnostics()).token;
1306
+ }
1307
+ async function waitForBackend(synapUrl, runner, opts = {}) {
1308
+ const timeoutMs = opts.timeoutMs ?? 2e4;
1309
+ const intervalMs = opts.intervalMs ?? 500;
1310
+ const url = `${synapUrl.replace(/\/+$/, "")}/api/hub/health`;
1311
+ const start = Date.now();
1312
+ while (Date.now() - start < timeoutMs) {
1313
+ try {
1314
+ const res = await runner.httpGet(url, {}, { timeoutMs: 1500 });
1315
+ if (res.status >= 200 && res.status < 500) return;
1316
+ } catch {
1317
+ }
1318
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
1319
+ }
1320
+ }
1321
+ async function ensurePodProvisioningToken() {
1322
+ const probe = await resolveProvisioningTokenWithDiagnostics();
1323
+ if (probe.token) {
1324
+ return { token: probe.token, source: "existing" };
1325
+ }
1326
+ const deployDir = findPodDeployDir();
1327
+ if (!deployDir) {
1328
+ throw new Error(
1329
+ "No pod deploy directory found \u2014 set SYNAP_DEPLOY_DIR or install synap-backend first (checked: " + POD_DEPLOY_DIR_CANDIDATES.join(", ") + ")."
1330
+ );
1331
+ }
1332
+ const token = randomBytes(32).toString("hex");
1333
+ const result = writeEnvVar(deployDir, "PROVISIONING_TOKEN", token);
1334
+ const envFilePath = join2(deployDir, ".env");
1335
+ const backendRestarted = restartBackendContainer(deployDir);
1336
+ void result;
1337
+ return {
1338
+ token,
1339
+ source: "generated",
1340
+ writtenTo: envFilePath,
1341
+ backendRestarted
1342
+ };
1343
+ }
1344
+ async function resolveProvisioningTokenWithDiagnostics() {
1345
+ const envName = process.env.EVE_PROVISIONING_TOKEN !== void 0 ? "EVE_PROVISIONING_TOKEN" : process.env.PROVISIONING_TOKEN !== void 0 ? "PROVISIONING_TOKEN" : null;
1346
+ const envValue = process.env.EVE_PROVISIONING_TOKEN ?? process.env.PROVISIONING_TOKEN;
1347
+ if (envName !== null && (envValue ?? "").trim().length === 0) {
1348
+ return {
1349
+ token: null,
1350
+ source: "empty",
1351
+ diagnosticReason: `${envName} is set but empty. Either unset it (so we can fall back to the pod's deploy/.env) or assign a real value (\`openssl rand -hex 32\`).`
1352
+ };
1353
+ }
1354
+ if (envValue && envValue.trim().length > 0) {
1355
+ return { token: envValue.trim(), source: "env", diagnosticReason: "" };
1356
+ }
1357
+ const dirCandidates = [
1358
+ process.env.SYNAP_DEPLOY_DIR,
1359
+ "/opt/synap-backend/deploy",
1360
+ "/opt/synap-backend",
1361
+ "/opt/synap/deploy",
1362
+ "/opt/synap",
1363
+ "/opt/synap-pod/deploy",
1364
+ "/opt/synap-pod",
1365
+ "/srv/synap-backend/deploy",
1366
+ "/srv/synap/deploy"
1367
+ ].filter((d) => typeof d === "string" && d.length > 0);
1368
+ let fileFoundEmpty = false;
1369
+ let fileFoundEmptyAt = "";
1370
+ for (const dir of dirCandidates) {
1371
+ const envPath2 = join2(dir, ".env");
1372
+ if (!existsSync2(envPath2)) continue;
1373
+ const probe = readEnvFileVarWithStatus(envPath2, "PROVISIONING_TOKEN");
1374
+ if (probe.value) {
1375
+ return { token: probe.value, source: "file", diagnosticReason: "" };
1376
+ }
1377
+ if (probe.status === "found-empty" && !fileFoundEmpty) {
1378
+ fileFoundEmpty = true;
1379
+ fileFoundEmptyAt = envPath2;
1380
+ }
1381
+ }
1382
+ const dockerProbe = await readProvisioningTokenFromDockerWithStatus();
1383
+ if (dockerProbe.value) {
1384
+ return { token: dockerProbe.value, source: "docker", diagnosticReason: "" };
1385
+ }
1386
+ if (fileFoundEmpty || dockerProbe.status === "found-empty") {
1387
+ const where = fileFoundEmpty ? fileFoundEmptyAt : "the running synap-backend container's env";
1388
+ return {
1389
+ token: null,
1390
+ source: "empty",
1391
+ diagnosticReason: `PROVISIONING_TOKEN is empty in ${where}. The pod was deployed without filling it in. Fix: \`openssl rand -hex 32\` and write the value into the pod's deploy/.env, then \`docker compose up -d backend\` (or \`docker restart synap-backend-backend-1\`) and retry. See \`eve auth bootstrap-token\` for an automated path once supported.`
1392
+ };
1393
+ }
1394
+ return {
1395
+ token: null,
1396
+ source: "missing",
1397
+ diagnosticReason: "PROVISIONING_TOKEN unavailable. Set EVE_PROVISIONING_TOKEN, or ensure the pod's deploy/.env is readable. PROVISIONING_TOKEN was generated when synap-backend was first installed and lives in deploy/.env on the pod host."
1398
+ };
1399
+ }
1400
+ function readEnvFileVarWithStatus(envPath2, key) {
1401
+ try {
1402
+ const text = readFileSync2(envPath2, "utf-8");
1403
+ let sawEmpty = false;
1404
+ for (const raw of text.split(/\r?\n/)) {
1405
+ const line = raw.trim();
1406
+ if (!line || line.startsWith("#")) continue;
1407
+ const eq = line.indexOf("=");
1408
+ if (eq < 0) continue;
1409
+ if (line.slice(0, eq).trim() === key) {
1410
+ const value = line.slice(eq + 1).trim();
1411
+ if (value.length > 0) return { value, status: "found" };
1412
+ sawEmpty = true;
1413
+ }
1414
+ }
1415
+ return { value: null, status: sawEmpty ? "found-empty" : "absent" };
1416
+ } catch {
1417
+ return { value: null, status: "absent" };
1418
+ }
1419
+ }
1420
+ async function readProvisioningTokenFromDockerWithStatus() {
1421
+ let sawEmpty = false;
1422
+ for (const container of SYNAP_BACKEND_CONTAINERS) {
1423
+ try {
1424
+ const { stdout: out } = await execAsync(
1425
+ `docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' ${container}`,
1426
+ { encoding: "utf-8", timeout: 4e3 }
1427
+ );
1428
+ for (const raw of out.split(/\r?\n/)) {
1429
+ const line = raw.trim();
1430
+ if (!line) continue;
1431
+ const eq = line.indexOf("=");
1432
+ if (eq < 0) continue;
1433
+ if (line.slice(0, eq) === "PROVISIONING_TOKEN") {
1434
+ const value = line.slice(eq + 1).trim();
1435
+ if (value.length > 0) return { value, status: "found" };
1436
+ sawEmpty = true;
1437
+ }
1438
+ }
1439
+ } catch {
1440
+ }
1441
+ }
1442
+ return { value: null, status: sawEmpty ? "found-empty" : "absent" };
1443
+ }
1444
+ var STRUCTURED_REASONS = /* @__PURE__ */ new Set([
1445
+ "key_revoked",
1446
+ "missing_scope",
1447
+ "expired",
1448
+ "invalid_format",
1449
+ "no_auth"
1450
+ ]);
1451
+ function parse401(httpStatus, body) {
1452
+ const parsed = tryJson(body);
1453
+ if (parsed && typeof parsed === "object") {
1454
+ const obj = parsed;
1455
+ const reasonStr = stringOr(obj.reason, "");
1456
+ if (reasonStr && STRUCTURED_REASONS.has(reasonStr)) {
1457
+ return {
1458
+ ok: false,
1459
+ failure: {
1460
+ reason: reasonStr,
1461
+ httpStatus,
1462
+ message: stringOr(obj.message, `unauthorized (${reasonStr})`),
1463
+ missingScope: stringOr(obj.missingScope, "") || void 0,
1464
+ keyIdPrefix: stringOr(obj.keyIdPrefix, "") || void 0,
1465
+ raw: parsed
1466
+ }
1467
+ };
1468
+ }
1469
+ }
1470
+ return {
1471
+ ok: false,
1472
+ failure: {
1473
+ reason: "key_revoked",
1474
+ httpStatus,
1475
+ message: `unauthorized (${httpStatus}) \u2014 backend did not return a structured envelope`,
1476
+ raw: parsed ?? body
1477
+ }
1478
+ };
1479
+ }
1480
+ function stringOr(v, fallback) {
1481
+ return typeof v === "string" ? v : fallback;
1482
+ }
1483
+ function stringOrNull(v) {
1484
+ return typeof v === "string" ? v : null;
1485
+ }
1486
+ function stringArray(v) {
1487
+ if (!Array.isArray(v)) return [];
1488
+ return v.filter((x) => typeof x === "string");
1489
+ }
1490
+ function tryJson(text) {
1491
+ if (!text) return null;
1492
+ try {
1493
+ return JSON.parse(text);
1494
+ } catch {
1495
+ return null;
1496
+ }
1497
+ }
1498
+ function computeAgeDays(createdAt) {
1499
+ const t = Date.parse(createdAt);
1500
+ if (Number.isNaN(t)) return 0;
1501
+ const ms = Date.now() - t;
1502
+ if (ms <= 0) return 0;
1503
+ return Math.floor(ms / (1e3 * 60 * 60 * 24));
1504
+ }
1505
+
1506
+ export {
1507
+ readEnvVar,
1508
+ readEnvFile,
1509
+ writeEnvVar,
1510
+ runHubProtocolProbes,
1511
+ probeSynapDiscovery,
1512
+ truncate,
1513
+ shortId,
1514
+ extractEntityId,
1515
+ deleteEntityBestEffort,
1516
+ combineSignals,
1517
+ isFetchTransportError,
1518
+ FetchRunner,
1519
+ getAuthStatus,
1520
+ isKeyValid,
1521
+ renewAgentKey,
1522
+ provisionAgent,
1523
+ provisionAllAgents,
1524
+ migrateLegacyToAgents,
1525
+ resolveProvisioningToken,
1526
+ waitForBackend,
1527
+ ensurePodProvisioningToken
1528
+ };
1529
+ //# sourceMappingURL=chunk-JO5H5G6U.js.map