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.
- package/package.json +1 -1
- package/site/content/docs/elements/ai.mdx +22 -1
- package/site/content/docs/elements/store.mdx +3 -1
- package/site/content/docs/elements/vault.mdx +19 -11
- package/site/content/docs/get-started/installation.mdx +7 -1
- package/site/content/docs/recipes/llama-cpp.mdx +10 -9
- package/site/content/docs/reference/cli.md +6 -2
- package/site/content/docs/reference/environment-variables.mdx +9 -9
- package/src/cli/ai-setup/ai-setup.test.ts +3 -1
- package/src/cli/ai-setup/apply.ts +61 -1
- package/src/cli/ask-seed.test.ts +4 -3
- package/src/cli/ask-seed.ts +5 -6
- package/src/cli/client-add.test.ts +2 -1
- package/src/cli/dev.test.ts +116 -0
- package/src/cli/dev.ts +107 -18
- package/src/cli/project-state.test.ts +50 -0
- package/src/cli/project-state.ts +123 -0
- package/src/cli/vault-cmd.test.ts +47 -18
- package/src/cli/vault-cmd.ts +2 -1
- package/src/compiler/extract.ts +12 -1
- package/src/console/server/console.test.ts +3 -1
- package/src/console/server/operator-db.test.ts +48 -17
- package/src/console/server/operator-db.ts +5 -1
- package/src/docker/derive.ts +24 -3
- package/src/docker/docker.test.ts +4 -2
- package/src/docker/index.ts +1 -0
- package/src/docker/recipes/index.ts +1 -0
- package/src/docker/recipes/llama-cpp.ts +20 -4
- package/src/drivers/ai-openai-compatible.ts +15 -3
- package/src/drivers/vault-builtin.test.ts +50 -42
- package/src/elements/ai/declare.ts +73 -3
- package/src/elements/ai/errors.test.ts +35 -0
- package/src/elements/ai/errors.ts +139 -0
- package/src/elements/ai/eval.ts +26 -1
- package/src/elements/ai/runtime.ts +140 -80
- package/src/elements/ai/tools.test.ts +1 -1
- package/src/elements/ai.test.ts +99 -2
- package/src/elements/ai.ts +11 -1
- package/src/elements/gate/config.ts +13 -3
- package/src/elements/gate/declare.ts +1 -1
- package/src/elements/gate/strategies.ts +2 -12
- package/src/elements/index.ts +2 -0
- package/src/elements/store/index-boot.test.ts +23 -6
- package/src/elements/store/resource.test.ts +38 -19
- package/src/elements/store/sql-session.test.ts +55 -58
- package/src/elements/vault/builtin-adapter.test.ts +115 -58
- package/src/elements/vault/builtin-adapter.ts +241 -47
- package/src/elements/vault/chaos-child.ts +424 -0
- package/src/elements/vault/chaos.test.ts +651 -0
- package/src/elements/vault/resilience.ts +6 -1
- package/src/elements/vault/security-checklist.test.ts +10 -8
- package/src/elements/vault/storage.ts +130 -27
- package/src/elements/vault/test-helpers.ts +368 -0
- package/src/elements/vault.ts +6 -0
- package/src/index.ts +2 -0
- package/src/kernel/app-auth.ts +98 -0
- package/src/kernel/app.ts +120 -78
- package/src/kernel/auto-registry.test.ts +52 -1
- package/src/kernel/boot.test.ts +4 -18
- package/src/kernel/element-registries.ts +19 -4
- package/src/kernel/errors.ts +3 -3
- package/src/kernel/fx.test.ts +25 -0
- package/src/kernel/fx.ts +4 -1
- package/src/manifest/types.ts +4 -0
- package/src/release/build-lib.ts +3 -0
- package/src/shared/lazy-src.ts +79 -0
- package/src/test/create-test-app.ts +16 -11
- 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
|
-
|
|
11
|
-
|
|
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 {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
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
|
-
//
|
|
484
|
-
// mirror `on`'s trigger drain
|
|
485
|
-
// Explicit `options.stores` /
|
|
486
|
-
//
|
|
487
|
-
//
|
|
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
|
-
? {
|
|
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 =
|
|
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
|
-
|
|
619
|
+
let authMaterialization: AuthHttpMaterialization | undefined;
|
|
620
|
+
let wiredAuth: WiredGateAuth | undefined;
|
|
621
|
+
let authBinding: AppAuthBinding | undefined;
|
|
559
622
|
if (gateConfig.auth) {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
|
|
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 ??
|
|
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 ??
|
|
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
|
-
|
|
983
|
-
|
|
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:
|
|
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
|
|
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
|
});
|
package/src/kernel/boot.test.ts
CHANGED
|
@@ -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
|
|
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
|
|
120
|
-
//
|
|
121
|
-
|
|
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()`,
|
|
4
|
-
*
|
|
5
|
-
* (mirrors `on.ts`'s
|
|
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
|
|
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[] = [];
|
package/src/kernel/errors.ts
CHANGED
|
@@ -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 (
|
|
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.
|
|
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.
|
|
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: {
|
package/src/kernel/fx.test.ts
CHANGED
|
@@ -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
|
-
|
|
1599
|
+
throw new Error(`fx.ask: AI runtime is not configured for prompt "${name}"`);
|
|
1597
1600
|
});
|
|
1598
1601
|
},
|
|
1599
1602
|
search(embed, query, opts) {
|
package/src/manifest/types.ts
CHANGED
|
@@ -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;
|
package/src/release/build-lib.ts
CHANGED
|
@@ -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
|
/**
|