@descryy/mcp 0.11.4 → 0.11.5

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 (441) hide show
  1. package/package.json +7 -7
  2. package/dist/action-handshake.d.ts +0 -38
  3. package/dist/action-handshake.d.ts.map +0 -1
  4. package/dist/action-handshake.js +0 -127
  5. package/dist/action-handshake.js.map +0 -1
  6. package/dist/bin/descry-mcp.d.ts +0 -7
  7. package/dist/bin/descry-mcp.d.ts.map +0 -1
  8. package/dist/bin/descry-mcp.js +0 -72
  9. package/dist/bin/descry-mcp.js.map +0 -1
  10. package/dist/browser/auth.d.ts +0 -84
  11. package/dist/browser/auth.d.ts.map +0 -1
  12. package/dist/browser/auth.js +0 -233
  13. package/dist/browser/auth.js.map +0 -1
  14. package/dist/browser/capability-probe.d.ts +0 -109
  15. package/dist/browser/capability-probe.d.ts.map +0 -1
  16. package/dist/browser/capability-probe.js +0 -201
  17. package/dist/browser/capability-probe.js.map +0 -1
  18. package/dist/browser/driver.d.ts +0 -591
  19. package/dist/browser/driver.d.ts.map +0 -1
  20. package/dist/browser/driver.js +0 -116
  21. package/dist/browser/driver.js.map +0 -1
  22. package/dist/browser/evidence.d.ts +0 -28
  23. package/dist/browser/evidence.d.ts.map +0 -1
  24. package/dist/browser/evidence.js +0 -82
  25. package/dist/browser/evidence.js.map +0 -1
  26. package/dist/browser/fake-driver.d.ts +0 -116
  27. package/dist/browser/fake-driver.d.ts.map +0 -1
  28. package/dist/browser/fake-driver.js +0 -416
  29. package/dist/browser/fake-driver.js.map +0 -1
  30. package/dist/browser/fault-attribution.d.ts +0 -178
  31. package/dist/browser/fault-attribution.d.ts.map +0 -1
  32. package/dist/browser/fault-attribution.js +0 -266
  33. package/dist/browser/fault-attribution.js.map +0 -1
  34. package/dist/browser/graph-write.d.ts +0 -45
  35. package/dist/browser/graph-write.d.ts.map +0 -1
  36. package/dist/browser/graph-write.js +0 -103
  37. package/dist/browser/graph-write.js.map +0 -1
  38. package/dist/browser/identity-graph-write.d.ts +0 -55
  39. package/dist/browser/identity-graph-write.d.ts.map +0 -1
  40. package/dist/browser/identity-graph-write.js +0 -45
  41. package/dist/browser/identity-graph-write.js.map +0 -1
  42. package/dist/browser/page-probe.d.ts +0 -81
  43. package/dist/browser/page-probe.d.ts.map +0 -1
  44. package/dist/browser/page-probe.js +0 -202
  45. package/dist/browser/page-probe.js.map +0 -1
  46. package/dist/browser/playwright-driver.d.ts +0 -114
  47. package/dist/browser/playwright-driver.d.ts.map +0 -1
  48. package/dist/browser/playwright-driver.js +0 -1518
  49. package/dist/browser/playwright-driver.js.map +0 -1
  50. package/dist/browser/provider.d.ts +0 -26
  51. package/dist/browser/provider.d.ts.map +0 -1
  52. package/dist/browser/provider.js +0 -14
  53. package/dist/browser/provider.js.map +0 -1
  54. package/dist/browser/reach-recording-session.d.ts +0 -44
  55. package/dist/browser/reach-recording-session.d.ts.map +0 -1
  56. package/dist/browser/reach-recording-session.js +0 -151
  57. package/dist/browser/reach-recording-session.js.map +0 -1
  58. package/dist/browser/reachability.d.ts +0 -168
  59. package/dist/browser/reachability.d.ts.map +0 -1
  60. package/dist/browser/reachability.js +0 -294
  61. package/dist/browser/reachability.js.map +0 -1
  62. package/dist/browser/registry.d.ts +0 -126
  63. package/dist/browser/registry.d.ts.map +0 -1
  64. package/dist/browser/registry.js +0 -177
  65. package/dist/browser/registry.js.map +0 -1
  66. package/dist/browser/scenario-provenance.d.ts +0 -23
  67. package/dist/browser/scenario-provenance.d.ts.map +0 -1
  68. package/dist/browser/scenario-provenance.js +0 -73
  69. package/dist/browser/scenario-provenance.js.map +0 -1
  70. package/dist/browser/scenario-resolve.d.ts +0 -28
  71. package/dist/browser/scenario-resolve.d.ts.map +0 -1
  72. package/dist/browser/scenario-resolve.js +0 -96
  73. package/dist/browser/scenario-resolve.js.map +0 -1
  74. package/dist/browser/scenario-runner.d.ts +0 -72
  75. package/dist/browser/scenario-runner.d.ts.map +0 -1
  76. package/dist/browser/scenario-runner.js +0 -353
  77. package/dist/browser/scenario-runner.js.map +0 -1
  78. package/dist/browser/stack-parser.d.ts +0 -19
  79. package/dist/browser/stack-parser.d.ts.map +0 -1
  80. package/dist/browser/stack-parser.js +0 -85
  81. package/dist/browser/stack-parser.js.map +0 -1
  82. package/dist/browser/tool-support.d.ts +0 -71
  83. package/dist/browser/tool-support.d.ts.map +0 -1
  84. package/dist/browser/tool-support.js +0 -216
  85. package/dist/browser/tool-support.js.map +0 -1
  86. package/dist/browser/url-scheme.d.ts +0 -14
  87. package/dist/browser/url-scheme.d.ts.map +0 -1
  88. package/dist/browser/url-scheme.js +0 -38
  89. package/dist/browser/url-scheme.js.map +0 -1
  90. package/dist/browser/wait-target.d.ts +0 -37
  91. package/dist/browser/wait-target.d.ts.map +0 -1
  92. package/dist/browser/wait-target.js +0 -54
  93. package/dist/browser/wait-target.js.map +0 -1
  94. package/dist/cancellation.d.ts +0 -20
  95. package/dist/cancellation.d.ts.map +0 -1
  96. package/dist/cancellation.js +0 -41
  97. package/dist/cancellation.js.map +0 -1
  98. package/dist/capped.d.ts +0 -39
  99. package/dist/capped.d.ts.map +0 -1
  100. package/dist/capped.js +0 -43
  101. package/dist/capped.js.map +0 -1
  102. package/dist/credential-store.d.ts +0 -22
  103. package/dist/credential-store.d.ts.map +0 -1
  104. package/dist/credential-store.js +0 -33
  105. package/dist/credential-store.js.map +0 -1
  106. package/dist/disclosure-ledger.d.ts +0 -14
  107. package/dist/disclosure-ledger.d.ts.map +0 -1
  108. package/dist/disclosure-ledger.js +0 -18
  109. package/dist/disclosure-ledger.js.map +0 -1
  110. package/dist/evidence/observed-route-evidence.d.ts +0 -25
  111. package/dist/evidence/observed-route-evidence.d.ts.map +0 -1
  112. package/dist/evidence/observed-route-evidence.js +0 -76
  113. package/dist/evidence/observed-route-evidence.js.map +0 -1
  114. package/dist/heap.d.ts +0 -61
  115. package/dist/heap.d.ts.map +0 -1
  116. package/dist/heap.js +0 -82
  117. package/dist/heap.js.map +0 -1
  118. package/dist/heartbeat.d.ts +0 -46
  119. package/dist/heartbeat.d.ts.map +0 -1
  120. package/dist/heartbeat.js +0 -66
  121. package/dist/heartbeat.js.map +0 -1
  122. package/dist/index.d.ts +0 -32
  123. package/dist/index.d.ts.map +0 -1
  124. package/dist/index.js +0 -19
  125. package/dist/index.js.map +0 -1
  126. package/dist/module-trust.d.ts +0 -37
  127. package/dist/module-trust.d.ts.map +0 -1
  128. package/dist/module-trust.js +0 -77
  129. package/dist/module-trust.js.map +0 -1
  130. package/dist/path-confinement.d.ts +0 -31
  131. package/dist/path-confinement.d.ts.map +0 -1
  132. package/dist/path-confinement.js +0 -44
  133. package/dist/path-confinement.js.map +0 -1
  134. package/dist/planner/predict-then-propose.d.ts +0 -134
  135. package/dist/planner/predict-then-propose.d.ts.map +0 -1
  136. package/dist/planner/predict-then-propose.js +0 -138
  137. package/dist/planner/predict-then-propose.js.map +0 -1
  138. package/dist/planner/propose-journey.d.ts +0 -75
  139. package/dist/planner/propose-journey.d.ts.map +0 -1
  140. package/dist/planner/propose-journey.js +0 -96
  141. package/dist/planner/propose-journey.js.map +0 -1
  142. package/dist/protocol.d.ts +0 -57
  143. package/dist/protocol.d.ts.map +0 -1
  144. package/dist/protocol.js +0 -90
  145. package/dist/protocol.js.map +0 -1
  146. package/dist/registry.d.ts +0 -103
  147. package/dist/registry.d.ts.map +0 -1
  148. package/dist/registry.js +0 -327
  149. package/dist/registry.js.map +0 -1
  150. package/dist/render.d.ts +0 -623
  151. package/dist/render.d.ts.map +0 -1
  152. package/dist/render.js +0 -968
  153. package/dist/render.js.map +0 -1
  154. package/dist/runtime-registry.d.ts +0 -22
  155. package/dist/runtime-registry.d.ts.map +0 -1
  156. package/dist/runtime-registry.js +0 -142
  157. package/dist/runtime-registry.js.map +0 -1
  158. package/dist/sandbox-defaults.d.ts +0 -41
  159. package/dist/sandbox-defaults.d.ts.map +0 -1
  160. package/dist/sandbox-defaults.js +0 -14
  161. package/dist/sandbox-defaults.js.map +0 -1
  162. package/dist/scenarios/browser-run-plan-projection.d.ts +0 -161
  163. package/dist/scenarios/browser-run-plan-projection.d.ts.map +0 -1
  164. package/dist/scenarios/browser-run-plan-projection.js +0 -264
  165. package/dist/scenarios/browser-run-plan-projection.js.map +0 -1
  166. package/dist/scenarios/credential-ref.d.ts +0 -102
  167. package/dist/scenarios/credential-ref.d.ts.map +0 -1
  168. package/dist/scenarios/credential-ref.js +0 -148
  169. package/dist/scenarios/credential-ref.js.map +0 -1
  170. package/dist/scenarios/index.d.ts +0 -7
  171. package/dist/scenarios/index.d.ts.map +0 -1
  172. package/dist/scenarios/index.js +0 -7
  173. package/dist/scenarios/index.js.map +0 -1
  174. package/dist/scenarios/parse.d.ts +0 -18
  175. package/dist/scenarios/parse.d.ts.map +0 -1
  176. package/dist/scenarios/parse.js +0 -212
  177. package/dist/scenarios/parse.js.map +0 -1
  178. package/dist/scenarios/scenario.d.ts +0 -93
  179. package/dist/scenarios/scenario.d.ts.map +0 -1
  180. package/dist/scenarios/scenario.js +0 -27
  181. package/dist/scenarios/scenario.js.map +0 -1
  182. package/dist/scenarios/secret-ref.d.ts +0 -86
  183. package/dist/scenarios/secret-ref.d.ts.map +0 -1
  184. package/dist/scenarios/secret-ref.js +0 -125
  185. package/dist/scenarios/secret-ref.js.map +0 -1
  186. package/dist/scenarios/storage.d.ts +0 -21
  187. package/dist/scenarios/storage.d.ts.map +0 -1
  188. package/dist/scenarios/storage.js +0 -103
  189. package/dist/scenarios/storage.js.map +0 -1
  190. package/dist/server.d.ts +0 -61
  191. package/dist/server.d.ts.map +0 -1
  192. package/dist/server.js +0 -651
  193. package/dist/server.js.map +0 -1
  194. package/dist/session.d.ts +0 -356
  195. package/dist/session.d.ts.map +0 -1
  196. package/dist/session.js +0 -779
  197. package/dist/session.js.map +0 -1
  198. package/dist/tools/alias-kit.d.ts +0 -29
  199. package/dist/tools/alias-kit.d.ts.map +0 -1
  200. package/dist/tools/alias-kit.js +0 -21
  201. package/dist/tools/alias-kit.js.map +0 -1
  202. package/dist/tools/aliases.d.ts +0 -42
  203. package/dist/tools/aliases.d.ts.map +0 -1
  204. package/dist/tools/aliases.js +0 -52
  205. package/dist/tools/aliases.js.map +0 -1
  206. package/dist/tools/analyze-workspace.d.ts +0 -52
  207. package/dist/tools/analyze-workspace.d.ts.map +0 -1
  208. package/dist/tools/analyze-workspace.js +0 -207
  209. package/dist/tools/analyze-workspace.js.map +0 -1
  210. package/dist/tools/analyze.d.ts +0 -163
  211. package/dist/tools/analyze.d.ts.map +0 -1
  212. package/dist/tools/analyze.js +0 -724
  213. package/dist/tools/analyze.js.map +0 -1
  214. package/dist/tools/browser-click.d.ts +0 -18
  215. package/dist/tools/browser-click.d.ts.map +0 -1
  216. package/dist/tools/browser-click.js +0 -110
  217. package/dist/tools/browser-click.js.map +0 -1
  218. package/dist/tools/browser-close-session.d.ts +0 -39
  219. package/dist/tools/browser-close-session.d.ts.map +0 -1
  220. package/dist/tools/browser-close-session.js +0 -120
  221. package/dist/tools/browser-close-session.js.map +0 -1
  222. package/dist/tools/browser-fill.d.ts +0 -18
  223. package/dist/tools/browser-fill.d.ts.map +0 -1
  224. package/dist/tools/browser-fill.js +0 -117
  225. package/dist/tools/browser-fill.js.map +0 -1
  226. package/dist/tools/browser-navigate.d.ts +0 -15
  227. package/dist/tools/browser-navigate.d.ts.map +0 -1
  228. package/dist/tools/browser-navigate.js +0 -104
  229. package/dist/tools/browser-navigate.js.map +0 -1
  230. package/dist/tools/browser-read.d.ts +0 -33
  231. package/dist/tools/browser-read.d.ts.map +0 -1
  232. package/dist/tools/browser-read.js +0 -113
  233. package/dist/tools/browser-read.js.map +0 -1
  234. package/dist/tools/browser-run-scenario.d.ts +0 -28
  235. package/dist/tools/browser-run-scenario.d.ts.map +0 -1
  236. package/dist/tools/browser-run-scenario.js +0 -225
  237. package/dist/tools/browser-run-scenario.js.map +0 -1
  238. package/dist/tools/browser-save-scenario.d.ts +0 -19
  239. package/dist/tools/browser-save-scenario.d.ts.map +0 -1
  240. package/dist/tools/browser-save-scenario.js +0 -447
  241. package/dist/tools/browser-save-scenario.js.map +0 -1
  242. package/dist/tools/browser-select.d.ts +0 -20
  243. package/dist/tools/browser-select.d.ts.map +0 -1
  244. package/dist/tools/browser-select.js +0 -120
  245. package/dist/tools/browser-select.js.map +0 -1
  246. package/dist/tools/browser-snapshot.d.ts +0 -46
  247. package/dist/tools/browser-snapshot.d.ts.map +0 -1
  248. package/dist/tools/browser-snapshot.js +0 -127
  249. package/dist/tools/browser-snapshot.js.map +0 -1
  250. package/dist/tools/browser-start-session.d.ts +0 -24
  251. package/dist/tools/browser-start-session.d.ts.map +0 -1
  252. package/dist/tools/browser-start-session.js +0 -640
  253. package/dist/tools/browser-start-session.js.map +0 -1
  254. package/dist/tools/browser-submit.d.ts +0 -18
  255. package/dist/tools/browser-submit.d.ts.map +0 -1
  256. package/dist/tools/browser-submit.js +0 -106
  257. package/dist/tools/browser-submit.js.map +0 -1
  258. package/dist/tools/browser-type.d.ts +0 -18
  259. package/dist/tools/browser-type.d.ts.map +0 -1
  260. package/dist/tools/browser-type.js +0 -115
  261. package/dist/tools/browser-type.js.map +0 -1
  262. package/dist/tools/browser-wait-for.d.ts +0 -45
  263. package/dist/tools/browser-wait-for.d.ts.map +0 -1
  264. package/dist/tools/browser-wait-for.js +0 -150
  265. package/dist/tools/browser-wait-for.js.map +0 -1
  266. package/dist/tools/browser.d.ts +0 -69
  267. package/dist/tools/browser.d.ts.map +0 -1
  268. package/dist/tools/browser.js +0 -386
  269. package/dist/tools/browser.js.map +0 -1
  270. package/dist/tools/chain.d.ts +0 -41
  271. package/dist/tools/chain.d.ts.map +0 -1
  272. package/dist/tools/chain.js +0 -105
  273. package/dist/tools/chain.js.map +0 -1
  274. package/dist/tools/change-scope.d.ts +0 -25
  275. package/dist/tools/change-scope.d.ts.map +0 -1
  276. package/dist/tools/change-scope.js +0 -110
  277. package/dist/tools/change-scope.js.map +0 -1
  278. package/dist/tools/code-context.d.ts +0 -83
  279. package/dist/tools/code-context.d.ts.map +0 -1
  280. package/dist/tools/code-context.js +0 -444
  281. package/dist/tools/code-context.js.map +0 -1
  282. package/dist/tools/contracts.d.ts +0 -74
  283. package/dist/tools/contracts.d.ts.map +0 -1
  284. package/dist/tools/contracts.js +0 -196
  285. package/dist/tools/contracts.js.map +0 -1
  286. package/dist/tools/cross-pr.d.ts +0 -178
  287. package/dist/tools/cross-pr.d.ts.map +0 -1
  288. package/dist/tools/cross-pr.js +0 -370
  289. package/dist/tools/cross-pr.js.map +0 -1
  290. package/dist/tools/disclosure.d.ts +0 -75
  291. package/dist/tools/disclosure.d.ts.map +0 -1
  292. package/dist/tools/disclosure.js +0 -307
  293. package/dist/tools/disclosure.js.map +0 -1
  294. package/dist/tools/draw-conclusion.d.ts +0 -55
  295. package/dist/tools/draw-conclusion.d.ts.map +0 -1
  296. package/dist/tools/draw-conclusion.js +0 -259
  297. package/dist/tools/draw-conclusion.js.map +0 -1
  298. package/dist/tools/git-context.d.ts +0 -39
  299. package/dist/tools/git-context.d.ts.map +0 -1
  300. package/dist/tools/git-context.js +0 -385
  301. package/dist/tools/git-context.js.map +0 -1
  302. package/dist/tools/git-diff.d.ts +0 -45
  303. package/dist/tools/git-diff.d.ts.map +0 -1
  304. package/dist/tools/git-diff.js +0 -338
  305. package/dist/tools/git-diff.js.map +0 -1
  306. package/dist/tools/git-history.d.ts +0 -16
  307. package/dist/tools/git-history.d.ts.map +0 -1
  308. package/dist/tools/git-history.js +0 -106
  309. package/dist/tools/git-history.js.map +0 -1
  310. package/dist/tools/history.d.ts +0 -34
  311. package/dist/tools/history.d.ts.map +0 -1
  312. package/dist/tools/history.js +0 -121
  313. package/dist/tools/history.js.map +0 -1
  314. package/dist/tools/impact.d.ts +0 -46
  315. package/dist/tools/impact.d.ts.map +0 -1
  316. package/dist/tools/impact.js +0 -225
  317. package/dist/tools/impact.js.map +0 -1
  318. package/dist/tools/index.d.ts +0 -59
  319. package/dist/tools/index.d.ts.map +0 -1
  320. package/dist/tools/index.js +0 -70
  321. package/dist/tools/index.js.map +0 -1
  322. package/dist/tools/kit.d.ts +0 -165
  323. package/dist/tools/kit.d.ts.map +0 -1
  324. package/dist/tools/kit.js +0 -153
  325. package/dist/tools/kit.js.map +0 -1
  326. package/dist/tools/link-workspace.d.ts +0 -24
  327. package/dist/tools/link-workspace.d.ts.map +0 -1
  328. package/dist/tools/link-workspace.js +0 -131
  329. package/dist/tools/link-workspace.js.map +0 -1
  330. package/dist/tools/lookup.d.ts +0 -19
  331. package/dist/tools/lookup.d.ts.map +0 -1
  332. package/dist/tools/lookup.js +0 -63
  333. package/dist/tools/lookup.js.map +0 -1
  334. package/dist/tools/mark-incident.d.ts +0 -28
  335. package/dist/tools/mark-incident.d.ts.map +0 -1
  336. package/dist/tools/mark-incident.js +0 -188
  337. package/dist/tools/mark-incident.js.map +0 -1
  338. package/dist/tools/merged-browser.d.ts +0 -11
  339. package/dist/tools/merged-browser.d.ts.map +0 -1
  340. package/dist/tools/merged-browser.js +0 -20
  341. package/dist/tools/merged-browser.js.map +0 -1
  342. package/dist/tools/merged-context.d.ts +0 -11
  343. package/dist/tools/merged-context.d.ts.map +0 -1
  344. package/dist/tools/merged-context.js +0 -21
  345. package/dist/tools/merged-context.js.map +0 -1
  346. package/dist/tools/merged-observe.d.ts +0 -13
  347. package/dist/tools/merged-observe.d.ts.map +0 -1
  348. package/dist/tools/merged-observe.js +0 -21
  349. package/dist/tools/merged-observe.js.map +0 -1
  350. package/dist/tools/merged-workspace.d.ts +0 -14
  351. package/dist/tools/merged-workspace.d.ts.map +0 -1
  352. package/dist/tools/merged-workspace.js +0 -21
  353. package/dist/tools/merged-workspace.js.map +0 -1
  354. package/dist/tools/observe-runtime.d.ts +0 -262
  355. package/dist/tools/observe-runtime.d.ts.map +0 -1
  356. package/dist/tools/observe-runtime.js +0 -1901
  357. package/dist/tools/observe-runtime.js.map +0 -1
  358. package/dist/tools/observe-tests.d.ts +0 -57
  359. package/dist/tools/observe-tests.d.ts.map +0 -1
  360. package/dist/tools/observe-tests.js +0 -368
  361. package/dist/tools/observe-tests.js.map +0 -1
  362. package/dist/tools/observe.d.ts +0 -28
  363. package/dist/tools/observe.d.ts.map +0 -1
  364. package/dist/tools/observe.js +0 -330
  365. package/dist/tools/observe.js.map +0 -1
  366. package/dist/tools/pr-analysis.d.ts +0 -189
  367. package/dist/tools/pr-analysis.d.ts.map +0 -1
  368. package/dist/tools/pr-analysis.js +0 -374
  369. package/dist/tools/pr-analysis.js.map +0 -1
  370. package/dist/tools/pre-push.d.ts +0 -168
  371. package/dist/tools/pre-push.d.ts.map +0 -1
  372. package/dist/tools/pre-push.js +0 -416
  373. package/dist/tools/pre-push.js.map +0 -1
  374. package/dist/tools/predict-reach.d.ts +0 -57
  375. package/dist/tools/predict-reach.d.ts.map +0 -1
  376. package/dist/tools/predict-reach.js +0 -159
  377. package/dist/tools/predict-reach.js.map +0 -1
  378. package/dist/tools/propagation.d.ts +0 -37
  379. package/dist/tools/propagation.d.ts.map +0 -1
  380. package/dist/tools/propagation.js +0 -162
  381. package/dist/tools/propagation.js.map +0 -1
  382. package/dist/tools/propose-journey.d.ts +0 -16
  383. package/dist/tools/propose-journey.d.ts.map +0 -1
  384. package/dist/tools/propose-journey.js +0 -121
  385. package/dist/tools/propose-journey.js.map +0 -1
  386. package/dist/tools/prove-reach.d.ts +0 -74
  387. package/dist/tools/prove-reach.d.ts.map +0 -1
  388. package/dist/tools/prove-reach.js +0 -286
  389. package/dist/tools/prove-reach.js.map +0 -1
  390. package/dist/tools/questions.d.ts +0 -30
  391. package/dist/tools/questions.d.ts.map +0 -1
  392. package/dist/tools/questions.js +0 -232
  393. package/dist/tools/questions.js.map +0 -1
  394. package/dist/tools/refusal-fetch.d.ts +0 -35
  395. package/dist/tools/refusal-fetch.d.ts.map +0 -1
  396. package/dist/tools/refusal-fetch.js +0 -123
  397. package/dist/tools/refusal-fetch.js.map +0 -1
  398. package/dist/tools/runtime-incident.d.ts +0 -33
  399. package/dist/tools/runtime-incident.d.ts.map +0 -1
  400. package/dist/tools/runtime-incident.js +0 -67
  401. package/dist/tools/runtime-incident.js.map +0 -1
  402. package/dist/tools/runtime-journey-drive.d.ts +0 -102
  403. package/dist/tools/runtime-journey-drive.d.ts.map +0 -1
  404. package/dist/tools/runtime-journey-drive.js +0 -247
  405. package/dist/tools/runtime-journey-drive.js.map +0 -1
  406. package/dist/tools/scope.d.ts +0 -45
  407. package/dist/tools/scope.d.ts.map +0 -1
  408. package/dist/tools/scope.js +0 -217
  409. package/dist/tools/scope.js.map +0 -1
  410. package/dist/tools/similar-incidents.d.ts +0 -43
  411. package/dist/tools/similar-incidents.d.ts.map +0 -1
  412. package/dist/tools/similar-incidents.js +0 -203
  413. package/dist/tools/similar-incidents.js.map +0 -1
  414. package/dist/tools/validate.d.ts +0 -133
  415. package/dist/tools/validate.d.ts.map +0 -1
  416. package/dist/tools/validate.js +0 -520
  417. package/dist/tools/validate.js.map +0 -1
  418. package/dist/tools/verb.d.ts +0 -46
  419. package/dist/tools/verb.d.ts.map +0 -1
  420. package/dist/tools/verb.js +0 -78
  421. package/dist/tools/verb.js.map +0 -1
  422. package/dist/tools/verification-status.d.ts +0 -38
  423. package/dist/tools/verification-status.d.ts.map +0 -1
  424. package/dist/tools/verification-status.js +0 -203
  425. package/dist/tools/verification-status.js.map +0 -1
  426. package/dist/tools/verify-claim.d.ts +0 -29
  427. package/dist/tools/verify-claim.d.ts.map +0 -1
  428. package/dist/tools/verify-claim.js +0 -220
  429. package/dist/tools/verify-claim.js.map +0 -1
  430. package/dist/tools/workspace.d.ts +0 -28
  431. package/dist/tools/workspace.d.ts.map +0 -1
  432. package/dist/tools/workspace.js +0 -101
  433. package/dist/tools/workspace.js.map +0 -1
  434. package/dist/transport.d.ts +0 -32
  435. package/dist/transport.d.ts.map +0 -1
  436. package/dist/transport.js +0 -85
  437. package/dist/transport.js.map +0 -1
  438. package/dist/workspace-index.d.ts +0 -164
  439. package/dist/workspace-index.d.ts.map +0 -1
  440. package/dist/workspace-index.js +0 -381
  441. package/dist/workspace-index.js.map +0 -1
@@ -1,1901 +0,0 @@
1
- /**
2
- * `observe_runtime` — boot or attach to a real application, write what was witnessed into the
3
- * graph as R4 facts. Composes existing descry-runtime stages, no new mechanism (DEC-NEXT-mcp-
4
- * runtime-dependency-boundary-for-r4-evidence). `action`/`evidence` (DEC-278), gated by
5
- * confirmToken + profile safetyLevel (DEC-270). Never emits a denial. */
6
- import { mkdir } from "node:fs/promises";
7
- import { dirname, isAbsolute, join } from "node:path";
8
- import { independentSignalTypes } from "@descryy/ir";
9
- import { checkNotSensitivePath, checkPathConfinement } from "../path-confinement.js";
10
- /** A-F6 escape hatches — see path-confinement.ts. */
11
- const ALLOW_EVIDENCE_PATH_OUTSIDE_REPO_ENV_VAR = "DESCRY_ALLOW_EVIDENCE_PATH_OUTSIDE_REPO";
12
- const ALLOW_SENSITIVE_LOG_PATH_ENV_VAR = "DESCRY_ALLOW_SENSITIVE_LOG_PATH";
13
- import { buildGraph, counts, createConfirmedIncidentSource, evictStaleConfirmedIncidentNodes, persistGraph, } from "@descryy/core";
14
- import { createDatabaseQueryCollector, createPostgresLogSource, createPostgresQueryCollector, } from "@descryy/runtime-database-observation";
15
- import { evaluateAction, PROFILE_MODES, SAFETY_LEVELS, validateProfile } from "@descryy/runtime-environment-profile";
16
- import { correlateExecution } from "@descryy/runtime-evidence-correlation";
17
- import { EvidenceStore } from "@descryy/runtime-evidence-store";
18
- import { confirmObservedFrontendCaller } from "@descryy/runtime-graph-correlator";
19
- import { createSourceRootResolver, runInstrumentedExecution } from "@descryy/runtime-orchestrator";
20
- import { answer, optionalBoolean, optionalEnum, optionalInteger, optionalString, ToolInputError, } from "./kit.js";
21
- import { cancellationHeadline, cancellationNotes, whenAborted } from "../cancellation.js";
22
- import { readScenario } from "../scenarios/index.js";
23
- import { createRuntimeJourneyDrive, projectScenarioToRunPlan, } from "./runtime-journey-drive.js";
24
- import { loadRuntimeAdapter, RuntimeAdapterLoadError } from "../runtime-registry.js";
25
- import { writeConfirmedIncident, writeSavedStart } from "../session.js";
26
- import { runtimeObservedIncident } from "./runtime-incident.js";
27
- /** Where evidence lands when the call does not say. Beside the graph, not inside it. */
28
- export const DEFAULT_EVIDENCE_RELATIVE_PATH = join(".descry", "evidence.db");
29
- /** How long collectors are drained after the services report ready, when unstated. */
30
- const DEFAULT_OBSERVE_MS = 5_000;
31
- const DEFAULT_TIMEOUT_MS = 60_000;
32
- const DEFAULT_READINESS_TIMEOUT_MS = 30_000;
33
- const READINESS_KINDS = ["http", "tcp-port", "command"];
34
- const SCHEMA = {
35
- type: "object",
36
- properties: {
37
- profile: {
38
- type: "object",
39
- description: "The environment this run targets. Every field is declared by you and never inferred from " +
40
- "any other (DEC-270): a profile named \"staging\" with safetyLevel \"readOnly\" is read-only, " +
41
- "and a profile named \"local\" with safetyLevel \"readOnly\" is too.",
42
- properties: {
43
- name: { type: "string", description: "Free-form. Matched against no vocabulary anywhere." },
44
- url: { type: "string", description: "The target's base URL. Must parse." },
45
- safetyLevel: {
46
- type: "string",
47
- // Same constant `describeProfileError` maps INVALID_SAFETY_LEVEL's
48
- // legal values from — one source, so schema and refusal cannot
49
- // drift apart (UAT phase 5, F10).
50
- enum: [...SAFETY_LEVELS],
51
- description: "Booting a service is a write against the target, so \"readOnly\" refuses a run that " +
52
- "spawns anything. A run in which every service uses \"attach\" spawns nothing and is " +
53
- "permitted under \"readOnly\".",
54
- },
55
- credentialRef: {
56
- type: "string",
57
- description: "An opaque key into a credential store — never the secret itself.",
58
- },
59
- mode: { type: "string", enum: [...PROFILE_MODES] },
60
- },
61
- required: ["name", "url", "safetyLevel", "credentialRef", "mode"],
62
- additionalProperties: false,
63
- },
64
- services: {
65
- type: "object",
66
- description: "One entry per service, keyed by the name evidence will be attributed to. Exactly one of " +
67
- "\"command\" or \"attach\" per service. Optional: when omitted, a repository that has a " +
68
- "prior successful run's services saved reuses them (disclosed on the reply) — otherwise " +
69
- "this call refuses and names that omission as the reason. Supplying this always overrides " +
70
- "whatever is saved, and a run that reaches readiness replaces the saved services with " +
71
- "this call's own, whichever source they came from.",
72
- additionalProperties: {
73
- type: "object",
74
- properties: {
75
- command: { type: "string", description: "How to start it. Omit when using \"attach\"." },
76
- cwd: {
77
- type: "string",
78
- description: "The directory this service is started in. Relative paths resolve against the " +
79
- "repository root. REQUIRED with \"command\". With \"attach\" it is required only " +
80
- "for a \"command\" readiness check, which is an executable Descry runs in it; for " +
81
- "an \"http\" or \"tcp-port\" check it is not needed at all, because nothing is " +
82
- "started and Descry never executes code in a process it did not spawn.",
83
- },
84
- // Fixes the tool's costliest ergonomic gap: 3 failed runs in one real investigation,
85
- // same root cause, explanation previously living only in a source comment.
86
- port: {
87
- type: "integer",
88
- description: "The port readiness checks against, and the two modes need opposite things from you. " +
89
- "With \"attach\": REQUIRED whenever readiness is \"http\" or \"tcp-port\" — the target " +
90
- "chose its port before Descry saw it, and nothing in a pid or a log path reveals which, " +
91
- "so this is refused up front rather than guessed. With \"command\": omit to get an " +
92
- "ephemeral port, which is passed to your command as PORT; set it only if your command " +
93
- "hardcodes a port, and then it must be THAT port — a readiness check against a port " +
94
- "your command did not bind fails while the service is perfectly healthy.",
95
- },
96
- dependsOn: {
97
- type: "array",
98
- items: { type: "string" },
99
- description: "Service names that must be ready first. Declared, never inferred.",
100
- },
101
- env: { type: "object", additionalProperties: { type: "string" } },
102
- attach: {
103
- type: "object",
104
- description: "Observe a process that is already running instead of spawning one. Descry never " +
105
- "executes code in, signals, or applies resource limits to a process it did not spawn.",
106
- properties: {
107
- pid: { type: "integer" },
108
- logFilePath: {
109
- type: "string",
110
- description: "A file the target already writes its stdout/stderr to.",
111
- },
112
- },
113
- required: ["pid", "logFilePath"],
114
- additionalProperties: false,
115
- },
116
- readiness: {
117
- type: "object",
118
- description: "Required per service — a run refuses rather than treat \"the process started\" as " +
119
- "\"the service is up\". Only the three mechanisms expressible as JSON are offered here; " +
120
- "\"log-pattern\" and \"custom-hook\" need a function and are not reachable through this " +
121
- "tool, which is disclosed on every call rather than left to be discovered.",
122
- properties: {
123
- kind: { type: "string", enum: [...READINESS_KINDS] },
124
- path: {
125
- type: "string",
126
- description: "kind \"http\": path appended to http://127.0.0.1:<resolved port>. Defaults to \"/\".",
127
- },
128
- expectedStatus: { type: "integer", description: "kind \"http\": defaults to any 2xx/3xx." },
129
- host: { type: "string", description: "kind \"tcp-port\": defaults to 127.0.0.1." },
130
- command: { type: "string", description: "kind \"command\": the executable to run." },
131
- args: { type: "array", items: { type: "string" }, description: "kind \"command\"." },
132
- timeoutMs: { type: "integer", description: `Defaults to ${DEFAULT_READINESS_TIMEOUT_MS}.` },
133
- },
134
- required: ["kind"],
135
- additionalProperties: false,
136
- },
137
- },
138
- required: ["readiness"],
139
- additionalProperties: false,
140
- },
141
- },
142
- adapter: {
143
- type: "object",
144
- description: "The runtime adapter to observe with, named as a module specifier and imported at run time. " +
145
- "This server depends on none of descry-runtime's per-language runtime adapters by design, and " +
146
- "names none of them anywhere — including here, which is why this description carries no " +
147
- "example specifier. Install the one matching the service's runtime alongside this server and " +
148
- "name its package here; descry-runtime publishes one runtime adapter package per supported " +
149
- "runtime, and its README lists them.",
150
- properties: {
151
- module: { type: "string" },
152
- export: {
153
- type: "string",
154
- description: "Defaults to the single export matching create*RuntimeAdapter. Two matches is an error, " +
155
- "not a coin toss — name one here.",
156
- },
157
- options: { type: "object", description: "Passed to the factory. Adapter-specific and opaque here." },
158
- },
159
- required: ["module"],
160
- additionalProperties: false,
161
- },
162
- scopeByService: {
163
- type: "object",
164
- description: "Service name → which repository its symbols resolve in. A service with no entry has its " +
165
- "symbol evidence left alone and its name reported, never resolved against a repository " +
166
- "nobody named. Defaults to this session's own repo for every declared service.",
167
- additionalProperties: {
168
- type: "object",
169
- properties: {
170
- repo: { type: "string" },
171
- repoRoot: { type: "string", description: "Absolute on-disk root, so observed absolute paths translate exactly." },
172
- cwd: { type: "string" },
173
- },
174
- required: ["repo"],
175
- additionalProperties: false,
176
- },
177
- },
178
- observeForMs: {
179
- type: "integer",
180
- description: `How long to drain collector output after the services are up. Defaults to ${DEFAULT_OBSERVE_MS}. ` +
181
- "There is no \"the application is done\" signal at this layer — a server runs until stopped — " +
182
- "so you state the observation window rather than this tool guessing at one.",
183
- },
184
- timeoutMs: { type: "integer", description: `Whole-execution budget. Defaults to ${DEFAULT_TIMEOUT_MS}.` },
185
- environmentTier: {
186
- type: "string",
187
- enum: ["tier-0-ci-attached", "tier-1-preview", "tier-2-container", "tier-2b-api-only", "tier-3-static-only"],
188
- description: "Recorded on the execution. Defaults to \"tier-2-container\" and is deliberately not derived " +
189
- "from \"profile.mode\" — DEC-270's rule is that a declared field is declared, not inferred " +
190
- "from a neighbouring one.",
191
- },
192
- fidelityLevel: {
193
- type: "integer",
194
- enum: [1, 2, 3, 4],
195
- description: "1 rule-aware stub · 2 real code + disposable DB · 3 real code + redacted recordings · " +
196
- "4 real staging. Defaults to 2. Not derived from anything else, same reason as environmentTier.",
197
- },
198
- evidencePath: {
199
- type: "string",
200
- description: `Where the evidence database lives. Defaults to ${DEFAULT_EVIDENCE_RELATIVE_PATH} under the repository root.`,
201
- },
202
- resourceLimits: {
203
- type: "object",
204
- description: "A-F5: caps on a spawned service, enforced by the OS (prlimit) — never applied to an attached " +
205
- "service, since Descry did not start it. Absent means unconstrained, which every reply discloses.",
206
- properties: {
207
- maxMemoryBytes: { type: "integer", description: "Virtual address space cap (prlimit --as)." },
208
- maxCpuSeconds: { type: "integer", description: "CPU time cap, in seconds (prlimit --cpu)." },
209
- maxProcesses: { type: "integer", description: "Process count cap, per real uid (prlimit --nproc)." },
210
- },
211
- additionalProperties: false,
212
- },
213
- filesystemPolicy: {
214
- type: "object",
215
- description: "A-F5: confines a spawned service's filesystem view to its own cwd plus these roots — real on " +
216
- "Linux (a bwrap mount namespace; everything else is not merely unreadable, it is not mounted at " +
217
- "all), refused rather than silently unenforced elsewhere. Absent means unconstrained.",
218
- properties: {
219
- allowedRoots: {
220
- type: "array",
221
- items: { type: "string" },
222
- description: "Absolute paths visible read-write in addition to the service's own cwd.",
223
- },
224
- },
225
- required: ["allowedRoots"],
226
- additionalProperties: false,
227
- },
228
- networkPolicy: {
229
- type: "object",
230
- description: 'A-F5: only { mode: "allow", hosts: [] } (full denial) is actually enforced today — a network ' +
231
- "namespace holding nothing but an unreachable loopback. Any other shape refuses the run rather " +
232
- "than starting unconstrained under a policy nobody enforced. Absent means unconstrained.",
233
- properties: {
234
- mode: { type: "string", enum: ["allow", "deny"] },
235
- hosts: { type: "array", items: { type: "string" } },
236
- },
237
- required: ["mode", "hosts"],
238
- additionalProperties: false,
239
- },
240
- sandboxBackend: {
241
- type: "string",
242
- enum: ["bwrap", "container"],
243
- description: 'Which mechanism enforces filesystemPolicy/networkPolicy. Defaults to "bwrap" (Linux-native). ' +
244
- '"container" routes through a real Docker container instead — the only option on macOS/Windows, ' +
245
- "and it does not compose with resourceLimits (disclosed on the reply when both are declared).",
246
- },
247
- confirmToken: {
248
- type: "string",
249
- description: "The token returned by an unconfirmed call. This tool performs nothing without it: the first " +
250
- "call describes what running would do and returns a token, and only a second call presenting " +
251
- "that exact token runs anything — with the arguments frozen when the token was minted, never " +
252
- "whatever the second call supplies. A conflicting argument on the confirming call is " +
253
- "discarded rather than applied, and the reply discloses which ones were.",
254
- },
255
- journey: {
256
- type: "object",
257
- description: "RG-4 — drive a real browser inside THIS run's readiness window, so browser and backend " +
258
- "evidence land in the same evidence store under the same executionId, rather than requiring " +
259
- "a separate browser_start_session call against an app that may no longer be in the state this " +
260
- "run put it in. Optional: omitted means backend-only, exactly as before. Declare exactly one " +
261
- "of \"scenario\" or \"startUrl\". No journey is invented — a scenario with no steps, or " +
262
- "startUrl alone, navigates and observes without interacting with anything.",
263
- properties: {
264
- service: {
265
- type: "string",
266
- description: "Which declared service this browser evidence is attributed to. Optional.",
267
- },
268
- scenario: {
269
- type: "string",
270
- description: "The name of a committed scenario under descry/scenarios/ (see browser_save_scenario), " +
271
- "replayed inside this run's readiness window. Its own targetUrl is used as the start URL.",
272
- },
273
- startUrl: {
274
- type: "string",
275
- description: "Navigate here and observe, with no saved scenario and no steps. Mutually exclusive with \"scenario\".",
276
- },
277
- recordVideo: {
278
- type: "boolean",
279
- description: "RG-6. Off by default. Captured after the drive completes and returned as a VIDEO evidence artifact.",
280
- },
281
- headless: { type: "boolean", description: "Defaults to true." },
282
- settleForMs: {
283
- type: "integer",
284
- description: "How long to keep the page alive after the last step so async console/network activity it provoked actually arrives. Defaults to 1000.",
285
- },
286
- },
287
- additionalProperties: false,
288
- },
289
- database: {
290
- type: "object",
291
- description: "RG-5(c) — the database channel. Node services get the node:sqlite observation preload " +
292
- "automatically (no declaration needed); this argument is for Postgres, whose statement log " +
293
- "lives on the server, not in a spawned service's own output. Declares where to tail it from, " +
294
- "so real queries issued during this run's observation window become DATABASE_QUERY evidence. " +
295
- "Omit entirely for no Postgres channel — the reply discloses that rather than staying silent.",
296
- properties: {
297
- statementLog: {
298
- type: "object",
299
- description: "Never turns Postgres logging on — reads whatever the deployment already produces.",
300
- properties: {
301
- kind: { type: "string", enum: ["file", "dockerContainer"] },
302
- path: { type: "string", description: "kind \"file\": a log file the deployment already writes to." },
303
- container: { type: "string", description: "kind \"dockerContainer\": tails `docker logs -f` for this container." },
304
- },
305
- required: ["kind"],
306
- additionalProperties: false,
307
- },
308
- service: {
309
- type: "string",
310
- description: "Which declared service this DATABASE_QUERY evidence is attributed to. Optional.",
311
- },
312
- },
313
- required: ["statementLog"],
314
- additionalProperties: false,
315
- },
316
- },
317
- required: ["profile", "adapter"],
318
- additionalProperties: false,
319
- };
320
- function asRecord(value, what) {
321
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
322
- throw new ToolInputError(`"${what}" must be an object`);
323
- }
324
- return value;
325
- }
326
- /**
327
- * `validateProfile`'s bare machine codes, mapped to a sentence naming what would
328
- * satisfy them — generated from `SAFETY_LEVELS`/`PROFILE_MODES`, the same constants
329
- * the schema's own `enum`s are built from above, so the two cannot drift (UAT phase
330
- * 5, F10). The codes without a closed value set (`EMPTY_NAME`, `INVALID_URL`, …)
331
- * still get a plain-language sentence rather than reaching the caller verbatim —
332
- * consistency, not just the two enum cases.
333
- */
334
- function describeProfileError(code) {
335
- switch (code) {
336
- case "EMPTY_NAME":
337
- return '"profile.name" must not be empty';
338
- case "EMPTY_CREDENTIAL_REF":
339
- return '"profile.credentialRef" must not be empty';
340
- case "INVALID_URL":
341
- return '"profile.url" must be a URL the URL constructor can parse';
342
- case "INVALID_SAFETY_LEVEL":
343
- return `"profile.safetyLevel" must be one of: ${SAFETY_LEVELS.join(", ")}`;
344
- case "INVALID_MODE":
345
- return `"profile.mode" must be one of: ${PROFILE_MODES.join(", ")}`;
346
- case "REMOTE_LOG_SOURCE_NOT_ALLOWED_FOR_MODE":
347
- return ('"profile.remoteLogSource" is not allowed when "profile.mode" is "localBooted" or ' +
348
- '"localAttached" — DEC-271\'s remote-log-ingestion mechanism only applies to "remote"/"production"');
349
- }
350
- }
351
- function readProfile(args) {
352
- const raw = asRecord(args["profile"], "profile");
353
- // Collect, don't short-circuit (UAT phase 5, F10): the old `field()` threw on
354
- // the first key that was not a string, costing up to five round trips to
355
- // enumerate five required fields ("seven refusals, one field at a time" in
356
- // one real investigation). Every field is checked before anything is thrown,
357
- // so a caller learns the whole shape of what is wrong in one call.
358
- const shapeErrors = [];
359
- const field = (key) => {
360
- const value = raw[key];
361
- if (typeof value !== "string") {
362
- shapeErrors.push(`"profile.${key}" must be a string`);
363
- return undefined;
364
- }
365
- return value;
366
- };
367
- const name = field("name");
368
- const url = field("url");
369
- const safetyLevel = field("safetyLevel");
370
- const credentialRef = field("credentialRef");
371
- const mode = field("mode");
372
- if (shapeErrors.length > 0) {
373
- throw new ToolInputError(`"profile" is not valid: ${shapeErrors.join("; ")}`);
374
- }
375
- const candidate = {
376
- name: name,
377
- url: url,
378
- safetyLevel: safetyLevel,
379
- credentialRef: credentialRef,
380
- mode: mode,
381
- };
382
- const errors = validateProfile(candidate);
383
- if (errors.length > 0) {
384
- throw new ToolInputError(`"profile" is not valid: ${errors.map(describeProfileError).join("; ")}`);
385
- }
386
- return candidate;
387
- }
388
- function readAdapterSpec(args) {
389
- const raw = asRecord(args["adapter"], "adapter");
390
- const module = raw["module"];
391
- if (typeof module !== "string" || module === "") {
392
- throw new ToolInputError('"adapter.module" is required and must be a non-empty string');
393
- }
394
- const exportName = raw["export"];
395
- if (exportName !== undefined && typeof exportName !== "string") {
396
- throw new ToolInputError('"adapter.export" must be a string');
397
- }
398
- const options = raw["options"];
399
- if (options !== undefined && (typeof options !== "object" || options === null)) {
400
- throw new ToolInputError('"adapter.options" must be an object');
401
- }
402
- return {
403
- module,
404
- ...(typeof exportName === "string" ? { export: exportName } : {}),
405
- ...(options === undefined ? {} : { options: options }),
406
- };
407
- }
408
- /** `readiness`'s raw JSON, captured as `SavedServiceStart.readiness` — before `readReadiness`
409
- * turns it into a closure that cannot be serialised. Reads the same fields `readReadiness`
410
- * itself validates, so this can only be called after that validation has already run. */
411
- function toSavedReadiness(raw) {
412
- const entry = asRecord(raw, "readiness");
413
- const kind = entry["kind"];
414
- const path = typeof entry["path"] === "string" ? entry["path"] : undefined;
415
- const expectedStatus = typeof entry["expectedStatus"] === "number" ? entry["expectedStatus"] : undefined;
416
- const host = typeof entry["host"] === "string" ? entry["host"] : undefined;
417
- const command = typeof entry["command"] === "string" ? entry["command"] : undefined;
418
- const rawArgs = entry["args"];
419
- const args = Array.isArray(rawArgs) ? rawArgs : undefined;
420
- const timeoutMs = typeof entry["timeoutMs"] === "number" ? entry["timeoutMs"] : undefined;
421
- return {
422
- kind,
423
- ...(path === undefined ? {} : { path }),
424
- ...(expectedStatus === undefined ? {} : { expectedStatus }),
425
- ...(host === undefined ? {} : { host }),
426
- ...(command === undefined ? {} : { command }),
427
- ...(args === undefined ? {} : { args }),
428
- ...(timeoutMs === undefined ? {} : { timeoutMs }),
429
- };
430
- }
431
- /** Rebuilds `DeclaredService[]` from what a prior successful run saved — the reuse path
432
- * (build item 3). `envNames` are re-read from THIS process's own environment, never from
433
- * disk: nothing here ever held a value to begin with. A name with nothing currently set
434
- * under it is reported back via `missingSavedEnv` rather than silently passing `undefined`
435
- * through — the caller (`run`) discloses it, per rule 7. */
436
- function declaredFromSaved(saved, repoPath) {
437
- const missingSavedEnv = [];
438
- const declared = Object.entries(saved.services).map(([name, svc]) => {
439
- const cwd = isAbsolute(svc.cwd) ? svc.cwd : join(repoPath, svc.cwd);
440
- const env = {};
441
- for (const key of svc.envNames ?? []) {
442
- const value = process.env[key];
443
- if (value === undefined)
444
- missingSavedEnv.push(`${name}.${key}`);
445
- else
446
- env[key] = value;
447
- }
448
- const configuration = {
449
- ...(svc.command === undefined ? {} : { command: svc.command }),
450
- cwd,
451
- ...(svc.port === undefined ? {} : { port: svc.port }),
452
- ...(svc.dependsOn === undefined ? {} : { dependsOn: svc.dependsOn }),
453
- ...(Object.keys(env).length === 0 ? {} : { env }),
454
- ...(svc.attach === undefined ? {} : { attach: svc.attach }),
455
- };
456
- return {
457
- name,
458
- configuration,
459
- readiness: readReadiness(svc.readiness, name, cwd),
460
- attached: svc.attach !== undefined,
461
- savable: svc,
462
- };
463
- });
464
- return { declared, missingSavedEnv };
465
- }
466
- /** A saved command or attach target, named plainly for a refusal/failure note (build item
467
- * 4) — never the whole saved shape, which would bury the one fact ("this is what didn't
468
- * work") a caller needs to read at a glance. */
469
- function describeSavedCommands(declared) {
470
- return declared
471
- .map((service) => service.attached
472
- ? `${service.name}: attach to pid ${String(service.configuration.attach?.pid ?? "?")}`
473
- : `${service.name}: "${service.configuration.command ?? "?"}"`)
474
- .join("; ");
475
- }
476
- function readServices(args, repoPath, savedFallback) {
477
- const rawServices = args["services"];
478
- if (rawServices === undefined) {
479
- if (savedFallback === undefined || Object.keys(savedFallback.services).length === 0) {
480
- throw new ToolInputError('"services" must declare at least one service, and no start settings have been saved yet ' +
481
- "for this repository. Declare it once with a run that reaches readiness and this tool " +
482
- "remembers it — after that, \"services\" can be omitted and the saved settings are " +
483
- "reused, disclosed on every call that reuses them.");
484
- }
485
- const { declared, missingSavedEnv } = declaredFromSaved(savedFallback, repoPath);
486
- return { declared, usedSaved: true, missingSavedEnv };
487
- }
488
- const raw = asRecord(rawServices, "services");
489
- const names = Object.keys(raw);
490
- if (names.length === 0)
491
- throw new ToolInputError('"services" must declare at least one service');
492
- const declared = names.map((name) => {
493
- const entry = asRecord(raw[name], `services.${name}`);
494
- const command = entry["command"];
495
- const attachRaw = entry["attach"];
496
- if ((command === undefined) === (attachRaw === undefined)) {
497
- throw new ToolInputError(`services.${name} must declare exactly one of "command" or "attach" — ` +
498
- (command === undefined ? "it declares neither" : "it declares both"));
499
- }
500
- if (command !== undefined && typeof command !== "string") {
501
- throw new ToolInputError(`"services.${name}.command" must be a string`);
502
- }
503
- // Read once: two rules below need it (whether cwd is required, whether attach needs a port).
504
- const readinessRaw = entry["readiness"];
505
- const readinessKind = typeof readinessRaw === "object" && readinessRaw !== null && !Array.isArray(readinessRaw)
506
- ? readinessRaw["kind"]
507
- : undefined;
508
- // cwd required only where something executes in it: attach never runs code Descry didn't
509
- // spawn, so http/tcp-port checks leave it inert; a "command" check does execute (DEC-388).
510
- const cwdRaw = entry["cwd"];
511
- if (cwdRaw !== undefined && (typeof cwdRaw !== "string" || cwdRaw === "")) {
512
- throw new ToolInputError(`"services.${name}.cwd" must be a non-empty string`);
513
- }
514
- if (cwdRaw === undefined && attachRaw === undefined) {
515
- throw new ToolInputError(`"services.${name}.cwd" is required and must be a non-empty string: it is the directory ` +
516
- `"${name}" is started in.`);
517
- }
518
- if (cwdRaw === undefined && attachRaw !== undefined && readinessKind === "command") {
519
- throw new ToolInputError(`"services.${name}.cwd" is required when "${name}" uses "attach" with a "command" ` +
520
- "readiness check: the check is an executable Descry runs, and it runs in this " +
521
- "directory. Attaching needs no cwd otherwise — nothing is started.");
522
- }
523
- const cwd = cwdRaw ?? repoPath;
524
- const port = entry["port"];
525
- if (port !== undefined && (typeof port !== "number" || !Number.isInteger(port) || port < 0)) {
526
- throw new ToolInputError(`"services.${name}.port" must be a non-negative integer`);
527
- }
528
- const dependsOn = entry["dependsOn"];
529
- if (dependsOn !== undefined &&
530
- (!Array.isArray(dependsOn) || dependsOn.some((d) => typeof d !== "string"))) {
531
- throw new ToolInputError(`"services.${name}.dependsOn" must be an array of strings`);
532
- }
533
- const env = entry["env"];
534
- if (env !== undefined) {
535
- const record = asRecord(env, `services.${name}.env`);
536
- for (const [key, value] of Object.entries(record)) {
537
- if (typeof value !== "string") {
538
- throw new ToolInputError(`"services.${name}.env.${key}" must be a string`);
539
- }
540
- }
541
- }
542
- let attach;
543
- if (attachRaw !== undefined) {
544
- const a = asRecord(attachRaw, `services.${name}.attach`);
545
- const pid = a["pid"];
546
- const logFilePath = a["logFilePath"];
547
- if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
548
- throw new ToolInputError(`"services.${name}.attach.pid" must be a positive integer`);
549
- }
550
- if (typeof logFilePath !== "string" || logFilePath === "") {
551
- throw new ToolInputError(`"services.${name}.attach.logFilePath" is required`);
552
- }
553
- // A-F6: logFilePath is read in full on every poll and can't be root-confined (a real
554
- // log lives anywhere) — narrower, disclosed name-based check instead (path-confinement.ts).
555
- const sensitivity = checkNotSensitivePath(logFilePath, ALLOW_SENSITIVE_LOG_PATH_ENV_VAR);
556
- if (sensitivity.sensitive) {
557
- throw new ToolInputError(`"services.${name}.attach.logFilePath": ${sensitivity.reason}`);
558
- }
559
- attach = { pid, logFilePath };
560
- // An attached service's port can't be allocated or inferred — nothing in a pid or log
561
- // path reveals it. Left as `port ?? 0` it silently times out against port 0; refused
562
- // here by name instead (cost a full failed run to rediscover, twice). Not for "command"
563
- // checks, which never ask where the service listens.
564
- if ((readinessKind === "http" || readinessKind === "tcp-port") && port === undefined) {
565
- throw new ToolInputError(`"services.${name}.port" is required when "${name}" uses "attach" with a ` +
566
- `"${readinessKind}" readiness check: the check needs a port and an attached target's ` +
567
- "port cannot be allocated or inferred — it is whatever the already-running process " +
568
- "bound. State it, or use a \"command\" readiness check, which needs none.");
569
- }
570
- }
571
- const configuration = {
572
- ...(typeof command === "string" ? { command } : {}),
573
- cwd: isAbsolute(cwd) ? cwd : join(repoPath, cwd),
574
- ...(port === undefined ? {} : { port: port }),
575
- ...(dependsOn === undefined ? {} : { dependsOn: dependsOn }),
576
- ...(env === undefined ? {} : { env: env }),
577
- ...(attach === undefined ? {} : { attach }),
578
- };
579
- const savable = {
580
- ...(typeof command === "string" ? { command } : {}),
581
- cwd: configuration.cwd,
582
- ...(port === undefined ? {} : { port: port }),
583
- ...(dependsOn === undefined ? {} : { dependsOn: dependsOn }),
584
- // Names only — see this file's own header and SavedServiceStart's doc. Never `env` itself.
585
- ...(env === undefined ? {} : { envNames: Object.keys(env) }),
586
- ...(attach === undefined ? {} : { attach }),
587
- readiness: toSavedReadiness(entry["readiness"]),
588
- };
589
- return {
590
- name,
591
- configuration,
592
- readiness: readReadiness(entry["readiness"], name, configuration.cwd),
593
- attached: attach !== undefined,
594
- savable,
595
- };
596
- });
597
- return { declared, usedSaved: false, missingSavedEnv: [] };
598
- }
599
- /** JSON→ReadinessCheck mapping. `log-pattern`/`custom-hook` can't survive a JSON boundary
600
- * (one closes over a live process, the other is a function) — absent from the enum and
601
- * stated in disclosures instead (rule 7, applied to a capability, not just a result). */
602
- function readReadiness(raw, service, cwd) {
603
- const entry = asRecord(raw, `services.${service}.readiness`);
604
- const kind = entry["kind"];
605
- if (typeof kind !== "string" || !READINESS_KINDS.includes(kind)) {
606
- throw new ToolInputError(`"services.${service}.readiness.kind" must be one of: ${READINESS_KINDS.join(", ")}`);
607
- }
608
- const timeoutMs = entry["timeoutMs"];
609
- if (timeoutMs !== undefined &&
610
- (typeof timeoutMs !== "number" || !Number.isInteger(timeoutMs) || timeoutMs < 1)) {
611
- throw new ToolInputError(`"services.${service}.readiness.timeoutMs" must be a positive integer`);
612
- }
613
- // Validated here, not lazily inside checks() — the controller only calls checks() after
614
- // spawning, so a bad argument caught late means a live process to clean up.
615
- const path = typeof entry["path"] === "string" ? entry["path"] : "/";
616
- const expectedStatus = entry["expectedStatus"];
617
- if (expectedStatus !== undefined && typeof expectedStatus !== "number") {
618
- throw new ToolInputError(`"services.${service}.readiness.expectedStatus" must be a number`);
619
- }
620
- const host = typeof entry["host"] === "string" ? entry["host"] : "127.0.0.1";
621
- const command = entry["command"];
622
- const commandArgs = entry["args"];
623
- if (kind === "command") {
624
- if (typeof command !== "string" || command === "") {
625
- throw new ToolInputError(`"services.${service}.readiness.command" is required for kind "command"`);
626
- }
627
- if (commandArgs !== undefined &&
628
- (!Array.isArray(commandArgs) || commandArgs.some((a) => typeof a !== "string"))) {
629
- throw new ToolInputError(`"services.${service}.readiness.args" must be an array of strings`);
630
- }
631
- }
632
- const checks = (info) => {
633
- if (kind === "http") {
634
- return [
635
- {
636
- kind: "http",
637
- url: `http://127.0.0.1:${String(info.port)}${path.startsWith("/") ? path : `/${path}`}`,
638
- ...(typeof expectedStatus === "number" ? { expectedStatus } : {}),
639
- },
640
- ];
641
- }
642
- if (kind === "tcp-port") {
643
- return [{ kind: "tcp-port", host, port: info.port }];
644
- }
645
- return [
646
- {
647
- kind: "command",
648
- command: command,
649
- ...(commandArgs === undefined ? {} : { args: commandArgs }),
650
- cwd,
651
- },
652
- ];
653
- };
654
- return {
655
- checks,
656
- timeoutMs: typeof timeoutMs === "number" ? timeoutMs : DEFAULT_READINESS_TIMEOUT_MS,
657
- };
658
- }
659
- function readScopes(args, declared, fallback) {
660
- const scopes = {};
661
- for (const service of declared)
662
- scopes[service.name] = fallback;
663
- const raw = args["scopeByService"];
664
- if (raw === undefined)
665
- return scopes;
666
- for (const [name, value] of Object.entries(asRecord(raw, "scopeByService"))) {
667
- const entry = asRecord(value, `scopeByService.${name}`);
668
- const repo = entry["repo"];
669
- if (typeof repo !== "string" || repo === "") {
670
- throw new ToolInputError(`"scopeByService.${name}.repo" is required and must be a non-empty string`);
671
- }
672
- const repoRoot = entry["repoRoot"];
673
- const cwd = entry["cwd"];
674
- if (repoRoot !== undefined && typeof repoRoot !== "string") {
675
- throw new ToolInputError(`"scopeByService.${name}.repoRoot" must be a string`);
676
- }
677
- if (cwd !== undefined && typeof cwd !== "string") {
678
- throw new ToolInputError(`"scopeByService.${name}.cwd" must be a string`);
679
- }
680
- scopes[name] = {
681
- repo,
682
- ...(repoRoot === undefined ? {} : { repoRoot }),
683
- ...(cwd === undefined ? {} : { cwd }),
684
- };
685
- }
686
- return scopes;
687
- }
688
- /**
689
- * `Execution.state`'s five terminal values (`@descryy/runtime-contracts`:
690
- * `COMPLETED`, `FAILED_START`, `FAILED`, `TIMED_OUT`, `CANCELLED`), reduced
691
- * to this reply's own three-way outcome. Was `state === "TIMED_OUT" ?
692
- * "timed_out" : "ok"` — checked for exactly one non-success terminal state
693
- * and read every other one, `FAILED_START` included, as success. A post-spawn
694
- * readiness failure sets `FAILED_START` without throwing (the service never
695
- * became ready, but nothing raised), so it fell straight through that
696
- * `: "ok"` default and reported a success-shaped headline over whatever
697
- * evidence happened to exist — real, but no proof the boot itself worked, and
698
- * sometimes none of it about this failure at all.
699
- *
700
- * Inverted here on purpose: only `COMPLETED` reads as `"ok"`. `TIMED_OUT`
701
- * keeps its own state (a partial answer, not a failure). Everything else —
702
- * `FAILED_START`, `FAILED`, `CANCELLED`, and any terminal state added later —
703
- * reads as `"failed"` by default, so a new failure mode can only ever under-
704
- * report as `failed` (safe) rather than silently rejoin `"ok"` (the bug).
705
- */
706
- function deriveRunOutcome(executionState) {
707
- if (executionState === "TIMED_OUT")
708
- return { state: "timed_out", failed: false, timedOut: true };
709
- if (executionState === "COMPLETED")
710
- return { state: "ok", failed: false, timedOut: false };
711
- return { state: "failed", failed: true, timedOut: false };
712
- }
713
- const EMPTY_WRITE = {
714
- promoted: [],
715
- created: [],
716
- confirmed: [],
717
- refused: [],
718
- contradictions: [],
719
- staleR4: [],
720
- confirmedFactConflict: [],
721
- unjoinedSplitEvidence: null,
722
- };
723
- /** Stated unconditionally on every call, not only when it bites: a caller unaware
724
- * log-pattern readiness is unreachable will misread a tcp-port check's empty evidence
725
- * as "the service produced nothing". */
726
- const STANDING_NOTES = [
727
- "Readiness here offers only the three mechanisms JSON can state — http, tcp-port and command. " +
728
- "log-pattern and custom-hook need a function and are unreachable through this tool; a run that " +
729
- "needs one of those is not degraded here, it is unsupported here.",
730
- "Edges are written only where an observation named both endpoints itself: a captured call-site " +
731
- "stack resolving to a function, against an endpoint the same observation named. Every other " +
732
- "correlated evidence item resolves a node and produces no edge, which is a gap in what this run " +
733
- "could prove rather than evidence that no such edge exists.",
734
- "That combination — HTTP evidence carrying a call-site stack — is produced here by the outbound-fetch " +
735
- "instrumentation in @descryy/runtime-external-service-observation, which this server installs into a " +
736
- "spawned service before any application code runs. So an observed outbound call CAN write this edge, " +
737
- "and a run that makes none writes none: the backend collectors attach a stack to log and error lines " +
738
- "(resolving a function) and none to inbound HTTP traffic (resolving an endpoint), so a service that " +
739
- "never calls out resolves both kinds of node and still writes nothing. A zero here means this run " +
740
- "observed no outbound call it could attribute, not that no such call exists in your code.",
741
- "One thing this still cannot witness. An ATTACHED service is not launched by Descry, so the client " +
742
- "instrumentation this outbound-fetch edge needs cannot be installed into it, and its outbound calls " +
743
- "carry no call-site stack — an attached run is never reported as instrumented the way a spawned one " +
744
- "is, whatever else it observes. @descryy/runtime-browser IS in this server's closure: declare " +
745
- "\"journey\" on this call to drive a browser inside this run's own readiness window, so browser and " +
746
- "backend evidence land under one executionId (RG-4). The separate browser_* tools are a different " +
747
- "lifetime — they require the application to already be running when the session starts, which this " +
748
- "run's own boot does not change.",
749
- "No denial is ever recorded. A run establishes that a call happened; it cannot establish that one " +
750
- "did not, because it exercises only the paths it took. Nothing here demotes an edge.",
751
- ];
752
- /** A-F5: honest degradation about real isolation, said on every reply rather than left for
753
- * the caller to discover. applySandbox enforces resourceLimits/filesystemPolicy/networkPolicy
754
- * for real on Linux when declared; never applied to an attached service (Descry didn't spawn it). */
755
- function sandboxDisclosure(options) {
756
- const { resourceLimits, filesystemPolicy, networkPolicy, sandboxBackend } = options;
757
- // True regardless of policy: spawnProcess merges the full ambient env into every spawned
758
- // process and bwrap doesn't clear it — out of this repo's reach (DEC-NEXT-runtime-sandbox-residual-gaps).
759
- const envCaveat = "This is unaffected by any policy above: the spawned process still receives this operator's full " +
760
- "environment, secrets included — the merge happens in @descryy/runtime-controller's spawnProcess and " +
761
- "cannot be narrowed from this server.";
762
- if (resourceLimits === undefined && filesystemPolicy === undefined && networkPolicy === undefined) {
763
- return ("No resourceLimits, filesystemPolicy or networkPolicy was declared for this run. Every spawned " +
764
- "service therefore ran with no OS-enforced isolation: it can read and write anything this operator's " +
765
- "account can, use as much memory/CPU/process count as the host allows, and reach any network this " +
766
- "operator's account can reach, and it received this operator's full environment. Attached services " +
767
- "are never sandboxed regardless — Descry did not spawn them. Declare resourceLimits/filesystemPolicy/" +
768
- "networkPolicy to change the filesystem/network/resource part of this for a spawned service.");
769
- }
770
- const applied = [];
771
- if (resourceLimits !== undefined)
772
- applied.push("resourceLimits");
773
- if (filesystemPolicy !== undefined)
774
- applied.push(`filesystemPolicy (allowedRoots: ${filesystemPolicy.allowedRoots.join(", ") || "none beyond cwd"})`);
775
- if (networkPolicy !== undefined)
776
- applied.push(`networkPolicy (${networkPolicy.mode}: ${networkPolicy.hosts.join(", ") || "none"})`);
777
- const backend = sandboxBackend ?? "bwrap";
778
- return (`Declared for this run, applied to every spawned (never attached) service via the "${backend}" ` +
779
- `backend: ${applied.join(", ")}. If any of these could not actually be enforced (wrong platform, an ` +
780
- "unsupported networkPolicy shape, bwrap/Docker unavailable), the run refused to start rather than " +
781
- `running unconstrained under a policy nobody enforced — see the headline if this reply is a refusal. ${envCaveat}`);
782
- }
783
- /** Said on every attaching run. Measured: a redirected process's stdout is block-buffered,
784
- * not line-buffered — 3 lines written over 0.6s were still absent from the file 3s later.
785
- * Nothing in Descry can see those bytes; a boundary to state, not a gap to close. */
786
- const ATTACH_BUFFERING_NOTE = "Attaching reads a file the target writes; it can only see what the target has already flushed " +
787
- "there. A process whose output is redirected to a file is usually block-buffered rather than " +
788
- "line-buffered — its own runtime holds whole lines in a userspace buffer, invisible from outside, " +
789
- "until the buffer fills or the process flushes. Output produced during this window may therefore " +
790
- "arrive in the file after the window closed and be absent here, which is a fact about the " +
791
- "target's buffering and not evidence that it did nothing. Run the target with its output " +
792
- "unbuffered or line-buffered if the timing matters.";
793
- /**
794
- * RT-024, restated where a caller can actually see it.
795
- *
796
- * `attachToRunningProcess` (descry-runtime) already discloses this in its own
797
- * source comment — `isProcessAlive` confirms *a* process with this pid
798
- * exists, not that it is the one that wrote `logFilePath`, because pids are
799
- * reused by the OS and nothing in a bare pid or path proves provenance. That
800
- * comment reaches nobody calling this tool. A real run hit exactly the gap it
801
- * describes: an unrelated orphaned process from an earlier failed boot wrote
802
- * a crash trace into the same log path a live attach target used, and it was
803
- * attributed to the live run with no indication anything could be wrong.
804
- */
805
- const ATTACH_IDENTITY_NOTE = "Attaching verifies the declared pid is alive and reads the declared log file; it does not verify " +
806
- "the lines in that file actually came from that pid. A pid can be reused by the OS, and nothing in " +
807
- "a bare pid or file path proves which process is writing to it — an unrelated process writing to " +
808
- "the same path would be attributed to this run with no way to tell the two apart from here.";
809
- /** Every argument this tool takes (readArguments, below), extracted so describeAction can
810
- * call it too — catches a bad argument before minting a token for a run that could never
811
- * happen (DEC-387). Touches nothing; exported for its own test. */
812
- /** A-F5: ResourceLimits, straight through to ExecutionConfiguration. applyResourceLimits
813
- * already refuses rather than silently running unconstrained; this only reads the shape. */
814
- function readResourceLimits(args) {
815
- const raw = args["resourceLimits"];
816
- if (raw === undefined || raw === null)
817
- return undefined;
818
- const record = asRecord(raw, "resourceLimits");
819
- const maxMemoryBytes = optionalInteger(record, "maxMemoryBytes", 1);
820
- const maxCpuSeconds = optionalInteger(record, "maxCpuSeconds", 1);
821
- const maxProcesses = optionalInteger(record, "maxProcesses", 1);
822
- return {
823
- ...(maxMemoryBytes === undefined ? {} : { maxMemoryBytes }),
824
- ...(maxCpuSeconds === undefined ? {} : { maxCpuSeconds }),
825
- ...(maxProcesses === undefined ? {} : { maxProcesses }),
826
- };
827
- }
828
- /** A-F5: FilesystemPolicy. allowedRoots is required whenever filesystemPolicy is present —
829
- * an empty-roots policy silently means "cwd only", and that must be stated, not defaulted into. */
830
- function readFilesystemPolicy(args) {
831
- const raw = args["filesystemPolicy"];
832
- if (raw === undefined || raw === null)
833
- return undefined;
834
- const record = asRecord(raw, "filesystemPolicy");
835
- const allowedRoots = record["allowedRoots"];
836
- if (!Array.isArray(allowedRoots) || allowedRoots.some((r) => typeof r !== "string")) {
837
- throw new ToolInputError('"filesystemPolicy.allowedRoots" must be an array of strings');
838
- }
839
- return { allowedRoots: allowedRoots };
840
- }
841
- /** A-F5: NetworkPolicy. Only `{mode:"allow", hosts:[]}` (full denial) is actually enforced
842
- * by applySandbox today; any other shape is accepted here and refused downstream by the
843
- * mechanism itself (execution.validationError), never pre-judged here (rule 1). */
844
- function readNetworkPolicy(args) {
845
- const raw = args["networkPolicy"];
846
- if (raw === undefined || raw === null)
847
- return undefined;
848
- const record = asRecord(raw, "networkPolicy");
849
- const mode = optionalEnum(record, "mode", ["allow", "deny"]);
850
- if (mode === undefined)
851
- throw new ToolInputError('"networkPolicy.mode" is required');
852
- const hosts = record["hosts"];
853
- if (!Array.isArray(hosts) || hosts.some((h) => typeof h !== "string")) {
854
- throw new ToolInputError('"networkPolicy.hosts" must be an array of strings');
855
- }
856
- return { mode, hosts: hosts };
857
- }
858
- function readSandboxBackend(args) {
859
- return optionalEnum(args, "sandboxBackend", ["bwrap", "container"]);
860
- }
861
- function readJourney(args) {
862
- const raw = args["journey"];
863
- if (raw === undefined || raw === null)
864
- return undefined;
865
- const record = asRecord(raw, "journey");
866
- const service = optionalString(record, "service");
867
- const scenarioName = optionalString(record, "scenario");
868
- const startUrl = optionalString(record, "startUrl");
869
- if ((scenarioName === undefined) === (startUrl === undefined)) {
870
- throw new ToolInputError('journey must declare exactly one of "scenario" or "startUrl" — ' +
871
- (scenarioName === undefined ? "it declares neither" : "it declares both"));
872
- }
873
- const recordVideo = optionalBoolean(record, "recordVideo");
874
- const headless = optionalBoolean(record, "headless");
875
- const settleForMs = optionalInteger(record, "settleForMs", 0);
876
- return { service, scenarioName, startUrl, recordVideo, headless, settleForMs };
877
- }
878
- function readDatabaseChannel(args) {
879
- const raw = args["database"];
880
- if (raw === undefined || raw === null)
881
- return undefined;
882
- const record = asRecord(raw, "database");
883
- const statementLogRaw = record["statementLog"];
884
- const statementLogRecord = asRecord(statementLogRaw, "database.statementLog");
885
- const kind = statementLogRecord["kind"];
886
- const service = optionalString(record, "service");
887
- if (kind === "file") {
888
- const path = statementLogRecord["path"];
889
- if (typeof path !== "string" || path === "") {
890
- throw new ToolInputError('"database.statementLog.path" is required and must be a non-empty string for kind "file"');
891
- }
892
- // A-F6: same narrower, name-based check `attach.logFilePath` uses — a real statement log
893
- // lives anywhere and can't be root-confined.
894
- const sensitivity = checkNotSensitivePath(path, ALLOW_SENSITIVE_LOG_PATH_ENV_VAR);
895
- if (sensitivity.sensitive)
896
- throw new ToolInputError(`"database.statementLog.path": ${sensitivity.reason}`);
897
- return { statementLog: { kind: "file", path }, service };
898
- }
899
- if (kind === "dockerContainer") {
900
- const container = statementLogRecord["container"];
901
- if (typeof container !== "string" || container === "") {
902
- throw new ToolInputError('"database.statementLog.container" is required and must be a non-empty string for kind "dockerContainer"');
903
- }
904
- return { statementLog: { kind: "dockerContainer", container }, service };
905
- }
906
- throw new ToolInputError('"database.statementLog.kind" must be "file" or "dockerContainer"');
907
- }
908
- export function readArguments(args, repoPath, savedStart) {
909
- const profile = readProfile(args);
910
- const adapterSpec = readAdapterSpec(args);
911
- const { declared, usedSaved: usedSavedServices, missingSavedEnv } = readServices(args, repoPath, savedStart);
912
- const observeForMs = optionalInteger(args, "observeForMs", 1) ?? DEFAULT_OBSERVE_MS;
913
- const timeoutMs = optionalInteger(args, "timeoutMs", 1) ?? DEFAULT_TIMEOUT_MS;
914
- const fidelityRaw = optionalInteger(args, "fidelityLevel", 1) ?? 2;
915
- if (fidelityRaw > 4)
916
- throw new ToolInputError('"fidelityLevel" must be 1, 2, 3 or 4');
917
- const environmentTier = optionalString(args, "environmentTier") ?? "tier-2-container";
918
- const evidenceArg = optionalString(args, "evidencePath");
919
- const evidenceCandidate = evidenceArg === undefined
920
- ? join(repoPath, DEFAULT_EVIDENCE_RELATIVE_PATH)
921
- : isAbsolute(evidenceArg)
922
- ? evidenceArg
923
- : join(repoPath, evidenceArg);
924
- // A-F6: join() doesn't stop ".." escaping repoPath — confined by default (path-confinement.ts).
925
- const confinement = checkPathConfinement({
926
- candidate: evidenceCandidate,
927
- allowedRoots: [repoPath],
928
- envVar: ALLOW_EVIDENCE_PATH_OUTSIDE_REPO_ENV_VAR,
929
- what: "evidencePath",
930
- });
931
- if (!confinement.allowed)
932
- throw new ToolInputError(confinement.reason);
933
- const evidencePath = confinement.resolved;
934
- const resourceLimits = readResourceLimits(args);
935
- const filesystemPolicy = readFilesystemPolicy(args);
936
- const networkPolicy = readNetworkPolicy(args);
937
- const sandboxBackend = readSandboxBackend(args);
938
- const journey = readJourney(args);
939
- const database = readDatabaseChannel(args);
940
- return {
941
- profile,
942
- adapterSpec,
943
- declared,
944
- usedSavedServices,
945
- missingSavedEnv,
946
- observeForMs,
947
- timeoutMs,
948
- fidelityRaw,
949
- environmentTier,
950
- evidencePath,
951
- resourceLimits,
952
- filesystemPolicy,
953
- networkPolicy,
954
- sandboxBackend,
955
- journey,
956
- database,
957
- };
958
- }
959
- /**
960
- * RG-5(a) — every service this tool spawns gets whatever database-observation
961
- * launch its own adapter declares, injected automatically with no caller
962
- * declaration needed. Reads `RuntimeAdapter.databaseObservationLaunch()`
963
- * (`@descryy/runtime-backend-observation`) — the same language-neutral seam
964
- * `outboundHttpLaunch()` already established for outbound-fetch
965
- * instrumentation — rather than comparing `adapter.language` against a name:
966
- * this file sits above the Canonical IR, where deciding behaviour from which
967
- * language an adapter is for is the exact leak architecture principle 7
968
- * forbids (enforced here by `eslint-plugin-descry-boundary`'s
969
- * `no-language-vocabulary`, which is what caught the first draft of this
970
- * function). The per-language answer — Node gets `node:sqlite`'s preload,
971
- * most runtimes get nothing yet — lives in each adapter package instead.
972
- *
973
- * Attach-mode services (no `command`) are untouched — Descry did not spawn
974
- * them, so there is no command line to inject into.
975
- */
976
- function withDatabaseObservationPreload(service, adapter) {
977
- if (service.command === undefined)
978
- return service;
979
- const launch = adapter.databaseObservationLaunch?.();
980
- if (launch === undefined || launch === null)
981
- return service;
982
- return {
983
- ...service,
984
- ...(launch.interpreterArgs === undefined ? {} : { interpreterArgs: [...(service.interpreterArgs ?? []), ...launch.interpreterArgs] }),
985
- ...(launch.env === undefined ? {} : { env: { ...service.env, ...launch.env } }),
986
- };
987
- }
988
- /**
989
- * RG-5(a) read-back half — desktop's `observeDatabaseQueries`, same shape:
990
- * replays each service's own already-captured output (the preload's
991
- * `DESCRY_DB_QUERY` marker lines) through `createDatabaseQueryCollector` and
992
- * writes real `DATABASE_QUERY` evidence into this run's store. Returns how
993
- * many records it wrote, so the caller can fold it into one disclosed count
994
- * alongside the Postgres channel's.
995
- */
996
- async function observeSqliteQueries(store, executionId, configuration, execution) {
997
- const serviceOutput = capturedServiceOutput(execution);
998
- if (serviceOutput === null)
999
- return 0;
1000
- let written = 0;
1001
- await Promise.all(Object.entries(serviceOutput).map(async ([serviceName, output]) => {
1002
- const source = {
1003
- processId: `${executionId}:${serviceName}`,
1004
- serviceName,
1005
- lines: (async function* () {
1006
- for (const text of output.split("\n")) {
1007
- if (text.length === 0)
1008
- continue;
1009
- yield { text, stream: "combined", observedAt: new Date().toISOString() };
1010
- }
1011
- })(),
1012
- };
1013
- const context = {
1014
- executionId,
1015
- configuration,
1016
- emit: (evidenceInput) => {
1017
- store.write({ ...evidenceInput, executionId });
1018
- written += 1;
1019
- },
1020
- };
1021
- const collector = createDatabaseQueryCollector({ source, service: serviceName });
1022
- await collector.start(context);
1023
- await collector.stop();
1024
- }));
1025
- return written;
1026
- }
1027
- /**
1028
- * RG-5(c) — the Postgres statement-log channel's own start/stop pair, same
1029
- * design as the desktop's `beginPostgresChannel`: started before
1030
- * `runInstrumentedExecution` (the server keeps running independently of
1031
- * whatever this tool spawns or tears down, so the only way to capture
1032
- * queries issued *during* the observation window is to already be tailing
1033
- * when they happen), buffered until a real `executionId` exists to attribute
1034
- * it to, and idempotent to `stop()` because the caller's own `finally` may
1035
- * call it a second time after an already-successful stop.
1036
- */
1037
- function beginPostgresChannel(database, processId) {
1038
- if (database === undefined)
1039
- return { kind: "not-declared" };
1040
- const captured = [];
1041
- const controller = new AbortController();
1042
- const serviceName = database.service ?? "postgres";
1043
- const source = createPostgresLogSource(database.statementLog, processId, serviceName, { signal: controller.signal });
1044
- const collector = createPostgresQueryCollector({ source, service: serviceName });
1045
- const context = {
1046
- executionId: processId,
1047
- configuration: { environmentTier: "tier-2-container", fidelityLevel: 2, timeoutMs: 0, services: {} },
1048
- emit: (evidence) => {
1049
- captured.push(evidence);
1050
- },
1051
- };
1052
- const started = collector.start(context);
1053
- let stopped;
1054
- return {
1055
- kind: "observing",
1056
- stop: () => {
1057
- if (stopped === undefined) {
1058
- stopped = (async () => {
1059
- await started;
1060
- controller.abort();
1061
- await collector.stop();
1062
- return captured;
1063
- })();
1064
- }
1065
- return stopped;
1066
- },
1067
- };
1068
- }
1069
- /** Exported so `observe.ts` can dispatch the "runtime" verb to this exact body — the merge
1070
- * reuses this function rather than reimplementing it (mcp-surface-consolidation.md). */
1071
- export async function run(args, ctx) {
1072
- const session = ctx.session;
1073
- const { profile, adapterSpec, declared, usedSavedServices, missingSavedEnv, observeForMs, timeoutMs, fidelityRaw, environmentTier, evidencePath, resourceLimits, filesystemPolicy, networkPolicy, sandboxBackend, journey, database, } = readArguments(args, session.repoPath, session.config.savedStart);
1074
- const notes = [...STANDING_NOTES, sandboxDisclosure({ resourceLimits, filesystemPolicy, networkPolicy, sandboxBackend })];
1075
- // RG-5(c). Stated on every call, success or refusal, same reasoning as the
1076
- // sandbox disclosure above: a caller unaware the Postgres channel needs an
1077
- // explicit statement-log source would misread a "channel absent" run as
1078
- // "this application makes no database calls."
1079
- notes.push(database === undefined
1080
- ? "database channel: not observed — no statement log configured. Declare \"database.statementLog\" " +
1081
- "(a file path, or a Docker container's logs) to capture real Postgres queries issued during this " +
1082
- "run. Node services using node:sqlite are observed automatically, with no declaration needed."
1083
- : `database channel: tailing the Postgres statement log via ${database.statementLog.kind === "file" ? `file "${database.statementLog.path}"` : `docker container "${database.statementLog.container}"`}.`);
1084
- // Added on refusal paths too — telling the caller only on success means telling them
1085
- // after the run whose result it would have explained (same reasoning as the line above).
1086
- if (usedSavedServices) {
1087
- const savedAt = session.config.savedStart?.savedAt ?? "an earlier run";
1088
- notes.push(`No "services" was supplied on this call, so it reused the start settings a prior successful run ` +
1089
- `saved (${savedAt}): ${describeSavedCommands(declared)}. Declare "services" explicitly to override ` +
1090
- "them — only another run that reaches readiness replaces what is saved.");
1091
- if (missingSavedEnv.length > 0) {
1092
- notes.push(`These environment variables were declared on the run that saved these settings, but nothing is ` +
1093
- `set under their names in this process's environment now, so they were not passed this time: ` +
1094
- `${missingSavedEnv.join(", ")}. Only variable names are ever saved, never their values.`);
1095
- }
1096
- }
1097
- if (declared.some((service) => service.attached)) {
1098
- notes.push(ATTACH_BUFFERING_NOTE, ATTACH_IDENTITY_NOTE);
1099
- }
1100
- const base = session.provider().baseStamp();
1101
- const refuse = (headline, data = {}, extraNotes = []) => answer({
1102
- headline,
1103
- state: "refused",
1104
- nameLevel: true,
1105
- // Nothing ran, so nothing was resolved — never leak the tier a successful path would reach.
1106
- resolutionFloor: 0,
1107
- commitSha: base.commitSha,
1108
- graphBuiltAt: base.graphBuiltAt,
1109
- irSchemaVersion: base.irSchemaVersion,
1110
- commitSpread: base.commitSpread,
1111
- repoCount: base.repoCount,
1112
- notes: [...notes, ...extraNotes],
1113
- data: {
1114
- executionId: null,
1115
- executionState: null,
1116
- adapterLanguage: null,
1117
- evidencePath,
1118
- services: [],
1119
- evidenceByType: {},
1120
- correlation: null,
1121
- wrote: EMPTY_WRITE,
1122
- journey: null,
1123
- database: null,
1124
- ...data,
1125
- },
1126
- });
1127
- // Spawning is a write against the target; attaching is not (Descry never executes code in,
1128
- // signals, or limits a process it didn't spawn). The profile's declared level decides, never its name.
1129
- const spawns = declared.some((service) => !service.attached);
1130
- const action = { write: spawns, destructive: false };
1131
- const decision = evaluateAction(profile, action);
1132
- if (decision !== "allow") {
1133
- return refuse(`Profile "${profile.name}" declares safetyLevel "${profile.safetyLevel}", which does not permit ` +
1134
- `${spawns ? "spawning a service" : "this run"}. Nothing was started and nothing was written. ` +
1135
- (spawns
1136
- ? "A run in which every service uses \"attach\" spawns nothing and is permitted under readOnly."
1137
- : ""));
1138
- }
1139
- if (!spawns) {
1140
- notes.push("Every declared service is attached to rather than spawned, so this run started nothing and " +
1141
- "applied no resource, filesystem or network policy to any process — Descry does not constrain " +
1142
- "a process it did not spawn.");
1143
- }
1144
- // Correlation resolves evidence against this graph; an empty graph resolving nothing must
1145
- // not be reported as a clean run with no findings — the "empty means broken" collapse.
1146
- const driver = session.store().driver;
1147
- const stored = counts(driver);
1148
- if (stored.nodes === 0) {
1149
- return refuse("This repository has no graph yet, so there is nothing for a run's evidence to be resolved " +
1150
- "against. Run analyze first — an observation that cannot name a node cannot become a fact.");
1151
- }
1152
- // --- the adapter ----------------------------------------------------------
1153
- ctx.progress(`Loading runtime adapter ${adapterSpec.module}`);
1154
- let adapter;
1155
- try {
1156
- adapter = await loadRuntimeAdapter(adapterSpec, session.repoPath);
1157
- }
1158
- catch (error) {
1159
- if (error instanceof RuntimeAdapterLoadError) {
1160
- notes.push("This server depends on none of descry-runtime's language adapters by design, so the adapter " +
1161
- "must be installed alongside it and named in the call. Nothing was started.");
1162
- return refuse(error.message);
1163
- }
1164
- throw error;
1165
- }
1166
- notes.push(`Observed with the runtime adapter for "${adapter.language}", loaded from ${adapterSpec.module}.`);
1167
- const root = await session.root();
1168
- const scopes = readScopes(args, declared, {
1169
- repo: root.repo,
1170
- repoRoot: root.absolutePath,
1171
- });
1172
- const services = {};
1173
- const readiness = {};
1174
- for (const service of declared) {
1175
- services[service.name] = withDatabaseObservationPreload(service.configuration, adapter);
1176
- readiness[service.name] = service.readiness;
1177
- }
1178
- const configuration = {
1179
- environmentTier: environmentTier,
1180
- fidelityLevel: fidelityRaw,
1181
- timeoutMs,
1182
- services,
1183
- // A-F5: wired through to the controller's real bwrap/container isolation
1184
- // — see sandboxDisclosure() above for what this reply says about it.
1185
- ...(resourceLimits === undefined ? {} : { resourceLimits }),
1186
- ...(filesystemPolicy === undefined ? {} : { filesystemPolicy }),
1187
- ...(networkPolicy === undefined ? {} : { networkPolicy }),
1188
- ...(sandboxBackend === undefined ? {} : { sandboxBackend }),
1189
- };
1190
- // Before anything is spawned — a caller who's already gone would leave a real process
1191
- // running with nothing left to ever stop it.
1192
- if (ctx.signal.aborted) {
1193
- return refuse(cancellationHeadline("no service was started") + " " + cancellationNotes("Nothing was spawned.")[0]);
1194
- }
1195
- // RG-4/RG-6 — resolve the declared journey into a drive-ready plan before anything is
1196
- // spawned. A scenario that cannot be read or projected (e.g. an unresolvable secret
1197
- // reference) degrades this run to backend-only with a named disclosure; it never fails
1198
- // the whole call, because the journey is additive to what observe_runtime already does.
1199
- let journeyPlan = null;
1200
- if (journey !== undefined) {
1201
- if (journey.scenarioName !== undefined) {
1202
- const stored = await readScenario(session.repoPath, journey.scenarioName);
1203
- if (!stored.valid) {
1204
- notes.push(`journey.scenario "${journey.scenarioName}" could not be read, so this run is backend-only: ` +
1205
- stored.problems.join(" "));
1206
- }
1207
- else {
1208
- const projected = projectScenarioToRunPlan(stored.scenario);
1209
- if (!projected.ok) {
1210
- notes.push(`journey.scenario "${journey.scenarioName}" could not be driven, so this run is backend-only: ` +
1211
- `${projected.reason} (verb "${projected.verb}")`);
1212
- }
1213
- else {
1214
- const MUTATING_KINDS = new Set(["click", "type", "select", "submit"]);
1215
- const mutates = projected.plan.steps.some((s) => MUTATING_KINDS.has(s.kind));
1216
- const readOnly = profile.safetyLevel === "readOnly";
1217
- const steps = mutates && readOnly ? projected.plan.steps.filter((s) => !MUTATING_KINDS.has(s.kind)) : projected.plan.steps;
1218
- if (mutates && readOnly) {
1219
- notes.push(`journey.scenario "${journey.scenarioName}" declares steps that would change the application ` +
1220
- `(click/type/select/submit), and profile "${profile.name}" declares safetyLevel "readOnly" — ` +
1221
- "those steps were not driven. The scenario's own navigation and any assertions still ran and were observed.");
1222
- }
1223
- journeyPlan = {
1224
- startUrl: projected.plan.startUrl,
1225
- steps,
1226
- ...(journey.service === undefined ? {} : { service: journey.service }),
1227
- ...(journey.recordVideo === undefined ? {} : { recordVideo: journey.recordVideo }),
1228
- ...(journey.headless === undefined ? {} : { headless: journey.headless }),
1229
- ...(journey.settleForMs === undefined ? {} : { settleForMs: journey.settleForMs }),
1230
- };
1231
- }
1232
- }
1233
- }
1234
- else if (journey.startUrl !== undefined) {
1235
- journeyPlan = {
1236
- startUrl: journey.startUrl,
1237
- ...(journey.service === undefined ? {} : { service: journey.service }),
1238
- ...(journey.recordVideo === undefined ? {} : { recordVideo: journey.recordVideo }),
1239
- ...(journey.headless === undefined ? {} : { headless: journey.headless }),
1240
- ...(journey.settleForMs === undefined ? {} : { settleForMs: journey.settleForMs }),
1241
- };
1242
- }
1243
- }
1244
- await mkdir(dirname(evidencePath), { recursive: true });
1245
- const evidenceStore = new EvidenceStore({ path: evidencePath });
1246
- /** Processes **this run started**, and only those — an attach-mode service is the
1247
- * developer's own process; killing it on cancellation would destroy something this
1248
- * call never created. Only the spawn path is cleaned up. */
1249
- const attached = new Set(declared.filter((service) => service.attached).map((service) => service.name));
1250
- const spawnedPids = new Set();
1251
- const abort = whenAborted(ctx.signal);
1252
- void abort.promise.then(() => {
1253
- for (const pid of spawnedPids)
1254
- terminateSpawnedProcess(pid);
1255
- });
1256
- // Set inside onReady below, read after the execution returns — the drive is the only
1257
- // thing that can report what it did, and it runs deep inside the orchestrator's own
1258
- // callback. `onReady`'s own contract says a throwing callback is NOT caught by the
1259
- // orchestrator — it would reject the whole execution and discard every piece of backend
1260
- // evidence already collected — so the drive itself never throws and this is a second belt.
1261
- let journeyOutcome;
1262
- // RG-5(c). Started before `runInstrumentedExecution`, not after: a Postgres
1263
- // statement log is the server's own, and the server keeps running
1264
- // independently of whatever this run spawns or tears down — the only way
1265
- // to capture queries issued *during* this run's observation window is to
1266
- // already be tailing when they happen (same reasoning as the journey drive
1267
- // being wired through `onReady` rather than run after the fact).
1268
- const postgresChannel = beginPostgresChannel(database, `postgres:${root.repo}`);
1269
- try {
1270
- ctx.progress(`Running ${declared.length} service(s), observing for ${String(observeForMs)}ms`);
1271
- const execution = await runInstrumentedExecution({
1272
- execution: {
1273
- application: root.repo,
1274
- repository: root.repo,
1275
- commit: root.commitSha,
1276
- configuration,
1277
- },
1278
- runOptions: {
1279
- readiness,
1280
- // The only channel naming a process while still alive — Execution.processes is
1281
- // complete only once the whole window has already slept, past where a cancel lands.
1282
- onProcessLifecycleEvent: (event) => {
1283
- if (event.kind !== "process-started" || attached.has(event.serviceName))
1284
- return;
1285
- if (event.handle.pid !== null)
1286
- spawnedPids.add(event.handle.pid);
1287
- },
1288
- },
1289
- adapter,
1290
- store: evidenceStore,
1291
- observeForMs,
1292
- // RG-4 — the one moment in a run when every service's readiness has already
1293
- // succeeded and nothing has been torn down yet. Browser evidence therefore lands
1294
- // in the same store under the same executionId as the backend's — the desktop
1295
- // composes identically (packages/session/src/pipeline/runtime-execution.ts).
1296
- ...(journeyPlan === null
1297
- ? {}
1298
- : {
1299
- onReady: async ({ execution: readyExecution }) => {
1300
- try {
1301
- const drive = createRuntimeJourneyDrive(journeyPlan);
1302
- journeyOutcome = await drive({
1303
- store: evidenceStore,
1304
- executionId: readyExecution.executionId,
1305
- configuration,
1306
- resolveSourceRoot: createSourceRootResolver(readyExecution.processes, configuration),
1307
- stackTraceParser: adapter.stackTraceParser,
1308
- });
1309
- }
1310
- catch (error) {
1311
- const cause = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
1312
- evidenceStore.write({
1313
- executionId: readyExecution.executionId,
1314
- timestamp: new Date().toISOString(),
1315
- source: "harness",
1316
- service: journeyPlan?.service ?? null,
1317
- process: null,
1318
- eventType: "COLLECTOR_ERROR",
1319
- payload: { raw: `the journey drive threw instead of degrading: ${cause}`, error: cause, collectorId: "runtime-journey-drive" },
1320
- traceId: null,
1321
- requestId: null,
1322
- correlationId: null,
1323
- graphNodeId: null,
1324
- sourceLocation: null,
1325
- stackTrace: null,
1326
- confidence: 1,
1327
- collectorVersion: "descry-mcp-observe-runtime@1",
1328
- });
1329
- }
1330
- },
1331
- }),
1332
- });
1333
- // The kill above doesn't shorten the window — this is where a cancelled run actually
1334
- // stops. Nothing is correlated or written: an R4 edge can't be re-derived and checked
1335
- // later, so minting one from a run nobody watched to the end would be unquestionable.
1336
- // RG-5(c): the postgres tail is stopped here too (releasing its child process) but its
1337
- // capture is discarded, same "nothing correlated or written" rule as everything else.
1338
- if (ctx.signal.aborted) {
1339
- if (postgresChannel.kind === "observing")
1340
- await postgresChannel.stop().catch(() => undefined);
1341
- return refuse(cancellationHeadline("the observed application was stopped"), {
1342
- executionId: execution.execution.executionId,
1343
- executionState: execution.execution.state,
1344
- adapterLanguage: adapter.language,
1345
- services: describeServices(execution.execution.processes, declared),
1346
- evidenceByType: tally(execution.evidence),
1347
- journey: journeyOutcome ?? null,
1348
- }, cancellationNotes(`Every service this run spawned was killed; ${String(attached.size)} attached service(s) were ` +
1349
- "left alone, because this call did not start them.", "No evidence was correlated and no edge was written. Evidence already collected is still on disk " +
1350
- `at ${evidencePath}, so nothing witnessed was thrown away.`));
1351
- }
1352
- // RG-5(a)/(c) — stop the postgres tail now that the observation window has closed and
1353
- // replay the sqlite preload's captured output, attributing both to this run's real
1354
- // executionId (unknown until now, which is why the postgres collector above buffered
1355
- // rather than wrote directly). Must happen before correlation, same ordering the desktop
1356
- // uses, so a database-node resolver can attribute what is already in the store.
1357
- const executionId = execution.execution.executionId;
1358
- let databaseRecordCount = 0;
1359
- if (postgresChannel.kind === "observing") {
1360
- const capturedPostgres = await postgresChannel.stop();
1361
- for (const evidence of capturedPostgres) {
1362
- evidenceStore.write({ ...evidence, executionId });
1363
- databaseRecordCount += 1;
1364
- }
1365
- }
1366
- databaseRecordCount += await observeSqliteQueries(evidenceStore, executionId, configuration, execution);
1367
- const databaseObservation = database === undefined
1368
- ? {
1369
- observed: false,
1370
- disclosure: "database channel: not observed — no statement log configured",
1371
- recordCount: databaseRecordCount,
1372
- }
1373
- : { observed: true, disclosure: null, recordCount: databaseRecordCount };
1374
- const observed = describeServices(execution.execution.processes, declared);
1375
- const evidenceByType = tally(execution.evidence);
1376
- // RG-4 — `execution.evidence` is `runInstrumentedExecution`'s own returned snapshot and
1377
- // never includes what the journey drive wrote directly into `evidenceStore` inside
1378
- // `onReady` (it isn't tracked by the orchestrator's own `emitted` array). Re-read from
1379
- // the store, scoped to this executionId, only when a journey actually ran — so the
1380
- // headline/notes below (which read `execution.evidence` directly, unchanged) still pin
1381
- // exactly what they always have on every backend-only run.
1382
- // RG-5(c): also re-read when the database channel wrote anything — same reasoning as the
1383
- // journey case, a different write path `execution.evidence` was already materialised
1384
- // before this run's own DATABASE_QUERY records ever reached the store.
1385
- const evidenceIncludingJourney = journeyOutcome === undefined && databaseRecordCount === 0
1386
- ? execution.evidence
1387
- : [...evidenceStore.getByExecution(execution.execution.executionId)];
1388
- if (execution.validationError !== null) {
1389
- // Refused before spawning — a fact about the declaration, not the application;
1390
- // emphatically not "the service is clean".
1391
- return refuse(`The execution refused to start: ${execution.validationError}. Nothing was spawned, no ` +
1392
- "evidence was collected, and no graph edge was written.", {
1393
- executionState: execution.execution.state,
1394
- adapterLanguage: adapter.language,
1395
- database: databaseObservation,
1396
- services: observed,
1397
- evidenceByType,
1398
- }, usedSavedServices
1399
- ? [
1400
- `These start settings came from a prior successful run — "services" was omitted on this ` +
1401
- `call — and they no longer work: ${describeSavedCommands(declared)}. Call observe_runtime ` +
1402
- 'again with "services" declared explicitly to replace what is saved; only another run ' +
1403
- "that reaches readiness updates it.",
1404
- ]
1405
- : []);
1406
- }
1407
- // See deriveRunOutcome for the full rule. Budget-expired case measured pre-clamp: a 1.5s
1408
- // budget over a 12s window returned at 12.3s with zero evidence (DEC-NEXT-observe-runtime-
1409
- // timeout-budget-is-not-enforced.md). Reported `timed_out`, not `ok` (§6) — real evidence,
1410
- // just less of it. A post-spawn readiness failure (FAILED_START) and any other non-success
1411
- // terminal state are reported `failed`, never `ok`, whatever evidence happens to exist.
1412
- const outcome = deriveRunOutcome(execution.execution.state);
1413
- const { failed, timedOut } = outcome;
1414
- if (timedOut) {
1415
- notes.push(`This run hit its ${String(timeoutMs)}ms whole-execution budget before the ` +
1416
- `${String(observeForMs)}ms observation window closed, so every service was stopped early and ` +
1417
- "the evidence below is partial — every count is a floor, not a total. Nothing here says the " +
1418
- "application produced no further output; only that this run stopped listening for it. Raise " +
1419
- '"timeoutMs" clear of the observation window, or shorten "observeForMs".');
1420
- }
1421
- const capturedOutput = capturedServiceOutput(execution);
1422
- // Only on failure: a healthy boot log is large and would crowd out every
1423
- // other field, and nobody is asking why a service that worked worked.
1424
- const services = failed ? describeServices(execution.execution.processes, declared, capturedOutput ?? {}) : observed;
1425
- if (failed) {
1426
- const outputNote = startupOutputNote(services, capturedOutput);
1427
- if (outputNote !== null)
1428
- notes.push(outputNote);
1429
- notes.push(`This run's execution ended in state "${execution.execution.state}", not "COMPLETED" — the ` +
1430
- "application did not finish booting successfully. Any evidence below may be partial, or " +
1431
- "left over from earlier in this same run, and does not mean the service ever became ready.");
1432
- if (usedSavedServices) {
1433
- notes.push(`These start settings came from a prior successful run — "services" was omitted on this call ` +
1434
- `— and they did not work this time: ${describeSavedCommands(declared)}. Call observe_runtime ` +
1435
- 'again with "services" declared explicitly to replace what is saved.');
1436
- }
1437
- }
1438
- ctx.progress(`Correlating ${String(execution.evidence.length)} evidence item(s) against the graph`);
1439
- const pass = correlateExecution({
1440
- store: evidenceStore,
1441
- driver,
1442
- executionId: execution.execution.executionId,
1443
- scopeByService: scopes,
1444
- });
1445
- const correlation = {
1446
- considered: pass.considered,
1447
- skipped: pass.skipped,
1448
- attributed: pass.attributed.length,
1449
- refused: pass.refusals.length,
1450
- unscopedServices: pass.unscopedServices,
1451
- harnessErrors: pass.harnessErrors.map((e) => `${e.detail} (${String(e.occurrences)}×)`),
1452
- };
1453
- if (pass.unscopedServices.length > 0) {
1454
- // Two sentences, not one: a named service with no scope is fixable by the caller; the
1455
- // "(no service)" sentinel is not — no collector stamps one, so there's no key to supply.
1456
- const named = pass.unscopedServices.filter((s) => s !== "(no service)");
1457
- const anonymous = pass.unscopedServices.length - named.length;
1458
- if (named.length > 0) {
1459
- notes.push(`Symbol evidence from ${named.join(", ")} was left unresolved: no scope named which ` +
1460
- "repository those symbols belong to, and resolving them against a repository nobody " +
1461
- "named would resolve the wrong one's identically-named file. Supply " +
1462
- '"scopeByService" for those services to have them resolved.');
1463
- }
1464
- if (anonymous > 0) {
1465
- notes.push("Some evidence carried a source location but no service name, so no scope could be " +
1466
- "looked up for it and its symbols were never resolved. This is not a missing argument: " +
1467
- "nothing in this server's runtime closure stamps a service name onto collector " +
1468
- "evidence, so there is no key a caller could supply a scope under. The items are " +
1469
- "counted as skipped rather than dropped, and what they would have resolved to is " +
1470
- "unknown rather than absent.");
1471
- }
1472
- }
1473
- if (pass.harnessErrors.length > 0) {
1474
- notes.push(`${String(pass.harnessErrors.length)} correlation failure(s) were the machinery breaking rather ` +
1475
- "than a resolver honestly declining — each is written into the evidence stream as a " +
1476
- "COLLECTOR_ERROR, and the counts below are correspondingly incomplete.");
1477
- }
1478
- // Recorded before the edge write so a failure in one doesn't silently cost the other.
1479
- // See runtime-incident.ts for why an EXCEPTION alone is not an incident.
1480
- const incident = runtimeObservedIncident({
1481
- repo: root.repo,
1482
- repoRoot: root.absolutePath,
1483
- runId: execution.execution.executionId,
1484
- services,
1485
- exceptionLocations: execution.evidence
1486
- .filter((e) => e.eventType === "EXCEPTION")
1487
- .map((e) => ({ file: e.sourceLocation?.file ?? null })),
1488
- exceptionTexts: execution.evidence
1489
- .filter((e) => e.eventType === "EXCEPTION")
1490
- .map((e) => (typeof e.payload === "string" ? e.payload : JSON.stringify(e.payload)))
1491
- .map((text) => text.split("\n")[0] ?? "")
1492
- .filter((line) => line !== ""),
1493
- });
1494
- if (incident !== null) {
1495
- ctx.progress("Recording the observed failure as an incident");
1496
- // `written.records` — the already-deduped list on file, not
1497
- // `session.config.confirmedIncidents` plus `incident` — the same
1498
- // resubmission hazard `mark_incident` has (UAT phase 5, F9): a run
1499
- // observing the identical failure twice must merge, not mint a second
1500
- // `INCIDENT` node sharing the first's id.
1501
- const written = await writeConfirmedIncident(session.repoPath, incident);
1502
- const projected = await createConfirmedIncidentSource({
1503
- repo: root.repo,
1504
- incidents: written.records,
1505
- }).emit({ root });
1506
- const built = buildGraph([projected], {
1507
- nodeExists: (id) => session.provider().node(id) !== undefined,
1508
- });
1509
- persistGraph(driver, [projected], built);
1510
- // Same fix as mark_incident's write path: this source's output is a
1511
- // small, fully-specified list every call, so a deleted incident whose
1512
- // files went with it is never caught by the shared eviction's file
1513
- // overlap check.
1514
- evictStaleConfirmedIncidentNodes(driver, projected.producedBy, new Set(built.nodes.filter((n) => n.producedBy === projected.producedBy).map((n) => n.id)));
1515
- notes.push(`A service died during this run (${incident.summary}), so it was recorded as an incident ` +
1516
- `correlated to ${String(incident.files.length)} file(s) this repository owns, and written ` +
1517
- "durably to .descry/config.json — a run is gone once the process exits. The correlation " +
1518
- "is every file an exception stack named during the run, which is not a claim about the " +
1519
- "cause: nothing here knows which exception killed the process, and choosing the last one " +
1520
- "would be recency standing in for causality.");
1521
- }
1522
- ctx.progress("Writing observed edges into the graph");
1523
- const wrote = writeObservations({
1524
- driver,
1525
- pass,
1526
- evidenceStore,
1527
- repo: root.repo,
1528
- repoRoot: root.absolutePath,
1529
- runId: execution.execution.executionId,
1530
- commitSha: root.commitSha,
1531
- });
1532
- const written = wrote.promoted.length + wrote.created.length;
1533
- if (written === 0) {
1534
- notes.push("No edge was written. Either no observation carried a call-site stack that resolved to a " +
1535
- "function this graph holds, or every one it did carry was already at R4. Both are real " +
1536
- "outcomes of this run, and neither says the graph's existing edges are wrong.");
1537
- }
1538
- if (wrote.unjoinedSplitEvidence !== null)
1539
- notes.push(wrote.unjoinedSplitEvidence);
1540
- notes.push(...wrote.confirmedFactConflict);
1541
- // Build item 2: save only on a real success, whichever source these services came
1542
- // from (fresh args or the saved settings this same call may have reused) — an
1543
- // idempotent overwrite either way. Never on `timedOut` or `failed`: neither
1544
- // establishes that every declared service actually reached readiness.
1545
- if (outcome.state === "ok") {
1546
- const toSave = {};
1547
- for (const service of declared)
1548
- toSave[service.name] = service.savable;
1549
- try {
1550
- const saved = await writeSavedStart(session.repoPath, toSave);
1551
- // Written to disk above; `reloadConfig()` (session.ts's own sole mutator of
1552
- // `session.config`) mirrors it into this live session immediately, so a second
1553
- // call on this same connection sees it with no explicit reload in between.
1554
- // Without this, `session.config.savedStart` stays whatever it was when this
1555
- // session was constructed — stale for the rest of the process's life — and this
1556
- // same write would also show up as spurious drift on this very reply, since
1557
- // `checkConfigDrift` (server.ts) diffs on-disk config against `session.config`
1558
- // right after this tool returns.
1559
- await session.reloadConfig();
1560
- notes.push(`This run's services were saved to ${saved.path} (auto-action, disclosed per rule 7 — never a ` +
1561
- "secret value, since environment variables are saved by name only). A later observe_runtime " +
1562
- 'call on this repository may omit "services" and these settings will be reused.');
1563
- }
1564
- catch (error) {
1565
- notes.push(`This run's start settings could not be saved (${error instanceof Error ? error.message : String(error)}); ` +
1566
- 'a later call must still declare "services" explicitly.');
1567
- }
1568
- }
1569
- const headline = failed
1570
- ? `The run failed to complete: execution ended in state "${execution.execution.state}", not ` +
1571
- `"COMPLETED". ${String(execution.evidence.length)} evidence item(s) were collected across ` +
1572
- `${String(declared.length)} service(s), but that does not mean the boot succeeded — see the ` +
1573
- "notes for what this run could and could not establish."
1574
- : `Observed ${String(execution.evidence.length)} evidence item(s) across ${String(declared.length)} ` +
1575
- `service(s); ${String(pass.attributed.length)} resolved to graph nodes; ` +
1576
- `${String(wrote.promoted.length)} edge(s) promoted to R4 and ${String(wrote.created.length)} minted at R4.`;
1577
- return answer({
1578
- headline,
1579
- // Not `empty` when nothing was witnessed — `empty` claims the population, and "this run
1580
- // took no path exercising the code" isn't "this code does nothing". `timed_out` when the
1581
- // budget bound this run, `failed` when the execution never reached `COMPLETED` at all
1582
- // (see deriveRunOutcome): `empty`/`ok` both claim the window ran to its end on a run that
1583
- // actually finished.
1584
- state: outcome.state,
1585
- nameLevel: true,
1586
- // R4 unconditionally: every `wrote` fact was witnessed at runtime (DEC-115, no inference).
1587
- // A run that wrote nothing reports R0 rather than borrowing the tier a success would reach.
1588
- resolutionFloor: (written > 0 ? 4 : 0),
1589
- // G3/G4's E, declared only when evidence actually came back — not gated on `written > 0`:
1590
- // rule 3 already caps the category via resolutionFloor above, so a run that saw channels
1591
- // but wrote no edge is `unconfirmed` by that cap, not by pretending it saw nothing.
1592
- ...(evidenceIncludingJourney.length === 0
1593
- ? {}
1594
- : { runtimeEvidence: { independentSignalTypes: witnessedSignalTypes(evidenceIncludingJourney) } }),
1595
- commitSha: base.commitSha,
1596
- graphBuiltAt: base.graphBuiltAt,
1597
- irSchemaVersion: base.irSchemaVersion,
1598
- commitSpread: base.commitSpread,
1599
- repoCount: base.repoCount,
1600
- notes,
1601
- data: {
1602
- executionId: execution.execution.executionId,
1603
- executionState: execution.execution.state,
1604
- adapterLanguage: adapter.language,
1605
- evidencePath,
1606
- services,
1607
- evidenceByType: journeyOutcome === undefined && databaseRecordCount === 0 ? evidenceByType : tally(evidenceIncludingJourney),
1608
- correlation,
1609
- wrote,
1610
- journey: journeyOutcome ?? null,
1611
- database: databaseObservation,
1612
- },
1613
- });
1614
- }
1615
- finally {
1616
- // Best-effort: normally already stopped above. Only still "observing" here when something
1617
- // threw before that point (e.g. runInstrumentedExecution itself), in which case the
1618
- // captured evidence has no real executionId to attribute to and is discarded — the same
1619
- // "no fabrication" posture as the cancellation path.
1620
- if (postgresChannel.kind === "observing")
1621
- await postgresChannel.stop().catch(() => undefined);
1622
- abort.dispose();
1623
- evidenceStore.close();
1624
- }
1625
- }
1626
- /** Stop a process this run spawned, and the group it leads — signalling the leader alone
1627
- * would leave forked children holding the port. ESRCH is ordinary: it may have already exited. */
1628
- function terminateSpawnedProcess(pid) {
1629
- try {
1630
- process.kill(-pid, "SIGTERM");
1631
- return;
1632
- }
1633
- catch (error) {
1634
- if (error.code === "ESRCH")
1635
- return;
1636
- }
1637
- try {
1638
- process.kill(pid, "SIGTERM");
1639
- }
1640
- catch {
1641
- // Already gone. Nothing to report and nothing to do.
1642
- }
1643
- }
1644
- /**
1645
- * The per-service captured output, when the installed runtime supplies it.
1646
- *
1647
- * Read structurally rather than off the declared type. `serviceOutput` is newer
1648
- * than the `@descryy/runtime-orchestrator` this package is pinned to, and the
1649
- * pin is exact while the engine is on 0.x (DEC-395), so the field is present at
1650
- * runtime only once that pin moves. An optional read means this half works the
1651
- * day the runtime ships it and degrades honestly — not silently — before then:
1652
- * `startupOutputNote` says so to the user rather than letting an empty field
1653
- * read as "the service printed nothing".
1654
- */
1655
- function capturedServiceOutput(execution) {
1656
- const candidate = execution.serviceOutput;
1657
- if (candidate === null || typeof candidate !== "object" || candidate === undefined)
1658
- return null;
1659
- const entries = Object.entries(candidate).filter((entry) => typeof entry[1] === "string");
1660
- return Object.fromEntries(entries);
1661
- }
1662
- /**
1663
- * What to tell the user when a run failed and no output came back with it.
1664
- *
1665
- * An exit code with no explanation is a fallback, and rule 7 says every
1666
- * fallback is labelled. The cost of not labelling this one is measured: a bare
1667
- * `126` was read as a nested-sandbox incompatibility and the wrong root cause
1668
- * outlived the session that filed it.
1669
- */
1670
- function startupOutputNote(services, captured) {
1671
- const failedSilently = services.filter((service) => service.started && service.startupOutput === null);
1672
- if (failedSilently.length === 0)
1673
- return null;
1674
- const names = failedSilently.map((service) => service.service).join(", ");
1675
- if (captured === null) {
1676
- return (`No startup output is shown for ${names} because the installed ` +
1677
- "@descryy/runtime-orchestrator does not return it — not because the service printed nothing. " +
1678
- "An exit code on its own does not say why a service failed, and can mislead: 126 means the " +
1679
- "command was found and could not be executed, which is also what a perfectly working sandbox " +
1680
- "reports. Upgrade the runtime to see the reason the process actually gave.");
1681
- }
1682
- return (`${names} produced no captured output before failing, so this run cannot say why beyond the exit ` +
1683
- "code and signal recorded above. That is an absence of evidence about the failure, not evidence " +
1684
- "that the service failed quietly.");
1685
- }
1686
- /**
1687
- * The tail of a failed service's captured output that survives into the reply.
1688
- *
1689
- * Tail rather than head: a process that dies prints its reason last, so
1690
- * clipping from the front is the one choice guaranteed to discard the line this
1691
- * field exists to carry.
1692
- */
1693
- export const STARTUP_OUTPUT_TAIL_LIMIT = 4_000;
1694
- /** One row per **declared** service, not per spawned process — a never-started service
1695
- * appears with `started: false` rather than vanishing ("it did nothing" vs "it never ran").
1696
- *
1697
- * `startupOutput` is keyed by service name. Callers pass `{}` for a run that did
1698
- * not fail; the policy lives at the call site, where the outcome is known, rather
1699
- * than in here. Exported for `observe-runtime-startup-output.test.ts`, which pins
1700
- * the mapping without needing a runtime new enough to produce real output. */
1701
- export function describeServices(processes, declared, startupOutput = {}) {
1702
- const byService = new Map();
1703
- for (const handle of processes) {
1704
- if (handle.serviceName !== null)
1705
- byService.set(handle.serviceName, handle);
1706
- }
1707
- return declared.map((service) => {
1708
- const handle = byService.get(service.name);
1709
- // A service with no process never ran, so anything keyed under its name is
1710
- // not its output — reporting it would attribute one service's words to
1711
- // another.
1712
- const captured = handle === undefined ? undefined : startupOutput[service.name];
1713
- const truncated = captured !== undefined && captured.length > STARTUP_OUTPUT_TAIL_LIMIT;
1714
- return {
1715
- service: service.name,
1716
- started: handle !== undefined,
1717
- attached: service.attached,
1718
- port: handle?.port ?? null,
1719
- exitedAt: handle?.exitedAt ?? null,
1720
- exitCode: handle?.exitCode ?? null,
1721
- signal: handle?.signal ?? null,
1722
- startupOutput: captured === undefined ? null : truncated ? captured.slice(-STARTUP_OUTPUT_TAIL_LIMIT) : captured,
1723
- startupOutputTruncated: truncated,
1724
- };
1725
- });
1726
- }
1727
- function tally(evidence) {
1728
- const byType = {};
1729
- for (const item of evidence)
1730
- byType[item.eventType] = (byType[item.eventType] ?? 0) + 1;
1731
- return byType;
1732
- }
1733
- /** One evidence row onto one of @descryy/ir's six RUNTIME_SIGNAL_TYPES, or null. Decided by
1734
- * `eventType`; `source` consulted only for the ambiguous EXCEPTION/STACK_TRACE pair (reading
1735
- * `source` alone was measured wrong). Unlisted kinds fail closed to null, never guess-mapped. */
1736
- function signalOf(item) {
1737
- switch (item.eventType) {
1738
- case "CONSOLE_MESSAGE":
1739
- return "browser-console";
1740
- case "NETWORK_REQUEST":
1741
- case "NETWORK_RESPONSE":
1742
- case "HTTP_ERROR":
1743
- case "WEBSOCKET_CLOSED":
1744
- return "network";
1745
- case "SCREENSHOT":
1746
- case "VIDEO":
1747
- return "browser-visual";
1748
- case "BACKEND_LOG":
1749
- return "backend-log";
1750
- case "DATABASE_QUERY":
1751
- return "database";
1752
- case "EXTERNAL_REQUEST":
1753
- return "external-service";
1754
- // The ambiguous pair: a thrown error reaches Descry through whichever collector saw it.
1755
- case "EXCEPTION":
1756
- case "STACK_TRACE":
1757
- return item.source === "browser-console" ? "browser-console" : "backend-log";
1758
- default:
1759
- return null;
1760
- }
1761
- }
1762
- /** `E` for this run — channels actually seen, via @descryy/ir's independentSignalTypes
1763
- * (clamp-to-six and null-drop are that function's rules, not duplicated here). Exported for
1764
- * its own test — this mapping decides `strongly supported` vs `unconfirmed`. */
1765
- export function witnessedSignalTypes(evidence) {
1766
- return independentSignalTypes(evidence.map((item) => ({ signal: signalOf(item), detail: item.eventType })));
1767
- }
1768
- /**
1769
- * The R4 write, and the one join this tool performs: for every evidence item resolved to an
1770
- * endpoint that also carries a call-site stack, ask confirmObservedFrontendCaller. Both come
1771
- * from one observation — nothing here pairs two separate evidence items, on purpose.
1772
- *
1773
- * A stack resolved on some OTHER evidence item (`family === "symbol"`, e.g. a BACKEND_LOG or
1774
- * EXCEPTION line elsewhere in the same run) is never joined to an endpoint item that lacks its
1775
- * own stack, even when both exist in the same run's evidence. `resolveObservedFrontendCaller`
1776
- * takes a stack on faith as "the call site that reached this endpoint" — it does not itself
1777
- * verify the direction, so the caller supplying the wrong stack is exactly how a wrong edge
1778
- * gets minted at R4 with full confidence. Nothing on `Evidence` disambiguates that direction
1779
- * for a cross-record pair: `traceId`/`requestId` can say two items belong to the same request,
1780
- * never that the stack-bearing one is the endpoint's CALLER rather than code running inside its
1781
- * own handling of that same request — which is the common shape (a handler's own exception
1782
- * stack, correlated to the request it was handling, not to whoever issued it). Joining on that
1783
- * key would systematically mint edges in the SERVES direction mislabelled as CALLS/USES_API —
1784
- * worse than the gap disclosed below (rule 2). So this is measured and reported via
1785
- * `unjoinedSplitEvidence`, never inferred into an edge.
1786
- */
1787
- export function writeObservations(input) {
1788
- const promoted = [];
1789
- const created = [];
1790
- const confirmed = [];
1791
- const refused = [];
1792
- const contradictions = [];
1793
- const staleR4 = [];
1794
- const confirmedFactConflict = [];
1795
- // One evidence item can be attributed twice; keyed on both so the same (evidence,
1796
- // endpoint) pair is never confirmed twice within one run.
1797
- const seen = new Set();
1798
- // Split-evidence gap, tracked rather than acted on — see this function's own doc.
1799
- let unpairedEndpoints = 0;
1800
- let resolvedSymbolsElsewhere = 0;
1801
- for (const attribution of input.pass.attributed) {
1802
- if (attribution.family === "symbol") {
1803
- const evidence = input.evidenceStore.getById(attribution.evidenceId);
1804
- if (evidence !== null && evidence.stackTrace !== null)
1805
- resolvedSymbolsElsewhere += 1;
1806
- continue;
1807
- }
1808
- // "endpoint" only — "log-text-endpoint" fires when a log line mentions a route, which
1809
- // means the function SERVES it, not calls it; minting from that could point the wrong
1810
- // way. Rule 2: a wrong edge is worse than a missing (disclosed) one. Omitted.
1811
- if (attribution.family !== "endpoint")
1812
- continue;
1813
- const key = `${attribution.evidenceId}::${attribution.graphNodeId}`;
1814
- if (seen.has(key))
1815
- continue;
1816
- seen.add(key);
1817
- const evidence = input.evidenceStore.getById(attribution.evidenceId);
1818
- if (evidence === null || evidence.stackTrace === null) {
1819
- unpairedEndpoints += 1;
1820
- continue;
1821
- }
1822
- const outcome = confirmObservedFrontendCaller(input.driver, {
1823
- endpointNodeId: attribution.graphNodeId,
1824
- stackTrace: evidence.stackTrace,
1825
- repo: input.repo,
1826
- runId: input.runId,
1827
- commitSha: input.commitSha,
1828
- repoRoot: input.repoRoot,
1829
- });
1830
- promoted.push(...outcome.confirmation.promoted);
1831
- created.push(...outcome.confirmation.created);
1832
- confirmed.push(...outcome.confirmation.confirmed);
1833
- refused.push(...outcome.confirmation.refused.map((r) => r.reason));
1834
- contradictions.push(...outcome.confirmation.contradictions.map((c) => c.detail));
1835
- staleR4.push(...outcome.confirmation.staleR4.map((s) => s.detail));
1836
- // `confirmedFactConflict` cannot be read here yet: `outcome.confirmation` is typed
1837
- // against `@descryy/runtime-graph-correlator`'s OWN pinned, nested `@descryy/core`
1838
- // (0.5.2 at last check — DEC-395's exact cross-repo pins), which predates this field.
1839
- // Also moot in practice today: this call path never emits a denial at all (see this
1840
- // function's own header doc, and `contradictions`/`staleR4` above, which are "always
1841
- // empty today" for the same reason) — so there is nothing for a human-confirmed edge
1842
- // to collide with here regardless. Once the correlator republishes against a core
1843
- // carrying `confirmedFactConflict`, thread it through the same way as the lines above.
1844
- }
1845
- const unjoinedSplitEvidence = unpairedEndpoints > 0 && resolvedSymbolsElsewhere > 0
1846
- ? `This run reached ${String(unpairedEndpoints)} endpoint attribution(s) carrying no call-site ` +
1847
- `stack of their own, and separately resolved ${String(resolvedSymbolsElsewhere)} call-site ` +
1848
- "stack(s) to a real function elsewhere in this run's evidence. The two are not joined: this " +
1849
- "tool only promotes an edge from one observation that names both the endpoint and the stack " +
1850
- "itself, because a stack found on a different evidence record cannot be shown to be the " +
1851
- "endpoint's CALLER rather than code that merely ran during its own handling of the same " +
1852
- "request — minting from that risks an edge pointing the wrong way, which this project's rules " +
1853
- "treat as worse than the gap this note discloses. No edge was written for this pairing; it is " +
1854
- "real signal this run could not safely turn into a fact, not evidence that no such call exists."
1855
- : null;
1856
- return { promoted, created, confirmed, refused, contradictions, staleR4, confirmedFactConflict, unjoinedSplitEvidence };
1857
- }
1858
- /** §7's `willDo` — and, since readArguments runs first, where an invalid call is refused.
1859
- * Always returns a string, never undefined: unlike `questions`, no call here has nothing
1860
- * to confirm. Reads the *parsed* services so the confirmation sentence matches the run exactly. */
1861
- /** Exported for the same reason as `run` above — `observe.ts` dispatches the "runtime" verb's
1862
- * consent sentence to this exact function. */
1863
- export function describeAction(args, ctx) {
1864
- const { declared } = readArguments(args, ctx.session.repoPath);
1865
- const spawned = declared.filter((service) => !service.attached).map((service) => service.name);
1866
- const attached = declared.filter((service) => service.attached).map((service) => service.name);
1867
- const parts = [];
1868
- if (spawned.length > 0)
1869
- parts.push(`start ${String(spawned.length)} service(s) (${spawned.join(", ")})`);
1870
- if (attached.length > 0) {
1871
- parts.push(`attach to ${String(attached.length)} already-running service(s) (${attached.join(", ")})`);
1872
- }
1873
- return (`${parts.join(" and ")}, observe them, and write any edge the run witnesses into this ` +
1874
- "repository's graph at R4 — a durable fact that raises every later finding resting on it to " +
1875
- "reliability class A, and that survives re-indexing. Nothing is ever demoted or deleted.");
1876
- }
1877
- export const observeRuntimeTool = {
1878
- name: "observe_runtime",
1879
- class: "action",
1880
- tier: "evidence",
1881
- version: "1.0.0",
1882
- title: "Run the application and record what was observed",
1883
- description: "Boot or attach to the declared services, watch them with a runtime adapter, resolve what was " +
1884
- "observed against this repository's graph, and record the edges the run actually witnessed at R4 " +
1885
- "— the one resolution level static analysis cannot reach. Promoted and newly minted edges become " +
1886
- "visible through impact, propagation and every other tool immediately, with no second call: they " +
1887
- "already read the resolution field. Running takes a two-call confirmation — the first call " +
1888
- "performs nothing and returns a token describing what it would do; call again with " +
1889
- "\"confirmToken\" to actually run it. The environment profile's declared safetyLevel is checked " +
1890
- "before anything starts: booting is a write, attaching is not. " +
1891
- "BEFORE CALLING: this observes an application you can already run — it does not help you get to " +
1892
- "a runnable state, and that boundary is real rather than apologetic. Its dependencies must be up, " +
1893
- "its environment set, and its migrations applied, all by you. One trap worth stating because it " +
1894
- "is invisible: overriding some environment variables does not isolate a run from the " +
1895
- "application's own configuration file — anything you did not explicitly override is still read " +
1896
- "from it, including values naming environments you did not intend to touch.",
1897
- inputSchema: SCHEMA,
1898
- run,
1899
- describeAction,
1900
- };
1901
- //# sourceMappingURL=observe-runtime.js.map