aui-agent-builder 0.4.32 → 0.4.34

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 (80) hide show
  1. package/dist/api-client/apollo-client.d.ts +64 -0
  2. package/dist/api-client/apollo-client.d.ts.map +1 -1
  3. package/dist/api-client/apollo-client.js +110 -21
  4. package/dist/api-client/apollo-client.js.map +1 -1
  5. package/dist/api-client/index.d.ts +11 -0
  6. package/dist/api-client/index.d.ts.map +1 -1
  7. package/dist/api-client/index.js +69 -28
  8. package/dist/api-client/index.js.map +1 -1
  9. package/dist/api-client/mock-db-client.d.ts +8 -0
  10. package/dist/api-client/mock-db-client.d.ts.map +1 -1
  11. package/dist/api-client/mock-db-client.js +33 -0
  12. package/dist/api-client/mock-db-client.js.map +1 -1
  13. package/dist/api-client/rag-client.d.ts +16 -0
  14. package/dist/api-client/rag-client.d.ts.map +1 -1
  15. package/dist/api-client/rag-client.js +56 -18
  16. package/dist/api-client/rag-client.js.map +1 -1
  17. package/dist/commands/agents.d.ts.map +1 -1
  18. package/dist/commands/agents.js +13 -5
  19. package/dist/commands/agents.js.map +1 -1
  20. package/dist/commands/init.d.ts.map +1 -1
  21. package/dist/commands/init.js +46 -0
  22. package/dist/commands/init.js.map +1 -1
  23. package/dist/commands/mockdb-guide.d.ts +1 -1
  24. package/dist/commands/mockdb-guide.d.ts.map +1 -1
  25. package/dist/commands/mockdb-guide.js +29 -17
  26. package/dist/commands/mockdb-guide.js.map +1 -1
  27. package/dist/commands/mockdb.d.ts +6 -0
  28. package/dist/commands/mockdb.d.ts.map +1 -1
  29. package/dist/commands/mockdb.js +68 -24
  30. package/dist/commands/mockdb.js.map +1 -1
  31. package/dist/commands/push.js +49 -0
  32. package/dist/commands/push.js.map +1 -1
  33. package/dist/commands/vault.d.ts +25 -0
  34. package/dist/commands/vault.d.ts.map +1 -0
  35. package/dist/commands/vault.js +220 -0
  36. package/dist/commands/vault.js.map +1 -0
  37. package/dist/config/index.d.ts +9 -9
  38. package/dist/config/index.js +4 -4
  39. package/dist/index.js +149 -23
  40. package/dist/index.js.map +1 -1
  41. package/dist/services/integration.service.d.ts +1 -1
  42. package/dist/services/integration.service.d.ts.map +1 -1
  43. package/dist/services/integration.service.js +4 -2
  44. package/dist/services/integration.service.js.map +1 -1
  45. package/dist/services/kb-identity.d.ts +54 -0
  46. package/dist/services/kb-identity.d.ts.map +1 -0
  47. package/dist/services/kb-identity.js +152 -0
  48. package/dist/services/kb-identity.js.map +1 -0
  49. package/dist/services/kb-view.service.d.ts.map +1 -1
  50. package/dist/services/kb-view.service.js +14 -3
  51. package/dist/services/kb-view.service.js.map +1 -1
  52. package/dist/services/mock-db.service.d.ts +22 -27
  53. package/dist/services/mock-db.service.d.ts.map +1 -1
  54. package/dist/services/mock-db.service.js +337 -179
  55. package/dist/services/mock-db.service.js.map +1 -1
  56. package/dist/services/vault.service.d.ts +58 -0
  57. package/dist/services/vault.service.d.ts.map +1 -0
  58. package/dist/services/vault.service.js +143 -0
  59. package/dist/services/vault.service.js.map +1 -0
  60. package/dist/types/index.d.ts +1 -0
  61. package/dist/types/index.d.ts.map +1 -1
  62. package/dist/types/index.js +1 -0
  63. package/dist/types/index.js.map +1 -1
  64. package/dist/types/vault.d.ts +31 -0
  65. package/dist/types/vault.d.ts.map +1 -0
  66. package/dist/types/vault.js +10 -0
  67. package/dist/types/vault.js.map +1 -0
  68. package/dist/ui/views/VaultView.d.ts +19 -0
  69. package/dist/ui/views/VaultView.d.ts.map +1 -0
  70. package/dist/ui/views/VaultView.js +35 -0
  71. package/dist/ui/views/VaultView.js.map +1 -0
  72. package/dist/utils/fetch-with-timeout.d.ts +26 -0
  73. package/dist/utils/fetch-with-timeout.d.ts.map +1 -1
  74. package/dist/utils/fetch-with-timeout.js +172 -1
  75. package/dist/utils/fetch-with-timeout.js.map +1 -1
  76. package/dist/utils/json-output.d.ts +11 -0
  77. package/dist/utils/json-output.d.ts.map +1 -1
  78. package/dist/utils/json-output.js +19 -1
  79. package/dist/utils/json-output.js.map +1 -1
  80. package/package.json +1 -1
@@ -19,6 +19,9 @@
19
19
  * calls use the per-agent `runtimeKey`. Keys are persisted under `~/.aui/`
20
20
  * (out of the project repo).
21
21
  */
22
+ import * as fs from "fs";
23
+ import * as path from "path";
24
+ import { trace } from "@opentelemetry/api";
22
25
  import { getConfig, loadProjectConfig, findProjectRoot, loadMockDbKeys, saveMockDbKeys, clearMockDbKeys, getMockDbBaseUrl, getLegacyMockDbBaseUrl, } from "../config/index.js";
23
26
  import { getAuthenticatedSession } from "./account.service.js";
24
27
  import { MockDbClient, } from "../api-client/mock-db-client.js";
@@ -28,7 +31,7 @@ import { CLIError, ConfigError } from "../errors/index.js";
28
31
  * Resolve session + `.auirc` into everything a mock-db call needs. Throws
29
32
  * clear config errors when the agent id can't be determined.
30
33
  */
31
- export async function resolveMockDbContext(options, opts = {}) {
34
+ export async function resolveMockDbContext(options) {
32
35
  const session = await getAuthenticatedSession();
33
36
  const projectRoot = options.path
34
37
  ? options.path
@@ -46,15 +49,14 @@ export async function resolveMockDbContext(options, opts = {}) {
46
49
  config.organizationId ||
47
50
  session.organization_id;
48
51
  const apiUrl = session.api_url || config.apiUrl;
49
- const { backend, baseUrl, deferredCommit } = await resolveMockDbBackend({
52
+ const { backend, baseUrl } = await resolveMockDbBackend({
50
53
  agentId,
51
54
  accountId,
52
55
  organizationId,
53
56
  apiUrl,
54
57
  authToken: session.auth_token,
55
58
  environment: session.environment,
56
- auiManaged: options.auiManaged,
57
- deferAuiManagedPersist: opts.deferAuiManagedPersist,
59
+ projectRoot,
58
60
  });
59
61
  const client = new MockDbClient({ environment: session.environment, baseUrl });
60
62
  return {
@@ -69,91 +71,180 @@ export async function resolveMockDbContext(options, opts = {}) {
69
71
  baseUrl,
70
72
  projectRoot,
71
73
  projectConfig,
72
- commitBackendOnProvision: deferredCommit,
73
74
  };
74
75
  }
75
76
  /**
76
- * Decide which mock-db deployment an agent talks to and the base URL for it.
77
+ * Best-effort span event on the active `aui.mockdb.*` span. Used to make the
78
+ * backend decision, key recovery, and cache writes visible in Logfire with full
79
+ * detail. Never throws — telemetry must never break a command.
80
+ */
81
+ function mockdbEvent(name, attrs) {
82
+ try {
83
+ trace.getActiveSpan()?.addEvent(name, attrs);
84
+ }
85
+ catch {
86
+ // telemetry is best-effort
87
+ }
88
+ }
89
+ /** Classify a mock-db execute URL by host → which backend owns the agent. */
90
+ function classifyMockDbHost(url) {
91
+ if (url.includes("mock-db-service.vercel.app"))
92
+ return "vercel";
93
+ if (url.includes("/api/mock-db/") || url.includes("/api/mock-db?")) {
94
+ return "aui-managed";
95
+ }
96
+ return null;
97
+ }
98
+ /** Pull the literal runtime key out of an integration endpoint's auth header.
99
+ * Handles both accepted forms (`Authorization: Bearer <key>` and
100
+ * `X-API-Key: <key>`); returns undefined for templated/placeholder values. */
101
+ function extractRuntimeKey(endpoint) {
102
+ const ep = endpoint;
103
+ const apiKey = ep?.authentication?.apiKey;
104
+ if (!apiKey || typeof apiKey.value !== "string")
105
+ return undefined;
106
+ const raw = apiKey.value.trim();
107
+ if (!raw || /\{\{.*\}\}/.test(raw))
108
+ return undefined; // templated → not a literal
109
+ const headerName = typeof apiKey.key === "string" ? apiKey.key.toLowerCase() : "";
110
+ const key = headerName === "authorization" ? raw.replace(/^Bearer\s+/i, "").trim() : raw;
111
+ return key.startsWith("mdb_run_") ? key : undefined;
112
+ }
113
+ /**
114
+ * Inspect the agent's pulled `integrations.aui.json` to determine which backend
115
+ * its mock DB lives on — the ground truth of what the deployed agent calls, and
116
+ * the ONLY signal available for a Vercel agent (Vercel has no `/status`). Also
117
+ * recovers the runtime key from the wired auth header when it's a literal.
77
118
  *
78
- * Precedence:
79
- * 1. `--aui-managed` explicit opt-in; sticky (persisted), always wins.
80
- * 2. `AUI_MOCKDB_URL` / `AUI_TEST_BASE_URL` — a fixed host overrides routing;
81
- * we keep the agent's known label but point at the override.
82
- * 3. Stored `backend` — the cached decision (fast path, no network).
83
- * 4. Legacy keys with no `backend` field — agents provisioned before this
84
- * field existed are, by definition, on the legacy (Vercel) deployment.
85
- * 5. No local state at all — a NEW agent (or one with no mock DB yet). New
86
- * mock DBs are ALWAYS created on the AUI-managed backend; the CLI never
87
- * creates a new one on the legacy Vercel deployment. (An agent that already
88
- * lives on Vercel keeps working via its cached backend/keys in step 4 until
89
- * it is auto-migrated.) No `/status` probe is needed — an AUI-owned agent
90
- * would bind here anyway, and skipping it also drops a round-trip on the
91
- * very management path that degrades under load.
119
+ * Backend-agnostic and offline: works for an imported agent with no local cache.
120
+ * Returns null when there's no project, no file, or no mock-db integration.
121
+ */
122
+ export function detectBackendFromIntegrations(projectRoot, agentId) {
123
+ if (!projectRoot)
124
+ return null;
125
+ let parsed;
126
+ try {
127
+ const file = path.join(projectRoot, "integrations.aui.json");
128
+ if (!fs.existsSync(file))
129
+ return null;
130
+ parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
131
+ }
132
+ catch {
133
+ return null; // unreadable / non-JSON → no signal
134
+ }
135
+ const list = Array.isArray(parsed?.integrations)
136
+ ? (parsed.integrations)
137
+ : Array.isArray(parsed)
138
+ ? parsed
139
+ : [];
140
+ // Match this agent's execute URL on either backend host.
141
+ const execRe = new RegExp(`/api/agents/${agentId.replace(/[^a-zA-Z0-9]/g, "\\$&")}/apis/[^/]+/execute`);
142
+ for (const intg of list) {
143
+ const endpoint = intg?.settings
144
+ ?.endpoint;
145
+ const url = typeof endpoint?.url === "string" ? endpoint.url : "";
146
+ if (!url || !execRe.test(url))
147
+ continue;
148
+ const backend = classifyMockDbHost(url);
149
+ if (!backend)
150
+ continue;
151
+ return { backend, runtimeKey: extractRuntimeKey(endpoint), matchedUrl: url };
152
+ }
153
+ return null;
154
+ }
155
+ /**
156
+ * Decide which mock-db deployment an agent talks to, deterministically and
157
+ * WITHOUT trusting the local cache as authority. Precedence (see the
158
+ * mock-database-simulation skill for the full design):
92
159
  *
93
- * The decision is cached to `~/.aui/mockdb/<agentId>.json` so steps 3/4 cover
94
- * essentially every call after the first.
160
+ * 0. `AUI_MOCKDB_URL` / `AUI_TEST_BASE_URL` a fixed override host wins
161
+ * (manual override / flow-test collapse); keep the cached label if any.
162
+ * 1. AUI `/status` (token auth) — authoritative owner-check for the
163
+ * AUI-managed backend. `exists && hasData === true` → AUI-managed. A
164
+ * blank/unreachable `/status` is treated as "unknown" (never a negative)
165
+ * and falls through.
166
+ * 2. `integrations.aui.json` — the ground truth of what the deployed agent
167
+ * actually calls, and the ONLY Vercel detector (Vercel has no `/status`).
168
+ * A vercel.app host → Vercel; a `/api/mock-db` gateway host → AUI-managed.
169
+ * The wired runtime key is recovered here too.
170
+ * 3. Nothing owns it and nothing is wired → a NEW agent → AUI-managed
171
+ * (provision creates it there; we never create a new DB on Vercel).
95
172
  *
96
- * Full design / rollout notes: ../../MOCKDB_AUI_MANAGED_BACKEND.md
173
+ * The result is written back to `~/.aui/mockdb/<agentId>.json` as a fast-path
174
+ * memo — but it is never CONSULTED as an authority (steps 1–3 re-derive it), so
175
+ * a stale/wrong cache can't misroute. Parallel work on DIFFERENT agents is fully
176
+ * isolated (per-agent file, per-agent keys).
97
177
  */
98
178
  async function resolveMockDbBackend(args) {
99
- const stored = loadMockDbKeys(args.agentId);
100
- // 1. Explicit opt-in — sticky.
101
- if (args.auiManaged) {
179
+ // 0. Fixed override host (manual override or flow-test collapse).
180
+ if (process.env.AUI_MOCKDB_URL || process.env.AUI_TEST_BASE_URL) {
181
+ const stored = loadMockDbKeys(args.agentId);
182
+ const backend = stored.backend ?? "aui-managed";
102
183
  const baseUrl = getMockDbBaseUrl(args.environment);
103
- // `provision` defers the commit: route to AUI-managed for this call but DON'T
104
- // rewrite the routing cache yet. provision commits it only after the agent is
105
- // actually established there, so a blanked/failed provision can never strand
106
- // the agent on a dead backend with the wrong (stale) keys.
107
- if (args.deferAuiManagedPersist) {
108
- return { backend: "aui-managed", baseUrl, deferredCommit: true };
184
+ mockdbEvent("mockdb.backend_resolved", {
185
+ "mockdb.resolve.method": "env_override",
186
+ "mockdb.backend": backend,
187
+ "mockdb.base_url": baseUrl,
188
+ "mockdb.agent_id": args.agentId,
189
+ });
190
+ return { backend, baseUrl };
191
+ }
192
+ // 1. AUI /status — authoritative for the AUI-managed backend.
193
+ try {
194
+ const status = await fetchAuiStatus(args);
195
+ if (status.exists && status.hasData === true) {
196
+ const baseUrl = getMockDbBaseUrl(args.environment);
197
+ persistBackend(args.agentId, "aui-managed", baseUrl);
198
+ mockdbEvent("mockdb.backend_resolved", {
199
+ "mockdb.resolve.method": "status",
200
+ "mockdb.backend": "aui-managed",
201
+ "mockdb.base_url": baseUrl,
202
+ "mockdb.agent_id": args.agentId,
203
+ });
204
+ return { backend: "aui-managed", baseUrl };
109
205
  }
110
- return persistBackend(args.agentId, "aui-managed", baseUrl);
111
206
  }
112
- // 2. A fixed override host (manual override or flow-test collapse) — route
113
- // there without re-deriving or probing. Keep the known label if any.
114
- if (process.env.AUI_MOCKDB_URL || process.env.AUI_TEST_BASE_URL) {
115
- return {
116
- backend: stored.backend ?? "vercel",
117
- baseUrl: getMockDbBaseUrl(args.environment),
118
- };
207
+ catch {
208
+ // unreachable / blank / no account "unknown", fall through (never negative)
119
209
  }
120
- // 3. Cached AUI-managed fast path. Never re-probe, never demote (an agent on
121
- // AUI-managed is committed there; nothing migrates it back to Vercel).
122
- if (stored.backend === "aui-managed") {
123
- return {
124
- backend: "aui-managed",
125
- baseUrl: stored.mockDbUrl ?? getMockDbBaseUrl(args.environment),
126
- };
210
+ // 2. integrations.aui.json ground truth of the wired backend (+ runtime key).
211
+ const detected = detectBackendFromIntegrations(args.projectRoot, args.agentId);
212
+ if (detected) {
213
+ const baseUrl = detected.backend === "vercel"
214
+ ? getLegacyMockDbBaseUrl()
215
+ : getMockDbBaseUrl(args.environment);
216
+ persistBackend(args.agentId, detected.backend, baseUrl);
217
+ if (detected.runtimeKey) {
218
+ const stored = loadMockDbKeys(args.agentId);
219
+ if (stored.runtimeKey !== detected.runtimeKey) {
220
+ saveMockDbKeys(args.agentId, { runtimeKey: detected.runtimeKey });
221
+ mockdbEvent("mockdb.runtime_key.recovered_from_integration", {
222
+ "mockdb.agent_id": args.agentId,
223
+ "mockdb.runtime_key_prefix": detected.runtimeKey.slice(0, 16),
224
+ "mockdb.matched_url": detected.matchedUrl,
225
+ });
226
+ }
227
+ }
228
+ mockdbEvent("mockdb.backend_resolved", {
229
+ "mockdb.resolve.method": "integration",
230
+ "mockdb.backend": detected.backend,
231
+ "mockdb.base_url": baseUrl,
232
+ "mockdb.agent_id": args.agentId,
233
+ "mockdb.runtime_key_recovered": !!detected.runtimeKey,
234
+ });
235
+ return { backend: detected.backend, baseUrl };
127
236
  }
128
- // 4. Vercel-cached explicit `"vercel"` OR legacy keys with no backend field.
129
- // These agents may since have been MIGRATED to AUI-managed (raw DB copy +
130
- // republished URLs). Opportunistically re-check `/status` (TTL-throttled)
131
- // and auto-switch when AUI now has the data, so users never have to clear a
132
- // stale cache by hand. Not migrated / unreachable / throttled → stay Vercel.
133
- const isVercelCached = stored.backend === "vercel" || !!(stored.managementKey || stored.runtimeKey);
134
- if (isVercelCached) {
135
- const migrated = await maybeAutoMigrate(args, stored).catch(() => null);
136
- if (migrated)
137
- return migrated;
138
- return persistBackend(args.agentId, "vercel", stored.mockDbUrl ?? getLegacyMockDbBaseUrl());
139
- }
140
- // 5. No local state → a NEW agent (or one with no mock DB yet). New mock DBs
141
- // are ALWAYS created on the AUI-managed backend now — we never create one
142
- // on the legacy Vercel deployment. (Agents already on Vercel keep working
143
- // via their cached backend/keys in step 4 until auto-migrated.)
237
+ // 3. Nothing owns it, nothing wired NEW AUI-managed.
144
238
  const baseUrl = getMockDbBaseUrl(args.environment);
145
- if (args.deferAuiManagedPersist) {
146
- // provision: route AUI-managed for this call but commit the routing only
147
- // after provision actually establishes the agent there — same no-stranding
148
- // guarantee as the explicit `--aui-managed` path (step 1).
149
- return { backend: "aui-managed", baseUrl, deferredCommit: true };
150
- }
151
- return persistBackend(args.agentId, "aui-managed", baseUrl);
152
- }
153
- /** How long to wait before re-checking whether a Vercel-cached agent has been
154
- * migrated to AUI-managed. Bounds the extra `/status` call to ~once per window
155
- * per agent (not per command), on the legacy path only. */
156
- const MIGRATION_RECHECK_TTL_MS = 6 * 60 * 60 * 1000; // 6h
239
+ persistBackend(args.agentId, "aui-managed", baseUrl);
240
+ mockdbEvent("mockdb.backend_resolved", {
241
+ "mockdb.resolve.method": "new",
242
+ "mockdb.backend": "aui-managed",
243
+ "mockdb.base_url": baseUrl,
244
+ "mockdb.agent_id": args.agentId,
245
+ });
246
+ return { backend: "aui-managed", baseUrl };
247
+ }
157
248
  /**
158
249
  * Read the AUI-managed `/status` for an agent (token-auth, read-only). Throws on
159
250
  * a blank body (transport blank under load) so callers treat it as "unknown"
@@ -184,60 +275,6 @@ async function fetchAuiStatus(args) {
184
275
  mgmtKeyPrefix: typeof r.mgmtKeyPrefix === "string" ? r.mgmtKeyPrefix : null,
185
276
  };
186
277
  }
187
- /**
188
- * Auto-detect a Vercel→AUI-managed migration for a Vercel-cached agent and
189
- * switch routing to AUI-managed, so no one has to delete a stale cache by hand.
190
- *
191
- * Safe by construction:
192
- * - **TTL-throttled** via `migrationCheckedAt` — at most one extra `/status`
193
- * per window per agent (and the timestamp is recorded even on a negative or
194
- * failed check, so we don't re-probe every command).
195
- * - **Switches only on strong evidence** (`exists && hasData === true`): never
196
- * binds an empty AUI shell; a not-yet-migrated agent stays on Vercel.
197
- * - **Never demotes** (only Vercel→AUI), and a blank/unreachable `/status`
198
- * leaves routing unchanged.
199
- * - **Key-safe**: a raw-copy migration preserves the keys (deterministic hash),
200
- * so when the stored runtime key's prefix matches AUI's we KEEP the keys. If
201
- * the prefixes differ (an export/replay migration minted new keys) we DROP
202
- * the stale keys so management falls back to the AUI token and `execute`
203
- * returns a clean "rotate-runtime-key" hint instead of a confusing 401.
204
- *
205
- * Returns the new AUI-managed routing on switch, or `null` to stay on Vercel.
206
- */
207
- async function maybeAutoMigrate(args, stored) {
208
- const now = Date.now();
209
- if (now - (stored.migrationCheckedAt ?? 0) < MIGRATION_RECHECK_TTL_MS) {
210
- return null; // checked recently — don't re-probe on every command
211
- }
212
- let status = null;
213
- try {
214
- status = await fetchAuiStatus(args);
215
- }
216
- catch {
217
- status = null; // unreachable / blank → leave routing unchanged
218
- }
219
- // Not migrated (or couldn't tell): record the check time so we throttle, stay Vercel.
220
- if (!(status && status.exists === true && status.hasData === true)) {
221
- saveMockDbKeys(args.agentId, { migrationCheckedAt: now });
222
- return null;
223
- }
224
- // Migrated → switch to AUI-managed.
225
- const baseUrl = getMockDbBaseUrl(args.environment);
226
- const keysCarryOver = !stored.runtimeKey ||
227
- !status.runtimeKeyPrefix ||
228
- stored.runtimeKey.slice(0, status.runtimeKeyPrefix.length) ===
229
- status.runtimeKeyPrefix;
230
- if (!keysCarryOver) {
231
- // Stale keys won't authenticate on the new backend — drop them cleanly.
232
- clearMockDbKeys(args.agentId);
233
- }
234
- saveMockDbKeys(args.agentId, {
235
- backend: "aui-managed",
236
- mockDbUrl: baseUrl,
237
- migrationCheckedAt: now,
238
- });
239
- return { backend: "aui-managed", baseUrl };
240
- }
241
278
  /** Persist the backend decision (cache) and return it. */
242
279
  function persistBackend(agentId, backend, baseUrl) {
243
280
  const stored = loadMockDbKeys(agentId);
@@ -266,40 +303,107 @@ function tokenAuth(ctx) {
266
303
  };
267
304
  }
268
305
  /**
269
- * Run a management call, preferring the stored per-agent management key, and
270
- * transparently fall back to the AUI token (Apollo-delegated) if that key is
271
- * rejected (401/403).
306
+ * Run a management call. The management key is sourced from the cache first, and
307
+ * a fresh one is MINTED (token-auth `rotate-mgmt-key`) + cached when the cache
308
+ * has none or the stored one is rejected (401/403). The AUI token is the safety
309
+ * net under every branch, so a missing/dead management key never fails a command
310
+ * (and minting that fails — e.g. an unprovisioned agent — falls back to it too).
272
311
  *
273
- * Why: after an auto-detected migration (Vercel→AUI-managed), a stored key may
274
- * not authenticate on the new backend (e.g. an export/replay migration minted
275
- * fresh keys). Rather than surfacing a confusing 401, we retry once with the
276
- * token — which always works for anyone with access to the agent's account — so
277
- * management keeps working seamlessly. A non-auth error is rethrown untouched.
312
+ * Parallel work on the SAME agent is explicitly out of scope: a concurrent mint
313
+ * can invalidate another caller's key, but each branch self-heals (re-mint /
314
+ * token fallback), so the worst case is one extra retry never a hard failure.
278
315
  */
279
316
  async function withManage(ctx, fn) {
280
- const { managementKey } = loadMockDbKeys(ctx.agentId);
281
- if (!managementKey)
282
- return fn(tokenAuth(ctx));
317
+ let managementKey = loadMockDbKeys(ctx.agentId).managementKey;
318
+ // 1. No cached key → mint one (+cache). If minting isn't possible, use the token.
319
+ if (!managementKey) {
320
+ managementKey = await tryMintManagementKey(ctx);
321
+ if (!managementKey)
322
+ return fn(tokenAuth(ctx));
323
+ }
324
+ // 2. Use the key; on rejection mint a fresh one (+cache) and retry once; the
325
+ // token is the final safety net.
283
326
  try {
284
327
  return await fn({ mode: "mgmt", managementKey });
285
328
  }
286
329
  catch (err) {
287
330
  if (err instanceof AUIAPIError && (err.status === 401 || err.status === 403)) {
331
+ const fresh = await tryMintManagementKey(ctx);
332
+ if (fresh) {
333
+ try {
334
+ return await fn({ mode: "mgmt", managementKey: fresh });
335
+ }
336
+ catch (err2) {
337
+ if (err2 instanceof AUIAPIError &&
338
+ (err2.status === 401 || err2.status === 403)) {
339
+ return fn(tokenAuth(ctx));
340
+ }
341
+ throw err2;
342
+ }
343
+ }
288
344
  return fn(tokenAuth(ctx));
289
345
  }
290
346
  throw err;
291
347
  }
292
348
  }
293
- /** Runtime auth from the stored per-agent runtime key. */
349
+ /**
350
+ * Mint + cache a fresh management key via the token-auth `rotate-mgmt-key` path.
351
+ * Returns the minted key, or `undefined` when minting isn't possible (the agent
352
+ * isn't provisioned yet, no account id, or the backend's mint path is failing)
353
+ * so callers fall back to token auth. Never throws.
354
+ */
355
+ async function tryMintManagementKey(ctx) {
356
+ try {
357
+ const resp = await rotateMgmtKey(ctx); // persists the key on success
358
+ const key = typeof resp.managementKey === "string" ? resp.managementKey : undefined;
359
+ if (key) {
360
+ mockdbEvent("mockdb.mgmt_key.minted_and_cached", {
361
+ "mockdb.agent_id": ctx.agentId,
362
+ "mockdb.mgmt_key_prefix": key.slice(0, 16),
363
+ });
364
+ }
365
+ return key;
366
+ }
367
+ catch {
368
+ return undefined;
369
+ }
370
+ }
371
+ /**
372
+ * Runtime auth from the per-agent runtime key: the stored cache first, else the
373
+ * key recovered from the agent's wired `integrations.aui.json` (persisted back
374
+ * when found). Throws when neither is available — `executeEndpoint` turns that
375
+ * into a mint-and-retry.
376
+ */
294
377
  function runtimeAuth(ctx, sessionKey) {
295
- const { runtimeKey } = loadMockDbKeys(ctx.agentId);
378
+ const runtimeKey = resolveRuntimeKey(ctx);
296
379
  if (!runtimeKey) {
297
- throw new ConfigError("No runtime key stored for this agent's mock DB.", {
298
- suggestion: "Run `aui mockdb provision` first it mints and stores the runtime key.",
380
+ throw new ConfigError("No runtime key available for this agent's mock DB.", {
381
+ suggestion: "Wire the agent's mock-db integration (its auth header carries the runtime key), or run `aui mockdb rotate-runtime-key` to mint one.",
299
382
  });
300
383
  }
301
384
  return { mode: "runtime", runtimeKey, sessionKey };
302
385
  }
386
+ /**
387
+ * Resolve the runtime key without minting: the stored cache first, then the
388
+ * literal recovered from `integrations.aui.json` (cached back when found, so the
389
+ * next call is a fast cache hit). Returns undefined when neither is available.
390
+ */
391
+ function resolveRuntimeKey(ctx) {
392
+ const stored = loadMockDbKeys(ctx.agentId).runtimeKey;
393
+ if (stored)
394
+ return stored;
395
+ const detected = detectBackendFromIntegrations(ctx.projectRoot, ctx.agentId);
396
+ if (detected?.runtimeKey) {
397
+ saveMockDbKeys(ctx.agentId, { runtimeKey: detected.runtimeKey });
398
+ mockdbEvent("mockdb.runtime_key.recovered_from_integration", {
399
+ "mockdb.agent_id": ctx.agentId,
400
+ "mockdb.runtime_key_prefix": detected.runtimeKey.slice(0, 16),
401
+ "mockdb.matched_url": detected.matchedUrl,
402
+ });
403
+ return detected.runtimeKey;
404
+ }
405
+ return undefined;
406
+ }
303
407
  /** A read result is "empty" only when the HTTP body was blank → `request()`
304
408
  * returned a zero-key `{}`. A real read (even of an empty DB) returns a keyed
305
409
  * object like `{ collections: [] }`, so this never flags a legitimately-empty
@@ -433,33 +537,23 @@ export async function provisionMockDb(ctx) {
433
537
  // server created anything.
434
538
  const resp = await retryWhile(isEmptyReadObject, () => ctx.client.provision(ctx.agentId, tokenAuth(ctx)));
435
539
  if (isEmptyReadObject(resp)) {
436
- // Blank/failed: nothing is committed (a deferred --aui-managed selection is
437
- // NOT written), so the agent stays on whatever backend it was on — never
438
- // stranded on the dead one.
439
540
  throw new CLIError(`mockdb provision: the mock-db service (${ctx.backend} @ ${ctx.baseUrl}) returned HTTP 2xx but a blank body for agent ${ctx.agentId}, even after retries.`, {
440
541
  code: "API_SERVER_ERROR",
441
- suggestion: "Transient backend overload (an empty 2xx body). The DB may or may not have been created — wait a moment, then run `aui mockdb status`; if it exists, mint keys with `aui mockdb rotate-runtime-key` / `rotate-mgmt-key`. Routing was NOT changed. Do NOT blindly re-provision; retry once the service is healthy.",
542
+ suggestion: "Transient backend overload (an empty 2xx body). The DB may or may not have been created — wait a moment, then run `aui mockdb status`; if it exists, mint keys with `aui mockdb rotate-runtime-key` / `rotate-mgmt-key`. Do NOT blindly re-provision; retry once the service is healthy.",
442
543
  });
443
544
  }
444
545
  persistReturnedKeys(ctx.agentId, resp);
445
- // Commit a DEFERRED `--aui-managed` routing decision only now that provision
446
- // actually ESTABLISHED the agent on AUI-managed (a fresh creation, or keys
447
- // were minted/healed). A bare `created:false` with no keys (an empty
448
- // pre-registered shell, often from a load-induced lazy registration) must NOT
449
- // flip routing — that's the stranding bug. Such a response leaves routing
450
- // unchanged and is surfaced to the user as `backendCommitted:false`.
451
- if (ctx.commitBackendOnProvision) {
452
- const established = resp.created === true ||
453
- typeof resp.runtimeKey === "string" ||
454
- typeof resp.managementKey === "string";
455
- if (established) {
456
- persistBackend(ctx.agentId, ctx.backend, ctx.baseUrl);
457
- resp.backendCommitted = true;
458
- }
459
- else {
460
- resp.backendCommitted = false;
461
- }
462
- }
546
+ // Cache the backend the agent was just established on as a fast-path memo.
547
+ // (Routing is still re-derived deterministically on later commands; this is
548
+ // only a hint, never consulted as authority.)
549
+ persistBackend(ctx.agentId, ctx.backend, ctx.baseUrl);
550
+ mockdbEvent("mockdb.keys.cache_saved", {
551
+ "mockdb.agent_id": ctx.agentId,
552
+ "mockdb.backend": ctx.backend,
553
+ "mockdb.created": resp.created === true,
554
+ "mockdb.runtime_key_returned": typeof resp.runtimeKey === "string",
555
+ "mockdb.mgmt_key_returned": typeof resp.managementKey === "string",
556
+ });
463
557
  return resp;
464
558
  }
465
559
  export async function describeMockDb(ctx) {
@@ -495,28 +589,82 @@ export async function createEndpoint(ctx, body) {
495
589
  export async function listEndpoints(ctx) {
496
590
  return readWithRetry(ctx, "endpoint list", () => withManage(ctx, (auth) => ctx.client.listEndpoints(ctx.agentId, auth)));
497
591
  }
592
+ export async function getEndpoint(ctx, slug) {
593
+ // Idempotent read (a real endpoint returns a keyed object; a 404 throws).
594
+ return readWithRetry(ctx, `endpoint get ${slug}`, () => withManage(ctx, (auth) => ctx.client.getEndpoint(ctx.agentId, auth, slug)));
595
+ }
596
+ export async function updateEndpoint(ctx, slug, body) {
597
+ // A PATCH-merge is a write — never auto-retry a blank (a repeat is safe here,
598
+ // but a blank may mean the merge already landed; surface it honestly).
599
+ const resp = await withManage(ctx, (auth) => ctx.client.updateEndpoint(ctx.agentId, auth, slug, body));
600
+ if (isEmptyReadObject(resp))
601
+ blankBodyError(ctx, `endpoint update ${slug}`, "write");
602
+ return resp;
603
+ }
604
+ export async function deleteEndpoint(ctx, slug) {
605
+ // Delete is idempotent server-side (a second delete 404s), but a blank 2xx is
606
+ // ambiguous — surface it rather than returning a false success.
607
+ const resp = await withManage(ctx, (auth) => ctx.client.deleteEndpoint(ctx.agentId, auth, slug));
608
+ if (isEmptyReadObject(resp))
609
+ blankBodyError(ctx, `endpoint delete ${slug}`, "write");
610
+ return resp;
611
+ }
612
+ /**
613
+ * Mint + cache a fresh runtime key (`rotate-runtime-key`, retries a blank mint).
614
+ * Emits a telemetry event flagging that the deployed agent's integration must be
615
+ * re-wired (its `Authorization`/`X-API-Key` header) and `aui push`ed — rotating
616
+ * invalidates the key the live agent is using. Throws loudly if no key is minted.
617
+ */
618
+ async function mintRuntimeKey(ctx, reason) {
619
+ const resp = await rotateRuntimeKey(ctx); // persists + loud-fails on a blank mint
620
+ const key = typeof resp.runtimeKey === "string" ? resp.runtimeKey : "";
621
+ mockdbEvent("mockdb.runtime_key.rotated", {
622
+ "mockdb.agent_id": ctx.agentId,
623
+ "mockdb.reason": reason,
624
+ "mockdb.runtime_key_prefix": key.slice(0, 16),
625
+ "mockdb.rewire_required": true,
626
+ });
627
+ return key;
628
+ }
498
629
  export async function executeEndpoint(ctx, slug, sessionKey, body) {
630
+ // Ensure a runtime key exists before the first attempt: cache → integration
631
+ // file → mint. (The runtime plane has NO token fallback, so a key is required.)
632
+ if (!resolveRuntimeKey(ctx)) {
633
+ await mintRuntimeKey(ctx, "no runtime key available (cache + integration empty)");
634
+ }
499
635
  const run = () => ctx.client.execute(ctx.agentId, slug, runtimeAuth(ctx, sessionKey), body);
500
636
  // A WRITE always requires a session (the service rejects a write with no
501
637
  // session key), so an execute WITHOUT `--session` is guaranteed a read and is
502
638
  // safe to retry on a transient blank. WITH a session the endpoint could be a
503
639
  // write, so we do NOT auto-retry (a repeat could duplicate rows).
640
+ const attempt = () => sessionKey === undefined ? retryWhile(isEmptyReadObject, run) : run();
504
641
  let resp;
505
642
  try {
506
- resp = sessionKey === undefined ? await retryWhile(isEmptyReadObject, run) : await run();
643
+ resp = await attempt();
507
644
  }
508
645
  catch (err) {
509
- // A stale/rotated runtime key (e.g. after a migration that minted new keys)
510
- // reads as 401/403 here. The runtime route only accepts the runtime key (no
511
- // token fallback is possible), so surface the precise remedy instead of a
512
- // confusing raw error — and never mislabel it as a transient blank.
646
+ // A stale/rotated runtime key reads as 401/403 here (no token fallback on the
647
+ // runtime plane). Self-heal: mint a fresh key (+cache) and retry once. This
648
+ // invalidates the key wired into the deployed agent re-wire + push after.
513
649
  if (err instanceof AUIAPIError && (err.status === 401 || err.status === 403)) {
514
- throw new CLIError(`mockdb execute ${slug}: the runtime key is invalid for agent ${ctx.agentId} on the ${ctx.backend} backend (HTTP ${err.status}).`, {
515
- code: "API_CLIENT_ERROR",
516
- suggestion: "The stored runtime key was rejected — it was likely rotated or replaced (e.g. by a backend migration). Mint a fresh one with `aui mockdb rotate-runtime-key`, then re-wire the agent's integration Authorization header and `aui push`.",
517
- });
650
+ await mintRuntimeKey(ctx, `runtime key rejected (HTTP ${err.status})`);
651
+ try {
652
+ resp = await attempt();
653
+ }
654
+ catch (err2) {
655
+ if (err2 instanceof AUIAPIError &&
656
+ (err2.status === 401 || err2.status === 403)) {
657
+ throw new CLIError(`mockdb execute ${slug}: the runtime key is still invalid for agent ${ctx.agentId} on the ${ctx.backend} backend even after minting a fresh one (HTTP ${err2.status}).`, {
658
+ code: "API_CLIENT_ERROR",
659
+ suggestion: "Confirm the agent is provisioned on this backend with `aui mockdb status`; the backend may be rejecting freshly-minted keys (report it). After a successful rotate, re-wire the agent's integration auth header and `aui push`.",
660
+ });
661
+ }
662
+ throw err2;
663
+ }
664
+ }
665
+ else {
666
+ throw err;
518
667
  }
519
- throw err;
520
668
  }
521
669
  if (isEmptyReadObject(resp)) {
522
670
  blankBodyError(ctx, `execute ${slug}`, sessionKey === undefined ? "read" : "write");
@@ -637,12 +785,22 @@ function assessMockDbStatus(ctx, aui, hasLocalKeys) {
637
785
  if (aui.exists && aui.hasData === false) {
638
786
  return {
639
787
  verdict: "empty",
640
- message: `Routing to the AUI-managed backend, but the AUI service reports this record is EMPTY (no data). If you expected data it may live on the legacy Vercel backend — delete ${cacheHint} and re-run to force re-detection, then \`aui mockdb describe\`.`,
788
+ message: `Routing to the AUI-managed backend, but the AUI service reports this record is EMPTY (no data). If you expected data, check the agent's integrations.aui.json (its wired mock-db URL shows the real backend — the resolver uses it); otherwise the DB is simply empty/unprovisioned — seed it. (The local cache ${cacheHint} is only a memo; routing is re-derived each command, so deleting it won't change the backend.)`,
789
+ };
790
+ }
791
+ // AUI doesn't own it. With local keys present this is a real drift (data
792
+ // likely on Vercel); with NO local keys it's just an unprovisioned/new agent
793
+ // (new agents now default to AUI-managed, so "routes AUI-managed + AUI
794
+ // doesn't own it yet + no keys" simply means "not provisioned").
795
+ if (!hasLocalKeys) {
796
+ return {
797
+ verdict: "new",
798
+ message: "No mock DB found on the AUI-managed backend and no local keys for this agent — it likely has no mock DB yet. Run `aui mockdb provision` to create one (new mock DBs are created on AUI-managed). If you expected existing data, confirm the agent's `integrations.aui.json` is pulled (its wired URL reveals the backend) or run `aui mockdb describe`.",
641
799
  };
642
800
  }
643
801
  return {
644
802
  verdict: "drift",
645
- message: `DRIFT: the local cache routes to AUI-managed, but the AUI service does NOT own this agent — the data is likely on the legacy Vercel backend. Delete ${cacheHint} and re-run to re-detect the correct backend (your data is not affected).`,
803
+ message: `DRIFT: resolved to AUI-managed, but the AUI service does NOT own this agent — the data is likely on the legacy Vercel backend. The deterministic resolver detects Vercel from the agent's integrations.aui.json (its wired mock-db URL), so make sure that file is pulled into the project — re-pull the agent if it's missing. Your data is not affected. (The local cache ${cacheHint} is only a memo; deleting it won't change routing.)`,
646
804
  };
647
805
  }
648
806
  // Vercel-bound (or defaulting to Vercel).
@@ -655,7 +813,7 @@ function assessMockDbStatus(ctx, aui, hasLocalKeys) {
655
813
  if (!hasLocalKeys) {
656
814
  return {
657
815
  verdict: "new",
658
- message: `No mock DB found on the AUI-managed backend and no local keys for this agent. Either it has no mock DB yet (provision a new one with \`aui mockdb provision --aui-managed\`), OR it has one on the legacy Vercel backend whose local cache was lost (run \`aui mockdb describe\` — if it returns data it's on Vercel; re-mint keys with \`rotate-runtime-key\`/\`rotate-mgmt-key\`). Until you provision, the CLI routes to Vercel by default.`,
816
+ message: `No mock DB found on the AUI-managed backend and no local keys for this agent. Either it has no mock DB yet (provision a new one with \`aui mockdb provision\`), OR it has one on the legacy Vercel backend whose local cache was lost (the resolver recovers that from the agent's \`integrations.aui.json\`; or run \`aui mockdb describe\` — if it returns data it's on Vercel — and re-mint keys with \`rotate-runtime-key\`/\`rotate-mgmt-key\`).`,
659
817
  };
660
818
  }
661
819
  return {