@oscharko-dev/keiko-server 0.2.8 → 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 (281) 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 +27 -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 +45 -0
  84. package/dist/files.d.ts.map +1 -1
  85. package/dist/files.js +631 -7
  86. package/dist/gateway-readiness.js +3 -3
  87. package/dist/gateway-setup.d.ts +2 -0
  88. package/dist/gateway-setup.d.ts.map +1 -1
  89. package/dist/gateway-setup.js +275 -11
  90. package/dist/gitDelivery/actionSheetProjection.d.ts +30 -0
  91. package/dist/gitDelivery/actionSheetProjection.d.ts.map +1 -0
  92. package/dist/gitDelivery/actionSheetProjection.js +206 -0
  93. package/dist/gitDelivery/actionSheetRoutes.d.ts +29 -0
  94. package/dist/gitDelivery/actionSheetRoutes.d.ts.map +1 -0
  95. package/dist/gitDelivery/actionSheetRoutes.js +293 -0
  96. package/dist/gitDelivery/agentOperationsRoutes.d.ts +33 -0
  97. package/dist/gitDelivery/agentOperationsRoutes.d.ts.map +1 -0
  98. package/dist/gitDelivery/agentOperationsRoutes.js +405 -0
  99. package/dist/gitDelivery/commitRoutes.d.ts +23 -0
  100. package/dist/gitDelivery/commitRoutes.d.ts.map +1 -0
  101. package/dist/gitDelivery/commitRoutes.js +204 -0
  102. package/dist/gitDelivery/evidenceRoutes.d.ts +9 -0
  103. package/dist/gitDelivery/evidenceRoutes.d.ts.map +1 -0
  104. package/dist/gitDelivery/evidenceRoutes.js +101 -0
  105. package/dist/gitDelivery/execution.d.ts +38 -0
  106. package/dist/gitDelivery/execution.d.ts.map +1 -0
  107. package/dist/gitDelivery/execution.js +117 -0
  108. package/dist/gitDelivery/localMutationRoutes.d.ts +30 -0
  109. package/dist/gitDelivery/localMutationRoutes.d.ts.map +1 -0
  110. package/dist/gitDelivery/localMutationRoutes.js +165 -0
  111. package/dist/gitDelivery/mergeExecution.d.ts +63 -0
  112. package/dist/gitDelivery/mergeExecution.d.ts.map +1 -0
  113. package/dist/gitDelivery/mergeExecution.js +168 -0
  114. package/dist/gitDelivery/mergeRoutes.d.ts +12 -0
  115. package/dist/gitDelivery/mergeRoutes.d.ts.map +1 -0
  116. package/dist/gitDelivery/mergeRoutes.js +218 -0
  117. package/dist/gitDelivery/mutationEvidenceLedger.d.ts +23 -0
  118. package/dist/gitDelivery/mutationEvidenceLedger.d.ts.map +1 -0
  119. package/dist/gitDelivery/mutationEvidenceLedger.js +87 -0
  120. package/dist/gitDelivery/prExecution.d.ts +54 -0
  121. package/dist/gitDelivery/prExecution.d.ts.map +1 -0
  122. package/dist/gitDelivery/prExecution.js +192 -0
  123. package/dist/gitDelivery/prRoutes.d.ts +12 -0
  124. package/dist/gitDelivery/prRoutes.d.ts.map +1 -0
  125. package/dist/gitDelivery/prRoutes.js +256 -0
  126. package/dist/gitDelivery/pushExecution.d.ts +43 -0
  127. package/dist/gitDelivery/pushExecution.d.ts.map +1 -0
  128. package/dist/gitDelivery/pushExecution.js +124 -0
  129. package/dist/gitDelivery/pushRoutes.d.ts +12 -0
  130. package/dist/gitDelivery/pushRoutes.d.ts.map +1 -0
  131. package/dist/gitDelivery/pushRoutes.js +200 -0
  132. package/dist/gitDelivery/requestGuards.d.ts +15 -0
  133. package/dist/gitDelivery/requestGuards.d.ts.map +1 -0
  134. package/dist/gitDelivery/requestGuards.js +97 -0
  135. package/dist/gitDelivery/syncEvidence.d.ts +37 -0
  136. package/dist/gitDelivery/syncEvidence.d.ts.map +1 -0
  137. package/dist/gitDelivery/syncEvidence.js +85 -0
  138. package/dist/gitDelivery/syncExecution.d.ts +30 -0
  139. package/dist/gitDelivery/syncExecution.d.ts.map +1 -0
  140. package/dist/gitDelivery/syncExecution.js +266 -0
  141. package/dist/gitDelivery/syncRoutes.d.ts +13 -0
  142. package/dist/gitDelivery/syncRoutes.d.ts.map +1 -0
  143. package/dist/gitDelivery/syncRoutes.js +200 -0
  144. package/dist/gitPorcelainStatus.d.ts +15 -0
  145. package/dist/gitPorcelainStatus.d.ts.map +1 -0
  146. package/dist/gitPorcelainStatus.js +104 -0
  147. package/dist/gitRepositoryReads.d.ts +10 -0
  148. package/dist/gitRepositoryReads.d.ts.map +1 -0
  149. package/dist/gitRepositoryReads.js +314 -0
  150. package/dist/gitRepositoryRoutes.d.ts +7 -0
  151. package/dist/gitRepositoryRoutes.d.ts.map +1 -0
  152. package/dist/gitRepositoryRoutes.js +221 -0
  153. package/dist/gitRoutes.d.ts +66 -0
  154. package/dist/gitRoutes.d.ts.map +1 -0
  155. package/dist/gitRoutes.js +543 -0
  156. package/dist/governed-workflow.d.ts +2 -0
  157. package/dist/governed-workflow.d.ts.map +1 -1
  158. package/dist/governed-workflow.js +4 -0
  159. package/dist/grounded-qa.d.ts +11 -0
  160. package/dist/grounded-qa.d.ts.map +1 -1
  161. package/dist/grounded-qa.js +13 -4
  162. package/dist/headers.d.ts +4 -1
  163. package/dist/headers.d.ts.map +1 -1
  164. package/dist/headers.js +11 -4
  165. package/dist/index.d.ts +8 -1
  166. package/dist/index.d.ts.map +1 -1
  167. package/dist/index.js +9 -1
  168. package/dist/qualityIntelligence/figmaSnapshotRoutes.d.ts +1 -1
  169. package/dist/qualityIntelligence/figmaSnapshotRoutes.d.ts.map +1 -1
  170. package/dist/qualityIntelligence/figmaSnapshotRoutes.js +1 -1
  171. package/dist/read-handlers.d.ts +5 -0
  172. package/dist/read-handlers.d.ts.map +1 -1
  173. package/dist/read-handlers.js +57 -1
  174. package/dist/routes.d.ts.map +1 -1
  175. package/dist/routes.js +259 -6
  176. package/dist/run-engine.d.ts.map +1 -1
  177. package/dist/run-engine.js +3 -0
  178. package/dist/run-handlers.d.ts.map +1 -1
  179. package/dist/run-handlers.js +74 -4
  180. package/dist/run-request.d.ts +11 -0
  181. package/dist/run-request.d.ts.map +1 -1
  182. package/dist/run-request.js +158 -10
  183. package/dist/runtime/capabilityDetector.d.ts +38 -0
  184. package/dist/runtime/capabilityDetector.d.ts.map +1 -0
  185. package/dist/runtime/capabilityDetector.js +443 -0
  186. package/dist/runtime/capabilityRoutes.d.ts +9 -0
  187. package/dist/runtime/capabilityRoutes.d.ts.map +1 -0
  188. package/dist/runtime/capabilityRoutes.js +45 -0
  189. package/dist/runtime/containerEngineDetector.d.ts +17 -0
  190. package/dist/runtime/containerEngineDetector.d.ts.map +1 -0
  191. package/dist/runtime/containerEngineDetector.js +222 -0
  192. package/dist/runtime/containerRoutes.d.ts +8 -0
  193. package/dist/runtime/containerRoutes.d.ts.map +1 -0
  194. package/dist/runtime/containerRoutes.js +207 -0
  195. package/dist/runtime/containerRunner-errors.d.ts +18 -0
  196. package/dist/runtime/containerRunner-errors.d.ts.map +1 -0
  197. package/dist/runtime/containerRunner-errors.js +42 -0
  198. package/dist/runtime/containerRunner-evidence.d.ts +24 -0
  199. package/dist/runtime/containerRunner-evidence.d.ts.map +1 -0
  200. package/dist/runtime/containerRunner-evidence.js +74 -0
  201. package/dist/runtime/containerRunner.d.ts +37 -0
  202. package/dist/runtime/containerRunner.d.ts.map +1 -0
  203. package/dist/runtime/containerRunner.js +443 -0
  204. package/dist/server.d.ts.map +1 -1
  205. package/dist/server.js +24 -4
  206. package/dist/store/schema.d.ts +1 -1
  207. package/dist/store/schema.d.ts.map +1 -1
  208. package/dist/store/schema.js +62 -1
  209. package/dist/task-workspace/active-store.d.ts +21 -0
  210. package/dist/task-workspace/active-store.d.ts.map +1 -0
  211. package/dist/task-workspace/active-store.js +55 -0
  212. package/dist/task-workspace/authorization.d.ts +7 -0
  213. package/dist/task-workspace/authorization.d.ts.map +1 -0
  214. package/dist/task-workspace/authorization.js +54 -0
  215. package/dist/task-workspace/binding.d.ts +3 -0
  216. package/dist/task-workspace/binding.d.ts.map +1 -0
  217. package/dist/task-workspace/binding.js +22 -0
  218. package/dist/task-workspace/cleanup.d.ts +4 -0
  219. package/dist/task-workspace/cleanup.d.ts.map +1 -0
  220. package/dist/task-workspace/cleanup.js +428 -0
  221. package/dist/task-workspace/errors.d.ts +14 -0
  222. package/dist/task-workspace/errors.d.ts.map +1 -0
  223. package/dist/task-workspace/errors.js +81 -0
  224. package/dist/task-workspace/evidence.d.ts +32 -0
  225. package/dist/task-workspace/evidence.d.ts.map +1 -0
  226. package/dist/task-workspace/evidence.js +52 -0
  227. package/dist/task-workspace/field-safety.d.ts +3 -0
  228. package/dist/task-workspace/field-safety.d.ts.map +1 -0
  229. package/dist/task-workspace/field-safety.js +42 -0
  230. package/dist/task-workspace/health.d.ts +4 -0
  231. package/dist/task-workspace/health.d.ts.map +1 -0
  232. package/dist/task-workspace/health.js +163 -0
  233. package/dist/task-workspace/lifecycle.d.ts +3 -0
  234. package/dist/task-workspace/lifecycle.d.ts.map +1 -0
  235. package/dist/task-workspace/lifecycle.js +248 -0
  236. package/dist/task-workspace/locks.d.ts +13 -0
  237. package/dist/task-workspace/locks.d.ts.map +1 -0
  238. package/dist/task-workspace/locks.js +44 -0
  239. package/dist/task-workspace/managed-root.d.ts +7 -0
  240. package/dist/task-workspace/managed-root.d.ts.map +1 -0
  241. package/dist/task-workspace/managed-root.js +98 -0
  242. package/dist/task-workspace/mutex.d.ts +8 -0
  243. package/dist/task-workspace/mutex.d.ts.map +1 -0
  244. package/dist/task-workspace/mutex.js +82 -0
  245. package/dist/task-workspace/naming.d.ts +15 -0
  246. package/dist/task-workspace/naming.d.ts.map +1 -0
  247. package/dist/task-workspace/naming.js +0 -0
  248. package/dist/task-workspace/provisioning.d.ts +3 -0
  249. package/dist/task-workspace/provisioning.d.ts.map +1 -0
  250. package/dist/task-workspace/provisioning.js +528 -0
  251. package/dist/task-workspace/reconciliation.d.ts +15 -0
  252. package/dist/task-workspace/reconciliation.d.ts.map +1 -0
  253. package/dist/task-workspace/reconciliation.js +274 -0
  254. package/dist/task-workspace/repair.d.ts +3 -0
  255. package/dist/task-workspace/repair.d.ts.map +1 -0
  256. package/dist/task-workspace/repair.js +286 -0
  257. package/dist/task-workspace/routes.d.ts +19 -0
  258. package/dist/task-workspace/routes.d.ts.map +1 -0
  259. package/dist/task-workspace/routes.js +481 -0
  260. package/dist/task-workspace/store.d.ts +12 -0
  261. package/dist/task-workspace/store.d.ts.map +1 -0
  262. package/dist/task-workspace/store.js +128 -0
  263. package/dist/task-workspace/types.d.ts +170 -0
  264. package/dist/task-workspace/types.d.ts.map +1 -0
  265. package/dist/task-workspace/types.js +5 -0
  266. package/dist/voice-action-governance.d.ts +23 -0
  267. package/dist/voice-action-governance.d.ts.map +1 -0
  268. package/dist/voice-action-governance.js +126 -0
  269. package/dist/voice-handlers.d.ts +6 -0
  270. package/dist/voice-handlers.d.ts.map +1 -0
  271. package/dist/voice-handlers.js +570 -0
  272. package/dist/voice-realtime-grounded-tool.d.ts +31 -0
  273. package/dist/voice-realtime-grounded-tool.d.ts.map +1 -0
  274. package/dist/voice-realtime-grounded-tool.js +322 -0
  275. package/dist/voice-realtime.d.ts +69 -0
  276. package/dist/voice-realtime.d.ts.map +1 -0
  277. package/dist/voice-realtime.js +787 -0
  278. package/dist/workspace-state-handlers.d.ts +5 -0
  279. package/dist/workspace-state-handlers.d.ts.map +1 -0
  280. package/dist/workspace-state-handlers.js +106 -0
  281. package/package.json +20 -19
@@ -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,4 +1,4 @@
1
1
  import type { DatabaseSync } from "node:sqlite";
2
- export declare const SCHEMA_VERSION = 6;
2
+ export declare const SCHEMA_VERSION = 8;
3
3
  export declare function runMigrations(db: DatabaseSync): void;
4
4
  //# sourceMappingURL=schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/store/schema.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,cAAc,IAAI,CAAC;AA4NhC,wBAAgB,aAAa,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAepD"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/store/schema.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,cAAc,IAAI,CAAC;AA2RhC,wBAAgB,aAAa,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAepD"}
@@ -1,7 +1,7 @@
1
1
  // ADR-0013 D5 — Schema v1 + migration runner via PRAGMA user_version. Forward-only, idempotent,
2
2
  // transactional. Each migration is a `.sql` string of one or more CREATE/ALTER statements; the
3
3
  // runner applies migrations whose 1-based index > current user_version.
4
- export const SCHEMA_VERSION = 6;
4
+ export const SCHEMA_VERSION = 8;
5
5
  const V1_SQL = `
6
6
  CREATE TABLE projects (
7
7
  path TEXT NOT NULL PRIMARY KEY,
@@ -189,6 +189,65 @@ CREATE INDEX idx_relationship_audit_relationship
189
189
  const V6_SQL = `
190
190
  ALTER TABLE chat_messages ADD COLUMN grounded_answer_json TEXT;
191
191
  `;
192
+ // V7 (issue #445, epic #443) — durable managed task-workspace instances. STRICT mode. One row per
193
+ // provisioned/activated task workspace; the partial unique index on (repository_id, task_id) enforces
194
+ // the idempotency invariant at the DB layer as a second barrier alongside the deterministic id
195
+ // derivation (AC3). All columns are content-free per the #444 contract (ids/hashes, enums, ISO
196
+ // timestamps, branch/path names); `lock_json`, `drift_markers_json`, and `recovery_hints_json` carry
197
+ // the nested WorkspaceLock / drift-marker / recovery-hint shapes the contract validator gates.
198
+ const V7_SQL = `
199
+ CREATE TABLE task_workspace_instances (
200
+ workspace_id TEXT NOT NULL PRIMARY KEY,
201
+ schema_version TEXT NOT NULL,
202
+ task_id TEXT NOT NULL,
203
+ repository_id TEXT NOT NULL,
204
+ repository_root TEXT NOT NULL,
205
+ base_branch TEXT NOT NULL,
206
+ task_branch TEXT NOT NULL,
207
+ managed_worktree_path TEXT NOT NULL,
208
+ gitdir_identity TEXT NOT NULL,
209
+ lifecycle_state TEXT NOT NULL,
210
+ health TEXT NOT NULL,
211
+ lock_json TEXT,
212
+ created_at TEXT NOT NULL,
213
+ updated_at TEXT NOT NULL,
214
+ last_verified_at TEXT,
215
+ last_verified_head TEXT,
216
+ drift_markers_json TEXT NOT NULL,
217
+ recovery_hints_json TEXT NOT NULL,
218
+ audit_correlation_id TEXT NOT NULL,
219
+ CHECK (
220
+ schema_version IN ('1')
221
+ AND lifecycle_state IN (
222
+ 'provisioning','active','paused','handoff-ready','archived','merged',
223
+ 'abandoned','recovery-required','failed','cleanup-pending'
224
+ )
225
+ AND health IN ('healthy','degraded','drifted','locked-out','missing','unknown')
226
+ )
227
+ ) STRICT;
228
+
229
+ CREATE UNIQUE INDEX uniq_task_workspace_repo_task
230
+ ON task_workspace_instances(repository_id, task_id);
231
+ CREATE INDEX idx_task_workspace_repository
232
+ ON task_workspace_instances(repository_id, updated_at);
233
+ `;
234
+ // V8 (issue #446, epic #443) — singleton active task-workspace pointer. Studio binds exactly ONE
235
+ // active task workspace at a time; this table holds at most one row (id pinned to the constant
236
+ // 'active', enforced by CHECK). The WorkspaceBinding surfaces consume is DERIVED from the referenced
237
+ // instance (binding.ts), never stored, so the active root has no second copy to drift. Content-free:
238
+ // an opaque workspace id + an opaque actor id + ISO timestamps only. ON DELETE CASCADE clears the
239
+ // pointer when the referenced instance is removed (the lifecycle service also clears it defensively).
240
+ const V8_SQL = `
241
+ CREATE TABLE task_workspace_active_pointer (
242
+ id TEXT NOT NULL PRIMARY KEY DEFAULT 'active',
243
+ workspace_id TEXT NOT NULL,
244
+ set_by TEXT NOT NULL,
245
+ set_at TEXT NOT NULL,
246
+ updated_at TEXT NOT NULL,
247
+ CHECK (id = 'active'),
248
+ FOREIGN KEY (workspace_id) REFERENCES task_workspace_instances(workspace_id) ON DELETE CASCADE
249
+ ) STRICT;
250
+ `;
192
251
  const MIGRATIONS = [
193
252
  { version: 1, sql: V1_SQL },
194
253
  { version: 2, sql: V2_SQL },
@@ -196,6 +255,8 @@ const MIGRATIONS = [
196
255
  { version: 4, sql: V4_SQL },
197
256
  { version: 5, sql: V5_SQL },
198
257
  { version: 6, sql: V6_SQL },
258
+ { version: 7, sql: V7_SQL },
259
+ { version: 8, sql: V8_SQL },
199
260
  ];
200
261
  function currentUserVersion(db) {
201
262
  const row = db.prepare("PRAGMA user_version").get();
@@ -0,0 +1,21 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+ export interface ActiveWorkspacePointer {
3
+ readonly workspaceId: string;
4
+ readonly setBy: string;
5
+ readonly setAt: string;
6
+ readonly updatedAt: string;
7
+ }
8
+ export interface ActiveWorkspacePointerStore {
9
+ /** The current active pointer, or undefined in unbound mode (no row). */
10
+ readonly get: () => ActiveWorkspacePointer | undefined;
11
+ /** Upsert the singleton row (id pinned to 'active'); returns the persisted pointer. */
12
+ readonly set: (input: {
13
+ readonly workspaceId: string;
14
+ readonly setBy: string;
15
+ readonly atIso: string;
16
+ }) => ActiveWorkspacePointer;
17
+ /** Delete the singleton row → unbound mode. Idempotent. */
18
+ readonly clear: () => void;
19
+ }
20
+ export declare function buildActiveWorkspacePointerStoreOverDatabase(db: DatabaseSync): ActiveWorkspacePointerStore;
21
+ //# sourceMappingURL=active-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"active-store.d.ts","sourceRoot":"","sources":["../../src/task-workspace/active-store.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,2BAA2B;IAC1C,yEAAyE;IACzE,QAAQ,CAAC,GAAG,EAAE,MAAM,sBAAsB,GAAG,SAAS,CAAC;IACvD,uFAAuF;IACvF,QAAQ,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE;QACpB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;QAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;KACxB,KAAK,sBAAsB,CAAC;IAC7B,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;CAC5B;AAsCD,wBAAgB,4CAA4C,CAC1D,EAAE,EAAE,YAAY,GACf,2BAA2B,CAgB7B"}