@odla-ai/cli 0.5.0 → 0.6.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/index.cjs CHANGED
@@ -33,13 +33,17 @@ var index_exports = {};
33
33
  __export(index_exports, {
34
34
  AGENT_HARNESSES: () => AGENT_HARNESSES,
35
35
  CAPABILITIES: () => CAPABILITIES,
36
+ SYSTEM_AI_PURPOSES: () => SYSTEM_AI_PURPOSES,
37
+ adminAi: () => adminAi,
36
38
  doctor: () => doctor,
39
+ getScopedPlatformToken: () => getScopedPlatformToken,
37
40
  initProject: () => initProject,
38
41
  installSkill: () => installSkill,
39
42
  printCapabilities: () => printCapabilities,
40
43
  provision: () => provision,
41
44
  redactSecrets: () => redactSecrets,
42
45
  runCli: () => runCli,
46
+ runHostedSecurity: () => runHostedSecurity,
43
47
  secretsPush: () => secretsPush,
44
48
  smoke: () => smoke
45
49
  });
@@ -50,7 +54,459 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
50
54
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
51
55
 
52
56
  // src/cli.ts
53
- var import_node_fs7 = require("fs");
57
+ var import_security2 = require("@odla-ai/security");
58
+
59
+ // src/admin-ai.ts
60
+ var import_node_process3 = __toESM(require("process"), 1);
61
+ var import_node_fs2 = require("fs");
62
+ var import_db2 = require("@odla-ai/db");
63
+
64
+ // src/open.ts
65
+ var import_node_child_process = require("child_process");
66
+ var import_node_process = __toESM(require("process"), 1);
67
+ async function openUrl(url, options = {}) {
68
+ const command = openerFor(options.platform ?? import_node_process.default.platform);
69
+ const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
70
+ await new Promise((resolve7, reject) => {
71
+ const child = doSpawn(command.cmd, [...command.args, url], {
72
+ stdio: "ignore",
73
+ detached: true
74
+ });
75
+ child.once("error", reject);
76
+ child.once("spawn", () => {
77
+ child.unref();
78
+ resolve7();
79
+ });
80
+ });
81
+ }
82
+ function openerFor(platform) {
83
+ if (platform === "darwin") return { cmd: "open", args: [] };
84
+ if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
85
+ return { cmd: "xdg-open", args: [] };
86
+ }
87
+
88
+ // src/token.ts
89
+ var import_db = require("@odla-ai/db");
90
+ var import_node_process2 = __toESM(require("process"), 1);
91
+
92
+ // src/local.ts
93
+ var import_node_fs = require("fs");
94
+ var import_node_path = require("path");
95
+ var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
96
+ function readJsonFile(path) {
97
+ try {
98
+ return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+ function writePrivateJson(path, value) {
104
+ writePrivateText(path, `${JSON.stringify(value, null, 2)}
105
+ `);
106
+ }
107
+ function readCredentials(path) {
108
+ if (!(0, import_node_fs.existsSync)(path)) return null;
109
+ let value;
110
+ try {
111
+ value = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
112
+ } catch {
113
+ throw new Error(`credentials file ${path} is not valid JSON; fix or remove it before provisioning`);
114
+ }
115
+ if (!value || typeof value !== "object" || typeof value.appId !== "string" || !value.envs || typeof value.envs !== "object") {
116
+ throw new Error(`credentials file ${path} has an invalid shape; fix or remove it before provisioning`);
117
+ }
118
+ return value;
119
+ }
120
+ function mergeCredential(current, update) {
121
+ const next = current ?? {
122
+ appId: update.appId,
123
+ platformUrl: update.platformUrl,
124
+ dbEndpoint: update.dbEndpoint,
125
+ updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
126
+ envs: {}
127
+ };
128
+ next.appId = update.appId;
129
+ next.platformUrl = update.platformUrl;
130
+ next.dbEndpoint = update.dbEndpoint;
131
+ next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
132
+ next.envs[update.env] = {
133
+ ...next.envs[update.env] ?? {},
134
+ tenantId: update.tenantId,
135
+ ...update.dbKey ? { dbKey: update.dbKey } : {},
136
+ ...update.o11yToken ? { o11yToken: update.o11yToken } : {}
137
+ };
138
+ return next;
139
+ }
140
+ function ensureGitignore(rootDir, localPaths = []) {
141
+ const path = (0, import_node_path.resolve)(rootDir, ".gitignore");
142
+ const existing = (0, import_node_fs.existsSync)(path) ? (0, import_node_fs.readFileSync)(path, "utf8") : "";
143
+ const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line) => !!line);
144
+ const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
145
+ const missing = wanted.filter((line) => !existing.split(/\r?\n/).includes(line));
146
+ if (missing.length === 0) return;
147
+ const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
148
+ (0, import_node_fs.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
149
+ `);
150
+ }
151
+ function o11yDevVars(cfg) {
152
+ if (!cfg.services.includes("o11y")) return void 0;
153
+ return {
154
+ endpoint: cfg.o11y?.endpoint ?? "https://o11y.odla.ai",
155
+ service: cfg.o11y?.service ?? cfg.app.id,
156
+ ...cfg.o11y?.version ? { version: cfg.o11y.version } : {}
157
+ };
158
+ }
159
+ function resolveWriteDevVarsTarget(cfg, requested) {
160
+ if (!requested) return null;
161
+ if (requested === true) return cfg.local.devVarsFile;
162
+ return (0, import_node_path.resolve)((0, import_node_path.dirname)(cfg.configPath), requested);
163
+ }
164
+ function writeDevVars(path, credentials, env, o11y) {
165
+ const entry = credentials.envs[env];
166
+ if (!entry) throw new Error(`no credentials for env "${env}" in ${path}`);
167
+ const lines = [`ODLA_PLATFORM="${credentials.platformUrl}"`];
168
+ if (entry.dbKey) lines.push(`ODLA_ENDPOINT="${credentials.dbEndpoint}"`);
169
+ lines.push(`ODLA_APP_ID="${credentials.appId}"`, `ODLA_ENV="${env}"`, `ODLA_TENANT="${entry.tenantId}"`);
170
+ if (entry.dbKey) lines.push(`ODLA_API_KEY="${entry.dbKey}"`);
171
+ if (o11y) {
172
+ lines.push(`ODLA_O11Y_ENDPOINT="${o11y.endpoint}"`, `ODLA_O11Y_SERVICE="${o11y.service}"`);
173
+ if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
174
+ if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
175
+ }
176
+ const existing = (0, import_node_fs.existsSync)(path) ? (0, import_node_fs.readFileSync)(path, "utf8") : "";
177
+ const retained = existing.split(/\r?\n/).filter((line) => !isManagedDevVar(line));
178
+ while (retained.at(-1) === "") retained.pop();
179
+ const prefix = retained.length ? `${retained.join("\n")}
180
+
181
+ ` : "";
182
+ writePrivateText(path, `${prefix}${lines.join("\n")}
183
+ `);
184
+ }
185
+ var MANAGED_DEV_VARS = /* @__PURE__ */ new Set([
186
+ "ODLA_PLATFORM",
187
+ "ODLA_ENDPOINT",
188
+ "ODLA_APP_ID",
189
+ "ODLA_ENV",
190
+ "ODLA_TENANT",
191
+ "ODLA_API_KEY",
192
+ "ODLA_O11Y_ENDPOINT",
193
+ "ODLA_O11Y_SERVICE",
194
+ "ODLA_O11Y_VERSION",
195
+ "ODLA_O11Y_TOKEN"
196
+ ]);
197
+ function isManagedDevVar(line) {
198
+ const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
199
+ return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
200
+ }
201
+ function writePrivateText(path, text) {
202
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
203
+ const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
204
+ (0, import_node_fs.writeFileSync)(temporary, text, { mode: 384 });
205
+ (0, import_node_fs.chmodSync)(temporary, 384);
206
+ (0, import_node_fs.renameSync)(temporary, path);
207
+ }
208
+ function gitignoreEntry(rootDir, path) {
209
+ const rel = (0, import_node_path.relative)((0, import_node_path.resolve)(rootDir), (0, import_node_path.resolve)(path));
210
+ if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path.isAbsolute)(rel)) return null;
211
+ return rel.replaceAll("\\", "/");
212
+ }
213
+ function displayPath(path, rootDir = process.cwd()) {
214
+ const rel = (0, import_node_path.relative)(rootDir, path);
215
+ return rel && !rel.startsWith("..") ? rel : path;
216
+ }
217
+
218
+ // src/token.ts
219
+ async function getDeveloperToken(cfg, options, doFetch, out) {
220
+ if (options.token) return options.token;
221
+ if (import_node_process2.default.env.ODLA_DEV_TOKEN) return import_node_process2.default.env.ODLA_DEV_TOKEN;
222
+ const cached = readJsonFile(cfg.local.tokenFile);
223
+ if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
224
+ out.log(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
225
+ return cached.token;
226
+ }
227
+ const browser = approvalBrowser(options);
228
+ const { token, expiresAt } = await (0, import_db.requestToken)({
229
+ endpoint: cfg.platformUrl,
230
+ label: `${cfg.app.id} provisioner`,
231
+ fetch: doFetch,
232
+ onCode: async ({ userCode, expiresIn }) => {
233
+ const approvalUrl = handshakeUrl(cfg.platformUrl, userCode);
234
+ out.log("");
235
+ out.log(`Approve code ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
236
+ if (browser.open) {
237
+ try {
238
+ await (options.openApprovalUrl ?? openUrl)(approvalUrl);
239
+ out.log(`auth: opened browser for approval${browser.mode === "auto" ? " (auto)" : ""}`);
240
+ } catch (err) {
241
+ out.log(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
242
+ }
243
+ } else if (browser.reason) {
244
+ out.log(`auth: browser launch skipped (${browser.reason})`);
245
+ }
246
+ out.log("");
247
+ }
248
+ });
249
+ writePrivateJson(cfg.local.tokenFile, { token, expiresAt });
250
+ out.log(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
251
+ return token;
252
+ }
253
+ function approvalBrowser(options) {
254
+ if (options.open === true) return { open: true, mode: "forced" };
255
+ if (options.open === false) return { open: false, reason: "disabled by --no-open" };
256
+ if (import_node_process2.default.env.CI) return { open: false, reason: "CI environment" };
257
+ const interactive = options.interactive ?? Boolean(import_node_process2.default.stdin.isTTY && import_node_process2.default.stdout.isTTY);
258
+ if (!interactive) return { open: false, reason: "non-interactive shell; pass --open to force" };
259
+ return { open: true, mode: "auto" };
260
+ }
261
+ function handshakeUrl(platformUrl, userCode) {
262
+ const url = new URL("/studio", platformUrl);
263
+ url.searchParams.set("code", userCode);
264
+ return url.toString();
265
+ }
266
+
267
+ // src/admin-ai.ts
268
+ var SYSTEM_AI_PURPOSES = ["o11y.triage", "security.discovery", "security.validation"];
269
+ async function getScopedPlatformToken(options) {
270
+ const fromEnv = import_node_process3.default.env.ODLA_ADMIN_TOKEN;
271
+ if (fromEnv) return fromEnv;
272
+ return scopedToken(
273
+ options.platform.replace(/\/$/, ""),
274
+ options.scope,
275
+ { action: "show", ...options },
276
+ options.fetch ?? fetch,
277
+ options.stdout ?? console
278
+ );
279
+ }
280
+ async function adminAi(options) {
281
+ const platform = (options.platform ?? import_node_process3.default.env.ODLA_PLATFORM ?? "https://odla.ai").replace(/\/$/, "");
282
+ const doFetch = options.fetch ?? fetch;
283
+ const out = options.stdout ?? console;
284
+ const scope = options.action === "set" || options.action === "credential-set" ? "platform:ai:write" : "platform:ai:read";
285
+ const token = options.token ?? import_node_process3.default.env.ODLA_ADMIN_TOKEN ?? await scopedToken(platform, scope, options, doFetch, out);
286
+ const headers = { authorization: `Bearer ${token}`, "content-type": "application/json" };
287
+ if (options.action === "credentials") {
288
+ const res2 = await doFetch(`${platform}/registry/platform/ai-credentials`, { headers });
289
+ const body2 = await responseBody(res2);
290
+ if (!res2.ok) throw new Error(apiError("read platform AI credential status", res2.status, body2));
291
+ const credentials = isRecord(body2) && isRecord(body2.credentials) ? body2.credentials : body2;
292
+ if (options.json) out.log(JSON.stringify({ credentials }, null, 2));
293
+ else for (const [provider, status] of Object.entries(isRecord(credentials) ? credentials : {})) {
294
+ out.log(`${provider} ${isRecord(status) && status.set === true ? "set" : "not set"}`);
295
+ }
296
+ return;
297
+ }
298
+ if (options.action === "credential-set") {
299
+ const provider = requireProvider(options.credentialProvider);
300
+ const value = await credentialValue(options);
301
+ const res2 = await doFetch(`${platform}/registry/platform/ai-credentials/${encodeURIComponent(provider)}`, {
302
+ method: "PUT",
303
+ headers,
304
+ body: JSON.stringify({ value })
305
+ });
306
+ const body2 = await responseBody(res2);
307
+ if (!res2.ok) throw new Error(apiError(`store ${provider} platform credential`, res2.status, body2));
308
+ out.log(`stored ${provider} platform credential (write-only; value was not returned)`);
309
+ return;
310
+ }
311
+ if (options.action === "show") {
312
+ const res2 = await doFetch(`${platform}/registry/platform/ai-policies`, { headers });
313
+ const body2 = await responseBody(res2);
314
+ if (!res2.ok) throw new Error(apiError("read platform AI policies", res2.status, body2));
315
+ const policies = policyArray(body2);
316
+ if (options.json) {
317
+ out.log(JSON.stringify({ policies }, null, 2));
318
+ return;
319
+ }
320
+ out.log("purpose enabled provider model max calls max input bytes max output tokens");
321
+ for (const policy2 of policies) {
322
+ out.log([
323
+ policy2.purpose,
324
+ String(policy2.enabled),
325
+ policy2.provider,
326
+ policy2.model,
327
+ String(policy2.maxCallsPerRun ?? ""),
328
+ String(policy2.maxInputBytes ?? ""),
329
+ String(policy2.maxOutputTokens ?? "")
330
+ ].join(" "));
331
+ }
332
+ return;
333
+ }
334
+ const purpose = requirePurpose(options.purpose);
335
+ if (!options.provider?.trim()) throw new Error("--provider is required");
336
+ if (!options.model?.trim()) throw new Error("--model is required");
337
+ const body = {
338
+ provider: options.provider.trim(),
339
+ model: options.model.trim(),
340
+ ...options.enabled === void 0 ? {} : { enabled: options.enabled },
341
+ ...options.maxInputBytes === void 0 ? {} : { maxInputBytes: options.maxInputBytes },
342
+ ...options.maxOutputTokens === void 0 ? {} : { maxOutputTokens: options.maxOutputTokens },
343
+ ...options.maxCallsPerRun === void 0 ? {} : { maxCallsPerRun: options.maxCallsPerRun }
344
+ };
345
+ const res = await doFetch(`${platform}/registry/platform/ai-policies/${encodeURIComponent(purpose)}`, {
346
+ method: "PUT",
347
+ headers,
348
+ body: JSON.stringify(body)
349
+ });
350
+ const response = await responseBody(res);
351
+ if (!res.ok) throw new Error(apiError(`update ${purpose}`, res.status, response));
352
+ const policy = isRecord(response) && isRecord(response.policy) ? response.policy : isRecord(response) ? response : {};
353
+ out.log(options.json ? JSON.stringify({ policy }, null, 2) : `updated ${purpose}: ${String(policy.provider ?? body.provider)}/${String(policy.model ?? body.model)}`);
354
+ }
355
+ async function scopedToken(platform, scope, options, doFetch, out) {
356
+ const tokenFile = options.tokenFile ?? `${import_node_process3.default.cwd()}/.odla/admin-token.local.json`;
357
+ const cache = readJsonFile(tokenFile);
358
+ const cached = cache?.platform === platform ? cache.tokens?.[scope] : void 0;
359
+ if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
360
+ out.log(`auth: using cached ${scope} grant (${tokenFile})`);
361
+ return cached.token;
362
+ }
363
+ const { token, expiresAt } = await (0, import_db2.requestToken)({
364
+ endpoint: platform,
365
+ label: `odla CLI admin AI (${scope})`,
366
+ scopes: [scope],
367
+ fetch: doFetch,
368
+ onCode: async ({ userCode, expiresIn }) => {
369
+ const approvalUrl = handshakeUrl(platform, userCode);
370
+ out.log(`Approve scoped request ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
371
+ 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);
372
+ if (shouldOpen) await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
373
+ }
374
+ });
375
+ const tokens = cache?.platform === platform ? { ...cache.tokens ?? {} } : {};
376
+ tokens[scope] = { token, expiresAt };
377
+ if ((0, import_node_fs2.existsSync)(`${import_node_process3.default.cwd()}/.git`)) ensureGitignore(import_node_process3.default.cwd(), [tokenFile]);
378
+ writePrivateJson(tokenFile, { platform, tokens });
379
+ out.log(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
380
+ return token;
381
+ }
382
+ function requirePurpose(value) {
383
+ if (!SYSTEM_AI_PURPOSES.includes(value)) {
384
+ throw new Error(`purpose must be one of: ${SYSTEM_AI_PURPOSES.join(", ")}`);
385
+ }
386
+ return value;
387
+ }
388
+ function requireProvider(value) {
389
+ if (value !== "anthropic" && value !== "openai" && value !== "google") {
390
+ throw new Error("provider must be one of: anthropic, openai, google");
391
+ }
392
+ return value;
393
+ }
394
+ async function credentialValue(options) {
395
+ if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
396
+ let value;
397
+ if (options.fromEnv) value = import_node_process3.default.env[options.fromEnv];
398
+ else if (options.stdin) value = await (options.readStdin ?? readStdin)();
399
+ else throw new Error("credential input required: use --from-env <NAME> or --stdin; values are never accepted as arguments");
400
+ value = value?.replace(/[\r\n]+$/, "");
401
+ if (!value) throw new Error(options.fromEnv ? `environment variable ${options.fromEnv} is empty or unset` : "stdin credential is empty");
402
+ if (new TextEncoder().encode(value).byteLength > 64 * 1024) throw new Error("credential exceeds 64 KiB");
403
+ return value;
404
+ }
405
+ async function readStdin() {
406
+ let value = "";
407
+ for await (const chunk of import_node_process3.default.stdin) {
408
+ value += String(chunk);
409
+ if (value.length > 64 * 1024) throw new Error("credential exceeds 64 KiB");
410
+ }
411
+ return value;
412
+ }
413
+ function policyArray(body) {
414
+ if (isRecord(body) && Array.isArray(body.policies)) return body.policies.filter(isRecord);
415
+ if (isRecord(body) && isRecord(body.policies)) return Object.values(body.policies).filter(isRecord);
416
+ throw new Error("platform returned an invalid AI policy response");
417
+ }
418
+ async function responseBody(res) {
419
+ const text = await res.text();
420
+ if (!text) return {};
421
+ try {
422
+ return JSON.parse(text);
423
+ } catch {
424
+ return { message: text.slice(0, 300) };
425
+ }
426
+ }
427
+ function apiError(action, status, body) {
428
+ const error = isRecord(body) && isRecord(body.error) ? body.error : void 0;
429
+ const message = error && typeof error.message === "string" ? error.message : isRecord(body) && typeof body.message === "string" ? body.message : "request failed";
430
+ return `${action} failed (${status}): ${message}`;
431
+ }
432
+ function isRecord(value) {
433
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
434
+ }
435
+
436
+ // src/argv.ts
437
+ function parseArgv(argv) {
438
+ const positionals = [];
439
+ const options = {};
440
+ for (let i = 0; i < argv.length; i++) {
441
+ const arg = argv[i];
442
+ if (!arg.startsWith("--")) {
443
+ positionals.push(arg);
444
+ continue;
445
+ }
446
+ const eq = arg.indexOf("=");
447
+ const rawName = arg.slice(2, eq === -1 ? void 0 : eq);
448
+ if (!rawName) continue;
449
+ if (rawName.startsWith("no-")) {
450
+ options[rawName.slice(3)] = false;
451
+ continue;
452
+ }
453
+ if (eq !== -1) {
454
+ addOption(options, rawName, arg.slice(eq + 1));
455
+ continue;
456
+ }
457
+ const value = argv[i + 1];
458
+ if (value !== void 0 && !value.startsWith("--")) {
459
+ i++;
460
+ addOption(options, rawName, value);
461
+ } else {
462
+ addOption(options, rawName, true);
463
+ }
464
+ }
465
+ return { positionals, options };
466
+ }
467
+ function assertArgs(parsed, allowedOptions, maxPositionals) {
468
+ const allowed = new Set(allowedOptions);
469
+ for (const name of Object.keys(parsed.options)) {
470
+ if (!allowed.has(name)) throw new Error(`unknown option "--${name}"; run "odla-ai help" for supported options`);
471
+ }
472
+ if (parsed.positionals.length > maxPositionals) {
473
+ throw new Error(`unexpected argument "${parsed.positionals[maxPositionals]}"; run "odla-ai help"`);
474
+ }
475
+ }
476
+ function requiredString(value, name) {
477
+ const result = stringOpt(value);
478
+ if (!result) throw new Error(`${name} is required`);
479
+ return result;
480
+ }
481
+ function stringOpt(value) {
482
+ if (typeof value === "string") return value;
483
+ if (Array.isArray(value)) return value[value.length - 1];
484
+ return void 0;
485
+ }
486
+ function listOpt(value) {
487
+ if (value === void 0 || typeof value === "boolean") return void 0;
488
+ const values = Array.isArray(value) ? value : [value];
489
+ return values.flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
490
+ }
491
+ function boolOpt(value) {
492
+ if (typeof value === "boolean") return value;
493
+ if (typeof value === "string" && (value === "true" || value === "false")) return value === "true";
494
+ if (value === void 0) return void 0;
495
+ throw new Error("boolean option must be true or false");
496
+ }
497
+ function numberOpt(value, flag) {
498
+ if (value === void 0) return void 0;
499
+ const raw = stringOpt(value);
500
+ const parsed = raw === void 0 ? Number.NaN : Number(raw);
501
+ if (!Number.isSafeInteger(parsed) || parsed < 1) throw new Error(`${flag} requires a positive integer`);
502
+ return parsed;
503
+ }
504
+ function addOption(options, name, value) {
505
+ const current = options[name];
506
+ if (current === void 0) options[name] = value;
507
+ else if (Array.isArray(current)) current.push(String(value));
508
+ else options[name] = [String(current), String(value)];
509
+ }
54
510
 
55
511
  // src/capabilities.ts
56
512
  var CAPABILITIES = {
@@ -59,7 +515,9 @@ var CAPABILITIES = {
59
515
  "issue, persist, and explicitly rotate configured service credentials",
60
516
  "write local Worker values and push deployed Worker secrets through Wrangler stdin",
61
517
  "push db schema/rules and configure platform AI, auth, and deployment links",
62
- "validate config offline and smoke-test a provisioned db environment"
518
+ "validate config offline and smoke-test a provisioned db environment",
519
+ "run app-attributed hosted security discovery and independent validation without provider keys",
520
+ "let admins manage system AI routes/credentials through short-lived exact-scope approval"
63
521
  ],
64
522
  agent: [
65
523
  "install and import the selected odla SDKs",
@@ -70,11 +528,13 @@ var CAPABILITIES = {
70
528
  "approve the odla device code and one-off third-party logins",
71
529
  "consent to production changes with --yes",
72
530
  "request destructive credential rotation explicitly",
73
- "review application semantics, security findings, releases, and merges"
531
+ "review application semantics, security findings, releases, and merges",
532
+ "approve redacted-source disclosure before hosted security reasoning"
74
533
  ],
75
534
  studio: [
76
535
  "view telemetry and environment state",
77
- "perform manual credential recovery when the CLI's local shown-once copy is unavailable"
536
+ "perform manual credential recovery when the CLI's local shown-once copy is unavailable",
537
+ "configure system AI purposes and view app/environment/run-attributed usage"
78
538
  ]
79
539
  };
80
540
  function printCapabilities(json = false, out = console) {
@@ -95,28 +555,28 @@ function printGroup(out, heading, items) {
95
555
  }
96
556
 
97
557
  // src/config.ts
98
- var import_node_fs = require("fs");
99
- var import_node_path = require("path");
558
+ var import_node_fs3 = require("fs");
559
+ var import_node_path2 = require("path");
100
560
  var import_node_url = require("url");
101
561
  var DEFAULT_PLATFORM = "https://odla.ai";
102
562
  var DEFAULT_ENVS = ["dev"];
103
563
  var DEFAULT_SERVICES = ["db", "ai"];
104
564
  async function loadProjectConfig(configPath = "odla.config.mjs") {
105
- const resolved = (0, import_node_path.resolve)(configPath);
106
- if (!(0, import_node_fs.existsSync)(resolved)) {
565
+ const resolved = (0, import_node_path2.resolve)(configPath);
566
+ if (!(0, import_node_fs3.existsSync)(resolved)) {
107
567
  throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
108
568
  }
109
569
  const raw = await loadConfigModule(resolved);
110
- const rootDir = (0, import_node_path.dirname)(resolved);
570
+ const rootDir = (0, import_node_path2.dirname)(resolved);
111
571
  validateRawConfig(raw, resolved);
112
572
  const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
113
573
  const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
114
574
  const envs = unique(raw.envs?.length ? raw.envs : DEFAULT_ENVS);
115
575
  const services = unique(raw.services?.length ? raw.services : DEFAULT_SERVICES);
116
576
  const local = {
117
- tokenFile: (0, import_node_path.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
118
- credentialsFile: (0, import_node_path.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
119
- devVarsFile: (0, import_node_path.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
577
+ tokenFile: (0, import_node_path2.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
578
+ credentialsFile: (0, import_node_path2.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
579
+ devVarsFile: (0, import_node_path2.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
120
580
  gitignore: raw.local?.gitignore ?? true
121
581
  };
122
582
  return {
@@ -133,9 +593,9 @@ async function loadProjectConfig(configPath = "odla.config.mjs") {
133
593
  async function resolveDataExport(cfg, value, names) {
134
594
  if (value === void 0 || value === null || value === false) return void 0;
135
595
  if (typeof value !== "string") return value;
136
- const target = (0, import_node_path.isAbsolute)(value) ? value : (0, import_node_path.resolve)(cfg.rootDir, value);
596
+ const target = (0, import_node_path2.isAbsolute)(value) ? value : (0, import_node_path2.resolve)(cfg.rootDir, value);
137
597
  if (target.endsWith(".json")) {
138
- return JSON.parse((0, import_node_fs.readFileSync)(target, "utf8"));
598
+ return JSON.parse((0, import_node_fs3.readFileSync)(target, "utf8"));
139
599
  }
140
600
  const mod = await import((0, import_node_url.pathToFileURL)(target).href);
141
601
  for (const name of names) {
@@ -186,175 +646,49 @@ function validId(value) {
186
646
  return typeof value === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value);
187
647
  }
188
648
  async function loadConfigModule(path) {
189
- if (path.endsWith(".json")) return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
649
+ if (path.endsWith(".json")) return JSON.parse((0, import_node_fs3.readFileSync)(path, "utf8"));
190
650
  const mod = await import(`${(0, import_node_url.pathToFileURL)(path).href}?t=${Date.now()}`);
191
651
  const value = mod.default ?? mod.config;
192
652
  if (typeof value === "function") return await value();
193
653
  return value;
194
654
  }
195
655
  function trimSlash(value) {
196
- return value.replace(/\/+$/, "");
197
- }
198
- function unique(values) {
199
- return [...new Set(values.filter(Boolean))];
200
- }
201
-
202
- // src/doctor-checks.ts
203
- var import_node_child_process2 = require("child_process");
204
- var import_node_fs4 = require("fs");
205
- var import_node_path4 = require("path");
206
-
207
- // src/redact.ts
208
- var REPLACEMENTS = [
209
- [/odla_(sk|dev)_[A-Za-z0-9._-]+/g, "odla_$1_[redacted]"],
210
- [/\bsk_(live|test)_[A-Za-z0-9]+/g, "sk_$1_[redacted]"],
211
- [/\bsk-[A-Za-z0-9._-]+/g, "sk-[redacted]"],
212
- [/\bwhsec_[A-Za-z0-9+/=]+/g, "whsec_[redacted]"],
213
- [/\bo11y_[A-Za-z0-9]+/g, "o11y_[redacted]"],
214
- [/\b(ghp|gho|github_pat)_[A-Za-z0-9_]+/g, "$1_[redacted]"],
215
- [/\bAKIA[A-Z0-9]{12,}/g, "AKIA[redacted]"]
216
- ];
217
- function redactSecrets(value) {
218
- let result = value;
219
- for (const [pattern, replacement] of REPLACEMENTS) result = result.replace(pattern, replacement);
220
- return result;
221
- }
222
- function looksSecret(value) {
223
- return redactSecrets(value) !== value || value.includes("-----BEGIN");
224
- }
225
-
226
- // src/local.ts
227
- var import_node_fs2 = require("fs");
228
- var import_node_path2 = require("path");
229
- var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
230
- function readJsonFile(path) {
231
- try {
232
- return JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
233
- } catch {
234
- return null;
235
- }
236
- }
237
- function writePrivateJson(path, value) {
238
- writePrivateText(path, `${JSON.stringify(value, null, 2)}
239
- `);
240
- }
241
- function readCredentials(path) {
242
- if (!(0, import_node_fs2.existsSync)(path)) return null;
243
- let value;
244
- try {
245
- value = JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
246
- } catch {
247
- throw new Error(`credentials file ${path} is not valid JSON; fix or remove it before provisioning`);
248
- }
249
- if (!value || typeof value !== "object" || typeof value.appId !== "string" || !value.envs || typeof value.envs !== "object") {
250
- throw new Error(`credentials file ${path} has an invalid shape; fix or remove it before provisioning`);
251
- }
252
- return value;
253
- }
254
- function mergeCredential(current, update) {
255
- const next = current ?? {
256
- appId: update.appId,
257
- platformUrl: update.platformUrl,
258
- dbEndpoint: update.dbEndpoint,
259
- updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
260
- envs: {}
261
- };
262
- next.appId = update.appId;
263
- next.platformUrl = update.platformUrl;
264
- next.dbEndpoint = update.dbEndpoint;
265
- next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
266
- next.envs[update.env] = {
267
- ...next.envs[update.env] ?? {},
268
- tenantId: update.tenantId,
269
- ...update.dbKey ? { dbKey: update.dbKey } : {},
270
- ...update.o11yToken ? { o11yToken: update.o11yToken } : {}
271
- };
272
- return next;
273
- }
274
- function ensureGitignore(rootDir, localPaths = []) {
275
- const path = (0, import_node_path2.resolve)(rootDir, ".gitignore");
276
- const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : "";
277
- const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line) => !!line);
278
- const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
279
- const missing = wanted.filter((line) => !existing.split(/\r?\n/).includes(line));
280
- if (missing.length === 0) return;
281
- const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
282
- (0, import_node_fs2.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
283
- `);
284
- }
285
- function o11yDevVars(cfg) {
286
- if (!cfg.services.includes("o11y")) return void 0;
287
- return {
288
- endpoint: cfg.o11y?.endpoint ?? "https://o11y.odla.ai",
289
- service: cfg.o11y?.service ?? cfg.app.id,
290
- ...cfg.o11y?.version ? { version: cfg.o11y.version } : {}
291
- };
292
- }
293
- function resolveWriteDevVarsTarget(cfg, requested) {
294
- if (!requested) return null;
295
- if (requested === true) return cfg.local.devVarsFile;
296
- return (0, import_node_path2.resolve)((0, import_node_path2.dirname)(cfg.configPath), requested);
297
- }
298
- function writeDevVars(path, credentials, env, o11y) {
299
- const entry = credentials.envs[env];
300
- if (!entry) throw new Error(`no credentials for env "${env}" in ${path}`);
301
- const lines = [`ODLA_PLATFORM="${credentials.platformUrl}"`];
302
- if (entry.dbKey) lines.push(`ODLA_ENDPOINT="${credentials.dbEndpoint}"`);
303
- lines.push(`ODLA_APP_ID="${credentials.appId}"`, `ODLA_ENV="${env}"`, `ODLA_TENANT="${entry.tenantId}"`);
304
- if (entry.dbKey) lines.push(`ODLA_API_KEY="${entry.dbKey}"`);
305
- if (o11y) {
306
- lines.push(`ODLA_O11Y_ENDPOINT="${o11y.endpoint}"`, `ODLA_O11Y_SERVICE="${o11y.service}"`);
307
- if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
308
- if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
309
- }
310
- const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : "";
311
- const retained = existing.split(/\r?\n/).filter((line) => !isManagedDevVar(line));
312
- while (retained.at(-1) === "") retained.pop();
313
- const prefix = retained.length ? `${retained.join("\n")}
314
-
315
- ` : "";
316
- writePrivateText(path, `${prefix}${lines.join("\n")}
317
- `);
318
- }
319
- var MANAGED_DEV_VARS = /* @__PURE__ */ new Set([
320
- "ODLA_PLATFORM",
321
- "ODLA_ENDPOINT",
322
- "ODLA_APP_ID",
323
- "ODLA_ENV",
324
- "ODLA_TENANT",
325
- "ODLA_API_KEY",
326
- "ODLA_O11Y_ENDPOINT",
327
- "ODLA_O11Y_SERVICE",
328
- "ODLA_O11Y_VERSION",
329
- "ODLA_O11Y_TOKEN"
330
- ]);
331
- function isManagedDevVar(line) {
332
- const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
333
- return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
656
+ return value.replace(/\/+$/, "");
334
657
  }
335
- function writePrivateText(path, text) {
336
- (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(path), { recursive: true });
337
- const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
338
- (0, import_node_fs2.writeFileSync)(temporary, text, { mode: 384 });
339
- (0, import_node_fs2.chmodSync)(temporary, 384);
340
- (0, import_node_fs2.renameSync)(temporary, path);
658
+ function unique(values) {
659
+ return [...new Set(values.filter(Boolean))];
341
660
  }
342
- function gitignoreEntry(rootDir, path) {
343
- const rel = (0, import_node_path2.relative)((0, import_node_path2.resolve)(rootDir), (0, import_node_path2.resolve)(path));
344
- if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path2.isAbsolute)(rel)) return null;
345
- return rel.replaceAll("\\", "/");
661
+
662
+ // src/doctor-checks.ts
663
+ var import_node_child_process3 = require("child_process");
664
+ var import_node_fs5 = require("fs");
665
+ var import_node_path4 = require("path");
666
+
667
+ // src/redact.ts
668
+ var REPLACEMENTS = [
669
+ [/odla_(sk|dev)_[A-Za-z0-9._-]+/g, "odla_$1_[redacted]"],
670
+ [/\bsk_(live|test)_[A-Za-z0-9]+/g, "sk_$1_[redacted]"],
671
+ [/\bsk-[A-Za-z0-9._-]+/g, "sk-[redacted]"],
672
+ [/\bwhsec_[A-Za-z0-9+/=]+/g, "whsec_[redacted]"],
673
+ [/\bo11y_[A-Za-z0-9]+/g, "o11y_[redacted]"],
674
+ [/\b(ghp|gho|github_pat)_[A-Za-z0-9_]+/g, "$1_[redacted]"],
675
+ [/\bAKIA[A-Z0-9]{12,}/g, "AKIA[redacted]"]
676
+ ];
677
+ function redactSecrets(value) {
678
+ let result = value;
679
+ for (const [pattern, replacement] of REPLACEMENTS) result = result.replace(pattern, replacement);
680
+ return result;
346
681
  }
347
- function displayPath(path, rootDir = process.cwd()) {
348
- const rel = (0, import_node_path2.relative)(rootDir, path);
349
- return rel && !rel.startsWith("..") ? rel : path;
682
+ function looksSecret(value) {
683
+ return redactSecrets(value) !== value || value.includes("-----BEGIN");
350
684
  }
351
685
 
352
686
  // src/wrangler.ts
353
- var import_node_child_process = require("child_process");
354
- var import_node_fs3 = require("fs");
687
+ var import_node_child_process2 = require("child_process");
688
+ var import_node_fs4 = require("fs");
355
689
  var import_node_path3 = require("path");
356
690
  var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
357
- const child = (0, import_node_child_process.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
691
+ const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
358
692
  let stdout = "";
359
693
  let stderr = "";
360
694
  child.stdout.on("data", (chunk) => stdout += chunk.toString());
@@ -367,14 +701,14 @@ var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]
367
701
  function findWranglerConfig(rootDir) {
368
702
  for (const name of WRANGLER_CONFIG_FILES) {
369
703
  const path = (0, import_node_path3.join)(rootDir, name);
370
- if ((0, import_node_fs3.existsSync)(path)) return path;
704
+ if ((0, import_node_fs4.existsSync)(path)) return path;
371
705
  }
372
706
  return null;
373
707
  }
374
708
  function readWranglerConfig(path) {
375
709
  if (path.endsWith(".toml")) return null;
376
710
  try {
377
- return JSON.parse(stripJsonComments((0, import_node_fs3.readFileSync)(path, "utf8")));
711
+ return JSON.parse(stripJsonComments((0, import_node_fs4.readFileSync)(path, "utf8")));
378
712
  } catch {
379
713
  return null;
380
714
  }
@@ -445,7 +779,7 @@ function lintRules(rules, entities, publicRead) {
445
779
  }
446
780
  return warnings;
447
781
  }
448
- var defaultExec = (cmd, args, cwd) => (0, import_node_child_process2.execFileSync)(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
782
+ var defaultExec = (cmd, args, cwd) => (0, import_node_child_process3.execFileSync)(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
449
783
  function trackedSecretFiles(rootDir, exec = defaultExec, localPaths = []) {
450
784
  let output;
451
785
  try {
@@ -475,7 +809,7 @@ function wranglerWarnings(rootDir) {
475
809
  const dir = (0, import_node_path4.resolve)(rootDir, assets.directory);
476
810
  if (dir === (0, import_node_path4.resolve)(rootDir)) {
477
811
  warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
478
- } else if ((0, import_node_fs4.existsSync)((0, import_node_path4.join)(dir, "node_modules"))) {
812
+ } else if ((0, import_node_fs5.existsSync)((0, import_node_path4.join)(dir, "node_modules"))) {
479
813
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
480
814
  }
481
815
  }
@@ -511,12 +845,12 @@ function o11yProjectWarnings(rootDir) {
511
845
  return warnings;
512
846
  }
513
847
  const main = typeof config.main === "string" ? (0, import_node_path4.resolve)(rootDir, config.main) : null;
514
- if (!main || !(0, import_node_fs4.existsSync)(main)) {
848
+ if (!main || !(0, import_node_fs5.existsSync)(main)) {
515
849
  warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
516
850
  } else {
517
851
  let source = "";
518
852
  try {
519
- source = (0, import_node_fs4.readFileSync)(main, "utf8");
853
+ source = (0, import_node_fs5.readFileSync)(main, "utf8");
520
854
  } catch {
521
855
  }
522
856
  if (!/\bwithObservability\b/.test(source)) {
@@ -531,7 +865,7 @@ function o11yProjectWarnings(rootDir) {
531
865
  }
532
866
  function readPackageJson(rootDir) {
533
867
  try {
534
- return JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path4.join)(rootDir, "package.json"), "utf8"));
868
+ return JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(rootDir, "package.json"), "utf8"));
535
869
  } catch {
536
870
  return null;
537
871
  }
@@ -598,13 +932,13 @@ async function doctor(options) {
598
932
  }
599
933
 
600
934
  // src/init.ts
601
- var import_node_fs5 = require("fs");
935
+ var import_node_fs6 = require("fs");
602
936
  var import_node_path5 = require("path");
603
937
  function initProject(options) {
604
938
  const out = options.stdout ?? console;
605
939
  const rootDir = (0, import_node_path5.resolve)(options.rootDir ?? process.cwd());
606
940
  const configPath = (0, import_node_path5.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
607
- if ((0, import_node_fs5.existsSync)(configPath) && !options.force) {
941
+ if ((0, import_node_fs6.existsSync)(configPath) && !options.force) {
608
942
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
609
943
  }
610
944
  if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
@@ -613,10 +947,10 @@ function initProject(options) {
613
947
  const envs = options.envs?.length ? options.envs : ["dev"];
614
948
  const services = options.services?.length ? options.services : ["db", "ai"];
615
949
  const aiProvider = options.aiProvider ?? "anthropic";
616
- (0, import_node_fs5.mkdirSync)((0, import_node_path5.dirname)(configPath), { recursive: true });
617
- (0, import_node_fs5.mkdirSync)((0, import_node_path5.resolve)(rootDir, "src/odla"), { recursive: true });
618
- (0, import_node_fs5.mkdirSync)((0, import_node_path5.resolve)(rootDir, ".odla"), { recursive: true });
619
- (0, import_node_fs5.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
950
+ (0, import_node_fs6.mkdirSync)((0, import_node_path5.dirname)(configPath), { recursive: true });
951
+ (0, import_node_fs6.mkdirSync)((0, import_node_path5.resolve)(rootDir, "src/odla"), { recursive: true });
952
+ (0, import_node_fs6.mkdirSync)((0, import_node_path5.resolve)(rootDir, ".odla"), { recursive: true });
953
+ (0, import_node_fs6.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
620
954
  writeIfMissing((0, import_node_path5.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
621
955
  writeIfMissing((0, import_node_path5.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
622
956
  ensureGitignore(rootDir);
@@ -625,8 +959,8 @@ function initProject(options) {
625
959
  out.log("updated .gitignore for local odla credentials");
626
960
  }
627
961
  function writeIfMissing(path, text) {
628
- if ((0, import_node_fs5.existsSync)(path)) return;
629
- (0, import_node_fs5.writeFileSync)(path, text);
962
+ if ((0, import_node_fs6.existsSync)(path)) return;
963
+ (0, import_node_fs6.writeFileSync)(path, text);
630
964
  }
631
965
  function configTemplate(input) {
632
966
  return `export default {
@@ -715,84 +1049,7 @@ function relativeDisplay(path, rootDir) {
715
1049
  // src/provision.ts
716
1050
  var import_apps2 = require("@odla-ai/apps");
717
1051
  var import_ai = require("@odla-ai/ai");
718
- var import_node_process3 = __toESM(require("process"), 1);
719
-
720
- // src/token.ts
721
- var import_db = require("@odla-ai/db");
722
- var import_node_process2 = __toESM(require("process"), 1);
723
-
724
- // src/open.ts
725
- var import_node_child_process3 = require("child_process");
726
- var import_node_process = __toESM(require("process"), 1);
727
- async function openUrl(url, options = {}) {
728
- const command = openerFor(options.platform ?? import_node_process.default.platform);
729
- const doSpawn = options.spawnImpl ?? import_node_child_process3.spawn;
730
- await new Promise((resolve6, reject) => {
731
- const child = doSpawn(command.cmd, [...command.args, url], {
732
- stdio: "ignore",
733
- detached: true
734
- });
735
- child.once("error", reject);
736
- child.once("spawn", () => {
737
- child.unref();
738
- resolve6();
739
- });
740
- });
741
- }
742
- function openerFor(platform) {
743
- if (platform === "darwin") return { cmd: "open", args: [] };
744
- if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
745
- return { cmd: "xdg-open", args: [] };
746
- }
747
-
748
- // src/token.ts
749
- async function getDeveloperToken(cfg, options, doFetch, out) {
750
- if (options.token) return options.token;
751
- if (import_node_process2.default.env.ODLA_DEV_TOKEN) return import_node_process2.default.env.ODLA_DEV_TOKEN;
752
- const cached = readJsonFile(cfg.local.tokenFile);
753
- if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
754
- out.log(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
755
- return cached.token;
756
- }
757
- const browser = approvalBrowser(options);
758
- const { token, expiresAt } = await (0, import_db.requestToken)({
759
- endpoint: cfg.platformUrl,
760
- label: `${cfg.app.id} provisioner`,
761
- fetch: doFetch,
762
- onCode: async ({ userCode, expiresIn }) => {
763
- const approvalUrl = handshakeUrl(cfg.platformUrl, userCode);
764
- out.log("");
765
- out.log(`Approve code ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
766
- if (browser.open) {
767
- try {
768
- await (options.openApprovalUrl ?? openUrl)(approvalUrl);
769
- out.log(`auth: opened browser for approval${browser.mode === "auto" ? " (auto)" : ""}`);
770
- } catch (err) {
771
- out.log(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
772
- }
773
- } else if (browser.reason) {
774
- out.log(`auth: browser launch skipped (${browser.reason})`);
775
- }
776
- out.log("");
777
- }
778
- });
779
- writePrivateJson(cfg.local.tokenFile, { token, expiresAt });
780
- out.log(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
781
- return token;
782
- }
783
- function approvalBrowser(options) {
784
- if (options.open === true) return { open: true, mode: "forced" };
785
- if (options.open === false) return { open: false, reason: "disabled by --no-open" };
786
- if (import_node_process2.default.env.CI) return { open: false, reason: "CI environment" };
787
- const interactive = options.interactive ?? Boolean(import_node_process2.default.stdin.isTTY && import_node_process2.default.stdout.isTTY);
788
- if (!interactive) return { open: false, reason: "non-interactive shell; pass --open to force" };
789
- return { open: true, mode: "auto" };
790
- }
791
- function handshakeUrl(platformUrl, userCode) {
792
- const url = new URL("/studio", platformUrl);
793
- url.searchParams.set("code", userCode);
794
- return url.toString();
795
- }
1052
+ var import_node_process4 = __toESM(require("process"), 1);
796
1053
 
797
1054
  // src/provision-credentials.ts
798
1055
  var import_apps = require("@odla-ai/apps");
@@ -1096,7 +1353,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
1096
1353
  out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
1097
1354
  }
1098
1355
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
1099
- const key = import_node_process3.default.env[cfg.ai.keyEnv];
1356
+ const key = import_node_process4.default.env[cfg.ai.keyEnv];
1100
1357
  if (key) {
1101
1358
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
1102
1359
  await (0, import_ai.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -1156,10 +1413,161 @@ async function safeText2(res) {
1156
1413
  }
1157
1414
  }
1158
1415
 
1416
+ // src/security.ts
1417
+ var import_node_path6 = require("path");
1418
+ var import_security = require("@odla-ai/security");
1419
+ var import_node = require("@odla-ai/security/node");
1420
+ async function runHostedSecurity(options) {
1421
+ if (options.sourceDisclosureAck !== "redacted") {
1422
+ throw new Error("Hosted security requires sourceDisclosureAck=redacted before repository source may leave this process");
1423
+ }
1424
+ const selfAudit = options.selfAudit === true;
1425
+ const cfg = selfAudit ? void 0 : await loadProjectConfig(options.configPath ?? "odla.config.mjs");
1426
+ const appId = selfAudit ? "odla-ai" : cfg.app.id;
1427
+ const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
1428
+ const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
1429
+ const target = (0, import_node_path6.resolve)(options.target ?? cfg?.rootDir ?? ".");
1430
+ const output = (0, import_node_path6.resolve)(options.out ?? (0, import_node_path6.resolve)(target, ".odla/security/hosted"));
1431
+ const outputRelative = (0, import_node_path6.relative)(target, output).split(import_node_path6.sep).join("/");
1432
+ if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
1433
+ const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
1434
+ const tokenRequest = {
1435
+ platform,
1436
+ appId,
1437
+ env,
1438
+ selfAudit,
1439
+ scope: selfAudit ? "platform:security:self" : null
1440
+ };
1441
+ const token = await injectedToken(options, tokenRequest);
1442
+ const snapshot = await (0, import_node.snapshotDirectory)(target, {
1443
+ exclude: !outputRelative.startsWith("../") && !(0, import_node_path6.isAbsolute)(outputRelative) ? [outputRelative] : []
1444
+ });
1445
+ const hosted = await (0, import_security.createPlatformSecurityReasoners)({
1446
+ platform,
1447
+ token,
1448
+ appId,
1449
+ env,
1450
+ repository: snapshot.repository,
1451
+ revision: snapshot.revision,
1452
+ snapshotDigest: snapshot.digest,
1453
+ sourceDisclosure: "redacted",
1454
+ clientRunId: options.runId ?? crypto.randomUUID(),
1455
+ ...selfAudit ? { selfAudit: { enabled: true, subject: "service:odla-security-self-audit" } } : {},
1456
+ fetch: options.fetch,
1457
+ signal: options.signal
1458
+ });
1459
+ const harness = (0, import_security.createSecurityHarness)({
1460
+ profile,
1461
+ store: new import_node.FileRunStore((0, import_node_path6.resolve)(output, "state")),
1462
+ discoveryReasoner: hosted.discoveryReasoner,
1463
+ validationReasoner: hosted.validationReasoner,
1464
+ policy: {
1465
+ modelSourceDisclosure: "redacted",
1466
+ active: false,
1467
+ allowNetwork: false
1468
+ }
1469
+ });
1470
+ const report = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
1471
+ await (0, import_node.writeSecurityArtifacts)(output, report);
1472
+ const reportDigest = await (0, import_security.securityFingerprint)(report);
1473
+ await hosted.complete({
1474
+ reportDigest,
1475
+ coverageStatus: report.coverageStatus,
1476
+ confirmed: report.metrics.confirmed,
1477
+ candidates: report.metrics.candidates
1478
+ }, { signal: options.signal });
1479
+ printSummary(options.stdout ?? console, appId, env, hosted.run, report, output);
1480
+ return Object.freeze({ report, run: hosted.run, output });
1481
+ }
1482
+ function selectEnv(requested, declared, configPath, rootDir) {
1483
+ const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
1484
+ if (!env || !declared.includes(env)) {
1485
+ const shown = (0, import_node_path6.relative)(rootDir, configPath) || configPath;
1486
+ throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
1487
+ }
1488
+ return env;
1489
+ }
1490
+ async function injectedToken(options, request) {
1491
+ const value = options.token ?? await options.getToken?.(Object.freeze({ ...request }));
1492
+ if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
1493
+ throw new Error(
1494
+ request.selfAudit ? "Self-audit requires an injected, scoped platform security token" : "Hosted security requires an injected app developer token or getToken callback"
1495
+ );
1496
+ }
1497
+ return value;
1498
+ }
1499
+ function profileFor(name, maxHuntTasks) {
1500
+ const profile = name === "odla" ? (0, import_security.odlaProfile)() : name === "cloudflare-app" ? (0, import_security.cloudflareAppProfile)() : name === "generic" ? (0, import_security.genericProfile)() : void 0;
1501
+ if (!profile) throw new Error(`unknown security profile "${String(name)}"`);
1502
+ if (maxHuntTasks === void 0) return profile;
1503
+ if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
1504
+ return { ...profile, maxHuntTasks };
1505
+ }
1506
+ function printSummary(out, appId, env, run, report, output) {
1507
+ const complete = report.coverage.filter((cell) => cell.state === "complete").length;
1508
+ out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
1509
+ out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
1510
+ out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
1511
+ out.log(` coverage: ${report.coverageStatus} ${complete}/${report.coverage.length} blocked=${report.metrics.blockedCells} shallow=${report.metrics.shallowCells} unscheduled=${report.metrics.unscheduledCells}`);
1512
+ out.log(` findings: confirmed=${report.metrics.confirmed} needs_reproduction=${report.metrics.needsReproduction} candidates=${report.metrics.candidates}`);
1513
+ out.log(` report: ${(0, import_node_path6.resolve)(output, "REPORT.md")}`);
1514
+ }
1515
+
1516
+ // src/help.ts
1517
+ var import_node_fs7 = require("fs");
1518
+ function cliVersion() {
1519
+ const pkg = JSON.parse((0, import_node_fs7.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
1520
+ return pkg.version ?? "unknown";
1521
+ }
1522
+ function printHelp() {
1523
+ console.log(`odla-ai
1524
+
1525
+ Usage:
1526
+ odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
1527
+ odla-ai init --app-id <id> --name <name> [--services db,ai,o11y] [--env dev --env prod]
1528
+ odla-ai doctor [--config odla.config.mjs]
1529
+ odla-ai capabilities [--json]
1530
+ odla-ai admin ai show [--platform https://odla.ai] [--json]
1531
+ odla-ai admin ai set <purpose> --provider <id> --model <id> [--enabled|--no-enabled]
1532
+ odla-ai admin ai credentials [--json]
1533
+ odla-ai admin ai credential set <provider> (--from-env <NAME>|--stdin)
1534
+ odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
1535
+ odla-ai security run [target] --self --ack-redacted-source
1536
+ odla-ai provision [--config odla.config.mjs] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
1537
+ odla-ai smoke [--config odla.config.mjs] [--env dev]
1538
+ odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
1539
+ odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
1540
+ odla-ai version
1541
+
1542
+ Commands:
1543
+ setup Install offline odla runbooks for common coding-agent harnesses.
1544
+ init Create a generic odla.config.mjs plus starter schema/rules files.
1545
+ doctor Validate and summarize the project config without network calls.
1546
+ capabilities Show what the CLI automates vs agent edits and human checkpoints.
1547
+ admin Manage platform-funded AI routing with a narrow admin-approved device grant.
1548
+ security Run hosted discovery + independent validation with app/run attribution.
1549
+ provision Register services, configure them, persist credentials, optionally push secrets.
1550
+ smoke Verify local credentials, public-config, live schema, and db aggregate.
1551
+ skill Same installer; --agent accepts all, claude, codex, cursor,
1552
+ copilot, gemini, or agents (repeatable or comma-separated).
1553
+ secrets Push configured db/o11y secrets into the Worker via wrangler stdin.
1554
+ version Print the CLI version.
1555
+
1556
+ Safety:
1557
+ New projects target dev only. Add prod explicitly to odla.config.mjs and pass
1558
+ --yes to provision it; use --dry-run first to inspect the resolved plan.
1559
+ Provision caches the approved developer token and service credentials under
1560
+ .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
1561
+ preflights Wrangler before any shown-once issuance or destructive rotation.
1562
+ Provision opens the approval page automatically in interactive terminals;
1563
+ use --open to force browser launch or --no-open to suppress it.
1564
+ `);
1565
+ }
1566
+
1159
1567
  // src/skill.ts
1160
- var import_node_fs6 = require("fs");
1568
+ var import_node_fs8 = require("fs");
1161
1569
  var import_node_os = require("os");
1162
- var import_node_path6 = require("path");
1570
+ var import_node_path7 = require("path");
1163
1571
  var import_node_url2 = require("url");
1164
1572
 
1165
1573
  // src/skill-adapters.ts
@@ -1220,8 +1628,8 @@ function installSkill(options = {}) {
1220
1628
  const files = listFiles(sourceDir);
1221
1629
  if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
1222
1630
  const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
1223
- const root = (0, import_node_path6.resolve)(options.dir ?? process.cwd());
1224
- const home = (0, import_node_path6.resolve)(options.homeDir ?? (0, import_node_os.homedir)());
1631
+ const root = (0, import_node_path7.resolve)(options.dir ?? process.cwd());
1632
+ const home = (0, import_node_path7.resolve)(options.homeDir ?? (0, import_node_os.homedir)());
1225
1633
  const plans = /* @__PURE__ */ new Map();
1226
1634
  const targets = /* @__PURE__ */ new Map();
1227
1635
  const rememberTarget = (harness, target) => {
@@ -1235,48 +1643,48 @@ function installSkill(options = {}) {
1235
1643
  plans.set(target, { target, content, boundary, managedMerge });
1236
1644
  };
1237
1645
  const planSkillTree = (targetDir2, boundary = root) => {
1238
- 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);
1646
+ 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);
1239
1647
  };
1240
1648
  let targetDir;
1241
1649
  if (options.global) {
1242
- const claudeRoot = (0, import_node_path6.join)(home, ".claude", "skills");
1243
- const codexRoot = (0, import_node_path6.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path6.join)(home, ".codex"), "skills");
1650
+ const claudeRoot = (0, import_node_path7.join)(home, ".claude", "skills");
1651
+ const codexRoot = (0, import_node_path7.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path7.join)(home, ".codex"), "skills");
1244
1652
  targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
1245
1653
  for (const harness of harnesses) {
1246
1654
  const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
1247
- planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path6.dirname)((0, import_node_path6.dirname)(codexRoot)));
1655
+ planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path7.dirname)((0, import_node_path7.dirname)(codexRoot)));
1248
1656
  rememberTarget(harness, skillRoot);
1249
1657
  }
1250
1658
  } else {
1251
- const sharedRoot = (0, import_node_path6.join)(root, ".agents", "skills");
1659
+ const sharedRoot = (0, import_node_path7.join)(root, ".agents", "skills");
1252
1660
  planSkillTree(sharedRoot);
1253
- const claudeRoot = (0, import_node_path6.join)(root, ".claude", "skills");
1661
+ const claudeRoot = (0, import_node_path7.join)(root, ".claude", "skills");
1254
1662
  targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
1255
1663
  for (const harness of harnesses) rememberTarget(harness, sharedRoot);
1256
1664
  if (harnesses.includes("claude")) {
1257
1665
  for (const skill of skillNames(files)) {
1258
- const canonical = (0, import_node_fs6.readFileSync)((0, import_node_path6.join)(sourceDir, skill, "SKILL.md"), "utf8");
1259
- plan((0, import_node_path6.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
1666
+ const canonical = (0, import_node_fs8.readFileSync)((0, import_node_path7.join)(sourceDir, skill, "SKILL.md"), "utf8");
1667
+ plan((0, import_node_path7.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
1260
1668
  }
1261
1669
  rememberTarget("claude", claudeRoot);
1262
1670
  }
1263
1671
  if (harnesses.includes("cursor")) {
1264
- const cursorRule = (0, import_node_path6.join)(root, ".cursor", "rules", "odla.mdc");
1672
+ const cursorRule = (0, import_node_path7.join)(root, ".cursor", "rules", "odla.mdc");
1265
1673
  plan(cursorRule, CURSOR_RULE);
1266
1674
  rememberTarget("cursor", cursorRule);
1267
1675
  }
1268
1676
  if (harnesses.includes("agents")) {
1269
- const agentsFile = (0, import_node_path6.join)(root, "AGENTS.md");
1677
+ const agentsFile = (0, import_node_path7.join)(root, "AGENTS.md");
1270
1678
  plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
1271
1679
  rememberTarget("agents", agentsFile);
1272
1680
  }
1273
1681
  if (harnesses.includes("copilot")) {
1274
- const copilotFile = (0, import_node_path6.join)(root, ".github", "copilot-instructions.md");
1682
+ const copilotFile = (0, import_node_path7.join)(root, ".github", "copilot-instructions.md");
1275
1683
  plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
1276
1684
  rememberTarget("copilot", copilotFile);
1277
1685
  }
1278
1686
  if (harnesses.includes("gemini")) {
1279
- const geminiFile = (0, import_node_path6.join)(root, "GEMINI.md");
1687
+ const geminiFile = (0, import_node_path7.join)(root, "GEMINI.md");
1280
1688
  plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
1281
1689
  rememberTarget("gemini", geminiFile);
1282
1690
  }
@@ -1290,11 +1698,11 @@ function installSkill(options = {}) {
1290
1698
  conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
1291
1699
  continue;
1292
1700
  }
1293
- if (!(0, import_node_fs6.existsSync)(file.target)) {
1701
+ if (!(0, import_node_fs8.existsSync)(file.target)) {
1294
1702
  writtenPaths.add(file.target);
1295
1703
  continue;
1296
1704
  }
1297
- const current = (0, import_node_fs6.readFileSync)(file.target, "utf8");
1705
+ const current = (0, import_node_fs8.readFileSync)(file.target, "utf8");
1298
1706
  if (current === file.content) {
1299
1707
  unchangedPaths.add(file.target);
1300
1708
  } else if (file.managedMerge || options.force) {
@@ -1311,9 +1719,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
1311
1719
  );
1312
1720
  }
1313
1721
  for (const file of plans.values()) {
1314
- if (!(0, import_node_fs6.existsSync)(file.target) || (0, import_node_fs6.readFileSync)(file.target, "utf8") !== file.content) {
1315
- (0, import_node_fs6.mkdirSync)((0, import_node_path6.dirname)(file.target), { recursive: true });
1316
- (0, import_node_fs6.writeFileSync)(file.target, file.content);
1722
+ if (!(0, import_node_fs8.existsSync)(file.target) || (0, import_node_fs8.readFileSync)(file.target, "utf8") !== file.content) {
1723
+ (0, import_node_fs8.mkdirSync)((0, import_node_path7.dirname)(file.target), { recursive: true });
1724
+ (0, import_node_fs8.writeFileSync)(file.target, file.content);
1317
1725
  }
1318
1726
  }
1319
1727
  const skills = skillNames(files);
@@ -1332,7 +1740,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
1332
1740
  };
1333
1741
  }
1334
1742
  function pathsUnder(root, paths) {
1335
- 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();
1743
+ 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();
1336
1744
  }
1337
1745
  function normalizeHarnesses(values, global) {
1338
1746
  const requested = values?.length ? values : ["claude"];
@@ -1354,9 +1762,9 @@ function normalizeHarnesses(values, global) {
1354
1762
  function managedFileContent(path, block, force, boundary) {
1355
1763
  const symlink = symlinkedComponent(boundary, path);
1356
1764
  if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
1357
- if (!(0, import_node_fs6.existsSync)(path)) return `${block}
1765
+ if (!(0, import_node_fs8.existsSync)(path)) return `${block}
1358
1766
  `;
1359
- const current = (0, import_node_fs6.readFileSync)(path, "utf8");
1767
+ const current = (0, import_node_fs8.readFileSync)(path, "utf8");
1360
1768
  const start = "<!-- odla-ai agent setup:start -->";
1361
1769
  const end = "<!-- odla-ai agent setup:end -->";
1362
1770
  const startAt = current.indexOf(start);
@@ -1377,15 +1785,15 @@ function managedFileContent(path, block, force, boundary) {
1377
1785
  return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
1378
1786
  }
1379
1787
  function symlinkedComponent(boundary, target) {
1380
- const rel = (0, import_node_path6.relative)(boundary, target);
1381
- if (rel === ".." || rel.startsWith(`..${import_node_path6.sep}`) || (0, import_node_path6.isAbsolute)(rel)) {
1788
+ const rel = (0, import_node_path7.relative)(boundary, target);
1789
+ if (rel === ".." || rel.startsWith(`..${import_node_path7.sep}`) || (0, import_node_path7.isAbsolute)(rel)) {
1382
1790
  throw new Error(`agent setup target escapes its install root: ${target}`);
1383
1791
  }
1384
1792
  let current = boundary;
1385
- for (const part of rel.split(import_node_path6.sep).filter(Boolean)) {
1386
- current = (0, import_node_path6.join)(current, part);
1793
+ for (const part of rel.split(import_node_path7.sep).filter(Boolean)) {
1794
+ current = (0, import_node_path7.join)(current, part);
1387
1795
  try {
1388
- if ((0, import_node_fs6.lstatSync)(current).isSymbolicLink()) return current;
1796
+ if ((0, import_node_fs8.lstatSync)(current).isSymbolicLink()) return current;
1389
1797
  } catch (error) {
1390
1798
  if (error.code !== "ENOENT") throw error;
1391
1799
  }
@@ -1396,13 +1804,13 @@ function skillNames(files) {
1396
1804
  return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
1397
1805
  }
1398
1806
  function listFiles(dir) {
1399
- if (!(0, import_node_fs6.existsSync)(dir)) return [];
1807
+ if (!(0, import_node_fs8.existsSync)(dir)) return [];
1400
1808
  const results = [];
1401
1809
  const walk = (current) => {
1402
- for (const entry of (0, import_node_fs6.readdirSync)(current, { withFileTypes: true })) {
1403
- const path = (0, import_node_path6.join)(current, entry.name);
1810
+ for (const entry of (0, import_node_fs8.readdirSync)(current, { withFileTypes: true })) {
1811
+ const path = (0, import_node_path7.join)(current, entry.name);
1404
1812
  if (entry.isDirectory()) walk(path);
1405
- else results.push((0, import_node_path6.relative)(dir, path));
1813
+ else results.push((0, import_node_path7.relative)(dir, path));
1406
1814
  }
1407
1815
  };
1408
1816
  walk(dir);
@@ -1527,6 +1935,99 @@ async function runCli(argv = process.argv.slice(2)) {
1527
1935
  printCapabilities(parsed.options.json === true);
1528
1936
  return;
1529
1937
  }
1938
+ if (command === "admin") {
1939
+ const area = parsed.positionals[1];
1940
+ const action = parsed.positionals[2];
1941
+ const credentialSet = action === "credential" && parsed.positionals[3] === "set";
1942
+ const credentials = action === "credentials";
1943
+ if (area !== "ai" || action !== "show" && action !== "set" && !credentialSet && !credentials) {
1944
+ throw new Error('unknown admin command. Try "odla-ai admin ai show".');
1945
+ }
1946
+ assertArgs(parsed, [
1947
+ "platform",
1948
+ "token",
1949
+ "json",
1950
+ "open",
1951
+ "provider",
1952
+ "model",
1953
+ "enabled",
1954
+ "max-input-bytes",
1955
+ "max-output-tokens",
1956
+ "max-calls-per-run",
1957
+ "from-env",
1958
+ "stdin"
1959
+ ], credentialSet ? 5 : action === "set" ? 4 : 3);
1960
+ await adminAi({
1961
+ action: credentialSet ? "credential-set" : credentials ? "credentials" : action,
1962
+ purpose: action === "set" ? parsed.positionals[3] : void 0,
1963
+ provider: stringOpt(parsed.options.provider),
1964
+ model: stringOpt(parsed.options.model),
1965
+ enabled: boolOpt(parsed.options.enabled),
1966
+ maxInputBytes: numberOpt(parsed.options["max-input-bytes"], "--max-input-bytes"),
1967
+ maxOutputTokens: numberOpt(parsed.options["max-output-tokens"], "--max-output-tokens"),
1968
+ maxCallsPerRun: numberOpt(parsed.options["max-calls-per-run"], "--max-calls-per-run"),
1969
+ platform: stringOpt(parsed.options.platform),
1970
+ token: stringOpt(parsed.options.token),
1971
+ json: parsed.options.json === true,
1972
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
1973
+ credentialProvider: credentialSet ? parsed.positionals[4] : void 0,
1974
+ fromEnv: stringOpt(parsed.options["from-env"]),
1975
+ stdin: parsed.options.stdin === true
1976
+ });
1977
+ return;
1978
+ }
1979
+ if (command === "security") {
1980
+ const sub = parsed.positionals[1];
1981
+ if (sub !== "run") throw new Error('unknown security command. Try "odla-ai security run".');
1982
+ assertArgs(parsed, [
1983
+ "config",
1984
+ "env",
1985
+ "out",
1986
+ "profile",
1987
+ "max-hunt-tasks",
1988
+ "run-id",
1989
+ "platform",
1990
+ "self",
1991
+ "ack-redacted-source",
1992
+ "open",
1993
+ "fail-on",
1994
+ "fail-on-candidates",
1995
+ "allow-incomplete"
1996
+ ], 3);
1997
+ const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
1998
+ const selfAudit = parsed.options.self === true;
1999
+ const open = parsed.options.open === false ? false : parsed.options.open === true ? true : void 0;
2000
+ const platform = stringOpt(parsed.options.platform);
2001
+ const result = await runHostedSecurity({
2002
+ configPath,
2003
+ selfAudit,
2004
+ target: parsed.positionals[2],
2005
+ out: stringOpt(parsed.options.out),
2006
+ env: stringOpt(parsed.options.env),
2007
+ profile: securityProfile(stringOpt(parsed.options.profile)),
2008
+ maxHuntTasks: numberOpt(parsed.options["max-hunt-tasks"], "--max-hunt-tasks"),
2009
+ runId: stringOpt(parsed.options["run-id"]),
2010
+ platform,
2011
+ sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
2012
+ getToken: async (request) => {
2013
+ if (request.scope === "platform:security:self") {
2014
+ return getScopedPlatformToken({ platform: request.platform, scope: request.scope, open });
2015
+ }
2016
+ const cfg = await loadProjectConfig(configPath);
2017
+ return getDeveloperToken(cfg, { configPath, open }, fetch, console);
2018
+ }
2019
+ });
2020
+ const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
2021
+ const candidateValue = parsed.options["fail-on-candidates"];
2022
+ const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
2023
+ const confirmed = (0, import_security2.findingsAtOrAbove)(result.report, failOn);
2024
+ const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(result.report, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
2025
+ const incomplete = result.report.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
2026
+ if (confirmed.length || leads.length || incomplete) {
2027
+ throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
2028
+ }
2029
+ return;
2030
+ }
1530
2031
  if (command === "provision") {
1531
2032
  assertArgs(
1532
2033
  parsed,
@@ -1607,71 +2108,14 @@ async function runCli(argv = process.argv.slice(2)) {
1607
2108
  }
1608
2109
  throw new Error(`unknown command "${command}". Run "odla-ai help".`);
1609
2110
  }
1610
- function assertArgs(parsed, allowedOptions, maxPositionals) {
1611
- const allowed = new Set(allowedOptions);
1612
- for (const name of Object.keys(parsed.options)) {
1613
- if (!allowed.has(name)) {
1614
- throw new Error(`unknown option "--${name}"; run "odla-ai help" for supported options`);
1615
- }
1616
- }
1617
- if (parsed.positionals.length > maxPositionals) {
1618
- throw new Error(`unexpected argument "${parsed.positionals[maxPositionals]}"; run "odla-ai help"`);
1619
- }
1620
- }
1621
- function parseArgv(argv) {
1622
- const positionals = [];
1623
- const options = {};
1624
- for (let i = 0; i < argv.length; i++) {
1625
- const arg = argv[i];
1626
- if (!arg.startsWith("--")) {
1627
- positionals.push(arg);
1628
- continue;
1629
- }
1630
- const eq = arg.indexOf("=");
1631
- const rawName = arg.slice(2, eq === -1 ? void 0 : eq);
1632
- if (!rawName) continue;
1633
- if (rawName.startsWith("no-")) {
1634
- options[rawName.slice(3)] = false;
1635
- continue;
1636
- }
1637
- if (eq !== -1) {
1638
- addOption(options, rawName, arg.slice(eq + 1));
1639
- continue;
1640
- }
1641
- const value = argv[i + 1];
1642
- if (value !== void 0 && !value.startsWith("--")) {
1643
- i++;
1644
- addOption(options, rawName, value);
1645
- } else {
1646
- addOption(options, rawName, true);
1647
- }
1648
- }
1649
- return { positionals, options };
1650
- }
1651
- function addOption(options, name, value) {
1652
- const cur = options[name];
1653
- if (cur === void 0) {
1654
- options[name] = value;
1655
- } else if (Array.isArray(cur)) {
1656
- cur.push(String(value));
1657
- } else {
1658
- options[name] = [String(cur), String(value)];
1659
- }
1660
- }
1661
- function requiredString(value, name) {
1662
- const s = stringOpt(value);
1663
- if (!s) throw new Error(`${name} is required`);
1664
- return s;
1665
- }
1666
- function stringOpt(value) {
1667
- if (typeof value === "string") return value;
1668
- if (Array.isArray(value)) return value[value.length - 1];
1669
- return void 0;
2111
+ function securityProfile(value) {
2112
+ if (value === void 0) return void 0;
2113
+ if (value === "odla" || value === "cloudflare-app" || value === "generic") return value;
2114
+ throw new Error("--profile must be odla, cloudflare-app, or generic");
1670
2115
  }
1671
- function listOpt(value) {
1672
- if (value === void 0 || value === false || value === true) return void 0;
1673
- const values = Array.isArray(value) ? value : [value];
1674
- return values.flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
2116
+ function severityOpt(value, flag) {
2117
+ if (["informational", "low", "medium", "high", "critical"].includes(value)) return value;
2118
+ throw new Error(`${flag} must be informational, low, medium, high, or critical`);
1675
2119
  }
1676
2120
  function harnessList(parsed) {
1677
2121
  const selected = [
@@ -1688,57 +2132,21 @@ function harnessOption(value, flag) {
1688
2132
  if (parts.some((item) => !item.trim())) throw new Error(`${flag} requires a harness name`);
1689
2133
  return parts.map((item) => item.trim());
1690
2134
  }
1691
- function cliVersion() {
1692
- const pkg = JSON.parse((0, import_node_fs7.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
1693
- return pkg.version ?? "unknown";
1694
- }
1695
- function printHelp() {
1696
- console.log(`odla-ai
1697
-
1698
- Usage:
1699
- odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
1700
- odla-ai init --app-id <id> --name <name> [--services db,ai,o11y] [--env dev --env prod]
1701
- odla-ai doctor [--config odla.config.mjs]
1702
- odla-ai capabilities [--json]
1703
- odla-ai provision [--config odla.config.mjs] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
1704
- odla-ai smoke [--config odla.config.mjs] [--env dev]
1705
- odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
1706
- odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
1707
- odla-ai version
1708
-
1709
- Commands:
1710
- setup Install offline odla runbooks for common coding-agent harnesses.
1711
- init Create a generic odla.config.mjs plus starter schema/rules files.
1712
- doctor Validate and summarize the project config without network calls.
1713
- capabilities Show what the CLI automates vs agent edits and human checkpoints.
1714
- provision Register services, configure them, persist credentials, optionally push secrets.
1715
- smoke Verify local credentials, public-config, live schema, and db aggregate.
1716
- skill Same installer; --agent accepts all, claude, codex, cursor,
1717
- copilot, gemini, or agents (repeatable or comma-separated).
1718
- secrets Push configured db/o11y secrets into the Worker via wrangler stdin.
1719
- version Print the CLI version.
1720
-
1721
- Safety:
1722
- New projects target dev only. Add prod explicitly to odla.config.mjs and pass
1723
- --yes to provision it; use --dry-run first to inspect the resolved plan.
1724
- Provision caches the approved developer token and service credentials under
1725
- .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
1726
- preflights Wrangler before any shown-once issuance or destructive rotation.
1727
- Provision opens the approval page automatically in interactive terminals;
1728
- use --open to force browser launch or --no-open to suppress it.
1729
- `);
1730
- }
1731
2135
  // Annotate the CommonJS export names for ESM import in node:
1732
2136
  0 && (module.exports = {
1733
2137
  AGENT_HARNESSES,
1734
2138
  CAPABILITIES,
2139
+ SYSTEM_AI_PURPOSES,
2140
+ adminAi,
1735
2141
  doctor,
2142
+ getScopedPlatformToken,
1736
2143
  initProject,
1737
2144
  installSkill,
1738
2145
  printCapabilities,
1739
2146
  provision,
1740
2147
  redactSecrets,
1741
2148
  runCli,
2149
+ runHostedSecurity,
1742
2150
  secretsPush,
1743
2151
  smoke
1744
2152
  });