okengine 0.22.0 → 0.23.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.
Files changed (262) hide show
  1. package/AGENTS.md +7 -0
  2. package/manifest.v1.schema.json +44 -0
  3. package/package.json +32 -19
  4. package/site/content/docs/ai/mcp.mdx +13 -9
  5. package/site/content/docs/ai/skills.mdx +1 -1
  6. package/site/content/docs/elements/ai/agents.mdx +61 -13
  7. package/site/content/docs/elements/ai/decide.mdx +536 -0
  8. package/site/content/docs/elements/ai/events.mdx +373 -0
  9. package/site/content/docs/elements/ai/index.mdx +9 -3
  10. package/site/content/docs/elements/ai/meta.json +1 -1
  11. package/site/content/docs/elements/ai/prompts.mdx +4 -2
  12. package/site/content/docs/elements/gate/auth.mdx +4 -1
  13. package/site/content/docs/elements/store/index.mdx +1 -1
  14. package/site/content/docs/elements/store/kv.mdx +5 -5
  15. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  16. package/site/content/docs/recipes/index.mdx +6 -6
  17. package/site/content/docs/recipes/postgres.mdx +12 -8
  18. package/site/content/docs/recipes/redis.mdx +4 -4
  19. package/site/content/docs/reference/cli.mdx +2 -1
  20. package/site/content/docs/reference/configuration.mdx +9 -7
  21. package/site/content/docs/reference/fx.mdx +7 -6
  22. package/src/auth/api-key-sql.test.ts +44 -0
  23. package/src/auth/api-key-sql.ts +1 -0
  24. package/src/auth/api-keys.ts +30 -12
  25. package/src/auth/config.ts +6 -2
  26. package/src/auth/gate-auth.test.ts +20 -0
  27. package/src/cli/agents-md.test.ts +25 -0
  28. package/src/cli/ask-vault-gaps.test.ts +38 -1
  29. package/src/cli/ask-vault-gaps.ts +35 -2
  30. package/src/cli/decide.ts +174 -0
  31. package/src/cli/decision-lock-watch.test.ts +45 -0
  32. package/src/cli/decision-lock-watch.ts +51 -0
  33. package/src/cli/dev-app-runner.ts +34 -8
  34. package/src/cli/dev-auth-secret.test.ts +49 -0
  35. package/src/cli/dev-auth-secret.ts +49 -0
  36. package/src/cli/dev-hot.test.ts +41 -0
  37. package/src/cli/dev-hot.ts +48 -0
  38. package/src/cli/dev.test.ts +1 -1
  39. package/src/cli/dev.ts +24 -6
  40. package/src/cli/eval.ts +142 -1
  41. package/src/cli/index.ts +5 -0
  42. package/src/cli/load-config.images.test.ts +2 -2
  43. package/src/cli/load-config.ts +1 -1
  44. package/src/cli/mcp-from-console.ts +48 -0
  45. package/src/cli/registry.ts +35 -1
  46. package/src/client/agent.ts +307 -0
  47. package/src/client/transport.test.ts +20 -0
  48. package/src/client/transport.ts +2 -1
  49. package/src/client/types.ts +1 -1
  50. package/src/client-react/index.ts +7 -1
  51. package/src/client-react/use-agent-run.test.ts +271 -0
  52. package/src/client-react/use-agent-run.ts +193 -0
  53. package/src/compiler/ai-approval.test.ts +36 -0
  54. package/src/compiler/ai-repair.test.ts +22 -0
  55. package/src/compiler/decisions.extract.test.ts +164 -0
  56. package/src/compiler/effects-infer.ts +27 -1
  57. package/src/compiler/extract.ts +268 -12
  58. package/src/compiler/response.ts +35 -2
  59. package/src/config/index.ts +21 -3
  60. package/src/console/server/ai-runs-flows.ts +269 -0
  61. package/src/console/server/ai-runs.test.ts +323 -0
  62. package/src/console/server/ai-runs.ts +562 -0
  63. package/src/console/server/ai.ts +12 -1
  64. package/src/console/server/app.ts +10 -1
  65. package/src/console/server/decisions-flows.ts +157 -0
  66. package/src/console/server/decisions.test.ts +66 -0
  67. package/src/console/server/decisions.ts +229 -0
  68. package/src/console/server/flows.ts +50 -2
  69. package/src/console/server/invoke-user-flow.ts +9 -0
  70. package/src/console/server/runs-ingest.ts +16 -0
  71. package/src/console/server/serve.ts +5 -0
  72. package/src/console/server/state.ts +25 -1
  73. package/src/console/server/store-stats.test.ts +47 -0
  74. package/src/console/server/store-stats.ts +92 -18
  75. package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
  76. package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-62Xts8Xi.js} +1 -1
  77. package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-DyeoKHKA.js} +1 -1
  78. package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DvfpLXsU.js} +1 -1
  79. package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-BY03SeCg.js} +1 -1
  80. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  81. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  82. package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
  83. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  84. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  85. package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
  86. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  87. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  88. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  89. package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
  90. package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-D7nNzpgB.js} +1 -1
  91. package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-DdL19zzh.js} +1 -1
  92. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  93. package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
  94. package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
  95. package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bb3FWUu9.js} +7 -7
  96. package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
  97. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  98. package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
  99. package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
  100. package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
  101. package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
  102. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  103. package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
  104. package/src/console/ui-next/dist/index.html +4 -3
  105. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  106. package/src/console/ui-next/src/client.ts +224 -1
  107. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  108. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  109. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  110. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  111. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  112. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  113. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  114. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  115. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  116. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  117. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  118. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  119. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  120. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  121. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  122. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  123. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  124. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  125. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  126. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  127. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  128. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  129. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  130. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  131. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  132. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  133. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  134. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  135. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  136. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  137. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
  138. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  139. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  140. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  141. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  142. package/src/console/ui-next/src/router.tsx +17 -0
  143. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  144. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  145. package/src/console/ui-next/vite.config.ts +9 -0
  146. package/src/docker/recipes/dragonfly.ts +3 -2
  147. package/src/docker/recipes/redis.ts +1 -1
  148. package/src/drivers/ai-anthropic.ts +212 -21
  149. package/src/drivers/ai-mock.ts +34 -1
  150. package/src/drivers/ai-openai-compatible.ts +67 -26
  151. package/src/drivers/ai-preconnect.ts +18 -0
  152. package/src/drivers/ai-stream.test.ts +62 -0
  153. package/src/drivers/ai-types.ts +20 -0
  154. package/src/drivers/journal-postgres.ts +289 -2
  155. package/src/drivers/memory-ddl.test.ts +18 -0
  156. package/src/drivers/memory.ts +21 -1
  157. package/src/drivers/pg-rls.test.ts +31 -0
  158. package/src/drivers/pg-rls.ts +15 -0
  159. package/src/drivers/postgres.test.ts +9 -0
  160. package/src/drivers/postgres.ts +49 -11
  161. package/src/elements/ai/agent-event-slot.ts +27 -0
  162. package/src/elements/ai/approval-http.ts +207 -0
  163. package/src/elements/ai/approval.test.ts +741 -0
  164. package/src/elements/ai/approval.ts +228 -0
  165. package/src/elements/ai/decisions/bind.ts +163 -0
  166. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  167. package/src/elements/ai/decisions/certificate.ts +377 -0
  168. package/src/elements/ai/decisions/certify.ts +314 -0
  169. package/src/elements/ai/decisions/decide.test.ts +953 -0
  170. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  171. package/src/elements/ai/decisions/export.test.ts +120 -0
  172. package/src/elements/ai/decisions/export.ts +146 -0
  173. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  174. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  175. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  176. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  177. package/src/elements/ai/decisions/http.test.ts +154 -0
  178. package/src/elements/ai/decisions/http.ts +240 -0
  179. package/src/elements/ai/decisions/labels.test.ts +282 -0
  180. package/src/elements/ai/decisions/labels.ts +257 -0
  181. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  182. package/src/elements/ai/decisions/openrouter.ts +36 -0
  183. package/src/elements/ai/decisions/provider.ts +102 -0
  184. package/src/elements/ai/decisions/typesafe.ts +36 -0
  185. package/src/elements/ai/declare.ts +252 -1
  186. package/src/elements/ai/events.test.ts +379 -0
  187. package/src/elements/ai/events.ts +138 -0
  188. package/src/elements/ai/run-events.test.ts +461 -0
  189. package/src/elements/ai/run-events.ts +522 -0
  190. package/src/elements/ai/runtime.ts +1186 -105
  191. package/src/elements/ai/stream-turn.ts +214 -0
  192. package/src/elements/ai/stream.live.test.ts +76 -0
  193. package/src/elements/ai/subagent.test.ts +136 -0
  194. package/src/elements/ai.test.ts +497 -3
  195. package/src/elements/ai.ts +16 -1
  196. package/src/elements/clock/durable.ts +27 -1
  197. package/src/elements/store/sql-session.test.ts +51 -0
  198. package/src/elements/store/sql-session.ts +111 -1
  199. package/src/i18n/catalogs/ar.ts +5 -0
  200. package/src/i18n/catalogs/en.ts +5 -0
  201. package/src/index.ts +2 -0
  202. package/src/kernel/agent-event-store.ts +448 -0
  203. package/src/kernel/app.ts +103 -1
  204. package/src/kernel/boot-bind/ai.ts +3 -0
  205. package/src/kernel/boot-bind/store.test.ts +26 -0
  206. package/src/kernel/boot-bind/store.ts +82 -6
  207. package/src/kernel/boot.ts +15 -4
  208. package/src/kernel/builtin-errors.ts +2 -0
  209. package/src/kernel/capability.ts +3 -0
  210. package/src/kernel/decision-label-store.ts +441 -0
  211. package/src/kernel/effects.test.ts +2 -1
  212. package/src/kernel/effects.ts +13 -1
  213. package/src/kernel/element-registries.ts +3 -0
  214. package/src/kernel/errors-text.ts +4 -0
  215. package/src/kernel/errors.ts +2 -0
  216. package/src/kernel/flow.test.ts +2 -3
  217. package/src/kernel/fx-decide.ts +761 -0
  218. package/src/kernel/fx.test.ts +12 -5
  219. package/src/kernel/fx.ts +312 -29
  220. package/src/kernel/http-frame.test.ts +94 -0
  221. package/src/kernel/http-frame.ts +203 -0
  222. package/src/kernel/journal.ts +104 -0
  223. package/src/kernel/json-result.ts +10 -0
  224. package/src/kernel/pipeline.test.ts +45 -0
  225. package/src/kernel/sse-id.ts +32 -0
  226. package/src/manifest/diff.ts +1 -0
  227. package/src/manifest/types.ts +29 -2
  228. package/src/mcp/ai-tools.test.ts +202 -0
  229. package/src/mcp/authorization.ts +66 -0
  230. package/src/mcp/session.ts +3 -0
  231. package/src/mcp/tools.ts +168 -0
  232. package/src/release/http-graph.test.ts +2 -0
  233. package/src/release/http-graph.ts +5 -0
  234. package/src/release/measure.ts +39 -14
  235. package/src/runs/collect.ts +4 -1
  236. package/src/runs/parquet.test.ts +27 -1
  237. package/src/runs/parquet.ts +19 -0
  238. package/src/runs/types.ts +31 -0
  239. package/src/runtime/json-code-block.test.ts +106 -47
  240. package/src/runtime/json-code-block.ts +1209 -46
  241. package/src/term.test.ts +13 -0
  242. package/src/term.ts +81 -49
  243. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  244. package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
  245. package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
  246. package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
  247. package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
  248. package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
  249. package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
  250. package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
  251. package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
  252. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  253. package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
  254. package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
  255. package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
  256. package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
  257. package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
  258. package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
  259. package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
  260. package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
  261. package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
  262. package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Read-only AI and decision MCP tools.
3
+ *
4
+ * Same authorization shape as `oke.traces.*`: one of the declared scopes,
5
+ * no confirmation, and no resolve or promote tool.
6
+ */
7
+
8
+ import { describe, expect, test } from "bun:test";
9
+ import { createSessionStore } from "../auth/sessions.ts";
10
+ import { Redacted } from "../kernel/redacted.ts";
11
+ import type { Manifest } from "../manifest/types.ts";
12
+ import { authorizeToolCall } from "./authorization.ts";
13
+ import { mintMcpSession, authenticateMcpRequest } from "./session.ts";
14
+ import { createToolRuntime, type McpContext } from "./tools.ts";
15
+
16
+ const SECRET = "mcp-ai-secret";
17
+
18
+ const runA = {
19
+ id: "run-a",
20
+ agent: "support",
21
+ tenant: "acme",
22
+ status: "interrupted",
23
+ };
24
+ const runB = {
25
+ id: "run-b",
26
+ agent: "support",
27
+ tenant: "other",
28
+ status: "finished",
29
+ };
30
+
31
+ describe("MCP AI and decision tools", () => {
32
+ test("traces scope cannot read agent runs, and there is no resolve tool", () => {
33
+ expect(
34
+ authorizeToolCall("oke.ai.runs.list", {}, ["mcp:traces:read"], { confirmed: true }).ok,
35
+ ).toBe(false);
36
+ const resolve = authorizeToolCall("oke.ai.approvals.resolve", {}, ["console:*"]);
37
+ expect(resolve.ok).toBe(false);
38
+ if (resolve.ok) return;
39
+ expect(resolve.reason).toBe("unknown-tool");
40
+ const promote = authorizeToolCall("oke.decisions.promote", {}, ["mcp:*"]);
41
+ expect(promote.ok).toBe(false);
42
+ if (promote.ok) return;
43
+ expect(promote.reason).toBe("unknown-tool");
44
+ expect(
45
+ authorizeToolCall("oke.ai.runs.list", {}, ["console:runs:read"], { confirmed: true }).ok,
46
+ ).toBe(true);
47
+ });
48
+
49
+ test("a tenant token sees only its tenant; an operator token sees every tenant", async () => {
50
+ const runtime = createToolRuntime({
51
+ getManifest: () => ({ oke: "1.0", app: "skyport" }) as Manifest,
52
+ listRuns: async () => [],
53
+ listAgentRuns: async () => [runA, runB],
54
+ getAgentRun: async (runId) => {
55
+ const run = [runA, runB].find((row) => row.id === runId);
56
+ if (!run) return undefined;
57
+ return { run, events: [{ seq: 1, event: { type: "RUN_FINISHED" } }] };
58
+ },
59
+ listApprovals: async () => [
60
+ { id: "a", tenant: "acme", tool: "refund" },
61
+ { id: "b", tenant: "other", tool: "refund" },
62
+ ],
63
+ listDecisions: async () => [
64
+ { name: "triage", tenant: "acme", state: "learning", pending: 1, drift: false },
65
+ { name: "other-call", tenant: "other", state: "learning", pending: 0, drift: false },
66
+ ],
67
+ });
68
+ const store = createSessionStore();
69
+ const issued = await mintMcpSession({
70
+ store,
71
+ secret: SECRET,
72
+ principalId: "op",
73
+ scopes: ["mcp:ai:read", "mcp:decisions:read"],
74
+ tenantId: "acme",
75
+ });
76
+ const requester = await authenticateMcpRequest(store, SECRET, issued.accessToken);
77
+
78
+ const listed = await runtime.callTool(requester, "oke.ai.runs.list", {});
79
+ expect(listed.ok).toBe(true);
80
+ if (!listed.ok) return;
81
+ const runs = (listed.data.content as { runs: { id: string }[] }).runs;
82
+ expect(runs.map((row) => row.id)).toEqual(["run-a"]);
83
+
84
+ const foreign = await runtime.callTool(requester, "oke.ai.runs.get", { runId: "run-b" });
85
+ expect(foreign.ok).toBe(false);
86
+ const namedOther = await runtime.callTool(requester, "oke.ai.runs.list", { tenant: "other" });
87
+ expect(namedOther.ok).toBe(true);
88
+ if (!namedOther.ok) return;
89
+ expect((namedOther.data.content as { runs: unknown[] }).runs).toEqual([]);
90
+ expect(foreign.ok).toBe(false);
91
+
92
+ const got = await runtime.callTool(requester, "oke.ai.runs.get", {
93
+ runId: "run-a",
94
+ tenant: "acme",
95
+ });
96
+ expect(got.ok).toBe(true);
97
+ if (!got.ok) return;
98
+ expect((got.data.content as { events: { seq: number }[] }).events[0]?.seq).toBe(1);
99
+
100
+ const approvals = await runtime.callTool(requester, "oke.ai.approvals.list", {
101
+ tenant: "acme",
102
+ });
103
+ expect(approvals.ok).toBe(true);
104
+ if (!approvals.ok) return;
105
+ expect(
106
+ (approvals.data.content as { rows: { id: string }[] }).rows.map((row) => row.id),
107
+ ).toEqual(["a"]);
108
+
109
+ const decisions = await runtime.callTool(requester, "oke.decisions.list", {});
110
+ expect(decisions.ok).toBe(true);
111
+ if (!decisions.ok) return;
112
+ expect(
113
+ (decisions.data.content as { decisions: { name: string }[] }).decisions.map(
114
+ (row) => row.name,
115
+ ),
116
+ ).toEqual(["triage"]);
117
+
118
+ const operatorIssued = await mintMcpSession({
119
+ store,
120
+ secret: SECRET,
121
+ principalId: "root",
122
+ scopes: ["mcp:*"],
123
+ });
124
+ const operator = await authenticateMcpRequest(store, SECRET, operatorIssued.accessToken);
125
+ const allRuns = await runtime.callTool(operator, "oke.ai.runs.list", {});
126
+ expect(allRuns.ok).toBe(true);
127
+ if (!allRuns.ok) return;
128
+ expect((allRuns.data.content as { runs: { id: string }[] }).runs.map((row) => row.id)).toEqual([
129
+ "run-a",
130
+ "run-b",
131
+ ]);
132
+ const otherApprovals = await runtime.callTool(operator, "oke.ai.approvals.list", {
133
+ tenant: "other",
134
+ });
135
+ expect(otherApprovals.ok).toBe(true);
136
+ if (!otherApprovals.ok) return;
137
+ expect(
138
+ (otherApprovals.data.content as { rows: { id: string }[] }).rows.map((row) => row.id),
139
+ ).toEqual(["b"]);
140
+ });
141
+
142
+ test("every AI tool masks PII fields and redacted secrets", async () => {
143
+ const secret = Redacted.of("sk-live");
144
+ const manifest = {
145
+ oke: "1.0",
146
+ app: "skyport",
147
+ stores: {
148
+ app: {
149
+ tables: {
150
+ people: { columns: { email: { pii: true }, apiKey: { sensitive: true } } },
151
+ },
152
+ },
153
+ },
154
+ } as unknown as Manifest;
155
+ const ctx: McpContext = {
156
+ getManifest: () => manifest,
157
+ listRuns: async () => [],
158
+ listAgentRuns: async () => [
159
+ {
160
+ id: "run-a",
161
+ tenant: "acme",
162
+ args: { email: "a@b.co", apiKey: secret },
163
+ trail: [{ args: { email: "a@b.co" } }],
164
+ },
165
+ ],
166
+ getAgentRun: async () => ({
167
+ run: { id: "run-a", tenant: "acme", args: { email: "a@b.co", apiKey: secret } },
168
+ events: [{ seq: 1, event: { type: "CUSTOM", value: { email: "a@b.co", apiKey: secret } } }],
169
+ }),
170
+ listApprovals: async () => [
171
+ { id: "a", tenant: "acme", args: { email: "a@b.co", apiKey: secret } },
172
+ ],
173
+ listDecisions: async () => [
174
+ { name: "triage", tenant: "acme", input: { email: "a@b.co", apiKey: secret } },
175
+ ],
176
+ };
177
+ const runtime = createToolRuntime(ctx);
178
+ const store = createSessionStore();
179
+ const issued = await mintMcpSession({
180
+ store,
181
+ secret: SECRET,
182
+ principalId: "op",
183
+ scopes: ["mcp:ai:read", "mcp:decisions:read"],
184
+ tenantId: "acme",
185
+ });
186
+ const requester = await authenticateMcpRequest(store, SECRET, issued.accessToken);
187
+ const blobs = [
188
+ await runtime.callTool(requester, "oke.ai.runs.list", {}),
189
+ await runtime.callTool(requester, "oke.ai.runs.get", { runId: "run-a" }),
190
+ await runtime.callTool(requester, "oke.ai.approvals.list", {}),
191
+ await runtime.callTool(requester, "oke.decisions.list", {}),
192
+ ];
193
+ for (const result of blobs) {
194
+ expect(result.ok).toBe(true);
195
+ if (!result.ok) continue;
196
+ const text = JSON.stringify(result.data.content);
197
+ expect(text).not.toContain("a@b.co");
198
+ expect(text).not.toContain("sk-live");
199
+ expect(text).toContain("[redacted]");
200
+ }
201
+ });
202
+ });
@@ -133,6 +133,72 @@ export const MCP_TOOL_POLICIES: readonly McpToolPolicy[] = [
133
133
  },
134
134
  params: [{ name: "runId", maxLength: 128 }],
135
135
  },
136
+ {
137
+ name: "oke.ai.runs.list",
138
+ scopes: ["mcp:ai:read", "console:runs:read"],
139
+ mutability: "read",
140
+ description: "List agent runs as inert data. Approvals stay in the Console.",
141
+ inputSchema: {
142
+ type: "object",
143
+ properties: {
144
+ limit: { type: "integer", minimum: 1, maximum: 200 },
145
+ tenant: { type: "string" },
146
+ },
147
+ additionalProperties: false,
148
+ },
149
+ params: [
150
+ { name: "limit", maxLength: 3 },
151
+ { name: "tenant", maxLength: 128 },
152
+ ],
153
+ },
154
+ {
155
+ name: "oke.ai.runs.get",
156
+ scopes: ["mcp:ai:read", "console:runs:read"],
157
+ mutability: "read",
158
+ description: "Return one agent run record and its follow events as inert data.",
159
+ inputSchema: {
160
+ type: "object",
161
+ properties: {
162
+ runId: { type: "string", minLength: 1 },
163
+ tenant: { type: "string" },
164
+ },
165
+ required: ["runId"],
166
+ additionalProperties: false,
167
+ },
168
+ params: [
169
+ { name: "runId", maxLength: 128 },
170
+ { name: "tenant", maxLength: 128 },
171
+ ],
172
+ },
173
+ {
174
+ name: "oke.ai.approvals.list",
175
+ scopes: ["mcp:ai:read", "console:runs:read"],
176
+ mutability: "read",
177
+ description: "List pending tool approvals as inert data. Resolve stays in the Console.",
178
+ inputSchema: {
179
+ type: "object",
180
+ properties: {
181
+ tenant: { type: "string" },
182
+ },
183
+ additionalProperties: false,
184
+ },
185
+ params: [{ name: "tenant", maxLength: 128 }],
186
+ },
187
+ {
188
+ name: "oke.decisions.list",
189
+ scopes: ["mcp:decisions:read", "console:runs:read"],
190
+ mutability: "read",
191
+ description:
192
+ "List decisions with state, pending count, certificate metrics, and drift. Promote stays in the CLI.",
193
+ inputSchema: {
194
+ type: "object",
195
+ properties: {
196
+ tenant: { type: "string" },
197
+ },
198
+ additionalProperties: false,
199
+ },
200
+ params: [{ name: "tenant", maxLength: 128 }],
201
+ },
136
202
  {
137
203
  name: "oke.action.invoke",
138
204
  scopes: ["mcp:action:invoke", "console:flows:invoke"],
@@ -38,6 +38,8 @@ export interface MintMcpSessionOptions {
38
38
  readonly secret: string;
39
39
  readonly principalId: string;
40
40
  readonly scopes: Iterable<string>;
41
+ /** Tenant stamped on the access token. Omitted tokens have no tenant. */
42
+ readonly tenantId?: string | null;
41
43
  readonly now?: () => number;
42
44
  }
43
45
 
@@ -58,6 +60,7 @@ export async function mintMcpSession(options: MintMcpSessionOptions): Promise<Is
58
60
  id: options.principalId,
59
61
  plane: "operator",
60
62
  scopes: options.scopes,
63
+ ...(options.tenantId !== undefined ? { tenantId: options.tenantId } : {}),
61
64
  },
62
65
  );
63
66
  }
package/src/mcp/tools.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * confirmation token (no session-level consent cache).
6
6
  */
7
7
 
8
+ import { lazyRequire } from "../kernel/lazy-require.ts";
8
9
  import type { Manifest } from "../manifest/types.ts";
9
10
  import type { WideEvent } from "../runs/types.ts";
10
11
  import { authorizeToolCall, MCP_TOOL_POLICIES, type McpToolPolicy } from "./authorization.ts";
@@ -22,6 +23,16 @@ export interface McpContext {
22
23
  readonly getManifest: () => Manifest | null;
23
24
  /** List runs/traces (already operator-scoped by the provider). */
24
25
  readonly listRuns: () => Promise<readonly WideEvent[]>;
26
+ /** Agent runs for `oke.ai.runs.*`. Absent providers return an empty list. */
27
+ readonly listAgentRuns?: () => Promise<readonly unknown[]>;
28
+ /** One agent run plus follow events. */
29
+ readonly getAgentRun?: (
30
+ runId: string,
31
+ ) => Promise<{ readonly run: unknown; readonly events: readonly unknown[] } | undefined>;
32
+ /** Pending tool approvals. Resolve is not an MCP tool. */
33
+ readonly listApprovals?: () => Promise<readonly unknown[]>;
34
+ /** Decision catalogue: state, pending count, metrics, drift. */
35
+ readonly listDecisions?: (tenant?: string) => Promise<readonly unknown[]>;
25
36
  /**
26
37
  * Safe invoke adapter. Called only after confirmation. Must NOT receive
27
38
  * the caller's Bearer token — capabilities are passed as structured input.
@@ -144,6 +155,14 @@ export function createToolRuntime(
144
155
  return tracesList(ctx, args);
145
156
  case "oke.traces.get":
146
157
  return tracesGet(ctx, args);
158
+ case "oke.ai.runs.list":
159
+ return aiRunsList(ctx, requester, args);
160
+ case "oke.ai.runs.get":
161
+ return aiRunsGet(ctx, requester, args);
162
+ case "oke.ai.approvals.list":
163
+ return aiApprovalsList(ctx, requester, args);
164
+ case "oke.decisions.list":
165
+ return decisionsList(ctx, requester, args);
147
166
  case "oke.action.confirm":
148
167
  return actionConfirm(confirm, requester, args);
149
168
  case "oke.action.invoke":
@@ -243,6 +262,155 @@ async function tracesGet(ctx: McpContext, args: Record<string, unknown>): Promis
243
262
  return okData(freezeData({ run: projectRun(run) }), "store-record");
244
263
  }
245
264
 
265
+ function tenantArg(args: Record<string, unknown>): string | undefined {
266
+ return typeof args.tenant === "string" && args.tenant.length > 0 ? args.tenant : undefined;
267
+ }
268
+
269
+ function sameTenant(row: unknown, tenant: string | undefined): boolean {
270
+ if (!tenant) return true;
271
+ if (!row || typeof row !== "object") return false;
272
+ return (row as { tenant?: unknown }).tenant === tenant;
273
+ }
274
+
275
+ /** `console:*` and `mcp:*` may list every tenant. A named tool scope stays on its token. */
276
+ function operatorTenantScope(scopes: readonly string[]): boolean {
277
+ return scopes.includes("console:*") || scopes.includes("mcp:*");
278
+ }
279
+
280
+ /**
281
+ * Tenant a read is allowed to see.
282
+ *
283
+ * The token's `tid` is the default. Naming another tenant, or listing every
284
+ * tenant, requires an operator scope. A non-operator token with no tenant
285
+ * sees nothing.
286
+ */
287
+ function visibleTenant(
288
+ requester: McpRequester,
289
+ args: Record<string, unknown>,
290
+ ): { readonly ok: true; readonly tenant: string | undefined } | { readonly ok: false } {
291
+ const requested = tenantArg(args);
292
+ const own = requester.claims.tid;
293
+ if (operatorTenantScope(requester.scopes)) {
294
+ return { ok: true, tenant: requested };
295
+ }
296
+ if (!own) return { ok: false };
297
+ if (requested !== undefined && requested !== own) return { ok: false };
298
+ return { ok: true, tenant: own };
299
+ }
300
+
301
+ function loadClassify(): typeof import("../elements/store/classify.ts") {
302
+ return lazyRequire(`${import.meta.dir}/../elements/store`, ["class", "ify"].join(""));
303
+ }
304
+
305
+ function loadRedacted(): typeof import("../kernel/redacted.ts") {
306
+ return lazyRequire(`${import.meta.dir}/../kernel`, ["redact", "ed"].join(""));
307
+ }
308
+
309
+ /**
310
+ * PII-classified and sensitive column names, plus {@link Redacted} values.
311
+ * Same mask token the Console traces projection uses.
312
+ */
313
+ function maskAiValue(manifest: Manifest | null | undefined, value: unknown): unknown {
314
+ return maskPiiWalk(loadRedacted().maskRedactedDeep(value), classifiedFieldNames(manifest));
315
+ }
316
+
317
+ function classifiedFieldNames(manifest: Manifest | null | undefined): ReadonlySet<string> {
318
+ const names = new Set<string>();
319
+ if (!manifest?.stores) return names;
320
+ for (const store of Object.values(manifest.stores)) {
321
+ for (const table of Object.values(store.tables ?? {})) {
322
+ for (const [col, tags] of Object.entries(table.columns ?? {})) {
323
+ if (tags?.pii || tags?.sensitive) loadClassify().addPiiFieldName(names, col);
324
+ }
325
+ }
326
+ }
327
+ return names;
328
+ }
329
+
330
+ function maskPiiWalk(value: unknown, fields: ReadonlySet<string>): unknown {
331
+ if (fields.size === 0 || value === null || value === undefined) return value;
332
+ if (Array.isArray(value)) return value.map((item) => maskPiiWalk(item, fields));
333
+ if (typeof value === "object") {
334
+ const out: Record<string, unknown> = {};
335
+ for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
336
+ out[key] = fields.has(key) ? loadClassify().PII_MASK : maskPiiWalk(child, fields);
337
+ }
338
+ return out;
339
+ }
340
+ return value;
341
+ }
342
+
343
+ async function aiRunsList(
344
+ ctx: McpContext,
345
+ requester: McpRequester,
346
+ args: Record<string, unknown>,
347
+ ): Promise<ToolCallResult> {
348
+ const limit =
349
+ typeof args.limit === "number" && Number.isFinite(args.limit)
350
+ ? Math.min(200, Math.max(1, Math.floor(args.limit)))
351
+ : 50;
352
+ const scope = visibleTenant(requester, args);
353
+ if (!scope.ok) return okData(freezeData({ runs: [] }), "trace");
354
+ const all = (await ctx.listAgentRuns?.()) ?? [];
355
+ const runs = all
356
+ .filter((row) => sameTenant(row, scope.tenant))
357
+ .slice(0, limit)
358
+ .map((row) => maskAiValue(ctx.getManifest(), row));
359
+ return okData(freezeData({ runs }), "trace");
360
+ }
361
+
362
+ async function aiRunsGet(
363
+ ctx: McpContext,
364
+ requester: McpRequester,
365
+ args: Record<string, unknown>,
366
+ ): Promise<ToolCallResult> {
367
+ const runId = String(args.runId ?? "");
368
+ const scope = visibleTenant(requester, args);
369
+ const found = scope.ok ? await ctx.getAgentRun?.(runId) : undefined;
370
+ if (!found || !sameTenant(found.run, scope.ok ? scope.tenant : undefined)) {
371
+ return {
372
+ ok: false,
373
+ code: "not-found",
374
+ message: `unknown run: ${runId}`,
375
+ data: asData({ runId }, "error"),
376
+ };
377
+ }
378
+ return okData(
379
+ freezeData({
380
+ run: maskAiValue(ctx.getManifest(), found.run),
381
+ events: maskAiValue(ctx.getManifest(), found.events),
382
+ }),
383
+ "store-record",
384
+ );
385
+ }
386
+
387
+ async function aiApprovalsList(
388
+ ctx: McpContext,
389
+ requester: McpRequester,
390
+ args: Record<string, unknown>,
391
+ ): Promise<ToolCallResult> {
392
+ const scope = visibleTenant(requester, args);
393
+ if (!scope.ok) return okData(freezeData({ rows: [] }), "store-record");
394
+ const all = (await ctx.listApprovals?.()) ?? [];
395
+ const rows = all
396
+ .filter((row) => sameTenant(row, scope.tenant))
397
+ .map((row) => maskAiValue(ctx.getManifest(), row));
398
+ return okData(freezeData({ rows }), "store-record");
399
+ }
400
+
401
+ async function decisionsList(
402
+ ctx: McpContext,
403
+ requester: McpRequester,
404
+ args: Record<string, unknown>,
405
+ ): Promise<ToolCallResult> {
406
+ const scope = visibleTenant(requester, args);
407
+ if (!scope.ok) return okData(freezeData({ decisions: [] }), "manifest");
408
+ const decisions = ((await ctx.listDecisions?.(scope.tenant)) ?? []).filter((row) =>
409
+ sameTenant(row, scope.tenant),
410
+ );
411
+ return okData(freezeData({ decisions: maskAiValue(ctx.getManifest(), decisions) }), "manifest");
412
+ }
413
+
246
414
  function actionConfirm(
247
415
  confirm: ReturnType<typeof createConfirmationGate>,
248
416
  requester: McpRequester,
@@ -19,6 +19,8 @@ describe("okengine/http static import graph", () => {
19
19
  expect(forbiddenHttpGraphHits(["src/elements/store/search-runtime.ts"])).toEqual([
20
20
  "hybrid-search runtime",
21
21
  ]);
22
+ expect(forbiddenHttpGraphHits(["src/kernel/fx.ts"])).toEqual(["fx runtime"]);
23
+ expect(forbiddenHttpGraphHits(["src/kernel/fx-decide.ts"])).toEqual([]);
22
24
  expect(forbiddenHttpGraphHits(["src/elements/store/search-lsh.ts"])).toEqual([]);
23
25
  expect(forbiddenHttpGraphHits(["src/kernel/app.ts"])).toEqual([]);
24
26
  });
@@ -39,6 +39,11 @@ export const HTTP_STATIC_GRAPH_FORBIDDEN: readonly HttpGraphForbidden[] = [
39
39
  specifier.includes("search-runtime.js") ||
40
40
  /(^|\/)search-runtime$/.test(specifier),
41
41
  },
42
+ {
43
+ id: "fx runtime",
44
+ test: (specifier) =>
45
+ specifier.endsWith("/fx.ts") || specifier.endsWith("/fx.js") || /(^|\/)fx$/.test(specifier),
46
+ },
42
47
  ];
43
48
 
44
49
  /** Metafile slice this gate reads. */
@@ -288,7 +288,7 @@ if (!artifact) { console.error("no output"); process.exit(1); }
288
288
  const raw = await artifact.arrayBuffer();
289
289
  process.stdout.write(String(Bun.gzipSync(new Uint8Array(raw)).byteLength));
290
290
  `;
291
- const proc = Bun.spawn(["bun", "-e", probe], {
291
+ const proc = Bun.spawn([process.execPath, "-e", probe], {
292
292
  cwd: ROOT,
293
293
  stdout: "pipe",
294
294
  stderr: "pipe",
@@ -316,20 +316,45 @@ process.stdout.write(String(Bun.gzipSync(new Uint8Array(raw)).byteLength));
316
316
  * first download when a shared chunk loads with it.
317
317
  */
318
318
  export async function measureClientGzipBytes(): Promise<number> {
319
- const entry = `${import.meta.dir}/../client/budget-entry.ts`;
320
- const result = await Bun.build({
321
- entrypoints: [entry],
322
- minify: true,
323
- target: "browser",
324
- format: "esm",
319
+ const entry = resolve(ROOT, "src/client/budget-entry.ts");
320
+ // `bun test` sets NODE_ENV=test. An in-process Bun.build then inlines that
321
+ // and the gzip drifts from `bun run budgets`. Spawn once, production, so
322
+ // both callers share this number.
323
+ const probe = `
324
+ const result = await Bun.build({
325
+ entrypoints: ${JSON.stringify([entry])},
326
+ minify: true,
327
+ target: "browser",
328
+ format: "esm",
329
+ });
330
+ if (!result.success) {
331
+ console.error(result.logs.map(String).join("\\n"));
332
+ process.exit(1);
333
+ }
334
+ const artifact = result.outputs[0];
335
+ if (!artifact) { console.error("no output"); process.exit(1); }
336
+ const raw = await artifact.arrayBuffer();
337
+ process.stdout.write(String(Bun.gzipSync(new Uint8Array(raw)).byteLength));
338
+ `;
339
+ const proc = Bun.spawn([process.execPath, "-e", probe], {
340
+ cwd: ROOT,
341
+ stdout: "pipe",
342
+ stderr: "pipe",
343
+ env: { ...process.env, NODE_ENV: "production" },
325
344
  });
326
- if (!result.success) {
327
- throw new Error(`client build failed:\n${result.logs.map(String).join("\n")}`);
345
+ const [stdout, stderr, exitCode] = await Promise.all([
346
+ new Response(proc.stdout).text(),
347
+ new Response(proc.stderr).text(),
348
+ proc.exited,
349
+ ]);
350
+ if (exitCode !== 0) {
351
+ throw new Error(`client build failed:\n${stderr || stdout}`);
328
352
  }
329
- const artifact = result.outputs[0];
330
- if (!artifact) throw new Error("client build produced no output");
331
- const raw = await artifact.arrayBuffer();
332
- return Bun.gzipSync(new Uint8Array(raw)).byteLength;
353
+ const n = Number(stdout.trim());
354
+ if (!Number.isFinite(n) || n <= 0) {
355
+ throw new Error(`client measure produced ${stdout}`);
356
+ }
357
+ return n;
333
358
  }
334
359
 
335
360
  /**
@@ -438,7 +463,7 @@ process.stdout.write(String(ms));
438
463
  `;
439
464
  const samples: number[] = [];
440
465
  for (let i = 0; i < 7; i++) {
441
- const proc = Bun.spawn(["bun", "-e", probe], {
466
+ const proc = Bun.spawn([process.execPath, "-e", probe], {
442
467
  cwd: ROOT,
443
468
  stdout: "pipe",
444
469
  stderr: "pipe",
@@ -13,7 +13,7 @@ import type { Trigger } from "../kernel/triggers.ts";
13
13
  import type { FlowPlane } from "../manifest/types.ts";
14
14
  import { cacheDimensionOf, type RunTelemetry } from "../kernel/run-telemetry.ts";
15
15
  import { okid } from "../okid.ts";
16
- import type { RunError, WideEvent } from "./types.ts";
16
+ import type { RunError, RunHttpFrame, WideEvent } from "./types.ts";
17
17
 
18
18
  /** Inputs for {@link collectWideEvent}. */
19
19
  export interface CollectWideEventInput {
@@ -42,6 +42,8 @@ export interface CollectWideEventInput {
42
42
  readonly input?: unknown;
43
43
  /** Flow return value when the run completed with an output. */
44
44
  readonly output?: unknown;
45
+ /** HTTP wire frame when the trigger was `http`. */
46
+ readonly http?: RunHttpFrame;
45
47
  /** Optional parent run id (causal chain). */
46
48
  readonly parentId?: string;
47
49
  /** Optional run id (generated when omitted). */
@@ -123,6 +125,7 @@ export function collectWideEvent(input: CollectWideEventInput): WideEvent {
123
125
  error,
124
126
  ...(input.input !== undefined ? { input: input.input } : {}),
125
127
  ...(input.output !== undefined ? { output: input.output } : {}),
128
+ ...(input.http !== undefined ? { http: input.http } : {}),
126
129
  effects: [...input.ledger.entries],
127
130
  logs: [...input.telemetry.logs],
128
131
  durationMs,
@@ -12,7 +12,13 @@ import { join } from "node:path";
12
12
 
13
13
  import { duckPath, openDuckDB } from "./duckdb.ts";
14
14
  import { createRunsRuntime } from "./runtime.ts";
15
- import { readParquet, wideEventToRow, writeParquet, type ParquetRow } from "./parquet.ts";
15
+ import {
16
+ readParquet,
17
+ rowToWideEvent,
18
+ wideEventToRow,
19
+ writeParquet,
20
+ type ParquetRow,
21
+ } from "./parquet.ts";
16
22
  import type { WideEvent } from "./types.ts";
17
23
 
18
24
  const temps: string[] = [];
@@ -62,6 +68,26 @@ async function writeLegacyParquet(path: string, rows: readonly ParquetRow[]): Pr
62
68
  }
63
69
  }
64
70
 
71
+ describe("parquet http frame", () => {
72
+ test("round-trips a frame and omits a null column", () => {
73
+ const event = sample("framed", null);
74
+ const framed = {
75
+ ...event,
76
+ http: {
77
+ request: {
78
+ method: "GET",
79
+ path: "/tasks",
80
+ query: { limit: "20" },
81
+ headers: { accept: "application/json" },
82
+ },
83
+ response: { status: 200, headers: { "content-type": "application/json" } },
84
+ },
85
+ };
86
+ expect(rowToWideEvent(wideEventToRow(framed)).http).toEqual(framed.http);
87
+ expect(rowToWideEvent(wideEventToRow(event)).http).toBeUndefined();
88
+ });
89
+ });
90
+
65
91
  describe("parquet error_code types", () => {
66
92
  test("readParquet unions all-null JSON error_code with VARCHAR Unauthorized", async () => {
67
93
  const dir = await tmpDir();