@wrenai/genbi 0.0.4 → 0.0.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.
Files changed (234) hide show
  1. package/README.md +65 -3
  2. package/dist/assets/{echarts-LTFC-ysd.js → echarts-DYM4hjvk.js} +2 -2
  3. package/dist/assets/index-A_Wrq7Ey.css +1 -0
  4. package/dist/assets/{index-Bo--QMUN.js → index-DsH129tV.js} +18 -17
  5. package/dist/index.html +2 -2
  6. package/dist-server/harness/bundle/schema.d.ts +38 -0
  7. package/dist-server/harness/bundle/schema.js +18 -10
  8. package/dist-server/harness/bundle/version.js +1 -1
  9. package/dist-server/harness/cli-args.d.ts +10 -2
  10. package/dist-server/harness/cli-args.js +24 -8
  11. package/dist-server/harness/cli.js +75 -9
  12. package/dist-server/harness/compile/cache.js +4 -4
  13. package/dist-server/harness/compile/compose-profile.d.ts +51 -14
  14. package/dist-server/harness/compile/compose-profile.js +85 -19
  15. package/dist-server/harness/compile/context-loader.d.ts +56 -0
  16. package/dist-server/harness/compile/context-loader.js +237 -0
  17. package/dist-server/harness/compile/errors.d.ts +19 -0
  18. package/dist-server/harness/compile/errors.js +35 -0
  19. package/dist-server/harness/compile/index.d.ts +4 -3
  20. package/dist-server/harness/compile/index.js +4 -3
  21. package/dist-server/harness/compile/pipeline.d.ts +15 -7
  22. package/dist-server/harness/compile/pipeline.js +62 -14
  23. package/dist-server/harness/compile/resolve-binary.d.ts +2 -2
  24. package/dist-server/harness/compile/resolve-binary.js +3 -3
  25. package/dist-server/harness/compile/types.d.ts +29 -0
  26. package/dist-server/harness/compile/warble-identity.d.ts +11 -0
  27. package/dist-server/harness/compile/warble-identity.js +13 -0
  28. package/dist-server/harness/components/ai-step.d.ts +4 -0
  29. package/dist-server/harness/components/ai-step.js +40 -0
  30. package/dist-server/harness/components/broker.d.ts +60 -0
  31. package/dist-server/harness/components/broker.js +117 -0
  32. package/dist-server/harness/components/capability-card.d.ts +98 -0
  33. package/dist-server/harness/components/capability-card.js +169 -0
  34. package/dist-server/harness/components/context.d.ts +2 -0
  35. package/dist-server/harness/components/context.js +30 -0
  36. package/dist-server/harness/components/display.d.ts +6 -0
  37. package/dist-server/harness/components/display.js +29 -0
  38. package/dist-server/harness/components/egress-judge.d.ts +12 -0
  39. package/dist-server/harness/components/egress-judge.js +27 -0
  40. package/dist-server/harness/components/egress.d.ts +142 -0
  41. package/dist-server/harness/components/egress.js +469 -0
  42. package/dist-server/harness/components/in-process.d.ts +7 -0
  43. package/dist-server/harness/components/in-process.js +245 -0
  44. package/dist-server/harness/components/normalize.d.ts +35 -0
  45. package/dist-server/harness/components/normalize.js +245 -0
  46. package/dist-server/harness/components/plan.d.ts +31 -0
  47. package/dist-server/harness/components/plan.js +184 -0
  48. package/dist-server/harness/components/report.d.ts +207 -0
  49. package/dist-server/harness/components/report.js +309 -0
  50. package/dist-server/harness/components/runner.d.ts +193 -0
  51. package/dist-server/harness/components/runner.js +492 -0
  52. package/dist-server/harness/components/semantic-guards.d.ts +20 -0
  53. package/dist-server/harness/components/semantic-guards.js +42 -0
  54. package/dist-server/harness/components/terminal-json.d.ts +18 -0
  55. package/dist-server/harness/components/terminal-json.js +82 -0
  56. package/dist-server/harness/components/usage.d.ts +14 -0
  57. package/dist-server/harness/components/usage.js +19 -0
  58. package/dist-server/harness/components/wren-access.d.ts +29 -0
  59. package/dist-server/harness/components/wren-access.js +233 -0
  60. package/dist-server/harness/components/zone-dry-run.d.ts +43 -0
  61. package/dist-server/harness/components/zone-dry-run.js +107 -0
  62. package/dist-server/harness/components/zone-gate.d.ts +76 -0
  63. package/dist-server/harness/components/zone-gate.js +144 -0
  64. package/dist-server/harness/events/index.d.ts +1 -1
  65. package/dist-server/harness/events/types.d.ts +52 -0
  66. package/dist-server/harness/index.d.ts +9 -2
  67. package/dist-server/harness/index.js +5 -1
  68. package/dist-server/harness/loop/executor.d.ts +1 -0
  69. package/dist-server/harness/loop/executor.js +37 -15
  70. package/dist-server/harness/npm-package-resolve.d.ts +2 -0
  71. package/dist-server/harness/npm-package-resolve.js +16 -4
  72. package/dist-server/harness/providers/adapters/openai-compatible.d.ts +13 -0
  73. package/dist-server/harness/providers/adapters/openai-compatible.js +17 -0
  74. package/dist-server/harness/providers/adapters/openai.d.ts +7 -0
  75. package/dist-server/harness/providers/adapters/openai.js +6 -0
  76. package/dist-server/harness/providers/adapters/pi-ai.d.ts +48 -0
  77. package/dist-server/harness/providers/adapters/pi-ai.js +518 -0
  78. package/dist-server/harness/providers/binding.d.ts +45 -4
  79. package/dist-server/harness/providers/binding.js +44 -8
  80. package/dist-server/harness/providers/index.d.ts +9 -3
  81. package/dist-server/harness/providers/index.js +10 -3
  82. package/dist-server/harness/providers/zone.d.ts +96 -0
  83. package/dist-server/harness/providers/zone.js +112 -0
  84. package/dist-server/harness/render/answering-query.d.ts +29 -0
  85. package/dist-server/harness/render/answering-query.js +62 -0
  86. package/dist-server/harness/render/envelope.d.ts +6 -0
  87. package/dist-server/harness/render/envelope.js +10 -3
  88. package/dist-server/harness/route/adapter-spec.d.ts +8 -9
  89. package/dist-server/harness/route/adapter-spec.js +13 -14
  90. package/dist-server/harness/route/codex-ask-events.js +2 -1
  91. package/dist-server/harness/route/codex-ask.d.ts +4 -0
  92. package/dist-server/harness/route/codex-ask.js +43 -45
  93. package/dist-server/harness/route/codex-local-manifest.d.ts +9 -1
  94. package/dist-server/harness/route/codex-local-manifest.js +96 -15
  95. package/dist-server/harness/route/describe.js +45 -6
  96. package/dist-server/harness/route/dispatched.d.ts +20 -0
  97. package/dist-server/harness/route/dispatched.js +122 -24
  98. package/dist-server/harness/route/host-query.d.ts +93 -0
  99. package/dist-server/harness/route/host-query.js +257 -0
  100. package/dist-server/harness/route/in-process.d.ts +22 -1
  101. package/dist-server/harness/route/in-process.js +54 -4
  102. package/dist-server/harness/route/route.js +8 -2
  103. package/dist-server/harness/route/types.d.ts +47 -1
  104. package/dist-server/harness/route/wren-host-proxy.d.ts +33 -0
  105. package/dist-server/harness/route/wren-host-proxy.js +228 -0
  106. package/dist-server/harness/session/run.js +30 -10
  107. package/dist-server/harness/setup/runner.d.ts +17 -3
  108. package/dist-server/harness/setup/runner.js +272 -89
  109. package/dist-server/harness/setup/yaml-scalar.d.ts +23 -0
  110. package/dist-server/harness/setup/yaml-scalar.js +62 -0
  111. package/dist-server/harness/tools/setup-native.d.ts +7 -0
  112. package/dist-server/harness/tools/setup-native.js +7 -4
  113. package/dist-server/server/adopt.js +1 -17
  114. package/dist-server/server/app.d.ts +1 -0
  115. package/dist-server/server/app.js +175 -17
  116. package/dist-server/server/auth-choice.d.ts +13 -1
  117. package/dist-server/server/auth-choice.js +66 -1
  118. package/dist-server/server/bin.js +185 -25
  119. package/dist-server/server/codex-native-session.d.ts +25 -0
  120. package/dist-server/server/codex-native-session.js +1 -0
  121. package/dist-server/server/codex-runtime-composition.d.ts +34 -0
  122. package/dist-server/server/codex-runtime-composition.js +250 -0
  123. package/dist-server/server/codex-structured-ask.d.ts +23 -0
  124. package/dist-server/server/codex-structured-ask.js +102 -0
  125. package/dist-server/server/conn-config.js +4 -16
  126. package/dist-server/server/conversation-socket.d.ts +16 -0
  127. package/dist-server/server/conversation-socket.js +65 -0
  128. package/dist-server/server/db.d.ts +46 -0
  129. package/dist-server/server/db.js +88 -9
  130. package/dist-server/server/env-detect.d.ts +6 -0
  131. package/dist-server/server/env-detect.js +7 -0
  132. package/dist-server/server/fold.d.ts +23 -4
  133. package/dist-server/server/fold.js +42 -5
  134. package/dist-server/server/harness.d.ts +2 -2
  135. package/dist-server/server/harness.js +33 -7
  136. package/dist-server/server/host-evidence-grounding.d.ts +21 -0
  137. package/dist-server/server/host-evidence-grounding.js +93 -0
  138. package/dist-server/server/interactive-terminal.d.ts +10 -6
  139. package/dist-server/server/interactive-terminal.js +33 -8
  140. package/dist-server/server/managed-wren-runtime.d.ts +116 -0
  141. package/dist-server/server/managed-wren-runtime.js +670 -0
  142. package/dist-server/server/native-artifacts.d.ts +112 -7
  143. package/dist-server/server/native-artifacts.js +194 -14
  144. package/dist-server/server/native-component-context.d.ts +19 -0
  145. package/dist-server/server/native-component-context.js +76 -0
  146. package/dist-server/server/native-component-preparation.d.ts +20 -0
  147. package/dist-server/server/native-component-preparation.js +129 -0
  148. package/dist-server/server/native-component-runtime.d.ts +36 -0
  149. package/dist-server/server/native-component-runtime.js +92 -0
  150. package/dist-server/server/native-components.d.ts +84 -0
  151. package/dist-server/server/native-components.js +160 -0
  152. package/dist-server/server/native-dispatch-process.d.ts +8 -0
  153. package/dist-server/server/native-dispatch-process.js +65 -0
  154. package/dist-server/server/native-runtime-spec.d.ts +89 -0
  155. package/dist-server/server/native-runtime-spec.js +169 -0
  156. package/dist-server/server/native-sessions.d.ts +172 -19
  157. package/dist-server/server/native-sessions.js +839 -244
  158. package/dist-server/server/native-shutdown.d.ts +6 -0
  159. package/dist-server/server/native-shutdown.js +28 -0
  160. package/dist-server/server/native-wren-home.d.ts +18 -0
  161. package/dist-server/server/native-wren-home.js +262 -0
  162. package/dist-server/server/node-pty-host.d.ts +2 -1
  163. package/dist-server/server/node-pty-host.js +3 -2
  164. package/dist-server/server/profile-registry.d.ts +139 -0
  165. package/dist-server/server/profile-registry.js +620 -0
  166. package/dist-server/server/runtime-binding.d.ts +4 -1
  167. package/dist-server/server/runtime-binding.js +50 -2
  168. package/dist-server/server/runtime-host/claude-component-step.d.ts +25 -0
  169. package/dist-server/server/runtime-host/claude-component-step.js +186 -0
  170. package/dist-server/server/runtime-host/codex-app-server.d.ts +75 -0
  171. package/dist-server/server/runtime-host/codex-app-server.js +239 -0
  172. package/dist-server/server/runtime-host/codex-compatibility.d.ts +110 -0
  173. package/dist-server/server/runtime-host/codex-compatibility.js +162 -0
  174. package/dist-server/server/runtime-host/codex-component-step.d.ts +19 -0
  175. package/dist-server/server/runtime-host/codex-component-step.js +308 -0
  176. package/dist-server/server/runtime-host/codex-conversation.d.ts +109 -0
  177. package/dist-server/server/runtime-host/codex-conversation.js +295 -0
  178. package/dist-server/server/runtime-host/codex-direct-tools.d.ts +23 -0
  179. package/dist-server/server/runtime-host/codex-direct-tools.js +59 -0
  180. package/dist-server/server/runtime-host/codex-events.d.ts +647 -0
  181. package/dist-server/server/runtime-host/codex-events.js +66 -0
  182. package/dist-server/server/runtime-host/codex-policy.d.ts +16 -0
  183. package/dist-server/server/runtime-host/codex-policy.js +151 -0
  184. package/dist-server/server/runtime-host/codex-process.d.ts +8 -0
  185. package/dist-server/server/runtime-host/codex-process.js +96 -0
  186. package/dist-server/server/runtime-host/codex-rpc.d.ts +47 -0
  187. package/dist-server/server/runtime-host/codex-rpc.js +182 -0
  188. package/dist-server/server/runtime-host/codex-session.d.ts +74 -0
  189. package/dist-server/server/runtime-host/codex-session.js +492 -0
  190. package/dist-server/server/runtime-host/codex-terminal-protocol.d.ts +35 -0
  191. package/dist-server/server/runtime-host/codex-terminal-protocol.js +243 -0
  192. package/dist-server/server/runtime-host/codex-terminal.d.ts +16 -0
  193. package/dist-server/server/runtime-host/codex-terminal.js +120 -0
  194. package/dist-server/server/runtime-host/index.d.ts +5 -0
  195. package/dist-server/server/runtime-host/index.js +3 -0
  196. package/dist-server/server/runtime-host/local.d.ts +29 -0
  197. package/dist-server/server/runtime-host/local.js +90 -0
  198. package/dist-server/server/runtime-host/policy.d.ts +8 -0
  199. package/dist-server/server/runtime-host/policy.js +78 -0
  200. package/dist-server/server/runtime-host/types.d.ts +52 -0
  201. package/dist-server/server/runtime-host/types.js +62 -0
  202. package/dist-server/server/structured-query-evidence.d.ts +7 -0
  203. package/dist-server/server/structured-query-evidence.js +47 -0
  204. package/dist-server/server/structured-runtime.d.ts +52 -0
  205. package/dist-server/server/structured-runtime.js +130 -0
  206. package/dist-server/server/structured-scope.d.ts +4 -0
  207. package/dist-server/server/structured-scope.js +12 -0
  208. package/dist-server/server/turn.d.ts +6 -1
  209. package/dist-server/server/turn.js +86 -26
  210. package/dist-server/server/wire-types.d.ts +67 -3
  211. package/dist-server/server/wren-profiles.js +1 -1
  212. package/managed-wren/README.md +60 -0
  213. package/managed-wren/dependency-wheels.json +282 -0
  214. package/managed-wren/manifest.json +373 -0
  215. package/managed-wren/release-inputs.json +45 -0
  216. package/managed-wren/runtime-tree.cjs +115 -0
  217. package/managed-wren/source-inputs.json +40 -0
  218. package/package.json +15 -6
  219. package/profiles/genbi-default/context/binding.yml +17 -7
  220. package/profiles/genbi-default/context/context.json +146 -0
  221. package/profiles/genbi-default/ir.golden.json +39 -43
  222. package/profiles/genbi-default/profile.yml +31 -0
  223. package/profiles/genbi-enrich-context/context/binding.yml +14 -3
  224. package/profiles/genbi-enrich-context/context/context.json +146 -0
  225. package/profiles/genbi-enrich-context/ir.golden.json +4 -5
  226. package/profiles/genbi-monitor/context/binding.yml +13 -5
  227. package/profiles/genbi-monitor/context/context.json +146 -0
  228. package/profiles/genbi-monitor/ir.golden.json +13 -7
  229. package/profiles/genbi-report/context/binding.yml +19 -0
  230. package/profiles/genbi-report/context/context.json +223 -0
  231. package/profiles/genbi-report/ir.golden.json +328 -0
  232. package/profiles/genbi-report/profile.yml +38 -0
  233. package/profiles/genbi-setup/ir.golden.json +3 -1
  234. package/dist/assets/index-Xs4q7pNo.css +0 -1
@@ -1,8 +1,10 @@
1
1
  import { type NativeSessionRow, type NativeStructuredAnswerRow, type Store } from "./db.js";
2
2
  import type { EnrichmentBinding } from "./enrichment.js";
3
+ import type { ComponentAccess } from "../harness/components/broker.js";
3
4
  export declare const NATIVE_MCP_CREDENTIAL_ENV_VAR = "WARBLE_MCP_CONNECTION_CREDENTIAL";
4
5
  export declare const NATIVE_MCP_TOOL_NAME = "save_dashboard";
5
6
  export declare const NATIVE_MCP_PERSIST_ANSWER_TOOL_NAME = "persist_answer";
7
+ export declare const NATIVE_MCP_QUERY_TOOL_NAME = "query";
6
8
  export declare const NATIVE_MCP_SERVER_LABEL = "GenBI MCP";
7
9
  export declare const NATIVE_MCP_ARTIFACTS_LABEL = "GenBI Artifacts";
8
10
  /**
@@ -20,7 +22,6 @@ export declare const NATIVE_SAVE_DASHBOARD_SEMANTIC_CONSTRAINTS: {
20
22
  readonly positionalRows: "Positional scalar rows in [x, ...series] order. Each row must contain exactly one x cell plus one cell per series entry.";
21
23
  };
22
24
  };
23
- /** The exact public MCP shape advertised to native clients and accepted by the host. */
24
25
  export declare const NATIVE_SAVE_DASHBOARD_INPUT_SCHEMA: {
25
26
  readonly type: "object";
26
27
  readonly additionalProperties: false;
@@ -90,7 +91,7 @@ export declare const NATIVE_SAVE_DASHBOARD_INPUT_SCHEMA: {
90
91
  };
91
92
  readonly answer_selection: {
92
93
  readonly const: "latest";
93
- readonly description: "Resolve the newest verified answer retained by this native session when its opaque answer_ref is no longer available in the conversation.";
94
+ readonly description: "Resolve the newest answer retained by this native session when its opaque answer_ref is no longer available in the conversation.";
94
95
  };
95
96
  readonly envelope: {
96
97
  readonly type: "object";
@@ -98,7 +99,8 @@ export declare const NATIVE_SAVE_DASHBOARD_INPUT_SCHEMA: {
98
99
  readonly required: readonly ["blocks", "verified"];
99
100
  readonly properties: {
100
101
  readonly verified: {
101
- readonly const: true;
102
+ readonly type: "boolean";
103
+ readonly description: "Ignored: the host decides verified itself. It records verified only for an answer it grounded: a grounded component run, or an answer whose every definition cites a query this session ran through the query tool. Anything submitted here is not a claim the host keeps.";
102
104
  };
103
105
  readonly summary: {
104
106
  readonly anyOf: readonly [{
@@ -286,6 +288,15 @@ export declare const NATIVE_SAVE_DASHBOARD_INPUT_SCHEMA: {
286
288
  };
287
289
  };
288
290
  };
291
+ readonly query_id: {
292
+ readonly type: "string";
293
+ readonly not: {
294
+ readonly pattern: "<\\s*\\/?(?:[sS][cC][rR][iI][pP][tT]|[hH][tT][mM][lL])\\b|[jJ][aA][vV][aA][sS][cC][rR][iI][pP][tT]\\s*:";
295
+ };
296
+ readonly minLength: 1;
297
+ readonly maxLength: 64;
298
+ readonly description: "Optional: the query_id the query tool returned for the query this definition describes.";
299
+ };
289
300
  };
290
301
  }, {
291
302
  readonly type: "object";
@@ -349,7 +360,8 @@ export declare const NATIVE_PERSIST_ANSWER_INPUT_SCHEMA: {
349
360
  readonly required: readonly ["blocks", "verified"];
350
361
  readonly properties: {
351
362
  readonly verified: {
352
- readonly const: true;
363
+ readonly type: "boolean";
364
+ readonly description: "Ignored: the host decides verified itself. It records verified only for an answer it grounded: a grounded component run, or an answer whose every definition cites a query this session ran through the query tool. Anything submitted here is not a claim the host keeps.";
353
365
  };
354
366
  readonly blocks: {
355
367
  readonly type: "array";
@@ -433,6 +445,15 @@ export declare const NATIVE_PERSIST_ANSWER_INPUT_SCHEMA: {
433
445
  };
434
446
  };
435
447
  };
448
+ readonly query_id: {
449
+ readonly type: "string";
450
+ readonly not: {
451
+ readonly pattern: "<\\s*\\/?(?:[sS][cC][rR][iI][pP][tT]|[hH][tT][mM][lL])\\b|[jJ][aA][vV][aA][sS][cC][rR][iI][pP][tT]\\s*:";
452
+ };
453
+ readonly minLength: 1;
454
+ readonly maxLength: 64;
455
+ readonly description: "Optional: the query_id the query tool returned for the query this definition describes.";
456
+ };
436
457
  };
437
458
  }];
438
459
  };
@@ -580,7 +601,7 @@ export declare const NATIVE_SAVE_DASHBOARD_CONTRACT: {
580
601
  };
581
602
  readonly answer_selection: {
582
603
  readonly const: "latest";
583
- readonly description: "Resolve the newest verified answer retained by this native session when its opaque answer_ref is no longer available in the conversation.";
604
+ readonly description: "Resolve the newest answer retained by this native session when its opaque answer_ref is no longer available in the conversation.";
584
605
  };
585
606
  readonly envelope: {
586
607
  readonly type: "object";
@@ -588,7 +609,8 @@ export declare const NATIVE_SAVE_DASHBOARD_CONTRACT: {
588
609
  readonly required: readonly ["blocks", "verified"];
589
610
  readonly properties: {
590
611
  readonly verified: {
591
- readonly const: true;
612
+ readonly type: "boolean";
613
+ readonly description: "Ignored: the host decides verified itself. It records verified only for an answer it grounded: a grounded component run, or an answer whose every definition cites a query this session ran through the query tool. Anything submitted here is not a claim the host keeps.";
592
614
  };
593
615
  readonly summary: {
594
616
  readonly anyOf: readonly [{
@@ -776,6 +798,15 @@ export declare const NATIVE_SAVE_DASHBOARD_CONTRACT: {
776
798
  };
777
799
  };
778
800
  };
801
+ readonly query_id: {
802
+ readonly type: "string";
803
+ readonly not: {
804
+ readonly pattern: "<\\s*\\/?(?:[sS][cC][rR][iI][pP][tT]|[hH][tT][mM][lL])\\b|[jJ][aA][vV][aA][sS][cC][rR][iI][pP][tT]\\s*:";
805
+ };
806
+ readonly minLength: 1;
807
+ readonly maxLength: 64;
808
+ readonly description: "Optional: the query_id the query tool returned for the query this definition describes.";
809
+ };
779
810
  };
780
811
  }, {
781
812
  readonly type: "object";
@@ -848,7 +879,8 @@ export declare const NATIVE_PERSIST_ANSWER_CONTRACT: {
848
879
  readonly required: readonly ["blocks", "verified"];
849
880
  readonly properties: {
850
881
  readonly verified: {
851
- readonly const: true;
882
+ readonly type: "boolean";
883
+ readonly description: "Ignored: the host decides verified itself. It records verified only for an answer it grounded: a grounded component run, or an answer whose every definition cites a query this session ran through the query tool. Anything submitted here is not a claim the host keeps.";
852
884
  };
853
885
  readonly blocks: {
854
886
  readonly type: "array";
@@ -932,6 +964,15 @@ export declare const NATIVE_PERSIST_ANSWER_CONTRACT: {
932
964
  };
933
965
  };
934
966
  };
967
+ readonly query_id: {
968
+ readonly type: "string";
969
+ readonly not: {
970
+ readonly pattern: "<\\s*\\/?(?:[sS][cC][rR][iI][pP][tT]|[hH][tT][mM][lL])\\b|[jJ][aA][vV][aA][sS][cC][rR][iI][pP][tT]\\s*:";
971
+ };
972
+ readonly minLength: 1;
973
+ readonly maxLength: 64;
974
+ readonly description: "Optional: the query_id the query tool returned for the query this definition describes.";
975
+ };
935
976
  };
936
977
  }];
937
978
  };
@@ -953,6 +994,29 @@ export declare const NATIVE_PERSIST_ANSWER_CONTRACT: {
953
994
  };
954
995
  readonly semanticConstraint: "The envelope contains exactly one typed table block and at most one typed definition block, with no presentation-only fields.";
955
996
  };
997
+ /** The session `query` tool: one read-only SQL query run by the host through governed access. */
998
+ export declare const NATIVE_QUERY_INPUT_SCHEMA: {
999
+ readonly type: "object";
1000
+ readonly additionalProperties: false;
1001
+ readonly required: readonly ["sql"];
1002
+ readonly properties: {
1003
+ readonly sql: {
1004
+ readonly type: "string";
1005
+ readonly minLength: 1;
1006
+ readonly description: "One read-only SELECT over the bound semantic models.";
1007
+ };
1008
+ readonly limit: {
1009
+ readonly type: "integer";
1010
+ readonly minimum: 1;
1011
+ readonly description: "Maximum rows to return (default 1000).";
1012
+ };
1013
+ };
1014
+ };
1015
+ export interface NativeQueryResult {
1016
+ readonly isError: boolean;
1017
+ readonly text: string;
1018
+ readonly result?: unknown;
1019
+ }
956
1020
  export interface NativeArtifactServiceOptions {
957
1021
  readonly store: Store;
958
1022
  readonly artifactsRoot: string;
@@ -961,6 +1025,11 @@ export interface NativeArtifactServiceOptions {
961
1025
  /** Optional override, accepted only when it is the expected local endpoint. */
962
1026
  readonly mcpUrl: string;
963
1027
  readonly getBinding: () => EnrichmentBinding | undefined;
1028
+ /**
1029
+ * Opens governed Wren access for one native analysis session's `query` tool, against the
1030
+ * session's bound project. Without it, the tool is not offered.
1031
+ */
1032
+ readonly openQueryAccess?: (binding: EnrichmentBinding, signal: AbortSignal) => Promise<ComponentAccess>;
964
1033
  /** Test-only seam; production writes one contained JSON file atomically. */
965
1034
  readonly writeAtomic?: (target: string, contents: string) => void;
966
1035
  /** Test-only seam for a persistence outage; production uses the typed Store method. */
@@ -970,6 +1039,7 @@ export interface NativeArtifactServiceOptions {
970
1039
  readonly idempotencyKey: string;
971
1040
  readonly envelopeJson: string;
972
1041
  readonly digest: string;
1042
+ readonly grounded: boolean;
973
1043
  }) => {
974
1044
  readonly row: NativeStructuredAnswerRow;
975
1045
  readonly created: boolean;
@@ -978,11 +1048,32 @@ export interface NativeArtifactServiceOptions {
978
1048
  export declare class NativeArtifactService {
979
1049
  private readonly options;
980
1050
  private readonly credentials;
1051
+ /** One host query recorder per native session: its observations are never shared across sessions. */
1052
+ private readonly queryRecorders;
1053
+ private readonly queryClosing;
981
1054
  constructor(options: NativeArtifactServiceOptions);
982
1055
  /** Validate the exact local BFF endpoint before advertising or issuing it. */
983
1056
  readiness(): NativeMcpReadiness;
984
1057
  issue(session: NativeSessionRow, binding: EnrichmentBinding | undefined): NativeMcpDescriptor;
985
1058
  revoke(credential: string | undefined): void;
1059
+ /** Whether native analysis sessions are offered the host `query` tool. */
1060
+ queryAvailable(): boolean;
1061
+ /** Resolves once every session query recorder closed so far has released its governed access. */
1062
+ queriesClosed(): Promise<void>;
1063
+ private closeQueries;
1064
+ /**
1065
+ * The session `query` tool: runs one read-only query through governed access against the
1066
+ * session's bound project, records it as this session's observation and returns its rows with
1067
+ * a session-tagged host `query_id` that a later persist_answer or save_dashboard may cite.
1068
+ */
1069
+ query(credential: string | undefined, input: unknown): Promise<NativeQueryResult>;
1070
+ /**
1071
+ * The host's grounding decision for a model-written envelope: the same envelope rebuilt from
1072
+ * this session's query observations when every query it cites grounds on them (see
1073
+ * `groundOnHostEvidence`), else `undefined`. Only this decision reaches a grounded entry from
1074
+ * the MCP surface.
1075
+ */
1076
+ private groundedEnvelope;
986
1077
  /** Releases opaque credentials when the owning BFF service shuts down. */
987
1078
  dispose(): void;
988
1079
  hasCredential(credential: string | undefined): boolean;
@@ -1000,5 +1091,19 @@ export declare class NativeArtifactService {
1000
1091
  * a later follow-up can only reference this stored result, never recompute.
1001
1092
  */
1002
1093
  persistAnswer(credential: string | undefined, input: unknown): PersistedStructuredAnswer;
1094
+ /**
1095
+ * Host-only entry for a grounded component root (never exposed over MCP).
1096
+ * The caller must already have checked the component's verified provenance.
1097
+ */
1098
+ persistGroundedAnswer(credential: string | undefined, input: unknown): PersistedStructuredAnswer;
1099
+ private persistStructured;
1003
1100
  save(credential: string | undefined, input: unknown): SavedNativeArtifact;
1101
+ /**
1102
+ * Host-only entry for a grounded component render root (never exposed over
1103
+ * MCP). The caller must already have checked verified provenance.
1104
+ */
1105
+ saveGrounded(credential: string | undefined, input: unknown): SavedNativeArtifact;
1106
+ /** The credential, session and binding checks every save runs before it reads its payload. */
1107
+ private authorizeSave;
1108
+ private saveArtifact;
1004
1109
  }
@@ -7,6 +7,8 @@ import { createHash, randomUUID } from "node:crypto";
7
7
  import { lstatSync, mkdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
8
8
  import path from "node:path";
9
9
  import { newId } from "./db.js";
10
+ import { createHostQueryRecorder } from "../harness/route/host-query.js";
11
+ import { groundOnHostEvidence } from "./host-evidence-grounding.js";
10
12
  const MAX_DASHBOARD_BYTES = 256 * 1024;
11
13
  const MAX_NAME_LENGTH = 120;
12
14
  const MAX_IDEMPOTENCY_KEY_LENGTH = 128;
@@ -14,6 +16,7 @@ const ANSWER_REFERENCE_PATTERN = /^answer-[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{
14
16
  export const NATIVE_MCP_CREDENTIAL_ENV_VAR = "WARBLE_MCP_CONNECTION_CREDENTIAL";
15
17
  export const NATIVE_MCP_TOOL_NAME = "save_dashboard";
16
18
  export const NATIVE_MCP_PERSIST_ANSWER_TOOL_NAME = "persist_answer";
19
+ export const NATIVE_MCP_QUERY_TOOL_NAME = "query";
17
20
  export const NATIVE_MCP_SERVER_LABEL = "GenBI MCP";
18
21
  export const NATIVE_MCP_ARTIFACTS_LABEL = "GenBI Artifacts";
19
22
  // Keep this JSON Schema compatible with the ECMA-262 regular-expression
@@ -53,6 +56,8 @@ const POSITIONAL_ROWS_SCHEMA = {
53
56
  items: { type: "array", items: SCALAR_SCHEMA },
54
57
  };
55
58
  /** The exact public MCP shape advertised to native clients and accepted by the host. */
59
+ const VERIFIED_FIELD_SCHEMA = { type: "boolean", description: "Ignored: the host decides verified itself. It records verified only for an answer it grounded: a grounded component run, or an answer whose every definition cites a query this session ran through the query tool. Anything submitted here is not a claim the host keeps." };
60
+ const QUERY_ID_SCHEMA = { ...SAFE_TEXT_SCHEMA, minLength: 1, maxLength: 64, description: "Optional: the query_id the query tool returned for the query this definition describes." };
56
61
  export const NATIVE_SAVE_DASHBOARD_INPUT_SCHEMA = {
57
62
  type: "object",
58
63
  additionalProperties: false,
@@ -67,18 +72,18 @@ export const NATIVE_SAVE_DASHBOARD_INPUT_SCHEMA = {
67
72
  name: { ...SAFE_TEXT_SCHEMA, minLength: 1, maxLength: MAX_NAME_LENGTH, pattern: "\\S" },
68
73
  idempotency_key: { ...SAFE_TEXT_SCHEMA, minLength: 8, maxLength: MAX_IDEMPOTENCY_KEY_LENGTH, pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]+$" },
69
74
  answer_ref: { ...SAFE_TEXT_SCHEMA, pattern: "^answer-[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", description: "Opaque reference returned by persist_answer for this native session." },
70
- answer_selection: { const: "latest", description: "Resolve the newest verified answer retained by this native session when its opaque answer_ref is no longer available in the conversation." },
75
+ answer_selection: { const: "latest", description: "Resolve the newest answer retained by this native session when its opaque answer_ref is no longer available in the conversation." },
71
76
  envelope: {
72
77
  type: "object", additionalProperties: false, required: ["blocks", "verified"],
73
78
  properties: {
74
- verified: { const: true }, summary: NULLABLE_SAFE_TEXT_SCHEMA, estimate: { type: ["boolean", "null"] },
79
+ verified: VERIFIED_FIELD_SCHEMA, summary: NULLABLE_SAFE_TEXT_SCHEMA, estimate: { type: ["boolean", "null"] },
75
80
  blocks: {
76
81
  type: "array", minItems: 1,
77
82
  items: { oneOf: [
78
83
  { type: "object", additionalProperties: false, required: ["type", "label", "value"], properties: { type: { const: "kpi_card" }, label: SAFE_TEXT_SCHEMA, value: SAFE_TEXT_OR_NUMBER_SCHEMA, unit: NULLABLE_SAFE_TEXT_SCHEMA, delta: { type: ["number", "null"] } } },
79
84
  { type: "object", additionalProperties: false, required: ["type", "columns", "rows"], properties: { type: { const: "table" }, columns: { type: "array", minItems: 1, uniqueItems: true, items: { ...SAFE_TEXT_SCHEMA, minLength: 1 } }, rows: { ...POSITIONAL_ROWS_SCHEMA, description: NATIVE_SAVE_DASHBOARD_SEMANTIC_CONSTRAINTS.table.positionalRows } } },
80
85
  { type: "object", additionalProperties: false, required: ["type", "chart_type", "x", "series", "rows"], properties: { type: { const: "chart" }, chart_type: { enum: ["bar", "line", "pie", "area", "scatter"] }, x: { ...SAFE_TEXT_SCHEMA, minLength: 1, description: NATIVE_SAVE_DASHBOARD_SEMANTIC_CONSTRAINTS.chart.x }, series: { type: "array", minItems: 1, uniqueItems: true, description: NATIVE_SAVE_DASHBOARD_SEMANTIC_CONSTRAINTS.chart.series, items: { ...SAFE_TEXT_SCHEMA, minLength: 1 } }, rows: { ...POSITIONAL_ROWS_SCHEMA, description: NATIVE_SAVE_DASHBOARD_SEMANTIC_CONSTRAINTS.chart.positionalRows } } },
81
- { type: "object", additionalProperties: false, required: ["type", "sql", "source_tables", "filters"], properties: { type: { const: "definition" }, sql: { ...SAFE_TEXT_SCHEMA, minLength: 1 }, source_tables: { type: "array", minItems: 1, items: { ...SAFE_TEXT_SCHEMA, minLength: 1 } }, filters: { type: "array", items: SAFE_TEXT_SCHEMA } } },
86
+ { type: "object", additionalProperties: false, required: ["type", "sql", "source_tables", "filters"], properties: { type: { const: "definition" }, sql: { ...SAFE_TEXT_SCHEMA, minLength: 1 }, source_tables: { type: "array", minItems: 1, items: { ...SAFE_TEXT_SCHEMA, minLength: 1 } }, filters: { type: "array", items: SAFE_TEXT_SCHEMA }, query_id: QUERY_ID_SCHEMA } },
82
87
  { type: "object", additionalProperties: false, required: ["type", "text"], properties: { type: { const: "narrative" }, text: { ...SAFE_TEXT_SCHEMA, minLength: 1 }, title: NULLABLE_SAFE_TEXT_SCHEMA } },
83
88
  ] },
84
89
  },
@@ -97,7 +102,7 @@ const NATIVE_STRUCTURED_ANSWER_ENVELOPE_SCHEMA = {
97
102
  additionalProperties: false,
98
103
  required: ["blocks", "verified"],
99
104
  properties: {
100
- verified: { const: true },
105
+ verified: VERIFIED_FIELD_SCHEMA,
101
106
  blocks: {
102
107
  type: "array",
103
108
  minItems: 1,
@@ -162,6 +167,30 @@ export const NATIVE_PERSIST_ANSWER_CONTRACT = {
162
167
  inputSchema: NATIVE_PERSIST_ANSWER_INPUT_SCHEMA,
163
168
  semanticConstraint: "The envelope contains exactly one typed table block and at most one typed definition block, with no presentation-only fields.",
164
169
  };
170
+ /** The session `query` tool: one read-only SQL query run by the host through governed access. */
171
+ export const NATIVE_QUERY_INPUT_SCHEMA = {
172
+ type: "object",
173
+ additionalProperties: false,
174
+ required: ["sql"],
175
+ properties: {
176
+ sql: { type: "string", minLength: 1, description: "One read-only SELECT over the bound semantic models." },
177
+ limit: { type: "integer", minimum: 1, description: "Maximum rows to return (default 1000)." },
178
+ },
179
+ };
180
+ /** The sentence the model sees per host error class; nothing from the governed transport is forwarded. */
181
+ const NATIVE_QUERY_ERROR_TEXT = {
182
+ invalid_request: "The request did not match the query contract: pass a non-empty sql string and an optional positive integer limit.",
183
+ model_not_found: "The query references a table that is not a model in the bound semantic context; query only the models it defines.",
184
+ policy_rejected: "The read-only analytical policy rejected the query: use one SELECT that reads at least one bound model.",
185
+ invalid_sql: "The SQL could not be parsed or planned against the semantic context.",
186
+ execution_failed: "The data source rejected the query at execution time, for example an unknown column or a type mismatch.",
187
+ timeout: "The query exceeded the statement time limit.",
188
+ datasource_unavailable: "The bound data source could not be reached.",
189
+ result_too_large: "The result exceeded the governed byte limit; narrow the query.",
190
+ budget_exhausted: "This session's query budget is exhausted; answer from the results you already have.",
191
+ internal: "The governed query failed for an unclassified reason.",
192
+ transport: "The host query service is unavailable.",
193
+ };
165
194
  function rejectsExecutableText(value) {
166
195
  if (typeof value === "string")
167
196
  return /<\s*\/?(?:script|html)\b|javascript\s*:/i.test(value);
@@ -201,7 +230,7 @@ function validBlock(block) {
201
230
  // Existing object rows remain exactly bound to x + series.
202
231
  : isRecord(row) && scalar(row[x]) && series.every((item) => scalar(row[item])) && Object.keys(row).every((key) => key === x || series.includes(key)));
203
232
  case "definition":
204
- return onlyKeys(block, ["type", "sql", "source_tables", "filters"]) && typeof block.sql === "string" && block.sql.length > 0 && Array.isArray(block.source_tables) && block.source_tables.length > 0 && block.source_tables.every((source) => typeof source === "string" && source.length > 0) && Array.isArray(block.filters) && block.filters.every((filter) => typeof filter === "string");
233
+ return (onlyKeys(block, ["type", "sql", "source_tables", "filters"]) || (onlyKeys(block, ["type", "sql", "source_tables", "filters", "query_id"]) && typeof block.query_id === "string" && block.query_id.length > 0 && block.query_id.length <= 64)) && typeof block.sql === "string" && block.sql.length > 0 && Array.isArray(block.source_tables) && block.source_tables.length > 0 && block.source_tables.every((source) => typeof source === "string" && source.length > 0) && Array.isArray(block.filters) && block.filters.every((filter) => typeof filter === "string");
205
234
  case "narrative":
206
235
  return (onlyKeys(block, ["type", "text"]) || onlyKeys(block, ["type", "text", "title"])) && typeof block.text === "string" && block.text.length > 0 && (block.title === undefined || block.title === null || typeof block.title === "string");
207
236
  default:
@@ -211,8 +240,8 @@ function validBlock(block) {
211
240
  function validateDashboardEnvelope(envelope) {
212
241
  if (!isRecord(envelope) || !Object.hasOwn(envelope, "blocks") || !Object.hasOwn(envelope, "verified") || !Object.keys(envelope).every((key) => ["blocks", "summary", "verified", "estimate"].includes(key)))
213
242
  throw new NativeArtifactError("invalid dashboard render envelope: expected blocks and verified with optional summary or estimate");
214
- if (envelope.verified !== true)
215
- throw new NativeArtifactError("invalid dashboard render envelope: verified must be true");
243
+ if (typeof envelope.verified !== "boolean")
244
+ throw new NativeArtifactError("invalid dashboard render envelope: verified must be a boolean");
216
245
  if (!Array.isArray(envelope.blocks) || envelope.blocks.length === 0)
217
246
  throw new NativeArtifactError("invalid dashboard render envelope: blocks must be a non-empty array");
218
247
  const invalidBlock = envelope.blocks.findIndex((block) => !validBlock(block));
@@ -258,6 +287,24 @@ function validateInput(value) {
258
287
  throw new NativeArtifactError("invalid Save to Artifacts payload: answer_selection must be latest");
259
288
  return { version: "1", name: value.name, answer_selection: "latest", idempotency_key: value.idempotency_key };
260
289
  }
290
+ function isValidSaveInput(value) {
291
+ try {
292
+ validateInput(value);
293
+ return true;
294
+ }
295
+ catch {
296
+ return false;
297
+ }
298
+ }
299
+ function isValidPersistedStructuredAnswer(value) {
300
+ try {
301
+ validatePersistedStructuredAnswer(value);
302
+ return true;
303
+ }
304
+ catch {
305
+ return false;
306
+ }
307
+ }
261
308
  function validatePersistedStructuredAnswer(value) {
262
309
  if (!isRecord(value) || !onlyKeys(value, ["version", "idempotency_key", "envelope"]))
263
310
  throw new NativeArtifactError("invalid structured answer payload: expected exactly version, idempotency_key, and envelope");
@@ -275,6 +322,14 @@ function validatePersistedStructuredAnswer(value) {
275
322
  }
276
323
  return { version: "1", idempotency_key: value.idempotency_key, envelope: value.envelope };
277
324
  }
325
+ /**
326
+ * The stored `verified` flag is host provenance, never the caller's claim: the
327
+ * persisted envelope carries exactly the flag the host recorded, so its own
328
+ * badge cannot disagree with the artifact row.
329
+ */
330
+ function withHostVerified(envelope, verified) {
331
+ return { ...envelope, verified };
332
+ }
278
333
  function structuredAnswerDigest(envelopeJson) {
279
334
  return `sha256:${createHash("sha256").update(envelopeJson).digest("hex")}`;
280
335
  }
@@ -296,6 +351,9 @@ function validateStoredStructuredAnswer(source, nativeSessionId) {
296
351
  export class NativeArtifactService {
297
352
  options;
298
353
  credentials = new Map();
354
+ /** One host query recorder per native session: its observations are never shared across sessions. */
355
+ queryRecorders = new Map();
356
+ queryClosing = new Set();
299
357
  constructor(options) {
300
358
  this.options = options;
301
359
  }
@@ -327,7 +385,78 @@ export class NativeArtifactService {
327
385
  revoke(credential) {
328
386
  if (!credential)
329
387
  return;
388
+ const context = this.credentials.get(credential);
330
389
  this.credentials.delete(credential);
390
+ // A session can hold more than one credential (a Codex session's terminal and its component
391
+ // runtime each get one). Its query recorder lives until the session ends: the last credential
392
+ // for the session is revoked, which every session end and `dispose()` do.
393
+ if (context && ![...this.credentials.values()].some((other) => other.session.id === context.session.id))
394
+ this.closeQueries(context.session.id);
395
+ }
396
+ /** Whether native analysis sessions are offered the host `query` tool. */
397
+ queryAvailable() { return this.options.openQueryAccess !== undefined; }
398
+ /** Resolves once every session query recorder closed so far has released its governed access. */
399
+ async queriesClosed() { await Promise.allSettled([...this.queryClosing]); }
400
+ closeQueries(sessionId) {
401
+ const recorder = this.queryRecorders.get(sessionId);
402
+ if (!recorder)
403
+ return;
404
+ this.queryRecorders.delete(sessionId);
405
+ const closing = recorder.close();
406
+ this.queryClosing.add(closing);
407
+ void closing.finally(() => this.queryClosing.delete(closing));
408
+ }
409
+ /**
410
+ * The session `query` tool: runs one read-only query through governed access against the
411
+ * session's bound project, records it as this session's observation and returns its rows with
412
+ * a session-tagged host `query_id` that a later persist_answer or save_dashboard may cite.
413
+ */
414
+ async query(credential, input) {
415
+ const session = this.authorize(credential);
416
+ const binding = this.credentials.get(credential)?.binding;
417
+ const openQueryAccess = this.options.openQueryAccess;
418
+ if (session.purpose !== "analysis" || !binding || !openQueryAccess)
419
+ throw new NativeArtifactError("the query tool is unavailable for this native session", 409);
420
+ const args = isRecord(input) ? input : {};
421
+ if (!isRecord(input) || !Object.keys(input).every((key) => key === "sql" || key === "limit"))
422
+ return { isError: true, text: NATIVE_QUERY_ERROR_TEXT.invalid_request };
423
+ let recorder = this.queryRecorders.get(session.id);
424
+ if (!recorder) {
425
+ recorder = createHostQueryRecorder({ openAccess: (signal) => openQueryAccess(binding, signal) });
426
+ this.queryRecorders.set(session.id, recorder);
427
+ }
428
+ const outcome = await recorder.execute(args.sql, args.limit);
429
+ if ("error" in outcome)
430
+ return { isError: true, text: `Governed Wren query failed [${outcome.error}]: ${NATIVE_QUERY_ERROR_TEXT[outcome.error] ?? NATIVE_QUERY_ERROR_TEXT.internal}` };
431
+ return { isError: false, text: JSON.stringify(outcome.result), result: outcome.result };
432
+ }
433
+ /**
434
+ * The host's grounding decision for a model-written envelope: the same envelope rebuilt from
435
+ * this session's query observations when every query it cites grounds on them (see
436
+ * `groundOnHostEvidence`), else `undefined`. Only this decision reaches a grounded entry from
437
+ * the MCP surface.
438
+ */
439
+ groundedEnvelope(credential, envelope) {
440
+ const context = credential === undefined ? undefined : this.credentials.get(credential);
441
+ const recorder = context ? this.queryRecorders.get(context.session.id) : undefined;
442
+ if (!recorder || !isRecord(envelope))
443
+ return undefined;
444
+ // The host rebuilds only table and definition blocks. Any other block (narrative, chart, KPI)
445
+ // or an `estimate` flag would be dropped by the rebuild, so such an envelope is never grounded:
446
+ // it is kept intact and stored unverified rather than losing content silently.
447
+ if (envelope.estimate !== undefined && envelope.estimate !== null)
448
+ return undefined;
449
+ if (!Array.isArray(envelope.blocks) || envelope.blocks.some((block) => !isRecord(block) || (block.type !== "table" && block.type !== "definition")))
450
+ return undefined;
451
+ const grounded = groundOnHostEvidence(envelope, envelope, recorder.observations());
452
+ if (grounded.verified !== true)
453
+ return undefined;
454
+ // The rebuild emits one table and one definition per cited query. If it has fewer of either
455
+ // than the envelope did (an extra uncited table, say), content would be lost: keep it intact.
456
+ const count = (value, type) => Array.isArray(value.blocks) ? value.blocks.filter((block) => isRecord(block) && block.type === type).length : 0;
457
+ if (count(grounded, "table") !== count(envelope, "table") || count(grounded, "definition") !== count(envelope, "definition"))
458
+ return undefined;
459
+ return grounded;
331
460
  }
332
461
  /** Releases opaque credentials when the owning BFF service shuts down. */
333
462
  dispose() { for (const credential of [...this.credentials.keys()])
@@ -377,11 +506,35 @@ export class NativeArtifactService {
377
506
  * a later follow-up can only reference this stored result, never recompute.
378
507
  */
379
508
  persistAnswer(credential, input) {
509
+ // Authorize before validating, as `persistStructured` does: a bad credential is a 401 and a
510
+ // non-analysis session a 409 whatever the payload.
380
511
  const session = this.authorize(credential);
381
512
  if (session.purpose !== "analysis")
382
513
  throw new NativeArtifactError("structured answer persistence is unavailable for this native session", 409);
383
514
  const payload = validatePersistedStructuredAnswer(input);
384
- const envelopeJson = JSON.stringify(payload.envelope);
515
+ const grounded = this.groundedEnvelope(credential, payload.envelope);
516
+ if (grounded) {
517
+ const candidate = { ...payload, envelope: grounded };
518
+ // A rebuilt envelope the persist contract cannot hold (a truncation note needs a summary,
519
+ // which this contract has no field for) is persisted unverified, as written.
520
+ if (isValidPersistedStructuredAnswer(candidate))
521
+ return this.persistStructured(credential, candidate, true);
522
+ }
523
+ return this.persistStructured(credential, input, false);
524
+ }
525
+ /**
526
+ * Host-only entry for a grounded component root (never exposed over MCP).
527
+ * The caller must already have checked the component's verified provenance.
528
+ */
529
+ persistGroundedAnswer(credential, input) {
530
+ return this.persistStructured(credential, input, true);
531
+ }
532
+ persistStructured(credential, input, grounded) {
533
+ const session = this.authorize(credential);
534
+ if (session.purpose !== "analysis")
535
+ throw new NativeArtifactError("structured answer persistence is unavailable for this native session", 409);
536
+ const payload = validatePersistedStructuredAnswer(input);
537
+ const envelopeJson = JSON.stringify(withHostVerified(payload.envelope, grounded));
385
538
  const digest = structuredAnswerDigest(envelopeJson);
386
539
  const existing = this.options.store.getNativeStructuredAnswerByIdempotency(session.id, payload.idempotency_key);
387
540
  if (existing) {
@@ -392,7 +545,7 @@ export class NativeArtifactService {
392
545
  }
393
546
  try {
394
547
  const persisted = (this.options.persistStructuredAnswer ?? ((params) => this.options.store.createNativeStructuredAnswer(params)))({
395
- id: newId("answer"), nativeSessionId: session.id, idempotencyKey: payload.idempotency_key, envelopeJson, digest,
548
+ id: newId("answer"), nativeSessionId: session.id, idempotencyKey: payload.idempotency_key, envelopeJson, digest, grounded,
396
549
  });
397
550
  if (persisted.row.digest !== digest || persisted.row.envelopeJson !== envelopeJson) {
398
551
  throw new NativeArtifactError("structured answer idempotency key is already bound to a different answer", 409);
@@ -409,6 +562,27 @@ export class NativeArtifactService {
409
562
  }
410
563
  }
411
564
  save(credential, input) {
565
+ // Authorize before validating, as `saveArtifact` does.
566
+ this.authorizeSave(credential);
567
+ const payload = validateInput(input);
568
+ // Only an envelope save is grounded here; a save by reference inherits the retained answer's provenance.
569
+ const grounded = "envelope" in payload ? this.groundedEnvelope(credential, payload.envelope) : undefined;
570
+ if (grounded) {
571
+ const candidate = { ...payload, envelope: grounded };
572
+ if (isValidSaveInput(candidate))
573
+ return this.saveArtifact(credential, candidate, true);
574
+ }
575
+ return this.saveArtifact(credential, input, false);
576
+ }
577
+ /**
578
+ * Host-only entry for a grounded component render root (never exposed over
579
+ * MCP). The caller must already have checked verified provenance.
580
+ */
581
+ saveGrounded(credential, input) {
582
+ return this.saveArtifact(credential, input, true);
583
+ }
584
+ /** The credential, session and binding checks every save runs before it reads its payload. */
585
+ authorizeSave(credential) {
412
586
  const context = credential === undefined ? undefined : this.credentials.get(credential);
413
587
  if (!context)
414
588
  throw new NativeArtifactError("GenBI MCP bearer credential is invalid. Restart this native session to refresh its GenBI MCP connection.", 401);
@@ -418,8 +592,11 @@ export class NativeArtifactService {
418
592
  this.revoke(credential);
419
593
  throw new NativeArtifactError("GenBI MCP session binding is stale. Start a new native session.", 409);
420
594
  }
595
+ return { session, binding };
596
+ }
597
+ saveArtifact(credential, input, grounded) {
598
+ const { session, binding } = this.authorizeSave(credential);
421
599
  const payload = validateInput(input);
422
- const referenceSave = "answer_ref" in payload || "answer_selection" in payload;
423
600
  const source = "answer_ref" in payload
424
601
  ? this.options.store.getNativeStructuredAnswer(payload.answer_ref)
425
602
  : "answer_selection" in payload
@@ -431,13 +608,16 @@ export class NativeArtifactService {
431
608
  throw new NativeArtifactError("no persisted answer is available for this native session", 404);
432
609
  if (source)
433
610
  validateStoredStructuredAnswer(source, session.id);
434
- const contents = referenceSave ? source.envelopeJson : JSON.stringify(payload.envelope);
435
- const digest = referenceSave ? source.digest : structuredAnswerDigest(contents);
611
+ // A reference save inherits the retained answer's provenance; an envelope
612
+ // save is verified only through the host-only grounded entry.
613
+ const verified = source ? source.grounded : grounded;
614
+ const contents = JSON.stringify(withHostVerified(source ? JSON.parse(source.envelopeJson) : "envelope" in payload ? payload.envelope : undefined, verified));
615
+ const digest = structuredAnswerDigest(contents);
436
616
  const sameSaveRequest = (artifact) => artifact !== undefined &&
437
617
  artifact.name === payload.name.trim() &&
438
618
  artifact.projectIdentity === binding.identity && artifact.bindingGeneration === binding.generation && artifact.projectRevision === binding.revision &&
439
619
  artifact.nativeVendor === session.vendor && artifact.nativeAgent === session.agent &&
440
- artifact.contentDigest === digest && artifact.sourceAnswerId === (source?.id ?? null);
620
+ artifact.contentDigest === digest && artifact.sourceAnswerId === (source?.id ?? null) && artifact.verified === verified;
441
621
  const existing = this.options.store.getNativeArtifactByIdempotency(session.id, payload.idempotency_key);
442
622
  if (existing) {
443
623
  if (!sameSaveRequest(existing))
@@ -450,7 +630,7 @@ export class NativeArtifactService {
450
630
  try {
451
631
  const safeTarget = assertSafeContainedTarget(this.options.artifactsRoot, target);
452
632
  (this.options.writeAtomic ?? writeContainedAtomically)(safeTarget, contents);
453
- const inserted = this.options.store.createNativeArtifact({ id, sessionId: session.id, nativeSessionId: session.id, name: payload.name.trim(), location, projectIdentity: binding.identity, bindingGeneration: binding.generation, projectRevision: binding.revision, vendor: session.vendor, agent: session.agent, digest, idempotencyKey: payload.idempotency_key, ...(source ? { sourceAnswerId: source.id } : {}) });
633
+ const inserted = this.options.store.createNativeArtifact({ id, sessionId: session.id, nativeSessionId: session.id, name: payload.name.trim(), location, projectIdentity: binding.identity, bindingGeneration: binding.generation, projectRevision: binding.revision, vendor: session.vendor, agent: session.agent, digest, idempotencyKey: payload.idempotency_key, verified, ...(source ? { sourceAnswerId: source.id } : {}) });
454
634
  if (!inserted.created && !sameSaveRequest(inserted.row)) {
455
635
  rmSync(target, { force: true });
456
636
  throw new NativeArtifactError("artifact idempotency key is already bound to a different save request", 409);
@@ -0,0 +1,19 @@
1
+ import { type CodexSessionTools } from "./runtime-host/codex-direct-tools.js";
2
+ import { type NativeComponentRuntimeOptions } from "./native-component-runtime.js";
3
+ import type { NativeComponentPreparation } from "./native-components.js";
4
+ export interface NativeComponentContextOptions extends Omit<NativeComponentRuntimeOptions, "contexts" | "wren"> {
5
+ /** All executables and the provider are resolved by the certified server provisioner. */
6
+ readonly contextLoaderBinary: string;
7
+ /** Explicit host-only credential environment supplied by the provisioner. */
8
+ readonly wrenEnvironment?: Readonly<NodeJS.ProcessEnv>;
9
+ readonly wrenBinary: string;
10
+ readonly project: string;
11
+ readonly signal: AbortSignal;
12
+ }
13
+ /** Capture file-only context before host admission; never opens a vendor or Wren process. */
14
+ export declare function prepareCapturedNativeComponentHost(irDocument: string, scope: Readonly<Record<string, unknown>>, options: NativeComponentContextOptions): Promise<NativeComponentPreparation | undefined>;
15
+ /** Direct-session variant shares the same captured project/credential provenance. */
16
+ export declare function prepareCapturedCodexDirectTools(irDocument: string, scope: Readonly<Record<string, unknown>>, options: NativeComponentContextOptions & {
17
+ readonly producerBinary: string;
18
+ readonly expiresAt: number;
19
+ }): Promise<CodexSessionTools>;