@tonyclaw/agent-inspector 3.0.44 → 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 (347) 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 +22707 -17099
  4. package/.output/server/_libs/extend.mjs +4 -8
  5. package/.output/server/_libs/jszip.mjs +9 -35
  6. package/.output/server/_libs/modelcontextprotocol__core.mjs +25 -25
  7. package/.output/server/_libs/modelcontextprotocol__server.mjs +2 -2
  8. package/.output/server/_libs/react-dom.mjs +2 -3
  9. package/.output/server/_libs/react.mjs +1 -27
  10. package/.output/server/_libs/tanstack__react-router.mjs +1 -329
  11. package/.output/server/_libs/tanstack__router-core.mjs +42 -100
  12. package/.output/server/_libs/use-sync-external-store.mjs +1 -64
  13. package/.output/server/_libs/ws.mjs +1 -1
  14. package/.output/server/_libs/zod.mjs +6 -6
  15. package/.output/server/_ssr/index.mjs +5 -4
  16. package/.output/server/_ssr/publicBasePathContext-IH2ilOfu.mjs +176 -0
  17. package/.output/server/_ssr/{router-DWcUFPR3.mjs → router-D18yUq36.mjs} +7877 -5597
  18. package/.output/server/_ssr/start-COQu_AgD.mjs +57 -0
  19. package/.output/server/_tanstack-start-manifest_v-DnbdNeun.mjs +4 -0
  20. package/.output/server/index.mjs +5 -145
  21. package/.output/{public/assets/CompareDrawer-DQHjxFO6.js → ui/assets/CompareDrawer-DAUuIJ6G.js} +1 -1
  22. package/.output/{public/assets/InspectorPet-D1A4505B.js → ui/assets/InspectorPet-BRBVjOWI.js} +1 -1
  23. package/.output/ui/assets/ProxyViewerContainer-D7Sq0ctc.js +59 -0
  24. package/.output/{public/assets/ReplayDialog-C5vmbecv.js → ui/assets/ReplayDialog-nDLsjOhs.js} +1 -1
  25. package/.output/{public/assets/RequestAnatomy-CPGNB8lk.js → ui/assets/RequestAnatomy-zd4BDgL0.js} +1 -1
  26. package/.output/ui/assets/ResponseView-AdIXGcSn.js +2 -0
  27. package/.output/ui/assets/StreamingChunkSequence-CesTz8He.js +1 -0
  28. package/.output/{public/assets/_sessionId-CDhRgzf6.js → ui/assets/_sessionId-BAfzrhSU.js} +1 -1
  29. package/.output/{public/assets/_sessionId-mw3aA8Bp.js → ui/assets/_sessionId-QnBomgPD.js} +1 -1
  30. package/.output/ui/assets/favicon-DYEkHSNl.svg +21 -0
  31. package/.output/{public/assets/index-B2eoeT-Z.js → ui/assets/index-BcsdxBAd.js} +1 -1
  32. package/.output/ui/assets/index-BvXp42al.css +1 -0
  33. package/.output/{public/assets/index-CX7_rf7o.js → ui/assets/index-D0rtCN9V.js} +1 -1
  34. package/.output/ui/assets/index-DHpwr08Z.js +73 -0
  35. package/.output/{public/assets/index-BfRhTvOd.js → ui/assets/index-dNuk2dsU.js} +1 -1
  36. package/.output/ui/assets/json-viewer-BvFHglMb.js +1 -0
  37. package/.output/{public/assets/jszip.min-C9rCFoU3.js → ui/assets/jszip.min-B5Z7gNZB.js} +1 -1
  38. package/.output/ui/index.html +229 -0
  39. package/.output/ui/runtime-config.js +2 -0
  40. package/.output/workers/logFinalizer.worker.js +983 -414
  41. package/.output/workers/sessionWorkerEntry.js +983 -414
  42. package/README.md +363 -35
  43. package/docs/instance-control.openapi.yaml +361 -0
  44. package/package.json +28 -5
  45. package/scripts/generate-theme-scales.mjs +3 -3
  46. package/src/{router.tsx → backend/router.tsx} +3 -15
  47. package/src/backend/routes/$.ts +26 -0
  48. package/src/backend/routes/__root.ts +4 -0
  49. package/src/backend/routes/api/-instances.ts +109 -0
  50. package/src/{routes → backend/routes}/api/alerts.summary.ts +3 -3
  51. package/src/{routes → backend/routes}/api/alerts.ts +4 -4
  52. package/src/{routes → backend/routes}/api/config.paths.ts +1 -1
  53. package/src/{routes → backend/routes}/api/config.ts +11 -2
  54. package/src/{routes → backend/routes}/api/ecosystem.packages.$packageId.help.ts +1 -1
  55. package/src/{routes → backend/routes}/api/ecosystem.packages.$packageId.install.ts +1 -1
  56. package/src/{routes → backend/routes}/api/ecosystem.packages.$packageId.runner-presets.ts +1 -1
  57. package/src/{routes → backend/routes}/api/ecosystem.packages.$packageId.upgrade.ts +1 -1
  58. package/src/{routes → backend/routes}/api/ecosystem.packages.ts +5 -1
  59. package/src/{routes → backend/routes}/api/ecosystem.recipes.$recipeId.run.ts +1 -1
  60. package/src/{routes → backend/routes}/api/ecosystem.tasks.$taskId.ts +1 -1
  61. package/src/{routes → backend/routes}/api/ecosystem.tasks.ts +1 -1
  62. package/src/{routes → backend/routes}/api/groups.$groupId.evidence.ts +2 -2
  63. package/src/{routes → backend/routes}/api/groups.$groupId.sessions.ts +2 -2
  64. package/src/{routes → backend/routes}/api/groups.$groupId.ts +2 -2
  65. package/src/{routes → backend/routes}/api/groups.ts +2 -2
  66. package/src/{routes → backend/routes}/api/health.ts +3 -3
  67. package/src/backend/routes/api/instances.$name.connection.ts +12 -0
  68. package/src/backend/routes/api/instances.$name.restart.ts +23 -0
  69. package/src/backend/routes/api/instances.$name.start.ts +18 -0
  70. package/src/backend/routes/api/instances.$name.stop.ts +23 -0
  71. package/src/backend/routes/api/instances.$name.ts +12 -0
  72. package/src/backend/routes/api/instances.ts +16 -0
  73. package/src/{routes → backend/routes}/api/knowledge.candidates.$candidateId.promote.ts +2 -2
  74. package/src/{routes → backend/routes}/api/knowledge.candidates.$candidateId.ts +2 -2
  75. package/src/{routes → backend/routes}/api/knowledge.candidates.ts +1 -1
  76. package/src/{routes → backend/routes}/api/knowledge.project-context.ts +2 -2
  77. package/src/{routes → backend/routes}/api/knowledge.search.ts +2 -2
  78. package/src/{routes → backend/routes}/api/knowledge.sessions.$sessionId.candidates.ts +3 -3
  79. package/src/{routes → backend/routes}/api/logs.$id.body.ts +6 -6
  80. package/src/{routes → backend/routes}/api/logs.$id.chunks.ts +5 -4
  81. package/src/{routes → backend/routes}/api/logs.$id.replay.ts +12 -11
  82. package/src/{routes → backend/routes}/api/logs.$id.ts +4 -3
  83. package/src/{routes → backend/routes}/api/logs.import.ts +3 -3
  84. package/src/{routes → backend/routes}/api/logs.stream.ts +11 -7
  85. package/src/{routes → backend/routes}/api/logs.ts +17 -9
  86. package/src/{routes → backend/routes}/api/mcp.ts +1 -1
  87. package/src/{routes → backend/routes}/api/models.ts +1 -1
  88. package/src/{routes → backend/routes}/api/pi-agent.ts +2 -2
  89. package/src/{routes → backend/routes}/api/providers.$providerId.model-metadata.ts +5 -5
  90. package/src/{routes → backend/routes}/api/providers.$providerId.test.log.ts +7 -7
  91. package/src/{routes → backend/routes}/api/providers.$providerId.ts +4 -4
  92. package/src/{routes → backend/routes}/api/providers.export.ts +1 -1
  93. package/src/{routes → backend/routes}/api/providers.import.ts +3 -3
  94. package/src/{routes → backend/routes}/api/providers.scan.ts +2 -2
  95. package/src/{routes → backend/routes}/api/providers.ts +9 -4
  96. package/src/{routes → backend/routes}/api/runs.$runId.evidence.ts +2 -2
  97. package/src/{routes → backend/routes}/api/runs.$runId.ts +2 -2
  98. package/src/{routes → backend/routes}/api/runs.ts +4 -4
  99. package/src/backend/routes/api/runtime.ts +46 -0
  100. package/src/{routes → backend/routes}/api/sessions.ts +3 -3
  101. package/src/{routes → backend/routes}/proxy/$.ts +1 -1
  102. package/src/{routes → backend/routes}/readyz.ts +1 -1
  103. package/src/backend/start.ts +10 -0
  104. package/src/cli/alias.ts +431 -0
  105. package/src/cli/doctor.ts +32 -9
  106. package/src/cli/instance.ts +1050 -0
  107. package/src/cli/instanceArgs.ts +285 -0
  108. package/src/cli/instanceControl.ts +181 -0
  109. package/src/cli/instanceModel.ts +267 -0
  110. package/src/cli/instanceRegistry.ts +731 -0
  111. package/src/cli/networkHints.ts +27 -14
  112. package/src/cli/onboard.ts +190 -35
  113. package/src/cli/onboardTarget.ts +228 -0
  114. package/src/cli/templates/codex-skill-onboard.ts +109 -31
  115. package/src/cli/templates/skill-onboard.ts +140 -70
  116. package/src/cli.ts +338 -77
  117. package/src/components/ProxyViewer.tsx +20 -10
  118. package/src/components/ProxyViewerContainer.tsx +12 -11
  119. package/src/components/pi-agent/PiAgentPanel.tsx +2 -2
  120. package/src/components/providers/ProviderCard.tsx +1 -1
  121. package/src/components/providers/ProviderTestResultsView.tsx +1 -1
  122. package/src/components/providers/SettingsDialog.tsx +160 -140
  123. package/src/components/proxy-viewer/ApplicationBar.tsx +2 -1
  124. package/src/components/proxy-viewer/LogEntry.tsx +1 -1
  125. package/src/components/proxy-viewer/log-formats/index.ts +1 -1
  126. package/src/contracts/index.ts +5 -0
  127. package/src/contracts/log.ts +33 -3
  128. package/src/knowledge/types.ts +3 -2
  129. package/src/lib/alertContract.ts +2 -2
  130. package/src/lib/apiClient.ts +30 -7
  131. package/src/lib/apiFormat.ts +29 -0
  132. package/src/lib/basePath.ts +31 -7
  133. package/src/lib/browserRuntimeContract.ts +124 -0
  134. package/src/lib/browserRuntimeResolver.ts +244 -0
  135. package/src/{proxy → lib}/claudeCodeStrip.ts +6 -7
  136. package/src/lib/codingAgentEndpoints.ts +54 -0
  137. package/src/lib/instanceContract.ts +236 -0
  138. package/src/lib/managedInstance.ts +104 -0
  139. package/src/lib/piAgentContract.ts +3 -2
  140. package/src/lib/publicBasePathContext.ts +137 -0
  141. package/src/lib/resourceLimits.ts +2 -0
  142. package/src/lib/runContract.ts +3 -3
  143. package/src/lib/sessionInfoContract.ts +3 -2
  144. package/src/mcp/instanceHandlers.ts +150 -0
  145. package/src/mcp/loopback.ts +16 -5
  146. package/src/mcp/mode.ts +25 -0
  147. package/src/mcp/server.ts +274 -39
  148. package/src/mcp/toolHandlers.ts +2 -2
  149. package/src/proxy/config.ts +25 -30
  150. package/src/proxy/dataDir.ts +43 -7
  151. package/src/proxy/ecosystemTasks.ts +161 -70
  152. package/src/proxy/formats/registry.ts +6 -21
  153. package/src/proxy/handler.ts +129 -29
  154. package/src/proxy/identityProxy.ts +1049 -50
  155. package/src/proxy/jsonlRecovery.ts +554 -0
  156. package/src/proxy/logFinalizer.ts +25 -10
  157. package/src/proxy/logImporter.ts +2 -1
  158. package/src/proxy/logIndex.ts +191 -95
  159. package/src/proxy/logger.ts +77 -14
  160. package/src/proxy/platformCommands.ts +41 -0
  161. package/src/proxy/privateDataPath.ts +310 -88
  162. package/src/proxy/rawStreamCapture.ts +194 -13
  163. package/src/proxy/runtimeAdmission.ts +5 -2
  164. package/src/proxy/runtimeHealth.ts +14 -1
  165. package/src/proxy/runtimeShutdown.ts +84 -2
  166. package/src/proxy/schemas.ts +7 -1
  167. package/src/proxy/sessionArchive.ts +78 -14
  168. package/src/proxy/sessionInfo.ts +5 -4
  169. package/src/proxy/sqliteLogIndex.ts +65 -2
  170. package/src/proxy/store.ts +141 -34
  171. package/src/proxy/uiAssetServer.ts +106 -0
  172. package/src/proxy/upstream.ts +29 -2
  173. package/src/ui/bootstrap.ts +167 -0
  174. package/src/ui/index.html +38 -0
  175. package/src/ui/logsStreamClient.ts +20 -0
  176. package/src/ui/main.tsx +13 -0
  177. package/src/ui/public/runtime-config.js +2 -0
  178. package/src/ui/router.tsx +13 -0
  179. package/src/ui/routes/__root.tsx +69 -0
  180. package/src/{routes → ui/routes}/index.tsx +2 -3
  181. package/src/{routes → ui/routes}/session/$sessionId.tsx +5 -11
  182. package/src/ui/runtimeDiscovery.ts +50 -0
  183. package/styles/globals.css +1 -0
  184. package/.output/public/assets/ProxyViewerContainer-BhpIgfBi.js +0 -126
  185. package/.output/public/assets/ResponseView-DUfrajCm.js +0 -2
  186. package/.output/public/assets/StreamingChunkSequence-nbeCQqOz.js +0 -1
  187. package/.output/public/assets/index-BWjeqxbk.css +0 -1
  188. package/.output/public/assets/index-C6qZwYlR.js +0 -70
  189. package/.output/public/assets/json-viewer-BLmF1sOZ.js +0 -1
  190. package/.output/server/_libs/@radix-ui/react-accessible-icon+[...].mjs +0 -1
  191. package/.output/server/_libs/@radix-ui/react-dismissable-layer+[...].mjs +0 -344
  192. package/.output/server/_libs/@radix-ui/react-navigation-menu+[...].mjs +0 -2
  193. package/.output/server/_libs/@radix-ui/react-one-time-password-field+[...].mjs +0 -2
  194. package/.output/server/_libs/@radix-ui/react-password-toggle-field+[...].mjs +0 -2
  195. package/.output/server/_libs/@radix-ui/react-use-callback-ref+[...].mjs +0 -11
  196. package/.output/server/_libs/@radix-ui/react-use-controllable-state+[...].mjs +0 -69
  197. package/.output/server/_libs/@radix-ui/react-use-effect-event+[...].mjs +0 -1
  198. package/.output/server/_libs/@radix-ui/react-use-is-hydrated+[...].mjs +0 -28
  199. package/.output/server/_libs/@radix-ui/react-use-layout-effect+[...].mjs +0 -6
  200. package/.output/server/_libs/@radix-ui/react-visually-hidden+[...].mjs +0 -34
  201. package/.output/server/_libs/aria-hidden.mjs +0 -122
  202. package/.output/server/_libs/bail.mjs +0 -8
  203. package/.output/server/_libs/character-entities.mjs +0 -2130
  204. package/.output/server/_libs/class-variance-authority.mjs +0 -44
  205. package/.output/server/_libs/clsx.mjs +0 -16
  206. package/.output/server/_libs/comma-separated-tokens.mjs +0 -10
  207. package/.output/server/_libs/decode-named-character-reference+[...].mjs +0 -8
  208. package/.output/server/_libs/dequal.mjs +0 -27
  209. package/.output/server/_libs/detect-node-es.mjs +0 -1
  210. package/.output/server/_libs/devlop.mjs +0 -8
  211. package/.output/server/_libs/diff.mjs +0 -320
  212. package/.output/server/_libs/estree-util-is-identifier-name.mjs +0 -11
  213. package/.output/server/_libs/floating-ui__core.mjs +0 -725
  214. package/.output/server/_libs/floating-ui__dom.mjs +0 -622
  215. package/.output/server/_libs/floating-ui__react-dom.mjs +0 -292
  216. package/.output/server/_libs/floating-ui__utils.mjs +0 -320
  217. package/.output/server/_libs/get-nonce.mjs +0 -9
  218. package/.output/server/_libs/hast-util-to-jsx-runtime.mjs +0 -388
  219. package/.output/server/_libs/hast-util-whitespace.mjs +0 -10
  220. package/.output/server/_libs/html-url-attributes.mjs +0 -26
  221. package/.output/server/_libs/inline-style-parser.mjs +0 -142
  222. package/.output/server/_libs/is-plain-obj.mjs +0 -10
  223. package/.output/server/_libs/lucide-react.mjs +0 -1027
  224. package/.output/server/_libs/mdast-util-from-markdown.mjs +0 -717
  225. package/.output/server/_libs/mdast-util-to-hast.mjs +0 -710
  226. package/.output/server/_libs/mdast-util-to-string.mjs +0 -38
  227. package/.output/server/_libs/micromark-core-commonmark.mjs +0 -2259
  228. package/.output/server/_libs/micromark-factory-destination.mjs +0 -94
  229. package/.output/server/_libs/micromark-factory-label.mjs +0 -63
  230. package/.output/server/_libs/micromark-factory-space.mjs +0 -24
  231. package/.output/server/_libs/micromark-factory-title.mjs +0 -65
  232. package/.output/server/_libs/micromark-factory-whitespace.mjs +0 -22
  233. package/.output/server/_libs/micromark-util-character.mjs +0 -44
  234. package/.output/server/_libs/micromark-util-chunked.mjs +0 -36
  235. package/.output/server/_libs/micromark-util-classify-character+[...].mjs +0 -12
  236. package/.output/server/_libs/micromark-util-combine-extensions+[...].mjs +0 -41
  237. package/.output/server/_libs/micromark-util-decode-numeric-character-reference+[...].mjs +0 -19
  238. package/.output/server/_libs/micromark-util-decode-string.mjs +0 -21
  239. package/.output/server/_libs/micromark-util-encode.mjs +0 -1
  240. package/.output/server/_libs/micromark-util-html-tag-name.mjs +0 -69
  241. package/.output/server/_libs/micromark-util-normalize-identifier+[...].mjs +0 -6
  242. package/.output/server/_libs/micromark-util-resolve-all.mjs +0 -15
  243. package/.output/server/_libs/micromark-util-sanitize-uri.mjs +0 -41
  244. package/.output/server/_libs/micromark-util-subtokenize.mjs +0 -346
  245. package/.output/server/_libs/micromark.mjs +0 -906
  246. package/.output/server/_libs/property-information.mjs +0 -1209
  247. package/.output/server/_libs/radix-ui.mjs +0 -1
  248. package/.output/server/_libs/radix-ui__number.mjs +0 -6
  249. package/.output/server/_libs/radix-ui__primitive.mjs +0 -11
  250. package/.output/server/_libs/radix-ui__react-accordion.mjs +0 -1
  251. package/.output/server/_libs/radix-ui__react-alert-dialog.mjs +0 -1
  252. package/.output/server/_libs/radix-ui__react-arrow.mjs +0 -23
  253. package/.output/server/_libs/radix-ui__react-aspect-ratio.mjs +0 -1
  254. package/.output/server/_libs/radix-ui__react-avatar.mjs +0 -1
  255. package/.output/server/_libs/radix-ui__react-checkbox.mjs +0 -1
  256. package/.output/server/_libs/radix-ui__react-collapsible.mjs +0 -144
  257. package/.output/server/_libs/radix-ui__react-collection.mjs +0 -69
  258. package/.output/server/_libs/radix-ui__react-compose-refs.mjs +0 -38
  259. package/.output/server/_libs/radix-ui__react-context-menu.mjs +0 -1
  260. package/.output/server/_libs/radix-ui__react-context.mjs +0 -64
  261. package/.output/server/_libs/radix-ui__react-dialog.mjs +0 -280
  262. package/.output/server/_libs/radix-ui__react-direction.mjs +0 -9
  263. package/.output/server/_libs/radix-ui__react-dropdown-menu.mjs +0 -1
  264. package/.output/server/_libs/radix-ui__react-focus-guards.mjs +0 -39
  265. package/.output/server/_libs/radix-ui__react-focus-scope.mjs +0 -210
  266. package/.output/server/_libs/radix-ui__react-form.mjs +0 -1
  267. package/.output/server/_libs/radix-ui__react-hover-card.mjs +0 -1
  268. package/.output/server/_libs/radix-ui__react-id.mjs +0 -14
  269. package/.output/server/_libs/radix-ui__react-label.mjs +0 -1
  270. package/.output/server/_libs/radix-ui__react-menu.mjs +0 -1
  271. package/.output/server/_libs/radix-ui__react-menubar.mjs +0 -1
  272. package/.output/server/_libs/radix-ui__react-popover.mjs +0 -1
  273. package/.output/server/_libs/radix-ui__react-popper.mjs +0 -340
  274. package/.output/server/_libs/radix-ui__react-portal.mjs +0 -16
  275. package/.output/server/_libs/radix-ui__react-presence.mjs +0 -173
  276. package/.output/server/_libs/radix-ui__react-primitive.mjs +0 -42
  277. package/.output/server/_libs/radix-ui__react-progress.mjs +0 -1
  278. package/.output/server/_libs/radix-ui__react-radio-group.mjs +0 -1
  279. package/.output/server/_libs/radix-ui__react-roving-focus.mjs +0 -235
  280. package/.output/server/_libs/radix-ui__react-scroll-area.mjs +0 -731
  281. package/.output/server/_libs/radix-ui__react-select.mjs +0 -1207
  282. package/.output/server/_libs/radix-ui__react-separator.mjs +0 -28
  283. package/.output/server/_libs/radix-ui__react-slider.mjs +0 -1
  284. package/.output/server/_libs/radix-ui__react-slot.mjs +0 -132
  285. package/.output/server/_libs/radix-ui__react-switch.mjs +0 -1
  286. package/.output/server/_libs/radix-ui__react-tabs.mjs +0 -194
  287. package/.output/server/_libs/radix-ui__react-toast.mjs +0 -2
  288. package/.output/server/_libs/radix-ui__react-toggle-group.mjs +0 -1
  289. package/.output/server/_libs/radix-ui__react-toggle.mjs +0 -1
  290. package/.output/server/_libs/radix-ui__react-toolbar.mjs +0 -1
  291. package/.output/server/_libs/radix-ui__react-tooltip.mjs +0 -498
  292. package/.output/server/_libs/radix-ui__react-use-previous.mjs +0 -14
  293. package/.output/server/_libs/radix-ui__react-use-size.mjs +0 -39
  294. package/.output/server/_libs/react-markdown.mjs +0 -235
  295. package/.output/server/_libs/react-remove-scroll-bar.mjs +0 -82
  296. package/.output/server/_libs/react-remove-scroll.mjs +0 -328
  297. package/.output/server/_libs/react-style-singleton.mjs +0 -69
  298. package/.output/server/_libs/remark-parse.mjs +0 -19
  299. package/.output/server/_libs/remark-rehype.mjs +0 -21
  300. package/.output/server/_libs/space-separated-tokens.mjs +0 -6
  301. package/.output/server/_libs/style-to-js.mjs +0 -72
  302. package/.output/server/_libs/style-to-object.mjs +0 -38
  303. package/.output/server/_libs/swr.mjs +0 -947
  304. package/.output/server/_libs/tailwind-merge.mjs +0 -3255
  305. package/.output/server/_libs/tanstack__react-virtual.mjs +0 -130
  306. package/.output/server/_libs/tanstack__virtual-core.mjs +0 -1319
  307. package/.output/server/_libs/three.mjs +0 -26016
  308. package/.output/server/_libs/trim-lines.mjs +0 -41
  309. package/.output/server/_libs/trough.mjs +0 -85
  310. package/.output/server/_libs/tslib.mjs +0 -1
  311. package/.output/server/_libs/ungap__structured-clone.mjs +0 -212
  312. package/.output/server/_libs/unified.mjs +0 -661
  313. package/.output/server/_libs/unist-util-is.mjs +0 -100
  314. package/.output/server/_libs/unist-util-position.mjs +0 -27
  315. package/.output/server/_libs/unist-util-stringify-position.mjs +0 -27
  316. package/.output/server/_libs/unist-util-visit-parents.mjs +0 -82
  317. package/.output/server/_libs/unist-util-visit.mjs +0 -24
  318. package/.output/server/_libs/use-callback-ref.mjs +0 -66
  319. package/.output/server/_libs/use-sidecar.mjs +0 -106
  320. package/.output/server/_libs/vfile-message.mjs +0 -138
  321. package/.output/server/_libs/vfile.mjs +0 -467
  322. package/.output/server/_sessionId-E_Dmopc-.mjs +0 -144
  323. package/.output/server/_sessionId-U54BAqdp.mjs +0 -82
  324. package/.output/server/_ssr/CompareDrawer-CksatPed.mjs +0 -1058
  325. package/.output/server/_ssr/InspectorPet-DNc4GQ7z.mjs +0 -936
  326. package/.output/server/_ssr/ProxyViewerContainer-B02Ey0p_.mjs +0 -17607
  327. package/.output/server/_ssr/ReplayDialog-DdMvHJF1.mjs +0 -723
  328. package/.output/server/_ssr/RequestAnatomy--vh2Z8__.mjs +0 -835
  329. package/.output/server/_ssr/ResponseView-Dhxc-cKC.mjs +0 -755
  330. package/.output/server/_ssr/StreamingChunkSequence-BG0EQmaD.mjs +0 -272
  331. package/.output/server/_ssr/index-D0At9nfd.mjs +0 -82
  332. package/.output/server/_ssr/index-lcfulIXa.mjs +0 -133
  333. package/.output/server/_ssr/json-viewer-My1J1_5R.mjs +0 -481
  334. package/.output/server/_ssr/start-HYkvq4Ni.mjs +0 -4
  335. package/.output/server/_tanstack-start-manifest_v-B2chR8ef.mjs +0 -4
  336. package/.output/server/node_modules/tslib/modules/index.js +0 -70
  337. package/.output/server/node_modules/tslib/modules/package.json +0 -3
  338. package/.output/server/node_modules/tslib/package.json +0 -47
  339. package/.output/server/node_modules/tslib/tslib.js +0 -484
  340. package/.output/server/package.json +0 -9
  341. package/src/routes/__root.tsx +0 -170
  342. /package/.output/{public → ui}/assets/agent-inspector-58K1_MsC.ico +0 -0
  343. /package/.output/{public → ui}/assets/alibaba-TTwafVwX.svg +0 -0
  344. /package/.output/{public → ui}/assets/minimax-BPMzvuL-.jpeg +0 -0
  345. /package/.output/{public → ui}/assets/qwen-mMn3f5ul.webp +0 -0
  346. /package/.output/{public → ui}/assets/zhipuai-BPNAnxo-.svg +0 -0
  347. /package/src/{routes → backend/routes}/livez.ts +0 -0
@@ -1,15 +1,30 @@
1
1
  import http from "node:http";
2
2
  import { Buffer } from "node:buffer";
3
- import { timingSafeEqual } from "node:crypto";
3
+ import { createHash, timingSafeEqual } from "node:crypto";
4
4
  import { isIP } from "node:net";
5
+ import {
6
+ BROWSER_RUNTIME_JSON_PLACEHOLDER,
7
+ BROWSER_RUNTIME_SCHEMA_VERSION,
8
+ type BrowserRuntimeDeployment,
9
+ } from "../lib/browserRuntimeContract";
5
10
  import {
6
11
  AGENT_INSPECTOR_BASE_PATH_META_NAME,
12
+ AGENT_INSPECTOR_PUBLIC_BASE_PATH_HEADER,
13
+ AGENT_INSPECTOR_PUBLIC_ORIGIN_HEADER,
14
+ AGENT_INSPECTOR_PUBLIC_PORT_HEADER,
7
15
  getConfiguredBasePath,
8
- normalizeBasePath,
16
+ normalizePublicBasePath,
9
17
  stripBasePath,
10
18
  stripBasePathFromRequestPath,
19
+ withBasePath,
11
20
  } from "../lib/basePath";
21
+ import {
22
+ MANAGED_INSTANCE_CONTROL_PATH,
23
+ MANAGED_INSTANCE_SHUTDOWN_PATH,
24
+ MANAGED_INSTANCE_TOKEN_HEADER,
25
+ } from "../lib/managedInstance";
12
26
  import { lookupClientByPort } from "./socketTracker";
27
+ import { loadUiAsset } from "./uiAssetServer";
13
28
 
14
29
  const IDENTITY_HEADERS = {
15
30
  pid: "x-agent-inspector-client-pid",
@@ -23,6 +38,180 @@ const PROXY_TOKEN_HEADER = "x-agent-inspector-proxy-token";
23
38
  const CSRF_HEADER = "x-agent-inspector-csrf";
24
39
  const DEFAULT_MAX_REQUEST_BYTES = 64 * 1024 * 1024;
25
40
  const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
41
+ const ALIAS_CONTROL_MAX_REQUEST_BYTES = 8 * 1024;
42
+
43
+ export const IDENTITY_PROXY_CONTROL_NAMESPACE = "/.agent-inspector";
44
+ export const IDENTITY_PROXY_ALIAS_CONTROL_PATH = `${IDENTITY_PROXY_CONTROL_NAMESPACE}/base-path-aliases`;
45
+
46
+ export type BasePathAliasResolution = {
47
+ effectiveBasePath: string;
48
+ isAlias: boolean;
49
+ upstreamPath: string;
50
+ };
51
+
52
+ export type BasePathAliasMutationResult =
53
+ | { ok: true; basePath: string }
54
+ | {
55
+ ok: false;
56
+ code: "canonical" | "duplicate" | "invalid" | "not-found" | "overlap" | "reserved";
57
+ error: string;
58
+ };
59
+
60
+ export type BasePathAliasRegistry = {
61
+ canonicalBasePath: string;
62
+ add: (basePath: string) => BasePathAliasMutationResult;
63
+ list: () => readonly string[];
64
+ remove: (basePath: string) => BasePathAliasMutationResult;
65
+ resolve: (rawPath: string) => BasePathAliasResolution;
66
+ };
67
+
68
+ function normalizedAliasCandidate(
69
+ value: string,
70
+ ): { ok: true; basePath: string } | { ok: false; code: "invalid" | "reserved"; error: string } {
71
+ const normalized = normalizePublicBasePath(value);
72
+ if (normalized === null) {
73
+ return {
74
+ ok: false,
75
+ code: "invalid",
76
+ error: "Base Path aliases may contain only URL-safe path segments.",
77
+ };
78
+ }
79
+ if (normalized.length === 0) {
80
+ return {
81
+ ok: false,
82
+ code: "invalid",
83
+ error: "Base Path aliases must be non-root paths such as /inspector.",
84
+ };
85
+ }
86
+ if (
87
+ normalized === IDENTITY_PROXY_CONTROL_NAMESPACE ||
88
+ normalized.startsWith(`${IDENTITY_PROXY_CONTROL_NAMESPACE}/`)
89
+ ) {
90
+ return {
91
+ ok: false,
92
+ code: "reserved",
93
+ error: `${normalized} collides with the reserved Agent Inspector control path.`,
94
+ };
95
+ }
96
+ const publicNamespace = ["/api", "/assets", "/proxy", "/session", "/livez", "/readyz"].find(
97
+ (namespace) => normalized === namespace || normalized.startsWith(`${namespace}/`),
98
+ );
99
+ if (publicNamespace !== undefined) {
100
+ return {
101
+ ok: false,
102
+ code: "reserved",
103
+ error: `${normalized} collides with the reserved ${publicNamespace} public namespace.`,
104
+ };
105
+ }
106
+ return { ok: true, basePath: normalized };
107
+ }
108
+
109
+ function pathMatchesBasePath(pathname: string, basePath: string): boolean {
110
+ return pathname === basePath || pathname.startsWith(`${basePath}/`);
111
+ }
112
+
113
+ function basePathsOverlap(left: string, right: string): boolean {
114
+ return pathMatchesBasePath(left, right) || pathMatchesBasePath(right, left);
115
+ }
116
+
117
+ /**
118
+ * Create an in-memory registry owned by one identity ingress. Runtime aliases
119
+ * intentionally disappear when that ingress exits.
120
+ */
121
+ export function createBasePathAliasRegistry(
122
+ canonicalBasePath: string,
123
+ initialAliases: readonly string[] = [],
124
+ ): BasePathAliasRegistry {
125
+ const canonical = normalizePublicBasePath(canonicalBasePath) ?? "";
126
+ const aliases: string[] = [];
127
+
128
+ const add = (value: string): BasePathAliasMutationResult => {
129
+ const candidate = normalizedAliasCandidate(value);
130
+ if (!candidate.ok) return candidate;
131
+ if (candidate.basePath === canonical) {
132
+ return {
133
+ ok: false,
134
+ code: "canonical",
135
+ error: `${candidate.basePath} is already the canonical Base Path.`,
136
+ };
137
+ }
138
+ if (canonical.length > 0 && basePathsOverlap(candidate.basePath, canonical)) {
139
+ return {
140
+ ok: false,
141
+ code: "overlap",
142
+ error: `${candidate.basePath} overlaps the canonical Base Path ${canonical}.`,
143
+ };
144
+ }
145
+ if (aliases.includes(candidate.basePath)) {
146
+ return {
147
+ ok: false,
148
+ code: "duplicate",
149
+ error: `${candidate.basePath} is already an active runtime alias.`,
150
+ };
151
+ }
152
+ const overlappingAlias = aliases.find((alias) => basePathsOverlap(candidate.basePath, alias));
153
+ if (overlappingAlias !== undefined) {
154
+ return {
155
+ ok: false,
156
+ code: "overlap",
157
+ error: `${candidate.basePath} overlaps the active runtime alias ${overlappingAlias}.`,
158
+ };
159
+ }
160
+ aliases.push(candidate.basePath);
161
+ aliases.sort((left, right) => right.length - left.length || left.localeCompare(right));
162
+ return { ok: true, basePath: candidate.basePath };
163
+ };
164
+
165
+ const remove = (value: string): BasePathAliasMutationResult => {
166
+ const candidate = normalizedAliasCandidate(value);
167
+ if (!candidate.ok) return candidate;
168
+ if (candidate.basePath === canonical) {
169
+ return {
170
+ ok: false,
171
+ code: "canonical",
172
+ error: `${candidate.basePath} is the canonical Base Path and cannot be removed.`,
173
+ };
174
+ }
175
+ const index = aliases.indexOf(candidate.basePath);
176
+ if (index === -1) {
177
+ return {
178
+ ok: false,
179
+ code: "not-found",
180
+ error: `${candidate.basePath} is not an active runtime alias.`,
181
+ };
182
+ }
183
+ aliases.splice(index, 1);
184
+ return { ok: true, basePath: candidate.basePath };
185
+ };
186
+
187
+ const resolve = (rawPath: string): BasePathAliasResolution => {
188
+ const requestUrl = new URL(rawPath, "http://localhost");
189
+ const alias = aliases.find((candidate) => pathMatchesBasePath(requestUrl.pathname, candidate));
190
+ const effectiveBasePath = alias ?? canonical;
191
+ return {
192
+ effectiveBasePath,
193
+ isAlias: alias !== undefined,
194
+ upstreamPath: stripBasePathFromRequestPath(rawPath, effectiveBasePath),
195
+ };
196
+ };
197
+
198
+ const registry: BasePathAliasRegistry = {
199
+ canonicalBasePath: canonical,
200
+ add,
201
+ list: () => [...aliases],
202
+ remove,
203
+ resolve,
204
+ };
205
+ for (const alias of initialAliases) add(alias);
206
+ return registry;
207
+ }
208
+
209
+ export function resolveBasePathAliasRequest(
210
+ registry: BasePathAliasRegistry,
211
+ rawPath: string,
212
+ ): BasePathAliasResolution {
213
+ return registry.resolve(rawPath);
214
+ }
26
215
 
27
216
  type IdentityProxyRuntimeState = {
28
217
  acceptingRequests: boolean;
@@ -83,34 +272,211 @@ function htmlAttributeEscape(value: string): string {
83
272
  .replace(/>/gu, ">");
84
273
  }
85
274
 
86
- export function rewriteHtmlForBasePath(html: string, basePath: string): string {
87
- const normalized = normalizeBasePath(basePath);
88
- if (normalized.length === 0) return html;
275
+ function rewriteRootRelativePublicPath(
276
+ value: string,
277
+ effectiveBasePath: string,
278
+ sourceBasePath: string,
279
+ ): string {
280
+ if (!value.startsWith("/") || value.startsWith("//")) return value;
281
+ const normalizedEffective = normalizePublicBasePath(effectiveBasePath);
282
+ const normalizedSource = normalizePublicBasePath(sourceBasePath);
283
+ if (normalizedEffective === null || normalizedSource === null) return value;
284
+ const suffixIndex = value.search(/[?#]/u);
285
+ const pathname = suffixIndex === -1 ? value : value.slice(0, suffixIndex);
286
+ const suffix = suffixIndex === -1 ? "" : value.slice(suffixIndex);
287
+ if (normalizedEffective.length > 0 && pathMatchesBasePath(pathname, normalizedEffective)) {
288
+ return value;
289
+ }
290
+ const rootRelativePath =
291
+ normalizedSource.length > 0 && pathMatchesBasePath(pathname, normalizedSource)
292
+ ? pathname.slice(normalizedSource.length) || "/"
293
+ : pathname;
294
+ const rewritten = withBasePath(rootRelativePath, normalizedEffective);
295
+ const trailingSlash = pathname.endsWith("/") && !rewritten.endsWith("/") ? "/" : "";
296
+ return `${rewritten}${trailingSlash}${suffix}`;
297
+ }
298
+
299
+ function rewriteQuotedAssetPaths(
300
+ source: string,
301
+ effectiveBasePath: string,
302
+ sourceBasePath: string,
303
+ escape: (value: string, quote: string) => string,
304
+ ): string {
305
+ return source.replace(
306
+ /(["'])(\/(?:[^"'/]+\/)*assets\/)/gu,
307
+ (_match: string, quote: string, assetPrefix: string) =>
308
+ `${quote}${escape(
309
+ rewriteRootRelativePublicPath(assetPrefix, effectiveBasePath, sourceBasePath),
310
+ quote,
311
+ )}`,
312
+ );
313
+ }
314
+
315
+ export function rewriteHtmlForBasePath(
316
+ html: string,
317
+ basePath: string,
318
+ sourceBasePath = "",
319
+ ): string {
320
+ const normalized = normalizePublicBasePath(basePath);
321
+ const normalizedSource = normalizePublicBasePath(sourceBasePath);
322
+ if (normalized === null || normalizedSource === null) return html;
89
323
  const escaped = htmlAttributeEscape(normalized);
90
324
  const meta = `<meta name="${AGENT_INSPECTOR_BASE_PATH_META_NAME}" content="${escaped}">`;
91
- const withMeta = html.includes(`name="${AGENT_INSPECTOR_BASE_PATH_META_NAME}"`)
92
- ? html
93
- : html.replace(/<head(\s[^>]*)?>/iu, (match) => `${match}${meta}`);
94
- const escapedBase = normalized.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
325
+ const metaPattern = new RegExp(
326
+ `<meta\\b[^>]*\\bname\\s*=\\s*("|')${AGENT_INSPECTOR_BASE_PATH_META_NAME}\\1[^>]*>`,
327
+ "giu",
328
+ );
329
+ let replacedMeta = false;
330
+ const withReplacedMeta = html.replace(metaPattern, () => {
331
+ if (replacedMeta) return "";
332
+ replacedMeta = true;
333
+ return meta;
334
+ });
335
+ const withMeta = replacedMeta
336
+ ? withReplacedMeta
337
+ : withReplacedMeta.replace(/<head(\s[^>]*)?>/iu, (match) => `${match}${meta}`);
95
338
  const withRewrittenAttributes = withMeta.replace(
96
- new RegExp(`\\b(href|src|action)=("|')/(?!/|${escapedBase.slice(1)}(?:/|$))`, "giu"),
97
- (_match: string, attr: string, quote: string) => `${attr}=${quote}${normalized}/`,
339
+ /\b(href|src|action)=(["'])(\/(?!\/)[^"']*)\2/giu,
340
+ (_match: string, attribute: string, quote: string, path: string) =>
341
+ `${attribute}=${quote}${htmlAttributeEscape(
342
+ rewriteRootRelativePublicPath(path, normalized, normalizedSource),
343
+ )}${quote}`,
344
+ );
345
+ return rewriteQuotedAssetPaths(withRewrittenAttributes, normalized, normalizedSource, (value) =>
346
+ htmlAttributeEscape(value),
98
347
  );
99
- return withRewrittenAttributes.replace(/(["'])\/assets\//gu, `$1${normalized}/assets/`);
100
348
  }
101
349
 
102
- function javaScriptStringEscape(value: string): string {
103
- return value.replace(/\\/gu, "\\\\").replace(/"/gu, '\\"');
350
+ function javaScriptStringEscape(value: string, quote: string): string {
351
+ const escaped = value
352
+ .replace(/\\/gu, "\\\\")
353
+ .replace(/\r/gu, "\\r")
354
+ .replace(/\n/gu, "\\n")
355
+ .replace(/\u2028/gu, "\\u2028")
356
+ .replace(/\u2029/gu, "\\u2029")
357
+ .replace(/&/gu, "\\u0026")
358
+ .replace(/</gu, "\\u003c")
359
+ .replace(/>/gu, "\\u003e");
360
+ return quote === "'" ? escaped.replace(/'/gu, "\\'") : escaped.replace(/"/gu, '\\"');
361
+ }
362
+
363
+ export function rewriteJavaScriptForBasePath(
364
+ source: string,
365
+ basePath: string,
366
+ sourceBasePath = "",
367
+ ): string {
368
+ const normalized = normalizePublicBasePath(basePath);
369
+ const normalizedSource = normalizePublicBasePath(sourceBasePath);
370
+ if (normalized === null || normalizedSource === null) return source;
371
+ const escaped = javaScriptStringEscape(normalized, '"');
372
+ const withRouterBasePath = source.replace(
373
+ /\bbasepath:\s*(["'])[^"']*\1/gu,
374
+ `basepath:"${escaped}"`,
375
+ );
376
+ const withDynamicPaths = withRouterBasePath.replace(
377
+ /return(["'])(\/(?!\/)[^"']*)\1\+([A-Za-z_$][\w$]*)/gu,
378
+ (_match: string, quote: string, path: string, variable: string) => {
379
+ const rewritten = rewriteRootRelativePublicPath(path, normalized, normalizedSource);
380
+ return `return${quote}${javaScriptStringEscape(rewritten, quote)}${quote}+${variable}`;
381
+ },
382
+ );
383
+ return rewriteQuotedAssetPaths(
384
+ withDynamicPaths,
385
+ normalized,
386
+ normalizedSource,
387
+ javaScriptStringEscape,
388
+ );
389
+ }
390
+
391
+ function publicOriginForRequest(req: http.IncomingMessage, options: IdentityProxyOptions): string {
392
+ if (options.publicOrigin !== undefined) {
393
+ try {
394
+ return new URL(options.publicOrigin).origin;
395
+ } catch {
396
+ // Invalid explicit values are rejected by the CLI; retain a safe local fallback for embedders.
397
+ }
398
+ }
399
+ return `http://${req.headers.host ?? "localhost"}`;
400
+ }
401
+
402
+ function browserRuntimeForRequest(
403
+ req: http.IncomingMessage,
404
+ options: IdentityProxyOptions,
405
+ basePath: string,
406
+ ): BrowserRuntimeDeployment {
407
+ const origin = publicOriginForRequest(req, options);
408
+ const normalized = normalizePublicBasePath(basePath) ?? "";
409
+ const baseUrl = normalized.length === 0 ? origin : `${origin}${normalized}`;
410
+ return {
411
+ schemaVersion: BROWSER_RUNTIME_SCHEMA_VERSION,
412
+ deploymentMode: "composed",
413
+ ui: { basePath: normalized.length === 0 ? "/" : normalized },
414
+ control: { baseUrl },
415
+ agent: { baseUrl },
416
+ capabilities: {
417
+ rest: true,
418
+ sse: true,
419
+ proxy: true,
420
+ mcp: true,
421
+ },
422
+ };
423
+ }
424
+
425
+ function safeInlineJson(value: unknown): string {
426
+ return JSON.stringify(value)
427
+ .replace(/&/gu, "\\u0026")
428
+ .replace(/</gu, "\\u003c")
429
+ .replace(/>/gu, "\\u003e")
430
+ .replace(/\u2028/gu, "\\u2028")
431
+ .replace(/\u2029/gu, "\\u2029");
432
+ }
433
+
434
+ function injectBrowserRuntime(html: string, runtime: BrowserRuntimeDeployment): string {
435
+ const runtimeJson = safeInlineJson(runtime);
436
+ if (html.includes(BROWSER_RUNTIME_JSON_PLACEHOLDER)) {
437
+ return html.replaceAll(BROWSER_RUNTIME_JSON_PLACEHOLDER, runtimeJson);
438
+ }
439
+ const bootstrap = `<script>window.__AGENT_INSPECTOR_RUNTIME__=${runtimeJson};</script>`;
440
+ return html.replace(/<head(\s[^>]*)?>/iu, (head) => `${head}${bootstrap}`);
104
441
  }
105
442
 
106
- export function rewriteJavaScriptForBasePath(source: string, basePath: string): string {
107
- const normalized = normalizeBasePath(basePath);
108
- if (normalized.length === 0) return source;
109
- const escaped = javaScriptStringEscape(normalized);
110
- return source
111
- .replace(/\bbasepath:\s*""/gu, `basepath:"${escaped}"`)
112
- .replace(/return"\/"\+([A-Za-z_$][\w$]*)/gu, `return"${escaped}/"+$1`)
113
- .replace(/(["'])\/assets\//gu, `$1${normalized}/assets/`);
443
+ async function tryServeUiAsset(
444
+ req: http.IncomingMessage,
445
+ res: http.ServerResponse,
446
+ options: IdentityProxyOptions,
447
+ pathname: string,
448
+ basePath: string,
449
+ sourceBasePath: string,
450
+ ): Promise<boolean> {
451
+ const method = (req.method ?? "GET").toUpperCase();
452
+ if ((method !== "GET" && method !== "HEAD") || options.uiDirectory === undefined) return false;
453
+ const asset = await loadUiAsset(options.uiDirectory, pathname);
454
+ if (asset === null) return false;
455
+ let body = asset.body;
456
+ if (asset.document) {
457
+ const rewritten = rewriteHtmlForBasePath(body.toString("utf8"), basePath, sourceBasePath);
458
+ body = Buffer.from(
459
+ injectBrowserRuntime(rewritten, browserRuntimeForRequest(req, options, basePath)),
460
+ "utf8",
461
+ );
462
+ } else if (basePath.length > 0 && asset.contentType.includes("javascript")) {
463
+ body = Buffer.from(
464
+ rewriteJavaScriptForBasePath(body.toString("utf8"), basePath, sourceBasePath),
465
+ "utf8",
466
+ );
467
+ }
468
+ const headers: Record<string, string | number> = {
469
+ ...BASE_SECURITY_HEADERS,
470
+ "cache-control": asset.cacheControl,
471
+ "content-length": body.byteLength,
472
+ "content-type": asset.contentType,
473
+ };
474
+ if (asset.document) headers["content-security-policy"] = DOCUMENT_CONTENT_SECURITY_POLICY;
475
+ res.writeHead(200, headers);
476
+ if (method === "HEAD") res.end();
477
+ else res.end(body);
478
+ req.resume();
479
+ return true;
114
480
  }
115
481
 
116
482
  export type IdentityProxyOptions = {
@@ -134,10 +500,32 @@ export type IdentityProxyOptions = {
134
500
  allowUnauthenticatedRemoteProxy?: boolean;
135
501
  /** Additional public hostnames accepted by the ingress Host boundary. */
136
502
  trustedHosts?: readonly string[];
503
+ /** Exact browser UI origins allowed to call control REST/SSE across origins. */
504
+ allowedUiOrigins?: readonly string[];
505
+ /** Independent static UI artifact directory served only by the protected ingress. */
506
+ uiDirectory?: string;
507
+ /** Explicit public origin for reverse-proxy deployments; never inferred from forwarded headers. */
508
+ publicOrigin?: string;
137
509
  /** Maximum declared or streamed request body size. */
138
510
  maxRequestBytes?: number;
139
511
  /** Deadline for receiving and forwarding one request body. */
140
512
  requestTimeoutMs?: number;
513
+ /** Runtime-scoped additional public Base Path mounts. */
514
+ aliasRegistry?: BasePathAliasRegistry;
515
+ /** Private lifecycle identity exposed only to the owning local instance manager. */
516
+ managedInstance?: ManagedInstanceControlOptions;
517
+ };
518
+
519
+ export type ManagedInstanceControlOptions = {
520
+ name: string;
521
+ instanceId: string;
522
+ launchId: string;
523
+ controlToken: string;
524
+ supervisorPid: number;
525
+ startedAt: string;
526
+ publicPort: number;
527
+ upstreamPort: number;
528
+ requestShutdown: () => void;
141
529
  };
142
530
 
143
531
  function isLoopbackPeer(address: string | undefined): boolean {
@@ -164,6 +552,13 @@ function tokensMatch(received: string | null, expected: string | undefined): boo
164
552
  );
165
553
  }
166
554
 
555
+ function managedInstanceTokensMatch(received: string | undefined, expected: string): boolean {
556
+ if (received === undefined || received.length === 0 || expected.length === 0) return false;
557
+ const receivedDigest = createHash("sha256").update(received).digest();
558
+ const expectedDigest = createHash("sha256").update(expected).digest();
559
+ return timingSafeEqual(receivedDigest, expectedDigest);
560
+ }
561
+
167
562
  function firstHeader(value: string | readonly string[] | undefined): string | undefined {
168
563
  return typeof value === "string" ? value : value?.[0];
169
564
  }
@@ -179,7 +574,52 @@ function normalizedHostname(value: string): string | null {
179
574
  }
180
575
 
181
576
  function isLoopbackHostname(hostname: string): boolean {
182
- return hostname === "localhost" || hostname === "::1" || hostname.startsWith("127.");
577
+ if (hostname === "localhost" || hostname === "::1") return true;
578
+ if (isIP(hostname) !== 4) return false;
579
+ return hostname.split(".", 1)[0] === "127";
580
+ }
581
+
582
+ export type UiOriginAllowlist = {
583
+ origins: readonly string[];
584
+ rejected: readonly string[];
585
+ };
586
+
587
+ function parseUrl(value: string): URL | null {
588
+ try {
589
+ return new URL(value);
590
+ } catch {
591
+ return null;
592
+ }
593
+ }
594
+
595
+ export function parseUiOriginAllowlist(value: string | undefined): UiOriginAllowlist {
596
+ const origins: string[] = [];
597
+ const rejected: string[] = [];
598
+ for (const entry of (value ?? "").split(",")) {
599
+ const candidate = entry.trim();
600
+ if (candidate.length === 0) continue;
601
+ const parsed = parseUrl(candidate);
602
+ const valid =
603
+ parsed !== null &&
604
+ (parsed.protocol === "http:" || parsed.protocol === "https:") &&
605
+ parsed.username.length === 0 &&
606
+ parsed.password.length === 0 &&
607
+ parsed.pathname === "/" &&
608
+ parsed.search.length === 0 &&
609
+ parsed.hash.length === 0 &&
610
+ isLoopbackHostname(parsed.hostname.toLowerCase().replace(/^\[|\]$/gu, ""));
611
+ if (!valid || parsed === null) {
612
+ rejected.push(candidate);
613
+ continue;
614
+ }
615
+ if (!origins.includes(parsed.origin)) origins.push(parsed.origin);
616
+ }
617
+ return { origins, rejected };
618
+ }
619
+
620
+ function normalizeTrustedPublicOrigin(value: string | undefined): string | null | undefined {
621
+ if (value === undefined) return undefined;
622
+ return normalizedRequestOrigin(value.trim());
183
623
  }
184
624
 
185
625
  export function isTrustedIdentityProxyHost(
@@ -203,9 +643,117 @@ function isSafeMethod(method: string | undefined): boolean {
203
643
  return normalized === "GET" || normalized === "HEAD" || normalized === "OPTIONS";
204
644
  }
205
645
 
646
+ const UI_CORS_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] as const;
647
+ const UI_CORS_HEADERS = ["authorization", "content-type", "last-event-id", CSRF_HEADER] as const;
648
+
649
+ function normalizedRequestOrigin(origin: string): string | null {
650
+ try {
651
+ const parsed = new URL(origin);
652
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
653
+ if (parsed.username.length > 0 || parsed.password.length > 0) return null;
654
+ if (parsed.pathname !== "/" || parsed.search.length > 0 || parsed.hash.length > 0) return null;
655
+ return parsed.origin;
656
+ } catch {
657
+ return null;
658
+ }
659
+ }
660
+
661
+ function isSameRequestOrigin(
662
+ origin: string,
663
+ host: string | undefined,
664
+ publicOrigin?: string,
665
+ ): boolean {
666
+ const parsedOrigin = normalizedRequestOrigin(origin);
667
+ if (parsedOrigin === null) return false;
668
+ const expected = normalizedRequestOrigin(publicOrigin ?? `http://${host ?? "localhost"}`);
669
+ return expected !== null && parsedOrigin === expected;
670
+ }
671
+
672
+ function matchedUiOrigin(
673
+ origin: string | undefined,
674
+ allowedUiOrigins: readonly string[] | undefined,
675
+ ): string | null {
676
+ if (origin === undefined) return null;
677
+ return (allowedUiOrigins ?? []).find((candidate) => candidate === origin) ?? null;
678
+ }
679
+
680
+ function isUiControlPath(pathname: string): boolean {
681
+ if (pathname === "/livez" || pathname === "/readyz") return true;
682
+ if (pathname === "/api/mcp") return false;
683
+ return pathname === "/api" || pathname.startsWith("/api/");
684
+ }
685
+
686
+ function isModelProxyPath(pathname: string): boolean {
687
+ return pathname === "/proxy" || pathname.startsWith("/proxy/");
688
+ }
689
+
690
+ function uiCorsHeaders(origin: string | null): Record<string, string> {
691
+ if (origin === null) return {};
692
+ return {
693
+ "access-control-allow-origin": origin,
694
+ "access-control-expose-headers": "Content-Disposition",
695
+ vary: "Origin",
696
+ };
697
+ }
698
+
699
+ function appendVaryHeader(existing: string | string[] | undefined, name: string): string {
700
+ const values = (Array.isArray(existing) ? existing.join(",") : (existing ?? ""))
701
+ .split(",")
702
+ .map((value) => value.trim())
703
+ .filter((value) => value.length > 0);
704
+ if (!values.some((value) => value.toLowerCase() === name.toLowerCase())) values.push(name);
705
+ return values.join(", ");
706
+ }
707
+
708
+ function requestedCorsHeaders(value: string | undefined): readonly string[] {
709
+ return (value ?? "")
710
+ .split(",")
711
+ .map((header) => header.trim().toLowerCase())
712
+ .filter((header) => header.length > 0);
713
+ }
714
+
715
+ function handleUiCorsPreflight(
716
+ req: http.IncomingMessage,
717
+ res: http.ServerResponse,
718
+ origin: string,
719
+ ): void {
720
+ const requestedMethod = firstHeader(req.headers["access-control-request-method"])?.toUpperCase();
721
+ const requestedHeaders = requestedCorsHeaders(
722
+ firstHeader(req.headers["access-control-request-headers"]),
723
+ );
724
+ const methodAllowed =
725
+ requestedMethod !== undefined && UI_CORS_METHODS.some((method) => method === requestedMethod);
726
+ const headersAllowed = requestedHeaders.every((header) =>
727
+ UI_CORS_HEADERS.some((allowedHeader) => allowedHeader === header),
728
+ );
729
+ if (!methodAllowed || !headersAllowed) {
730
+ res.writeHead(403, {
731
+ "content-type": "application/json; charset=utf-8",
732
+ "cache-control": "no-store",
733
+ ...uiCorsHeaders(origin),
734
+ });
735
+ res.end(JSON.stringify({ error: "CORS preflight is not allowed for this request." }));
736
+ req.resume();
737
+ return;
738
+ }
739
+ res.writeHead(204, {
740
+ "cache-control": "no-store",
741
+ "access-control-allow-origin": origin,
742
+ "access-control-allow-methods": UI_CORS_METHODS.join(", "),
743
+ "access-control-allow-headers": UI_CORS_HEADERS.join(", "),
744
+ "access-control-max-age": "600",
745
+ vary: "Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
746
+ });
747
+ res.end();
748
+ req.resume();
749
+ }
750
+
206
751
  function isPublicHealthPath(pathname: string): boolean {
207
- const path = stripBasePath(pathname, getConfiguredBasePath(process.env));
208
- return path === "/livez" || path === "/readyz" || path === "/api/health";
752
+ return pathname === "/livez" || pathname === "/readyz" || pathname === "/api/health";
753
+ }
754
+
755
+ function isInstanceManagementPath(pathname: string): boolean {
756
+ return pathname === "/api/instances" || pathname.startsWith("/api/instances/");
209
757
  }
210
758
 
211
759
  function configuredPositiveInteger(value: number | undefined, fallback: number): number {
@@ -221,28 +769,41 @@ function declaredContentLength(value: string | undefined): number | null {
221
769
 
222
770
  export function isBrowserMutationAllowed(input: {
223
771
  path: string;
772
+ basePath?: string;
224
773
  method?: string;
225
774
  host?: string;
226
775
  origin?: string;
227
776
  secFetchSite?: string;
228
777
  csrfToken?: string;
778
+ allowedUiOrigins?: readonly string[];
779
+ publicOrigin?: string;
229
780
  }): boolean {
230
781
  if (isSafeMethod(input.method)) return true;
231
782
  const pathname = stripBasePath(
232
783
  new URL(input.path, "http://localhost").pathname,
233
- getConfiguredBasePath(process.env),
784
+ input.basePath ?? getConfiguredBasePath(process.env),
234
785
  );
235
- if (pathname === "/proxy" || pathname.startsWith("/proxy/")) return true;
236
-
237
786
  const fetchSite = input.secFetchSite?.toLowerCase();
238
787
  const browserRequest = input.origin !== undefined || fetchSite !== undefined;
239
788
  if (!browserRequest) return true;
240
- if (fetchSite !== undefined && fetchSite !== "same-origin") return false;
789
+ if (isModelProxyPath(pathname)) {
790
+ if (fetchSite !== undefined && fetchSite !== "same-origin") return false;
791
+ if (
792
+ input.origin !== undefined &&
793
+ !isSameRequestOrigin(input.origin, input.host, input.publicOrigin)
794
+ ) {
795
+ return false;
796
+ }
797
+ return input.csrfToken === "1";
798
+ }
799
+ const allowedOrigin = matchedUiOrigin(input.origin, input.allowedUiOrigins);
800
+ if (fetchSite !== undefined && fetchSite !== "same-origin" && allowedOrigin === null)
801
+ return false;
241
802
  if (input.origin !== undefined) {
242
- try {
243
- const originHost = new URL(input.origin).host.toLowerCase();
244
- if (originHost !== input.host?.toLowerCase()) return false;
245
- } catch {
803
+ if (
804
+ !isSameRequestOrigin(input.origin, input.host, input.publicOrigin) &&
805
+ allowedOrigin === null
806
+ ) {
246
807
  return false;
247
808
  }
248
809
  }
@@ -251,6 +812,7 @@ export function isBrowserMutationAllowed(input: {
251
812
 
252
813
  export function isIdentityProxyRequestAllowed(input: {
253
814
  path: string;
815
+ basePath?: string;
254
816
  remoteAddress?: string;
255
817
  authorization?: string;
256
818
  proxyAuthorization?: string;
@@ -264,14 +826,17 @@ export function isIdentityProxyRequestAllowed(input: {
264
826
  | "allowUnauthenticatedRemoteProxy"
265
827
  >;
266
828
  }): boolean {
829
+ const pathname = stripBasePath(
830
+ new URL(input.path, "http://localhost").pathname,
831
+ input.basePath ?? getConfiguredBasePath(process.env),
832
+ );
833
+ if (!isLoopbackPeer(input.remoteAddress) && isInstanceManagementPath(pathname)) {
834
+ return tokensMatch(bearerToken(input.authorization), input.options.controlToken);
835
+ }
267
836
  if (input.options.restrictRemoteControl !== true) return true;
268
837
  if (input.options.allowRemoteControl === true) return true;
269
838
  if (isLoopbackPeer(input.remoteAddress)) return true;
270
839
 
271
- const pathname = stripBasePath(
272
- new URL(input.path, "http://localhost").pathname,
273
- getConfiguredBasePath(process.env),
274
- );
275
840
  if (pathname === "/proxy" || pathname.startsWith("/proxy/")) {
276
841
  if (input.options.allowUnauthenticatedRemoteProxy === true) return true;
277
842
  const proxyCredential = bearerToken(input.proxyAuthorization) ?? input.proxyTokenHeader ?? null;
@@ -281,6 +846,267 @@ export function isIdentityProxyRequestAllowed(input: {
281
846
  return tokensMatch(bearerToken(input.authorization), input.options.controlToken);
282
847
  }
283
848
 
849
+ type AliasControlBodyResult =
850
+ | { ok: true; body: string }
851
+ | { ok: false; status: number; error: string };
852
+
853
+ function readAliasControlBody(req: http.IncomingMessage): Promise<AliasControlBodyResult> {
854
+ const contentLength = declaredContentLength(req.headers["content-length"]);
855
+ if (contentLength !== null && (!Number.isFinite(contentLength) || contentLength < 0)) {
856
+ req.resume();
857
+ return Promise.resolve({ ok: false, status: 400, error: "Invalid Content-Length header" });
858
+ }
859
+ if (contentLength !== null && contentLength > ALIAS_CONTROL_MAX_REQUEST_BYTES) {
860
+ req.resume();
861
+ return Promise.resolve({
862
+ ok: false,
863
+ status: 413,
864
+ error: `Alias control body exceeds ${String(ALIAS_CONTROL_MAX_REQUEST_BYTES)} bytes`,
865
+ });
866
+ }
867
+
868
+ return new Promise((resolve) => {
869
+ const chunks: Buffer[] = [];
870
+ let receivedBytes = 0;
871
+ let settled = false;
872
+ const finish = (result: AliasControlBodyResult): void => {
873
+ if (settled) return;
874
+ settled = true;
875
+ resolve(result);
876
+ };
877
+ req.on("data", (chunk: unknown) => {
878
+ if (settled) return;
879
+ const buffer = toBuffer(chunk);
880
+ receivedBytes += buffer.byteLength;
881
+ if (receivedBytes > ALIAS_CONTROL_MAX_REQUEST_BYTES) {
882
+ finish({
883
+ ok: false,
884
+ status: 413,
885
+ error: `Alias control body exceeds ${String(ALIAS_CONTROL_MAX_REQUEST_BYTES)} bytes`,
886
+ });
887
+ return;
888
+ }
889
+ chunks.push(buffer);
890
+ });
891
+ req.on("end", () => {
892
+ finish({ ok: true, body: Buffer.concat(chunks).toString("utf8") });
893
+ });
894
+ req.on("aborted", () => {
895
+ finish({ ok: false, status: 400, error: "Client aborted alias control request" });
896
+ });
897
+ req.on("error", () => {
898
+ finish({ ok: false, status: 400, error: "Alias control request body failed" });
899
+ });
900
+ });
901
+ }
902
+
903
+ function isUnknownRecord(value: unknown): value is Record<string, unknown> {
904
+ return typeof value === "object" && value !== null && !Array.isArray(value);
905
+ }
906
+
907
+ function aliasBasePathFromJson(
908
+ body: string,
909
+ ): { ok: true; basePath: string } | { ok: false; error: string } {
910
+ let parsed: unknown;
911
+ try {
912
+ parsed = JSON.parse(body);
913
+ } catch {
914
+ return { ok: false, error: "Alias control body must be valid JSON." };
915
+ }
916
+ if (!isUnknownRecord(parsed) || typeof parsed.basePath !== "string") {
917
+ return { ok: false, error: 'Alias control body must contain a string "basePath" field.' };
918
+ }
919
+ return { ok: true, basePath: parsed.basePath };
920
+ }
921
+
922
+ function writeAliasControlResponse(
923
+ res: http.ServerResponse,
924
+ registry: BasePathAliasRegistry,
925
+ status: number,
926
+ result:
927
+ | { basePath?: string; code?: string; error?: string }
928
+ | { basePath: string }
929
+ | undefined = undefined,
930
+ extraHeaders: Readonly<Record<string, string>> = {},
931
+ ): void {
932
+ res.writeHead(status, {
933
+ "content-type": "application/json; charset=utf-8",
934
+ "cache-control": "no-store",
935
+ ...extraHeaders,
936
+ });
937
+ res.end(
938
+ JSON.stringify({
939
+ canonicalBasePath: registry.canonicalBasePath,
940
+ aliases: registry.list(),
941
+ runtimeScoped: true,
942
+ ...result,
943
+ }),
944
+ );
945
+ }
946
+
947
+ async function handleAliasControlRequest(
948
+ req: http.IncomingMessage,
949
+ res: http.ServerResponse,
950
+ registry: BasePathAliasRegistry,
951
+ ): Promise<void> {
952
+ if (!isLoopbackPeer(req.socket.remoteAddress)) {
953
+ req.resume();
954
+ writeAliasControlResponse(res, registry, 403, {
955
+ error: "Base Path aliases can only be managed from the local machine.",
956
+ });
957
+ return;
958
+ }
959
+ if (
960
+ !isBrowserMutationAllowed({
961
+ path: req.url ?? IDENTITY_PROXY_ALIAS_CONTROL_PATH,
962
+ basePath: "",
963
+ method: req.method,
964
+ host: req.headers.host,
965
+ origin: req.headers.origin,
966
+ secFetchSite: firstHeader(req.headers["sec-fetch-site"]),
967
+ csrfToken: firstHeader(req.headers[CSRF_HEADER]),
968
+ })
969
+ ) {
970
+ req.resume();
971
+ writeAliasControlResponse(res, registry, 403, {
972
+ error: "Browser mutation requires same-origin CSRF proof.",
973
+ });
974
+ return;
975
+ }
976
+
977
+ const method = (req.method ?? "GET").toUpperCase();
978
+ if (method === "GET") {
979
+ req.resume();
980
+ writeAliasControlResponse(res, registry, 200);
981
+ return;
982
+ }
983
+ if (method !== "POST" && method !== "DELETE") {
984
+ req.resume();
985
+ writeAliasControlResponse(
986
+ res,
987
+ registry,
988
+ 405,
989
+ { error: "Alias control supports GET, POST, and DELETE." },
990
+ { allow: "GET, POST, DELETE" },
991
+ );
992
+ return;
993
+ }
994
+
995
+ const bodyResult = await readAliasControlBody(req);
996
+ if (!bodyResult.ok) {
997
+ writeAliasControlResponse(res, registry, bodyResult.status, { error: bodyResult.error });
998
+ return;
999
+ }
1000
+ const input = aliasBasePathFromJson(bodyResult.body);
1001
+ if (!input.ok) {
1002
+ writeAliasControlResponse(res, registry, 400, { error: input.error });
1003
+ return;
1004
+ }
1005
+ const mutation =
1006
+ method === "POST" ? registry.add(input.basePath) : registry.remove(input.basePath);
1007
+ if (mutation.ok) {
1008
+ writeAliasControlResponse(res, registry, method === "POST" ? 201 : 200, {
1009
+ basePath: mutation.basePath,
1010
+ });
1011
+ return;
1012
+ }
1013
+ const status =
1014
+ mutation.code === "duplicate" || mutation.code === "overlap"
1015
+ ? 409
1016
+ : mutation.code === "not-found"
1017
+ ? 404
1018
+ : 400;
1019
+ writeAliasControlResponse(res, registry, status, {
1020
+ code: mutation.code,
1021
+ error: mutation.error,
1022
+ });
1023
+ }
1024
+
1025
+ function writeManagedInstanceControlResponse(
1026
+ res: http.ServerResponse,
1027
+ status: number,
1028
+ body: Readonly<Record<string, unknown>>,
1029
+ extraHeaders: Readonly<Record<string, string>> = {},
1030
+ onFinished: (() => void) | undefined = undefined,
1031
+ ): void {
1032
+ res.writeHead(status, {
1033
+ "content-type": "application/json; charset=utf-8",
1034
+ "cache-control": "no-store",
1035
+ ...extraHeaders,
1036
+ });
1037
+ res.end(JSON.stringify(body), onFinished);
1038
+ }
1039
+
1040
+ function handleManagedInstanceControlRequest(
1041
+ req: http.IncomingMessage,
1042
+ res: http.ServerResponse,
1043
+ pathname: string,
1044
+ managedInstance: ManagedInstanceControlOptions | undefined,
1045
+ ): void {
1046
+ req.resume();
1047
+ if (managedInstance === undefined) {
1048
+ writeManagedInstanceControlResponse(res, 404, { error: "Control endpoint not found" });
1049
+ return;
1050
+ }
1051
+ const authorized = managedInstanceTokensMatch(
1052
+ firstHeader(req.headers[MANAGED_INSTANCE_TOKEN_HEADER]),
1053
+ managedInstance.controlToken,
1054
+ );
1055
+ if (!authorized) {
1056
+ writeManagedInstanceControlResponse(res, 403, { error: "Instance control access denied" });
1057
+ return;
1058
+ }
1059
+
1060
+ const method = (req.method ?? "GET").toUpperCase();
1061
+ if (pathname === MANAGED_INSTANCE_CONTROL_PATH) {
1062
+ if (method !== "GET") {
1063
+ writeManagedInstanceControlResponse(
1064
+ res,
1065
+ 405,
1066
+ { error: "Instance identity supports GET only" },
1067
+ { allow: "GET" },
1068
+ );
1069
+ return;
1070
+ }
1071
+ writeManagedInstanceControlResponse(res, 200, {
1072
+ schemaVersion: 1,
1073
+ name: managedInstance.name,
1074
+ instanceId: managedInstance.instanceId,
1075
+ launchId: managedInstance.launchId,
1076
+ supervisorPid: managedInstance.supervisorPid,
1077
+ startedAt: managedInstance.startedAt,
1078
+ publicPort: managedInstance.publicPort,
1079
+ upstreamPort: managedInstance.upstreamPort,
1080
+ });
1081
+ return;
1082
+ }
1083
+ if (method !== "POST") {
1084
+ writeManagedInstanceControlResponse(
1085
+ res,
1086
+ 405,
1087
+ { error: "Instance shutdown supports POST only" },
1088
+ { allow: "POST" },
1089
+ );
1090
+ return;
1091
+ }
1092
+ writeManagedInstanceControlResponse(
1093
+ res,
1094
+ 202,
1095
+ { accepted: true, launchId: managedInstance.launchId },
1096
+ {},
1097
+ () => {
1098
+ process.nextTick(managedInstance.requestShutdown);
1099
+ },
1100
+ );
1101
+ }
1102
+
1103
+ function isIdentityProxyControlPath(pathname: string): boolean {
1104
+ return (
1105
+ pathname === IDENTITY_PROXY_CONTROL_NAMESPACE ||
1106
+ pathname.startsWith(`${IDENTITY_PROXY_CONTROL_NAMESPACE}/`)
1107
+ );
1108
+ }
1109
+
284
1110
  /**
285
1111
  * Start a tiny native Node HTTP server that:
286
1112
  * 1. Reads the client connection's source port (`req.socket.remotePort`).
@@ -305,8 +1131,31 @@ export function isIdentityProxyRequestAllowed(input: {
305
1131
  * best-effort scan inside TanStack Start.
306
1132
  */
307
1133
  export function startIdentityProxy(options: IdentityProxyOptions): Promise<http.Server> {
1134
+ const allowedUiOrigins = parseUiOriginAllowlist(options.allowedUiOrigins?.join(","));
1135
+ if (allowedUiOrigins.rejected.length > 0) {
1136
+ return Promise.reject(
1137
+ new Error(
1138
+ `Unsafe UI origins are not accepted: ${allowedUiOrigins.rejected.join(", ")}. Only exact loopback HTTP(S) origins are supported.`,
1139
+ ),
1140
+ );
1141
+ }
1142
+ const publicOrigin = normalizeTrustedPublicOrigin(options.publicOrigin);
1143
+ if (publicOrigin === null) {
1144
+ return Promise.reject(
1145
+ new Error(
1146
+ "publicOrigin must be an exact HTTP(S) origin without credentials, path, or query.",
1147
+ ),
1148
+ );
1149
+ }
1150
+ const effectiveOptions: IdentityProxyOptions = {
1151
+ ...options,
1152
+ allowedUiOrigins: allowedUiOrigins.origins,
1153
+ publicOrigin,
1154
+ };
308
1155
  return new Promise((resolve, reject) => {
309
1156
  const server = http.createServer();
1157
+ const aliasRegistry =
1158
+ options.aliasRegistry ?? createBasePathAliasRegistry(getConfiguredBasePath(process.env));
310
1159
  const runtimeState: IdentityProxyRuntimeState = {
311
1160
  acceptingRequests: true,
312
1161
  activeRequests: 0,
@@ -349,7 +1198,7 @@ export function startIdentityProxy(options: IdentityProxyOptions): Promise<http.
349
1198
  };
350
1199
  res.once("finish", release);
351
1200
  res.once("close", release);
352
- void handleRequest(req, res, options).catch((err: unknown) => {
1201
+ void handleRequest(req, res, effectiveOptions, aliasRegistry).catch((err: unknown) => {
353
1202
  const message = err instanceof Error ? err.message : String(err);
354
1203
  if (!res.headersSent) {
355
1204
  res.writeHead(502, { "content-type": "text/plain" });
@@ -400,9 +1249,8 @@ async function handleRequest(
400
1249
  req: http.IncomingMessage,
401
1250
  res: http.ServerResponse,
402
1251
  options: IdentityProxyOptions,
1252
+ aliasRegistry: BasePathAliasRegistry,
403
1253
  ): Promise<void> {
404
- const basePath = getConfiguredBasePath(process.env);
405
- const upstreamPath = stripBasePathFromRequestPath(req.url ?? "/", basePath);
406
1254
  const requestUrl = new URL(req.url ?? "/", "http://localhost");
407
1255
  if (!isTrustedIdentityProxyHost(req.headers.host, options)) {
408
1256
  res.writeHead(400, {
@@ -414,6 +1262,78 @@ async function handleRequest(
414
1262
  return;
415
1263
  }
416
1264
 
1265
+ if (requestUrl.pathname === IDENTITY_PROXY_ALIAS_CONTROL_PATH) {
1266
+ await handleAliasControlRequest(req, res, aliasRegistry);
1267
+ return;
1268
+ }
1269
+
1270
+ if (
1271
+ requestUrl.pathname === MANAGED_INSTANCE_CONTROL_PATH ||
1272
+ requestUrl.pathname === MANAGED_INSTANCE_SHUTDOWN_PATH
1273
+ ) {
1274
+ handleManagedInstanceControlRequest(req, res, requestUrl.pathname, options.managedInstance);
1275
+ return;
1276
+ }
1277
+
1278
+ if (isIdentityProxyControlPath(requestUrl.pathname)) {
1279
+ req.resume();
1280
+ writeManagedInstanceControlResponse(res, 404, { error: "Control endpoint not found" });
1281
+ return;
1282
+ }
1283
+
1284
+ const resolution = resolveBasePathAliasRequest(aliasRegistry, req.url ?? "/");
1285
+ const basePath = resolution.effectiveBasePath;
1286
+ const upstreamPath = resolution.upstreamPath;
1287
+ const upstreamPathname = new URL(upstreamPath, "http://localhost").pathname;
1288
+ const requestOrigin = req.headers.origin;
1289
+ const requestPublicOrigin = publicOriginForRequest(req, options);
1290
+ const crossOriginRequest =
1291
+ requestOrigin !== undefined &&
1292
+ !isSameRequestOrigin(requestOrigin, req.headers.host, requestPublicOrigin);
1293
+ const allowedUiOrigin = crossOriginRequest
1294
+ ? matchedUiOrigin(requestOrigin, options.allowedUiOrigins)
1295
+ : null;
1296
+
1297
+ if (crossOriginRequest && isModelProxyPath(upstreamPathname)) {
1298
+ res.writeHead(403, {
1299
+ "content-type": "application/json; charset=utf-8",
1300
+ "cache-control": "no-store",
1301
+ });
1302
+ res.end(
1303
+ JSON.stringify({ error: "Cross-origin browser access to the model proxy is forbidden." }),
1304
+ );
1305
+ req.resume();
1306
+ return;
1307
+ }
1308
+
1309
+ if (crossOriginRequest && upstreamPathname === "/api/mcp") {
1310
+ res.writeHead(403, {
1311
+ "content-type": "application/json; charset=utf-8",
1312
+ "cache-control": "no-store",
1313
+ });
1314
+ res.end(JSON.stringify({ error: "Browser CORS is not enabled for the MCP endpoint." }));
1315
+ req.resume();
1316
+ return;
1317
+ }
1318
+
1319
+ if (crossOriginRequest && isUiControlPath(upstreamPathname)) {
1320
+ if (allowedUiOrigin === null) {
1321
+ res.writeHead(403, {
1322
+ "content-type": "application/json; charset=utf-8",
1323
+ "cache-control": "no-store",
1324
+ });
1325
+ res.end(
1326
+ JSON.stringify({ error: "UI origin is not allowed by this Agent Inspector runtime." }),
1327
+ );
1328
+ req.resume();
1329
+ return;
1330
+ }
1331
+ if ((req.method ?? "GET").toUpperCase() === "OPTIONS") {
1332
+ handleUiCorsPreflight(req, res, allowedUiOrigin);
1333
+ return;
1334
+ }
1335
+ }
1336
+
417
1337
  if (basePath.length > 0 && requestUrl.pathname.startsWith("/assets/")) {
418
1338
  res.writeHead(307, {
419
1339
  location: `${basePath}${requestUrl.pathname}${requestUrl.search}`,
@@ -427,16 +1347,20 @@ async function handleRequest(
427
1347
  if (
428
1348
  !isBrowserMutationAllowed({
429
1349
  path: req.url ?? "/",
1350
+ basePath,
430
1351
  method: req.method,
431
1352
  host: req.headers.host,
432
1353
  origin: req.headers.origin,
433
1354
  secFetchSite: firstHeader(req.headers["sec-fetch-site"]),
434
1355
  csrfToken: firstHeader(req.headers[CSRF_HEADER]),
1356
+ allowedUiOrigins: options.allowedUiOrigins,
1357
+ publicOrigin: requestPublicOrigin,
435
1358
  })
436
1359
  ) {
437
1360
  res.writeHead(403, {
438
1361
  "content-type": "application/json; charset=utf-8",
439
1362
  "cache-control": "no-store",
1363
+ ...uiCorsHeaders(allowedUiOrigin),
440
1364
  });
441
1365
  res.end(JSON.stringify({ error: "Browser mutation requires same-origin CSRF proof" }));
442
1366
  req.resume();
@@ -446,6 +1370,7 @@ async function handleRequest(
446
1370
  if (
447
1371
  !isIdentityProxyRequestAllowed({
448
1372
  path: req.url ?? "/",
1373
+ basePath,
449
1374
  remoteAddress: req.socket.remoteAddress,
450
1375
  authorization: req.headers.authorization,
451
1376
  proxyAuthorization: req.headers["proxy-authorization"],
@@ -456,6 +1381,7 @@ async function handleRequest(
456
1381
  res.writeHead(403, {
457
1382
  "content-type": "application/json; charset=utf-8",
458
1383
  "cache-control": "no-store",
1384
+ ...uiCorsHeaders(allowedUiOrigin),
459
1385
  });
460
1386
  res.end(
461
1387
  JSON.stringify({
@@ -466,6 +1392,19 @@ async function handleRequest(
466
1392
  return;
467
1393
  }
468
1394
 
1395
+ if (
1396
+ await tryServeUiAsset(
1397
+ req,
1398
+ res,
1399
+ options,
1400
+ upstreamPathname,
1401
+ basePath,
1402
+ aliasRegistry.canonicalBasePath,
1403
+ )
1404
+ ) {
1405
+ return;
1406
+ }
1407
+
469
1408
  // The connection-time lookup already populated the per-port cache; this
470
1409
  // call now hits the cache and returns the result synchronously.
471
1410
  const remotePort = req.socket.remotePort;
@@ -489,6 +1428,7 @@ async function handleRequest(
489
1428
  res.writeHead(400, {
490
1429
  "content-type": "application/json; charset=utf-8",
491
1430
  "cache-control": "no-store",
1431
+ ...uiCorsHeaders(allowedUiOrigin),
492
1432
  });
493
1433
  res.end(JSON.stringify({ error: "Invalid Content-Length header" }));
494
1434
  req.resume();
@@ -498,6 +1438,7 @@ async function handleRequest(
498
1438
  res.writeHead(413, {
499
1439
  "content-type": "application/json; charset=utf-8",
500
1440
  "cache-control": "no-store",
1441
+ ...uiCorsHeaders(allowedUiOrigin),
501
1442
  });
502
1443
  res.end(JSON.stringify({ error: `Request body exceeds ${String(maxRequestBytes)} bytes` }));
503
1444
  req.resume();
@@ -508,11 +1449,22 @@ async function handleRequest(
508
1449
  for (const [name, value] of Object.entries(req.headers)) {
509
1450
  const lowerName = name.toLowerCase();
510
1451
  if (REQUEST_HOP_BY_HOP.has(lowerName)) continue;
511
- if (basePath.length > 0 && lowerName === "accept-encoding") continue;
1452
+ if (
1453
+ basePath.length > 0 &&
1454
+ (lowerName === "accept-encoding" || lowerName === "range" || lowerName === "if-range")
1455
+ )
1456
+ continue;
512
1457
  if (lowerName.startsWith(IDENTITY_HEADER_PREFIX)) continue;
1458
+ if (lowerName === "authorization" && !isModelProxyPath(upstreamPathname)) continue;
513
1459
  if (value === undefined) continue;
514
1460
  upstreamHeaders[name] = value;
515
1461
  }
1462
+ upstreamHeaders[AGENT_INSPECTOR_PUBLIC_BASE_PATH_HEADER] = basePath.length === 0 ? "/" : basePath;
1463
+ upstreamHeaders[AGENT_INSPECTOR_PUBLIC_ORIGIN_HEADER] = requestPublicOrigin;
1464
+ const publicPort = req.socket.localPort;
1465
+ if (publicPort !== undefined && publicPort > 0 && publicPort <= 65_535) {
1466
+ upstreamHeaders[AGENT_INSPECTOR_PUBLIC_PORT_HEADER] = String(publicPort);
1467
+ }
516
1468
  if (identity !== null && identity.pid !== null) {
517
1469
  upstreamHeaders[IDENTITY_HEADERS.pid] = String(identity.pid);
518
1470
  if (identity.port !== null) {
@@ -540,10 +1492,25 @@ async function handleRequest(
540
1492
  headers: upstreamHeaders,
541
1493
  },
542
1494
  (proxyRes) => {
543
- if (terminal) {
544
- proxyRes.resume();
1495
+ if (terminal || res.destroyed) {
1496
+ proxyRes.destroy();
545
1497
  return;
546
1498
  }
1499
+ const destroyUpstreamResponse = (): void => {
1500
+ proxyRes.destroy();
1501
+ proxyRes.socket.destroy();
1502
+ proxyReq.destroy();
1503
+ };
1504
+ const removeDownstreamListeners = (): void => {
1505
+ res.removeListener("close", destroyUpstreamResponse);
1506
+ res.removeListener("error", destroyUpstreamResponse);
1507
+ req.socket.removeListener("close", destroyUpstreamResponse);
1508
+ };
1509
+ res.once("close", destroyUpstreamResponse);
1510
+ res.once("error", destroyUpstreamResponse);
1511
+ req.socket.once("close", destroyUpstreamResponse);
1512
+ proxyRes.once("end", removeDownstreamListeners);
1513
+ proxyRes.once("close", removeDownstreamListeners);
547
1514
  const outHeaders: Record<string, string | string[]> = {};
548
1515
  const contentType = firstHeader(proxyRes.headers["content-type"]);
549
1516
  const shouldRewriteHtml =
@@ -556,17 +1523,48 @@ async function handleRequest(
556
1523
  for (const [name, value] of Object.entries(proxyRes.headers)) {
557
1524
  const lowerName = name.toLowerCase();
558
1525
  if (RESPONSE_HOP_BY_HOP.has(lowerName)) continue;
559
- if (shouldRewriteBody && lowerName === "content-length") continue;
1526
+ if (
1527
+ shouldRewriteBody &&
1528
+ (lowerName === "content-length" ||
1529
+ lowerName === "content-range" ||
1530
+ lowerName === "accept-ranges" ||
1531
+ lowerName === "etag" ||
1532
+ lowerName === "last-modified" ||
1533
+ lowerName === "content-md5" ||
1534
+ lowerName === "digest")
1535
+ )
1536
+ continue;
560
1537
  if (value === undefined) continue;
561
- outHeaders[name] = value;
1538
+ if (
1539
+ lowerName === "location" &&
1540
+ basePath.length > 0 &&
1541
+ typeof value === "string" &&
1542
+ value.startsWith("/") &&
1543
+ !value.startsWith("//")
1544
+ ) {
1545
+ outHeaders[name] = rewriteRootRelativePublicPath(
1546
+ value,
1547
+ basePath,
1548
+ aliasRegistry.canonicalBasePath,
1549
+ );
1550
+ } else {
1551
+ outHeaders[name] = value;
1552
+ }
562
1553
  }
563
1554
  for (const [name, value] of Object.entries(BASE_SECURITY_HEADERS)) {
564
1555
  outHeaders[name] = value;
565
1556
  }
1557
+ if (allowedUiOrigin !== null && isUiControlPath(upstreamPathname)) {
1558
+ outHeaders["access-control-allow-origin"] = allowedUiOrigin;
1559
+ outHeaders["access-control-expose-headers"] = "Content-Disposition";
1560
+ outHeaders["vary"] = appendVaryHeader(outHeaders["vary"], "Origin");
1561
+ }
566
1562
  if (contentType?.toLowerCase().includes("text/html") === true) {
567
1563
  outHeaders["content-security-policy"] = DOCUMENT_CONTENT_SECURITY_POLICY;
568
1564
  }
569
- res.writeHead(proxyRes.statusCode ?? 502, outHeaders);
1565
+ const responseStatus =
1566
+ shouldRewriteBody && proxyRes.statusCode === 206 ? 200 : (proxyRes.statusCode ?? 502);
1567
+ res.writeHead(responseStatus, outHeaders);
570
1568
  if (shouldRewriteBody) {
571
1569
  const chunks: Buffer[] = [];
572
1570
  proxyRes.on("data", (chunk: unknown) => {
@@ -574,11 +1572,12 @@ async function handleRequest(
574
1572
  });
575
1573
  proxyRes.on("end", () => {
576
1574
  const body = Buffer.concat(chunks).toString("utf8");
577
- res.end(
1575
+ res.write(
578
1576
  shouldRewriteHtml
579
- ? rewriteHtmlForBasePath(body, basePath)
580
- : rewriteJavaScriptForBasePath(body, basePath),
1577
+ ? rewriteHtmlForBasePath(body, basePath, aliasRegistry.canonicalBasePath)
1578
+ : rewriteJavaScriptForBasePath(body, basePath, aliasRegistry.canonicalBasePath),
581
1579
  );
1580
+ res.end();
582
1581
  });
583
1582
  proxyRes.on("error", () => {
584
1583
  if (!res.writableEnded) res.destroy();
@@ -626,7 +1625,7 @@ async function handleRequest(
626
1625
  proxyReq.on("drain", () => req.resume());
627
1626
  res.once("close", () => {
628
1627
  clearDeadline();
629
- if (!res.writableEnded) proxyReq.destroy();
1628
+ proxyReq.destroy();
630
1629
  });
631
1630
 
632
1631
  let receivedBytes = 0;