@agenthippo-ai/agenthippo 1.110.11 → 1.110.20

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 (534) hide show
  1. package/out/agentpack/types.js +2 -5
  2. package/out/analytics/dashboard/artifactManager.js +2 -439
  3. package/out/analytics/dashboard/fleetDisplayUtils.js +2 -162
  4. package/out/analytics/dashboard/fleetOrchestrator.js +3 -142
  5. package/out/analytics/dashboard/fleetRegistry.js +2 -79
  6. package/out/analytics/dashboard/fleetRowDefaults.js +2 -87
  7. package/out/analytics/dashboard/fleetScriptAdapter.js +2 -146
  8. package/out/analytics/dashboard/fleetTypes.js +2 -29
  9. package/out/analytics/dashboard/index.js +2 -17
  10. package/out/analytics/dashboard/indexingService.js +2 -422
  11. package/out/analytics/dashboard/localFleetAdapter.js +2 -302
  12. package/out/analytics/dashboard/performanceMonitor.js +5 -76
  13. package/out/analytics/dashboard/processManager.js +2 -209
  14. package/out/analytics/dashboard/provider.js +38 -3817
  15. package/out/analytics/dashboard/queryRunner.js +62 -750
  16. package/out/analytics/dashboard/scheduledFleetStorage.js +2 -241
  17. package/out/analytics/dashboard/sessionJsonlFleet.js +3 -204
  18. package/out/analytics/dashboard/teamViewDiscovery.js +2 -106
  19. package/out/analytics/dashboard/webview/panel.js +34 -2318
  20. package/out/analytics/dashboard/webview/runtime-bridge.js +2 -82
  21. package/out/analytics/lifecycle.js +2 -43
  22. package/out/analytics/service.js +8 -1283
  23. package/out/auth/authBaseUrl.js +2 -59
  24. package/out/auth/bootstrapApply.js +2 -71
  25. package/out/auth/brokerClient.js +2 -160
  26. package/out/auth/callbackUri.js +2 -59
  27. package/out/auth/githubStoreGate.js +2 -70
  28. package/out/auth/loopbackAuthServer.js +4 -159
  29. package/out/auth/onboardingPrompt.js +2 -112
  30. package/out/auth/pkce.js +2 -9
  31. package/out/auth/provider.js +2 -226
  32. package/out/auth/sessionStore.js +2 -156
  33. package/out/auth/signInFlow.js +2 -183
  34. package/out/cli/index.js +618 -564
  35. package/out/cli/run-custom-engine-runner.js +3 -318
  36. package/out/cli/vscode-shim.js +2 -86
  37. package/out/engines/factory.js +2 -45
  38. package/out/engines/types.js +2 -17
  39. package/out/examples/run-headless-repl.js +6 -286
  40. package/out/examples/run-headless.js +2 -187
  41. package/out/extension.js +32 -4473
  42. package/out/gateway/gatewayRouteIndex.js +6 -371
  43. package/out/gateway/gatewaySessionStore.js +3 -243
  44. package/out/gateway/index.js +2 -31
  45. package/out/gateway/resetMappings.js +2 -52
  46. package/out/gateway/service.js +5 -1249
  47. package/out/mcp/publish.js +2 -143
  48. package/out/remote/handleRemoteChat.js +2 -169
  49. package/out/remote/remotePackStatusBar.js +3 -103
  50. package/out/remote/remoteTrust.js +6 -277
  51. package/out/remote/serveClient.js +4 -277
  52. package/out/remoteSkills.js +3 -191
  53. package/out/runtime/index.js +2 -32
  54. package/out/runtime/vscode/index.js +2 -20
  55. package/out/runtime/vscode/vscode-config-provider.js +2 -77
  56. package/out/runtime/vscode/vscode-edit-tracker.js +2 -122
  57. package/out/runtime/vscode/vscode-logger.js +2 -30
  58. package/out/runtime/vscode/vscode-permission-handler.js +2 -72
  59. package/out/scripts/agentide-litellm.sh +0 -1
  60. package/out/scripts/agentide_litellm_hooks_v1.py +127 -459
  61. package/out/scripts/analytics/agent-fleet-widget.js +18 -257
  62. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +0 -4
  63. package/out/scripts/analytics/duckdbLocator.js +2 -28
  64. package/out/scripts/analytics/generate-default-charts.py +43 -323
  65. package/out/scripts/analytics/hotspot-analysis-widget.js +4 -430
  66. package/out/scripts/analytics/index-traces.py +78 -1036
  67. package/out/scripts/analytics/session-comparison-widget.js +26 -472
  68. package/out/scripts/analytics/session-timeline-widget.js +25 -846
  69. package/out/scripts/analytics/token-cost-flamegraph-widget.js +4 -461
  70. package/out/scripts/analytics/token-cost-trend-widget.js +4 -534
  71. package/out/scripts/analytics/workflows-collector.cjs +4 -314
  72. package/out/scripts/analytics/workflows-daemon.mjs +9 -185
  73. package/out/scripts/analytics/workflows-widget.cjs +15 -159
  74. package/out/scripts/analytics/wow-model-widget.js +10 -120
  75. package/out/scripts/vendor-tools.mjs +2 -233
  76. package/out/skills/publish.js +3 -178
  77. package/out/store/github/setup.js +7 -212
  78. package/out/store/oramaIndex.js +2 -884
  79. package/out/sync.js +26 -1829
  80. package/out/terminalLogger.js +5 -358
  81. package/out/utils/pathSafety.js +2 -60
  82. package/out/utils/pythonRunner.js +4 -258
  83. package/out/utils/toolPath.js +2 -83
  84. package/out/workspaceFolders.js +2 -59
  85. package/package.json +10 -6
  86. package/out/agentpack/github.js +0 -547
  87. package/out/agentpack/index.js +0 -69
  88. package/out/agentpack/install.js +0 -297
  89. package/out/agentpack/manager.js +0 -441
  90. package/out/agentpack/manifest.js +0 -135
  91. package/out/agentpack/paths.js +0 -521
  92. package/out/agentpack/publish.js +0 -248
  93. package/out/agentpack/stores.js +0 -554
  94. package/out/analytics/dashboard/fleetStatusUtils.js +0 -30
  95. package/out/analytics/dashboard/fleetTitleUtils.js +0 -68
  96. package/out/analytics/dashboard/refreshPolicy.js +0 -16
  97. package/out/analytics/dashboard/scheduledFleetFingerprint.js +0 -22
  98. package/out/analytics/dashboard/scheduledFleetUtils.js +0 -22
  99. package/out/analytics/dashboard/sessionEventFleetStats.js +0 -118
  100. package/out/analytics/dashboard/viewsConfigMerge.js +0 -221
  101. package/out/analytics/dashboard/webview/codicon.ttf +0 -0
  102. package/out/analytics/identity.js +0 -0
  103. package/out/analytics/sessionEvents.js +0 -324
  104. package/out/auth/callbackUriParse.js +0 -59
  105. package/out/auth/types.js +0 -12
  106. package/out/auth-broker/config.js +0 -112
  107. package/out/auth-broker/jwt.js +0 -105
  108. package/out/auth-broker/pkce.js +0 -59
  109. package/out/auth-broker/server.js +0 -681
  110. package/out/auth-broker/store.js +0 -275
  111. package/out/authority/warrant.js +0 -206
  112. package/out/base-prompts/mcp-servers/.mcp.json +0 -13
  113. package/out/base-prompts/mcp-servers/langfuse-analytics/README.md +0 -142
  114. package/out/base-prompts/mcp-servers/langfuse-analytics/mcp.json +0 -13
  115. package/out/base-prompts/mcp-servers/langfuse-analytics/package-lock.json +0 -1192
  116. package/out/base-prompts/mcp-servers/langfuse-analytics/package.json +0 -23
  117. package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/run-with-env.sh +0 -12
  118. package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/sdk-test.js +0 -102
  119. package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/simple-test.js +0 -137
  120. package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/test-client.js +0 -118
  121. package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/test-server.js +0 -209
  122. package/out/base-prompts/mcp-servers/langfuse-analytics/src/client.ts +0 -431
  123. package/out/base-prompts/mcp-servers/langfuse-analytics/src/index.ts +0 -270
  124. package/out/base-prompts/mcp-servers/langfuse-analytics/tsconfig.json +0 -18
  125. package/out/base-prompts/rules/AGENTS.md +0 -204
  126. package/out/base-prompts/skills/agent-browser/SKILL.md +0 -102
  127. package/out/base-prompts/skills/agent-builder/SKILL.md +0 -73
  128. package/out/base-prompts/skills/agent-builder/references/agent-pack.md +0 -63
  129. package/out/base-prompts/skills/agent-builder/references/crewai.md +0 -66
  130. package/out/base-prompts/skills/agent-builder/references/langchain.md +0 -60
  131. package/out/base-prompts/skills/agent-builder/references/raw-sdk.md +0 -73
  132. package/out/base-prompts/skills/agent-engine-creator/SKILL.md +0 -586
  133. package/out/base-prompts/skills/agent-engine-installer/SKILL.md +0 -106
  134. package/out/base-prompts/skills/agent-engine-installer/scripts/install-engine.mjs +0 -259
  135. package/out/base-prompts/skills/agent-engine-installer/scripts/install-engine.ps1 +0 -189
  136. package/out/base-prompts/skills/agenthippo-cli/SKILL.md +0 -69
  137. package/out/base-prompts/skills/agenthippo-cli/scripts/install.sh +0 -63
  138. package/out/base-prompts/skills/agentpack-creator/SKILL.md +0 -375
  139. package/out/base-prompts/skills/agentpack-creator/references/agent-templates.md +0 -261
  140. package/out/base-prompts/skills/agentpack-creator/references/agents-md-guide.md +0 -195
  141. package/out/base-prompts/skills/agentpack-creator/scripts/init_pack.py +0 -391
  142. package/out/base-prompts/skills/agentpack-creator/scripts/init_pack_test.py +0 -294
  143. package/out/base-prompts/skills/agentpack-testbench/SKILL.md +0 -135
  144. package/out/base-prompts/skills/algorithmic-art/LICENSE.txt +0 -202
  145. package/out/base-prompts/skills/algorithmic-art/SKILL.md +0 -405
  146. package/out/base-prompts/skills/algorithmic-art/templates/generator_template.js +0 -223
  147. package/out/base-prompts/skills/algorithmic-art/templates/viewer.html +0 -599
  148. package/out/base-prompts/skills/debug-agent/SKILL.md +0 -83
  149. package/out/base-prompts/skills/desktop-automation/SKILL.md +0 -166
  150. package/out/base-prompts/skills/desktop-automation/references/agenthippo-ui-recipes.md +0 -441
  151. package/out/base-prompts/skills/desktop-automation/scripts/windows-desktop-uia.psm1 +0 -280
  152. package/out/base-prompts/skills/docx/LICENSE.txt +0 -30
  153. package/out/base-prompts/skills/docx/SKILL.md +0 -197
  154. package/out/base-prompts/skills/docx/docx-js.md +0 -350
  155. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +0 -1499
  156. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +0 -146
  157. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +0 -1085
  158. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +0 -11
  159. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +0 -3081
  160. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +0 -23
  161. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +0 -185
  162. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +0 -287
  163. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +0 -1676
  164. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +0 -28
  165. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +0 -144
  166. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +0 -174
  167. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +0 -25
  168. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +0 -18
  169. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +0 -59
  170. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +0 -56
  171. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +0 -195
  172. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +0 -582
  173. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +0 -25
  174. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +0 -4439
  175. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +0 -570
  176. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +0 -509
  177. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +0 -12
  178. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +0 -108
  179. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +0 -96
  180. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +0 -3646
  181. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +0 -116
  182. package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +0 -42
  183. package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +0 -50
  184. package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +0 -49
  185. package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +0 -33
  186. package/out/base-prompts/skills/docx/ooxml/schemas/mce/mc.xsd +0 -75
  187. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-2010.xsd +0 -560
  188. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-2012.xsd +0 -67
  189. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-2018.xsd +0 -14
  190. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-cex-2018.xsd +0 -20
  191. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-cid-2016.xsd +0 -13
  192. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +0 -4
  193. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-symex-2015.xsd +0 -8
  194. package/out/base-prompts/skills/docx/ooxml/scripts/pack.py +0 -159
  195. package/out/base-prompts/skills/docx/ooxml/scripts/unpack.py +0 -29
  196. package/out/base-prompts/skills/docx/ooxml/scripts/validate.py +0 -69
  197. package/out/base-prompts/skills/docx/ooxml/scripts/validation/__init__.py +0 -15
  198. package/out/base-prompts/skills/docx/ooxml/scripts/validation/base.py +0 -951
  199. package/out/base-prompts/skills/docx/ooxml/scripts/validation/docx.py +0 -274
  200. package/out/base-prompts/skills/docx/ooxml/scripts/validation/pptx.py +0 -315
  201. package/out/base-prompts/skills/docx/ooxml/scripts/validation/redlining.py +0 -279
  202. package/out/base-prompts/skills/docx/ooxml.md +0 -610
  203. package/out/base-prompts/skills/docx/scripts/__init__.py +0 -1
  204. package/out/base-prompts/skills/docx/scripts/document.py +0 -1276
  205. package/out/base-prompts/skills/docx/scripts/templates/comments.xml +0 -3
  206. package/out/base-prompts/skills/docx/scripts/templates/commentsExtended.xml +0 -3
  207. package/out/base-prompts/skills/docx/scripts/templates/commentsExtensible.xml +0 -3
  208. package/out/base-prompts/skills/docx/scripts/templates/commentsIds.xml +0 -3
  209. package/out/base-prompts/skills/docx/scripts/templates/people.xml +0 -3
  210. package/out/base-prompts/skills/docx/scripts/utilities.py +0 -374
  211. package/out/base-prompts/skills/gateway-outbound/SKILL.md +0 -136
  212. package/out/base-prompts/skills/mcp-builder/LICENSE.txt +0 -202
  213. package/out/base-prompts/skills/mcp-builder/SKILL.md +0 -328
  214. package/out/base-prompts/skills/mcp-builder/reference/evaluation.md +0 -602
  215. package/out/base-prompts/skills/mcp-builder/reference/mcp_best_practices.md +0 -249
  216. package/out/base-prompts/skills/mcp-builder/reference/node_mcp_server.md +0 -970
  217. package/out/base-prompts/skills/mcp-builder/reference/python_mcp_server.md +0 -719
  218. package/out/base-prompts/skills/mcp-builder/scripts/connections.py +0 -151
  219. package/out/base-prompts/skills/mcp-builder/scripts/evaluation.py +0 -373
  220. package/out/base-prompts/skills/mcp-builder/scripts/example_evaluation.xml +0 -22
  221. package/out/base-prompts/skills/mcp-builder/scripts/requirements.txt +0 -2
  222. package/out/base-prompts/skills/openclaw/SKILL.md +0 -406
  223. package/out/base-prompts/skills/openclaw/scripts/run-smoke.sh +0 -61
  224. package/out/base-prompts/skills/pdf/LICENSE.txt +0 -30
  225. package/out/base-prompts/skills/pdf/SKILL.md +0 -294
  226. package/out/base-prompts/skills/pdf/forms.md +0 -205
  227. package/out/base-prompts/skills/pdf/reference.md +0 -612
  228. package/out/base-prompts/skills/pdf/scripts/check_bounding_boxes.py +0 -70
  229. package/out/base-prompts/skills/pdf/scripts/check_bounding_boxes_test.py +0 -226
  230. package/out/base-prompts/skills/pdf/scripts/check_fillable_fields.py +0 -12
  231. package/out/base-prompts/skills/pdf/scripts/convert_pdf_to_images.py +0 -35
  232. package/out/base-prompts/skills/pdf/scripts/create_validation_image.py +0 -41
  233. package/out/base-prompts/skills/pdf/scripts/extract_form_field_info.py +0 -152
  234. package/out/base-prompts/skills/pdf/scripts/fill_fillable_fields.py +0 -114
  235. package/out/base-prompts/skills/pdf/scripts/fill_pdf_form_with_annotations.py +0 -108
  236. package/out/base-prompts/skills/pptx/LICENSE.txt +0 -30
  237. package/out/base-prompts/skills/pptx/SKILL.md +0 -484
  238. package/out/base-prompts/skills/pptx/html2pptx.md +0 -625
  239. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +0 -1499
  240. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +0 -146
  241. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +0 -1085
  242. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +0 -11
  243. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +0 -3081
  244. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +0 -23
  245. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +0 -185
  246. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +0 -287
  247. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +0 -1676
  248. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +0 -28
  249. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +0 -144
  250. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +0 -174
  251. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +0 -25
  252. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +0 -18
  253. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +0 -59
  254. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +0 -56
  255. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +0 -195
  256. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +0 -582
  257. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +0 -25
  258. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +0 -4439
  259. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +0 -570
  260. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +0 -509
  261. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +0 -12
  262. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +0 -108
  263. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +0 -96
  264. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +0 -3646
  265. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +0 -116
  266. package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +0 -42
  267. package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +0 -50
  268. package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +0 -49
  269. package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +0 -33
  270. package/out/base-prompts/skills/pptx/ooxml/schemas/mce/mc.xsd +0 -75
  271. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-2010.xsd +0 -560
  272. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-2012.xsd +0 -67
  273. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-2018.xsd +0 -14
  274. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-cex-2018.xsd +0 -20
  275. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-cid-2016.xsd +0 -13
  276. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +0 -4
  277. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-symex-2015.xsd +0 -8
  278. package/out/base-prompts/skills/pptx/ooxml/scripts/pack.py +0 -159
  279. package/out/base-prompts/skills/pptx/ooxml/scripts/unpack.py +0 -29
  280. package/out/base-prompts/skills/pptx/ooxml/scripts/validate.py +0 -69
  281. package/out/base-prompts/skills/pptx/ooxml/scripts/validation/__init__.py +0 -15
  282. package/out/base-prompts/skills/pptx/ooxml/scripts/validation/base.py +0 -951
  283. package/out/base-prompts/skills/pptx/ooxml/scripts/validation/docx.py +0 -274
  284. package/out/base-prompts/skills/pptx/ooxml/scripts/validation/pptx.py +0 -315
  285. package/out/base-prompts/skills/pptx/ooxml/scripts/validation/redlining.py +0 -279
  286. package/out/base-prompts/skills/pptx/ooxml.md +0 -427
  287. package/out/base-prompts/skills/pptx/scripts/html2pptx.js +0 -984
  288. package/out/base-prompts/skills/pptx/scripts/inventory.py +0 -1020
  289. package/out/base-prompts/skills/pptx/scripts/rearrange.py +0 -231
  290. package/out/base-prompts/skills/pptx/scripts/replace.py +0 -385
  291. package/out/base-prompts/skills/pptx/scripts/thumbnail.py +0 -450
  292. package/out/base-prompts/skills/prompt-lab/SKILL.md +0 -149
  293. package/out/base-prompts/skills/prompt-lab/scripts/query-analytics.sh +0 -2551
  294. package/out/base-prompts/skills/skill-creator/LICENSE.txt +0 -202
  295. package/out/base-prompts/skills/skill-creator/SKILL.md +0 -371
  296. package/out/base-prompts/skills/skill-creator/references/output-patterns.md +0 -82
  297. package/out/base-prompts/skills/skill-creator/references/workflows.md +0 -28
  298. package/out/base-prompts/skills/skill-creator/scripts/init_skill.py +0 -303
  299. package/out/base-prompts/skills/skill-creator/scripts/package_skill.py +0 -110
  300. package/out/base-prompts/skills/skill-creator/scripts/quick_validate.py +0 -95
  301. package/out/base-prompts/skills/skill-installer/SKILL.md +0 -251
  302. package/out/base-prompts/skills/skill-installer/references/store-api.md +0 -219
  303. package/out/base-prompts/skills/spotlight-dashboard/SKILL.md +0 -83
  304. package/out/base-prompts/skills/spotlight-insights/SKILL.md +0 -101
  305. package/out/base-prompts/skills/spotlight-optimize/SKILL.md +0 -93
  306. package/out/base-prompts/skills/spotlight-optimize/scripts/query-analytics.sh +0 -2551
  307. package/out/base-prompts/skills/spotlight-query/SKILL.md +0 -314
  308. package/out/base-prompts/skills/spotlight-query/references/otel-schema.md +0 -163
  309. package/out/base-prompts/skills/spotlight-query/scripts/query-analytics.sh +0 -2551
  310. package/out/base-prompts/skills/spotlight-query/scripts/query-analytics.test.sh +0 -744
  311. package/out/base-prompts/skills/spotlight-query/scripts/test-fixtures/analytics/sessions/sess-test-1.jsonl +0 -14
  312. package/out/base-prompts/skills/spotlight-query/scripts/test-fixtures/analytics/traces/.duckdb-index.json +0 -7
  313. package/out/base-prompts/skills/spotlight-query/scripts/test-fixtures/analytics/traces/sample.jsonl +0 -5
  314. package/out/base-prompts/skills/spotlight-report/SKILL.md +0 -86
  315. package/out/base-prompts/skills/spotlight-visualize/SKILL.md +0 -92
  316. package/out/base-prompts/skills/spotlight-visualize/scripts/query-analytics.sh +0 -2551
  317. package/out/base-prompts/skills/xlsx/LICENSE.txt +0 -30
  318. package/out/base-prompts/skills/xlsx/SKILL.md +0 -289
  319. package/out/base-prompts/skills/xlsx/recalc.py +0 -178
  320. package/out/branding.js +0 -49
  321. package/out/cli/commands/anywhere.js +0 -66
  322. package/out/cli/commands/ask.js +0 -246
  323. package/out/cli/commands/auth-broker.js +0 -24
  324. package/out/cli/commands/auth.js +0 -219
  325. package/out/cli/commands/chat.js +0 -357
  326. package/out/cli/commands/conformance.js +0 -249
  327. package/out/cli/commands/data-plane.js +0 -151
  328. package/out/cli/commands/deploy.js +0 -263
  329. package/out/cli/commands/gateway.js +0 -218
  330. package/out/cli/commands/idp.js +0 -85
  331. package/out/cli/commands/pack.js +0 -399
  332. package/out/cli/commands/probe-custom-engine.js +0 -82
  333. package/out/cli/commands/sandbox.js +0 -351
  334. package/out/cli/commands/serve.js +0 -1950
  335. package/out/cli/commands/store.js +0 -927
  336. package/out/cli/credential-context.js +0 -108
  337. package/out/cli/data-plane/broker.js +0 -78
  338. package/out/cli/data-plane/connector-host.js +0 -121
  339. package/out/cli/data-plane/egress.js +0 -189
  340. package/out/cli/data-plane/gateway.js +0 -218
  341. package/out/cli/data-plane/index.js +0 -153
  342. package/out/cli/data-plane/registry.js +0 -89
  343. package/out/cli/data-plane/remote.js +0 -106
  344. package/out/cli/data-plane/sidecar.js +0 -225
  345. package/out/cli/deploy/blueprints/registry.js +0 -84
  346. package/out/cli/deploy/layers/identity.js +0 -175
  347. package/out/cli/deploy-init.js +0 -3023
  348. package/out/cli/deps.js +0 -342
  349. package/out/cli/idp-init.js +0 -338
  350. package/out/cli/pack-install-ref.js +0 -15
  351. package/out/cli/pack-loader.js +0 -418
  352. package/out/cli/pack-runtime.js +0 -61
  353. package/out/cli/runtime.js +0 -450
  354. package/out/cli/serve-hardening.js +0 -776
  355. package/out/cli/state-sync.js +0 -424
  356. package/out/cli/utils.js +0 -231
  357. package/out/engines/base.js +0 -188
  358. package/out/engines/claude.js +0 -1254
  359. package/out/engines/codex.js +0 -862
  360. package/out/engines/custom.js +0 -680
  361. package/out/engines/customEngineIpc.js +0 -95
  362. package/out/engines/customEngineRunnerEmbed.js +0 -107
  363. package/out/engines/customRegistry.js +0 -274
  364. package/out/engines/embeddedCustomEngineRunner.generated.js +0 -9
  365. package/out/engines/engineApiKey.js +0 -138
  366. package/out/engines/install.js +0 -372
  367. package/out/engines/persistentEngineWorker.js +0 -320
  368. package/out/engines/publish.js +0 -341
  369. package/out/evidence/sessionShip.js +0 -643
  370. package/out/gateway/bridge.js +0 -843
  371. package/out/gateway/cli-agent-id.js +0 -36
  372. package/out/gateway/client.js +0 -336
  373. package/out/gateway/client.test.js +0 -395
  374. package/out/gateway/gateway-scope-errors.js +0 -14
  375. package/out/gateway/gatewayBackup.js +0 -76
  376. package/out/gateway/installSource.js +0 -68
  377. package/out/gateway/lifecycle.js +0 -3190
  378. package/out/gateway/openclaw-channel-routing.js +0 -317
  379. package/out/gateway/outboundHandles.js +0 -373
  380. package/out/gateway/outboundPaths.js +0 -75
  381. package/out/gateway/outboundSend.js +0 -254
  382. package/out/gateway/protocol.js +0 -48
  383. package/out/gateway/secretsPreflight.js +0 -90
  384. package/out/gateway/sessionRouting.js +0 -77
  385. package/out/headless/index.js +0 -546
  386. package/out/headless/sync.js +0 -473
  387. package/out/live-e2e/mcp-demo/server.mjs +0 -32
  388. package/out/live-probe/connector.mjs +0 -8
  389. package/out/live-probe/env-report.mjs +0 -8
  390. package/out/mcp.js +0 -654
  391. package/out/pack/caps.js +0 -271
  392. package/out/pack/governor.js +0 -326
  393. package/out/pack/lease-schema.js +0 -273
  394. package/out/pack/lease.js +0 -682
  395. package/out/pack/reconcile.js +0 -217
  396. package/out/pack/signature.js +0 -839
  397. package/out/permissions.js +0 -108
  398. package/out/permissions.mapping.yaml +0 -68
  399. package/out/registry/index.js +0 -332
  400. package/out/runner/host.js +0 -141
  401. package/out/runner/protocol.js +0 -128
  402. package/out/runner/remoteEngine.js +0 -166
  403. package/out/runtime/interfaces/config-provider.js +0 -32
  404. package/out/runtime/interfaces/edit-tracker.js +0 -24
  405. package/out/runtime/interfaces/index.js +0 -29
  406. package/out/runtime/interfaces/logger.js +0 -40
  407. package/out/runtime/interfaces/permission-handler.js +0 -66
  408. package/out/runtime/interfaces/session-store.js +0 -342
  409. package/out/runtime/vscode/vscode-session-store.js +0 -22
  410. package/out/sandbox/containerDriver.js +0 -572
  411. package/out/sandbox/processDriver.js +0 -318
  412. package/out/sandbox/sandboxRunner.js +0 -303
  413. package/out/sandbox/support.js +0 -301
  414. package/out/sandbox/types.js +0 -55
  415. package/out/scripts/analytics/fleet-local-adapter.js +0 -246
  416. package/out/scripts/analytics/live-fleet-4-collector.cjs +0 -315
  417. package/out/scripts/analytics/live-fleet-4-daemon.mjs +0 -134
  418. package/out/scripts/analytics/live-fleet-4-widget.cjs +0 -433
  419. package/out/scripts/gateway/Dockerfile.serve +0 -45
  420. package/out/scripts/gateway/docker-serve.sh +0 -248
  421. package/out/scripts/gateway/setup-gateway-server.ps1 +0 -953
  422. package/out/scripts/gateway/setup-gateway-server.sh +0 -935
  423. package/out/session.js +0 -272
  424. package/out/shared/agentEngines.js +0 -170
  425. package/out/shared/index.js +0 -109
  426. package/out/shared/models.js +0 -136
  427. package/out/shared/providers.js +0 -205
  428. package/out/spotlight/install.js +0 -345
  429. package/out/spotlight/publish.js +0 -544
  430. package/out/spotlight/yaml.js +0 -31
  431. package/out/store/index.js +0 -786
  432. package/out/store/shared.js +0 -241
  433. package/out/test/agentEnvFile.test.js +0 -97
  434. package/out/test/agentYaml.test.js +0 -373
  435. package/out/test/agentpack.test.js +0 -639
  436. package/out/test/analytics-lifecycle.test.js +0 -174
  437. package/out/test/analytics.test.js +0 -756
  438. package/out/test/auth-baseurl.test.js +0 -132
  439. package/out/test/auth-bootstrapApply.test.js +0 -123
  440. package/out/test/auth-broker-hardening.test.js +0 -227
  441. package/out/test/auth-broker.test.js +0 -541
  442. package/out/test/auth-callback.test.js +0 -62
  443. package/out/test/blueprint-registry.test.js +0 -105
  444. package/out/test/branding.test.js +0 -242
  445. package/out/test/caps-governor.test.js +0 -460
  446. package/out/test/cli-agent-id.test.js +0 -55
  447. package/out/test/codex-new-file-survival.test.js +0 -119
  448. package/out/test/configuration-scopes.test.js +0 -131
  449. package/out/test/credential-context.test.js +0 -160
  450. package/out/test/custom-env-layering.test.js +0 -151
  451. package/out/test/customEngineInstall.test.js +0 -100
  452. package/out/test/customRegistry.test.js +0 -122
  453. package/out/test/data-plane-engine-wiring.test.js +0 -282
  454. package/out/test/data-plane.test.js +0 -624
  455. package/out/test/deploy-init.test.js +0 -860
  456. package/out/test/deps-auto-install.test.js +0 -81
  457. package/out/test/egress-broker.test.js +0 -172
  458. package/out/test/engine-integration.test.js +0 -297
  459. package/out/test/enginePaths.test.js +0 -195
  460. package/out/test/enginePublish.test.js +0 -82
  461. package/out/test/fleetDisplayUtils.test.js +0 -100
  462. package/out/test/gateway-client.test.js +0 -409
  463. package/out/test/gateway-install-source.test.js +0 -111
  464. package/out/test/gateway-lifecycle.test.js +0 -608
  465. package/out/test/gateway-outbound.test.js +0 -538
  466. package/out/test/gateway-reset-mappings.test.js +0 -221
  467. package/out/test/gateway-route-index.test.js +0 -235
  468. package/out/test/gateway-secrets-preflight.test.js +0 -127
  469. package/out/test/gateway-session-routing.test.js +0 -65
  470. package/out/test/gateway-session-store.test.js +0 -216
  471. package/out/test/github.test.js +0 -232
  472. package/out/test/governor-hardening.test.js +0 -222
  473. package/out/test/headless-sync.test.js +0 -301
  474. package/out/test/helpers/manifest-fixtures.js +0 -27
  475. package/out/test/idp-init.test.js +0 -151
  476. package/out/test/lease-signature.test.js +0 -357
  477. package/out/test/lease-trust-root.test.js +0 -273
  478. package/out/test/lease-validation.test.js +0 -224
  479. package/out/test/lease.test.js +0 -114
  480. package/out/test/localFleetAdapter.test.js +0 -91
  481. package/out/test/mcp-obo.test.js +0 -107
  482. package/out/test/mcp.test.js +0 -389
  483. package/out/test/model-routing.test.js +0 -133
  484. package/out/test/openclaw-channel-routing.test.js +0 -191
  485. package/out/test/pack-admission.test.js +0 -216
  486. package/out/test/pack-digest-parity.test.js +0 -140
  487. package/out/test/pack-reconcile.test.js +0 -183
  488. package/out/test/pack-signature-artifact.test.js +0 -103
  489. package/out/test/pack-signature.test.js +0 -176
  490. package/out/test/pack-trust-root.test.js +0 -202
  491. package/out/test/pathSafety.test.js +0 -63
  492. package/out/test/permissions.test.js +0 -175
  493. package/out/test/persistent-engine-worker.test.js +0 -220
  494. package/out/test/refreshPolicy.test.js +0 -60
  495. package/out/test/registry.test.js +0 -351
  496. package/out/test/remote-trust.test.js +0 -242
  497. package/out/test/runner-protocol.test.js +0 -249
  498. package/out/test/runtime.test.js +0 -349
  499. package/out/test/sandbox-driver.test.js +0 -553
  500. package/out/test/sandbox-hardening.test.js +0 -406
  501. package/out/test/sandbox-runner.test.js +0 -343
  502. package/out/test/sandbox-scope.test.js +0 -449
  503. package/out/test/scheduledFleetStorage.test.js +0 -72
  504. package/out/test/serve-cron-acceptance.test.js +0 -370
  505. package/out/test/serve-gateway-turn.test.js +0 -389
  506. package/out/test/serve-hardening.test.js +0 -591
  507. package/out/test/serveClient.test.js +0 -209
  508. package/out/test/sessionJsonlFleet.test.js +0 -78
  509. package/out/test/sessionShip.test.js +0 -618
  510. package/out/test/shared.test.js +0 -173
  511. package/out/test/skills-mcp.test.js +0 -299
  512. package/out/test/spotlightPublish.test.js +0 -327
  513. package/out/test/spotlightYaml.test.js +0 -58
  514. package/out/test/state-sync-peer.test.js +0 -156
  515. package/out/test/store-install-safe.test.js +0 -69
  516. package/out/test/store.test.js +0 -331
  517. package/out/test/stores.test.js +0 -504
  518. package/out/test/sync-switch.test.js +0 -251
  519. package/out/test/teamViewDiscovery.test.js +0 -90
  520. package/out/test/tls-bypass.test.js +0 -90
  521. package/out/test/toolPath.test.js +0 -120
  522. package/out/test/utils.test.js +0 -308
  523. package/out/test/viewsConfigMerge.test.js +0 -180
  524. package/out/test/warrant.test.js +0 -138
  525. package/out/test/windows-command-line.test.js +0 -75
  526. package/out/utils/agentEnvFile.js +0 -131
  527. package/out/utils/binary.js +0 -175
  528. package/out/utils/index.js +0 -37
  529. package/out/utils/network.js +0 -74
  530. package/out/utils/paths.js +0 -116
  531. package/out/utils/pid.js +0 -180
  532. package/out/utils/resolveNodeBinary.js +0 -168
  533. package/out/utils/windowsCommandLine.js +0 -29
  534. package/out/utils.js +0 -968
@@ -1,1950 +0,0 @@
1
- "use strict";
2
- /*---------------------------------------------------------------------------------------------
3
- * Copyright (c) AgentHippo.ai. All rights reserved.
4
- *--------------------------------------------------------------------------------------------*/
5
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
- if (k2 === undefined) k2 = k;
7
- var desc = Object.getOwnPropertyDescriptor(m, k);
8
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
- desc = { enumerable: true, get: function() { return m[k]; } };
10
- }
11
- Object.defineProperty(o, k2, desc);
12
- }) : (function(o, m, k, k2) {
13
- if (k2 === undefined) k2 = k;
14
- o[k2] = m[k];
15
- }));
16
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
- Object.defineProperty(o, "default", { enumerable: true, value: v });
18
- }) : function(o, v) {
19
- o["default"] = v;
20
- });
21
- var __importStar = (this && this.__importStar) || (function () {
22
- var ownKeys = function(o) {
23
- ownKeys = Object.getOwnPropertyNames || function (o) {
24
- var ar = [];
25
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
- return ar;
27
- };
28
- return ownKeys(o);
29
- };
30
- return function (mod) {
31
- if (mod && mod.__esModule) return mod;
32
- var result = {};
33
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
- __setModuleDefault(result, mod);
35
- return result;
36
- };
37
- })();
38
- Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.serveCommand = serveCommand;
40
- exports.resolveGatewaySessionKey = resolveGatewaySessionKey;
41
- exports.runGatewayTurn = runGatewayTurn;
42
- exports.runBridgeTurn = runBridgeTurn;
43
- /**
44
- * Serve Command - HTTP Server
45
- */
46
- const crypto = __importStar(require("crypto"));
47
- const fs = __importStar(require("fs"));
48
- const os = __importStar(require("os"));
49
- const http = __importStar(require("http"));
50
- const path = __importStar(require("path"));
51
- const node_crypto_1 = require("node:crypto");
52
- const sandboxRunner_1 = require("../../sandbox/sandboxRunner");
53
- const deps_1 = require("../deps");
54
- const signature_1 = require("../../pack/signature");
55
- const reconcile_1 = require("../../pack/reconcile");
56
- const index_1 = require("../../store/index");
57
- const lease_1 = require("../../pack/lease");
58
- const caps_1 = require("../../pack/caps");
59
- const governor_1 = require("../../pack/governor");
60
- const containerDriver_1 = require("../../sandbox/containerDriver");
61
- const processDriver_1 = require("../../sandbox/processDriver");
62
- const support_1 = require("../../sandbox/support");
63
- const headless_1 = require("../../headless");
64
- const pack_loader_1 = require("../pack-loader");
65
- const utils_1 = require("../utils");
66
- const pack_runtime_1 = require("../pack-runtime");
67
- const sessionShip_1 = require("../../evidence/sessionShip");
68
- const serve_hardening_1 = require("../serve-hardening");
69
- const credential_context_1 = require("../credential-context");
70
- const lease_2 = require("../../pack/lease");
71
- const identity_1 = require("../../analytics/identity");
72
- const data_plane_1 = require("../data-plane");
73
- const runtime_1 = require("../runtime");
74
- const outboundSend_1 = require("../../gateway/outboundSend");
75
- const sessionEvents_1 = require("../../analytics/sessionEvents");
76
- const base_1 = require("../../engines/base");
77
- const state_sync_1 = require("../state-sync");
78
- const cli_agent_id_js_1 = require("../../gateway/cli-agent-id.js");
79
- const openclaw_channel_routing_js_1 = require("../../gateway/openclaw-channel-routing.js");
80
- const customRegistry_1 = require("../../engines/customRegistry");
81
- const BANNER_LINE = '-'.repeat(62);
82
- async function serveCommand(options) {
83
- const apiConfig = (0, pack_loader_1.getApiConfig)();
84
- if (apiConfig.useLiteLLM && !apiConfig.litellmApiKey) {
85
- // Loud, because the failure mode it replaces was silent: traffic reached the gateway
86
- // authenticated with whatever sat in the provider-key slot, with analytics disabled.
87
- console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} AGENTHIPPO_LITELLM_BASE_URL is set without AGENTHIPPO_LITELLM_API_KEY — gateway calls will authenticate as 'sk-dummy' (a strict gateway will 401). Provider keys are never sent to the gateway.`);
88
- }
89
- // Load agent pack (required)
90
- let pack;
91
- const syncLogger = options.verbose ? new headless_1.ConsoleLogger('[Sync]') : new headless_1.NoOpLogger();
92
- try {
93
- pack = await (0, pack_loader_1.loadAgentPack)(options.agent, options.workspace);
94
- }
95
- catch (err) {
96
- console.error(`Error: ${err instanceof Error ? err.message : err}`);
97
- process.exit(1);
98
- }
99
- const packId = pack.manifest.metadata.name;
100
- const gatewayAgentId = (0, cli_agent_id_js_1.resolveCliGatewayAgentId)(packId);
101
- const defaultEngine = (options.engine || pack.manifest.spec.engine || (0, pack_loader_1.getDefaultConfig)().engine).trim().toLowerCase();
102
- const customEngineDefaultModel = !['claude', 'codex'].includes(defaultEngine)
103
- ? (0, customRegistry_1.tryResolveCustomEngineManifest)(options.workspace, defaultEngine)?.model?.defaultModel
104
- : undefined;
105
- // Custom engine manifest defaultModel beats the global AGENTHIPPO_DEFAULT_MODEL
106
- // so --engine gemini without --model uses gemini's manifest default, not the
107
- // user's global Claude/Codex fallback.
108
- const rawModel = options.model || pack.manifest.spec.model || customEngineDefaultModel || (0, pack_loader_1.getEnvDefaultModel)() || (0, pack_loader_1.getDefaultConfig)().model;
109
- const defaultModel = (0, headless_1.resolveModelAlias)(rawModel);
110
- const rawCheapModel = pack.manifest.spec.cheapModel?.trim();
111
- const claudeCheapModel = rawCheapModel ? (0, headless_1.resolveModelAlias)(rawCheapModel) : undefined;
112
- // Engine-aware: a codex deploy needs an OpenAI (or LiteLLM) key; a claude deploy needs an
113
- // Anthropic (or LiteLLM) key. Fail loudly at boot rather than 401 at the first model call.
114
- if (!(0, pack_loader_1.hasCredentialForEngine)(defaultEngine, apiConfig)) {
115
- const need = defaultEngine === 'claude' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY';
116
- console.error(`Error: No usable API key for engine "${defaultEngine}". Set ${need}, or configure the LiteLLM proxy (AGENTHIPPO_LITELLM_API_KEY + AGENTHIPPO_LITELLM_BASE_URL).`);
117
- for (const line of (0, utils_1.getLiteLLMQuickstartHintLines)()) {
118
- console.error(line);
119
- }
120
- process.exit(1);
121
- }
122
- const logger = options.verbose ? new headless_1.ConsoleLogger('[Server]') : new headless_1.NoOpLogger();
123
- // ---------------------------------------------------------------------------------------
124
- // Pack admission — BEFORE anything from the pack reaches the workspace.
125
- //
126
- // `syncPackToWorkspace` used to run here, hundreds of lines before the signature was checked,
127
- // so a pack that was about to be refused had already materialised its skills, AGENTS.md and
128
- // MCP server definitions into the workspace and into the derived .claude/.codex directories.
129
- // The refusal then stopped the turn while the content it was refusing sat on disk, reachable
130
- // by anything else pointed at that workspace. Verification moves ahead of the write.
131
- // ---------------------------------------------------------------------------------------
132
- let activePackDir = pack.packPath;
133
- let activeVersion = pack.manifest.metadata.version ?? '0.0.0';
134
- let packDigestValue;
135
- /**
136
- * Trusted pack-signing keys. NOT frozen at boot.
137
- *
138
- * A trust root that is CONFIGURED BUT UNREACHABLE (a 503 during a rolling restart, a DNS blip)
139
- * is now distinguishable from one that was never configured, and it is retried on the lease
140
- * poll — one transient fetch at the wrong second must fail the turn, not brick the process.
141
- */
142
- let packTrust = await (0, signature_1.loadPackTrustRoot)({ leaseUrl: process.env.AGENTHIPPO_LEASE_URL });
143
- const reportTrustRoot = (t, label = 'pack') => {
144
- for (const w of t.warnings) {
145
- console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} ${w}`);
146
- }
147
- if (t.status === 'unavailable') {
148
- console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} ${label} trust root is configured but unusable: ${t.reason}`);
149
- }
150
- };
151
- reportTrustRoot(packTrust);
152
- /**
153
- * Verified per turn, with a cheap change probe — see `createPackVerifier`. The RESULT is
154
- * cached only as long as the pack tree looks untouched and never longer than the full-reverify
155
- * interval; the REQUIREMENT is re-read from the lease on every poll, so an operator can turn
156
- * enforcement on for a fleet already running.
157
- */
158
- const packVerifier = (0, signature_1.createPackVerifier)();
159
- let packSignature = { ok: false, kind: 'no-pack', reason: 'not verified' };
160
- const verifyPackAt = (packDir, version) => {
161
- packSignature = packVerifier.refresh({ packDir, pack: packId, packVersion: version, keys: packTrust.keys });
162
- packDigestValue = packSignature.ok ? packSignature.claims.digest : undefined;
163
- logger.info(packSignature.ok
164
- ? `[Pack] signature verified for ${packId}`
165
- : `[Pack] unverified — ${packSignature.reason}`);
166
- return packSignature;
167
- };
168
- /** The refusal an operator sees, with the remedy for THIS failure rather than a generic one. */
169
- const packRefusalText = (result) => {
170
- if (result.ok) {
171
- return '';
172
- }
173
- // "Configured but unreachable" outranks whatever the verifier concluded: with no usable
174
- // keys every signature fails as `no-trust-root`, and telling an operator to configure a
175
- // trust root they already configured is the least useful thing we could say.
176
- const kind = packTrust.status === 'unavailable' && result.kind === 'no-trust-root' ? 'trust-root-unavailable' : result.kind;
177
- return (0, signature_1.describePackRefusal)({
178
- kind,
179
- detail: kind === 'trust-root-unavailable' ? (packTrust.reason ?? result.reason) : result.reason,
180
- pack: packId,
181
- packVersion: activeVersion,
182
- packDir: activePackDir,
183
- trustRootHint: packTrust.url ? `Trust root: ${packTrust.url} (${packTrust.reason ?? 'unavailable'})` : packTrust.reason,
184
- });
185
- };
186
- verifyPackAt(activePackDir, activeVersion);
187
- /**
188
- * Signing REQUIRED by default.
189
- *
190
- * This is a deliberate break with the previous shipped behaviour, which logged
191
- * `[Pack] unsigned or unverifiable — …` and then ran the pack anyway. Warn-and-run is not a
192
- * gate; it is a log line next to an ungated code path, and it meant the default deployment
193
- * shape executed whatever was in the pack directory.
194
- *
195
- * Tighten-only survives intact: the escape hatch below relaxes only this INSTANCE's default,
196
- * and `resolveRequireSignedPack` checks the lease's demand first, so a control plane that sets
197
- * `requireSignedPack` cannot be opted out of locally.
198
- */
199
- const allowUnsignedPack = Boolean(options.allowUnsignedPack) || (0, signature_1.allowUnsignedPackFromEnv)();
200
- if (!packSignature.ok) {
201
- if (!allowUnsignedPack) {
202
- console.error(`${utils_1.COLORS.red}Error: ${packRefusalText(packSignature)}${utils_1.COLORS.reset}`);
203
- process.exit(1);
204
- }
205
- // Unmissable, every boot, at warn level regardless of --verbose.
206
- console.warn(BANNER_LINE);
207
- console.warn(`${utils_1.COLORS.yellow}!! RUNNING AN UNVERIFIED PACK — signature checking is DISABLED on this instance.${utils_1.COLORS.reset}`);
208
- console.warn(`${utils_1.COLORS.yellow}!! ${packSignature.reason}${utils_1.COLORS.reset}`);
209
- console.warn(`${utils_1.COLORS.yellow}!! Whatever is in ${activePackDir} will be executed, verified by nobody.${utils_1.COLORS.reset}`);
210
- console.warn(`${utils_1.COLORS.yellow}!! This is a LOCAL DEVELOPMENT setting (AGENTHIPPO_ALLOW_UNSIGNED_PACK). Do not ship it.${utils_1.COLORS.reset}`);
211
- console.warn(BANNER_LINE);
212
- }
213
- // Sync pack content to workspace — only now that the pack has been admitted.
214
- const bundledPromptsDir = (0, utils_1.findBundledBasePromptsDir)();
215
- await (0, headless_1.syncPackToWorkspace)(options.workspace, pack.packPath, packId, defaultEngine, syncLogger, bundledPromptsDir);
216
- console.log(`OK: Loaded agent: ${pack.manifest.metadata.name} v${pack.manifest.metadata.version}`);
217
- // Structural attribution (pack-standard WS2) — greppable for CISO/SRE boot audits.
218
- console.log(`agenthippo pack=${pack.manifest.metadata.name}@${pack.manifest.metadata.version} engine=${defaultEngine}`);
219
- // Resolve persistent session state (serve-v3 §A1: replaces the tmpdir default).
220
- const { dir: storagePath, ephemeral } = (0, serve_hardening_1.resolveStateDir)(options.stateDir);
221
- const bindHost = (0, serve_hardening_1.resolveBindHost)(options.host);
222
- // Who serve believes a caller to be decides whose session state and whose data credentials they
223
- // reach, so an unpinned identity header is not a detail to leave in a docstring.
224
- for (const line of (0, serve_hardening_1.identityHeaderAdvice)(process.env, bindHost)) {
225
- console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} ${line}`);
226
- }
227
- // A pinned header means multiple users; a tier-0 or sandbox-less serve means one filesystem for
228
- // all of them. That pairing had no boot-time signal at all, while the strictly SAFER container
229
- // case (--shared-workspace) has warned since it shipped.
230
- const tenantIsolation = (0, serve_hardening_1.tenantIsolationAdvice)({
231
- sandbox: options.sandbox,
232
- sharedWorkspace: Boolean(options.sharedWorkspace),
233
- workspace: options.workspace,
234
- stateDir: storagePath,
235
- });
236
- if (tenantIsolation.level === 'refuse') {
237
- for (const line of tenantIsolation.lines) {
238
- console.error(`${utils_1.COLORS.red}Error:${utils_1.COLORS.reset} ${line}`);
239
- }
240
- console.error(`Refusing to start because ${serve_hardening_1.REQUIRE_TENANT_ISOLATION_ENV} is set. Unset it to downgrade this to a warning.`);
241
- process.exit(1);
242
- }
243
- for (const line of tenantIsolation.lines) {
244
- console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} ${line}`);
245
- }
246
- // Passed to createPackRunner below so the runner's own SessionEventWriter uses the same
247
- // root — a fail-closed warrant denial lands in the same per-session JSONL as the tool events
248
- // it precedes. The denial happens before runner.run() is ever called, so it needs its own
249
- // writer here too. See resolveAnalyticsSessionsDir's docstring for the precedence.
250
- const analyticsSessionsDir = (0, serve_hardening_1.resolveAnalyticsSessionsDir)(storagePath, ephemeral);
251
- const sessionEventWriter = new sessionEvents_1.SessionEventWriter(analyticsSessionsDir);
252
- const logWarrantDenied = (sessionKey) => (reason) => sessionEventWriter.append(sessionKey, (0, base_1.generateRequestId)(), 'warrant_denied', { reason });
253
- // Data-plane gateway (secure-obo-design §3): when the pack declares spec.dataPlane,
254
- // serve runs the connector broker-side and engines get a per-turn nonce instead of the
255
- // credential. The raw data credential never crosses into the agent's trust domain.
256
- const dataPlaneCfg = (0, data_plane_1.resolveDataPlaneConfig)(pack.manifest.spec, pack.packPath);
257
- // EDGE-registration mode (P1 fix, obo-inprogress §2): the trusted edge (IdP Caddy)
258
- // registers each turn with the sidecar and injects the nonce as a request header; serve
259
- // merely adopts it. serve holds NO registration credential at all — nothing in this
260
- // container's /proc can mint a nonce for an arbitrary userId. Takes precedence over the
261
- // token-based sidecar mode below.
262
- const edgeCfg = (0, data_plane_1.resolveEdgeDataPlane)();
263
- const edgeDataPlane = edgeCfg ? new data_plane_1.EdgeDataPlane(edgeCfg) : null;
264
- // Data-plane SIDECAR mode (secure-obo-design §7.6): when the connector runs in its OWN
265
- // container, AGENTHIPPO_DATA_PLANE_CONTROL_URL/TOKEN + AGENTHIPPO_DATA_MCP_URL are set and
266
- // serve holds NO backend base credential — it registers turns with the sidecar and hands the
267
- // engine only the nonce. Takes precedence over an in-process connector.
268
- const remoteCfg = edgeDataPlane ? null : (0, data_plane_1.resolveRemoteDataPlane)();
269
- const remoteDataPlane = remoteCfg ? new data_plane_1.RemoteDataPlane(remoteCfg) : null;
270
- // One shared registry: a single per-turn nonce grants BOTH the data-plane and egress
271
- // capabilities (secure-obo-design §7.6 step 2). Created when either LOCAL broker will run.
272
- const turnRegistry = ((dataPlaneCfg && !remoteDataPlane && !edgeDataPlane) || options.gatewayBridge) ? new data_plane_1.TurnRegistry() : null;
273
- /**
274
- * Every data-plane audit row carries BOTH principals (PLAN/agent-identity.md §1 failure 3,
275
- * §10).
276
- *
277
- * The user dimension was already on these rows. The agent was not, so evidence could answer
278
- * "which user touched which rows" but not "via which agent" — the auditor's second question,
279
- * and the one the whole design exists to make answerable. It was only ever *implied*, by which
280
- * deployment happened to write the file, and that implication breaks entirely the moment two
281
- * serves share a connector (§4.4).
282
- *
283
- * Read at emit time, not captured once: `activeVersion` and `packDigestValue` both change on a
284
- * pack hot-swap, and a row must name the pack that actually served it.
285
- *
286
- * The digest is carried in full rather than truncated — it is not a secret, and a prefix cannot
287
- * be checked against a signed artifact, which is the entire point of recording it.
288
- */
289
- const auditEvent = (sessionKey, event, fields) => sessionEventWriter.append(sessionKey, (0, base_1.generateRequestId)(), event, {
290
- ...fields,
291
- agent: { pack: packId, version: activeVersion, digest: packDigestValue ?? null },
292
- });
293
- let dataPlane = null;
294
- if (edgeDataPlane) {
295
- process.env[data_plane_1.DATA_MCP_URL_ENV] = edgeDataPlane.url;
296
- // Belt-and-braces: edge mode must never see a register token, but if one leaked into the
297
- // deploy env anyway, strip it before any engine child inherits it.
298
- delete process.env.AGENTHIPPO_DATA_PLANE_TOKEN;
299
- delete process.env.AGENTHIPPO_DATA_PLANE_CONTROL_URL;
300
- console.log(`OK: Data plane EDGE-registered — gateway ${edgeDataPlane.url}; turn nonces are minted at the edge, serve holds no registration credential`);
301
- }
302
- else if (remoteDataPlane) {
303
- process.env[data_plane_1.DATA_MCP_URL_ENV] = remoteDataPlane.url;
304
- // The control-API token is captured in the RemoteDataPlane (serve memory). Strip it from
305
- // the process env so no engine child inherits it — otherwise the agent could register a
306
- // turn as ANY user and read their rows (forge-identity escalation). The engine keeps the
307
- // gateway URL + its own per-turn nonce, which is all it needs (secure-obo-design §7.6).
308
- delete process.env.AGENTHIPPO_DATA_PLANE_TOKEN;
309
- console.log(`OK: Data plane SIDECAR — gateway ${remoteDataPlane.url}; base credentials stay in the connector container, not here`);
310
- }
311
- else if (dataPlaneCfg) {
312
- try {
313
- dataPlane = await data_plane_1.DataPlane.start(dataPlaneCfg, {
314
- logger: options.verbose ? new headless_1.ConsoleLogger('[DataPlane]') : new headless_1.NoOpLogger(),
315
- audit: auditEvent,
316
- registry: turnRegistry ?? undefined,
317
- });
318
- // Boot-stable process env: engines resolve ${env:AGENTHIPPO_DATA_MCP_URL} from it,
319
- // so codex's config.toml stays byte-identical across turns. The NONCE is never set
320
- // here — it rides the per-spawn child env only.
321
- process.env[data_plane_1.DATA_MCP_URL_ENV] = dataPlane.url;
322
- console.log(`OK: Data gateway (provider=${dataPlane.broker.provider.name}) on loopback — connector: ${dataPlaneCfg.connectorPath}`);
323
- }
324
- catch (err) {
325
- console.error(`${utils_1.COLORS.red}Error: failed to start the data-plane gateway:${utils_1.COLORS.reset} ${err instanceof Error ? err.message : err}`);
326
- process.exit(1);
327
- }
328
- }
329
- // Egress broker (secure-obo-design §7.6): capture the gateway token broker-side BEFORE any
330
- // engine spawns, so it can be stripped from the process env the children inherit. The broker
331
- // starts alongside the gateway bridge below; the token is held only in this closure.
332
- // Captured UNCONDITIONALLY, and the env strip is immediate (serve-v4 §10 hole 2): the strip
333
- // used to live inside the broker branch, so a serve without --gateway-bridge kept the token
334
- // in process.env and every engine child (they all spread it) inherited a credential nothing
335
- // in this deployment would ever legitimately use.
336
- const brokerGatewayToken = (0, runtime_1.readOpenClawGatewayToken)();
337
- delete process.env.OPENCLAW_GATEWAY_TOKEN;
338
- let egressBroker = null;
339
- // Mint one nonce per turn and assemble the per-spawn child env. Returns null outside any
340
- // broker (no capability to grant). The URLs ride process.env (boot-stable) for config
341
- // invariance; only the nonce is per-spawn.
342
- const registerServeTurn = async (reg) => {
343
- // Edge mode: the trusted edge already registered this turn and injected the nonce as a
344
- // request header — adopt it. No edge nonce (misrouted request, or a channel turn that
345
- // never traversed the edge) ⇒ no data capability, fail closed at the gateway.
346
- if (edgeDataPlane) {
347
- const edgeNonce = reg.edgeNonce?.trim();
348
- if (!edgeNonce) {
349
- console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} edge data plane active but no ${data_plane_1.TURN_NONCE_HEADER} header on this turn — data tools will fail closed (check the edge route's forward_auth)`);
350
- return null;
351
- }
352
- const r = edgeDataPlane.adoptTurn(edgeNonce);
353
- const env = { ...r.env };
354
- if (egressBroker) {
355
- env[data_plane_1.EGRESS_URL_ENV] = egressBroker.url;
356
- }
357
- return { nonce: r.nonce, env, release: r.release };
358
- }
359
- // Sidecar: register with the connector container; serve never sees a base credential.
360
- if (remoteDataPlane) {
361
- // Name the agent on every registration (PR-8). Asserted, not proven — the sidecar
362
- // records it and flags a mismatch against the pack it loaded, which is how "one
363
- // connector, one agent" (§4.4) becomes observable rather than merely assumed.
364
- const r = await remoteDataPlane.registerTurn({
365
- ...reg,
366
- agent: { pack: packId, version: activeVersion, digest: packDigestValue },
367
- });
368
- const env = { ...r.env };
369
- if (egressBroker) {
370
- env[data_plane_1.EGRESS_URL_ENV] = egressBroker.url;
371
- }
372
- return { nonce: r.nonce, env, release: r.release };
373
- }
374
- if (!turnRegistry) {
375
- return null;
376
- }
377
- const { nonce, release } = turnRegistry.register(reg);
378
- const env = { [data_plane_1.TURN_NONCE_ENV]: nonce };
379
- if (dataPlane) {
380
- env[data_plane_1.DATA_MCP_URL_ENV] = dataPlane.url;
381
- }
382
- if (egressBroker) {
383
- env[data_plane_1.EGRESS_URL_ENV] = egressBroker.url;
384
- }
385
- return { nonce, env, release };
386
- };
387
- // Edge-registered turn nonce, injected by the trusted edge per chat request (edge mode only —
388
- // otherwise ignored). Trusted like the pinned identity header: only the in-network edge can
389
- // reach serve, and a forged value fails closed at the gateway (unknown nonce ⇒ 401).
390
- const edgeTurnNonce = (headers) => {
391
- if (!edgeDataPlane) {
392
- return undefined;
393
- }
394
- const v = headers[data_plane_1.TURN_NONCE_HEADER];
395
- return Array.isArray(v) ? v[0] : v;
396
- };
397
- const turnLimiter = new serve_hardening_1.TurnLimiter(options.maxTurnsPerSession ?? 0);
398
- // Per-identity request-rate cap (req/min). Keyed on the trusted user id (falling back to
399
- // client IP) so rotating session_id can't bypass it. AGENTHIPPO_SERVE_RATE_LIMIT=0 disables.
400
- const rateLimitPerMin = Number(process.env.AGENTHIPPO_SERVE_RATE_LIMIT ?? 120);
401
- const rateLimiter = new serve_hardening_1.RateLimiter(Number.isFinite(rateLimitPerMin) ? rateLimitPerMin : 120);
402
- // Process-wide inflight cap (serve-v4 P0.1). Prevents a burst from spawning N engine
403
- // subprocesses and OOMing the container. AGENTHIPPO_MAX_CONCURRENT_TURNS=0 disables.
404
- const turnGate = (0, serve_hardening_1.createTurnGateFromEnv)();
405
- const warnGateReject = (0, serve_hardening_1.createRejectionWarner)();
406
- let landingPageHtml;
407
- if (options.wwwDir?.trim()) {
408
- const indexPath = path.join(path.resolve(options.wwwDir.trim()), 'index.html');
409
- try {
410
- landingPageHtml = fs.readFileSync(indexPath, 'utf8');
411
- }
412
- catch {
413
- console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} --www-dir set but unreadable: ${indexPath}`);
414
- }
415
- }
416
- if (ephemeral) {
417
- console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} Session state is in a tmpdir (ephemeral). Pass --state-dir or set AGENTHIPPO_HOME to persist sessions across restarts.`);
418
- }
419
- // Q1 + serve-v4 P1.2: object-store state sync (no-op unless AGENTHIPPO_STATE_S3_URI is set).
420
- // Multi-replica: AGENTHIPPO_STATE_RESTORE_BOOT=none + restore-on-miss per session.
421
- // Single-instance default: boot-restore the whole prefix (backward compatible).
422
- const s3State = (0, state_sync_1.resolveS3StateConfig)();
423
- const restoreBoot = (0, state_sync_1.resolveRestoreBootMode)();
424
- if (s3State) {
425
- // Preflight: state sync shells out to `aws`. If it's missing, sessions would
426
- // silently never persist — fail fast since durable state was explicitly opted into.
427
- if (!(await (0, state_sync_1.isAwsCliAvailable)())) {
428
- console.error(`${utils_1.COLORS.red}Error: AGENTHIPPO_STATE_S3_URI is set but the \`aws\` CLI was not found.${utils_1.COLORS.reset}`);
429
- console.error(' State sync requires the AWS CLI (S3-compatible via --endpoint-url). Install it or unset AGENTHIPPO_STATE_S3_URI.');
430
- process.exit(1);
431
- }
432
- if (restoreBoot === 'all') {
433
- console.log(`State sync: boot-restore from ${s3State.uri} …`);
434
- await (0, state_sync_1.restoreState)(s3State, storagePath, (m) => console.log(` ${m}`));
435
- }
436
- else {
437
- console.log(`State sync: restore-on-miss from ${s3State.uri} (boot restore disabled)`);
438
- }
439
- }
440
- const statePusher = s3State ? (0, state_sync_1.makeStatePusher)(s3State, storagePath, options.verbose ? (m) => console.error(m) : undefined) : null;
441
- const stateLog = options.verbose ? (m) => console.error(m) : undefined;
442
- const prepareSession = async (sessionKey) => {
443
- if (s3State) {
444
- await (0, state_sync_1.ensureSessionLocal)(s3State, storagePath, sessionKey, stateLog);
445
- }
446
- };
447
- // Find SDK CLI paths
448
- const claudeCodePath = (0, utils_1.findClaudeCodePath)();
449
- const codexPath = (0, utils_1.findCodexPath)();
450
- // Validate required CLI is available — but ONLY when this process will run the engine.
451
- // A container sandbox runs the engine inside its own image, so requiring the SDK on the host
452
- // is wrong and refuses to boot on exactly the minimal host a tier-2 deployment wants
453
- // (observed live: a Linux host with no npm/codex could not start `--sandbox container`).
454
- // The process driver shares this host, so it still needs the CLI present.
455
- const engineRunsInSandbox = options.sandbox === 'container';
456
- if (!engineRunsInSandbox && defaultEngine === 'claude' && !claudeCodePath) {
457
- console.error(`${utils_1.COLORS.red}Error: Claude Code CLI not found.${utils_1.COLORS.reset}`);
458
- console.error(' Install: npm install -g @anthropic-ai/claude-code');
459
- console.error(' Or set CLAUDE_CODE_PATH environment variable');
460
- process.exit(1);
461
- }
462
- if (!engineRunsInSandbox && defaultEngine === 'codex' && !codexPath) {
463
- console.error(`${utils_1.COLORS.red}Error: Codex CLI not found.${utils_1.COLORS.reset}`);
464
- console.error(' Install: npm install -g @openai/codex');
465
- console.error(' Or set CODEX_PATH environment variable');
466
- process.exit(1);
467
- }
468
- // V1 permissions are non-interactive in server mode.
469
- const permissionHandler = new headless_1.AutoApprovePermissionHandler();
470
- const permissions = {
471
- fileAccess: (0, pack_loader_1.getFileAccess)(pack),
472
- };
473
- // R-3 policy provenance (PLAN/compliance.md §3.1): remember WHICH policy is in force and
474
- // announce changes. `policy_applied` marks the boundary; every turn between two boundaries
475
- // carries the version via the runner's provenance stamp below, so "was the cap enforced?"
476
- // stays answerable after anyone edits a cap.
477
- let lastPolicyVersion;
478
- let provenanceAnnounced = false;
479
- const notePolicy = (lease) => {
480
- const v = (0, lease_1.policyVersion)(lease);
481
- // Announce once at boot even with no lease (unmanaged deployments still have a digest,
482
- // an engine build and a tier), then again on every policy change. This event is the
483
- // PROVENANCE DIMENSION: turns don't repeat these facts — the evidence index joins a
484
- // turn to the latest announcement for its instance at or before the turn's timestamp.
485
- if (provenanceAnnounced && v === lastPolicyVersion) {
486
- return;
487
- }
488
- provenanceAnnounced = true;
489
- lastPolicyVersion = v;
490
- void sessionEventWriter.append('serve', (0, base_1.generateRequestId)(), 'policy_applied', {
491
- instance_id: instanceId,
492
- policy_version: v ?? null,
493
- pack_digest: packDigestValue ?? null,
494
- engine_version: (0, deps_1.installedEngineVersion)(defaultEngine) ?? null,
495
- tier: options.sandbox === 'container' ? 2 : options.sandbox ? 0 : null,
496
- enabled: lease?.enabled ?? null,
497
- pinned_version: lease?.version ?? null,
498
- caps: lease?.caps ?? null,
499
- deny_count: lease?.deny?.length ?? 0,
500
- });
501
- };
502
- // Same factory a sandbox uses (cli/pack-runtime.ts) — built after activeVersion is known.
503
- let inProcessRunner;
504
- /**
505
- * Where sandboxes read the pack from.
506
- *
507
- * With --pull-packs this is a SYMLINK (`<state>/packs/current`), never the resolved directory:
508
- * the sandbox drivers capture this path once at construction, so a stable name whose target is
509
- * swapped is what lets a later version reach new sandboxes without rebuilding the runner. A
510
- * sandbox already starting keeps the directory it opened, which is exactly the drain semantics
511
- * version convergence already has.
512
- */
513
- const packsRoot = path.join(storagePath, 'packs');
514
- const storeDownloader = async (slug, version) => {
515
- const files = await (0, index_1.downloadFromStore)({ type: 'pack', slug, version });
516
- return files.map((f) => ({ path: f.path, content: f.content }));
517
- };
518
- /** Converge to the pinned version if we can; never take the fleet down trying. */
519
- const reconcileToPin = async (pinned) => {
520
- if (!options.pullPacks || !pinned || pinned === activeVersion) {
521
- return;
522
- }
523
- const res = await (0, reconcile_1.reconcilePinnedPack)({
524
- pack: packId,
525
- currentVersion: activeVersion,
526
- pinnedVersion: pinned,
527
- packsRoot,
528
- download: storeDownloader,
529
- publicKeysByKid: packTrust.keys,
530
- logger: { info: (m) => logger.info(m), warn: (m) => logger.info(m) },
531
- });
532
- if (res.ok) {
533
- activeVersion = res.version;
534
- activePackDir = res.packDir;
535
- verifyPackAt(activePackDir, activeVersion);
536
- logger.info(`[Pack] now serving ${packId}@${activeVersion} from ${res.packDir}`);
537
- return;
538
- }
539
- // Deliberately not fatal: the lease fails OPEN on transport, and a bad tag or a store
540
- // outage must not become a fleet-wide stop. The instance keeps reporting the version it is
541
- // really running, so the console shows "pinned to X, running Y" rather than an outage.
542
- console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} cannot converge to ${packId}@${pinned}: ${res.reason}. Still serving @${activeVersion}.`);
543
- };
544
- const warrantKey = (0, support_1.loadWarrantKeypair)(logger);
545
- // Lease: kill switch + version pin. No URL configured ⇒ unmanaged deployment, always admits.
546
- /** Set once the sandbox runner exists; the lease-poll identity reads it lazily. */
547
- let sandboxRunnerRef;
548
- // Stable across restarts, unique per instance: an operator watching the console must see one
549
- // row per instance that survives a redeploy, not a new row each boot (and not one row per NAT
550
- // address for a whole fleet). Persisted beside the instance's own state.
551
- const instanceId = (() => {
552
- try {
553
- const file = path.join((0, serve_hardening_1.resolveStateDir)(options.stateDir).dir, 'instance-id');
554
- if (fs.existsSync(file)) {
555
- return fs.readFileSync(file, 'utf-8').trim();
556
- }
557
- const id = `${os.hostname()}-${crypto.randomBytes(4).toString('hex')}`;
558
- fs.mkdirSync(path.dirname(file), { recursive: true });
559
- fs.writeFileSync(file, id);
560
- return id;
561
- }
562
- catch {
563
- // Never let bookkeeping stop a boot; a per-process id still beats collapsing by address.
564
- return `${os.hostname()}-${crypto.randomBytes(4).toString('hex')}`;
565
- }
566
- })();
567
- /** Lazily read by the lease-poll identity, which is built before the pack path is resolved. */
568
- const activeVersionRef = { get: () => pack?.manifest.metadata.version };
569
- // ONE constant for the poll interval and everything derived from it. The cache used to default
570
- // to 60s while every poll defaulted to 30s and the banner printed 30 — so the real kill latency
571
- // was up to 3x what an operator was told.
572
- const leasePollSeconds = (0, lease_1.resolveLeasePollSeconds)(options.leasePollSeconds);
573
- /**
574
- * Trusted LEASE-signing keys — a separate root from the pack one.
575
- *
576
- * These used to be the same keyring (`signingKeys: () => packTrust.keys`), which made signed
577
- * leases unreachable end to end: the control plane publishes the lease public key under purpose
578
- * `lease-signing` and refuses to register one kid under two purposes, so a correctly-configured
579
- * plane and a correctly-configured serve could never name the same key. With `requireSignedLease`
580
- * sticky and no plane-side undo, that disagreement latches an instance into refusing turns.
581
- *
582
- * A DERIVED root that comes back `unavailable` is the normal state of every plane that publishes
583
- * no lease-signing key, so it is only reported at boot when an operator named one outright.
584
- * Otherwise `leaseSigningKeys` says everything there is to say, and only once a signed lease has
585
- * actually arrived.
586
- */
587
- let leaseTrust = await (0, signature_1.loadLeaseTrustRoot)({ leaseUrl: process.env.AGENTHIPPO_LEASE_URL });
588
- if (leaseTrust.explicitlyConfigured) {
589
- reportTrustRoot(leaseTrust, 'lease');
590
- }
591
- let leaseTrustFallbackWarned = false;
592
- /**
593
- * BACK-COMPAT, deliberately not a cutover.
594
- *
595
- * A deployment that verifies signed leases against the pack keyring today keeps working: with no
596
- * lease root named outright, the pack keys stay in play and the (possibly empty) derived lease
597
- * keys are added to them. The warning fires ONCE, and only when a signed lease is actually being
598
- * verified, so a deployment that never uses signed leases never sees it.
599
- *
600
- * Not a hard switch because `requireSignedLease` latches per instance and the plane has no undo:
601
- * an instance that stopped recognising its lease key would refuse turns and could not be talked
602
- * out of it from the console.
603
- */
604
- const leaseSigningKeys = () => {
605
- const selected = (0, signature_1.selectLeaseSigningKeys)(leaseTrust, packTrust);
606
- if (selected.packFallback && !leaseTrustFallbackWarned) {
607
- leaseTrustFallbackWarned = true;
608
- console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} verifying signed leases against the PACK trust root — ` +
609
- 'no lease trust root is configured on this instance. The control plane publishes lease keys under ' +
610
- 'purpose `lease-signing` and refuses to register one kid under two purposes, so this stops working ' +
611
- 'the moment the plane rotates to a dedicated lease key — and requireSignedLease is sticky with no ' +
612
- 'plane-side undo, so that failure cannot be fixed from the console. Set ' +
613
- `${signature_1.LEASE_JWKS_URL_ENV}=https://<plane>/.well-known/jwks.json?purpose=lease-signing ` +
614
- `(or ${signature_1.LEASE_SIGNING_KEYS_ENV} with inline {"<kid>":"<public PEM>"} for an air-gapped install).` +
615
- (leaseTrust.reason ? ` The derived lease trust root is unusable: ${leaseTrust.reason}` : ''));
616
- }
617
- return selected.keys;
618
- };
619
- const leaseSource = (0, lease_1.createLeaseSource)({
620
- url: process.env.AGENTHIPPO_LEASE_URL,
621
- // The poll IS the registration — see LeaseSourceOptions.identity.
622
- identity: () => ({
623
- instance: instanceId,
624
- pack: packId,
625
- // The version actually being served: after a pull this is the pinned one, and the
626
- // console's whole convergence view depends on it being the truth rather than boot state.
627
- version: activeVersionRef.get(),
628
- tier: options.sandbox === 'container' ? 2 : options.sandbox ? 0 : undefined,
629
- sandboxes: sandboxRunnerRef?.sandboxes().length,
630
- // Which engine, and which BUILD of it. Pack version alone cannot explain a fleet-wide
631
- // behaviour change that came from an engine upgrade, and the CLI is not engine-version
632
- // agnostic, so the console needs both to answer "what is actually running?".
633
- engine: defaultEngine,
634
- engineversion: (0, deps_1.installedEngineVersion)(defaultEngine),
635
- }),
636
- // Derived from the poll interval by ONE function, so the two can no longer drift. Worst-case
637
- // kill latency is poll + cache, and that is the number the boot banner prints.
638
- cacheSeconds: (0, lease_1.leaseCacheSecondsFor)(leasePollSeconds),
639
- // Signed-lease verification uses the LEASE trust root, falling back to the pack one while no
640
- // lease root is configured. Read through a closure so a keyring that arrives on a later poll
641
- // is picked up without rebuilding this.
642
- signingKeys: () => leaseSigningKeys(),
643
- requireSignedLease: options.requireSignedLease,
644
- instanceId,
645
- ...(options.leaseMaxStalenessSeconds !== undefined ? { maxStalenessSeconds: options.leaseMaxStalenessSeconds } : {}),
646
- logger: {
647
- // Lease warnings are security-relevant (unsigned lease, staleness ceiling, insecure URL)
648
- // and must not be hidden behind --verbose.
649
- info: (m) => logger.info(m),
650
- warn: (m) => console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} ${m}`),
651
- },
652
- });
653
- /**
654
- * Retry the trust root while it is missing, and re-verify once it arrives.
655
- *
656
- * Only while MISSING: a loaded trust root is not re-fetched, so key rotation still needs the
657
- * documented restart and a compromised plane cannot swap the trust root under a running
658
- * instance mid-flight. This exists solely so a transient failure at boot is transient — the
659
- * `unavailable` status is exactly what makes "transient" distinguishable from "not configured".
660
- */
661
- const retrySigningKeys = async () => {
662
- if (packTrust.status !== 'configured') {
663
- const next = await (0, signature_1.loadPackTrustRoot)({ leaseUrl: process.env.AGENTHIPPO_LEASE_URL });
664
- if (next.status === 'configured') {
665
- packTrust = next;
666
- reportTrustRoot(next);
667
- logger.info(`[Pack] signing keys now available (${Object.keys(next.keys).length}); re-verifying`);
668
- verifyPackAt(activePackDir, activeVersion);
669
- }
670
- }
671
- // The lease root gets the SAME rule, not a better one: retried only while missing, never
672
- // re-fetched once configured. Improving one and not the other would leave two roots with two
673
- // rotation stories, which is how the rotation runbook silently stops being true for half of it.
674
- if (leaseTrust.status !== 'configured') {
675
- const next = await (0, signature_1.loadLeaseTrustRoot)({ leaseUrl: process.env.AGENTHIPPO_LEASE_URL });
676
- if (next.status === 'configured') {
677
- leaseTrust = next;
678
- if (next.explicitlyConfigured) {
679
- reportTrustRoot(next, 'lease');
680
- }
681
- logger.info(`[Lease] lease-signing keys now available (${Object.keys(next.keys).length})`);
682
- }
683
- }
684
- };
685
- // Record the boot policy and watch for changes on the lease-poll cadence. The lease source
686
- // caches, so this adds no extra fetches — it re-reads whatever the poll already trusts.
687
- void leaseSource.get().then(notePolicy).catch(() => undefined);
688
- {
689
- const everyMs = leasePollSeconds * 1000;
690
- const policyTimer = setInterval(() => {
691
- void leaseSource.get().then(notePolicy).catch(() => undefined);
692
- void retrySigningKeys().catch(() => undefined);
693
- }, everyMs);
694
- policyTimer.unref?.();
695
- }
696
- if (options.pullPacks) {
697
- fs.mkdirSync(packsRoot, { recursive: true });
698
- // Materialise whatever we boot with, so `current` is valid before any pin is considered and
699
- // the drivers can capture one stable path.
700
- const bootDir = path.join(packsRoot, activeVersion);
701
- if (!fs.existsSync(bootDir)) {
702
- fs.cpSync(pack.packPath, bootDir, { recursive: true });
703
- }
704
- (0, reconcile_1.promoteCurrent)(packsRoot, bootDir);
705
- activePackDir = path.join(packsRoot, reconcile_1.CURRENT_LINK);
706
- await reconcileToPin((await leaseSource.get())?.version);
707
- // The pack is now read through `current`, which is a different path than the one verified
708
- // above. Re-verify so `packSignature` describes the tree that will actually run.
709
- verifyPackAt(activePackDir, activeVersion);
710
- }
711
- activeVersionRef.get = () => activeVersion;
712
- inProcessRunner = (0, pack_runtime_1.createPackRunner)({
713
- storagePath,
714
- analyticsSessionsDir,
715
- packId,
716
- packVersion: activeVersion,
717
- provenance: () => ({
718
- policyVersion: lastPolicyVersion,
719
- packDigest: packDigestValue,
720
- engineVersion: (0, deps_1.installedEngineVersion)(defaultEngine),
721
- tier: options.sandbox === 'container' ? 2 : options.sandbox ? 0 : undefined,
722
- instanceId,
723
- }),
724
- workspaceRoot: options.workspace,
725
- credentials: {
726
- apiKey: apiConfig.apiKey,
727
- baseUrl: apiConfig.baseUrl,
728
- useLiteLLM: apiConfig.useLiteLLM,
729
- litellmApiKey: apiConfig.litellmApiKey,
730
- openaiApiKey: apiConfig.openaiApiKey,
731
- anthropicApiKey: apiConfig.anthropicApiKey,
732
- },
733
- logger,
734
- permissionHandler,
735
- claudeCheapModel,
736
- omitGatewayToken: true,
737
- });
738
- // Converge on the same cadence the fleet already polls, so pinning a version in the console
739
- // reaches instances without a redeploy. Errors are swallowed by reconcileToPin itself.
740
- if (options.pullPacks) {
741
- const every = leasePollSeconds * 1000;
742
- const timer = setInterval(() => {
743
- void leaseSource.get().then((l) => reconcileToPin(l?.version)).catch(() => undefined);
744
- }, every);
745
- timer.unref?.();
746
- }
747
- // Container sandboxes got NO filesystem mounts at all — the driver honours `fs`, but serve never
748
- // passed it, so a sandbox's `/state/workspace` was container-private storage. Everything the
749
- // agent wrote was invisible on the host and lost when the sandbox died. The process driver needs
750
- // no mounts: tier 0 shares this host's filesystem already.
751
- //
752
- // The workspace is PER SESSION unless --shared-workspace says otherwise. One host directory
753
- // mounted writable into every session's sandbox is a shared disk between users of the same
754
- // agent: whatever session A writes (a query export, a downloaded file), session B reads on its
755
- // next turn. That silently contradicts "one disposable sandbox per session" for everything that
756
- // matters, since the container boundary does not extend to a directory both containers mount.
757
- const sharedWorkspace = Boolean(options.sharedWorkspace);
758
- // One sandbox per conversation unless told otherwise. Every key below derives from this, and
759
- // setting it back to `session` is the whole rollback.
760
- const sandboxScope = options.sandboxScope === 'user' ? 'user' : 'session';
761
- const sandboxMounts = options.sandbox === 'container'
762
- ? {
763
- // serve's resolved pack, not the image's baked copy — the two cannot then disagree
764
- // about which pack version is running.
765
- readonly: [`${activePackDir}:${containerDriver_1.SANDBOX_PACK_DIR}`],
766
- writable: [`${options.workspace}:${containerDriver_1.SANDBOX_WORKSPACE_DIR}`],
767
- }
768
- : undefined;
769
- if (sandboxMounts && sharedWorkspace) {
770
- console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} --shared-workspace: every session's sandbox mounts ` +
771
- `${options.workspace} read-write, so sessions can read and overwrite each other's files. ` +
772
- `Only safe when all sessions belong to one trust domain.`);
773
- }
774
- // serve wrote that workspace as root; the sandbox runs non-root and must be able to write it too.
775
- // Boot-time and once, before the first turn can arrive.
776
- let sandboxImageUser;
777
- if (sandboxMounts && options.sandboxImage) {
778
- await (0, support_1.alignSandboxWorkspaceOwnership)({
779
- exec: support_1.dockerExec,
780
- image: options.sandboxImage,
781
- writable: sandboxMounts.writable,
782
- // A failure here is what an operator must see even without --verbose; success is noise.
783
- logger: { info: (m) => logger.info(m), warn: (m) => console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} ${m}`) },
784
- });
785
- sandboxImageUser = await (0, support_1.resolveSandboxImageUser)(support_1.dockerExec, options.sandboxImage);
786
- }
787
- // Engine state must outlive the sandboxes that use it or the conversation restarts every time a
788
- // sandbox is reaped (serve-v4-sandbox §8.4). Keyed at SANDBOX scope, under serve's own state
789
- // dir: one directory per session by default, or one per user under `--sandbox-scope user`,
790
- // where the sessions sharing it stay separate beneath in `provider-sessions/`.
791
- const sandboxStateDirFor = (sandboxKey) => (0, support_1.ensureSandboxStateDir)(path.join(storagePath, 'sandbox-state', (0, state_sync_1.safeSessionId)(sandboxKey)), sandboxImageUser);
792
- /**
793
- * Mounts for ONE sandbox: the pack read-only, and a workspace only that sandbox sees.
794
- *
795
- * The workspace lives beside the engine state it belongs to, so the same reap/prune machinery
796
- * already covers it and the agent's output stays visible on the host — the property the shared
797
- * mount was added for — without being visible to every other sandbox.
798
- *
799
- * Keyed at sandbox scope, never narrower: the in-sandbox host chdir's here on every turn, and
800
- * that is only safe while every session sharing a sandbox resolves the same directory (see the
801
- * INVARIANT note in cli/commands/sandbox.ts). Under user scope this means a user's own sessions
802
- * share a workspace — same trust domain, on the same terms their engine homes already shared.
803
- */
804
- const sandboxFsFor = async (sandboxKey) => {
805
- if (!sandboxMounts) {
806
- return { readonly: [], writable: [] };
807
- }
808
- if (sharedWorkspace) {
809
- return sandboxMounts;
810
- }
811
- const dir = (0, support_1.ensureSandboxStateDir)(path.join(storagePath, 'sandbox-workspace', (0, state_sync_1.safeSessionId)(sandboxKey)), sandboxImageUser);
812
- return { readonly: sandboxMounts.readonly, writable: [`${dir}:${containerDriver_1.SANDBOX_WORKSPACE_DIR}`] };
813
- };
814
- /**
815
- * "May this pack run, at this version, for this session?" — signature requirement, version pin,
816
- * image digest and scoped deny rules, in ONE place.
817
- *
818
- * Both gates call this: sandbox creation (where it decides whether a sandbox may be born) and,
819
- * on deployments with no sandbox, the per-turn governor gate. Two copies of these four checks
820
- * is exactly the drift this codebase keeps paying for, and the no-sandbox path having no copy
821
- * at all is why `requireSignedPack` and a version pin could be switched on in the console and
822
- * enforce nothing on the default deployment shape.
823
- */
824
- const assertPackAdmitted = (currentLease, sessionKey, userId) => {
825
- const required = (0, signature_1.resolveRequireSignedPack)({
826
- // The lease's demand is read FIRST and cannot be waived by any local setting — that is
827
- // what keeps the requirement tighten-only now that the local default is "required".
828
- lease: currentLease?.requireSignedPack,
829
- localRequire: options.requireSignedPack,
830
- localAllowUnsigned: allowUnsignedPack,
831
- });
832
- if (required) {
833
- // RE-VERIFIED, not read off a boot-time boolean. In the default no-sandbox shape the
834
- // engine runs in THIS process against a pack scaffolded with `fileAccess: full`, so the
835
- // agent can edit its own pack directory between turns; a cached "verified once at boot"
836
- // would pass every one of those later turns. `createPackVerifier` keeps this cheap — a
837
- // stat-only probe in the common case, a full digest when the tree looks touched or the
838
- // re-verify interval has elapsed.
839
- const result = packVerifier.verify({
840
- packDir: activePackDir, pack: packId, packVersion: activeVersion, keys: packTrust.keys,
841
- });
842
- if (result !== packSignature) {
843
- packSignature = result;
844
- packDigestValue = result.ok ? result.claims.digest : undefined;
845
- }
846
- if (!result.ok) {
847
- throw new lease_1.LeaseDeniedError(packRefusalText(result));
848
- }
849
- }
850
- // sessionKey makes admission SCOPED: a deny rule naming this session or its user refuses
851
- // at the same gate that enforces the fleet-wide switch.
852
- (0, lease_1.assertAdmitted)(currentLease, {
853
- pack: packId,
854
- version: activeVersion,
855
- image: options.sandboxImage,
856
- sessionKey,
857
- userHash: serve_hardening_1.userNamespaceHash,
858
- userId,
859
- });
860
- };
861
- // Assigned just below; the identity closure above reads it lazily so a poll can report live
862
- // sandbox count without forcing the runner to be constructed before the lease source.
863
- // eslint-disable-next-line prefer-const
864
- // Sandbox mode: the same HeadlessRunner shape, body relocated into a per-session sandbox.
865
- // serve's turn loop below is untouched — see sandbox/sandboxRunner.ts.
866
- const sandboxRunner = options.sandbox
867
- ? (0, sandboxRunner_1.createSandboxRunner)({
868
- driver: options.sandbox === 'container'
869
- // A non-internal route network is a real weakening of the tier-2 claim, so its warning
870
- // must reach an operator who did not pass --verbose.
871
- ? new containerDriver_1.ContainerSandboxDriver({
872
- exec: support_1.dockerExec,
873
- socketOwner: sandboxImageUser,
874
- logger: {
875
- info: (m) => logger.info(m),
876
- warn: (m) => console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} ${m}`),
877
- },
878
- })
879
- : new processDriver_1.ProcessSandboxDriver({
880
- command: process.execPath,
881
- // --workspace is REQUIRED, not optional: a tier-0 sandbox shares this host's
882
- // filesystem, and serve sends this same path as `workspaceRoot` on every run
883
- // frame. Omitting it left the sandbox on the container-image default
884
- // (/state/workspace), so a serve-launched process sandbox died at boot with
885
- // ENOENT: mkdir '/state/workspace'.
886
- // pack.packPath, not options.agent: serve has ALREADY resolved the pack (a
887
- // name resolves against ~/.agent-hippo/agents and the workspace), and the
888
- // sandbox must run that exact directory rather than re-resolve a name from a
889
- // different cwd and possibly land on a different pack.
890
- args: [
891
- // Only forward the script path when the process actually HAS one. A
892
- // single-file build reports `process.argv[1]` as a path inside its own
893
- // virtual filesystem (`/$bunfs/root/index.js` for `bun build --compile`,
894
- // `/snapshot/...` for pkg / node SEA). Passing that made the child parse
895
- // it as the subcommand and die with `unknown command '/$bunfs/...'`, so
896
- // every tier-0 sandbox failed with a blank "sandbox channel closed".
897
- // An existence check is NOT enough to detect this: those runtimes mount
898
- // the virtual path, so `fs.existsSync` cheerfully returns true for it.
899
- ...(0, support_1.sandboxScriptArg)(),
900
- 'sandbox', '--agent', activePackDir, '--stdio',
901
- '--state-dir', storagePath, '--workspace', options.workspace,
902
- ],
903
- // PID/user-namespace wrap where the host allows it (residual P4: on a
904
- // single-uid host, a concurrent tenant reads a turn's nonce from
905
- // /proc/<pid>/environ — and the claude CLI's argv from /proc/<pid>/cmdline,
906
- // which is world-readable). Default 'auto': hardened where possible, honest
907
- // warning where not; 'require' makes multi-tenant hosts fail closed.
908
- namespaceIsolation: (0, processDriver_1.namespaceIsolationFromEnv)(),
909
- logger: {
910
- info: (m) => logger.info(m),
911
- warn: (m) => console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} ${m}`),
912
- },
913
- }),
914
- image: options.sandboxImage ?? '',
915
- tier: options.sandbox === 'container' ? 2 : 0,
916
- ...(sandboxMounts ? { fs: sandboxMounts, fsFor: sandboxFsFor, stateDirFor: sandboxStateDirFor } : {}),
917
- // The only routes a sandbox may reach. Everything else is unroutable, not merely denied.
918
- routes: [
919
- ...(process.env.AGENTHIPPO_DATA_MCP_URL ? [{ kind: 'data', target: process.env.AGENTHIPPO_DATA_MCP_URL }] : []),
920
- ...(apiConfig.baseUrl ? [{ kind: 'inference', target: apiConfig.baseUrl }] : []),
921
- ],
922
- limits: { ttlSeconds: options.sandboxTtlSeconds ?? 30 * 60 },
923
- provenanceEnv: () => (0, runtime_1.buildProvenanceEnv)({
924
- policyVersion: lastPolicyVersion,
925
- packDigest: packDigestValue,
926
- engineVersion: (0, deps_1.installedEngineVersion)(defaultEngine),
927
- tier: options.sandbox === 'container' ? 2 : options.sandbox ? 0 : undefined,
928
- instanceId,
929
- }),
930
- // Kill switch + rollback, checked before a sandbox can exist. Revoking the lease stops
931
- // new work fleet-wide immediately; TTL bounds the sandboxes already running.
932
- // sessionKey, not sandboxKey: deny rules can name a SESSION, and under user scope the
933
- // sandbox key names the user instead — checking that would stop matching them.
934
- admit: async ({ sessionKey }) => {
935
- const currentLease = await leaseSource.get();
936
- assertPackAdmitted(currentLease, sessionKey);
937
- },
938
- inferenceBaseUrl: apiConfig.baseUrl,
939
- // Per-sandbox inference warrant — signed, audience-bound, and naming the sandbox it was
940
- // minted for. The provider key never enters a sandbox; the proxy verifies with a public
941
- // key and can never mint, so a sandbox cannot re-issue itself more budget.
942
- //
943
- // `user` is the verified end user from the turn's credential scope, not the pack: the
944
- // gateway strips caller-supplied x-litellm-metadata-user-id and re-stamps it from this
945
- // claim, so the warrant is the only attribution channel that survives it — stamping
946
- // packId here made every downstream user_id (LiteLLM, spend rows, evidence provenance)
947
- // the pack. Every mint runs inside runWithCredential (create-time on first-turn
948
- // acquire, then per-turn re-mints in RemoteEngine.stream), so the ambient credential is
949
- // the turn's own. An unidentified turn gets a sandbox-scoped marker rather than
950
- // silently borrowing the pack's name — the sandbox key is user-namespaced under either
951
- // scope, so the marker cannot collide across identified users.
952
- mintModelToken: (sandboxId, sandboxKey) => (0, support_1.mintSandboxInferenceWarrant)(warrantKey, {
953
- user: (0, credential_context_1.currentCredential)()?.userId?.trim() || `anon:${sandboxKey}`,
954
- pack: packId,
955
- packVersion: activeVersion,
956
- sandboxId,
957
- tier: options.sandbox === 'container' ? 2 : 0,
958
- // A sandbox outlives one turn, so its model warrant gets the full cap rather than a
959
- // turn-length TTL; the spend cap, not the clock, is what bounds it.
960
- ttlSeconds: 300,
961
- }),
962
- logger,
963
- runtime: { logger, editTracker: undefined, configProvider: undefined, permissionHandler },
964
- })
965
- : undefined;
966
- sandboxRunnerRef = sandboxRunner ?? undefined;
967
- // Caps + kill switch. Enforcement is non-cooperative — it revokes a route or destroys the
968
- // sandbox — so it exists only where there ARE sandboxes. Without --sandbox we say so rather than
969
- // pretending a cap is in force.
970
- // 0 / NaN mean "unlimited" for every one of these — `--max-turns-per-session` has documented
971
- // that meaning since before caps existed, and passing its default 0 through as a real cap
972
- // refused the very first turn (`turn cap reached (1/0)`, observed live).
973
- const positive = (n) => typeof n === 'number' && Number.isFinite(n) && n > 0 ? n : undefined;
974
- const localCaps = {
975
- ...(0, caps_1.capsFromEnv)(),
976
- ...(positive(options.maxTurnsPerSession) !== undefined ? { maxTurnsPerSession: options.maxTurnsPerSession } : {}),
977
- ...(positive(options.maxSessionSeconds) !== undefined ? { maxSessionSeconds: options.maxSessionSeconds } : {}),
978
- ...(positive(options.maxConcurrentSandboxes) !== undefined ? { maxConcurrentSandboxes: options.maxConcurrentSandboxes } : {}),
979
- ...(options.capOnBreach ? { onBreach: options.capOnBreach } : {}),
980
- };
981
- const capsRequested = localCaps.maxTurnsPerSession !== undefined
982
- || localCaps.maxSessionSeconds !== undefined
983
- || localCaps.maxConcurrentSandboxes !== undefined;
984
- // A deployment must be governable whether or not it runs sandboxes. Gating the governor on
985
- // `sandboxRunner` meant a plain `serve` never polled its lease at all: invisible to the control
986
- // plane AND immune to the kill switch, which is the opposite of what an operator configuring
987
- // a lease URL is asking for. Without sandboxes the enforcement that needs one degrades (no route
988
- // revocation, no concurrency bound) but the substance survives — the turn is still refused.
989
- const nullSandboxes = {
990
- sandboxes: () => [],
991
- destroySession: async () => undefined,
992
- revokeRoute: async () => undefined,
993
- // There is nothing here that can stop a turn already executing in this process, so a
994
- // `kill` deny rule degrades to `drain`. The governor now says so loudly instead of
995
- // sweeping an empty list and reporting the rule as applied.
996
- supportsInFlightTermination: false,
997
- };
998
- const governed = Boolean(sandboxRunner || process.env.AGENTHIPPO_LEASE_URL || capsRequested);
999
- const governor = governed
1000
- ? (0, governor_1.createGovernor)({
1001
- // The lease's caps bind; local flags may tighten them and never loosen them.
1002
- caps: (0, caps_1.mergeCaps)((await leaseSource.get())?.caps, localCaps),
1003
- sandboxes: sandboxRunner ?? nullSandboxes,
1004
- getLease: () => leaseSource.get(),
1005
- pollSeconds: leasePollSeconds,
1006
- // Scope for deny rules: the pack this instance runs, and the same hash that namespaces
1007
- // session keys — so the console can name a user by email without serve holding a roster.
1008
- pack: packId,
1009
- userHash: serve_hardening_1.userNamespaceHash,
1010
- // Only when there is no sandbox: with one, the same checks gate a sandbox's BIRTH and
1011
- // running sandboxes are meant to drain on TTL. Without one, this is the only gate that
1012
- // exists, so an unsigned pack or an unpinned version would otherwise keep serving.
1013
- ...(sandboxRunner ? {} : {
1014
- admitPack: (sessionKey, lease) => {
1015
- try {
1016
- // No userId here on purpose: the governor has ALREADY evaluated the deny rules for
1017
- // this turn (with its followed-user map, which this call site cannot see), so the
1018
- // only thing left for this gate is the signature/pin/digest half.
1019
- assertPackAdmitted(lease, sessionKey);
1020
- return { allowed: true };
1021
- }
1022
- catch (err) {
1023
- return { allowed: false, reason: err instanceof Error ? err.message : String(err) };
1024
- }
1025
- },
1026
- }),
1027
- logger,
1028
- audit: (event, fields) => auditEvent(String(fields.sessionKey ?? 'instance'), event, fields),
1029
- })
1030
- : undefined;
1031
- if (capsRequested && !sandboxRunner) {
1032
- console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} caps are configured but --sandbox is not set. Turn and duration caps still refuse the turn; the concurrency cap and route revocation need sandboxes, so those are IGNORED.`);
1033
- }
1034
- const baseRunner = sandboxRunner ?? inProcessRunner;
1035
- const runner = governor ? (0, governor_1.withGovernor)(baseRunner, governor) : baseRunner;
1036
- if (governor) {
1037
- governor.start();
1038
- const p = governor.policy();
1039
- // The banner prints the number an operator will be held to: worst-case kill latency is
1040
- // poll + cache, not the poll alone. It used to print "lease poll 30s" while the cache
1041
- // defaulted to 60, so a kill could take three times as long as advertised.
1042
- const killLatency = (0, lease_1.effectiveKillLatencySeconds)(leasePollSeconds);
1043
- logger.info(`[Serve] control plane: caps turns=${p.maxTurnsPerSession ?? '∞'} session=${p.maxSessionSeconds ?? '∞'}s sandboxes=${p.maxConcurrentSandboxes ?? '∞'} onBreach=${p.onBreach ?? 'revoke-inference'}; lease poll ${leasePollSeconds}s (worst-case kill latency ${killLatency}s; an admin refresh is immediate)`);
1044
- if (!sandboxRunner) {
1045
- logger.info('[Serve] no sandbox driver: a deny rule with mode=kill takes effect as DRAIN — the in-flight turn finishes and only the next one is refused.');
1046
- }
1047
- const leaseStatus = leaseSource.status();
1048
- if (process.env.AGENTHIPPO_LEASE_URL && !leaseStatus.requireSignedLease) {
1049
- logger.info('[Serve] lease signature is NOT required — set --require-signed-lease (or lease.requireSignedLease) once the control plane signs leases.');
1050
- }
1051
- }
1052
- /**
1053
- * Unattended runs (PLAN/agent-identity.md §5). Two gates, BOTH operator-side, plus a third
1054
- * that needs no code here: the identity must be mapped in the connector's role map or the
1055
- * connector fails the call closed exactly as it does for an unmapped human.
1056
- *
1057
- * Nothing in the pack participates. Enabling or disabling this never requires a re-sign.
1058
- */
1059
- const serviceIdentity = (0, serve_hardening_1.resolveServiceIdentity)();
1060
- if (serviceIdentity.error) {
1061
- // A malformed value must not degrade to "anonymous": that shape looks configured and runs
1062
- // every scheduled job with no identity at all.
1063
- console.error(`${utils_1.COLORS.red}Error:${utils_1.COLORS.reset} ${serviceIdentity.error}`);
1064
- process.exit(1);
1065
- }
1066
- if (serviceIdentity.warning) {
1067
- console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} ${serviceIdentity.warning}`);
1068
- }
1069
- const serviceTurnsManaged = Boolean(process.env.AGENTHIPPO_LEASE_URL);
1070
- /** Resolved per turn, not at boot: revoking in the console must take effect on the next poll. */
1071
- const serviceUserIfPermitted = async () => {
1072
- if (!serviceIdentity.identity) {
1073
- return undefined;
1074
- }
1075
- const gate = (0, lease_2.resolveAllowServiceTurns)({
1076
- lease: (await leaseSource.get())?.allowServiceTurns,
1077
- managed: serviceTurnsManaged,
1078
- });
1079
- if (!gate.allowed) {
1080
- // Named gate, so the operator reads which knob to turn rather than "denied".
1081
- auditEvent('service-turn', 'service_turn_denied', {
1082
- identity: (0, identity_1.hashIdentity)(serviceIdentity.identity),
1083
- gate: gate.gate,
1084
- detail: (0, lease_2.describeServiceTurnGate)(gate.gate),
1085
- });
1086
- return undefined;
1087
- }
1088
- return serviceIdentity.identity;
1089
- };
1090
- if (serviceIdentity.identity) {
1091
- const boot = (0, lease_2.resolveAllowServiceTurns)({ lease: undefined, managed: serviceTurnsManaged });
1092
- logger.info(`[Serve] unattended runs: identity configured (${serve_hardening_1.SERVICE_USER_ENV}); `
1093
- + (serviceTurnsManaged
1094
- ? 'the control plane decides per turn via lease.allowServiceTurns — absent means deny'
1095
- : (0, lease_2.describeServiceTurnGate)(boot.gate)));
1096
- }
1097
- if (sandboxRunner) {
1098
- logger.info(`[Serve] sandbox mode: ${options.sandbox} (tier ${options.sandbox === 'container' ? 2 : 0}) — engines run in per-session sandboxes`);
1099
- // A sandbox's ONLY model credential is the per-sandbox warrant, which is meaningful only to an
1100
- // inference proxy. With no proxy configured there is nothing to send it to, so the engine
1101
- // dials the provider bare and the first turn dies with an opaque 401/reconnect loop. Say so
1102
- // at boot — same reason the engine credential check above is a boot check, not a turn check.
1103
- if (!apiConfig.baseUrl) {
1104
- console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} --sandbox without an inference proxy: a sandbox holds no provider key, so model calls will fail 401. Set AGENTHIPPO_LITELLM_BASE_URL.`);
1105
- }
1106
- }
1107
- /** Set when `--gateway-bridge` is enabled; cleared on shutdown. */
1108
- let gatewayBridge = null;
1109
- /** True once the pack is synced and (if bridged) the gateway is connected. Drives GET /ready. */
1110
- let ready = false;
1111
- // Create HTTP server
1112
- let evidenceShipHandle;
1113
- const server = http.createServer(async (req, res) => {
1114
- const cors = (0, serve_hardening_1.corsHeaders)();
1115
- for (const [k, v] of Object.entries(cors)) {
1116
- res.setHeader(k, v);
1117
- }
1118
- if (req.method === 'OPTIONS') {
1119
- res.writeHead(204);
1120
- res.end();
1121
- return;
1122
- }
1123
- try {
1124
- // Liveness — always open.
1125
- if (req.method === 'GET' && req.url === '/health') {
1126
- const health = { status: 'ok' };
1127
- // Reported unconditionally: with only `last_ok_at`, "shipping is off" and "shipping
1128
- // is on but has never once succeeded" look identical to an operator and to alerting.
1129
- health.evidence_ship_enabled = Boolean(evidenceShipHandle);
1130
- const lastOk = evidenceShipHandle?.lastOkAt();
1131
- if (lastOk !== undefined) {
1132
- health.evidence_ship_last_ok_at = new Date(lastOk).toISOString();
1133
- }
1134
- // R-13 gap detection: evidence write failures and permanently-skipped files are
1135
- // COUNTED now rather than swallowed, but a counter nobody can read is the same as no
1136
- // counter. `disabledReason` matters most — "shipping is off because
1137
- // AGENTHIPPO_EVIDENCE_SHIP=0" and "shipping is on and failing" produce the same
1138
- // `enabled: false` shaped alert otherwise.
1139
- const ship = (0, sessionShip_1.getEvidenceShipStatus)();
1140
- health.evidence_ship_disabled_reason = ship.disabledReason ?? null;
1141
- health.evidence_ship_consecutive_failures = ship.consecutiveFailures;
1142
- health.evidence_records_staged = ship.recordsStaged;
1143
- health.evidence_records_shipped = ship.recordsShipped;
1144
- // Non-zero means the record is known to be INCOMPLETE, not merely late.
1145
- health.evidence_records_skipped_too_old = ship.recordsSkippedTooOld;
1146
- health.evidence_files_skipped_too_old = ship.filesSkippedTooOld;
1147
- health.evidence_content_capture = ship.contentCapture;
1148
- if (ship.lastHeartbeatAt !== undefined) {
1149
- health.evidence_ship_last_heartbeat_at = new Date(ship.lastHeartbeatAt).toISOString();
1150
- }
1151
- const writes = (0, sessionEvents_1.getSessionEventWriterStats)();
1152
- health.evidence_writes_ok = writes.written;
1153
- health.evidence_writes_failed = writes.failed;
1154
- if (writes.lastError) {
1155
- health.evidence_write_last_error = {
1156
- at: new Date(writes.lastError.at).toISOString(),
1157
- event_type: writes.lastError.eventType,
1158
- message: writes.lastError.message,
1159
- };
1160
- }
1161
- // Governance surface: an operator asking "is this instance actually governed?" gets
1162
- // the answer here rather than from boot logs that have long since scrolled away.
1163
- const leaseState = leaseSource.status();
1164
- health.lease_configured = Boolean(process.env.AGENTHIPPO_LEASE_URL);
1165
- health.lease_signed = leaseState.signed;
1166
- health.lease_signature_required = leaseState.requireSignedLease;
1167
- health.lease_staleness_seconds = leaseState.stalenessSeconds ?? null;
1168
- health.lease_fail_closed = leaseState.failClosed;
1169
- health.pack_signature_ok = packSignature.ok;
1170
- health.pack_signature_required = (0, signature_1.resolveRequireSignedPack)({
1171
- localRequire: options.requireSignedPack, localAllowUnsigned: allowUnsignedPack,
1172
- });
1173
- health.pack_trust_root = packTrust.status;
1174
- // Reported separately from the pack root: "signed leases are required and the lease
1175
- // keyring is unavailable" is an outage in the making, and it is invisible in
1176
- // `pack_trust_root`. `lease_trust_root_source` distinguishes an operator-named root
1177
- // from the pack-keyring fallback, which is the state that stops working on rotation.
1178
- health.lease_trust_root = leaseTrust.status;
1179
- health.lease_trust_root_source = leaseTrust.explicitlyConfigured ? 'lease' : 'pack-fallback';
1180
- health.kill_in_flight_supported = Boolean(sandboxRunner);
1181
- res.writeHead(200, { 'Content-Type': 'application/json' });
1182
- res.end(JSON.stringify(health));
1183
- return;
1184
- }
1185
- // Readiness — pack synced (+ gateway connected when bridged).
1186
- // Inflight/queue depth for operators and load balancers (serve-v4 P0.1).
1187
- if (req.method === 'GET' && req.url === '/ready') {
1188
- res.writeHead(ready ? 200 : 503, { 'Content-Type': 'application/json' });
1189
- res.end(JSON.stringify({
1190
- ready,
1191
- inflight: turnGate.inflight,
1192
- queued: turnGate.queued,
1193
- max: turnGate.max,
1194
- }));
1195
- return;
1196
- }
1197
- const urlPath = req.url?.split('?')[0] ?? '';
1198
- if (req.method === 'GET' && landingPageHtml && (urlPath === '/' || urlPath === '/index.html')) {
1199
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
1200
- res.end(landingPageHtml);
1201
- return;
1202
- }
1203
- // Chat endpoint
1204
- if (req.method === 'POST' && urlPath === '/v1/chat') {
1205
- const body = await parseBody(req);
1206
- const userId = (0, serve_hardening_1.resolveUserId)(req.headers, body);
1207
- const dataToken = (0, serve_hardening_1.resolveDataToken)(req.headers);
1208
- // Rate-limit on the trusted proxy identity (or client IP), never the client-supplied
1209
- // body user_id — otherwise a caller rotates user_id per request to reset the bucket.
1210
- const rlKey = (0, serve_hardening_1.resolveUserId)(req.headers, undefined) ?? (0, serve_hardening_1.clientIp)(req);
1211
- if (!rateLimiter.tryConsume(rlKey)) {
1212
- res.writeHead(429, { 'Content-Type': 'application/json', 'Retry-After': String(rateLimiter.retryAfterSeconds(rlKey)) });
1213
- res.end(JSON.stringify({ error: 'Rate limit exceeded' }));
1214
- return;
1215
- }
1216
- // User-namespaced session key (secure-obo-design §3.4): kills cross-user session
1217
- // attachment via a guessed session_id. Fails closed on pinned-identity deployments.
1218
- const { sessionKey, sandboxKey, rejected } = (0, serve_hardening_1.resolveServeSessionKey)(body.session_id, userId, sandboxScope);
1219
- if (rejected) {
1220
- sessionEventWriter.append('unverified', (0, base_1.generateRequestId)(), 'warrant_denied', { reason: rejected });
1221
- res.writeHead(403, { 'Content-Type': 'application/json' });
1222
- res.end(JSON.stringify({ error: 'verified user identity required' }));
1223
- return;
1224
- }
1225
- // Acquire before turn-limiter so a capacity reject does not burn a turn slot.
1226
- const release = await turnGate.acquire();
1227
- if (!release) {
1228
- warnGateReject(`TurnGate: rejecting /v1/chat (inflight=${turnGate.inflight} queued=${turnGate.queued} max=${turnGate.max})`);
1229
- res.writeHead(429, {
1230
- 'Content-Type': 'application/json',
1231
- 'Retry-After': String(serve_hardening_1.TURN_GATE_RETRY_AFTER_SECONDS),
1232
- });
1233
- res.end(JSON.stringify({ error: 'At capacity' }));
1234
- return;
1235
- }
1236
- // Turn-scoped data capability: mint a nonce bound to the verified user; the
1237
- // engine gets it via per-spawn env only. Deleted in the finally — a stale
1238
- // engine/tool fails closed at the gateway after that (secure-obo-design §3.1).
1239
- let turnReg = null;
1240
- try {
1241
- if (!turnLimiter.tryConsume(sessionKey)) {
1242
- res.writeHead(429, { 'Content-Type': 'application/json' });
1243
- res.end(JSON.stringify({ error: 'Per-session turn limit reached' }));
1244
- return;
1245
- }
1246
- await prepareSession(sessionKey);
1247
- if (userId?.trim()) {
1248
- turnReg = await registerServeTurn({ userId: userId.trim(), sessionKey, inboundToken: dataToken, edgeNonce: edgeTurnNonce(req.headers) });
1249
- }
1250
- const result = await (0, credential_context_1.runWithCredential)({ userId, dataToken }, () => handleChat(runner, body, sessionKey, sandboxKey, defaultEngine, defaultModel, options.workspace, permissions, userId, turnReg?.env), { onDenied: logWarrantDenied(sessionKey) });
1251
- res.writeHead(200, { 'Content-Type': 'application/json' });
1252
- res.end(JSON.stringify(result));
1253
- statePusher?.schedule(sessionKey); // per-session delta push, off the response path
1254
- }
1255
- finally {
1256
- await turnReg?.release();
1257
- release();
1258
- }
1259
- return;
1260
- }
1261
- // Streaming chat endpoint
1262
- if (req.method === 'POST' && urlPath === '/v1/chat/stream') {
1263
- const body = await parseBody(req);
1264
- const userId = (0, serve_hardening_1.resolveUserId)(req.headers, body);
1265
- const dataToken = (0, serve_hardening_1.resolveDataToken)(req.headers);
1266
- // Rate-limit on the trusted proxy identity (or client IP), never the client-supplied
1267
- // body user_id — otherwise a caller rotates user_id per request to reset the bucket.
1268
- const rlKey = (0, serve_hardening_1.resolveUserId)(req.headers, undefined) ?? (0, serve_hardening_1.clientIp)(req);
1269
- if (!rateLimiter.tryConsume(rlKey)) {
1270
- res.writeHead(429, { 'Content-Type': 'application/json', 'Retry-After': String(rateLimiter.retryAfterSeconds(rlKey)) });
1271
- res.end(JSON.stringify({ error: 'Rate limit exceeded' }));
1272
- return;
1273
- }
1274
- // Same user-namespaced session key + fail-closed rule as /v1/chat above.
1275
- const { sessionKey, sandboxKey, rejected } = (0, serve_hardening_1.resolveServeSessionKey)(body.session_id, userId, sandboxScope);
1276
- if (rejected) {
1277
- sessionEventWriter.append('unverified', (0, base_1.generateRequestId)(), 'warrant_denied', { reason: rejected });
1278
- res.writeHead(403, { 'Content-Type': 'application/json' });
1279
- res.end(JSON.stringify({ error: 'verified user identity required' }));
1280
- return;
1281
- }
1282
- // Acquire BEFORE writeHead(200, SSE) so a reject still gets a clean 429.
1283
- const release = await turnGate.acquire();
1284
- if (!release) {
1285
- warnGateReject(`TurnGate: rejecting /v1/chat/stream (inflight=${turnGate.inflight} queued=${turnGate.queued} max=${turnGate.max})`);
1286
- res.writeHead(429, {
1287
- 'Content-Type': 'application/json',
1288
- 'Retry-After': String(serve_hardening_1.TURN_GATE_RETRY_AFTER_SECONDS),
1289
- });
1290
- res.end(JSON.stringify({ error: 'At capacity' }));
1291
- return;
1292
- }
1293
- // Turn-scoped data capability — same rules as /v1/chat above.
1294
- let turnReg = null;
1295
- try {
1296
- if (!turnLimiter.tryConsume(sessionKey)) {
1297
- res.writeHead(429, { 'Content-Type': 'application/json' });
1298
- res.end(JSON.stringify({ error: 'Per-session turn limit reached' }));
1299
- return;
1300
- }
1301
- await prepareSession(sessionKey);
1302
- if (userId?.trim()) {
1303
- turnReg = await registerServeTurn({ userId: userId.trim(), sessionKey, inboundToken: dataToken, edgeNonce: edgeTurnNonce(req.headers) });
1304
- }
1305
- // Fail-closed credential check MUST run before SSE writeHead(200).
1306
- // Otherwise a warrant denial lands after headers are committed, the outer
1307
- // catch can only append raw JSON (not an SSE error event), and the IDE
1308
- // client's SSE parser falls through to emitter.done() → silent success.
1309
- await (0, credential_context_1.runWithCredential)({ userId, dataToken }, async () => {
1310
- res.writeHead(200, {
1311
- 'Content-Type': 'text/event-stream',
1312
- 'Cache-Control': 'no-cache',
1313
- 'Connection': 'keep-alive',
1314
- });
1315
- await handleStreamingChat(runner, body, sessionKey, sandboxKey, defaultEngine, defaultModel, options.workspace, permissions, res, userId, turnReg?.env);
1316
- }, { onDenied: logWarrantDenied(sessionKey) });
1317
- statePusher?.schedule(sessionKey); // per-session delta push, off the response path
1318
- }
1319
- finally {
1320
- await turnReg?.release();
1321
- release();
1322
- }
1323
- return;
1324
- }
1325
- // 404
1326
- res.writeHead(404, { 'Content-Type': 'application/json' });
1327
- res.end(JSON.stringify({ error: 'Not found' }));
1328
- }
1329
- catch (err) {
1330
- const tooLarge = err?.tooLarge === true;
1331
- if (tooLarge) {
1332
- if (!res.headersSent) {
1333
- // Connection: close — we stopped reading mid-upload; don't reuse this socket.
1334
- res.writeHead(413, { 'Content-Type': 'application/json', 'Connection': 'close' });
1335
- }
1336
- res.end(JSON.stringify({ error: 'Payload too large' }));
1337
- return;
1338
- }
1339
- console.error('Request error:', err);
1340
- if (!res.headersSent) {
1341
- res.writeHead(500, { 'Content-Type': 'application/json' });
1342
- }
1343
- res.end(JSON.stringify({ error: err instanceof Error ? err.message : 'Internal error' }));
1344
- }
1345
- });
1346
- // Start server
1347
- server.listen(options.port, bindHost, () => {
1348
- void (async () => {
1349
- console.log('');
1350
- console.log(BANNER_LINE);
1351
- console.log('Agent Hippo Server');
1352
- console.log(BANNER_LINE);
1353
- console.log(`Host: ${bindHost}`);
1354
- console.log(`Port: ${options.port}`);
1355
- console.log(`Engine: ${defaultEngine}`);
1356
- console.log(`Model: ${defaultModel}`);
1357
- if (claudeCheapModel) {
1358
- console.log(`Cheap: ${claudeCheapModel}`);
1359
- }
1360
- console.log(`Workspace: ${options.workspace}`);
1361
- if (pack) {
1362
- console.log(`Pack: ${pack.manifest.metadata.name}@${pack.manifest.metadata.version}`);
1363
- console.log(`Gateway: ${gatewayAgentId}`);
1364
- }
1365
- console.log(BANNER_LINE);
1366
- console.log('');
1367
- console.log('Endpoints:');
1368
- console.log(` POST http://localhost:${options.port}/v1/chat`);
1369
- console.log(` POST http://localhost:${options.port}/v1/chat/stream`);
1370
- console.log(` GET http://localhost:${options.port}/health`);
1371
- console.log('');
1372
- console.log('Press Ctrl+C to stop.');
1373
- console.log('');
1374
- const shipConfig = (0, sessionShip_1.resolveSessionShipConfig)(instanceId, analyticsSessionsDir, storagePath);
1375
- if (shipConfig) {
1376
- evidenceShipHandle = (0, sessionShip_1.startSessionShipper)({
1377
- ...shipConfig,
1378
- logger: { info: (m) => logger.info(m), warn: (m) => console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} ${m}`) },
1379
- });
1380
- }
1381
- // Bind channels before bridge connect so config.patch reload does not race registration.
1382
- const bindSpec = options.bind?.trim();
1383
- if (bindSpec) {
1384
- try {
1385
- // The captured token — the env var was stripped at boot (§10 hole 2).
1386
- await bindToGatewayChannels(gatewayAgentId, bindSpec, brokerGatewayToken);
1387
- }
1388
- catch (err) {
1389
- console.error(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} Failed to bind channels: ${err instanceof Error ? err.message : String(err)}`);
1390
- }
1391
- }
1392
- if (options.gatewayBridge) {
1393
- // Egress broker (secure-obo-design §7.6): start before the bridge so the URL is in
1394
- // process.env when the first channel turn spawns an engine. The broker holds the
1395
- // gateway token; the agent presents only the turn nonce.
1396
- if (turnRegistry) {
1397
- try {
1398
- egressBroker = await (0, data_plane_1.startEgressBroker)({
1399
- registry: turnRegistry,
1400
- workspaceRoot: options.workspace,
1401
- agentId: pack.manifest.metadata.name,
1402
- send: async (handle, message, media) => {
1403
- const r = await (0, outboundSend_1.sendGatewayOutboundMessage)(handle, { message, media, token: brokerGatewayToken });
1404
- return { ok: r.ok, via: r.via, stdout: r.stdout, stderr: r.stderr };
1405
- },
1406
- logger: options.verbose ? new headless_1.ConsoleLogger('[Egress]') : new headless_1.NoOpLogger(),
1407
- audit: auditEvent,
1408
- });
1409
- process.env[data_plane_1.EGRESS_URL_ENV] = egressBroker.url;
1410
- // The token itself was stripped from process.env at boot, broker or not.
1411
- console.log('OK: Egress broker on loopback — gateway token stays broker-side; agent sends via turn nonce');
1412
- }
1413
- catch (err) {
1414
- // Fail CLOSED: without the broker, AGENTHIPPO_EGRESS_URL stays unset and the
1415
- // runtime would bake the gateway token back into engine children (§7.6 undone
1416
- // by a port-bind hiccup). Refuse to serve rather than degrade silently.
1417
- console.error(`${utils_1.COLORS.red}Error:${utils_1.COLORS.reset} Egress broker failed to start: ${err instanceof Error ? err.message : String(err)}`);
1418
- process.exit(1);
1419
- }
1420
- }
1421
- try {
1422
- gatewayBridge = await startServeGatewayBridge({
1423
- gatewayAgentId,
1424
- packName: pack.manifest.metadata.name,
1425
- workspace: options.workspace,
1426
- registerTurn: registerServeTurn,
1427
- brokerGatewayToken,
1428
- engine: defaultEngine,
1429
- defaultModel,
1430
- runner,
1431
- permissions,
1432
- turnGate,
1433
- rateLimiter,
1434
- turnLimiter,
1435
- warnGateReject,
1436
- prepareSession,
1437
- sandboxScope,
1438
- onTurnComplete: (sessionId) => statePusher?.schedule(sessionId),
1439
- logWarrantDenied,
1440
- serviceUserIfPermitted,
1441
- });
1442
- }
1443
- catch (err) {
1444
- console.error(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} Gateway bridge failed: ${err instanceof Error ? err.message : String(err)}`);
1445
- }
1446
- }
1447
- // Pack is synced (above) and any gateway bridge is connected → ready for traffic.
1448
- ready = true;
1449
- })();
1450
- });
1451
- // Graceful shutdown — drain in-flight work, stop accepting new connections.
1452
- let shuttingDown = false;
1453
- const shutdown = () => {
1454
- if (shuttingDown) {
1455
- return;
1456
- }
1457
- shuttingDown = true;
1458
- ready = false; // /ready flips to 503 so a load balancer stops routing here
1459
- console.log('\nShutting down...');
1460
- void (async () => {
1461
- if (statePusher) {
1462
- try {
1463
- await statePusher.flush(); // final durable push before exit
1464
- }
1465
- catch {
1466
- // best-effort
1467
- }
1468
- }
1469
- if (gatewayBridge) {
1470
- try {
1471
- await gatewayBridge.unregisterAgent(gatewayAgentId);
1472
- }
1473
- catch {
1474
- // ignore
1475
- }
1476
- gatewayBridge = null;
1477
- }
1478
- if (dataPlane) {
1479
- try {
1480
- await dataPlane.close();
1481
- }
1482
- catch {
1483
- // best-effort
1484
- }
1485
- }
1486
- if (egressBroker) {
1487
- try {
1488
- await egressBroker.close();
1489
- }
1490
- catch {
1491
- // best-effort
1492
- }
1493
- }
1494
- // After the planes are closed, so the tail includes whatever they just wrote. SIGTERM is
1495
- // how every redeploy ends, and without this each one drops up to one interval of
1496
- // session evidence — permanently, when the state dir (and its cursor) is ephemeral.
1497
- if (evidenceShipHandle) {
1498
- try {
1499
- await evidenceShipHandle.flush();
1500
- }
1501
- catch {
1502
- // best-effort — never block shutdown on the evidence server being reachable
1503
- }
1504
- }
1505
- server.close(() => {
1506
- runner.dispose();
1507
- process.exit(0);
1508
- });
1509
- // Safety net: force exit if connections don't drain promptly.
1510
- setTimeout(() => {
1511
- runner.dispose();
1512
- process.exit(0);
1513
- }, 10_000).unref();
1514
- })();
1515
- };
1516
- process.on('SIGINT', shutdown);
1517
- process.on('SIGTERM', shutdown);
1518
- }
1519
- async function parseBody(req) {
1520
- const raw = await (0, serve_hardening_1.readBodyWithCap)(req);
1521
- try {
1522
- return JSON.parse(raw);
1523
- }
1524
- catch {
1525
- throw new Error('Invalid JSON');
1526
- }
1527
- }
1528
- async function handleChat(runner, body, sessionKey, sandboxKey, defaultEngine, defaultModel, workspace, permissions, userId, turnEnv) {
1529
- const result = await runner.run({
1530
- engineId: resolveRequestedEngineId(body.engine, defaultEngine),
1531
- modelId: body.model || defaultModel,
1532
- message: body.message,
1533
- workspaceRoot: workspace,
1534
- // The user-namespaced key (never raw body.session_id) so engine conversation
1535
- // state and per-session artifacts are per-user — see resolveServeSessionKey.
1536
- sessionId: sessionKey,
1537
- // Which sandbox runs it. Equal to sessionKey unless --sandbox-scope user; ignored
1538
- // entirely by the in-process runner.
1539
- sandboxKey,
1540
- analyticsUserId: userId,
1541
- permissions,
1542
- turnEnv,
1543
- });
1544
- return {
1545
- success: result.success,
1546
- text: result.text,
1547
- thinking: result.thinking || undefined,
1548
- tool_calls: result.toolCalls,
1549
- error: result.error,
1550
- };
1551
- }
1552
- /**
1553
- * Session key for a gateway turn. Prefers the caller's own session identifiers; when the
1554
- * sender has a resolved identity but no session id, keys off that identity (stable, no
1555
- * collision risk — it's unique to that sender). Only when NEITHER a session id nor an
1556
- * identity is available does this fall back to a per-message unique key, rather than a
1557
- * shared 'gateway-anon' bucket — a shared bucket would let two unrelated, genuinely
1558
- * unidentified senders share one TurnLimiter budget and, when AGENTHIPPO_STATE_S3_URI is
1559
- * set, one S3 session-state prefix (ensureSessionLocal/pushSessionState), letting one
1560
- * anonymous sender's state be restored into another's turn.
1561
- *
1562
- * `durable` is false only for that one-off fallback key. A key that is by construction
1563
- * never reused can never hit its own TurnLimiter cap and can never have prior S3 state to
1564
- * restore or a future turn to restore it for — callers should skip per-session tracking
1565
- * (TurnLimiter, prepareSession/ensureSessionLocal, onTurnComplete/statePusher.schedule) for
1566
- * it entirely, rather than accumulate a permanent, never-useful entry in those stores.
1567
- *
1568
- * `sessionKey` is namespaced by the resolved sender identity when an explicit session id is
1569
- * supplied (secure-obo-design §3.4 — a caller-supplied id must not attach to another
1570
- * sender's engine session). `outboundKey` preserves the raw gateway key: the bridge records
1571
- * reply handles under it (bridge.ts upsertGatewayOutboundHandle), so the outbound-routing
1572
- * file must keep resolving from the un-namespaced key or reply-to-sender breaks.
1573
- */
1574
- function resolveGatewaySessionKey(opts, userId, scope = 'session',
1575
- /**
1576
- * This turn runs as the deployment's SERVICE identity — no human behind it
1577
- * (PLAN/agent-identity.md §5.4). Passed explicitly rather than sniffed from a prefix on
1578
- * `userId`: deriving behaviour from the shape of an identity string is how the `"false"` class
1579
- * of bug happens, and §5.2 deliberately allows identities with no recognisable prefix at all.
1580
- */
1581
- isServiceIdentity = false) {
1582
- if (isServiceIdentity && userId) {
1583
- // Two independent reasons this branch cannot reuse the durable human shape.
1584
- //
1585
- // sessionKey FRESH: TurnLimiter is a monotonic per-session counter with no window, no
1586
- // reset and no decay (serve-hardening.ts). A recurring job on a stable key therefore burns
1587
- // a LIFETIME budget one run per night and, on night maxTurnsPerSession + 1, is rejected
1588
- // with `turn_limit` forever — recovering only on a serve restart. A nightly job that dies
1589
- // after N nights, where N is a cap nobody associates with cron.
1590
- //
1591
- // sandboxKey STABLE: under the default --sandbox-scope session it would otherwise track
1592
- // sessionKey, so "fresh key per run" would silently mean a NEW CONTAINER every night —
1593
- // cold start, a new governor entry per run, and maxSessionSeconds that never accumulates.
1594
- // Holding it stable gives the bot one warm sandbox and, more importantly, ONE handle for
1595
- // caps and the kill switch.
1596
- //
1597
- // Net shape: fresh conversation, warm sandbox.
1598
- const stable = `gateway-${userId}`;
1599
- return {
1600
- sessionKey: `${stable}--${(0, node_crypto_1.randomUUID)()}`,
1601
- sandboxKey: stable,
1602
- // Replies must still route: the egress broker resolves this run's destinations from the
1603
- // raw channel key, so a cron job that messages Slack keeps working.
1604
- outboundKey: opts.sessionId?.trim() || opts.gatewaySessionKey?.trim() || stable,
1605
- // No prior state to restore and no future turn to restore it for. A batch job that
1606
- // silently accumulates context also silently accumulates cost; continuity across runs
1607
- // stays available by passing a stable session_id, which takes the durable branch below.
1608
- durable: false,
1609
- };
1610
- }
1611
- // The SAME derivation resolveServeSessionKey uses. If the two disagreed, one person reaching
1612
- // the same serve over HTTP and over a channel would get two sandboxes under user scope —
1613
- // neither the old behaviour nor the new one.
1614
- const userSandboxKey = userId ? `u${(0, serve_hardening_1.userNamespaceHash)(userId)}` : undefined;
1615
- const explicit = opts.sessionId?.trim() || opts.gatewaySessionKey?.trim();
1616
- if (explicit) {
1617
- const sessionKey = userId ? `u${(0, serve_hardening_1.userNamespaceHash)(userId)}--${explicit}` : explicit;
1618
- const sandboxKey = scope === 'user' && userSandboxKey ? userSandboxKey : sessionKey;
1619
- return { sessionKey, sandboxKey, outboundKey: explicit, durable: true };
1620
- }
1621
- if (userId) {
1622
- // Already keyed on the sender's own identity — no cross-user attachment surface.
1623
- const sessionKey = `gateway-${userId}`;
1624
- return {
1625
- sessionKey,
1626
- sandboxKey: scope === 'user' && userSandboxKey ? userSandboxKey : sessionKey,
1627
- outboundKey: sessionKey,
1628
- durable: true,
1629
- };
1630
- }
1631
- // randomUUID (122 bits), not generateRequestId (~32 bits) — this is the sole thing
1632
- // standing between two unrelated anonymous senders and a real collision, so it needs to
1633
- // be architecturally collision-free, not just statistically unlikely.
1634
- const anon = `gateway-anon-${(0, node_crypto_1.randomUUID)()}`;
1635
- // Its own sandbox, under either scope: with no identity there is no user to group by, and
1636
- // grouping unidentified senders would be inventing one.
1637
- return { sessionKey: anon, sandboxKey: anon, outboundKey: anon, durable: false };
1638
- }
1639
- /**
1640
- * Gateway-channel turn admission: rate limit → turn gate (capacity) → per-session turn cap,
1641
- * then run. Extracted from the gateway bridge's messageHandler so this is directly testable
1642
- * without a live gateway connection — this exact sequence (mirroring the HTTP endpoints'
1643
- * admission order) previously didn't exist for gateway-originated turns at all: channel
1644
- * messages skipped both the identity-keyed rate limiter and the per-session turn cap.
1645
- * Gateway channels can't receive an HTTP 429, so each rejection emits a short channel
1646
- * message instead (best-effort — a channel-notice failure must not mask the rejection).
1647
- */
1648
- async function runGatewayTurn(params) {
1649
- const notify = async (text) => {
1650
- try {
1651
- await params.emitter?.text(text);
1652
- await params.emitter?.done();
1653
- }
1654
- catch {
1655
- // best-effort channel notice
1656
- }
1657
- };
1658
- if (!params.rateLimiter.tryConsume(params.rlKey)) {
1659
- await notify(serve_hardening_1.RATE_LIMIT_BUSY_MESSAGE);
1660
- return { success: false, error: 'rate_limited' };
1661
- }
1662
- // Acquire before the turn-limiter so a capacity reject does not burn a turn slot.
1663
- const release = await params.turnGate.acquire();
1664
- if (!release) {
1665
- params.warnGateReject(`TurnGate: rejecting gateway turn (inflight=${params.turnGate.inflight} queued=${params.turnGate.queued} max=${params.turnGate.max})`);
1666
- await notify(serve_hardening_1.TURN_GATE_BUSY_MESSAGE);
1667
- return { success: false, error: 'busy' };
1668
- }
1669
- try {
1670
- if (params.trackSession !== false && !params.turnLimiter.tryConsume(params.sessionKey)) {
1671
- await notify(serve_hardening_1.TURN_LIMIT_BUSY_MESSAGE);
1672
- return { success: false, error: 'turn_limit' };
1673
- }
1674
- return await params.run();
1675
- }
1676
- finally {
1677
- release();
1678
- }
1679
- }
1680
- /**
1681
- * Register this serve process as an OpenClaw external agent (WebSocket `agent.run` → headless runner).
1682
- * Enables `openclaw agent --agent <packId> --message "…"` against the same gateway.
1683
- */
1684
- async function startServeGatewayBridge(params) {
1685
- const { GatewayBridge: Bridge } = await import('../../gateway/bridge.js');
1686
- const DEFAULT_GATEWAY_PORT = 18789;
1687
- const gatewayUrl = process.env.OPENCLAW_GATEWAY_URL ?? `ws://127.0.0.1:${DEFAULT_GATEWAY_PORT}`;
1688
- // The captured token — OPENCLAW_GATEWAY_TOKEN is stripped from process.env at boot (§7.6,
1689
- // §10 hole 2), so the bridge must never depend on the env var.
1690
- const token = params.brokerGatewayToken;
1691
- const bridge = new Bridge({ gatewayUrl, token });
1692
- await bridge.connect();
1693
- await bridge.registerAgent({
1694
- id: params.gatewayAgentId,
1695
- agentId: params.packName,
1696
- name: `${params.packName} (CLI)`,
1697
- workspace: params.workspace,
1698
- engineId: params.engine,
1699
- syncOpenClawConfigAgent: true,
1700
- messageHandler: (message, opts) => runBridgeTurn(params, message, opts),
1701
- });
1702
- console.log(`${utils_1.COLORS.green}✓${utils_1.COLORS.reset} Gateway bridge: registered "${params.gatewayAgentId}" at ${gatewayUrl}`);
1703
- console.log(`${utils_1.COLORS.dim} Gateway outbound routes: ${params.workspace}/.agent-hippo/sessions/<sessionKey>/gateway-outbound.json${utils_1.COLORS.reset}`);
1704
- console.log(`${utils_1.COLORS.dim} openclaw agent --agent ${params.gatewayAgentId} --message "…"${utils_1.COLORS.reset}`);
1705
- return bridge;
1706
- }
1707
- /**
1708
- * One gateway-bridge turn, extracted from the message handler so the shipped path is directly
1709
- * testable.
1710
- *
1711
- * It was previously an inline closure inside `bridge.registerAgent`, reachable only through a live
1712
- * OpenClaw websocket — so the composition that actually delivers unattended runs (five identity
1713
- * sites, four session-key values, the limiter, the registry) had no test that ran it. Transcribing
1714
- * it into a test would have been a second implementation, free to drift from the one that ships.
1715
- */
1716
- async function runBridgeTurn(params, message, opts) {
1717
- const requestedEngine = opts.engineId?.trim().toLowerCase();
1718
- const effectiveEngine = requestedEngine === 'custom'
1719
- ? params.engine
1720
- : (requestedEngine || params.engine);
1721
- // #4: the channel sender becomes the per-user user_id (analytics + per-user data scoping).
1722
- const peerUser = (0, serve_hardening_1.peerToUserId)(opts.peerInfo);
1723
- // No chat peer ⇒ this is an unattended run (cron, or a channel event with no sender).
1724
- // It may act as the deployment's own service identity if BOTH operator-side gates
1725
- // allow it (PLAN/agent-identity.md §5.6). Never on the anonymous HTTP lane — that is
1726
- // externally reachable, and an unauthenticated caller must not be promotable to a
1727
- // standing identity; this handler is the gateway-bridge lane only.
1728
- const svcUser = peerUser ? undefined : await params.serviceUserIfPermitted?.();
1729
- const effectiveUser = peerUser ?? svcUser;
1730
- // effectiveUser flows to FIVE places, not two. Missing any of them degrades the design
1731
- // silently rather than loudly — see each site below.
1732
- const { sessionKey, sandboxKey, outboundKey, durable } = resolveGatewaySessionKey(opts, effectiveUser, params.sandboxScope, Boolean(svcUser));
1733
- return runGatewayTurn({
1734
- rateLimiter: params.rateLimiter,
1735
- turnLimiter: params.turnLimiter,
1736
- turnGate: params.turnGate,
1737
- warnGateReject: params.warnGateReject,
1738
- // (1) Same identity-keyed cap the HTTP endpoints enforce — a channel sender is already
1739
- // trusted enough here to gate the OBO credential scope below, so it's trusted enough
1740
- // to rate-limit on directly. Unidentified senders share one 'gateway' bucket rather
1741
- // than being exempt from throttling entirely. A service turn gets its OWN bucket:
1742
- // leaving it in the shared pool would let one bot's schedule throttle every
1743
- // anonymous sender, and vice versa.
1744
- rlKey: effectiveUser ?? 'gateway',
1745
- sessionKey,
1746
- trackSession: durable,
1747
- emitter: opts.emitter,
1748
- run: async () => {
1749
- // A one-off anonymous sessionKey can never have prior state to restore, and
1750
- // nothing will ever look it up again to push state for — skip both S3 round-trips.
1751
- if (durable) {
1752
- await params.prepareSession(sessionKey);
1753
- }
1754
- // Channel-originated turns carry no inbound platform token: with the
1755
- // passthrough provider they fail closed at the gateway (an unlinked chat
1756
- // peer gets NO data credential — secure-obo-design §7.5 bind-before-
1757
- // credential); the identity provider maps the peer id server-side instead.
1758
- // egressStoreKey is the RAW gateway key (outboundKey) the bridge records reply
1759
- // handles under — it lets the egress broker resolve this session's bound
1760
- // destinations server-side (§7.6). userId gates BOTH capabilities: an
1761
- // unidentified peer gets neither a nonce nor an egress route.
1762
- // (2) Data authority.
1763
- const turnReg = effectiveUser?.trim()
1764
- ? await params.registerTurn({ userId: effectiveUser.trim(), sessionKey, egressStoreKey: outboundKey })
1765
- : null;
1766
- try {
1767
- // (3) Warrant + spend. The easy site to miss and the expensive one: the
1768
- // warrant's `user` claim reads `currentCredential()?.userId ?? anon:<sandboxKey>`,
1769
- // so registering without ALSO scoping here gives the bot data access under its
1770
- // service identity while its SPEND attributes to `anon:` — outside the per-user
1771
- // meter and unattributable in evidence.
1772
- const result = await (0, credential_context_1.runWithCredential)({ userId: effectiveUser }, () => params.runner.run({
1773
- engineId: effectiveEngine,
1774
- modelId: params.defaultModel,
1775
- message,
1776
- workspaceRoot: params.workspace,
1777
- // Use the resolved sessionKey (not the raw opts fields) so the engine's own
1778
- // session state and the S3 restore/push above key off the same, user-namespaced
1779
- // identity. The outbound-routing file (buildSessionEnvPatch) keeps the raw
1780
- // gateway key — the bridge records reply handles under it, so namespacing it
1781
- // here would break reply-to-sender. See resolveGatewaySessionKey.
1782
- sessionId: sessionKey,
1783
- // (4) Which sandbox runs it — stable across runs for a service
1784
- // identity, so a nightly job reuses one warm container.
1785
- sandboxKey,
1786
- gatewaySessionKey: outboundKey,
1787
- // (5) Attribution. Left unset, Spotlight rows would have no user
1788
- // while the warrant claims the service identity — two attribution
1789
- // channels disagreeing, which is worse than one being wrong.
1790
- analyticsUserId: effectiveUser,
1791
- emitter: opts.emitter,
1792
- permissions: params.permissions,
1793
- turnEnv: turnReg?.env,
1794
- }), { onDenied: params.logWarrantDenied(sessionKey) });
1795
- if (durable) {
1796
- params.onTurnComplete?.(sessionKey);
1797
- }
1798
- return { success: result.success, error: result.error };
1799
- }
1800
- finally {
1801
- await turnReg?.release();
1802
- }
1803
- },
1804
- });
1805
- }
1806
- /**
1807
- * Connect to the gateway and set channel bindings for this agent.
1808
- * Called when --bind is specified (e.g., --bind "telegram,whatsapp" or --bind "*").
1809
- */
1810
- async function bindToGatewayChannels(agentId, bindSpec, token) {
1811
- const { createAgentIDEGatewayClient } = await import('../../gateway/client.js');
1812
- const DEFAULT_GATEWAY_PORT = 18789;
1813
- const gatewayUrl = process.env.OPENCLAW_GATEWAY_URL ?? `ws://127.0.0.1:${DEFAULT_GATEWAY_PORT}`;
1814
- const { client, clientInfo } = createAgentIDEGatewayClient(gatewayUrl, token);
1815
- try {
1816
- await client.connect(clientInfo);
1817
- }
1818
- catch (err) {
1819
- throw new Error(`Cannot connect to gateway at ${gatewayUrl}: ${err instanceof Error ? err.message : String(err)}`);
1820
- }
1821
- try {
1822
- let channels;
1823
- let channelAccountOverrides;
1824
- let statusPayload;
1825
- if (bindSpec.trim() === '*') {
1826
- const ctx = await (0, openclaw_channel_routing_js_1.fetchMeetAgentChannelContext)(client.request.bind(client));
1827
- channels = ctx.rows.map((ch) => ch.id);
1828
- statusPayload = ctx.statusPayload;
1829
- }
1830
- else {
1831
- const parsed = (0, openclaw_channel_routing_js_1.parseChannelBindSpecs)(bindSpec);
1832
- channels = parsed.map((p) => p.channelId);
1833
- channelAccountOverrides = Object.fromEntries(parsed
1834
- .filter((p) => p.accountId)
1835
- .map((p) => [p.channelId, p.accountId]));
1836
- if (Object.keys(channelAccountOverrides).length === 0) {
1837
- channelAccountOverrides = undefined;
1838
- }
1839
- }
1840
- if (channels.length === 0) {
1841
- console.log(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} No channels to bind.`);
1842
- return;
1843
- }
1844
- const patched = await (0, openclaw_channel_routing_js_1.applyAgentChannelBindingsViaConfigPatch)(client.request.bind(client), {
1845
- agentId,
1846
- selectedChannelIds: channels,
1847
- channelAccountOverrides,
1848
- statusPayload,
1849
- });
1850
- if (patched) {
1851
- console.log(`${utils_1.COLORS.green}✓${utils_1.COLORS.reset} Bound ${agentId} to: ${channels.join(', ')}`);
1852
- }
1853
- else {
1854
- console.log(`${utils_1.COLORS.dim}Slack already bound to ${agentId}; skipped config.patch${utils_1.COLORS.reset}`);
1855
- }
1856
- }
1857
- finally {
1858
- client.close();
1859
- }
1860
- }
1861
- async function handleStreamingChat(runner, body, sessionKey, sandboxKey, defaultEngine, defaultModel, workspace, permissions, res, userId, turnEnv) {
1862
- let streamEnded = false;
1863
- const sendEvent = (type, data) => {
1864
- if (streamEnded || res.writableEnded) {
1865
- return;
1866
- }
1867
- res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`);
1868
- };
1869
- const endStream = () => {
1870
- if (streamEnded) {
1871
- return;
1872
- }
1873
- streamEnded = true;
1874
- res.end();
1875
- };
1876
- const emitter = {
1877
- async thinking(delta) {
1878
- sendEvent('thinking', { delta });
1879
- },
1880
- async text(delta) {
1881
- sendEvent('text', { delta });
1882
- },
1883
- async toolStart(name, input, toolCallId) {
1884
- sendEvent('tool_start', { name, input, tool_call_id: toolCallId });
1885
- },
1886
- async toolEnd(name, toolCallId, result, isError) {
1887
- sendEvent('tool_end', { name, tool_call_id: toolCallId, result, is_error: isError });
1888
- },
1889
- async progress(message) {
1890
- sendEvent('progress', { message });
1891
- },
1892
- async question(payload) {
1893
- sendEvent('question', { payload });
1894
- // No sessionResource in serve path → can't use workbench askQuestions. Emit for visibility; return declined so the run continues (see PLAN/ARCHITECTURE.md § Stream Question).
1895
- return {
1896
- selected: [],
1897
- freeText: null,
1898
- declined: true,
1899
- };
1900
- },
1901
- async error(message) {
1902
- sendEvent('error', { message });
1903
- },
1904
- async done() {
1905
- sendEvent('done', {});
1906
- endStream();
1907
- },
1908
- };
1909
- try {
1910
- const result = await runner.run({
1911
- engineId: resolveRequestedEngineId(body.engine, defaultEngine),
1912
- modelId: body.model || defaultModel,
1913
- message: body.message,
1914
- workspaceRoot: workspace,
1915
- // The user-namespaced key (never raw body.session_id) — see resolveServeSessionKey.
1916
- sessionId: sessionKey,
1917
- sandboxKey,
1918
- analyticsUserId: userId,
1919
- emitter,
1920
- permissions,
1921
- turnEnv,
1922
- });
1923
- // A policy refusal is a RESULT, not an exception: `withGovernor` returns
1924
- // `{success:false, error}` without ever touching the emitter. Ignoring it sent the client
1925
- // HTTP 200, zero events, then `done` — a kill switch, a cap breach or a deny rule looked
1926
- // exactly like the agent choosing to say nothing, and the operator's reason reached nobody.
1927
- // /v1/chat already returns the reason in its body; the two endpoints must not disagree.
1928
- if (!result.success && !streamEnded) {
1929
- sendEvent('error', { message: result.error ?? 'refused by policy' });
1930
- }
1931
- if (!streamEnded) {
1932
- sendEvent('done', {});
1933
- endStream();
1934
- }
1935
- }
1936
- catch (err) {
1937
- sendEvent('error', { message: err instanceof Error ? err.message : 'Unknown error' });
1938
- endStream();
1939
- }
1940
- }
1941
- function resolveRequestedEngineId(requestedEngine, fallbackEngine) {
1942
- const normalizedRequested = requestedEngine?.trim().toLowerCase();
1943
- if (!normalizedRequested) {
1944
- return fallbackEngine;
1945
- }
1946
- if (normalizedRequested === 'custom') {
1947
- return fallbackEngine;
1948
- }
1949
- return normalizedRequested;
1950
- }