@odla-ai/cli 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.cjs CHANGED
@@ -28,7 +28,724 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
28
28
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
29
29
 
30
30
  // src/cli.ts
31
- var import_node_fs7 = require("fs");
31
+ var import_security2 = require("@odla-ai/security");
32
+
33
+ // src/admin-ai.ts
34
+ var import_node_process4 = __toESM(require("process"), 1);
35
+
36
+ // src/token.ts
37
+ var import_db = require("@odla-ai/db");
38
+ var import_node_process2 = __toESM(require("process"), 1);
39
+
40
+ // src/open.ts
41
+ var import_node_child_process = require("child_process");
42
+ var import_node_process = __toESM(require("process"), 1);
43
+ async function openUrl(url, options = {}) {
44
+ const command = openerFor(options.platform ?? import_node_process.default.platform);
45
+ const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
46
+ await new Promise((resolve7, reject) => {
47
+ const child = doSpawn(command.cmd, [...command.args, url], {
48
+ stdio: "ignore",
49
+ detached: true
50
+ });
51
+ child.once("error", reject);
52
+ child.once("spawn", () => {
53
+ child.unref();
54
+ resolve7();
55
+ });
56
+ });
57
+ }
58
+ function openerFor(platform) {
59
+ if (platform === "darwin") return { cmd: "open", args: [] };
60
+ if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
61
+ return { cmd: "xdg-open", args: [] };
62
+ }
63
+
64
+ // src/local.ts
65
+ var import_node_fs = require("fs");
66
+ var import_node_path = require("path");
67
+ var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
68
+ function readJsonFile(path) {
69
+ try {
70
+ return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+ function writePrivateJson(path, value) {
76
+ writePrivateText(path, `${JSON.stringify(value, null, 2)}
77
+ `);
78
+ }
79
+ function readCredentials(path) {
80
+ if (!(0, import_node_fs.existsSync)(path)) return null;
81
+ let value;
82
+ try {
83
+ value = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
84
+ } catch {
85
+ throw new Error(`credentials file ${path} is not valid JSON; fix or remove it before provisioning`);
86
+ }
87
+ if (!value || typeof value !== "object" || typeof value.appId !== "string" || !value.envs || typeof value.envs !== "object") {
88
+ throw new Error(`credentials file ${path} has an invalid shape; fix or remove it before provisioning`);
89
+ }
90
+ return value;
91
+ }
92
+ function mergeCredential(current, update) {
93
+ const next = current ?? {
94
+ appId: update.appId,
95
+ platformUrl: update.platformUrl,
96
+ dbEndpoint: update.dbEndpoint,
97
+ updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
98
+ envs: {}
99
+ };
100
+ next.appId = update.appId;
101
+ next.platformUrl = update.platformUrl;
102
+ next.dbEndpoint = update.dbEndpoint;
103
+ next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
104
+ next.envs[update.env] = {
105
+ ...next.envs[update.env] ?? {},
106
+ tenantId: update.tenantId,
107
+ ...update.dbKey ? { dbKey: update.dbKey } : {},
108
+ ...update.o11yToken ? { o11yToken: update.o11yToken } : {}
109
+ };
110
+ return next;
111
+ }
112
+ function ensureGitignore(rootDir, localPaths = []) {
113
+ const path = (0, import_node_path.resolve)(rootDir, ".gitignore");
114
+ const existing = (0, import_node_fs.existsSync)(path) ? (0, import_node_fs.readFileSync)(path, "utf8") : "";
115
+ const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line) => !!line);
116
+ const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
117
+ const missing = wanted.filter((line) => !existing.split(/\r?\n/).includes(line));
118
+ if (missing.length === 0) return;
119
+ const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
120
+ (0, import_node_fs.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
121
+ `);
122
+ }
123
+ function o11yDevVars(cfg) {
124
+ if (!cfg.services.includes("o11y")) return void 0;
125
+ return {
126
+ endpoint: cfg.o11y?.endpoint ?? "https://o11y.odla.ai",
127
+ service: cfg.o11y?.service ?? cfg.app.id,
128
+ ...cfg.o11y?.version ? { version: cfg.o11y.version } : {}
129
+ };
130
+ }
131
+ function resolveWriteDevVarsTarget(cfg, requested) {
132
+ if (!requested) return null;
133
+ if (requested === true) return cfg.local.devVarsFile;
134
+ return (0, import_node_path.resolve)((0, import_node_path.dirname)(cfg.configPath), requested);
135
+ }
136
+ function writeDevVars(path, credentials, env, o11y) {
137
+ const entry = credentials.envs[env];
138
+ if (!entry) throw new Error(`no credentials for env "${env}" in ${path}`);
139
+ const lines = [`ODLA_PLATFORM="${credentials.platformUrl}"`];
140
+ if (entry.dbKey) lines.push(`ODLA_ENDPOINT="${credentials.dbEndpoint}"`);
141
+ lines.push(`ODLA_APP_ID="${credentials.appId}"`, `ODLA_ENV="${env}"`, `ODLA_TENANT="${entry.tenantId}"`);
142
+ if (entry.dbKey) lines.push(`ODLA_API_KEY="${entry.dbKey}"`);
143
+ if (o11y) {
144
+ lines.push(`ODLA_O11Y_ENDPOINT="${o11y.endpoint}"`, `ODLA_O11Y_SERVICE="${o11y.service}"`);
145
+ if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
146
+ if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
147
+ }
148
+ const existing = (0, import_node_fs.existsSync)(path) ? (0, import_node_fs.readFileSync)(path, "utf8") : "";
149
+ const retained = existing.split(/\r?\n/).filter((line) => !isManagedDevVar(line));
150
+ while (retained.at(-1) === "") retained.pop();
151
+ const prefix = retained.length ? `${retained.join("\n")}
152
+
153
+ ` : "";
154
+ writePrivateText(path, `${prefix}${lines.join("\n")}
155
+ `);
156
+ }
157
+ var MANAGED_DEV_VARS = /* @__PURE__ */ new Set([
158
+ "ODLA_PLATFORM",
159
+ "ODLA_ENDPOINT",
160
+ "ODLA_APP_ID",
161
+ "ODLA_ENV",
162
+ "ODLA_TENANT",
163
+ "ODLA_API_KEY",
164
+ "ODLA_O11Y_ENDPOINT",
165
+ "ODLA_O11Y_SERVICE",
166
+ "ODLA_O11Y_VERSION",
167
+ "ODLA_O11Y_TOKEN"
168
+ ]);
169
+ function isManagedDevVar(line) {
170
+ const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
171
+ return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
172
+ }
173
+ function writePrivateText(path, text) {
174
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
175
+ const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
176
+ (0, import_node_fs.writeFileSync)(temporary, text, { mode: 384 });
177
+ (0, import_node_fs.chmodSync)(temporary, 384);
178
+ (0, import_node_fs.renameSync)(temporary, path);
179
+ }
180
+ function gitignoreEntry(rootDir, path) {
181
+ const rel = (0, import_node_path.relative)((0, import_node_path.resolve)(rootDir), (0, import_node_path.resolve)(path));
182
+ if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path.isAbsolute)(rel)) return null;
183
+ return rel.replaceAll("\\", "/");
184
+ }
185
+ function displayPath(path, rootDir = process.cwd()) {
186
+ const rel = (0, import_node_path.relative)(rootDir, path);
187
+ return rel && !rel.startsWith("..") ? rel : path;
188
+ }
189
+
190
+ // src/token.ts
191
+ async function getDeveloperToken(cfg, options, doFetch, out) {
192
+ if (options.token) return options.token;
193
+ const audience = platformAudience(cfg.platformUrl);
194
+ if (import_node_process2.default.env.ODLA_DEV_TOKEN) {
195
+ const declared = import_node_process2.default.env.ODLA_DEV_TOKEN_AUDIENCE;
196
+ if (declared) {
197
+ if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
198
+ } else if (audience !== "https://odla.ai") {
199
+ throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
200
+ }
201
+ return import_node_process2.default.env.ODLA_DEV_TOKEN;
202
+ }
203
+ const cached = readJsonFile(cfg.local.tokenFile);
204
+ if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
205
+ out.log(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
206
+ return cached.token;
207
+ }
208
+ const browser = approvalBrowser(options);
209
+ const { token, expiresAt } = await (0, import_db.requestToken)({
210
+ endpoint: cfg.platformUrl,
211
+ label: `${cfg.app.id} provisioner`,
212
+ fetch: doFetch,
213
+ onCode: async ({ userCode, expiresIn }) => {
214
+ const approvalUrl = handshakeUrl(cfg.platformUrl, userCode);
215
+ out.log("");
216
+ out.log(`Approve code ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
217
+ if (browser.open) {
218
+ try {
219
+ await (options.openApprovalUrl ?? openUrl)(approvalUrl);
220
+ out.log(`auth: opened browser for approval${browser.mode === "auto" ? " (auto)" : ""}`);
221
+ } catch (err) {
222
+ out.log(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
223
+ }
224
+ } else if (browser.reason) {
225
+ out.log(`auth: browser launch skipped (${browser.reason})`);
226
+ }
227
+ out.log("");
228
+ }
229
+ });
230
+ writePrivateJson(cfg.local.tokenFile, { platform: audience, token, expiresAt });
231
+ out.log(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
232
+ return token;
233
+ }
234
+ function approvalBrowser(options) {
235
+ if (options.open === true) return { open: true, mode: "forced" };
236
+ if (options.open === false) return { open: false, reason: "disabled by --no-open" };
237
+ if (import_node_process2.default.env.CI) return { open: false, reason: "CI environment" };
238
+ const interactive = options.interactive ?? Boolean(import_node_process2.default.stdin.isTTY && import_node_process2.default.stdout.isTTY);
239
+ if (!interactive) return { open: false, reason: "non-interactive shell; pass --open to force" };
240
+ return { open: true, mode: "auto" };
241
+ }
242
+ function handshakeUrl(platformUrl, userCode) {
243
+ const url = new URL("/studio", platformUrl);
244
+ url.searchParams.set("code", userCode);
245
+ return url.toString();
246
+ }
247
+ function platformAudience(value) {
248
+ let url;
249
+ try {
250
+ url = new URL(value);
251
+ } catch {
252
+ throw new Error("platform must be an absolute URL");
253
+ }
254
+ const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
255
+ if (url.protocol !== "https:" && !(local && url.protocol === "http:")) {
256
+ throw new Error("platform credentials require HTTPS except for loopback development");
257
+ }
258
+ if (url.username || url.password || url.search || url.hash) {
259
+ throw new Error("platform URL must not contain credentials, query, or fragment");
260
+ }
261
+ return url.origin;
262
+ }
263
+
264
+ // src/admin-ai-auth.ts
265
+ var import_node_fs2 = require("fs");
266
+ var import_node_process3 = __toESM(require("process"), 1);
267
+ var import_db2 = require("@odla-ai/db");
268
+ async function getScopedPlatformToken(options) {
269
+ return resolveAdminPlatformToken(options);
270
+ }
271
+ async function resolveAdminPlatformToken(options) {
272
+ const audience = platformAudience(options.platform);
273
+ if (options.token) return options.token;
274
+ const fromEnv = import_node_process3.default.env.ODLA_ADMIN_TOKEN;
275
+ if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
276
+ return scopedToken(
277
+ audience,
278
+ options.scope,
279
+ options,
280
+ options.fetch ?? fetch,
281
+ options.stdout ?? console
282
+ );
283
+ }
284
+ function audienceBoundEnvToken(token, platform) {
285
+ const audience = platformAudience(platform);
286
+ const declared = import_node_process3.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
287
+ if (declared) {
288
+ if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
289
+ } else if (audience !== "https://odla.ai") {
290
+ throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE is required for a non-default platform");
291
+ }
292
+ return token;
293
+ }
294
+ async function scopedToken(platform, scope, options, doFetch, out) {
295
+ const audience = platformAudience(platform);
296
+ const tokenFile = options.tokenFile ?? `${import_node_process3.default.cwd()}/.odla/admin-token.local.json`;
297
+ const cache = readJsonFile(tokenFile);
298
+ const cached = cache?.platform === audience ? cache.tokens?.[scope] : void 0;
299
+ if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
300
+ out.log(`auth: using cached ${scope} grant (${tokenFile})`);
301
+ return cached.token;
302
+ }
303
+ const { token, expiresAt } = await (0, import_db2.requestToken)({
304
+ endpoint: audience,
305
+ label: `odla CLI admin AI (${scope})`,
306
+ scopes: [scope],
307
+ fetch: doFetch,
308
+ onCode: async ({ userCode, expiresIn }) => {
309
+ const approvalUrl = handshakeUrl(audience, userCode);
310
+ out.log(`Approve scoped request ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
311
+ const shouldOpen = options.open === true || options.open !== false && !import_node_process3.default.env.CI && Boolean(import_node_process3.default.stdin.isTTY && import_node_process3.default.stdout.isTTY);
312
+ if (shouldOpen) await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
313
+ }
314
+ });
315
+ const tokens = cache?.platform === audience ? { ...cache.tokens ?? {} } : {};
316
+ tokens[scope] = { token, expiresAt };
317
+ if ((0, import_node_fs2.existsSync)(`${import_node_process3.default.cwd()}/.git`)) ensureGitignore(import_node_process3.default.cwd(), [tokenFile]);
318
+ writePrivateJson(tokenFile, { platform: audience, tokens });
319
+ out.log(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
320
+ return token;
321
+ }
322
+
323
+ // src/admin-ai-audit.ts
324
+ function adminAiAuditQuery(filters) {
325
+ if (filters.limit === void 0) return "";
326
+ if (!Number.isSafeInteger(filters.limit) || filters.limit < 1 || filters.limit > 200) {
327
+ throw new Error("audit limit must be an integer from 1 to 200");
328
+ }
329
+ return `?limit=${filters.limit}`;
330
+ }
331
+ async function readAdminAiAudit(request) {
332
+ const response = await request.fetch(`${request.platform}/registry/platform/ai-audit${request.query}`, {
333
+ headers: request.headers
334
+ });
335
+ const body = await responseBody(response);
336
+ if (!response.ok) throw new Error(apiError(response.status, body));
337
+ if (request.json) {
338
+ request.stdout.log(JSON.stringify(body, null, 2));
339
+ return;
340
+ }
341
+ const events = isRecord(body) && Array.isArray(body.events) ? body.events.filter(isRecord) : [];
342
+ request.stdout.log("when change target before -> after actor");
343
+ for (const event of events) {
344
+ const before = isRecord(event.oldPolicy) ? event.oldPolicy : void 0;
345
+ const after = isRecord(event.newPolicy) ? event.newPolicy : void 0;
346
+ 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";
347
+ request.stdout.log([
348
+ timestamp(event.createdAt),
349
+ String(event.changeKind ?? ""),
350
+ String(event.purpose ?? event.provider ?? ""),
351
+ route,
352
+ `${String(event.actorType ?? "")}:${String(event.actorId ?? "")}`
353
+ ].join(" "));
354
+ }
355
+ }
356
+ async function responseBody(response) {
357
+ const text = await response.text();
358
+ if (!text) return {};
359
+ try {
360
+ return JSON.parse(text);
361
+ } catch {
362
+ return { message: text.slice(0, 300) };
363
+ }
364
+ }
365
+ function apiError(status, body) {
366
+ const error = isRecord(body) && isRecord(body.error) ? body.error : void 0;
367
+ const message = error && typeof error.message === "string" ? error.message : isRecord(body) && typeof body.message === "string" ? body.message : "request failed";
368
+ return `read System AI admin changes failed (${status}): ${message}`;
369
+ }
370
+ function timestamp(value) {
371
+ if (typeof value !== "number" || !Number.isFinite(value)) return String(value ?? "");
372
+ const date = new Date(value);
373
+ return Number.isFinite(date.valueOf()) ? date.toISOString() : "";
374
+ }
375
+ function isRecord(value) {
376
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
377
+ }
378
+
379
+ // src/admin-ai-policy.ts
380
+ var SYSTEM_AI_PURPOSES = ["o11y.triage", "security.discovery", "security.validation"];
381
+ function requireSystemAiPurpose(value) {
382
+ if (!SYSTEM_AI_PURPOSES.includes(value)) {
383
+ throw new Error(`purpose must be one of: ${SYSTEM_AI_PURPOSES.join(", ")}`);
384
+ }
385
+ return value;
386
+ }
387
+
388
+ // src/admin-ai-usage.ts
389
+ function adminAiUsageQuery(filters) {
390
+ const params = new URLSearchParams();
391
+ if (filters.appId?.trim()) params.set("appId", filters.appId.trim());
392
+ if (filters.env?.trim()) params.set("env", filters.env.trim());
393
+ if (filters.runId?.trim()) params.set("runId", filters.runId.trim());
394
+ if (filters.limit !== void 0) params.set("limit", String(usageLimit(filters.limit)));
395
+ const query = params.toString();
396
+ return query ? `?${query}` : "";
397
+ }
398
+ async function readAdminAiUsage(request) {
399
+ const res = await request.fetch(`${request.platform}/registry/platform/ai-usage${request.query}`, {
400
+ headers: request.headers
401
+ });
402
+ const body = await responseBody2(res);
403
+ if (!res.ok) throw new Error(apiError2("read platform AI usage", res.status, body));
404
+ if (request.json) request.stdout.log(JSON.stringify(body, null, 2));
405
+ else printUsage(body, request.stdout);
406
+ }
407
+ function usageLimit(value) {
408
+ if (!Number.isSafeInteger(value) || value < 1 || value > 500) {
409
+ throw new Error("usage limit must be an integer from 1 to 500");
410
+ }
411
+ return value;
412
+ }
413
+ function printUsage(body, out) {
414
+ const aggregates = isRecord2(body) && isRecord2(body.aggregates) && Array.isArray(body.aggregates.byStatus) ? body.aggregates.byStatus.filter(isRecord2) : [];
415
+ out.log(`All retained hosted-security status totals (up to ${String(isRecord2(body) ? body.retentionDays ?? 90 : 90)} days; o11y triage excluded)
416
+ status calls input tokens output tokens cost unpriced calls`);
417
+ for (const row of aggregates) {
418
+ out.log([
419
+ String(row.status ?? ""),
420
+ String(row.calls ?? ""),
421
+ String(row.input_tokens ?? ""),
422
+ String(row.output_tokens ?? ""),
423
+ formatMicrousd(row.cost_microusd),
424
+ String(row.unpriced_calls ?? "")
425
+ ].join(" "));
426
+ }
427
+ const events = isRecord2(body) && Array.isArray(body.events) ? body.events.filter(isRecord2) : [];
428
+ out.log(`Latest ${String(isRecord2(body) ? body.eventLimit ?? events.length : events.length)} hosted-security events
429
+ when app/env run actor purpose/role route / policy tokens cost status`);
430
+ for (const event of events) {
431
+ const input = numeric(event.input_tokens);
432
+ const output = numeric(event.output_tokens);
433
+ const priced = event.priced === true || event.priced === 1;
434
+ out.log([
435
+ timestamp2(event.created_at),
436
+ `${String(event.app_id ?? "")}/${String(event.env ?? "")}`,
437
+ String(event.run_id ?? ""),
438
+ `${String(event.actor_type ?? "")}:${String(event.actor_id ?? "")}`,
439
+ `${String(event.purpose ?? "")}/${String(event.role ?? "")}`,
440
+ `${String(event.provider ?? "")}/${String(event.model ?? "")}@v${String(event.policy_version ?? "unknown")}`,
441
+ String(input + output),
442
+ priced ? formatMicrousd(event.cost_microusd) : "unpriced",
443
+ `${String(event.status ?? "")}${event.error_code ? `:${String(event.error_code)}` : ""}`
444
+ ].join(" "));
445
+ }
446
+ }
447
+ function numeric(value) {
448
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
449
+ }
450
+ function formatMicrousd(value) {
451
+ return typeof value === "number" && Number.isFinite(value) ? `$${(value / 1e6).toFixed(6)}` : "unpriced";
452
+ }
453
+ function timestamp2(value) {
454
+ if (typeof value !== "number" || !Number.isFinite(value)) return String(value ?? "");
455
+ const date = new Date(value);
456
+ return Number.isFinite(date.valueOf()) ? date.toISOString() : "";
457
+ }
458
+ async function responseBody2(res) {
459
+ const text = await res.text();
460
+ if (!text) return {};
461
+ try {
462
+ return JSON.parse(text);
463
+ } catch {
464
+ return { message: text.slice(0, 300) };
465
+ }
466
+ }
467
+ function apiError2(action, status, body) {
468
+ const error = isRecord2(body) && isRecord2(body.error) ? body.error : void 0;
469
+ const message = error && typeof error.message === "string" ? error.message : isRecord2(body) && typeof body.message === "string" ? body.message : "request failed";
470
+ return `${action} failed (${status}): ${message}`;
471
+ }
472
+ function isRecord2(value) {
473
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
474
+ }
475
+
476
+ // src/admin-ai.ts
477
+ async function adminAi(options) {
478
+ const platform = platformAudience(options.platform ?? import_node_process4.default.env.ODLA_PLATFORM ?? "https://odla.ai");
479
+ const doFetch = options.fetch ?? fetch;
480
+ const out = options.stdout ?? console;
481
+ const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
482
+ const auditQuery = options.action === "audit" ? adminAiAuditQuery(options) : void 0;
483
+ 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";
484
+ const token = await resolveAdminPlatformToken({
485
+ platform,
486
+ scope,
487
+ token: options.token,
488
+ open: options.open,
489
+ fetch: doFetch,
490
+ stdout: out,
491
+ openApprovalUrl: options.openApprovalUrl,
492
+ tokenFile: options.tokenFile
493
+ });
494
+ const headers = { authorization: `Bearer ${token}`, "content-type": "application/json" };
495
+ if (options.action === "usage") {
496
+ await readAdminAiUsage({ platform, query: usageQuery ?? "", headers, json: options.json, fetch: doFetch, stdout: out });
497
+ return;
498
+ }
499
+ if (options.action === "audit") {
500
+ await readAdminAiAudit({ platform, query: auditQuery ?? "", headers, json: options.json, fetch: doFetch, stdout: out });
501
+ return;
502
+ }
503
+ if (options.action === "credentials") {
504
+ const res2 = await doFetch(`${platform}/registry/platform/ai-credentials`, { headers });
505
+ const body2 = await responseBody3(res2);
506
+ if (!res2.ok) throw new Error(apiError3("read platform AI credential status", res2.status, body2));
507
+ const credentials = isRecord3(body2) && isRecord3(body2.credentials) ? body2.credentials : body2;
508
+ if (options.json) out.log(JSON.stringify({ credentials }, null, 2));
509
+ else for (const [provider, status] of Object.entries(isRecord3(credentials) ? credentials : {})) {
510
+ const label = isRecord3(status) && status.set === true ? status.readable === true ? "vault readable" : "stored; vault unreadable" : "not set";
511
+ out.log(`${provider} ${label}`);
512
+ }
513
+ return;
514
+ }
515
+ if (options.action === "credential-set") {
516
+ const provider = requireProvider(options.credentialProvider);
517
+ const value = await credentialValue(options);
518
+ const res2 = await doFetch(`${platform}/registry/platform/ai-credentials/${encodeURIComponent(provider)}`, {
519
+ method: "PUT",
520
+ headers,
521
+ body: JSON.stringify({ value })
522
+ });
523
+ const body2 = await responseBody3(res2);
524
+ if (!res2.ok) throw new Error(apiError3(`store ${provider} platform credential`, res2.status, body2));
525
+ out.log(`stored ${provider} platform credential (write-only; value was not returned)`);
526
+ return;
527
+ }
528
+ if (options.action === "show" || options.action === "models") {
529
+ const res2 = await doFetch(`${platform}/registry/platform/ai-policies`, { headers });
530
+ const body2 = await responseBody3(res2);
531
+ if (!res2.ok) throw new Error(apiError3("read platform AI policies", res2.status, body2));
532
+ if (options.action === "models") {
533
+ const models = catalogModels(body2).filter((model) => !options.provider || model.provider === options.provider);
534
+ if (!models.length) throw new Error(options.provider ? `no System AI models found for provider ${options.provider}` : "platform returned no System AI models");
535
+ if (options.json) out.log(JSON.stringify({ models }, null, 2));
536
+ else for (const model of models) out.log(`${model.provider} ${model.id}`);
537
+ return;
538
+ }
539
+ const policies = policyArray(body2);
540
+ if (options.json) {
541
+ out.log(JSON.stringify({ policies, models: catalogModels(body2) }, null, 2));
542
+ return;
543
+ }
544
+ out.log("purpose enabled provider model max calls max input bytes max output tokens");
545
+ for (const policy2 of policies) {
546
+ out.log([
547
+ policy2.purpose,
548
+ String(policy2.enabled),
549
+ policy2.provider,
550
+ policy2.model,
551
+ String(policy2.maxCallsPerRun ?? ""),
552
+ String(policy2.maxInputBytes ?? ""),
553
+ String(policy2.maxOutputTokens ?? "")
554
+ ].join(" "));
555
+ }
556
+ return;
557
+ }
558
+ if (options.purpose === "security") {
559
+ const currentResponse = await doFetch(`${platform}/registry/platform/ai-policies`, { headers });
560
+ const currentBody = await responseBody3(currentResponse);
561
+ if (!currentResponse.ok) throw new Error(apiError3("read current security AI policies", currentResponse.status, currentBody));
562
+ const policies = policyArray(currentBody);
563
+ const discovery = policies.find((policy2) => policy2.purpose === "security.discovery");
564
+ const validation = policies.find((policy2) => policy2.purpose === "security.validation");
565
+ if (!discovery || !validation || !Number.isSafeInteger(discovery.version) || !Number.isSafeInteger(validation.version)) {
566
+ throw new Error("platform returned invalid security AI policy revisions");
567
+ }
568
+ const shared = {
569
+ ...options.enabled === void 0 ? {} : { enabled: options.enabled },
570
+ ...options.maxInputBytes === void 0 ? {} : { maxInputBytes: options.maxInputBytes },
571
+ ...options.maxOutputTokens === void 0 ? {} : { maxOutputTokens: options.maxOutputTokens },
572
+ ...options.maxCallsPerRun === void 0 ? {} : { maxCallsPerRun: options.maxCallsPerRun }
573
+ };
574
+ const discoveryUpdate = {
575
+ ...shared,
576
+ ...options.discoveryProvider?.trim() ? { provider: options.discoveryProvider.trim() } : {},
577
+ ...options.discoveryModel?.trim() ? { model: options.discoveryModel.trim() } : {}
578
+ };
579
+ const validationUpdate = {
580
+ ...shared,
581
+ ...options.validationProvider?.trim() ? { provider: options.validationProvider.trim() } : {},
582
+ ...options.validationModel?.trim() ? { model: options.validationModel.trim() } : {}
583
+ };
584
+ if (!Object.keys(discoveryUpdate).length && !Object.keys(validationUpdate).length) {
585
+ throw new Error("security set requires a route, enabled state, or budget change");
586
+ }
587
+ const res2 = await doFetch(`${platform}/registry/platform/ai-policies/security`, {
588
+ method: "PUT",
589
+ headers,
590
+ body: JSON.stringify({
591
+ expectedVersions: { discovery: discovery.version, validation: validation.version },
592
+ discovery: discoveryUpdate,
593
+ validation: validationUpdate
594
+ })
595
+ });
596
+ const response2 = await responseBody3(res2);
597
+ if (!res2.ok) throw new Error(apiError3("update security review routes", res2.status, response2));
598
+ out.log(options.json ? JSON.stringify(response2, null, 2) : "updated security review discovery and independent validation routes atomically");
599
+ return;
600
+ }
601
+ const purpose = requireSystemAiPurpose(options.purpose);
602
+ const body = {
603
+ ...options.provider?.trim() ? { provider: options.provider.trim() } : {},
604
+ ...options.model?.trim() ? { model: options.model.trim() } : {},
605
+ ...options.enabled === void 0 ? {} : { enabled: options.enabled },
606
+ ...options.maxInputBytes === void 0 ? {} : { maxInputBytes: options.maxInputBytes },
607
+ ...options.maxOutputTokens === void 0 ? {} : { maxOutputTokens: options.maxOutputTokens },
608
+ ...options.maxCallsPerRun === void 0 ? {} : { maxCallsPerRun: options.maxCallsPerRun }
609
+ };
610
+ if (!Object.keys(body).length) throw new Error("set requires a provider, model, enabled state, or budget change");
611
+ const res = await doFetch(`${platform}/registry/platform/ai-policies/${encodeURIComponent(purpose)}`, {
612
+ method: "PUT",
613
+ headers,
614
+ body: JSON.stringify(body)
615
+ });
616
+ const response = await responseBody3(res);
617
+ if (!res.ok) throw new Error(apiError3(`update ${purpose}`, res.status, response));
618
+ const policy = isRecord3(response) && isRecord3(response.policy) ? response.policy : isRecord3(response) ? response : {};
619
+ out.log(options.json ? JSON.stringify({ policy }, null, 2) : `updated ${purpose}: ${String(policy.provider ?? "unchanged")}/${String(policy.model ?? "unchanged")}`);
620
+ }
621
+ function requireProvider(value) {
622
+ if (value !== "anthropic" && value !== "openai" && value !== "google") {
623
+ throw new Error("provider must be one of: anthropic, openai, google");
624
+ }
625
+ return value;
626
+ }
627
+ async function credentialValue(options) {
628
+ if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
629
+ let value;
630
+ if (options.fromEnv) value = import_node_process4.default.env[options.fromEnv];
631
+ else if (options.stdin) value = await (options.readStdin ?? readStdin)();
632
+ else throw new Error("credential input required: use --from-env <NAME> or --stdin; values are never accepted as arguments");
633
+ value = value?.replace(/[\r\n]+$/, "");
634
+ if (!value) throw new Error(options.fromEnv ? `environment variable ${options.fromEnv} is empty or unset` : "stdin credential is empty");
635
+ if (new TextEncoder().encode(value).byteLength > 64 * 1024) throw new Error("credential exceeds 64 KiB");
636
+ return value;
637
+ }
638
+ async function readStdin() {
639
+ let value = "";
640
+ for await (const chunk of import_node_process4.default.stdin) {
641
+ value += String(chunk);
642
+ if (value.length > 64 * 1024) throw new Error("credential exceeds 64 KiB");
643
+ }
644
+ return value;
645
+ }
646
+ function policyArray(body) {
647
+ if (isRecord3(body) && Array.isArray(body.policies)) return body.policies.filter(isRecord3);
648
+ if (isRecord3(body) && isRecord3(body.policies)) return Object.values(body.policies).filter(isRecord3);
649
+ throw new Error("platform returned an invalid AI policy response");
650
+ }
651
+ function catalogModels(body) {
652
+ if (!isRecord3(body) || !isRecord3(body.catalog) || !Array.isArray(body.catalog.models)) {
653
+ throw new Error("platform returned an invalid System AI model catalog");
654
+ }
655
+ return body.catalog.models.filter((value) => isRecord3(value) && typeof value.id === "string" && typeof value.provider === "string");
656
+ }
657
+ async function responseBody3(res) {
658
+ const text = await res.text();
659
+ if (!text) return {};
660
+ try {
661
+ return JSON.parse(text);
662
+ } catch {
663
+ return { message: text.slice(0, 300) };
664
+ }
665
+ }
666
+ function apiError3(action, status, body) {
667
+ const error = isRecord3(body) && isRecord3(body.error) ? body.error : void 0;
668
+ const message = error && typeof error.message === "string" ? error.message : isRecord3(body) && typeof body.message === "string" ? body.message : "request failed";
669
+ return `${action} failed (${status}): ${message}`;
670
+ }
671
+ function isRecord3(value) {
672
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
673
+ }
674
+
675
+ // src/argv.ts
676
+ function parseArgv(argv) {
677
+ const positionals = [];
678
+ const options = {};
679
+ for (let i = 0; i < argv.length; i++) {
680
+ const arg = argv[i];
681
+ if (!arg.startsWith("--")) {
682
+ positionals.push(arg);
683
+ continue;
684
+ }
685
+ const eq = arg.indexOf("=");
686
+ const rawName = arg.slice(2, eq === -1 ? void 0 : eq);
687
+ if (!rawName) continue;
688
+ if (rawName.startsWith("no-")) {
689
+ options[rawName.slice(3)] = false;
690
+ continue;
691
+ }
692
+ if (eq !== -1) {
693
+ addOption(options, rawName, arg.slice(eq + 1));
694
+ continue;
695
+ }
696
+ const value = argv[i + 1];
697
+ if (value !== void 0 && !value.startsWith("--")) {
698
+ i++;
699
+ addOption(options, rawName, value);
700
+ } else {
701
+ addOption(options, rawName, true);
702
+ }
703
+ }
704
+ return { positionals, options };
705
+ }
706
+ function assertArgs(parsed, allowedOptions, maxPositionals) {
707
+ const allowed = new Set(allowedOptions);
708
+ for (const name of Object.keys(parsed.options)) {
709
+ if (!allowed.has(name)) throw new Error(`unknown option "--${name}"; run "odla-ai help" for supported options`);
710
+ }
711
+ if (parsed.positionals.length > maxPositionals) {
712
+ throw new Error(`unexpected argument "${parsed.positionals[maxPositionals]}"; run "odla-ai help"`);
713
+ }
714
+ }
715
+ function requiredString(value, name) {
716
+ const result = stringOpt(value);
717
+ if (!result) throw new Error(`${name} is required`);
718
+ return result;
719
+ }
720
+ function stringOpt(value) {
721
+ if (typeof value === "string") return value;
722
+ if (Array.isArray(value)) return value[value.length - 1];
723
+ return void 0;
724
+ }
725
+ function listOpt(value) {
726
+ if (value === void 0 || typeof value === "boolean") return void 0;
727
+ const values = Array.isArray(value) ? value : [value];
728
+ return values.flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
729
+ }
730
+ function boolOpt(value) {
731
+ if (typeof value === "boolean") return value;
732
+ if (typeof value === "string" && (value === "true" || value === "false")) return value === "true";
733
+ if (value === void 0) return void 0;
734
+ throw new Error("boolean option must be true or false");
735
+ }
736
+ function numberOpt(value, flag) {
737
+ if (value === void 0) return void 0;
738
+ const raw = stringOpt(value);
739
+ const parsed = raw === void 0 ? Number.NaN : Number(raw);
740
+ if (!Number.isSafeInteger(parsed) || parsed < 1) throw new Error(`${flag} requires a positive integer`);
741
+ return parsed;
742
+ }
743
+ function addOption(options, name, value) {
744
+ const current = options[name];
745
+ if (current === void 0) options[name] = value;
746
+ else if (Array.isArray(current)) current.push(String(value));
747
+ else options[name] = [String(current), String(value)];
748
+ }
32
749
 
33
750
  // src/capabilities.ts
34
751
  var CAPABILITIES = {
@@ -37,7 +754,9 @@ var CAPABILITIES = {
37
754
  "issue, persist, and explicitly rotate configured service credentials",
38
755
  "write local Worker values and push deployed Worker secrets through Wrangler stdin",
39
756
  "push db schema/rules and configure platform AI, auth, and deployment links",
40
- "validate config offline and smoke-test a provisioned db environment"
757
+ "validate config offline and smoke-test a provisioned db environment",
758
+ "run app-attributed hosted security discovery and independent validation without provider keys",
759
+ "let admins manage system AI policy and credentials, inspect attributed usage, and read immutable admin changes through separate short-lived capability-only approvals"
41
760
  ],
42
761
  agent: [
43
762
  "install and import the selected odla SDKs",
@@ -48,11 +767,13 @@ var CAPABILITIES = {
48
767
  "approve the odla device code and one-off third-party logins",
49
768
  "consent to production changes with --yes",
50
769
  "request destructive credential rotation explicitly",
51
- "review application semantics, security findings, releases, and merges"
770
+ "review application semantics, security findings, releases, and merges",
771
+ "approve redacted-source disclosure before hosted security reasoning"
52
772
  ],
53
773
  studio: [
54
774
  "view telemetry and environment state",
55
- "perform manual credential recovery when the CLI's local shown-once copy is unavailable"
775
+ "perform manual credential recovery when the CLI's local shown-once copy is unavailable",
776
+ "configure system AI purposes and view app/environment/run-attributed usage"
56
777
  ]
57
778
  };
58
779
  function printCapabilities(json = false, out = console) {
@@ -73,28 +794,28 @@ function printGroup(out, heading, items) {
73
794
  }
74
795
 
75
796
  // src/config.ts
76
- var import_node_fs = require("fs");
77
- var import_node_path = require("path");
797
+ var import_node_fs3 = require("fs");
798
+ var import_node_path2 = require("path");
78
799
  var import_node_url = require("url");
79
800
  var DEFAULT_PLATFORM = "https://odla.ai";
80
801
  var DEFAULT_ENVS = ["dev"];
81
802
  var DEFAULT_SERVICES = ["db", "ai"];
82
803
  async function loadProjectConfig(configPath = "odla.config.mjs") {
83
- const resolved = (0, import_node_path.resolve)(configPath);
84
- if (!(0, import_node_fs.existsSync)(resolved)) {
804
+ const resolved = (0, import_node_path2.resolve)(configPath);
805
+ if (!(0, import_node_fs3.existsSync)(resolved)) {
85
806
  throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
86
807
  }
87
808
  const raw = await loadConfigModule(resolved);
88
- const rootDir = (0, import_node_path.dirname)(resolved);
809
+ const rootDir = (0, import_node_path2.dirname)(resolved);
89
810
  validateRawConfig(raw, resolved);
90
811
  const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
91
812
  const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
92
813
  const envs = unique(raw.envs?.length ? raw.envs : DEFAULT_ENVS);
93
814
  const services = unique(raw.services?.length ? raw.services : DEFAULT_SERVICES);
94
815
  const local = {
95
- tokenFile: (0, import_node_path.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
96
- credentialsFile: (0, import_node_path.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
97
- devVarsFile: (0, import_node_path.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
816
+ tokenFile: (0, import_node_path2.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
817
+ credentialsFile: (0, import_node_path2.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
818
+ devVarsFile: (0, import_node_path2.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
98
819
  gitignore: raw.local?.gitignore ?? true
99
820
  };
100
821
  return {
@@ -111,9 +832,9 @@ async function loadProjectConfig(configPath = "odla.config.mjs") {
111
832
  async function resolveDataExport(cfg, value, names) {
112
833
  if (value === void 0 || value === null || value === false) return void 0;
113
834
  if (typeof value !== "string") return value;
114
- const target = (0, import_node_path.isAbsolute)(value) ? value : (0, import_node_path.resolve)(cfg.rootDir, value);
835
+ const target = (0, import_node_path2.isAbsolute)(value) ? value : (0, import_node_path2.resolve)(cfg.rootDir, value);
115
836
  if (target.endsWith(".json")) {
116
- return JSON.parse((0, import_node_fs.readFileSync)(target, "utf8"));
837
+ return JSON.parse((0, import_node_fs3.readFileSync)(target, "utf8"));
117
838
  }
118
839
  const mod = await import((0, import_node_url.pathToFileURL)(target).href);
119
840
  for (const name of names) {
@@ -164,175 +885,49 @@ function validId(value) {
164
885
  return typeof value === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value);
165
886
  }
166
887
  async function loadConfigModule(path) {
167
- if (path.endsWith(".json")) return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
888
+ if (path.endsWith(".json")) return JSON.parse((0, import_node_fs3.readFileSync)(path, "utf8"));
168
889
  const mod = await import(`${(0, import_node_url.pathToFileURL)(path).href}?t=${Date.now()}`);
169
890
  const value = mod.default ?? mod.config;
170
891
  if (typeof value === "function") return await value();
171
892
  return value;
172
893
  }
173
- function trimSlash(value) {
174
- return value.replace(/\/+$/, "");
175
- }
176
- function unique(values) {
177
- return [...new Set(values.filter(Boolean))];
178
- }
179
-
180
- // src/doctor-checks.ts
181
- var import_node_child_process2 = require("child_process");
182
- var import_node_fs4 = require("fs");
183
- var import_node_path4 = require("path");
184
-
185
- // src/redact.ts
186
- var REPLACEMENTS = [
187
- [/odla_(sk|dev)_[A-Za-z0-9._-]+/g, "odla_$1_[redacted]"],
188
- [/\bsk_(live|test)_[A-Za-z0-9]+/g, "sk_$1_[redacted]"],
189
- [/\bsk-[A-Za-z0-9._-]+/g, "sk-[redacted]"],
190
- [/\bwhsec_[A-Za-z0-9+/=]+/g, "whsec_[redacted]"],
191
- [/\bo11y_[A-Za-z0-9]+/g, "o11y_[redacted]"],
192
- [/\b(ghp|gho|github_pat)_[A-Za-z0-9_]+/g, "$1_[redacted]"],
193
- [/\bAKIA[A-Z0-9]{12,}/g, "AKIA[redacted]"]
194
- ];
195
- function redactSecrets(value) {
196
- let result = value;
197
- for (const [pattern, replacement] of REPLACEMENTS) result = result.replace(pattern, replacement);
198
- return result;
199
- }
200
- function looksSecret(value) {
201
- return redactSecrets(value) !== value || value.includes("-----BEGIN");
202
- }
203
-
204
- // src/local.ts
205
- var import_node_fs2 = require("fs");
206
- var import_node_path2 = require("path");
207
- var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
208
- function readJsonFile(path) {
209
- try {
210
- return JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
211
- } catch {
212
- return null;
213
- }
214
- }
215
- function writePrivateJson(path, value) {
216
- writePrivateText(path, `${JSON.stringify(value, null, 2)}
217
- `);
218
- }
219
- function readCredentials(path) {
220
- if (!(0, import_node_fs2.existsSync)(path)) return null;
221
- let value;
222
- try {
223
- value = JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
224
- } catch {
225
- throw new Error(`credentials file ${path} is not valid JSON; fix or remove it before provisioning`);
226
- }
227
- if (!value || typeof value !== "object" || typeof value.appId !== "string" || !value.envs || typeof value.envs !== "object") {
228
- throw new Error(`credentials file ${path} has an invalid shape; fix or remove it before provisioning`);
229
- }
230
- return value;
231
- }
232
- function mergeCredential(current, update) {
233
- const next = current ?? {
234
- appId: update.appId,
235
- platformUrl: update.platformUrl,
236
- dbEndpoint: update.dbEndpoint,
237
- updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
238
- envs: {}
239
- };
240
- next.appId = update.appId;
241
- next.platformUrl = update.platformUrl;
242
- next.dbEndpoint = update.dbEndpoint;
243
- next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
244
- next.envs[update.env] = {
245
- ...next.envs[update.env] ?? {},
246
- tenantId: update.tenantId,
247
- ...update.dbKey ? { dbKey: update.dbKey } : {},
248
- ...update.o11yToken ? { o11yToken: update.o11yToken } : {}
249
- };
250
- return next;
251
- }
252
- function ensureGitignore(rootDir, localPaths = []) {
253
- const path = (0, import_node_path2.resolve)(rootDir, ".gitignore");
254
- const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : "";
255
- const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line) => !!line);
256
- const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
257
- const missing = wanted.filter((line) => !existing.split(/\r?\n/).includes(line));
258
- if (missing.length === 0) return;
259
- const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
260
- (0, import_node_fs2.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
261
- `);
262
- }
263
- function o11yDevVars(cfg) {
264
- if (!cfg.services.includes("o11y")) return void 0;
265
- return {
266
- endpoint: cfg.o11y?.endpoint ?? "https://o11y.odla.ai",
267
- service: cfg.o11y?.service ?? cfg.app.id,
268
- ...cfg.o11y?.version ? { version: cfg.o11y.version } : {}
269
- };
270
- }
271
- function resolveWriteDevVarsTarget(cfg, requested) {
272
- if (!requested) return null;
273
- if (requested === true) return cfg.local.devVarsFile;
274
- return (0, import_node_path2.resolve)((0, import_node_path2.dirname)(cfg.configPath), requested);
275
- }
276
- function writeDevVars(path, credentials, env, o11y) {
277
- const entry = credentials.envs[env];
278
- if (!entry) throw new Error(`no credentials for env "${env}" in ${path}`);
279
- const lines = [`ODLA_PLATFORM="${credentials.platformUrl}"`];
280
- if (entry.dbKey) lines.push(`ODLA_ENDPOINT="${credentials.dbEndpoint}"`);
281
- lines.push(`ODLA_APP_ID="${credentials.appId}"`, `ODLA_ENV="${env}"`, `ODLA_TENANT="${entry.tenantId}"`);
282
- if (entry.dbKey) lines.push(`ODLA_API_KEY="${entry.dbKey}"`);
283
- if (o11y) {
284
- lines.push(`ODLA_O11Y_ENDPOINT="${o11y.endpoint}"`, `ODLA_O11Y_SERVICE="${o11y.service}"`);
285
- if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
286
- if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
287
- }
288
- const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : "";
289
- const retained = existing.split(/\r?\n/).filter((line) => !isManagedDevVar(line));
290
- while (retained.at(-1) === "") retained.pop();
291
- const prefix = retained.length ? `${retained.join("\n")}
292
-
293
- ` : "";
294
- writePrivateText(path, `${prefix}${lines.join("\n")}
295
- `);
296
- }
297
- var MANAGED_DEV_VARS = /* @__PURE__ */ new Set([
298
- "ODLA_PLATFORM",
299
- "ODLA_ENDPOINT",
300
- "ODLA_APP_ID",
301
- "ODLA_ENV",
302
- "ODLA_TENANT",
303
- "ODLA_API_KEY",
304
- "ODLA_O11Y_ENDPOINT",
305
- "ODLA_O11Y_SERVICE",
306
- "ODLA_O11Y_VERSION",
307
- "ODLA_O11Y_TOKEN"
308
- ]);
309
- function isManagedDevVar(line) {
310
- const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
311
- return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
312
- }
313
- function writePrivateText(path, text) {
314
- (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(path), { recursive: true });
315
- const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
316
- (0, import_node_fs2.writeFileSync)(temporary, text, { mode: 384 });
317
- (0, import_node_fs2.chmodSync)(temporary, 384);
318
- (0, import_node_fs2.renameSync)(temporary, path);
894
+ function trimSlash(value) {
895
+ return value.replace(/\/+$/, "");
319
896
  }
320
- function gitignoreEntry(rootDir, path) {
321
- const rel = (0, import_node_path2.relative)((0, import_node_path2.resolve)(rootDir), (0, import_node_path2.resolve)(path));
322
- if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path2.isAbsolute)(rel)) return null;
323
- return rel.replaceAll("\\", "/");
897
+ function unique(values) {
898
+ return [...new Set(values.filter(Boolean))];
324
899
  }
325
- function displayPath(path, rootDir = process.cwd()) {
326
- const rel = (0, import_node_path2.relative)(rootDir, path);
327
- return rel && !rel.startsWith("..") ? rel : path;
900
+
901
+ // src/doctor-checks.ts
902
+ var import_node_child_process3 = require("child_process");
903
+ var import_node_fs5 = require("fs");
904
+ var import_node_path4 = require("path");
905
+
906
+ // src/redact.ts
907
+ var REPLACEMENTS = [
908
+ [/odla_(sk|dev)_[A-Za-z0-9._-]+/g, "odla_$1_[redacted]"],
909
+ [/\bsk_(live|test)_[A-Za-z0-9]+/g, "sk_$1_[redacted]"],
910
+ [/\bsk-[A-Za-z0-9._-]+/g, "sk-[redacted]"],
911
+ [/\bwhsec_[A-Za-z0-9+/=]+/g, "whsec_[redacted]"],
912
+ [/\bo11y_[A-Za-z0-9]+/g, "o11y_[redacted]"],
913
+ [/\b(ghp|gho|github_pat)_[A-Za-z0-9_]+/g, "$1_[redacted]"],
914
+ [/\bAKIA[A-Z0-9]{12,}/g, "AKIA[redacted]"]
915
+ ];
916
+ function redactSecrets(value) {
917
+ let result = value;
918
+ for (const [pattern, replacement] of REPLACEMENTS) result = result.replace(pattern, replacement);
919
+ return result;
920
+ }
921
+ function looksSecret(value) {
922
+ return redactSecrets(value) !== value || value.includes("-----BEGIN");
328
923
  }
329
924
 
330
925
  // src/wrangler.ts
331
- var import_node_child_process = require("child_process");
332
- var import_node_fs3 = require("fs");
926
+ var import_node_child_process2 = require("child_process");
927
+ var import_node_fs4 = require("fs");
333
928
  var import_node_path3 = require("path");
334
929
  var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
335
- const child = (0, import_node_child_process.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
930
+ const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
336
931
  let stdout = "";
337
932
  let stderr = "";
338
933
  child.stdout.on("data", (chunk) => stdout += chunk.toString());
@@ -345,14 +940,14 @@ var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]
345
940
  function findWranglerConfig(rootDir) {
346
941
  for (const name of WRANGLER_CONFIG_FILES) {
347
942
  const path = (0, import_node_path3.join)(rootDir, name);
348
- if ((0, import_node_fs3.existsSync)(path)) return path;
943
+ if ((0, import_node_fs4.existsSync)(path)) return path;
349
944
  }
350
945
  return null;
351
946
  }
352
947
  function readWranglerConfig(path) {
353
948
  if (path.endsWith(".toml")) return null;
354
949
  try {
355
- return JSON.parse(stripJsonComments((0, import_node_fs3.readFileSync)(path, "utf8")));
950
+ return JSON.parse(stripJsonComments((0, import_node_fs4.readFileSync)(path, "utf8")));
356
951
  } catch {
357
952
  return null;
358
953
  }
@@ -423,7 +1018,7 @@ function lintRules(rules, entities, publicRead) {
423
1018
  }
424
1019
  return warnings;
425
1020
  }
426
- var defaultExec = (cmd, args, cwd) => (0, import_node_child_process2.execFileSync)(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
1021
+ var defaultExec = (cmd, args, cwd) => (0, import_node_child_process3.execFileSync)(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
427
1022
  function trackedSecretFiles(rootDir, exec = defaultExec, localPaths = []) {
428
1023
  let output;
429
1024
  try {
@@ -453,7 +1048,7 @@ function wranglerWarnings(rootDir) {
453
1048
  const dir = (0, import_node_path4.resolve)(rootDir, assets.directory);
454
1049
  if (dir === (0, import_node_path4.resolve)(rootDir)) {
455
1050
  warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
456
- } else if ((0, import_node_fs4.existsSync)((0, import_node_path4.join)(dir, "node_modules"))) {
1051
+ } else if ((0, import_node_fs5.existsSync)((0, import_node_path4.join)(dir, "node_modules"))) {
457
1052
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
458
1053
  }
459
1054
  }
@@ -489,12 +1084,12 @@ function o11yProjectWarnings(rootDir) {
489
1084
  return warnings;
490
1085
  }
491
1086
  const main = typeof config.main === "string" ? (0, import_node_path4.resolve)(rootDir, config.main) : null;
492
- if (!main || !(0, import_node_fs4.existsSync)(main)) {
1087
+ if (!main || !(0, import_node_fs5.existsSync)(main)) {
493
1088
  warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
494
1089
  } else {
495
1090
  let source = "";
496
1091
  try {
497
- source = (0, import_node_fs4.readFileSync)(main, "utf8");
1092
+ source = (0, import_node_fs5.readFileSync)(main, "utf8");
498
1093
  } catch {
499
1094
  }
500
1095
  if (!/\bwithObservability\b/.test(source)) {
@@ -509,7 +1104,7 @@ function o11yProjectWarnings(rootDir) {
509
1104
  }
510
1105
  function readPackageJson(rootDir) {
511
1106
  try {
512
- return JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path4.join)(rootDir, "package.json"), "utf8"));
1107
+ return JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(rootDir, "package.json"), "utf8"));
513
1108
  } catch {
514
1109
  return null;
515
1110
  }
@@ -576,13 +1171,13 @@ async function doctor(options) {
576
1171
  }
577
1172
 
578
1173
  // src/init.ts
579
- var import_node_fs5 = require("fs");
1174
+ var import_node_fs6 = require("fs");
580
1175
  var import_node_path5 = require("path");
581
1176
  function initProject(options) {
582
1177
  const out = options.stdout ?? console;
583
1178
  const rootDir = (0, import_node_path5.resolve)(options.rootDir ?? process.cwd());
584
1179
  const configPath = (0, import_node_path5.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
585
- if ((0, import_node_fs5.existsSync)(configPath) && !options.force) {
1180
+ if ((0, import_node_fs6.existsSync)(configPath) && !options.force) {
586
1181
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
587
1182
  }
588
1183
  if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
@@ -591,10 +1186,10 @@ function initProject(options) {
591
1186
  const envs = options.envs?.length ? options.envs : ["dev"];
592
1187
  const services = options.services?.length ? options.services : ["db", "ai"];
593
1188
  const aiProvider = options.aiProvider ?? "anthropic";
594
- (0, import_node_fs5.mkdirSync)((0, import_node_path5.dirname)(configPath), { recursive: true });
595
- (0, import_node_fs5.mkdirSync)((0, import_node_path5.resolve)(rootDir, "src/odla"), { recursive: true });
596
- (0, import_node_fs5.mkdirSync)((0, import_node_path5.resolve)(rootDir, ".odla"), { recursive: true });
597
- (0, import_node_fs5.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
1189
+ (0, import_node_fs6.mkdirSync)((0, import_node_path5.dirname)(configPath), { recursive: true });
1190
+ (0, import_node_fs6.mkdirSync)((0, import_node_path5.resolve)(rootDir, "src/odla"), { recursive: true });
1191
+ (0, import_node_fs6.mkdirSync)((0, import_node_path5.resolve)(rootDir, ".odla"), { recursive: true });
1192
+ (0, import_node_fs6.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
598
1193
  writeIfMissing((0, import_node_path5.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
599
1194
  writeIfMissing((0, import_node_path5.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
600
1195
  ensureGitignore(rootDir);
@@ -603,8 +1198,8 @@ function initProject(options) {
603
1198
  out.log("updated .gitignore for local odla credentials");
604
1199
  }
605
1200
  function writeIfMissing(path, text) {
606
- if ((0, import_node_fs5.existsSync)(path)) return;
607
- (0, import_node_fs5.writeFileSync)(path, text);
1201
+ if ((0, import_node_fs6.existsSync)(path)) return;
1202
+ (0, import_node_fs6.writeFileSync)(path, text);
608
1203
  }
609
1204
  function configTemplate(input) {
610
1205
  return `export default {
@@ -693,84 +1288,7 @@ function relativeDisplay(path, rootDir) {
693
1288
  // src/provision.ts
694
1289
  var import_apps2 = require("@odla-ai/apps");
695
1290
  var import_ai = require("@odla-ai/ai");
696
- var import_node_process3 = __toESM(require("process"), 1);
697
-
698
- // src/token.ts
699
- var import_db = require("@odla-ai/db");
700
- var import_node_process2 = __toESM(require("process"), 1);
701
-
702
- // src/open.ts
703
- var import_node_child_process3 = require("child_process");
704
- var import_node_process = __toESM(require("process"), 1);
705
- async function openUrl(url, options = {}) {
706
- const command = openerFor(options.platform ?? import_node_process.default.platform);
707
- const doSpawn = options.spawnImpl ?? import_node_child_process3.spawn;
708
- await new Promise((resolve6, reject) => {
709
- const child = doSpawn(command.cmd, [...command.args, url], {
710
- stdio: "ignore",
711
- detached: true
712
- });
713
- child.once("error", reject);
714
- child.once("spawn", () => {
715
- child.unref();
716
- resolve6();
717
- });
718
- });
719
- }
720
- function openerFor(platform) {
721
- if (platform === "darwin") return { cmd: "open", args: [] };
722
- if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
723
- return { cmd: "xdg-open", args: [] };
724
- }
725
-
726
- // src/token.ts
727
- async function getDeveloperToken(cfg, options, doFetch, out) {
728
- if (options.token) return options.token;
729
- if (import_node_process2.default.env.ODLA_DEV_TOKEN) return import_node_process2.default.env.ODLA_DEV_TOKEN;
730
- const cached = readJsonFile(cfg.local.tokenFile);
731
- if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
732
- out.log(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
733
- return cached.token;
734
- }
735
- const browser = approvalBrowser(options);
736
- const { token, expiresAt } = await (0, import_db.requestToken)({
737
- endpoint: cfg.platformUrl,
738
- label: `${cfg.app.id} provisioner`,
739
- fetch: doFetch,
740
- onCode: async ({ userCode, expiresIn }) => {
741
- const approvalUrl = handshakeUrl(cfg.platformUrl, userCode);
742
- out.log("");
743
- out.log(`Approve code ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
744
- if (browser.open) {
745
- try {
746
- await (options.openApprovalUrl ?? openUrl)(approvalUrl);
747
- out.log(`auth: opened browser for approval${browser.mode === "auto" ? " (auto)" : ""}`);
748
- } catch (err) {
749
- out.log(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
750
- }
751
- } else if (browser.reason) {
752
- out.log(`auth: browser launch skipped (${browser.reason})`);
753
- }
754
- out.log("");
755
- }
756
- });
757
- writePrivateJson(cfg.local.tokenFile, { token, expiresAt });
758
- out.log(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
759
- return token;
760
- }
761
- function approvalBrowser(options) {
762
- if (options.open === true) return { open: true, mode: "forced" };
763
- if (options.open === false) return { open: false, reason: "disabled by --no-open" };
764
- if (import_node_process2.default.env.CI) return { open: false, reason: "CI environment" };
765
- const interactive = options.interactive ?? Boolean(import_node_process2.default.stdin.isTTY && import_node_process2.default.stdout.isTTY);
766
- if (!interactive) return { open: false, reason: "non-interactive shell; pass --open to force" };
767
- return { open: true, mode: "auto" };
768
- }
769
- function handshakeUrl(platformUrl, userCode) {
770
- const url = new URL("/studio", platformUrl);
771
- url.searchParams.set("code", userCode);
772
- return url.toString();
773
- }
1291
+ var import_node_process5 = __toESM(require("process"), 1);
774
1292
 
775
1293
  // src/provision-credentials.ts
776
1294
  var import_apps = require("@odla-ai/apps");
@@ -1074,7 +1592,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
1074
1592
  out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
1075
1593
  }
1076
1594
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
1077
- const key = import_node_process3.default.env[cfg.ai.keyEnv];
1595
+ const key = import_node_process5.default.env[cfg.ai.keyEnv];
1078
1596
  if (key) {
1079
1597
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
1080
1598
  await (0, import_ai.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -1134,10 +1652,170 @@ async function safeText2(res) {
1134
1652
  }
1135
1653
  }
1136
1654
 
1655
+ // src/security.ts
1656
+ var import_node_path6 = require("path");
1657
+ var import_security = require("@odla-ai/security");
1658
+ var import_node = require("@odla-ai/security/node");
1659
+ async function runHostedSecurity(options) {
1660
+ if (options.sourceDisclosureAck !== "redacted") {
1661
+ throw new Error("Hosted security requires sourceDisclosureAck=redacted before repository source may leave this process");
1662
+ }
1663
+ const selfAudit = options.selfAudit === true;
1664
+ const cfg = selfAudit ? void 0 : await loadProjectConfig(options.configPath ?? "odla.config.mjs");
1665
+ const appId = selfAudit ? "odla-ai" : cfg.app.id;
1666
+ const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
1667
+ const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
1668
+ const target = (0, import_node_path6.resolve)(options.target ?? cfg?.rootDir ?? ".");
1669
+ const output = (0, import_node_path6.resolve)(options.out ?? (0, import_node_path6.resolve)(target, ".odla/security/hosted"));
1670
+ const outputRelative = (0, import_node_path6.relative)(target, output).split(import_node_path6.sep).join("/");
1671
+ if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
1672
+ const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
1673
+ const tokenRequest = {
1674
+ platform,
1675
+ appId,
1676
+ env,
1677
+ selfAudit,
1678
+ scope: selfAudit ? "platform:security:self" : null
1679
+ };
1680
+ const token = await injectedToken(options, tokenRequest);
1681
+ const snapshot = await (0, import_node.snapshotDirectory)(target, {
1682
+ exclude: !outputRelative.startsWith("../") && !(0, import_node_path6.isAbsolute)(outputRelative) ? [outputRelative] : []
1683
+ });
1684
+ const hosted = await (0, import_security.createPlatformSecurityReasoners)({
1685
+ platform,
1686
+ token,
1687
+ appId,
1688
+ env,
1689
+ repository: snapshot.repository,
1690
+ revision: snapshot.revision,
1691
+ snapshotDigest: snapshot.digest,
1692
+ sourceDisclosure: "redacted",
1693
+ clientRunId: options.runId ?? crypto.randomUUID(),
1694
+ ...selfAudit ? { selfAudit: { enabled: true, subject: "service:odla-security-self-audit" } } : {},
1695
+ fetch: options.fetch,
1696
+ signal: options.signal
1697
+ });
1698
+ const harness = (0, import_security.createSecurityHarness)({
1699
+ profile,
1700
+ store: new import_node.FileRunStore((0, import_node_path6.resolve)(output, "state")),
1701
+ discoveryReasoner: hosted.discoveryReasoner,
1702
+ validationReasoner: hosted.validationReasoner,
1703
+ policy: {
1704
+ modelSourceDisclosure: "redacted",
1705
+ active: false,
1706
+ allowNetwork: false
1707
+ }
1708
+ });
1709
+ const report = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
1710
+ await (0, import_node.writeSecurityArtifacts)(output, report);
1711
+ const reportDigest = await (0, import_security.securityFingerprint)(report);
1712
+ await hosted.complete({
1713
+ reportDigest,
1714
+ coverageStatus: report.coverageStatus,
1715
+ confirmed: report.metrics.confirmed,
1716
+ candidates: report.metrics.candidates
1717
+ }, { signal: options.signal });
1718
+ printSummary(options.stdout ?? console, appId, env, hosted.run, report, output);
1719
+ return Object.freeze({ report, run: hosted.run, output });
1720
+ }
1721
+ function selectEnv(requested, declared, configPath, rootDir) {
1722
+ const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
1723
+ if (!env || !declared.includes(env)) {
1724
+ const shown = (0, import_node_path6.relative)(rootDir, configPath) || configPath;
1725
+ throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
1726
+ }
1727
+ return env;
1728
+ }
1729
+ async function injectedToken(options, request) {
1730
+ const value = options.token ?? await options.getToken?.(Object.freeze({ ...request }));
1731
+ if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
1732
+ throw new Error(
1733
+ request.selfAudit ? "Self-audit requires an injected, scoped platform security token" : "Hosted security requires an injected app developer token or getToken callback"
1734
+ );
1735
+ }
1736
+ return value;
1737
+ }
1738
+ function profileFor(name, maxHuntTasks) {
1739
+ const profile = name === "odla" ? (0, import_security.odlaProfile)() : name === "cloudflare-app" ? (0, import_security.cloudflareAppProfile)() : name === "generic" ? (0, import_security.genericProfile)() : void 0;
1740
+ if (!profile) throw new Error(`unknown security profile "${String(name)}"`);
1741
+ if (maxHuntTasks === void 0) return profile;
1742
+ if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
1743
+ return { ...profile, maxHuntTasks };
1744
+ }
1745
+ function printSummary(out, appId, env, run, report, output) {
1746
+ const complete = report.coverage.filter((cell) => cell.state === "complete").length;
1747
+ out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
1748
+ out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
1749
+ out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
1750
+ 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}`);
1751
+ if (report.callBudget) out.log(` calls: discovery=${formatBudget(report.callBudget.discovery)} validation=${formatBudget(report.callBudget.validation)}`);
1752
+ out.log(` findings: confirmed=${report.metrics.confirmed} needs_reproduction=${report.metrics.needsReproduction} candidates=${report.metrics.candidates}`);
1753
+ out.log(` report: ${(0, import_node_path6.resolve)(output, "REPORT.md")}`);
1754
+ }
1755
+ function formatBudget(usage) {
1756
+ return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
1757
+ }
1758
+
1759
+ // src/help.ts
1760
+ var import_node_fs7 = require("fs");
1761
+ function cliVersion() {
1762
+ const pkg = JSON.parse((0, import_node_fs7.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
1763
+ return pkg.version ?? "unknown";
1764
+ }
1765
+ function printHelp() {
1766
+ console.log(`odla-ai
1767
+
1768
+ Usage:
1769
+ odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
1770
+ odla-ai init --app-id <id> --name <name> [--services db,ai,o11y] [--env dev --env prod]
1771
+ odla-ai doctor [--config odla.config.mjs]
1772
+ odla-ai capabilities [--json]
1773
+ odla-ai admin ai show [--platform https://odla.ai] [--json]
1774
+ odla-ai admin ai models [--provider <id>] [--json]
1775
+ odla-ai admin ai set <purpose> [--provider <id>] [--model <id>] [--enabled|--no-enabled]
1776
+ odla-ai admin ai set security --discovery-provider <id> --discovery-model <id>
1777
+ --validation-provider <id> --validation-model <id> [--enabled|--no-enabled]
1778
+ odla-ai admin ai credentials [--json]
1779
+ odla-ai admin ai credential set <provider> (--from-env <NAME>|--stdin)
1780
+ odla-ai admin ai usage [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
1781
+ odla-ai admin ai audit [--limit <1-200>] [--json]
1782
+ odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
1783
+ odla-ai security run [target] --self --ack-redacted-source
1784
+ odla-ai provision [--config odla.config.mjs] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
1785
+ odla-ai smoke [--config odla.config.mjs] [--env dev]
1786
+ odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
1787
+ odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
1788
+ odla-ai version
1789
+
1790
+ Commands:
1791
+ setup Install offline odla runbooks for common coding-agent harnesses.
1792
+ init Create a generic odla.config.mjs plus starter schema/rules files.
1793
+ doctor Validate and summarize the project config without network calls.
1794
+ capabilities Show what the CLI automates vs agent edits and human checkpoints.
1795
+ admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
1796
+ security Run hosted discovery + independent validation with app/run attribution.
1797
+ provision Register services, configure them, persist credentials, optionally push secrets.
1798
+ smoke Verify local credentials, public-config, live schema, and db aggregate.
1799
+ skill Same installer; --agent accepts all, claude, codex, cursor,
1800
+ copilot, gemini, or agents (repeatable or comma-separated).
1801
+ secrets Push configured db/o11y secrets into the Worker via wrangler stdin.
1802
+ version Print the CLI version.
1803
+
1804
+ Safety:
1805
+ New projects target dev only. Add prod explicitly to odla.config.mjs and pass
1806
+ --yes to provision it; use --dry-run first to inspect the resolved plan.
1807
+ Provision caches the approved developer token and service credentials under
1808
+ .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
1809
+ preflights Wrangler before any shown-once issuance or destructive rotation.
1810
+ Provision opens the approval page automatically in interactive terminals;
1811
+ use --open to force browser launch or --no-open to suppress it.
1812
+ `);
1813
+ }
1814
+
1137
1815
  // src/skill.ts
1138
- var import_node_fs6 = require("fs");
1816
+ var import_node_fs8 = require("fs");
1139
1817
  var import_node_os = require("os");
1140
- var import_node_path6 = require("path");
1818
+ var import_node_path7 = require("path");
1141
1819
  var import_node_url2 = require("url");
1142
1820
 
1143
1821
  // src/skill-adapters.ts
@@ -1198,8 +1876,8 @@ function installSkill(options = {}) {
1198
1876
  const files = listFiles(sourceDir);
1199
1877
  if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
1200
1878
  const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
1201
- const root = (0, import_node_path6.resolve)(options.dir ?? process.cwd());
1202
- const home = (0, import_node_path6.resolve)(options.homeDir ?? (0, import_node_os.homedir)());
1879
+ const root = (0, import_node_path7.resolve)(options.dir ?? process.cwd());
1880
+ const home = (0, import_node_path7.resolve)(options.homeDir ?? (0, import_node_os.homedir)());
1203
1881
  const plans = /* @__PURE__ */ new Map();
1204
1882
  const targets = /* @__PURE__ */ new Map();
1205
1883
  const rememberTarget = (harness, target) => {
@@ -1213,48 +1891,48 @@ function installSkill(options = {}) {
1213
1891
  plans.set(target, { target, content, boundary, managedMerge });
1214
1892
  };
1215
1893
  const planSkillTree = (targetDir2, boundary = root) => {
1216
- for (const rel of files) plan((0, import_node_path6.join)(targetDir2, rel), (0, import_node_fs6.readFileSync)((0, import_node_path6.join)(sourceDir, rel), "utf8"), false, boundary);
1894
+ for (const rel of files) plan((0, import_node_path7.join)(targetDir2, rel), (0, import_node_fs8.readFileSync)((0, import_node_path7.join)(sourceDir, rel), "utf8"), false, boundary);
1217
1895
  };
1218
1896
  let targetDir;
1219
1897
  if (options.global) {
1220
- const claudeRoot = (0, import_node_path6.join)(home, ".claude", "skills");
1221
- const codexRoot = (0, import_node_path6.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path6.join)(home, ".codex"), "skills");
1898
+ const claudeRoot = (0, import_node_path7.join)(home, ".claude", "skills");
1899
+ const codexRoot = (0, import_node_path7.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path7.join)(home, ".codex"), "skills");
1222
1900
  targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
1223
1901
  for (const harness of harnesses) {
1224
1902
  const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
1225
- planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path6.dirname)((0, import_node_path6.dirname)(codexRoot)));
1903
+ planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path7.dirname)((0, import_node_path7.dirname)(codexRoot)));
1226
1904
  rememberTarget(harness, skillRoot);
1227
1905
  }
1228
1906
  } else {
1229
- const sharedRoot = (0, import_node_path6.join)(root, ".agents", "skills");
1907
+ const sharedRoot = (0, import_node_path7.join)(root, ".agents", "skills");
1230
1908
  planSkillTree(sharedRoot);
1231
- const claudeRoot = (0, import_node_path6.join)(root, ".claude", "skills");
1909
+ const claudeRoot = (0, import_node_path7.join)(root, ".claude", "skills");
1232
1910
  targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
1233
1911
  for (const harness of harnesses) rememberTarget(harness, sharedRoot);
1234
1912
  if (harnesses.includes("claude")) {
1235
1913
  for (const skill of skillNames(files)) {
1236
- const canonical = (0, import_node_fs6.readFileSync)((0, import_node_path6.join)(sourceDir, skill, "SKILL.md"), "utf8");
1237
- plan((0, import_node_path6.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
1914
+ const canonical = (0, import_node_fs8.readFileSync)((0, import_node_path7.join)(sourceDir, skill, "SKILL.md"), "utf8");
1915
+ plan((0, import_node_path7.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
1238
1916
  }
1239
1917
  rememberTarget("claude", claudeRoot);
1240
1918
  }
1241
1919
  if (harnesses.includes("cursor")) {
1242
- const cursorRule = (0, import_node_path6.join)(root, ".cursor", "rules", "odla.mdc");
1920
+ const cursorRule = (0, import_node_path7.join)(root, ".cursor", "rules", "odla.mdc");
1243
1921
  plan(cursorRule, CURSOR_RULE);
1244
1922
  rememberTarget("cursor", cursorRule);
1245
1923
  }
1246
1924
  if (harnesses.includes("agents")) {
1247
- const agentsFile = (0, import_node_path6.join)(root, "AGENTS.md");
1925
+ const agentsFile = (0, import_node_path7.join)(root, "AGENTS.md");
1248
1926
  plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
1249
1927
  rememberTarget("agents", agentsFile);
1250
1928
  }
1251
1929
  if (harnesses.includes("copilot")) {
1252
- const copilotFile = (0, import_node_path6.join)(root, ".github", "copilot-instructions.md");
1930
+ const copilotFile = (0, import_node_path7.join)(root, ".github", "copilot-instructions.md");
1253
1931
  plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
1254
1932
  rememberTarget("copilot", copilotFile);
1255
1933
  }
1256
1934
  if (harnesses.includes("gemini")) {
1257
- const geminiFile = (0, import_node_path6.join)(root, "GEMINI.md");
1935
+ const geminiFile = (0, import_node_path7.join)(root, "GEMINI.md");
1258
1936
  plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
1259
1937
  rememberTarget("gemini", geminiFile);
1260
1938
  }
@@ -1268,11 +1946,11 @@ function installSkill(options = {}) {
1268
1946
  conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
1269
1947
  continue;
1270
1948
  }
1271
- if (!(0, import_node_fs6.existsSync)(file.target)) {
1949
+ if (!(0, import_node_fs8.existsSync)(file.target)) {
1272
1950
  writtenPaths.add(file.target);
1273
1951
  continue;
1274
1952
  }
1275
- const current = (0, import_node_fs6.readFileSync)(file.target, "utf8");
1953
+ const current = (0, import_node_fs8.readFileSync)(file.target, "utf8");
1276
1954
  if (current === file.content) {
1277
1955
  unchangedPaths.add(file.target);
1278
1956
  } else if (file.managedMerge || options.force) {
@@ -1289,9 +1967,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
1289
1967
  );
1290
1968
  }
1291
1969
  for (const file of plans.values()) {
1292
- if (!(0, import_node_fs6.existsSync)(file.target) || (0, import_node_fs6.readFileSync)(file.target, "utf8") !== file.content) {
1293
- (0, import_node_fs6.mkdirSync)((0, import_node_path6.dirname)(file.target), { recursive: true });
1294
- (0, import_node_fs6.writeFileSync)(file.target, file.content);
1970
+ if (!(0, import_node_fs8.existsSync)(file.target) || (0, import_node_fs8.readFileSync)(file.target, "utf8") !== file.content) {
1971
+ (0, import_node_fs8.mkdirSync)((0, import_node_path7.dirname)(file.target), { recursive: true });
1972
+ (0, import_node_fs8.writeFileSync)(file.target, file.content);
1295
1973
  }
1296
1974
  }
1297
1975
  const skills = skillNames(files);
@@ -1310,7 +1988,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
1310
1988
  };
1311
1989
  }
1312
1990
  function pathsUnder(root, paths) {
1313
- return [...paths].map((path) => (0, import_node_path6.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path6.sep}`) && !(0, import_node_path6.isAbsolute)(path)).sort();
1991
+ return [...paths].map((path) => (0, import_node_path7.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path7.sep}`) && !(0, import_node_path7.isAbsolute)(path)).sort();
1314
1992
  }
1315
1993
  function normalizeHarnesses(values, global) {
1316
1994
  const requested = values?.length ? values : ["claude"];
@@ -1332,9 +2010,9 @@ function normalizeHarnesses(values, global) {
1332
2010
  function managedFileContent(path, block, force, boundary) {
1333
2011
  const symlink = symlinkedComponent(boundary, path);
1334
2012
  if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
1335
- if (!(0, import_node_fs6.existsSync)(path)) return `${block}
2013
+ if (!(0, import_node_fs8.existsSync)(path)) return `${block}
1336
2014
  `;
1337
- const current = (0, import_node_fs6.readFileSync)(path, "utf8");
2015
+ const current = (0, import_node_fs8.readFileSync)(path, "utf8");
1338
2016
  const start = "<!-- odla-ai agent setup:start -->";
1339
2017
  const end = "<!-- odla-ai agent setup:end -->";
1340
2018
  const startAt = current.indexOf(start);
@@ -1355,15 +2033,15 @@ function managedFileContent(path, block, force, boundary) {
1355
2033
  return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
1356
2034
  }
1357
2035
  function symlinkedComponent(boundary, target) {
1358
- const rel = (0, import_node_path6.relative)(boundary, target);
1359
- if (rel === ".." || rel.startsWith(`..${import_node_path6.sep}`) || (0, import_node_path6.isAbsolute)(rel)) {
2036
+ const rel = (0, import_node_path7.relative)(boundary, target);
2037
+ if (rel === ".." || rel.startsWith(`..${import_node_path7.sep}`) || (0, import_node_path7.isAbsolute)(rel)) {
1360
2038
  throw new Error(`agent setup target escapes its install root: ${target}`);
1361
2039
  }
1362
2040
  let current = boundary;
1363
- for (const part of rel.split(import_node_path6.sep).filter(Boolean)) {
1364
- current = (0, import_node_path6.join)(current, part);
2041
+ for (const part of rel.split(import_node_path7.sep).filter(Boolean)) {
2042
+ current = (0, import_node_path7.join)(current, part);
1365
2043
  try {
1366
- if ((0, import_node_fs6.lstatSync)(current).isSymbolicLink()) return current;
2044
+ if ((0, import_node_fs8.lstatSync)(current).isSymbolicLink()) return current;
1367
2045
  } catch (error) {
1368
2046
  if (error.code !== "ENOENT") throw error;
1369
2047
  }
@@ -1374,13 +2052,13 @@ function skillNames(files) {
1374
2052
  return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
1375
2053
  }
1376
2054
  function listFiles(dir) {
1377
- if (!(0, import_node_fs6.existsSync)(dir)) return [];
2055
+ if (!(0, import_node_fs8.existsSync)(dir)) return [];
1378
2056
  const results = [];
1379
2057
  const walk = (current) => {
1380
- for (const entry of (0, import_node_fs6.readdirSync)(current, { withFileTypes: true })) {
1381
- const path = (0, import_node_path6.join)(current, entry.name);
2058
+ for (const entry of (0, import_node_fs8.readdirSync)(current, { withFileTypes: true })) {
2059
+ const path = (0, import_node_path7.join)(current, entry.name);
1382
2060
  if (entry.isDirectory()) walk(path);
1383
- else results.push((0, import_node_path6.relative)(dir, path));
2061
+ else results.push((0, import_node_path7.relative)(dir, path));
1384
2062
  }
1385
2063
  };
1386
2064
  walk(dir);
@@ -1505,6 +2183,119 @@ async function runCli(argv = process.argv.slice(2)) {
1505
2183
  printCapabilities(parsed.options.json === true);
1506
2184
  return;
1507
2185
  }
2186
+ if (command === "admin") {
2187
+ const area = parsed.positionals[1];
2188
+ const action = parsed.positionals[2];
2189
+ const credentialSet = action === "credential" && parsed.positionals[3] === "set";
2190
+ const credentials = action === "credentials";
2191
+ const models = action === "models";
2192
+ const usage = action === "usage";
2193
+ const audit = action === "audit";
2194
+ if (area !== "ai" || action !== "show" && action !== "set" && !credentialSet && !credentials && !models && !usage && !audit) {
2195
+ throw new Error('unknown admin command. Try "odla-ai admin ai show".');
2196
+ }
2197
+ assertArgs(parsed, [
2198
+ "platform",
2199
+ "json",
2200
+ "open",
2201
+ "provider",
2202
+ "model",
2203
+ "enabled",
2204
+ "max-input-bytes",
2205
+ "max-output-tokens",
2206
+ "max-calls-per-run",
2207
+ "from-env",
2208
+ "stdin",
2209
+ "discovery-provider",
2210
+ "discovery-model",
2211
+ "validation-provider",
2212
+ "validation-model",
2213
+ "app-id",
2214
+ "env",
2215
+ "run-id",
2216
+ "limit"
2217
+ ], credentialSet ? 5 : action === "set" ? 4 : 3);
2218
+ await adminAi({
2219
+ action: credentialSet ? "credential-set" : credentials ? "credentials" : models ? "models" : usage ? "usage" : audit ? "audit" : action,
2220
+ purpose: action === "set" ? parsed.positionals[3] : void 0,
2221
+ provider: stringOpt(parsed.options.provider),
2222
+ model: stringOpt(parsed.options.model),
2223
+ enabled: boolOpt(parsed.options.enabled),
2224
+ maxInputBytes: numberOpt(parsed.options["max-input-bytes"], "--max-input-bytes"),
2225
+ maxOutputTokens: numberOpt(parsed.options["max-output-tokens"], "--max-output-tokens"),
2226
+ maxCallsPerRun: numberOpt(parsed.options["max-calls-per-run"], "--max-calls-per-run"),
2227
+ platform: stringOpt(parsed.options.platform),
2228
+ json: parsed.options.json === true,
2229
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
2230
+ credentialProvider: credentialSet ? parsed.positionals[4] : void 0,
2231
+ fromEnv: stringOpt(parsed.options["from-env"]),
2232
+ stdin: parsed.options.stdin === true,
2233
+ discoveryProvider: stringOpt(parsed.options["discovery-provider"]),
2234
+ discoveryModel: stringOpt(parsed.options["discovery-model"]),
2235
+ validationProvider: stringOpt(parsed.options["validation-provider"]),
2236
+ validationModel: stringOpt(parsed.options["validation-model"]),
2237
+ appId: stringOpt(parsed.options["app-id"]),
2238
+ env: stringOpt(parsed.options.env),
2239
+ runId: stringOpt(parsed.options["run-id"]),
2240
+ limit: numberOpt(parsed.options.limit, "--limit")
2241
+ });
2242
+ return;
2243
+ }
2244
+ if (command === "security") {
2245
+ const sub = parsed.positionals[1];
2246
+ if (sub !== "run") throw new Error('unknown security command. Try "odla-ai security run".');
2247
+ assertArgs(parsed, [
2248
+ "config",
2249
+ "env",
2250
+ "out",
2251
+ "profile",
2252
+ "max-hunt-tasks",
2253
+ "run-id",
2254
+ "platform",
2255
+ "self",
2256
+ "ack-redacted-source",
2257
+ "open",
2258
+ "fail-on",
2259
+ "fail-on-candidates",
2260
+ "allow-incomplete"
2261
+ ], 3);
2262
+ const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
2263
+ const selfAudit = parsed.options.self === true;
2264
+ const open = parsed.options.open === false ? false : parsed.options.open === true ? true : void 0;
2265
+ const platform = stringOpt(parsed.options.platform);
2266
+ const result = await runHostedSecurity({
2267
+ configPath,
2268
+ selfAudit,
2269
+ target: parsed.positionals[2],
2270
+ out: stringOpt(parsed.options.out),
2271
+ env: stringOpt(parsed.options.env),
2272
+ profile: securityProfile(stringOpt(parsed.options.profile)),
2273
+ maxHuntTasks: numberOpt(parsed.options["max-hunt-tasks"], "--max-hunt-tasks"),
2274
+ runId: stringOpt(parsed.options["run-id"]),
2275
+ platform,
2276
+ sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
2277
+ getToken: async (request) => {
2278
+ if (request.scope === "platform:security:self") {
2279
+ return getScopedPlatformToken({ platform: request.platform, scope: request.scope, open });
2280
+ }
2281
+ const cfg = await loadProjectConfig(configPath);
2282
+ if (platformAudience(cfg.platformUrl) !== platformAudience(request.platform)) {
2283
+ throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
2284
+ }
2285
+ return getDeveloperToken(cfg, { configPath, open }, fetch, console);
2286
+ }
2287
+ });
2288
+ const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
2289
+ const candidateValue = parsed.options["fail-on-candidates"];
2290
+ const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
2291
+ const confirmed = (0, import_security2.findingsAtOrAbove)(result.report, failOn);
2292
+ const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(result.report, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
2293
+ const incomplete = result.report.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
2294
+ if (confirmed.length || leads.length || incomplete) {
2295
+ throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
2296
+ }
2297
+ return;
2298
+ }
1508
2299
  if (command === "provision") {
1509
2300
  assertArgs(
1510
2301
  parsed,
@@ -1585,71 +2376,14 @@ async function runCli(argv = process.argv.slice(2)) {
1585
2376
  }
1586
2377
  throw new Error(`unknown command "${command}". Run "odla-ai help".`);
1587
2378
  }
1588
- function assertArgs(parsed, allowedOptions, maxPositionals) {
1589
- const allowed = new Set(allowedOptions);
1590
- for (const name of Object.keys(parsed.options)) {
1591
- if (!allowed.has(name)) {
1592
- throw new Error(`unknown option "--${name}"; run "odla-ai help" for supported options`);
1593
- }
1594
- }
1595
- if (parsed.positionals.length > maxPositionals) {
1596
- throw new Error(`unexpected argument "${parsed.positionals[maxPositionals]}"; run "odla-ai help"`);
1597
- }
1598
- }
1599
- function parseArgv(argv) {
1600
- const positionals = [];
1601
- const options = {};
1602
- for (let i = 0; i < argv.length; i++) {
1603
- const arg = argv[i];
1604
- if (!arg.startsWith("--")) {
1605
- positionals.push(arg);
1606
- continue;
1607
- }
1608
- const eq = arg.indexOf("=");
1609
- const rawName = arg.slice(2, eq === -1 ? void 0 : eq);
1610
- if (!rawName) continue;
1611
- if (rawName.startsWith("no-")) {
1612
- options[rawName.slice(3)] = false;
1613
- continue;
1614
- }
1615
- if (eq !== -1) {
1616
- addOption(options, rawName, arg.slice(eq + 1));
1617
- continue;
1618
- }
1619
- const value = argv[i + 1];
1620
- if (value !== void 0 && !value.startsWith("--")) {
1621
- i++;
1622
- addOption(options, rawName, value);
1623
- } else {
1624
- addOption(options, rawName, true);
1625
- }
1626
- }
1627
- return { positionals, options };
1628
- }
1629
- function addOption(options, name, value) {
1630
- const cur = options[name];
1631
- if (cur === void 0) {
1632
- options[name] = value;
1633
- } else if (Array.isArray(cur)) {
1634
- cur.push(String(value));
1635
- } else {
1636
- options[name] = [String(cur), String(value)];
1637
- }
1638
- }
1639
- function requiredString(value, name) {
1640
- const s = stringOpt(value);
1641
- if (!s) throw new Error(`${name} is required`);
1642
- return s;
1643
- }
1644
- function stringOpt(value) {
1645
- if (typeof value === "string") return value;
1646
- if (Array.isArray(value)) return value[value.length - 1];
1647
- return void 0;
2379
+ function securityProfile(value) {
2380
+ if (value === void 0) return void 0;
2381
+ if (value === "odla" || value === "cloudflare-app" || value === "generic") return value;
2382
+ throw new Error("--profile must be odla, cloudflare-app, or generic");
1648
2383
  }
1649
- function listOpt(value) {
1650
- if (value === void 0 || value === false || value === true) return void 0;
1651
- const values = Array.isArray(value) ? value : [value];
1652
- return values.flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
2384
+ function severityOpt(value, flag) {
2385
+ if (["informational", "low", "medium", "high", "critical"].includes(value)) return value;
2386
+ throw new Error(`${flag} must be informational, low, medium, high, or critical`);
1653
2387
  }
1654
2388
  function harnessList(parsed) {
1655
2389
  const selected = [
@@ -1666,46 +2400,6 @@ function harnessOption(value, flag) {
1666
2400
  if (parts.some((item) => !item.trim())) throw new Error(`${flag} requires a harness name`);
1667
2401
  return parts.map((item) => item.trim());
1668
2402
  }
1669
- function cliVersion() {
1670
- const pkg = JSON.parse((0, import_node_fs7.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
1671
- return pkg.version ?? "unknown";
1672
- }
1673
- function printHelp() {
1674
- console.log(`odla-ai
1675
-
1676
- Usage:
1677
- odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
1678
- odla-ai init --app-id <id> --name <name> [--services db,ai,o11y] [--env dev --env prod]
1679
- odla-ai doctor [--config odla.config.mjs]
1680
- odla-ai capabilities [--json]
1681
- odla-ai provision [--config odla.config.mjs] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
1682
- odla-ai smoke [--config odla.config.mjs] [--env dev]
1683
- odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
1684
- odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
1685
- odla-ai version
1686
-
1687
- Commands:
1688
- setup Install offline odla runbooks for common coding-agent harnesses.
1689
- init Create a generic odla.config.mjs plus starter schema/rules files.
1690
- doctor Validate and summarize the project config without network calls.
1691
- capabilities Show what the CLI automates vs agent edits and human checkpoints.
1692
- provision Register services, configure them, persist credentials, optionally push secrets.
1693
- smoke Verify local credentials, public-config, live schema, and db aggregate.
1694
- skill Same installer; --agent accepts all, claude, codex, cursor,
1695
- copilot, gemini, or agents (repeatable or comma-separated).
1696
- secrets Push configured db/o11y secrets into the Worker via wrangler stdin.
1697
- version Print the CLI version.
1698
-
1699
- Safety:
1700
- New projects target dev only. Add prod explicitly to odla.config.mjs and pass
1701
- --yes to provision it; use --dry-run first to inspect the resolved plan.
1702
- Provision caches the approved developer token and service credentials under
1703
- .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
1704
- preflights Wrangler before any shown-once issuance or destructive rotation.
1705
- Provision opens the approval page automatically in interactive terminals;
1706
- use --open to force browser launch or --no-open to suppress it.
1707
- `);
1708
- }
1709
2403
 
1710
2404
  // src/bin.ts
1711
2405
  runCli().catch((err) => {