@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.
@@ -1,5 +1,382 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/admin-ai.ts
4
+ import process4 from "process";
5
+ import { existsSync as existsSync2 } from "fs";
6
+ import { requestToken as requestToken2 } from "@odla-ai/db";
7
+
8
+ // src/open.ts
9
+ import { spawn } from "child_process";
10
+ import process2 from "process";
11
+ async function openUrl(url, options = {}) {
12
+ const command = openerFor(options.platform ?? process2.platform);
13
+ const doSpawn = options.spawnImpl ?? spawn;
14
+ await new Promise((resolve7, reject) => {
15
+ const child = doSpawn(command.cmd, [...command.args, url], {
16
+ stdio: "ignore",
17
+ detached: true
18
+ });
19
+ child.once("error", reject);
20
+ child.once("spawn", () => {
21
+ child.unref();
22
+ resolve7();
23
+ });
24
+ });
25
+ }
26
+ function openerFor(platform) {
27
+ if (platform === "darwin") return { cmd: "open", args: [] };
28
+ if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
29
+ return { cmd: "xdg-open", args: [] };
30
+ }
31
+
32
+ // src/token.ts
33
+ import { requestToken } from "@odla-ai/db";
34
+ import process3 from "process";
35
+
36
+ // src/local.ts
37
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
38
+ import { dirname, isAbsolute, relative, resolve } from "path";
39
+ var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
40
+ function readJsonFile(path) {
41
+ try {
42
+ return JSON.parse(readFileSync(path, "utf8"));
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+ function writePrivateJson(path, value) {
48
+ writePrivateText(path, `${JSON.stringify(value, null, 2)}
49
+ `);
50
+ }
51
+ function readCredentials(path) {
52
+ if (!existsSync(path)) return null;
53
+ let value;
54
+ try {
55
+ value = JSON.parse(readFileSync(path, "utf8"));
56
+ } catch {
57
+ throw new Error(`credentials file ${path} is not valid JSON; fix or remove it before provisioning`);
58
+ }
59
+ if (!value || typeof value !== "object" || typeof value.appId !== "string" || !value.envs || typeof value.envs !== "object") {
60
+ throw new Error(`credentials file ${path} has an invalid shape; fix or remove it before provisioning`);
61
+ }
62
+ return value;
63
+ }
64
+ function mergeCredential(current, update) {
65
+ const next = current ?? {
66
+ appId: update.appId,
67
+ platformUrl: update.platformUrl,
68
+ dbEndpoint: update.dbEndpoint,
69
+ updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
70
+ envs: {}
71
+ };
72
+ next.appId = update.appId;
73
+ next.platformUrl = update.platformUrl;
74
+ next.dbEndpoint = update.dbEndpoint;
75
+ next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
76
+ next.envs[update.env] = {
77
+ ...next.envs[update.env] ?? {},
78
+ tenantId: update.tenantId,
79
+ ...update.dbKey ? { dbKey: update.dbKey } : {},
80
+ ...update.o11yToken ? { o11yToken: update.o11yToken } : {}
81
+ };
82
+ return next;
83
+ }
84
+ function ensureGitignore(rootDir, localPaths = []) {
85
+ const path = resolve(rootDir, ".gitignore");
86
+ const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
87
+ const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line) => !!line);
88
+ const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
89
+ const missing = wanted.filter((line) => !existing.split(/\r?\n/).includes(line));
90
+ if (missing.length === 0) return;
91
+ const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
92
+ writeFileSync(path, `${existing}${prefix}${missing.join("\n")}
93
+ `);
94
+ }
95
+ function o11yDevVars(cfg) {
96
+ if (!cfg.services.includes("o11y")) return void 0;
97
+ return {
98
+ endpoint: cfg.o11y?.endpoint ?? "https://o11y.odla.ai",
99
+ service: cfg.o11y?.service ?? cfg.app.id,
100
+ ...cfg.o11y?.version ? { version: cfg.o11y.version } : {}
101
+ };
102
+ }
103
+ function resolveWriteDevVarsTarget(cfg, requested) {
104
+ if (!requested) return null;
105
+ if (requested === true) return cfg.local.devVarsFile;
106
+ return resolve(dirname(cfg.configPath), requested);
107
+ }
108
+ function writeDevVars(path, credentials, env, o11y) {
109
+ const entry = credentials.envs[env];
110
+ if (!entry) throw new Error(`no credentials for env "${env}" in ${path}`);
111
+ const lines = [`ODLA_PLATFORM="${credentials.platformUrl}"`];
112
+ if (entry.dbKey) lines.push(`ODLA_ENDPOINT="${credentials.dbEndpoint}"`);
113
+ lines.push(`ODLA_APP_ID="${credentials.appId}"`, `ODLA_ENV="${env}"`, `ODLA_TENANT="${entry.tenantId}"`);
114
+ if (entry.dbKey) lines.push(`ODLA_API_KEY="${entry.dbKey}"`);
115
+ if (o11y) {
116
+ lines.push(`ODLA_O11Y_ENDPOINT="${o11y.endpoint}"`, `ODLA_O11Y_SERVICE="${o11y.service}"`);
117
+ if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
118
+ if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
119
+ }
120
+ const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
121
+ const retained = existing.split(/\r?\n/).filter((line) => !isManagedDevVar(line));
122
+ while (retained.at(-1) === "") retained.pop();
123
+ const prefix = retained.length ? `${retained.join("\n")}
124
+
125
+ ` : "";
126
+ writePrivateText(path, `${prefix}${lines.join("\n")}
127
+ `);
128
+ }
129
+ var MANAGED_DEV_VARS = /* @__PURE__ */ new Set([
130
+ "ODLA_PLATFORM",
131
+ "ODLA_ENDPOINT",
132
+ "ODLA_APP_ID",
133
+ "ODLA_ENV",
134
+ "ODLA_TENANT",
135
+ "ODLA_API_KEY",
136
+ "ODLA_O11Y_ENDPOINT",
137
+ "ODLA_O11Y_SERVICE",
138
+ "ODLA_O11Y_VERSION",
139
+ "ODLA_O11Y_TOKEN"
140
+ ]);
141
+ function isManagedDevVar(line) {
142
+ const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
143
+ return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
144
+ }
145
+ function writePrivateText(path, text) {
146
+ mkdirSync(dirname(path), { recursive: true });
147
+ const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
148
+ writeFileSync(temporary, text, { mode: 384 });
149
+ chmodSync(temporary, 384);
150
+ renameSync(temporary, path);
151
+ }
152
+ function gitignoreEntry(rootDir, path) {
153
+ const rel = relative(resolve(rootDir), resolve(path));
154
+ if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(rel)) return null;
155
+ return rel.replaceAll("\\", "/");
156
+ }
157
+ function displayPath(path, rootDir = process.cwd()) {
158
+ const rel = relative(rootDir, path);
159
+ return rel && !rel.startsWith("..") ? rel : path;
160
+ }
161
+
162
+ // src/token.ts
163
+ async function getDeveloperToken(cfg, options, doFetch, out) {
164
+ if (options.token) return options.token;
165
+ if (process3.env.ODLA_DEV_TOKEN) return process3.env.ODLA_DEV_TOKEN;
166
+ const cached = readJsonFile(cfg.local.tokenFile);
167
+ if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
168
+ out.log(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
169
+ return cached.token;
170
+ }
171
+ const browser = approvalBrowser(options);
172
+ const { token, expiresAt } = await requestToken({
173
+ endpoint: cfg.platformUrl,
174
+ label: `${cfg.app.id} provisioner`,
175
+ fetch: doFetch,
176
+ onCode: async ({ userCode, expiresIn }) => {
177
+ const approvalUrl = handshakeUrl(cfg.platformUrl, userCode);
178
+ out.log("");
179
+ out.log(`Approve code ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
180
+ if (browser.open) {
181
+ try {
182
+ await (options.openApprovalUrl ?? openUrl)(approvalUrl);
183
+ out.log(`auth: opened browser for approval${browser.mode === "auto" ? " (auto)" : ""}`);
184
+ } catch (err) {
185
+ out.log(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
186
+ }
187
+ } else if (browser.reason) {
188
+ out.log(`auth: browser launch skipped (${browser.reason})`);
189
+ }
190
+ out.log("");
191
+ }
192
+ });
193
+ writePrivateJson(cfg.local.tokenFile, { token, expiresAt });
194
+ out.log(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
195
+ return token;
196
+ }
197
+ function approvalBrowser(options) {
198
+ if (options.open === true) return { open: true, mode: "forced" };
199
+ if (options.open === false) return { open: false, reason: "disabled by --no-open" };
200
+ if (process3.env.CI) return { open: false, reason: "CI environment" };
201
+ const interactive = options.interactive ?? Boolean(process3.stdin.isTTY && process3.stdout.isTTY);
202
+ if (!interactive) return { open: false, reason: "non-interactive shell; pass --open to force" };
203
+ return { open: true, mode: "auto" };
204
+ }
205
+ function handshakeUrl(platformUrl, userCode) {
206
+ const url = new URL("/studio", platformUrl);
207
+ url.searchParams.set("code", userCode);
208
+ return url.toString();
209
+ }
210
+
211
+ // src/admin-ai.ts
212
+ var SYSTEM_AI_PURPOSES = ["o11y.triage", "security.discovery", "security.validation"];
213
+ async function getScopedPlatformToken(options) {
214
+ const fromEnv = process4.env.ODLA_ADMIN_TOKEN;
215
+ if (fromEnv) return fromEnv;
216
+ return scopedToken(
217
+ options.platform.replace(/\/$/, ""),
218
+ options.scope,
219
+ { action: "show", ...options },
220
+ options.fetch ?? fetch,
221
+ options.stdout ?? console
222
+ );
223
+ }
224
+ async function adminAi(options) {
225
+ const platform = (options.platform ?? process4.env.ODLA_PLATFORM ?? "https://odla.ai").replace(/\/$/, "");
226
+ const doFetch = options.fetch ?? fetch;
227
+ const out = options.stdout ?? console;
228
+ const scope = options.action === "set" || options.action === "credential-set" ? "platform:ai:write" : "platform:ai:read";
229
+ const token = options.token ?? process4.env.ODLA_ADMIN_TOKEN ?? await scopedToken(platform, scope, options, doFetch, out);
230
+ const headers = { authorization: `Bearer ${token}`, "content-type": "application/json" };
231
+ if (options.action === "credentials") {
232
+ const res2 = await doFetch(`${platform}/registry/platform/ai-credentials`, { headers });
233
+ const body2 = await responseBody(res2);
234
+ if (!res2.ok) throw new Error(apiError("read platform AI credential status", res2.status, body2));
235
+ const credentials = isRecord(body2) && isRecord(body2.credentials) ? body2.credentials : body2;
236
+ if (options.json) out.log(JSON.stringify({ credentials }, null, 2));
237
+ else for (const [provider, status] of Object.entries(isRecord(credentials) ? credentials : {})) {
238
+ out.log(`${provider} ${isRecord(status) && status.set === true ? "set" : "not set"}`);
239
+ }
240
+ return;
241
+ }
242
+ if (options.action === "credential-set") {
243
+ const provider = requireProvider(options.credentialProvider);
244
+ const value = await credentialValue(options);
245
+ const res2 = await doFetch(`${platform}/registry/platform/ai-credentials/${encodeURIComponent(provider)}`, {
246
+ method: "PUT",
247
+ headers,
248
+ body: JSON.stringify({ value })
249
+ });
250
+ const body2 = await responseBody(res2);
251
+ if (!res2.ok) throw new Error(apiError(`store ${provider} platform credential`, res2.status, body2));
252
+ out.log(`stored ${provider} platform credential (write-only; value was not returned)`);
253
+ return;
254
+ }
255
+ if (options.action === "show") {
256
+ const res2 = await doFetch(`${platform}/registry/platform/ai-policies`, { headers });
257
+ const body2 = await responseBody(res2);
258
+ if (!res2.ok) throw new Error(apiError("read platform AI policies", res2.status, body2));
259
+ const policies = policyArray(body2);
260
+ if (options.json) {
261
+ out.log(JSON.stringify({ policies }, null, 2));
262
+ return;
263
+ }
264
+ out.log("purpose enabled provider model max calls max input bytes max output tokens");
265
+ for (const policy2 of policies) {
266
+ out.log([
267
+ policy2.purpose,
268
+ String(policy2.enabled),
269
+ policy2.provider,
270
+ policy2.model,
271
+ String(policy2.maxCallsPerRun ?? ""),
272
+ String(policy2.maxInputBytes ?? ""),
273
+ String(policy2.maxOutputTokens ?? "")
274
+ ].join(" "));
275
+ }
276
+ return;
277
+ }
278
+ const purpose = requirePurpose(options.purpose);
279
+ if (!options.provider?.trim()) throw new Error("--provider is required");
280
+ if (!options.model?.trim()) throw new Error("--model is required");
281
+ const body = {
282
+ provider: options.provider.trim(),
283
+ model: options.model.trim(),
284
+ ...options.enabled === void 0 ? {} : { enabled: options.enabled },
285
+ ...options.maxInputBytes === void 0 ? {} : { maxInputBytes: options.maxInputBytes },
286
+ ...options.maxOutputTokens === void 0 ? {} : { maxOutputTokens: options.maxOutputTokens },
287
+ ...options.maxCallsPerRun === void 0 ? {} : { maxCallsPerRun: options.maxCallsPerRun }
288
+ };
289
+ const res = await doFetch(`${platform}/registry/platform/ai-policies/${encodeURIComponent(purpose)}`, {
290
+ method: "PUT",
291
+ headers,
292
+ body: JSON.stringify(body)
293
+ });
294
+ const response = await responseBody(res);
295
+ if (!res.ok) throw new Error(apiError(`update ${purpose}`, res.status, response));
296
+ const policy = isRecord(response) && isRecord(response.policy) ? response.policy : isRecord(response) ? response : {};
297
+ out.log(options.json ? JSON.stringify({ policy }, null, 2) : `updated ${purpose}: ${String(policy.provider ?? body.provider)}/${String(policy.model ?? body.model)}`);
298
+ }
299
+ async function scopedToken(platform, scope, options, doFetch, out) {
300
+ const tokenFile = options.tokenFile ?? `${process4.cwd()}/.odla/admin-token.local.json`;
301
+ const cache = readJsonFile(tokenFile);
302
+ const cached = cache?.platform === platform ? cache.tokens?.[scope] : void 0;
303
+ if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
304
+ out.log(`auth: using cached ${scope} grant (${tokenFile})`);
305
+ return cached.token;
306
+ }
307
+ const { token, expiresAt } = await requestToken2({
308
+ endpoint: platform,
309
+ label: `odla CLI admin AI (${scope})`,
310
+ scopes: [scope],
311
+ fetch: doFetch,
312
+ onCode: async ({ userCode, expiresIn }) => {
313
+ const approvalUrl = handshakeUrl(platform, userCode);
314
+ out.log(`Approve scoped request ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
315
+ const shouldOpen = options.open === true || options.open !== false && !process4.env.CI && Boolean(process4.stdin.isTTY && process4.stdout.isTTY);
316
+ if (shouldOpen) await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
317
+ }
318
+ });
319
+ const tokens = cache?.platform === platform ? { ...cache.tokens ?? {} } : {};
320
+ tokens[scope] = { token, expiresAt };
321
+ if (existsSync2(`${process4.cwd()}/.git`)) ensureGitignore(process4.cwd(), [tokenFile]);
322
+ writePrivateJson(tokenFile, { platform, tokens });
323
+ out.log(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
324
+ return token;
325
+ }
326
+ function requirePurpose(value) {
327
+ if (!SYSTEM_AI_PURPOSES.includes(value)) {
328
+ throw new Error(`purpose must be one of: ${SYSTEM_AI_PURPOSES.join(", ")}`);
329
+ }
330
+ return value;
331
+ }
332
+ function requireProvider(value) {
333
+ if (value !== "anthropic" && value !== "openai" && value !== "google") {
334
+ throw new Error("provider must be one of: anthropic, openai, google");
335
+ }
336
+ return value;
337
+ }
338
+ async function credentialValue(options) {
339
+ if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
340
+ let value;
341
+ if (options.fromEnv) value = process4.env[options.fromEnv];
342
+ else if (options.stdin) value = await (options.readStdin ?? readStdin)();
343
+ else throw new Error("credential input required: use --from-env <NAME> or --stdin; values are never accepted as arguments");
344
+ value = value?.replace(/[\r\n]+$/, "");
345
+ if (!value) throw new Error(options.fromEnv ? `environment variable ${options.fromEnv} is empty or unset` : "stdin credential is empty");
346
+ if (new TextEncoder().encode(value).byteLength > 64 * 1024) throw new Error("credential exceeds 64 KiB");
347
+ return value;
348
+ }
349
+ async function readStdin() {
350
+ let value = "";
351
+ for await (const chunk of process4.stdin) {
352
+ value += String(chunk);
353
+ if (value.length > 64 * 1024) throw new Error("credential exceeds 64 KiB");
354
+ }
355
+ return value;
356
+ }
357
+ function policyArray(body) {
358
+ if (isRecord(body) && Array.isArray(body.policies)) return body.policies.filter(isRecord);
359
+ if (isRecord(body) && isRecord(body.policies)) return Object.values(body.policies).filter(isRecord);
360
+ throw new Error("platform returned an invalid AI policy response");
361
+ }
362
+ async function responseBody(res) {
363
+ const text = await res.text();
364
+ if (!text) return {};
365
+ try {
366
+ return JSON.parse(text);
367
+ } catch {
368
+ return { message: text.slice(0, 300) };
369
+ }
370
+ }
371
+ function apiError(action, status, body) {
372
+ const error = isRecord(body) && isRecord(body.error) ? body.error : void 0;
373
+ const message = error && typeof error.message === "string" ? error.message : isRecord(body) && typeof body.message === "string" ? body.message : "request failed";
374
+ return `${action} failed (${status}): ${message}`;
375
+ }
376
+ function isRecord(value) {
377
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
378
+ }
379
+
3
380
  // src/capabilities.ts
4
381
  var CAPABILITIES = {
5
382
  cli: [
@@ -7,7 +384,9 @@ var CAPABILITIES = {
7
384
  "issue, persist, and explicitly rotate configured service credentials",
8
385
  "write local Worker values and push deployed Worker secrets through Wrangler stdin",
9
386
  "push db schema/rules and configure platform AI, auth, and deployment links",
10
- "validate config offline and smoke-test a provisioned db environment"
387
+ "validate config offline and smoke-test a provisioned db environment",
388
+ "run app-attributed hosted security discovery and independent validation without provider keys",
389
+ "let admins manage system AI routes/credentials through short-lived exact-scope approval"
11
390
  ],
12
391
  agent: [
13
392
  "install and import the selected odla SDKs",
@@ -18,11 +397,13 @@ var CAPABILITIES = {
18
397
  "approve the odla device code and one-off third-party logins",
19
398
  "consent to production changes with --yes",
20
399
  "request destructive credential rotation explicitly",
21
- "review application semantics, security findings, releases, and merges"
400
+ "review application semantics, security findings, releases, and merges",
401
+ "approve redacted-source disclosure before hosted security reasoning"
22
402
  ],
23
403
  studio: [
24
404
  "view telemetry and environment state",
25
- "perform manual credential recovery when the CLI's local shown-once copy is unavailable"
405
+ "perform manual credential recovery when the CLI's local shown-once copy is unavailable",
406
+ "configure system AI purposes and view app/environment/run-attributed usage"
26
407
  ]
27
408
  };
28
409
  function printCapabilities(json = false, out = console) {
@@ -62,28 +443,28 @@ function looksSecret(value) {
62
443
  }
63
444
 
64
445
  // src/config.ts
65
- import { existsSync, readFileSync } from "fs";
66
- import { dirname, isAbsolute, resolve } from "path";
446
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
447
+ import { dirname as dirname2, isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
67
448
  import { pathToFileURL } from "url";
68
449
  var DEFAULT_PLATFORM = "https://odla.ai";
69
450
  var DEFAULT_ENVS = ["dev"];
70
451
  var DEFAULT_SERVICES = ["db", "ai"];
71
452
  async function loadProjectConfig(configPath = "odla.config.mjs") {
72
- const resolved = resolve(configPath);
73
- if (!existsSync(resolved)) {
453
+ const resolved = resolve2(configPath);
454
+ if (!existsSync3(resolved)) {
74
455
  throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
75
456
  }
76
457
  const raw = await loadConfigModule(resolved);
77
- const rootDir = dirname(resolved);
458
+ const rootDir = dirname2(resolved);
78
459
  validateRawConfig(raw, resolved);
79
460
  const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
80
461
  const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
81
462
  const envs = unique(raw.envs?.length ? raw.envs : DEFAULT_ENVS);
82
463
  const services = unique(raw.services?.length ? raw.services : DEFAULT_SERVICES);
83
464
  const local = {
84
- tokenFile: resolve(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
85
- credentialsFile: resolve(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
86
- devVarsFile: resolve(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
465
+ tokenFile: resolve2(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
466
+ credentialsFile: resolve2(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
467
+ devVarsFile: resolve2(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
87
468
  gitignore: raw.local?.gitignore ?? true
88
469
  };
89
470
  return {
@@ -100,9 +481,9 @@ async function loadProjectConfig(configPath = "odla.config.mjs") {
100
481
  async function resolveDataExport(cfg, value, names) {
101
482
  if (value === void 0 || value === null || value === false) return void 0;
102
483
  if (typeof value !== "string") return value;
103
- const target = isAbsolute(value) ? value : resolve(cfg.rootDir, value);
484
+ const target = isAbsolute2(value) ? value : resolve2(cfg.rootDir, value);
104
485
  if (target.endsWith(".json")) {
105
- return JSON.parse(readFileSync(target, "utf8"));
486
+ return JSON.parse(readFileSync2(target, "utf8"));
106
487
  }
107
488
  const mod = await import(pathToFileURL(target).href);
108
489
  for (const name of names) {
@@ -153,7 +534,7 @@ function validId(value) {
153
534
  return typeof value === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value);
154
535
  }
155
536
  async function loadConfigModule(path) {
156
- if (path.endsWith(".json")) return JSON.parse(readFileSync(path, "utf8"));
537
+ if (path.endsWith(".json")) return JSON.parse(readFileSync2(path, "utf8"));
157
538
  const mod = await import(`${pathToFileURL(path).href}?t=${Date.now()}`);
158
539
  const value = mod.default ?? mod.config;
159
540
  if (typeof value === "function") return await value();
@@ -168,141 +549,15 @@ function unique(values) {
168
549
 
169
550
  // src/doctor-checks.ts
170
551
  import { execFileSync } from "child_process";
171
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
552
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
172
553
  import { join as join2, resolve as resolve3 } from "path";
173
554
 
174
- // src/local.ts
175
- import { chmodSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
176
- import { dirname as dirname2, isAbsolute as isAbsolute2, relative, resolve as resolve2 } from "path";
177
- var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
178
- function readJsonFile(path) {
179
- try {
180
- return JSON.parse(readFileSync2(path, "utf8"));
181
- } catch {
182
- return null;
183
- }
184
- }
185
- function writePrivateJson(path, value) {
186
- writePrivateText(path, `${JSON.stringify(value, null, 2)}
187
- `);
188
- }
189
- function readCredentials(path) {
190
- if (!existsSync2(path)) return null;
191
- let value;
192
- try {
193
- value = JSON.parse(readFileSync2(path, "utf8"));
194
- } catch {
195
- throw new Error(`credentials file ${path} is not valid JSON; fix or remove it before provisioning`);
196
- }
197
- if (!value || typeof value !== "object" || typeof value.appId !== "string" || !value.envs || typeof value.envs !== "object") {
198
- throw new Error(`credentials file ${path} has an invalid shape; fix or remove it before provisioning`);
199
- }
200
- return value;
201
- }
202
- function mergeCredential(current, update) {
203
- const next = current ?? {
204
- appId: update.appId,
205
- platformUrl: update.platformUrl,
206
- dbEndpoint: update.dbEndpoint,
207
- updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
208
- envs: {}
209
- };
210
- next.appId = update.appId;
211
- next.platformUrl = update.platformUrl;
212
- next.dbEndpoint = update.dbEndpoint;
213
- next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
214
- next.envs[update.env] = {
215
- ...next.envs[update.env] ?? {},
216
- tenantId: update.tenantId,
217
- ...update.dbKey ? { dbKey: update.dbKey } : {},
218
- ...update.o11yToken ? { o11yToken: update.o11yToken } : {}
219
- };
220
- return next;
221
- }
222
- function ensureGitignore(rootDir, localPaths = []) {
223
- const path = resolve2(rootDir, ".gitignore");
224
- const existing = existsSync2(path) ? readFileSync2(path, "utf8") : "";
225
- const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line) => !!line);
226
- const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
227
- const missing = wanted.filter((line) => !existing.split(/\r?\n/).includes(line));
228
- if (missing.length === 0) return;
229
- const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
230
- writeFileSync(path, `${existing}${prefix}${missing.join("\n")}
231
- `);
232
- }
233
- function o11yDevVars(cfg) {
234
- if (!cfg.services.includes("o11y")) return void 0;
235
- return {
236
- endpoint: cfg.o11y?.endpoint ?? "https://o11y.odla.ai",
237
- service: cfg.o11y?.service ?? cfg.app.id,
238
- ...cfg.o11y?.version ? { version: cfg.o11y.version } : {}
239
- };
240
- }
241
- function resolveWriteDevVarsTarget(cfg, requested) {
242
- if (!requested) return null;
243
- if (requested === true) return cfg.local.devVarsFile;
244
- return resolve2(dirname2(cfg.configPath), requested);
245
- }
246
- function writeDevVars(path, credentials, env, o11y) {
247
- const entry = credentials.envs[env];
248
- if (!entry) throw new Error(`no credentials for env "${env}" in ${path}`);
249
- const lines = [`ODLA_PLATFORM="${credentials.platformUrl}"`];
250
- if (entry.dbKey) lines.push(`ODLA_ENDPOINT="${credentials.dbEndpoint}"`);
251
- lines.push(`ODLA_APP_ID="${credentials.appId}"`, `ODLA_ENV="${env}"`, `ODLA_TENANT="${entry.tenantId}"`);
252
- if (entry.dbKey) lines.push(`ODLA_API_KEY="${entry.dbKey}"`);
253
- if (o11y) {
254
- lines.push(`ODLA_O11Y_ENDPOINT="${o11y.endpoint}"`, `ODLA_O11Y_SERVICE="${o11y.service}"`);
255
- if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
256
- if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
257
- }
258
- const existing = existsSync2(path) ? readFileSync2(path, "utf8") : "";
259
- const retained = existing.split(/\r?\n/).filter((line) => !isManagedDevVar(line));
260
- while (retained.at(-1) === "") retained.pop();
261
- const prefix = retained.length ? `${retained.join("\n")}
262
-
263
- ` : "";
264
- writePrivateText(path, `${prefix}${lines.join("\n")}
265
- `);
266
- }
267
- var MANAGED_DEV_VARS = /* @__PURE__ */ new Set([
268
- "ODLA_PLATFORM",
269
- "ODLA_ENDPOINT",
270
- "ODLA_APP_ID",
271
- "ODLA_ENV",
272
- "ODLA_TENANT",
273
- "ODLA_API_KEY",
274
- "ODLA_O11Y_ENDPOINT",
275
- "ODLA_O11Y_SERVICE",
276
- "ODLA_O11Y_VERSION",
277
- "ODLA_O11Y_TOKEN"
278
- ]);
279
- function isManagedDevVar(line) {
280
- const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
281
- return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
282
- }
283
- function writePrivateText(path, text) {
284
- mkdirSync(dirname2(path), { recursive: true });
285
- const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
286
- writeFileSync(temporary, text, { mode: 384 });
287
- chmodSync(temporary, 384);
288
- renameSync(temporary, path);
289
- }
290
- function gitignoreEntry(rootDir, path) {
291
- const rel = relative(resolve2(rootDir), resolve2(path));
292
- if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute2(rel)) return null;
293
- return rel.replaceAll("\\", "/");
294
- }
295
- function displayPath(path, rootDir = process.cwd()) {
296
- const rel = relative(rootDir, path);
297
- return rel && !rel.startsWith("..") ? rel : path;
298
- }
299
-
300
555
  // src/wrangler.ts
301
- import { spawn } from "child_process";
302
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
556
+ import { spawn as spawn2 } from "child_process";
557
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
303
558
  import { join } from "path";
304
559
  var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
305
- const child = spawn(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
560
+ const child = spawn2(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
306
561
  let stdout = "";
307
562
  let stderr = "";
308
563
  child.stdout.on("data", (chunk) => stdout += chunk.toString());
@@ -315,7 +570,7 @@ var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]
315
570
  function findWranglerConfig(rootDir) {
316
571
  for (const name of WRANGLER_CONFIG_FILES) {
317
572
  const path = join(rootDir, name);
318
- if (existsSync3(path)) return path;
573
+ if (existsSync4(path)) return path;
319
574
  }
320
575
  return null;
321
576
  }
@@ -423,7 +678,7 @@ function wranglerWarnings(rootDir) {
423
678
  const dir = resolve3(rootDir, assets.directory);
424
679
  if (dir === resolve3(rootDir)) {
425
680
  warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
426
- } else if (existsSync4(join2(dir, "node_modules"))) {
681
+ } else if (existsSync5(join2(dir, "node_modules"))) {
427
682
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
428
683
  }
429
684
  }
@@ -459,7 +714,7 @@ function o11yProjectWarnings(rootDir) {
459
714
  return warnings;
460
715
  }
461
716
  const main = typeof config.main === "string" ? resolve3(rootDir, config.main) : null;
462
- if (!main || !existsSync4(main)) {
717
+ if (!main || !existsSync5(main)) {
463
718
  warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
464
719
  } else {
465
720
  let source = "";
@@ -546,13 +801,13 @@ async function doctor(options) {
546
801
  }
547
802
 
548
803
  // src/init.ts
549
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
804
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
550
805
  import { dirname as dirname3, resolve as resolve4 } from "path";
551
806
  function initProject(options) {
552
807
  const out = options.stdout ?? console;
553
808
  const rootDir = resolve4(options.rootDir ?? process.cwd());
554
809
  const configPath = resolve4(rootDir, options.configPath ?? "odla.config.mjs");
555
- if (existsSync5(configPath) && !options.force) {
810
+ if (existsSync6(configPath) && !options.force) {
556
811
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
557
812
  }
558
813
  if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
@@ -573,7 +828,7 @@ function initProject(options) {
573
828
  out.log("updated .gitignore for local odla credentials");
574
829
  }
575
830
  function writeIfMissing(path, text) {
576
- if (existsSync5(path)) return;
831
+ if (existsSync6(path)) return;
577
832
  writeFileSync2(path, text);
578
833
  }
579
834
  function configTemplate(input) {
@@ -688,133 +943,56 @@ async function secretsPushImpl(options, preflight) {
688
943
  if (!entry) throw new Error(`no credentials for env "${env}" in ${credentialsPath} \u2014 run "odla-ai provision" first`);
689
944
  const secrets = [];
690
945
  if (cfg.services.includes("o11y")) {
691
- if (!entry.o11yToken) throw new Error(`no o11y token for env "${env}" in ${credentialsPath} \u2014 run "odla-ai provision" first`);
692
- secrets.push({ name: "ODLA_O11Y_TOKEN", value: entry.o11yToken });
693
- }
694
- if (cfg.services.includes("db")) {
695
- if (!entry.dbKey) throw new Error(`no db key for env "${env}" in ${credentialsPath} \u2014 run "odla-ai provision" first`);
696
- secrets.push({ name: "ODLA_API_KEY", value: entry.dbKey });
697
- }
698
- if (secrets.length === 0) {
699
- out.log(`${env}: no configured Worker secrets to push`);
700
- return;
701
- }
702
- const wranglerEnv = PROD_ENV_NAMES.has(env) ? void 0 : env;
703
- const target = wranglerEnv ? `wrangler env "${wranglerEnv}"` : "the top-level (prod) wrangler env";
704
- if (options.dryRun) {
705
- assertWranglerConfig(cfg);
706
- for (const s of secrets) out.log(`dry run: would push ${s.name} (${redactSecrets(s.value)}) to ${target}`);
707
- return;
708
- }
709
- const run = options.runner ?? defaultRunner;
710
- if (preflight) await preflightLoadedConfig(cfg, run);
711
- for (const s of secrets) {
712
- const result = await wranglerPutSecret(run, { name: s.name, value: s.value, env: wranglerEnv, cwd: cfg.rootDir });
713
- if (result.code !== 0) {
714
- throw new Error(`wrangler secret put ${s.name} failed (exit ${result.code}): ${redactSecrets(`${result.stderr || result.stdout}`.trim())}`);
715
- }
716
- out.log(`${s.name} pushed to ${target} (value read from ${credentialsPath}, never echoed)`);
717
- }
718
- }
719
- async function preflightSecretsPush(options) {
720
- const cfg = await loadProjectConfig(options.configPath);
721
- if (!cfg.services.some((service) => service === "db" || service === "o11y")) return;
722
- await preflightLoadedConfig(cfg, options.runner ?? defaultRunner);
723
- }
724
- async function preflightLoadedConfig(cfg, run) {
725
- assertWranglerConfig(cfg);
726
- if (!await wranglerLoggedIn(run, cfg.rootDir)) {
727
- throw new Error(`wrangler is not logged in \u2014 run "wrangler login" (a browser step for the human)`);
728
- }
729
- }
730
- function assertWranglerConfig(cfg) {
731
- if (!findWranglerConfig(cfg.rootDir)) {
732
- throw new Error(`no wrangler config found in ${cfg.rootDir} (wrangler.jsonc, wrangler.json, or wrangler.toml)`);
733
- }
734
- }
735
-
736
- // src/provision.ts
737
- import { createAppsClient, tenantIdFor as tenantIdFor2 } from "@odla-ai/apps";
738
- import { DEFAULT_SECRET_NAMES, putSecret } from "@odla-ai/ai";
739
- import process4 from "process";
740
-
741
- // src/token.ts
742
- import { requestToken } from "@odla-ai/db";
743
- import process3 from "process";
744
-
745
- // src/open.ts
746
- import { spawn as spawn2 } from "child_process";
747
- import process2 from "process";
748
- async function openUrl(url, options = {}) {
749
- const command = openerFor(options.platform ?? process2.platform);
750
- const doSpawn = options.spawnImpl ?? spawn2;
751
- await new Promise((resolve6, reject) => {
752
- const child = doSpawn(command.cmd, [...command.args, url], {
753
- stdio: "ignore",
754
- detached: true
755
- });
756
- child.once("error", reject);
757
- child.once("spawn", () => {
758
- child.unref();
759
- resolve6();
760
- });
761
- });
762
- }
763
- function openerFor(platform) {
764
- if (platform === "darwin") return { cmd: "open", args: [] };
765
- if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
766
- return { cmd: "xdg-open", args: [] };
767
- }
768
-
769
- // src/token.ts
770
- async function getDeveloperToken(cfg, options, doFetch, out) {
771
- if (options.token) return options.token;
772
- if (process3.env.ODLA_DEV_TOKEN) return process3.env.ODLA_DEV_TOKEN;
773
- const cached = readJsonFile(cfg.local.tokenFile);
774
- if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
775
- out.log(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
776
- return cached.token;
777
- }
778
- const browser = approvalBrowser(options);
779
- const { token, expiresAt } = await requestToken({
780
- endpoint: cfg.platformUrl,
781
- label: `${cfg.app.id} provisioner`,
782
- fetch: doFetch,
783
- onCode: async ({ userCode, expiresIn }) => {
784
- const approvalUrl = handshakeUrl(cfg.platformUrl, userCode);
785
- out.log("");
786
- out.log(`Approve code ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
787
- if (browser.open) {
788
- try {
789
- await (options.openApprovalUrl ?? openUrl)(approvalUrl);
790
- out.log(`auth: opened browser for approval${browser.mode === "auto" ? " (auto)" : ""}`);
791
- } catch (err) {
792
- out.log(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
793
- }
794
- } else if (browser.reason) {
795
- out.log(`auth: browser launch skipped (${browser.reason})`);
796
- }
797
- out.log("");
946
+ if (!entry.o11yToken) throw new Error(`no o11y token for env "${env}" in ${credentialsPath} \u2014 run "odla-ai provision" first`);
947
+ secrets.push({ name: "ODLA_O11Y_TOKEN", value: entry.o11yToken });
948
+ }
949
+ if (cfg.services.includes("db")) {
950
+ if (!entry.dbKey) throw new Error(`no db key for env "${env}" in ${credentialsPath} \u2014 run "odla-ai provision" first`);
951
+ secrets.push({ name: "ODLA_API_KEY", value: entry.dbKey });
952
+ }
953
+ if (secrets.length === 0) {
954
+ out.log(`${env}: no configured Worker secrets to push`);
955
+ return;
956
+ }
957
+ const wranglerEnv = PROD_ENV_NAMES.has(env) ? void 0 : env;
958
+ const target = wranglerEnv ? `wrangler env "${wranglerEnv}"` : "the top-level (prod) wrangler env";
959
+ if (options.dryRun) {
960
+ assertWranglerConfig(cfg);
961
+ for (const s of secrets) out.log(`dry run: would push ${s.name} (${redactSecrets(s.value)}) to ${target}`);
962
+ return;
963
+ }
964
+ const run = options.runner ?? defaultRunner;
965
+ if (preflight) await preflightLoadedConfig(cfg, run);
966
+ for (const s of secrets) {
967
+ const result = await wranglerPutSecret(run, { name: s.name, value: s.value, env: wranglerEnv, cwd: cfg.rootDir });
968
+ if (result.code !== 0) {
969
+ throw new Error(`wrangler secret put ${s.name} failed (exit ${result.code}): ${redactSecrets(`${result.stderr || result.stdout}`.trim())}`);
798
970
  }
799
- });
800
- writePrivateJson(cfg.local.tokenFile, { token, expiresAt });
801
- out.log(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
802
- return token;
971
+ out.log(`${s.name} pushed to ${target} (value read from ${credentialsPath}, never echoed)`);
972
+ }
803
973
  }
804
- function approvalBrowser(options) {
805
- if (options.open === true) return { open: true, mode: "forced" };
806
- if (options.open === false) return { open: false, reason: "disabled by --no-open" };
807
- if (process3.env.CI) return { open: false, reason: "CI environment" };
808
- const interactive = options.interactive ?? Boolean(process3.stdin.isTTY && process3.stdout.isTTY);
809
- if (!interactive) return { open: false, reason: "non-interactive shell; pass --open to force" };
810
- return { open: true, mode: "auto" };
974
+ async function preflightSecretsPush(options) {
975
+ const cfg = await loadProjectConfig(options.configPath);
976
+ if (!cfg.services.some((service) => service === "db" || service === "o11y")) return;
977
+ await preflightLoadedConfig(cfg, options.runner ?? defaultRunner);
811
978
  }
812
- function handshakeUrl(platformUrl, userCode) {
813
- const url = new URL("/studio", platformUrl);
814
- url.searchParams.set("code", userCode);
815
- return url.toString();
979
+ async function preflightLoadedConfig(cfg, run) {
980
+ assertWranglerConfig(cfg);
981
+ if (!await wranglerLoggedIn(run, cfg.rootDir)) {
982
+ throw new Error(`wrangler is not logged in \u2014 run "wrangler login" (a browser step for the human)`);
983
+ }
984
+ }
985
+ function assertWranglerConfig(cfg) {
986
+ if (!findWranglerConfig(cfg.rootDir)) {
987
+ throw new Error(`no wrangler config found in ${cfg.rootDir} (wrangler.jsonc, wrangler.json, or wrangler.toml)`);
988
+ }
816
989
  }
817
990
 
991
+ // src/provision.ts
992
+ import { createAppsClient, tenantIdFor as tenantIdFor2 } from "@odla-ai/apps";
993
+ import { DEFAULT_SECRET_NAMES, putSecret } from "@odla-ai/ai";
994
+ import process5 from "process";
995
+
818
996
  // src/provision-credentials.ts
819
997
  import { tenantIdFor } from "@odla-ai/apps";
820
998
  async function provisionEnvCredentials(opts) {
@@ -1044,7 +1222,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
1044
1222
  out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
1045
1223
  }
1046
1224
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
1047
- const key = process4.env[cfg.ai.keyEnv];
1225
+ const key = process5.env[cfg.ai.keyEnv];
1048
1226
  if (key) {
1049
1227
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
1050
1228
  await putSecret({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -1104,10 +1282,117 @@ async function safeText2(res) {
1104
1282
  }
1105
1283
  }
1106
1284
 
1285
+ // src/security.ts
1286
+ import { isAbsolute as isAbsolute3, relative as relative2, resolve as resolve5, sep } from "path";
1287
+ import {
1288
+ cloudflareAppProfile,
1289
+ createPlatformSecurityReasoners,
1290
+ createSecurityHarness,
1291
+ genericProfile,
1292
+ odlaProfile,
1293
+ securityFingerprint
1294
+ } from "@odla-ai/security";
1295
+ import { FileRunStore, snapshotDirectory, writeSecurityArtifacts } from "@odla-ai/security/node";
1296
+ async function runHostedSecurity(options) {
1297
+ if (options.sourceDisclosureAck !== "redacted") {
1298
+ throw new Error("Hosted security requires sourceDisclosureAck=redacted before repository source may leave this process");
1299
+ }
1300
+ const selfAudit = options.selfAudit === true;
1301
+ const cfg = selfAudit ? void 0 : await loadProjectConfig(options.configPath ?? "odla.config.mjs");
1302
+ const appId = selfAudit ? "odla-ai" : cfg.app.id;
1303
+ const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
1304
+ const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
1305
+ const target = resolve5(options.target ?? cfg?.rootDir ?? ".");
1306
+ const output = resolve5(options.out ?? resolve5(target, ".odla/security/hosted"));
1307
+ const outputRelative = relative2(target, output).split(sep).join("/");
1308
+ if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
1309
+ const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
1310
+ const tokenRequest = {
1311
+ platform,
1312
+ appId,
1313
+ env,
1314
+ selfAudit,
1315
+ scope: selfAudit ? "platform:security:self" : null
1316
+ };
1317
+ const token = await injectedToken(options, tokenRequest);
1318
+ const snapshot = await snapshotDirectory(target, {
1319
+ exclude: !outputRelative.startsWith("../") && !isAbsolute3(outputRelative) ? [outputRelative] : []
1320
+ });
1321
+ const hosted = await createPlatformSecurityReasoners({
1322
+ platform,
1323
+ token,
1324
+ appId,
1325
+ env,
1326
+ repository: snapshot.repository,
1327
+ revision: snapshot.revision,
1328
+ snapshotDigest: snapshot.digest,
1329
+ sourceDisclosure: "redacted",
1330
+ clientRunId: options.runId ?? crypto.randomUUID(),
1331
+ ...selfAudit ? { selfAudit: { enabled: true, subject: "service:odla-security-self-audit" } } : {},
1332
+ fetch: options.fetch,
1333
+ signal: options.signal
1334
+ });
1335
+ const harness = createSecurityHarness({
1336
+ profile,
1337
+ store: new FileRunStore(resolve5(output, "state")),
1338
+ discoveryReasoner: hosted.discoveryReasoner,
1339
+ validationReasoner: hosted.validationReasoner,
1340
+ policy: {
1341
+ modelSourceDisclosure: "redacted",
1342
+ active: false,
1343
+ allowNetwork: false
1344
+ }
1345
+ });
1346
+ const report = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
1347
+ await writeSecurityArtifacts(output, report);
1348
+ const reportDigest = await securityFingerprint(report);
1349
+ await hosted.complete({
1350
+ reportDigest,
1351
+ coverageStatus: report.coverageStatus,
1352
+ confirmed: report.metrics.confirmed,
1353
+ candidates: report.metrics.candidates
1354
+ }, { signal: options.signal });
1355
+ printSummary(options.stdout ?? console, appId, env, hosted.run, report, output);
1356
+ return Object.freeze({ report, run: hosted.run, output });
1357
+ }
1358
+ function selectEnv(requested, declared, configPath, rootDir) {
1359
+ const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
1360
+ if (!env || !declared.includes(env)) {
1361
+ const shown = relative2(rootDir, configPath) || configPath;
1362
+ throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
1363
+ }
1364
+ return env;
1365
+ }
1366
+ async function injectedToken(options, request) {
1367
+ const value = options.token ?? await options.getToken?.(Object.freeze({ ...request }));
1368
+ if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
1369
+ throw new Error(
1370
+ request.selfAudit ? "Self-audit requires an injected, scoped platform security token" : "Hosted security requires an injected app developer token or getToken callback"
1371
+ );
1372
+ }
1373
+ return value;
1374
+ }
1375
+ function profileFor(name, maxHuntTasks) {
1376
+ const profile = name === "odla" ? odlaProfile() : name === "cloudflare-app" ? cloudflareAppProfile() : name === "generic" ? genericProfile() : void 0;
1377
+ if (!profile) throw new Error(`unknown security profile "${String(name)}"`);
1378
+ if (maxHuntTasks === void 0) return profile;
1379
+ if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
1380
+ return { ...profile, maxHuntTasks };
1381
+ }
1382
+ function printSummary(out, appId, env, run, report, output) {
1383
+ const complete = report.coverage.filter((cell) => cell.state === "complete").length;
1384
+ out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
1385
+ out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
1386
+ out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
1387
+ out.log(` coverage: ${report.coverageStatus} ${complete}/${report.coverage.length} blocked=${report.metrics.blockedCells} shallow=${report.metrics.shallowCells} unscheduled=${report.metrics.unscheduledCells}`);
1388
+ out.log(` findings: confirmed=${report.metrics.confirmed} needs_reproduction=${report.metrics.needsReproduction} candidates=${report.metrics.candidates}`);
1389
+ out.log(` report: ${resolve5(output, "REPORT.md")}`);
1390
+ }
1391
+
1107
1392
  // src/skill.ts
1108
- import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync5, readdirSync, writeFileSync as writeFileSync3 } from "fs";
1393
+ import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync5, readdirSync, writeFileSync as writeFileSync3 } from "fs";
1109
1394
  import { homedir } from "os";
1110
- import { dirname as dirname4, isAbsolute as isAbsolute3, join as join3, relative as relative2, resolve as resolve5, sep } from "path";
1395
+ import { dirname as dirname4, isAbsolute as isAbsolute4, join as join3, relative as relative3, resolve as resolve6, sep as sep2 } from "path";
1111
1396
  import { fileURLToPath } from "url";
1112
1397
 
1113
1398
  // src/skill-adapters.ts
@@ -1168,8 +1453,8 @@ function installSkill(options = {}) {
1168
1453
  const files = listFiles(sourceDir);
1169
1454
  if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
1170
1455
  const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
1171
- const root = resolve5(options.dir ?? process.cwd());
1172
- const home = resolve5(options.homeDir ?? homedir());
1456
+ const root = resolve6(options.dir ?? process.cwd());
1457
+ const home = resolve6(options.homeDir ?? homedir());
1173
1458
  const plans = /* @__PURE__ */ new Map();
1174
1459
  const targets = /* @__PURE__ */ new Map();
1175
1460
  const rememberTarget = (harness, target) => {
@@ -1188,7 +1473,7 @@ function installSkill(options = {}) {
1188
1473
  let targetDir;
1189
1474
  if (options.global) {
1190
1475
  const claudeRoot = join3(home, ".claude", "skills");
1191
- const codexRoot = resolve5(options.codexHomeDir ?? process.env.CODEX_HOME ?? join3(home, ".codex"), "skills");
1476
+ const codexRoot = resolve6(options.codexHomeDir ?? process.env.CODEX_HOME ?? join3(home, ".codex"), "skills");
1192
1477
  targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
1193
1478
  for (const harness of harnesses) {
1194
1479
  const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
@@ -1238,7 +1523,7 @@ function installSkill(options = {}) {
1238
1523
  conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
1239
1524
  continue;
1240
1525
  }
1241
- if (!existsSync6(file.target)) {
1526
+ if (!existsSync7(file.target)) {
1242
1527
  writtenPaths.add(file.target);
1243
1528
  continue;
1244
1529
  }
@@ -1259,7 +1544,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
1259
1544
  );
1260
1545
  }
1261
1546
  for (const file of plans.values()) {
1262
- if (!existsSync6(file.target) || readFileSync5(file.target, "utf8") !== file.content) {
1547
+ if (!existsSync7(file.target) || readFileSync5(file.target, "utf8") !== file.content) {
1263
1548
  mkdirSync3(dirname4(file.target), { recursive: true });
1264
1549
  writeFileSync3(file.target, file.content);
1265
1550
  }
@@ -1280,7 +1565,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
1280
1565
  };
1281
1566
  }
1282
1567
  function pathsUnder(root, paths) {
1283
- return [...paths].map((path) => relative2(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute3(path)).sort();
1568
+ return [...paths].map((path) => relative3(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${sep2}`) && !isAbsolute4(path)).sort();
1284
1569
  }
1285
1570
  function normalizeHarnesses(values, global) {
1286
1571
  const requested = values?.length ? values : ["claude"];
@@ -1302,7 +1587,7 @@ function normalizeHarnesses(values, global) {
1302
1587
  function managedFileContent(path, block, force, boundary) {
1303
1588
  const symlink = symlinkedComponent(boundary, path);
1304
1589
  if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
1305
- if (!existsSync6(path)) return `${block}
1590
+ if (!existsSync7(path)) return `${block}
1306
1591
  `;
1307
1592
  const current = readFileSync5(path, "utf8");
1308
1593
  const start = "<!-- odla-ai agent setup:start -->";
@@ -1325,12 +1610,12 @@ function managedFileContent(path, block, force, boundary) {
1325
1610
  return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
1326
1611
  }
1327
1612
  function symlinkedComponent(boundary, target) {
1328
- const rel = relative2(boundary, target);
1329
- if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute3(rel)) {
1613
+ const rel = relative3(boundary, target);
1614
+ if (rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute4(rel)) {
1330
1615
  throw new Error(`agent setup target escapes its install root: ${target}`);
1331
1616
  }
1332
1617
  let current = boundary;
1333
- for (const part of rel.split(sep).filter(Boolean)) {
1618
+ for (const part of rel.split(sep2).filter(Boolean)) {
1334
1619
  current = join3(current, part);
1335
1620
  try {
1336
1621
  if (lstatSync(current).isSymbolicLink()) return current;
@@ -1344,13 +1629,13 @@ function skillNames(files) {
1344
1629
  return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
1345
1630
  }
1346
1631
  function listFiles(dir) {
1347
- if (!existsSync6(dir)) return [];
1632
+ if (!existsSync7(dir)) return [];
1348
1633
  const results = [];
1349
1634
  const walk = (current) => {
1350
1635
  for (const entry of readdirSync(current, { withFileTypes: true })) {
1351
1636
  const path = join3(current, entry.name);
1352
1637
  if (entry.isDirectory()) walk(path);
1353
- else results.push(relative2(dir, path));
1638
+ else results.push(relative3(dir, path));
1354
1639
  }
1355
1640
  };
1356
1641
  walk(dir);
@@ -1439,7 +1724,135 @@ async function safeText3(res) {
1439
1724
  }
1440
1725
 
1441
1726
  // src/cli.ts
1727
+ import { findingsAtOrAbove } from "@odla-ai/security";
1728
+
1729
+ // src/argv.ts
1730
+ function parseArgv(argv) {
1731
+ const positionals = [];
1732
+ const options = {};
1733
+ for (let i = 0; i < argv.length; i++) {
1734
+ const arg = argv[i];
1735
+ if (!arg.startsWith("--")) {
1736
+ positionals.push(arg);
1737
+ continue;
1738
+ }
1739
+ const eq = arg.indexOf("=");
1740
+ const rawName = arg.slice(2, eq === -1 ? void 0 : eq);
1741
+ if (!rawName) continue;
1742
+ if (rawName.startsWith("no-")) {
1743
+ options[rawName.slice(3)] = false;
1744
+ continue;
1745
+ }
1746
+ if (eq !== -1) {
1747
+ addOption(options, rawName, arg.slice(eq + 1));
1748
+ continue;
1749
+ }
1750
+ const value = argv[i + 1];
1751
+ if (value !== void 0 && !value.startsWith("--")) {
1752
+ i++;
1753
+ addOption(options, rawName, value);
1754
+ } else {
1755
+ addOption(options, rawName, true);
1756
+ }
1757
+ }
1758
+ return { positionals, options };
1759
+ }
1760
+ function assertArgs(parsed, allowedOptions, maxPositionals) {
1761
+ const allowed = new Set(allowedOptions);
1762
+ for (const name of Object.keys(parsed.options)) {
1763
+ if (!allowed.has(name)) throw new Error(`unknown option "--${name}"; run "odla-ai help" for supported options`);
1764
+ }
1765
+ if (parsed.positionals.length > maxPositionals) {
1766
+ throw new Error(`unexpected argument "${parsed.positionals[maxPositionals]}"; run "odla-ai help"`);
1767
+ }
1768
+ }
1769
+ function requiredString(value, name) {
1770
+ const result = stringOpt(value);
1771
+ if (!result) throw new Error(`${name} is required`);
1772
+ return result;
1773
+ }
1774
+ function stringOpt(value) {
1775
+ if (typeof value === "string") return value;
1776
+ if (Array.isArray(value)) return value[value.length - 1];
1777
+ return void 0;
1778
+ }
1779
+ function listOpt(value) {
1780
+ if (value === void 0 || typeof value === "boolean") return void 0;
1781
+ const values = Array.isArray(value) ? value : [value];
1782
+ return values.flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
1783
+ }
1784
+ function boolOpt(value) {
1785
+ if (typeof value === "boolean") return value;
1786
+ if (typeof value === "string" && (value === "true" || value === "false")) return value === "true";
1787
+ if (value === void 0) return void 0;
1788
+ throw new Error("boolean option must be true or false");
1789
+ }
1790
+ function numberOpt(value, flag) {
1791
+ if (value === void 0) return void 0;
1792
+ const raw = stringOpt(value);
1793
+ const parsed = raw === void 0 ? Number.NaN : Number(raw);
1794
+ if (!Number.isSafeInteger(parsed) || parsed < 1) throw new Error(`${flag} requires a positive integer`);
1795
+ return parsed;
1796
+ }
1797
+ function addOption(options, name, value) {
1798
+ const current = options[name];
1799
+ if (current === void 0) options[name] = value;
1800
+ else if (Array.isArray(current)) current.push(String(value));
1801
+ else options[name] = [String(current), String(value)];
1802
+ }
1803
+
1804
+ // src/help.ts
1442
1805
  import { readFileSync as readFileSync6 } from "fs";
1806
+ function cliVersion() {
1807
+ const pkg = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf8"));
1808
+ return pkg.version ?? "unknown";
1809
+ }
1810
+ function printHelp() {
1811
+ console.log(`odla-ai
1812
+
1813
+ Usage:
1814
+ odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
1815
+ odla-ai init --app-id <id> --name <name> [--services db,ai,o11y] [--env dev --env prod]
1816
+ odla-ai doctor [--config odla.config.mjs]
1817
+ odla-ai capabilities [--json]
1818
+ odla-ai admin ai show [--platform https://odla.ai] [--json]
1819
+ odla-ai admin ai set <purpose> --provider <id> --model <id> [--enabled|--no-enabled]
1820
+ odla-ai admin ai credentials [--json]
1821
+ odla-ai admin ai credential set <provider> (--from-env <NAME>|--stdin)
1822
+ odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
1823
+ odla-ai security run [target] --self --ack-redacted-source
1824
+ odla-ai provision [--config odla.config.mjs] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
1825
+ odla-ai smoke [--config odla.config.mjs] [--env dev]
1826
+ odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
1827
+ odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
1828
+ odla-ai version
1829
+
1830
+ Commands:
1831
+ setup Install offline odla runbooks for common coding-agent harnesses.
1832
+ init Create a generic odla.config.mjs plus starter schema/rules files.
1833
+ doctor Validate and summarize the project config without network calls.
1834
+ capabilities Show what the CLI automates vs agent edits and human checkpoints.
1835
+ admin Manage platform-funded AI routing with a narrow admin-approved device grant.
1836
+ security Run hosted discovery + independent validation with app/run attribution.
1837
+ provision Register services, configure them, persist credentials, optionally push secrets.
1838
+ smoke Verify local credentials, public-config, live schema, and db aggregate.
1839
+ skill Same installer; --agent accepts all, claude, codex, cursor,
1840
+ copilot, gemini, or agents (repeatable or comma-separated).
1841
+ secrets Push configured db/o11y secrets into the Worker via wrangler stdin.
1842
+ version Print the CLI version.
1843
+
1844
+ Safety:
1845
+ New projects target dev only. Add prod explicitly to odla.config.mjs and pass
1846
+ --yes to provision it; use --dry-run first to inspect the resolved plan.
1847
+ Provision caches the approved developer token and service credentials under
1848
+ .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
1849
+ preflights Wrangler before any shown-once issuance or destructive rotation.
1850
+ Provision opens the approval page automatically in interactive terminals;
1851
+ use --open to force browser launch or --no-open to suppress it.
1852
+ `);
1853
+ }
1854
+
1855
+ // src/cli.ts
1443
1856
  async function runCli(argv = process.argv.slice(2)) {
1444
1857
  const parsed = parseArgv(argv);
1445
1858
  const command = parsed.positionals[0] ?? "help";
@@ -1476,6 +1889,99 @@ async function runCli(argv = process.argv.slice(2)) {
1476
1889
  printCapabilities(parsed.options.json === true);
1477
1890
  return;
1478
1891
  }
1892
+ if (command === "admin") {
1893
+ const area = parsed.positionals[1];
1894
+ const action = parsed.positionals[2];
1895
+ const credentialSet = action === "credential" && parsed.positionals[3] === "set";
1896
+ const credentials = action === "credentials";
1897
+ if (area !== "ai" || action !== "show" && action !== "set" && !credentialSet && !credentials) {
1898
+ throw new Error('unknown admin command. Try "odla-ai admin ai show".');
1899
+ }
1900
+ assertArgs(parsed, [
1901
+ "platform",
1902
+ "token",
1903
+ "json",
1904
+ "open",
1905
+ "provider",
1906
+ "model",
1907
+ "enabled",
1908
+ "max-input-bytes",
1909
+ "max-output-tokens",
1910
+ "max-calls-per-run",
1911
+ "from-env",
1912
+ "stdin"
1913
+ ], credentialSet ? 5 : action === "set" ? 4 : 3);
1914
+ await adminAi({
1915
+ action: credentialSet ? "credential-set" : credentials ? "credentials" : action,
1916
+ purpose: action === "set" ? parsed.positionals[3] : void 0,
1917
+ provider: stringOpt(parsed.options.provider),
1918
+ model: stringOpt(parsed.options.model),
1919
+ enabled: boolOpt(parsed.options.enabled),
1920
+ maxInputBytes: numberOpt(parsed.options["max-input-bytes"], "--max-input-bytes"),
1921
+ maxOutputTokens: numberOpt(parsed.options["max-output-tokens"], "--max-output-tokens"),
1922
+ maxCallsPerRun: numberOpt(parsed.options["max-calls-per-run"], "--max-calls-per-run"),
1923
+ platform: stringOpt(parsed.options.platform),
1924
+ token: stringOpt(parsed.options.token),
1925
+ json: parsed.options.json === true,
1926
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
1927
+ credentialProvider: credentialSet ? parsed.positionals[4] : void 0,
1928
+ fromEnv: stringOpt(parsed.options["from-env"]),
1929
+ stdin: parsed.options.stdin === true
1930
+ });
1931
+ return;
1932
+ }
1933
+ if (command === "security") {
1934
+ const sub = parsed.positionals[1];
1935
+ if (sub !== "run") throw new Error('unknown security command. Try "odla-ai security run".');
1936
+ assertArgs(parsed, [
1937
+ "config",
1938
+ "env",
1939
+ "out",
1940
+ "profile",
1941
+ "max-hunt-tasks",
1942
+ "run-id",
1943
+ "platform",
1944
+ "self",
1945
+ "ack-redacted-source",
1946
+ "open",
1947
+ "fail-on",
1948
+ "fail-on-candidates",
1949
+ "allow-incomplete"
1950
+ ], 3);
1951
+ const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
1952
+ const selfAudit = parsed.options.self === true;
1953
+ const open = parsed.options.open === false ? false : parsed.options.open === true ? true : void 0;
1954
+ const platform = stringOpt(parsed.options.platform);
1955
+ const result = await runHostedSecurity({
1956
+ configPath,
1957
+ selfAudit,
1958
+ target: parsed.positionals[2],
1959
+ out: stringOpt(parsed.options.out),
1960
+ env: stringOpt(parsed.options.env),
1961
+ profile: securityProfile(stringOpt(parsed.options.profile)),
1962
+ maxHuntTasks: numberOpt(parsed.options["max-hunt-tasks"], "--max-hunt-tasks"),
1963
+ runId: stringOpt(parsed.options["run-id"]),
1964
+ platform,
1965
+ sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
1966
+ getToken: async (request) => {
1967
+ if (request.scope === "platform:security:self") {
1968
+ return getScopedPlatformToken({ platform: request.platform, scope: request.scope, open });
1969
+ }
1970
+ const cfg = await loadProjectConfig(configPath);
1971
+ return getDeveloperToken(cfg, { configPath, open }, fetch, console);
1972
+ }
1973
+ });
1974
+ const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
1975
+ const candidateValue = parsed.options["fail-on-candidates"];
1976
+ const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
1977
+ const confirmed = findingsAtOrAbove(result.report, failOn);
1978
+ const leads = failOnCandidates ? findingsAtOrAbove(result.report, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
1979
+ const incomplete = result.report.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
1980
+ if (confirmed.length || leads.length || incomplete) {
1981
+ throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
1982
+ }
1983
+ return;
1984
+ }
1479
1985
  if (command === "provision") {
1480
1986
  assertArgs(
1481
1987
  parsed,
@@ -1556,71 +2062,14 @@ async function runCli(argv = process.argv.slice(2)) {
1556
2062
  }
1557
2063
  throw new Error(`unknown command "${command}". Run "odla-ai help".`);
1558
2064
  }
1559
- function assertArgs(parsed, allowedOptions, maxPositionals) {
1560
- const allowed = new Set(allowedOptions);
1561
- for (const name of Object.keys(parsed.options)) {
1562
- if (!allowed.has(name)) {
1563
- throw new Error(`unknown option "--${name}"; run "odla-ai help" for supported options`);
1564
- }
1565
- }
1566
- if (parsed.positionals.length > maxPositionals) {
1567
- throw new Error(`unexpected argument "${parsed.positionals[maxPositionals]}"; run "odla-ai help"`);
1568
- }
1569
- }
1570
- function parseArgv(argv) {
1571
- const positionals = [];
1572
- const options = {};
1573
- for (let i = 0; i < argv.length; i++) {
1574
- const arg = argv[i];
1575
- if (!arg.startsWith("--")) {
1576
- positionals.push(arg);
1577
- continue;
1578
- }
1579
- const eq = arg.indexOf("=");
1580
- const rawName = arg.slice(2, eq === -1 ? void 0 : eq);
1581
- if (!rawName) continue;
1582
- if (rawName.startsWith("no-")) {
1583
- options[rawName.slice(3)] = false;
1584
- continue;
1585
- }
1586
- if (eq !== -1) {
1587
- addOption(options, rawName, arg.slice(eq + 1));
1588
- continue;
1589
- }
1590
- const value = argv[i + 1];
1591
- if (value !== void 0 && !value.startsWith("--")) {
1592
- i++;
1593
- addOption(options, rawName, value);
1594
- } else {
1595
- addOption(options, rawName, true);
1596
- }
1597
- }
1598
- return { positionals, options };
1599
- }
1600
- function addOption(options, name, value) {
1601
- const cur = options[name];
1602
- if (cur === void 0) {
1603
- options[name] = value;
1604
- } else if (Array.isArray(cur)) {
1605
- cur.push(String(value));
1606
- } else {
1607
- options[name] = [String(cur), String(value)];
1608
- }
1609
- }
1610
- function requiredString(value, name) {
1611
- const s = stringOpt(value);
1612
- if (!s) throw new Error(`${name} is required`);
1613
- return s;
1614
- }
1615
- function stringOpt(value) {
1616
- if (typeof value === "string") return value;
1617
- if (Array.isArray(value)) return value[value.length - 1];
1618
- return void 0;
2065
+ function securityProfile(value) {
2066
+ if (value === void 0) return void 0;
2067
+ if (value === "odla" || value === "cloudflare-app" || value === "generic") return value;
2068
+ throw new Error("--profile must be odla, cloudflare-app, or generic");
1619
2069
  }
1620
- function listOpt(value) {
1621
- if (value === void 0 || value === false || value === true) return void 0;
1622
- const values = Array.isArray(value) ? value : [value];
1623
- return values.flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
2070
+ function severityOpt(value, flag) {
2071
+ if (["informational", "low", "medium", "high", "critical"].includes(value)) return value;
2072
+ throw new Error(`${flag} must be informational, low, medium, high, or critical`);
1624
2073
  }
1625
2074
  function harnessList(parsed) {
1626
2075
  const selected = [
@@ -1637,48 +2086,11 @@ function harnessOption(value, flag) {
1637
2086
  if (parts.some((item) => !item.trim())) throw new Error(`${flag} requires a harness name`);
1638
2087
  return parts.map((item) => item.trim());
1639
2088
  }
1640
- function cliVersion() {
1641
- const pkg = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf8"));
1642
- return pkg.version ?? "unknown";
1643
- }
1644
- function printHelp() {
1645
- console.log(`odla-ai
1646
-
1647
- Usage:
1648
- odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
1649
- odla-ai init --app-id <id> --name <name> [--services db,ai,o11y] [--env dev --env prod]
1650
- odla-ai doctor [--config odla.config.mjs]
1651
- odla-ai capabilities [--json]
1652
- odla-ai provision [--config odla.config.mjs] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
1653
- odla-ai smoke [--config odla.config.mjs] [--env dev]
1654
- odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
1655
- odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
1656
- odla-ai version
1657
-
1658
- Commands:
1659
- setup Install offline odla runbooks for common coding-agent harnesses.
1660
- init Create a generic odla.config.mjs plus starter schema/rules files.
1661
- doctor Validate and summarize the project config without network calls.
1662
- capabilities Show what the CLI automates vs agent edits and human checkpoints.
1663
- provision Register services, configure them, persist credentials, optionally push secrets.
1664
- smoke Verify local credentials, public-config, live schema, and db aggregate.
1665
- skill Same installer; --agent accepts all, claude, codex, cursor,
1666
- copilot, gemini, or agents (repeatable or comma-separated).
1667
- secrets Push configured db/o11y secrets into the Worker via wrangler stdin.
1668
- version Print the CLI version.
1669
-
1670
- Safety:
1671
- New projects target dev only. Add prod explicitly to odla.config.mjs and pass
1672
- --yes to provision it; use --dry-run first to inspect the resolved plan.
1673
- Provision caches the approved developer token and service credentials under
1674
- .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
1675
- preflights Wrangler before any shown-once issuance or destructive rotation.
1676
- Provision opens the approval page automatically in interactive terminals;
1677
- use --open to force browser launch or --no-open to suppress it.
1678
- `);
1679
- }
1680
2089
 
1681
2090
  export {
2091
+ SYSTEM_AI_PURPOSES,
2092
+ getScopedPlatformToken,
2093
+ adminAi,
1682
2094
  CAPABILITIES,
1683
2095
  printCapabilities,
1684
2096
  redactSecrets,
@@ -1686,9 +2098,10 @@ export {
1686
2098
  initProject,
1687
2099
  secretsPush,
1688
2100
  provision,
2101
+ runHostedSecurity,
1689
2102
  AGENT_HARNESSES,
1690
2103
  installSkill,
1691
2104
  smoke,
1692
2105
  runCli
1693
2106
  };
1694
- //# sourceMappingURL=chunk-7WZIZCGA.js.map
2107
+ //# sourceMappingURL=chunk-OERLHVLH.js.map