okengine 0.22.0 → 0.23.1
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 +7 -0
- package/manifest.v1.schema.json +44 -0
- package/package.json +32 -19
- package/site/content/docs/ai/mcp.mdx +14 -10
- package/site/content/docs/ai/skills.mdx +1 -1
- package/site/content/docs/elements/ai/agents.mdx +61 -13
- package/site/content/docs/elements/ai/decide.mdx +536 -0
- package/site/content/docs/elements/ai/events.mdx +373 -0
- package/site/content/docs/elements/ai/index.mdx +9 -3
- package/site/content/docs/elements/ai/meta.json +1 -1
- package/site/content/docs/elements/ai/prompts.mdx +4 -2
- package/site/content/docs/elements/channel/index.mdx +4 -4
- package/site/content/docs/elements/channel/receipts.mdx +7 -5
- package/site/content/docs/elements/channel/sms.mdx +2 -1
- package/site/content/docs/elements/flow/index.mdx +22 -18
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/signal/index.mdx +15 -14
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- package/site/content/docs/plugins/otp.mdx +3 -3
- package/site/content/docs/recipes/dragonfly.mdx +5 -5
- package/site/content/docs/recipes/index.mdx +6 -6
- package/site/content/docs/recipes/postgres.mdx +12 -8
- package/site/content/docs/recipes/redis.mdx +4 -4
- package/site/content/docs/reference/cli.mdx +2 -1
- package/site/content/docs/reference/configuration.mdx +9 -7
- package/site/content/docs/reference/fx.mdx +19 -10
- package/src/auth/api-key-sql.test.ts +44 -0
- package/src/auth/api-key-sql.ts +1 -0
- package/src/auth/api-keys.ts +30 -12
- package/src/auth/config.ts +6 -2
- package/src/auth/gate-auth.test.ts +20 -0
- package/src/auth/tenants.ts +17 -0
- package/src/cli/agents-md.test.ts +25 -0
- package/src/cli/ask-vault-gaps.test.ts +38 -1
- package/src/cli/ask-vault-gaps.ts +35 -2
- package/src/cli/decide.ts +174 -0
- package/src/cli/decision-lock-watch.test.ts +45 -0
- package/src/cli/decision-lock-watch.ts +51 -0
- package/src/cli/dev-app-runner.ts +34 -8
- package/src/cli/dev-auth-secret.test.ts +49 -0
- package/src/cli/dev-auth-secret.ts +49 -0
- package/src/cli/dev-hot.test.ts +41 -0
- package/src/cli/dev-hot.ts +48 -0
- package/src/cli/dev.test.ts +1 -1
- package/src/cli/dev.ts +24 -6
- package/src/cli/doctor-diff.test.ts +29 -1
- package/src/cli/doctor-diff.ts +35 -0
- package/src/cli/eval.ts +142 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/load-config.images.test.ts +2 -2
- package/src/cli/load-config.ts +1 -1
- package/src/cli/manifest-pr-diff.ts +16 -0
- package/src/cli/mcp-from-console.ts +48 -0
- package/src/cli/registry.ts +35 -1
- package/src/client/agent.ts +307 -0
- package/src/client/transport.test.ts +20 -0
- package/src/client/transport.ts +2 -1
- package/src/client/types.ts +1 -1
- package/src/client-react/index.ts +7 -1
- package/src/client-react/use-agent-run.test.ts +271 -0
- package/src/client-react/use-agent-run.ts +193 -0
- package/src/compiler/ai-approval.test.ts +36 -0
- package/src/compiler/ai-repair.test.ts +22 -0
- package/src/compiler/decisions.extract.test.ts +164 -0
- package/src/compiler/effects-fetch.test.ts +77 -0
- package/src/compiler/effects-infer.ts +114 -17
- package/src/compiler/effects-join.test.ts +48 -0
- package/src/compiler/extract.test.ts +28 -0
- package/src/compiler/extract.ts +279 -13
- package/src/compiler/fx-index.ts +58 -4
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- package/src/console/index.ts +1 -1
- package/src/console/server/ai-runs-flows.ts +269 -0
- package/src/console/server/ai-runs.test.ts +323 -0
- package/src/console/server/ai-runs.ts +562 -0
- package/src/console/server/ai.ts +12 -1
- package/src/console/server/app.ts +10 -1
- package/src/console/server/decisions-flows.ts +157 -0
- package/src/console/server/decisions.test.ts +66 -0
- package/src/console/server/decisions.ts +229 -0
- package/src/console/server/flows.ts +50 -2
- package/src/console/server/invoke-user-flow.ts +9 -0
- package/src/console/server/runs-ingest.ts +16 -0
- package/src/console/server/serve.ts +5 -0
- package/src/console/server/state.ts +25 -1
- package/src/console/server/store-stats.test.ts +47 -0
- package/src/console/server/store-stats.ts +92 -18
- package/src/console/ui-next/dist/assets/access-page-C_N4qvai.js +4 -0
- package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-Cq1TxniW.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-BaI4uxZz.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DNyK7IIY.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-CRhBJMZR.js} +1 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
- package/src/console/ui-next/dist/assets/decisions-page-D9twj8Xy.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-CsHWlVPU.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-ZaTJAhR9.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-DQ-EMAfz.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-Cn_Fl79s.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-CmRFeWdT.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-Cn6R_dXz.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bx6EZhSe.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DmdiUrfh.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-8P6vGXnn.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-BfGvpFSv.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CWdUHbc0.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-CZOFZzay.js +1 -0
- package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-ki_ishmX.js +2 -0
- package/src/console/ui-next/dist/index.html +4 -3
- package/src/console/ui-next/seed-parked-approval.ts +126 -0
- package/src/console/ui-next/src/client.ts +224 -1
- package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
- package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
- package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
- package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
- package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
- package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
- package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
- package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
- package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
- package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
- package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
- package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
- package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
- package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
- package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
- package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
- package/src/console/ui-next/src/router.tsx +17 -0
- package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
- package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
- package/src/console/ui-next/vite.config.ts +9 -0
- package/src/docker/recipes/dragonfly.ts +3 -2
- package/src/docker/recipes/redis.ts +1 -1
- package/src/drivers/ai-anthropic.ts +212 -21
- package/src/drivers/ai-mock.ts +34 -1
- package/src/drivers/ai-openai-compatible.ts +67 -26
- package/src/drivers/ai-preconnect.ts +18 -0
- package/src/drivers/ai-stream.test.ts +62 -0
- package/src/drivers/ai-types.ts +20 -0
- package/src/drivers/journal-postgres.test.ts +22 -1
- package/src/drivers/journal-postgres.ts +511 -13
- package/src/drivers/memory-ddl.test.ts +18 -0
- package/src/drivers/memory.ts +21 -1
- package/src/drivers/pg-rls.test.ts +31 -0
- package/src/drivers/pg-rls.ts +15 -0
- package/src/drivers/postgres.test.ts +9 -0
- package/src/drivers/postgres.ts +49 -11
- package/src/drivers/signal-compete.test.ts +234 -0
- package/src/drivers/signal-postgres.ts +39 -19
- package/src/drivers/signal-redis.ts +167 -11
- package/src/drivers/signal-types.ts +12 -0
- package/src/elements/ai/agent-event-slot.ts +27 -0
- package/src/elements/ai/approval-http.ts +207 -0
- package/src/elements/ai/approval.test.ts +741 -0
- package/src/elements/ai/approval.ts +228 -0
- package/src/elements/ai/decisions/bind.ts +163 -0
- package/src/elements/ai/decisions/certificate.test.ts +291 -0
- package/src/elements/ai/decisions/certificate.ts +377 -0
- package/src/elements/ai/decisions/certify.ts +314 -0
- package/src/elements/ai/decisions/decide.test.ts +953 -0
- package/src/elements/ai/decisions/e2e.test.ts +399 -0
- package/src/elements/ai/decisions/export.test.ts +120 -0
- package/src/elements/ai/decisions/export.ts +146 -0
- package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
- package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
- package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
- package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
- package/src/elements/ai/decisions/http.test.ts +154 -0
- package/src/elements/ai/decisions/http.ts +240 -0
- package/src/elements/ai/decisions/labels.test.ts +282 -0
- package/src/elements/ai/decisions/labels.ts +257 -0
- package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
- package/src/elements/ai/decisions/openrouter.ts +36 -0
- package/src/elements/ai/decisions/provider.ts +102 -0
- package/src/elements/ai/decisions/typesafe.ts +36 -0
- package/src/elements/ai/declare.ts +252 -1
- package/src/elements/ai/events.test.ts +379 -0
- package/src/elements/ai/events.ts +138 -0
- package/src/elements/ai/mcp-protocol.ts +5 -6
- package/src/elements/ai/run-events.test.ts +461 -0
- package/src/elements/ai/run-events.ts +522 -0
- package/src/elements/ai/runtime.ts +1187 -105
- package/src/elements/ai/stream-turn.ts +214 -0
- package/src/elements/ai/stream.live.test.ts +76 -0
- package/src/elements/ai/subagent.test.ts +136 -0
- package/src/elements/ai.test.ts +497 -3
- package/src/elements/ai.ts +16 -1
- package/src/elements/channel/runtime.ts +11 -0
- package/src/elements/channel/sql-ledger.ts +251 -0
- package/src/elements/clock/durable.ts +27 -1
- package/src/elements/signal/runtime.ts +9 -0
- package/src/elements/signal.test.ts +1 -0
- package/src/elements/store/cache.test.ts +120 -15
- package/src/elements/store/cache.ts +200 -39
- package/src/elements/store/prepare-row.test.ts +6 -2
- package/src/elements/store/sql-session.test.ts +53 -0
- package/src/elements/store/sql-session.ts +206 -10
- package/src/elements/store.ts +1 -0
- package/src/i18n/catalogs/ar.ts +5 -0
- package/src/i18n/catalogs/en.ts +5 -0
- package/src/index.ts +2 -0
- package/src/kernel/agent-event-store.ts +448 -0
- package/src/kernel/app.ts +135 -17
- package/src/kernel/auto-cache.test.ts +228 -0
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/channel.ts +63 -2
- package/src/kernel/boot-bind/honor-config.test.ts +40 -10
- package/src/kernel/boot-bind/signal.ts +61 -25
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot.ts +31 -6
- package/src/kernel/builtin-errors.ts +2 -0
- package/src/kernel/capability.ts +3 -0
- package/src/kernel/decision-label-store.ts +441 -0
- package/src/kernel/effects.test.ts +2 -1
- package/src/kernel/effects.ts +13 -1
- package/src/kernel/element-registries.ts +3 -0
- package/src/kernel/errors-compiler.ts +11 -0
- package/src/kernel/errors-text.ts +20 -0
- package/src/kernel/errors.ts +10 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/flow.ts +33 -3
- package/src/kernel/fx-call-types.test.ts +32 -0
- package/src/kernel/fx-decide.ts +765 -0
- package/src/kernel/fx-sql-handle.ts +46 -7
- package/src/kernel/fx.test.ts +13 -8
- package/src/kernel/fx.ts +339 -45
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/idempotency.test.ts +2 -1
- package/src/kernel/journal.test.ts +114 -0
- package/src/kernel/journal.ts +471 -51
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline-tenant.ts +5 -1
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/pipeline.ts +5 -0
- package/src/kernel/signal-tx.ts +28 -0
- package/src/kernel/sse-id.ts +32 -0
- package/src/kernel/store-transaction.test.ts +95 -0
- package/src/manifest/diff.ts +1 -0
- package/src/manifest/types.ts +29 -2
- package/src/mcp/ai-tools.test.ts +202 -0
- package/src/mcp/authorization.ts +66 -0
- package/src/mcp/docs-server.ts +19 -2
- package/src/mcp/mcp.test.ts +83 -0
- package/src/mcp/protocol.ts +8 -2
- package/src/mcp/server.ts +23 -2
- package/src/mcp/session.ts +3 -0
- package/src/mcp/tools.ts +168 -0
- package/src/mcp/versions.ts +60 -0
- package/src/plugins/index.ts +1 -0
- package/src/plugins/mena.ts +56 -0
- package/src/release/http-graph.test.ts +2 -0
- package/src/release/http-graph.ts +5 -0
- package/src/release/measure.ts +39 -14
- package/src/runs/collect.ts +4 -1
- package/src/runs/parquet.test.ts +27 -1
- package/src/runs/parquet.ts +19 -0
- package/src/runs/types.ts +31 -0
- package/src/runtime/json-code-block.test.ts +106 -47
- package/src/runtime/json-code-block.ts +1209 -46
- package/src/term.test.ts +13 -0
- package/src/term.ts +81 -49
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
- package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
- package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
- package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
- package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
- package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
- package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
|
@@ -544,11 +544,11 @@ writes (`set` · `delete`). Declare the same refs when you hand-write
|
|
|
544
544
|
|
|
545
545
|
## Drivers
|
|
546
546
|
|
|
547
|
-
| Driver | Runs as
|
|
548
|
-
| -------------------------------------------- |
|
|
549
|
-
| `redis` |
|
|
550
|
-
| `memory` | Process map
|
|
551
|
-
| Durable (`postgres` / `pglite` via `oke_kv`) | Shared SQL URL
|
|
547
|
+
| Driver | Runs as | Best for |
|
|
548
|
+
| -------------------------------------------- | --------------------------------- | ---------------------------------- |
|
|
549
|
+
| `redis` | Dragonfly by default (Redis-wire) | Dev + prod default |
|
|
550
|
+
| `memory` | Process map | Test default |
|
|
551
|
+
| Durable (`postgres` / `pglite` via `oke_kv`) | Shared SQL URL | Namespaces that must outlive Redis |
|
|
552
552
|
|
|
553
553
|
Defaults: `redis` / `memory` / `redis` (dev / test / prod). Pin overrides in
|
|
554
554
|
`oke.config.ts`; image pins stay under `images.store.kv` — the driver id stays
|
|
@@ -22,9 +22,9 @@ app-owned delivery across the channels you declare.
|
|
|
22
22
|
</Callout>
|
|
23
23
|
|
|
24
24
|
<Callout type="info" title="fx.sendOtp vs otp()">
|
|
25
|
-
`fx.sendOtp` / `fx.verifyOtp` are
|
|
26
|
-
build routes, sessions, rates, and storage yourself. `otp()` provider mode wraps that path
|
|
27
|
-
|
|
25
|
+
`fx.sendOtp` / `fx.verifyOtp` / `fx.deliverOtp` are supported Channel methods. Direct use means
|
|
26
|
+
you build routes, sessions, rates, and storage yourself. `otp()` provider mode wraps that path.
|
|
27
|
+
The `mena` plugin (`menaChannels`) is an additional way to open the same providers.
|
|
28
28
|
</Callout>
|
|
29
29
|
|
|
30
30
|
## Quick start
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Dragonfly"
|
|
3
|
-
description: "
|
|
3
|
+
description: "Default store.kv image — multi-threaded Redis-wire runtime, memlock ulimit, OKE_STORE_KV_PASSWORD."
|
|
4
4
|
icon: "Zap"
|
|
5
5
|
source: "docs/spec/unified-theory.md"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
Dragonfly is a multi-threaded
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
Dragonfly is the default `store.kv` image in `dev` and `prod`: a multi-threaded Redis-protocol engine.
|
|
9
|
+
Same `redis://` wire format and the same `redis` driver, so Flows do not change.
|
|
10
|
+
Pin Redis or Valkey when you want those engines instead.
|
|
11
11
|
|
|
12
12
|
<Callout title="The one rule">
|
|
13
13
|
Driver id stays `redis`. Dragonfly is an image choice — every KV call through `fx.store` behaves
|
|
@@ -23,7 +23,7 @@ zero Flow changes.
|
|
|
23
23
|
|
|
24
24
|
```typescript title="oke.config.ts"
|
|
25
25
|
images: {
|
|
26
|
-
"store.kv": "docker.dragonflydb.io/dragonflydb/dragonfly",
|
|
26
|
+
"store.kv": "docker.dragonflydb.io/dragonflydb/dragonfly:v2.0.0",
|
|
27
27
|
},
|
|
28
28
|
```
|
|
29
29
|
|
|
@@ -51,15 +51,15 @@ Driver id stays `redis` for every image below.
|
|
|
51
51
|
|
|
52
52
|
<Cards>
|
|
53
53
|
<Card
|
|
54
|
-
title="
|
|
55
|
-
description="Default store.kv —
|
|
56
|
-
href="/docs/recipes/
|
|
54
|
+
title="Dragonfly"
|
|
55
|
+
description="Default store.kv — multi-threaded Redis-wire runtime."
|
|
56
|
+
href="/docs/recipes/dragonfly"
|
|
57
57
|
/>
|
|
58
58
|
<Card title="Valkey" description="BSD-licensed Redis-wire fork." href="/docs/recipes/valkey" />
|
|
59
59
|
<Card
|
|
60
|
-
title="
|
|
61
|
-
description="
|
|
62
|
-
href="/docs/recipes/
|
|
60
|
+
title="Redis"
|
|
61
|
+
description="Opt-in Redis Open Source — requirepass + maxmemory."
|
|
62
|
+
href="/docs/recipes/redis"
|
|
63
63
|
/>
|
|
64
64
|
</Cards>
|
|
65
65
|
|
|
@@ -106,11 +106,11 @@ Scale out with [PgDog](/docs/recipes/pgdog) so `N × Bun.SQL pool` does not exha
|
|
|
106
106
|
Recreate `store-sql` after a recipe change — the data volume can stay.
|
|
107
107
|
</Callout>
|
|
108
108
|
|
|
109
|
-
| Step | What you do
|
|
110
|
-
| ------- |
|
|
111
|
-
| Preload | Recipe already sets `shared_preload_libraries=pg_stat_statements`
|
|
112
|
-
| Create | `CREATE EXTENSION IF NOT EXISTS pg_stat_statements`
|
|
113
|
-
| Console | Store → SQL band → **Performance**
|
|
109
|
+
| Step | What you do |
|
|
110
|
+
| ------- | ---------------------------------------------------------------------- |
|
|
111
|
+
| Preload | Recipe already sets `shared_preload_libraries=pg_stat_statements` |
|
|
112
|
+
| Create | `oke db push` runs `CREATE EXTENSION IF NOT EXISTS pg_stat_statements` |
|
|
113
|
+
| Console | Store → SQL band → **Performance** |
|
|
114
114
|
|
|
115
115
|
Default `postgres:18-alpine` does **not** ship `hypopg` / `index_advisor`. Pin the
|
|
116
116
|
opt-in image when you want Suggest indexes:
|
|
@@ -137,8 +137,8 @@ set. Re-run `oke dev` so the stack writes `.env.local`, or export
|
|
|
137
137
|
<Accordion title="PgStatStatementsNotPreloaded">
|
|
138
138
|
|
|
139
139
|
The Console Performance view needs the library preloaded **and** the extension
|
|
140
|
-
created. Recreate `store-sql` so the new `command` applies, then run
|
|
141
|
-
|
|
140
|
+
created. Recreate `store-sql` so the new `command` applies, then run `oke db push`
|
|
141
|
+
(or open Performance again — it creates the extension when preload is already on).
|
|
142
142
|
|
|
143
143
|
Existing clusters that started before this recipe change keep the old
|
|
144
144
|
postmaster flags until recreate.
|
|
@@ -167,5 +167,9 @@ Postgres refusing auth.
|
|
|
167
167
|
<Cards>
|
|
168
168
|
<Card title="PgDog" description="Add pooling in front of Postgres." href="/docs/recipes/pgdog" />
|
|
169
169
|
<Card title="Neon" description="A managed alternative." href="/docs/providers/neon" />
|
|
170
|
-
<Card
|
|
170
|
+
<Card
|
|
171
|
+
title="Dragonfly"
|
|
172
|
+
description="The default store.kv image."
|
|
173
|
+
href="/docs/recipes/dragonfly"
|
|
174
|
+
/>
|
|
171
175
|
</Cards>
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Redis"
|
|
3
|
-
description: "
|
|
3
|
+
description: "Opt-in store.kv image — OKE_STORE_KV_PASSWORD, maxmemory policy, ephemeral data, REDIS_URL."
|
|
4
4
|
icon: "Zap"
|
|
5
5
|
source: "docs/spec/unified-theory.md"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
Redis is
|
|
9
|
-
references containing `redis` or `keydb` and derives a
|
|
10
|
-
unauthenticated instance.
|
|
8
|
+
Redis is an opt-in `store.kv` image. The default pin is [Dragonfly](/docs/recipes/dragonfly).
|
|
9
|
+
`oke docker` matches image references containing `redis` or `keydb` and derives a
|
|
10
|
+
password-required server — no unauthenticated instance.
|
|
11
11
|
|
|
12
12
|
<Callout title="The one rule">
|
|
13
13
|
The driver id stays `redis` for every Redis-wire image — Redis, Valkey, Dragonfly, or a managed
|
|
@@ -134,7 +134,8 @@ Override app / console / mcp listen ports in `oke.config.ts` `ports` — see
|
|
|
134
134
|
| `docker` | Derive compose · clean leftover stacks | `--prod\|-p`, `--out`, `--config`, `--manifest`; sub `clean` |
|
|
135
135
|
| `images` | List / pin digests | `list` · `pin` |
|
|
136
136
|
| `build` | Tree-shaken bundle | `--target\|-t bun\|node\|edge`, `--entry`, `--outdir` |
|
|
137
|
-
| `eval` | Prompt eval sets (CI gate) | `--manifest`
|
|
137
|
+
| `eval` | Prompt eval sets (CI gate) | `--manifest`, `--certify` (decision seed certificate; prompt evals stay as they are) |
|
|
138
|
+
| `decide` | Write a decision lockfile from the operator candidate | `promote <name>` (`--origin`, `--lock`) |
|
|
138
139
|
| `ai` | Configure AI driver + models | `setup` (`--provider`, `--chat`, `--vision`, `--embed`, `--yes`) |
|
|
139
140
|
| `branch` | Fork journaled state | `<name>`, `--at\|-a` |
|
|
140
141
|
| `replay` | Re-invoke a past Flow from Runs | `--request-id\|-r`, `--entry`, `--dry-run`, `--live` |
|
|
@@ -136,11 +136,13 @@ sql: {
|
|
|
136
136
|
},
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
-
| Field | Type | Meaning
|
|
140
|
-
| ---------- | ----------------- |
|
|
141
|
-
| `url` | connection string | Overrides the env-var resolution
|
|
142
|
-
| `pool` | `{ max?, min? }` | SQL pool
|
|
143
|
-
| `replicas` | `string[]` | Read-only
|
|
139
|
+
| Field | Type | Meaning |
|
|
140
|
+
| ---------- | ----------------- | --------------------------------------------------------------------------------- |
|
|
141
|
+
| `url` | connection string | Overrides the env-var resolution |
|
|
142
|
+
| `pool` | `{ max?, min? }` | `max` is the Bun.SQL pool size. Unset keeps 8. `min` is accepted and not applied |
|
|
143
|
+
| `replicas` | `string[]` | Read-only flows use these URLs. Each replica opens its own pool at the same `max` |
|
|
144
|
+
|
|
145
|
+
A store `max` of 20 does not share a pool another part of the process already opened at 8. Writes stay on the primary.
|
|
144
146
|
|
|
145
147
|
## images
|
|
146
148
|
|
|
@@ -150,7 +152,7 @@ Nested pins by element role — vendor choice lives here, never in driver ids:
|
|
|
150
152
|
images: {
|
|
151
153
|
store: {
|
|
152
154
|
sql: "postgres:18-alpine",
|
|
153
|
-
kv: "
|
|
155
|
+
kv: "docker.dragonflydb.io/dragonflydb/dragonfly:v2.0.0",
|
|
154
156
|
files: "rustfs/rustfs:1.0.0",
|
|
155
157
|
},
|
|
156
158
|
channel: { email: "axllent/mailpit:v1.31.2" },
|
|
@@ -161,7 +163,7 @@ images: {
|
|
|
161
163
|
|
|
162
164
|
Omitted image keys mean no container for that role. When both `store.sql` and `pgdog` are pinned, `DATABASE_URL` points at PgDog — see [Store](/docs/elements/store#multiple-environments).
|
|
163
165
|
|
|
164
|
-
For `store.kv`, pin
|
|
166
|
+
For `store.kv`, pin Dragonfly (default), Redis, or Valkey — driver id stays `redis`.
|
|
165
167
|
`{ durable: true }` KV lives in SQL (`oke_kv` on `DATABASE_URL`), not a second Redis image.
|
|
166
168
|
|
|
167
169
|
Compose does not manage AI inference — [OpenRouter](/docs/recipes/openrouter) or BYO
|
|
@@ -158,7 +158,13 @@ on(
|
|
|
158
158
|
| `fx.auth.upsertTenantRole({ tenantId, roleName, scopes })` | `write` `auth:tenants` | Application scopes only — `console:*` is unknown_scope |
|
|
159
159
|
|
|
160
160
|
`fx.call` starts the callee with an **empty** `fx.auth` (fail-closed) and propagates `fx.tenant.id`.
|
|
161
|
-
Read `fx.principal` for audit — it does not copy into `fx.auth`.
|
|
161
|
+
Read `fx.principal` for audit — it does not copy into `fx.auth`. Passing a Flow handle types
|
|
162
|
+
`input` and the result. A string name stays `Promise<unknown>`.
|
|
163
|
+
|
|
164
|
+
`fx.store(db).transaction(async (tx) => { … })` pins one SQL connection. `fx.emit` inside the
|
|
165
|
+
callback stages on the signal outbox and publishes after commit. A throw rolls that batch back.
|
|
166
|
+
`fx.store(db).run(builder)` runs a Drizzle builder through `toSQL()`. Joins stay on that builder;
|
|
167
|
+
the hand-rolled chain does not grow a join API. Inference still records every joined table.
|
|
162
168
|
|
|
163
169
|
<Callout>
|
|
164
170
|
Gates never consult `fx.principal`. `return fx.fail(...)` is a `FlowFailure`; an unhandled `throw`
|
|
@@ -240,12 +246,13 @@ elements.
|
|
|
240
246
|
|
|
241
247
|
## AI
|
|
242
248
|
|
|
243
|
-
| Signature | Records | Returns
|
|
244
|
-
| ----------------------------------------------------- | ------------------------- |
|
|
245
|
-
| `fx.ask(prompt, input?, { via?, tools?, maxSteps? })` | `ask` (+ `call` per tool) | Object validated against the prompt's `out`
|
|
246
|
-
| `fx.run(agent, input?)` | `ask` | Agent result
|
|
247
|
-
| `fx.stream(model, { prompt?, data?, via? })` | `ask` | `AsyncIterable<string>` — real driver stream; cancels via ambient `fx.signal` (HTTP disconnect included)
|
|
248
|
-
| `fx.search(embed, query, { topK? })` | `read` | Matches from the index/embed
|
|
249
|
+
| Signature | Records | Returns |
|
|
250
|
+
| ----------------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
251
|
+
| `fx.ask(prompt, input?, { via?, tools?, maxSteps? })` | `ask` (+ `call` per tool) | Object validated against the prompt's `out` |
|
|
252
|
+
| `fx.run(agent, input?)` | `ask` | Agent result |
|
|
253
|
+
| `fx.stream(model, { prompt?, data?, via? })` | `ask` | `AsyncIterable<string>` — real driver stream; cancels via ambient `fx.signal` (HTTP disconnect included) |
|
|
254
|
+
| `fx.search(embed, query, { topK? })` | `read` | Matches from the index/embed |
|
|
255
|
+
| `fx.decide(decision, input)` | `decide` | `$.<question>.how` is `auto`, `reviewed`, or `abstained`. `audited` is a boolean flag, not a `how`. Abstain makes the value `null`. |
|
|
249
256
|
|
|
250
257
|
AI calls are nondeterministic: journaling is forced on and auto-cache disabled around them. `tools` are Flow refs — each model tool call goes through `fx.call` (same capability and Runs path).
|
|
251
258
|
|
|
@@ -280,9 +287,11 @@ Durations: `"200ms"` · `"30s"` · `"2m"` · `"1h"` · `"7d"`. A `"d"` is 86_400
|
|
|
280
287
|
|
|
281
288
|
## Cache
|
|
282
289
|
|
|
283
|
-
|
|
284
|
-
`fx.
|
|
285
|
-
|
|
290
|
+
Pure store-read flows cache by default. A send, emit, fetch, vault read, ask,
|
|
291
|
+
decide, or `fx.call` is never cached. The key includes tenant, locale, scopes,
|
|
292
|
+
and membership roles. Writes through `fx` invalidate those keys. `cache: false`
|
|
293
|
+
always disables. `oke({ cache: { auto: false } })` turns the default off.
|
|
294
|
+
Cross-instance invalidation and out-of-band writes are not covered.
|
|
286
295
|
|
|
287
296
|
`fx.cache` is the manual (tier-3) surface:
|
|
288
297
|
|
|
@@ -44,4 +44,48 @@ describe("api-key SQL persist", () => {
|
|
|
44
44
|
expect(other.keys.get("key_sql")?.creatorId).toBe("u1");
|
|
45
45
|
expect(other.keys.get("key_sql")?.hash).toBe(created.row.hash);
|
|
46
46
|
});
|
|
47
|
+
|
|
48
|
+
test("authenticate reloads an oke_ secret missing from memory", async () => {
|
|
49
|
+
const { authenticateApiKey } = await import("./api-keys.ts");
|
|
50
|
+
const rows = new Map<string, Record<string, unknown>>();
|
|
51
|
+
const sql: ApiKeySqlExec = {
|
|
52
|
+
async execute(_q, params) {
|
|
53
|
+
const id = String(params[0]);
|
|
54
|
+
rows.set(id, {
|
|
55
|
+
id,
|
|
56
|
+
plane: params[1],
|
|
57
|
+
hash: params[2],
|
|
58
|
+
name: params[3],
|
|
59
|
+
scopes: params[4],
|
|
60
|
+
expires_at: params[5],
|
|
61
|
+
rate_limit: params[6],
|
|
62
|
+
ip_allowlist: params[7],
|
|
63
|
+
creator_id: params[8],
|
|
64
|
+
creator_scopes: params[9],
|
|
65
|
+
created_at: params[10],
|
|
66
|
+
last_used_at: params[11],
|
|
67
|
+
revoked_at: params[12],
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
async all() {
|
|
71
|
+
return [...rows.values()];
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
const writer = createApiKeyStore({ pepper: "shared" });
|
|
75
|
+
bindApiKeySqlPersist(writer, sql);
|
|
76
|
+
const created = await createApiKey(writer, {
|
|
77
|
+
plane: "user",
|
|
78
|
+
name: "Auth Test",
|
|
79
|
+
scopes: ["member"],
|
|
80
|
+
creatorId: "user_admin",
|
|
81
|
+
creatorScopes: ["member"],
|
|
82
|
+
id: "key_live",
|
|
83
|
+
});
|
|
84
|
+
const reader = createApiKeyStore({ pepper: "shared" });
|
|
85
|
+
bindApiKeySqlPersist(reader, sql);
|
|
86
|
+
expect(reader.keys.size).toBe(0);
|
|
87
|
+
const row = await authenticateApiKey(reader, created.secret);
|
|
88
|
+
expect(row?.id).toBe("key_live");
|
|
89
|
+
expect(reader.keys.has("key_live")).toBe(true);
|
|
90
|
+
});
|
|
47
91
|
});
|
package/src/auth/api-key-sql.ts
CHANGED
|
@@ -97,6 +97,7 @@ export function bindApiKeySqlPersist(
|
|
|
97
97
|
table: string = AUTH_TABLES.apiKeys,
|
|
98
98
|
): void {
|
|
99
99
|
store.persist = (row) => persistApiKeyRow(sql, row, table);
|
|
100
|
+
store.reload = () => hydrateApiKeyStore(sql, store, table);
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
/** Runtime that can open the app's shared primary SQL connection. */
|
package/src/auth/api-keys.ts
CHANGED
|
@@ -85,6 +85,11 @@ export interface ApiKeyStore {
|
|
|
85
85
|
pepper?: string;
|
|
86
86
|
/** Optional write-through persist (SQL). */
|
|
87
87
|
persist?: (row: ApiKeyRow) => void | Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Reload rows from the durable table. `oke dev` keeps Console and the app
|
|
90
|
+
* in separate processes; a miss on an `oke_` secret refreshes this cache.
|
|
91
|
+
*/
|
|
92
|
+
reload?: () => void | Promise<void>;
|
|
88
93
|
/** Sliding-window hits for per-key rateLimit (subject = key id). */
|
|
89
94
|
rateHits?: Map<string, number[]>;
|
|
90
95
|
}
|
|
@@ -202,20 +207,33 @@ export async function authenticateApiKey(
|
|
|
202
207
|
const now = options.now ?? (() => Date.now());
|
|
203
208
|
const pepper = store.pepper ?? DEFAULT_PEPPER;
|
|
204
209
|
const hash = await hashApiKeySecret(secret, pepper);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (!allowed) return null;
|
|
210
|
+
let row = matchApiKeyHash(store, hash);
|
|
211
|
+
if (!row && secret.startsWith("oke_") && store.reload) {
|
|
212
|
+
try {
|
|
213
|
+
await store.reload();
|
|
214
|
+
} catch {
|
|
215
|
+
// Durable read missed — the in-memory miss still stands.
|
|
212
216
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
+
row = matchApiKeyHash(store, hash);
|
|
218
|
+
}
|
|
219
|
+
if (!row) return null;
|
|
220
|
+
if (row.revokedAt !== null) return null;
|
|
221
|
+
if (row.expiresAt !== null && row.expiresAt <= now()) return null;
|
|
222
|
+
if (row.ipAllowlist.length > 0) {
|
|
223
|
+
const allowed = await allowlistAllowsIp(row.ipAllowlist, options.ip, options.lookup);
|
|
224
|
+
if (!allowed) return null;
|
|
225
|
+
}
|
|
226
|
+
if (row.rateLimit && !takeKeyRate(store, row.id, row.rateLimit, now())) return null;
|
|
227
|
+
row.lastUsedAt = now();
|
|
228
|
+
await persistRow(store, row);
|
|
229
|
+
return row;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function matchApiKeyHash(store: ApiKeyStore, hash: string): ApiKeyRow | undefined {
|
|
233
|
+
for (const row of store.keys.values()) {
|
|
234
|
+
if (row.hash === hash) return row;
|
|
217
235
|
}
|
|
218
|
-
return
|
|
236
|
+
return undefined;
|
|
219
237
|
}
|
|
220
238
|
|
|
221
239
|
/**
|
package/src/auth/config.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Better Auth is inspiration for shape only; this is OKE-native.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { createEnv } from "../runtime/primitives.ts";
|
|
7
8
|
import type { PasswordHashOptions } from "../runtime/types.ts";
|
|
8
9
|
import type { SessionStore } from "./sessions.ts";
|
|
9
10
|
import { createApiKeyStore, type ApiKeyStore } from "./api-keys.ts";
|
|
@@ -226,9 +227,12 @@ export function mintDevAuthSecret(): string {
|
|
|
226
227
|
export function resolveGateAuth(options: ResolveGateAuthOptions): ResolvedGateAuth {
|
|
227
228
|
const { auth, env } = options;
|
|
228
229
|
const isProd = env === "prod" || env === "production";
|
|
229
|
-
let secret = auth.secret;
|
|
230
|
+
let secret = auth.secret?.trim() ?? "";
|
|
230
231
|
let secretMinted = false;
|
|
231
|
-
if (
|
|
232
|
+
if (secret.length === 0) {
|
|
233
|
+
secret = createEnv().get("OKE_AUTH_SECRET")?.trim() ?? "";
|
|
234
|
+
}
|
|
235
|
+
if (secret.length === 0) {
|
|
232
236
|
if (isProd) {
|
|
233
237
|
throw new Error(
|
|
234
238
|
"gate.auth: secret is required in production (set gate.auth.secret or OKE_AUTH_SECRET)",
|
|
@@ -21,13 +21,33 @@ afterEach(() => {
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
describe("gate.auth — config", () => {
|
|
24
|
+
const prevAuthSecret = process.env.OKE_AUTH_SECRET;
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
if (prevAuthSecret === undefined) delete process.env.OKE_AUTH_SECRET;
|
|
28
|
+
else process.env.OKE_AUTH_SECRET = prevAuthSecret;
|
|
29
|
+
});
|
|
30
|
+
|
|
24
31
|
test("prod without secret fails", () => {
|
|
32
|
+
delete process.env.OKE_AUTH_SECRET;
|
|
25
33
|
expect(() =>
|
|
26
34
|
resolveGateAuth({ auth: { emailAndPassword: { enabled: true } }, env: "prod" }),
|
|
27
35
|
).toThrow(/secret is required/);
|
|
28
36
|
});
|
|
29
37
|
|
|
38
|
+
test("OKE_AUTH_SECRET is the pepper when gate.auth.secret is omitted", () => {
|
|
39
|
+
process.env.OKE_AUTH_SECRET = "shared-dev-pepper";
|
|
40
|
+
const resolved = resolveGateAuth({
|
|
41
|
+
auth: { emailAndPassword: { enabled: true } },
|
|
42
|
+
env: "dev",
|
|
43
|
+
});
|
|
44
|
+
expect(resolved.secret).toBe("shared-dev-pepper");
|
|
45
|
+
expect(resolved.secretMinted).toBe(false);
|
|
46
|
+
expect(resolved.apiKeyStore.pepper).toBe("shared-dev-pepper");
|
|
47
|
+
});
|
|
48
|
+
|
|
30
49
|
test("dev mints secret", () => {
|
|
50
|
+
delete process.env.OKE_AUTH_SECRET;
|
|
31
51
|
const resolved = resolveGateAuth({
|
|
32
52
|
auth: { emailAndPassword: { enabled: true } },
|
|
33
53
|
env: "dev",
|
package/src/auth/tenants.ts
CHANGED
|
@@ -359,3 +359,20 @@ export function tenantScopesForMember(
|
|
|
359
359
|
const role = store.roles.get(roleKey(tenantId, member.role));
|
|
360
360
|
return role?.scopes ?? [];
|
|
361
361
|
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Membership role name for the tier-1 cache key.
|
|
365
|
+
*
|
|
366
|
+
* Same member lookup as {@link tenantScopesForMember}. Not an authorization input.
|
|
367
|
+
*
|
|
368
|
+
* @param store - Tenant store
|
|
369
|
+
* @param tenantId - Tenant id
|
|
370
|
+
* @param userId - User id
|
|
371
|
+
*/
|
|
372
|
+
export function tenantRoleForMember(
|
|
373
|
+
store: TenantStore,
|
|
374
|
+
tenantId: string,
|
|
375
|
+
userId: string,
|
|
376
|
+
): string | null {
|
|
377
|
+
return getMember(store, tenantId, userId)?.role ?? null;
|
|
378
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commands named in the agent contract must exist on the CLI.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { describe, expect, test } from "bun:test";
|
|
7
|
+
import { commandNames } from "./registry.ts";
|
|
8
|
+
|
|
9
|
+
describe("AGENTS.md commands", () => {
|
|
10
|
+
test("every oke command mentioned in AGENTS.md is registered", () => {
|
|
11
|
+
const text = readFileSync(new URL("../../AGENTS.md", import.meta.url), "utf8");
|
|
12
|
+
const registered = new Set(commandNames());
|
|
13
|
+
const mentioned = new Set<string>();
|
|
14
|
+
for (const match of text.matchAll(/`oke ([a-z][a-z0-9-]*)/g)) {
|
|
15
|
+
const name = match[1];
|
|
16
|
+
if (name) mentioned.add(name);
|
|
17
|
+
}
|
|
18
|
+
expect(mentioned.has("eval")).toBe(true);
|
|
19
|
+
expect(mentioned.has("decide")).toBe(true);
|
|
20
|
+
expect(text).not.toContain("oke decide certify");
|
|
21
|
+
for (const name of mentioned) {
|
|
22
|
+
expect(registered.has(name)).toBe(true);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import { EventEmitter } from "node:events";
|
|
6
7
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
7
8
|
import { tmpdir } from "node:os";
|
|
8
9
|
import { join, resolve } from "node:path";
|
|
9
10
|
import { resetRequiredEnvNames, resetSecrets } from "../elements/vault/declare.ts";
|
|
10
11
|
import { resetBindings } from "../kernel/on.ts";
|
|
11
|
-
import { maybeAskVaultGaps, probeVaultGaps } from "./ask-vault-gaps.ts";
|
|
12
|
+
import { maybeAskVaultGaps, probeVaultGaps, secretPromptWrite } from "./ask-vault-gaps.ts";
|
|
13
|
+
import { promptHidden } from "./vault-secure-input.ts";
|
|
12
14
|
|
|
13
15
|
const OKE = resolve(import.meta.dir, "../index.ts");
|
|
14
16
|
|
|
@@ -93,6 +95,41 @@ describe("maybeAskVaultGaps", () => {
|
|
|
93
95
|
});
|
|
94
96
|
});
|
|
95
97
|
|
|
98
|
+
describe("secretPromptWrite", () => {
|
|
99
|
+
test("keeps the mask on the prompt line", async () => {
|
|
100
|
+
const stdin = new EventEmitter() as EventEmitter & {
|
|
101
|
+
setRawMode?: (mode: boolean) => void;
|
|
102
|
+
off?: (event: string, listener: (...args: unknown[]) => void) => EventEmitter;
|
|
103
|
+
};
|
|
104
|
+
stdin.setRawMode = () => undefined;
|
|
105
|
+
stdin.off = (event: string | symbol, listener: (...args: unknown[]) => void) => {
|
|
106
|
+
stdin.removeListener(event, listener);
|
|
107
|
+
return stdin;
|
|
108
|
+
};
|
|
109
|
+
const shown: string[] = [];
|
|
110
|
+
const echoed: string[] = [];
|
|
111
|
+
const pending = promptHidden("Enter value for OPENROUTER_API_KEY: ", {
|
|
112
|
+
stdin,
|
|
113
|
+
write: secretPromptWrite(
|
|
114
|
+
(text) => shown.push(text),
|
|
115
|
+
(text) => echoed.push(text),
|
|
116
|
+
),
|
|
117
|
+
exit: () => {
|
|
118
|
+
throw new Error("exit should not run");
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
await Promise.resolve();
|
|
122
|
+
stdin.emit("data", "sk");
|
|
123
|
+
stdin.emit("data", "\r");
|
|
124
|
+
expect(await pending).toBe("sk");
|
|
125
|
+
expect(shown).toHaveLength(1);
|
|
126
|
+
expect(shown[0]).toContain("Enter value for OPENROUTER_API_KEY: ");
|
|
127
|
+
expect(shown[0]?.endsWith("\n")).toBe(false);
|
|
128
|
+
expect(echoed.join("")).toBe("**\n");
|
|
129
|
+
expect(shown.join("")).not.toContain("*");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
96
133
|
describe("probeVaultGaps", () => {
|
|
97
134
|
test("sees secrets on oke({ secrets }) after consume drains the registry", async () => {
|
|
98
135
|
const dir = mkdtempSync(join(tmpdir(), "probe-vault-"));
|
|
@@ -14,7 +14,7 @@ import { buildVaultBootChain, normalizeVaultDriverId } from "../elements/vault/b
|
|
|
14
14
|
import { createVaultRuntime, VaultBootError, type VaultGap } from "../elements/vault/runtime.ts";
|
|
15
15
|
import type { OkeOptions } from "../kernel/app.ts";
|
|
16
16
|
import { requiredEnvRegistry, secretRegistry } from "../kernel/element-registries.ts";
|
|
17
|
-
import { formatCliChrome } from "../term.ts";
|
|
17
|
+
import { formatCliChrome, formatStatusLine } from "../term.ts";
|
|
18
18
|
import { loadOkeConfig } from "./load-config.ts";
|
|
19
19
|
import { openEnvStore } from "./vault-cmd.ts";
|
|
20
20
|
import { promptHidden } from "./vault-secure-input.ts";
|
|
@@ -147,6 +147,35 @@ function unionSecrets(
|
|
|
147
147
|
return out;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Writer for {@link promptHidden} during `oke dev`.
|
|
152
|
+
*
|
|
153
|
+
* The question is one chrome row with no trailing newline, so mask
|
|
154
|
+
* characters stay on that row. Echo (`*`, backspace, Enter) goes straight
|
|
155
|
+
* to stdout: `formatCliChrome` would turn each `*` into its own `│` line,
|
|
156
|
+
* and the anchored board would count each of those writes as a new row.
|
|
157
|
+
*
|
|
158
|
+
* @param sink - Line-oriented stdout (the dev board wrapper)
|
|
159
|
+
* @param echo - Raw echo. Defaults to `process.stdout.write`
|
|
160
|
+
*/
|
|
161
|
+
export function secretPromptWrite(
|
|
162
|
+
sink: (text: string) => void,
|
|
163
|
+
echo: (text: string) => void = (text) => {
|
|
164
|
+
process.stdout.write(text);
|
|
165
|
+
},
|
|
166
|
+
): (text: string) => void {
|
|
167
|
+
let opened = false;
|
|
168
|
+
return (text: string) => {
|
|
169
|
+
if (!opened) {
|
|
170
|
+
opened = true;
|
|
171
|
+
const line = formatStatusLine(text);
|
|
172
|
+
sink(line.endsWith("\n") ? line.slice(0, -1) : line);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
echo(text);
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
150
179
|
/**
|
|
151
180
|
* After compose hydrate (and usually schema push), fill Vault boot gaps
|
|
152
181
|
* one-by-one into `.env.local`. Skips when non-TTY or no gaps.
|
|
@@ -175,7 +204,11 @@ export async function maybeAskVaultGaps(options: AskVaultGapsOptions): Promise<n
|
|
|
175
204
|
const envPath = resolve(cwd, options.envFile ?? ".env.local");
|
|
176
205
|
const store = await openEnvStore(envPath);
|
|
177
206
|
const read =
|
|
178
|
-
options.readSecret ??
|
|
207
|
+
options.readSecret ??
|
|
208
|
+
((prompt: string) =>
|
|
209
|
+
promptHidden(prompt, {
|
|
210
|
+
write: secretPromptWrite(write),
|
|
211
|
+
}));
|
|
179
212
|
|
|
180
213
|
const remaining: VaultGap[] = [];
|
|
181
214
|
for (const gap of gaps) {
|