okengine 0.3.4 → 0.3.6
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 +4 -0
- package/manifest.v1.schema.json +43 -1
- package/package.json +16 -2
- package/site/content/docs/ai/skills.mdx +9 -7
- package/site/content/docs/console/vault.mdx +4 -0
- package/site/content/docs/elements/channel.mdx +5 -4
- package/site/content/docs/elements/clock.mdx +1 -0
- package/site/content/docs/elements/flow.mdx +15 -0
- package/site/content/docs/elements/gate.mdx +16 -11
- package/site/content/docs/elements/signal.mdx +8 -7
- package/site/content/docs/elements/store.mdx +48 -8
- package/site/content/docs/elements/vault.mdx +1 -1
- package/site/content/docs/plugins/ip-allowlist.mdx +2 -2
- package/site/content/docs/reference/configuration.mdx +2 -2
- package/site/content/docs/reference/environment-variables.mdx +8 -0
- package/site/content/docs/reference/fx.mdx +40 -0
- package/site/content/docs/reference/plugins.mdx +18 -18
- package/src/compiler/extract.test.ts +63 -0
- package/src/compiler/extract.ts +36 -15
- package/src/console/server/channels.ts +2 -0
- package/src/console/server/clock.ts +3 -0
- package/src/console/server/flows.ts +13 -0
- package/src/console/server/gates.ts +2 -0
- package/src/console/server/plugins.ts +20 -1
- package/src/console/server/signals.ts +5 -0
- package/src/console/server/store.test.ts +17 -0
- package/src/console/server/store.ts +24 -0
- package/src/console/ui/channels/types.ts +1 -0
- package/src/console/ui/clock/types.ts +1 -0
- package/src/console/ui/display.test.ts +14 -0
- package/src/console/ui/display.ts +9 -0
- package/src/console/ui/dist/assets/{index-BWo8R7NR.js → index-CrKMmO__.js} +2 -2
- package/src/console/ui/dist/assets/panel-channels-DCDd4WAC.js +1 -0
- package/src/console/ui/dist/assets/panel-clock-DjGGFPzr.js +1 -0
- package/src/console/ui/dist/assets/panel-gates-B5eTE8XH.js +1 -0
- package/src/console/ui/dist/assets/{panel-overview-BznEOTnb.js → panel-overview-BsFvDdts.js} +1 -1
- package/src/console/ui/dist/assets/panel-plugins-Cj7DK1er.js +1 -0
- package/src/console/ui/dist/assets/{panel-runs-CGWNHLR4.js → panel-runs-C0gmnoYL.js} +1 -1
- package/src/console/ui/dist/assets/panel-signals-whmDXIg3.js +1 -0
- package/src/console/ui/dist/assets/panel-store-CEMHLvaw.js +1 -0
- package/src/console/ui/dist/assets/{panel-traces-DBLx2ilD.js → panel-traces-BDiAuVSK.js} +1 -1
- package/src/console/ui/dist/assets/panel-vault-C9wjbki8.js +1 -0
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/gates/types.ts +1 -0
- package/src/console/ui/plugins/fixture.ts +7 -0
- package/src/console/ui/plugins/types.ts +3 -0
- package/src/console/ui/shell/client.ts +3 -0
- package/src/console/ui/shell/panels/channels/ChannelsPanel.tsx +7 -2
- package/src/console/ui/shell/panels/clock/ClockPanel.tsx +9 -2
- package/src/console/ui/shell/panels/gates/GatesPanel.tsx +12 -5
- package/src/console/ui/shell/panels/plugins/PluginsPanel.tsx +18 -4
- package/src/console/ui/shell/panels/signals/SignalsPanel.tsx +13 -2
- package/src/console/ui/shell/panels/store/StorePanel.tsx +11 -4
- package/src/console/ui/shell/panels/vault/VaultPanel.tsx +9 -3
- package/src/console/ui/signals/types.ts +1 -0
- package/src/console/ui/store/fixture.ts +5 -0
- package/src/console/ui/store/types.ts +2 -0
- package/src/drivers/conformance.test.ts +11 -0
- package/src/drivers/drizzle-dialect.test.ts +4 -0
- package/src/drivers/drizzle-dialect.ts +8 -4
- package/src/drivers/index.ts +4 -0
- package/src/drivers/libsql.ts +179 -0
- package/src/drivers/pglite.ts +79 -0
- package/src/drivers/pgvector.ts +54 -19
- package/src/drivers/types.ts +16 -7
- package/src/elements/channel/declare.ts +5 -0
- package/src/elements/clock/declare.ts +5 -0
- package/src/elements/clock/durable.ts +7 -1
- package/src/elements/gate/declare.ts +28 -5
- package/src/elements/gate.ts +1 -0
- package/src/elements/signal/declare.ts +5 -0
- package/src/elements/store/declare.ts +10 -2
- package/src/elements/store/index-boot.test.ts +257 -0
- package/src/elements/store/runtime.ts +67 -9
- package/src/elements/store/schema-decl.ts +7 -0
- package/src/kernel/abort-scope.ts +116 -0
- package/src/kernel/app.ts +12 -2
- package/src/kernel/boot-bind/store.test.ts +59 -1
- package/src/kernel/boot-bind/store.ts +64 -2
- package/src/kernel/concurrency.test.ts +236 -0
- package/src/kernel/concurrency.ts +172 -0
- package/src/kernel/flow.ts +9 -0
- package/src/kernel/fx.test.ts +12 -0
- package/src/kernel/fx.ts +44 -0
- package/src/kernel/index.ts +14 -0
- package/src/kernel/journal.ts +9 -0
- package/src/kernel/plugin/capabilities.test.ts +18 -0
- package/src/kernel/plugin.ts +11 -3
- package/src/kernel/registry.ts +29 -6
- package/src/manifest/types.ts +21 -0
- package/src/release/measure.ts +4 -0
- package/src/console/ui/dist/assets/panel-channels-BOmQ-onL.js +0 -1
- package/src/console/ui/dist/assets/panel-clock-giAq0Ccv.js +0 -1
- package/src/console/ui/dist/assets/panel-gates-XclZxWD5.js +0 -1
- package/src/console/ui/dist/assets/panel-plugins-CcGM1g64.js +0 -1
- package/src/console/ui/dist/assets/panel-signals-CNywkdak.js +0 -1
- package/src/console/ui/dist/assets/panel-store-KmTbFHMH.js +0 -1
- package/src/console/ui/dist/assets/panel-vault-CEnFc0dk.js +0 -1
package/AGENTS.md
CHANGED
|
@@ -92,3 +92,7 @@ Engine: Bun `>=1.3`.
|
|
|
92
92
|
| Manifest | `manifest.v1.schema.json` |
|
|
93
93
|
|
|
94
94
|
**If the documentation is silent, stop and ask.**
|
|
95
|
+
|
|
96
|
+
## After every implementation
|
|
97
|
+
|
|
98
|
+
Before claiming work done: run [`.agents/skills/oke-ship`](.agents/skills/oke-ship/SKILL.md) — append notes to `changelog.md` under `## Unreleased` (never under a shipped `## v…` section), and update site docs via [`.agents/skills/oke-docs`](.agents/skills/oke-docs/SKILL.md) for any user-facing surface. Version bump is separate: `bun run bump` promotes Unreleased into the next `## vX.Y.Z`.
|
package/manifest.v1.schema.json
CHANGED
|
@@ -321,6 +321,10 @@
|
|
|
321
321
|
"type": "string",
|
|
322
322
|
"enum": ["once", "broadcast", "live"]
|
|
323
323
|
},
|
|
324
|
+
"description": {
|
|
325
|
+
"description": "Optional human description; consumers fall back to the signal map key.",
|
|
326
|
+
"type": "string"
|
|
327
|
+
},
|
|
324
328
|
"retries": { "type": "integer", "minimum": 0 },
|
|
325
329
|
"deadLetter": { "type": "boolean" },
|
|
326
330
|
"schema": { "$ref": "#/$defs/JsonSchema" },
|
|
@@ -354,6 +358,10 @@
|
|
|
354
358
|
]
|
|
355
359
|
},
|
|
356
360
|
"sqlName": { "type": "string", "minLength": 1 },
|
|
361
|
+
"description": {
|
|
362
|
+
"description": "Optional human description; consumers fall back to the column map key.",
|
|
363
|
+
"type": "string"
|
|
364
|
+
},
|
|
357
365
|
"pii": { "type": "boolean" },
|
|
358
366
|
"sensitive": { "type": "boolean" },
|
|
359
367
|
"retain": { "type": "string", "minLength": 1 },
|
|
@@ -408,6 +416,10 @@
|
|
|
408
416
|
"type": "string",
|
|
409
417
|
"enum": ["sql", "kv", "files", "index"]
|
|
410
418
|
},
|
|
419
|
+
"description": {
|
|
420
|
+
"description": "Optional human description; consumers fall back to the store map key.",
|
|
421
|
+
"type": "string"
|
|
422
|
+
},
|
|
411
423
|
"tables": {
|
|
412
424
|
"type": "object",
|
|
413
425
|
"propertyNames": { "type": "string", "minLength": 1 },
|
|
@@ -451,7 +463,11 @@
|
|
|
451
463
|
"cron": { "type": "string", "minLength": 1 },
|
|
452
464
|
"every": { "type": "string", "minLength": 1 },
|
|
453
465
|
"timezone": { "type": "string", "minLength": 1 },
|
|
454
|
-
"overridable": { "type": "boolean" }
|
|
466
|
+
"overridable": { "type": "boolean" },
|
|
467
|
+
"description": {
|
|
468
|
+
"description": "Optional human description; consumers fall back to the clock map key.",
|
|
469
|
+
"type": "string"
|
|
470
|
+
}
|
|
455
471
|
}
|
|
456
472
|
},
|
|
457
473
|
"Gate": {
|
|
@@ -485,6 +501,10 @@
|
|
|
485
501
|
"type": "array",
|
|
486
502
|
"items": { "type": "string", "minLength": 1 },
|
|
487
503
|
"uniqueItems": true
|
|
504
|
+
},
|
|
505
|
+
"description": {
|
|
506
|
+
"description": "Optional human description; consumers fall back to the gate map key.",
|
|
507
|
+
"type": "string"
|
|
488
508
|
}
|
|
489
509
|
}
|
|
490
510
|
},
|
|
@@ -502,6 +522,10 @@
|
|
|
502
522
|
"type": "object",
|
|
503
523
|
"additionalProperties": false,
|
|
504
524
|
"properties": {
|
|
525
|
+
"description": {
|
|
526
|
+
"description": "Optional human description; consumers fall back to the channel map key.",
|
|
527
|
+
"type": "string"
|
|
528
|
+
},
|
|
505
529
|
"medium": {
|
|
506
530
|
"type": "string",
|
|
507
531
|
"enum": ["email", "sms", "whatsapp", "push", "any"]
|
|
@@ -580,6 +604,18 @@
|
|
|
580
604
|
}
|
|
581
605
|
}
|
|
582
606
|
},
|
|
607
|
+
"PluginTable": {
|
|
608
|
+
"description": "Optional metadata for a plugin-contributed table.",
|
|
609
|
+
"type": "object",
|
|
610
|
+
"additionalProperties": false,
|
|
611
|
+
"properties": {
|
|
612
|
+
"plane": { "type": "string", "minLength": 1 },
|
|
613
|
+
"description": {
|
|
614
|
+
"description": "Optional human description; consumers fall back to the table name.",
|
|
615
|
+
"type": "string"
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
},
|
|
583
619
|
"Plugin": {
|
|
584
620
|
"type": "object",
|
|
585
621
|
"additionalProperties": false,
|
|
@@ -600,6 +636,12 @@
|
|
|
600
636
|
"type": "array",
|
|
601
637
|
"items": { "type": "string", "minLength": 1 },
|
|
602
638
|
"uniqueItems": true
|
|
639
|
+
},
|
|
640
|
+
"tables": {
|
|
641
|
+
"description": "Optional metadata for table:* contributions.",
|
|
642
|
+
"type": "object",
|
|
643
|
+
"propertyNames": { "type": "string", "minLength": 1 },
|
|
644
|
+
"additionalProperties": { "$ref": "#/$defs/PluginTable" }
|
|
603
645
|
}
|
|
604
646
|
}
|
|
605
647
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "okengine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "One law. Eight elements. Ten exports. One package. One manifest. Every backend need is derived, never added.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -59,7 +59,6 @@
|
|
|
59
59
|
"gate": "bun test src/cli/doc-staleness.test.ts src/drivers/vault-driver-removal.test.ts src/kernel/errors.registry.test.ts src/upgrade/codemods.test.ts",
|
|
60
60
|
"dev": "bun run --cwd site dev",
|
|
61
61
|
"site:build": "bun run --cwd site build",
|
|
62
|
-
"ci:workflow": "bun scripts/ci.ts --workflow",
|
|
63
62
|
"ci": "bun scripts/ci.ts",
|
|
64
63
|
"bump": "bun run scripts/bump-version.ts",
|
|
65
64
|
"release": "bun run scripts/publish.ts",
|
|
@@ -80,6 +79,9 @@
|
|
|
80
79
|
"@codemirror/state": "^6.7.1",
|
|
81
80
|
"@codemirror/theme-one-dark": "^6.1.3",
|
|
82
81
|
"@codemirror/view": "^6.43.6",
|
|
82
|
+
"@electric-sql/pglite": "^0.5.4",
|
|
83
|
+
"@electric-sql/pglite-pgvector": "^0.0.5",
|
|
84
|
+
"@libsql/client": "^0.17.4",
|
|
83
85
|
"@oxc-project/types": "^0.141.0",
|
|
84
86
|
"@playwright/test": "^1.62.0",
|
|
85
87
|
"@sinclair/typebox": "^0.34.52",
|
|
@@ -113,12 +115,24 @@
|
|
|
113
115
|
"zod": "^4.4.3"
|
|
114
116
|
},
|
|
115
117
|
"peerDependencies": {
|
|
118
|
+
"@electric-sql/pglite": ">=0.5.0",
|
|
119
|
+
"@electric-sql/pglite-pgvector": ">=0.0.5",
|
|
120
|
+
"@libsql/client": ">=0.15.0",
|
|
116
121
|
"drizzle-kit": ">=1.0.0-rc.0",
|
|
117
122
|
"drizzle-orm": ">=1.0.0-rc.0",
|
|
118
123
|
"drizzle-seed": ">=0.3.0",
|
|
119
124
|
"zod": ">=3.23.0"
|
|
120
125
|
},
|
|
121
126
|
"peerDependenciesMeta": {
|
|
127
|
+
"@electric-sql/pglite": {
|
|
128
|
+
"optional": true
|
|
129
|
+
},
|
|
130
|
+
"@electric-sql/pglite-pgvector": {
|
|
131
|
+
"optional": true
|
|
132
|
+
},
|
|
133
|
+
"@libsql/client": {
|
|
134
|
+
"optional": true
|
|
135
|
+
},
|
|
122
136
|
"drizzle-kit": {
|
|
123
137
|
"optional": true
|
|
124
138
|
},
|
|
@@ -7,13 +7,14 @@ source: "docs/spec/unified-theory.md"
|
|
|
7
7
|
|
|
8
8
|
Tools alone don't make a good operator — an agent also needs to know the _vocabulary_: what a Flow is, why `fx` is the only door, which driver ids are legal. OKE ships that knowledge as contracts and skills that agents load automatically, so sessions start aligned instead of drifting and getting corrected.
|
|
9
9
|
|
|
10
|
-
## The
|
|
10
|
+
## The four layers
|
|
11
11
|
|
|
12
12
|
| Layer | Path | Loaded when | Teaches |
|
|
13
13
|
| ------------------ | -------------------------- | ------------------------------------------------- | --------------------------------------------------------------------- |
|
|
14
14
|
| **Agent contract** | `AGENTS.md` (repo root) | Every agent session, automatically | The one law, eight elements, ten exports, the fx rule, ports, budgets |
|
|
15
15
|
| **Element skill** | `.agents/skills/oke/` | Building or changing an okengine app | The element contract in depth — declaration patterns per element |
|
|
16
16
|
| **Docs skill** | `.agents/skills/oke-docs/` | Writing or editing docs under `site/content/docs` | The documentation information-architecture standard and its gates |
|
|
17
|
+
| **Ship skill** | `.agents/skills/oke-ship/` | After any implementation, before claiming done | Changelog under the current version + docs sync via `oke-docs` |
|
|
17
18
|
|
|
18
19
|
## AGENTS.md — the root contract
|
|
19
20
|
|
|
@@ -21,14 +22,15 @@ Every OKE app's repo carries an `AGENTS.md` that agents (Cursor, Claude Code, an
|
|
|
21
22
|
|
|
22
23
|
## Skills — installable know-how
|
|
23
24
|
|
|
24
|
-
Skills are `SKILL.md` packages an agent loads when the work matches their description. The
|
|
25
|
+
Skills are `SKILL.md` packages an agent loads when the work matches their description. The three OKE ships:
|
|
25
26
|
|
|
26
|
-
| Skill | Use it for
|
|
27
|
-
| ---------- |
|
|
28
|
-
| `oke` | App work — flows, elements, drivers
|
|
29
|
-
| `oke-docs` | Docs work — new pages, rewrites
|
|
27
|
+
| Skill | Use it for | Inside |
|
|
28
|
+
| ---------- | -------------------------------------------- | ------------------------------------------------------------------------- |
|
|
29
|
+
| `oke` | App work — flows, elements, drivers | The agent contract, element patterns, the fx invariants |
|
|
30
|
+
| `oke-docs` | Docs work — new pages, rewrites | The page skeleton, verification sources, the density gate |
|
|
31
|
+
| `oke-ship` | Closing an implementation — changelog + docs | Append under `## Unreleased`; `bun run bump` promotes it into `## vX.Y.Z` |
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
All live in the repo under `.agents/skills/`, so they travel with the code and stay versioned with what they describe.
|
|
32
34
|
|
|
33
35
|
## How contracts compose with MCP
|
|
34
36
|
|
|
@@ -14,6 +14,10 @@ Answers: **secret contracts, who can read each, rotation due**
|
|
|
14
14
|
## What this panel shows
|
|
15
15
|
|
|
16
16
|
<Cards>
|
|
17
|
+
<Card
|
|
18
|
+
title="Contracts"
|
|
19
|
+
description="Optional description as the human title; technical key stays visible."
|
|
20
|
+
/>
|
|
17
21
|
<Card
|
|
18
22
|
title="Fingerprints"
|
|
19
23
|
description="Salted hash per environment — rotation and drift without exposure."
|
|
@@ -71,10 +71,11 @@ Locally the `console` driver captures mail into an inbox instead of sending; in
|
|
|
71
71
|
| `binder.template(name, opts)` | Typed template bound to that medium |
|
|
72
72
|
| `channel.template(name, opts)` | Medium-agnostic template (one body, any medium) |
|
|
73
73
|
|
|
74
|
-
| Template option | Type | Meaning
|
|
75
|
-
| --------------- | --------------------- |
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
74
|
+
| Template option | Type | Meaning |
|
|
75
|
+
| --------------- | --------------------- | ---------------------------------------------------------- |
|
|
76
|
+
| `description` | string | Human title in the Console (falls back to the template id) |
|
|
77
|
+
| `schema` | zod / Standard Schema | The data the template may reference — typed sends |
|
|
78
|
+
| `locales` | string[] | Languages this template is rendered in |
|
|
78
79
|
|
|
79
80
|
## The human physics
|
|
80
81
|
|
|
@@ -98,6 +98,7 @@ Both are triggers consumed with the same `on(trigger, flow)` — the flow undern
|
|
|
98
98
|
| `every` | string | — | Fixed interval: `"30s"` · `"10m"` · `"1h"` · `"7d"` |
|
|
99
99
|
| `timezone` | string | `"UTC"` | IANA timezone for cron evaluation |
|
|
100
100
|
| `overridable` | boolean | `false` | Allow the Console to edit / pause the schedule |
|
|
101
|
+
| `description` | string | — | Human title in the Console (falls back to the clock name) |
|
|
101
102
|
|
|
102
103
|
## Sleeping inside a flow
|
|
103
104
|
|
|
@@ -77,6 +77,7 @@ curl -X POST localhost:6530/orders -d '{"sku":"SKU-1","qty":2}' -H 'content-type
|
|
|
77
77
|
| `in` | Input contract — validated before `do` runs; bad input is a 422 |
|
|
78
78
|
| `out` | Output contract — the return value is checked against it |
|
|
79
79
|
| `errors` | Typed failures — **returned** with `fx.fail`, never thrown |
|
|
80
|
+
| `retry` | Optional whole-`do` backoff on thrown errors (same journal) |
|
|
80
81
|
| `do` | The work — every read, write, emit, and call goes through `fx` |
|
|
81
82
|
|
|
82
83
|
Failures are values, not exceptions:
|
|
@@ -195,6 +196,8 @@ Everything a flow may touch, on one object:
|
|
|
195
196
|
| `fx.clock.now()` / `.sleep(…)` | — | Injected time / durable sleep |
|
|
196
197
|
| `fx.cache.get/set` | — | Shared cache with effect-aware invalidation |
|
|
197
198
|
| `fx.step(name, fn)` | — | Named durable step — never re-runs on replay |
|
|
199
|
+
| `fx.all` / `fx.race` / `fx.retry` | — | Structured concurrency + backoff retry |
|
|
200
|
+
| `fx.signal` | — | Ambient `AbortSignal` for the current branch |
|
|
198
201
|
| `fx.id()` · `fx.log` · `fx.t` | — | UUIDs, redacting logger, i18n |
|
|
199
202
|
| `fx.auth` · `fx.operator` · `fx.tenant` | — | Who is calling (user / operator / tenant) |
|
|
200
203
|
|
|
@@ -227,6 +230,8 @@ export const chargeOrder = flow({
|
|
|
227
230
|
|
|
228
231
|
**Consequence:** kill the process between the two steps and the run **resumes at `confirm`** — completed steps replay from the journal, so the card is not charged twice. This is verified by the engine's own test suite: after resume, `create-intent` has run exactly once.
|
|
229
232
|
|
|
233
|
+
For flaky sub-steps, wrap the work in `fx.retry` **inside** `fx.step` so a completed charge is never retried on resume. Coarse whole-body retry is also available as `flow({ retry: { retries, delay, backoff, jitter } })` and reuses the same journal session.
|
|
234
|
+
|
|
230
235
|
## Composition is just calls
|
|
231
236
|
|
|
232
237
|
Wiring is values flowing between flows — declared in code, never configured in a dashboard:
|
|
@@ -251,6 +256,16 @@ That bypasses the effect graph: the Manifest can't see the call, cache keys miss
|
|
|
251
256
|
|
|
252
257
|
`fx.fail(code, data)` for expected outcomes — anything in `errors`. A throw is a crash: the run fails with a 500-shaped error and no typed code for the client. Expected failures are values so clients can switch on `error.code`.
|
|
253
258
|
|
|
259
|
+
</Accordion>
|
|
260
|
+
<Accordion title="Promise.all left orphan work running after one branch failed">
|
|
261
|
+
|
|
262
|
+
Use `fx.all` (or `fx.race`) with thunks. The first rejection aborts siblings through `fx.signal`. Bare `Promise.all` / `Promise.race` do not cancel losers.
|
|
263
|
+
|
|
264
|
+
</Accordion>
|
|
265
|
+
<Accordion title="Durable flow retried a completed charge">
|
|
266
|
+
|
|
267
|
+
Put `fx.retry` **inside** `fx.step`, not around it. Completed steps replay from the journal and never re-run. `flow({ retry })` is for coarse whole-body retries on the same session.
|
|
268
|
+
|
|
254
269
|
</Accordion>
|
|
255
270
|
<Accordion title="How do I share logic between flows — a private function?">
|
|
256
271
|
|
|
@@ -74,10 +74,14 @@ A denied request never reaches `do`. It returns one of three typed failures, lik
|
|
|
74
74
|
|
|
75
75
|
## Two kinds of gates
|
|
76
76
|
|
|
77
|
-
| Declaration
|
|
78
|
-
|
|
|
79
|
-
| `gate.policy(name, check)` | Is this principal allowed? (ABAC) | auth / operator / request metadata |
|
|
80
|
-
| `gate.rate(options)`
|
|
77
|
+
| Declaration | Question it answers | Evaluated against |
|
|
78
|
+
| ------------------------------------- | -------------------------------------- | ---------------------------------- |
|
|
79
|
+
| `gate.policy(name, check \| options)` | Is this principal allowed? (ABAC) | auth / operator / request metadata |
|
|
80
|
+
| `gate.rate(options)` | Is there budget left for this subject? | an atomic counter on the kv driver |
|
|
81
|
+
|
|
82
|
+
`gate.policy(name, check)` stays the terse form. Pass
|
|
83
|
+
`{ check, description }` when you want a human title in the Console
|
|
84
|
+
(falls back to the policy name).
|
|
81
85
|
|
|
82
86
|
### The policy context
|
|
83
87
|
|
|
@@ -97,13 +101,14 @@ A policy name containing `:` is also a `Module:Action` permission — the same d
|
|
|
97
101
|
|
|
98
102
|
### Rate options
|
|
99
103
|
|
|
100
|
-
| Option | Type | Default | Meaning
|
|
101
|
-
| ------------- | ------------ | -------------------------- |
|
|
102
|
-
| `max` | number | — (**required**) | Takes allowed within `per`
|
|
103
|
-
| `per` | string | — (**required**) | Window / refill period (`"1m"`, `"60s"`, …)
|
|
104
|
-
| `keyBy` | string | — | Subject dimension (`"ip"`, `"user"`, …)
|
|
105
|
-
| `strategy` | RateStrategy | `"sliding-window-counter"` | Algorithm (below)
|
|
106
|
-
| `overridable` | boolean | `false` | Allow the Console to retune `max`/`per` live
|
|
104
|
+
| Option | Type | Default | Meaning |
|
|
105
|
+
| ------------- | ------------ | -------------------------- | -------------------------------------------------------- |
|
|
106
|
+
| `max` | number | — (**required**) | Takes allowed within `per` |
|
|
107
|
+
| `per` | string | — (**required**) | Window / refill period (`"1m"`, `"60s"`, …) |
|
|
108
|
+
| `keyBy` | string | — | Subject dimension (`"ip"`, `"user"`, …) |
|
|
109
|
+
| `strategy` | RateStrategy | `"sliding-window-counter"` | Algorithm (below) |
|
|
110
|
+
| `overridable` | boolean | `false` | Allow the Console to retune `max`/`per` live |
|
|
111
|
+
| `description` | string | — | Human title in the Console (falls back to the rate name) |
|
|
107
112
|
|
|
108
113
|
| Strategy | Behavior |
|
|
109
114
|
| ------------------------ | ------------------------------------------------------ |
|
|
@@ -86,13 +86,14 @@ The declaration is identical in shape for all three — switching physics later
|
|
|
86
86
|
|
|
87
87
|
### Options
|
|
88
88
|
|
|
89
|
-
| Option
|
|
90
|
-
|
|
|
91
|
-
| `delivery`
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
89
|
+
| Option | Type | Default | Meaning |
|
|
90
|
+
| ------------- | --------------------------------- | ---------------- | -------------------------------------------------------------- |
|
|
91
|
+
| `delivery` | `"once" \| "broadcast" \| "live"` | — (**required**) | Delivery physics |
|
|
92
|
+
| `description` | string | — | Human title in the Console (falls back to the signal name) |
|
|
93
|
+
| `schema` | zod / Standard Schema | — | Payload contract; typed emits and Manifest docs |
|
|
94
|
+
| `retries` | number | `3` | Max delivery attempts before dead-letter (`once`) |
|
|
95
|
+
| `deadLetter` | boolean | `true` | Preserve exhausted messages in the DLQ (`once`) |
|
|
96
|
+
| `optional` | boolean | `false` | Allow emitting while nobody subscribes (skip the orphan check) |
|
|
96
97
|
|
|
97
98
|
## When delivery fails
|
|
98
99
|
|
|
@@ -90,17 +90,23 @@ export const createNote = on(
|
|
|
90
90
|
|
|
91
91
|
Pick the facet that matches the physics of your data. Each declaration is one line; the runtime handle shows what flows can do with it.
|
|
92
92
|
|
|
93
|
-
| Facet
|
|
94
|
-
|
|
|
95
|
-
| `store.sql(name, opts)`
|
|
96
|
-
| `store.kv(name)`
|
|
97
|
-
| `store.files(name)`
|
|
98
|
-
| `store.index(name, opts)`
|
|
93
|
+
| Facet | Declares | Best for | `fx.store(…)` handle |
|
|
94
|
+
| -------------------------- | ----------------- | ------------------------------------- | ------------------------------------------------------ |
|
|
95
|
+
| `store.sql(name, opts)` | a SQL database | domain tables, relations, constraints | `select` · `insert` · `update` · `delete` · `findById` |
|
|
96
|
+
| `store.kv(name, opts?)` | a key-value space | cache, sessions, rate limits | `get` · `set(key, value, ttl?)` · `delete` · `list` |
|
|
97
|
+
| `store.files(name, opts?)` | a blob bucket | uploads, exports, attachments | `put` · `get` · `delete` · `list(prefix?)` |
|
|
98
|
+
| `store.index(name, opts)` | a vector index | semantic search / RAG (`dims`) | `upsert` · `search(vector, topK?)` · `delete` |
|
|
99
|
+
|
|
100
|
+
Every facet accepts optional `description` — a human title in the Console
|
|
101
|
+
(falls back to the store name). `store.index` also takes `dims`.
|
|
99
102
|
|
|
100
103
|
```typescript
|
|
101
|
-
export const cache = store.kv("sessions");
|
|
104
|
+
export const cache = store.kv("sessions", { description: "Session cache" });
|
|
102
105
|
export const uploads = store.files("attachments");
|
|
103
|
-
export const embeddings = store.index("docs", {
|
|
106
|
+
export const embeddings = store.index("docs", {
|
|
107
|
+
dims: 1536,
|
|
108
|
+
description: "Document embeddings",
|
|
109
|
+
});
|
|
104
110
|
```
|
|
105
111
|
|
|
106
112
|
## CRUD without boilerplate — `store.resource`
|
|
@@ -175,6 +181,7 @@ The recommended path: declare tables ORM-agnostically, then let `oke db` emit re
|
|
|
175
181
|
| `.default(v)` · `.defaultFn(id \| now)` | defaults |
|
|
176
182
|
| `.pii()` · `.sensitive()` · `.retain("30d")` | privacy classification |
|
|
177
183
|
| `.as("sql_name")` | override the automatic `camelCase → snake_case` |
|
|
184
|
+
| `.describe("…")` | human title in the Console (falls back to key) |
|
|
178
185
|
| `.references(() => col, { onDelete })` | foreign key |
|
|
179
186
|
|
|
180
187
|
### Foreign keys and relations
|
|
@@ -242,6 +249,39 @@ drivers: {
|
|
|
242
249
|
|
|
243
250
|
Container images come from the `images` map — change the vendor by changing the pin, never the driver id.
|
|
244
251
|
|
|
252
|
+
### Opt-in SQL drivers
|
|
253
|
+
|
|
254
|
+
Two more `store.sql` drivers cover specific parity needs. Both are optional peers — `bun add` them yourself; neither changes the `sqlite` local default.
|
|
255
|
+
|
|
256
|
+
| Driver | Install | Wire dialect | Choose for |
|
|
257
|
+
| -------- | -------------------------------------------------------- | ------------ | ------------------------------------------------------ |
|
|
258
|
+
| `libsql` | `@libsql/client` | sqlite | local vector search without Docker (`index: "libsql"`) |
|
|
259
|
+
| `pglite` | `@electric-sql/pglite` + `@electric-sql/pglite-pgvector` | postgresql | the real Postgres dialect + pgvector, in-process |
|
|
260
|
+
|
|
261
|
+
<Callout title="PGlite is not a latency play">
|
|
262
|
+
PGlite pays a one-time ~1 s WASM init, and warm CRUD is roughly 15× slower than `bun:sqlite`.
|
|
263
|
+
Choose it for dialect/pgvector parity with prod — never as a faster local default.
|
|
264
|
+
</Callout>
|
|
265
|
+
|
|
266
|
+
A SQL-backed index shares the sql facet's already-open connection, so configure the pair together:
|
|
267
|
+
|
|
268
|
+
```typescript title="oke.config.ts"
|
|
269
|
+
drivers: {
|
|
270
|
+
store: {
|
|
271
|
+
sql: { local: "libsql" },
|
|
272
|
+
index: { local: "libsql" },
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
| `store.index` id | Shares connection from | Real ANN via |
|
|
278
|
+
| ---------------- | -------------------------- | ------------------------------------ |
|
|
279
|
+
| `memory` | — (in-process, default) | — |
|
|
280
|
+
| `pgvector` | `postgres` or `pglite` sql | HNSW + `cosineDistance` |
|
|
281
|
+
| `libsql` | `libsql` sql | `libsql_vector_idx` + `vector_top_k` |
|
|
282
|
+
|
|
283
|
+
**Consequence:** a configured SQL-backed index that cannot reach its engine — missing peer, missing `vector` extension, wrong sql driver — fails loudly at first use. It never silently falls back to `memory`.
|
|
284
|
+
|
|
245
285
|
## Privacy built in
|
|
246
286
|
|
|
247
287
|
Columns tagged `.pii()` or `.sensitive()` are masked at the store boundary — flows, logs, and the Console see a mask, not the value. Revealing cleartext PII requires an explicit `pii:reveal` gate on the flow, so access is a permission, not a convention.
|
|
@@ -91,7 +91,7 @@ That's the whole loop: declare → set → read. Everything below is what OKE gu
|
|
|
91
91
|
|
|
92
92
|
| Option | Type | Meaning |
|
|
93
93
|
| ------------- | --------------------- | ------------------------------------------------------------------------ |
|
|
94
|
-
| `description` | string |
|
|
94
|
+
| `description` | string | Human title in boot-gap listings and the Console (falls back to the key) |
|
|
95
95
|
| `rotate` | string | Rotation hint (`"90d"`) — drives the rotation-due signal in the Console |
|
|
96
96
|
| `schema` | zod / Standard Schema | Validated at boot; a bad value fails boot like a missing one |
|
|
97
97
|
| `dev` | string | Local-only fallback when no source provides a value (never used in prod) |
|
|
@@ -46,8 +46,8 @@ A client whose IP is not on the list receives `403` with a typed denial:
|
|
|
46
46
|
<Callout type="error">
|
|
47
47
|
Standard reverse proxies **append** to `X-Forwarded-For` — left-side hops are attacker-controlled.
|
|
48
48
|
The plugin trusts the hop `trustedProxyDepth` from the **right** (default `1` = last hop). Set
|
|
49
|
-
this to your real proxy count
|
|
50
|
-
drop-in.
|
|
49
|
+
this to your real proxy count — wrong depth bypasses the allowlist (topology-dependent, not
|
|
50
|
+
drop-in).
|
|
51
51
|
</Callout>
|
|
52
52
|
|
|
53
53
|
## Notes
|
|
@@ -36,10 +36,10 @@ drivers: {
|
|
|
36
36
|
|
|
37
37
|
| Key | Shape | Driver ids (verified) |
|
|
38
38
|
| ------------------ | -------------- | ---------------------------------------------------------------------------- |
|
|
39
|
-
| `store.sql` | env driver map | `sqlite` · `postgres` · `memory`
|
|
39
|
+
| `store.sql` | env driver map | `sqlite` · `postgres` · `libsql` · `pglite` · `memory` |
|
|
40
40
|
| `store.kv` | env driver map | `memory` · `redis` |
|
|
41
41
|
| `store.files` | env driver map | `memory` · `fs` · `s3` |
|
|
42
|
-
| `store.index` | env driver map | `memory` · `pgvector`
|
|
42
|
+
| `store.index` | env driver map | `memory` · `pgvector` · `libsql` |
|
|
43
43
|
| `signal` | env driver map | `memory` · `postgres` · `redis` · `nats` |
|
|
44
44
|
| `clock` | env driver map | `memory` · `postgres` · `frozen` |
|
|
45
45
|
| `vault` | env driver map | `dotenv` · `openbao` · `memory` |
|
|
@@ -18,8 +18,16 @@ OKE reads environment variables at boot for connection detail and secrets — ne
|
|
|
18
18
|
| `DATABASE_URL` | Postgres connection (`postgres` driver, drizzle-kit) | `postgres://localhost:5432/oke` |
|
|
19
19
|
| `OKE_STORE_SQL_URL` | Explicit SQL URL override | — |
|
|
20
20
|
| `OKE_SQLITE_URL` | SQLite file path (`sqlite` driver) | `.oke/app.sqlite` |
|
|
21
|
+
| `OKE_LIBSQL_URL` | libSQL URL or file path (`libsql` driver) | `.oke/app.libsql` |
|
|
22
|
+
| `OKE_PGLITE_URL` | PGlite data dir or `memory://` (`pglite` driver) | `.oke/pgdata` |
|
|
21
23
|
| `OKE_SQL_DRIVER` | Force the sql driver id at boot | config map |
|
|
22
24
|
|
|
25
|
+
## Index store
|
|
26
|
+
|
|
27
|
+
| Variable | Used for | Default when unset |
|
|
28
|
+
| ------------------ | ------------------------- | ------------------ |
|
|
29
|
+
| `OKE_INDEX_DRIVER` | Force the index driver id | config map |
|
|
30
|
+
|
|
23
31
|
## KV store
|
|
24
32
|
|
|
25
33
|
| Variable | Used for | Default when unset |
|
|
@@ -41,8 +41,48 @@ See [Store](/docs/elements/store) for the query-builder surface.
|
|
|
41
41
|
| ---------------------------- | ------- | ----------------------------------------------------- |
|
|
42
42
|
| `fx.call(flow, input?)` | `call` | The callee's `out` — runs through the same pipeline |
|
|
43
43
|
| `fx.step(name, fn)` | — | Durable step: replays from the journal, never re-runs |
|
|
44
|
+
| `fx.all([...thunks])` | — | Parallel; first rejection aborts siblings |
|
|
45
|
+
| `fx.race([...thunks])` | — | First settle wins; losers aborted |
|
|
46
|
+
| `fx.retry(fn, opts?)` | — | Exponential backoff + jitter (plain Promise) |
|
|
47
|
+
| `fx.signal` | — | Ambient `AbortSignal` for the current branch |
|
|
44
48
|
| `fx.fail(code, data, opts?)` | — | Typed failure value (`opts.message` overrides) |
|
|
45
49
|
|
|
50
|
+
## Concurrency and retry
|
|
51
|
+
|
|
52
|
+
Pass **thunks** to `all` / `race` — not already-started Promises — so each branch gets an abort scope before work begins.
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
const [user, stock] = await fx.all([
|
|
56
|
+
() => fx.store(db).findById(users, input.userId),
|
|
57
|
+
() => fx.store(db).findById(inventory, input.sku),
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const charge = await fx.step("charge", () =>
|
|
61
|
+
fx.retry(() => fx.call(stripeCharge, { amount: input.total }), {
|
|
62
|
+
retries: 3,
|
|
63
|
+
delay: "100ms",
|
|
64
|
+
backoff: 2,
|
|
65
|
+
jitter: true,
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
| `fx.retry` option | Default | Meaning |
|
|
71
|
+
| ----------------- | ------- | -------------------------------------------- |
|
|
72
|
+
| `retries` | `0` | Extra attempts after the first |
|
|
73
|
+
| `delay` | `50` | Initial backoff — ms number or `"100ms"` |
|
|
74
|
+
| `backoff` | `2` | Multiplier after each retry |
|
|
75
|
+
| `jitter` | `true` | Full jitter on the delay (thundering-herd) |
|
|
76
|
+
| `when` | thrown | Predicate; skips `AbortError` and sleep park |
|
|
77
|
+
|
|
78
|
+
<Callout title="Cooperative cancel">
|
|
79
|
+
Losing branches see `fx.signal` abort. Drivers that do not yet honor the signal may still finish
|
|
80
|
+
in the background — check `fx.signal.aborted` in long user work, and prefer `fx.all` over bare
|
|
81
|
+
`Promise.all`.
|
|
82
|
+
</Callout>
|
|
83
|
+
|
|
84
|
+
**Consequence:** put `fx.retry` inside `fx.step` on durable flows so a completed charge never re-runs on resume. Coarse whole-body retry is `flow({ retry: { … } })` on the same journal session.
|
|
85
|
+
|
|
46
86
|
## Channel
|
|
47
87
|
|
|
48
88
|
| Signature | Records | Notes |
|
|
@@ -64,22 +64,22 @@ Plugin flows appear in the Manifest, plugin tables land in `schema.generated.ts`
|
|
|
64
64
|
|
|
65
65
|
Every method below exists on both the fluent definition and the boot-time builder the registry records:
|
|
66
66
|
|
|
67
|
-
| Method | Contributes
|
|
68
|
-
| -------------------------------- |
|
|
69
|
-
| `.flow(def)` | An ordinary flow — Manifest, Console, client types
|
|
70
|
-
| `.hook(stage, fn)` | A per-request intercept at one pipeline stage
|
|
71
|
-
| `.edge(fn)` | A handler for HTTP requests that match **no** flow
|
|
72
|
-
| `.decorate(key, value)` | A typed context decoration, visible to flows
|
|
73
|
-
| `.element({ kind, name })` | An element contribution (e.g. `store.sql` facet)
|
|
74
|
-
| `.driver(id, impl)` | A protocol-named driver for an existing element
|
|
75
|
-
| `.image(role, recipe)` | An image recipe for a docker role
|
|
76
|
-
| `.table(name, columns, options)` | A whole DB table, merged into the generated schema
|
|
77
|
-
| `.errors(map)` | Typed errors flows can fail with
|
|
78
|
-
| `.client(name, ext)` | A typed client extension
|
|
79
|
-
| `.consolePanel(panel)` | A Console panel (ESM entry loaded at runtime)
|
|
80
|
-
| `.cli(name, handler)` | An `oke <name>` CLI command
|
|
81
|
-
| `.config(schema)` | A config schema; values live on the plugin identity
|
|
82
|
-
| `.needs(dep)` | A declared dependency (e.g. `"store.kv"`)
|
|
67
|
+
| Method | Contributes |
|
|
68
|
+
| -------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
69
|
+
| `.flow(def)` | An ordinary flow — Manifest, Console, client types |
|
|
70
|
+
| `.hook(stage, fn)` | A per-request intercept at one pipeline stage |
|
|
71
|
+
| `.edge(fn)` | A handler for HTTP requests that match **no** flow |
|
|
72
|
+
| `.decorate(key, value)` | A typed context decoration, visible to flows |
|
|
73
|
+
| `.element({ kind, name })` | An element contribution (e.g. `store.sql` facet) |
|
|
74
|
+
| `.driver(id, impl)` | A protocol-named driver for an existing element |
|
|
75
|
+
| `.image(role, recipe)` | An image recipe for a docker role |
|
|
76
|
+
| `.table(name, columns, options)` | A whole DB table, merged into the generated schema (`options.description` / `plane` optional) |
|
|
77
|
+
| `.errors(map)` | Typed errors flows can fail with |
|
|
78
|
+
| `.client(name, ext)` | A typed client extension |
|
|
79
|
+
| `.consolePanel(panel)` | A Console panel (ESM entry loaded at runtime) |
|
|
80
|
+
| `.cli(name, handler)` | An `oke <name>` CLI command |
|
|
81
|
+
| `.config(schema)` | A config schema; values live on the plugin identity |
|
|
82
|
+
| `.needs(dep)` | A declared dependency (e.g. `"store.kv"`) |
|
|
83
83
|
|
|
84
84
|
New infrastructure is a **driver** for an existing element, never a ninth element — plugins follow the same law.
|
|
85
85
|
|
|
@@ -194,11 +194,11 @@ A plugin may declare **its own tables** with `field.*` columns, merged into the
|
|
|
194
194
|
plugin("billing", { version: "2.1.0" }).table(
|
|
195
195
|
"invoices",
|
|
196
196
|
{ id: field.text().primaryKey().defaultFn(id) },
|
|
197
|
-
{ plane: "user" },
|
|
197
|
+
{ plane: "user", description: "Customer invoices" },
|
|
198
198
|
);
|
|
199
199
|
```
|
|
200
200
|
|
|
201
|
-
Extending an existing **app-owned** table with plugin columns is not supported in v1 — contribute a separate table and reference the app's by key. The optional `plane` metadata (`"operator" | "user" | "shared"`) keeps data-plane isolation intact for privacy tooling.
|
|
201
|
+
Extending an existing **app-owned** table with plugin columns is not supported in v1 — contribute a separate table and reference the app's by key. The optional `plane` metadata (`"operator" | "user" | "shared"`) keeps data-plane isolation intact for privacy tooling. Optional `description` is a human title in the Console (falls back to the table name).
|
|
202
202
|
|
|
203
203
|
## Identity, config, and dependencies
|
|
204
204
|
|