@wrenai/genbi 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +141 -0
  3. package/bin/genbi.mjs +31 -0
  4. package/dist/assets/echarts-LTFC-ysd.js +40 -0
  5. package/dist/assets/index-Bo--QMUN.js +441 -0
  6. package/dist/assets/index-Xs4q7pNo.css +1 -0
  7. package/dist/index.html +14 -0
  8. package/dist/wrenai_logo.png +0 -0
  9. package/dist/wrenai_logo_white.png +0 -0
  10. package/dist-server/harness/auth/detect.d.ts +18 -0
  11. package/dist-server/harness/auth/detect.js +37 -0
  12. package/dist-server/harness/auth/index.d.ts +4 -0
  13. package/dist-server/harness/auth/index.js +2 -0
  14. package/dist-server/harness/auth/probe.d.ts +18 -0
  15. package/dist-server/harness/auth/probe.js +52 -0
  16. package/dist-server/harness/auth/types.d.ts +42 -0
  17. package/dist-server/harness/auth/types.js +9 -0
  18. package/dist-server/harness/bundle/loader.d.ts +32 -0
  19. package/dist-server/harness/bundle/loader.js +43 -0
  20. package/dist-server/harness/bundle/schema.d.ts +199 -0
  21. package/dist-server/harness/bundle/schema.js +127 -0
  22. package/dist-server/harness/bundle/version.d.ts +10 -0
  23. package/dist-server/harness/bundle/version.js +43 -0
  24. package/dist-server/harness/capability/gate.d.ts +6 -0
  25. package/dist-server/harness/capability/gate.js +29 -0
  26. package/dist-server/harness/capability/registry.d.ts +6 -0
  27. package/dist-server/harness/capability/registry.js +17 -0
  28. package/dist-server/harness/cli-args.d.ts +123 -0
  29. package/dist-server/harness/cli-args.js +247 -0
  30. package/dist-server/harness/cli.d.ts +2 -0
  31. package/dist-server/harness/cli.js +151 -0
  32. package/dist-server/harness/compile/cache.d.ts +23 -0
  33. package/dist-server/harness/compile/cache.js +109 -0
  34. package/dist-server/harness/compile/compose-profile.d.ts +34 -0
  35. package/dist-server/harness/compile/compose-profile.js +95 -0
  36. package/dist-server/harness/compile/errors.d.ts +22 -0
  37. package/dist-server/harness/compile/errors.js +41 -0
  38. package/dist-server/harness/compile/fingerprint.d.ts +22 -0
  39. package/dist-server/harness/compile/fingerprint.js +84 -0
  40. package/dist-server/harness/compile/index.d.ts +8 -0
  41. package/dist-server/harness/compile/index.js +7 -0
  42. package/dist-server/harness/compile/pipeline.d.ts +66 -0
  43. package/dist-server/harness/compile/pipeline.js +176 -0
  44. package/dist-server/harness/compile/resolve-binary.d.ts +55 -0
  45. package/dist-server/harness/compile/resolve-binary.js +133 -0
  46. package/dist-server/harness/compile/types.d.ts +107 -0
  47. package/dist-server/harness/compile/types.js +1 -0
  48. package/dist-server/harness/compile/warble-identity.d.ts +12 -0
  49. package/dist-server/harness/compile/warble-identity.js +33 -0
  50. package/dist-server/harness/compliance/gate.d.ts +35 -0
  51. package/dist-server/harness/compliance/gate.js +48 -0
  52. package/dist-server/harness/compliance/index.d.ts +2 -0
  53. package/dist-server/harness/compliance/index.js +1 -0
  54. package/dist-server/harness/compliance/types.d.ts +16 -0
  55. package/dist-server/harness/compliance/types.js +1 -0
  56. package/dist-server/harness/events/emitter.d.ts +34 -0
  57. package/dist-server/harness/events/emitter.js +25 -0
  58. package/dist-server/harness/events/index.d.ts +4 -0
  59. package/dist-server/harness/events/index.js +2 -0
  60. package/dist-server/harness/events/sse.d.ts +20 -0
  61. package/dist-server/harness/events/sse.js +48 -0
  62. package/dist-server/harness/events/types.d.ts +131 -0
  63. package/dist-server/harness/events/types.js +1 -0
  64. package/dist-server/harness/exec/errors.d.ts +16 -0
  65. package/dist-server/harness/exec/errors.js +29 -0
  66. package/dist-server/harness/exec/index.d.ts +5 -0
  67. package/dist-server/harness/exec/index.js +2 -0
  68. package/dist-server/harness/exec/local.d.ts +32 -0
  69. package/dist-server/harness/exec/local.js +134 -0
  70. package/dist-server/harness/exec/policy.d.ts +12 -0
  71. package/dist-server/harness/exec/policy.js +1 -0
  72. package/dist-server/harness/exec/types.d.ts +63 -0
  73. package/dist-server/harness/exec/types.js +1 -0
  74. package/dist-server/harness/guardrails/index.d.ts +2 -0
  75. package/dist-server/harness/guardrails/index.js +1 -0
  76. package/dist-server/harness/guardrails/policy.d.ts +45 -0
  77. package/dist-server/harness/guardrails/policy.js +57 -0
  78. package/dist-server/harness/index.d.ts +37 -0
  79. package/dist-server/harness/index.js +19 -0
  80. package/dist-server/harness/loop/errors.d.ts +53 -0
  81. package/dist-server/harness/loop/errors.js +72 -0
  82. package/dist-server/harness/loop/executor.d.ts +123 -0
  83. package/dist-server/harness/loop/executor.js +380 -0
  84. package/dist-server/harness/loop/guard.d.ts +16 -0
  85. package/dist-server/harness/loop/guard.js +1 -0
  86. package/dist-server/harness/loop/index.d.ts +5 -0
  87. package/dist-server/harness/loop/index.js +3 -0
  88. package/dist-server/harness/native-producer/index.d.ts +109 -0
  89. package/dist-server/harness/native-producer/index.js +302 -0
  90. package/dist-server/harness/npm-package-resolve.d.ts +28 -0
  91. package/dist-server/harness/npm-package-resolve.js +47 -0
  92. package/dist-server/harness/providers/adapters/anthropic.d.ts +7 -0
  93. package/dist-server/harness/providers/adapters/anthropic.js +6 -0
  94. package/dist-server/harness/providers/adapters/mock.d.ts +4 -0
  95. package/dist-server/harness/providers/adapters/mock.js +5 -0
  96. package/dist-server/harness/providers/adapters/openai-compatible.d.ts +10 -0
  97. package/dist-server/harness/providers/adapters/openai-compatible.js +10 -0
  98. package/dist-server/harness/providers/binding.d.ts +23 -0
  99. package/dist-server/harness/providers/binding.js +13 -0
  100. package/dist-server/harness/providers/errors.d.ts +6 -0
  101. package/dist-server/harness/providers/errors.js +12 -0
  102. package/dist-server/harness/providers/index.d.ts +18 -0
  103. package/dist-server/harness/providers/index.js +22 -0
  104. package/dist-server/harness/providers/registry.d.ts +16 -0
  105. package/dist-server/harness/providers/registry.js +24 -0
  106. package/dist-server/harness/render/envelope.d.ts +145 -0
  107. package/dist-server/harness/render/envelope.js +428 -0
  108. package/dist-server/harness/render/errors.d.ts +33 -0
  109. package/dist-server/harness/render/errors.js +46 -0
  110. package/dist-server/harness/render/index.d.ts +5 -0
  111. package/dist-server/harness/render/index.js +3 -0
  112. package/dist-server/harness/render/validate.d.ts +13 -0
  113. package/dist-server/harness/render/validate.js +88 -0
  114. package/dist-server/harness/route/adapter-spec.d.ts +40 -0
  115. package/dist-server/harness/route/adapter-spec.js +61 -0
  116. package/dist-server/harness/route/agent-sdk-cli.d.ts +30 -0
  117. package/dist-server/harness/route/agent-sdk-cli.js +86 -0
  118. package/dist-server/harness/route/agent-sdk-manifest.d.ts +31 -0
  119. package/dist-server/harness/route/agent-sdk-manifest.js +56 -0
  120. package/dist-server/harness/route/artifact-content.d.ts +53 -0
  121. package/dist-server/harness/route/artifact-content.js +83 -0
  122. package/dist-server/harness/route/chat-event-mapper.d.ts +95 -0
  123. package/dist-server/harness/route/chat-event-mapper.js +164 -0
  124. package/dist-server/harness/route/codex-ask-events.d.ts +25 -0
  125. package/dist-server/harness/route/codex-ask-events.js +395 -0
  126. package/dist-server/harness/route/codex-ask.d.ts +15 -0
  127. package/dist-server/harness/route/codex-ask.js +242 -0
  128. package/dist-server/harness/route/codex-local-cli.d.ts +20 -0
  129. package/dist-server/harness/route/codex-local-cli.js +78 -0
  130. package/dist-server/harness/route/codex-local-manifest.d.ts +22 -0
  131. package/dist-server/harness/route/codex-local-manifest.js +180 -0
  132. package/dist-server/harness/route/describe.d.ts +54 -0
  133. package/dist-server/harness/route/describe.js +84 -0
  134. package/dist-server/harness/route/dispatched.d.ts +138 -0
  135. package/dist-server/harness/route/dispatched.js +339 -0
  136. package/dist-server/harness/route/in-process.d.ts +43 -0
  137. package/dist-server/harness/route/in-process.js +106 -0
  138. package/dist-server/harness/route/index.d.ts +19 -0
  139. package/dist-server/harness/route/index.js +12 -0
  140. package/dist-server/harness/route/profile-source.d.ts +37 -0
  141. package/dist-server/harness/route/profile-source.js +76 -0
  142. package/dist-server/harness/route/route.d.ts +33 -0
  143. package/dist-server/harness/route/route.js +114 -0
  144. package/dist-server/harness/route/tier-binding.d.ts +36 -0
  145. package/dist-server/harness/route/tier-binding.js +63 -0
  146. package/dist-server/harness/route/types.d.ts +286 -0
  147. package/dist-server/harness/route/types.js +1 -0
  148. package/dist-server/harness/session/errors.d.ts +4 -0
  149. package/dist-server/harness/session/errors.js +7 -0
  150. package/dist-server/harness/session/gate.d.ts +11 -0
  151. package/dist-server/harness/session/gate.js +13 -0
  152. package/dist-server/harness/session/index.d.ts +5 -0
  153. package/dist-server/harness/session/index.js +3 -0
  154. package/dist-server/harness/session/run.d.ts +60 -0
  155. package/dist-server/harness/session/run.js +333 -0
  156. package/dist-server/harness/session/types.d.ts +29 -0
  157. package/dist-server/harness/session/types.js +1 -0
  158. package/dist-server/harness/setup/codex-events.d.ts +12 -0
  159. package/dist-server/harness/setup/codex-events.js +108 -0
  160. package/dist-server/harness/setup/runner.d.ts +497 -0
  161. package/dist-server/harness/setup/runner.js +1314 -0
  162. package/dist-server/harness/tools/errors.d.ts +107 -0
  163. package/dist-server/harness/tools/errors.js +141 -0
  164. package/dist-server/harness/tools/index.d.ts +13 -0
  165. package/dist-server/harness/tools/index.js +8 -0
  166. package/dist-server/harness/tools/mcp.d.ts +26 -0
  167. package/dist-server/harness/tools/mcp.js +15 -0
  168. package/dist-server/harness/tools/native.d.ts +179 -0
  169. package/dist-server/harness/tools/native.js +322 -0
  170. package/dist-server/harness/tools/parse.d.ts +14 -0
  171. package/dist-server/harness/tools/parse.js +18 -0
  172. package/dist-server/harness/tools/resolve-wren-binary.d.ts +12 -0
  173. package/dist-server/harness/tools/resolve-wren-binary.js +21 -0
  174. package/dist-server/harness/tools/resolve.d.ts +51 -0
  175. package/dist-server/harness/tools/resolve.js +79 -0
  176. package/dist-server/harness/tools/setup-native.d.ts +84 -0
  177. package/dist-server/harness/tools/setup-native.js +353 -0
  178. package/dist-server/harness/tools/wiring.d.ts +10 -0
  179. package/dist-server/harness/tools/wiring.js +16 -0
  180. package/dist-server/providers/setup.provider.yaml +31 -0
  181. package/dist-server/providers/wren.provider.yaml +33 -0
  182. package/dist-server/server/adopt.d.ts +87 -0
  183. package/dist-server/server/adopt.js +263 -0
  184. package/dist-server/server/app.d.ts +6 -0
  185. package/dist-server/server/app.js +2521 -0
  186. package/dist-server/server/auth-choice.d.ts +16 -0
  187. package/dist-server/server/auth-choice.js +36 -0
  188. package/dist-server/server/bin.d.ts +2 -0
  189. package/dist-server/server/bin.js +601 -0
  190. package/dist-server/server/bootstrap-project-binding.d.ts +31 -0
  191. package/dist-server/server/bootstrap-project-binding.js +71 -0
  192. package/dist-server/server/clarify.d.ts +10 -0
  193. package/dist-server/server/clarify.js +17 -0
  194. package/dist-server/server/codex-setup-mcp.d.ts +2 -0
  195. package/dist-server/server/codex-setup-mcp.js +147 -0
  196. package/dist-server/server/compose.d.ts +89 -0
  197. package/dist-server/server/compose.js +271 -0
  198. package/dist-server/server/conn-config.d.ts +30 -0
  199. package/dist-server/server/conn-config.js +253 -0
  200. package/dist-server/server/context-files.d.ts +12 -0
  201. package/dist-server/server/context-files.js +117 -0
  202. package/dist-server/server/context-map.d.ts +30 -0
  203. package/dist-server/server/context-map.js +95 -0
  204. package/dist-server/server/context-source.d.ts +68 -0
  205. package/dist-server/server/context-source.js +85 -0
  206. package/dist-server/server/db.d.ts +862 -0
  207. package/dist-server/server/db.js +1839 -0
  208. package/dist-server/server/enrichment-runner.d.ts +184 -0
  209. package/dist-server/server/enrichment-runner.js +213 -0
  210. package/dist-server/server/enrichment-verify.d.ts +73 -0
  211. package/dist-server/server/enrichment-verify.js +290 -0
  212. package/dist-server/server/enrichment.d.ts +232 -0
  213. package/dist-server/server/enrichment.js +199 -0
  214. package/dist-server/server/env-detect.d.ts +8 -0
  215. package/dist-server/server/env-detect.js +17 -0
  216. package/dist-server/server/env-field-hints.d.ts +46 -0
  217. package/dist-server/server/env-field-hints.js +48 -0
  218. package/dist-server/server/fold.d.ts +133 -0
  219. package/dist-server/server/fold.js +407 -0
  220. package/dist-server/server/harness-profile-sources.d.ts +3 -0
  221. package/dist-server/server/harness-profile-sources.js +24 -0
  222. package/dist-server/server/harness.d.ts +19 -0
  223. package/dist-server/server/harness.js +439 -0
  224. package/dist-server/server/impact.d.ts +18 -0
  225. package/dist-server/server/impact.js +85 -0
  226. package/dist-server/server/interactive-terminal.d.ts +175 -0
  227. package/dist-server/server/interactive-terminal.js +336 -0
  228. package/dist-server/server/native-artifacts.d.ts +1004 -0
  229. package/dist-server/server/native-artifacts.js +564 -0
  230. package/dist-server/server/native-dispatch-registry.d.ts +42 -0
  231. package/dist-server/server/native-dispatch-registry.js +45 -0
  232. package/dist-server/server/native-producer-fence.d.ts +14 -0
  233. package/dist-server/server/native-producer-fence.js +48 -0
  234. package/dist-server/server/native-session-resume.d.ts +6 -0
  235. package/dist-server/server/native-session-resume.js +103 -0
  236. package/dist-server/server/native-session-workspace.d.ts +40 -0
  237. package/dist-server/server/native-session-workspace.js +175 -0
  238. package/dist-server/server/native-sessions.d.ts +374 -0
  239. package/dist-server/server/native-sessions.js +1517 -0
  240. package/dist-server/server/native-wren-runtime.d.ts +20 -0
  241. package/dist-server/server/native-wren-runtime.js +259 -0
  242. package/dist-server/server/node-pty-host.d.ts +31 -0
  243. package/dist-server/server/node-pty-host.js +100 -0
  244. package/dist-server/server/route-intent.d.ts +40 -0
  245. package/dist-server/server/route-intent.js +23 -0
  246. package/dist-server/server/runtime-binding.d.ts +52 -0
  247. package/dist-server/server/runtime-binding.js +258 -0
  248. package/dist-server/server/source-catalog.d.ts +84 -0
  249. package/dist-server/server/source-catalog.js +221 -0
  250. package/dist-server/server/source-driver.d.ts +35 -0
  251. package/dist-server/server/source-driver.js +124 -0
  252. package/dist-server/server/spa.d.ts +2 -0
  253. package/dist-server/server/spa.js +51 -0
  254. package/dist-server/server/subscription-model-catalog.d.ts +27 -0
  255. package/dist-server/server/subscription-model-catalog.js +203 -0
  256. package/dist-server/server/turn.d.ts +249 -0
  257. package/dist-server/server/turn.js +1074 -0
  258. package/dist-server/server/wire-types.d.ts +799 -0
  259. package/dist-server/server/wire-types.js +1 -0
  260. package/dist-server/server/wren-home.d.ts +19 -0
  261. package/dist-server/server/wren-home.js +33 -0
  262. package/dist-server/server/wren-profiles.d.ts +43 -0
  263. package/dist-server/server/wren-profiles.js +123 -0
  264. package/package.json +93 -0
  265. package/profiles/genbi-default/context/binding.yml +9 -0
  266. package/profiles/genbi-default/ir.golden.json +506 -0
  267. package/profiles/genbi-default/profile.yml +16 -0
  268. package/profiles/genbi-enrich-context/components/apply_enrichment/component.yml +42 -0
  269. package/profiles/genbi-enrich-context/components/draft_enrichment/component.yml +30 -0
  270. package/profiles/genbi-enrich-context/components/draft_enrichment/steps/draft.md +34 -0
  271. package/profiles/genbi-enrich-context/components/inspect_context/component.yml +32 -0
  272. package/profiles/genbi-enrich-context/components/inspect_context/steps/inspect.md +13 -0
  273. package/profiles/genbi-enrich-context/context/binding.yml +4 -0
  274. package/profiles/genbi-enrich-context/ir.golden.json +302 -0
  275. package/profiles/genbi-enrich-context/profile.yml +15 -0
  276. package/profiles/genbi-monitor/context/binding.yml +7 -0
  277. package/profiles/genbi-monitor/ir.golden.json +188 -0
  278. package/profiles/genbi-monitor/profile.yml +27 -0
  279. package/profiles/genbi-setup/components/build_context/component.yml +44 -0
  280. package/profiles/genbi-setup/components/build_context/steps/build.md +14 -0
  281. package/profiles/genbi-setup/components/connect_source/component.yml +51 -0
  282. package/profiles/genbi-setup/components/connect_source/steps/connect.md +23 -0
  283. package/profiles/genbi-setup/context/binding.yml +8 -0
  284. package/profiles/genbi-setup/ir.golden.json +140 -0
  285. package/profiles/genbi-setup/profile.yml +18 -0
  286. package/profiles/genbi-setup/raw/schema.json +5 -0
@@ -0,0 +1,564 @@
1
+ /**
2
+ * Native-session Save to Artifacts boundary. The MCP route is deliberately
3
+ * thin: this service owns every validation and persistence decision so a
4
+ * future typed HTTP caller cannot drift from the agent-facing contract.
5
+ */
6
+ import { createHash, randomUUID } from "node:crypto";
7
+ import { lstatSync, mkdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
8
+ import path from "node:path";
9
+ import { newId } from "./db.js";
10
+ const MAX_DASHBOARD_BYTES = 256 * 1024;
11
+ const MAX_NAME_LENGTH = 120;
12
+ const MAX_IDEMPOTENCY_KEY_LENGTH = 128;
13
+ const ANSWER_REFERENCE_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}$/;
14
+ export const NATIVE_MCP_CREDENTIAL_ENV_VAR = "WARBLE_MCP_CONNECTION_CREDENTIAL";
15
+ export const NATIVE_MCP_TOOL_NAME = "save_dashboard";
16
+ export const NATIVE_MCP_PERSIST_ANSWER_TOOL_NAME = "persist_answer";
17
+ export const NATIVE_MCP_SERVER_LABEL = "GenBI MCP";
18
+ export const NATIVE_MCP_ARTIFACTS_LABEL = "GenBI Artifacts";
19
+ // Keep this JSON Schema compatible with the ECMA-262 regular-expression
20
+ // dialect required by JSON Schema: JavaScript has no inline case-insensitive
21
+ // flag, so the executable-text fence is spelled out with character classes.
22
+ const EXECUTABLE_TEXT_PATTERN = "<\\s*\\/?(?:[sS][cC][rR][iI][pP][tT]|[hH][tT][mM][lL])\\b|[jJ][aA][vV][aA][sS][cC][rR][iI][pP][tT]\\s*:";
23
+ const SAFE_TEXT_SCHEMA = { type: "string", not: { pattern: EXECUTABLE_TEXT_PATTERN } };
24
+ const NULLABLE_SAFE_TEXT_SCHEMA = { anyOf: [SAFE_TEXT_SCHEMA, { type: "null" }] };
25
+ const SAFE_TEXT_OR_NUMBER_SCHEMA = { anyOf: [SAFE_TEXT_SCHEMA, { type: "number" }] };
26
+ const SCALAR_SCHEMA = { anyOf: [SAFE_TEXT_SCHEMA, { type: "number" }, { type: "boolean" }, { type: "null" }] };
27
+ /**
28
+ * Rules that relate sibling fields and therefore cannot be expressed by the
29
+ * JSON Schema vocabulary used by MCP. These descriptions and the executable
30
+ * helpers below form the advertised second validation layer.
31
+ */
32
+ export const NATIVE_SAVE_DASHBOARD_SEMANTIC_CONSTRAINTS = {
33
+ table: {
34
+ positionalRows: "Positional scalar rows in columns order. Each row must contain exactly one cell per columns entry.",
35
+ },
36
+ chart: {
37
+ x: "The x field must not also appear in series.",
38
+ series: "Each series item must differ from x.",
39
+ positionalRows: "Positional scalar rows in [x, ...series] order. Each row must contain exactly one x cell plus one cell per series entry.",
40
+ },
41
+ };
42
+ /**
43
+ * The canonical row transport form is a positional scalar array. A standard
44
+ * JSON Schema cannot make an item-array's length depend on sibling `columns`
45
+ * or `[x, ...series]`, so object-keyed rows are deliberately not advertised.
46
+ * The host still enforces that positional lengths exactly match those sibling
47
+ * fields before persistence.
48
+ * The host continues to accept its previous, exactly bound object-row form
49
+ * for stored-client compatibility.
50
+ */
51
+ const POSITIONAL_ROWS_SCHEMA = {
52
+ type: "array",
53
+ items: { type: "array", items: SCALAR_SCHEMA },
54
+ };
55
+ /** The exact public MCP shape advertised to native clients and accepted by the host. */
56
+ export const NATIVE_SAVE_DASHBOARD_INPUT_SCHEMA = {
57
+ type: "object",
58
+ additionalProperties: false,
59
+ required: ["version", "name", "idempotency_key"],
60
+ oneOf: [
61
+ { required: ["envelope"], not: { anyOf: [{ required: ["answer_ref"] }, { required: ["answer_selection"] }] } },
62
+ { required: ["answer_ref"], not: { anyOf: [{ required: ["envelope"] }, { required: ["answer_selection"] }] } },
63
+ { required: ["answer_selection"], not: { anyOf: [{ required: ["envelope"] }, { required: ["answer_ref"] }] } },
64
+ ],
65
+ properties: {
66
+ version: { anyOf: [{ const: "1" }, { const: 1 }], description: "Use the canonical string literal \"1\". Numeric 1 is accepted for Claude Code compatibility and is canonicalized before persistence." },
67
+ name: { ...SAFE_TEXT_SCHEMA, minLength: 1, maxLength: MAX_NAME_LENGTH, pattern: "\\S" },
68
+ idempotency_key: { ...SAFE_TEXT_SCHEMA, minLength: 8, maxLength: MAX_IDEMPOTENCY_KEY_LENGTH, pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]+$" },
69
+ 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." },
71
+ envelope: {
72
+ type: "object", additionalProperties: false, required: ["blocks", "verified"],
73
+ properties: {
74
+ verified: { const: true }, summary: NULLABLE_SAFE_TEXT_SCHEMA, estimate: { type: ["boolean", "null"] },
75
+ blocks: {
76
+ type: "array", minItems: 1,
77
+ items: { oneOf: [
78
+ { 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
+ { 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
+ { 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 } } },
82
+ { type: "object", additionalProperties: false, required: ["type", "text"], properties: { type: { const: "narrative" }, text: { ...SAFE_TEXT_SCHEMA, minLength: 1 }, title: NULLABLE_SAFE_TEXT_SCHEMA } },
83
+ ] },
84
+ },
85
+ },
86
+ },
87
+ },
88
+ };
89
+ const NATIVE_STRUCTURED_ANSWER_BLOCK_SCHEMA = {
90
+ oneOf: [
91
+ NATIVE_SAVE_DASHBOARD_INPUT_SCHEMA.properties.envelope.properties.blocks.items.oneOf[1],
92
+ NATIVE_SAVE_DASHBOARD_INPUT_SCHEMA.properties.envelope.properties.blocks.items.oneOf[3],
93
+ ],
94
+ };
95
+ const NATIVE_STRUCTURED_ANSWER_ENVELOPE_SCHEMA = {
96
+ type: "object",
97
+ additionalProperties: false,
98
+ required: ["blocks", "verified"],
99
+ properties: {
100
+ verified: { const: true },
101
+ blocks: {
102
+ type: "array",
103
+ minItems: 1,
104
+ maxItems: 2,
105
+ items: NATIVE_STRUCTURED_ANSWER_BLOCK_SCHEMA,
106
+ allOf: [
107
+ { contains: { type: "object", properties: { type: { const: "table" } }, required: ["type"] } },
108
+ ],
109
+ },
110
+ },
111
+ };
112
+ /**
113
+ * The producer persists only the existing answer_query result blocks. The
114
+ * semantic validator below requires exactly one table and allows at most one
115
+ * definition block. It rejects summary/chart/raw/transcript-shaped additions.
116
+ */
117
+ export const NATIVE_PERSIST_ANSWER_INPUT_SCHEMA = {
118
+ type: "object",
119
+ additionalProperties: false,
120
+ required: ["version", "idempotency_key", "envelope"],
121
+ properties: {
122
+ version: { const: "1" },
123
+ idempotency_key: { ...SAFE_TEXT_SCHEMA, minLength: 8, maxLength: MAX_IDEMPOTENCY_KEY_LENGTH, pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]+$", description: "Caller retry key only; the host mints canonical answer provenance." },
124
+ envelope: NATIVE_STRUCTURED_ANSWER_ENVELOPE_SCHEMA,
125
+ },
126
+ };
127
+ function sameBinding(a, b) {
128
+ return b !== undefined && a.path === b.path && a.identity === b.identity && a.generation === b.generation && a.revision === b.revision;
129
+ }
130
+ export class NativeArtifactError extends Error {
131
+ status;
132
+ constructor(message, status = 400) {
133
+ super(message);
134
+ this.status = status;
135
+ }
136
+ }
137
+ function isRecord(value) {
138
+ return typeof value === "object" && value !== null && !Array.isArray(value);
139
+ }
140
+ function onlyKeys(value, keys) {
141
+ return Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
142
+ }
143
+ function scalar(value) {
144
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
145
+ }
146
+ /** Shared executable counterpart of the advertised positional-row rules. */
147
+ export function hasExactNativePositionalRow(row, expectedLength) {
148
+ return Array.isArray(row) && row.length === expectedLength && row.every(scalar);
149
+ }
150
+ /** Shared executable counterpart of the advertised chart x/series rule. */
151
+ export function hasDistinctNativeChartSeries(x, series) {
152
+ return typeof x === "string" && x.length > 0 && Array.isArray(series) && series.length > 0 && series.every((item) => typeof item === "string" && item.length > 0 && item !== x) && new Set(series).size === series.length;
153
+ }
154
+ /** The complete MCP contract: mechanically checked schema plus semantic rules. */
155
+ export const NATIVE_SAVE_DASHBOARD_CONTRACT = {
156
+ inputSchema: NATIVE_SAVE_DASHBOARD_INPUT_SCHEMA,
157
+ semanticConstraints: NATIVE_SAVE_DASHBOARD_SEMANTIC_CONSTRAINTS,
158
+ hasExactPositionalRow: hasExactNativePositionalRow,
159
+ hasDistinctChartSeries: hasDistinctNativeChartSeries,
160
+ };
161
+ export const NATIVE_PERSIST_ANSWER_CONTRACT = {
162
+ inputSchema: NATIVE_PERSIST_ANSWER_INPUT_SCHEMA,
163
+ semanticConstraint: "The envelope contains exactly one typed table block and at most one typed definition block, with no presentation-only fields.",
164
+ };
165
+ function rejectsExecutableText(value) {
166
+ if (typeof value === "string")
167
+ return /<\s*\/?(?:script|html)\b|javascript\s*:/i.test(value);
168
+ if (Array.isArray(value))
169
+ return value.some(rejectsExecutableText);
170
+ return isRecord(value) && Object.entries(value).some(([key, child]) => /(?:^|_)(?:path|file|html)(?:$|_)/i.test(key) || rejectsExecutableText(child));
171
+ }
172
+ function validRows(value, columns) {
173
+ return Array.isArray(value) && value.every((row) => Array.isArray(row)
174
+ // JSON Schema cannot bind length to sibling `columns`, but the host must:
175
+ // ignored trailing cells would weaken the persistence boundary.
176
+ ? NATIVE_SAVE_DASHBOARD_CONTRACT.hasExactPositionalRow(row, columns.length)
177
+ // Object rows remain a private backward-compatible input form and keep
178
+ // the original exact column binding.
179
+ : isRecord(row) && columns.every((column) => scalar(row[column])) && Object.keys(row).every((key) => columns.includes(key)));
180
+ }
181
+ function validBlock(block) {
182
+ if (!isRecord(block) || typeof block.type !== "string")
183
+ return false;
184
+ switch (block.type) {
185
+ case "kpi_card":
186
+ return onlyKeys(block, ["type", "label", "value"]) || onlyKeys(block, ["type", "label", "value", "unit"]) || onlyKeys(block, ["type", "label", "value", "delta"]) || onlyKeys(block, ["type", "label", "value", "unit", "delta"])
187
+ ? typeof block.label === "string" && (typeof block.value === "string" || typeof block.value === "number") && (block.unit === undefined || block.unit === null || typeof block.unit === "string") && (block.delta === undefined || block.delta === null || typeof block.delta === "number")
188
+ : false;
189
+ case "table":
190
+ return onlyKeys(block, ["type", "columns", "rows"]) && Array.isArray(block.columns) && block.columns.length > 0 && block.columns.every((column) => typeof column === "string" && column.length > 0) && new Set(block.columns).size === block.columns.length && validRows(block.rows, block.columns);
191
+ case "chart":
192
+ if (!onlyKeys(block, ["type", "chart_type", "x", "series", "rows"]))
193
+ return false;
194
+ const x = block.x;
195
+ const series = block.series;
196
+ const rows = block.rows;
197
+ return ["bar", "line", "pie", "area", "scatter"].includes(String(block.chart_type)) && typeof x === "string" && NATIVE_SAVE_DASHBOARD_CONTRACT.hasDistinctChartSeries(x, series) && Array.isArray(rows) && rows.every((row) => Array.isArray(row)
198
+ // JSON Schema cannot bind length to sibling x + series, but the host
199
+ // must keep the persisted row exact (never silently ignore a cell).
200
+ ? NATIVE_SAVE_DASHBOARD_CONTRACT.hasExactPositionalRow(row, series.length + 1)
201
+ // Existing object rows remain exactly bound to x + series.
202
+ : isRecord(row) && scalar(row[x]) && series.every((item) => scalar(row[item])) && Object.keys(row).every((key) => key === x || series.includes(key)));
203
+ 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");
205
+ case "narrative":
206
+ 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
+ default:
208
+ return false;
209
+ }
210
+ }
211
+ function validateDashboardEnvelope(envelope) {
212
+ if (!isRecord(envelope) || !Object.hasOwn(envelope, "blocks") || !Object.hasOwn(envelope, "verified") || !Object.keys(envelope).every((key) => ["blocks", "summary", "verified", "estimate"].includes(key)))
213
+ 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");
216
+ if (!Array.isArray(envelope.blocks) || envelope.blocks.length === 0)
217
+ throw new NativeArtifactError("invalid dashboard render envelope: blocks must be a non-empty array");
218
+ const invalidBlock = envelope.blocks.findIndex((block) => !validBlock(block));
219
+ if (invalidBlock !== -1)
220
+ throw new NativeArtifactError(`invalid dashboard render envelope: block ${invalidBlock + 1} has an unsupported type, field, or row shape`);
221
+ if (envelope.summary !== undefined && envelope.summary !== null && typeof envelope.summary !== "string")
222
+ throw new NativeArtifactError("invalid dashboard render envelope: summary must be a string or null");
223
+ if (envelope.estimate !== undefined && envelope.estimate !== null && typeof envelope.estimate !== "boolean")
224
+ throw new NativeArtifactError("invalid dashboard render envelope: estimate must be a boolean or null");
225
+ }
226
+ function validateInput(value) {
227
+ if (!isRecord(value) || !Object.keys(value).every((key) => ["version", "name", "envelope", "answer_ref", "answer_selection", "idempotency_key"].includes(key)))
228
+ throw new NativeArtifactError("invalid Save to Artifacts payload: expected version, name, idempotency_key, and exactly one answer source");
229
+ // Claude Code 2.1.227 was observed coercing the schema's const "1" to numeric
230
+ // 1. Accept that one semantically identical wire alias, then canonicalize it;
231
+ // every other version remains a loud fail.
232
+ if (value.version !== "1" && value.version !== 1)
233
+ throw new NativeArtifactError("invalid Save to Artifacts payload: version must be the string \"1\" or numeric 1");
234
+ if (typeof value.name !== "string" || value.name.trim().length === 0 || value.name.length > MAX_NAME_LENGTH)
235
+ throw new NativeArtifactError("invalid Save to Artifacts payload: name must be a non-empty string of at most 120 characters");
236
+ if (typeof value.idempotency_key !== "string" || value.idempotency_key.length < 8 || value.idempotency_key.length > MAX_IDEMPOTENCY_KEY_LENGTH || !/^[A-Za-z0-9][A-Za-z0-9._:-]+$/.test(value.idempotency_key))
237
+ throw new NativeArtifactError("invalid Save to Artifacts payload: idempotency_key is invalid");
238
+ const bytes = Buffer.byteLength(JSON.stringify(value));
239
+ if (bytes > MAX_DASHBOARD_BYTES)
240
+ throw new NativeArtifactError("invalid Save to Artifacts payload: payload exceeds 256 KiB");
241
+ if (rejectsExecutableText(value))
242
+ throw new NativeArtifactError("invalid Save to Artifacts payload: executable content or file fields are not allowed");
243
+ const hasEnvelope = Object.hasOwn(value, "envelope");
244
+ const hasReference = Object.hasOwn(value, "answer_ref");
245
+ const hasSelection = Object.hasOwn(value, "answer_selection");
246
+ if ([hasEnvelope, hasReference, hasSelection].filter(Boolean).length !== 1)
247
+ throw new NativeArtifactError("invalid Save to Artifacts payload: provide exactly one of envelope, answer_ref, or answer_selection");
248
+ if (hasEnvelope) {
249
+ validateDashboardEnvelope(value.envelope);
250
+ return { version: "1", name: value.name, envelope: value.envelope, idempotency_key: value.idempotency_key };
251
+ }
252
+ if (hasReference) {
253
+ if (typeof value.answer_ref !== "string" || !ANSWER_REFERENCE_PATTERN.test(value.answer_ref))
254
+ throw new NativeArtifactError("invalid Save to Artifacts payload: answer_ref is invalid");
255
+ return { version: "1", name: value.name, answer_ref: value.answer_ref, idempotency_key: value.idempotency_key };
256
+ }
257
+ if (value.answer_selection !== "latest")
258
+ throw new NativeArtifactError("invalid Save to Artifacts payload: answer_selection must be latest");
259
+ return { version: "1", name: value.name, answer_selection: "latest", idempotency_key: value.idempotency_key };
260
+ }
261
+ function validatePersistedStructuredAnswer(value) {
262
+ if (!isRecord(value) || !onlyKeys(value, ["version", "idempotency_key", "envelope"]))
263
+ throw new NativeArtifactError("invalid structured answer payload: expected exactly version, idempotency_key, and envelope");
264
+ if (value.version !== "1")
265
+ throw new NativeArtifactError("invalid structured answer payload: version must be the string \"1\"");
266
+ if (typeof value.idempotency_key !== "string" || value.idempotency_key.length < 8 || value.idempotency_key.length > MAX_IDEMPOTENCY_KEY_LENGTH || !/^[A-Za-z0-9][A-Za-z0-9._:-]+$/.test(value.idempotency_key))
267
+ throw new NativeArtifactError("invalid structured answer payload: idempotency_key is invalid");
268
+ if (Buffer.byteLength(JSON.stringify(value)) > MAX_DASHBOARD_BYTES)
269
+ throw new NativeArtifactError("invalid structured answer payload: payload exceeds 256 KiB");
270
+ if (rejectsExecutableText(value))
271
+ throw new NativeArtifactError("invalid structured answer payload: executable content or file fields are not allowed");
272
+ validateDashboardEnvelope(value.envelope);
273
+ if (!isRecord(value.envelope) || !onlyKeys(value.envelope, ["blocks", "verified"]) || !Array.isArray(value.envelope.blocks) || value.envelope.blocks.length < 1 || value.envelope.blocks.length > 2 || value.envelope.blocks.filter((block) => isRecord(block) && block.type === "table").length !== 1 || value.envelope.blocks.filter((block) => isRecord(block) && block.type === "definition").length > 1 || value.envelope.blocks.some((block) => !isRecord(block) || (block.type !== "table" && block.type !== "definition"))) {
274
+ throw new NativeArtifactError("invalid structured answer payload: envelope must contain exactly one table and at most one definition block");
275
+ }
276
+ return { version: "1", idempotency_key: value.idempotency_key, envelope: value.envelope };
277
+ }
278
+ function structuredAnswerDigest(envelopeJson) {
279
+ return `sha256:${createHash("sha256").update(envelopeJson).digest("hex")}`;
280
+ }
281
+ function validateStoredStructuredAnswer(source, nativeSessionId) {
282
+ if (source.nativeSessionId !== nativeSessionId)
283
+ throw new NativeArtifactError("persisted answer reference does not belong to this native session", 409);
284
+ if (structuredAnswerDigest(source.envelopeJson) !== source.digest)
285
+ throw new NativeArtifactError("persisted answer reference failed integrity validation", 409);
286
+ try {
287
+ const envelope = JSON.parse(source.envelopeJson);
288
+ validatePersistedStructuredAnswer({ version: "1", idempotency_key: "stored-answer-validation", envelope });
289
+ }
290
+ catch (error) {
291
+ if (error instanceof NativeArtifactError && error.message === "persisted answer reference failed integrity validation")
292
+ throw error;
293
+ throw new NativeArtifactError("persisted answer reference failed integrity validation", 409);
294
+ }
295
+ }
296
+ export class NativeArtifactService {
297
+ options;
298
+ credentials = new Map();
299
+ constructor(options) {
300
+ this.options = options;
301
+ }
302
+ /** Validate the exact local BFF endpoint before advertising or issuing it. */
303
+ readiness() {
304
+ try {
305
+ const url = new URL(this.options.mcpUrl);
306
+ const expected = new URL(this.options.expectedMcpUrl);
307
+ const validLocalEndpoint = (candidate) => candidate.protocol === "http:" && candidate.hostname === "127.0.0.1" && !candidate.username && !candidate.password && candidate.pathname === "/api/native-sessions/mcp" && !candidate.search && !candidate.hash;
308
+ if (!validLocalEndpoint(url) || !validLocalEndpoint(expected) || url.href !== expected.href) {
309
+ return { available: false, reason: "native MCP URL is invalid" };
310
+ }
311
+ return { available: true };
312
+ }
313
+ catch {
314
+ return { available: false, reason: "native MCP URL is invalid" };
315
+ }
316
+ }
317
+ issue(session, binding) {
318
+ if (!this.readiness().available)
319
+ throw new NativeArtifactError("native MCP URL is invalid", 500);
320
+ const credential = randomUUID();
321
+ // Vendor discovery material contains this opaque credential and cannot be
322
+ // patched in a live terminal. Keep it valid for the owning live session;
323
+ // every terminal/runtime/binding lifecycle edge revokes it explicitly.
324
+ this.credentials.set(credential, { session, binding });
325
+ return { version: "1", url: this.options.mcpUrl, credential };
326
+ }
327
+ revoke(credential) {
328
+ if (!credential)
329
+ return;
330
+ this.credentials.delete(credential);
331
+ }
332
+ /** Releases opaque credentials when the owning BFF service shuts down. */
333
+ dispose() { for (const credential of [...this.credentials.keys()])
334
+ this.revoke(credential); }
335
+ hasCredential(credential) { return credential !== undefined && this.credentials.has(credential); }
336
+ health() {
337
+ return {
338
+ server: NATIVE_MCP_SERVER_LABEL,
339
+ tool: NATIVE_MCP_TOOL_NAME,
340
+ destination: NATIVE_MCP_ARTIFACTS_LABEL,
341
+ ...this.readiness(),
342
+ };
343
+ }
344
+ /**
345
+ * Resolves the live session behind an opaque MCP credential without exposing
346
+ * its secret or binding. The generic MCP route uses this before purpose/tool
347
+ * allowlisting; artifact persistence applies its stricter bound-project
348
+ * fence below.
349
+ */
350
+ authorize(credential) {
351
+ const context = credential === undefined ? undefined : this.credentials.get(credential);
352
+ if (!context)
353
+ throw new NativeArtifactError("GenBI MCP bearer credential is invalid. Restart this native session to refresh its GenBI MCP connection.", 401);
354
+ const current = this.options.store.getNativeSession(context.session.id);
355
+ const original = context.session;
356
+ const liveBinding = this.options.getBinding();
357
+ const bindingCurrent = context.binding === undefined
358
+ ? original.projectIdentity === null && original.bindingGeneration === null && original.projectRevision === null
359
+ : sameBinding(context.binding, liveBinding);
360
+ const liveRuntime = this.options.store.getNativeRuntimeBinding();
361
+ const runtimeCurrent = original.runtimeGeneration === null && original.dispatchTarget === null
362
+ ? true
363
+ : liveRuntime.configured && original.runtimeGeneration === liveRuntime.generation && original.dispatchTarget === liveRuntime.target;
364
+ if (!current || (current.status !== "running" && current.status !== "detached") ||
365
+ current.purpose !== original.purpose || current.vendor !== original.vendor || current.agent !== original.agent ||
366
+ current.scopeKind !== original.scopeKind || current.scopeId !== original.scopeId ||
367
+ current.projectIdentity !== original.projectIdentity || current.bindingGeneration !== original.bindingGeneration || current.projectRevision !== original.projectRevision ||
368
+ !bindingCurrent || !runtimeCurrent) {
369
+ this.revoke(credential);
370
+ throw new NativeArtifactError("GenBI MCP session is no longer active or is stale. Start a new native session.", 409);
371
+ }
372
+ return current;
373
+ }
374
+ /**
375
+ * Retains the exact typed answer before conversational presentation. This
376
+ * contract is intentionally independent of dashboard naming/save intent so
377
+ * a later follow-up can only reference this stored result, never recompute.
378
+ */
379
+ persistAnswer(credential, input) {
380
+ const session = this.authorize(credential);
381
+ if (session.purpose !== "analysis")
382
+ throw new NativeArtifactError("structured answer persistence is unavailable for this native session", 409);
383
+ const payload = validatePersistedStructuredAnswer(input);
384
+ const envelopeJson = JSON.stringify(payload.envelope);
385
+ const digest = structuredAnswerDigest(envelopeJson);
386
+ const existing = this.options.store.getNativeStructuredAnswerByIdempotency(session.id, payload.idempotency_key);
387
+ if (existing) {
388
+ if (existing.digest !== digest || existing.envelopeJson !== envelopeJson) {
389
+ throw new NativeArtifactError("structured answer idempotency key is already bound to a different answer", 409);
390
+ }
391
+ return { answer_ref: existing.id, digest: existing.digest, persisted_at: existing.createdAt };
392
+ }
393
+ try {
394
+ const persisted = (this.options.persistStructuredAnswer ?? ((params) => this.options.store.createNativeStructuredAnswer(params)))({
395
+ id: newId("answer"), nativeSessionId: session.id, idempotencyKey: payload.idempotency_key, envelopeJson, digest,
396
+ });
397
+ if (persisted.row.digest !== digest || persisted.row.envelopeJson !== envelopeJson) {
398
+ throw new NativeArtifactError("structured answer idempotency key is already bound to a different answer", 409);
399
+ }
400
+ return { answer_ref: persisted.row.id, digest: persisted.row.digest, persisted_at: persisted.row.createdAt };
401
+ }
402
+ catch (error) {
403
+ if (error instanceof NativeArtifactError)
404
+ throw error;
405
+ // This safe, explicit message is part of the driver contract: it can
406
+ // present the already-computed answer while truthfully warning that a
407
+ // later reference save is unavailable. No save path may recompute it.
408
+ throw new NativeArtifactError("structured answer persistence failed; this answer cannot be saved by reference", 500);
409
+ }
410
+ }
411
+ save(credential, input) {
412
+ const context = credential === undefined ? undefined : this.credentials.get(credential);
413
+ if (!context)
414
+ throw new NativeArtifactError("GenBI MCP bearer credential is invalid. Restart this native session to refresh its GenBI MCP connection.", 401);
415
+ const { session, binding } = context;
416
+ const currentSession = this.authorize(credential);
417
+ if (!binding || currentSession.projectIdentity !== binding.identity || currentSession.bindingGeneration !== binding.generation || currentSession.projectRevision !== binding.revision) {
418
+ this.revoke(credential);
419
+ throw new NativeArtifactError("GenBI MCP session binding is stale. Start a new native session.", 409);
420
+ }
421
+ const payload = validateInput(input);
422
+ const referenceSave = "answer_ref" in payload || "answer_selection" in payload;
423
+ const source = "answer_ref" in payload
424
+ ? this.options.store.getNativeStructuredAnswer(payload.answer_ref)
425
+ : "answer_selection" in payload
426
+ ? this.options.store.getLatestNativeStructuredAnswer(session.id)
427
+ : undefined;
428
+ if ("answer_ref" in payload && !source)
429
+ throw new NativeArtifactError("persisted answer reference was not found", 404);
430
+ if ("answer_selection" in payload && !source)
431
+ throw new NativeArtifactError("no persisted answer is available for this native session", 404);
432
+ if (source)
433
+ validateStoredStructuredAnswer(source, session.id);
434
+ const contents = referenceSave ? source.envelopeJson : JSON.stringify(payload.envelope);
435
+ const digest = referenceSave ? source.digest : structuredAnswerDigest(contents);
436
+ const sameSaveRequest = (artifact) => artifact !== undefined &&
437
+ artifact.name === payload.name.trim() &&
438
+ artifact.projectIdentity === binding.identity && artifact.bindingGeneration === binding.generation && artifact.projectRevision === binding.revision &&
439
+ artifact.nativeVendor === session.vendor && artifact.nativeAgent === session.agent &&
440
+ artifact.contentDigest === digest && artifact.sourceAnswerId === (source?.id ?? null);
441
+ const existing = this.options.store.getNativeArtifactByIdempotency(session.id, payload.idempotency_key);
442
+ if (existing) {
443
+ if (!sameSaveRequest(existing))
444
+ throw new NativeArtifactError("artifact idempotency key is already bound to a different save request", 409);
445
+ return { artifact_id: existing.id, saved_at: existing.savedAt, source_href: `/sessions/${session.id}` };
446
+ }
447
+ const id = newId("artifact");
448
+ const location = path.posix.join("native", `${id}.json`);
449
+ const target = path.resolve(this.options.artifactsRoot, location);
450
+ try {
451
+ const safeTarget = assertSafeContainedTarget(this.options.artifactsRoot, target);
452
+ (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 } : {}) });
454
+ if (!inserted.created && !sameSaveRequest(inserted.row)) {
455
+ rmSync(target, { force: true });
456
+ throw new NativeArtifactError("artifact idempotency key is already bound to a different save request", 409);
457
+ }
458
+ if (!inserted.created)
459
+ rmSync(target, { force: true });
460
+ return { artifact_id: inserted.row.id, saved_at: inserted.row.savedAt, source_href: `/sessions/${session.id}` };
461
+ }
462
+ catch (error) {
463
+ rmSync(target, { force: true });
464
+ if (error instanceof NativeArtifactError)
465
+ throw error;
466
+ throw new NativeArtifactError("artifact persistence failed", 500);
467
+ }
468
+ }
469
+ }
470
+ function artifactStorageError() { return new NativeArtifactError("artifact storage is unavailable", 500); }
471
+ /**
472
+ * Makes every directory from the filesystem root to `directory` explicit and
473
+ * refuses symlinks before creating a temporary or final artifact file. The
474
+ * artifact root is host configuration, but this still prevents a pre-existing
475
+ * `native` directory (or an ancestor) from redirecting an agent write.
476
+ */
477
+ function ensureDirectoriesWithoutSymlinks(directory) {
478
+ const absolute = path.resolve(directory);
479
+ const parsed = path.parse(absolute);
480
+ let current = parsed.root;
481
+ for (const part of absolute.slice(parsed.root.length).split(path.sep).filter(Boolean)) {
482
+ current = path.join(current, part);
483
+ try {
484
+ const stat = lstatSync(current);
485
+ if (!stat.isDirectory() || stat.isSymbolicLink())
486
+ throw artifactStorageError();
487
+ }
488
+ catch (error) {
489
+ if (error.code !== "ENOENT")
490
+ throw error;
491
+ try {
492
+ mkdirSync(current, { mode: 0o700 });
493
+ const created = lstatSync(current);
494
+ if (!created.isDirectory() || created.isSymbolicLink())
495
+ throw artifactStorageError();
496
+ }
497
+ catch (createError) {
498
+ if (createError instanceof NativeArtifactError)
499
+ throw createError;
500
+ throw artifactStorageError();
501
+ }
502
+ }
503
+ }
504
+ }
505
+ function canonicalStorageRoot(root) {
506
+ const absolute = path.resolve(root);
507
+ try {
508
+ if (lstatSync(absolute).isSymbolicLink())
509
+ throw artifactStorageError();
510
+ }
511
+ catch (error) {
512
+ if (error.code !== "ENOENT")
513
+ throw error;
514
+ }
515
+ let ancestor = absolute;
516
+ const tail = [];
517
+ while (true) {
518
+ try {
519
+ lstatSync(ancestor);
520
+ return tail.length === 0 ? realpathSync(ancestor) : path.join(realpathSync(ancestor), ...tail);
521
+ }
522
+ catch (error) {
523
+ if (error.code !== "ENOENT")
524
+ throw artifactStorageError();
525
+ const parent = path.dirname(ancestor);
526
+ if (parent === ancestor)
527
+ throw artifactStorageError();
528
+ tail.unshift(path.basename(ancestor));
529
+ ancestor = parent;
530
+ }
531
+ }
532
+ }
533
+ function assertSafeContainedTarget(root, target) {
534
+ const absoluteRoot = path.resolve(root);
535
+ const relative = path.relative(absoluteRoot, target);
536
+ if (relative.startsWith("..") || path.isAbsolute(relative))
537
+ throw artifactStorageError();
538
+ const safeRoot = canonicalStorageRoot(absoluteRoot);
539
+ const safeTarget = path.resolve(safeRoot, relative);
540
+ ensureDirectoriesWithoutSymlinks(safeRoot);
541
+ ensureDirectoriesWithoutSymlinks(path.dirname(safeTarget));
542
+ try {
543
+ // IDs are host-created and targets must be brand new. Reject every
544
+ // pre-existing final component, including a symlink, rather than allowing
545
+ // rename() to replace it.
546
+ lstatSync(safeTarget);
547
+ throw artifactStorageError();
548
+ }
549
+ catch (error) {
550
+ if (error.code !== "ENOENT")
551
+ throw error;
552
+ }
553
+ return safeTarget;
554
+ }
555
+ function writeContainedAtomically(target, contents) {
556
+ const temporary = path.join(path.dirname(target), `.${path.basename(target)}.${randomUUID()}.tmp`);
557
+ try {
558
+ writeFileSync(temporary, contents, { encoding: "utf8", mode: 0o600, flag: "wx" });
559
+ renameSync(temporary, target);
560
+ }
561
+ finally {
562
+ rmSync(temporary, { force: true });
563
+ }
564
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Closed native-session dispatch contract. The browser supplies a product
3
+ * purpose only; this module resolves the fixed Warble profile plus the one
4
+ * interactive target selected by the saved Runtime configuration.
5
+ */
6
+ import type { InteractiveTarget } from "./interactive-terminal.js";
7
+ import type { RuntimeSettings, SubscriptionProvider } from "./wire-types.js";
8
+ export declare const NATIVE_PURPOSES: readonly ["analysis", "setup", "context_enrichment"];
9
+ export type NativePurpose = (typeof NATIVE_PURPOSES)[number];
10
+ export type NativeVendor = SubscriptionProvider;
11
+ export interface NativeDispatchDefinition {
12
+ readonly purpose: NativePurpose;
13
+ readonly profile: "genbi-default" | "genbi-setup" | "genbi-enrich-context";
14
+ readonly scopeKind: "bootstrap" | "bound_project";
15
+ }
16
+ export declare const NATIVE_DISPATCH_REGISTRY: Readonly<Record<NativePurpose, NativeDispatchDefinition>>;
17
+ /** Stable, read-only response for a source or compiled-bundle identity mismatch. */
18
+ export declare const HARNESS_PROFILE_IDENTITY_ERROR = "harness profile identity does not match the requested read-only purpose";
19
+ /**
20
+ * Every Harness source and emitted bundle must retain the fixed identity for
21
+ * the closed purpose chosen by the BFF. This is deliberately independent of
22
+ * browser input: callers choose a purpose, never a profile authority.
23
+ */
24
+ export declare function assertHarnessPurposeProfile(purpose: NativePurpose, profile: string): void;
25
+ export interface NativeRuntimeBinding {
26
+ readonly configured: boolean;
27
+ readonly generation: number;
28
+ readonly provider?: NativeVendor;
29
+ readonly target?: InteractiveTarget;
30
+ readonly targetLabel?: "Claude CLI" | "Codex CLI";
31
+ }
32
+ export declare function targetForProvider(provider: NativeVendor): InteractiveTarget;
33
+ export declare function nativeTargetLabel(provider: NativeVendor): "Claude CLI" | "Codex CLI";
34
+ /** Explicit marker, not seed/default settings, is the authority. */
35
+ export declare function resolveNativeRuntimeBinding(settings: RuntimeSettings, explicit: boolean, generation: number): NativeRuntimeBinding;
36
+ /** A launch may proceed only while the whole captured Runtime authority remains current. */
37
+ export declare function sameNativeRuntimeBinding(left: NativeRuntimeBinding, right: NativeRuntimeBinding): boolean;
38
+ export declare function dispatchForPurpose(purpose: NativePurpose, binding: NativeRuntimeBinding): (NativeDispatchDefinition & {
39
+ readonly provider: NativeVendor;
40
+ readonly target: InteractiveTarget;
41
+ readonly targetLabel: "Claude CLI" | "Codex CLI";
42
+ }) | undefined;
@@ -0,0 +1,45 @@
1
+ export const NATIVE_PURPOSES = ["analysis", "setup", "context_enrichment"];
2
+ export const NATIVE_DISPATCH_REGISTRY = {
3
+ setup: { purpose: "setup", profile: "genbi-setup", scopeKind: "bootstrap" },
4
+ analysis: { purpose: "analysis", profile: "genbi-default", scopeKind: "bound_project" },
5
+ context_enrichment: { purpose: "context_enrichment", profile: "genbi-enrich-context", scopeKind: "bound_project" },
6
+ };
7
+ /** Stable, read-only response for a source or compiled-bundle identity mismatch. */
8
+ export const HARNESS_PROFILE_IDENTITY_ERROR = "harness profile identity does not match the requested read-only purpose";
9
+ /**
10
+ * Every Harness source and emitted bundle must retain the fixed identity for
11
+ * the closed purpose chosen by the BFF. This is deliberately independent of
12
+ * browser input: callers choose a purpose, never a profile authority.
13
+ */
14
+ export function assertHarnessPurposeProfile(purpose, profile) {
15
+ if (profile !== NATIVE_DISPATCH_REGISTRY[purpose].profile) {
16
+ throw new Error(HARNESS_PROFILE_IDENTITY_ERROR);
17
+ }
18
+ }
19
+ export function targetForProvider(provider) {
20
+ return provider === "claude" ? "claude-code:interactive" : "codex:interactive";
21
+ }
22
+ export function nativeTargetLabel(provider) {
23
+ return provider === "claude" ? "Claude CLI" : "Codex CLI";
24
+ }
25
+ /** Explicit marker, not seed/default settings, is the authority. */
26
+ export function resolveNativeRuntimeBinding(settings, explicit, generation) {
27
+ if (!explicit || settings.authMode !== "subscription" || (settings.subscriptionProvider !== "claude" && settings.subscriptionProvider !== "codex")) {
28
+ return { configured: false, generation };
29
+ }
30
+ const provider = settings.subscriptionProvider;
31
+ return { configured: true, generation, provider, target: targetForProvider(provider), targetLabel: nativeTargetLabel(provider) };
32
+ }
33
+ /** A launch may proceed only while the whole captured Runtime authority remains current. */
34
+ export function sameNativeRuntimeBinding(left, right) {
35
+ return left.configured === right.configured
36
+ && left.generation === right.generation
37
+ && left.provider === right.provider
38
+ && left.target === right.target
39
+ && left.targetLabel === right.targetLabel;
40
+ }
41
+ export function dispatchForPurpose(purpose, binding) {
42
+ if (!binding.configured || !binding.provider || !binding.target || !binding.targetLabel)
43
+ return undefined;
44
+ return { ...NATIVE_DISPATCH_REGISTRY[purpose], provider: binding.provider, target: binding.target, targetLabel: binding.targetLabel };
45
+ }