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.
- package/dist/api-client/apollo-client.d.ts +64 -0
- package/dist/api-client/apollo-client.d.ts.map +1 -1
- package/dist/api-client/apollo-client.js +110 -21
- package/dist/api-client/apollo-client.js.map +1 -1
- package/dist/api-client/index.d.ts +11 -0
- package/dist/api-client/index.d.ts.map +1 -1
- package/dist/api-client/index.js +69 -28
- package/dist/api-client/index.js.map +1 -1
- package/dist/api-client/mock-db-client.d.ts +8 -0
- package/dist/api-client/mock-db-client.d.ts.map +1 -1
- package/dist/api-client/mock-db-client.js +33 -0
- package/dist/api-client/mock-db-client.js.map +1 -1
- package/dist/api-client/rag-client.d.ts +16 -0
- package/dist/api-client/rag-client.d.ts.map +1 -1
- package/dist/api-client/rag-client.js +56 -18
- package/dist/api-client/rag-client.js.map +1 -1
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +13 -5
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +46 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mockdb-guide.d.ts +1 -1
- package/dist/commands/mockdb-guide.d.ts.map +1 -1
- package/dist/commands/mockdb-guide.js +29 -17
- package/dist/commands/mockdb-guide.js.map +1 -1
- package/dist/commands/mockdb.d.ts +6 -0
- package/dist/commands/mockdb.d.ts.map +1 -1
- package/dist/commands/mockdb.js +68 -24
- package/dist/commands/mockdb.js.map +1 -1
- package/dist/commands/push.js +49 -0
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/vault.d.ts +25 -0
- package/dist/commands/vault.d.ts.map +1 -0
- package/dist/commands/vault.js +220 -0
- package/dist/commands/vault.js.map +1 -0
- package/dist/config/index.d.ts +9 -9
- package/dist/config/index.js +4 -4
- package/dist/index.js +149 -23
- package/dist/index.js.map +1 -1
- package/dist/services/integration.service.d.ts +1 -1
- package/dist/services/integration.service.d.ts.map +1 -1
- package/dist/services/integration.service.js +4 -2
- package/dist/services/integration.service.js.map +1 -1
- package/dist/services/kb-identity.d.ts +54 -0
- package/dist/services/kb-identity.d.ts.map +1 -0
- package/dist/services/kb-identity.js +152 -0
- package/dist/services/kb-identity.js.map +1 -0
- package/dist/services/kb-view.service.d.ts.map +1 -1
- package/dist/services/kb-view.service.js +14 -3
- package/dist/services/kb-view.service.js.map +1 -1
- package/dist/services/mock-db.service.d.ts +22 -27
- package/dist/services/mock-db.service.d.ts.map +1 -1
- package/dist/services/mock-db.service.js +337 -179
- package/dist/services/mock-db.service.js.map +1 -1
- package/dist/services/vault.service.d.ts +58 -0
- package/dist/services/vault.service.d.ts.map +1 -0
- package/dist/services/vault.service.js +143 -0
- package/dist/services/vault.service.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/vault.d.ts +31 -0
- package/dist/types/vault.d.ts.map +1 -0
- package/dist/types/vault.js +10 -0
- package/dist/types/vault.js.map +1 -0
- package/dist/ui/views/VaultView.d.ts +19 -0
- package/dist/ui/views/VaultView.d.ts.map +1 -0
- package/dist/ui/views/VaultView.js +35 -0
- package/dist/ui/views/VaultView.js.map +1 -0
- package/dist/utils/fetch-with-timeout.d.ts +26 -0
- package/dist/utils/fetch-with-timeout.d.ts.map +1 -1
- package/dist/utils/fetch-with-timeout.js +172 -1
- package/dist/utils/fetch-with-timeout.js.map +1 -1
- package/dist/utils/json-output.d.ts +11 -0
- package/dist/utils/json-output.d.ts.map +1 -1
- package/dist/utils/json-output.js +19 -1
- package/dist/utils/json-output.js.map +1 -1
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
79
|
-
*
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
*
|
|
94
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
//
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
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
|
|
270
|
-
*
|
|
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
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
/**
|
|
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
|
|
378
|
+
const runtimeKey = resolveRuntimeKey(ctx);
|
|
296
379
|
if (!runtimeKey) {
|
|
297
|
-
throw new ConfigError("No runtime key
|
|
298
|
-
suggestion: "
|
|
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`.
|
|
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
|
-
//
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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 =
|
|
643
|
+
resp = await attempt();
|
|
507
644
|
}
|
|
508
645
|
catch (err) {
|
|
509
|
-
// A stale/rotated runtime key
|
|
510
|
-
//
|
|
511
|
-
//
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
|
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:
|
|
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
|
|
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 {
|