@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,230 @@
1
+ // Node spawn adapter for the governed LSP process manager (Issue #1381, Epic #1491, ADR-0069 D2).
2
+ // This is the only module in the lsp/ subdir that touches `node:child_process` and the filesystem;
3
+ // the manager consumes the injected `LspSpawnFn` port so every lifecycle branch is testable against
4
+ // the in-memory fake without a real subprocess.
5
+ //
6
+ // SECURITY BOUNDARY (FIX 8): the deny-by-default preflight (`isCommandAllowed`, I5), the workspace-root
7
+ // containment check (I2), and the copy-only env allowlist (`buildSandboxEnv`) are the MANAGER's
8
+ // responsibility — it calls `preflightSpawnEnv` and `resolveExecutableOutsideWorkspace` BEFORE ever
9
+ // invoking the injected `LspSpawnFn`. `defaultLspSpawnFn` therefore assumes it is handed an
10
+ // already-resolved, ABSOLUTE, allowlisted executable path plus the already-built sandbox env; it adds
11
+ // only an ephemeral empty HOME (so the server cannot read or write the operator's real home) and the
12
+ // process-group spawn/kill wiring. As defense-in-depth it asserts the executable is an absolute path,
13
+ // so a future DIRECT caller that bypassed the manager's resolution cannot spawn a relative/bare name.
14
+ // POSIX spawns detached and kills the whole process group; Windows kills the single child.
15
+ import { spawn } from "node:child_process";
16
+ import { accessSync, constants, mkdtempSync, realpathSync, rmSync } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { delimiter, isAbsolute, join, resolve as resolvePath } from "node:path";
19
+ import { isCommandAllowed, buildSandboxEnv } from "@oscharko-dev/keiko-tools";
20
+ import { isWithinWorkspace } from "@oscharko-dev/keiko-workspace";
21
+ // Typed failure carrying only a content-free `LspProcessErrorCode` — never a path, server output, or
22
+ // stack-derived message beyond the code itself (ADR-0069 D6).
23
+ export class LspProcessError extends Error {
24
+ code;
25
+ constructor(code) {
26
+ super(code);
27
+ this.name = "LspProcessError";
28
+ this.code = code;
29
+ }
30
+ }
31
+ function pathEntries(processEnv) {
32
+ const pathValue = processEnv.PATH ?? "";
33
+ return pathValue.length === 0 ? [] : pathValue.split(delimiter).filter(Boolean);
34
+ }
35
+ function executableExtensions(processEnv) {
36
+ if (process.platform !== "win32") {
37
+ return [""];
38
+ }
39
+ return (processEnv.PATHEXT ?? ".EXE;.CMD;.BAT;.COM")
40
+ .split(";")
41
+ .filter((value) => value.length > 0);
42
+ }
43
+ function probeCandidate(directory, name, ext) {
44
+ const candidate = resolvePath(resolvePath(directory), name + ext);
45
+ try {
46
+ accessSync(candidate, constants.X_OK);
47
+ return { path: candidate, real: realpathSync(candidate) };
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ }
53
+ function realWorkspaceRoot(root) {
54
+ try {
55
+ return realpathSync(root);
56
+ }
57
+ catch {
58
+ return root;
59
+ }
60
+ }
61
+ // Mirrors `exec.ts assertExecutableOutsideWorkspace`: a candidate is rejected if EITHER its lexical
62
+ // path OR its symlink-resolved real path lies inside the workspace, so a symlink planted in the
63
+ // workspace cannot point at a workspace-external binary and bypass the check (ADR-0069 I2).
64
+ function resolvesInsideWorkspace(candidate, lexicalRoot, realRoot) {
65
+ return (isWithinWorkspace(lexicalRoot, candidate.path) || isWithinWorkspace(realRoot, candidate.real));
66
+ }
67
+ // Resolves a bare executable name on the operator's PATH to an absolute real path that lies OUTSIDE
68
+ // the workspace root (ADR-0069 I2/I5). Throws `EXECUTABLE_NOT_FOUND` when the name has a separator,
69
+ // is absent from PATH, or resolves inside the workspace.
70
+ export function resolveExecutableOutsideWorkspace(name, workspace, processEnv) {
71
+ if (name.length === 0 || name.includes("/") || name.includes("\\") || name.includes(" ")) {
72
+ throw new LspProcessError("EXECUTABLE_NOT_FOUND");
73
+ }
74
+ const lexicalRoot = workspace.root;
75
+ const realRoot = realWorkspaceRoot(lexicalRoot);
76
+ for (const directory of pathEntries(processEnv)) {
77
+ for (const ext of executableExtensions(processEnv)) {
78
+ const candidate = probeCandidate(directory, name, ext);
79
+ if (candidate === undefined) {
80
+ continue;
81
+ }
82
+ if (resolvesInsideWorkspace(candidate, lexicalRoot, realRoot)) {
83
+ throw new LspProcessError("EXECUTABLE_NOT_FOUND");
84
+ }
85
+ return candidate.real;
86
+ }
87
+ }
88
+ throw new LspProcessError("EXECUTABLE_NOT_FOUND");
89
+ }
90
+ // Creates an empty per-process HOME directory. The server child receives this as HOME/USERPROFILE so
91
+ // it cannot read or write the operator's real home; `cleanup` removes it best-effort on dispose.
92
+ export function createEphemeralHome() {
93
+ const path = mkdtempSync(join(tmpdir(), "keiko-lsp-home-"));
94
+ return {
95
+ path,
96
+ cleanup: () => {
97
+ try {
98
+ rmSync(path, { recursive: true, force: true });
99
+ }
100
+ catch {
101
+ // Best-effort: a failed cleanup of a temp directory must never block dispose.
102
+ }
103
+ },
104
+ };
105
+ }
106
+ // POSIX group kill: signals the whole process group (`-pid`) so the LSP server's grandchildren are
107
+ // included (ADR-0069 D2). Windows has no process groups here, so the single child is killed. A failed
108
+ // kill (process already gone) is swallowed; the escalation timer still owns the SIGKILL fallback.
109
+ function nodeGroupKill(pid, child, signal) {
110
+ if (process.platform !== "win32") {
111
+ try {
112
+ process.kill(-pid, signal);
113
+ return;
114
+ }
115
+ catch {
116
+ // Group may already be gone; fall through to a direct child kill.
117
+ }
118
+ }
119
+ try {
120
+ child.kill(signal);
121
+ }
122
+ catch {
123
+ // The child may have already exited.
124
+ }
125
+ }
126
+ function safeKill(child, signal) {
127
+ try {
128
+ child.kill(signal);
129
+ }
130
+ catch {
131
+ // The child may have already exited; the escalation timer owns the SIGKILL fallback.
132
+ }
133
+ }
134
+ // Escalates termination: SIGTERM, then SIGKILL after `gracePeriodMs` measured on the injected clock.
135
+ // Resolves immediately if the child has already exited, or as soon as it exits during the grace
136
+ // window (via the optional `whenExited` registration), or when the window elapses and SIGKILL is sent.
137
+ // Resolving early on a mid-window exit is what lets a prompt shutdown avoid waiting the full grace
138
+ // (ADR-0069 D4 / FIX 2). The child's own `kill` carries the POSIX-group-vs-Windows distinction (see
139
+ // `defaultLspSpawnFn`), so this stays platform-agnostic and test-safe.
140
+ export function escalateKill(child, gracePeriodMs, exited, scheduler = defaultKillScheduler, whenExited) {
141
+ safeKill(child, "SIGTERM");
142
+ if (exited()) {
143
+ return Promise.resolve();
144
+ }
145
+ return new Promise((resolve) => {
146
+ let settled = false;
147
+ const finish = (sendKill) => {
148
+ if (settled)
149
+ return;
150
+ settled = true;
151
+ if (sendKill && !exited()) {
152
+ safeKill(child, "SIGKILL");
153
+ }
154
+ resolve();
155
+ };
156
+ whenExited?.(() => {
157
+ finish(false);
158
+ });
159
+ scheduler.setTimer(() => {
160
+ finish(true);
161
+ }, gracePeriodMs);
162
+ });
163
+ }
164
+ const defaultKillScheduler = {
165
+ setTimer: (callback, delayMs) => setTimeout(callback, delayMs).unref(),
166
+ };
167
+ function wrapChild(child) {
168
+ const stdin = child.stdin;
169
+ const stdout = child.stdout;
170
+ const stderr = child.stderr;
171
+ if (stdin === null || stdout === null || stderr === null) {
172
+ throw new LspProcessError("SPAWN_FAILED");
173
+ }
174
+ return {
175
+ stdin: { write: (chunk) => void stdin.write(chunk) },
176
+ stdout,
177
+ stderr,
178
+ pid: child.pid,
179
+ kill: (signal) => {
180
+ const pid = child.pid;
181
+ if (pid === undefined) {
182
+ safeKill(child, signal);
183
+ return;
184
+ }
185
+ nodeGroupKill(pid, child, signal);
186
+ },
187
+ onExit: (callback) => {
188
+ child.on("exit", (code) => {
189
+ callback(code);
190
+ });
191
+ },
192
+ onError: (callback) => {
193
+ child.on("error", callback);
194
+ },
195
+ };
196
+ }
197
+ // Default spawn adapter (ADR-0069 D2). The manager has already run the deny-by-default preflight (I5),
198
+ // resolved the executable to an absolute workspace-external path (I2), and built the copy-only env;
199
+ // this adapter substitutes an ephemeral HOME/USERPROFILE and spawns detached on POSIX so the manager
200
+ // can group-kill grandchildren. As defense-in-depth (FIX 8) it rejects a non-absolute executable, so a
201
+ // future caller that bypassed `resolveExecutableOutsideWorkspace` cannot spawn a bare/relative name.
202
+ export const defaultLspSpawnFn = (executable, args, env, cwd) => {
203
+ if (!isAbsolute(executable)) {
204
+ throw new LspProcessError("EXECUTABLE_NOT_FOUND");
205
+ }
206
+ const home = createEphemeralHome();
207
+ const childEnv = { ...env, HOME: home.path, USERPROFILE: home.path };
208
+ const child = spawn(executable, [...args], {
209
+ cwd,
210
+ env: childEnv,
211
+ stdio: ["pipe", "pipe", "pipe"],
212
+ detached: process.platform !== "win32",
213
+ windowsHide: true,
214
+ });
215
+ const wrapped = wrapChild(child);
216
+ wrapped.onExit(() => {
217
+ home.cleanup();
218
+ });
219
+ return wrapped;
220
+ };
221
+ // Convenience preflight used by the manager before it calls the injected spawn fn: proves the command
222
+ // is allowlisted (I5) and builds the copy-only child env (no parent secrets leak). Returns the env on
223
+ // success; throws `EXECUTABLE_NOT_FOUND` on a denied command.
224
+ export function preflightSpawnEnv(rules, executable, args, processEnv, envAllowlist) {
225
+ const decision = isCommandAllowed(rules, executable, args);
226
+ if (!decision.allowed) {
227
+ throw new LspProcessError("EXECUTABLE_NOT_FOUND");
228
+ }
229
+ return buildSandboxEnv(processEnv, envAllowlist);
230
+ }
@@ -0,0 +1,24 @@
1
+ import type { LanguageServiceOperation, LspLifecycleEvent, LspProcessConfig, LspProcessStatus } from "@oscharko-dev/keiko-contracts";
2
+ import type { CommandRule } from "@oscharko-dev/keiko-tools";
3
+ import type { WorkspaceInfo } from "@oscharko-dev/keiko-workspace";
4
+ import type { LspSpawnFn } from "./lspNodeAdapter.js";
5
+ import type { LspManagerLanguageProvider } from "./lspLanguageProvider.js";
6
+ export interface LspProcessManagerDeps {
7
+ readonly config: LspProcessConfig;
8
+ readonly workspace: WorkspaceInfo;
9
+ readonly processEnv: NodeJS.ProcessEnv;
10
+ readonly commandRules: readonly CommandRule[];
11
+ readonly spawn?: LspSpawnFn | undefined;
12
+ readonly now?: (() => number) | undefined;
13
+ readonly onLifecycleEvent?: ((event: LspLifecycleEvent) => void) | undefined;
14
+ }
15
+ export interface LspProcessManager {
16
+ getLspProcessStatus(): LspProcessStatus;
17
+ asLanguageProvider(languages: readonly string[], operations: readonly LanguageServiceOperation[]): LspManagerLanguageProvider;
18
+ sendRequest<T>(method: string, params: unknown, signal: AbortSignal): Promise<T>;
19
+ sendNotification(method: string, params: unknown): void;
20
+ onNotification(handler: (method: string, params: unknown) => void): void;
21
+ dispose(): Promise<void>;
22
+ }
23
+ export declare function createLspProcessManager(deps: LspProcessManagerDeps): LspProcessManager;
24
+ //# sourceMappingURL=lspProcessManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lspProcessManager.d.ts","sourceRoot":"","sources":["../../../src/editor/lsp/lspProcessManager.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,wBAAwB,EACxB,iBAAiB,EAEjB,gBAAgB,EAEhB,gBAAgB,EACjB,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAWnE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAQtD,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AAE3E,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;IAClC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;IACvC,QAAQ,CAAC,YAAY,EAAE,SAAS,WAAW,EAAE,CAAC;IAC9C,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CAC9E;AAED,MAAM,WAAW,iBAAiB;IAChC,mBAAmB,IAAI,gBAAgB,CAAC;IACxC,kBAAkB,CAChB,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,UAAU,EAAE,SAAS,wBAAwB,EAAE,GAC9C,0BAA0B,CAAC;IAC9B,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACjF,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IACxD,cAAc,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI,CAAC;IACzE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAoHD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,qBAAqB,GAAG,iBAAiB,CAiBtF"}
@@ -0,0 +1,255 @@
1
+ // Governed LSP process manager (Issue #1381, Epic #1491, ADR-0069 D2/D4/D5). A long-lived supervisor
2
+ // for one external language-server child over stdio JSON-RPC: it runs the deny-by-default preflight,
3
+ // spawns through the injected `LspSpawnFn`, drives the `initialize` handshake under a timeout, serves
4
+ // requests under a per-request deadline plus AbortSignal cancellation, restarts on crash within a
5
+ // rolling throttle window, and shuts the process down gracefully on dispose. Every state transition
6
+ // emits a content-free `LspLifecycleEvent` (ADR-0069 I4/D6); no source text, paths, or method names
7
+ // ever cross the audit boundary.
8
+ import { createLspTransport } from "./lspTransport.js";
9
+ import { LspFrameRejectError } from "./lspFrameCodec.js";
10
+ import { LspProcessError, defaultLspSpawnFn, escalateKill, preflightSpawnEnv, resolveExecutableOutsideWorkspace, } from "./lspNodeAdapter.js";
11
+ import { createLspRestartThrottle } from "./lspRestartThrottle.js";
12
+ import { LspRpcCancelledError, LspRpcDisposedError, LspRpcTimeoutError, } from "./lspJsonRpcClient.js";
13
+ import { buildLanguageProvider } from "./lspLanguageProvider.js";
14
+ function supervisorOnCrash(ctx, crashGeneration) {
15
+ // A late `exit`/`error` from a child already superseded by a restart carries a stale generation;
16
+ // discard it so it cannot debit the throttle or re-enter CRASHED (FIX 4). The same-generation
17
+ // error+exit pair coalesces because the first call advances `exited`/`childGeneration` so the second
18
+ // is caught here (stale generation after a restart) or by the `exited` guard below (no restart).
19
+ if (crashGeneration !== ctx.state.childGeneration || ctx.state.exited) {
20
+ return;
21
+ }
22
+ // Marking the child exited BEFORE the disposed early-return lets `escalateKill`'s stop-predicate
23
+ // (`() => state.exited`) observe a prompt exit during dispose and resolve without waiting the full
24
+ // grace window (FIX 2).
25
+ ctx.state.exited = true;
26
+ if (ctx.state.disposed) {
27
+ return;
28
+ }
29
+ ctx.transition("CRASHED", "CRASHED");
30
+ if (ctx.throttle.recordCrashAndMayRestart(ctx.now())) {
31
+ ctx.state.restartCount = ctx.throttle.restartCount();
32
+ supervisorStart(ctx);
33
+ }
34
+ else {
35
+ ctx.transition("RESTART_THROTTLED", "RESTART_THROTTLED");
36
+ }
37
+ }
38
+ function supervisorStart(ctx) {
39
+ if (ctx.state.disposed) {
40
+ return;
41
+ }
42
+ const env = preflightOrFail(ctx.deps, ctx.transition);
43
+ if (env === undefined) {
44
+ return;
45
+ }
46
+ const executable = resolveOrFail(ctx.deps, ctx.transition);
47
+ if (executable === undefined) {
48
+ return;
49
+ }
50
+ spawnAndInitialize(ctx.state, ctx.deps, ctx.spawn, ctx.now, ctx.transition, executable, env, (generation) => {
51
+ supervisorOnCrash(ctx, generation);
52
+ });
53
+ }
54
+ function createManagerRuntime(deps) {
55
+ const now = deps.now ?? Date.now;
56
+ const state = {
57
+ status: "STARTING",
58
+ transport: undefined,
59
+ child: undefined,
60
+ exited: false,
61
+ restartCount: 0,
62
+ disposed: false,
63
+ childGeneration: 0,
64
+ };
65
+ const transition = (status, code) => {
66
+ state.status = status;
67
+ deps.onLifecycleEvent?.(buildLifecycleEvent(deps.config.managerId, state, now(), code));
68
+ };
69
+ const ctx = {
70
+ deps,
71
+ state,
72
+ now,
73
+ spawn: deps.spawn ?? defaultLspSpawnFn,
74
+ throttle: createLspRestartThrottle(deps.config.restartWindowMs, deps.config.maxRestartsInWindow),
75
+ transition,
76
+ };
77
+ transition("STARTING");
78
+ supervisorStart(ctx);
79
+ return { state, now, transition };
80
+ }
81
+ export function createLspProcessManager(deps) {
82
+ const { state, now, transition } = createManagerRuntime(deps);
83
+ return {
84
+ getLspProcessStatus: () => state.status,
85
+ asLanguageProvider: (languages, operations) => buildLanguageProvider(deps.config.managerId, languages, operations, () => state.status),
86
+ sendRequest: (method, params, signal) => sendRequest(state, deps, now, method, params, signal),
87
+ sendNotification: (method, params) => {
88
+ if (state.status === "READY")
89
+ state.transport?.client.notify(method, params);
90
+ },
91
+ onNotification: (handler) => {
92
+ state.transport?.client.onNotification(handler);
93
+ },
94
+ dispose: () => disposeManager(state, deps, now, transition),
95
+ };
96
+ }
97
+ function buildLifecycleEvent(managerId, state, timestampMs, errorCode) {
98
+ return {
99
+ schemaVersion: "1",
100
+ managerId,
101
+ status: state.status,
102
+ ...(errorCode !== undefined ? { errorCode } : {}),
103
+ timestampMs,
104
+ pendingRequestCount: state.transport?.client.pendingCount() ?? 0,
105
+ restartCount: state.restartCount,
106
+ stderrBytesSeen: state.transport?.stderrBytesSeen() ?? 0,
107
+ };
108
+ }
109
+ function spawnAndInitialize(state, deps, spawn, now, transition, executable, env, onCrash) {
110
+ // On a restart, reject any request still pending against the dead transport IMMEDIATELY instead of
111
+ // letting it linger until requestTimeoutMs: dispose rejects pending with LspRpcDisposedError, which
112
+ // maps to DISPOSED (FIX 3). The new generation guards stale callbacks from the superseded child.
113
+ state.transport?.dispose();
114
+ state.childGeneration += 1;
115
+ const generation = state.childGeneration;
116
+ state.exited = false;
117
+ const child = trySpawn(spawn, executable, env, deps, transition);
118
+ if (child === undefined) {
119
+ return;
120
+ }
121
+ state.child = child;
122
+ state.transport = createLspTransport(child, deps.config.maxFrameBytes, {
123
+ onReaderError: () => {
124
+ onCrash(generation);
125
+ },
126
+ });
127
+ child.onExit(() => {
128
+ onCrash(generation);
129
+ });
130
+ child.onError(() => {
131
+ onCrash(generation);
132
+ });
133
+ transition("INITIALIZING");
134
+ void runInitialize(state, deps, now, transition);
135
+ }
136
+ function preflightOrFail(deps, transition) {
137
+ try {
138
+ return preflightSpawnEnv(deps.commandRules, deps.config.executableName, deps.config.executableArgs ?? [], deps.processEnv, deps.config.envAllowlist);
139
+ }
140
+ catch {
141
+ transition("EXECUTABLE_NOT_FOUND", "EXECUTABLE_NOT_FOUND");
142
+ return undefined;
143
+ }
144
+ }
145
+ function resolveOrFail(deps, transition) {
146
+ try {
147
+ return resolveExecutableOutsideWorkspace(deps.config.executableName, deps.workspace, deps.processEnv);
148
+ }
149
+ catch {
150
+ transition("EXECUTABLE_NOT_FOUND", "EXECUTABLE_NOT_FOUND");
151
+ return undefined;
152
+ }
153
+ }
154
+ function trySpawn(spawn, executable, env, deps, transition) {
155
+ try {
156
+ return spawn(executable, deps.config.executableArgs ?? [], env, deps.workspace.root);
157
+ }
158
+ catch {
159
+ transition("SPAWN_FAILED", "SPAWN_FAILED");
160
+ return undefined;
161
+ }
162
+ }
163
+ async function runInitialize(state, deps, now, transition) {
164
+ const client = state.transport?.client;
165
+ if (client === undefined) {
166
+ return;
167
+ }
168
+ const networkPolicy = deps.config.networkPolicy ?? "inherit";
169
+ try {
170
+ await client.request("initialize", { capabilities: {}, networkPolicy }, { deadlineMs: deps.config.initializeTimeoutMs, now });
171
+ if (state.status === "INITIALIZING") {
172
+ transition("READY");
173
+ }
174
+ }
175
+ catch (error) {
176
+ if (state.status === "INITIALIZING") {
177
+ transition("INITIALIZE_TIMEOUT", classifyInitFailure(error));
178
+ }
179
+ }
180
+ }
181
+ function classifyInitFailure(error) {
182
+ return error instanceof LspRpcTimeoutError ? "INITIALIZE_TIMEOUT" : "INITIALIZE_FAILED";
183
+ }
184
+ async function sendRequest(state, deps, now, method, params, signal) {
185
+ if (state.disposed || state.status === "DISPOSED") {
186
+ throw new LspProcessError("DISPOSED");
187
+ }
188
+ const client = state.transport?.client;
189
+ if (client === undefined || state.status !== "READY") {
190
+ throw new LspProcessError("CRASHED");
191
+ }
192
+ try {
193
+ return await client.request(method, params, {
194
+ signal,
195
+ deadlineMs: deps.config.requestTimeoutMs,
196
+ now,
197
+ });
198
+ }
199
+ catch (error) {
200
+ throw mapRequestError(error);
201
+ }
202
+ }
203
+ function mapRequestError(error) {
204
+ if (error instanceof LspProcessError) {
205
+ return error;
206
+ }
207
+ if (error instanceof LspRpcTimeoutError) {
208
+ return new LspProcessError("REQUEST_TIMED_OUT");
209
+ }
210
+ if (error instanceof LspRpcCancelledError) {
211
+ return new LspProcessError("CANCELLED");
212
+ }
213
+ if (error instanceof LspRpcDisposedError) {
214
+ return new LspProcessError("DISPOSED");
215
+ }
216
+ // RESPONSE_TOO_LARGE is reserved for an ACTUAL frame-size rejection, never a generic RPC failure
217
+ // (FIX 9). A frame reject for an oversized body keeps that code; any other frame reject (malformed
218
+ // header) poisons the channel and surfaces as CRASHED. A plain server-side RPC error (the
219
+ // `new Error("LSP error")` raised in `settleResponse`) is a transport/protocol fault for that
220
+ // request, mapped to CRASHED — honest and content-free, never echoing the server's message text.
221
+ if (error instanceof LspFrameRejectError) {
222
+ return new LspProcessError(error.reason === "RESPONSE_TOO_LARGE" ? "RESPONSE_TOO_LARGE" : "CRASHED");
223
+ }
224
+ return new LspProcessError("CRASHED");
225
+ }
226
+ async function disposeManager(state, deps, now, transition) {
227
+ if (state.disposed) {
228
+ return;
229
+ }
230
+ state.disposed = true;
231
+ transition("SHUTDOWN");
232
+ const child = state.child;
233
+ const transport = state.transport;
234
+ await requestGracefulShutdown(transport, deps, now);
235
+ transport?.dispose();
236
+ if (child !== undefined) {
237
+ await escalateKill(child, deps.config.shutdownTimeoutMs, () => state.exited, undefined, (onExit) => {
238
+ child.onExit(onExit);
239
+ });
240
+ }
241
+ transition("DISPOSED", "DISPOSED");
242
+ }
243
+ async function requestGracefulShutdown(transport, deps, now) {
244
+ const client = transport?.client;
245
+ if (client === undefined) {
246
+ return;
247
+ }
248
+ try {
249
+ await client.request("shutdown", null, { deadlineMs: deps.config.shutdownTimeoutMs, now });
250
+ }
251
+ catch {
252
+ // A server that never answers shutdown is forced down by escalateKill (ADR-0069 D4).
253
+ }
254
+ client.notify("exit", null);
255
+ }
@@ -0,0 +1,6 @@
1
+ export interface LspRestartThrottle {
2
+ recordCrashAndMayRestart(nowMs: number): boolean;
3
+ restartCount(): number;
4
+ }
5
+ export declare function createLspRestartThrottle(windowMs: number, maxInWindow: number): LspRestartThrottle;
6
+ //# sourceMappingURL=lspRestartThrottle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lspRestartThrottle.d.ts","sourceRoot":"","sources":["../../../src/editor/lsp/lspRestartThrottle.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,kBAAkB;IAEjC,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IACjD,YAAY,IAAI,MAAM,CAAC;CACxB;AAED,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,GAClB,kBAAkB,CAmBpB"}
@@ -0,0 +1,24 @@
1
+ // Pure rolling-window restart throttle for the LSP process manager (Issue #1381, ADR-0069 D4). A
2
+ // crash records a timestamp; the manager may restart while the number of crashes within the trailing
3
+ // `windowMs` stays below `maxInWindow`. Once the window is saturated the manager transitions to
4
+ // RESTART_THROTTLED and stays down. The state is a plain timestamp ring kept entirely in memory and
5
+ // driven by an injected clock so every branch is deterministically testable.
6
+ export function createLspRestartThrottle(windowMs, maxInWindow) {
7
+ const crashes = [];
8
+ let totalRestarts = 0;
9
+ return {
10
+ recordCrashAndMayRestart: (nowMs) => {
11
+ crashes.push(nowMs);
12
+ const cutoff = nowMs - windowMs;
13
+ const withinWindow = crashes.filter((timestamp) => timestamp > cutoff);
14
+ crashes.length = 0;
15
+ crashes.push(...withinWindow);
16
+ if (withinWindow.length > maxInWindow) {
17
+ return false;
18
+ }
19
+ totalRestarts += 1;
20
+ return true;
21
+ },
22
+ restartCount: () => totalRestarts,
23
+ };
24
+ }
@@ -0,0 +1,8 @@
1
+ import type { EnvSource } from "@oscharko-dev/keiko-model-gateway";
2
+ import { type RouteResult } from "../../routes.js";
3
+ export interface LspStatusRouteDeps {
4
+ readonly env: EnvSource;
5
+ }
6
+ export declare function isLspStatusTrusted(env: EnvSource): boolean;
7
+ export declare function handleEditorLspStatus(_ctx: unknown, deps: LspStatusRouteDeps): RouteResult;
8
+ //# sourceMappingURL=lspStatusRoute.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lspStatusRoute.d.ts","sourceRoot":"","sources":["../../../src/editor/lsp/lspStatusRoute.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AACnE,OAAO,EAAa,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAG9D,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC;CACzB;AAID,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAG1D;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,kBAAkB,GAAG,WAAW,CAK1F"}
@@ -0,0 +1,22 @@
1
+ // Issue #1381, ADR-0069 D5/D6 — read-only status route for the governed LSP process manager. It serves
2
+ // the bounded, content-free lifecycle ledger so an operator can inspect recent LSP process activity
3
+ // (spawn, initialize, crash, restart, dispose) without any source text, paths, or method names ever
4
+ // crossing the boundary. The route is env-gated default-OFF (`KEIKO_EDITOR_LSP_STATUS`): until an
5
+ // operator explicitly enables it, the endpoint reports 404 NOT_FOUND so it is not even discoverable.
6
+ //
7
+ // GET only, no CSRF (a read route mutates nothing). No CSP/connect-src change is needed: the response
8
+ // is same-origin BFF JSON consumed over the existing fetch path, identical to /api/editor/agent/audit.
9
+ import { errorBody } from "../../routes.js";
10
+ import { listLspLifecycleEvents } from "./lspLifecycleLedger.js";
11
+ // Default-OFF: the status surface is only exposed when the operator opts in. Any value other than the
12
+ // explicit truthy tokens leaves it disabled (fail-closed), so a typo cannot accidentally expose it.
13
+ export function isLspStatusTrusted(env) {
14
+ const raw = env.KEIKO_EDITOR_LSP_STATUS;
15
+ return raw === "1" || raw === "true";
16
+ }
17
+ export function handleEditorLspStatus(_ctx, deps) {
18
+ if (!isLspStatusTrusted(deps.env)) {
19
+ return { status: 404, body: errorBody("NOT_FOUND", "The requested resource was not found.") };
20
+ }
21
+ return { status: 200, body: { events: listLspLifecycleEvents() } };
22
+ }
@@ -0,0 +1,19 @@
1
+ import type { LspByteSink, LspByteSource } from "./lspFrameCodec.js";
2
+ import type { LspJsonRpcClient, LspRpcScheduler } from "./lspJsonRpcClient.js";
3
+ export interface LspSpawnHandle {
4
+ readonly stdin: LspByteSink;
5
+ readonly stdout: LspByteSource;
6
+ readonly stderr: LspByteSource;
7
+ readonly pid: number | undefined;
8
+ }
9
+ export interface LspTransport {
10
+ readonly client: LspJsonRpcClient;
11
+ stderrBytesSeen(): number;
12
+ dispose(): void;
13
+ }
14
+ export interface LspTransportDeps {
15
+ readonly scheduler?: LspRpcScheduler | undefined;
16
+ readonly onReaderError?: ((error: unknown) => void) | undefined;
17
+ }
18
+ export declare function createLspTransport(handle: LspSpawnHandle, maxFrameBytes: number, deps?: LspTransportDeps): LspTransport;
19
+ //# sourceMappingURL=lspTransport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lspTransport.d.ts","sourceRoot":"","sources":["../../../src/editor/lsp/lspTransport.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAY,MAAM,oBAAoB,CAAC;AAE/E,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAK/E,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,eAAe,IAAI,MAAM,CAAC;IAC1B,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC;IAGjD,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CACjE;AAED,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,cAAc,EACtB,aAAa,EAAE,MAAM,EACrB,IAAI,GAAE,gBAAqB,GAC1B,YAAY,CA0Bd"}
@@ -0,0 +1,55 @@
1
+ // Transport wiring (Issue #1381, Epic #1491, ADR-0069 D3). Binds a spawned LSP child's stdio to the
2
+ // pure frame codec and the JSON-RPC client: stdout frames feed the client's request/response
3
+ // correlation, the client's outbound frames are written to stdin, and stderr is drained for a byte
4
+ // COUNT only — never stored, logged, or surfaced (ADR-0069 I3/I4/D6 content-free invariant). Only the
5
+ // drained-byte count is exposed via `stderrBytesSeen()` for the lifecycle event's `stderrBytesSeen`.
6
+ import { createLspFrameReader, writeLspFrame } from "./lspFrameCodec.js";
7
+ import { createLspJsonRpcClient } from "./lspJsonRpcClient.js";
8
+ export function createLspTransport(handle, maxFrameBytes, deps = {}) {
9
+ let stderrBytes = 0;
10
+ let disposed = false;
11
+ const reader = createLspFrameReader(handle.stdout, maxFrameBytes);
12
+ const guardedSource = guardReader(reader, deps.onReaderError);
13
+ const client = createLspJsonRpcClient({
14
+ source: guardedSource,
15
+ sendFrame: (body) => { writeLspFrame(handle.stdin, body); },
16
+ ...(deps.scheduler !== undefined ? { scheduler: deps.scheduler } : {}),
17
+ });
18
+ void drainStderr(handle.stderr, (count) => {
19
+ stderrBytes += count;
20
+ });
21
+ return {
22
+ client,
23
+ stderrBytesSeen: () => stderrBytes,
24
+ dispose: () => {
25
+ if (disposed)
26
+ return;
27
+ disposed = true;
28
+ client.dispose();
29
+ },
30
+ };
31
+ }
32
+ // Wraps the frame reader so a frame-level reject (oversized/malformed) is surfaced to the manager via
33
+ // `onReaderError` and then ends the stream cleanly, rather than propagating as an unhandled rejection
34
+ // in the client's detached consume loop. The reader stops after the first reject (the connection is
35
+ // considered poisoned, ADR-0069 I3).
36
+ async function* guardReader(reader, onReaderError) {
37
+ try {
38
+ yield* reader;
39
+ }
40
+ catch (error) {
41
+ onReaderError?.(error);
42
+ }
43
+ }
44
+ // Counts stderr bytes without storing them. The bytes are read and immediately discarded; only the
45
+ // running total survives (ADR-0069 D6: raw stderr never crosses an audit or status boundary).
46
+ async function drainStderr(stderr, onCount) {
47
+ try {
48
+ for await (const chunk of stderr) {
49
+ onCount(chunk.length);
50
+ }
51
+ }
52
+ catch {
53
+ // A stderr read error is non-fatal to the protocol channel; the count simply stops advancing.
54
+ }
55
+ }