@oscharko-dev/keiko-server 0.2.7 → 0.2.9

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 (302) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/chat-handlers.d.ts +18 -2
  3. package/dist/chat-handlers.d.ts.map +1 -1
  4. package/dist/chat-handlers.js +185 -3
  5. package/dist/command-runner-errors.d.ts +17 -0
  6. package/dist/command-runner-errors.d.ts.map +1 -0
  7. package/dist/command-runner-errors.js +37 -0
  8. package/dist/command-runner-evidence.d.ts +23 -0
  9. package/dist/command-runner-evidence.d.ts.map +1 -0
  10. package/dist/command-runner-evidence.js +69 -0
  11. package/dist/command-runner-routes.d.ts +7 -0
  12. package/dist/command-runner-routes.d.ts.map +1 -0
  13. package/dist/command-runner-routes.js +175 -0
  14. package/dist/command-runner.d.ts +29 -0
  15. package/dist/command-runner.d.ts.map +1 -0
  16. package/dist/command-runner.js +348 -0
  17. package/dist/conversation-prompt.d.ts +2 -2
  18. package/dist/conversation-prompt.d.ts.map +1 -1
  19. package/dist/conversation-prompt.js +17 -1
  20. package/dist/csp.d.ts.map +1 -1
  21. package/dist/csp.js +3 -0
  22. package/dist/deps.d.ts +28 -1
  23. package/dist/deps.d.ts.map +1 -1
  24. package/dist/deps.js +288 -13
  25. package/dist/discussion-prompt.d.ts +4 -0
  26. package/dist/discussion-prompt.d.ts.map +1 -0
  27. package/dist/discussion-prompt.js +19 -0
  28. package/dist/editor/agentActionAudit.d.ts +18 -0
  29. package/dist/editor/agentActionAudit.d.ts.map +1 -0
  30. package/dist/editor/agentActionAudit.js +80 -0
  31. package/dist/editor/agentRoutes.d.ts +1 -0
  32. package/dist/editor/agentRoutes.d.ts.map +1 -1
  33. package/dist/editor/agentRoutes.js +292 -55
  34. package/dist/editor/agentSessionRegistry.d.ts +35 -0
  35. package/dist/editor/agentSessionRegistry.d.ts.map +1 -0
  36. package/dist/editor/agentSessionRegistry.js +243 -0
  37. package/dist/editor/completionRoutes.d.ts.map +1 -1
  38. package/dist/editor/completionRoutes.js +5 -10
  39. package/dist/editor/languageRoutes.d.ts +12 -1
  40. package/dist/editor/languageRoutes.d.ts.map +1 -1
  41. package/dist/editor/languageRoutes.js +71 -8
  42. package/dist/editor/languageService.d.ts +3 -2
  43. package/dist/editor/languageService.d.ts.map +1 -1
  44. package/dist/editor/languageService.js +41 -3
  45. package/dist/editor/languageServiceHost.d.ts.map +1 -1
  46. package/dist/editor/languageServiceHost.js +2 -2
  47. package/dist/editor/lsp/hostLanguageOperation.d.ts +17 -0
  48. package/dist/editor/lsp/hostLanguageOperation.d.ts.map +1 -0
  49. package/dist/editor/lsp/hostLanguageOperation.js +436 -0
  50. package/dist/editor/lsp/hostLanguageProviders.d.ts +26 -0
  51. package/dist/editor/lsp/hostLanguageProviders.d.ts.map +1 -0
  52. package/dist/editor/lsp/hostLanguageProviders.js +161 -0
  53. package/dist/editor/lsp/lspFrameCodec.d.ts +13 -0
  54. package/dist/editor/lsp/lspFrameCodec.d.ts.map +1 -0
  55. package/dist/editor/lsp/lspFrameCodec.js +164 -0
  56. package/dist/editor/lsp/lspJsonRpcClient.d.ts +34 -0
  57. package/dist/editor/lsp/lspJsonRpcClient.d.ts.map +1 -0
  58. package/dist/editor/lsp/lspJsonRpcClient.js +173 -0
  59. package/dist/editor/lsp/lspLanguageProvider.d.ts +7 -0
  60. package/dist/editor/lsp/lspLanguageProvider.d.ts.map +1 -0
  61. package/dist/editor/lsp/lspLanguageProvider.js +29 -0
  62. package/dist/editor/lsp/lspLifecycleLedger.d.ts +5 -0
  63. package/dist/editor/lsp/lspLifecycleLedger.d.ts.map +1 -0
  64. package/dist/editor/lsp/lspLifecycleLedger.js +37 -0
  65. package/dist/editor/lsp/lspNodeAdapter.d.ts +31 -0
  66. package/dist/editor/lsp/lspNodeAdapter.d.ts.map +1 -0
  67. package/dist/editor/lsp/lspNodeAdapter.js +230 -0
  68. package/dist/editor/lsp/lspProcessManager.d.ts +24 -0
  69. package/dist/editor/lsp/lspProcessManager.d.ts.map +1 -0
  70. package/dist/editor/lsp/lspProcessManager.js +255 -0
  71. package/dist/editor/lsp/lspRestartThrottle.d.ts +6 -0
  72. package/dist/editor/lsp/lspRestartThrottle.d.ts.map +1 -0
  73. package/dist/editor/lsp/lspRestartThrottle.js +24 -0
  74. package/dist/editor/lsp/lspStatusRoute.d.ts +8 -0
  75. package/dist/editor/lsp/lspStatusRoute.d.ts.map +1 -0
  76. package/dist/editor/lsp/lspStatusRoute.js +22 -0
  77. package/dist/editor/lsp/lspTransport.d.ts +19 -0
  78. package/dist/editor/lsp/lspTransport.d.ts.map +1 -0
  79. package/dist/editor/lsp/lspTransport.js +55 -0
  80. package/dist/editor/lsp/testing/fakeLspProcess.d.ts +23 -0
  81. package/dist/editor/lsp/testing/fakeLspProcess.d.ts.map +1 -0
  82. package/dist/editor/lsp/testing/fakeLspProcess.js +132 -0
  83. package/dist/files.d.ts +63 -0
  84. package/dist/files.d.ts.map +1 -1
  85. package/dist/files.js +799 -1
  86. package/dist/gateway-readiness.d.ts +6 -0
  87. package/dist/gateway-readiness.d.ts.map +1 -0
  88. package/dist/gateway-readiness.js +624 -0
  89. package/dist/gateway-setup.d.ts +2 -0
  90. package/dist/gateway-setup.d.ts.map +1 -1
  91. package/dist/gateway-setup.js +275 -11
  92. package/dist/gitDelivery/actionSheetProjection.d.ts +30 -0
  93. package/dist/gitDelivery/actionSheetProjection.d.ts.map +1 -0
  94. package/dist/gitDelivery/actionSheetProjection.js +206 -0
  95. package/dist/gitDelivery/actionSheetRoutes.d.ts +29 -0
  96. package/dist/gitDelivery/actionSheetRoutes.d.ts.map +1 -0
  97. package/dist/gitDelivery/actionSheetRoutes.js +293 -0
  98. package/dist/gitDelivery/agentOperationsRoutes.d.ts +33 -0
  99. package/dist/gitDelivery/agentOperationsRoutes.d.ts.map +1 -0
  100. package/dist/gitDelivery/agentOperationsRoutes.js +405 -0
  101. package/dist/gitDelivery/commitRoutes.d.ts +23 -0
  102. package/dist/gitDelivery/commitRoutes.d.ts.map +1 -0
  103. package/dist/gitDelivery/commitRoutes.js +204 -0
  104. package/dist/gitDelivery/evidenceRoutes.d.ts +9 -0
  105. package/dist/gitDelivery/evidenceRoutes.d.ts.map +1 -0
  106. package/dist/gitDelivery/evidenceRoutes.js +101 -0
  107. package/dist/gitDelivery/execution.d.ts +38 -0
  108. package/dist/gitDelivery/execution.d.ts.map +1 -0
  109. package/dist/gitDelivery/execution.js +117 -0
  110. package/dist/gitDelivery/localMutationRoutes.d.ts +30 -0
  111. package/dist/gitDelivery/localMutationRoutes.d.ts.map +1 -0
  112. package/dist/gitDelivery/localMutationRoutes.js +165 -0
  113. package/dist/gitDelivery/mergeExecution.d.ts +63 -0
  114. package/dist/gitDelivery/mergeExecution.d.ts.map +1 -0
  115. package/dist/gitDelivery/mergeExecution.js +168 -0
  116. package/dist/gitDelivery/mergeRoutes.d.ts +12 -0
  117. package/dist/gitDelivery/mergeRoutes.d.ts.map +1 -0
  118. package/dist/gitDelivery/mergeRoutes.js +218 -0
  119. package/dist/gitDelivery/mutationEvidenceLedger.d.ts +23 -0
  120. package/dist/gitDelivery/mutationEvidenceLedger.d.ts.map +1 -0
  121. package/dist/gitDelivery/mutationEvidenceLedger.js +87 -0
  122. package/dist/gitDelivery/prExecution.d.ts +54 -0
  123. package/dist/gitDelivery/prExecution.d.ts.map +1 -0
  124. package/dist/gitDelivery/prExecution.js +192 -0
  125. package/dist/gitDelivery/prRoutes.d.ts +12 -0
  126. package/dist/gitDelivery/prRoutes.d.ts.map +1 -0
  127. package/dist/gitDelivery/prRoutes.js +256 -0
  128. package/dist/gitDelivery/pushExecution.d.ts +43 -0
  129. package/dist/gitDelivery/pushExecution.d.ts.map +1 -0
  130. package/dist/gitDelivery/pushExecution.js +124 -0
  131. package/dist/gitDelivery/pushRoutes.d.ts +12 -0
  132. package/dist/gitDelivery/pushRoutes.d.ts.map +1 -0
  133. package/dist/gitDelivery/pushRoutes.js +200 -0
  134. package/dist/gitDelivery/requestGuards.d.ts +15 -0
  135. package/dist/gitDelivery/requestGuards.d.ts.map +1 -0
  136. package/dist/gitDelivery/requestGuards.js +97 -0
  137. package/dist/gitDelivery/syncEvidence.d.ts +37 -0
  138. package/dist/gitDelivery/syncEvidence.d.ts.map +1 -0
  139. package/dist/gitDelivery/syncEvidence.js +85 -0
  140. package/dist/gitDelivery/syncExecution.d.ts +30 -0
  141. package/dist/gitDelivery/syncExecution.d.ts.map +1 -0
  142. package/dist/gitDelivery/syncExecution.js +266 -0
  143. package/dist/gitDelivery/syncRoutes.d.ts +13 -0
  144. package/dist/gitDelivery/syncRoutes.d.ts.map +1 -0
  145. package/dist/gitDelivery/syncRoutes.js +200 -0
  146. package/dist/gitPorcelainStatus.d.ts +15 -0
  147. package/dist/gitPorcelainStatus.d.ts.map +1 -0
  148. package/dist/gitPorcelainStatus.js +104 -0
  149. package/dist/gitRepositoryReads.d.ts +10 -0
  150. package/dist/gitRepositoryReads.d.ts.map +1 -0
  151. package/dist/gitRepositoryReads.js +314 -0
  152. package/dist/gitRepositoryRoutes.d.ts +7 -0
  153. package/dist/gitRepositoryRoutes.d.ts.map +1 -0
  154. package/dist/gitRepositoryRoutes.js +221 -0
  155. package/dist/gitRoutes.d.ts +66 -0
  156. package/dist/gitRoutes.d.ts.map +1 -0
  157. package/dist/gitRoutes.js +543 -0
  158. package/dist/governed-workflow.d.ts +2 -0
  159. package/dist/governed-workflow.d.ts.map +1 -1
  160. package/dist/governed-workflow.js +4 -0
  161. package/dist/grounded-qa-hybrid.d.ts.map +1 -1
  162. package/dist/grounded-qa-hybrid.js +2 -0
  163. package/dist/grounded-qa-multi-source.d.ts.map +1 -1
  164. package/dist/grounded-qa-multi-source.js +1 -0
  165. package/dist/grounded-qa.d.ts +11 -0
  166. package/dist/grounded-qa.d.ts.map +1 -1
  167. package/dist/grounded-qa.js +14 -4
  168. package/dist/headers.d.ts +4 -1
  169. package/dist/headers.d.ts.map +1 -1
  170. package/dist/headers.js +11 -4
  171. package/dist/index.d.ts +8 -1
  172. package/dist/index.d.ts.map +1 -1
  173. package/dist/index.js +9 -1
  174. package/dist/local-knowledge-grounded-qa.d.ts.map +1 -1
  175. package/dist/local-knowledge-grounded-qa.js +11 -2
  176. package/dist/qualityIntelligence/figmaSnapshotRoutes.d.ts +1 -1
  177. package/dist/qualityIntelligence/figmaSnapshotRoutes.d.ts.map +1 -1
  178. package/dist/qualityIntelligence/figmaSnapshotRoutes.js +1 -1
  179. package/dist/read-handlers.d.ts +5 -0
  180. package/dist/read-handlers.d.ts.map +1 -1
  181. package/dist/read-handlers.js +57 -1
  182. package/dist/routes.d.ts.map +1 -1
  183. package/dist/routes.js +260 -12
  184. package/dist/run-engine.d.ts.map +1 -1
  185. package/dist/run-engine.js +3 -0
  186. package/dist/run-handlers.d.ts +0 -1
  187. package/dist/run-handlers.d.ts.map +1 -1
  188. package/dist/run-handlers.js +64 -211
  189. package/dist/run-request.d.ts +11 -0
  190. package/dist/run-request.d.ts.map +1 -1
  191. package/dist/run-request.js +158 -10
  192. package/dist/runtime/capabilityDetector.d.ts +38 -0
  193. package/dist/runtime/capabilityDetector.d.ts.map +1 -0
  194. package/dist/runtime/capabilityDetector.js +443 -0
  195. package/dist/runtime/capabilityRoutes.d.ts +9 -0
  196. package/dist/runtime/capabilityRoutes.d.ts.map +1 -0
  197. package/dist/runtime/capabilityRoutes.js +45 -0
  198. package/dist/runtime/containerEngineDetector.d.ts +17 -0
  199. package/dist/runtime/containerEngineDetector.d.ts.map +1 -0
  200. package/dist/runtime/containerEngineDetector.js +222 -0
  201. package/dist/runtime/containerRoutes.d.ts +8 -0
  202. package/dist/runtime/containerRoutes.d.ts.map +1 -0
  203. package/dist/runtime/containerRoutes.js +207 -0
  204. package/dist/runtime/containerRunner-errors.d.ts +18 -0
  205. package/dist/runtime/containerRunner-errors.d.ts.map +1 -0
  206. package/dist/runtime/containerRunner-errors.js +42 -0
  207. package/dist/runtime/containerRunner-evidence.d.ts +24 -0
  208. package/dist/runtime/containerRunner-evidence.d.ts.map +1 -0
  209. package/dist/runtime/containerRunner-evidence.js +74 -0
  210. package/dist/runtime/containerRunner.d.ts +37 -0
  211. package/dist/runtime/containerRunner.d.ts.map +1 -0
  212. package/dist/runtime/containerRunner.js +443 -0
  213. package/dist/server.d.ts.map +1 -1
  214. package/dist/server.js +24 -4
  215. package/dist/store/db.d.ts.map +1 -1
  216. package/dist/store/db.js +2 -1
  217. package/dist/store/index.d.ts +1 -1
  218. package/dist/store/index.d.ts.map +1 -1
  219. package/dist/store/messages.d.ts +2 -1
  220. package/dist/store/messages.d.ts.map +1 -1
  221. package/dist/store/messages.js +46 -4
  222. package/dist/store/schema.d.ts +1 -1
  223. package/dist/store/schema.d.ts.map +1 -1
  224. package/dist/store/schema.js +68 -1
  225. package/dist/store/types.d.ts +3 -2
  226. package/dist/store/types.d.ts.map +1 -1
  227. package/dist/task-workspace/active-store.d.ts +21 -0
  228. package/dist/task-workspace/active-store.d.ts.map +1 -0
  229. package/dist/task-workspace/active-store.js +55 -0
  230. package/dist/task-workspace/authorization.d.ts +7 -0
  231. package/dist/task-workspace/authorization.d.ts.map +1 -0
  232. package/dist/task-workspace/authorization.js +54 -0
  233. package/dist/task-workspace/binding.d.ts +3 -0
  234. package/dist/task-workspace/binding.d.ts.map +1 -0
  235. package/dist/task-workspace/binding.js +22 -0
  236. package/dist/task-workspace/cleanup.d.ts +4 -0
  237. package/dist/task-workspace/cleanup.d.ts.map +1 -0
  238. package/dist/task-workspace/cleanup.js +428 -0
  239. package/dist/task-workspace/errors.d.ts +14 -0
  240. package/dist/task-workspace/errors.d.ts.map +1 -0
  241. package/dist/task-workspace/errors.js +81 -0
  242. package/dist/task-workspace/evidence.d.ts +32 -0
  243. package/dist/task-workspace/evidence.d.ts.map +1 -0
  244. package/dist/task-workspace/evidence.js +52 -0
  245. package/dist/task-workspace/field-safety.d.ts +3 -0
  246. package/dist/task-workspace/field-safety.d.ts.map +1 -0
  247. package/dist/task-workspace/field-safety.js +42 -0
  248. package/dist/task-workspace/health.d.ts +4 -0
  249. package/dist/task-workspace/health.d.ts.map +1 -0
  250. package/dist/task-workspace/health.js +163 -0
  251. package/dist/task-workspace/lifecycle.d.ts +3 -0
  252. package/dist/task-workspace/lifecycle.d.ts.map +1 -0
  253. package/dist/task-workspace/lifecycle.js +248 -0
  254. package/dist/task-workspace/locks.d.ts +13 -0
  255. package/dist/task-workspace/locks.d.ts.map +1 -0
  256. package/dist/task-workspace/locks.js +44 -0
  257. package/dist/task-workspace/managed-root.d.ts +7 -0
  258. package/dist/task-workspace/managed-root.d.ts.map +1 -0
  259. package/dist/task-workspace/managed-root.js +98 -0
  260. package/dist/task-workspace/mutex.d.ts +8 -0
  261. package/dist/task-workspace/mutex.d.ts.map +1 -0
  262. package/dist/task-workspace/mutex.js +82 -0
  263. package/dist/task-workspace/naming.d.ts +15 -0
  264. package/dist/task-workspace/naming.d.ts.map +1 -0
  265. package/dist/task-workspace/naming.js +0 -0
  266. package/dist/task-workspace/provisioning.d.ts +3 -0
  267. package/dist/task-workspace/provisioning.d.ts.map +1 -0
  268. package/dist/task-workspace/provisioning.js +528 -0
  269. package/dist/task-workspace/reconciliation.d.ts +15 -0
  270. package/dist/task-workspace/reconciliation.d.ts.map +1 -0
  271. package/dist/task-workspace/reconciliation.js +274 -0
  272. package/dist/task-workspace/repair.d.ts +3 -0
  273. package/dist/task-workspace/repair.d.ts.map +1 -0
  274. package/dist/task-workspace/repair.js +286 -0
  275. package/dist/task-workspace/routes.d.ts +19 -0
  276. package/dist/task-workspace/routes.d.ts.map +1 -0
  277. package/dist/task-workspace/routes.js +481 -0
  278. package/dist/task-workspace/store.d.ts +12 -0
  279. package/dist/task-workspace/store.d.ts.map +1 -0
  280. package/dist/task-workspace/store.js +128 -0
  281. package/dist/task-workspace/types.d.ts +170 -0
  282. package/dist/task-workspace/types.d.ts.map +1 -0
  283. package/dist/task-workspace/types.js +5 -0
  284. package/dist/voice-action-governance.d.ts +23 -0
  285. package/dist/voice-action-governance.d.ts.map +1 -0
  286. package/dist/voice-action-governance.js +126 -0
  287. package/dist/voice-handlers.d.ts +6 -0
  288. package/dist/voice-handlers.d.ts.map +1 -0
  289. package/dist/voice-handlers.js +570 -0
  290. package/dist/voice-realtime-grounded-tool.d.ts +31 -0
  291. package/dist/voice-realtime-grounded-tool.d.ts.map +1 -0
  292. package/dist/voice-realtime-grounded-tool.js +322 -0
  293. package/dist/voice-realtime.d.ts +69 -0
  294. package/dist/voice-realtime.d.ts.map +1 -0
  295. package/dist/voice-realtime.js +787 -0
  296. package/dist/workspace-state-handlers.d.ts +5 -0
  297. package/dist/workspace-state-handlers.d.ts.map +1 -0
  298. package/dist/workspace-state-handlers.js +106 -0
  299. package/package.json +20 -19
  300. package/dist/grounded-handoff.d.ts +0 -4
  301. package/dist/grounded-handoff.d.ts.map +0 -1
  302. package/dist/grounded-handoff.js +0 -445
@@ -0,0 +1,443 @@
1
+ // Issue #1388 (Epic #1491, ADR-0070, D2/D3) — the governed container-run pilot manager. A request
2
+ // names a `taskId` from a server-frozen CLOSED catalog; the server resolves it to a vetted
3
+ // ContainerTask whose image is in a closed allowlist and constructs the EXACT, server-frozen
4
+ // `docker run` argv (no client-supplied image/argv/mount/flag). Execution reuses the single governed
5
+ // runCommand spawn boundary (ADR-0043 D2) exactly as the #1387 command runner does — no Docker SDK,
6
+ // no daemon-socket client, no second spawn path.
7
+ //
8
+ // Engine-unavailable is a GOVERNANCE failure, not an execution outcome: `execute` THROWS
9
+ // ContainerRunnerError("CONTAINER_ENGINE_UNAVAILABLE") BEFORE any run id is minted, so there is no
10
+ // run/event/evidence for a run that never started. Only real execution outcomes become a
11
+ // ContainerRunResult.
12
+ import { randomUUID } from "node:crypto";
13
+ import { CommandCancelledError, CommandDeniedError, CommandTimeoutError, DEFAULT_SANDBOX_POLICY, runCommand, } from "@oscharko-dev/keiko-tools";
14
+ import { nodeSpawnFn } from "@oscharko-dev/keiko-tools/internal/exec";
15
+ import { nodeWorkspaceFs } from "@oscharko-dev/keiko-workspace/internal/fs";
16
+ import { CONTAINER_RUNTIME_SCHEMA_VERSION, CONTAINER_TASK_RULES, } from "@oscharko-dev/keiko-contracts";
17
+ import { ContainerRunnerError } from "./containerRunner-errors.js";
18
+ import { appendContainerRunEvidence, buildContainerRunEvidenceEntry, } from "./containerRunner-evidence.js";
19
+ import { detectContainerEngines } from "./containerEngineDetector.js";
20
+ // Tight cap — a container run is a high-trust surface, so a small number of concurrent runs.
21
+ const MAX_CONCURRENT_CONTAINER_RUNS = 2;
22
+ const MIN_TIMEOUT_MS = 1_000;
23
+ // ─── Defaults (conservative; justified inline) ─────────────────────────────────────
24
+ export const DEFAULT_CONTAINER_RESOURCE_LIMITS = {
25
+ pidsLimit: 256, // enough for a small toolchain; blocks fork-bombs
26
+ memoryBytes: 536_870_912, // 512 MiB — bounded, fits a diagnostic image
27
+ cpus: 1, // single core; deterministic, no host saturation
28
+ };
29
+ // The pilot image is pinned by TAG (not digest) because no published Keiko-vetted digest exists yet;
30
+ // the digest-pin path is documented in docs/container-runtime/security-notes.md and is a one-line
31
+ // change here once a digest is vetted. `--pull never` means the image MUST already be present
32
+ // locally — a missing image is a structured image-missing failure, never a silent network fetch.
33
+ const PILOT_IMAGE = "docker.io/library/alpine:3.20";
34
+ export const DEFAULT_CONTAINER_EXECUTION_POLICY = {
35
+ // Closed set: exactly the one pinned pilot image. An image ∉ this list → IMAGE_NOT_ALLOWED.
36
+ imageAllowlist: Object.freeze([PILOT_IMAGE]),
37
+ limits: DEFAULT_CONTAINER_RESOURCE_LIMITS,
38
+ mountMode: "read-only",
39
+ networkMode: "none",
40
+ workspaceMountPath: "/workspace",
41
+ pull: "never",
42
+ };
43
+ export const DEFAULT_CONTAINER_TASKS = Object.freeze([
44
+ {
45
+ id: "container-pilot:diagnostic",
46
+ kind: "diagnostic",
47
+ label: "Container engine pilot diagnostic",
48
+ image: PILOT_IMAGE,
49
+ // Trivial in-container command (NOT docker flags). Proves the detection+policy+spawn composition.
50
+ // Deliberately NOT `sh -c …`: `-c` is a CONTAINER_DENY_FLAGS member (a transitive-shell escalation
51
+ // flag), so a `-c` anywhere in the argv would self-deny at the runCommand boundary. A bare
52
+ // `echo …` argv keeps the frozen hardened argv passing isCommandAllowed (the §1.8 LOAD-BEARING
53
+ // no-self-deny invariant).
54
+ args: Object.freeze(["echo", "keiko-container-pilot ok"]),
55
+ engine: "docker",
56
+ },
57
+ ]);
58
+ // ─── Server-frozen argv (D3) — PURE, unit-tested in isolation ──────────────────────
59
+ // Returns the EXACT docker/podman argv. The image MUST already be asserted ∈ policy.imageAllowlist
60
+ // by the caller (execute does this and throws IMAGE_NOT_ALLOWED otherwise); this builder additionally
61
+ // re-asserts so a misuse can never emit a non-allowlisted image. No client input reaches this argv.
62
+ export function buildContainerRunArgv(task, policy, workspaceRoot) {
63
+ if (!policy.imageAllowlist.includes(task.image)) {
64
+ throw new ContainerRunnerError("IMAGE_NOT_ALLOWED", "Task image is not allowlisted.");
65
+ }
66
+ return [
67
+ "run",
68
+ "--rm", // no container persistence (NIST 800-190 4.4)
69
+ "--network",
70
+ policy.networkMode, // "none" — the container has no network (4.1.3)
71
+ "--read-only", // read-only root filesystem (3.1.2 / 4.5.2)
72
+ "--cap-drop",
73
+ "ALL", // drop all Linux capabilities (4.5.3)
74
+ "--security-opt",
75
+ "no-new-privileges", // forbid privilege escalation (4.5.3)
76
+ "--pids-limit",
77
+ String(policy.limits.pidsLimit),
78
+ "--memory",
79
+ String(policy.limits.memoryBytes),
80
+ "--cpus",
81
+ String(policy.limits.cpus),
82
+ "--pull",
83
+ policy.pull, // "never" — the image must already be present locally (3.1.1 / 4.2)
84
+ "-v",
85
+ // read-only workspace bind mount at a fixed in-container path; NO read-write/broad host mount,
86
+ // and NEVER the Docker socket.
87
+ `${workspaceRoot}:${policy.workspaceMountPath}:ro`,
88
+ task.image,
89
+ ...task.args,
90
+ ];
91
+ }
92
+ const IMAGE_MISSING_SIGNATURES = [
93
+ "no such image",
94
+ "unable to find image",
95
+ "image not known",
96
+ "manifest unknown",
97
+ ];
98
+ const PULL_DENIED_SIGNATURES = [
99
+ "pull policy",
100
+ "pull never",
101
+ "--pull=never",
102
+ "image must be present",
103
+ ];
104
+ const MOUNT_FAILURE_SIGNATURES = [
105
+ "error mounting",
106
+ "invalid mount",
107
+ "bind mount",
108
+ "no such file or directory", // bind source absent
109
+ "are you trying to mount",
110
+ ];
111
+ function matchesSignature(haystack, signatures) {
112
+ const lower = haystack.toLowerCase();
113
+ return signatures.some((needle) => lower.includes(needle));
114
+ }
115
+ // Classifies a NON-ZERO exit into a container-specific failure reason by stderr signature, so the
116
+ // audit distinguishes a missing image / denied pull / mount failure from a plain non-zero exit.
117
+ function classifyNonZeroFailure(stderr) {
118
+ if (matchesSignature(stderr, IMAGE_MISSING_SIGNATURES))
119
+ return "image-missing";
120
+ if (matchesSignature(stderr, PULL_DENIED_SIGNATURES))
121
+ return "pull-denied";
122
+ if (matchesSignature(stderr, MOUNT_FAILURE_SIGNATURES))
123
+ return "mount-failure";
124
+ return "non-zero-exit";
125
+ }
126
+ function outcomeFromResult(result) {
127
+ // runCommand resolves only on a real process exit (timeout/cancel reject). A truncation that
128
+ // killed the child is terminal → output-capped; otherwise classify by exit code + stderr.
129
+ const failureReason = result.truncated
130
+ ? "output-capped"
131
+ : result.exitCode === 0
132
+ ? "none"
133
+ : classifyNonZeroFailure(result.stderr);
134
+ return {
135
+ exitCode: result.exitCode,
136
+ durationMs: result.durationMs,
137
+ truncated: result.truncated,
138
+ timedOut: false,
139
+ failureReason,
140
+ eventKind: failureReason === "none" ? "run-completed" : "run-failed",
141
+ stdout: result.stdout,
142
+ stderr: result.stderr,
143
+ };
144
+ }
145
+ function deniedFailureReason(error) {
146
+ if (error instanceof CommandDeniedError && !error.message.includes("not found on PATH")) {
147
+ return "denied";
148
+ }
149
+ return "spawn-error";
150
+ }
151
+ function outcomeFromError(error, cancelledByUser, durationMs) {
152
+ const base = { exitCode: null, durationMs, truncated: false, stdout: "", stderr: "" };
153
+ if (error instanceof CommandCancelledError || cancelledByUser) {
154
+ return { ...base, timedOut: false, failureReason: "cancelled", eventKind: "run-cancelled" };
155
+ }
156
+ if (error instanceof CommandTimeoutError) {
157
+ return { ...base, timedOut: true, failureReason: "timed-out", eventKind: "run-failed" };
158
+ }
159
+ return {
160
+ ...base,
161
+ timedOut: false,
162
+ failureReason: deniedFailureReason(error),
163
+ eventKind: "run-failed",
164
+ };
165
+ }
166
+ function clampTimeout(requested, ceiling) {
167
+ if (requested === undefined || !Number.isFinite(requested)) {
168
+ return ceiling;
169
+ }
170
+ const rounded = Math.round(requested);
171
+ if (rounded <= MIN_TIMEOUT_MS)
172
+ return MIN_TIMEOUT_MS;
173
+ if (rounded >= ceiling)
174
+ return ceiling;
175
+ return rounded;
176
+ }
177
+ function requestIdPayload(input) {
178
+ return input.requestId === undefined ? {} : { requestId: input.requestId };
179
+ }
180
+ function projectFor(store, projectId) {
181
+ for (const project of store.listProjects()) {
182
+ if (project.path === projectId) {
183
+ return project;
184
+ }
185
+ }
186
+ return undefined;
187
+ }
188
+ function projectRootOrThrow(project) {
189
+ try {
190
+ return nodeWorkspaceFs.realPath(project.path);
191
+ }
192
+ catch {
193
+ throw new ContainerRunnerError("PROJECT_NOT_FOUND", "Project root path could not be resolved.");
194
+ }
195
+ }
196
+ function buildWorkspaceInfo(projectRoot) {
197
+ return {
198
+ root: projectRoot,
199
+ name: undefined,
200
+ version: undefined,
201
+ testFramework: "unknown",
202
+ sourceDirs: [],
203
+ testDirs: [],
204
+ languages: [],
205
+ ignoreLines: [],
206
+ };
207
+ }
208
+ class ContainerRunnerManagerImpl {
209
+ store;
210
+ evidenceStore;
211
+ policy;
212
+ executionPolicy;
213
+ catalog;
214
+ processEnv;
215
+ redactor;
216
+ runDeps;
217
+ detect;
218
+ now;
219
+ runs = new Map();
220
+ subscribers = new Set();
221
+ constructor(opts) {
222
+ this.store = opts.store;
223
+ this.evidenceStore = opts.evidenceStore;
224
+ this.policy = opts.policy ?? DEFAULT_SANDBOX_POLICY;
225
+ this.executionPolicy = opts.executionPolicy ?? DEFAULT_CONTAINER_EXECUTION_POLICY;
226
+ this.catalog = opts.catalog ?? DEFAULT_CONTAINER_TASKS;
227
+ this.processEnv = opts.processEnv ?? process.env;
228
+ this.redactor = opts.redactor ?? ((input) => input);
229
+ this.runDeps = opts.runDeps ?? {};
230
+ this.detect = opts.detect;
231
+ this.now = opts.now ?? Date.now;
232
+ }
233
+ inFlightCount = () => this.runs.size;
234
+ subscribe = (listener) => {
235
+ this.subscribers.add(listener);
236
+ return () => {
237
+ this.subscribers.delete(listener);
238
+ };
239
+ };
240
+ abort = (runId) => {
241
+ const entry = this.runs.get(runId);
242
+ if (entry === undefined)
243
+ return false;
244
+ entry.cancelledByUser = true;
245
+ entry.controller.abort();
246
+ return true;
247
+ };
248
+ capability = (projectId) => {
249
+ return this.resolveCapability(projectId);
250
+ };
251
+ listCatalog = async (projectId) => {
252
+ const capability = await this.resolveCapability(projectId);
253
+ // Graceful degradation: no engine → engineAvailable:false + tasks:[] (never an error).
254
+ const engineAvailable = capability.anyAvailable;
255
+ return {
256
+ schemaVersion: CONTAINER_RUNTIME_SCHEMA_VERSION,
257
+ projectId,
258
+ engineAvailable,
259
+ tasks: engineAvailable ? this.catalog : [],
260
+ };
261
+ };
262
+ execute = async (input) => {
263
+ const workspace = this.resolveWorkspace(input.projectId);
264
+ // Engine-unavailable is a GOVERNANCE failure: throw BEFORE minting a run id (route → 503).
265
+ const capability = await this.resolveCapability(input.projectId);
266
+ if (!capability.anyAvailable) {
267
+ throw new ContainerRunnerError("CONTAINER_ENGINE_UNAVAILABLE", "No container engine is available.");
268
+ }
269
+ const task = this.catalog.find((entry) => entry.id === input.taskId);
270
+ if (task === undefined) {
271
+ throw new ContainerRunnerError("TASK_NOT_FOUND", "Task is not in the container catalog.");
272
+ }
273
+ if (!this.executionPolicy.imageAllowlist.includes(task.image)) {
274
+ throw new ContainerRunnerError("IMAGE_NOT_ALLOWED", "Task image is not allowlisted.");
275
+ }
276
+ if (this.runs.size >= MAX_CONCURRENT_CONTAINER_RUNS) {
277
+ throw new ContainerRunnerError("RUN_LIMIT_EXCEEDED", "Too many in-flight container runs.");
278
+ }
279
+ return this.runExecution(task, workspace, input);
280
+ };
281
+ resolveCapability(projectId) {
282
+ if (this.detect !== undefined) {
283
+ return this.detect(projectId);
284
+ }
285
+ const probeDeps = {
286
+ runCommand,
287
+ workspace: this.tryWorkspace(projectId),
288
+ policy: this.policy,
289
+ processEnv: this.processEnv,
290
+ now: this.now,
291
+ };
292
+ return detectContainerEngines(probeDeps);
293
+ }
294
+ tryWorkspace(projectId) {
295
+ const project = projectFor(this.store, projectId);
296
+ if (project === undefined) {
297
+ return undefined;
298
+ }
299
+ try {
300
+ return buildWorkspaceInfo(projectRootOrThrow(project));
301
+ }
302
+ catch {
303
+ return undefined;
304
+ }
305
+ }
306
+ resolveWorkspace(projectId) {
307
+ const project = projectFor(this.store, projectId);
308
+ if (project === undefined) {
309
+ throw new ContainerRunnerError("PROJECT_NOT_FOUND", "Project not found.");
310
+ }
311
+ return buildWorkspaceInfo(projectRootOrThrow(project));
312
+ }
313
+ buildRunDeps(workspace) {
314
+ return {
315
+ workspace,
316
+ // Host CLI policy: network:"inherit" so the docker/podman CLI reaches the daemon socket. The
317
+ // CONTAINER is isolated by --network none in the frozen argv (D4).
318
+ policy: { ...this.policy, network: "inherit" },
319
+ commandRules: CONTAINER_TASK_RULES,
320
+ spawn: this.runDeps.spawn ?? nodeSpawnFn,
321
+ processEnv: this.processEnv,
322
+ now: this.runDeps.now ?? this.now,
323
+ ...(this.runDeps.resolveExecutable === undefined
324
+ ? {}
325
+ : { resolveExecutable: this.runDeps.resolveExecutable }),
326
+ ...(this.runDeps.fs === undefined ? {} : { fs: this.runDeps.fs }),
327
+ ...(this.runDeps.home === undefined ? {} : { home: this.runDeps.home }),
328
+ };
329
+ }
330
+ async runExecution(task, workspace, input) {
331
+ const runId = randomUUID();
332
+ const controller = new AbortController();
333
+ const entry = { controller, cancelledByUser: false };
334
+ this.runs.set(runId, entry);
335
+ const startedAt = this.now();
336
+ this.emit({
337
+ kind: "run-started",
338
+ runId,
339
+ payload: {
340
+ taskId: task.id,
341
+ kind: task.kind,
342
+ engine: task.engine,
343
+ startedAt,
344
+ ...requestIdPayload(input),
345
+ },
346
+ });
347
+ try {
348
+ return await this.invoke(runId, task, workspace, input, entry, startedAt);
349
+ }
350
+ finally {
351
+ this.runs.delete(runId);
352
+ }
353
+ }
354
+ async invoke(runId, task, workspace, input, entry, startedAt) {
355
+ const deps = this.buildRunDeps(workspace);
356
+ const argv = buildContainerRunArgv(task, this.executionPolicy, workspace.root);
357
+ const timeoutMs = clampTimeout(input.timeoutMs, this.policy.defaultTimeoutMs);
358
+ try {
359
+ // Single governed spawn boundary; the injected fake spawn (if any) rides in `deps.spawn`.
360
+ const result = await runCommand({
361
+ command: task.engine,
362
+ args: argv,
363
+ cwd: undefined,
364
+ timeoutMs,
365
+ signal: entry.controller.signal,
366
+ }, deps);
367
+ return this.finalize(runId, task, argv.length, input, outcomeFromResult(result), startedAt);
368
+ }
369
+ catch (error) {
370
+ const outcome = outcomeFromError(error, entry.cancelledByUser, this.now() - startedAt);
371
+ return this.finalize(runId, task, argv.length, input, outcome, startedAt);
372
+ }
373
+ }
374
+ finalize(runId, task, argCount, input, outcome, startedAt) {
375
+ this.persist(runId, task, argCount, input, outcome, startedAt);
376
+ this.emit({
377
+ kind: outcome.eventKind,
378
+ runId,
379
+ payload: {
380
+ exitCode: outcome.exitCode,
381
+ durationMs: outcome.durationMs,
382
+ truncated: outcome.truncated,
383
+ timedOut: outcome.timedOut,
384
+ failureReason: outcome.failureReason,
385
+ ...requestIdPayload(input),
386
+ },
387
+ });
388
+ return {
389
+ schemaVersion: CONTAINER_RUNTIME_SCHEMA_VERSION,
390
+ runId,
391
+ taskId: task.id,
392
+ kind: task.kind,
393
+ engine: task.engine,
394
+ exitCode: outcome.exitCode,
395
+ durationMs: outcome.durationMs,
396
+ truncated: outcome.truncated,
397
+ timedOut: outcome.timedOut,
398
+ failureReason: outcome.failureReason,
399
+ stdout: outcome.stdout,
400
+ stderr: outcome.stderr,
401
+ };
402
+ }
403
+ persist(runId, task, argCount, input, outcome, startedAt) {
404
+ if (this.evidenceStore === undefined)
405
+ return;
406
+ try {
407
+ const evidence = buildContainerRunEvidenceEntry({
408
+ runId,
409
+ projectId: input.projectId,
410
+ taskId: task.id,
411
+ kind: task.kind,
412
+ engine: task.engine,
413
+ imageId: task.id, // closed-catalog id, NOT the raw image ref free-text
414
+ argCount,
415
+ exitCode: outcome.exitCode,
416
+ durationMs: outcome.durationMs,
417
+ timedOut: outcome.timedOut,
418
+ truncated: outcome.truncated,
419
+ failureReason: outcome.failureReason,
420
+ stdoutBytes: Buffer.byteLength(outcome.stdout, "utf8"),
421
+ stderrBytes: Buffer.byteLength(outcome.stderr, "utf8"),
422
+ startedAt,
423
+ });
424
+ appendContainerRunEvidence(this.evidenceStore, evidence, this.redactor);
425
+ }
426
+ catch {
427
+ // Evidence is best-effort process-evidence; a write hiccup must not corrupt a real run result.
428
+ }
429
+ }
430
+ emit(event) {
431
+ for (const listener of [...this.subscribers]) {
432
+ try {
433
+ listener(event);
434
+ }
435
+ catch {
436
+ // A subscriber throwing must not stop fan-out (matches the command-runner pattern).
437
+ }
438
+ }
439
+ }
440
+ }
441
+ export function createContainerRunnerManager(opts) {
442
+ return new ContainerRunnerManagerImpl(opts);
443
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAIA,OAAO,EAAsC,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;AAejG,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAI9D,eAAO,MAAM,eAAe,OAAO,CAAC;AACpC,eAAO,MAAM,OAAO,cAAc,CAAC;AAEnC,MAAM,WAAW,YAAY;IAE3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAE5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAGrB,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;IAEpE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAGtB,QAAQ,CAAC,WAAW,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;CAClD;AA0JD,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAgBzD"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAIA,OAAO,EAAsC,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;AAejG,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAM9D,eAAO,MAAM,eAAe,OAAO,CAAC;AACpC,eAAO,MAAM,OAAO,cAAc,CAAC;AAEnC,MAAM,WAAW,YAAY;IAE3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAE5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAGrB,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;IAEpE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAGtB,QAAQ,CAAC,WAAW,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;CAClD;AAiKD,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CA2BzD"}
package/dist/server.js CHANGED
@@ -8,6 +8,8 @@ import { isAllowedHost } from "./host-check.js";
8
8
  import { resolveContainedPath, serveFile } from "./static.js";
9
9
  import { errorBody, isApiPath, matchRoute, methodNotAllowedBody, notFoundBody, STREAMING, } from "./routes.js";
10
10
  import { buildRedactor } from "./deps.js";
11
+ import { isVoiceDictationCapable, isVoiceRealtimeCapable } from "./read-handlers.js";
12
+ import { createVoiceControlPlane } from "./voice-realtime.js";
11
13
  import { createRunRegistry } from "./runs.js";
12
14
  import { createInMemoryUiStore } from "./store/index.js";
13
15
  export const DEFAULT_UI_PORT = 1983;
@@ -117,7 +119,12 @@ async function resolveCsp(deps) {
117
119
  async function handle(deps, handlerDeps, req, res) {
118
120
  const url = new URL(req.url ?? "/", `http://${UI_HOST}`);
119
121
  const apiPath = isApiPath(url.pathname);
120
- applySecurityHeaders(res, await resolveCsp(deps), apiPath);
122
+ // Issue #495/#497 — scope the Permissions-Policy microphone directive to deployments that advertise
123
+ // speech-to-text dictation OR full-realtime voice (whose WebRTC capture track also needs the mic);
124
+ // a no-voice deployment keeps the strict `microphone=()` default, never widened beyond `(self)`.
125
+ applySecurityHeaders(res, await resolveCsp(deps), apiPath, {
126
+ allowMicrophone: isVoiceDictationCapable(handlerDeps) || isVoiceRealtimeCapable(handlerDeps),
127
+ });
121
128
  if (!isAllowedHost(req, deps.port)) {
122
129
  rejectForbiddenHost(res);
123
130
  return;
@@ -130,10 +137,16 @@ async function handle(deps, handlerDeps, req, res) {
130
137
  await serveStatic(res, deps.staticRoot, url.pathname);
131
138
  }
132
139
  // Creates the BFF server. The caller binds it with `server.listen(deps.port, UI_HOST)` so it never
133
- // listens on a non-loopback interface. The previous PTY WebSocket upgrade handler is removed —
134
- // the terminal tool is now bounded-exec over plain HTTP (ADR-0018 D1/D8).
140
+ // listens on a non-loopback interface. The previous PTY WebSocket upgrade handler is removed — the
141
+ // terminal tool is now bounded-exec over plain HTTP (ADR-0018 D1/D8). Issue #497 (ADR-0058 D3,
142
+ // ADR-0059) re-opens the upgrade for the single loopback voice control path `/api/voice/control`, and
143
+ // ONLY when the deployment is full-realtime voice capable; every other upgrade keeps the hard reject.
135
144
  export function createUiServer(deps) {
136
145
  const handlerDeps = deps.handlerDeps ?? fallbackDeps();
146
+ const voiceControl = createVoiceControlPlane({
147
+ port: deps.port,
148
+ handlerDeps: () => handlerDeps,
149
+ });
137
150
  const server = createServer((req, res) => {
138
151
  void handle(deps, handlerDeps, req, res).catch(() => {
139
152
  if (!res.headersSent) {
@@ -144,9 +157,16 @@ export function createUiServer(deps) {
144
157
  }
145
158
  });
146
159
  });
147
- server.on("upgrade", (_req, socket) => {
160
+ server.on("upgrade", (req, socket, head) => {
161
+ if (voiceControl.handleUpgrade(req, socket, head)) {
162
+ return;
163
+ }
164
+ // Default: every non-voice-control or ungated upgrade is hard-rejected, as before.
148
165
  socket.write("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
149
166
  socket.destroy();
150
167
  });
168
+ server.on("close", () => {
169
+ voiceControl.closeAll();
170
+ });
151
171
  return server;
152
172
  }
@@ -1 +1 @@
1
- {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/store/db.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAI3C,OAAO,KAAK,EAMV,OAAO,EACP,qBAAqB,EAKtB,MAAM,YAAY,CAAC;AAiCpB,wBAAgB,kBAAkB,CAAC,OAAO,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAM9E;AA+JD,eAAO,MAAM,qBAAqB,OAAQ,CAAC;AAa3C,wBAAgB,qBAAqB,CAAC,IAAI,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAI3E;AAgCD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAkB/D;AAED,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAEhG;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAEvF"}
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/store/db.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAI3C,OAAO,KAAK,EAMV,OAAO,EACP,qBAAqB,EAKtB,MAAM,YAAY,CAAC;AAkCpB,wBAAgB,kBAAkB,CAAC,OAAO,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAM9E;AAiKD,eAAO,MAAM,qBAAqB,OAAQ,CAAC;AAa3C,wBAAgB,qBAAqB,CAAC,IAAI,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAI3E;AAgCD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAkB/D;AAED,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAEhG;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAEvF"}
package/dist/store/db.js CHANGED
@@ -8,7 +8,7 @@ import { randomUUID } from "node:crypto";
8
8
  import { runMigrations } from "./schema.js";
9
9
  import { deleteProject as sqlDeleteProject, getProject as sqlGetProject, listProjects as sqlListProjects, updateProject as sqlUpdateProject, upsertProject as sqlUpsertProject, } from "./projects.js";
10
10
  import { deleteChat as sqlDeleteChat, findChatById as sqlFindChatById, insertChat as sqlInsertChat, listChats as sqlListChats, listChatsLimited as sqlListChatsLimited, touchChat as sqlTouchChat, updateChat as sqlUpdateChat, } from "./chats.js";
11
- import { findMessageById as sqlFindMessageById, insertMessage as sqlInsertMessage, listMessages as sqlListMessages, listMessagesLimited as sqlListMessagesLimited, updateMessage as sqlUpdateMessage, } from "./messages.js";
11
+ import { findMessageById as sqlFindMessageById, attachGroundedAnswer as sqlAttachGroundedAnswer, insertMessage as sqlInsertMessage, listMessages as sqlListMessages, listMessagesLimited as sqlListMessagesLimited, updateMessage as sqlUpdateMessage, } from "./messages.js";
12
12
  import { validateProjectPath } from "./validation.js";
13
13
  import { basename } from "node:path";
14
14
  import { invalidRequest } from "./errors.js";
@@ -111,6 +111,7 @@ function buildStore(db, options) {
111
111
  },
112
112
  createMessages: (messages) => createMessageBatch(db, options, messages),
113
113
  updateMessage: (id, patch) => sqlUpdateMessage(db, id, patch, options.redactString),
114
+ attachGroundedAnswer: (id, answer) => sqlAttachGroundedAnswer(db, id, answer, options.redactString),
114
115
  close: () => {
115
116
  db.close();
116
117
  },
@@ -1,4 +1,4 @@
1
- export type { Chat, ChatConnectedScope, ChatLocalKnowledgeScope, ChatMessage, ChatRole, CreateChatOptions, NewChatMessage, Project, UiStore, UiStoreFactoryOptions, UpdateChatOptions, UpdateChatMessagePatch, UpdateChatPatch, UpdateProjectPatch, WorkflowStatus, } from "./types.js";
1
+ export type { Chat, ChatConnectedScope, ChatLocalKnowledgeScope, ChatMessage, ChatRole, CreateChatOptions, GroundedAnswer, NewChatMessage, Project, UiStore, UiStoreFactoryOptions, UpdateChatOptions, UpdateChatMessagePatch, UpdateChatPatch, UpdateProjectPatch, WorkflowStatus, } from "./types.js";
2
2
  export { UiStoreError, type UiStoreErrorCode, invalidPath, invalidRequest, notFound, pathNotDirectory, pathNotFound, projectExists, } from "./errors.js";
3
3
  export { classifyPathShape, validateProjectPath, type PathShape, type ValidateProjectPathOptions, } from "./validation.js";
4
4
  export { assertUiDbOutsideProject, resolveUiDbPath, UI_DB_FILENAME, UI_DB_DIRNAME, } from "./paths.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/store/index.ts"],"names":[],"mappings":"AAEA,YAAY,EACV,IAAI,EACJ,kBAAkB,EAClB,uBAAuB,EACvB,WAAW,EACX,QAAQ,EACR,iBAAiB,EACjB,cAAc,EACd,OAAO,EACP,OAAO,EACP,qBAAqB,EACrB,iBAAiB,EACjB,sBAAsB,EACtB,eAAe,EACf,kBAAkB,EAClB,cAAc,GACf,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,YAAY,EACZ,KAAK,gBAAgB,EACrB,WAAW,EACX,cAAc,EACd,QAAQ,EACR,gBAAgB,EAChB,YAAY,EACZ,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,SAAS,EACd,KAAK,0BAA0B,GAChC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,wBAAwB,EACxB,eAAe,EACf,cAAc,EACd,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EACL,wBAAwB,EACxB,qBAAqB,EACrB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/store/index.ts"],"names":[],"mappings":"AAEA,YAAY,EACV,IAAI,EACJ,kBAAkB,EAClB,uBAAuB,EACvB,WAAW,EACX,QAAQ,EACR,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,OAAO,EACP,OAAO,EACP,qBAAqB,EACrB,iBAAiB,EACjB,sBAAsB,EACtB,eAAe,EACf,kBAAkB,EAClB,cAAc,GACf,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,YAAY,EACZ,KAAK,gBAAgB,EACrB,WAAW,EACX,cAAc,EACd,QAAQ,EACR,gBAAgB,EAChB,YAAY,EACZ,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,SAAS,EACd,KAAK,0BAA0B,GAChC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,wBAAwB,EACxB,eAAe,EACf,cAAc,EACd,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EACL,wBAAwB,EACxB,qBAAqB,EACrB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,SAAS,CAAC"}
@@ -1,8 +1,9 @@
1
1
  import type { DatabaseSync } from "node:sqlite";
2
- import type { ChatMessage, NewChatMessage, UpdateChatMessagePatch } from "./types.js";
2
+ import type { ChatMessage, GroundedAnswer, NewChatMessage, UpdateChatMessagePatch } from "./types.js";
3
3
  export declare function listMessages(db: DatabaseSync, chatId: string): readonly ChatMessage[];
4
4
  export declare function listMessagesLimited(db: DatabaseSync, chatId: string, limit: number): readonly ChatMessage[];
5
5
  export declare function findMessageById(db: DatabaseSync, id: string): ChatMessage | undefined;
6
6
  export declare function insertMessage(db: DatabaseSync, id: string, msg: NewChatMessage, redactString: (s: string) => string): ChatMessage;
7
+ export declare function attachGroundedAnswer(db: DatabaseSync, id: string, answer: GroundedAnswer, redactString: (s: string) => string): ChatMessage;
7
8
  export declare function updateMessage(db: DatabaseSync, id: string, patch: UpdateChatMessagePatch, redactString: (s: string) => string): ChatMessage;
8
9
  //# sourceMappingURL=messages.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/store/messages.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EACV,WAAW,EAEX,cAAc,EACd,sBAAsB,EAEvB,MAAM,YAAY,CAAC;AA+GpB,wBAAgB,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,WAAW,EAAE,CAErF;AAED,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,YAAY,EAChB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,SAAS,WAAW,EAAE,CAOxB;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAGrF;AAED,wBAAgB,aAAa,CAC3B,EAAE,EAAE,YAAY,EAChB,EAAE,EAAE,MAAM,EACV,GAAG,EAAE,cAAc,EACnB,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAClC,WAAW,CAoBb;AAOD,wBAAgB,aAAa,CAC3B,EAAE,EAAE,YAAY,EAChB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,sBAAsB,EAC7B,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAClC,WAAW,CA6Bb"}
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/store/messages.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EACV,WAAW,EAEX,cAAc,EACd,cAAc,EACd,sBAAsB,EAEvB,MAAM,YAAY,CAAC;AA4IpB,wBAAgB,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,WAAW,EAAE,CAErF;AAED,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,YAAY,EAChB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,SAAS,WAAW,EAAE,CAOxB;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAGrF;AAED,wBAAgB,aAAa,CAC3B,EAAE,EAAE,YAAY,EAChB,EAAE,EAAE,MAAM,EACV,GAAG,EAAE,cAAc,EACnB,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAClC,WAAW,CAsBb;AAED,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,YAAY,EAChB,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,cAAc,EACtB,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAClC,WAAW,CAcb;AAOD,wBAAgB,aAAa,CAC3B,EAAE,EAAE,YAAY,EAChB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,sBAAsB,EAC7B,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAClC,WAAW,CA6Bb"}
@@ -17,7 +17,18 @@ const STATUSES = new Set([
17
17
  "failed",
18
18
  "cancelled",
19
19
  ]);
20
+ function parseGroundedAnswer(raw) {
21
+ if (raw === null)
22
+ return undefined;
23
+ try {
24
+ return JSON.parse(raw);
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ }
20
30
  function rowToMessage(row) {
31
+ const groundedAnswer = parseGroundedAnswer(row.grounded_answer_json);
21
32
  return {
22
33
  id: row.id,
23
34
  chatId: row.chat_id,
@@ -29,17 +40,18 @@ function rowToMessage(row) {
29
40
  workflowStatus: (row.workflow_status ?? undefined),
30
41
  shortResult: row.short_result ?? undefined,
31
42
  taskType: row.task_type ?? undefined,
43
+ ...(groundedAnswer === undefined ? {} : { groundedAnswer }),
32
44
  };
33
45
  }
34
- const COLUMNS = "id, chat_id, role, content, timestamp, run_id, workflow_id, workflow_status, short_result, task_type";
46
+ const COLUMNS = "id, chat_id, role, content, timestamp, run_id, workflow_id, workflow_status, short_result, task_type, grounded_answer_json";
35
47
  const SQL_LIST = `SELECT ${COLUMNS} FROM chat_messages WHERE chat_id = ? ORDER BY timestamp ASC, rowid ASC`;
36
48
  const SQL_LIST_LIMITED = `${SQL_LIST} LIMIT ?`;
37
49
  const SQL_FIND_BY_ID = `SELECT ${COLUMNS} FROM chat_messages WHERE id = ? LIMIT 1`;
38
50
  const SQL_CHAT_EXISTS = "SELECT 1 FROM chats WHERE id = ?";
39
51
  const SQL_INSERT = `
40
52
  INSERT INTO chat_messages
41
- (id, chat_id, role, content, timestamp, run_id, workflow_id, workflow_status, short_result, task_type)
42
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
53
+ (id, chat_id, role, content, timestamp, run_id, workflow_id, workflow_status, short_result, task_type, grounded_answer_json)
54
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
43
55
  RETURNING ${COLUMNS}
44
56
  `;
45
57
  function validateTaskType(value) {
@@ -66,6 +78,9 @@ function validateRunSummaryScope(msg) {
66
78
  if (hasRunSummaryFields(msg) && (msg.role !== "system" || msg.runId === undefined)) {
67
79
  throw invalidRequest("Run summary fields require a system message with runId.");
68
80
  }
81
+ if (msg.groundedAnswer !== undefined && msg.role !== "assistant") {
82
+ throw invalidRequest("Grounded answer metadata requires an assistant message.");
83
+ }
69
84
  }
70
85
  function validateMessage(msg) {
71
86
  if (!ROLES.has(msg.role))
@@ -86,6 +101,18 @@ function processShortResult(raw, redactString) {
86
101
  const redacted = redactString(raw);
87
102
  return redacted.length > MAX_SHORT_RESULT ? redacted.slice(0, MAX_SHORT_RESULT) : redacted;
88
103
  }
104
+ function processGroundedAnswer(raw, redactString) {
105
+ if (raw === undefined)
106
+ return null;
107
+ const redacted = redactString(JSON.stringify(raw));
108
+ try {
109
+ JSON.parse(redacted);
110
+ }
111
+ catch {
112
+ throw invalidRequest("Grounded answer metadata is invalid.");
113
+ }
114
+ return redacted;
115
+ }
89
116
  export function listMessages(db, chatId) {
90
117
  return db.prepare(SQL_LIST).all(chatId).map(rowToMessage);
91
118
  }
@@ -105,9 +132,24 @@ export function insertMessage(db, id, msg, redactString) {
105
132
  if (!chatExists)
106
133
  throw notFound("Chat");
107
134
  const shortResult = processShortResult(msg.shortResult, redactString);
135
+ const groundedAnswer = processGroundedAnswer(msg.groundedAnswer, redactString);
108
136
  const row = db
109
137
  .prepare(SQL_INSERT)
110
- .get(id, msg.chatId, msg.role, msg.content, msg.timestamp, msg.runId ?? null, msg.workflowId ?? null, msg.workflowStatus ?? null, shortResult, msg.taskType ?? null);
138
+ .get(id, msg.chatId, msg.role, msg.content, msg.timestamp, msg.runId ?? null, msg.workflowId ?? null, msg.workflowStatus ?? null, shortResult, msg.taskType ?? null, groundedAnswer);
139
+ return rowToMessage(row);
140
+ }
141
+ export function attachGroundedAnswer(db, id, answer, redactString) {
142
+ const groundedAnswer = processGroundedAnswer(answer, redactString);
143
+ const row = db
144
+ .prepare(`
145
+ UPDATE chat_messages
146
+ SET grounded_answer_json = ?
147
+ WHERE id = ? AND role = 'assistant'
148
+ RETURNING ${COLUMNS}
149
+ `)
150
+ .get(groundedAnswer, id);
151
+ if (row === undefined)
152
+ throw notFound("Message");
111
153
  return rowToMessage(row);
112
154
  }
113
155
  // Issue #66 — Partial PATCH on a system run-summary message. Builds a dynamic SET clause from the
@@ -1,4 +1,4 @@
1
1
  import type { DatabaseSync } from "node:sqlite";
2
- export declare const SCHEMA_VERSION = 5;
2
+ export declare const SCHEMA_VERSION = 8;
3
3
  export declare function runMigrations(db: DatabaseSync): void;
4
4
  //# sourceMappingURL=schema.d.ts.map