okengine 0.11.0 → 0.11.2

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.
Files changed (68) hide show
  1. package/package.json +1 -1
  2. package/site/content/docs/elements/ai.mdx +22 -1
  3. package/site/content/docs/elements/store.mdx +3 -1
  4. package/site/content/docs/elements/vault.mdx +19 -11
  5. package/site/content/docs/get-started/installation.mdx +7 -1
  6. package/site/content/docs/recipes/llama-cpp.mdx +10 -9
  7. package/site/content/docs/reference/cli.md +6 -2
  8. package/site/content/docs/reference/environment-variables.mdx +9 -9
  9. package/src/cli/ai-setup/ai-setup.test.ts +3 -1
  10. package/src/cli/ai-setup/apply.ts +61 -1
  11. package/src/cli/ask-seed.test.ts +4 -3
  12. package/src/cli/ask-seed.ts +5 -6
  13. package/src/cli/client-add.test.ts +2 -1
  14. package/src/cli/dev.test.ts +116 -0
  15. package/src/cli/dev.ts +107 -18
  16. package/src/cli/project-state.test.ts +50 -0
  17. package/src/cli/project-state.ts +123 -0
  18. package/src/cli/vault-cmd.test.ts +47 -18
  19. package/src/cli/vault-cmd.ts +2 -1
  20. package/src/compiler/extract.ts +12 -1
  21. package/src/console/server/console.test.ts +3 -1
  22. package/src/console/server/operator-db.test.ts +48 -17
  23. package/src/console/server/operator-db.ts +5 -1
  24. package/src/docker/derive.ts +24 -3
  25. package/src/docker/docker.test.ts +4 -2
  26. package/src/docker/index.ts +1 -0
  27. package/src/docker/recipes/index.ts +1 -0
  28. package/src/docker/recipes/llama-cpp.ts +20 -4
  29. package/src/drivers/ai-openai-compatible.ts +15 -3
  30. package/src/drivers/vault-builtin.test.ts +50 -42
  31. package/src/elements/ai/declare.ts +73 -3
  32. package/src/elements/ai/errors.test.ts +35 -0
  33. package/src/elements/ai/errors.ts +139 -0
  34. package/src/elements/ai/eval.ts +26 -1
  35. package/src/elements/ai/runtime.ts +140 -80
  36. package/src/elements/ai/tools.test.ts +1 -1
  37. package/src/elements/ai.test.ts +99 -2
  38. package/src/elements/ai.ts +11 -1
  39. package/src/elements/gate/config.ts +13 -3
  40. package/src/elements/gate/declare.ts +1 -1
  41. package/src/elements/gate/strategies.ts +2 -12
  42. package/src/elements/index.ts +2 -0
  43. package/src/elements/store/index-boot.test.ts +23 -6
  44. package/src/elements/store/resource.test.ts +38 -19
  45. package/src/elements/store/sql-session.test.ts +55 -58
  46. package/src/elements/vault/builtin-adapter.test.ts +115 -58
  47. package/src/elements/vault/builtin-adapter.ts +241 -47
  48. package/src/elements/vault/chaos-child.ts +424 -0
  49. package/src/elements/vault/chaos.test.ts +651 -0
  50. package/src/elements/vault/resilience.ts +6 -1
  51. package/src/elements/vault/security-checklist.test.ts +10 -8
  52. package/src/elements/vault/storage.ts +130 -27
  53. package/src/elements/vault/test-helpers.ts +368 -0
  54. package/src/elements/vault.ts +6 -0
  55. package/src/index.ts +2 -0
  56. package/src/kernel/app-auth.ts +98 -0
  57. package/src/kernel/app.ts +120 -78
  58. package/src/kernel/auto-registry.test.ts +52 -1
  59. package/src/kernel/boot.test.ts +4 -18
  60. package/src/kernel/element-registries.ts +19 -4
  61. package/src/kernel/errors.ts +3 -3
  62. package/src/kernel/fx.test.ts +25 -0
  63. package/src/kernel/fx.ts +4 -1
  64. package/src/manifest/types.ts +4 -0
  65. package/src/release/build-lib.ts +3 -0
  66. package/src/shared/lazy-src.ts +79 -0
  67. package/src/test/create-test-app.ts +16 -11
  68. package/src/test/reset-element-registries.ts +17 -9
package/src/cli/dev.ts CHANGED
@@ -38,6 +38,7 @@ import {
38
38
  formatServiceLine,
39
39
  formatStackSummary,
40
40
  formatStatusLine,
41
+ termColorEnabled,
41
42
  type BootProgress,
42
43
  type DevStatus,
43
44
  type StackSummaryService,
@@ -358,8 +359,14 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
358
359
  });
359
360
  try {
360
361
  await syncAdoptBarrel(cwd);
361
- } catch {
362
- /* best-effort boot-time assertAdoptBarrelFresh (rootDir opt-in) is the real gate */
362
+ } catch (err) {
363
+ // Best-effort: boot-time assertAdoptBarrelFresh (rootDir opt-in) is the real gate.
364
+ // Still surface so a broken generator is not fully silent in the boot log.
365
+ write(
366
+ formatStatusLine(
367
+ `.adopt() barrel sync skipped — ${err instanceof Error ? err.message : String(err)}`,
368
+ ),
369
+ );
363
370
  }
364
371
 
365
372
  const preferredApp = options.appPort ?? Number(Bun.env.PORT ?? APP_PORT);
@@ -425,7 +432,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
425
432
  };
426
433
  if (typeof pkg.version === "string") okeVersion = pkg.version;
427
434
  } catch {
428
- // shipped binary may not sit next to package.json
435
+ // Inconsequential: banner version only — shipped binaries may not sit next to package.json.
429
436
  }
430
437
  const earlyProfile = resolveDevProfile({ docker: true, nodeEnv: "development" });
431
438
  write(
@@ -473,8 +480,13 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
473
480
  let loadedConfig: Awaited<ReturnType<typeof loadOkeConfig>>["config"] | null = null;
474
481
  try {
475
482
  loadedConfig = (await loadOkeConfig(cwd)).config;
476
- } catch {
483
+ } catch (err) {
477
484
  loadedConfig = null;
485
+ const msg = err instanceof Error ? err.message : String(err);
486
+ // Missing config is common for injectable/`--images` trees; surface parse bugs only.
487
+ if (!/no oke\.config\.ts found/i.test(msg)) {
488
+ write(formatStatusLine(`oke.config.ts load skipped — ${msg}`));
489
+ }
478
490
  }
479
491
 
480
492
  {
@@ -496,8 +508,12 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
496
508
  } else if (!loadedConfig) {
497
509
  try {
498
510
  loadedConfig = (await loadOkeConfig(cwd)).config;
499
- } catch {
511
+ } catch (err) {
500
512
  loadedConfig = null;
513
+ const msg = err instanceof Error ? err.message : String(err);
514
+ if (!/no oke\.config\.ts found/i.test(msg)) {
515
+ write(formatStatusLine(`oke.config.ts load skipped — ${msg}`));
516
+ }
501
517
  }
502
518
  }
503
519
  if (Array.isArray(options.docker)) {
@@ -1003,11 +1019,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1003
1019
  });
1004
1020
  }
1005
1021
  } catch (err) {
1006
- write(
1007
- formatStatusLine(
1008
- `oke db push (dev) skipped — ${err instanceof Error ? err.message : String(err)}`,
1009
- ),
1010
- );
1022
+ write(await formatDevSchemaSyncFailure(cwd, err, "boot"));
1011
1023
  }
1012
1024
  }
1013
1025
 
@@ -1054,7 +1066,8 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1054
1066
  const next = await extractManifest({ rootDir: cwd });
1055
1067
  feedManifest(state, next);
1056
1068
  } catch {
1057
- // Source may be mid-edit keep the last good Manifest.
1069
+ // Inconsequential while watching: mid-edit parse failures are expected on every
1070
+ // keystroke — keep the last good Manifest. Logging here would flood the Logs pane.
1058
1071
  }
1059
1072
  }
1060
1073
 
@@ -1130,8 +1143,13 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1130
1143
  out: resolve(cwd, "oke-client.d.ts"),
1131
1144
  });
1132
1145
  write(formatStatusLine("regenerated oke-client.d.ts"));
1133
- } catch {
1134
- // App may not be ready yet — ignore until next save.
1146
+ } catch (err) {
1147
+ // App may not be ready yet on first tick still surface so a stuck regen is visible.
1148
+ write(
1149
+ formatStatusLine(
1150
+ `oke-client.d.ts regen skipped — ${err instanceof Error ? err.message : String(err)}`,
1151
+ ),
1152
+ );
1135
1153
  }
1136
1154
  });
1137
1155
 
@@ -1196,8 +1214,9 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1196
1214
  write(formatStatusLine("oke db push (schema change)"));
1197
1215
  try {
1198
1216
  await dbPush(cwd);
1199
- } catch {
1200
- // Mid-edit / kit unavailablenext save retries.
1217
+ } catch (err) {
1218
+ // Dev-loop stays up (next save retries) but definition errors must not look like a quiet skip.
1219
+ write(await formatDevSchemaSyncFailure(cwd, err, "watch"));
1201
1220
  }
1202
1221
  });
1203
1222
 
@@ -1394,7 +1413,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1394
1413
  // Esc / unmount without q — still tear down.
1395
1414
  stop();
1396
1415
  } catch {
1397
- // Ink optional — fall back to keep-alive without keyboard chrome.
1416
+ // Inconsequential: Ink/TUI is optional chrome — fall back to keep-alive without keyboard.
1398
1417
  await new Promise(() => {});
1399
1418
  }
1400
1419
  return { code: 0, plan: session.plan, session };
@@ -1420,6 +1439,75 @@ function isSparseManifest(manifest: Manifest): boolean {
1420
1439
  return Object.keys(flows).length === 0;
1421
1440
  }
1422
1441
 
1442
+ /**
1443
+ * Environmental syncDevSchema throws that are fine to soft-skip in the
1444
+ * compose (`dev`) boot path — no Store / no docker images yet, etc.
1445
+ *
1446
+ * @param message - Caught error message
1447
+ */
1448
+ function isBenignDevSchemaEnvSkip(message: string): boolean {
1449
+ return (
1450
+ message.startsWith("docker mode: oke.config.ts not found") ||
1451
+ message.startsWith("docker mode: no images configured")
1452
+ );
1453
+ }
1454
+
1455
+ /**
1456
+ * Whether a caught sync/push error is a schema.decl definition failure
1457
+ * (import evaluation, emit, duplicate tables, bad `.references()`, …).
1458
+ *
1459
+ * Message text alone is insufficient — emit-time bugs often look like bare
1460
+ * TypeErrors (`target.tableName`) — so also inspect the stack for declare /
1461
+ * emit frames.
1462
+ *
1463
+ * @param err - Caught value
1464
+ * @param declarePath - Resolved schema.decl path for this project
1465
+ */
1466
+ function isSchemaDeclDefinitionError(err: unknown, declarePath: string): boolean {
1467
+ const msg = err instanceof Error ? err.message : String(err);
1468
+ const stack = err instanceof Error ? (err.stack ?? "") : "";
1469
+ const blob = `${msg}\n${stack}`;
1470
+ if (/failed to load schema declare|store schema:|oke schema emit:/i.test(blob)) return true;
1471
+ if (/schema\.decl/i.test(blob)) return true;
1472
+ if (/emit-drizzle|emitColumnSource|emitDrizzleSource|maybeEmitDomainSchema/i.test(blob)) {
1473
+ return true;
1474
+ }
1475
+ return declarePath.length > 0 && blob.includes(declarePath);
1476
+ }
1477
+
1478
+ /**
1479
+ * Frame a schema-sync failure for the `oke dev` loop: warn loud for developer
1480
+ * bugs, soft-skip only known environmental gaps. Never crashes the session.
1481
+ *
1482
+ * Matches the fail-loud status style used by compose controls (`failed —` +
1483
+ * red ●), and ADOPT_BARREL_STALE's "never silent" posture — without turning
1484
+ * this path into a hard boot failure.
1485
+ *
1486
+ * @param cwd - Project root
1487
+ * @param err - Caught value from {@link syncDevSchema} / watch push
1488
+ * @param context - Boot one-shot vs schema-watch auto-push
1489
+ */
1490
+ async function formatDevSchemaSyncFailure(
1491
+ cwd: string,
1492
+ err: unknown,
1493
+ context: "boot" | "watch",
1494
+ ): Promise<string> {
1495
+ const msg = err instanceof Error ? err.message : String(err);
1496
+ const color = termColorEnabled();
1497
+ if (isBenignDevSchemaEnvSkip(msg)) {
1498
+ return formatStatusLine(`oke db push (dev) skipped — ${msg}`, color);
1499
+ }
1500
+
1501
+ const { resolveEmitPaths } = await import("../elements/store/emit-drizzle.ts");
1502
+ const { declarePath } = resolveEmitPaths(cwd);
1503
+ if (isSchemaDeclDefinitionError(err, declarePath)) {
1504
+ return formatStatusLine(`schema.decl.ts has an error — ${msg}`, color, "error");
1505
+ }
1506
+
1507
+ const label = context === "watch" ? "oke db push (schema change)" : "oke db push (dev)";
1508
+ return formatStatusLine(`${label} failed — ${msg}`, color, "error");
1509
+ }
1510
+
1423
1511
  /**
1424
1512
  * Load Manifest — prefer AoT extract from `src/` when it has flows;
1425
1513
  * otherwise on-disk JSON snapshots; otherwise a sparse extract.
@@ -1432,7 +1520,8 @@ async function tryLoadProjectManifest(cwd: string): Promise<Manifest | null | un
1432
1520
  const { extractManifest } = await import("../compiler/extract.ts");
1433
1521
  extracted = await extractManifest({ rootDir: cwd });
1434
1522
  } catch {
1435
- // Fall through to JSON snapshots.
1523
+ // Inconsequential here: extract is one of two sources — fall through to on-disk JSON
1524
+ // snapshots. A broken source tree still boots Console with the last good Manifest file.
1436
1525
  }
1437
1526
 
1438
1527
  let fromDisk: Manifest | undefined;
@@ -1492,7 +1581,7 @@ async function startAppHot(
1492
1581
  // open (create-oke afterEach `rmSync` otherwise races the child exit).
1493
1582
  proc.kill("SIGKILL");
1494
1583
  } catch {
1495
- // already exited
1584
+ // Inconsequential: process already exited — kill is idempotent best-effort.
1496
1585
  }
1497
1586
  void unlink(readyPath).catch(() => {});
1498
1587
  };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Tests for durable `.oke/state.json` project markers.
3
+ */
4
+
5
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, readFileSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { describe, expect, test } from "bun:test";
9
+ import {
10
+ isProjectSeeded,
11
+ LEGACY_SEEDED_MARKER,
12
+ markProjectSeeded,
13
+ PROJECT_STATE_REL,
14
+ readProjectState,
15
+ } from "./project-state.ts";
16
+
17
+ describe("project-state", () => {
18
+ test("markProjectSeeded writes seededAt under .oke/state.json", async () => {
19
+ const dir = mkdtempSync(join(tmpdir(), "oke-state-"));
20
+ try {
21
+ expect(await isProjectSeeded(dir)).toBe(false);
22
+ await markProjectSeeded(dir, "2026-01-02T03:04:05.000Z");
23
+ expect(await isProjectSeeded(dir)).toBe(true);
24
+ const state = await readProjectState(dir);
25
+ expect(state.seededAt).toBe("2026-01-02T03:04:05.000Z");
26
+ expect(existsSync(join(dir, PROJECT_STATE_REL))).toBe(true);
27
+ expect(existsSync(join(dir, LEGACY_SEEDED_MARKER))).toBe(false);
28
+ } finally {
29
+ rmSync(dir, { recursive: true, force: true });
30
+ }
31
+ });
32
+
33
+ test("migrates legacy .oke/seeded into state.json and removes marker", async () => {
34
+ const dir = mkdtempSync(join(tmpdir(), "oke-state-"));
35
+ try {
36
+ mkdirSync(join(dir, ".oke"), { recursive: true });
37
+ writeFileSync(join(dir, LEGACY_SEEDED_MARKER), "2025-12-01T00:00:00.000Z\n");
38
+ const state = await readProjectState(dir);
39
+ expect(state.seededAt).toBe("2025-12-01T00:00:00.000Z");
40
+ expect(existsSync(join(dir, PROJECT_STATE_REL))).toBe(true);
41
+ expect(existsSync(join(dir, LEGACY_SEEDED_MARKER))).toBe(false);
42
+ const raw = JSON.parse(readFileSync(join(dir, PROJECT_STATE_REL), "utf8")) as {
43
+ seededAt?: string;
44
+ };
45
+ expect(raw.seededAt).toBe("2025-12-01T00:00:00.000Z");
46
+ } finally {
47
+ rmSync(dir, { recursive: true, force: true });
48
+ }
49
+ });
50
+ });
@@ -0,0 +1,123 @@
1
+ /**
2
+ * `.oke/state.json` — durable local project markers (survive across `oke dev`
3
+ * sessions). Distinct from `.oke/dev.json` (live session lock) and
4
+ * `.oke/console.secret` (Console session signing key).
5
+ */
6
+
7
+ import { mkdir, unlink } from "node:fs/promises";
8
+ import { dirname, resolve } from "node:path";
9
+
10
+ /** Relative path under the project root. */
11
+ export const PROJECT_STATE_REL = ".oke/state.json";
12
+
13
+ /**
14
+ * Legacy one-shot seed marker (pre-state.json). Migrated on read then removed.
15
+ * @deprecated Prefer {@link PROJECT_STATE_REL} `seededAt`.
16
+ */
17
+ export const LEGACY_SEEDED_MARKER = ".oke/seeded";
18
+
19
+ /** Durable local project state (gitignored under `.oke/`). */
20
+ export type ProjectState = {
21
+ /** ISO timestamp when prompted `oke db seed` last succeeded. */
22
+ readonly seededAt?: string;
23
+ };
24
+
25
+ /**
26
+ * Absolute path to `.oke/state.json`.
27
+ *
28
+ * @param cwd - Project root
29
+ */
30
+ export function projectStatePath(cwd: string): string {
31
+ return resolve(cwd, PROJECT_STATE_REL);
32
+ }
33
+
34
+ /**
35
+ * Parse and validate a state object from JSON.
36
+ *
37
+ * @param raw - Unknown JSON value
38
+ */
39
+ export function parseProjectState(raw: unknown): ProjectState {
40
+ if (raw === null || typeof raw !== "object") return {};
41
+ const o = raw as Record<string, unknown>;
42
+ const seededAt = o["seededAt"];
43
+ if (typeof seededAt === "string" && seededAt.trim().length > 0) {
44
+ return { seededAt: seededAt.trim() };
45
+ }
46
+ return {};
47
+ }
48
+
49
+ /**
50
+ * Read project state, migrating a legacy `.oke/seeded` marker when present.
51
+ *
52
+ * @param cwd - Project root
53
+ */
54
+ export async function readProjectState(cwd: string): Promise<ProjectState> {
55
+ const path = projectStatePath(cwd);
56
+ let state: ProjectState = {};
57
+ if (await Bun.file(path).exists()) {
58
+ try {
59
+ state = parseProjectState(await Bun.file(path).json());
60
+ } catch {
61
+ state = {};
62
+ }
63
+ }
64
+
65
+ if (state.seededAt) return state;
66
+
67
+ const legacy = resolve(cwd, LEGACY_SEEDED_MARKER);
68
+ if (!(await Bun.file(legacy).exists())) return state;
69
+
70
+ let seededAt = new Date().toISOString();
71
+ try {
72
+ const text = (await Bun.file(legacy).text()).trim();
73
+ if (text.length > 0) seededAt = text.split(/\r?\n/, 1)[0]!.trim() || seededAt;
74
+ } catch {
75
+ // keep generated timestamp
76
+ }
77
+ const migrated: ProjectState = { ...state, seededAt };
78
+ await writeProjectState(cwd, migrated);
79
+ try {
80
+ await unlink(legacy);
81
+ } catch {
82
+ // best-effort
83
+ }
84
+ return migrated;
85
+ }
86
+
87
+ /**
88
+ * Write project state (creates `.oke/` as needed).
89
+ *
90
+ * @param cwd - Project root
91
+ * @param state - Full state payload
92
+ */
93
+ export async function writeProjectState(cwd: string, state: ProjectState): Promise<void> {
94
+ const path = projectStatePath(cwd);
95
+ await mkdir(dirname(path), { recursive: true });
96
+ const body: Record<string, string> = {};
97
+ if (state.seededAt) body.seededAt = state.seededAt;
98
+ await Bun.write(path, `${JSON.stringify(body, null, 2)}\n`);
99
+ }
100
+
101
+ /**
102
+ * Whether prompted seed already completed for this project.
103
+ *
104
+ * @param cwd - Project root
105
+ */
106
+ export async function isProjectSeeded(cwd: string): Promise<boolean> {
107
+ const state = await readProjectState(cwd);
108
+ return typeof state.seededAt === "string" && state.seededAt.length > 0;
109
+ }
110
+
111
+ /**
112
+ * Record a successful prompted seed (ISO timestamp).
113
+ *
114
+ * @param cwd - Project root
115
+ * @param at - Optional timestamp (defaults to now)
116
+ */
117
+ export async function markProjectSeeded(
118
+ cwd: string,
119
+ at: string = new Date().toISOString(),
120
+ ): Promise<void> {
121
+ const prev = await readProjectState(cwd);
122
+ await writeProjectState(cwd, { ...prev, seededAt: at });
123
+ }
@@ -2,12 +2,13 @@
2
2
  * `oke vault` builtin loop — init · status · seal/unseal · rotate · audit ·
3
3
  * backup/restore.
4
4
  *
5
- * Every case runs against a real PGlite instance shared across the
6
- * subcommands of one scenario, which is what a live vault looks like: each
7
- * `oke vault …` is a fresh process that must re-supply the master key.
5
+ * Logic-only cases (init / status seal-state) use the in-memory Vault SQL
6
+ * fake so they do not pay PGlite WASM cold-start. Dialect-heavy cases
7
+ * (rotate, backup, audit purge, …) share one warmed file-scoped PGlite
8
+ * (cold WASM once; {@link resetVaultTables} between tests).
8
9
  */
9
10
 
10
- import { describe, expect, test } from "bun:test";
11
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
11
12
  import { mkdtemp } from "node:fs/promises";
12
13
  import { tmpdir } from "node:os";
13
14
  import { join } from "node:path";
@@ -18,8 +19,20 @@ import {
18
19
  sqlConnectionAsExec,
19
20
  type BuiltinVaultAdapter,
20
21
  } from "../elements/vault/builtin-adapter.ts";
22
+ import { createMemoryVaultSql, resetVaultTables } from "../elements/vault/test-helpers.ts";
21
23
  import { vaultCli } from "./vault-cmd.ts";
22
24
 
25
+ /** File-scoped warmed PGlite for dialect-heavy CLI cases. */
26
+ let sharedPglite: SqlConnection;
27
+
28
+ beforeAll(async () => {
29
+ sharedPglite = await connectPglite({ url: "memory://vault-cli-shared" });
30
+ }, 15_000);
31
+
32
+ afterAll(async () => {
33
+ await sharedPglite.close();
34
+ });
35
+
23
36
  /** A CLI bound to one in-memory vault, plus its captured stdout. */
24
37
  interface Harness {
25
38
  readonly sql: SqlConnection;
@@ -31,13 +44,28 @@ interface Harness {
31
44
  close(): Promise<void>;
32
45
  }
33
46
 
47
+ /** Options for {@link harness}. */
48
+ interface HarnessOptions {
49
+ /** Project root for relative file arguments. */
50
+ readonly cwd?: string;
51
+ /**
52
+ * Pre-opened SQL. Defaults to the file-scoped warmed PGlite. Pass
53
+ * {@link createMemoryVaultSql} for logic-only cases that must not pay
54
+ * WASM cold-start.
55
+ */
56
+ readonly sql?: SqlConnection;
57
+ }
58
+
34
59
  /**
35
- * Open a CLI harness over a fresh PGlite instance.
60
+ * Open a CLI harness over an injected or shared PGlite connection.
36
61
  *
37
- * @param cwd - Project root for relative file arguments
62
+ * @param options - Cwd / SQL backend
38
63
  */
39
- async function harness(cwd?: string): Promise<Harness> {
40
- const sql = await connectPglite({ url: "memory://vault-cli-test" });
64
+ async function harness(options: HarnessOptions = {}): Promise<Harness> {
65
+ const injected = options.sql !== undefined;
66
+ if (!injected) await resetVaultTables(sharedPglite);
67
+ const sql = options.sql ?? sharedPglite;
68
+ const cwd = options.cwd;
41
69
  let captured = "";
42
70
  return {
43
71
  sql,
@@ -55,7 +83,7 @@ async function harness(cwd?: string): Promise<Harness> {
55
83
  reset: () => {
56
84
  captured = "";
57
85
  },
58
- close: () => sql.close(),
86
+ close: () => (injected ? sql.close() : Promise.resolve()),
59
87
  };
60
88
  }
61
89
 
@@ -84,7 +112,8 @@ async function attach(sql: SqlConnection, masterKey: string): Promise<BuiltinVau
84
112
 
85
113
  describe("oke vault — builtin lifecycle", () => {
86
114
  test("init prints the master key once and status reflects seal state", async () => {
87
- const h = await harness();
115
+ // Logic-only: seal-state + init CLI copy — no Postgres dialect under test.
116
+ const h = await harness({ sql: createMemoryVaultSql() });
88
117
  try {
89
118
  // A never-initialized vault reports rather than crashes.
90
119
  expect(await h.run(["status"])).toBe(0);
@@ -121,7 +150,7 @@ describe("oke vault — builtin lifecycle", () => {
121
150
  });
122
151
 
123
152
  test("unseal requires a key and seal records the transition", async () => {
124
- const h = await harness();
153
+ const h = await harness({});
125
154
  try {
126
155
  await h.run(["init"]);
127
156
  const masterKey = masterKeyFrom(h.output());
@@ -149,7 +178,7 @@ describe("oke vault — builtin lifecycle", () => {
149
178
 
150
179
  describe("oke vault rotate", () => {
151
180
  test("rotate re-encrypts the current value under a new version", async () => {
152
- const h = await harness();
181
+ const h = await harness({});
153
182
  try {
154
183
  await h.run(["init"]);
155
184
  const masterKey = masterKeyFrom(h.output());
@@ -179,7 +208,7 @@ describe("oke vault rotate", () => {
179
208
  });
180
209
 
181
210
  test("rotate-master issues a new key and retires the old one", async () => {
182
- const h = await harness();
211
+ const h = await harness({});
183
212
  try {
184
213
  await h.run(["init"]);
185
214
  const first = masterKeyFrom(h.output());
@@ -211,7 +240,7 @@ describe("oke vault rotate", () => {
211
240
 
212
241
  describe("oke vault audit", () => {
213
242
  test("lists rows, verifies the chain, and purges by date", async () => {
214
- const h = await harness();
243
+ const h = await harness({});
215
244
  try {
216
245
  await h.run(["init"]);
217
246
  const masterKey = masterKeyFrom(h.output());
@@ -256,7 +285,7 @@ describe("oke vault audit", () => {
256
285
  describe("oke vault backup / restore", () => {
257
286
  test("round-trips every live secret through an encrypted bundle", async () => {
258
287
  const dir = await mkdtemp(join(tmpdir(), "oke-vault-cli-"));
259
- const h = await harness(dir);
288
+ const h = await harness({ cwd: dir });
260
289
  try {
261
290
  await h.run(["init"]);
262
291
  const masterKey = masterKeyFrom(h.output());
@@ -319,7 +348,7 @@ describe("oke vault help", () => {
319
348
 
320
349
  describe("oke vault secure master-key input", () => {
321
350
  test("unseal accepts --key - via injected stdin", async () => {
322
- const h = await harness();
351
+ const h = await harness({});
323
352
  try {
324
353
  await h.run(["init"]);
325
354
  const masterKey = masterKeyFrom(h.output());
@@ -340,7 +369,7 @@ describe("oke vault secure master-key input", () => {
340
369
  });
341
370
 
342
371
  test("unseal accepts an injected interactive prompt", async () => {
343
- const h = await harness();
372
+ const h = await harness({});
344
373
  try {
345
374
  await h.run(["init"]);
346
375
  const masterKey = masterKeyFrom(h.output());
@@ -367,7 +396,7 @@ describe("oke vault secure master-key input", () => {
367
396
 
368
397
  describe("oke vault purge-expired", () => {
369
398
  test("dry-run counts without deleting; live purge removes expired rows", async () => {
370
- const h = await harness();
399
+ const h = await harness({});
371
400
  try {
372
401
  await h.run(["init"]);
373
402
  const masterKey = masterKeyFrom(h.output());
@@ -755,7 +755,8 @@ async function runVaultBackup(
755
755
  return 1;
756
756
  }
757
757
  const blob = await withVault(options, flags, key, (a) => a.exportBackup());
758
- await Bun.write(resolve(cwd, file), blob);
758
+ const { writeBackupFileAtomic } = await import("../elements/vault/builtin-adapter.ts");
759
+ await writeBackupFileAtomic(resolve(cwd, file), blob);
759
760
  write(`oke vault: wrote ${blob.byteLength} encrypted byte(s) to ${file}\n`);
760
761
  return 0;
761
762
  }
@@ -803,7 +803,7 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
803
803
  }
804
804
  }
805
805
 
806
- // model.prompt("ticket-triage", { version, evals, budget })
806
+ // model.prompt("ticket-triage", { version, evals, budget, via, timeout })
807
807
  if (prop === "prompt") {
808
808
  const promptName = stringArg(call.arguments[0]);
809
809
  const opts = objectArg(call.arguments[1]);
@@ -811,6 +811,10 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
811
811
  const version = numberProp(opts, "version");
812
812
  const evals = stringProp(opts, "evals");
813
813
  const budgetObj = objectProp(opts, "budget");
814
+ const via = stringArrayProp(opts, "via");
815
+ const timeoutStr = stringProp(opts, "timeout");
816
+ const timeoutNum = numberProp(opts, "timeout");
817
+ const timeout = timeoutStr ?? timeoutNum;
814
818
  const prompt: AiPrompt = {
815
819
  ...(version !== undefined ? { version } : {}),
816
820
  ...(evals ? { evals } : {}),
@@ -822,9 +826,16 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
822
826
  maxCostPerCall: numberProp(budgetObj, "maxCostPerCall"),
823
827
  }
824
828
  : {}),
829
+ ...(numberProp(budgetObj, "maxCostPerRun") !== undefined
830
+ ? {
831
+ maxCostPerRun: numberProp(budgetObj, "maxCostPerRun"),
832
+ }
833
+ : {}),
825
834
  },
826
835
  }
827
836
  : {}),
837
+ ...(via && via.length > 0 ? { via } : {}),
838
+ ...(timeout !== undefined ? { timeout } : {}),
828
839
  };
829
840
  scope.ai.prompts = scope.ai.prompts ?? {};
830
841
  scope.ai.prompts[promptName] = prompt;
@@ -323,11 +323,13 @@ describe("console serve security", () => {
323
323
  secret: "serve-secret",
324
324
  silentClaim: true,
325
325
  env: "test",
326
+ // Security suite is hermetic — do not inherit a host DATABASE_URL.
327
+ persist: false,
326
328
  });
327
329
  });
328
330
 
329
331
  afterAll(() => {
330
- server.stop(true);
332
+ server?.stop(true);
331
333
  });
332
334
 
333
335
  test("rejects unexpected Host", async () => {