@tonyclaw/agent-inspector 3.0.45 → 3.0.46

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 (338) hide show
  1. package/.output/{nitro.json → backend/nitro.json} +3 -3
  2. package/.output/backend-public/assets/index-D4LX-sw-.js +11 -0
  3. package/.output/cli.js +2326 -516
  4. package/.output/server/_libs/extend.mjs +4 -8
  5. package/.output/server/_libs/jszip.mjs +9 -35
  6. package/.output/server/_libs/react-dom.mjs +2 -3
  7. package/.output/server/_libs/react.mjs +1 -27
  8. package/.output/server/_libs/tanstack__react-router.mjs +1 -329
  9. package/.output/server/_libs/tanstack__router-core.mjs +42 -100
  10. package/.output/server/_libs/use-sync-external-store.mjs +1 -64
  11. package/.output/server/_libs/ws.mjs +1 -1
  12. package/.output/server/_ssr/index.mjs +4 -4
  13. package/.output/server/_ssr/{publicBasePathContext-5264L3rX.mjs → publicBasePathContext-IH2ilOfu.mjs} +52 -24
  14. package/.output/server/_ssr/{router-BJ_ltLim.mjs → router-D18yUq36.mjs} +19770 -19447
  15. package/.output/server/_ssr/{start-C6MjPjxp.mjs → start-COQu_AgD.mjs} +1 -1
  16. package/.output/server/_tanstack-start-manifest_v-DnbdNeun.mjs +4 -0
  17. package/.output/server/index.mjs +5 -145
  18. package/.output/{public/assets/CompareDrawer-B3_TLTJh.js → ui/assets/CompareDrawer-DAUuIJ6G.js} +1 -1
  19. package/.output/{public/assets/InspectorPet-Cpn6hv4P.js → ui/assets/InspectorPet-BRBVjOWI.js} +1 -1
  20. package/.output/ui/assets/ProxyViewerContainer-D7Sq0ctc.js +59 -0
  21. package/.output/{public/assets/ReplayDialog-B3EKVdJk.js → ui/assets/ReplayDialog-nDLsjOhs.js} +1 -1
  22. package/.output/{public/assets/RequestAnatomy-COOQjyUC.js → ui/assets/RequestAnatomy-zd4BDgL0.js} +1 -1
  23. package/.output/ui/assets/ResponseView-AdIXGcSn.js +2 -0
  24. package/.output/ui/assets/StreamingChunkSequence-CesTz8He.js +1 -0
  25. package/.output/{public/assets/_sessionId-zbgYl7ZJ.js → ui/assets/_sessionId-BAfzrhSU.js} +1 -1
  26. package/.output/{public/assets/_sessionId-BJlM3nmt.js → ui/assets/_sessionId-QnBomgPD.js} +1 -1
  27. package/.output/ui/assets/favicon-DYEkHSNl.svg +21 -0
  28. package/.output/{public/assets/index-BIK4bPTv.js → ui/assets/index-BcsdxBAd.js} +1 -1
  29. package/.output/ui/assets/index-BvXp42al.css +1 -0
  30. package/.output/{public/assets/index-D2Qw6Ri2.js → ui/assets/index-D0rtCN9V.js} +1 -1
  31. package/.output/ui/assets/index-DHpwr08Z.js +73 -0
  32. package/.output/{public/assets/index-xF_NY8Yb.js → ui/assets/index-dNuk2dsU.js} +5 -5
  33. package/.output/ui/assets/json-viewer-BvFHglMb.js +1 -0
  34. package/.output/{public/assets/jszip.min-B_rfAUCe.js → ui/assets/jszip.min-B5Z7gNZB.js} +1 -1
  35. package/.output/ui/index.html +229 -0
  36. package/.output/ui/runtime-config.js +2 -0
  37. package/.output/workers/logFinalizer.worker.js +979 -414
  38. package/.output/workers/sessionWorkerEntry.js +979 -414
  39. package/README.md +175 -43
  40. package/docs/instance-control.openapi.yaml +59 -4
  41. package/package.json +25 -5
  42. package/scripts/generate-theme-scales.mjs +3 -3
  43. package/src/{router.tsx → backend/router.tsx} +3 -15
  44. package/src/backend/routes/$.ts +26 -0
  45. package/src/backend/routes/__root.ts +4 -0
  46. package/src/{routes → backend/routes}/api/-instances.ts +3 -3
  47. package/src/{routes → backend/routes}/api/alerts.summary.ts +3 -3
  48. package/src/{routes → backend/routes}/api/alerts.ts +4 -4
  49. package/src/{routes → backend/routes}/api/config.paths.ts +1 -1
  50. package/src/{routes → backend/routes}/api/config.ts +11 -2
  51. package/src/{routes → backend/routes}/api/ecosystem.packages.$packageId.help.ts +1 -1
  52. package/src/{routes → backend/routes}/api/ecosystem.packages.$packageId.install.ts +1 -1
  53. package/src/{routes → backend/routes}/api/ecosystem.packages.$packageId.runner-presets.ts +1 -1
  54. package/src/{routes → backend/routes}/api/ecosystem.packages.$packageId.upgrade.ts +1 -1
  55. package/src/{routes → backend/routes}/api/ecosystem.packages.ts +5 -1
  56. package/src/{routes → backend/routes}/api/ecosystem.recipes.$recipeId.run.ts +1 -1
  57. package/src/{routes → backend/routes}/api/ecosystem.tasks.$taskId.ts +1 -1
  58. package/src/{routes → backend/routes}/api/ecosystem.tasks.ts +1 -1
  59. package/src/{routes → backend/routes}/api/groups.$groupId.evidence.ts +2 -2
  60. package/src/{routes → backend/routes}/api/groups.$groupId.sessions.ts +2 -2
  61. package/src/{routes → backend/routes}/api/groups.$groupId.ts +2 -2
  62. package/src/{routes → backend/routes}/api/groups.ts +2 -2
  63. package/src/{routes → backend/routes}/api/health.ts +3 -3
  64. package/src/{routes → backend/routes}/api/instances.$name.connection.ts +1 -1
  65. package/src/{routes → backend/routes}/api/instances.$name.restart.ts +2 -2
  66. package/src/{routes → backend/routes}/api/instances.$name.start.ts +2 -2
  67. package/src/{routes → backend/routes}/api/instances.$name.stop.ts +2 -2
  68. package/src/{routes → backend/routes}/api/instances.$name.ts +1 -1
  69. package/src/{routes → backend/routes}/api/instances.ts +1 -1
  70. package/src/{routes → backend/routes}/api/knowledge.candidates.$candidateId.promote.ts +2 -2
  71. package/src/{routes → backend/routes}/api/knowledge.candidates.$candidateId.ts +2 -2
  72. package/src/{routes → backend/routes}/api/knowledge.candidates.ts +1 -1
  73. package/src/{routes → backend/routes}/api/knowledge.project-context.ts +2 -2
  74. package/src/{routes → backend/routes}/api/knowledge.search.ts +2 -2
  75. package/src/{routes → backend/routes}/api/knowledge.sessions.$sessionId.candidates.ts +3 -3
  76. package/src/{routes → backend/routes}/api/logs.$id.body.ts +6 -6
  77. package/src/{routes → backend/routes}/api/logs.$id.chunks.ts +5 -4
  78. package/src/{routes → backend/routes}/api/logs.$id.replay.ts +12 -11
  79. package/src/{routes → backend/routes}/api/logs.$id.ts +4 -3
  80. package/src/{routes → backend/routes}/api/logs.import.ts +3 -3
  81. package/src/{routes → backend/routes}/api/logs.stream.ts +11 -7
  82. package/src/{routes → backend/routes}/api/logs.ts +17 -9
  83. package/src/{routes → backend/routes}/api/mcp.ts +1 -1
  84. package/src/{routes → backend/routes}/api/models.ts +1 -1
  85. package/src/{routes → backend/routes}/api/pi-agent.ts +2 -2
  86. package/src/{routes → backend/routes}/api/providers.$providerId.model-metadata.ts +5 -5
  87. package/src/{routes → backend/routes}/api/providers.$providerId.test.log.ts +7 -7
  88. package/src/{routes → backend/routes}/api/providers.$providerId.ts +4 -4
  89. package/src/{routes → backend/routes}/api/providers.export.ts +1 -1
  90. package/src/{routes → backend/routes}/api/providers.import.ts +3 -3
  91. package/src/{routes → backend/routes}/api/providers.scan.ts +2 -2
  92. package/src/{routes → backend/routes}/api/providers.ts +9 -4
  93. package/src/{routes → backend/routes}/api/runs.$runId.evidence.ts +2 -2
  94. package/src/{routes → backend/routes}/api/runs.$runId.ts +2 -2
  95. package/src/{routes → backend/routes}/api/runs.ts +4 -4
  96. package/src/backend/routes/api/runtime.ts +46 -0
  97. package/src/{routes → backend/routes}/api/sessions.ts +3 -3
  98. package/src/{routes → backend/routes}/proxy/$.ts +1 -1
  99. package/src/{routes → backend/routes}/readyz.ts +1 -1
  100. package/src/{start.ts → backend/start.ts} +1 -1
  101. package/src/cli/alias.ts +2 -11
  102. package/src/cli/doctor.ts +32 -9
  103. package/src/cli/instance.ts +26 -13
  104. package/src/cli/instanceArgs.ts +30 -11
  105. package/src/cli/instanceControl.ts +0 -2
  106. package/src/cli/instanceModel.ts +9 -17
  107. package/src/cli/instanceRegistry.ts +0 -9
  108. package/src/cli/networkHints.ts +27 -14
  109. package/src/cli/onboard.ts +69 -2
  110. package/src/cli/onboardTarget.ts +89 -40
  111. package/src/cli/templates/codex-skill-onboard.ts +44 -14
  112. package/src/cli/templates/skill-onboard.ts +64 -28
  113. package/src/cli.ts +184 -61
  114. package/src/components/ProxyViewer.tsx +11 -9
  115. package/src/components/ProxyViewerContainer.tsx +12 -11
  116. package/src/components/pi-agent/PiAgentPanel.tsx +2 -2
  117. package/src/components/providers/ProviderCard.tsx +1 -1
  118. package/src/components/providers/ProviderTestResultsView.tsx +1 -1
  119. package/src/components/providers/SettingsDialog.tsx +159 -144
  120. package/src/components/proxy-viewer/ApplicationBar.tsx +2 -1
  121. package/src/components/proxy-viewer/LogEntry.tsx +1 -1
  122. package/src/components/proxy-viewer/log-formats/index.ts +1 -1
  123. package/src/contracts/index.ts +5 -0
  124. package/src/contracts/log.ts +33 -3
  125. package/src/knowledge/types.ts +3 -2
  126. package/src/lib/alertContract.ts +2 -2
  127. package/src/lib/apiClient.ts +30 -7
  128. package/src/lib/apiFormat.ts +29 -0
  129. package/src/lib/basePath.ts +29 -11
  130. package/src/lib/browserRuntimeContract.ts +124 -0
  131. package/src/lib/browserRuntimeResolver.ts +244 -0
  132. package/src/{proxy → lib}/claudeCodeStrip.ts +6 -7
  133. package/src/lib/codingAgentEndpoints.ts +38 -0
  134. package/src/lib/instanceContract.ts +27 -11
  135. package/src/lib/piAgentContract.ts +3 -2
  136. package/src/lib/publicBasePathContext.ts +40 -10
  137. package/src/lib/resourceLimits.ts +2 -0
  138. package/src/lib/runContract.ts +3 -3
  139. package/src/lib/sessionInfoContract.ts +3 -2
  140. package/src/mcp/instanceHandlers.ts +1 -0
  141. package/src/mcp/server.ts +19 -24
  142. package/src/proxy/config.ts +25 -30
  143. package/src/proxy/dataDir.ts +43 -7
  144. package/src/proxy/ecosystemTasks.ts +161 -70
  145. package/src/proxy/formats/registry.ts +6 -21
  146. package/src/proxy/handler.ts +129 -29
  147. package/src/proxy/identityProxy.ts +419 -39
  148. package/src/proxy/jsonlRecovery.ts +554 -0
  149. package/src/proxy/logFinalizer.ts +25 -10
  150. package/src/proxy/logImporter.ts +2 -1
  151. package/src/proxy/logIndex.ts +191 -95
  152. package/src/proxy/logger.ts +77 -14
  153. package/src/proxy/platformCommands.ts +41 -0
  154. package/src/proxy/privateDataPath.ts +310 -88
  155. package/src/proxy/rawStreamCapture.ts +194 -13
  156. package/src/proxy/runtimeAdmission.ts +5 -2
  157. package/src/proxy/runtimeHealth.ts +14 -1
  158. package/src/proxy/runtimeShutdown.ts +11 -1
  159. package/src/proxy/schemas.ts +7 -1
  160. package/src/proxy/sessionArchive.ts +78 -14
  161. package/src/proxy/sqliteLogIndex.ts +65 -2
  162. package/src/proxy/store.ts +141 -34
  163. package/src/proxy/uiAssetServer.ts +106 -0
  164. package/src/proxy/upstream.ts +29 -2
  165. package/src/ui/bootstrap.ts +167 -0
  166. package/src/ui/index.html +38 -0
  167. package/src/ui/logsStreamClient.ts +20 -0
  168. package/src/ui/main.tsx +13 -0
  169. package/src/ui/public/runtime-config.js +2 -0
  170. package/src/ui/router.tsx +13 -0
  171. package/src/ui/routes/__root.tsx +69 -0
  172. package/src/{routes → ui/routes}/index.tsx +2 -3
  173. package/src/{routes → ui/routes}/session/$sessionId.tsx +5 -11
  174. package/src/ui/runtimeDiscovery.ts +50 -0
  175. package/styles/globals.css +1 -0
  176. package/.output/public/assets/ProxyViewerContainer-BEAhuGOJ.js +0 -126
  177. package/.output/public/assets/ResponseView-DBCjkwye.js +0 -2
  178. package/.output/public/assets/StreamingChunkSequence-CuXhDgMo.js +0 -1
  179. package/.output/public/assets/index-BZo_8koU.js +0 -70
  180. package/.output/public/assets/index-ZWschkCh.css +0 -1
  181. package/.output/public/assets/json-viewer-B-Ao6pwW.js +0 -1
  182. package/.output/server/_libs/@radix-ui/react-accessible-icon+[...].mjs +0 -1
  183. package/.output/server/_libs/@radix-ui/react-dismissable-layer+[...].mjs +0 -344
  184. package/.output/server/_libs/@radix-ui/react-navigation-menu+[...].mjs +0 -2
  185. package/.output/server/_libs/@radix-ui/react-one-time-password-field+[...].mjs +0 -2
  186. package/.output/server/_libs/@radix-ui/react-password-toggle-field+[...].mjs +0 -2
  187. package/.output/server/_libs/@radix-ui/react-use-callback-ref+[...].mjs +0 -11
  188. package/.output/server/_libs/@radix-ui/react-use-controllable-state+[...].mjs +0 -69
  189. package/.output/server/_libs/@radix-ui/react-use-effect-event+[...].mjs +0 -1
  190. package/.output/server/_libs/@radix-ui/react-use-is-hydrated+[...].mjs +0 -28
  191. package/.output/server/_libs/@radix-ui/react-use-layout-effect+[...].mjs +0 -6
  192. package/.output/server/_libs/@radix-ui/react-visually-hidden+[...].mjs +0 -34
  193. package/.output/server/_libs/aria-hidden.mjs +0 -122
  194. package/.output/server/_libs/bail.mjs +0 -8
  195. package/.output/server/_libs/character-entities.mjs +0 -2130
  196. package/.output/server/_libs/class-variance-authority.mjs +0 -44
  197. package/.output/server/_libs/clsx.mjs +0 -16
  198. package/.output/server/_libs/comma-separated-tokens.mjs +0 -10
  199. package/.output/server/_libs/decode-named-character-reference+[...].mjs +0 -8
  200. package/.output/server/_libs/dequal.mjs +0 -27
  201. package/.output/server/_libs/detect-node-es.mjs +0 -1
  202. package/.output/server/_libs/devlop.mjs +0 -8
  203. package/.output/server/_libs/diff.mjs +0 -320
  204. package/.output/server/_libs/estree-util-is-identifier-name.mjs +0 -11
  205. package/.output/server/_libs/floating-ui__core.mjs +0 -725
  206. package/.output/server/_libs/floating-ui__dom.mjs +0 -622
  207. package/.output/server/_libs/floating-ui__react-dom.mjs +0 -292
  208. package/.output/server/_libs/floating-ui__utils.mjs +0 -320
  209. package/.output/server/_libs/get-nonce.mjs +0 -9
  210. package/.output/server/_libs/hast-util-to-jsx-runtime.mjs +0 -388
  211. package/.output/server/_libs/hast-util-whitespace.mjs +0 -10
  212. package/.output/server/_libs/html-url-attributes.mjs +0 -26
  213. package/.output/server/_libs/inline-style-parser.mjs +0 -142
  214. package/.output/server/_libs/is-plain-obj.mjs +0 -10
  215. package/.output/server/_libs/lucide-react.mjs +0 -1027
  216. package/.output/server/_libs/mdast-util-from-markdown.mjs +0 -717
  217. package/.output/server/_libs/mdast-util-to-hast.mjs +0 -710
  218. package/.output/server/_libs/mdast-util-to-string.mjs +0 -38
  219. package/.output/server/_libs/micromark-core-commonmark.mjs +0 -2259
  220. package/.output/server/_libs/micromark-factory-destination.mjs +0 -94
  221. package/.output/server/_libs/micromark-factory-label.mjs +0 -63
  222. package/.output/server/_libs/micromark-factory-space.mjs +0 -24
  223. package/.output/server/_libs/micromark-factory-title.mjs +0 -65
  224. package/.output/server/_libs/micromark-factory-whitespace.mjs +0 -22
  225. package/.output/server/_libs/micromark-util-character.mjs +0 -44
  226. package/.output/server/_libs/micromark-util-chunked.mjs +0 -36
  227. package/.output/server/_libs/micromark-util-classify-character+[...].mjs +0 -12
  228. package/.output/server/_libs/micromark-util-combine-extensions+[...].mjs +0 -41
  229. package/.output/server/_libs/micromark-util-decode-numeric-character-reference+[...].mjs +0 -19
  230. package/.output/server/_libs/micromark-util-decode-string.mjs +0 -21
  231. package/.output/server/_libs/micromark-util-encode.mjs +0 -1
  232. package/.output/server/_libs/micromark-util-html-tag-name.mjs +0 -69
  233. package/.output/server/_libs/micromark-util-normalize-identifier+[...].mjs +0 -6
  234. package/.output/server/_libs/micromark-util-resolve-all.mjs +0 -15
  235. package/.output/server/_libs/micromark-util-sanitize-uri.mjs +0 -41
  236. package/.output/server/_libs/micromark-util-subtokenize.mjs +0 -346
  237. package/.output/server/_libs/micromark.mjs +0 -906
  238. package/.output/server/_libs/property-information.mjs +0 -1209
  239. package/.output/server/_libs/radix-ui.mjs +0 -1
  240. package/.output/server/_libs/radix-ui__number.mjs +0 -6
  241. package/.output/server/_libs/radix-ui__primitive.mjs +0 -11
  242. package/.output/server/_libs/radix-ui__react-accordion.mjs +0 -1
  243. package/.output/server/_libs/radix-ui__react-alert-dialog.mjs +0 -1
  244. package/.output/server/_libs/radix-ui__react-arrow.mjs +0 -23
  245. package/.output/server/_libs/radix-ui__react-aspect-ratio.mjs +0 -1
  246. package/.output/server/_libs/radix-ui__react-avatar.mjs +0 -1
  247. package/.output/server/_libs/radix-ui__react-checkbox.mjs +0 -1
  248. package/.output/server/_libs/radix-ui__react-collapsible.mjs +0 -144
  249. package/.output/server/_libs/radix-ui__react-collection.mjs +0 -69
  250. package/.output/server/_libs/radix-ui__react-compose-refs.mjs +0 -38
  251. package/.output/server/_libs/radix-ui__react-context-menu.mjs +0 -1
  252. package/.output/server/_libs/radix-ui__react-context.mjs +0 -64
  253. package/.output/server/_libs/radix-ui__react-dialog.mjs +0 -280
  254. package/.output/server/_libs/radix-ui__react-direction.mjs +0 -9
  255. package/.output/server/_libs/radix-ui__react-dropdown-menu.mjs +0 -1
  256. package/.output/server/_libs/radix-ui__react-focus-guards.mjs +0 -39
  257. package/.output/server/_libs/radix-ui__react-focus-scope.mjs +0 -210
  258. package/.output/server/_libs/radix-ui__react-form.mjs +0 -1
  259. package/.output/server/_libs/radix-ui__react-hover-card.mjs +0 -1
  260. package/.output/server/_libs/radix-ui__react-id.mjs +0 -14
  261. package/.output/server/_libs/radix-ui__react-label.mjs +0 -1
  262. package/.output/server/_libs/radix-ui__react-menu.mjs +0 -1
  263. package/.output/server/_libs/radix-ui__react-menubar.mjs +0 -1
  264. package/.output/server/_libs/radix-ui__react-popover.mjs +0 -1
  265. package/.output/server/_libs/radix-ui__react-popper.mjs +0 -340
  266. package/.output/server/_libs/radix-ui__react-portal.mjs +0 -16
  267. package/.output/server/_libs/radix-ui__react-presence.mjs +0 -173
  268. package/.output/server/_libs/radix-ui__react-primitive.mjs +0 -42
  269. package/.output/server/_libs/radix-ui__react-progress.mjs +0 -1
  270. package/.output/server/_libs/radix-ui__react-radio-group.mjs +0 -1
  271. package/.output/server/_libs/radix-ui__react-roving-focus.mjs +0 -235
  272. package/.output/server/_libs/radix-ui__react-scroll-area.mjs +0 -731
  273. package/.output/server/_libs/radix-ui__react-select.mjs +0 -1207
  274. package/.output/server/_libs/radix-ui__react-separator.mjs +0 -28
  275. package/.output/server/_libs/radix-ui__react-slider.mjs +0 -1
  276. package/.output/server/_libs/radix-ui__react-slot.mjs +0 -132
  277. package/.output/server/_libs/radix-ui__react-switch.mjs +0 -1
  278. package/.output/server/_libs/radix-ui__react-tabs.mjs +0 -194
  279. package/.output/server/_libs/radix-ui__react-toast.mjs +0 -2
  280. package/.output/server/_libs/radix-ui__react-toggle-group.mjs +0 -1
  281. package/.output/server/_libs/radix-ui__react-toggle.mjs +0 -1
  282. package/.output/server/_libs/radix-ui__react-toolbar.mjs +0 -1
  283. package/.output/server/_libs/radix-ui__react-tooltip.mjs +0 -498
  284. package/.output/server/_libs/radix-ui__react-use-previous.mjs +0 -14
  285. package/.output/server/_libs/radix-ui__react-use-size.mjs +0 -39
  286. package/.output/server/_libs/react-markdown.mjs +0 -235
  287. package/.output/server/_libs/react-remove-scroll-bar.mjs +0 -82
  288. package/.output/server/_libs/react-remove-scroll.mjs +0 -328
  289. package/.output/server/_libs/react-style-singleton.mjs +0 -69
  290. package/.output/server/_libs/remark-parse.mjs +0 -19
  291. package/.output/server/_libs/remark-rehype.mjs +0 -21
  292. package/.output/server/_libs/space-separated-tokens.mjs +0 -6
  293. package/.output/server/_libs/style-to-js.mjs +0 -72
  294. package/.output/server/_libs/style-to-object.mjs +0 -38
  295. package/.output/server/_libs/swr.mjs +0 -947
  296. package/.output/server/_libs/tailwind-merge.mjs +0 -3255
  297. package/.output/server/_libs/tanstack__react-virtual.mjs +0 -130
  298. package/.output/server/_libs/tanstack__virtual-core.mjs +0 -1319
  299. package/.output/server/_libs/three.mjs +0 -26016
  300. package/.output/server/_libs/trim-lines.mjs +0 -41
  301. package/.output/server/_libs/trough.mjs +0 -85
  302. package/.output/server/_libs/tslib.mjs +0 -1
  303. package/.output/server/_libs/ungap__structured-clone.mjs +0 -212
  304. package/.output/server/_libs/unified.mjs +0 -661
  305. package/.output/server/_libs/unist-util-is.mjs +0 -100
  306. package/.output/server/_libs/unist-util-position.mjs +0 -27
  307. package/.output/server/_libs/unist-util-stringify-position.mjs +0 -27
  308. package/.output/server/_libs/unist-util-visit-parents.mjs +0 -82
  309. package/.output/server/_libs/unist-util-visit.mjs +0 -24
  310. package/.output/server/_libs/use-callback-ref.mjs +0 -66
  311. package/.output/server/_libs/use-sidecar.mjs +0 -106
  312. package/.output/server/_libs/vfile-message.mjs +0 -138
  313. package/.output/server/_libs/vfile.mjs +0 -467
  314. package/.output/server/_sessionId-CJ99gZFV.mjs +0 -84
  315. package/.output/server/_sessionId-DEjScBpK.mjs +0 -146
  316. package/.output/server/_ssr/CompareDrawer-DtOIMeQB.mjs +0 -1060
  317. package/.output/server/_ssr/InspectorPet-CXc4oCBQ.mjs +0 -938
  318. package/.output/server/_ssr/ProxyViewerContainer-DVPlwfkt.mjs +0 -17632
  319. package/.output/server/_ssr/ReplayDialog-BQEA95qC.mjs +0 -725
  320. package/.output/server/_ssr/RequestAnatomy-BMyd8B7Q.mjs +0 -837
  321. package/.output/server/_ssr/ResponseView-qrTaEIM8.mjs +0 -757
  322. package/.output/server/_ssr/StreamingChunkSequence-BFTL8mIy.mjs +0 -274
  323. package/.output/server/_ssr/index-Bcj1Hcy4.mjs +0 -135
  324. package/.output/server/_ssr/index-DW-M7XUG.mjs +0 -84
  325. package/.output/server/_ssr/json-viewer-DiI0Yjsn.mjs +0 -483
  326. package/.output/server/_tanstack-start-manifest_v-C6pXXX9R.mjs +0 -4
  327. package/.output/server/node_modules/tslib/modules/index.js +0 -70
  328. package/.output/server/node_modules/tslib/modules/package.json +0 -3
  329. package/.output/server/node_modules/tslib/package.json +0 -47
  330. package/.output/server/node_modules/tslib/tslib.js +0 -484
  331. package/.output/server/package.json +0 -9
  332. package/src/routes/__root.tsx +0 -170
  333. /package/.output/{public → ui}/assets/agent-inspector-58K1_MsC.ico +0 -0
  334. /package/.output/{public → ui}/assets/alibaba-TTwafVwX.svg +0 -0
  335. /package/.output/{public → ui}/assets/minimax-BPMzvuL-.jpeg +0 -0
  336. /package/.output/{public → ui}/assets/qwen-mMn3f5ul.webp +0 -0
  337. /package/.output/{public → ui}/assets/zhipuai-BPNAnxo-.svg +0 -0
  338. /package/src/{routes → backend/routes}/livez.ts +0 -0
package/.output/cli.js CHANGED
@@ -18,6 +18,15 @@ var __export = (target, all) => {
18
18
  function trimSlashes(value) {
19
19
  return value.replace(/^\/+/u, "").replace(/\/+$/u, "");
20
20
  }
21
+ function normalizePublicBasePath(value) {
22
+ const trimmed = value?.trim() ?? "";
23
+ if (trimmed.length === 0 || trimmed === "/") return "";
24
+ if (!trimmed.startsWith("/") || trimmed.startsWith("//")) return null;
25
+ const normalized = normalizeBasePath(trimmed);
26
+ if (!SAFE_PUBLIC_BASE_PATH_PATTERN.test(normalized)) return null;
27
+ if (normalized.split("/").some((segment) => segment === "." || segment === "..")) return null;
28
+ return normalized;
29
+ }
21
30
  function normalizeBasePath(value) {
22
31
  const trimmed = value?.trim();
23
32
  if (trimmed === void 0 || trimmed.length === 0 || trimmed === "/") return "";
@@ -34,14 +43,10 @@ function readProcessBasePath() {
34
43
  return process.env[AGENT_INSPECTOR_BASE_PATH_ENV];
35
44
  }
36
45
  function getConfiguredBasePath(env) {
37
- if (env !== void 0) {
38
- return normalizeBasePath(
39
- env[AGENT_INSPECTOR_BASE_PATH_ENV] ?? DEFAULT_AGENT_INSPECTOR_BASE_PATH
40
- );
41
- }
42
- return normalizeBasePath(
43
- readProcessBasePath() ?? readDocumentBasePath() ?? import.meta.env?.BASE_URL ?? DEFAULT_AGENT_INSPECTOR_BASE_PATH
44
- );
46
+ const configured = env !== void 0 ? env[AGENT_INSPECTOR_BASE_PATH_ENV] ?? DEFAULT_AGENT_INSPECTOR_BASE_PATH : readProcessBasePath() ?? readDocumentBasePath() ?? import.meta.env?.BASE_URL ?? DEFAULT_AGENT_INSPECTOR_BASE_PATH;
47
+ const normalized = normalizePublicBasePath(configured);
48
+ if (normalized !== null) return normalized;
49
+ return normalizePublicBasePath(DEFAULT_AGENT_INSPECTOR_BASE_PATH) ?? "";
45
50
  }
46
51
  function withBasePath(path2, basePath = getConfiguredBasePath()) {
47
52
  const normalized = normalizeBasePath(basePath);
@@ -67,7 +72,7 @@ function stripBasePathFromRequestPath(rawPath, basePath) {
67
72
  const url2 = new URL(rawPath, "http://localhost");
68
73
  return `${stripBasePath(url2.pathname, basePath)}${url2.search}`;
69
74
  }
70
- var AGENT_INSPECTOR_BASE_PATH_ENV, AGENT_INSPECTOR_INTERNAL_STRIPPED_BASE_PATH_ENV, AGENT_INSPECTOR_BASE_PATH_META_NAME, AGENT_INSPECTOR_PUBLIC_BASE_PATH_HEADER, AGENT_INSPECTOR_PUBLIC_PORT_HEADER, DEFAULT_AGENT_INSPECTOR_BASE_PATH;
75
+ var AGENT_INSPECTOR_BASE_PATH_ENV, AGENT_INSPECTOR_INTERNAL_STRIPPED_BASE_PATH_ENV, AGENT_INSPECTOR_BASE_PATH_META_NAME, AGENT_INSPECTOR_PUBLIC_BASE_PATH_HEADER, AGENT_INSPECTOR_PUBLIC_PORT_HEADER, AGENT_INSPECTOR_PUBLIC_ORIGIN_HEADER, DEFAULT_AGENT_INSPECTOR_BASE_PATH, SAFE_PUBLIC_BASE_PATH_PATTERN;
71
76
  var init_basePath = __esm({
72
77
  "src/lib/basePath.ts"() {
73
78
  "use strict";
@@ -76,7 +81,9 @@ var init_basePath = __esm({
76
81
  AGENT_INSPECTOR_BASE_PATH_META_NAME = "agent-inspector-base-path";
77
82
  AGENT_INSPECTOR_PUBLIC_BASE_PATH_HEADER = "x-agent-inspector-public-base-path";
78
83
  AGENT_INSPECTOR_PUBLIC_PORT_HEADER = "x-agent-inspector-public-port";
84
+ AGENT_INSPECTOR_PUBLIC_ORIGIN_HEADER = "x-agent-inspector-public-origin";
79
85
  DEFAULT_AGENT_INSPECTOR_BASE_PATH = "/inspector";
86
+ SAFE_PUBLIC_BASE_PATH_PATTERN = /^\/[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*$/u;
80
87
  }
81
88
  });
82
89
 
@@ -139,6 +146,7 @@ var init_managedInstance = __esm({
139
146
 
140
147
  // src/cli/networkHints.ts
141
148
  import { networkInterfaces } from "node:os";
149
+ import { isIP } from "node:net";
142
150
  function localUrlForPort(port) {
143
151
  return `http://localhost:${String(port)}`;
144
152
  }
@@ -153,7 +161,9 @@ function isWildcardHost(host) {
153
161
  function isLoopbackHost(host) {
154
162
  const trimmed = trimHost(host);
155
163
  if (trimmed === void 0) return false;
156
- return trimmed === "localhost" || trimmed === "::1" || trimmed === "[::1]" || trimmed.startsWith("127.");
164
+ const normalized = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1).toLowerCase() : trimmed.toLowerCase();
165
+ if (normalized === "localhost" || normalized === "::1") return true;
166
+ return isIP(normalized) === 4 && normalized.split(".", 1)[0] === "127";
157
167
  }
158
168
  function bracketIpv6(host) {
159
169
  if (!host.includes(":")) return host;
@@ -215,20 +225,25 @@ function networkUrlsForBindHost(port, host, interfaces = networkInterfaces()) {
215
225
  function accessUrl(origin, path2, basePath) {
216
226
  return appendBasePathToOrigin(origin, path2, basePath);
217
227
  }
218
- function formatAccessHintLines(port, networkUrls, host = void 0, basePath = "") {
228
+ function appendSurfaceLines(lines, label, origin, basePath, uiEnabled) {
229
+ const baseUrl = accessUrl(origin, "/", basePath);
230
+ if (uiEnabled) lines.push(` ${label} UI: ${baseUrl}`);
231
+ lines.push(` ${label} control Base: ${baseUrl}`);
232
+ lines.push(` ${label} agent Base: ${baseUrl}`);
233
+ lines.push(` ${label} REST API: ${accessUrl(origin, "/api", basePath)}`);
234
+ lines.push(` ${label} proxy: ${accessUrl(origin, "/proxy", basePath)}`);
235
+ lines.push(` ${label} MCP: ${accessUrl(origin, "/api/mcp", basePath)}`);
236
+ }
237
+ function formatAccessHintLines(port, networkUrls, host = void 0, basePath = "", uiEnabled = true) {
219
238
  const normalizedBasePath = normalizeBasePath(basePath);
220
239
  const localUrl = localUrlForPort(port);
221
240
  const hostLabel = isWildcardHost(host) ? "all interfaces (0.0.0.0)" : trimHost(host) ?? "";
222
241
  const lines = ["Access URLs:", ` Bind host: ${hostLabel}`];
223
242
  if (isLoopbackHost(host) || isWildcardHost(host)) {
224
- lines.push(` Local UI: ${accessUrl(localUrl, "/", normalizedBasePath)}`);
225
- lines.push(` Local proxy: ${accessUrl(localUrl, "/proxy", normalizedBasePath)}`);
226
- lines.push(` Local MCP: ${accessUrl(localUrl, "/api/mcp", normalizedBasePath)}`);
243
+ appendSurfaceLines(lines, "Local", localUrl, normalizedBasePath, uiEnabled);
227
244
  } else {
228
245
  const boundUrl = urlForHost(port, host);
229
- lines.push(` Bound UI: ${accessUrl(boundUrl, "/", normalizedBasePath)}`);
230
- lines.push(` Bound proxy: ${accessUrl(boundUrl, "/proxy", normalizedBasePath)}`);
231
- lines.push(` Bound MCP: ${accessUrl(boundUrl, "/api/mcp", normalizedBasePath)}`);
246
+ appendSurfaceLines(lines, "Bound", boundUrl, normalizedBasePath, uiEnabled);
232
247
  lines.push(" Localhost: may not work because the server is bound to a specific host");
233
248
  }
234
249
  const firstNetworkUrl = networkUrls[0];
@@ -237,8 +252,7 @@ function formatAccessHintLines(port, networkUrls, host = void 0, basePath = "")
237
252
  } else if (firstNetworkUrl === void 0) {
238
253
  lines.push(" Network: no non-loopback IPv4 address detected");
239
254
  } else {
240
- lines.push(` Network UI: ${accessUrl(firstNetworkUrl, "/", normalizedBasePath)}`);
241
- lines.push(` Network proxy: ${accessUrl(firstNetworkUrl, "/proxy", normalizedBasePath)}`);
255
+ appendSurfaceLines(lines, "Network", firstNetworkUrl, normalizedBasePath, uiEnabled);
242
256
  const moreUrls = networkUrls.slice(1);
243
257
  if (moreUrls.length > 0) {
244
258
  lines.push(` Other IPs: ${moreUrls.join(", ")}`);
@@ -15337,10 +15351,103 @@ var init_zod = __esm({
15337
15351
  }
15338
15352
  });
15339
15353
 
15354
+ // src/lib/browserRuntimeContract.ts
15355
+ function parseAbsoluteUrl(value) {
15356
+ try {
15357
+ return new URL(value);
15358
+ } catch {
15359
+ return null;
15360
+ }
15361
+ }
15362
+ function hasUnsafePathSegment(pathname) {
15363
+ return pathname.split("/").some((segment) => segment === "." || segment === ".." || /%(?:2e|2f|5c)/iu.test(segment));
15364
+ }
15365
+ function hasControlCharacter(value) {
15366
+ return [...value].some((character) => {
15367
+ const code = character.charCodeAt(0);
15368
+ return code < 32 || code === 127;
15369
+ });
15370
+ }
15371
+ function normalizeBrowserRuntimeBasePath(value) {
15372
+ const trimmed = value.trim();
15373
+ if (trimmed.length === 0) return null;
15374
+ const normalized = normalizePublicBasePath(trimmed);
15375
+ if (normalized === null) return null;
15376
+ return normalized.length === 0 ? "/" : normalized;
15377
+ }
15378
+ function normalizeBrowserRuntimeBaseUrl(value) {
15379
+ if (value.trim() !== value || value.includes("\\") || hasControlCharacter(value) || hasUnsafePathSegment(value)) {
15380
+ return null;
15381
+ }
15382
+ const parsed = parseAbsoluteUrl(value);
15383
+ if (parsed === null) return null;
15384
+ switch (parsed.protocol) {
15385
+ case "http:":
15386
+ case "https:":
15387
+ break;
15388
+ default:
15389
+ return null;
15390
+ }
15391
+ if (parsed.username.length > 0 || parsed.password.length > 0 || parsed.search.length > 0 || parsed.hash.length > 0 || hasUnsafePathSegment(parsed.pathname)) {
15392
+ return null;
15393
+ }
15394
+ const basePath = normalizeBrowserRuntimeBasePath(parsed.pathname);
15395
+ if (basePath === null) return null;
15396
+ return basePath === "/" ? parsed.origin : `${parsed.origin}${basePath}`;
15397
+ }
15398
+ var BROWSER_RUNTIME_SCHEMA_VERSION, BROWSER_RUNTIME_JSON_PLACEHOLDER, BrowserRuntimeBasePathSchema, BrowserRuntimeBaseUrlSchema, BrowserRuntimeCapabilitiesSchema, BrowserRuntimeDeploymentSchema;
15399
+ var init_browserRuntimeContract = __esm({
15400
+ "src/lib/browserRuntimeContract.ts"() {
15401
+ "use strict";
15402
+ init_zod();
15403
+ init_basePath();
15404
+ BROWSER_RUNTIME_SCHEMA_VERSION = 1;
15405
+ BROWSER_RUNTIME_JSON_PLACEHOLDER = "__AGENT_INSPECTOR_RUNTIME_JSON__";
15406
+ BrowserRuntimeBasePathSchema = external_exports.string().min(1).max(2048).refine(
15407
+ (value) => normalizeBrowserRuntimeBasePath(value) !== null,
15408
+ "UI Base Path must be an absolute path without query, hash, backslash, or traversal"
15409
+ ).transform((value) => normalizeBrowserRuntimeBasePath(value) ?? value);
15410
+ BrowserRuntimeBaseUrlSchema = external_exports.string().min(1).max(2048).refine(
15411
+ (value) => normalizeBrowserRuntimeBaseUrl(value) !== null,
15412
+ "Runtime Base URL must be an HTTP(S) URL without credentials, query, hash, or traversal"
15413
+ ).transform((value) => normalizeBrowserRuntimeBaseUrl(value) ?? value);
15414
+ BrowserRuntimeCapabilitiesSchema = external_exports.object({
15415
+ rest: external_exports.boolean(),
15416
+ sse: external_exports.boolean(),
15417
+ proxy: external_exports.boolean(),
15418
+ mcp: external_exports.boolean()
15419
+ }).strict();
15420
+ BrowserRuntimeDeploymentSchema = external_exports.object({
15421
+ schemaVersion: external_exports.literal(BROWSER_RUNTIME_SCHEMA_VERSION),
15422
+ deploymentMode: external_exports.enum(["composed", "backend-only", "separated"]),
15423
+ ui: external_exports.object({
15424
+ basePath: BrowserRuntimeBasePathSchema
15425
+ }).strict(),
15426
+ control: external_exports.object({
15427
+ baseUrl: BrowserRuntimeBaseUrlSchema
15428
+ }).strict(),
15429
+ agent: external_exports.object({
15430
+ baseUrl: BrowserRuntimeBaseUrlSchema
15431
+ }).strict(),
15432
+ capabilities: BrowserRuntimeCapabilitiesSchema
15433
+ }).strict();
15434
+ }
15435
+ });
15436
+
15340
15437
  // src/proxy/privateDataPath.ts
15341
15438
  import { spawnSync } from "node:child_process";
15342
- import { chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, statSync } from "node:fs";
15343
- import { dirname, join } from "node:path";
15439
+ import {
15440
+ closeSync,
15441
+ constants,
15442
+ fchmodSync,
15443
+ fstatSync,
15444
+ lstatSync,
15445
+ mkdirSync,
15446
+ openSync,
15447
+ readdirSync,
15448
+ realpathSync
15449
+ } from "node:fs";
15450
+ import { join, parse as parse3, resolve, sep } from "node:path";
15344
15451
  function windowsSystemPath(executable) {
15345
15452
  const systemRoot = process.env["SystemRoot"] ?? "C:\\Windows";
15346
15453
  return `${systemRoot}\\System32\\${executable}`;
@@ -15369,9 +15476,7 @@ function resolveWindowsUserSid(run) {
15369
15476
  function windowsBroadGroupRemovalArgs() {
15370
15477
  return ["/remove:g", "*S-1-1-0", "*S-1-5-11", "*S-1-5-32-545"];
15371
15478
  }
15372
- function applyWindowsUserOnlyAcl(path2, kind, run = runWindowsCommand) {
15373
- const sid = resolveWindowsUserSid(run);
15374
- if (sid === null) return false;
15479
+ function applyWindowsAclWithSid(path2, kind, sid, run) {
15375
15480
  const access = kind === "directory" ? `(OI)(CI)F` : "F";
15376
15481
  const result = run("icacls.exe", [
15377
15482
  path2,
@@ -15383,112 +15488,244 @@ function applyWindowsUserOnlyAcl(path2, kind, run = runWindowsCommand) {
15383
15488
  ]);
15384
15489
  return result.status === 0;
15385
15490
  }
15386
- function applyWindowsUserOnlyTreeAcl(root, run = runWindowsCommand) {
15491
+ function applyWindowsUserOnlyAcl(path2, kind, run = runWindowsCommand) {
15387
15492
  const sid = resolveWindowsUserSid(run);
15388
- if (sid === null) return false;
15389
- const result = run("icacls.exe", [
15390
- root,
15391
- "/inheritance:r",
15392
- "/grant:r",
15393
- `*${sid}:(OI)(CI)F`,
15394
- `*${sid}:F`,
15395
- ...windowsBroadGroupRemovalArgs(),
15396
- "/T",
15397
- "/C",
15398
- "/Q"
15399
- ]);
15400
- return result.status === 0;
15493
+ return sid !== null && applyWindowsAclWithSid(path2, kind, sid, run);
15401
15494
  }
15402
- function applyPrivatePermissionsSync(path2, kind) {
15495
+ function comparablePath(path2) {
15496
+ let normalized = path2;
15403
15497
  if (process.platform === "win32") {
15404
- return applyWindowsUserOnlyAcl(path2, kind);
15405
- }
15406
- try {
15407
- chmodSync(path2, kind === "directory" ? PRIVATE_DIRECTORY_MODE : PRIVATE_FILE_MODE);
15408
- return true;
15409
- } catch {
15410
- return false;
15498
+ if (normalized.startsWith("\\\\?\\UNC\\")) {
15499
+ normalized = `\\\\${normalized.slice(8)}`;
15500
+ } else if (normalized.startsWith("\\\\?\\")) {
15501
+ normalized = normalized.slice(4);
15502
+ }
15411
15503
  }
15504
+ const absolute = resolve(normalized);
15505
+ return process.platform === "win32" ? absolute.toLowerCase() : absolute;
15412
15506
  }
15413
- function secureOnce(path2, kind) {
15414
- const cacheKey = `${kind}:${path2}`;
15415
- if (securedPaths.has(cacheKey)) return true;
15416
- if (!applyPrivatePermissionsSync(path2, kind)) return false;
15417
- securedPaths.add(cacheKey);
15418
- return true;
15507
+ function pathIdentity(stats) {
15508
+ return `${String(stats.dev)}:${String(stats.ino)}:${String(stats.birthtimeMs)}`;
15419
15509
  }
15420
- function ensurePrivateDirectorySync(path2) {
15510
+ function readPathEntry(path2) {
15511
+ let stats;
15421
15512
  try {
15422
- mkdirSync(path2, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
15513
+ const observed = lstatSync(path2);
15514
+ if (observed === void 0) return { status: "missing" };
15515
+ stats = observed;
15423
15516
  } catch {
15424
- return false;
15517
+ return { status: "missing" };
15425
15518
  }
15426
- return secureOnce(path2, "directory");
15427
- }
15428
- function securePrivateFileSync(path2, forceAcl = false) {
15429
- let identity;
15519
+ if (stats.isSymbolicLink()) return { status: "unsafe" };
15520
+ const kind = stats.isDirectory() ? "directory" : stats.isFile() ? "file" : null;
15521
+ if (kind === null) return { status: "unsafe" };
15522
+ let canonical;
15430
15523
  try {
15431
- const stats = statSync(path2);
15432
- identity = `${String(stats.dev)}:${String(stats.ino)}:${String(stats.birthtimeMs)}`;
15524
+ canonical = realpathSync.native(path2);
15433
15525
  } catch {
15434
- return false;
15526
+ return { status: "unsafe" };
15435
15527
  }
15436
- if (securedFileIdentities.get(path2) === identity) return true;
15437
- if (process.platform === "win32" && !forceAcl && securedPaths.has(`directory:${dirname(path2)}`)) {
15438
- securedFileIdentities.set(path2, identity);
15439
- return true;
15528
+ if (comparablePath(canonical) !== comparablePath(path2)) return { status: "unsafe" };
15529
+ return {
15530
+ status: "ok",
15531
+ entry: {
15532
+ path: path2,
15533
+ kind,
15534
+ identity: pathIdentity(stats),
15535
+ mode: stats.mode & 511
15536
+ }
15537
+ };
15538
+ }
15539
+ function snapshotPathChain(path2) {
15540
+ const absolutePath = resolve(path2);
15541
+ const root = parse3(absolutePath).root;
15542
+ const suffix = absolutePath.slice(root.length);
15543
+ const parts = suffix === "" ? [] : suffix.split(sep).filter((part) => part !== "");
15544
+ const entries = [];
15545
+ let current = root;
15546
+ const rootRead = readPathEntry(root);
15547
+ if (rootRead.status !== "ok" || rootRead.entry.kind !== "directory") return null;
15548
+ entries.push(rootRead.entry);
15549
+ for (let index = 0; index < parts.length; index += 1) {
15550
+ const part = parts[index];
15551
+ if (part === void 0) return null;
15552
+ current = join(current, part);
15553
+ const read = readPathEntry(current);
15554
+ if (read.status === "unsafe") return null;
15555
+ if (read.status === "missing") {
15556
+ return {
15557
+ absolutePath,
15558
+ complete: false,
15559
+ entries,
15560
+ missingParts: parts.slice(index)
15561
+ };
15562
+ }
15563
+ if (index < parts.length - 1 && read.entry.kind !== "directory") return null;
15564
+ entries.push(read.entry);
15440
15565
  }
15441
- if (!applyPrivatePermissionsSync(path2, "file")) return false;
15442
- securedFileIdentities.set(path2, identity);
15443
- return true;
15566
+ return { absolutePath, complete: true, entries, missingParts: [] };
15444
15567
  }
15445
- function securePrivateTreeSync(root) {
15446
- if (!existsSync(root)) return false;
15447
- let stats;
15568
+ function chainFingerprint(snapshot) {
15569
+ return snapshot.entries.map((entry) => `${entry.path}:${entry.identity}`).join("|");
15570
+ }
15571
+ function snapshotsRetainPrefix(before, after) {
15572
+ if (after.entries.length < before.entries.length) return false;
15573
+ return before.entries.every((entry, index) => {
15574
+ const next = after.entries[index];
15575
+ return next !== void 0 && next.path === entry.path && next.identity === entry.identity;
15576
+ });
15577
+ }
15578
+ function completeSnapshotForKind(path2, kind) {
15579
+ const snapshot = snapshotPathChain(path2);
15580
+ if (snapshot === null || !snapshot.complete) return null;
15581
+ const leaf = snapshot.entries.at(-1);
15582
+ return leaf !== void 0 && leaf.kind === kind ? snapshot : null;
15583
+ }
15584
+ function applyPosixPermissions(path2, kind, expectedIdentity2) {
15585
+ let descriptor = null;
15448
15586
  try {
15449
- stats = lstatSync(root);
15587
+ const directoryFlag = kind === "directory" ? constants.O_DIRECTORY : 0;
15588
+ descriptor = openSync(path2, constants.O_RDONLY | constants.O_NOFOLLOW | directoryFlag);
15589
+ const before = fstatSync(descriptor);
15590
+ if (pathIdentity(before) !== expectedIdentity2) return false;
15591
+ fchmodSync(descriptor, kind === "directory" ? PRIVATE_DIRECTORY_MODE : PRIVATE_FILE_MODE);
15592
+ const after = fstatSync(descriptor);
15593
+ return pathIdentity(after) === expectedIdentity2 && (after.mode & 511) === (kind === "directory" ? PRIVATE_DIRECTORY_MODE : PRIVATE_FILE_MODE);
15450
15594
  } catch {
15451
15595
  return false;
15596
+ } finally {
15597
+ if (descriptor !== null) {
15598
+ try {
15599
+ closeSync(descriptor);
15600
+ } catch {
15601
+ }
15602
+ }
15452
15603
  }
15453
- if (stats.isSymbolicLink()) return true;
15454
- if (!stats.isDirectory()) return securePrivateFileSync(root, true);
15455
- if (process.platform === "win32") {
15456
- if (!applyWindowsUserOnlyTreeAcl(root)) return false;
15457
- securedPaths.add(`directory:${root}`);
15458
- return true;
15604
+ }
15605
+ function permissionsArePrivate(entry) {
15606
+ if (process.platform === "win32") return true;
15607
+ return entry.mode === (entry.kind === "directory" ? PRIVATE_DIRECTORY_MODE : PRIVATE_FILE_MODE);
15608
+ }
15609
+ function secureExistingPath(path2, kind, forcePermissions = false) {
15610
+ const before = completeSnapshotForKind(path2, kind);
15611
+ if (before === null) return false;
15612
+ const leaf = before.entries.at(-1);
15613
+ if (leaf === void 0) return false;
15614
+ const cacheKey = `${kind}:${before.absolutePath}`;
15615
+ const beforeFingerprint = chainFingerprint(before);
15616
+ if (!forcePermissions && securedPathFingerprints.get(cacheKey) === beforeFingerprint && permissionsArePrivate(leaf)) {
15617
+ const cachedCheck = completeSnapshotForKind(path2, kind);
15618
+ return cachedCheck !== null && chainFingerprint(cachedCheck) === beforeFingerprint;
15619
+ }
15620
+ const applied = process.platform === "win32" ? applyWindowsUserOnlyAcl(path2, kind) : applyPosixPermissions(path2, kind, leaf.identity);
15621
+ if (!applied) return false;
15622
+ const after = completeSnapshotForKind(path2, kind);
15623
+ if (after === null || !snapshotsRetainPrefix(before, after)) return false;
15624
+ const afterLeaf = after.entries.at(-1);
15625
+ if (afterLeaf === void 0 || afterLeaf.identity !== leaf.identity) return false;
15626
+ if (!permissionsArePrivate(afterLeaf)) return false;
15627
+ securedPathFingerprints.set(cacheKey, chainFingerprint(after));
15628
+ return true;
15629
+ }
15630
+ function ensurePrivateDirectorySync(path2) {
15631
+ const before = snapshotPathChain(path2);
15632
+ if (before === null) return false;
15633
+ let current = before.entries.at(-1)?.path;
15634
+ if (current === void 0) return false;
15635
+ for (const part of before.missingParts) {
15636
+ const next = join(current, part);
15637
+ try {
15638
+ mkdirSync(next, { mode: PRIVATE_DIRECTORY_MODE });
15639
+ } catch {
15640
+ const raced = readPathEntry(next);
15641
+ if (raced.status !== "ok" || raced.entry.kind !== "directory") return false;
15642
+ }
15643
+ if (!secureExistingPath(next, "directory", true)) return false;
15644
+ current = next;
15459
15645
  }
15460
- if (!ensurePrivateDirectorySync(root)) return false;
15461
- let entries;
15462
- try {
15463
- entries = readdirSync(root);
15464
- } catch {
15465
- return false;
15646
+ const complete = completeSnapshotForKind(path2, "directory");
15647
+ if (complete === null || !snapshotsRetainPrefix(before, complete)) return false;
15648
+ return secureExistingPath(path2, "directory");
15649
+ }
15650
+ function securePrivateFileSync(path2, forceAcl = false) {
15651
+ return secureExistingPath(path2, "file", forceAcl);
15652
+ }
15653
+ function collectSafeTree(root) {
15654
+ const collected = [];
15655
+ const visit = (path2) => {
15656
+ const snapshot = snapshotPathChain(path2);
15657
+ if (snapshot === null || !snapshot.complete) return false;
15658
+ const leaf = snapshot.entries.at(-1);
15659
+ if (leaf === void 0) return false;
15660
+ const fingerprint = chainFingerprint(snapshot);
15661
+ collected.push({ path: snapshot.absolutePath, kind: leaf.kind, fingerprint });
15662
+ if (leaf.kind === "file") return true;
15663
+ let entries;
15664
+ try {
15665
+ entries = readdirSync(path2);
15666
+ } catch {
15667
+ return false;
15668
+ }
15669
+ const afterRead = completeSnapshotForKind(path2, "directory");
15670
+ if (afterRead === null || chainFingerprint(afterRead) !== fingerprint) return false;
15671
+ for (const entry of entries) {
15672
+ if (!visit(join(path2, entry))) return false;
15673
+ }
15674
+ return true;
15675
+ };
15676
+ return visit(root) ? collected : null;
15677
+ }
15678
+ function treeFingerprintsMatch(expected, observed) {
15679
+ if (observed === null || observed.length !== expected.length) return false;
15680
+ return expected.every((entry, index) => {
15681
+ const next = observed[index];
15682
+ return next !== void 0 && next.path === entry.path && next.kind === entry.kind && next.fingerprint === entry.fingerprint;
15683
+ });
15684
+ }
15685
+ function isPathTreeFreeOfIndirectionSync(root) {
15686
+ return collectSafeTree(root) !== null;
15687
+ }
15688
+ function applyWindowsUserOnlyTreeAcl(root, run = runWindowsCommand) {
15689
+ const entries = collectSafeTree(root);
15690
+ if (entries === null) return false;
15691
+ const sid = resolveWindowsUserSid(run);
15692
+ if (sid === null) return false;
15693
+ for (const entry of entries) {
15694
+ const before = completeSnapshotForKind(entry.path, entry.kind);
15695
+ if (before === null || chainFingerprint(before) !== entry.fingerprint) return false;
15696
+ if (!applyWindowsAclWithSid(entry.path, entry.kind, sid, run)) return false;
15697
+ const after = completeSnapshotForKind(entry.path, entry.kind);
15698
+ if (after === null || chainFingerprint(after) !== entry.fingerprint) return false;
15699
+ securedPathFingerprints.set(`${entry.kind}:${entry.path}`, entry.fingerprint);
15466
15700
  }
15467
- let secured = true;
15701
+ return treeFingerprintsMatch(entries, collectSafeTree(root));
15702
+ }
15703
+ function securePrivateTreeSync(root) {
15704
+ const entries = collectSafeTree(root);
15705
+ if (entries === null) return false;
15706
+ if (process.platform === "win32") return applyWindowsUserOnlyTreeAcl(root);
15468
15707
  for (const entry of entries) {
15469
- const childPath = join(root, entry);
15470
- if (!securePrivateTreeSync(childPath)) secured = false;
15708
+ if (!secureExistingPath(entry.path, entry.kind, true)) return false;
15471
15709
  }
15472
- return secured;
15710
+ return treeFingerprintsMatch(entries, collectSafeTree(root));
15473
15711
  }
15474
- var PRIVATE_DIRECTORY_MODE, PRIVATE_FILE_MODE, securedPaths, securedFileIdentities, cachedWindowsSid;
15712
+ var PRIVATE_DIRECTORY_MODE, PRIVATE_FILE_MODE, securedPathFingerprints, cachedWindowsSid;
15475
15713
  var init_privateDataPath = __esm({
15476
15714
  "src/proxy/privateDataPath.ts"() {
15477
15715
  "use strict";
15478
15716
  PRIVATE_DIRECTORY_MODE = 448;
15479
15717
  PRIVATE_FILE_MODE = 384;
15480
- securedPaths = /* @__PURE__ */ new Set();
15481
- securedFileIdentities = /* @__PURE__ */ new Map();
15718
+ securedPathFingerprints = /* @__PURE__ */ new Map();
15482
15719
  cachedWindowsSid = null;
15483
15720
  }
15484
15721
  });
15485
15722
 
15486
15723
  // src/proxy/dataDir.ts
15487
- import { cpSync, existsSync as existsSync2, rmSync } from "node:fs";
15724
+ import { cpSync, existsSync, readdirSync as readdirSync2, renameSync, rmSync } from "node:fs";
15488
15725
  import { isAbsolute, join as join2 } from "node:path";
15489
15726
  function getDataDir() {
15490
15727
  migrateLegacyDataDirs();
15491
- return resolveDataDir(existsSync2);
15728
+ return resolveDataDir(existsSync);
15492
15729
  }
15493
15730
  function hasDataDirState(pathExists, dir) {
15494
15731
  return pathExists(join2(dir, "providers.json")) || pathExists(join2(dir, "config.json")) || pathExists(join2(dir, "logs")) || pathExists(join2(dir, "chunks"));
@@ -15515,16 +15752,42 @@ function migrateLegacyDataDirs() {
15515
15752
  if (hasExplicitDataDir()) return;
15516
15753
  const base = defaultBaseDir();
15517
15754
  const currentDir = join2(base, CURRENT_DATA_DIR_NAME);
15518
- if (hasDataDirState(existsSync2, currentDir)) return;
15755
+ if (hasDataDirState(existsSync, currentDir)) return;
15519
15756
  for (const legacyName of LEGACY_DATA_DIR_NAMES) {
15520
15757
  const legacyDir = join2(base, legacyName);
15521
- if (!existsSync2(legacyDir)) continue;
15758
+ if (!existsSync(legacyDir)) continue;
15759
+ const stagingDir = join2(
15760
+ base,
15761
+ `${CURRENT_DATA_DIR_NAME}.migration-${String(process.pid)}-${String(Date.now())}`
15762
+ );
15522
15763
  try {
15523
- if (!ensurePrivateDirectorySync(currentDir)) return;
15524
- cpSync(legacyDir, currentDir, { recursive: true, force: true });
15525
- if (!securePrivateTreeSync(currentDir)) return;
15764
+ if (!isPathTreeFreeOfIndirectionSync(legacyDir)) return;
15765
+ if (existsSync(currentDir)) {
15766
+ if (readdirSync2(currentDir).length > 0) return;
15767
+ rmSync(currentDir, { recursive: true, force: false });
15768
+ }
15769
+ if (!ensurePrivateDirectorySync(stagingDir)) return;
15770
+ for (const entry of readdirSync2(legacyDir)) {
15771
+ cpSync(join2(legacyDir, entry), join2(stagingDir, entry), {
15772
+ recursive: true,
15773
+ force: false,
15774
+ errorOnExist: true
15775
+ });
15776
+ }
15777
+ if (!isPathTreeFreeOfIndirectionSync(legacyDir)) {
15778
+ rmSync(stagingDir, { recursive: true, force: true });
15779
+ return;
15780
+ }
15781
+ if (!securePrivateTreeSync(stagingDir)) {
15782
+ rmSync(stagingDir, { recursive: true, force: true });
15783
+ return;
15784
+ }
15785
+ renameSync(stagingDir, currentDir);
15526
15786
  rmSync(legacyDir, { recursive: true, force: true });
15787
+ return;
15527
15788
  } catch {
15789
+ if (existsSync(stagingDir)) rmSync(stagingDir, { recursive: true, force: true });
15790
+ return;
15528
15791
  }
15529
15792
  }
15530
15793
  }
@@ -15539,9 +15802,9 @@ var init_dataDir = __esm({
15539
15802
  });
15540
15803
 
15541
15804
  // src/proxy/platformCommands.ts
15542
- import { statSync as statSync2 } from "node:fs";
15805
+ import { statSync } from "node:fs";
15543
15806
  import { execFileSync } from "node:child_process";
15544
- import { delimiter, isAbsolute as isAbsolute2, join as join3 } from "node:path";
15807
+ import { delimiter, isAbsolute as isAbsolute2, join as join4 } from "node:path";
15545
15808
  function pathEntries(envPath) {
15546
15809
  if (envPath === void 0 || envPath.length === 0) return [];
15547
15810
  return envPath.split(delimiter).filter((entry) => entry.length > 0);
@@ -15554,10 +15817,10 @@ function windowsExecutableExtensions(envPathExt) {
15554
15817
  }
15555
15818
  function isExecutableCandidate(path2, platform) {
15556
15819
  try {
15557
- const stat3 = statSync2(path2);
15558
- if (!stat3.isFile()) return false;
15820
+ const stat4 = statSync(path2);
15821
+ if (!stat4.isFile()) return false;
15559
15822
  if (platform === "win32" || process.platform === "win32") return true;
15560
- return (stat3.mode & 73) !== 0;
15823
+ return (stat4.mode & 73) !== 0;
15561
15824
  } catch {
15562
15825
  return false;
15563
15826
  }
@@ -15565,14 +15828,14 @@ function isExecutableCandidate(path2, platform) {
15565
15828
  function executableCandidates(bin, platform, env) {
15566
15829
  if (isAbsolute2(bin)) return [bin];
15567
15830
  const entries = pathEntries(env["PATH"] ?? env["Path"] ?? env["path"]);
15568
- if (platform !== "win32") return entries.map((entry) => join3(entry, bin));
15831
+ if (platform !== "win32") return entries.map((entry) => join4(entry, bin));
15569
15832
  const hasExtension = /\.[^\\/]+$/.test(bin);
15570
15833
  const extensions = hasExtension ? [""] : windowsExecutableExtensions(env["PATHEXT"]);
15571
15834
  const candidates = [];
15572
15835
  for (const entry of entries) {
15573
15836
  for (const extension of extensions) {
15574
- candidates.push(join3(entry, `${bin}${extension.toLowerCase()}`));
15575
- candidates.push(join3(entry, `${bin}${extension.toUpperCase()}`));
15837
+ candidates.push(join4(entry, `${bin}${extension.toLowerCase()}`));
15838
+ candidates.push(join4(entry, `${bin}${extension.toUpperCase()}`));
15576
15839
  }
15577
15840
  }
15578
15841
  return candidates;
@@ -15693,7 +15956,7 @@ var init_platformCommands = __esm({
15693
15956
 
15694
15957
  // src/cli/detect-tools.ts
15695
15958
  import { existsSync as existsSync3 } from "node:fs";
15696
- import { join as join4 } from "node:path";
15959
+ import { join as join5 } from "node:path";
15697
15960
  import { homedir } from "node:os";
15698
15961
  function which(bin) {
15699
15962
  return resolveExecutableOnPath(bin);
@@ -15702,31 +15965,31 @@ function tryDir(path2) {
15702
15965
  return existsSync3(path2) ? path2 : null;
15703
15966
  }
15704
15967
  function detectClaudeCode() {
15705
- const configDir = tryDir(join4(homedir(), ".claude"));
15968
+ const configDir = tryDir(join5(homedir(), ".claude"));
15706
15969
  const bin = which("claude");
15707
15970
  if (configDir === null && bin === null) return { found: false };
15708
15971
  return { found: true, path: configDir ?? bin ?? "" };
15709
15972
  }
15710
15973
  function detectOpenCode() {
15711
- const configDir = tryDir(join4(homedir(), ".config", "opencode"));
15974
+ const configDir = tryDir(join5(homedir(), ".config", "opencode"));
15712
15975
  const bin = which("opencode");
15713
15976
  if (configDir === null && bin === null) return { found: false };
15714
15977
  return { found: true, path: configDir ?? bin ?? "" };
15715
15978
  }
15716
15979
  function detectMiMo() {
15717
- const configDir = tryDir(join4(homedir(), ".config", "mimocode")) ?? tryDir(join4(homedir(), ".mimocode"));
15980
+ const configDir = tryDir(join5(homedir(), ".config", "mimocode")) ?? tryDir(join5(homedir(), ".mimocode"));
15718
15981
  const bin = which("mimo");
15719
15982
  if (configDir === null && bin === null) return { found: false };
15720
15983
  return { found: true, path: configDir ?? bin ?? "" };
15721
15984
  }
15722
15985
  function detectCursor() {
15723
- const configDir = tryDir(join4(homedir(), ".cursor"));
15986
+ const configDir = tryDir(join5(homedir(), ".cursor"));
15724
15987
  const bin = which("cursor");
15725
15988
  if (configDir === null && bin === null) return { found: false };
15726
15989
  return { found: true, path: configDir ?? bin ?? "" };
15727
15990
  }
15728
15991
  function detectCody() {
15729
- const configDir = tryDir(join4(homedir(), ".config", "cody"));
15992
+ const configDir = tryDir(join5(homedir(), ".config", "cody"));
15730
15993
  const bin = which("cody");
15731
15994
  if (configDir === null && bin === null) return { found: false };
15732
15995
  return { found: true, path: configDir ?? bin ?? "" };
@@ -15786,7 +16049,7 @@ var init_runtimeConfig = __esm({
15786
16049
  });
15787
16050
 
15788
16051
  // src/cli/instanceModel.ts
15789
- import { randomBytes, randomUUID } from "node:crypto";
16052
+ import { randomBytes, randomUUID as randomUUID2 } from "node:crypto";
15790
16053
  import { isAbsolute as isAbsolute3 } from "node:path";
15791
16054
  function instanceRegistrySuccess(value) {
15792
16055
  return { kind: "success", value };
@@ -15803,19 +16066,13 @@ function validateInstanceName(rawName) {
15803
16066
  ) : instanceRegistrySuccess(normalized);
15804
16067
  }
15805
16068
  function normalizeInstanceBasePath(rawBasePath) {
15806
- const value = rawBasePath ?? "";
15807
- const trimmed = value.trim();
15808
- const hasControlCharacter = [...trimmed].some((character) => {
15809
- const codePoint = character.codePointAt(0) ?? 0;
15810
- return codePoint < 32 || codePoint === 127;
15811
- });
15812
- if (trimmed.length > 0 && (!trimmed.startsWith("/") || trimmed.includes("?") || trimmed.includes("#") || trimmed.includes("\\") || hasControlCharacter)) {
16069
+ const normalized = normalizePublicBasePath(rawBasePath);
16070
+ if (normalized === null) {
15813
16071
  return instanceRegistryError(
15814
16072
  "record_invalid",
15815
- "Instance Base Path must be an absolute URL path without a query, hash, backslash, or control character."
16073
+ "Instance Base Path must use URL-safe path segments without traversal, encoded separators, or markup characters."
15816
16074
  );
15817
16075
  }
15818
- const normalized = normalizeBasePath(trimmed);
15819
16076
  const reservedControlRoot = MANAGED_INSTANCE_CONTROL_PATH.replace(/\/instance$/u, "");
15820
16077
  if (normalized === reservedControlRoot || normalized.startsWith(`${reservedControlRoot}/`)) {
15821
16078
  return instanceRegistryError(
@@ -15827,7 +16084,7 @@ function normalizeInstanceBasePath(rawBasePath) {
15827
16084
  }
15828
16085
  function createLaunch(factory, now) {
15829
16086
  return {
15830
- launchId: factory.createLaunchId?.() ?? randomUUID(),
16087
+ launchId: factory.createLaunchId?.() ?? randomUUID2(),
15831
16088
  controlToken: factory.createControlToken?.() ?? randomBytes(32).toString("base64url"),
15832
16089
  startedAt: now.toISOString()
15833
16090
  };
@@ -15842,7 +16099,7 @@ function createInstanceRecord(input, factory = {}) {
15842
16099
  const candidate = {
15843
16100
  schemaVersion: INSTANCE_RECORD_SCHEMA_VERSION,
15844
16101
  name: name.value,
15845
- instanceId: factory.createInstanceId?.() ?? randomUUID(),
16102
+ instanceId: factory.createInstanceId?.() ?? randomUUID2(),
15846
16103
  createdAt: timestamp,
15847
16104
  updatedAt: timestamp,
15848
16105
  host: input.host.trim(),
@@ -15850,6 +16107,7 @@ function createInstanceRecord(input, factory = {}) {
15850
16107
  upstreamPort: input.upstreamPort,
15851
16108
  basePath: basePath.value,
15852
16109
  captureMode: input.captureMode,
16110
+ uiEnabled: input.uiEnabled ?? true,
15853
16111
  dataDir: input.dataDir,
15854
16112
  launch: createLaunch(factory, now)
15855
16113
  };
@@ -15911,6 +16169,7 @@ var init_instanceModel = __esm({
15911
16169
  upstreamPort: PortSchema,
15912
16170
  basePath: NormalizedBasePathSchema,
15913
16171
  captureMode: CaptureModeSchema,
16172
+ uiEnabled: external_exports.boolean().default(true),
15914
16173
  dataDir: external_exports.string().min(1).refine((path2) => isAbsolute3(path2)),
15915
16174
  launch: InstanceLaunchSchema
15916
16175
  }).strict().superRefine((record2, context) => {
@@ -15926,23 +16185,23 @@ var init_instanceModel = __esm({
15926
16185
  });
15927
16186
 
15928
16187
  // src/cli/instanceRegistry.ts
15929
- import { randomUUID as randomUUID2 } from "node:crypto";
16188
+ import { randomUUID as randomUUID3 } from "node:crypto";
15930
16189
  import {
15931
- closeSync,
16190
+ closeSync as closeSync3,
15932
16191
  existsSync as existsSync4,
15933
16192
  mkdirSync as mkdirSync2,
15934
- openSync,
16193
+ openSync as openSync3,
15935
16194
  readFileSync,
15936
- readdirSync as readdirSync2,
15937
- realpathSync,
15938
- renameSync,
16195
+ readdirSync as readdirSync3,
16196
+ realpathSync as realpathSync2,
16197
+ renameSync as renameSync2,
15939
16198
  rmSync as rmSync2,
15940
- statSync as statSync3,
16199
+ statSync as statSync2,
15941
16200
  writeFileSync
15942
16201
  } from "node:fs";
15943
16202
  import { createServer } from "node:net";
15944
16203
  import { homedir as homedir2 } from "node:os";
15945
- import { join as join5, posix, win32 } from "node:path";
16204
+ import { join as join6, posix, win32 } from "node:path";
15946
16205
  function getInstancesRoot(env = process.env, platform = process.platform) {
15947
16206
  const pathApi = platform === "win32" ? win32 : posix;
15948
16207
  const explicitRoot = env[AGENT_INSPECTOR_INSTANCES_DIR_ENV];
@@ -15965,13 +16224,13 @@ function getInstancesRoot(env = process.env, platform = process.platform) {
15965
16224
  function getInstancePaths(root, rawName) {
15966
16225
  const name = validateInstanceName(rawName);
15967
16226
  if (name.kind === "error") return name;
15968
- const instanceDir = join5(root, name.value);
16227
+ const instanceDir = join6(root, name.value);
15969
16228
  return instanceRegistrySuccess({
15970
16229
  root,
15971
16230
  instanceDir,
15972
- dataDir: join5(instanceDir, "data"),
15973
- recordPath: join5(instanceDir, INSTANCE_RECORD_FILE_NAME),
15974
- lockPath: join5(instanceDir, INSTANCE_LOCK_DIRECTORY_NAME)
16231
+ dataDir: join6(instanceDir, "data"),
16232
+ recordPath: join6(instanceDir, INSTANCE_RECORD_FILE_NAME),
16233
+ lockPath: join6(instanceDir, INSTANCE_LOCK_DIRECTORY_NAME)
15975
16234
  });
15976
16235
  }
15977
16236
  function ensureInstancesRoot(root = getInstancesRoot()) {
@@ -16011,7 +16270,7 @@ function listInstanceRecords(root) {
16011
16270
  if (!existsSync4(root)) return instanceRegistrySuccess([]);
16012
16271
  let entries;
16013
16272
  try {
16014
- entries = readdirSync2(root, { withFileTypes: true });
16273
+ entries = readdirSync3(root, { withFileTypes: true });
16015
16274
  } catch {
16016
16275
  return instanceRegistryError("registry_io", `Cannot read the instance registry at ${root}.`);
16017
16276
  }
@@ -16019,7 +16278,7 @@ function listInstanceRecords(root) {
16019
16278
  const names = /* @__PURE__ */ new Set();
16020
16279
  for (const entry of entries) {
16021
16280
  if (!entry.isDirectory()) continue;
16022
- const recordPath = join5(root, entry.name, INSTANCE_RECORD_FILE_NAME);
16281
+ const recordPath = join6(root, entry.name, INSTANCE_RECORD_FILE_NAME);
16023
16282
  if (!existsSync4(recordPath)) continue;
16024
16283
  const record2 = readRecordFile(recordPath);
16025
16284
  if (record2.kind === "error") return record2;
@@ -16066,16 +16325,16 @@ function writeInstanceRecord(root, record2) {
16066
16325
  `Cannot create or secure the data directory for instance ${validated.data.name}.`
16067
16326
  );
16068
16327
  }
16069
- const temporaryPath = join5(
16328
+ const temporaryPath = join6(
16070
16329
  paths.value.instanceDir,
16071
- `.${INSTANCE_RECORD_FILE_NAME}.${String(process.pid)}.${randomUUID2()}.tmp`
16330
+ `.${INSTANCE_RECORD_FILE_NAME}.${String(process.pid)}.${randomUUID3()}.tmp`
16072
16331
  );
16073
16332
  let descriptor = null;
16074
16333
  try {
16075
- descriptor = openSync(temporaryPath, "wx", PRIVATE_FILE_MODE);
16334
+ descriptor = openSync3(temporaryPath, "wx", PRIVATE_FILE_MODE);
16076
16335
  writeFileSync(descriptor, `${JSON.stringify(validated.data, null, 2)}
16077
16336
  `, "utf8");
16078
- closeSync(descriptor);
16337
+ closeSync3(descriptor);
16079
16338
  descriptor = null;
16080
16339
  if (!securePrivateFileSync(temporaryPath)) {
16081
16340
  cleanupFile(temporaryPath);
@@ -16084,12 +16343,12 @@ function writeInstanceRecord(root, record2) {
16084
16343
  `Cannot secure the saved record for instance ${validated.data.name}.`
16085
16344
  );
16086
16345
  }
16087
- renameSync(temporaryPath, paths.value.recordPath);
16346
+ renameSync2(temporaryPath, paths.value.recordPath);
16088
16347
  return instanceRegistrySuccess(validated.data);
16089
16348
  } catch {
16090
16349
  if (descriptor !== null) {
16091
16350
  try {
16092
- closeSync(descriptor);
16351
+ closeSync3(descriptor);
16093
16352
  } catch {
16094
16353
  }
16095
16354
  }
@@ -16116,7 +16375,7 @@ function defaultIsProcessAlive(pid) {
16116
16375
  }
16117
16376
  function readLockOwner(lockPath) {
16118
16377
  try {
16119
- const raw = JSON.parse(readFileSync(join5(lockPath, LOCK_OWNER_FILE_NAME), "utf8"));
16378
+ const raw = JSON.parse(readFileSync(join6(lockPath, LOCK_OWNER_FILE_NAME), "utf8"));
16120
16379
  const parsed = InstanceLockOwnerSchema.safeParse(raw);
16121
16380
  return parsed.success ? parsed.data : null;
16122
16381
  } catch {
@@ -16126,7 +16385,7 @@ function readLockOwner(lockPath) {
16126
16385
  function lockCreatedAt(lockPath, owner) {
16127
16386
  if (owner !== null) return Date.parse(owner.createdAt);
16128
16387
  try {
16129
- return statSync3(lockPath).mtimeMs;
16388
+ return statSync2(lockPath).mtimeMs;
16130
16389
  } catch {
16131
16390
  return null;
16132
16391
  }
@@ -16136,9 +16395,9 @@ function recoverStaleLock(lockPath, staleAfterMs, now, isProcessAlive) {
16136
16395
  const createdAt = lockCreatedAt(lockPath, owner);
16137
16396
  if (createdAt === null || now - createdAt < staleAfterMs) return false;
16138
16397
  if (owner !== null && isProcessAlive(owner.pid)) return false;
16139
- const recoveredPath = `${lockPath}.stale-${randomUUID2()}`;
16398
+ const recoveredPath = `${lockPath}.stale-${randomUUID3()}`;
16140
16399
  try {
16141
- renameSync(lockPath, recoveredPath);
16400
+ renameSync2(lockPath, recoveredPath);
16142
16401
  rmSync2(recoveredPath, { recursive: true, force: true });
16143
16402
  return true;
16144
16403
  } catch {
@@ -16169,7 +16428,7 @@ async function acquireDirectoryLock(lockPath, options) {
16169
16428
  const now = options.now ?? Date.now;
16170
16429
  const sleep3 = options.sleep ?? defaultSleep;
16171
16430
  const isProcessAlive = options.isProcessAlive ?? defaultIsProcessAlive;
16172
- const createToken = options.createToken ?? randomUUID2;
16431
+ const createToken = options.createToken ?? randomUUID3;
16173
16432
  const startedAt = now();
16174
16433
  while (now() - startedAt <= timeoutMs) {
16175
16434
  const token = createToken();
@@ -16195,7 +16454,7 @@ async function acquireDirectoryLock(lockPath, options) {
16195
16454
  token,
16196
16455
  createdAt: new Date(now()).toISOString()
16197
16456
  };
16198
- const ownerPath = join5(lockPath, LOCK_OWNER_FILE_NAME);
16457
+ const ownerPath = join6(lockPath, LOCK_OWNER_FILE_NAME);
16199
16458
  writeFileSync(ownerPath, `${JSON.stringify(owner)}
16200
16459
  `, {
16201
16460
  encoding: "utf8",
@@ -16225,7 +16484,7 @@ async function acquireDirectoryLock(lockPath, options) {
16225
16484
  }
16226
16485
  async function acquireRegistryLock(root, options = {}) {
16227
16486
  const ensured = ensureInstancesRoot(root);
16228
- return ensured.kind === "error" ? ensured : acquireDirectoryLock(join5(root, ROOT_LOCK_DIRECTORY_NAME), options);
16487
+ return ensured.kind === "error" ? ensured : acquireDirectoryLock(join6(root, ROOT_LOCK_DIRECTORY_NAME), options);
16229
16488
  }
16230
16489
  async function acquireInstanceLock(root, rawName, options = {}) {
16231
16490
  const paths = getInstancePaths(root, rawName);
@@ -16268,7 +16527,7 @@ function canonicalizeDataDirectory(rawPath, options = {}) {
16268
16527
  const resolvedPath = resolveRealPathWithMissingTail(
16269
16528
  absolutePath,
16270
16529
  pathApi,
16271
- options.realpath ?? realpathSync.native
16530
+ options.realpath ?? realpathSync2.native
16272
16531
  );
16273
16532
  const normalizedPath = pathApi.normalize(resolvedPath);
16274
16533
  const root = pathApi.parse(normalizedPath).root;
@@ -16419,18 +16678,21 @@ var init_instanceRegistry = __esm({
16419
16678
  });
16420
16679
 
16421
16680
  // src/cli/onboardTarget.ts
16422
- function targetFromBase(kind, instanceName, mcpName, port, baseUrl) {
16681
+ function targetFromSurfaces(kind, instanceName, mcpName, port, controlBaseUrl, agentBaseUrl, uiUrl) {
16423
16682
  return {
16424
16683
  kind,
16425
16684
  instanceName,
16426
16685
  mcpName,
16427
16686
  port,
16428
- baseUrl,
16429
- apiUrl: `${baseUrl}/api`,
16430
- healthUrl: `${baseUrl}/api/health`,
16431
- anthropicProxyUrl: `${baseUrl}/proxy`,
16432
- openAiProxyUrl: `${baseUrl}/proxy/v1`,
16433
- mcpUrl: `${baseUrl}/api/mcp`
16687
+ uiUrl,
16688
+ controlBaseUrl,
16689
+ agentBaseUrl,
16690
+ baseUrl: controlBaseUrl,
16691
+ apiUrl: `${controlBaseUrl}/api`,
16692
+ healthUrl: `${controlBaseUrl}/api/health`,
16693
+ anthropicProxyUrl: `${agentBaseUrl}/proxy`,
16694
+ openAiProxyUrl: `${agentBaseUrl}/proxy/v1`,
16695
+ mcpUrl: `${agentBaseUrl}/api/mcp`
16434
16696
  };
16435
16697
  }
16436
16698
  function validateMcpName(rawName) {
@@ -16438,29 +16700,35 @@ function validateMcpName(rawName) {
16438
16700
  return name === rawName && MCP_NAME_PATTERN.test(name) ? name : null;
16439
16701
  }
16440
16702
  function normalizeExplicitBaseUrl(rawBaseUrl) {
16441
- const candidate = rawBaseUrl.trim();
16442
- if (candidate.length === 0 || candidate !== rawBaseUrl || candidate.includes("?") || candidate.includes("#")) {
16703
+ const baseUrl = normalizeBrowserRuntimeBaseUrl(rawBaseUrl);
16704
+ if (baseUrl === null) return null;
16705
+ try {
16706
+ const parsed = new URL(baseUrl);
16707
+ const defaultPort = parsed.protocol === "https:" ? 443 : 80;
16708
+ const port = parsed.port === "" ? defaultPort : Number(parsed.port);
16709
+ return Number.isInteger(port) && port >= 1 && port <= 65535 ? { baseUrl, port } : null;
16710
+ } catch {
16443
16711
  return null;
16444
16712
  }
16713
+ }
16714
+ function normalizePublicOrigin(rawOrigin) {
16715
+ if (rawOrigin.trim() !== rawOrigin) return null;
16445
16716
  try {
16446
- const parsed = new URL(candidate);
16447
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.hostname.length === 0 || parsed.username.length > 0 || parsed.password.length > 0) {
16717
+ const parsed = new URL(rawOrigin);
16718
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.username.length > 0 || parsed.password.length > 0 || parsed.pathname !== "/" || parsed.search.length > 0 || parsed.hash.length > 0) {
16448
16719
  return null;
16449
16720
  }
16450
- const pathname = parsed.pathname.replace(/\/+$/u, "");
16451
- const baseUrl = `${parsed.origin}${pathname === "" ? "" : pathname}`;
16452
- const defaultPort = parsed.protocol === "https:" ? 443 : 80;
16453
- const port = parsed.port === "" ? defaultPort : Number(parsed.port);
16454
- return Number.isInteger(port) && port >= 1 && port <= 65535 ? { baseUrl, port } : null;
16721
+ return parsed.origin;
16455
16722
  } catch {
16456
16723
  return null;
16457
16724
  }
16458
16725
  }
16459
16726
  function resolveOnboardTarget(options, env = process.env) {
16460
- if (options.instanceName !== null && options.baseUrl !== null) {
16727
+ const hasExplicitSurface = options.baseUrl !== null || options.controlBaseUrl !== null && options.controlBaseUrl !== void 0 || options.agentBaseUrl !== null && options.agentBaseUrl !== void 0 || options.uiUrl !== void 0;
16728
+ if (options.instanceName !== null && hasExplicitSurface) {
16461
16729
  return {
16462
16730
  ok: false,
16463
- message: "--instance and --base-url are mutually exclusive"
16731
+ message: "--instance and explicit --base-url/--control-base-url/--agent-base-url/--ui-url options are mutually exclusive"
16464
16732
  };
16465
16733
  }
16466
16734
  const explicitMcpName = options.mcpName === null ? null : validateMcpName(options.mcpName);
@@ -16470,22 +16738,29 @@ function resolveOnboardTarget(options, env = process.env) {
16470
16738
  message: "--mcp-name must be 1-128 letters, digits, dots, hyphens, or underscores and start with a letter or digit"
16471
16739
  };
16472
16740
  }
16473
- if (options.baseUrl !== null) {
16474
- const explicit = normalizeExplicitBaseUrl(options.baseUrl);
16475
- if (explicit === null) {
16741
+ if (hasExplicitSurface) {
16742
+ const sharedRaw = options.baseUrl;
16743
+ const controlRaw = options.controlBaseUrl ?? sharedRaw ?? options.agentBaseUrl;
16744
+ const agentRaw = options.agentBaseUrl ?? sharedRaw ?? options.controlBaseUrl;
16745
+ const control = controlRaw === null || controlRaw === void 0 ? null : normalizeExplicitBaseUrl(controlRaw);
16746
+ const agent = agentRaw === null || agentRaw === void 0 ? null : normalizeExplicitBaseUrl(agentRaw);
16747
+ const ui = options.uiUrl === void 0 || options.uiUrl === null ? null : normalizeExplicitBaseUrl(options.uiUrl);
16748
+ if (control === null || agent === null || options.uiUrl !== void 0 && options.uiUrl !== null && ui === null) {
16476
16749
  return {
16477
16750
  ok: false,
16478
- message: "--base-url must be a credential-free HTTP or HTTPS URL without a query or fragment"
16751
+ message: "Explicit UI, control, and agent Base URLs must be credential-free HTTP(S) URLs without a query or fragment"
16479
16752
  };
16480
16753
  }
16481
16754
  return {
16482
16755
  ok: true,
16483
- target: targetFromBase(
16756
+ target: targetFromSurfaces(
16484
16757
  "base-url",
16485
16758
  null,
16486
16759
  explicitMcpName ?? "agent-inspector",
16487
- explicit.port,
16488
- explicit.baseUrl
16760
+ control.port,
16761
+ control.baseUrl,
16762
+ agent.baseUrl,
16763
+ ui?.baseUrl ?? null
16489
16764
  )
16490
16765
  };
16491
16766
  }
@@ -16508,36 +16783,50 @@ function resolveOnboardTarget(options, env = process.env) {
16508
16783
  const baseUrl2 = appendBasePathToOrigin(origin, "/", record2.value.basePath);
16509
16784
  return {
16510
16785
  ok: true,
16511
- target: targetFromBase(
16786
+ target: targetFromSurfaces(
16512
16787
  "instance",
16513
16788
  record2.value.name,
16514
16789
  explicitMcpName ?? `agent-inspector-${record2.value.name}`,
16515
16790
  record2.value.publicPort,
16516
- baseUrl2
16791
+ baseUrl2,
16792
+ baseUrl2,
16793
+ record2.value.uiEnabled ? baseUrl2 : null
16517
16794
  )
16518
16795
  };
16519
16796
  }
16520
- const baseUrl = appendBasePathToOrigin(DEFAULT_ORIGIN, "/", getConfiguredBasePath(env));
16797
+ const configuredPublicOrigin = env[AGENT_INSPECTOR_PUBLIC_ORIGIN_ENV];
16798
+ const publicOrigin = configuredPublicOrigin === void 0 ? DEFAULT_ORIGIN : normalizePublicOrigin(configuredPublicOrigin);
16799
+ if (publicOrigin === null) {
16800
+ return {
16801
+ ok: false,
16802
+ message: "AGENT_INSPECTOR_PUBLIC_ORIGIN must be an exact credential-free HTTP(S) origin without a path, query, or fragment"
16803
+ };
16804
+ }
16805
+ const baseUrl = appendBasePathToOrigin(publicOrigin, "/", getConfiguredBasePath(env));
16521
16806
  return {
16522
16807
  ok: true,
16523
- target: targetFromBase(
16808
+ target: targetFromSurfaces(
16524
16809
  "default",
16525
16810
  null,
16526
16811
  explicitMcpName ?? "agent-inspector",
16527
16812
  DEFAULT_PORT,
16528
- baseUrl
16813
+ baseUrl,
16814
+ baseUrl,
16815
+ env["AGENT_INSPECTOR_BACKEND_ONLY"] === "1" ? null : baseUrl
16529
16816
  )
16530
16817
  };
16531
16818
  }
16532
- var DEFAULT_PORT, DEFAULT_ORIGIN, MCP_NAME_PATTERN;
16819
+ var DEFAULT_PORT, DEFAULT_ORIGIN, AGENT_INSPECTOR_PUBLIC_ORIGIN_ENV, MCP_NAME_PATTERN;
16533
16820
  var init_onboardTarget = __esm({
16534
16821
  "src/cli/onboardTarget.ts"() {
16535
16822
  "use strict";
16536
16823
  init_basePath();
16824
+ init_browserRuntimeContract();
16537
16825
  init_instanceRegistry();
16538
16826
  init_networkHints();
16539
16827
  DEFAULT_PORT = 9527;
16540
16828
  DEFAULT_ORIGIN = `http://localhost:${String(DEFAULT_PORT)}`;
16829
+ AGENT_INSPECTOR_PUBLIC_ORIGIN_ENV = "AGENT_INSPECTOR_PUBLIC_ORIGIN";
16541
16830
  MCP_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
16542
16831
  }
16543
16832
  });
@@ -16564,22 +16853,28 @@ var init_command_onboard = __esm({
16564
16853
 
16565
16854
  // src/cli/templates/codex-skill-onboard.ts
16566
16855
  function renderCodexSkillOnboard(ctx) {
16567
- const { version: version2, baseUrl } = ctx;
16856
+ const { version: version2 } = ctx;
16857
+ const controlBaseUrl = ctx.controlBaseUrl ?? ctx.baseUrl;
16858
+ const agentBaseUrl = ctx.agentBaseUrl ?? ctx.baseUrl;
16859
+ const uiUrl = ctx.uiUrl === void 0 ? ctx.baseUrl : ctx.uiUrl;
16568
16860
  const mcpName = ctx.mcpName ?? "agent-inspector";
16569
16861
  const targetKind = ctx.targetKind ?? "default";
16570
16862
  const targetArguments = ctx.onboardTargetArguments ?? "";
16571
16863
  const instanceName = ctx.instanceName ?? null;
16572
- const healthUrl = `${baseUrl}/api/health`;
16573
- const configUrl = `${baseUrl}/api/config`;
16574
- const anthropicProxyUrl = `${baseUrl}/proxy`;
16575
- const openAiProxyUrl = `${baseUrl}/proxy/v1`;
16576
- const mcpUrl = `${baseUrl}/api/mcp`;
16864
+ const healthUrl = `${controlBaseUrl}/api/health`;
16865
+ const runtimeUrl = `${controlBaseUrl}/api/runtime`;
16866
+ const configUrl = `${controlBaseUrl}/api/config`;
16867
+ const anthropicProxyUrl = `${agentBaseUrl}/proxy`;
16868
+ const openAiProxyUrl = `${agentBaseUrl}/proxy/v1`;
16869
+ const mcpUrl = `${agentBaseUrl}/api/mcp`;
16577
16870
  const mcpTomlKey = /^[A-Za-z0-9_-]+$/u.test(mcpName) ? mcpName : JSON.stringify(mcpName);
16578
16871
  const statusCommand = `agent-inspector onboard --status${targetArguments}`;
16579
16872
  const refreshCommand = `agent-inspector onboard --force${targetArguments}`;
16580
- const startCommand = targetKind === "instance" && instanceName !== null ? `agent-inspector instance start ${instanceName}` : targetKind === "base-url" ? `# Explicit target ${baseUrl}: do not start a replacement local runtime.` : "agent-inspector --mode simple --background --no-open";
16873
+ const startCommand = targetKind === "instance" && instanceName !== null ? `agent-inspector instance start ${instanceName}` : targetKind === "base-url" ? `# Explicit control target ${controlBaseUrl}: do not start a replacement local runtime.` : "agent-inspector --mode simple --background --no-open";
16581
16874
  const fullModeCommand = targetKind === "instance" && instanceName !== null ? `agent-inspector instance restart ${instanceName} --mode full` : targetKind === "base-url" ? "ask the target operator to enable Full capture mode" : "agent-inspector --mode full --force-restart";
16582
- const startExplanation = targetKind === "base-url" ? `The selected Inspector target is the explicit Base URL \`${baseUrl}\`. Do not start the default local runtime as a substitute; verify the selected target and ask its operator for help if it is unavailable.` : `Start or reuse the selected Agent Inspector runtime before wiring MCP. Prefer the installed binary so Windows users get the branded runtime in Task Manager. Use \`${fullModeCommand}\` only when the user needs raw headers, raw response, or SSE chunks.`;
16875
+ const startExplanation = targetKind === "base-url" ? `The selected Inspector target uses control Base URL \`${controlBaseUrl}\` and agent Base URL \`${agentBaseUrl}\`. Do not start the default local runtime as a substitute; verify the selected target and ask its operator for help if it is unavailable.` : `Start or reuse the selected Agent Inspector backend before wiring MCP. The default command composes a UI, while backend-only named instances retain proxy, REST, health, and MCP without UI files. Prefer the installed binary so Windows users get the branded runtime in Task Manager. Use \`${fullModeCommand}\` only when the user needs raw headers, raw response, or SSE chunks.`;
16876
+ const selectedUi = uiUrl === null ? "not supplied (do not infer one from a backend URL)" : `\`${uiUrl}\``;
16877
+ const providerTestUiGuidance = uiUrl === null ? `No UI URL was supplied. Do not open \`${controlBaseUrl}\` or \`${agentBaseUrl}\` as a page; use the MCP Provider Test tools and log/session resources, or ask the operator for a standalone UI URL.` : `The operator supplied the explicit composed or standalone UI URL \`${uiUrl}\`. Use that URL for browser steps; do not substitute the control or agent Base URL.`;
16583
16878
  return `---
16584
16879
  name: agent-inspector-onboard
16585
16880
  description: Guide Codex users through connecting Agent Inspector v${version2} as a local MCP server.
@@ -16592,7 +16887,13 @@ metadata:
16592
16887
 
16593
16888
  Use this skill when the user wants to set up Agent Inspector inside Codex, connect the Agent Inspector MCP server, inspect captured model traffic from Codex, or turn Inspector sessions into reviewable knowledge candidates.
16594
16889
 
16595
- Agent Inspector runs a local web UI, proxy, REST API, and MCP endpoint on the same port. The default MCP endpoint is:
16890
+ The selected deployment has these explicit surfaces:
16891
+
16892
+ - UI URL: ${selectedUi}
16893
+ - Control Base URL: \`${controlBaseUrl}\`
16894
+ - Agent Base URL: \`${agentBaseUrl}\`
16895
+
16896
+ Composed mode may use one URL for all three, but backend-only mode and separately hosted UI deployments do not require UI and backend to share a process, origin, or port. The selected MCP endpoint is:
16596
16897
 
16597
16898
  \`\`\`text
16598
16899
  ${mcpUrl}
@@ -16610,6 +16911,8 @@ Do not configure both transports in the same MCP client, or the client may show
16610
16911
 
16611
16912
  Keep this setup local and explicit. Agent Inspector MCP tools can read captured requests and provider configuration, so only wire it into trusted local Codex environments.
16612
16913
 
16914
+ The backend publishes token-free runtime discovery at \`${runtimeUrl}\`. Treat \`backend-only\` as healthy when REST, proxy, and MCP capabilities are present.
16915
+
16613
16916
  Agent Inspector starts in **Simple mode** by default. Keep Simple mode for everyday capture: requests, responses, tools, timing, and token usage stay available without retaining heavier raw headers, raw response, or detailed SSE chunk artifacts. Use **Full mode** only when the user is debugging provider headers, raw response payloads, or SSE stream details.
16614
16917
 
16615
16918
  ---
@@ -16652,12 +16955,14 @@ ${startExplanation}
16652
16955
  \`\`\`powershell
16653
16956
  ${startCommand}
16654
16957
  Invoke-RestMethod -Uri "${healthUrl}" -TimeoutSec 3
16958
+ Invoke-RestMethod -Uri "${runtimeUrl}" -TimeoutSec 3 | Select-Object deploymentMode, capabilities
16655
16959
  Invoke-RestMethod -Uri "${configUrl}" -TimeoutSec 3 | Select-Object captureMode
16656
16960
  \`\`\`
16657
16961
 
16658
16962
  \`\`\`bash
16659
16963
  ${startCommand}
16660
16964
  curl -fsS "${healthUrl}"
16965
+ curl -fsS "${runtimeUrl}"
16661
16966
  curl -fsS "${configUrl}" | grep -o '"captureMode":"[^"]*"'
16662
16967
  \`\`\`
16663
16968
 
@@ -16701,7 +17006,7 @@ When editing:
16701
17006
  - Keep existing \`mcp_servers.*\`, \`projects.*\`, profiles, sandbox, approval, and model settings intact.
16702
17007
  - Remove any duplicate Inspector MCP entry that points to the same \`/api/mcp\` endpoint or runs the same \`agent-inspector-mcp stdio\` bridge under another name.
16703
17008
  - Do not write provider API keys into Codex config.
16704
- - Do not expose Agent Inspector MCP beyond localhost.
17009
+ - Do not expose a raw Inspector listener to untrusted networks. For remote use, require the operator's authenticated same-origin gateway and keep control credentials in environment variables rather than generated config.
16705
17010
 
16706
17011
  ---
16707
17012
 
@@ -16782,14 +17087,18 @@ Inspector, \`localhost\` points at the tool's own environment. Replace \`localho
16782
17087
  address reachable from that environment, then verify the network/firewall path before debugging
16783
17088
  provider credentials.
16784
17089
 
16785
- For a Provider Test memory probe, run a provider test in the Agent Inspector UI:
17090
+ For a Provider Test memory probe, first read \`${runtimeUrl}\`. ${providerTestUiGuidance}
17091
+
17092
+ When an explicit UI URL is available:
16786
17093
 
16787
- 1. Open \`${baseUrl}\`.
17094
+ 1. Open the supplied UI URL.
16788
17095
  2. Go to Settings -> Providers.
16789
17096
  3. Add or edit a Provider.
16790
17097
  4. Run Test.
16791
17098
  5. Check the \`provider-test\` session.
16792
17099
 
17100
+ If discovery reports \`backend-only\`, do not fail onboarding. A separately hosted UI may still exist only when its URL was explicitly supplied; otherwise use the MCP Provider Test tools and log/session resources. Proxy, REST, health, and MCP remain fully usable without UI files.
17101
+
16793
17102
  Then ask Codex:
16794
17103
 
16795
17104
  \`\`\`text
@@ -16823,35 +17132,43 @@ var init_codex_skill_onboard = __esm({
16823
17132
 
16824
17133
  // src/cli/templates/skill-onboard.ts
16825
17134
  function renderSkillOnboard(ctx) {
16826
- const { version: version2, port, baseUrl, detectedSummary } = ctx;
17135
+ const { version: version2, port, detectedSummary } = ctx;
17136
+ const controlBaseUrl = ctx.controlBaseUrl ?? ctx.baseUrl;
17137
+ const agentBaseUrl = ctx.agentBaseUrl ?? ctx.baseUrl;
17138
+ const uiUrl = ctx.uiUrl === void 0 ? ctx.baseUrl : ctx.uiUrl;
16827
17139
  const mcpName = ctx.mcpName ?? "agent-inspector";
16828
17140
  const targetKind = ctx.targetKind ?? "default";
16829
17141
  const targetArguments = ctx.onboardTargetArguments ?? "";
16830
17142
  const instanceName = ctx.instanceName ?? null;
16831
- const healthUrl = `${baseUrl}/api/health`;
16832
- const configUrl = `${baseUrl}/api/config`;
16833
- const anthropicProxyUrl = `${baseUrl}/proxy`;
16834
- const openAiProxyUrl = `${baseUrl}/proxy/v1`;
16835
- const mcpUrl = `${baseUrl}/api/mcp`;
16836
- const logsUrl = `${baseUrl}/api/logs`;
17143
+ const healthUrl = `${controlBaseUrl}/api/health`;
17144
+ const runtimeUrl = `${controlBaseUrl}/api/runtime`;
17145
+ const configUrl = `${controlBaseUrl}/api/config`;
17146
+ const anthropicProxyUrl = `${agentBaseUrl}/proxy`;
17147
+ const openAiProxyUrl = `${agentBaseUrl}/proxy/v1`;
17148
+ const mcpUrl = `${agentBaseUrl}/api/mcp`;
17149
+ const logsUrl = `${controlBaseUrl}/api/logs`;
17150
+ const providersUrl = `${controlBaseUrl}/api/providers`;
16837
17151
  const statusCommand = `agent-inspector onboard --status${targetArguments}`;
16838
17152
  const refreshCommand = `agent-inspector onboard --force${targetArguments}`;
16839
- const startCommand = targetKind === "instance" && instanceName !== null ? `agent-inspector instance start ${instanceName}` : targetKind === "base-url" ? `# Explicit target ${baseUrl}: do not start a replacement local runtime.` : "agent-inspector --mode simple --background --no-open";
17153
+ const startCommand = targetKind === "instance" && instanceName !== null ? `agent-inspector instance start ${instanceName}` : targetKind === "base-url" ? `# Explicit control target ${controlBaseUrl}: do not start a replacement local runtime.` : "agent-inspector --mode simple --background --no-open";
16840
17154
  const fullModeCommand = targetKind === "instance" && instanceName !== null ? `agent-inspector instance restart ${instanceName} --mode full` : targetKind === "base-url" ? "ask the target operator to enable Full capture mode" : "agent-inspector --mode full --force-restart";
16841
17155
  const bashStartSnippet = targetKind === "base-url" ? startCommand : `${startCommand} > /tmp/agent-inspector.log 2>&1`;
16842
17156
  const windowsStartArguments = targetKind === "instance" && instanceName !== null ? `instance start ${instanceName}` : "--mode simple --background --no-open";
16843
17157
  const powershellStartSnippet = targetKind === "base-url" ? startCommand : `$found = Get-Command agent-inspector -ErrorAction SilentlyContinue
16844
17158
  if (-not $found) { throw 'agent-inspector is not on PATH' }
16845
17159
  & $found.Source ${windowsStartArguments}`;
16846
- const startExplanation = targetKind === "base-url" ? `The selected Inspector target is the explicit Base URL \`${baseUrl}\`. Do not launch a default local runtime as a substitute; verify the selected target and ask its operator for help if it is unavailable.` : `Start or reuse the selected Inspector runtime. It listens on port ${String(port)} and preserves the selected Base Path. Use \`${fullModeCommand}\` only when the user needs raw headers, raw response, or SSE chunks.`;
16847
- const stopSnippet = targetKind === "instance" && instanceName !== null ? `agent-inspector instance stop ${instanceName}` : targetKind === "base-url" ? `# Ask the operator of ${baseUrl} to stop the selected runtime.` : `# Unix / macOS
17160
+ const startExplanation = targetKind === "base-url" ? `The selected Inspector target uses control Base URL \`${controlBaseUrl}\` and agent Base URL \`${agentBaseUrl}\`. Do not launch a default local runtime as a substitute; verify the selected target and ask its operator for help if it is unavailable.` : `Start or reuse the selected Inspector backend. Its public control and agent endpoints preserve the selected Base Path; a Web UI may be composed at the same URL, hosted separately, or intentionally absent in backend-only mode. Use \`${fullModeCommand}\` only when the user needs raw headers, raw response, or SSE chunks.`;
17161
+ const stopSnippet = targetKind === "instance" && instanceName !== null ? `agent-inspector instance stop ${instanceName}` : targetKind === "base-url" ? `# Ask the operator of ${controlBaseUrl} to stop the selected runtime.` : `# Unix / macOS
16848
17162
  lsof -ti:${String(port)} | xargs -r kill -9
16849
17163
 
16850
17164
  # Windows PowerShell
16851
17165
  Get-NetTCPConnection -LocalPort ${String(port)} | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force }`;
17166
+ const providerScopeGuidance = targetKind === "default" ? "The unmanaged default target owns the local Agent Inspector data directory used by the file checks below." : `The selected target is not the unmanaged default data directory. Do not inspect or edit local ~/.agent-inspector Provider files as a substitute. Query \`${providersUrl}\`, use the selected target's MCP Provider tools with explicit approval, or ask its operator to configure Providers. Skip local-file commands below.`;
17167
+ const selectedUi = uiUrl === null ? "not supplied (do not infer one from a backend URL)" : `\`${uiUrl}\``;
17168
+ const uiTour = uiUrl === null ? "not supplied; continue with MCP/REST, and ask the operator for a standalone UI URL only if the user needs one" : `\`${uiUrl}\` \u2014 use this explicit composed or standalone browser URL; do not replace it with the control or agent Base URL`;
16852
17169
  return `---
16853
17170
  name: agent-inspector-onboard
16854
- description: Guided setup for Agent Inspector v${version2}: start the proxy, wire your AI coding tool, capture your first request, and learn the Web UI.
17171
+ description: Guided setup for Agent Inspector v${version2}: start or select the backend, wire your AI coding tool, and capture your first request with or without a Web UI.
16855
17172
  metadata:
16856
17173
  author: agent-inspector
16857
17174
  version: ${version2}
@@ -16859,12 +17176,16 @@ metadata:
16859
17176
 
16860
17177
  # Agent Inspector onboard
16861
17178
 
16862
- Guide the user from "I just installed Agent Inspector" to "I can see my AI tool's traffic in the Web UI". This is a teaching experience: you'll do real work in their environment while explaining each step.
17179
+ Guide the user from "I just installed Agent Inspector" to "my coding agent can use the proxy, REST API, and MCP surface". The backend works in composed and headless deployments; treat the Web UI as an optional presentation surface.
16863
17180
 
16864
17181
  Environment detected by the installer:
16865
17182
  ${detectedSummary || " (no known AI tool detected \u2014 the user can still use the generic curl example in Phase 4)"}
16866
17183
 
16867
- Selected Inspector target: \`${baseUrl}\` (MCP key: \`${mcpName}\`).
17184
+ Selected Inspector surfaces:
17185
+ - UI URL: ${selectedUi}
17186
+ - Control Base URL: \`${controlBaseUrl}\`
17187
+ - Agent Base URL: \`${agentBaseUrl}\`
17188
+ - MCP key: \`${mcpName}\`
16868
17189
 
16869
17190
  > **PAUSE protocol.** Every \`**PAUSE**\` marker in this skill is a real stop.
16870
17191
  > Use the \`AskUserQuestion\` tool to actually wait for the user before
@@ -16892,9 +17213,11 @@ ${statusCommand}
16892
17213
 
16893
17214
  **DO:** Then probe the three pieces of state this skill touches. Use targeted checks \u2014 do **not** read large JSON files into the conversation.
16894
17215
 
17216
+ **DO:** ${providerScopeGuidance}
17217
+
16895
17218
  \`\`\`bash
16896
- # 1. Is the proxy already up?
16897
- curl -fsS "${healthUrl}" 2>/dev/null && echo "PROXY: up" || echo "PROXY: down"
17219
+ # 1. Is the backend already up?
17220
+ curl -fsS "${healthUrl}" 2>/dev/null && echo "BACKEND: up" || echo "BACKEND: down"
16898
17221
 
16899
17222
  # 2. Does the provider config have a real provider key?
16900
17223
  agent_inspector_data_dir() {
@@ -16960,12 +17283,12 @@ $dataDir = Resolve-AgentInspectorDataDir
16960
17283
  $providers = Join-Path $dataDir 'providers.json'
16961
17284
  $legacyConfig = Join-Path $dataDir 'config.json'
16962
17285
 
16963
- # 1. Is the proxy already up?
17286
+ # 1. Is the backend already up?
16964
17287
  try {
16965
17288
  $null = Invoke-RestMethod -Uri "${healthUrl}" -TimeoutSec 2 -ErrorAction Stop
16966
- Write-Host 'PROXY: up'
17289
+ Write-Host 'BACKEND: up'
16967
17290
  } catch {
16968
- Write-Host 'PROXY: down'
17291
+ Write-Host 'BACKEND: down'
16969
17292
  }
16970
17293
 
16971
17294
  # 2. Does the config have a real provider key?
@@ -17035,7 +17358,7 @@ node -e "const fs=require('node:fs'),p=require('node:path');const names=process.
17035
17358
  \`\`\`
17036
17359
  ## Welcome to Agent Inspector!
17037
17360
 
17038
- Agent Inspector is an agent observability and knowledge-capture platform for AI coding tools. The \`agent-inspector\` CLI runs a transparent local proxy + Web UI, so you can see model requests and responses, system prompts, tool definitions, message history, timing, and token counts captured live in a browser tab.
17361
+ Agent Inspector is an agent observability and knowledge-capture platform for AI coding tools. Its backend owns the transparent model proxy, REST/SSE APIs, capture storage, and MCP server. The default CLI composes a Web UI at the same public Base URL, while \`--backend-only\` keeps every Coding Agent surface available without serving UI files.
17039
17362
 
17040
17363
  Agent Inspector starts in **Simple mode** by default. Simple mode is the right habit for everyday use: it keeps requests, responses, tools, timing, and token usage while avoiding heavier raw headers, raw response, and detailed SSE chunk artifacts. Use **Full mode** only when the user is debugging provider headers, raw response payloads, or SSE stream details.
17041
17364
 
@@ -17055,6 +17378,8 @@ Ready? Let's start with the provider.
17055
17378
 
17056
17379
  ## Phase 2: Provider setup
17057
17380
 
17381
+ **DO:** ${providerScopeGuidance}
17382
+
17058
17383
  **EXPLAIN:** "A 'provider' is an upstream LLM endpoint \u2014 Anthropic, OpenAI, MiniMax, etc. agent-inspector routes each request to the right upstream based on the model name. You need at least one provider configured for the proxy to forward traffic."
17059
17384
 
17060
17385
  **DO:** First, re-check whether \`<dataDir>/providers.json\` already has a real key (Phase 0 may have raced with a manual edit). Use the same data-dir resolution and \`grep -qE '"apiKey":"sk-'\` (bash) or \`Select-String\` (PowerShell) check from Phase 0. If a real key is present, skip the rest of this phase.
@@ -17155,7 +17480,7 @@ $json | Set-Content -Path $file -Encoding UTF8
17155
17480
 
17156
17481
  **EXPLAIN:** "${startExplanation}"
17157
17482
 
17158
- **DO:** Skip this phase entirely if the Phase 0 health check already reported \`PROXY: up\` and the user opted to skip done phases.
17483
+ **DO:** Skip this phase entirely if the Phase 0 health check already reported \`BACKEND: up\` and the user opted to skip done phases.
17159
17484
 
17160
17485
  **DO:** Otherwise, use the selected target command below. An explicit wrapper Base URL is connection-only and must not cause a replacement local runtime to start.
17161
17486
 
@@ -17168,7 +17493,7 @@ ${bashStartSnippet}
17168
17493
  ${powershellStartSnippet}
17169
17494
  \`\`\`
17170
17495
 
17171
- Then wait for the port to be ready:
17496
+ Then wait for the selected backend health URL to be ready:
17172
17497
 
17173
17498
  \`\`\`bash
17174
17499
  for i in $(seq 1 20); do
@@ -17177,12 +17502,18 @@ for i in $(seq 1 20); do
17177
17502
  done
17178
17503
  \`\`\`
17179
17504
 
17180
- **DO:** Hit the health endpoint to confirm the proxy is alive:
17505
+ **DO:** Hit the health endpoint to confirm the backend is alive:
17181
17506
 
17182
17507
  \`\`\`bash
17183
17508
  curl -sS "${healthUrl}"
17184
17509
  \`\`\`
17185
17510
 
17511
+ **DO:** Read the token-free runtime discovery document. Report \`deploymentMode\` and capabilities; do not treat \`backend-only\` as a failure or infer that a UI is present.
17512
+
17513
+ \`\`\`bash
17514
+ curl -sS "${runtimeUrl}"
17515
+ \`\`\`
17516
+
17186
17517
  **DO:** Read \`/api/config\` and tell the user which capture mode is active. If deeper capture is required, offer \`${fullModeCommand}\`.
17187
17518
 
17188
17519
  \`\`\`bash
@@ -17322,10 +17653,11 @@ done
17322
17653
 
17323
17654
  ## Phase 6: Tour & wrap
17324
17655
 
17325
- **EXPLAIN:** "Everything's working. Here's the cheat sheet for the Web UI and the supporting surfaces:"
17656
+ **EXPLAIN:** "Everything's working. Here's the cheat sheet for the backend surfaces and the optional Web UI:"
17326
17657
 
17327
- - **Web UI**: \`${baseUrl}\` \u2014 collapsible log rows, per-tab Copy/Expand in the log header, Diff with Raw (request body), Diff with Previous (compare adjacent requests), Replay (re-send a request), Export (JSON ZIP).
17328
- - **MCP server**: \`${mcpUrl}\` \u2014 connect from your coding agent to query logs, replay, and test providers without leaving the editor. Look for the "MCP Ready" badge in the Web UI header.
17658
+ - **Runtime discovery**: \`${runtimeUrl}\` \u2014 token-free deployment mode, UI Base Path, control/agent Base URLs, and capabilities.
17659
+ - **Web UI**: ${uiTour}.
17660
+ - **MCP server**: \`${mcpUrl}\` \u2014 connect from the coding agent to query logs, replay, and test providers; it does not require a Web UI process.
17329
17661
  - **REST API**: \`/api/logs\`, \`/api/sessions\`, \`/api/providers\` \u2014 for scripting and shell-based inspection.
17330
17662
  - **Stop the proxy**:
17331
17663
 
@@ -17336,7 +17668,7 @@ ${stopSnippet}
17336
17668
  - **Re-run onboard**: \`${refreshCommand}\` refreshes this skill.
17337
17669
  - **Check onboard files**: \`${statusCommand}\` shows whether generated skills are
17338
17670
  current, outdated, missing, newer, or custom.
17339
- - **Full docs**: see the project README (linked from the Web UI footer).
17671
+ - **Full docs**: see the project README; do not depend on a UI footer being available.
17340
17672
 
17341
17673
  > **PAUSE** \u2014 use \`AskUserQuestion\` with header \`All set?\` and options \`["All set, I'm done", "Wait, I want to revisit a phase"]\`. Wait for the answer.
17342
17674
 
@@ -17372,11 +17704,11 @@ import {
17372
17704
  existsSync as existsSync5,
17373
17705
  readFileSync as readFileSync2,
17374
17706
  unlinkSync,
17375
- readdirSync as readdirSync3,
17707
+ readdirSync as readdirSync4,
17376
17708
  rmdirSync
17377
17709
  } from "node:fs";
17378
17710
  import { homedir as homedir3 } from "node:os";
17379
- import { dirname as dirname2, join as join6 } from "node:path";
17711
+ import { dirname, join as join7 } from "node:path";
17380
17712
  import { fileURLToPath } from "node:url";
17381
17713
  function actionForStatus(label, state) {
17382
17714
  switch (state) {
@@ -17422,6 +17754,9 @@ function parseFlags(argv) {
17422
17754
  mimoMcpUrl: null,
17423
17755
  instanceName: null,
17424
17756
  baseUrl: null,
17757
+ controlBaseUrl: null,
17758
+ agentBaseUrl: null,
17759
+ uiUrl: void 0,
17425
17760
  mcpName: null,
17426
17761
  uninstall: false,
17427
17762
  status: false,
@@ -17572,6 +17907,39 @@ function parseFlags(argv) {
17572
17907
  i++;
17573
17908
  break;
17574
17909
  }
17910
+ case "--control-base-url": {
17911
+ const next = argv[i + 1];
17912
+ if (next === void 0 || next.trim().length === 0) {
17913
+ process.stderr.write("agent-inspector onboard: --control-base-url requires a URL\n");
17914
+ process.exit(2);
17915
+ }
17916
+ flags.controlBaseUrl = next;
17917
+ i++;
17918
+ break;
17919
+ }
17920
+ case "--agent-base-url": {
17921
+ const next = argv[i + 1];
17922
+ if (next === void 0 || next.trim().length === 0) {
17923
+ process.stderr.write("agent-inspector onboard: --agent-base-url requires a URL\n");
17924
+ process.exit(2);
17925
+ }
17926
+ flags.agentBaseUrl = next;
17927
+ i++;
17928
+ break;
17929
+ }
17930
+ case "--ui-url": {
17931
+ const next = argv[i + 1];
17932
+ if (next === void 0 || next.trim().length === 0) {
17933
+ process.stderr.write("agent-inspector onboard: --ui-url requires a URL\n");
17934
+ process.exit(2);
17935
+ }
17936
+ flags.uiUrl = next;
17937
+ i++;
17938
+ break;
17939
+ }
17940
+ case "--no-ui":
17941
+ flags.uiUrl = null;
17942
+ break;
17575
17943
  case "--mcp-name": {
17576
17944
  const next = argv[i + 1];
17577
17945
  if (next === void 0 || next.trim().length === 0) {
@@ -17652,7 +18020,11 @@ Options:
17652
18020
  MiMo Code MCP transport to write (default: local)
17653
18021
  --mimo-mcp-url <url> Agent Inspector MCP endpoint for MiMo Code
17654
18022
  --instance <name> Target a registered named Inspector instance
17655
- --base-url <url> Target an explicit credential-free Inspector Base URL
18023
+ --base-url <url> Target an explicit credential-free backend control/agent Base URL
18024
+ --control-base-url <url> Control API Base URL for a separated deployment
18025
+ --agent-base-url <url> Proxy and MCP Base URL for Coding Agents
18026
+ --ui-url <url> Browser UI URL for a separated deployment
18027
+ --no-ui Declare that the explicit target has no browser UI
17656
18028
  --mcp-name <name> MCP config key (named default: agent-inspector-<instance>)
17657
18029
  --uninstall Remove generated files whose version matches this package
17658
18030
  --status Show installed onboarding file versions and suggested action
@@ -17668,15 +18040,15 @@ Exit codes:
17668
18040
  `);
17669
18041
  }
17670
18042
  function resolveTargets(flags) {
17671
- const claudeRoot = flags.skillDir ?? join6(homedir3(), ".claude");
17672
- const claudeSkillDir = join6(claudeRoot, "skills", SKILL_DIR_NAME);
17673
- const claudeCommandsDir = join6(claudeRoot, "commands");
17674
- const codexRoot = process.env["CODEX_HOME"] ?? join6(homedir3(), ".codex");
17675
- const codexSkillsDir = flags.codexSkillDir ?? join6(codexRoot, "skills");
18043
+ const claudeRoot = flags.skillDir ?? join7(homedir3(), ".claude");
18044
+ const claudeSkillDir = join7(claudeRoot, "skills", SKILL_DIR_NAME);
18045
+ const claudeCommandsDir = join7(claudeRoot, "commands");
18046
+ const codexRoot = process.env["CODEX_HOME"] ?? join7(homedir3(), ".codex");
18047
+ const codexSkillsDir = flags.codexSkillDir ?? join7(codexRoot, "skills");
17676
18048
  return {
17677
- claudeSkillFile: join6(claudeSkillDir, SKILL_FILE_NAME),
17678
- claudeCommandFile: join6(claudeCommandsDir, COMMAND_FILE_NAME),
17679
- codexSkillFile: join6(codexSkillsDir, SKILL_DIR_NAME, SKILL_FILE_NAME)
18049
+ claudeSkillFile: join7(claudeSkillDir, SKILL_FILE_NAME),
18050
+ claudeCommandFile: join7(claudeCommandsDir, COMMAND_FILE_NAME),
18051
+ codexSkillFile: join7(codexSkillsDir, SKILL_DIR_NAME, SKILL_FILE_NAME)
17680
18052
  };
17681
18053
  }
17682
18054
  function isObject2(value) {
@@ -17695,32 +18067,32 @@ function resolveOpenCodeConfigPath(flags) {
17695
18067
  if (flags.opencodeConfig !== null) {
17696
18068
  return flags.opencodeConfig;
17697
18069
  }
17698
- const configDir = join6(homedir3(), ".config", "opencode");
17699
- const jsoncPath = join6(configDir, "opencode.jsonc");
18070
+ const configDir = join7(homedir3(), ".config", "opencode");
18071
+ const jsoncPath = join7(configDir, "opencode.jsonc");
17700
18072
  if (existsSync5(jsoncPath)) {
17701
18073
  return jsoncPath;
17702
18074
  }
17703
- return join6(configDir, "opencode.json");
18075
+ return join7(configDir, "opencode.json");
17704
18076
  }
17705
18077
  function resolveMiMoConfigPath(flags) {
17706
18078
  if (flags.mimoConfig !== null) {
17707
18079
  return flags.mimoConfig;
17708
18080
  }
17709
- const configDir = join6(homedir3(), ".config", "mimocode");
17710
- const jsoncPath = join6(configDir, "mimocode.jsonc");
18081
+ const configDir = join7(homedir3(), ".config", "mimocode");
18082
+ const jsoncPath = join7(configDir, "mimocode.jsonc");
17711
18083
  if (existsSync5(jsoncPath)) {
17712
18084
  return jsoncPath;
17713
18085
  }
17714
- const jsonPath = join6(configDir, "mimocode.json");
18086
+ const jsonPath = join7(configDir, "mimocode.json");
17715
18087
  if (existsSync5(jsonPath)) {
17716
18088
  return jsonPath;
17717
18089
  }
17718
- const legacyDir = join6(homedir3(), ".mimocode");
17719
- const legacyJsoncPath = join6(legacyDir, "config.jsonc");
18090
+ const legacyDir = join7(homedir3(), ".mimocode");
18091
+ const legacyJsoncPath = join7(legacyDir, "config.jsonc");
17720
18092
  if (existsSync5(legacyJsoncPath)) {
17721
18093
  return legacyJsoncPath;
17722
18094
  }
17723
- const legacyJsonPath = join6(legacyDir, "config.json");
18095
+ const legacyJsonPath = join7(legacyDir, "config.json");
17724
18096
  if (existsSync5(legacyJsonPath)) {
17725
18097
  return legacyJsonPath;
17726
18098
  }
@@ -17731,7 +18103,18 @@ function onboardSelectionArguments(flags, target) {
17731
18103
  if (target.kind === "instance" && target.instanceName !== null) {
17732
18104
  args.push("--instance", target.instanceName);
17733
18105
  } else if (target.kind === "base-url") {
17734
- args.push("--base-url", target.baseUrl);
18106
+ const usedSharedBaseUrl = flags.baseUrl !== null && flags.controlBaseUrl === null && flags.agentBaseUrl === null && flags.uiUrl === void 0;
18107
+ if (usedSharedBaseUrl) {
18108
+ args.push("--base-url", target.baseUrl);
18109
+ } else {
18110
+ args.push("--control-base-url", target.controlBaseUrl);
18111
+ args.push("--agent-base-url", target.agentBaseUrl);
18112
+ if (target.uiUrl === null) {
18113
+ args.push("--no-ui");
18114
+ } else {
18115
+ args.push("--ui-url", target.uiUrl);
18116
+ }
18117
+ }
17735
18118
  }
17736
18119
  if (flags.mcpName !== null) args.push("--mcp-name", target.mcpName);
17737
18120
  return args.length === 0 ? "" : ` ${args.join(" ")}`;
@@ -18114,8 +18497,8 @@ function buildDetectedSummary() {
18114
18497
  }
18115
18498
  function readPackageVersion() {
18116
18499
  const packageJsonPaths = [
18117
- join6(__dirname, "..", "package.json"),
18118
- join6(__dirname, "..", "..", "package.json")
18500
+ join7(__dirname, "..", "package.json"),
18501
+ join7(__dirname, "..", "..", "package.json")
18119
18502
  ];
18120
18503
  try {
18121
18504
  for (const packageJsonPath of packageJsonPaths) {
@@ -18289,6 +18672,9 @@ function buildPlannedFiles(flags, targets, version2, onboardTarget) {
18289
18672
  version: version2,
18290
18673
  port: onboardTarget.port,
18291
18674
  baseUrl: onboardTarget.baseUrl,
18675
+ controlBaseUrl: onboardTarget.controlBaseUrl,
18676
+ agentBaseUrl: onboardTarget.agentBaseUrl,
18677
+ uiUrl: onboardTarget.uiUrl,
18292
18678
  mcpName: onboardTarget.mcpName,
18293
18679
  targetKind: onboardTarget.kind,
18294
18680
  instanceName: onboardTarget.instanceName,
@@ -18300,6 +18686,9 @@ function buildPlannedFiles(flags, targets, version2, onboardTarget) {
18300
18686
  version: version2,
18301
18687
  port: onboardTarget.port,
18302
18688
  baseUrl: onboardTarget.baseUrl,
18689
+ controlBaseUrl: onboardTarget.controlBaseUrl,
18690
+ agentBaseUrl: onboardTarget.agentBaseUrl,
18691
+ uiUrl: onboardTarget.uiUrl,
18303
18692
  mcpName: onboardTarget.mcpName,
18304
18693
  targetKind: onboardTarget.kind,
18305
18694
  instanceName: onboardTarget.instanceName,
@@ -18345,7 +18734,7 @@ function shouldUninstall(file2, currentVersion) {
18345
18734
  }
18346
18735
  function removeEmptyDirectory(path2) {
18347
18736
  try {
18348
- if (readdirSync3(path2).length === 0) {
18737
+ if (readdirSync4(path2).length === 0) {
18349
18738
  rmdirSync(path2);
18350
18739
  }
18351
18740
  } catch {
@@ -18393,13 +18782,13 @@ No files were removed.
18393
18782
  try {
18394
18783
  for (const file2 of filesToRemove) {
18395
18784
  unlinkSync(file2.path);
18396
- removeEmptyDirectory(dirname2(file2.path));
18785
+ removeEmptyDirectory(dirname(file2.path));
18397
18786
  process.stdout.write(`Removed ${file2.label}: ${file2.path}
18398
18787
  `);
18399
18788
  }
18400
18789
  for (const plan of agentPlans) {
18401
18790
  if (plan.shouldRemove && plan.body !== null) {
18402
- mkdirSync3(dirname2(plan.status.path), { recursive: true });
18791
+ mkdirSync3(dirname(plan.status.path), { recursive: true });
18403
18792
  writeFileSync2(plan.status.path, plan.body, "utf8");
18404
18793
  process.stdout.write(`Removed ${plan.status.label} entry from: ${plan.status.path}
18405
18794
  `);
@@ -18516,6 +18905,9 @@ function runOnboardSync(argv) {
18516
18905
  const resolvedTarget = resolveOnboardTarget({
18517
18906
  instanceName: flags.instanceName,
18518
18907
  baseUrl: flags.baseUrl,
18908
+ controlBaseUrl: flags.controlBaseUrl,
18909
+ agentBaseUrl: flags.agentBaseUrl,
18910
+ uiUrl: flags.uiUrl,
18519
18911
  mcpName: flags.mcpName
18520
18912
  });
18521
18913
  if (!resolvedTarget.ok) {
@@ -18636,12 +19028,12 @@ No files were written.
18636
19028
  }
18637
19029
  try {
18638
19030
  for (const file2 of filesToWrite) {
18639
- mkdirSync3(dirname2(file2.path), { recursive: true });
19031
+ mkdirSync3(dirname(file2.path), { recursive: true });
18640
19032
  writeFileSync2(file2.path, file2.body, "utf8");
18641
19033
  }
18642
19034
  for (const plan of agentPlansToWrite) {
18643
19035
  if (plan.body !== null) {
18644
- mkdirSync3(dirname2(plan.status.path), { recursive: true });
19036
+ mkdirSync3(dirname(plan.status.path), { recursive: true });
18645
19037
  writeFileSync2(plan.status.path, plan.body, "utf8");
18646
19038
  }
18647
19039
  }
@@ -18701,7 +19093,7 @@ var init_onboard = __esm({
18701
19093
  init_codex_skill_onboard();
18702
19094
  init_skill_onboard();
18703
19095
  __filename = fileURLToPath(import.meta.url);
18704
- __dirname = dirname2(__filename);
19096
+ __dirname = dirname(__filename);
18705
19097
  DEFAULT_PORT2 = 9527;
18706
19098
  DEFAULT_ORIGIN2 = `http://localhost:${DEFAULT_PORT2}`;
18707
19099
  DEFAULT_MCP_URL = appendBasePathToOrigin(
@@ -18730,8 +19122,8 @@ __export(doctor_exports, {
18730
19122
  parseDoctorArgs: () => parseDoctorArgs,
18731
19123
  runDoctor: () => runDoctor
18732
19124
  });
18733
- import { existsSync as existsSync6, readFileSync as readFileSync3, readdirSync as readdirSync4, statSync as statSync4 } from "node:fs";
18734
- import { isAbsolute as isAbsolute4, join as join7, resolve } from "node:path";
19125
+ import { existsSync as existsSync6, readFileSync as readFileSync3, readdirSync as readdirSync5, statSync as statSync3 } from "node:fs";
19126
+ import { isAbsolute as isAbsolute4, join as join8, resolve as resolve4 } from "node:path";
18735
19127
  import { createConnection } from "node:net";
18736
19128
  function check2(name, severity, message, hint = "") {
18737
19129
  return {
@@ -18834,7 +19226,7 @@ function providerCountFromJson(raw) {
18834
19226
  }
18835
19227
  }
18836
19228
  function resolveMaybeRelative(base, value) {
18837
- return isAbsolute4(value) ? value : resolve(base, value);
19229
+ return isAbsolute4(value) ? value : resolve4(base, value);
18838
19230
  }
18839
19231
  function resolveDefaultDataDir(env, platform, exists) {
18840
19232
  return resolveDataDir(exists, env, platform);
@@ -18846,10 +19238,10 @@ function providerConfigCandidates(options, deps) {
18846
19238
  }
18847
19239
  if (options.configDir !== null) {
18848
19240
  const dir = resolveMaybeRelative(deps.cwd, options.configDir);
18849
- return [join7(dir, "providers.json"), join7(dir, "config.json")];
19241
+ return [join8(dir, "providers.json"), join8(dir, "config.json")];
18850
19242
  }
18851
19243
  const dataDir = resolveDefaultDataDir(deps.env, deps.platform, deps.exists);
18852
- return [join7(dataDir, "providers.json"), join7(dataDir, "config.json")];
19244
+ return [join8(dataDir, "providers.json"), join8(dataDir, "config.json")];
18853
19245
  }
18854
19246
  function checkProviderConfig(options, deps) {
18855
19247
  const providersJson = options.providersJson ?? deps.env["AGENT_INSPECTOR_PROVIDERS_JSON"] ?? null;
@@ -18895,58 +19287,71 @@ function checkProviderConfig(options, deps) {
18895
19287
  "Provider config",
18896
19288
  "warn",
18897
19289
  `Provider config exists but no providers were found: ${candidate}`,
18898
- "Add a provider in Settings or import provider settings."
19290
+ "Add a provider through the control API/MCP, or use Settings when a UI is available."
18899
19291
  );
18900
19292
  }
18901
19293
  return check2(
18902
19294
  "Provider config",
18903
19295
  "warn",
18904
19296
  "No provider config was found.",
18905
- "Add a provider in the web UI, import providers, or pass --providers for this check."
19297
+ "Add a provider through the control API/MCP, use Settings when a UI is available, or pass --providers for this check."
18906
19298
  );
18907
19299
  }
19300
+ function publicOriginForDoctor(port, env) {
19301
+ const configured = env["AGENT_INSPECTOR_PUBLIC_ORIGIN"];
19302
+ if (configured !== void 0 && configured.trim() === configured) {
19303
+ try {
19304
+ const parsed = new URL(configured);
19305
+ if ((parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.username.length === 0 && parsed.password.length === 0 && parsed.pathname === "/" && parsed.search.length === 0 && parsed.hash.length === 0) {
19306
+ return parsed.origin;
19307
+ }
19308
+ } catch {
19309
+ }
19310
+ }
19311
+ return `http://localhost:${String(port)}`;
19312
+ }
18908
19313
  async function checkProxyHealth(port, deps) {
18909
19314
  const basePath = getConfiguredBasePath(deps.env);
18910
19315
  const health = await deps.fetchHealth(port, basePath);
18911
- const url2 = `http://localhost:${port}`;
19316
+ const url2 = publicOriginForDoctor(port, deps.env);
18912
19317
  const proxyUrl = appendBasePathToOrigin(url2, "/proxy", basePath);
18913
19318
  const healthPath = appendBasePathToOrigin("", "/api/health", basePath);
18914
19319
  if (health.ok) {
18915
19320
  return check2(
18916
- "Proxy health",
19321
+ "Backend health",
18917
19322
  "pass",
18918
- `agent-inspector is healthy at ${appendBasePathToOrigin(url2, "/", basePath)}.`,
18919
- `Proxy URL: ${proxyUrl}`
19323
+ `Agent Inspector backend is healthy at ${appendBasePathToOrigin(url2, "/", basePath)}.`,
19324
+ `Agent proxy URL: ${proxyUrl}. UI availability is not part of this backend check.`
18920
19325
  );
18921
19326
  }
18922
19327
  if (await deps.isPortOpen(port)) {
18923
19328
  const suffix = health.status === null ? "" : ` (HTTP ${health.status})`;
18924
19329
  return check2(
18925
- "Proxy health",
19330
+ "Backend health",
18926
19331
  "fail",
18927
19332
  `Port ${port} is accepting connections, but ${healthPath} is not healthy${suffix}.`,
18928
19333
  "Stop that process, choose --port <n>, or start agent-inspector with --force-restart."
18929
19334
  );
18930
19335
  }
18931
19336
  return check2(
18932
- "Proxy health",
19337
+ "Backend health",
18933
19338
  "warn",
18934
19339
  `No process is listening on port ${port}.`,
18935
19340
  "Start with `agent-inspector`, `agent-inspector --background`, or `bun run dev`."
18936
19341
  );
18937
19342
  }
18938
19343
  function findProjectRoot(startDir, deps) {
18939
- let current = resolve(startDir);
19344
+ let current = resolve4(startDir);
18940
19345
  for (let i = 0; i < 6; i++) {
18941
- if (deps.exists(join7(current, "package.json"))) return current;
18942
- const next = resolve(current, "..");
19346
+ if (deps.exists(join8(current, "package.json"))) return current;
19347
+ const next = resolve4(current, "..");
18943
19348
  if (next === current) return startDir;
18944
19349
  current = next;
18945
19350
  }
18946
19351
  return startDir;
18947
19352
  }
18948
19353
  function checkPackage(rootDir, deps) {
18949
- const packagePath = join7(rootDir, "package.json");
19354
+ const packagePath = join8(rootDir, "package.json");
18950
19355
  const text = deps.readText(packagePath);
18951
19356
  if (text === null) {
18952
19357
  return check2("Package metadata", "warn", "package.json was not found near the CLI bundle.");
@@ -18971,7 +19376,7 @@ function countFilesInDir(dir, deps) {
18971
19376
  if (files === null) return null;
18972
19377
  let count = 0;
18973
19378
  for (const file2 of files) {
18974
- const fileStat = deps.stat(join7(dir, file2));
19379
+ const fileStat = deps.stat(join8(dir, file2));
18975
19380
  if (fileStat !== null && !fileStat.isDirectory) count += 1;
18976
19381
  }
18977
19382
  return count;
@@ -18991,24 +19396,24 @@ function dirSize(dir, deps, depth = 0) {
18991
19396
  if (files === null) return null;
18992
19397
  let total = 0;
18993
19398
  for (const file2 of files) {
18994
- const childSize = dirSize(join7(dir, file2), deps, depth + 1);
19399
+ const childSize = dirSize(join8(dir, file2), deps, depth + 1);
18995
19400
  if (childSize !== null) total += childSize;
18996
19401
  }
18997
19402
  return total;
18998
19403
  }
18999
19404
  function verboseChecks(options, deps) {
19000
19405
  const dataDir = resolveDefaultDataDir(deps.env, deps.platform, deps.exists);
19001
- const logsDir = join7(dataDir, "logs");
19002
- const chunksDir = join7(dataDir, "chunks");
19406
+ const logsDir = join8(dataDir, "logs");
19407
+ const chunksDir = join8(dataDir, "chunks");
19003
19408
  const providerCandidates = providerConfigCandidates(options, deps);
19004
19409
  const captureMode = deps.env["AGENT_INSPECTOR_MODE"] ?? deps.env["AGENT_INSPECTOR_CAPTURE_MODE"] ?? "simple";
19005
19410
  const logCount = countFilesInDir(logsDir, deps);
19006
19411
  const logSize = dirSize(logsDir, deps);
19007
19412
  const chunkSize = dirSize(chunksDir, deps);
19008
19413
  const homeDir = deps.platform === "win32" ? deps.env["USERPROFILE"] ?? deps.env["APPDATA"] ?? null : deps.env["HOME"] ?? null;
19009
- const claudeSkill = homeDir === null ? null : join7(homeDir, ".claude", "skills", "agent-inspector-onboard", "SKILL.md");
19010
- const claudeCommand = homeDir === null ? null : join7(homeDir, ".claude", "commands", "agent-inspector-onboard.md");
19011
- const codexSkill = homeDir === null ? null : join7(homeDir, ".codex", "skills", "agent-inspector-onboard", "SKILL.md");
19414
+ const claudeSkill = homeDir === null ? null : join8(homeDir, ".claude", "skills", "agent-inspector-onboard", "SKILL.md");
19415
+ const claudeCommand = homeDir === null ? null : join8(homeDir, ".claude", "commands", "agent-inspector-onboard.md");
19416
+ const codexSkill = homeDir === null ? null : join8(homeDir, ".codex", "skills", "agent-inspector-onboard", "SKILL.md");
19012
19417
  const hasClaudeOnboarding = claudeSkill !== null && claudeCommand !== null && deps.exists(claudeSkill) && deps.exists(claudeCommand);
19013
19418
  const hasCodexOnboarding = codexSkill !== null && deps.exists(codexSkill);
19014
19419
  return [
@@ -19040,8 +19445,8 @@ function verboseChecks(options, deps) {
19040
19445
  ];
19041
19446
  }
19042
19447
  function checkExtensionSource(rootDir, deps) {
19043
- const extensionDir = join7(rootDir, "extensions", "chrome");
19044
- const missing = EXTENSION_REQUIRED_FILES.filter((file2) => !deps.exists(join7(extensionDir, file2)));
19448
+ const extensionDir = join8(rootDir, "extensions", "chrome");
19449
+ const missing = EXTENSION_REQUIRED_FILES.filter((file2) => !deps.exists(join8(extensionDir, file2)));
19045
19450
  if (missing.length === 0) {
19046
19451
  return check2(
19047
19452
  "Chrome extension source",
@@ -19057,7 +19462,7 @@ function checkExtensionSource(rootDir, deps) {
19057
19462
  );
19058
19463
  }
19059
19464
  function extensionManifestVersion(rootDir, deps) {
19060
- const manifestText = deps.readText(join7(rootDir, "extensions", "chrome", "manifest.json"));
19465
+ const manifestText = deps.readText(join8(rootDir, "extensions", "chrome", "manifest.json"));
19061
19466
  if (manifestText === null) return null;
19062
19467
  try {
19063
19468
  const manifest = JSON.parse(manifestText);
@@ -19069,7 +19474,7 @@ function extensionManifestVersion(rootDir, deps) {
19069
19474
  }
19070
19475
  }
19071
19476
  function checkExtensionPackage(rootDir, deps) {
19072
- const distDir = join7(rootDir, "dist", "chrome-extension");
19477
+ const distDir = join8(rootDir, "dist", "chrome-extension");
19073
19478
  const files = deps.listDir(distDir);
19074
19479
  if (files === null) {
19075
19480
  return check2(
@@ -19169,7 +19574,8 @@ function doctorHelp() {
19169
19574
  "Usage:",
19170
19575
  " agent-inspector doctor [--port <port>] [--config-dir <dir>] [--providers <json>] [--fix] [--json] [--verbose] [--chrome-extension]",
19171
19576
  "",
19172
- "Checks local proxy health, provider config presence, and package metadata.",
19577
+ "Checks local backend health, provider config presence, and package metadata.",
19578
+ "The backend check probes the local health endpoint and does not require a composed Web UI.",
19173
19579
  "Pass --fix to run safe local repairs before checks, including legacy data-dir migration.",
19174
19580
  "Pass --json to emit a machine-readable report for CI, scripts, and Jenkins.",
19175
19581
  "Pass --verbose to include local paths, capture mode, and log storage checks.",
@@ -19218,14 +19624,14 @@ function readText(path2) {
19218
19624
  }
19219
19625
  function listDir(path2) {
19220
19626
  try {
19221
- return readdirSync4(path2);
19627
+ return readdirSync5(path2);
19222
19628
  } catch {
19223
19629
  return null;
19224
19630
  }
19225
19631
  }
19226
- function stat2(path2) {
19632
+ function stat3(path2) {
19227
19633
  try {
19228
- const fileStat = statSync4(path2);
19634
+ const fileStat = statSync3(path2);
19229
19635
  return { isDirectory: fileStat.isDirectory(), size: fileStat.size };
19230
19636
  } catch {
19231
19637
  return null;
@@ -19239,7 +19645,7 @@ function createDoctorDeps() {
19239
19645
  exists: existsSync6,
19240
19646
  readText,
19241
19647
  listDir,
19242
- stat: stat2,
19648
+ stat: stat3,
19243
19649
  fetchHealth,
19244
19650
  isPortOpen
19245
19651
  };
@@ -19298,11 +19704,7 @@ function parsePort2(raw) {
19298
19704
  return port;
19299
19705
  }
19300
19706
  function normalizeAliasPath(raw) {
19301
- const trimmed = raw.trim();
19302
- if (!trimmed.startsWith("/") || trimmed.includes("?") || trimmed.includes("#") || trimmed.includes("\\")) {
19303
- return null;
19304
- }
19305
- return normalizeBasePath(trimmed);
19707
+ return normalizePublicBasePath(raw);
19306
19708
  }
19307
19709
  function isValidHost(host) {
19308
19710
  const trimmed = host.trim();
@@ -19627,6 +20029,10 @@ var init_alias = __esm({
19627
20029
  });
19628
20030
 
19629
20031
  // src/lib/instanceContract.ts
20032
+ function isCanonicalPublicBasePath(value) {
20033
+ const normalized = normalizePublicBasePath(value);
20034
+ return normalized !== null && (normalized.length === 0 ? "/" : normalized) === value;
20035
+ }
19630
20036
  function isRetryableInstanceError(code) {
19631
20037
  switch (code) {
19632
20038
  case "launch_failed":
@@ -19653,11 +20059,12 @@ function isRetryableInstanceError(code) {
19653
20059
  return false;
19654
20060
  }
19655
20061
  }
19656
- var INSTANCE_API_SCHEMA_VERSION, InstanceStateSchema, InstanceStateReasonSchema, InstanceUrlsSchema, InstanceStatusOutputSchema, InstanceConnectionOutputSchema, InstanceStartRequestSchema, InstanceEmptyRequestSchema, InstanceOperationSchema, InstanceOperationOutcomeSchema, InstanceOperationOutputSchema, InstanceListOutputSchema, InstancePublicErrorCodeSchema, InstancePublicErrorSchema, InstanceErrorOutputSchema;
20062
+ var INSTANCE_API_SCHEMA_VERSION, InstanceStateSchema, InstanceStateReasonSchema, InstanceUrlsSchema, InstanceStatusOutputSchema, InstanceConnectionSurfacesSchema, InstanceConnectionOutputSchema, InstanceStartRequestSchema, InstanceEmptyRequestSchema, InstanceOperationSchema, InstanceOperationOutcomeSchema, InstanceOperationOutputSchema, InstanceListOutputSchema, InstancePublicErrorCodeSchema, InstancePublicErrorSchema, InstanceErrorOutputSchema;
19657
20063
  var init_instanceContract = __esm({
19658
20064
  "src/lib/instanceContract.ts"() {
19659
20065
  "use strict";
19660
20066
  init_zod();
20067
+ init_basePath();
19661
20068
  INSTANCE_API_SCHEMA_VERSION = 1;
19662
20069
  InstanceStateSchema = external_exports.enum(["running", "stopped", "conflict"]);
19663
20070
  InstanceStateReasonSchema = external_exports.enum(["identity_mismatch", "port_conflict"]);
@@ -19685,15 +20092,27 @@ var init_instanceContract = __esm({
19685
20092
  host: external_exports.string().min(1),
19686
20093
  publicPort: external_exports.number().int().min(1).max(65535),
19687
20094
  upstreamPort: external_exports.number().int().min(1).max(65535),
19688
- basePath: external_exports.string().startsWith("/"),
20095
+ basePath: external_exports.string().refine(isCanonicalPublicBasePath, "Invalid public Base Path"),
19689
20096
  captureMode: external_exports.enum(["simple", "full"]),
20097
+ uiEnabled: external_exports.boolean(),
19690
20098
  dataDir: external_exports.string().min(1),
19691
20099
  supervisorPid: external_exports.number().int().positive().nullable(),
19692
20100
  urls: InstanceUrlsSchema
19693
20101
  }).strict();
20102
+ InstanceConnectionSurfacesSchema = external_exports.object({
20103
+ ui: external_exports.object({
20104
+ enabled: external_exports.boolean(),
20105
+ baseUrl: external_exports.url().nullable()
20106
+ }).strict(),
20107
+ control: external_exports.object({ baseUrl: external_exports.url() }).strict(),
20108
+ agent: external_exports.object({ baseUrl: external_exports.url() }).strict()
20109
+ }).strict();
19694
20110
  InstanceConnectionOutputSchema = external_exports.object({
19695
20111
  schemaVersion: external_exports.literal(INSTANCE_API_SCHEMA_VERSION),
20112
+ uiEnabled: external_exports.boolean(),
19696
20113
  instance: InstanceStatusOutputSchema,
20114
+ surfaces: InstanceConnectionSurfacesSchema,
20115
+ /** @deprecated Use surfaces.control.baseUrl or surfaces.agent.baseUrl. */
19697
20116
  baseUrl: external_exports.url(),
19698
20117
  endpoints: InstanceUrlsSchema,
19699
20118
  environment: external_exports.object({
@@ -19716,14 +20135,15 @@ var init_instanceContract = __esm({
19716
20135
  InstanceStartRequestSchema = external_exports.object({
19717
20136
  port: external_exports.number().int().min(1).max(65535).optional(),
19718
20137
  basePath: external_exports.string().min(1).max(2048).refine(
19719
- (value) => value.startsWith("/") && !value.includes("?") && !value.includes("#") && !value.includes("\\"),
19720
- "Base Path must be an absolute URL path without query, hash, or backslash"
20138
+ (value) => normalizePublicBasePath(value) !== null,
20139
+ "Base Path must use URL-safe path segments without traversal, encoded separators, or markup characters"
19721
20140
  ).optional(),
19722
20141
  host: external_exports.string().min(1).max(253).refine(
19723
20142
  (value) => value.trim() === value && !value.includes("://") && !value.includes("/") && !value.includes("\\") && !/\s/u.test(value),
19724
20143
  "Host must be a hostname or IP address without protocol, path, or whitespace"
19725
20144
  ).optional(),
19726
20145
  captureMode: external_exports.enum(["simple", "full"]).optional(),
20146
+ uiEnabled: external_exports.boolean().optional(),
19727
20147
  dataDir: external_exports.string().trim().min(1).max(32767).optional()
19728
20148
  }).strict();
19729
20149
  InstanceEmptyRequestSchema = external_exports.object({}).strict();
@@ -19790,10 +20210,8 @@ function parseMode(value) {
19790
20210
  return value === "simple" || value === "full" ? value : null;
19791
20211
  }
19792
20212
  function parseBasePath(value) {
19793
- if (value === void 0 || !value.startsWith("/") || value.includes("?") || value.includes("#") || value.includes("\\")) {
19794
- return null;
19795
- }
19796
- return normalizeBasePath(value);
20213
+ if (value === void 0) return null;
20214
+ return normalizePublicBasePath(value);
19797
20215
  }
19798
20216
  function optionWithEquals(arg) {
19799
20217
  if (!arg.startsWith("--") || !arg.includes("=")) return null;
@@ -19807,6 +20225,7 @@ function emptyOverrides() {
19807
20225
  host: null,
19808
20226
  captureMode: null,
19809
20227
  dataDir: null,
20228
+ uiEnabled: null,
19810
20229
  open: false
19811
20230
  };
19812
20231
  }
@@ -19880,6 +20299,24 @@ function parseInstanceArgs(argv) {
19880
20299
  case "--open":
19881
20300
  options.overrides.open = true;
19882
20301
  break;
20302
+ case "--backend-only":
20303
+ if (options.overrides.uiEnabled === true) {
20304
+ return {
20305
+ kind: "error",
20306
+ message: "instance: --backend-only and --with-ui are mutually exclusive"
20307
+ };
20308
+ }
20309
+ options.overrides.uiEnabled = false;
20310
+ break;
20311
+ case "--with-ui":
20312
+ if (options.overrides.uiEnabled === false) {
20313
+ return {
20314
+ kind: "error",
20315
+ message: "instance: --backend-only and --with-ui are mutually exclusive"
20316
+ };
20317
+ }
20318
+ options.overrides.uiEnabled = true;
20319
+ break;
19883
20320
  case "--port":
19884
20321
  case "-p":
19885
20322
  case "--base-path":
@@ -19906,7 +20343,7 @@ function parseInstanceArgs(argv) {
19906
20343
  if (options.positional.length !== 1) {
19907
20344
  return { kind: "error", message: "instance: list does not accept an instance name" };
19908
20345
  }
19909
- if (options.overrides.port !== null || options.overrides.basePath !== null || options.overrides.host !== null || options.overrides.captureMode !== null || options.overrides.dataDir !== null || options.overrides.open) {
20346
+ if (options.overrides.port !== null || options.overrides.basePath !== null || options.overrides.host !== null || options.overrides.captureMode !== null || options.overrides.dataDir !== null || options.overrides.uiEnabled !== null || options.overrides.open) {
19910
20347
  return { kind: "error", message: "instance: list accepts only --json" };
19911
20348
  }
19912
20349
  return { kind: "ok", action: { kind: "list", json: options.json } };
@@ -19916,7 +20353,7 @@ function parseInstanceArgs(argv) {
19916
20353
  if (name === void 0 || options.positional.length !== 2) {
19917
20354
  return { kind: "error", message: `instance: ${command} requires exactly one instance name` };
19918
20355
  }
19919
- if (options.overrides.port !== null || options.overrides.basePath !== null || options.overrides.host !== null || options.overrides.captureMode !== null || options.overrides.dataDir !== null || options.overrides.open) {
20356
+ if (options.overrides.port !== null || options.overrides.basePath !== null || options.overrides.host !== null || options.overrides.captureMode !== null || options.overrides.dataDir !== null || options.overrides.uiEnabled !== null || options.overrides.open) {
19920
20357
  return {
19921
20358
  kind: "error",
19922
20359
  message: `instance: ${command} accepts only an instance name and --json`
@@ -19932,6 +20369,9 @@ function parseInstanceArgs(argv) {
19932
20369
  if (name === void 0 || options.positional.length !== 2) {
19933
20370
  return { kind: "error", message: `instance: ${command} requires exactly one instance name` };
19934
20371
  }
20372
+ if (options.overrides.uiEnabled === false && options.overrides.open) {
20373
+ return { kind: "error", message: "instance: --open cannot be used with --backend-only" };
20374
+ }
19935
20375
  return {
19936
20376
  kind: "ok",
19937
20377
  action: { kind: command, name, overrides: options.overrides, json: options.json }
@@ -19962,6 +20402,8 @@ Start/restart options:
19962
20402
  --host, -H <host> Bind host (default: 127.0.0.1)
19963
20403
  --mode <simple|full> Capture mode (default: simple)
19964
20404
  --data-dir <path> Isolated data/config directory
20405
+ --backend-only Persist a headless backend without serving the UI
20406
+ --with-ui Persist composed UI serving (the default for new instances)
19965
20407
  --open Open the UI after readiness
19966
20408
  --json Emit machine-readable output
19967
20409
  --help, -h Show this help
@@ -20093,9 +20535,9 @@ var init_instanceControl = __esm({
20093
20535
  // src/cli/instance.ts
20094
20536
  var instance_exports = {};
20095
20537
  __export(instance_exports, {
20538
+ buildInstanceLaunchArgs: () => buildInstanceLaunchArgs,
20096
20539
  getNamedInstance: () => getNamedInstance,
20097
20540
  getNamedInstanceConnection: () => getNamedInstanceConnection,
20098
- instanceLifecycleForTests: () => instanceLifecycleForTests,
20099
20541
  listNamedInstances: () => listNamedInstances,
20100
20542
  restartNamedInstance: () => restartNamedInstance,
20101
20543
  runInstance: () => runInstance,
@@ -20152,6 +20594,7 @@ function statusOutput(record2, live, checkedAt = /* @__PURE__ */ new Date()) {
20152
20594
  upstreamPort: record2.upstreamPort,
20153
20595
  basePath: displayBasePath2(record2.basePath),
20154
20596
  captureMode: record2.captureMode,
20597
+ uiEnabled: record2.uiEnabled,
20155
20598
  dataDir: record2.dataDir,
20156
20599
  supervisorPid: live.supervisorPid,
20157
20600
  urls: recordUrls(record2)
@@ -20169,7 +20612,16 @@ function connectionOutput(instance) {
20169
20612
  const serverName = `agent-inspector-${instance.name}`;
20170
20613
  return {
20171
20614
  schemaVersion: INSTANCE_API_SCHEMA_VERSION,
20615
+ uiEnabled: instance.uiEnabled,
20172
20616
  instance,
20617
+ surfaces: {
20618
+ ui: {
20619
+ enabled: instance.uiEnabled,
20620
+ baseUrl: instance.uiEnabled ? instance.urls.ui : null
20621
+ },
20622
+ control: { baseUrl: instance.urls.ui },
20623
+ agent: { baseUrl: instance.urls.ui }
20624
+ },
20173
20625
  baseUrl: instance.urls.ui,
20174
20626
  endpoints: instance.urls,
20175
20627
  environment: {
@@ -20235,7 +20687,7 @@ function defaultOpenUrl(targetUrl) {
20235
20687
  child.once("error", () => void 0);
20236
20688
  child.unref();
20237
20689
  }
20238
- function launchArgs(record2, open) {
20690
+ function buildInstanceLaunchArgs(record2, open2) {
20239
20691
  const args = [
20240
20692
  "--background",
20241
20693
  "--no-open",
@@ -20246,11 +20698,12 @@ function launchArgs(record2, open) {
20246
20698
  "--mode",
20247
20699
  record2.captureMode
20248
20700
  ];
20249
- if (open) args.push("--open");
20701
+ if (!record2.uiEnabled) args.push("--backend-only");
20702
+ if (open2 && record2.uiEnabled) args.push("--open");
20250
20703
  return args;
20251
20704
  }
20252
- async function defaultLaunch(record2, open, cliEntry) {
20253
- const child = spawn(process.execPath, [cliEntry, ...launchArgs(record2, open)], {
20705
+ async function defaultLaunch(record2, open2, cliEntry) {
20706
+ const child = spawn(process.execPath, [cliEntry, ...buildInstanceLaunchArgs(record2, open2)], {
20254
20707
  stdio: "ignore",
20255
20708
  windowsHide: true,
20256
20709
  env: {
@@ -20268,12 +20721,12 @@ async function defaultLaunch(record2, open, cliEntry) {
20268
20721
  });
20269
20722
  return await waitForProcess(child) === 0;
20270
20723
  }
20271
- async function launchRecord(record2, open, deps) {
20724
+ async function launchRecord(record2, open2, deps) {
20272
20725
  const cliEntry = deps.cliEntry ?? process.env[AGENT_INSPECTOR_CLI_ENTRY_ENV] ?? process.argv[1];
20273
20726
  if (deps.launch === void 0 && (cliEntry === void 0 || cliEntry.length === 0)) {
20274
20727
  return failure("launch_unavailable", "Cannot resolve the packaged Agent Inspector CLI entry.");
20275
20728
  }
20276
- const launched = deps.launch === void 0 ? await defaultLaunch(record2, open, cliEntry ?? "") : await deps.launch(record2, open);
20729
+ const launched = deps.launch === void 0 ? await defaultLaunch(record2, open2, cliEntry ?? "") : await deps.launch(record2, open2);
20277
20730
  if (!launched) {
20278
20731
  return failure(
20279
20732
  "launch_failed",
@@ -20379,6 +20832,7 @@ function overridesDiffer(record2, action) {
20379
20832
  if (overrides.basePath !== null && overrides.basePath !== record2.basePath) return true;
20380
20833
  if (overrides.host !== null && overrides.host !== record2.host) return true;
20381
20834
  if (overrides.captureMode !== null && overrides.captureMode !== record2.captureMode) return true;
20835
+ if (overrides.uiEnabled !== null && overrides.uiEnabled !== record2.uiEnabled) return true;
20382
20836
  if (overrides.dataDir !== null) {
20383
20837
  const requested = canonicalizeDataDirectory(overrides.dataDir);
20384
20838
  const saved = canonicalizeDataDirectory(record2.dataDir);
@@ -20396,6 +20850,7 @@ function updatedRecord(record2, action, pair, dataDir) {
20396
20850
  upstreamPort: pair.upstreamPort,
20397
20851
  basePath: action.overrides.basePath ?? record2.basePath,
20398
20852
  captureMode: action.overrides.captureMode ?? record2.captureMode,
20853
+ uiEnabled: action.overrides.uiEnabled ?? record2.uiEnabled,
20399
20854
  dataDir
20400
20855
  };
20401
20856
  const parsed = InstanceRecordSchema.safeParse(candidate);
@@ -20446,6 +20901,7 @@ async function createNewRecord(root, action, deps) {
20446
20901
  upstreamPort: pairResult.value.upstreamPort,
20447
20902
  basePath: action.overrides.basePath ?? getConfiguredBasePath(process.env),
20448
20903
  captureMode: action.overrides.captureMode ?? DEFAULT_INSTANCE_CAPTURE_MODE,
20904
+ uiEnabled: action.overrides.uiEnabled ?? true,
20449
20905
  dataDir: dataDirResult.value
20450
20906
  });
20451
20907
  return created.kind === "error" ? fromRegistry(created) : saveRecord(root, created.value);
@@ -20486,7 +20942,9 @@ async function startAction(root, action, deps) {
20486
20942
  }
20487
20943
  const live = await classifyRecord(record2, deps);
20488
20944
  if (live.state === "running") {
20489
- if (action.overrides.open) (deps.openUrl ?? defaultOpenUrl)(recordUrls(record2).ui);
20945
+ if (action.overrides.open && record2.uiEnabled) {
20946
+ (deps.openUrl ?? defaultOpenUrl)(recordUrls(record2).ui);
20947
+ }
20490
20948
  return success2(operationOutput("start", "already_running", statusOutput(record2, live)));
20491
20949
  }
20492
20950
  if (live.state === "conflict") {
@@ -20616,9 +21074,10 @@ async function listAction(root, deps) {
20616
21074
  function requestActionOverrides(input) {
20617
21075
  return {
20618
21076
  port: input.port ?? null,
20619
- basePath: input.basePath === void 0 ? null : normalizeBasePath(input.basePath),
21077
+ basePath: input.basePath === void 0 ? null : normalizePublicBasePath(input.basePath),
20620
21078
  host: input.host ?? null,
20621
21079
  captureMode: input.captureMode ?? null,
21080
+ uiEnabled: input.uiEnabled ?? null,
20622
21081
  dataDir: input.dataDir ?? null,
20623
21082
  open: false
20624
21083
  };
@@ -20668,8 +21127,10 @@ async function listNamedInstances(deps = {}) {
20668
21127
  function formatStatus(output) {
20669
21128
  const pid = output.supervisorPid === null ? "-" : String(output.supervisorPid);
20670
21129
  const reason = output.stateReason === null ? "-" : output.stateReason;
21130
+ const ui = output.uiEnabled ? output.urls.ui : `disabled (configured URL: ${output.urls.ui})`;
20671
21131
  return `Instance ${output.name}: ${output.state}
20672
- UI: ${output.urls.ui}
21132
+ UI: ${ui}
21133
+ UI enabled: ${output.uiEnabled ? "yes" : "no"}
20673
21134
  Proxy: ${output.urls.proxy}
20674
21135
  OpenAI /v1: ${output.urls.openai}
20675
21136
  MCP: ${output.urls.mcp}
@@ -20810,7 +21271,7 @@ ${instanceHelp()}`);
20810
21271
  }
20811
21272
  }
20812
21273
  }
20813
- var DEFAULT_INSTANCE_HOST, DEFAULT_INSTANCE_CAPTURE_MODE, INSTANCE_READY_TIMEOUT_MS, INSTANCE_STOP_TIMEOUT_MS, INSTANCE_POLL_INTERVAL_MS, processIo2, instanceLifecycleForTests;
21274
+ var DEFAULT_INSTANCE_HOST, DEFAULT_INSTANCE_CAPTURE_MODE, INSTANCE_READY_TIMEOUT_MS, INSTANCE_STOP_TIMEOUT_MS, INSTANCE_POLL_INTERVAL_MS, processIo2;
20814
21275
  var init_instance = __esm({
20815
21276
  "src/cli/instance.ts"() {
20816
21277
  "use strict";
@@ -20832,26 +21293,21 @@ var init_instance = __esm({
20832
21293
  stdout: (text) => process.stdout.write(text),
20833
21294
  stderr: (text) => process.stderr.write(text)
20834
21295
  };
20835
- instanceLifecycleForTests = {
20836
- classifyRecord,
20837
- launchArgs,
20838
- statusOutput
20839
- };
20840
21296
  }
20841
21297
  });
20842
21298
 
20843
21299
  // src/cli.ts
20844
- import { Buffer as Buffer4 } from "node:buffer";
21300
+ import { Buffer as Buffer5 } from "node:buffer";
20845
21301
  import { spawn as spawn2 } from "node:child_process";
20846
21302
  import { createConnection as createConnection2 } from "node:net";
20847
21303
  import { fileURLToPath as fileURLToPath2 } from "node:url";
20848
- import { dirname as dirname3, join as join8, resolve as resolvePath2 } from "node:path";
21304
+ import { dirname as dirname2, join as join9, resolve as resolvePath2 } from "node:path";
20849
21305
  import { existsSync as existsSync7 } from "node:fs";
20850
21306
 
20851
21307
  // package.json
20852
21308
  var package_default = {
20853
21309
  name: "@tonyclaw/agent-inspector",
20854
- version: "3.0.45",
21310
+ version: "3.0.46",
20855
21311
  type: "module",
20856
21312
  description: "Agent observability and knowledge capture layer for AI coding tools.",
20857
21313
  license: "MIT",
@@ -20895,7 +21351,8 @@ var package_default = {
20895
21351
  "!src/**/*.test.ts",
20896
21352
  "!src/**/*.stories.tsx",
20897
21353
  "!src/**/__fixtures__",
20898
- "!src/routeTree.gen.ts",
21354
+ "!src/ui/routeTree.gen.ts",
21355
+ "!src/backend/routeTree.gen.ts",
20899
21356
  "scripts/setup-windows-runtime.mjs",
20900
21357
  "scripts/setup-agent-skills.mjs",
20901
21358
  "scripts/generate-theme-scales.mjs",
@@ -20905,21 +21362,37 @@ var package_default = {
20905
21362
  "!.output/*.exe"
20906
21363
  ],
20907
21364
  scripts: {
20908
- dev: "vite dev",
21365
+ dev: "node scripts/dev-decoupled.mjs",
21366
+ "dev:ui": "vite dev --config vite.ui.config.ts",
21367
+ "dev:backend": "node scripts/dev-backend.mjs",
20909
21368
  "test:smoke": "bun scripts/run-test-suite.mjs smoke",
20910
21369
  "test:fast": "bun scripts/run-test-suite.mjs smoke",
20911
21370
  "test:unit": "bun scripts/run-test-suite.mjs unit",
20912
21371
  "test:integration": "bun scripts/run-test-suite.mjs integration",
20913
21372
  "test:changed": "bun scripts/run-test-suite.mjs changed",
20914
21373
  "test:all": "bun scripts/run-test-suite.mjs all",
21374
+ "test:compatibility-tooling": "node --test scripts/compatibility/*.test.mjs",
20915
21375
  start: "node .output/cli.js",
20916
- build: "vite build && bun build:workers && bun build:cli && bun build:mcp && bun check:bundle",
21376
+ "start:backend": "node .output/cli.js --backend-only --no-open",
21377
+ "preview:ui": "vite preview --config vite.ui.config.ts",
21378
+ "clean:output": "node scripts/clean-output.mjs",
21379
+ build: "bun clean:output && bun build:backend && bun build:ui && bun smoke:ui-preview && bun build:workers && bun build:cli && bun build:mcp && bun check:bundle && bun check:boundaries",
21380
+ "build:backend": "vite build --config vite.backend.config.ts",
21381
+ "build:ui": "vite build --config vite.ui.config.ts",
20917
21382
  "build:workers": "npx esbuild src/proxy/logFinalizer.worker.ts src/proxy/sessionWorkerEntry.ts --bundle --platform=node --target=node22 --format=esm --outdir=.output/workers --external:better-sqlite3",
20918
21383
  "smoke:workers": "node scripts/smoke-production-workers.mjs",
20919
21384
  "smoke:packed-workers": "node scripts/smoke-packed-workers.mjs",
20920
21385
  "smoke:instances": "node scripts/smoke-windows-instances.mjs",
20921
21386
  "smoke:coding-agent-instances": "node scripts/smoke-coding-agent-instances.mjs",
21387
+ "smoke:ui-preview": "node scripts/smoke-decoupled-ui-preview.mjs",
20922
21388
  "check:bundle": "node scripts/check-client-bundle.mjs",
21389
+ "check:boundaries": "node scripts/check-runtime-boundaries.mjs",
21390
+ "compatibility:bdd": "node scripts/lint-bdd-catalog.mjs",
21391
+ "compatibility:compare": "node scripts/compatibility/cli.mjs compare",
21392
+ "compatibility:drift": "node scripts/check-backend-compatibility.mjs",
21393
+ "compatibility:fixtures": "bun scripts/validate-compatibility-fixtures.ts",
21394
+ "compatibility:freeze": "node scripts/freeze-backend-baseline.mjs",
21395
+ "compatibility:run": "node scripts/compatibility/cli.mjs run",
20923
21396
  "build:cli": "npx esbuild src/cli.ts --bundle --platform=node --target=node22 --format=esm --outfile=.output/cli.js",
20924
21397
  "build:mcp": "npx esbuild packages/agent-inspector-mcp/src/cli.ts --bundle --platform=node --target=node22 --format=esm --outfile=packages/agent-inspector-mcp/dist/cli.js",
20925
21398
  "themes:generate": "node scripts/generate-theme-scales.mjs",
@@ -20934,7 +21407,7 @@ var package_default = {
20934
21407
  "extension:release": "node scripts/release-chrome-extension.mjs",
20935
21408
  release: "node scripts/release.mjs",
20936
21409
  knip: "knip",
20937
- check: "bun scripts/run-static-checks.mjs --format",
21410
+ check: "bun scripts/run-static-checks.mjs --format && bun check:boundaries && bun compatibility:bdd",
20938
21411
  prepare: "husky",
20939
21412
  ladle: "ladle serve",
20940
21413
  "ladle:build": "ladle build"
@@ -20973,6 +21446,7 @@ var package_default = {
20973
21446
  "@ladle/react": "^5.1.1",
20974
21447
  "@radix-ui/colors": "^3.0.0",
20975
21448
  "@tailwindcss/vite": "^4.3.2",
21449
+ "@tanstack/router-plugin": "^1.168.20",
20976
21450
  "@types/bun": "^1.3.14",
20977
21451
  "@types/react": "^19.2.17",
20978
21452
  "@types/react-dom": "^19.2.3",
@@ -20980,6 +21454,8 @@ var package_default = {
20980
21454
  "@typescript-eslint/eslint-plugin": "^8.64.0",
20981
21455
  "@typescript-eslint/parser": "^8.64.0",
20982
21456
  "@vitejs/plugin-react": "^5.2.0",
21457
+ ajv: "8.20.0",
21458
+ "ajv-formats": "3.0.1",
20983
21459
  "colorjs.io": "^0.7.0",
20984
21460
  esbuild: "^0.28.1",
20985
21461
  eslint: "^9.39.5",
@@ -21023,53 +21499,887 @@ function shouldSuppressServerOutputLine(line, context) {
21023
21499
  }
21024
21500
 
21025
21501
  // src/proxy/identityProxy.ts
21502
+ init_browserRuntimeContract();
21026
21503
  init_basePath();
21027
21504
  init_managedInstance();
21028
21505
  import http from "node:http";
21029
- import { Buffer as Buffer3 } from "node:buffer";
21030
- import { createHash, timingSafeEqual } from "node:crypto";
21031
- import { isIP } from "node:net";
21506
+ import { Buffer as Buffer4 } from "node:buffer";
21507
+ import { createHash as createHash2, timingSafeEqual } from "node:crypto";
21508
+ import { isIP as isIP2 } from "node:net";
21032
21509
 
21033
21510
  // src/proxy/socketTracker.ts
21034
21511
  init_zod();
21035
21512
  import { execFile } from "node:child_process";
21036
- import { readFile } from "node:fs/promises";
21513
+ import { readFile as readFile2 } from "node:fs/promises";
21037
21514
  import { promisify } from "node:util";
21038
21515
 
21039
21516
  // src/proxy/logger.ts
21040
21517
  init_dataDir();
21041
21518
  init_privateDataPath();
21042
- import { Buffer as Buffer2 } from "node:buffer";
21043
- import { appendFileSync } from "node:fs";
21044
- import { appendFile, readdir, rename, stat, unlink } from "node:fs/promises";
21519
+ import { Buffer as Buffer3 } from "node:buffer";
21520
+ import { closeSync as closeSync2, constants as constants3, fsyncSync, openSync as openSync2, writeSync } from "node:fs";
21521
+ import { appendFile, readdir as readdir2, rename, stat as stat2, unlink as unlink2 } from "node:fs/promises";
21045
21522
  import path from "node:path";
21046
- var DAY_MS = 24 * 60 * 60 * 1e3;
21047
- var DEFAULT_RETENTION_DAYS = 7;
21048
- var DEFAULT_APP_LOG_MAX_BYTES = 10 * 1024 * 1024;
21049
- var DEFAULT_APP_LOG_MAX_FILES = 5;
21050
- function writeStderrFallback(message, error51) {
21051
- process.stderr.write(`${message} ${String(error51)}
21052
- `);
21053
- }
21054
- function resolveLogDir() {
21055
- const logDirEnv = process.env["LOG_DIR"];
21056
- if (logDirEnv !== void 0 && logDirEnv !== "") {
21057
- return path.isAbsolute(logDirEnv) ? logDirEnv : path.join(getDataDir(), logDirEnv);
21058
- }
21059
- return path.join(getDataDir(), "logs");
21060
- }
21061
- function getLogFilePath() {
21062
- const date5 = /* @__PURE__ */ new Date();
21063
- const yyyy = date5.getUTCFullYear();
21064
- const mm = String(date5.getUTCMonth() + 1).padStart(2, "0");
21065
- const dd = String(date5.getUTCDate()).padStart(2, "0");
21066
- return path.join(resolveLogDir(), `${yyyy}-${mm}-${dd}.jsonl`);
21067
- }
21068
- function getInspectorLogPath() {
21069
- const logFileEnv = process.env["AGENT_INSPECTOR_LOG_FILE"];
21070
- if (logFileEnv !== void 0 && logFileEnv !== "") {
21071
- return logFileEnv;
21072
- }
21523
+
21524
+ // src/proxy/jsonlRecovery.ts
21525
+ import { createHash, randomUUID } from "node:crypto";
21526
+ import { Buffer as Buffer2 } from "node:buffer";
21527
+ import { constants as constants2, existsSync as existsSync2, lstatSync as lstatSync2 } from "node:fs";
21528
+ import { link, lstat, open, readFile, readdir, stat, unlink } from "node:fs/promises";
21529
+ import { basename, join as join3, resolve as resolve2 } from "node:path";
21530
+
21531
+ // src/proxy/schemas.ts
21532
+ init_zod();
21533
+
21534
+ // src/contracts/json.ts
21535
+ init_zod();
21536
+ var JsonValueSchema = external_exports.lazy(
21537
+ () => external_exports.union([
21538
+ external_exports.string(),
21539
+ external_exports.number(),
21540
+ external_exports.boolean(),
21541
+ external_exports.null(),
21542
+ external_exports.array(JsonValueSchema),
21543
+ external_exports.record(external_exports.string(), JsonValueSchema)
21544
+ ])
21545
+ );
21546
+
21547
+ // src/contracts/log.ts
21548
+ init_zod();
21549
+ var LogIdSchema = external_exports.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
21550
+ var CaptureIncompleteReasonSchema = external_exports.enum([
21551
+ "capacity-exceeded",
21552
+ "pipeline-closed",
21553
+ "write-failed",
21554
+ "finalize-failed",
21555
+ "upstream-stream-error",
21556
+ "shutdown"
21557
+ ]);
21558
+ var StreamingChunkSchema = external_exports.object({
21559
+ index: external_exports.number(),
21560
+ timestamp: external_exports.number(),
21561
+ type: external_exports.string(),
21562
+ data: JsonValueSchema
21563
+ });
21564
+ var StreamingChunksArraySchema = external_exports.object({
21565
+ chunks: external_exports.array(StreamingChunkSchema),
21566
+ truncated: external_exports.boolean().optional().default(false)
21567
+ });
21568
+ var LogBodyPartSchema = external_exports.enum(["request", "response"]);
21569
+ var LogBodyChunkSchema = external_exports.object({
21570
+ logId: LogIdSchema,
21571
+ part: LogBodyPartSchema,
21572
+ text: external_exports.string(),
21573
+ offset: external_exports.number().int().nonnegative(),
21574
+ limit: external_exports.number().int().positive(),
21575
+ totalBytes: external_exports.number().int().nonnegative(),
21576
+ textBytes: external_exports.number().int().nonnegative(),
21577
+ nextOffset: external_exports.number().int().nonnegative().nullable(),
21578
+ hasMore: external_exports.boolean(),
21579
+ contentMode: external_exports.enum(["empty", "partial", "full"])
21580
+ });
21581
+ var CapturedLogSchema = external_exports.object({
21582
+ id: LogIdSchema,
21583
+ timestamp: external_exports.string(),
21584
+ method: external_exports.string(),
21585
+ path: external_exports.string(),
21586
+ model: external_exports.string().nullable(),
21587
+ sessionId: external_exports.string().nullable(),
21588
+ rawRequestBody: external_exports.string().nullable(),
21589
+ responseStatus: external_exports.number().nullable(),
21590
+ responseText: external_exports.string().nullable(),
21591
+ inputTokens: external_exports.number().nullable(),
21592
+ outputTokens: external_exports.number().nullable(),
21593
+ cacheCreationInputTokens: external_exports.number().nullable(),
21594
+ cacheReadInputTokens: external_exports.number().nullable(),
21595
+ elapsedMs: external_exports.number().nullable(),
21596
+ firstChunkMs: external_exports.number().nullable().optional(),
21597
+ totalStreamMs: external_exports.number().nullable().optional(),
21598
+ tokensPerSecond: external_exports.number().nullable().optional(),
21599
+ streaming: external_exports.boolean(),
21600
+ userAgent: external_exports.string().nullable(),
21601
+ origin: external_exports.string().nullable(),
21602
+ rawHeaders: external_exports.record(external_exports.string(), external_exports.string()).optional(),
21603
+ /** Headers sent to the upstream LLM */
21604
+ headers: external_exports.record(external_exports.string(), external_exports.string()).optional(),
21605
+ apiFormat: external_exports.enum(["anthropic", "openai", "unknown"]).default("unknown"),
21606
+ isTest: external_exports.boolean().optional().default(false),
21607
+ replayOfLogId: LogIdSchema.nullable().optional(),
21608
+ providerName: external_exports.string().nullable().optional(),
21609
+ clientPort: external_exports.number().nullable().optional(),
21610
+ clientPid: external_exports.number().nullable().optional(),
21611
+ clientCwd: external_exports.string().nullable().optional(),
21612
+ clientProjectFolder: external_exports.string().nullable().optional(),
21613
+ streamingChunks: StreamingChunksArraySchema.optional(),
21614
+ streamingChunksPath: external_exports.string().nullable().optional(),
21615
+ rawRequestBodyBytes: external_exports.number().int().nonnegative().nullable().optional(),
21616
+ responseTextBytes: external_exports.number().int().nonnegative().nullable().optional(),
21617
+ bodyContentMode: external_exports.enum(["full", "compact", "truncated"]).optional(),
21618
+ captureIncomplete: external_exports.boolean().optional(),
21619
+ droppedChunks: external_exports.number().int().nonnegative().optional(),
21620
+ droppedBytes: external_exports.number().int().nonnegative().optional(),
21621
+ captureIncompleteReason: CaptureIncompleteReasonSchema.nullable().optional(),
21622
+ warnings: external_exports.array(external_exports.string()).optional(),
21623
+ /** Error message from streaming response (e.g., SSE error event) */
21624
+ error: external_exports.string().nullable().optional()
21625
+ });
21626
+
21627
+ // src/contracts/anthropic.ts
21628
+ init_zod();
21629
+ var CacheControl = external_exports.object({
21630
+ type: external_exports.string(),
21631
+ ttl: external_exports.string().optional(),
21632
+ scope: external_exports.string().optional()
21633
+ });
21634
+ var TextContentBlock = external_exports.object({
21635
+ type: external_exports.literal("text"),
21636
+ text: external_exports.string(),
21637
+ cache_control: CacheControl.optional()
21638
+ });
21639
+ var ThinkingContentBlock = external_exports.object({
21640
+ type: external_exports.literal("thinking"),
21641
+ thinking: external_exports.string(),
21642
+ signature: external_exports.string().optional()
21643
+ });
21644
+ var ThinkContentBlock = external_exports.object({
21645
+ type: external_exports.literal("think"),
21646
+ thinking: external_exports.string(),
21647
+ signature: external_exports.string().optional()
21648
+ });
21649
+ var ImageSourceBlock = external_exports.object({
21650
+ type: external_exports.literal("base64"),
21651
+ media_type: external_exports.string(),
21652
+ data: external_exports.string()
21653
+ });
21654
+ var ImageContentBlock = external_exports.object({
21655
+ type: external_exports.literal("image"),
21656
+ source: ImageSourceBlock
21657
+ });
21658
+ var ToolUseContentBlock = external_exports.object({
21659
+ type: external_exports.literal("tool_use"),
21660
+ id: external_exports.string(),
21661
+ name: external_exports.string(),
21662
+ input: external_exports.record(external_exports.string(), JsonValueSchema)
21663
+ });
21664
+ var ToolResultContentItem = external_exports.discriminatedUnion("type", [TextContentBlock, ImageContentBlock]);
21665
+ var ToolResultContentBlock = external_exports.object({
21666
+ type: external_exports.literal("tool_result"),
21667
+ tool_use_id: external_exports.string().optional(),
21668
+ content: external_exports.union([external_exports.string(), external_exports.array(ToolResultContentItem)]),
21669
+ is_error: external_exports.boolean().optional()
21670
+ });
21671
+ var ContentBlock = external_exports.discriminatedUnion("type", [
21672
+ TextContentBlock,
21673
+ ThinkingContentBlock,
21674
+ ImageContentBlock,
21675
+ ToolUseContentBlock,
21676
+ ToolResultContentBlock
21677
+ ]);
21678
+ var MessageContent = external_exports.union([external_exports.string(), external_exports.array(ContentBlock)]);
21679
+ var Message = external_exports.object({
21680
+ role: external_exports.enum(["user", "assistant"]),
21681
+ content: MessageContent
21682
+ });
21683
+ var SystemBlock = external_exports.object({
21684
+ type: external_exports.literal("text"),
21685
+ text: external_exports.string(),
21686
+ cache_control: CacheControl.optional()
21687
+ });
21688
+ var InputSchema = external_exports.object({
21689
+ type: external_exports.string(),
21690
+ properties: external_exports.record(external_exports.string(), external_exports.record(external_exports.string(), JsonValueSchema)).optional(),
21691
+ required: external_exports.array(external_exports.string()).optional(),
21692
+ additionalProperties: external_exports.boolean().optional(),
21693
+ $schema: external_exports.string().optional()
21694
+ });
21695
+ var ToolDefinition = external_exports.object({
21696
+ name: external_exports.string(),
21697
+ description: external_exports.string().optional(),
21698
+ input_schema: InputSchema.optional(),
21699
+ cache_control: CacheControl.optional()
21700
+ });
21701
+ var ThinkingConfig = external_exports.discriminatedUnion("type", [
21702
+ external_exports.object({ type: external_exports.literal("enabled"), budget_tokens: external_exports.number() }),
21703
+ external_exports.object({ type: external_exports.literal("disabled") }),
21704
+ external_exports.object({ type: external_exports.literal("adaptive") })
21705
+ ]);
21706
+ var AnthropicRequestSchema = external_exports.object({
21707
+ model: external_exports.string(),
21708
+ messages: external_exports.array(Message),
21709
+ system: external_exports.array(SystemBlock).optional(),
21710
+ tools: external_exports.array(ToolDefinition).optional(),
21711
+ max_tokens: external_exports.number().optional(),
21712
+ temperature: external_exports.number().optional(),
21713
+ stream: external_exports.boolean().optional(),
21714
+ thinking: ThinkingConfig.optional(),
21715
+ metadata: external_exports.object({
21716
+ user_id: external_exports.string().optional()
21717
+ }).optional()
21718
+ });
21719
+ var ResponseContentBlock = external_exports.discriminatedUnion("type", [
21720
+ TextContentBlock,
21721
+ ThinkingContentBlock,
21722
+ ThinkContentBlock,
21723
+ ToolUseContentBlock
21724
+ ]);
21725
+ var ResponseUsageSchema = external_exports.object({
21726
+ input_tokens: external_exports.number().nullable().optional(),
21727
+ output_tokens: external_exports.number().nullable().optional(),
21728
+ cache_creation_input_tokens: external_exports.number().nullable().optional(),
21729
+ cache_read_input_tokens: external_exports.number().nullable().optional()
21730
+ }).passthrough();
21731
+ var AnthropicResponseSchema = external_exports.object({
21732
+ id: external_exports.string(),
21733
+ type: external_exports.literal("message"),
21734
+ model: external_exports.string(),
21735
+ role: external_exports.literal("assistant"),
21736
+ content: external_exports.array(ResponseContentBlock),
21737
+ stop_reason: external_exports.string().nullable(),
21738
+ stop_sequence: external_exports.string().nullable().optional(),
21739
+ usage: ResponseUsageSchema
21740
+ }).passthrough();
21741
+ var SseMessageStartEvent = external_exports.object({
21742
+ type: external_exports.literal("message_start"),
21743
+ message: external_exports.object({
21744
+ id: external_exports.string(),
21745
+ type: external_exports.literal("message"),
21746
+ model: external_exports.string(),
21747
+ role: external_exports.literal("assistant"),
21748
+ content: external_exports.array(ResponseContentBlock),
21749
+ stop_reason: external_exports.null(),
21750
+ stop_sequence: external_exports.null(),
21751
+ usage: external_exports.object({
21752
+ input_tokens: external_exports.number(),
21753
+ cache_creation_input_tokens: external_exports.number().optional(),
21754
+ cache_read_input_tokens: external_exports.number().optional()
21755
+ }).passthrough()
21756
+ })
21757
+ });
21758
+ var SseContentBlockStartEvent = external_exports.object({
21759
+ type: external_exports.literal("content_block_start"),
21760
+ index: external_exports.number(),
21761
+ content_block: ResponseContentBlock
21762
+ });
21763
+ var SseDeltaBlock = external_exports.discriminatedUnion("type", [
21764
+ external_exports.object({ type: external_exports.literal("text_delta"), text: external_exports.string() }),
21765
+ external_exports.object({ type: external_exports.literal("input_json_delta"), partial_json: external_exports.string() }),
21766
+ external_exports.object({ type: external_exports.literal("thinking_delta"), thinking: external_exports.string() }),
21767
+ external_exports.object({ type: external_exports.literal("signature_delta"), signature: external_exports.string() })
21768
+ ]);
21769
+ var SseContentBlockDeltaEvent = external_exports.object({
21770
+ type: external_exports.literal("content_block_delta"),
21771
+ index: external_exports.number(),
21772
+ delta: SseDeltaBlock
21773
+ });
21774
+ var SseContentBlockStopEvent = external_exports.object({
21775
+ type: external_exports.literal("content_block_stop"),
21776
+ index: external_exports.number()
21777
+ });
21778
+ var SseMessageDeltaEvent = external_exports.object({
21779
+ type: external_exports.literal("message_delta"),
21780
+ delta: external_exports.object({
21781
+ stop_reason: external_exports.string().nullable(),
21782
+ stop_sequence: external_exports.string().nullable().optional()
21783
+ }),
21784
+ usage: external_exports.object({
21785
+ output_tokens: external_exports.number(),
21786
+ input_tokens: external_exports.number().optional(),
21787
+ cache_creation_input_tokens: external_exports.number().optional(),
21788
+ cache_read_input_tokens: external_exports.number().optional()
21789
+ }).passthrough()
21790
+ });
21791
+ var SseMessageStopEvent = external_exports.object({
21792
+ type: external_exports.literal("message_stop")
21793
+ });
21794
+ var SsePingEvent = external_exports.object({
21795
+ type: external_exports.literal("ping")
21796
+ });
21797
+ var SseErrorEvent = external_exports.object({
21798
+ type: external_exports.literal("error"),
21799
+ error: external_exports.object({
21800
+ type: external_exports.string(),
21801
+ message: external_exports.string()
21802
+ })
21803
+ });
21804
+ var SseEventSchema = external_exports.discriminatedUnion("type", [
21805
+ SseMessageStartEvent,
21806
+ SseContentBlockStartEvent,
21807
+ SseContentBlockDeltaEvent,
21808
+ SseContentBlockStopEvent,
21809
+ SseMessageDeltaEvent,
21810
+ SseMessageStopEvent,
21811
+ SsePingEvent,
21812
+ SseErrorEvent
21813
+ ]);
21814
+
21815
+ // src/contracts/openai.ts
21816
+ init_zod();
21817
+ var OpenAIMessageContent = external_exports.union([
21818
+ external_exports.string(),
21819
+ external_exports.array(
21820
+ external_exports.discriminatedUnion("type", [
21821
+ external_exports.object({ type: external_exports.literal("text"), text: external_exports.string() }),
21822
+ external_exports.object({
21823
+ type: external_exports.literal("image_url"),
21824
+ image_url: external_exports.object({ url: external_exports.string(), detail: external_exports.string().optional() })
21825
+ })
21826
+ ])
21827
+ )
21828
+ ]);
21829
+ var OpenAIMessage = external_exports.object({
21830
+ role: external_exports.enum(["system", "user", "assistant", "tool"]),
21831
+ content: OpenAIMessageContent,
21832
+ name: external_exports.string().optional(),
21833
+ reasoning_content: external_exports.string().optional(),
21834
+ thinking: external_exports.string().optional(),
21835
+ think: external_exports.string().optional()
21836
+ });
21837
+ var OpenAIFunctionCall = external_exports.object({
21838
+ name: external_exports.string(),
21839
+ arguments: external_exports.string()
21840
+ });
21841
+ var OpenAIToolCallSchema = external_exports.object({
21842
+ index: external_exports.number().optional(),
21843
+ id: external_exports.string().optional(),
21844
+ type: external_exports.literal("function").optional(),
21845
+ function: external_exports.object({
21846
+ name: external_exports.string().optional(),
21847
+ arguments: external_exports.string().optional()
21848
+ })
21849
+ });
21850
+ var OpenAIMessageWithFunctionCall = OpenAIMessage.extend({
21851
+ content: external_exports.union([external_exports.string(), external_exports.array(external_exports.object({ type: external_exports.literal("text"), text: external_exports.string() }))]).optional(),
21852
+ function_call: OpenAIFunctionCall.optional(),
21853
+ tool_calls: external_exports.array(OpenAIToolCallSchema).optional()
21854
+ });
21855
+ var OpenAIToolDefinition = external_exports.object({
21856
+ type: external_exports.literal("function"),
21857
+ function: external_exports.object({
21858
+ name: external_exports.string(),
21859
+ description: external_exports.string().optional(),
21860
+ parameters: external_exports.record(external_exports.string(), JsonValueSchema)
21861
+ })
21862
+ });
21863
+ var OpenAIRequestSchema = external_exports.object({
21864
+ model: external_exports.string(),
21865
+ messages: external_exports.array(OpenAIMessage),
21866
+ temperature: external_exports.number().optional(),
21867
+ max_tokens: external_exports.number().optional(),
21868
+ stream: external_exports.boolean().optional(),
21869
+ tools: external_exports.array(OpenAIToolDefinition).optional(),
21870
+ tool_choice: external_exports.union([
21871
+ external_exports.enum(["auto", "none", "required"]),
21872
+ external_exports.object({ type: external_exports.literal("auto") }),
21873
+ external_exports.object({ type: external_exports.literal("none") }),
21874
+ external_exports.object({ type: external_exports.literal("function"), function: external_exports.object({ name: external_exports.string() }) })
21875
+ ]).optional(),
21876
+ user: external_exports.string().optional()
21877
+ });
21878
+ var OpenAIResponsesToolDefinition = external_exports.object({
21879
+ type: external_exports.string(),
21880
+ name: external_exports.string().optional(),
21881
+ description: external_exports.string().optional(),
21882
+ parameters: JsonValueSchema.optional(),
21883
+ function: external_exports.object({
21884
+ name: external_exports.string().optional(),
21885
+ description: external_exports.string().optional(),
21886
+ parameters: JsonValueSchema.optional()
21887
+ }).optional()
21888
+ }).passthrough();
21889
+ var OpenAIResponsesRequestSchema = external_exports.object({
21890
+ model: external_exports.string(),
21891
+ input: external_exports.unknown().optional(),
21892
+ instructions: external_exports.union([external_exports.string(), external_exports.null()]).optional(),
21893
+ stream: external_exports.boolean().optional(),
21894
+ tools: external_exports.array(OpenAIResponsesToolDefinition).optional(),
21895
+ tool_choice: external_exports.unknown().optional(),
21896
+ max_output_tokens: external_exports.number().optional(),
21897
+ temperature: external_exports.number().optional(),
21898
+ user: external_exports.string().optional(),
21899
+ metadata: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
21900
+ }).passthrough();
21901
+ var OpenAIChoiceDelta = external_exports.object({
21902
+ role: external_exports.enum(["assistant"]).optional(),
21903
+ content: external_exports.string().nullable().optional(),
21904
+ reasoning_content: external_exports.string().nullable().optional(),
21905
+ thinking: external_exports.string().nullable().optional(),
21906
+ think: external_exports.string().nullable().optional(),
21907
+ function_call: external_exports.object({ name: external_exports.string().optional(), arguments: external_exports.string().optional() }).nullable().optional(),
21908
+ tool_calls: external_exports.array(
21909
+ external_exports.object({
21910
+ index: external_exports.number(),
21911
+ id: external_exports.string().optional(),
21912
+ type: external_exports.literal("function").optional(),
21913
+ function: external_exports.object({
21914
+ name: external_exports.string().optional(),
21915
+ arguments: external_exports.string().optional()
21916
+ })
21917
+ })
21918
+ ).nullable().optional()
21919
+ });
21920
+ var OpenAIChoice = external_exports.object({
21921
+ index: external_exports.number(),
21922
+ message: external_exports.object({
21923
+ role: external_exports.enum(["assistant"]),
21924
+ content: external_exports.string().nullable(),
21925
+ reasoning_content: external_exports.string().optional(),
21926
+ thinking: external_exports.string().optional(),
21927
+ think: external_exports.string().optional(),
21928
+ function_call: external_exports.object({ name: external_exports.string(), arguments: external_exports.string() }).nullable().optional(),
21929
+ tool_calls: external_exports.array(OpenAIToolCallSchema).optional()
21930
+ }).optional(),
21931
+ delta: OpenAIChoiceDelta.optional(),
21932
+ finish_reason: external_exports.string().nullable()
21933
+ });
21934
+ var OpenAIResponseSchema = external_exports.object({
21935
+ id: external_exports.string(),
21936
+ object: external_exports.literal("chat.completion"),
21937
+ created: external_exports.number(),
21938
+ model: external_exports.string(),
21939
+ choices: external_exports.array(OpenAIChoice),
21940
+ usage: external_exports.object({
21941
+ prompt_tokens: external_exports.number().nullable().optional(),
21942
+ completion_tokens: external_exports.number().nullable().optional(),
21943
+ total_tokens: external_exports.number().nullable().optional()
21944
+ }).passthrough(),
21945
+ service_tier: external_exports.string().nullable().optional(),
21946
+ system_fingerprint: external_exports.string().nullable().optional()
21947
+ }).passthrough();
21948
+ var OpenAISSERawChunkSchema = external_exports.object({
21949
+ id: external_exports.string(),
21950
+ object: external_exports.literal("chat.completion.chunk"),
21951
+ created: external_exports.number(),
21952
+ model: external_exports.string(),
21953
+ choices: external_exports.array(
21954
+ external_exports.object({
21955
+ index: external_exports.number(),
21956
+ delta: OpenAIChoiceDelta,
21957
+ finish_reason: external_exports.string().nullable().optional()
21958
+ })
21959
+ ),
21960
+ usage: external_exports.object({
21961
+ prompt_tokens: external_exports.number().nullable().optional(),
21962
+ completion_tokens: external_exports.number().nullable().optional(),
21963
+ total_tokens: external_exports.number().nullable().optional()
21964
+ }).passthrough().nullable().optional()
21965
+ }).passthrough();
21966
+ var OpenAIResponsesUsageSchema = external_exports.object({
21967
+ input_tokens: external_exports.number().nullable().optional(),
21968
+ output_tokens: external_exports.number().nullable().optional(),
21969
+ total_tokens: external_exports.number().nullable().optional(),
21970
+ input_tokens_details: external_exports.object({
21971
+ cached_tokens: external_exports.number().nullable().optional()
21972
+ }).passthrough().nullable().optional(),
21973
+ output_tokens_details: external_exports.object({
21974
+ reasoning_tokens: external_exports.number().nullable().optional()
21975
+ }).passthrough().nullable().optional()
21976
+ }).passthrough();
21977
+ var OpenAIResponsesOutputItemSchema = external_exports.object({
21978
+ type: external_exports.string()
21979
+ }).passthrough();
21980
+ var OpenAIResponsesResponseSchema = external_exports.object({
21981
+ id: external_exports.string(),
21982
+ object: external_exports.literal("response").optional(),
21983
+ created_at: external_exports.number().optional(),
21984
+ status: external_exports.string().nullable().optional(),
21985
+ model: external_exports.string().optional(),
21986
+ output: external_exports.array(OpenAIResponsesOutputItemSchema).optional(),
21987
+ output_text: external_exports.string().optional(),
21988
+ usage: OpenAIResponsesUsageSchema.nullable().optional(),
21989
+ error: external_exports.unknown().optional()
21990
+ }).passthrough();
21991
+ var OpenAIResponsesSSEventSchema = external_exports.object({
21992
+ type: external_exports.string()
21993
+ }).passthrough();
21994
+
21995
+ // src/proxy/schemas.ts
21996
+ var RequestModelSchema = external_exports.object({
21997
+ model: external_exports.string()
21998
+ });
21999
+ var LooseRequestSchema = external_exports.object({
22000
+ model: external_exports.string().optional(),
22001
+ metadata: external_exports.object({ user_id: external_exports.string().optional() }).passthrough().optional()
22002
+ });
22003
+
22004
+ // src/proxy/jsonlRecovery.ts
22005
+ init_privateDataPath();
22006
+ var READ_CHUNK_BYTES = 64 * 1024;
22007
+ var QUARANTINE_DIRECTORY = "quarantine";
22008
+ var activeContext = null;
22009
+ function emptyHealth() {
22010
+ return {
22011
+ level: "ok",
22012
+ diagnostic: null,
22013
+ repairedFiles: 0,
22014
+ delimiterRepairs: 0,
22015
+ quarantinedTails: 0
22016
+ };
22017
+ }
22018
+ function contextFor(logDir) {
22019
+ const root = resolve2(logDir);
22020
+ if (activeContext === null || activeContext.root !== root) {
22021
+ activeContext = {
22022
+ root,
22023
+ health: emptyHealth(),
22024
+ startup: null,
22025
+ startupComplete: false,
22026
+ safeFiles: /* @__PURE__ */ new Map()
22027
+ };
22028
+ }
22029
+ return activeContext;
22030
+ }
22031
+ function copyHealth(health) {
22032
+ return { ...health };
22033
+ }
22034
+ function hashBytes(value) {
22035
+ return createHash("sha256").update(value).digest("hex");
22036
+ }
22037
+ function signatureFromStats(stats) {
22038
+ return {
22039
+ size: stats.size,
22040
+ modifiedAt: stats.mtimeMs,
22041
+ changedAt: stats.ctimeMs,
22042
+ device: stats.dev,
22043
+ inode: stats.ino
22044
+ };
22045
+ }
22046
+ function signaturesMatch(left, right) {
22047
+ return left.size === right.size && left.modifiedAt === right.modifiedAt && left.changedAt === right.changedAt && left.device === right.device && left.inode === right.inode;
22048
+ }
22049
+ function validateRecord(bytes) {
22050
+ const withoutCarriageReturn = bytes.length > 0 && bytes[bytes.length - 1] === 13 ? bytes.subarray(0, -1) : bytes;
22051
+ const text = withoutCarriageReturn.toString("utf-8");
22052
+ if (text.trim() === "") return { valid: true };
22053
+ let parsed;
22054
+ try {
22055
+ parsed = JSON.parse(text);
22056
+ } catch {
22057
+ return { valid: false, reason: "invalid-json" };
22058
+ }
22059
+ return CapturedLogSchema.safeParse(parsed).success ? { valid: true } : { valid: false, reason: "invalid-log-schema" };
22060
+ }
22061
+ async function scanFile(handle, size) {
22062
+ const readBuffer = Buffer2.alloc(READ_CHUNK_BYTES);
22063
+ let position = 0;
22064
+ let pending = Buffer2.alloc(0);
22065
+ let pendingOffset = 0;
22066
+ let line = 1;
22067
+ while (position < size) {
22068
+ const length = Math.min(READ_CHUNK_BYTES, size - position);
22069
+ let bytesRead;
22070
+ try {
22071
+ const result = await handle.read(readBuffer, 0, length, position);
22072
+ bytesRead = result.bytesRead;
22073
+ } catch (error51) {
22074
+ return { status: "failed", reason: `read failed: ${String(error51)}` };
22075
+ }
22076
+ if (bytesRead === 0) {
22077
+ return { status: "failed", reason: "captured-log file changed while being scanned" };
22078
+ }
22079
+ const chunk = readBuffer.subarray(0, bytesRead);
22080
+ const combined = pending.length === 0 ? Buffer2.from(chunk) : Buffer2.concat([pending, chunk]);
22081
+ let consumed = 0;
22082
+ let newline = combined.indexOf(10, consumed);
22083
+ while (newline !== -1) {
22084
+ const record2 = combined.subarray(consumed, newline);
22085
+ const validation = validateRecord(record2);
22086
+ if (!validation.valid) {
22087
+ return {
22088
+ status: "interior-corruption",
22089
+ line,
22090
+ byteOffset: pendingOffset + consumed,
22091
+ recordHash: hashBytes(record2),
22092
+ reason: validation.reason
22093
+ };
22094
+ }
22095
+ consumed = newline + 1;
22096
+ line += 1;
22097
+ newline = combined.indexOf(10, consumed);
22098
+ }
22099
+ pending = Buffer2.from(combined.subarray(consumed));
22100
+ pendingOffset += consumed;
22101
+ position += bytesRead;
22102
+ }
22103
+ return { status: "complete", pending, pendingOffset, nextLine: line };
22104
+ }
22105
+ async function syncDirectory(directory) {
22106
+ let handle = null;
22107
+ try {
22108
+ handle = await open(directory, "r");
22109
+ await handle.sync();
22110
+ } catch {
22111
+ } finally {
22112
+ await handle?.close().catch(() => void 0);
22113
+ }
22114
+ }
22115
+ async function removeTemporaryFile(filePath) {
22116
+ await unlink(filePath).catch(() => void 0);
22117
+ }
22118
+ function readOwnProperty(value, name) {
22119
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
22120
+ return Object.getOwnPropertyDescriptor(value, name)?.value;
22121
+ }
22122
+ async function inspectExistingQuarantine(filePath, expected) {
22123
+ if (!existsSync2(filePath)) return "missing";
22124
+ try {
22125
+ const parsed = JSON.parse(await readFile(filePath, "utf-8"));
22126
+ return readOwnProperty(parsed, "schemaVersion") === expected.schemaVersion && readOwnProperty(parsed, "reason") === expected.reason && readOwnProperty(parsed, "sourceFile") === expected.sourceFile && readOwnProperty(parsed, "sourcePathHash") === expected.sourcePathHash && readOwnProperty(parsed, "tailOffset") === expected.tailOffset && readOwnProperty(parsed, "tailBytes") === expected.tailBytes && readOwnProperty(parsed, "tailSha256") === expected.tailSha256 && readOwnProperty(parsed, "tailBase64") === expected.tailBase64 ? "matching" : "conflicting";
22127
+ } catch {
22128
+ return "conflicting";
22129
+ }
22130
+ }
22131
+ async function quarantineTail(context, sourceFile, tail, tailOffset) {
22132
+ const quarantineDirectory = join3(context.root, QUARANTINE_DIRECTORY);
22133
+ if (!ensurePrivateDirectorySync(quarantineDirectory)) return null;
22134
+ const tailSha256 = hashBytes(tail);
22135
+ const sourceFileName = basename(sourceFile);
22136
+ const destination = join3(quarantineDirectory, `${sourceFileName}.tail-${tailSha256}.json`);
22137
+ const temporary = join3(
22138
+ quarantineDirectory,
22139
+ `.${sourceFileName}.${String(process.pid)}.${randomUUID()}.tmp`
22140
+ );
22141
+ const record2 = {
22142
+ schemaVersion: 1,
22143
+ reason: "invalid-final-jsonl-record",
22144
+ sourceFile: sourceFileName,
22145
+ sourcePathHash: hashBytes(Buffer2.from(resolve2(sourceFile), "utf-8")),
22146
+ recoveredAt: (/* @__PURE__ */ new Date()).toISOString(),
22147
+ tailOffset,
22148
+ tailBytes: tail.length,
22149
+ tailSha256,
22150
+ tailBase64: tail.toString("base64")
22151
+ };
22152
+ const existing = await inspectExistingQuarantine(destination, record2);
22153
+ if (existing === "matching") {
22154
+ return securePrivateFileSync(destination) ? destination : null;
22155
+ }
22156
+ if (existing === "conflicting") return null;
22157
+ let handle = null;
22158
+ try {
22159
+ handle = await open(temporary, "wx", PRIVATE_FILE_MODE);
22160
+ await handle.writeFile(`${JSON.stringify(record2)}
22161
+ `, "utf-8");
22162
+ await handle.sync();
22163
+ await handle.close();
22164
+ handle = null;
22165
+ if (!securePrivateFileSync(temporary)) {
22166
+ await removeTemporaryFile(temporary);
22167
+ return null;
22168
+ }
22169
+ await link(temporary, destination);
22170
+ await unlink(temporary);
22171
+ if (!securePrivateFileSync(destination)) return null;
22172
+ await syncDirectory(quarantineDirectory);
22173
+ return destination;
22174
+ } catch {
22175
+ await handle?.close().catch(() => void 0);
22176
+ await removeTemporaryFile(temporary);
22177
+ const racedExisting = await inspectExistingQuarantine(destination, record2);
22178
+ return racedExisting === "matching" && securePrivateFileSync(destination) ? destination : null;
22179
+ }
22180
+ }
22181
+ async function recoverFile(context, filePath) {
22182
+ if (!existsSync2(filePath)) {
22183
+ context.safeFiles.delete(filePath);
22184
+ return {
22185
+ status: "clean",
22186
+ signature: { size: 0, modifiedAt: 0, changedAt: 0, device: 0, inode: 0 }
22187
+ };
22188
+ }
22189
+ let fileStats;
22190
+ try {
22191
+ fileStats = await lstat(filePath);
22192
+ } catch (error51) {
22193
+ return { status: "failed", reason: `stat failed: ${String(error51)}` };
22194
+ }
22195
+ if (fileStats.isSymbolicLink() || !fileStats.isFile()) {
22196
+ return { status: "failed", reason: "captured-log path is not a regular private file" };
22197
+ }
22198
+ const initialSignature = signatureFromStats(fileStats);
22199
+ const safeSignature = context.safeFiles.get(filePath);
22200
+ if (safeSignature !== void 0 && signaturesMatch(safeSignature, initialSignature)) {
22201
+ return { status: "clean", signature: initialSignature };
22202
+ }
22203
+ let handle = null;
22204
+ try {
22205
+ handle = await open(filePath, constants2.O_RDWR | constants2.O_NOFOLLOW);
22206
+ const openedStats = await handle.stat();
22207
+ if (!openedStats.isFile()) {
22208
+ return { status: "failed", reason: "captured-log handle is not a regular file" };
22209
+ }
22210
+ const openedSignature = signatureFromStats(openedStats);
22211
+ if (!signaturesMatch(initialSignature, openedSignature)) {
22212
+ return {
22213
+ status: "failed",
22214
+ reason: "captured-log path changed between metadata validation and open"
22215
+ };
22216
+ }
22217
+ const scan = await scanFile(handle, openedStats.size);
22218
+ const scannedSignature = signatureFromStats(await handle.stat());
22219
+ if (!signaturesMatch(openedSignature, scannedSignature)) {
22220
+ return {
22221
+ status: "failed",
22222
+ reason: "captured-log file changed while recovery was scanning it"
22223
+ };
22224
+ }
22225
+ switch (scan.status) {
22226
+ case "failed":
22227
+ return scan;
22228
+ case "interior-corruption":
22229
+ return scan;
22230
+ case "complete": {
22231
+ if (scan.pending.length === 0) {
22232
+ return { status: "clean", signature: signatureFromStats(await handle.stat()) };
22233
+ }
22234
+ const validation = validateRecord(scan.pending);
22235
+ if (validation.valid) {
22236
+ const write = await handle.write(Buffer2.from("\n", "utf-8"), 0, 1, openedStats.size);
22237
+ if (write.bytesWritten !== 1) {
22238
+ return { status: "failed", reason: "captured-log delimiter append was incomplete" };
22239
+ }
22240
+ await handle.sync();
22241
+ return {
22242
+ status: "delimiter-appended",
22243
+ signature: signatureFromStats(await handle.stat())
22244
+ };
22245
+ }
22246
+ const quarantinePath = await quarantineTail(
22247
+ context,
22248
+ filePath,
22249
+ scan.pending,
22250
+ scan.pendingOffset
22251
+ );
22252
+ if (quarantinePath === null) {
22253
+ return { status: "failed", reason: "invalid tail could not be privately quarantined" };
22254
+ }
22255
+ const beforeTruncate = signatureFromStats(await handle.stat());
22256
+ if (!signaturesMatch(openedSignature, beforeTruncate)) {
22257
+ return {
22258
+ status: "failed",
22259
+ reason: "captured-log file changed before invalid-tail truncation"
22260
+ };
22261
+ }
22262
+ await handle.truncate(scan.pendingOffset);
22263
+ await handle.sync();
22264
+ return {
22265
+ status: "tail-quarantined",
22266
+ signature: signatureFromStats(await handle.stat())
22267
+ };
22268
+ }
22269
+ }
22270
+ return { status: "failed", reason: "unsupported captured-log scan state" };
22271
+ } catch (error51) {
22272
+ return { status: "failed", reason: String(error51) };
22273
+ } finally {
22274
+ await handle?.close().catch(() => void 0);
22275
+ }
22276
+ }
22277
+ function applyFileResult(context, filePath, result) {
22278
+ const file2 = basename(filePath);
22279
+ switch (result.status) {
22280
+ case "clean":
22281
+ context.safeFiles.set(filePath, result.signature);
22282
+ return;
22283
+ case "delimiter-appended":
22284
+ context.safeFiles.set(filePath, result.signature);
22285
+ context.health.delimiterRepairs += 1;
22286
+ context.health.repairedFiles += 1;
22287
+ return;
22288
+ case "tail-quarantined":
22289
+ context.safeFiles.set(filePath, result.signature);
22290
+ context.health.quarantinedTails += 1;
22291
+ context.health.repairedFiles += 1;
22292
+ if (context.health.level !== "unavailable") {
22293
+ context.health.level = "degraded";
22294
+ context.health.diagnostic = `Recovered and quarantined an invalid JSONL tail in ${file2}`;
22295
+ }
22296
+ return;
22297
+ case "interior-corruption":
22298
+ context.safeFiles.delete(filePath);
22299
+ context.health.level = "unavailable";
22300
+ context.health.diagnostic = `JSONL corruption in ${file2} at line ${String(result.line)} (offset ${String(result.byteOffset)}, ${result.reason}, sha256 ${result.recordHash})`;
22301
+ return;
22302
+ case "failed":
22303
+ context.safeFiles.delete(filePath);
22304
+ context.health.level = "unavailable";
22305
+ context.health.diagnostic = `JSONL recovery failed for ${file2}: ${result.reason}`;
22306
+ }
22307
+ }
22308
+ async function runStartupRecovery(context) {
22309
+ if (!ensurePrivateDirectorySync(context.root)) {
22310
+ context.health.level = "unavailable";
22311
+ context.health.diagnostic = "Captured-log directory could not be made private";
22312
+ return copyHealth(context.health);
22313
+ }
22314
+ let entries;
22315
+ try {
22316
+ entries = await readdir(context.root);
22317
+ } catch (error51) {
22318
+ context.health.level = "unavailable";
22319
+ context.health.diagnostic = `Captured-log directory could not be listed: ${String(error51)}`;
22320
+ return copyHealth(context.health);
22321
+ }
22322
+ const files = entries.filter((entry) => entry.endsWith(".jsonl")).sort();
22323
+ for (const file2 of files) {
22324
+ const filePath = join3(context.root, file2);
22325
+ const result = await recoverFile(context, filePath);
22326
+ applyFileResult(context, filePath, result);
22327
+ if (context.health.level === "unavailable") break;
22328
+ }
22329
+ return copyHealth(context.health);
22330
+ }
22331
+ async function recoverCapturedLogStorage(logDir) {
22332
+ const context = contextFor(logDir);
22333
+ if (context.startup === null) {
22334
+ context.startup = runStartupRecovery(context).then((health) => {
22335
+ context.startupComplete = true;
22336
+ return health;
22337
+ });
22338
+ }
22339
+ return await context.startup;
22340
+ }
22341
+ function canAppendCapturedLogFileOnExit(logDir, filePath) {
22342
+ const context = contextFor(logDir);
22343
+ if (!context.startupComplete || context.health.level === "unavailable") return false;
22344
+ const trusted = context.safeFiles.get(filePath);
22345
+ if (trusted === void 0) return false;
22346
+ try {
22347
+ const observed = lstatSync2(filePath);
22348
+ if (observed === void 0 || observed.isSymbolicLink() || !observed.isFile()) return false;
22349
+ return signaturesMatch(trusted, signatureFromStats(observed));
22350
+ } catch {
22351
+ return false;
22352
+ }
22353
+ }
22354
+
22355
+ // src/proxy/logger.ts
22356
+ var DAY_MS = 24 * 60 * 60 * 1e3;
22357
+ var DEFAULT_RETENTION_DAYS = 7;
22358
+ var DEFAULT_APP_LOG_MAX_BYTES = 10 * 1024 * 1024;
22359
+ var DEFAULT_APP_LOG_MAX_FILES = 5;
22360
+ function writeStderrFallback(message, error51) {
22361
+ process.stderr.write(`${message} ${String(error51)}
22362
+ `);
22363
+ }
22364
+ function resolveLogDir() {
22365
+ const logDirEnv = process.env["LOG_DIR"];
22366
+ if (logDirEnv !== void 0 && logDirEnv !== "") {
22367
+ return path.isAbsolute(logDirEnv) ? logDirEnv : path.join(getDataDir(), logDirEnv);
22368
+ }
22369
+ return path.join(getDataDir(), "logs");
22370
+ }
22371
+ function getLogFilePath() {
22372
+ const date5 = /* @__PURE__ */ new Date();
22373
+ const yyyy = date5.getUTCFullYear();
22374
+ const mm = String(date5.getUTCMonth() + 1).padStart(2, "0");
22375
+ const dd = String(date5.getUTCDate()).padStart(2, "0");
22376
+ return path.join(resolveLogDir(), `${yyyy}-${mm}-${dd}.jsonl`);
22377
+ }
22378
+ function getInspectorLogPath() {
22379
+ const logFileEnv = process.env["AGENT_INSPECTOR_LOG_FILE"];
22380
+ if (logFileEnv !== void 0 && logFileEnv !== "") {
22381
+ return logFileEnv;
22382
+ }
21073
22383
  return path.join(getDataDir(), "logs", "inspector.log");
21074
22384
  }
21075
22385
  function readBoundedInteger(name, fallback, minimum, maximum) {
@@ -21109,14 +22419,14 @@ function getRotatedGeneration(entry, baseName) {
21109
22419
  async function listApplicationLogFiles(logPath) {
21110
22420
  const directory = path.dirname(logPath);
21111
22421
  const baseName = path.basename(logPath);
21112
- const entries = await readdir(directory);
22422
+ const entries = await readdir2(directory);
21113
22423
  const files = [];
21114
22424
  for (const entry of entries) {
21115
22425
  const generation = entry === baseName ? 0 : getRotatedGeneration(entry, baseName);
21116
22426
  if (generation === null) continue;
21117
22427
  const filePath = path.join(directory, entry);
21118
22428
  try {
21119
- const fileStat = await stat(filePath);
22429
+ const fileStat = await stat2(filePath);
21120
22430
  if (!fileStat.isFile()) continue;
21121
22431
  files.push({ generation, modifiedAt: fileStat.mtimeMs, path: filePath });
21122
22432
  } catch {
@@ -21126,12 +22436,12 @@ async function listApplicationLogFiles(logPath) {
21126
22436
  }
21127
22437
  async function removeApplicationLog(filePath) {
21128
22438
  try {
21129
- await stat(filePath);
22439
+ await stat2(filePath);
21130
22440
  } catch {
21131
22441
  return true;
21132
22442
  }
21133
22443
  try {
21134
- await unlink(filePath);
22444
+ await unlink2(filePath);
21135
22445
  return true;
21136
22446
  } catch {
21137
22447
  return false;
@@ -21164,7 +22474,7 @@ async function maintainApplicationLogs(logPath, policy) {
21164
22474
  }
21165
22475
  async function moveApplicationLog(source, destination) {
21166
22476
  try {
21167
- await stat(source);
22477
+ await stat2(source);
21168
22478
  } catch {
21169
22479
  return true;
21170
22480
  }
@@ -21197,7 +22507,7 @@ async function rotateApplicationLog(logPath, maxFiles) {
21197
22507
  }
21198
22508
  async function rotateApplicationLogIfNeeded(logPath, incomingBytes, policy) {
21199
22509
  try {
21200
- const fileStat = await stat(logPath);
22510
+ const fileStat = await stat2(logPath);
21201
22511
  if (fileStat.size > 0 && fileStat.size + incomingBytes > policy.maxBytes) {
21202
22512
  return await rotateApplicationLog(logPath, policy.maxFiles);
21203
22513
  }
@@ -21216,16 +22526,24 @@ async function initLogger() {
21216
22526
  } else if (!securePrivateTreeSync(dir)) {
21217
22527
  writeStderrFallback("[logger] Failed to secure existing log files:", dir);
21218
22528
  } else {
21219
- const entries = await readdir(dir);
21220
- for (const entry of entries) {
21221
- if (!entry.endsWith(".jsonl")) continue;
21222
- const fullPath = path.join(dir, entry);
21223
- try {
21224
- const s = await stat(fullPath);
21225
- if (s.mtimeMs < cutoff) {
21226
- await unlink(fullPath);
22529
+ const recovery = await recoverCapturedLogStorage(dir);
22530
+ if (recovery.level === "unavailable") {
22531
+ writeStderrFallback(
22532
+ "[logger] Captured-log recovery blocked readiness:",
22533
+ recovery.diagnostic ?? "unknown recovery failure"
22534
+ );
22535
+ } else {
22536
+ const entries = await readdir2(dir);
22537
+ for (const entry of entries) {
22538
+ if (!entry.endsWith(".jsonl")) continue;
22539
+ const fullPath = path.join(dir, entry);
22540
+ try {
22541
+ const s = await stat2(fullPath);
22542
+ if (s.mtimeMs < cutoff) {
22543
+ await unlink2(fullPath);
22544
+ }
22545
+ } catch {
21227
22546
  }
21228
- } catch {
21229
22547
  }
21230
22548
  }
21231
22549
  }
@@ -21252,7 +22570,7 @@ async function appendApplicationLogLine(logPath, line, policy) {
21252
22570
  return { ok: false, reason: "application log directory is not private" };
21253
22571
  }
21254
22572
  try {
21255
- if (!await rotateApplicationLogIfNeeded(logPath, Buffer2.byteLength(line, "utf-8"), policy)) {
22573
+ if (!await rotateApplicationLogIfNeeded(logPath, Buffer3.byteLength(line, "utf-8"), policy)) {
21256
22574
  return { ok: false, reason: "application log rotation failed" };
21257
22575
  }
21258
22576
  await appendFile(logPath, line, { encoding: "utf-8", mode: PRIVATE_FILE_MODE });
@@ -21309,7 +22627,24 @@ process.on("exit", () => {
21309
22627
  try {
21310
22628
  const filePath = getLogFilePath();
21311
22629
  if (!ensurePrivateDirectorySync(path.dirname(filePath))) return;
21312
- appendFileSync(filePath, toWrite, { encoding: "utf-8", mode: PRIVATE_FILE_MODE });
22630
+ if (!canAppendCapturedLogFileOnExit(resolveLogDir(), filePath)) return;
22631
+ const descriptor = openSync2(
22632
+ filePath,
22633
+ constants3.O_WRONLY | constants3.O_APPEND | constants3.O_NOFOLLOW,
22634
+ PRIVATE_FILE_MODE
22635
+ );
22636
+ try {
22637
+ const payload = Buffer3.from(toWrite, "utf-8");
22638
+ let written = 0;
22639
+ while (written < payload.length) {
22640
+ const writeCount = writeSync(descriptor, payload, written, payload.length - written);
22641
+ if (writeCount === 0) return;
22642
+ written += writeCount;
22643
+ }
22644
+ fsyncSync(descriptor);
22645
+ } finally {
22646
+ closeSync2(descriptor);
22647
+ }
21313
22648
  void securePrivateFileSync(filePath);
21314
22649
  } catch {
21315
22650
  }
@@ -21323,15 +22658,15 @@ var RemotePortSchema = external_exports.number().int().min(1).max(65535);
21323
22658
  var SocketSchema = external_exports.object({
21324
22659
  remotePort: RemotePortSchema.nullish()
21325
22660
  }).passthrough();
21326
- function parseCommandLine(commandLine, sep) {
22661
+ function parseCommandLine(commandLine, sep3) {
21327
22662
  if (!commandLine) return { cwd: null, projectFolder: null };
21328
22663
  const exeMatch = commandLine.match(/^"([^"]+)"/) || commandLine.match(/^([^\s]+)/);
21329
22664
  if (exeMatch?.[1] !== void 0) {
21330
22665
  const exePath = exeMatch[1];
21331
- const lastSep = exePath.lastIndexOf(sep);
22666
+ const lastSep = exePath.lastIndexOf(sep3);
21332
22667
  if (lastSep > 0) {
21333
22668
  const exeDir = exePath.substring(0, lastSep);
21334
- return { cwd: exeDir, projectFolder: exeDir.split(sep).pop() ?? null };
22669
+ return { cwd: exeDir, projectFolder: exeDir.split(sep3).pop() ?? null };
21335
22670
  }
21336
22671
  }
21337
22672
  return { cwd: null, projectFolder: null };
@@ -21357,7 +22692,7 @@ async function lookupProcessInfo(pid) {
21357
22692
  if (commandLine.length > 0) return parseCommandLine(commandLine, "\\");
21358
22693
  } else {
21359
22694
  try {
21360
- const raw = await readFile(`/proc/${String(pid)}/cmdline`, "utf8");
22695
+ const raw = await readFile2(`/proc/${String(pid)}/cmdline`, "utf8");
21361
22696
  const commandLine = raw.replace(/\0/g, " ").trim();
21362
22697
  if (commandLine) {
21363
22698
  return parseCommandLine(commandLine, "/");
@@ -21473,6 +22808,88 @@ async function lookupPidByLocalPort(clientPort) {
21473
22808
  }
21474
22809
  }
21475
22810
 
22811
+ // src/proxy/uiAssetServer.ts
22812
+ import { readFile as readFile3 } from "node:fs/promises";
22813
+ import { extname, relative, resolve as resolve3, sep as sep2 } from "node:path";
22814
+ var CONTENT_TYPES = {
22815
+ ".css": "text/css; charset=utf-8",
22816
+ ".html": "text/html; charset=utf-8",
22817
+ ".ico": "image/x-icon",
22818
+ ".jpeg": "image/jpeg",
22819
+ ".jpg": "image/jpeg",
22820
+ ".js": "text/javascript; charset=utf-8",
22821
+ ".json": "application/json; charset=utf-8",
22822
+ ".map": "application/json; charset=utf-8",
22823
+ ".png": "image/png",
22824
+ ".svg": "image/svg+xml",
22825
+ ".webp": "image/webp",
22826
+ ".woff": "font/woff",
22827
+ ".woff2": "font/woff2"
22828
+ };
22829
+ function safeDecodedPath(pathname) {
22830
+ let decoded;
22831
+ try {
22832
+ decoded = decodeURIComponent(pathname);
22833
+ } catch {
22834
+ return null;
22835
+ }
22836
+ if (decoded.includes("\\") || decoded.includes("\0")) return null;
22837
+ if (decoded.split("/").some((segment) => segment === "." || segment === "..")) return null;
22838
+ return decoded;
22839
+ }
22840
+ function isInsideDirectory(path2, directory) {
22841
+ const child = relative(directory, path2);
22842
+ return child === "" || !child.startsWith("..") && !child.startsWith(sep2);
22843
+ }
22844
+ function assetPath(uiDirectory, pathname) {
22845
+ const decoded = safeDecodedPath(pathname);
22846
+ if (decoded === null) return null;
22847
+ const target = resolve3(uiDirectory, `.${decoded}`);
22848
+ return isInsideDirectory(target, resolve3(uiDirectory)) ? target : null;
22849
+ }
22850
+ async function readAsset(path2) {
22851
+ try {
22852
+ return await readFile3(path2);
22853
+ } catch {
22854
+ return null;
22855
+ }
22856
+ }
22857
+ function contentTypeFor(path2) {
22858
+ return CONTENT_TYPES[extname(path2).toLowerCase()] ?? "application/octet-stream";
22859
+ }
22860
+ function isBackendNamespace(pathname) {
22861
+ return pathname === "/api" || pathname.startsWith("/api/") || pathname === "/proxy" || pathname.startsWith("/proxy/") || pathname === "/livez" || pathname === "/readyz";
22862
+ }
22863
+ function shouldUseDocumentFallback(pathname) {
22864
+ if (pathname === "/" || pathname.length === 0) return true;
22865
+ const lastSegment = pathname.split("/").at(-1) ?? "";
22866
+ return !lastSegment.includes(".");
22867
+ }
22868
+ async function loadUiAsset(uiDirectory, pathname) {
22869
+ if (isBackendNamespace(pathname)) return null;
22870
+ const requestedPath = assetPath(uiDirectory, pathname);
22871
+ if (requestedPath === null) return null;
22872
+ const requestedBody = await readAsset(requestedPath);
22873
+ if (requestedBody !== null) {
22874
+ return {
22875
+ body: requestedBody,
22876
+ cacheControl: pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache",
22877
+ contentType: contentTypeFor(requestedPath),
22878
+ document: extname(requestedPath).toLowerCase() === ".html"
22879
+ };
22880
+ }
22881
+ if (!shouldUseDocumentFallback(pathname)) return null;
22882
+ const indexPath = resolve3(uiDirectory, "index.html");
22883
+ const indexBody = await readAsset(indexPath);
22884
+ if (indexBody === null) return null;
22885
+ return {
22886
+ body: indexBody,
22887
+ cacheControl: "no-store",
22888
+ contentType: CONTENT_TYPES[".html"] ?? "text/html; charset=utf-8",
22889
+ document: true
22890
+ };
22891
+ }
22892
+
21476
22893
  // src/proxy/identityProxy.ts
21477
22894
  var IDENTITY_HEADERS = {
21478
22895
  pid: "x-agent-inspector-client-pid",
@@ -21489,26 +22906,19 @@ var ALIAS_CONTROL_MAX_REQUEST_BYTES = 8 * 1024;
21489
22906
  var IDENTITY_PROXY_CONTROL_NAMESPACE = "/.agent-inspector";
21490
22907
  var IDENTITY_PROXY_ALIAS_CONTROL_PATH = `${IDENTITY_PROXY_CONTROL_NAMESPACE}/base-path-aliases`;
21491
22908
  function normalizedAliasCandidate(value) {
21492
- const normalized = normalizeBasePath(value);
21493
- if (normalized.length === 0) {
21494
- return {
21495
- ok: false,
21496
- code: "invalid",
21497
- error: "Base Path aliases must be non-root paths such as /inspector."
21498
- };
21499
- }
21500
- if (!/^\/[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*$/u.test(normalized)) {
22909
+ const normalized = normalizePublicBasePath(value);
22910
+ if (normalized === null) {
21501
22911
  return {
21502
22912
  ok: false,
21503
22913
  code: "invalid",
21504
22914
  error: "Base Path aliases may contain only URL-safe path segments."
21505
22915
  };
21506
22916
  }
21507
- if (normalized.split("/").some((segment) => segment === "." || segment === "..")) {
22917
+ if (normalized.length === 0) {
21508
22918
  return {
21509
22919
  ok: false,
21510
22920
  code: "invalid",
21511
- error: "Base Path aliases cannot contain dot path segments."
22921
+ error: "Base Path aliases must be non-root paths such as /inspector."
21512
22922
  };
21513
22923
  }
21514
22924
  if (normalized === IDENTITY_PROXY_CONTROL_NAMESPACE || normalized.startsWith(`${IDENTITY_PROXY_CONTROL_NAMESPACE}/`)) {
@@ -21537,7 +22947,7 @@ function basePathsOverlap(left, right) {
21537
22947
  return pathMatchesBasePath(left, right) || pathMatchesBasePath(right, left);
21538
22948
  }
21539
22949
  function createBasePathAliasRegistry(canonicalBasePath, initialAliases = []) {
21540
- const canonical = normalizeBasePath(canonicalBasePath);
22950
+ const canonical = normalizePublicBasePath(canonicalBasePath) ?? "";
21541
22951
  const aliases = [];
21542
22952
  const add = (value) => {
21543
22953
  const candidate = normalizedAliasCandidate(value);
@@ -21596,7 +23006,7 @@ function createBasePathAliasRegistry(canonicalBasePath, initialAliases = []) {
21596
23006
  aliases.splice(index, 1);
21597
23007
  return { ok: true, basePath: candidate.basePath };
21598
23008
  };
21599
- const resolve2 = (rawPath) => {
23009
+ const resolve5 = (rawPath) => {
21600
23010
  const requestUrl = new URL(rawPath, "http://localhost");
21601
23011
  const alias = aliases.find((candidate) => pathMatchesBasePath(requestUrl.pathname, candidate));
21602
23012
  const effectiveBasePath = alias ?? canonical;
@@ -21611,7 +23021,7 @@ function createBasePathAliasRegistry(canonicalBasePath, initialAliases = []) {
21611
23021
  add,
21612
23022
  list: () => [...aliases],
21613
23023
  remove,
21614
- resolve: resolve2
23024
+ resolve: resolve5
21615
23025
  };
21616
23026
  for (const alias of initialAliases) add(alias);
21617
23027
  return registry2;
@@ -21664,8 +23074,9 @@ function htmlAttributeEscape(value) {
21664
23074
  }
21665
23075
  function rewriteRootRelativePublicPath(value, effectiveBasePath, sourceBasePath) {
21666
23076
  if (!value.startsWith("/") || value.startsWith("//")) return value;
21667
- const normalizedEffective = normalizeBasePath(effectiveBasePath);
21668
- const normalizedSource = normalizeBasePath(sourceBasePath);
23077
+ const normalizedEffective = normalizePublicBasePath(effectiveBasePath);
23078
+ const normalizedSource = normalizePublicBasePath(sourceBasePath);
23079
+ if (normalizedEffective === null || normalizedSource === null) return value;
21669
23080
  const suffixIndex = value.search(/[?#]/u);
21670
23081
  const pathname = suffixIndex === -1 ? value : value.slice(0, suffixIndex);
21671
23082
  const suffix = suffixIndex === -1 ? "" : value.slice(suffixIndex);
@@ -21677,14 +23088,19 @@ function rewriteRootRelativePublicPath(value, effectiveBasePath, sourceBasePath)
21677
23088
  const trailingSlash = pathname.endsWith("/") && !rewritten.endsWith("/") ? "/" : "";
21678
23089
  return `${rewritten}${trailingSlash}${suffix}`;
21679
23090
  }
21680
- function rewriteQuotedAssetPaths(source, effectiveBasePath, sourceBasePath) {
23091
+ function rewriteQuotedAssetPaths(source, effectiveBasePath, sourceBasePath, escape) {
21681
23092
  return source.replace(
21682
23093
  /(["'])(\/(?:[^"'/]+\/)*assets\/)/gu,
21683
- (_match, quote, assetPrefix) => `${quote}${rewriteRootRelativePublicPath(assetPrefix, effectiveBasePath, sourceBasePath)}`
23094
+ (_match, quote, assetPrefix) => `${quote}${escape(
23095
+ rewriteRootRelativePublicPath(assetPrefix, effectiveBasePath, sourceBasePath),
23096
+ quote
23097
+ )}`
21684
23098
  );
21685
23099
  }
21686
23100
  function rewriteHtmlForBasePath(html, basePath, sourceBasePath = "") {
21687
- const normalized = normalizeBasePath(basePath);
23101
+ const normalized = normalizePublicBasePath(basePath);
23102
+ const normalizedSource = normalizePublicBasePath(sourceBasePath);
23103
+ if (normalized === null || normalizedSource === null) return html;
21688
23104
  const escaped = htmlAttributeEscape(normalized);
21689
23105
  const meta3 = `<meta name="${AGENT_INSPECTOR_BASE_PATH_META_NAME}" content="${escaped}">`;
21690
23106
  const metaPattern = new RegExp(
@@ -21700,29 +23116,112 @@ function rewriteHtmlForBasePath(html, basePath, sourceBasePath = "") {
21700
23116
  const withMeta = replacedMeta ? withReplacedMeta : withReplacedMeta.replace(/<head(\s[^>]*)?>/iu, (match) => `${match}${meta3}`);
21701
23117
  const withRewrittenAttributes = withMeta.replace(
21702
23118
  /\b(href|src|action)=(["'])(\/(?!\/)[^"']*)\2/giu,
21703
- (_match, attribute, quote, path2) => `${attribute}=${quote}${rewriteRootRelativePublicPath(path2, normalized, sourceBasePath)}${quote}`
23119
+ (_match, attribute, quote, path2) => `${attribute}=${quote}${htmlAttributeEscape(
23120
+ rewriteRootRelativePublicPath(path2, normalized, normalizedSource)
23121
+ )}${quote}`
23122
+ );
23123
+ return rewriteQuotedAssetPaths(
23124
+ withRewrittenAttributes,
23125
+ normalized,
23126
+ normalizedSource,
23127
+ (value) => htmlAttributeEscape(value)
21704
23128
  );
21705
- return rewriteQuotedAssetPaths(withRewrittenAttributes, normalized, sourceBasePath);
21706
23129
  }
21707
- function javaScriptStringEscape(value) {
21708
- return value.replace(/\\/gu, "\\\\").replace(/"/gu, '\\"');
23130
+ function javaScriptStringEscape(value, quote) {
23131
+ const escaped = value.replace(/\\/gu, "\\\\").replace(/\r/gu, "\\r").replace(/\n/gu, "\\n").replace(/\u2028/gu, "\\u2028").replace(/\u2029/gu, "\\u2029").replace(/&/gu, "\\u0026").replace(/</gu, "\\u003c").replace(/>/gu, "\\u003e");
23132
+ return quote === "'" ? escaped.replace(/'/gu, "\\'") : escaped.replace(/"/gu, '\\"');
21709
23133
  }
21710
23134
  function rewriteJavaScriptForBasePath(source, basePath, sourceBasePath = "") {
21711
- const normalized = normalizeBasePath(basePath);
21712
- const escaped = javaScriptStringEscape(normalized);
23135
+ const normalized = normalizePublicBasePath(basePath);
23136
+ const normalizedSource = normalizePublicBasePath(sourceBasePath);
23137
+ if (normalized === null || normalizedSource === null) return source;
23138
+ const escaped = javaScriptStringEscape(normalized, '"');
21713
23139
  const withRouterBasePath = source.replace(
21714
23140
  /\bbasepath:\s*(["'])[^"']*\1/gu,
21715
23141
  `basepath:"${escaped}"`
21716
23142
  );
21717
23143
  const withDynamicPaths = withRouterBasePath.replace(
21718
23144
  /return(["'])(\/(?!\/)[^"']*)\1\+([A-Za-z_$][\w$]*)/gu,
21719
- (_match, quote, path2, variable) => `return${quote}${rewriteRootRelativePublicPath(
21720
- path2,
21721
- normalized,
21722
- sourceBasePath
21723
- )}${quote}+${variable}`
23145
+ (_match, quote, path2, variable) => {
23146
+ const rewritten = rewriteRootRelativePublicPath(path2, normalized, normalizedSource);
23147
+ return `return${quote}${javaScriptStringEscape(rewritten, quote)}${quote}+${variable}`;
23148
+ }
23149
+ );
23150
+ return rewriteQuotedAssetPaths(
23151
+ withDynamicPaths,
23152
+ normalized,
23153
+ normalizedSource,
23154
+ javaScriptStringEscape
21724
23155
  );
21725
- return rewriteQuotedAssetPaths(withDynamicPaths, normalized, sourceBasePath);
23156
+ }
23157
+ function publicOriginForRequest(req, options) {
23158
+ if (options.publicOrigin !== void 0) {
23159
+ try {
23160
+ return new URL(options.publicOrigin).origin;
23161
+ } catch {
23162
+ }
23163
+ }
23164
+ return `http://${req.headers.host ?? "localhost"}`;
23165
+ }
23166
+ function browserRuntimeForRequest(req, options, basePath) {
23167
+ const origin = publicOriginForRequest(req, options);
23168
+ const normalized = normalizePublicBasePath(basePath) ?? "";
23169
+ const baseUrl = normalized.length === 0 ? origin : `${origin}${normalized}`;
23170
+ return {
23171
+ schemaVersion: BROWSER_RUNTIME_SCHEMA_VERSION,
23172
+ deploymentMode: "composed",
23173
+ ui: { basePath: normalized.length === 0 ? "/" : normalized },
23174
+ control: { baseUrl },
23175
+ agent: { baseUrl },
23176
+ capabilities: {
23177
+ rest: true,
23178
+ sse: true,
23179
+ proxy: true,
23180
+ mcp: true
23181
+ }
23182
+ };
23183
+ }
23184
+ function safeInlineJson(value) {
23185
+ return JSON.stringify(value).replace(/&/gu, "\\u0026").replace(/</gu, "\\u003c").replace(/>/gu, "\\u003e").replace(/\u2028/gu, "\\u2028").replace(/\u2029/gu, "\\u2029");
23186
+ }
23187
+ function injectBrowserRuntime(html, runtime) {
23188
+ const runtimeJson = safeInlineJson(runtime);
23189
+ if (html.includes(BROWSER_RUNTIME_JSON_PLACEHOLDER)) {
23190
+ return html.replaceAll(BROWSER_RUNTIME_JSON_PLACEHOLDER, runtimeJson);
23191
+ }
23192
+ const bootstrap = `<script>window.__AGENT_INSPECTOR_RUNTIME__=${runtimeJson};</script>`;
23193
+ return html.replace(/<head(\s[^>]*)?>/iu, (head) => `${head}${bootstrap}`);
23194
+ }
23195
+ async function tryServeUiAsset(req, res, options, pathname, basePath, sourceBasePath) {
23196
+ const method = (req.method ?? "GET").toUpperCase();
23197
+ if (method !== "GET" && method !== "HEAD" || options.uiDirectory === void 0) return false;
23198
+ const asset = await loadUiAsset(options.uiDirectory, pathname);
23199
+ if (asset === null) return false;
23200
+ let body = asset.body;
23201
+ if (asset.document) {
23202
+ const rewritten = rewriteHtmlForBasePath(body.toString("utf8"), basePath, sourceBasePath);
23203
+ body = Buffer4.from(
23204
+ injectBrowserRuntime(rewritten, browserRuntimeForRequest(req, options, basePath)),
23205
+ "utf8"
23206
+ );
23207
+ } else if (basePath.length > 0 && asset.contentType.includes("javascript")) {
23208
+ body = Buffer4.from(
23209
+ rewriteJavaScriptForBasePath(body.toString("utf8"), basePath, sourceBasePath),
23210
+ "utf8"
23211
+ );
23212
+ }
23213
+ const headers = {
23214
+ ...BASE_SECURITY_HEADERS,
23215
+ "cache-control": asset.cacheControl,
23216
+ "content-length": body.byteLength,
23217
+ "content-type": asset.contentType
23218
+ };
23219
+ if (asset.document) headers["content-security-policy"] = DOCUMENT_CONTENT_SECURITY_POLICY;
23220
+ res.writeHead(200, headers);
23221
+ if (method === "HEAD") res.end();
23222
+ else res.end(body);
23223
+ req.resume();
23224
+ return true;
21726
23225
  }
21727
23226
  function isLoopbackPeer(address) {
21728
23227
  if (address === void 0) return false;
@@ -21736,14 +23235,14 @@ function bearerToken(authorization) {
21736
23235
  }
21737
23236
  function tokensMatch(received, expected) {
21738
23237
  if (received === null || expected === void 0 || expected === "") return false;
21739
- const receivedBuffer = Buffer3.from(received);
21740
- const expectedBuffer = Buffer3.from(expected);
23238
+ const receivedBuffer = Buffer4.from(received);
23239
+ const expectedBuffer = Buffer4.from(expected);
21741
23240
  return receivedBuffer.byteLength === expectedBuffer.byteLength && timingSafeEqual(receivedBuffer, expectedBuffer);
21742
23241
  }
21743
23242
  function managedInstanceTokensMatch(received, expected) {
21744
23243
  if (received === void 0 || received.length === 0 || expected.length === 0) return false;
21745
- const receivedDigest = createHash("sha256").update(received).digest();
21746
- const expectedDigest = createHash("sha256").update(expected).digest();
23244
+ const receivedDigest = createHash2("sha256").update(received).digest();
23245
+ const expectedDigest = createHash2("sha256").update(expected).digest();
21747
23246
  return timingSafeEqual(receivedDigest, expectedDigest);
21748
23247
  }
21749
23248
  function firstHeader(value) {
@@ -21759,13 +23258,42 @@ function normalizedHostname(value) {
21759
23258
  }
21760
23259
  }
21761
23260
  function isLoopbackHostname(hostname3) {
21762
- return hostname3 === "localhost" || hostname3 === "::1" || hostname3.startsWith("127.");
23261
+ if (hostname3 === "localhost" || hostname3 === "::1") return true;
23262
+ if (isIP2(hostname3) !== 4) return false;
23263
+ return hostname3.split(".", 1)[0] === "127";
23264
+ }
23265
+ function parseUrl(value) {
23266
+ try {
23267
+ return new URL(value);
23268
+ } catch {
23269
+ return null;
23270
+ }
23271
+ }
23272
+ function parseUiOriginAllowlist(value) {
23273
+ const origins = [];
23274
+ const rejected = [];
23275
+ for (const entry of (value ?? "").split(",")) {
23276
+ const candidate = entry.trim();
23277
+ if (candidate.length === 0) continue;
23278
+ const parsed = parseUrl(candidate);
23279
+ const valid = parsed !== null && (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.username.length === 0 && parsed.password.length === 0 && parsed.pathname === "/" && parsed.search.length === 0 && parsed.hash.length === 0 && isLoopbackHostname(parsed.hostname.toLowerCase().replace(/^\[|\]$/gu, ""));
23280
+ if (!valid || parsed === null) {
23281
+ rejected.push(candidate);
23282
+ continue;
23283
+ }
23284
+ if (!origins.includes(parsed.origin)) origins.push(parsed.origin);
23285
+ }
23286
+ return { origins, rejected };
23287
+ }
23288
+ function normalizeTrustedPublicOrigin(value) {
23289
+ if (value === void 0) return void 0;
23290
+ return normalizedRequestOrigin(value.trim());
21763
23291
  }
21764
23292
  function isTrustedIdentityProxyHost(host, options) {
21765
23293
  if (host === void 0) return false;
21766
23294
  const hostname3 = normalizedHostname(host);
21767
23295
  if (hostname3 === null) return false;
21768
- if (isLoopbackHostname(hostname3) || isIP(hostname3) !== 0) return true;
23296
+ if (isLoopbackHostname(hostname3) || isIP2(hostname3) !== 0) return true;
21769
23297
  const listenHostname = normalizedHostname(options.listenHost);
21770
23298
  if (listenHostname !== null && hostname3 === listenHostname) return true;
21771
23299
  return (options.trustedHosts ?? []).some(
@@ -21776,6 +23304,83 @@ function isSafeMethod(method) {
21776
23304
  const normalized = (method ?? "GET").toUpperCase();
21777
23305
  return normalized === "GET" || normalized === "HEAD" || normalized === "OPTIONS";
21778
23306
  }
23307
+ var UI_CORS_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
23308
+ var UI_CORS_HEADERS = ["authorization", "content-type", "last-event-id", CSRF_HEADER];
23309
+ function normalizedRequestOrigin(origin) {
23310
+ try {
23311
+ const parsed = new URL(origin);
23312
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
23313
+ if (parsed.username.length > 0 || parsed.password.length > 0) return null;
23314
+ if (parsed.pathname !== "/" || parsed.search.length > 0 || parsed.hash.length > 0) return null;
23315
+ return parsed.origin;
23316
+ } catch {
23317
+ return null;
23318
+ }
23319
+ }
23320
+ function isSameRequestOrigin(origin, host, publicOrigin) {
23321
+ const parsedOrigin = normalizedRequestOrigin(origin);
23322
+ if (parsedOrigin === null) return false;
23323
+ const expected = normalizedRequestOrigin(publicOrigin ?? `http://${host ?? "localhost"}`);
23324
+ return expected !== null && parsedOrigin === expected;
23325
+ }
23326
+ function matchedUiOrigin(origin, allowedUiOrigins) {
23327
+ if (origin === void 0) return null;
23328
+ return (allowedUiOrigins ?? []).find((candidate) => candidate === origin) ?? null;
23329
+ }
23330
+ function isUiControlPath(pathname) {
23331
+ if (pathname === "/livez" || pathname === "/readyz") return true;
23332
+ if (pathname === "/api/mcp") return false;
23333
+ return pathname === "/api" || pathname.startsWith("/api/");
23334
+ }
23335
+ function isModelProxyPath(pathname) {
23336
+ return pathname === "/proxy" || pathname.startsWith("/proxy/");
23337
+ }
23338
+ function uiCorsHeaders(origin) {
23339
+ if (origin === null) return {};
23340
+ return {
23341
+ "access-control-allow-origin": origin,
23342
+ "access-control-expose-headers": "Content-Disposition",
23343
+ vary: "Origin"
23344
+ };
23345
+ }
23346
+ function appendVaryHeader(existing, name) {
23347
+ const values = (Array.isArray(existing) ? existing.join(",") : existing ?? "").split(",").map((value) => value.trim()).filter((value) => value.length > 0);
23348
+ if (!values.some((value) => value.toLowerCase() === name.toLowerCase())) values.push(name);
23349
+ return values.join(", ");
23350
+ }
23351
+ function requestedCorsHeaders(value) {
23352
+ return (value ?? "").split(",").map((header) => header.trim().toLowerCase()).filter((header) => header.length > 0);
23353
+ }
23354
+ function handleUiCorsPreflight(req, res, origin) {
23355
+ const requestedMethod = firstHeader(req.headers["access-control-request-method"])?.toUpperCase();
23356
+ const requestedHeaders = requestedCorsHeaders(
23357
+ firstHeader(req.headers["access-control-request-headers"])
23358
+ );
23359
+ const methodAllowed = requestedMethod !== void 0 && UI_CORS_METHODS.some((method) => method === requestedMethod);
23360
+ const headersAllowed = requestedHeaders.every(
23361
+ (header) => UI_CORS_HEADERS.some((allowedHeader) => allowedHeader === header)
23362
+ );
23363
+ if (!methodAllowed || !headersAllowed) {
23364
+ res.writeHead(403, {
23365
+ "content-type": "application/json; charset=utf-8",
23366
+ "cache-control": "no-store",
23367
+ ...uiCorsHeaders(origin)
23368
+ });
23369
+ res.end(JSON.stringify({ error: "CORS preflight is not allowed for this request." }));
23370
+ req.resume();
23371
+ return;
23372
+ }
23373
+ res.writeHead(204, {
23374
+ "cache-control": "no-store",
23375
+ "access-control-allow-origin": origin,
23376
+ "access-control-allow-methods": UI_CORS_METHODS.join(", "),
23377
+ "access-control-allow-headers": UI_CORS_HEADERS.join(", "),
23378
+ "access-control-max-age": "600",
23379
+ vary: "Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
23380
+ });
23381
+ res.end();
23382
+ req.resume();
23383
+ }
21779
23384
  function isPublicHealthPath(pathname) {
21780
23385
  return pathname === "/livez" || pathname === "/readyz" || pathname === "/api/health";
21781
23386
  }
@@ -21797,16 +23402,21 @@ function isBrowserMutationAllowed(input) {
21797
23402
  new URL(input.path, "http://localhost").pathname,
21798
23403
  input.basePath ?? getConfiguredBasePath(process.env)
21799
23404
  );
21800
- if (pathname === "/proxy" || pathname.startsWith("/proxy/")) return true;
21801
23405
  const fetchSite = input.secFetchSite?.toLowerCase();
21802
23406
  const browserRequest = input.origin !== void 0 || fetchSite !== void 0;
21803
23407
  if (!browserRequest) return true;
21804
- if (fetchSite !== void 0 && fetchSite !== "same-origin") return false;
23408
+ if (isModelProxyPath(pathname)) {
23409
+ if (fetchSite !== void 0 && fetchSite !== "same-origin") return false;
23410
+ if (input.origin !== void 0 && !isSameRequestOrigin(input.origin, input.host, input.publicOrigin)) {
23411
+ return false;
23412
+ }
23413
+ return input.csrfToken === "1";
23414
+ }
23415
+ const allowedOrigin = matchedUiOrigin(input.origin, input.allowedUiOrigins);
23416
+ if (fetchSite !== void 0 && fetchSite !== "same-origin" && allowedOrigin === null)
23417
+ return false;
21805
23418
  if (input.origin !== void 0) {
21806
- try {
21807
- const originHost = new URL(input.origin).host.toLowerCase();
21808
- if (originHost !== input.host?.toLowerCase()) return false;
21809
- } catch {
23419
+ if (!isSameRequestOrigin(input.origin, input.host, input.publicOrigin) && allowedOrigin === null) {
21810
23420
  return false;
21811
23421
  }
21812
23422
  }
@@ -21845,14 +23455,14 @@ function readAliasControlBody(req) {
21845
23455
  error: `Alias control body exceeds ${String(ALIAS_CONTROL_MAX_REQUEST_BYTES)} bytes`
21846
23456
  });
21847
23457
  }
21848
- return new Promise((resolve2) => {
23458
+ return new Promise((resolve5) => {
21849
23459
  const chunks = [];
21850
23460
  let receivedBytes = 0;
21851
23461
  let settled = false;
21852
23462
  const finish = (result) => {
21853
23463
  if (settled) return;
21854
23464
  settled = true;
21855
- resolve2(result);
23465
+ resolve5(result);
21856
23466
  };
21857
23467
  req.on("data", (chunk) => {
21858
23468
  if (settled) return;
@@ -21869,7 +23479,7 @@ function readAliasControlBody(req) {
21869
23479
  chunks.push(buffer);
21870
23480
  });
21871
23481
  req.on("end", () => {
21872
- finish({ ok: true, body: Buffer3.concat(chunks).toString("utf8") });
23482
+ finish({ ok: true, body: Buffer4.concat(chunks).toString("utf8") });
21873
23483
  });
21874
23484
  req.on("aborted", () => {
21875
23485
  finish({ ok: false, status: 400, error: "Client aborted alias control request" });
@@ -22040,7 +23650,28 @@ function isIdentityProxyControlPath(pathname) {
22040
23650
  return pathname === IDENTITY_PROXY_CONTROL_NAMESPACE || pathname.startsWith(`${IDENTITY_PROXY_CONTROL_NAMESPACE}/`);
22041
23651
  }
22042
23652
  function startIdentityProxy(options) {
22043
- return new Promise((resolve2, reject) => {
23653
+ const allowedUiOrigins = parseUiOriginAllowlist(options.allowedUiOrigins?.join(","));
23654
+ if (allowedUiOrigins.rejected.length > 0) {
23655
+ return Promise.reject(
23656
+ new Error(
23657
+ `Unsafe UI origins are not accepted: ${allowedUiOrigins.rejected.join(", ")}. Only exact loopback HTTP(S) origins are supported.`
23658
+ )
23659
+ );
23660
+ }
23661
+ const publicOrigin = normalizeTrustedPublicOrigin(options.publicOrigin);
23662
+ if (publicOrigin === null) {
23663
+ return Promise.reject(
23664
+ new Error(
23665
+ "publicOrigin must be an exact HTTP(S) origin without credentials, path, or query."
23666
+ )
23667
+ );
23668
+ }
23669
+ const effectiveOptions = {
23670
+ ...options,
23671
+ allowedUiOrigins: allowedUiOrigins.origins,
23672
+ publicOrigin
23673
+ };
23674
+ return new Promise((resolve5, reject) => {
22044
23675
  const server = http.createServer();
22045
23676
  const aliasRegistry = options.aliasRegistry ?? createBasePathAliasRegistry(getConfiguredBasePath(process.env));
22046
23677
  const runtimeState = {
@@ -22079,7 +23710,7 @@ function startIdentityProxy(options) {
22079
23710
  };
22080
23711
  res.once("finish", release);
22081
23712
  res.once("close", release);
22082
- void handleRequest(req, res, options, aliasRegistry).catch((err) => {
23713
+ void handleRequest(req, res, effectiveOptions, aliasRegistry).catch((err) => {
22083
23714
  const message = err instanceof Error ? err.message : String(err);
22084
23715
  if (!res.headersSent) {
22085
23716
  res.writeHead(502, { "content-type": "text/plain" });
@@ -22090,7 +23721,7 @@ function startIdentityProxy(options) {
22090
23721
  server.once("error", reject);
22091
23722
  server.listen(options.listenPort, options.listenHost, () => {
22092
23723
  server.removeListener("error", reject);
22093
- resolve2(server);
23724
+ resolve5(server);
22094
23725
  });
22095
23726
  });
22096
23727
  }
@@ -22101,18 +23732,18 @@ function stopIdentityProxyAdmission(server) {
22101
23732
  async function waitForIdentityProxyDrain(server) {
22102
23733
  const state = identityProxyRuntimeStates.get(server);
22103
23734
  if (state === void 0 || state.activeRequests === 0) return;
22104
- await new Promise((resolve2) => {
22105
- state.drainWaiters.add(resolve2);
23735
+ await new Promise((resolve5) => {
23736
+ state.drainWaiters.add(resolve5);
22106
23737
  });
22107
23738
  }
22108
23739
  async function closeIdentityProxyGracefully(server) {
22109
23740
  stopIdentityProxyAdmission(server);
22110
- const closed = new Promise((resolve2) => {
23741
+ const closed = new Promise((resolve5) => {
22111
23742
  if (!server.listening) {
22112
- resolve2();
23743
+ resolve5();
22113
23744
  return;
22114
23745
  }
22115
- server.close(() => resolve2());
23746
+ server.close(() => resolve5());
22116
23747
  });
22117
23748
  await Promise.all([closed, waitForIdentityProxyDrain(server)]);
22118
23749
  }
@@ -22148,6 +23779,48 @@ async function handleRequest(req, res, options, aliasRegistry) {
22148
23779
  const resolution = resolveBasePathAliasRequest(aliasRegistry, req.url ?? "/");
22149
23780
  const basePath = resolution.effectiveBasePath;
22150
23781
  const upstreamPath = resolution.upstreamPath;
23782
+ const upstreamPathname = new URL(upstreamPath, "http://localhost").pathname;
23783
+ const requestOrigin = req.headers.origin;
23784
+ const requestPublicOrigin = publicOriginForRequest(req, options);
23785
+ const crossOriginRequest = requestOrigin !== void 0 && !isSameRequestOrigin(requestOrigin, req.headers.host, requestPublicOrigin);
23786
+ const allowedUiOrigin = crossOriginRequest ? matchedUiOrigin(requestOrigin, options.allowedUiOrigins) : null;
23787
+ if (crossOriginRequest && isModelProxyPath(upstreamPathname)) {
23788
+ res.writeHead(403, {
23789
+ "content-type": "application/json; charset=utf-8",
23790
+ "cache-control": "no-store"
23791
+ });
23792
+ res.end(
23793
+ JSON.stringify({ error: "Cross-origin browser access to the model proxy is forbidden." })
23794
+ );
23795
+ req.resume();
23796
+ return;
23797
+ }
23798
+ if (crossOriginRequest && upstreamPathname === "/api/mcp") {
23799
+ res.writeHead(403, {
23800
+ "content-type": "application/json; charset=utf-8",
23801
+ "cache-control": "no-store"
23802
+ });
23803
+ res.end(JSON.stringify({ error: "Browser CORS is not enabled for the MCP endpoint." }));
23804
+ req.resume();
23805
+ return;
23806
+ }
23807
+ if (crossOriginRequest && isUiControlPath(upstreamPathname)) {
23808
+ if (allowedUiOrigin === null) {
23809
+ res.writeHead(403, {
23810
+ "content-type": "application/json; charset=utf-8",
23811
+ "cache-control": "no-store"
23812
+ });
23813
+ res.end(
23814
+ JSON.stringify({ error: "UI origin is not allowed by this Agent Inspector runtime." })
23815
+ );
23816
+ req.resume();
23817
+ return;
23818
+ }
23819
+ if ((req.method ?? "GET").toUpperCase() === "OPTIONS") {
23820
+ handleUiCorsPreflight(req, res, allowedUiOrigin);
23821
+ return;
23822
+ }
23823
+ }
22151
23824
  if (basePath.length > 0 && requestUrl.pathname.startsWith("/assets/")) {
22152
23825
  res.writeHead(307, {
22153
23826
  location: `${basePath}${requestUrl.pathname}${requestUrl.search}`,
@@ -22164,11 +23837,14 @@ async function handleRequest(req, res, options, aliasRegistry) {
22164
23837
  host: req.headers.host,
22165
23838
  origin: req.headers.origin,
22166
23839
  secFetchSite: firstHeader(req.headers["sec-fetch-site"]),
22167
- csrfToken: firstHeader(req.headers[CSRF_HEADER])
23840
+ csrfToken: firstHeader(req.headers[CSRF_HEADER]),
23841
+ allowedUiOrigins: options.allowedUiOrigins,
23842
+ publicOrigin: requestPublicOrigin
22168
23843
  })) {
22169
23844
  res.writeHead(403, {
22170
23845
  "content-type": "application/json; charset=utf-8",
22171
- "cache-control": "no-store"
23846
+ "cache-control": "no-store",
23847
+ ...uiCorsHeaders(allowedUiOrigin)
22172
23848
  });
22173
23849
  res.end(JSON.stringify({ error: "Browser mutation requires same-origin CSRF proof" }));
22174
23850
  req.resume();
@@ -22185,7 +23861,8 @@ async function handleRequest(req, res, options, aliasRegistry) {
22185
23861
  })) {
22186
23862
  res.writeHead(403, {
22187
23863
  "content-type": "application/json; charset=utf-8",
22188
- "cache-control": "no-store"
23864
+ "cache-control": "no-store",
23865
+ ...uiCorsHeaders(allowedUiOrigin)
22189
23866
  });
22190
23867
  res.end(
22191
23868
  JSON.stringify({
@@ -22195,6 +23872,16 @@ async function handleRequest(req, res, options, aliasRegistry) {
22195
23872
  req.resume();
22196
23873
  return;
22197
23874
  }
23875
+ if (await tryServeUiAsset(
23876
+ req,
23877
+ res,
23878
+ options,
23879
+ upstreamPathname,
23880
+ basePath,
23881
+ aliasRegistry.canonicalBasePath
23882
+ )) {
23883
+ return;
23884
+ }
22198
23885
  const remotePort = req.socket.remotePort;
22199
23886
  let identity = null;
22200
23887
  if (remotePort !== void 0 && remotePort > 0) {
@@ -22212,7 +23899,8 @@ async function handleRequest(req, res, options, aliasRegistry) {
22212
23899
  if (contentLength !== null && (!Number.isFinite(contentLength) || contentLength < 0)) {
22213
23900
  res.writeHead(400, {
22214
23901
  "content-type": "application/json; charset=utf-8",
22215
- "cache-control": "no-store"
23902
+ "cache-control": "no-store",
23903
+ ...uiCorsHeaders(allowedUiOrigin)
22216
23904
  });
22217
23905
  res.end(JSON.stringify({ error: "Invalid Content-Length header" }));
22218
23906
  req.resume();
@@ -22221,7 +23909,8 @@ async function handleRequest(req, res, options, aliasRegistry) {
22221
23909
  if (contentLength !== null && contentLength > maxRequestBytes) {
22222
23910
  res.writeHead(413, {
22223
23911
  "content-type": "application/json; charset=utf-8",
22224
- "cache-control": "no-store"
23912
+ "cache-control": "no-store",
23913
+ ...uiCorsHeaders(allowedUiOrigin)
22225
23914
  });
22226
23915
  res.end(JSON.stringify({ error: `Request body exceeds ${String(maxRequestBytes)} bytes` }));
22227
23916
  req.resume();
@@ -22234,10 +23923,12 @@ async function handleRequest(req, res, options, aliasRegistry) {
22234
23923
  if (basePath.length > 0 && (lowerName === "accept-encoding" || lowerName === "range" || lowerName === "if-range"))
22235
23924
  continue;
22236
23925
  if (lowerName.startsWith(IDENTITY_HEADER_PREFIX)) continue;
23926
+ if (lowerName === "authorization" && !isModelProxyPath(upstreamPathname)) continue;
22237
23927
  if (value === void 0) continue;
22238
23928
  upstreamHeaders[name] = value;
22239
23929
  }
22240
23930
  upstreamHeaders[AGENT_INSPECTOR_PUBLIC_BASE_PATH_HEADER] = basePath.length === 0 ? "/" : basePath;
23931
+ upstreamHeaders[AGENT_INSPECTOR_PUBLIC_ORIGIN_HEADER] = requestPublicOrigin;
22241
23932
  const publicPort = req.socket.localPort;
22242
23933
  if (publicPort !== void 0 && publicPort > 0 && publicPort <= 65535) {
22243
23934
  upstreamHeaders[AGENT_INSPECTOR_PUBLIC_PORT_HEADER] = String(publicPort);
@@ -22311,6 +24002,11 @@ async function handleRequest(req, res, options, aliasRegistry) {
22311
24002
  for (const [name, value] of Object.entries(BASE_SECURITY_HEADERS)) {
22312
24003
  outHeaders[name] = value;
22313
24004
  }
24005
+ if (allowedUiOrigin !== null && isUiControlPath(upstreamPathname)) {
24006
+ outHeaders["access-control-allow-origin"] = allowedUiOrigin;
24007
+ outHeaders["access-control-expose-headers"] = "Content-Disposition";
24008
+ outHeaders["vary"] = appendVaryHeader(outHeaders["vary"], "Origin");
24009
+ }
22314
24010
  if (contentType?.toLowerCase().includes("text/html") === true) {
22315
24011
  outHeaders["content-security-policy"] = DOCUMENT_CONTENT_SECURITY_POLICY;
22316
24012
  }
@@ -22322,7 +24018,7 @@ async function handleRequest(req, res, options, aliasRegistry) {
22322
24018
  chunks.push(toBuffer(chunk));
22323
24019
  });
22324
24020
  proxyRes.on("end", () => {
22325
- const body = Buffer3.concat(chunks).toString("utf8");
24021
+ const body = Buffer4.concat(chunks).toString("utf8");
22326
24022
  res.write(
22327
24023
  shouldRewriteHtml ? rewriteHtmlForBasePath(body, basePath, aliasRegistry.canonicalBasePath) : rewriteJavaScriptForBasePath(body, basePath, aliasRegistry.canonicalBasePath)
22328
24024
  );
@@ -22397,18 +24093,18 @@ async function handleRequest(req, res, options, aliasRegistry) {
22397
24093
  });
22398
24094
  }
22399
24095
  function toBuffer(chunk) {
22400
- if (Buffer3.isBuffer(chunk)) return chunk;
22401
- if (typeof chunk === "string") return Buffer3.from(chunk);
22402
- if (chunk instanceof ArrayBuffer) return Buffer3.from(chunk);
24096
+ if (Buffer4.isBuffer(chunk)) return chunk;
24097
+ if (typeof chunk === "string") return Buffer4.from(chunk);
24098
+ if (chunk instanceof ArrayBuffer) return Buffer4.from(chunk);
22403
24099
  if (chunk instanceof Uint8Array)
22404
- return Buffer3.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
22405
- return Buffer3.alloc(0);
24100
+ return Buffer4.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
24101
+ return Buffer4.alloc(0);
22406
24102
  }
22407
24103
 
22408
24104
  // src/cli.ts
22409
24105
  init_platformCommands();
22410
24106
  var __filename2 = fileURLToPath2(import.meta.url);
22411
- var __dirname2 = dirname3(__filename2);
24107
+ var __dirname2 = dirname2(__filename2);
22412
24108
  var DEFAULT_PORT4 = 9527;
22413
24109
  var LEGACY_ALIAS_PORT = 9527;
22414
24110
  var DEFAULT_UPSTREAM_PORT = 9529;
@@ -22434,6 +24130,14 @@ Global options:
22434
24130
  -h, --help Show this help without starting a runtime
22435
24131
  -v, --version Print the package version
22436
24132
 
24133
+ Start options:
24134
+ --backend-only, --headless Start the API, proxy, and MCP runtime without serving the UI
24135
+ --with-ui Serve the independently built UI through the protected ingress
24136
+ --port, -p <port> Public ingress port (default: 9527)
24137
+ --host, -H <host> Bind host (default: 127.0.0.1)
24138
+ --background Start a hidden supervisor and return after readiness
24139
+ --no-open Do not open the UI after startup
24140
+
22437
24141
  Run \`agent-inspector instance --help\`, \`agent-inspector onboard --help\`, or
22438
24142
  \`agent-inspector doctor --help\` for command-specific options.`);
22439
24143
  }
@@ -22523,12 +24227,12 @@ async function getRunningCaptureMode(port, host, basePath = "") {
22523
24227
  }
22524
24228
  }
22525
24229
  function isPortAcceptingConnections(port, host) {
22526
- return new Promise((resolve2) => {
24230
+ return new Promise((resolve5) => {
22527
24231
  const socket = createConnection2({ host: probeHostForBindHost(host), port });
22528
24232
  const finish = (value) => {
22529
24233
  socket.removeAllListeners();
22530
24234
  socket.destroy();
22531
- resolve2(value);
24235
+ resolve5(value);
22532
24236
  };
22533
24237
  socket.setTimeout(LOCAL_PROBE_TIMEOUT_MS);
22534
24238
  socket.once("connect", () => finish(true));
@@ -22537,8 +24241,8 @@ function isPortAcceptingConnections(port, host) {
22537
24241
  });
22538
24242
  }
22539
24243
  function sleep2(ms) {
22540
- return new Promise((resolve2) => {
22541
- setTimeout(resolve2, ms);
24244
+ return new Promise((resolve5) => {
24245
+ setTimeout(resolve5, ms);
22542
24246
  });
22543
24247
  }
22544
24248
  async function waitForInspectorHealthy(port, timeoutMs, host, basePath = "") {
@@ -22562,12 +24266,12 @@ function openBrowser(targetUrl) {
22562
24266
  browserProcess.unref();
22563
24267
  }
22564
24268
  function waitForProcessExit(child) {
22565
- return new Promise((resolve2) => {
24269
+ return new Promise((resolve5) => {
22566
24270
  child.once("exit", (code) => {
22567
- resolve2(code ?? 1);
24271
+ resolve5(code ?? 1);
22568
24272
  });
22569
24273
  child.once("error", () => {
22570
- resolve2(1);
24274
+ resolve5(1);
22571
24275
  });
22572
24276
  });
22573
24277
  }
@@ -22579,7 +24283,7 @@ function pipeServerOutputStream(stream, target, context) {
22579
24283
  if (stream === null) return;
22580
24284
  let buffered = "";
22581
24285
  stream.on("data", (chunk) => {
22582
- const text = Buffer4.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
24286
+ const text = Buffer5.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
22583
24287
  const lines = `${buffered}${text}`.split(/\r?\n/);
22584
24288
  buffered = lines.pop() ?? "";
22585
24289
  for (const line of lines) {
@@ -22598,7 +24302,7 @@ function pipeServerOutputStream(stream, target, context) {
22598
24302
  });
22599
24303
  }
22600
24304
  function resolveServerCommand(outputDir, serverPath) {
22601
- const brandedRuntime = join8(outputDir, BRANDED_WINDOWS_RUNTIME_EXE);
24305
+ const brandedRuntime = join9(outputDir, BRANDED_WINDOWS_RUNTIME_EXE);
22602
24306
  return process.platform === "win32" && existsSync7(brandedRuntime) ? { command: brandedRuntime, args: [serverPath] } : { command: process.execPath, args: [serverPath] };
22603
24307
  }
22604
24308
  function parseCaptureMode(value) {
@@ -22619,11 +24323,20 @@ function readCaptureMode(raw) {
22619
24323
  const value = descriptor?.value;
22620
24324
  return typeof value === "string" ? parseCaptureMode(value) : null;
22621
24325
  }
22622
- function printAccessHints(port, host, basePath = "") {
24326
+ function printAccessHints(port, host, basePath, serveUi, publicOrigin) {
22623
24327
  const networkUrls = networkUrlsForBindHost(port, host);
22624
- for (const line of formatAccessHintLines(port, networkUrls, host, basePath)) {
24328
+ for (const line of formatAccessHintLines(port, networkUrls, host, basePath, serveUi)) {
22625
24329
  console.log(line);
22626
24330
  }
24331
+ if (publicOrigin !== void 0) {
24332
+ const publicBaseUrl = appendBasePathToOrigin(publicOrigin, "/", basePath);
24333
+ if (serveUi) console.log(` Public UI: ${publicBaseUrl}`);
24334
+ console.log(` Public control Base: ${publicBaseUrl}`);
24335
+ console.log(` Public agent Base: ${publicBaseUrl}`);
24336
+ console.log(` Public REST API: ${appendBasePathToOrigin(publicOrigin, "/api", basePath)}`);
24337
+ console.log(` Public proxy: ${appendBasePathToOrigin(publicOrigin, "/proxy", basePath)}`);
24338
+ console.log(` Public MCP: ${appendBasePathToOrigin(publicOrigin, "/api/mcp", basePath)}`);
24339
+ }
22627
24340
  if (!isLoopbackHost(host)) {
22628
24341
  const proxyTokenConfigured = (process.env["AGENT_INSPECTOR_PROXY_TOKEN"] ?? "") !== "";
22629
24342
  const legacyProxyEnabled = process.env["AGENT_INSPECTOR_ALLOW_UNAUTHENTICATED_REMOTE_PROXY"] === "1";
@@ -22652,6 +24365,18 @@ function readPositiveIntegerEnv(name) {
22652
24365
  console.warn(`Ignoring invalid ${name}=${raw}; expected a positive integer.`);
22653
24366
  return void 0;
22654
24367
  }
24368
+ function normalizeExplicitPublicOrigin(value) {
24369
+ if (value === void 0 || value.trim().length === 0) return void 0;
24370
+ try {
24371
+ const parsed = new URL(value.trim());
24372
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
24373
+ if (parsed.username.length > 0 || parsed.password.length > 0) return null;
24374
+ if (parsed.pathname !== "/" || parsed.search.length > 0 || parsed.hash.length > 0) return null;
24375
+ return parsed.origin;
24376
+ } catch {
24377
+ return null;
24378
+ }
24379
+ }
22655
24380
  function buildBackgroundSupervisorArgs(options) {
22656
24381
  const args = ["--no-open", "--port", String(options.port), "--mode", options.captureMode];
22657
24382
  if (options.host !== void 0) {
@@ -22669,6 +24394,9 @@ function buildBackgroundSupervisorArgs(options) {
22669
24394
  if (options.legacyAliasEnabled) {
22670
24395
  args.push("--legacy-port");
22671
24396
  }
24397
+ if (!options.serveUi) {
24398
+ args.push("--backend-only");
24399
+ }
22672
24400
  return args;
22673
24401
  }
22674
24402
  function pickUpstreamPort(publicPort, legacyAliasPort) {
@@ -22681,7 +24409,7 @@ function pickUpstreamPort(publicPort, legacyAliasPort) {
22681
24409
  }
22682
24410
  return DEFAULT_UPSTREAM_PORT;
22683
24411
  }
22684
- async function tryStartIdentityProxy(identityPort, upstreamPort, listenHost, upstreamHost, managedInstance) {
24412
+ async function tryStartIdentityProxy(identityPort, upstreamPort, listenHost, upstreamHost, managedInstance, allowedUiOrigins, uiDirectory, publicOrigin) {
22685
24413
  try {
22686
24414
  const server = await startIdentityProxy({
22687
24415
  listenPort: identityPort,
@@ -22694,6 +24422,9 @@ async function tryStartIdentityProxy(identityPort, upstreamPort, listenHost, ups
22694
24422
  proxyToken: process.env["AGENT_INSPECTOR_PROXY_TOKEN"],
22695
24423
  allowUnauthenticatedRemoteProxy: process.env["AGENT_INSPECTOR_ALLOW_UNAUTHENTICATED_REMOTE_PROXY"] === "1",
22696
24424
  trustedHosts: (process.env["AGENT_INSPECTOR_TRUSTED_HOSTS"] ?? "").split(",").map((host) => host.trim()).filter((host) => host.length > 0),
24425
+ allowedUiOrigins,
24426
+ uiDirectory,
24427
+ publicOrigin,
22697
24428
  maxRequestBytes: readPositiveIntegerEnv("AGENT_INSPECTOR_MAX_REQUEST_BYTES"),
22698
24429
  requestTimeoutMs: readPositiveIntegerEnv("AGENT_INSPECTOR_REQUEST_TIMEOUT_MS"),
22699
24430
  managedInstance
@@ -22701,9 +24432,7 @@ async function tryStartIdentityProxy(identityPort, upstreamPort, listenHost, ups
22701
24432
  return server;
22702
24433
  } catch (err) {
22703
24434
  const message = err instanceof Error ? err.message : String(err);
22704
- console.warn(
22705
- `[identity-proxy] Could not listen on port ${identityPort}: ${message}. Falling back to OS-scan heuristic on port ${upstreamPort}. Pass --identity-proxy-port <n> to choose another port, or --no-identity-proxy to disable.`
22706
- );
24435
+ console.warn(`[identity-proxy] Could not listen on port ${identityPort}: ${message}.`);
22707
24436
  return null;
22708
24437
  }
22709
24438
  }
@@ -22721,7 +24450,7 @@ async function runStart(args) {
22721
24450
  const envMode = process.env["AGENT_INSPECTOR_CAPTURE_MODE"] ?? process.env["AGENT_INSPECTOR_MODE"];
22722
24451
  let port = portDefault;
22723
24452
  let host = envHost !== void 0 && envHost.trim().length > 0 ? envHost.trim() : DEFAULT_BIND_HOST;
22724
- let open = true;
24453
+ let open2 = true;
22725
24454
  let openWasSpecified = false;
22726
24455
  let background = false;
22727
24456
  let forceRestart = false;
@@ -22731,6 +24460,7 @@ async function runStart(args) {
22731
24460
  let captureModeWasSpecified = false;
22732
24461
  let enableIdentityProxy = true;
22733
24462
  let legacyAliasEnabled = false;
24463
+ let serveUi = process.env["AGENT_INSPECTOR_BACKEND_ONLY"] !== "1";
22734
24464
  if (envMode !== void 0 && envMode !== "") {
22735
24465
  const parsedMode = parseCaptureMode(envMode);
22736
24466
  if (parsedMode === null) {
@@ -22793,11 +24523,11 @@ async function runStart(args) {
22793
24523
  break;
22794
24524
  }
22795
24525
  case "--no-open":
22796
- open = false;
24526
+ open2 = false;
22797
24527
  openWasSpecified = true;
22798
24528
  break;
22799
24529
  case "--open":
22800
- open = true;
24530
+ open2 = true;
22801
24531
  openWasSpecified = true;
22802
24532
  break;
22803
24533
  case "--force-restart":
@@ -22807,6 +24537,14 @@ async function runStart(args) {
22807
24537
  case "--background":
22808
24538
  background = true;
22809
24539
  break;
24540
+ case "--backend-only":
24541
+ case "--headless":
24542
+ serveUi = false;
24543
+ open2 = false;
24544
+ break;
24545
+ case "--with-ui":
24546
+ serveUi = true;
24547
+ break;
22810
24548
  case "--config-dir":
22811
24549
  configDir = args[i + 1];
22812
24550
  i++;
@@ -22877,6 +24615,40 @@ async function runStart(args) {
22877
24615
  process.exitCode = 1;
22878
24616
  return;
22879
24617
  }
24618
+ if (!serveUi) open2 = false;
24619
+ if (!enableIdentityProxy && serveUi) {
24620
+ console.error(
24621
+ "The decoupled UI is composed only by the protected ingress. Use --backend-only with --no-identity-proxy, or keep the identity proxy enabled."
24622
+ );
24623
+ process.exitCode = 1;
24624
+ return;
24625
+ }
24626
+ const uiOriginAllowlist = parseUiOriginAllowlist(process.env["AGENT_INSPECTOR_UI_ORIGINS"]);
24627
+ if (uiOriginAllowlist.rejected.length > 0) {
24628
+ console.error(
24629
+ `Invalid AGENT_INSPECTOR_UI_ORIGINS entries: ${uiOriginAllowlist.rejected.join(", ")}. Only exact loopback http(s) origins are accepted; use a same-origin authenticated gateway for remote UI hosting.`
24630
+ );
24631
+ process.exitCode = 1;
24632
+ return;
24633
+ }
24634
+ const publicOrigin = normalizeExplicitPublicOrigin(process.env["AGENT_INSPECTOR_PUBLIC_ORIGIN"]);
24635
+ if (publicOrigin === null) {
24636
+ console.error(
24637
+ "Invalid AGENT_INSPECTOR_PUBLIC_ORIGIN. Use an exact http(s) origin without credentials, path, query, or fragment."
24638
+ );
24639
+ process.exitCode = 1;
24640
+ return;
24641
+ }
24642
+ const uiDirectoryCandidate = join9(__dirname2, "../.output/ui");
24643
+ const uiIndexPath = join9(uiDirectoryCandidate, "index.html");
24644
+ const uiDirectory = serveUi && existsSync7(uiIndexPath) ? uiDirectoryCandidate : void 0;
24645
+ if (serveUi && uiDirectory === void 0) {
24646
+ console.error(
24647
+ `Agent Inspector UI artifact is missing at ${uiIndexPath}. Run the composed build or use --backend-only.`
24648
+ );
24649
+ process.exitCode = 1;
24650
+ return;
24651
+ }
22880
24652
  function killProcessOnPort(targetPort) {
22881
24653
  const pids = findPidsByPort(targetPort);
22882
24654
  for (const pid of pids) {
@@ -22893,17 +24665,30 @@ async function runStart(args) {
22893
24665
  }
22894
24666
  const upstreamHost = enableIdentityProxy ? DEFAULT_BIND_HOST : host;
22895
24667
  process.env["PORT"] = String(upstreamPort);
24668
+ const configuredBasePath = process.env[AGENT_INSPECTOR_BASE_PATH_ENV];
24669
+ if (configuredBasePath !== void 0 && normalizePublicBasePath(configuredBasePath) === null) {
24670
+ console.error(
24671
+ `Invalid ${AGENT_INSPECTOR_BASE_PATH_ENV}. Use / for root or URL-safe path segments such as /inspector or /foo-bar/v1.`
24672
+ );
24673
+ process.exitCode = 1;
24674
+ return;
24675
+ }
22896
24676
  const basePath = getConfiguredBasePath(process.env);
22897
- const url2 = urlForHost(port, host);
22898
- const uiUrl = appendBasePathToOrigin(url2, "/", basePath);
22899
- const proxyUrl = appendBasePathToOrigin(url2, "/proxy", basePath);
22900
- const proxyMessagesUrl = appendBasePathToOrigin(url2, "/proxy/v1/messages", basePath);
24677
+ const bindOrigin = urlForHost(port, host);
24678
+ const outwardOrigin = publicOrigin ?? bindOrigin;
24679
+ const surfaceBaseUrl = appendBasePathToOrigin(outwardOrigin, "/", basePath);
24680
+ const uiUrl = serveUi ? surfaceBaseUrl : null;
24681
+ const proxyUrl = appendBasePathToOrigin(outwardOrigin, "/proxy", basePath);
24682
+ const openAiProxyUrl = appendBasePathToOrigin(outwardOrigin, "/proxy/v1", basePath);
24683
+ const proxyMessagesUrl = appendBasePathToOrigin(outwardOrigin, "/proxy/v1/messages", basePath);
22901
24684
  const legacyUrl = legacyAliasPort === null ? null : appendBasePathToOrigin(localUrlForPort(legacyAliasPort), "/", basePath);
22902
24685
  const legacyProxyUrl = legacyAliasPort === null ? null : appendBasePathToOrigin(localUrlForPort(legacyAliasPort), "/proxy", basePath);
22903
24686
  const legacyMcpUrl = legacyAliasPort === null ? null : appendBasePathToOrigin(localUrlForPort(legacyAliasPort), "/api/mcp", basePath);
22904
24687
  if (!forceRestart && await isInspectorHealthy(port, host, basePath)) {
22905
- console.log(`agent-inspector is already running at ${uiUrl}`);
22906
- printAccessHints(port, host, basePath);
24688
+ console.log(
24689
+ `agent-inspector ${serveUi ? "is already running" : "backend is already running"} at ${surfaceBaseUrl}`
24690
+ );
24691
+ printAccessHints(port, host, basePath, serveUi, publicOrigin);
22907
24692
  if (captureModeWasSpecified) {
22908
24693
  const runningMode = await getRunningCaptureMode(port, host, basePath);
22909
24694
  if (runningMode !== null && runningMode !== captureMode) {
@@ -22912,7 +24697,7 @@ async function runStart(args) {
22912
24697
  }
22913
24698
  }
22914
24699
  console.log(`Use --force-restart to restart the existing instance.`);
22915
- if (open && openWasSpecified) {
24700
+ if (open2 && openWasSpecified && uiUrl !== null) {
22916
24701
  openBrowser(uiUrl);
22917
24702
  }
22918
24703
  return;
@@ -22948,7 +24733,8 @@ async function runStart(args) {
22948
24733
  providersJson,
22949
24734
  captureMode,
22950
24735
  enableIdentityProxy,
22951
- legacyAliasEnabled
24736
+ legacyAliasEnabled,
24737
+ serveUi
22952
24738
  });
22953
24739
  const supervisorProcess = spawn2(process.execPath, [cliEntry, ...supervisorArgs], {
22954
24740
  stdio: "ignore",
@@ -22961,8 +24747,10 @@ async function runStart(args) {
22961
24747
  });
22962
24748
  supervisorProcess.unref();
22963
24749
  if (await waitForInspectorHealthy(port, 1e4, host, basePath)) {
22964
- console.log(`agent-inspector background server is ready at ${uiUrl}`);
22965
- printAccessHints(port, host, basePath);
24750
+ console.log(
24751
+ `agent-inspector background ${serveUi ? "server" : "backend"} is ready at ${surfaceBaseUrl}`
24752
+ );
24753
+ printAccessHints(port, host, basePath, serveUi, publicOrigin);
22966
24754
  if (legacyProxyUrl !== null && legacyMcpUrl !== null) {
22967
24755
  console.log(` Legacy proxy: ${legacyProxyUrl}`);
22968
24756
  console.log(` Legacy MCP: ${legacyMcpUrl}`);
@@ -22971,46 +24759,18 @@ async function runStart(args) {
22971
24759
  console.log(`Route AI coding tools through the identity proxy for accurate PID attribution:`);
22972
24760
  console.log(` Claude Code: ANTHROPIC_BASE_URL=${proxyUrl} claude`);
22973
24761
  console.log(` OpenCode: LLM_BASE_URL=${proxyUrl} opencode`);
22974
- console.log(` MiMo Code: OPENAI_BASE_URL=${proxyUrl} mimo`);
22975
- if (open && openWasSpecified) {
24762
+ console.log(` MiMo Code: OPENAI_BASE_URL=${openAiProxyUrl} mimo`);
24763
+ if (open2 && openWasSpecified && uiUrl !== null) {
22976
24764
  openBrowser(uiUrl);
22977
24765
  }
22978
24766
  return;
22979
24767
  }
22980
- console.error(`agent-inspector background server did not become ready at ${uiUrl}.`);
24768
+ console.error(`agent-inspector background server did not become ready at ${surfaceBaseUrl}.`);
22981
24769
  process.exitCode = 1;
22982
24770
  return;
22983
24771
  }
22984
- console.log(`Server running at ${uiUrl}`);
22985
- console.log(` Capture mode: ${captureMode}`);
22986
- console.log(` Proxy: ${proxyUrl}`);
22987
- printAccessHints(port, host, basePath);
22988
- if (legacyUrl !== null && legacyProxyUrl !== null && legacyMcpUrl !== null) {
22989
- console.log(` Legacy UI: ${legacyUrl}`);
22990
- console.log(` Legacy proxy: ${legacyProxyUrl}`);
22991
- console.log(` Legacy MCP: ${legacyMcpUrl}`);
22992
- }
22993
- console.log(``);
22994
- console.log(`Route AI coding tools through the proxy:`);
22995
- console.log(` Claude Code: ANTHROPIC_BASE_URL=${proxyUrl} claude`);
22996
- console.log(` OpenCode: LLM_BASE_URL=${proxyUrl} opencode`);
22997
- console.log(` MiMo Code: OPENAI_BASE_URL=${proxyUrl} mimo`);
22998
- console.log(` Direct HTTP: curl ${proxyMessagesUrl} -d '{"model":"...","messages":[...]}'`);
22999
- console.log(
23000
- ` Remote/container tools: use the Network proxy URL above when shown; otherwise restart with --host 0.0.0.0 or a reachable IP.`
23001
- );
23002
- console.log(``);
23003
- console.log(`Routing environment variables:`);
23004
- console.log(` ROUTES JSON map of model prefix -> upstream URL`);
23005
- console.log(` DEFAULT_UPSTREAM Fallback upstream for unmatched models`);
23006
- console.log(
23007
- ` Example: ROUTES='{"claude-":"https://api.anthropic.com","MiniMax":"https://api.minimaxi.com/anthropic"}'`
23008
- );
23009
- if (open) {
23010
- openBrowser(uiUrl);
23011
- }
23012
24772
  const outputDir = __dirname2;
23013
- const serverPath = join8(outputDir, "../.output/server/index.mjs");
24773
+ const serverPath = join9(outputDir, "../.output/server/index.mjs");
23014
24774
  const serverCommand = resolveServerCommand(outputDir, serverPath);
23015
24775
  const serverEnv = { ...process.env };
23016
24776
  const currentCliEntry = process.env[AGENT_INSPECTOR_CLI_ENTRY_ENV] ?? process.argv[1];
@@ -23018,7 +24778,7 @@ async function runStart(args) {
23018
24778
  serverEnv[AGENT_INSPECTOR_CLI_ENTRY_ENV] = currentCliEntry;
23019
24779
  }
23020
24780
  if (configDir !== void 0) {
23021
- let resolvedPath = join8(configDir);
24781
+ let resolvedPath = join9(configDir);
23022
24782
  const msysMatch = /^\\([a-z])\\(.*)$/i.exec(resolvedPath);
23023
24783
  if (msysMatch !== null) {
23024
24784
  const drive = (msysMatch[1] ?? "").toUpperCase();
@@ -23040,7 +24800,8 @@ async function runStart(args) {
23040
24800
  serverEnv["AGENT_INSPECTOR_PUBLIC_PORT"] = String(port);
23041
24801
  serverEnv["PROXY_PORT"] = String(port);
23042
24802
  serverEnv["AGENT_INSPECTOR_CAPTURE_MODE"] = captureMode;
23043
- const workerDir = join8(__dirname2, "workers");
24803
+ serverEnv["AGENT_INSPECTOR_BACKEND_ONLY"] = serveUi ? "0" : "1";
24804
+ const workerDir = join9(__dirname2, "workers");
23044
24805
  if (existsSync7(workerDir)) {
23045
24806
  serverEnv["AGENT_INSPECTOR_WORKER_DIR"] = workerDir;
23046
24807
  }
@@ -23080,12 +24841,12 @@ async function runStart(args) {
23080
24841
  serverExit.then(() => void 0),
23081
24842
  ...identityProxies.map(async (proxy) => await closeIdentityProxyGracefully(proxy.server))
23082
24843
  ]).then(() => void 0);
23083
- const timedOut = new Promise((resolve2) => {
24844
+ const timedOut = new Promise((resolve5) => {
23084
24845
  deadline = setTimeout(() => {
23085
24846
  forced = true;
23086
24847
  for (const proxy of identityProxies) forceCloseIdentityProxy(proxy.server);
23087
24848
  if (serverProcess.exitCode === null) serverProcess.kill("SIGKILL");
23088
- resolve2();
24849
+ resolve5();
23089
24850
  }, childDeadline + SUPERVISOR_SHUTDOWN_GRACE_MS);
23090
24851
  });
23091
24852
  supervisorShutdownPromise = Promise.race([graceful, timedOut]).finally(() => {
@@ -23116,18 +24877,34 @@ async function runStart(args) {
23116
24877
  upstreamPort,
23117
24878
  host,
23118
24879
  upstreamHost,
23119
- managedInstanceControl
24880
+ managedInstanceControl,
24881
+ uiOriginAllowlist.origins,
24882
+ uiDirectory,
24883
+ publicOrigin
23120
24884
  );
23121
- if (primaryProxy !== null) {
23122
- identityProxies.push({ label: "primary", port, server: primaryProxy });
24885
+ if (primaryProxy === null) {
24886
+ console.error(
24887
+ `Agent Inspector could not bind its protected public ingress at ${bindOrigin}; stopping the private backend.`
24888
+ );
24889
+ requestSupervisorShutdown("SIGTERM");
24890
+ const failedStartupShutdown = currentSupervisorShutdown();
24891
+ if (failedStartupShutdown !== null) await failedStartupShutdown;
24892
+ process.off("SIGINT", handleSigint);
24893
+ process.off("SIGTERM", handleSigterm);
24894
+ process.exitCode = 1;
24895
+ return;
23123
24896
  }
24897
+ identityProxies.push({ label: "primary", port, server: primaryProxy });
23124
24898
  if (legacyAliasPort !== null) {
23125
24899
  const legacyProxy = await tryStartIdentityProxy(
23126
24900
  legacyAliasPort,
23127
24901
  upstreamPort,
23128
24902
  host,
23129
24903
  upstreamHost,
23130
- managedInstanceControl
24904
+ managedInstanceControl,
24905
+ uiOriginAllowlist.origins,
24906
+ uiDirectory,
24907
+ publicOrigin
23131
24908
  );
23132
24909
  if (legacyProxy !== null) {
23133
24910
  identityProxies.push({ label: "legacy", port: legacyAliasPort, server: legacyProxy });
@@ -23138,11 +24915,13 @@ async function runStart(args) {
23138
24915
  serverProcess.unref();
23139
24916
  const upstreamProbeBasePath = enableIdentityProxy ? "" : basePath;
23140
24917
  if (await waitForInspectorHealthy(upstreamPort, 5e3, host, upstreamProbeBasePath)) {
23141
- console.log(`agent-inspector background server is ready at ${uiUrl}`);
24918
+ console.log(
24919
+ `agent-inspector background ${serveUi ? "server" : "backend"} is ready at ${surfaceBaseUrl}`
24920
+ );
23142
24921
  if (identityProxies.length > 0) {
23143
24922
  console.log(`Identity proxy ready on port ${port} (PID-attributing proxy).`);
23144
24923
  }
23145
- printAccessHints(port, host, basePath);
24924
+ printAccessHints(port, host, basePath, serveUi, publicOrigin);
23146
24925
  if (legacyProxyUrl !== null && legacyMcpUrl !== null) {
23147
24926
  console.log(` Legacy proxy: ${legacyProxyUrl}`);
23148
24927
  console.log(` Legacy MCP: ${legacyMcpUrl}`);
@@ -23153,19 +24932,29 @@ async function runStart(args) {
23153
24932
  `Route AI coding tools through the identity proxy for accurate PID attribution:`,
23154
24933
  ` Claude Code: ANTHROPIC_BASE_URL=${proxyUrl} claude`,
23155
24934
  ` OpenCode: LLM_BASE_URL=${proxyUrl} opencode`,
23156
- ` MiMo Code: OPENAI_BASE_URL=${proxyUrl} mimo`,
24935
+ ` MiMo Code: OPENAI_BASE_URL=${openAiProxyUrl} mimo`,
23157
24936
  legacyProxyUrl === null ? ` Override the port with --port <n> when needed.` : ` Legacy configs using ${legacyProxyUrl} still work.`
23158
24937
  );
23159
24938
  }
23160
24939
  return;
23161
24940
  }
23162
- console.error(`agent-inspector background server did not become ready at ${uiUrl}.`);
24941
+ console.error(`agent-inspector background server did not become ready at ${surfaceBaseUrl}.`);
24942
+ process.exitCode = 1;
24943
+ return;
24944
+ }
24945
+ if (!await waitForInspectorHealthy(port, 1e4, host, basePath)) {
24946
+ console.error(`Agent Inspector did not become ready at ${surfaceBaseUrl}; stopping startup.`);
24947
+ requestSupervisorShutdown("SIGTERM");
24948
+ const failedStartupShutdown = currentSupervisorShutdown();
24949
+ if (failedStartupShutdown !== null) await failedStartupShutdown;
24950
+ process.off("SIGINT", handleSigint);
24951
+ process.off("SIGTERM", handleSigterm);
23163
24952
  process.exitCode = 1;
23164
24953
  return;
23165
24954
  }
23166
24955
  if (identityProxies.length > 0) {
23167
24956
  console.log(``);
23168
- console.log(`Identity proxy ready at ${url2} (PID-attributing public entrypoint).`);
24957
+ console.log(`Identity proxy ready at ${bindOrigin} (PID-attributing ingress).`);
23169
24958
  if (legacyAliasPort !== null) {
23170
24959
  const legacyReady = identityProxies.some((proxy) => proxy.label === "legacy");
23171
24960
  if (legacyReady) {
@@ -23176,6 +24965,27 @@ async function runStart(args) {
23176
24965
  console.log(` Internal runtime port ${upstreamPort} is hidden behind the public proxy.`);
23177
24966
  }
23178
24967
  }
24968
+ console.log(
24969
+ `${serveUi ? "UI and backend" : "Backend control and agent runtime"} running at ${surfaceBaseUrl}`
24970
+ );
24971
+ console.log(` Capture mode: ${captureMode}`);
24972
+ console.log(` Proxy: ${proxyUrl}`);
24973
+ printAccessHints(port, host, basePath, serveUi, publicOrigin);
24974
+ if (legacyUrl !== null && legacyProxyUrl !== null && legacyMcpUrl !== null) {
24975
+ console.log(` Legacy UI: ${legacyUrl}`);
24976
+ console.log(` Legacy proxy: ${legacyProxyUrl}`);
24977
+ console.log(` Legacy MCP: ${legacyMcpUrl}`);
24978
+ }
24979
+ console.log(``);
24980
+ console.log(`Route AI coding tools through the proxy:`);
24981
+ console.log(` Claude Code: ANTHROPIC_BASE_URL=${proxyUrl} claude`);
24982
+ console.log(` OpenCode: LLM_BASE_URL=${proxyUrl} opencode`);
24983
+ console.log(` MiMo Code: OPENAI_BASE_URL=${openAiProxyUrl} mimo`);
24984
+ console.log(` Direct HTTP: curl ${proxyMessagesUrl} -d '{"model":"...","messages":[...]}'`);
24985
+ console.log(
24986
+ ` Remote/container tools: use the Network proxy URL above when shown; otherwise restart with --host 0.0.0.0 or a reachable IP.`
24987
+ );
24988
+ if (open2 && uiUrl !== null) openBrowser(uiUrl);
23179
24989
  process.exitCode = await serverExit;
23180
24990
  const activeSupervisorShutdown = currentSupervisorShutdown();
23181
24991
  if (activeSupervisorShutdown !== null) {