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
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Sync auth wiring for `oke({ gate: { auth } })`.
3
+ *
4
+ * Loaded only when auth is configured so HTTP-only cold graphs never import
5
+ * bindings / sessions / identity (`requirePackageModule` in `app.ts`).
6
+ */
7
+
8
+ import type { ResolvedGateAuth } from "../auth/config.ts";
9
+ import { auth as authPlugin } from "../auth/plugin.ts";
10
+ import { createAuthHttpBindings, type AuthHttpMaterialization } from "../auth/bindings.ts";
11
+ import { tokenFromCookieHeader } from "../auth/cookies.ts";
12
+ import { setActiveGateAuthContext } from "../auth/method-context.ts";
13
+ import type { ResolvedGateConfig } from "../elements/gate/config.ts";
14
+ import { createAppAuthBinding, verifyBearerToken, type AppAuthBinding } from "./auth-resolve.ts";
15
+ import type { PluginDef } from "./plugin.ts";
16
+
17
+ /** Result of wiring Gate auth at construction. */
18
+ export interface WiredGateAuth {
19
+ readonly materialization: AuthHttpMaterialization | undefined;
20
+ readonly authPlugin: PluginDef | undefined;
21
+ readonly authBinding: AppAuthBinding;
22
+ readonly verifyBearerToken: typeof verifyBearerToken;
23
+ readonly tokenFromCookieHeader: typeof tokenFromCookieHeader;
24
+ /** Rebuild binding when boot supplies a clock. */
25
+ rebind(now: () => number): AppAuthBinding;
26
+ }
27
+
28
+ /** Options for {@link wireGateAuth}. */
29
+ export interface WireGateAuthOptions {
30
+ readonly gateConfig: ResolvedGateConfig & { readonly auth: ResolvedGateAuth };
31
+ /** Clock fallback when `gate.auth.now` is omitted (`oke({ fx: { now } })`). */
32
+ readonly now?: () => number;
33
+ }
34
+
35
+ /**
36
+ * Materialize auth HTTP bindings, absorb the builtin auth plugin, and publish
37
+ * the active Gate auth context for `.plug(username())` etc.
38
+ *
39
+ * @param options - Resolved Gate bag + optional clock
40
+ */
41
+ export function wireGateAuth(options: WireGateAuthOptions): WiredGateAuth {
42
+ const { gateConfig } = options;
43
+ const auth = gateConfig.auth;
44
+ let materialization: AuthHttpMaterialization | undefined;
45
+ if (auth.http) {
46
+ materialization = createAuthHttpBindings(auth, {
47
+ rateLimitEnabled: gateConfig.rateLimitEnabled,
48
+ sessions: auth.sessions,
49
+ });
50
+ }
51
+
52
+ const pluginDef = authPlugin({
53
+ secret: auth.secret,
54
+ accessTtlMs: auth.session.accessTtlMs,
55
+ refreshTtlMs: auth.session.refreshTtlMs,
56
+ session: {
57
+ accessTtlMs: auth.session.accessTtlMs,
58
+ refreshTtlMs: auth.session.refreshTtlMs,
59
+ idleTtlMs: auth.session.idleTtlMs,
60
+ absoluteTtlMs: auth.session.absoluteTtlMs,
61
+ singleSessionPerUser: auth.session.singleSessionPerUser,
62
+ },
63
+ password: auth.password,
64
+ passwordPolicy: auth.passwordPolicy,
65
+ breachCheck: auth.breachCheck,
66
+ });
67
+
68
+ let authBinding = createAppAuthBinding({
69
+ secret: auth.secret,
70
+ sessions: materialization?.ctx.sessions ?? auth.sessions,
71
+ now: auth.now ?? options.now,
72
+ });
73
+
74
+ setActiveGateAuthContext({
75
+ secret: authBinding.secret,
76
+ sessions: authBinding.sessions,
77
+ now: authBinding.now,
78
+ passwordPolicy: auth.passwordPolicy,
79
+ password: auth.password,
80
+ breachCheck: auth.breachCheck,
81
+ });
82
+
83
+ return {
84
+ materialization,
85
+ authPlugin: pluginDef,
86
+ authBinding,
87
+ verifyBearerToken,
88
+ tokenFromCookieHeader,
89
+ rebind(now) {
90
+ authBinding = createAppAuthBinding({
91
+ secret: authBinding.secret,
92
+ sessions: authBinding.sessions,
93
+ now,
94
+ });
95
+ return authBinding;
96
+ },
97
+ };
98
+ }
package/src/kernel/app.ts CHANGED
@@ -6,12 +6,9 @@
6
6
  * pipeline, and wires `fx.call` to untriggered (and triggered) flows.
7
7
  */
8
8
 
9
- import {
10
- compileRoute,
11
- encodeExecuteResult,
12
- encodeFailure,
13
- type CompiledRoute,
14
- } from "../compiler/index.ts";
9
+ import { compileRoute } from "../compiler/dynamic.ts";
10
+ import type { CompiledRoute } from "../compiler/aot.ts";
11
+ import { encodeExecuteResult, encodeFailure } from "../compiler/response.ts";
15
12
  import { validate } from "../validation/standard-schema.ts";
16
13
  import {
17
14
  accumulateAdoptArgs,
@@ -25,16 +22,15 @@ import {
25
22
  // when an app actually boots (AGENTS.md / unified-theory budget: cold start < 75 ms).
26
23
  import type { BootOptions, BootResult, ElementRuntimes } from "./boot.ts";
27
24
  import type { CapabilityToken } from "./capability.ts";
28
- import { createAppAuthBinding, verifyBearerToken, type AppAuthBinding } from "./auth-resolve.ts";
29
- import { createAuthHttpBindings, type AuthHttpMaterialization } from "../auth/bindings.ts";
30
- import { auth as authPlugin } from "../auth/plugin.ts";
31
- import { tokenFromCookieHeader } from "../auth/cookies.ts";
32
- import { setActiveGateAuthContext } from "../auth/method-context.ts";
25
+ import type { AppAuthBinding } from "./auth-resolve.ts";
26
+ import type { AuthHttpMaterialization } from "../auth/bindings.ts";
27
+ import type { WiredGateAuth } from "./app-auth.ts";
33
28
  import {
34
29
  resolveGateConfig,
35
30
  type GateOptions,
36
31
  type ResolvedGateConfig,
37
32
  } from "../elements/gate/config.ts";
33
+ import { requirePackageModule } from "../shared/lazy-src.ts";
38
34
  import type { TemplateCatalog } from "../elements/channel/runtime.ts";
39
35
  import { parseAcceptLanguage } from "../elements/channel/locale.ts";
40
36
  import { runWithLocale } from "../i18n/locale-context.ts";
@@ -85,6 +81,10 @@ import { releaseInstanceLeases } from "./graceful-shutdown.ts";
85
81
  import type { JournalRuntime } from "./boot-bind/journal.ts";
86
82
  import { listBindings, resetBindings, type Binding } from "./on.ts";
87
83
  import {
84
+ aiAgentRegistry,
85
+ aiEmbedRegistry,
86
+ aiModelRegistry,
87
+ aiPromptRegistry,
88
88
  channelTemplateRegistry,
89
89
  requiredEnvRegistry,
90
90
  secretRegistry,
@@ -465,6 +465,45 @@ function mergeUnique<T>(explicit: readonly T[] | undefined, fromRegistry: readon
465
465
  return out;
466
466
  }
467
467
 
468
+ /**
469
+ * Merge explicit `oke({ ai })` with auto-drained AI decls. Returns
470
+ * `undefined` when neither side contributes — a defined empty object would
471
+ * flip {@link resolveElementNeeds}'s `ai` flag for every app.
472
+ *
473
+ * @param explicit - Hand-passed AI boot options
474
+ * @param fromRegistry - Models / prompts / embeds / agents from declare registries
475
+ */
476
+ function mergeAiOptions(
477
+ explicit: BootOptions["ai"] | undefined,
478
+ fromRegistry: {
479
+ readonly models: NonNullable<BootOptions["ai"]>["models"];
480
+ readonly prompts: NonNullable<BootOptions["ai"]>["prompts"];
481
+ readonly embeds: NonNullable<BootOptions["ai"]>["embeds"];
482
+ readonly agents: NonNullable<BootOptions["ai"]>["agents"];
483
+ },
484
+ ): BootOptions["ai"] | undefined {
485
+ const models = mergeUnique(explicit?.models, fromRegistry.models ?? []);
486
+ const prompts = mergeUnique(explicit?.prompts, fromRegistry.prompts ?? []);
487
+ const embeds = mergeUnique(explicit?.embeds, fromRegistry.embeds ?? []);
488
+ const agents = mergeUnique(explicit?.agents, fromRegistry.agents ?? []);
489
+ if (
490
+ explicit === undefined &&
491
+ models.length === 0 &&
492
+ prompts.length === 0 &&
493
+ embeds.length === 0 &&
494
+ agents.length === 0
495
+ ) {
496
+ return undefined;
497
+ }
498
+ return {
499
+ ...(explicit ?? {}),
500
+ ...(models.length > 0 ? { models } : {}),
501
+ ...(prompts.length > 0 ? { prompts } : {}),
502
+ ...(embeds.length > 0 ? { embeds } : {}),
503
+ ...(agents.length > 0 ? { agents } : {}),
504
+ };
505
+ }
506
+
468
507
  /**
469
508
  * Create an application. Adopts bindings registered via {@link on}.
470
509
  *
@@ -480,20 +519,35 @@ export function oke(options: OkeOptions): OkeApp {
480
519
  if (registry === "consume") resetBindings();
481
520
 
482
521
  // Same registry mode drains store.sql/store.files, vault.secret, signal(),
483
- // and channel.<medium>().template() module-evaluation registries that
484
- // mirror `on`'s trigger drain (`listBindings`/`resetBindings` above).
485
- // Explicit `options.stores` / `secrets` / `signals` / `channel.templates`
486
- // are additive, never silently ignored deduped by reference so an
487
- // explicitly-passed decl that is also in the registry is not doubled.
522
+ // channel.<medium>().template(), and ai.model/prompt/embed/agent
523
+ // module-evaluation registries that mirror `on`'s trigger drain
524
+ // (`listBindings`/`resetBindings` above). Explicit `options.stores` /
525
+ // `secrets` / `signals` / `channel.templates` / `ai` are additive, never
526
+ // silently ignored deduped by reference so an explicitly-passed decl
527
+ // that is also in the registry is not doubled.
488
528
  const registrySnapshot =
489
529
  registry === "ignore"
490
- ? { stores: [], secrets: [], requiredEnv: [], signals: [], channelTemplates: [] }
530
+ ? {
531
+ stores: [],
532
+ secrets: [],
533
+ requiredEnv: [],
534
+ signals: [],
535
+ channelTemplates: [],
536
+ aiModels: [],
537
+ aiPrompts: [],
538
+ aiEmbeds: [],
539
+ aiAgents: [],
540
+ }
491
541
  : {
492
542
  stores: storeRegistry.slice(),
493
543
  secrets: secretRegistry.slice(),
494
544
  requiredEnv: requiredEnvRegistry.slice(),
495
545
  signals: signalRegistry.slice(),
496
546
  channelTemplates: channelTemplateRegistry.slice(),
547
+ aiModels: aiModelRegistry.slice(),
548
+ aiPrompts: aiPromptRegistry.slice(),
549
+ aiEmbeds: aiEmbedRegistry.slice(),
550
+ aiAgents: aiAgentRegistry.slice(),
497
551
  };
498
552
  if (registry === "consume") {
499
553
  storeRegistry.length = 0;
@@ -501,6 +555,10 @@ export function oke(options: OkeOptions): OkeApp {
501
555
  requiredEnvRegistry.length = 0;
502
556
  signalRegistry.length = 0;
503
557
  channelTemplateRegistry.length = 0;
558
+ aiModelRegistry.length = 0;
559
+ aiPromptRegistry.length = 0;
560
+ aiEmbedRegistry.length = 0;
561
+ aiAgentRegistry.length = 0;
504
562
  }
505
563
  const effectiveStores = mergeUnique(options.stores, registrySnapshot.stores);
506
564
  const effectiveSecrets = mergeUnique(options.secrets, registrySnapshot.secrets);
@@ -527,6 +585,14 @@ export function oke(options: OkeOptions): OkeApp {
527
585
  ...(options.channel ?? {}),
528
586
  templates: mergeUnique(options.channel?.templates, registrySnapshot.channelTemplates),
529
587
  };
588
+ // Same undefined-when-empty rule for AI — a bare `{}` would force the AI
589
+ // runtime open even when the app never declared models / prompts.
590
+ const effectiveAi = mergeAiOptions(options.ai, {
591
+ models: registrySnapshot.aiModels,
592
+ prompts: registrySnapshot.aiPrompts,
593
+ embeds: registrySnapshot.aiEmbeds,
594
+ agents: registrySnapshot.aiAgents,
595
+ });
530
596
 
531
597
  // Resolve Gate bag early so auth HTTP Bindings join `adopted` + the router
532
598
  // before posture audit (same ensureBoot → doBoot path — never a side channel).
@@ -534,17 +600,12 @@ export function oke(options: OkeOptions): OkeApp {
534
600
  gate: options.gate,
535
601
  env: options.env,
536
602
  });
537
- let authMaterialization: AuthHttpMaterialization | undefined;
538
- if (gateConfig.auth?.http) {
539
- authMaterialization = createAuthHttpBindings(gateConfig.auth, {
540
- rateLimitEnabled: gateConfig.rateLimitEnabled,
541
- sessions: gateConfig.auth.sessions,
542
- });
543
- adopted.push(...authMaterialization.bindings);
544
- }
545
603
 
546
- /** Retained for test harness / boot merges. */
547
- const $options = options;
604
+ /** Retained for test harness / boot merges (includes auto-drained AI decls). */
605
+ const $options: OkeOptions =
606
+ effectiveAi === undefined || options.ai === effectiveAi
607
+ ? options
608
+ : { ...options, ai: effectiveAi };
548
609
 
549
610
  const appHooks: HookMap = {};
550
611
  const unitHooks = new Map<string, HookMap>();
@@ -555,27 +616,26 @@ export function oke(options: OkeOptions): OkeApp {
555
616
  /** Runtime route table — types accumulate on the returned {@link OkeApp}. */
556
617
  const routes: RuntimeRouteMap = {};
557
618
 
558
- // Absorb auth() tables into gate.auth — `.needs("auth")` sees plugin name.
619
+ let authMaterialization: AuthHttpMaterialization | undefined;
620
+ let wiredAuth: WiredGateAuth | undefined;
621
+ let authBinding: AppAuthBinding | undefined;
559
622
  if (gateConfig.auth) {
560
- applyPlugin(
561
- pluginRegistry,
562
- authPlugin({
563
- secret: gateConfig.auth.secret,
564
- accessTtlMs: gateConfig.auth.session.accessTtlMs,
565
- refreshTtlMs: gateConfig.auth.session.refreshTtlMs,
566
- session: {
567
- accessTtlMs: gateConfig.auth.session.accessTtlMs,
568
- refreshTtlMs: gateConfig.auth.session.refreshTtlMs,
569
- idleTtlMs: gateConfig.auth.session.idleTtlMs,
570
- absoluteTtlMs: gateConfig.auth.session.absoluteTtlMs,
571
- singleSessionPerUser: gateConfig.auth.session.singleSessionPerUser,
572
- },
573
- password: gateConfig.auth.password,
574
- passwordPolicy: gateConfig.auth.passwordPolicy,
575
- breachCheck: gateConfig.auth.breachCheck,
576
- }),
577
- appPluginScope,
623
+ const { wireGateAuth } = requirePackageModule<typeof import("./app-auth.ts")>(
624
+ "kernel/app-auth",
625
+ "app-auth",
578
626
  );
627
+ wiredAuth = wireGateAuth({
628
+ gateConfig: { ...gateConfig, auth: gateConfig.auth },
629
+ now: options.fx?.now,
630
+ });
631
+ authMaterialization = wiredAuth.materialization;
632
+ authBinding = wiredAuth.authBinding;
633
+ if (authMaterialization) {
634
+ adopted.push(...authMaterialization.bindings);
635
+ }
636
+ if (wiredAuth.authPlugin) {
637
+ applyPlugin(pluginRegistry, wiredAuth.authPlugin, appPluginScope);
638
+ }
579
639
  }
580
640
 
581
641
  const smart = createRouter<Binding>(options.router ?? "default");
@@ -671,29 +731,10 @@ export function oke(options: OkeOptions): OkeApp {
671
731
  let bootPromise: Promise<BootResult> | undefined;
672
732
  let readyState: ReadyState = "booting";
673
733
  let bootEnv: BootOptions["env"] = options.env;
674
- let authBinding: AppAuthBinding | undefined =
675
- gateConfig.auth !== undefined
676
- ? createAppAuthBinding({
677
- secret: gateConfig.auth.secret,
678
- sessions: authMaterialization?.ctx.sessions ?? gateConfig.auth.sessions,
679
- now: gateConfig.auth.now ?? options.fx?.now,
680
- })
681
- : undefined;
682
- if (authBinding) {
683
- setActiveGateAuthContext({
684
- secret: authBinding.secret,
685
- sessions: authBinding.sessions,
686
- now: authBinding.now,
687
- passwordPolicy: gateConfig.auth?.passwordPolicy,
688
- password: gateConfig.auth?.password,
689
- breachCheck: gateConfig.auth?.breachCheck,
690
- });
691
- } else {
692
- setActiveGateAuthContext(undefined);
693
- }
694
734
  // Fallback journal for pre-boot / `autoBoot: false` unit tests. A booted
695
735
  // app replaces this with the bound `drivers.journal` store (postgres etc.).
696
- const fallbackJournalStore = createMemoryJournalStore();
736
+ // Constructed on first use so non-durable HTTP apps skip the alloc.
737
+ let fallbackJournalStore: JournalStore | undefined;
697
738
  const fallbackJournalInstanceId = `app-${crypto.randomUUID()}`;
698
739
  const sleepingRuns = new Map<
699
740
  string,
@@ -715,6 +756,7 @@ export function oke(options: OkeOptions): OkeApp {
715
756
  if (bound) {
716
757
  return { store: bound.store, instanceId: bound.instanceId, leaseMs: bound.leaseMs };
717
758
  }
759
+ fallbackJournalStore ??= createMemoryJournalStore();
718
760
  return {
719
761
  store: fallbackJournalStore,
720
762
  instanceId: fallbackJournalInstanceId,
@@ -859,7 +901,7 @@ export function oke(options: OkeOptions): OkeApp {
859
901
  clocks: overrides?.clocks ?? options.clocks,
860
902
  stores: overrides?.stores ?? effectiveStores,
861
903
  channel: overrides?.channel ?? effectiveChannel,
862
- ai: overrides?.ai ?? options.ai,
904
+ ai: overrides?.ai ?? effectiveAi,
863
905
  runs: overrides?.runs ?? options.runs,
864
906
  bindings: adopted,
865
907
  flows: [...flowsByName.values()],
@@ -938,7 +980,7 @@ export function oke(options: OkeOptions): OkeApp {
938
980
  (overrides?.config ?? options.config)?.i18n?.default ??
939
981
  "en",
940
982
  },
941
- ai: overrides?.ai ?? options.ai,
983
+ ai: overrides?.ai ?? effectiveAi,
942
984
  runs: overrides?.runs ?? options.runs,
943
985
  now: overrides?.now ?? options.fx?.now,
944
986
  instanceId: overrides?.instanceId,
@@ -978,12 +1020,9 @@ export function oke(options: OkeOptions): OkeApp {
978
1020
  readyState = "ready";
979
1021
  }
980
1022
  // Prefer the booted clock for access-token expiry checks.
981
- if (authBinding) {
982
- authBinding = createAppAuthBinding({
983
- secret: authBinding.secret,
984
- sessions: authBinding.sessions,
985
- now: result.clock?.now.bind(result.clock) ?? authBinding.now,
986
- });
1023
+ if (wiredAuth && authBinding) {
1024
+ const now = result.clock?.now.bind(result.clock) ?? authBinding.now;
1025
+ authBinding = wiredAuth.rebind(now);
987
1026
  }
988
1027
  return result;
989
1028
  }
@@ -1144,21 +1183,24 @@ export function oke(options: OkeOptions): OkeApp {
1144
1183
 
1145
1184
  const binding = authBinding;
1146
1185
  const cookieOpts = gateConfig.auth?.cookies;
1186
+ const verifyBearer = wiredAuth?.verifyBearerToken;
1187
+ const cookieToken = wiredAuth?.tokenFromCookieHeader;
1147
1188
  const elementHooks = createElementPipelineHooks({
1148
1189
  gates: booted.gate,
1149
1190
  principals,
1150
1191
  telemetry,
1151
1192
  allowTestPrincipals: testMode,
1152
- verifyBearer: binding ? async (token) => verifyBearerToken(binding, token) : undefined,
1193
+ verifyBearer:
1194
+ binding && verifyBearer ? async (token) => verifyBearer(binding, token) : undefined,
1153
1195
  // Phase 1a: opt-in cookie → Bearer when Authorization is absent.
1154
1196
  resolveToken:
1155
- binding && cookieOpts?.enabled
1197
+ binding && cookieOpts?.enabled && cookieToken
1156
1198
  ? (request) => {
1157
1199
  const header = request.headers.get("authorization");
1158
1200
  if (header?.startsWith("Bearer ")) {
1159
1201
  return header.slice("Bearer ".length).trim() || undefined;
1160
1202
  }
1161
- return tokenFromCookieHeader(request.headers.get("cookie"), cookieOpts);
1203
+ return cookieToken(request.headers.get("cookie"), cookieOpts);
1162
1204
  }
1163
1205
  : undefined,
1164
1206
  });
@@ -20,7 +20,7 @@ import { flow } from "./flow.ts";
20
20
  import { on, resetBindings } from "./on.ts";
21
21
  import { http } from "./triggers.ts";
22
22
 
23
- describe("oke() auto-registry — stores/secrets/signals/channel.templates", () => {
23
+ describe("oke() auto-registry — stores/secrets/signals/channel.templates/ai", () => {
24
24
  test("before/after: zero explicit arrays boots identically to the explicit-array form", async () => {
25
25
  resetBindings();
26
26
 
@@ -195,4 +195,55 @@ describe("oke() auto-registry — stores/secrets/signals/channel.templates", ()
195
195
  expect(appB.bootResult?.signal).toBeUndefined();
196
196
  expect(appB.bootResult?.channel?.templates.has("leak-template") ?? false).toBe(false);
197
197
  });
198
+
199
+ test("ai.model / .prompt auto-drain into oke options (no explicit oke({ ai }))", async () => {
200
+ const { ai } = await import("../elements/ai/declare.ts");
201
+ const { mockAiDriver } = await import("../drivers/ai-mock.ts");
202
+ const { createAiRuntime } = await import("../elements/ai/runtime.ts");
203
+
204
+ const smart = ai.model("smart", { provider: "mock" });
205
+ const summarizeNote = smart.prompt("summarize-note", {
206
+ version: 1,
207
+ });
208
+
209
+ const ping = flow("ai.ping", {
210
+ effects: { asks: ["summarize-note"] },
211
+ do: async (_input, fx) => fx.ask("summarize-note", { title: "t", body: "b" }),
212
+ });
213
+ on(http.post("/ai-ping"), ping);
214
+
215
+ const app = oke({
216
+ name: "ai-auto-registry",
217
+ autoBoot: false,
218
+ startScheduler: false,
219
+ gate: { unguardedHttp: "allow" },
220
+ });
221
+
222
+ expect(app.$options.ai?.models?.some((m) => m.name === "smart")).toBe(true);
223
+ expect(app.$options.ai?.prompts?.some((p) => p.name === "summarize-note")).toBe(true);
224
+
225
+ const runtime = createAiRuntime({
226
+ models: [smart],
227
+ prompts: [summarizeNote],
228
+ defaultDriver: {
229
+ id: "mock",
230
+ open: () =>
231
+ mockAiDriver.open({
232
+ mockResponses: { "*": { summary: "one-line summary" } },
233
+ }),
234
+ },
235
+ });
236
+ await app.boot({
237
+ env: "test",
238
+ unguardedHttp: "allow",
239
+ elements: { ai: runtime },
240
+ });
241
+
242
+ const res = await app.fetch(
243
+ new Request("http://127.0.0.1/ai-ping", { method: "POST", body: "{}" }),
244
+ );
245
+ expect(res.status).toBe(200);
246
+ const json = (await res.json()) as { data: { summary: string } };
247
+ expect(json.data.summary).toBe("one-line summary");
248
+ });
198
249
  });
@@ -79,7 +79,7 @@ describe("boot — lazy element needs", () => {
79
79
  expect(needs.signal).toBe(false);
80
80
  });
81
81
 
82
- test("oke() Store-only graph stays under the prior 54.3 kB baseline", async () => {
82
+ test("oke() Store-only graph stays under the prior 39 kB baseline", async () => {
83
83
  const dir = await mkdtemp(join(tmpdir(), "oke-store-only-"));
84
84
  const entry = join(dir, "entry.ts");
85
85
  const appPath = join(import.meta.dir, "app.ts");
@@ -116,23 +116,9 @@ describe("boot — lazy element needs", () => {
116
116
  if (raw.byteLength === 0) continue;
117
117
  total += Bun.gzipSync(new Uint8Array(raw)).byteLength;
118
118
  }
119
- // Rebased after fx.sendOtp/verifyOtp on the shared fx surface (~50.1 kB
120
- // gzip), durable-journal lease surface (~51.2 kB), fx.deliverOtp +
121
- // unified otp() (~50.9 kB → 53 kB), then fx.runs / SLO window on the
122
- // shared surface (~53.3 kB → 54 kB cap). Clock/channel/journal drivers
123
- // stay lazy-bound; far below eager bind. Rebased again for the
124
- // store/vault/signal/channel auto-registry drain (`element-registries.ts`
125
- // + the `oke()` merge/dedup logic) — every app now carries this
126
- // regardless of which elements it uses, since the drain must run
127
- // synchronously at construction (same isolation guarantee as the
128
- // `on()` binding registry) and can't be deferred behind a lazy import
129
- // (~54.0 kB → 54.3 kB cap). Rebased again for `assertAdoptBarrelFresh`
130
- // + `tryListFlowsUnits` in `boot.ts` (`.adopt()` barrel staleness
131
- // check, OKE1009) — part of the already-lazily-loaded boot graph, not
132
- // the `oke()` construction path, but `Bun.build` still traces and
133
- // sums the dynamic-import chunk (~54.3 kB → 54.7 kB → 55.0 kB after
134
- // ConfigEnv local/docker → dev/test/prod + sqlite removal).
135
- expect(total).toBeLessThan(55_000);
119
+ // Rebased after lazy `gate.auth` / auth-config chunks + `gate.public`
120
+ // decoupling from kv-lua strategies (~38.9 kB gzip with export externals).
121
+ expect(total).toBeLessThan(40_000);
136
122
  } finally {
137
123
  await rm(dir, { recursive: true, force: true });
138
124
  }
@@ -1,17 +1,24 @@
1
1
  /**
2
2
  * Module-evaluation registries for `store.sql` / `store.files`, `vault.secret`,
3
- * `vault.env.required`, `signal()`, and `channel.<medium>().template()` — the
4
- * plain arrays behind each declare module's `listX()` / `resetX()` pair
5
- * (mirrors `on.ts`'s trigger-drain `bindings` array).
3
+ * `vault.env.required`, `signal()`, `channel.<medium>().template()`, and
4
+ * `ai.model` / `.prompt` / `ai.embed` / `ai.agent` — the plain arrays behind
5
+ * each declare module's `listX()` / `resetX()` pair (mirrors `on.ts`'s
6
+ * trigger-drain `bindings` array).
6
7
  *
7
8
  * Lives here, not in the declare modules themselves, so {@link oke} can read
8
9
  * every registry with one lightweight import instead of statically pulling
9
- * in all four element `declare.ts` modules (and every unrelated export they
10
+ * in all element `declare.ts` modules (and every unrelated export they
10
11
  * carry) into every app's bundle — see the `oke()` Store-only bundle-size
11
12
  * budget in `boot.test.ts`. Only type imports cross back to the declare
12
13
  * modules, so this file has zero runtime dependencies of its own.
13
14
  */
14
15
 
16
+ import type {
17
+ AiAgentDecl,
18
+ AiEmbedDecl,
19
+ AiModelDecl,
20
+ AiPromptDecl,
21
+ } from "../elements/ai/declare.ts";
15
22
  import type { StoreDecl } from "../elements/store/declare.ts";
16
23
  import type { VaultSecretDecl } from "../elements/vault/declare.ts";
17
24
  import type { SignalDecl } from "../elements/signal/declare.ts";
@@ -27,3 +34,11 @@ export const requiredEnvRegistry: string[] = [];
27
34
  export const signalRegistry: SignalDecl[] = [];
28
35
  /** Medium-binder `.template()` declarations since the last reset. */
29
36
  export const channelTemplateRegistry: ChannelTemplateDecl[] = [];
37
+ /** `ai.model` declarations since the last reset. */
38
+ export const aiModelRegistry: AiModelDecl[] = [];
39
+ /** `model.prompt` declarations since the last reset. */
40
+ export const aiPromptRegistry: AiPromptDecl[] = [];
41
+ /** `ai.embed` declarations since the last reset. */
42
+ export const aiEmbedRegistry: AiEmbedDecl[] = [];
43
+ /** `ai.agent` declarations since the last reset. */
44
+ export const aiAgentRegistry: AiAgentDecl[] = [];
@@ -144,19 +144,19 @@ export const OKE_ERRORS = {
144
144
  },
145
145
  /**
146
146
  * Flow has no declared `effects` and no Manifest-derived effects were
147
- * available to stamp at boot (docker / prod — never a silent open token).
147
+ * available to stamp at boot (dev+compose / prod — never a silent open token).
148
148
  */
149
149
  NO_EFFECTS_DECLARED: {
150
150
  code: 1008,
151
151
  cause: 'Flow "{flow}" has no declared effects and no Manifest to derive them from.',
152
152
  fix:
153
153
  "Add explicit `effects` to this flow, or boot with a Manifest (`oke build`) / " +
154
- "`rootDir` so effects can be derived. docker/prod refuse an open capability token.",
154
+ "`rootDir` so effects can be derived. dev+compose/prod refuse an open capability token.",
155
155
  },
156
156
  /**
157
157
  * A `src/flows/<unit>` folder exists on disk but no adopted flow carries
158
158
  * that unit — the generated `.adopt()` barrel (`src/flows/generated.ts`)
159
- * is stale or was hand-edited. docker/prod — never a silently-incomplete
159
+ * is stale or was hand-edited. dev+compose / prod — never a silently-incomplete
160
160
  * route table in a deploy-shaped environment.
161
161
  */
162
162
  ADOPT_BARREL_STALE: {
@@ -93,6 +93,31 @@ describe("fx — effect ledger", () => {
93
93
  },
94
94
  ledger,
95
95
  secrets: { STRIPE_KEY: "sk_test" },
96
+ aiRuntime: {
97
+ prompts: new Map(),
98
+ agents: new Map(),
99
+ embeds: new Map(),
100
+ autoCacheDisabled: true,
101
+ journalingForced: false,
102
+ denials: [],
103
+ agentRuns: [],
104
+ journal: [],
105
+ async ask() {
106
+ return { ok: true };
107
+ },
108
+ async runAgent() {
109
+ return { ok: true, steps: 0, denials: [], output: {} };
110
+ },
111
+ async *stream() {
112
+ /* no tokens */
113
+ },
114
+ async search() {
115
+ return [];
116
+ },
117
+ async embed() {
118
+ return { vectors: [] };
119
+ },
120
+ } as never,
96
121
  });
97
122
 
98
123
  await stub(fx, "sql:bookings").get("x");
package/src/kernel/fx.ts CHANGED
@@ -297,6 +297,8 @@ export interface FxDeliverOtpOptions {
297
297
  /** Options for {@link Fx.ask}. */
298
298
  export interface FxAskOptions {
299
299
  readonly via?: readonly NamedRef[];
300
+ /** Per-call deadline — overrides prompt `timeout` (`"30s"` or ms). */
301
+ readonly timeout?: string | number;
300
302
  /** Flow refs offered as tools — each model call goes through `fx.call`. */
301
303
  readonly tools?: readonly NamedRef[];
302
304
  /** Bound on tool invocations (default 6). */
@@ -1587,13 +1589,14 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1587
1589
  if (options.aiRuntime) {
1588
1590
  return options.aiRuntime.ask(name, input, {
1589
1591
  via: opts?.via?.map(resolveName),
1592
+ ...(opts?.timeout !== undefined ? { timeout: opts.timeout } : {}),
1590
1593
  tools: opts?.tools?.map(resolveName),
1591
1594
  maxSteps: opts?.maxSteps,
1592
1595
  // Host fx.call — same capability / ledger / Runs path as any call.
1593
1596
  callTool: (tool, toolInput) => fx.call(tool, toolInput),
1594
1597
  });
1595
1598
  }
1596
- return {};
1599
+ throw new Error(`fx.ask: AI runtime is not configured for prompt "${name}"`);
1597
1600
  });
1598
1601
  },
1599
1602
  search(embed, query, opts) {
@@ -300,6 +300,10 @@ export interface AiPrompt {
300
300
  version?: number;
301
301
  evals?: string;
302
302
  budget?: AiBudget;
303
+ /** Ordered recovery chain of logical model names. */
304
+ via?: string[];
305
+ /** Per-command deadline (`"30s"` or milliseconds). */
306
+ timeout?: string | number;
303
307
  model?: string;
304
308
  in?: JsonSchema;
305
309
  out?: JsonSchema;
@@ -22,6 +22,9 @@ const ENTRIES: readonly { readonly src: string; readonly out: string }[] = [
22
22
  { src: "src/i18n-entry.ts", out: "dist/i18n-entry.js" },
23
23
  { src: "src/compiler-entry.ts", out: "dist/compiler-entry.js" },
24
24
  { src: "src/journal-entry.ts", out: "dist/journal-entry.js" },
25
+ // Lazy sync chunks — loaded via requirePackageModule when gate.auth is set.
26
+ { src: "src/kernel/app-auth.ts", out: "dist/app-auth.js" },
27
+ { src: "src/auth/config.ts", out: "dist/auth-config.js" },
25
28
  ];
26
29
 
27
30
  /**