okengine 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/manifest.v1.schema.json +9 -1
- package/package.json +3 -3
- package/site/content/docs/console/runs.mdx +10 -0
- package/site/content/docs/console/traces.mdx +4 -0
- package/site/content/docs/elements/ai.mdx +54 -32
- package/site/content/docs/elements/channel.mdx +105 -14
- package/site/content/docs/elements/clock.mdx +1 -0
- package/site/content/docs/elements/flow.mdx +42 -8
- package/site/content/docs/get-started/basic-usage.mdx +5 -1
- package/site/content/docs/get-started/installation.mdx +9 -5
- package/site/content/docs/meta.json +2 -0
- package/site/content/docs/plugins/otp.mdx +58 -46
- package/site/content/docs/providers/azure-redis.mdx +85 -0
- package/site/content/docs/providers/cockroachdb.mdx +86 -0
- package/site/content/docs/providers/digitalocean-caching.mdx +88 -0
- package/site/content/docs/providers/dragonfly-cloud.mdx +82 -0
- package/site/content/docs/providers/elasticache.mdx +92 -0
- package/site/content/docs/providers/index.mdx +101 -0
- package/site/content/docs/providers/memorystore.mdx +89 -0
- package/site/content/docs/providers/meta.json +20 -0
- package/site/content/docs/providers/neon.mdx +115 -0
- package/site/content/docs/providers/redis-cloud.mdx +93 -0
- package/site/content/docs/providers/supabase.mdx +117 -0
- package/site/content/docs/providers/upstash.mdx +91 -0
- package/site/content/docs/providers/yugabytedb.mdx +95 -0
- package/site/content/docs/recipes/caddy.mdx +128 -0
- package/site/content/docs/recipes/cockroachdb.mdx +127 -0
- package/site/content/docs/recipes/dragonfly.mdx +125 -0
- package/site/content/docs/recipes/index.mdx +146 -0
- package/site/content/docs/recipes/llama-cpp.mdx +153 -0
- package/site/content/docs/recipes/mailpit.mdx +136 -0
- package/site/content/docs/recipes/meilisearch.mdx +139 -0
- package/site/content/docs/recipes/meta.json +31 -0
- package/site/content/docs/recipes/ollama.mdx +142 -0
- package/site/content/docs/recipes/openbao.mdx +141 -0
- package/site/content/docs/recipes/pgdog.mdx +137 -0
- package/site/content/docs/recipes/postgres.mdx +136 -0
- package/site/content/docs/recipes/redis.mdx +130 -0
- package/site/content/docs/recipes/rustfs.mdx +148 -0
- package/site/content/docs/recipes/sglang.mdx +105 -0
- package/site/content/docs/recipes/supabase-docker.mdx +145 -0
- package/site/content/docs/recipes/timescale.mdx +129 -0
- package/site/content/docs/recipes/traefik.mdx +136 -0
- package/site/content/docs/recipes/valkey.mdx +121 -0
- package/site/content/docs/recipes/vllm.mdx +106 -0
- package/site/content/docs/recipes/yugabytedb.mdx +135 -0
- package/site/content/docs/reference/cli.md +42 -6
- package/site/content/docs/reference/configuration.mdx +5 -3
- package/site/content/docs/reference/environment-variables.mdx +12 -12
- package/site/content/docs/reference/fx.mdx +38 -4
- package/src/auth/otp-capability.ts +14 -14
- package/src/cli/ai-setup/ai-setup.test.ts +32 -12
- package/src/cli/ai-setup/apply.ts +65 -23
- package/src/cli/ai-setup/catalog.ts +1316 -35
- package/src/cli/ai-setup/detect-ollama.ts +47 -0
- package/src/cli/ai-setup/index.ts +44 -7
- package/src/cli/ai-setup/prompts.ts +430 -486
- package/src/cli/ai-setup/recommend.test.ts +150 -50
- package/src/cli/ai-setup/recommend.ts +118 -101
- package/src/cli/ask-seed.test.ts +68 -0
- package/src/cli/ask-seed.ts +79 -0
- package/src/cli/db-seed-redact.test.ts +25 -0
- package/src/cli/db-seed.ts +36 -7
- package/src/cli/db.test.ts +1 -1
- package/src/cli/db.ts +2 -2
- package/src/cli/dev-controls.test.ts +107 -0
- package/src/cli/dev-controls.ts +354 -0
- package/src/cli/dev-schema-sync.ts +12 -4
- package/src/cli/dev.test.ts +50 -30
- package/src/cli/dev.ts +607 -66
- package/src/cli/drizzle-env.test.ts +40 -6
- package/src/cli/drizzle-env.ts +22 -1
- package/src/cli/ensure-drizzle-config.ts +6 -4
- package/src/cli/hero-meta.test.ts +13 -1
- package/src/cli/hero-meta.ts +28 -6
- package/src/cli/index.ts +5 -0
- package/src/cli/registry.ts +24 -0
- package/src/cli/replay.test.ts +105 -0
- package/src/cli/replay.ts +206 -0
- package/src/compiler/extract.ts +3 -3
- package/src/config/index.ts +3 -3
- package/src/console/server/claim.ts +3 -1
- package/src/console/ui/dist/assets/{index-ClX0ZCe5.js → index-D-9KFce5.js} +2 -2
- package/src/console/ui/dist/assets/panel-overview-CW0CQEc2.js +1 -0
- package/src/console/ui/dist/assets/panel-runs-BxuyDI3x.js +1 -0
- package/src/console/ui/dist/assets/{panel-signals-Dsxdu_AR.js → panel-signals-BXofbj7v.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-hy7O8HOs.js → panel-store-B8mH-arU.js} +1 -1
- package/src/console/ui/dist/assets/{panel-traces-CBiAe5go.js → panel-traces-DSA-ZWnf.js} +1 -1
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/overview/slo.ts +31 -3
- package/src/console/ui/overview/types.ts +6 -0
- package/src/console/ui/runs/errors.test.ts +53 -0
- package/src/console/ui/runs/errors.ts +88 -0
- package/src/console/ui/runs/index.ts +8 -0
- package/src/console/ui/runs/search.ts +16 -0
- package/src/console/ui/shell/panels/runs/RunsPanel.tsx +72 -1
- package/src/docker/ai-model-status.test.ts +101 -0
- package/src/docker/ai-model-status.ts +276 -0
- package/src/docker/compose-health.test.ts +115 -0
- package/src/docker/compose-health.ts +234 -0
- package/src/docker/compose.ts +24 -8
- package/src/docker/derive.ts +35 -9
- package/src/docker/docker.test.ts +304 -12
- package/src/docker/helpers.ts +56 -0
- package/src/docker/index.ts +52 -0
- package/src/docker/ollama-pull.ts +232 -0
- package/src/docker/recipes/cockroach.ts +24 -0
- package/src/docker/recipes/index.ts +32 -0
- package/src/docker/recipes/llama-cpp.ts +273 -0
- package/src/docker/recipes/ollama.ts +19 -23
- package/src/docker/recipes/pgdog.ts +4 -1
- package/src/docker/recipes/postgres.ts +5 -18
- package/src/docker/recipes/sglang.ts +55 -0
- package/src/docker/recipes/supabase.ts +21 -0
- package/src/docker/recipes/timescale.ts +18 -0
- package/src/docker/recipes/vllm.ts +44 -0
- package/src/docker/recipes/yugabyte.ts +23 -0
- package/src/docker/stack-id.test.ts +22 -0
- package/src/docker/stack-id.ts +38 -4
- package/src/docker/types.ts +15 -0
- package/src/drivers/ai-ollama.integration.test.ts +7 -4
- package/src/drivers/signal-engine.ts +10 -0
- package/src/drivers/signal-postgres.ts +24 -2
- package/src/drivers/signal-types.ts +10 -0
- package/src/elements/channel/fallback.test.ts +91 -0
- package/src/elements/channel/hard-bounce.test.ts +82 -0
- package/src/elements/channel/injection.test.ts +153 -0
- package/src/elements/channel/locale-ar.test.ts +102 -0
- package/src/elements/channel/otp-delivery.ts +5 -2
- package/src/elements/channel/receipts-ledger.test.ts +100 -0
- package/src/elements/channel/runtime.ts +24 -1
- package/src/elements/channel/test-helpers.ts +60 -0
- package/src/elements/channel/whatsapp-compliance.test.ts +124 -0
- package/src/elements/store/cache.ts +1 -1
- package/src/elements/store/emit-drizzle.ts +43 -9
- package/src/elements/store/runtime.ts +1 -1
- package/src/elements/store/schema-decl.test.ts +2 -0
- package/src/elements/store/schema-decl.ts +15 -0
- package/src/elements/store/seed.ts +1 -1
- package/src/index.ts +1 -0
- package/src/kernel/app.ts +118 -11
- package/src/kernel/boot-bind/signal.ts +6 -2
- package/src/kernel/boot-bind/store.ts +2 -1
- package/src/kernel/boot.test.ts +6 -5
- package/src/kernel/boot.ts +8 -3
- package/src/kernel/compensate.test.ts +131 -0
- package/src/kernel/correlation.test.ts +108 -0
- package/src/kernel/effect-timing.test.ts +82 -0
- package/src/kernel/flow.ts +38 -0
- package/src/kernel/fx-runs.test.ts +49 -0
- package/src/kernel/fx.test.ts +14 -0
- package/src/kernel/fx.ts +111 -1
- package/src/manifest/types.ts +8 -2
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/auth-methods.security.test.ts +3 -3
- package/src/plugins/auth-methods.test.ts +1 -1
- package/src/plugins/index.ts +2 -2
- package/src/plugins/otp.test.ts +21 -19
- package/src/plugins/otp.ts +36 -34
- package/src/plugins/taqnyat.live.test.ts +3 -3
- package/src/runs/collect.ts +3 -0
- package/src/runs/export-otlp.ts +64 -0
- package/src/runs/index.ts +16 -0
- package/src/runs/parquet.ts +9 -0
- package/src/runs/types.ts +5 -0
- package/src/runs/window.test.ts +68 -0
- package/src/runs/window.ts +148 -0
- package/src/runtime/boot-warn.test.ts +45 -0
- package/src/runtime/boot-warn.ts +17 -0
- package/src/runtime/dev-request-log.ts +5 -5
- package/src/term.test.ts +95 -10
- package/src/term.ts +369 -14
- package/src/console/ui/dist/assets/panel-overview-DONDzBd2.js +0 -1
- package/src/console/ui/dist/assets/panel-runs-D5zf-D9c.js +0 -1
|
@@ -1,28 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Postgres / pgvector
|
|
2
|
+
* Postgres / pgvector image recipe (≤15 lines of recipe body).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import type { ImageRecipe
|
|
5
|
+
import { postgresEnv, postgresHealth } from "../helpers.ts";
|
|
6
|
+
import type { ImageRecipe } from "../types.ts";
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
POSTGRES_USER: credEnv(s, "USER"),
|
|
10
|
-
POSTGRES_PASSWORD: credEnv(s, "PASSWORD"),
|
|
11
|
-
POSTGRES_DB: credEnv(s, "DB"),
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const postgresHealth = {
|
|
15
|
-
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"],
|
|
16
|
-
interval: "5s",
|
|
17
|
-
timeout: "3s",
|
|
18
|
-
retries: 10,
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
/** Postgres-protocol servers (Postgres · Neon · Supabase · pgvector · Timescale). */
|
|
8
|
+
/** Postgres-protocol servers (Postgres · pgvector). Timescale / Supabase match first. */
|
|
22
9
|
export const postgres: ImageRecipe = {
|
|
23
10
|
id: "postgres",
|
|
24
11
|
port: 5432,
|
|
25
|
-
match: (i) => /postgres|pgvector
|
|
12
|
+
match: (i) => /postgres|pgvector/i.test(i),
|
|
26
13
|
apply: (s) => ({ environment: postgresEnv(s), healthcheck: postgresHealth }),
|
|
27
14
|
url: (_s, c) =>
|
|
28
15
|
`postgres://${c.user}:${encodeURIComponent(c.password)}@${c.host}:${c.port}/${c.database}`,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SGLang image recipe — production-tier structured/agent inference.
|
|
3
|
+
*
|
|
4
|
+
* Prefer a managed provider unless you run inference yourself. Use SGLang when
|
|
5
|
+
* the workload is structured generation / agent-heavy on real GPU hardware;
|
|
6
|
+
* use vLLM for general multi-user serving. Pin an explicit tag — never
|
|
7
|
+
* `latest`. Host publish is loopback-only.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ImageRecipe } from "../types.ts";
|
|
11
|
+
|
|
12
|
+
/** Pinned SGLang runtime image — never `latest`. */
|
|
13
|
+
export const SGLANG_IMAGE = "lmsysorg/sglang:v0.5.16-runtime";
|
|
14
|
+
|
|
15
|
+
/** SGLang — OpenAI-compatible HTTP on 30000; GPU reservation; loopback publish. */
|
|
16
|
+
export const sglang: ImageRecipe = {
|
|
17
|
+
id: "sglang",
|
|
18
|
+
port: 30000,
|
|
19
|
+
match: (i) => /sglang/i.test(i),
|
|
20
|
+
apply: (s) => ({
|
|
21
|
+
command: [
|
|
22
|
+
"python3",
|
|
23
|
+
"-m",
|
|
24
|
+
"sglang.launch_server",
|
|
25
|
+
"--model-path",
|
|
26
|
+
"${OKE_AI_MODEL:-Qwen/Qwen3-0.6B}",
|
|
27
|
+
"--host",
|
|
28
|
+
"0.0.0.0",
|
|
29
|
+
"--port",
|
|
30
|
+
"30000",
|
|
31
|
+
],
|
|
32
|
+
environment: {
|
|
33
|
+
OKE_AI_MODEL: "${OKE_AI_MODEL:-Qwen/Qwen3-0.6B}",
|
|
34
|
+
HF_HOME: "/root/.cache/huggingface",
|
|
35
|
+
},
|
|
36
|
+
volumes: [`${s.serviceName}-models:/root/.cache/huggingface`],
|
|
37
|
+
ipc: "host",
|
|
38
|
+
publishBind: "127.0.0.1",
|
|
39
|
+
deploy: {
|
|
40
|
+
resources: {
|
|
41
|
+
reservations: {
|
|
42
|
+
devices: [{ driver: "nvidia", count: "all", capabilities: ["gpu"] }],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
healthcheck: {
|
|
47
|
+
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:30000/health >/dev/null || exit 1"],
|
|
48
|
+
interval: "10s",
|
|
49
|
+
timeout: "5s",
|
|
50
|
+
retries: 30,
|
|
51
|
+
start_period: "120s",
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
url: (_s, c) => `http://${c.host}:${c.port}/v1`,
|
|
55
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supabase image recipe — self-hosted `supabase/postgres` (Postgres + bundled extensions).
|
|
3
|
+
*
|
|
4
|
+
* Postgres-protocol only: the Auth / Storage / Realtime / Kong / Studio layer of the
|
|
5
|
+
* full Supabase stack is not run here — oke already covers those with Vault, Store ·
|
|
6
|
+
* files, and Signal. This recipe just gets you Postgres plus Supabase's extension
|
|
7
|
+
* bundle (pgvector, pg_graphql, pg_cron, wrappers, …) without the rest of the platform.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { postgresEnv, postgresHealth } from "../helpers.ts";
|
|
11
|
+
import type { ImageRecipe } from "../types.ts";
|
|
12
|
+
|
|
13
|
+
/** Supabase Postgres image — checked before the generic `postgres` recipe. */
|
|
14
|
+
export const supabase: ImageRecipe = {
|
|
15
|
+
id: "supabase",
|
|
16
|
+
port: 5432,
|
|
17
|
+
match: (i) => /supabase\/postgres/i.test(i),
|
|
18
|
+
apply: (s) => ({ environment: postgresEnv(s), healthcheck: postgresHealth }),
|
|
19
|
+
url: (_s, c) =>
|
|
20
|
+
`postgres://${c.user}:${encodeURIComponent(c.password)}@${c.host}:${c.port}/${c.database}`,
|
|
21
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TimescaleDB image recipe — Postgres + hypertables (same POSTGRES_* contract).
|
|
3
|
+
*
|
|
4
|
+
* Driver id stays `postgres`. Matched ahead of the generic Postgres recipe.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { postgresEnv, postgresHealth } from "../helpers.ts";
|
|
8
|
+
import type { ImageRecipe } from "../types.ts";
|
|
9
|
+
|
|
10
|
+
/** TimescaleDB — Postgres protocol on 5432. */
|
|
11
|
+
export const timescale: ImageRecipe = {
|
|
12
|
+
id: "timescale",
|
|
13
|
+
port: 5432,
|
|
14
|
+
match: (i) => /timescale/i.test(i),
|
|
15
|
+
apply: (s) => ({ environment: postgresEnv(s), healthcheck: postgresHealth }),
|
|
16
|
+
url: (_s, c) =>
|
|
17
|
+
`postgres://${c.user}:${encodeURIComponent(c.password)}@${c.host}:${c.port}/${c.database}`,
|
|
18
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vLLM image recipe — production-tier local inference (`vllm/vllm-openai`).
|
|
3
|
+
*
|
|
4
|
+
* OpenAI-compatible server for multi-user GPU concurrency. Prefer a managed
|
|
5
|
+
* provider unless you run inference yourself. Pin an explicit tag — never
|
|
6
|
+
* `latest`. Host publish is loopback-only (same hard rule as llama.cpp).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ImageRecipe } from "../types.ts";
|
|
10
|
+
|
|
11
|
+
/** Pinned vLLM OpenAI server image — never `latest`. */
|
|
12
|
+
export const VLLM_IMAGE = "vllm/vllm-openai:v0.26.0";
|
|
13
|
+
|
|
14
|
+
/** vLLM — OpenAI-compatible HTTP on 8000; GPU reservation; loopback publish. */
|
|
15
|
+
export const vllm: ImageRecipe = {
|
|
16
|
+
id: "vllm",
|
|
17
|
+
port: 8000,
|
|
18
|
+
match: (i) => /(?:^|\/)vllm(?:[:@/]|$)/i.test(i),
|
|
19
|
+
apply: (s) => ({
|
|
20
|
+
command: ["--model", "${OKE_AI_MODEL:-Qwen/Qwen3-0.6B}", "--host", "0.0.0.0", "--port", "8000"],
|
|
21
|
+
environment: {
|
|
22
|
+
OKE_AI_MODEL: "${OKE_AI_MODEL:-Qwen/Qwen3-0.6B}",
|
|
23
|
+
HF_HOME: "/root/.cache/huggingface",
|
|
24
|
+
},
|
|
25
|
+
volumes: [`${s.serviceName}-models:/root/.cache/huggingface`],
|
|
26
|
+
ipc: "host",
|
|
27
|
+
publishBind: "127.0.0.1",
|
|
28
|
+
deploy: {
|
|
29
|
+
resources: {
|
|
30
|
+
reservations: {
|
|
31
|
+
devices: [{ driver: "nvidia", count: "all", capabilities: ["gpu"] }],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
healthcheck: {
|
|
36
|
+
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8000/health >/dev/null || exit 1"],
|
|
37
|
+
interval: "10s",
|
|
38
|
+
timeout: "5s",
|
|
39
|
+
retries: 30,
|
|
40
|
+
start_period: "120s",
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
url: (_s, c) => `http://${c.host}:${c.port}/v1`,
|
|
44
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YugabyteDB image recipe — self-hosted single-node YSQL (Postgres-wire).
|
|
3
|
+
*
|
|
4
|
+
* Driver id stays `postgres`. Official image: `yugabytedb/yugabyte`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { yugabyteEnv, yugabyteHealth } from "../helpers.ts";
|
|
8
|
+
import type { ImageRecipe } from "../types.ts";
|
|
9
|
+
|
|
10
|
+
/** YugabyteDB — YSQL on 5433. */
|
|
11
|
+
export const yugabyte: ImageRecipe = {
|
|
12
|
+
id: "yugabyte",
|
|
13
|
+
port: 5433,
|
|
14
|
+
match: (i) => /yugabyte/i.test(i),
|
|
15
|
+
apply: (s) => ({
|
|
16
|
+
environment: yugabyteEnv(s),
|
|
17
|
+
command: ["bin/yugabyted", "start", "--base_dir=/home/yugabyte/yb_data", "--background=false"],
|
|
18
|
+
healthcheck: yugabyteHealth,
|
|
19
|
+
volumes: [`${s.serviceName}-data:/home/yugabyte/yb_data`],
|
|
20
|
+
}),
|
|
21
|
+
url: (_s, c) =>
|
|
22
|
+
`postgres://${c.user}:${encodeURIComponent(c.password)}@${c.host}:${c.port}/${c.database}`,
|
|
23
|
+
};
|
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
6
9
|
import {
|
|
7
10
|
extraHostPortForInstance,
|
|
8
11
|
hostPortForInstance,
|
|
9
12
|
instancePortOffset,
|
|
13
|
+
loadExistingStackControls,
|
|
10
14
|
parseStackControls,
|
|
11
15
|
parseStackCredentials,
|
|
12
16
|
stackAppSlug,
|
|
@@ -51,6 +55,24 @@ describe("hostPortForInstance", () => {
|
|
|
51
55
|
});
|
|
52
56
|
});
|
|
53
57
|
|
|
58
|
+
describe("loadExistingStackControls", () => {
|
|
59
|
+
test("seeds OKE_AI_MODEL from .env.local when .env.docker omits it", async () => {
|
|
60
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-stack-ai-"));
|
|
61
|
+
await writeFile(join(dir, ".env.local"), "OKE_AI_MODEL=gemma4:e4b-q4_K_M\n", "utf8");
|
|
62
|
+
const controls = await loadExistingStackControls(dir);
|
|
63
|
+
expect(controls?.OKE_AI_MODEL).toBe("gemma4:e4b-q4_K_M");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("prefers active OKE_AI_MODEL in docker/.env.docker over .env.local", async () => {
|
|
67
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-stack-ai-"));
|
|
68
|
+
await mkdir(join(dir, "docker"), { recursive: true });
|
|
69
|
+
await writeFile(join(dir, "docker", ".env.docker"), "OKE_AI_MODEL=smollm2\n", "utf8");
|
|
70
|
+
await writeFile(join(dir, ".env.local"), "OKE_AI_MODEL=gemma4:e4b-q4_K_M\n", "utf8");
|
|
71
|
+
const controls = await loadExistingStackControls(dir);
|
|
72
|
+
expect(controls?.OKE_AI_MODEL).toBe("smollm2");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
54
76
|
describe("parseStackCredentials", () => {
|
|
55
77
|
test("reads OKE_* credential keys", () => {
|
|
56
78
|
const text = `
|
package/src/docker/stack-id.ts
CHANGED
|
@@ -103,7 +103,9 @@ export function parseStackCredentials(
|
|
|
103
103
|
map.get(`${prefix}_USER`) ?? (role === "store.files" ? map.get("S3_ACCESS_KEY_ID") : "oke");
|
|
104
104
|
const password =
|
|
105
105
|
map.get(`${prefix}_PASSWORD`) ??
|
|
106
|
-
(role === "store.files" ? map.get("S3_SECRET_ACCESS_KEY") : undefined)
|
|
106
|
+
(role === "store.files" ? map.get("S3_SECRET_ACCESS_KEY") : undefined) ??
|
|
107
|
+
// Meilisearch master key is written as OKE_STORE_INDEX_KEY (not *_PASSWORD).
|
|
108
|
+
(role === "store.index" ? map.get("OKE_STORE_INDEX_KEY") : undefined);
|
|
107
109
|
const database = map.get(`${prefix}_DB`) ?? "oke";
|
|
108
110
|
if (user && password && database) {
|
|
109
111
|
out[role] = { user, password, database };
|
|
@@ -128,6 +130,9 @@ export const STACK_CONTROL_KEYS = [
|
|
|
128
130
|
"OKE_AI_MODEL",
|
|
129
131
|
"OKE_PROXY_HOST",
|
|
130
132
|
"OKE_PROXY_ACME_EMAIL",
|
|
133
|
+
// OpenBao app token — minted by bootstrap, must survive `writeDerivedFiles`.
|
|
134
|
+
"OKE_VAULT_TOKEN",
|
|
135
|
+
"OKE_VAULT_MOUNT",
|
|
131
136
|
] as const;
|
|
132
137
|
|
|
133
138
|
/**
|
|
@@ -170,19 +175,48 @@ export async function loadExistingStackCredentials(
|
|
|
170
175
|
/**
|
|
171
176
|
* Load optional controls from `docker/.env.docker` when present.
|
|
172
177
|
*
|
|
178
|
+
* Falls back to `.oke/openbao/app.token` for `OKE_VAULT_TOKEN` when the env
|
|
179
|
+
* file was regenerated without the bootstrap overlay (per-project token).
|
|
180
|
+
*
|
|
173
181
|
* @param cwd - Project root
|
|
174
182
|
*/
|
|
175
183
|
export async function loadExistingStackControls(
|
|
176
184
|
cwd: string,
|
|
177
185
|
): Promise<Readonly<Record<string, string>> | undefined> {
|
|
178
186
|
const candidates = [resolve(cwd, DEFAULT_DOCKER_DIR, ".env.docker"), resolve(cwd, ".env.docker")];
|
|
187
|
+
let controls: Record<string, string> = {};
|
|
179
188
|
for (const path of candidates) {
|
|
180
189
|
const file = Bun.file(path);
|
|
181
190
|
if (!(await file.exists())) continue;
|
|
182
|
-
|
|
183
|
-
|
|
191
|
+
controls = parseStackControls(await file.text());
|
|
192
|
+
break;
|
|
184
193
|
}
|
|
185
|
-
|
|
194
|
+
// create-oke / `oke ai setup` write the chosen model into `.env.local`.
|
|
195
|
+
// Seed it into stack controls on first docker boot so `LLAMA_ARG_DOCKER_REPO`
|
|
196
|
+
// is not stuck on the recipe default (`smollm2`).
|
|
197
|
+
if (!controls.OKE_AI_MODEL) {
|
|
198
|
+
const localEnv = Bun.file(resolve(cwd, ".env.local"));
|
|
199
|
+
if (await localEnv.exists()) {
|
|
200
|
+
const model = parseDotenv(await localEnv.text())
|
|
201
|
+
.get("OKE_AI_MODEL")
|
|
202
|
+
?.trim();
|
|
203
|
+
if (model) controls = { ...controls, OKE_AI_MODEL: model };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (!controls.OKE_VAULT_TOKEN) {
|
|
207
|
+
const tokenFile = Bun.file(resolve(cwd, ".oke", "openbao", "app.token"));
|
|
208
|
+
if (await tokenFile.exists()) {
|
|
209
|
+
const token = (await tokenFile.text()).trim();
|
|
210
|
+
if (token) {
|
|
211
|
+
controls = {
|
|
212
|
+
...controls,
|
|
213
|
+
OKE_VAULT_TOKEN: token,
|
|
214
|
+
OKE_VAULT_MOUNT: controls.OKE_VAULT_MOUNT ?? "secret",
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return Object.keys(controls).length > 0 ? controls : undefined;
|
|
186
220
|
}
|
|
187
221
|
|
|
188
222
|
function parseDotenv(text: string): Map<string, string> {
|
package/src/docker/types.ts
CHANGED
|
@@ -56,6 +56,12 @@ export interface RecipeExtraPort {
|
|
|
56
56
|
readonly container: number;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Host bind for published ports — AI inference recipes set `127.0.0.1` so the
|
|
61
|
+
* raw API never lands on `0.0.0.0` (LAN/WAN). Omit for Docker's default bind.
|
|
62
|
+
*/
|
|
63
|
+
export type RecipePublishBind = "127.0.0.1";
|
|
64
|
+
|
|
59
65
|
/** Compose `ulimits` value — soft/hard object or a single limit. */
|
|
60
66
|
export type RecipeUlimit = number | { readonly soft?: number; readonly hard?: number };
|
|
61
67
|
|
|
@@ -73,6 +79,15 @@ export interface RecipeApplyResult {
|
|
|
73
79
|
readonly labels?: Readonly<Record<string, string>>;
|
|
74
80
|
/** Additional published ports (e.g. Mailpit UI, RustFS console). */
|
|
75
81
|
readonly extraPorts?: readonly RecipeExtraPort[];
|
|
82
|
+
/**
|
|
83
|
+
* Host interface for published ports. AI recipes use `127.0.0.1` — never
|
|
84
|
+
* omit this for inference servers (raw API must not bind `0.0.0.0`).
|
|
85
|
+
*/
|
|
86
|
+
readonly publishBind?: RecipePublishBind;
|
|
87
|
+
/** Compose `ipc` (e.g. vLLM `host` for shared-memory workers). */
|
|
88
|
+
readonly ipc?: string;
|
|
89
|
+
/** Compose `deploy` (e.g. GPU device reservations for vLLM / SGLang). */
|
|
90
|
+
readonly deploy?: Readonly<Record<string, unknown>>;
|
|
76
91
|
/**
|
|
77
92
|
* Compose `depends_on` (e.g. pooler waits for Postgres).
|
|
78
93
|
* Keys are compose service names (`store-sql`), not role keys.
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Opt-in (never auto-pull a multi-GB model in ordinary `bun test`):
|
|
5
5
|
* 1. `OKE_TEST_OLLAMA_URL` — point at any reachable Ollama
|
|
6
|
-
* 2. `OKE_TEST_OLLAMA_DOCKER=1` — start `ollama/ollama` via the recipe
|
|
7
|
-
* `qwen3.5:9b` (slow first run)
|
|
6
|
+
* 2. `OKE_TEST_OLLAMA_DOCKER=1` — start `ollama/ollama` via the recipe; host-side
|
|
7
|
+
* `ensureOllamaModel` POSTs `/api/pull` for `qwen3.5:9b` (slow first run)
|
|
8
8
|
*
|
|
9
9
|
* Without one of those, the suite skips with a visible reason (never an empty pass).
|
|
10
10
|
* A local daemon on :11434 is not enough — set `OKE_TEST_OLLAMA_URL` explicitly.
|
|
@@ -123,8 +123,11 @@ async function resolveLiveUrl(): Promise<{ url: string; model: string }> {
|
|
|
123
123
|
await rm(dir, { recursive: true, force: true });
|
|
124
124
|
};
|
|
125
125
|
|
|
126
|
-
//
|
|
127
|
-
await waitForOllama(url,
|
|
126
|
+
// Serve must answer before we POST /api/pull (same path as `oke dev -d`).
|
|
127
|
+
await waitForOllama(url, 120_000, false);
|
|
128
|
+
const { ensureOllamaModel } = await import("../docker/ollama-pull.ts");
|
|
129
|
+
await ensureOllamaModel({ url, model: OLLAMA_DEFAULT_MODEL });
|
|
130
|
+
await waitForOllama(url, 60_000, true);
|
|
128
131
|
return { url, model: OLLAMA_DEFAULT_MODEL };
|
|
129
132
|
}
|
|
130
133
|
|
|
@@ -39,6 +39,8 @@ interface MutMessage {
|
|
|
39
39
|
payload: unknown;
|
|
40
40
|
/** Optional per-key ordering token (`once` serialization). */
|
|
41
41
|
key: string | null;
|
|
42
|
+
/** Producer run id for trace parent/child joins. */
|
|
43
|
+
parentRunId: string | null;
|
|
42
44
|
delivery: SignalDelivery;
|
|
43
45
|
attempts: number;
|
|
44
46
|
failures: SignalFailureReason[];
|
|
@@ -163,6 +165,7 @@ export async function createSignalEngine(
|
|
|
163
165
|
messages = snap.messages.map((m) => ({
|
|
164
166
|
...m,
|
|
165
167
|
key: m.key ?? null,
|
|
168
|
+
parentRunId: m.parentRunId ?? null,
|
|
166
169
|
lockedBy: m.lockedBy,
|
|
167
170
|
leaseExpiresAt: m.leaseExpiresAt ?? null,
|
|
168
171
|
deliveredTo: new Set(m.deliveredTo),
|
|
@@ -179,6 +182,7 @@ export async function createSignalEngine(
|
|
|
179
182
|
signal: m.signal,
|
|
180
183
|
payload: m.payload,
|
|
181
184
|
key: m.key,
|
|
185
|
+
parentRunId: m.parentRunId,
|
|
182
186
|
delivery: m.delivery,
|
|
183
187
|
attempts: m.attempts,
|
|
184
188
|
failures: m.failures,
|
|
@@ -230,11 +234,16 @@ export async function createSignalEngine(
|
|
|
230
234
|
const value = await validateSignalEmitPayload(name, decl, payload);
|
|
231
235
|
const t = now();
|
|
232
236
|
const key = typeof options?.key === "string" && options.key.length > 0 ? options.key : null;
|
|
237
|
+
const parentRunId =
|
|
238
|
+
typeof options?.parentRunId === "string" && options.parentRunId.length > 0
|
|
239
|
+
? options.parentRunId
|
|
240
|
+
: null;
|
|
233
241
|
stagedMessages.push({
|
|
234
242
|
id: crypto.randomUUID(),
|
|
235
243
|
signal: name,
|
|
236
244
|
payload: value,
|
|
237
245
|
key,
|
|
246
|
+
parentRunId,
|
|
238
247
|
delivery: decl.delivery,
|
|
239
248
|
attempts: 0,
|
|
240
249
|
failures: [],
|
|
@@ -332,6 +341,7 @@ export async function createSignalEngine(
|
|
|
332
341
|
signal: m.signal,
|
|
333
342
|
payload: m.payload,
|
|
334
343
|
...(m.key !== null ? { key: m.key } : {}),
|
|
344
|
+
...(m.parentRunId !== null ? { parentRunId: m.parentRunId } : {}),
|
|
335
345
|
delivery: m.delivery,
|
|
336
346
|
attempts: m.attempts,
|
|
337
347
|
failures: [...m.failures],
|
|
@@ -44,6 +44,7 @@ interface MsgRow {
|
|
|
44
44
|
locked_by: string | null;
|
|
45
45
|
lease_expires_at: number | null;
|
|
46
46
|
delivered_to: string;
|
|
47
|
+
parent_run_id: string | null;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
/** Minimal SQL + listen surface for the postgres signal driver. */
|
|
@@ -113,6 +114,7 @@ export function createPostgresSignalFake(options?: {
|
|
|
113
114
|
...m,
|
|
114
115
|
ordering_key: m.ordering_key ?? null,
|
|
115
116
|
lease_expires_at: m.lease_expires_at ?? null,
|
|
117
|
+
parent_run_id: m.parent_run_id ?? null,
|
|
116
118
|
})),
|
|
117
119
|
writes: new Map(snap.writes),
|
|
118
120
|
};
|
|
@@ -304,6 +306,10 @@ export function createPostgresSignalFake(options?: {
|
|
|
304
306
|
? null
|
|
305
307
|
: Number(row.lease_expires_at),
|
|
306
308
|
delivered_to: String(row.delivered_to ?? "[]"),
|
|
309
|
+
parent_run_id:
|
|
310
|
+
row.parent_run_id === undefined || row.parent_run_id === null
|
|
311
|
+
? null
|
|
312
|
+
: String(row.parent_run_id),
|
|
307
313
|
});
|
|
308
314
|
return { changes: 1 };
|
|
309
315
|
}
|
|
@@ -409,7 +415,8 @@ async function ensureSchema(sql: PostgresSignalSql): Promise<void> {
|
|
|
409
415
|
status TEXT,
|
|
410
416
|
locked_by TEXT,
|
|
411
417
|
lease_expires_at BIGINT,
|
|
412
|
-
delivered_to TEXT
|
|
418
|
+
delivered_to TEXT,
|
|
419
|
+
parent_run_id TEXT
|
|
413
420
|
)`);
|
|
414
421
|
// Existing tables created before leases / keys: add columns in place.
|
|
415
422
|
try {
|
|
@@ -424,6 +431,11 @@ async function ensureSchema(sql: PostgresSignalSql): Promise<void> {
|
|
|
424
431
|
} catch {
|
|
425
432
|
/* fake / older engines without IF NOT EXISTS — ignore */
|
|
426
433
|
}
|
|
434
|
+
try {
|
|
435
|
+
await sql.exec(`ALTER TABLE oke_signal_messages ADD COLUMN IF NOT EXISTS parent_run_id TEXT`);
|
|
436
|
+
} catch {
|
|
437
|
+
/* fake / older engines without IF NOT EXISTS — ignore */
|
|
438
|
+
}
|
|
427
439
|
await sql.exec(`CREATE TABLE IF NOT EXISTS oke_signal_writes (
|
|
428
440
|
key TEXT PRIMARY KEY,
|
|
429
441
|
value TEXT
|
|
@@ -435,11 +447,16 @@ function rowToMessage(row: Record<string, unknown>): SignalMessage {
|
|
|
435
447
|
row.ordering_key === undefined || row.ordering_key === null || row.ordering_key === ""
|
|
436
448
|
? undefined
|
|
437
449
|
: String(row.ordering_key);
|
|
450
|
+
const parentRunId =
|
|
451
|
+
row.parent_run_id === undefined || row.parent_run_id === null || row.parent_run_id === ""
|
|
452
|
+
? undefined
|
|
453
|
+
: String(row.parent_run_id);
|
|
438
454
|
return {
|
|
439
455
|
id: String(row.id),
|
|
440
456
|
signal: String(row.signal),
|
|
441
457
|
payload: JSON.parse(String(row.payload)),
|
|
442
458
|
...(key !== undefined ? { key } : {}),
|
|
459
|
+
...(parentRunId !== undefined ? { parentRunId } : {}),
|
|
443
460
|
delivery: row.delivery as SignalDelivery,
|
|
444
461
|
attempts: Number(row.attempts),
|
|
445
462
|
failures: JSON.parse(String(row.failures ?? "[]")) as SignalFailureReason[],
|
|
@@ -554,8 +571,12 @@ export async function openPostgresSignal(options: SignalOpenOptions): Promise<Si
|
|
|
554
571
|
const t = now();
|
|
555
572
|
const orderingKey =
|
|
556
573
|
typeof options?.key === "string" && options.key.length > 0 ? options.key : null;
|
|
574
|
+
const parentRunId =
|
|
575
|
+
typeof options?.parentRunId === "string" && options.parentRunId.length > 0
|
|
576
|
+
? options.parentRunId
|
|
577
|
+
: null;
|
|
557
578
|
await tx.exec(
|
|
558
|
-
`INSERT INTO oke_signal_messages (id, signal, payload, ordering_key, delivery, attempts, failures, created_at, available_at, status, locked_by, lease_expires_at, delivered_to) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
579
|
+
`INSERT INTO oke_signal_messages (id, signal, payload, ordering_key, delivery, attempts, failures, created_at, available_at, status, locked_by, lease_expires_at, delivered_to, parent_run_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
559
580
|
[
|
|
560
581
|
crypto.randomUUID(),
|
|
561
582
|
signal,
|
|
@@ -570,6 +591,7 @@ export async function openPostgresSignal(options: SignalOpenOptions): Promise<Si
|
|
|
570
591
|
null,
|
|
571
592
|
null,
|
|
572
593
|
"[]",
|
|
594
|
+
parentRunId,
|
|
573
595
|
],
|
|
574
596
|
);
|
|
575
597
|
}
|
|
@@ -40,6 +40,11 @@ export interface SignalEmitOptions {
|
|
|
40
40
|
* Omit for pure competing-consumer behavior (no ordering guarantee).
|
|
41
41
|
*/
|
|
42
42
|
readonly key?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Producer WideEvent / run id for Console trace chains.
|
|
45
|
+
* Set automatically by `fx.emit` when the emitter has a run id.
|
|
46
|
+
*/
|
|
47
|
+
readonly parentRunId?: string;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
/** A durable signal message. */
|
|
@@ -49,6 +54,11 @@ export interface SignalMessage {
|
|
|
49
54
|
readonly payload: unknown;
|
|
50
55
|
/** Optional ordering key from {@link SignalEmitOptions.key}. */
|
|
51
56
|
readonly key?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Producer run id when the emit originated from a Flow execution.
|
|
59
|
+
* Consuming Runs record this as {@link import("../runs/types.ts").WideEvent.parentId}.
|
|
60
|
+
*/
|
|
61
|
+
readonly parentRunId?: string;
|
|
52
62
|
readonly delivery: SignalDelivery;
|
|
53
63
|
readonly attempts: number;
|
|
54
64
|
readonly failures: readonly SignalFailureReason[];
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same-medium FallbackTransport adversarial proofs:
|
|
3
|
+
* - provider error advances the chain and delivers via secondary
|
|
4
|
+
* - permanent client errors (status 400 / invalid address) do NOT failover
|
|
5
|
+
* (aligned with shouldFallbackOtpMedium)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import { channel, createChannelRuntime } from "../channel.ts";
|
|
10
|
+
import { countingOkTransport, driverFromTransport, failingTransport } from "./test-helpers.ts";
|
|
11
|
+
|
|
12
|
+
describe("FallbackTransport same-medium failover", () => {
|
|
13
|
+
test("provider error advances to secondary and delivers", async () => {
|
|
14
|
+
const secondary = countingOkTransport("resend");
|
|
15
|
+
const runtime = createChannelRuntime({
|
|
16
|
+
templates: [channel.template("booking", { medium: "email" })],
|
|
17
|
+
drivers: [
|
|
18
|
+
driverFromTransport(
|
|
19
|
+
"smtp",
|
|
20
|
+
failingTransport(
|
|
21
|
+
"smtp",
|
|
22
|
+
Object.assign(new Error("provider timeout"), { statusCode: 500 }),
|
|
23
|
+
),
|
|
24
|
+
),
|
|
25
|
+
driverFromTransport("resend", secondary.transport),
|
|
26
|
+
],
|
|
27
|
+
catalog: { booking: { en: { subject: "Booked", text: "ok" } } },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const result = await runtime.send("booking", {
|
|
31
|
+
to: "user@example.com",
|
|
32
|
+
via: ["smtp", "resend"],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(result.ok).toBe(true);
|
|
36
|
+
expect(secondary.calls.count).toBe(1);
|
|
37
|
+
expect(result.attempts.some((a) => a.driverId === "smtp" && !a.ok)).toBe(true);
|
|
38
|
+
expect(result.attempts.some((a) => a.ok)).toBe(true);
|
|
39
|
+
|
|
40
|
+
const receipt = runtime.receipts.forTemplate("booking")[0]!;
|
|
41
|
+
expect(receipt.status).toBe("fallback");
|
|
42
|
+
expect(receipt.attempts.length).toBeGreaterThanOrEqual(2);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("statusCode 400 does not advance to secondary", async () => {
|
|
46
|
+
const secondary = countingOkTransport("resend");
|
|
47
|
+
const runtime = createChannelRuntime({
|
|
48
|
+
templates: [channel.template("booking", { medium: "email" })],
|
|
49
|
+
drivers: [
|
|
50
|
+
driverFromTransport(
|
|
51
|
+
"smtp",
|
|
52
|
+
failingTransport("smtp", Object.assign(new Error("bad request"), { statusCode: 400 })),
|
|
53
|
+
),
|
|
54
|
+
driverFromTransport("resend", secondary.transport),
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const result = await runtime.send("booking", {
|
|
59
|
+
to: "user@example.com",
|
|
60
|
+
via: ["smtp", "resend"],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(secondary.calls.count).toBe(0);
|
|
64
|
+
expect(result.ok).toBe(false);
|
|
65
|
+
expect(result.attempts.every((a) => a.driverId !== "resend")).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("invalid recipient address does not advance to secondary", async () => {
|
|
69
|
+
const secondary = countingOkTransport("resend");
|
|
70
|
+
const runtime = createChannelRuntime({
|
|
71
|
+
templates: [channel.template("booking", { medium: "email" })],
|
|
72
|
+
drivers: [
|
|
73
|
+
driverFromTransport(
|
|
74
|
+
"smtp",
|
|
75
|
+
failingTransport("smtp", new Error("invalid recipient address")),
|
|
76
|
+
),
|
|
77
|
+
driverFromTransport("resend", secondary.transport),
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const result = await runtime.send("booking", {
|
|
82
|
+
to: "not-an-email",
|
|
83
|
+
via: ["smtp", "resend"],
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(secondary.calls.count).toBe(0);
|
|
87
|
+
expect(result.ok).toBe(false);
|
|
88
|
+
const receipt = runtime.receipts.forTemplate("booking")[0]!;
|
|
89
|
+
expect(receipt.status).toBe("blocked/invalid-address");
|
|
90
|
+
});
|
|
91
|
+
});
|