@odla-ai/cli 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -9
- package/dist/bin.cjs +386 -92
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-OERLHVLH.js → chunk-643B2AKG.js} +411 -117
- package/dist/chunk-643B2AKG.js.map +1 -0
- package/dist/index.cjs +386 -92
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +36 -20
- package/dist/index.d.ts +36 -20
- package/dist/index.js +1 -1
- package/llms.txt +79 -30
- package/package.json +4 -4
- package/skills/odla/references/build.md +6 -4
- package/skills/odla/references/sdks.md +2 -2
- package/skills/odla-migrate/references/phase-2-db.md +2 -2
- package/skills/odla-migrate/references/phase-5-cutover.md +2 -2
- package/skills/odla-migrate/references/secrets-map.md +1 -1
- package/dist/chunk-OERLHVLH.js.map +0 -1
|
@@ -1,38 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/admin-ai.ts
|
|
4
|
-
import process4 from "process";
|
|
3
|
+
// src/admin-ai-auth.ts
|
|
5
4
|
import { existsSync as existsSync2 } from "fs";
|
|
5
|
+
import process4 from "process";
|
|
6
6
|
import { requestToken as requestToken2 } from "@odla-ai/db";
|
|
7
7
|
|
|
8
|
-
// src/open.ts
|
|
9
|
-
import { spawn } from "child_process";
|
|
10
|
-
import process2 from "process";
|
|
11
|
-
async function openUrl(url, options = {}) {
|
|
12
|
-
const command = openerFor(options.platform ?? process2.platform);
|
|
13
|
-
const doSpawn = options.spawnImpl ?? spawn;
|
|
14
|
-
await new Promise((resolve7, reject) => {
|
|
15
|
-
const child = doSpawn(command.cmd, [...command.args, url], {
|
|
16
|
-
stdio: "ignore",
|
|
17
|
-
detached: true
|
|
18
|
-
});
|
|
19
|
-
child.once("error", reject);
|
|
20
|
-
child.once("spawn", () => {
|
|
21
|
-
child.unref();
|
|
22
|
-
resolve7();
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
function openerFor(platform) {
|
|
27
|
-
if (platform === "darwin") return { cmd: "open", args: [] };
|
|
28
|
-
if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
|
|
29
|
-
return { cmd: "xdg-open", args: [] };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// src/token.ts
|
|
33
|
-
import { requestToken } from "@odla-ai/db";
|
|
34
|
-
import process3 from "process";
|
|
35
|
-
|
|
36
8
|
// src/local.ts
|
|
37
9
|
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
38
10
|
import { dirname, isAbsolute, relative, resolve } from "path";
|
|
@@ -159,12 +131,47 @@ function displayPath(path, rootDir = process.cwd()) {
|
|
|
159
131
|
return rel && !rel.startsWith("..") ? rel : path;
|
|
160
132
|
}
|
|
161
133
|
|
|
134
|
+
// src/open.ts
|
|
135
|
+
import { spawn } from "child_process";
|
|
136
|
+
import process2 from "process";
|
|
137
|
+
async function openUrl(url, options = {}) {
|
|
138
|
+
const command = openerFor(options.platform ?? process2.platform);
|
|
139
|
+
const doSpawn = options.spawnImpl ?? spawn;
|
|
140
|
+
await new Promise((resolve7, reject) => {
|
|
141
|
+
const child = doSpawn(command.cmd, [...command.args, url], {
|
|
142
|
+
stdio: "ignore",
|
|
143
|
+
detached: true
|
|
144
|
+
});
|
|
145
|
+
child.once("error", reject);
|
|
146
|
+
child.once("spawn", () => {
|
|
147
|
+
child.unref();
|
|
148
|
+
resolve7();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function openerFor(platform) {
|
|
153
|
+
if (platform === "darwin") return { cmd: "open", args: [] };
|
|
154
|
+
if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
|
|
155
|
+
return { cmd: "xdg-open", args: [] };
|
|
156
|
+
}
|
|
157
|
+
|
|
162
158
|
// src/token.ts
|
|
159
|
+
import { requestToken } from "@odla-ai/db";
|
|
160
|
+
import process3 from "process";
|
|
163
161
|
async function getDeveloperToken(cfg, options, doFetch, out) {
|
|
164
162
|
if (options.token) return options.token;
|
|
165
|
-
|
|
163
|
+
const audience = platformAudience(cfg.platformUrl);
|
|
164
|
+
if (process3.env.ODLA_DEV_TOKEN) {
|
|
165
|
+
const declared = process3.env.ODLA_DEV_TOKEN_AUDIENCE;
|
|
166
|
+
if (declared) {
|
|
167
|
+
if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
|
|
168
|
+
} else if (audience !== "https://odla.ai") {
|
|
169
|
+
throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
|
|
170
|
+
}
|
|
171
|
+
return process3.env.ODLA_DEV_TOKEN;
|
|
172
|
+
}
|
|
166
173
|
const cached = readJsonFile(cfg.local.tokenFile);
|
|
167
|
-
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
174
|
+
if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
168
175
|
out.log(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
169
176
|
return cached.token;
|
|
170
177
|
}
|
|
@@ -190,7 +197,7 @@ async function getDeveloperToken(cfg, options, doFetch, out) {
|
|
|
190
197
|
out.log("");
|
|
191
198
|
}
|
|
192
199
|
});
|
|
193
|
-
writePrivateJson(cfg.local.tokenFile, { token, expiresAt });
|
|
200
|
+
writePrivateJson(cfg.local.tokenFile, { platform: audience, token, expiresAt });
|
|
194
201
|
out.log(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
195
202
|
return token;
|
|
196
203
|
}
|
|
@@ -207,35 +214,271 @@ function handshakeUrl(platformUrl, userCode) {
|
|
|
207
214
|
url.searchParams.set("code", userCode);
|
|
208
215
|
return url.toString();
|
|
209
216
|
}
|
|
217
|
+
function platformAudience(value) {
|
|
218
|
+
let url;
|
|
219
|
+
try {
|
|
220
|
+
url = new URL(value);
|
|
221
|
+
} catch {
|
|
222
|
+
throw new Error("platform must be an absolute URL");
|
|
223
|
+
}
|
|
224
|
+
const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
225
|
+
if (url.protocol !== "https:" && !(local && url.protocol === "http:")) {
|
|
226
|
+
throw new Error("platform credentials require HTTPS except for loopback development");
|
|
227
|
+
}
|
|
228
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
229
|
+
throw new Error("platform URL must not contain credentials, query, or fragment");
|
|
230
|
+
}
|
|
231
|
+
return url.origin;
|
|
232
|
+
}
|
|
210
233
|
|
|
211
|
-
// src/admin-ai.ts
|
|
212
|
-
var SYSTEM_AI_PURPOSES = ["o11y.triage", "security.discovery", "security.validation"];
|
|
234
|
+
// src/admin-ai-auth.ts
|
|
213
235
|
async function getScopedPlatformToken(options) {
|
|
236
|
+
return resolveAdminPlatformToken(options);
|
|
237
|
+
}
|
|
238
|
+
async function resolveAdminPlatformToken(options) {
|
|
239
|
+
const audience = platformAudience(options.platform);
|
|
240
|
+
if (options.token) return options.token;
|
|
214
241
|
const fromEnv = process4.env.ODLA_ADMIN_TOKEN;
|
|
215
|
-
if (fromEnv) return fromEnv;
|
|
242
|
+
if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
|
|
216
243
|
return scopedToken(
|
|
217
|
-
|
|
244
|
+
audience,
|
|
218
245
|
options.scope,
|
|
219
|
-
|
|
246
|
+
options,
|
|
220
247
|
options.fetch ?? fetch,
|
|
221
248
|
options.stdout ?? console
|
|
222
249
|
);
|
|
223
250
|
}
|
|
251
|
+
function audienceBoundEnvToken(token, platform) {
|
|
252
|
+
const audience = platformAudience(platform);
|
|
253
|
+
const declared = process4.env.ODLA_ADMIN_TOKEN_AUDIENCE;
|
|
254
|
+
if (declared) {
|
|
255
|
+
if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
|
|
256
|
+
} else if (audience !== "https://odla.ai") {
|
|
257
|
+
throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE is required for a non-default platform");
|
|
258
|
+
}
|
|
259
|
+
return token;
|
|
260
|
+
}
|
|
261
|
+
async function scopedToken(platform, scope, options, doFetch, out) {
|
|
262
|
+
const audience = platformAudience(platform);
|
|
263
|
+
const tokenFile = options.tokenFile ?? `${process4.cwd()}/.odla/admin-token.local.json`;
|
|
264
|
+
const cache = readJsonFile(tokenFile);
|
|
265
|
+
const cached = cache?.platform === audience ? cache.tokens?.[scope] : void 0;
|
|
266
|
+
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
267
|
+
out.log(`auth: using cached ${scope} grant (${tokenFile})`);
|
|
268
|
+
return cached.token;
|
|
269
|
+
}
|
|
270
|
+
const { token, expiresAt } = await requestToken2({
|
|
271
|
+
endpoint: audience,
|
|
272
|
+
label: `odla CLI admin AI (${scope})`,
|
|
273
|
+
scopes: [scope],
|
|
274
|
+
fetch: doFetch,
|
|
275
|
+
onCode: async ({ userCode, expiresIn }) => {
|
|
276
|
+
const approvalUrl = handshakeUrl(audience, userCode);
|
|
277
|
+
out.log(`Approve scoped request ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
|
|
278
|
+
const shouldOpen = options.open === true || options.open !== false && !process4.env.CI && Boolean(process4.stdin.isTTY && process4.stdout.isTTY);
|
|
279
|
+
if (shouldOpen) await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
const tokens = cache?.platform === audience ? { ...cache.tokens ?? {} } : {};
|
|
283
|
+
tokens[scope] = { token, expiresAt };
|
|
284
|
+
if (existsSync2(`${process4.cwd()}/.git`)) ensureGitignore(process4.cwd(), [tokenFile]);
|
|
285
|
+
writePrivateJson(tokenFile, { platform: audience, tokens });
|
|
286
|
+
out.log(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
|
|
287
|
+
return token;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/admin-ai-policy.ts
|
|
291
|
+
var SYSTEM_AI_PURPOSES = ["o11y.triage", "security.discovery", "security.validation"];
|
|
292
|
+
function requireSystemAiPurpose(value) {
|
|
293
|
+
if (!SYSTEM_AI_PURPOSES.includes(value)) {
|
|
294
|
+
throw new Error(`purpose must be one of: ${SYSTEM_AI_PURPOSES.join(", ")}`);
|
|
295
|
+
}
|
|
296
|
+
return value;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/admin-ai.ts
|
|
300
|
+
import process5 from "process";
|
|
301
|
+
|
|
302
|
+
// src/admin-ai-audit.ts
|
|
303
|
+
function adminAiAuditQuery(filters) {
|
|
304
|
+
if (filters.limit === void 0) return "";
|
|
305
|
+
if (!Number.isSafeInteger(filters.limit) || filters.limit < 1 || filters.limit > 200) {
|
|
306
|
+
throw new Error("audit limit must be an integer from 1 to 200");
|
|
307
|
+
}
|
|
308
|
+
return `?limit=${filters.limit}`;
|
|
309
|
+
}
|
|
310
|
+
async function readAdminAiAudit(request) {
|
|
311
|
+
const response = await request.fetch(`${request.platform}/registry/platform/ai-audit${request.query}`, {
|
|
312
|
+
headers: request.headers
|
|
313
|
+
});
|
|
314
|
+
const body = await responseBody(response);
|
|
315
|
+
if (!response.ok) throw new Error(apiError(response.status, body));
|
|
316
|
+
if (request.json) {
|
|
317
|
+
request.stdout.log(JSON.stringify(body, null, 2));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const events = isRecord(body) && Array.isArray(body.events) ? body.events.filter(isRecord) : [];
|
|
321
|
+
request.stdout.log("when change target before -> after actor");
|
|
322
|
+
for (const event of events) {
|
|
323
|
+
const before = isRecord(event.oldPolicy) ? event.oldPolicy : void 0;
|
|
324
|
+
const after = isRecord(event.newPolicy) ? event.newPolicy : void 0;
|
|
325
|
+
const route = before && after ? `${String(before.provider)}/${String(before.model)}@v${String(before.version)} -> ${String(after.provider)}/${String(after.model)}@v${String(after.version)}` : "value not retained";
|
|
326
|
+
request.stdout.log([
|
|
327
|
+
timestamp(event.createdAt),
|
|
328
|
+
String(event.changeKind ?? ""),
|
|
329
|
+
String(event.purpose ?? event.provider ?? ""),
|
|
330
|
+
route,
|
|
331
|
+
`${String(event.actorType ?? "")}:${String(event.actorId ?? "")}`
|
|
332
|
+
].join(" "));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async function responseBody(response) {
|
|
336
|
+
const text = await response.text();
|
|
337
|
+
if (!text) return {};
|
|
338
|
+
try {
|
|
339
|
+
return JSON.parse(text);
|
|
340
|
+
} catch {
|
|
341
|
+
return { message: text.slice(0, 300) };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function apiError(status, body) {
|
|
345
|
+
const error = isRecord(body) && isRecord(body.error) ? body.error : void 0;
|
|
346
|
+
const message = error && typeof error.message === "string" ? error.message : isRecord(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
347
|
+
return `read System AI admin changes failed (${status}): ${message}`;
|
|
348
|
+
}
|
|
349
|
+
function timestamp(value) {
|
|
350
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return String(value ?? "");
|
|
351
|
+
const date = new Date(value);
|
|
352
|
+
return Number.isFinite(date.valueOf()) ? date.toISOString() : "";
|
|
353
|
+
}
|
|
354
|
+
function isRecord(value) {
|
|
355
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/admin-ai-usage.ts
|
|
359
|
+
function adminAiUsageQuery(filters) {
|
|
360
|
+
const params = new URLSearchParams();
|
|
361
|
+
if (filters.appId?.trim()) params.set("appId", filters.appId.trim());
|
|
362
|
+
if (filters.env?.trim()) params.set("env", filters.env.trim());
|
|
363
|
+
if (filters.runId?.trim()) params.set("runId", filters.runId.trim());
|
|
364
|
+
if (filters.limit !== void 0) params.set("limit", String(usageLimit(filters.limit)));
|
|
365
|
+
const query = params.toString();
|
|
366
|
+
return query ? `?${query}` : "";
|
|
367
|
+
}
|
|
368
|
+
async function readAdminAiUsage(request) {
|
|
369
|
+
const res = await request.fetch(`${request.platform}/registry/platform/ai-usage${request.query}`, {
|
|
370
|
+
headers: request.headers
|
|
371
|
+
});
|
|
372
|
+
const body = await responseBody2(res);
|
|
373
|
+
if (!res.ok) throw new Error(apiError2("read platform AI usage", res.status, body));
|
|
374
|
+
if (request.json) request.stdout.log(JSON.stringify(body, null, 2));
|
|
375
|
+
else printUsage(body, request.stdout);
|
|
376
|
+
}
|
|
377
|
+
function usageLimit(value) {
|
|
378
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 500) {
|
|
379
|
+
throw new Error("usage limit must be an integer from 1 to 500");
|
|
380
|
+
}
|
|
381
|
+
return value;
|
|
382
|
+
}
|
|
383
|
+
function printUsage(body, out) {
|
|
384
|
+
const aggregates = isRecord2(body) && isRecord2(body.aggregates) && Array.isArray(body.aggregates.byStatus) ? body.aggregates.byStatus.filter(isRecord2) : [];
|
|
385
|
+
out.log(`All retained hosted-security status totals (up to ${String(isRecord2(body) ? body.retentionDays ?? 90 : 90)} days; o11y triage excluded)
|
|
386
|
+
status calls input tokens output tokens cost unpriced calls`);
|
|
387
|
+
for (const row of aggregates) {
|
|
388
|
+
out.log([
|
|
389
|
+
String(row.status ?? ""),
|
|
390
|
+
String(row.calls ?? ""),
|
|
391
|
+
String(row.input_tokens ?? ""),
|
|
392
|
+
String(row.output_tokens ?? ""),
|
|
393
|
+
formatMicrousd(row.cost_microusd),
|
|
394
|
+
String(row.unpriced_calls ?? "")
|
|
395
|
+
].join(" "));
|
|
396
|
+
}
|
|
397
|
+
const events = isRecord2(body) && Array.isArray(body.events) ? body.events.filter(isRecord2) : [];
|
|
398
|
+
out.log(`Latest ${String(isRecord2(body) ? body.eventLimit ?? events.length : events.length)} hosted-security events
|
|
399
|
+
when app/env run actor purpose/role route / policy tokens cost status`);
|
|
400
|
+
for (const event of events) {
|
|
401
|
+
const input = numeric(event.input_tokens);
|
|
402
|
+
const output = numeric(event.output_tokens);
|
|
403
|
+
const priced = event.priced === true || event.priced === 1;
|
|
404
|
+
out.log([
|
|
405
|
+
timestamp2(event.created_at),
|
|
406
|
+
`${String(event.app_id ?? "")}/${String(event.env ?? "")}`,
|
|
407
|
+
String(event.run_id ?? ""),
|
|
408
|
+
`${String(event.actor_type ?? "")}:${String(event.actor_id ?? "")}`,
|
|
409
|
+
`${String(event.purpose ?? "")}/${String(event.role ?? "")}`,
|
|
410
|
+
`${String(event.provider ?? "")}/${String(event.model ?? "")}@v${String(event.policy_version ?? "unknown")}`,
|
|
411
|
+
String(input + output),
|
|
412
|
+
priced ? formatMicrousd(event.cost_microusd) : "unpriced",
|
|
413
|
+
`${String(event.status ?? "")}${event.error_code ? `:${String(event.error_code)}` : ""}`
|
|
414
|
+
].join(" "));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function numeric(value) {
|
|
418
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
419
|
+
}
|
|
420
|
+
function formatMicrousd(value) {
|
|
421
|
+
return typeof value === "number" && Number.isFinite(value) ? `$${(value / 1e6).toFixed(6)}` : "unpriced";
|
|
422
|
+
}
|
|
423
|
+
function timestamp2(value) {
|
|
424
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return String(value ?? "");
|
|
425
|
+
const date = new Date(value);
|
|
426
|
+
return Number.isFinite(date.valueOf()) ? date.toISOString() : "";
|
|
427
|
+
}
|
|
428
|
+
async function responseBody2(res) {
|
|
429
|
+
const text = await res.text();
|
|
430
|
+
if (!text) return {};
|
|
431
|
+
try {
|
|
432
|
+
return JSON.parse(text);
|
|
433
|
+
} catch {
|
|
434
|
+
return { message: text.slice(0, 300) };
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function apiError2(action, status, body) {
|
|
438
|
+
const error = isRecord2(body) && isRecord2(body.error) ? body.error : void 0;
|
|
439
|
+
const message = error && typeof error.message === "string" ? error.message : isRecord2(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
440
|
+
return `${action} failed (${status}): ${message}`;
|
|
441
|
+
}
|
|
442
|
+
function isRecord2(value) {
|
|
443
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/admin-ai.ts
|
|
224
447
|
async function adminAi(options) {
|
|
225
|
-
const platform = (options.platform ??
|
|
448
|
+
const platform = platformAudience(options.platform ?? process5.env.ODLA_PLATFORM ?? "https://odla.ai");
|
|
226
449
|
const doFetch = options.fetch ?? fetch;
|
|
227
450
|
const out = options.stdout ?? console;
|
|
228
|
-
const
|
|
229
|
-
const
|
|
451
|
+
const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
|
|
452
|
+
const auditQuery = options.action === "audit" ? adminAiAuditQuery(options) : void 0;
|
|
453
|
+
const scope = options.action === "set" ? "platform:ai:policy:write" : options.action === "credential-set" ? "platform:ai:credential:write" : options.action === "credentials" ? "platform:ai:credential:read" : options.action === "usage" ? "platform:ai:usage:read" : options.action === "audit" ? "platform:ai:audit:read" : "platform:ai:policy:read";
|
|
454
|
+
const token = await resolveAdminPlatformToken({
|
|
455
|
+
platform,
|
|
456
|
+
scope,
|
|
457
|
+
token: options.token,
|
|
458
|
+
open: options.open,
|
|
459
|
+
fetch: doFetch,
|
|
460
|
+
stdout: out,
|
|
461
|
+
openApprovalUrl: options.openApprovalUrl,
|
|
462
|
+
tokenFile: options.tokenFile
|
|
463
|
+
});
|
|
230
464
|
const headers = { authorization: `Bearer ${token}`, "content-type": "application/json" };
|
|
465
|
+
if (options.action === "usage") {
|
|
466
|
+
await readAdminAiUsage({ platform, query: usageQuery ?? "", headers, json: options.json, fetch: doFetch, stdout: out });
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (options.action === "audit") {
|
|
470
|
+
await readAdminAiAudit({ platform, query: auditQuery ?? "", headers, json: options.json, fetch: doFetch, stdout: out });
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
231
473
|
if (options.action === "credentials") {
|
|
232
474
|
const res2 = await doFetch(`${platform}/registry/platform/ai-credentials`, { headers });
|
|
233
|
-
const body2 = await
|
|
234
|
-
if (!res2.ok) throw new Error(
|
|
235
|
-
const credentials =
|
|
475
|
+
const body2 = await responseBody3(res2);
|
|
476
|
+
if (!res2.ok) throw new Error(apiError3("read platform AI credential status", res2.status, body2));
|
|
477
|
+
const credentials = isRecord3(body2) && isRecord3(body2.credentials) ? body2.credentials : body2;
|
|
236
478
|
if (options.json) out.log(JSON.stringify({ credentials }, null, 2));
|
|
237
|
-
else for (const [provider, status] of Object.entries(
|
|
238
|
-
|
|
479
|
+
else for (const [provider, status] of Object.entries(isRecord3(credentials) ? credentials : {})) {
|
|
480
|
+
const label = isRecord3(status) && status.set === true ? status.readable === true ? "vault readable" : "stored; vault unreadable" : "not set";
|
|
481
|
+
out.log(`${provider} ${label}`);
|
|
239
482
|
}
|
|
240
483
|
return;
|
|
241
484
|
}
|
|
@@ -247,18 +490,25 @@ async function adminAi(options) {
|
|
|
247
490
|
headers,
|
|
248
491
|
body: JSON.stringify({ value })
|
|
249
492
|
});
|
|
250
|
-
const body2 = await
|
|
251
|
-
if (!res2.ok) throw new Error(
|
|
493
|
+
const body2 = await responseBody3(res2);
|
|
494
|
+
if (!res2.ok) throw new Error(apiError3(`store ${provider} platform credential`, res2.status, body2));
|
|
252
495
|
out.log(`stored ${provider} platform credential (write-only; value was not returned)`);
|
|
253
496
|
return;
|
|
254
497
|
}
|
|
255
|
-
if (options.action === "show") {
|
|
498
|
+
if (options.action === "show" || options.action === "models") {
|
|
256
499
|
const res2 = await doFetch(`${platform}/registry/platform/ai-policies`, { headers });
|
|
257
|
-
const body2 = await
|
|
258
|
-
if (!res2.ok) throw new Error(
|
|
500
|
+
const body2 = await responseBody3(res2);
|
|
501
|
+
if (!res2.ok) throw new Error(apiError3("read platform AI policies", res2.status, body2));
|
|
502
|
+
if (options.action === "models") {
|
|
503
|
+
const models = catalogModels(body2).filter((model) => !options.provider || model.provider === options.provider);
|
|
504
|
+
if (!models.length) throw new Error(options.provider ? `no System AI models found for provider ${options.provider}` : "platform returned no System AI models");
|
|
505
|
+
if (options.json) out.log(JSON.stringify({ models }, null, 2));
|
|
506
|
+
else for (const model of models) out.log(`${model.provider} ${model.id}`);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
259
509
|
const policies = policyArray(body2);
|
|
260
510
|
if (options.json) {
|
|
261
|
-
out.log(JSON.stringify({ policies }, null, 2));
|
|
511
|
+
out.log(JSON.stringify({ policies, models: catalogModels(body2) }, null, 2));
|
|
262
512
|
return;
|
|
263
513
|
}
|
|
264
514
|
out.log("purpose enabled provider model max calls max input bytes max output tokens");
|
|
@@ -275,59 +525,68 @@ async function adminAi(options) {
|
|
|
275
525
|
}
|
|
276
526
|
return;
|
|
277
527
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
528
|
+
if (options.purpose === "security") {
|
|
529
|
+
const currentResponse = await doFetch(`${platform}/registry/platform/ai-policies`, { headers });
|
|
530
|
+
const currentBody = await responseBody3(currentResponse);
|
|
531
|
+
if (!currentResponse.ok) throw new Error(apiError3("read current security AI policies", currentResponse.status, currentBody));
|
|
532
|
+
const policies = policyArray(currentBody);
|
|
533
|
+
const discovery = policies.find((policy2) => policy2.purpose === "security.discovery");
|
|
534
|
+
const validation = policies.find((policy2) => policy2.purpose === "security.validation");
|
|
535
|
+
if (!discovery || !validation || !Number.isSafeInteger(discovery.version) || !Number.isSafeInteger(validation.version)) {
|
|
536
|
+
throw new Error("platform returned invalid security AI policy revisions");
|
|
537
|
+
}
|
|
538
|
+
const shared = {
|
|
539
|
+
...options.enabled === void 0 ? {} : { enabled: options.enabled },
|
|
540
|
+
...options.maxInputBytes === void 0 ? {} : { maxInputBytes: options.maxInputBytes },
|
|
541
|
+
...options.maxOutputTokens === void 0 ? {} : { maxOutputTokens: options.maxOutputTokens },
|
|
542
|
+
...options.maxCallsPerRun === void 0 ? {} : { maxCallsPerRun: options.maxCallsPerRun }
|
|
543
|
+
};
|
|
544
|
+
const discoveryUpdate = {
|
|
545
|
+
...shared,
|
|
546
|
+
...options.discoveryProvider?.trim() ? { provider: options.discoveryProvider.trim() } : {},
|
|
547
|
+
...options.discoveryModel?.trim() ? { model: options.discoveryModel.trim() } : {}
|
|
548
|
+
};
|
|
549
|
+
const validationUpdate = {
|
|
550
|
+
...shared,
|
|
551
|
+
...options.validationProvider?.trim() ? { provider: options.validationProvider.trim() } : {},
|
|
552
|
+
...options.validationModel?.trim() ? { model: options.validationModel.trim() } : {}
|
|
553
|
+
};
|
|
554
|
+
if (!Object.keys(discoveryUpdate).length && !Object.keys(validationUpdate).length) {
|
|
555
|
+
throw new Error("security set requires a route, enabled state, or budget change");
|
|
556
|
+
}
|
|
557
|
+
const res2 = await doFetch(`${platform}/registry/platform/ai-policies/security`, {
|
|
558
|
+
method: "PUT",
|
|
559
|
+
headers,
|
|
560
|
+
body: JSON.stringify({
|
|
561
|
+
expectedVersions: { discovery: discovery.version, validation: validation.version },
|
|
562
|
+
discovery: discoveryUpdate,
|
|
563
|
+
validation: validationUpdate
|
|
564
|
+
})
|
|
565
|
+
});
|
|
566
|
+
const response2 = await responseBody3(res2);
|
|
567
|
+
if (!res2.ok) throw new Error(apiError3("update security review routes", res2.status, response2));
|
|
568
|
+
out.log(options.json ? JSON.stringify(response2, null, 2) : "updated security review discovery and independent validation routes atomically");
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const purpose = requireSystemAiPurpose(options.purpose);
|
|
281
572
|
const body = {
|
|
282
|
-
provider: options.provider.trim(),
|
|
283
|
-
model: options.model.trim(),
|
|
573
|
+
...options.provider?.trim() ? { provider: options.provider.trim() } : {},
|
|
574
|
+
...options.model?.trim() ? { model: options.model.trim() } : {},
|
|
284
575
|
...options.enabled === void 0 ? {} : { enabled: options.enabled },
|
|
285
576
|
...options.maxInputBytes === void 0 ? {} : { maxInputBytes: options.maxInputBytes },
|
|
286
577
|
...options.maxOutputTokens === void 0 ? {} : { maxOutputTokens: options.maxOutputTokens },
|
|
287
578
|
...options.maxCallsPerRun === void 0 ? {} : { maxCallsPerRun: options.maxCallsPerRun }
|
|
288
579
|
};
|
|
580
|
+
if (!Object.keys(body).length) throw new Error("set requires a provider, model, enabled state, or budget change");
|
|
289
581
|
const res = await doFetch(`${platform}/registry/platform/ai-policies/${encodeURIComponent(purpose)}`, {
|
|
290
582
|
method: "PUT",
|
|
291
583
|
headers,
|
|
292
584
|
body: JSON.stringify(body)
|
|
293
585
|
});
|
|
294
|
-
const response = await
|
|
295
|
-
if (!res.ok) throw new Error(
|
|
296
|
-
const policy =
|
|
297
|
-
out.log(options.json ? JSON.stringify({ policy }, null, 2) : `updated ${purpose}: ${String(policy.provider ??
|
|
298
|
-
}
|
|
299
|
-
async function scopedToken(platform, scope, options, doFetch, out) {
|
|
300
|
-
const tokenFile = options.tokenFile ?? `${process4.cwd()}/.odla/admin-token.local.json`;
|
|
301
|
-
const cache = readJsonFile(tokenFile);
|
|
302
|
-
const cached = cache?.platform === platform ? cache.tokens?.[scope] : void 0;
|
|
303
|
-
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
304
|
-
out.log(`auth: using cached ${scope} grant (${tokenFile})`);
|
|
305
|
-
return cached.token;
|
|
306
|
-
}
|
|
307
|
-
const { token, expiresAt } = await requestToken2({
|
|
308
|
-
endpoint: platform,
|
|
309
|
-
label: `odla CLI admin AI (${scope})`,
|
|
310
|
-
scopes: [scope],
|
|
311
|
-
fetch: doFetch,
|
|
312
|
-
onCode: async ({ userCode, expiresIn }) => {
|
|
313
|
-
const approvalUrl = handshakeUrl(platform, userCode);
|
|
314
|
-
out.log(`Approve scoped request ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
|
|
315
|
-
const shouldOpen = options.open === true || options.open !== false && !process4.env.CI && Boolean(process4.stdin.isTTY && process4.stdout.isTTY);
|
|
316
|
-
if (shouldOpen) await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
|
|
317
|
-
}
|
|
318
|
-
});
|
|
319
|
-
const tokens = cache?.platform === platform ? { ...cache.tokens ?? {} } : {};
|
|
320
|
-
tokens[scope] = { token, expiresAt };
|
|
321
|
-
if (existsSync2(`${process4.cwd()}/.git`)) ensureGitignore(process4.cwd(), [tokenFile]);
|
|
322
|
-
writePrivateJson(tokenFile, { platform, tokens });
|
|
323
|
-
out.log(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
|
|
324
|
-
return token;
|
|
325
|
-
}
|
|
326
|
-
function requirePurpose(value) {
|
|
327
|
-
if (!SYSTEM_AI_PURPOSES.includes(value)) {
|
|
328
|
-
throw new Error(`purpose must be one of: ${SYSTEM_AI_PURPOSES.join(", ")}`);
|
|
329
|
-
}
|
|
330
|
-
return value;
|
|
586
|
+
const response = await responseBody3(res);
|
|
587
|
+
if (!res.ok) throw new Error(apiError3(`update ${purpose}`, res.status, response));
|
|
588
|
+
const policy = isRecord3(response) && isRecord3(response.policy) ? response.policy : isRecord3(response) ? response : {};
|
|
589
|
+
out.log(options.json ? JSON.stringify({ policy }, null, 2) : `updated ${purpose}: ${String(policy.provider ?? "unchanged")}/${String(policy.model ?? "unchanged")}`);
|
|
331
590
|
}
|
|
332
591
|
function requireProvider(value) {
|
|
333
592
|
if (value !== "anthropic" && value !== "openai" && value !== "google") {
|
|
@@ -338,7 +597,7 @@ function requireProvider(value) {
|
|
|
338
597
|
async function credentialValue(options) {
|
|
339
598
|
if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
|
|
340
599
|
let value;
|
|
341
|
-
if (options.fromEnv) value =
|
|
600
|
+
if (options.fromEnv) value = process5.env[options.fromEnv];
|
|
342
601
|
else if (options.stdin) value = await (options.readStdin ?? readStdin)();
|
|
343
602
|
else throw new Error("credential input required: use --from-env <NAME> or --stdin; values are never accepted as arguments");
|
|
344
603
|
value = value?.replace(/[\r\n]+$/, "");
|
|
@@ -348,18 +607,24 @@ async function credentialValue(options) {
|
|
|
348
607
|
}
|
|
349
608
|
async function readStdin() {
|
|
350
609
|
let value = "";
|
|
351
|
-
for await (const chunk of
|
|
610
|
+
for await (const chunk of process5.stdin) {
|
|
352
611
|
value += String(chunk);
|
|
353
612
|
if (value.length > 64 * 1024) throw new Error("credential exceeds 64 KiB");
|
|
354
613
|
}
|
|
355
614
|
return value;
|
|
356
615
|
}
|
|
357
616
|
function policyArray(body) {
|
|
358
|
-
if (
|
|
359
|
-
if (
|
|
617
|
+
if (isRecord3(body) && Array.isArray(body.policies)) return body.policies.filter(isRecord3);
|
|
618
|
+
if (isRecord3(body) && isRecord3(body.policies)) return Object.values(body.policies).filter(isRecord3);
|
|
360
619
|
throw new Error("platform returned an invalid AI policy response");
|
|
361
620
|
}
|
|
362
|
-
|
|
621
|
+
function catalogModels(body) {
|
|
622
|
+
if (!isRecord3(body) || !isRecord3(body.catalog) || !Array.isArray(body.catalog.models)) {
|
|
623
|
+
throw new Error("platform returned an invalid System AI model catalog");
|
|
624
|
+
}
|
|
625
|
+
return body.catalog.models.filter((value) => isRecord3(value) && typeof value.id === "string" && typeof value.provider === "string");
|
|
626
|
+
}
|
|
627
|
+
async function responseBody3(res) {
|
|
363
628
|
const text = await res.text();
|
|
364
629
|
if (!text) return {};
|
|
365
630
|
try {
|
|
@@ -368,12 +633,12 @@ async function responseBody(res) {
|
|
|
368
633
|
return { message: text.slice(0, 300) };
|
|
369
634
|
}
|
|
370
635
|
}
|
|
371
|
-
function
|
|
372
|
-
const error =
|
|
373
|
-
const message = error && typeof error.message === "string" ? error.message :
|
|
636
|
+
function apiError3(action, status, body) {
|
|
637
|
+
const error = isRecord3(body) && isRecord3(body.error) ? body.error : void 0;
|
|
638
|
+
const message = error && typeof error.message === "string" ? error.message : isRecord3(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
374
639
|
return `${action} failed (${status}): ${message}`;
|
|
375
640
|
}
|
|
376
|
-
function
|
|
641
|
+
function isRecord3(value) {
|
|
377
642
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
378
643
|
}
|
|
379
644
|
|
|
@@ -386,7 +651,7 @@ var CAPABILITIES = {
|
|
|
386
651
|
"push db schema/rules and configure platform AI, auth, and deployment links",
|
|
387
652
|
"validate config offline and smoke-test a provisioned db environment",
|
|
388
653
|
"run app-attributed hosted security discovery and independent validation without provider keys",
|
|
389
|
-
"let admins manage system AI
|
|
654
|
+
"let admins manage system AI policy and credentials, inspect attributed usage, and read immutable admin changes through separate short-lived capability-only approvals"
|
|
390
655
|
],
|
|
391
656
|
agent: [
|
|
392
657
|
"install and import the selected odla SDKs",
|
|
@@ -991,7 +1256,7 @@ function assertWranglerConfig(cfg) {
|
|
|
991
1256
|
// src/provision.ts
|
|
992
1257
|
import { createAppsClient, tenantIdFor as tenantIdFor2 } from "@odla-ai/apps";
|
|
993
1258
|
import { DEFAULT_SECRET_NAMES, putSecret } from "@odla-ai/ai";
|
|
994
|
-
import
|
|
1259
|
+
import process6 from "process";
|
|
995
1260
|
|
|
996
1261
|
// src/provision-credentials.ts
|
|
997
1262
|
import { tenantIdFor } from "@odla-ai/apps";
|
|
@@ -1222,7 +1487,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
1222
1487
|
out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
|
|
1223
1488
|
}
|
|
1224
1489
|
if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
1225
|
-
const key =
|
|
1490
|
+
const key = process6.env[cfg.ai.keyEnv];
|
|
1226
1491
|
if (key) {
|
|
1227
1492
|
const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
|
|
1228
1493
|
await putSecret({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
|
|
@@ -1384,10 +1649,14 @@ function printSummary(out, appId, env, run, report, output) {
|
|
|
1384
1649
|
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
1385
1650
|
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
1386
1651
|
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
1387
|
-
out.log(` coverage: ${report.coverageStatus} ${complete}/${report.coverage.length} blocked=${report.metrics.blockedCells} shallow=${report.metrics.shallowCells} unscheduled=${report.metrics.unscheduledCells}`);
|
|
1652
|
+
out.log(` coverage: ${report.coverageStatus} ${complete}/${report.coverage.length} blocked=${report.metrics.blockedCells} shallow=${report.metrics.shallowCells} unscheduled=${report.metrics.unscheduledCells} budget_exhausted=${report.metrics.budgetExhaustedCells}`);
|
|
1653
|
+
if (report.callBudget) out.log(` calls: discovery=${formatBudget(report.callBudget.discovery)} validation=${formatBudget(report.callBudget.validation)}`);
|
|
1388
1654
|
out.log(` findings: confirmed=${report.metrics.confirmed} needs_reproduction=${report.metrics.needsReproduction} candidates=${report.metrics.candidates}`);
|
|
1389
1655
|
out.log(` report: ${resolve5(output, "REPORT.md")}`);
|
|
1390
1656
|
}
|
|
1657
|
+
function formatBudget(usage) {
|
|
1658
|
+
return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
|
|
1659
|
+
}
|
|
1391
1660
|
|
|
1392
1661
|
// src/skill.ts
|
|
1393
1662
|
import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync5, readdirSync, writeFileSync as writeFileSync3 } from "fs";
|
|
@@ -1816,9 +2085,14 @@ Usage:
|
|
|
1816
2085
|
odla-ai doctor [--config odla.config.mjs]
|
|
1817
2086
|
odla-ai capabilities [--json]
|
|
1818
2087
|
odla-ai admin ai show [--platform https://odla.ai] [--json]
|
|
1819
|
-
odla-ai admin ai
|
|
2088
|
+
odla-ai admin ai models [--provider <id>] [--json]
|
|
2089
|
+
odla-ai admin ai set <purpose> [--provider <id>] [--model <id>] [--enabled|--no-enabled]
|
|
2090
|
+
odla-ai admin ai set security --discovery-provider <id> --discovery-model <id>
|
|
2091
|
+
--validation-provider <id> --validation-model <id> [--enabled|--no-enabled]
|
|
1820
2092
|
odla-ai admin ai credentials [--json]
|
|
1821
2093
|
odla-ai admin ai credential set <provider> (--from-env <NAME>|--stdin)
|
|
2094
|
+
odla-ai admin ai usage [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
|
|
2095
|
+
odla-ai admin ai audit [--limit <1-200>] [--json]
|
|
1822
2096
|
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
1823
2097
|
odla-ai security run [target] --self --ack-redacted-source
|
|
1824
2098
|
odla-ai provision [--config odla.config.mjs] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
@@ -1832,7 +2106,7 @@ Commands:
|
|
|
1832
2106
|
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
1833
2107
|
doctor Validate and summarize the project config without network calls.
|
|
1834
2108
|
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
1835
|
-
admin Manage platform-funded AI routing with
|
|
2109
|
+
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
1836
2110
|
security Run hosted discovery + independent validation with app/run attribution.
|
|
1837
2111
|
provision Register services, configure them, persist credentials, optionally push secrets.
|
|
1838
2112
|
smoke Verify local credentials, public-config, live schema, and db aggregate.
|
|
@@ -1894,12 +2168,14 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
1894
2168
|
const action = parsed.positionals[2];
|
|
1895
2169
|
const credentialSet = action === "credential" && parsed.positionals[3] === "set";
|
|
1896
2170
|
const credentials = action === "credentials";
|
|
1897
|
-
|
|
2171
|
+
const models = action === "models";
|
|
2172
|
+
const usage = action === "usage";
|
|
2173
|
+
const audit = action === "audit";
|
|
2174
|
+
if (area !== "ai" || action !== "show" && action !== "set" && !credentialSet && !credentials && !models && !usage && !audit) {
|
|
1898
2175
|
throw new Error('unknown admin command. Try "odla-ai admin ai show".');
|
|
1899
2176
|
}
|
|
1900
2177
|
assertArgs(parsed, [
|
|
1901
2178
|
"platform",
|
|
1902
|
-
"token",
|
|
1903
2179
|
"json",
|
|
1904
2180
|
"open",
|
|
1905
2181
|
"provider",
|
|
@@ -1909,10 +2185,18 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
1909
2185
|
"max-output-tokens",
|
|
1910
2186
|
"max-calls-per-run",
|
|
1911
2187
|
"from-env",
|
|
1912
|
-
"stdin"
|
|
2188
|
+
"stdin",
|
|
2189
|
+
"discovery-provider",
|
|
2190
|
+
"discovery-model",
|
|
2191
|
+
"validation-provider",
|
|
2192
|
+
"validation-model",
|
|
2193
|
+
"app-id",
|
|
2194
|
+
"env",
|
|
2195
|
+
"run-id",
|
|
2196
|
+
"limit"
|
|
1913
2197
|
], credentialSet ? 5 : action === "set" ? 4 : 3);
|
|
1914
2198
|
await adminAi({
|
|
1915
|
-
action: credentialSet ? "credential-set" : credentials ? "credentials" : action,
|
|
2199
|
+
action: credentialSet ? "credential-set" : credentials ? "credentials" : models ? "models" : usage ? "usage" : audit ? "audit" : action,
|
|
1916
2200
|
purpose: action === "set" ? parsed.positionals[3] : void 0,
|
|
1917
2201
|
provider: stringOpt(parsed.options.provider),
|
|
1918
2202
|
model: stringOpt(parsed.options.model),
|
|
@@ -1921,12 +2205,19 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
1921
2205
|
maxOutputTokens: numberOpt(parsed.options["max-output-tokens"], "--max-output-tokens"),
|
|
1922
2206
|
maxCallsPerRun: numberOpt(parsed.options["max-calls-per-run"], "--max-calls-per-run"),
|
|
1923
2207
|
platform: stringOpt(parsed.options.platform),
|
|
1924
|
-
token: stringOpt(parsed.options.token),
|
|
1925
2208
|
json: parsed.options.json === true,
|
|
1926
2209
|
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
1927
2210
|
credentialProvider: credentialSet ? parsed.positionals[4] : void 0,
|
|
1928
2211
|
fromEnv: stringOpt(parsed.options["from-env"]),
|
|
1929
|
-
stdin: parsed.options.stdin === true
|
|
2212
|
+
stdin: parsed.options.stdin === true,
|
|
2213
|
+
discoveryProvider: stringOpt(parsed.options["discovery-provider"]),
|
|
2214
|
+
discoveryModel: stringOpt(parsed.options["discovery-model"]),
|
|
2215
|
+
validationProvider: stringOpt(parsed.options["validation-provider"]),
|
|
2216
|
+
validationModel: stringOpt(parsed.options["validation-model"]),
|
|
2217
|
+
appId: stringOpt(parsed.options["app-id"]),
|
|
2218
|
+
env: stringOpt(parsed.options.env),
|
|
2219
|
+
runId: stringOpt(parsed.options["run-id"]),
|
|
2220
|
+
limit: numberOpt(parsed.options.limit, "--limit")
|
|
1930
2221
|
});
|
|
1931
2222
|
return;
|
|
1932
2223
|
}
|
|
@@ -1968,6 +2259,9 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
1968
2259
|
return getScopedPlatformToken({ platform: request.platform, scope: request.scope, open });
|
|
1969
2260
|
}
|
|
1970
2261
|
const cfg = await loadProjectConfig(configPath);
|
|
2262
|
+
if (platformAudience(cfg.platformUrl) !== platformAudience(request.platform)) {
|
|
2263
|
+
throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
|
|
2264
|
+
}
|
|
1971
2265
|
return getDeveloperToken(cfg, { configPath, open }, fetch, console);
|
|
1972
2266
|
}
|
|
1973
2267
|
});
|
|
@@ -2088,8 +2382,8 @@ function harnessOption(value, flag) {
|
|
|
2088
2382
|
}
|
|
2089
2383
|
|
|
2090
2384
|
export {
|
|
2091
|
-
SYSTEM_AI_PURPOSES,
|
|
2092
2385
|
getScopedPlatformToken,
|
|
2386
|
+
SYSTEM_AI_PURPOSES,
|
|
2093
2387
|
adminAi,
|
|
2094
2388
|
CAPABILITIES,
|
|
2095
2389
|
printCapabilities,
|
|
@@ -2104,4 +2398,4 @@ export {
|
|
|
2104
2398
|
smoke,
|
|
2105
2399
|
runCli
|
|
2106
2400
|
};
|
|
2107
|
-
//# sourceMappingURL=chunk-
|
|
2401
|
+
//# sourceMappingURL=chunk-643B2AKG.js.map
|