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