@agenthippo-ai/agenthippo 0.1.0

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 (480) hide show
  1. package/LICENSE +21 -0
  2. package/bin/agenthippo.js +2 -0
  3. package/out/agentpack/github.js +547 -0
  4. package/out/agentpack/index.js +66 -0
  5. package/out/agentpack/install.js +297 -0
  6. package/out/agentpack/manager.js +441 -0
  7. package/out/agentpack/manifest.js +135 -0
  8. package/out/agentpack/paths.js +475 -0
  9. package/out/agentpack/publish.js +248 -0
  10. package/out/agentpack/stores.js +554 -0
  11. package/out/agentpack/types.js +5 -0
  12. package/out/analytics/dashboard/artifactManager.js +439 -0
  13. package/out/analytics/dashboard/fleetDisplayUtils.js +159 -0
  14. package/out/analytics/dashboard/fleetOrchestrator.js +142 -0
  15. package/out/analytics/dashboard/fleetRegistry.js +79 -0
  16. package/out/analytics/dashboard/fleetRowDefaults.js +87 -0
  17. package/out/analytics/dashboard/fleetScriptAdapter.js +146 -0
  18. package/out/analytics/dashboard/fleetStatusUtils.js +30 -0
  19. package/out/analytics/dashboard/fleetTitleUtils.js +68 -0
  20. package/out/analytics/dashboard/fleetTypes.js +29 -0
  21. package/out/analytics/dashboard/index.js +17 -0
  22. package/out/analytics/dashboard/indexingService.js +422 -0
  23. package/out/analytics/dashboard/localFleetAdapter.js +222 -0
  24. package/out/analytics/dashboard/performanceMonitor.js +76 -0
  25. package/out/analytics/dashboard/processManager.js +209 -0
  26. package/out/analytics/dashboard/provider.js +3885 -0
  27. package/out/analytics/dashboard/queryRunner.js +994 -0
  28. package/out/analytics/dashboard/refreshPolicy.js +16 -0
  29. package/out/analytics/dashboard/scheduledFleetFingerprint.js +22 -0
  30. package/out/analytics/dashboard/scheduledFleetStorage.js +241 -0
  31. package/out/analytics/dashboard/scheduledFleetUtils.js +22 -0
  32. package/out/analytics/dashboard/sessionEventFleetStats.js +118 -0
  33. package/out/analytics/dashboard/sessionJsonlFleet.js +204 -0
  34. package/out/analytics/dashboard/teamViewDiscovery.js +106 -0
  35. package/out/analytics/dashboard/viewsConfigMerge.js +221 -0
  36. package/out/analytics/dashboard/webview/analytics-panel.html +155 -0
  37. package/out/analytics/dashboard/webview/codicon.ttf +0 -0
  38. package/out/analytics/dashboard/webview/index.html +78 -0
  39. package/out/analytics/dashboard/webview/panel.js +2295 -0
  40. package/out/analytics/dashboard/webview/runtime-bridge.js +82 -0
  41. package/out/analytics/dashboard/webview/styles.css +1009 -0
  42. package/out/analytics/lifecycle.js +43 -0
  43. package/out/analytics/service.js +1279 -0
  44. package/out/analytics/sessionEvents.js +231 -0
  45. package/out/auth/bootstrapApply.js +72 -0
  46. package/out/auth/brokerClient.js +126 -0
  47. package/out/auth/callbackUri.js +59 -0
  48. package/out/auth/callbackUriParse.js +59 -0
  49. package/out/auth/githubStoreGate.js +70 -0
  50. package/out/auth/loopbackAuthServer.js +165 -0
  51. package/out/auth/pkce.js +9 -0
  52. package/out/auth/provider.js +239 -0
  53. package/out/auth/sessionStore.js +193 -0
  54. package/out/auth/signInFlow.js +182 -0
  55. package/out/auth/types.js +12 -0
  56. package/out/auth-broker/config.js +62 -0
  57. package/out/auth-broker/jwt.js +105 -0
  58. package/out/auth-broker/pkce.js +57 -0
  59. package/out/auth-broker/server.js +539 -0
  60. package/out/auth-broker/store.js +273 -0
  61. package/out/base-prompts/gateway/openclaw-starter.json5 +144 -0
  62. package/out/base-prompts/gateway/slack-socket-mode-minimal.manifest.json +44 -0
  63. package/out/base-prompts/mcp-servers/.mcp.json +13 -0
  64. package/out/base-prompts/mcp-servers/langfuse-analytics/README.md +142 -0
  65. package/out/base-prompts/mcp-servers/langfuse-analytics/mcp.json +13 -0
  66. package/out/base-prompts/mcp-servers/langfuse-analytics/package-lock.json +1192 -0
  67. package/out/base-prompts/mcp-servers/langfuse-analytics/package.json +23 -0
  68. package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/run-with-env.sh +12 -0
  69. package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/sdk-test.js +102 -0
  70. package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/simple-test.js +137 -0
  71. package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/test-client.js +118 -0
  72. package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/test-server.js +209 -0
  73. package/out/base-prompts/mcp-servers/langfuse-analytics/src/client.ts +431 -0
  74. package/out/base-prompts/mcp-servers/langfuse-analytics/src/index.ts +270 -0
  75. package/out/base-prompts/mcp-servers/langfuse-analytics/tsconfig.json +18 -0
  76. package/out/base-prompts/rules/AGENTS.md +204 -0
  77. package/out/base-prompts/skills/agent-browser/SKILL.md +102 -0
  78. package/out/base-prompts/skills/agent-builder/SKILL.md +73 -0
  79. package/out/base-prompts/skills/agent-builder/references/agent-pack.md +63 -0
  80. package/out/base-prompts/skills/agent-builder/references/crewai.md +66 -0
  81. package/out/base-prompts/skills/agent-builder/references/langchain.md +60 -0
  82. package/out/base-prompts/skills/agent-builder/references/raw-sdk.md +73 -0
  83. package/out/base-prompts/skills/agent-engine-creator/SKILL.md +586 -0
  84. package/out/base-prompts/skills/agent-engine-installer/SKILL.md +106 -0
  85. package/out/base-prompts/skills/agent-engine-installer/scripts/install-engine.mjs +259 -0
  86. package/out/base-prompts/skills/agent-engine-installer/scripts/install-engine.ps1 +189 -0
  87. package/out/base-prompts/skills/agenthippo-cli/SKILL.md +69 -0
  88. package/out/base-prompts/skills/agenthippo-cli/scripts/install.sh +63 -0
  89. package/out/base-prompts/skills/agentpack-creator/SKILL.md +375 -0
  90. package/out/base-prompts/skills/agentpack-creator/references/agent-templates.md +261 -0
  91. package/out/base-prompts/skills/agentpack-creator/references/agents-md-guide.md +195 -0
  92. package/out/base-prompts/skills/agentpack-creator/scripts/init_pack.py +391 -0
  93. package/out/base-prompts/skills/agentpack-creator/scripts/init_pack_test.py +294 -0
  94. package/out/base-prompts/skills/agentpack-testbench/SKILL.md +135 -0
  95. package/out/base-prompts/skills/algorithmic-art/LICENSE.txt +202 -0
  96. package/out/base-prompts/skills/algorithmic-art/SKILL.md +405 -0
  97. package/out/base-prompts/skills/algorithmic-art/templates/generator_template.js +223 -0
  98. package/out/base-prompts/skills/algorithmic-art/templates/viewer.html +599 -0
  99. package/out/base-prompts/skills/debug-agent/SKILL.md +83 -0
  100. package/out/base-prompts/skills/desktop-automation/SKILL.md +166 -0
  101. package/out/base-prompts/skills/desktop-automation/references/agenthippo-ui-recipes.md +441 -0
  102. package/out/base-prompts/skills/desktop-automation/scripts/windows-desktop-uia.psm1 +280 -0
  103. package/out/base-prompts/skills/docx/LICENSE.txt +30 -0
  104. package/out/base-prompts/skills/docx/SKILL.md +197 -0
  105. package/out/base-prompts/skills/docx/docx-js.md +350 -0
  106. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  107. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  108. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  109. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  110. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  111. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  112. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  113. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  114. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  115. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  116. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  117. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  118. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  119. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  120. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  121. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  122. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  123. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  124. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  125. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  126. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  127. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  128. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  129. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  130. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  131. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  132. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  133. package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  134. package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  135. package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  136. package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  137. package/out/base-prompts/skills/docx/ooxml/schemas/mce/mc.xsd +75 -0
  138. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
  139. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
  140. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
  141. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
  142. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
  143. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  144. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
  145. package/out/base-prompts/skills/docx/ooxml/scripts/pack.py +159 -0
  146. package/out/base-prompts/skills/docx/ooxml/scripts/unpack.py +29 -0
  147. package/out/base-prompts/skills/docx/ooxml/scripts/validate.py +69 -0
  148. package/out/base-prompts/skills/docx/ooxml/scripts/validation/__init__.py +15 -0
  149. package/out/base-prompts/skills/docx/ooxml/scripts/validation/base.py +951 -0
  150. package/out/base-prompts/skills/docx/ooxml/scripts/validation/docx.py +274 -0
  151. package/out/base-prompts/skills/docx/ooxml/scripts/validation/pptx.py +315 -0
  152. package/out/base-prompts/skills/docx/ooxml/scripts/validation/redlining.py +279 -0
  153. package/out/base-prompts/skills/docx/ooxml.md +610 -0
  154. package/out/base-prompts/skills/docx/scripts/__init__.py +1 -0
  155. package/out/base-prompts/skills/docx/scripts/document.py +1276 -0
  156. package/out/base-prompts/skills/docx/scripts/templates/comments.xml +3 -0
  157. package/out/base-prompts/skills/docx/scripts/templates/commentsExtended.xml +3 -0
  158. package/out/base-prompts/skills/docx/scripts/templates/commentsExtensible.xml +3 -0
  159. package/out/base-prompts/skills/docx/scripts/templates/commentsIds.xml +3 -0
  160. package/out/base-prompts/skills/docx/scripts/templates/people.xml +3 -0
  161. package/out/base-prompts/skills/docx/scripts/utilities.py +374 -0
  162. package/out/base-prompts/skills/gateway-outbound/SKILL.md +136 -0
  163. package/out/base-prompts/skills/mcp-builder/LICENSE.txt +202 -0
  164. package/out/base-prompts/skills/mcp-builder/SKILL.md +328 -0
  165. package/out/base-prompts/skills/mcp-builder/reference/evaluation.md +602 -0
  166. package/out/base-prompts/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
  167. package/out/base-prompts/skills/mcp-builder/reference/node_mcp_server.md +970 -0
  168. package/out/base-prompts/skills/mcp-builder/reference/python_mcp_server.md +719 -0
  169. package/out/base-prompts/skills/mcp-builder/scripts/connections.py +151 -0
  170. package/out/base-prompts/skills/mcp-builder/scripts/evaluation.py +373 -0
  171. package/out/base-prompts/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  172. package/out/base-prompts/skills/mcp-builder/scripts/requirements.txt +2 -0
  173. package/out/base-prompts/skills/openclaw/SKILL.md +406 -0
  174. package/out/base-prompts/skills/openclaw/scripts/run-smoke.sh +61 -0
  175. package/out/base-prompts/skills/pdf/LICENSE.txt +30 -0
  176. package/out/base-prompts/skills/pdf/SKILL.md +294 -0
  177. package/out/base-prompts/skills/pdf/forms.md +205 -0
  178. package/out/base-prompts/skills/pdf/reference.md +612 -0
  179. package/out/base-prompts/skills/pdf/scripts/check_bounding_boxes.py +70 -0
  180. package/out/base-prompts/skills/pdf/scripts/check_bounding_boxes_test.py +226 -0
  181. package/out/base-prompts/skills/pdf/scripts/check_fillable_fields.py +12 -0
  182. package/out/base-prompts/skills/pdf/scripts/convert_pdf_to_images.py +35 -0
  183. package/out/base-prompts/skills/pdf/scripts/create_validation_image.py +41 -0
  184. package/out/base-prompts/skills/pdf/scripts/extract_form_field_info.py +152 -0
  185. package/out/base-prompts/skills/pdf/scripts/fill_fillable_fields.py +114 -0
  186. package/out/base-prompts/skills/pdf/scripts/fill_pdf_form_with_annotations.py +108 -0
  187. package/out/base-prompts/skills/pptx/LICENSE.txt +30 -0
  188. package/out/base-prompts/skills/pptx/SKILL.md +484 -0
  189. package/out/base-prompts/skills/pptx/html2pptx.md +625 -0
  190. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  191. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  192. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  193. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  194. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  195. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  196. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  197. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  198. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  199. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  200. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  201. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  202. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  203. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  204. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  205. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  206. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  207. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  208. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  209. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  210. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  211. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  212. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  213. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  214. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  215. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  216. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  217. package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  218. package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  219. package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  220. package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  221. package/out/base-prompts/skills/pptx/ooxml/schemas/mce/mc.xsd +75 -0
  222. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
  223. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
  224. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
  225. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
  226. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
  227. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  228. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
  229. package/out/base-prompts/skills/pptx/ooxml/scripts/pack.py +159 -0
  230. package/out/base-prompts/skills/pptx/ooxml/scripts/unpack.py +29 -0
  231. package/out/base-prompts/skills/pptx/ooxml/scripts/validate.py +69 -0
  232. package/out/base-prompts/skills/pptx/ooxml/scripts/validation/__init__.py +15 -0
  233. package/out/base-prompts/skills/pptx/ooxml/scripts/validation/base.py +951 -0
  234. package/out/base-prompts/skills/pptx/ooxml/scripts/validation/docx.py +274 -0
  235. package/out/base-prompts/skills/pptx/ooxml/scripts/validation/pptx.py +315 -0
  236. package/out/base-prompts/skills/pptx/ooxml/scripts/validation/redlining.py +279 -0
  237. package/out/base-prompts/skills/pptx/ooxml.md +427 -0
  238. package/out/base-prompts/skills/pptx/scripts/html2pptx.js +984 -0
  239. package/out/base-prompts/skills/pptx/scripts/inventory.py +1020 -0
  240. package/out/base-prompts/skills/pptx/scripts/rearrange.py +231 -0
  241. package/out/base-prompts/skills/pptx/scripts/replace.py +385 -0
  242. package/out/base-prompts/skills/pptx/scripts/thumbnail.py +450 -0
  243. package/out/base-prompts/skills/prompt-lab/SKILL.md +149 -0
  244. package/out/base-prompts/skills/prompt-lab/scripts/query-analytics.sh +2551 -0
  245. package/out/base-prompts/skills/skill-creator/LICENSE.txt +202 -0
  246. package/out/base-prompts/skills/skill-creator/SKILL.md +371 -0
  247. package/out/base-prompts/skills/skill-creator/references/output-patterns.md +82 -0
  248. package/out/base-prompts/skills/skill-creator/references/workflows.md +28 -0
  249. package/out/base-prompts/skills/skill-creator/scripts/init_skill.py +303 -0
  250. package/out/base-prompts/skills/skill-creator/scripts/package_skill.py +110 -0
  251. package/out/base-prompts/skills/skill-creator/scripts/quick_validate.py +95 -0
  252. package/out/base-prompts/skills/skill-installer/SKILL.md +251 -0
  253. package/out/base-prompts/skills/skill-installer/references/store-api.md +219 -0
  254. package/out/base-prompts/skills/spotlight-dashboard/SKILL.md +83 -0
  255. package/out/base-prompts/skills/spotlight-insights/SKILL.md +101 -0
  256. package/out/base-prompts/skills/spotlight-optimize/SKILL.md +93 -0
  257. package/out/base-prompts/skills/spotlight-optimize/scripts/query-analytics.sh +2551 -0
  258. package/out/base-prompts/skills/spotlight-query/SKILL.md +314 -0
  259. package/out/base-prompts/skills/spotlight-query/references/otel-schema.md +163 -0
  260. package/out/base-prompts/skills/spotlight-query/scripts/query-analytics.sh +2551 -0
  261. package/out/base-prompts/skills/spotlight-query/scripts/query-analytics.test.sh +744 -0
  262. package/out/base-prompts/skills/spotlight-query/scripts/test-fixtures/analytics/sessions/sess-test-1.jsonl +14 -0
  263. package/out/base-prompts/skills/spotlight-query/scripts/test-fixtures/analytics/traces/.duckdb-index.json +7 -0
  264. package/out/base-prompts/skills/spotlight-query/scripts/test-fixtures/analytics/traces/sample.jsonl +5 -0
  265. package/out/base-prompts/skills/spotlight-report/SKILL.md +86 -0
  266. package/out/base-prompts/skills/spotlight-visualize/SKILL.md +92 -0
  267. package/out/base-prompts/skills/spotlight-visualize/scripts/query-analytics.sh +2551 -0
  268. package/out/base-prompts/skills/xlsx/LICENSE.txt +30 -0
  269. package/out/base-prompts/skills/xlsx/SKILL.md +289 -0
  270. package/out/base-prompts/skills/xlsx/recalc.py +178 -0
  271. package/out/branding.js +23 -0
  272. package/out/cli/commands/anywhere.js +66 -0
  273. package/out/cli/commands/ask.js +243 -0
  274. package/out/cli/commands/auth-broker.js +24 -0
  275. package/out/cli/commands/auth.js +219 -0
  276. package/out/cli/commands/chat.js +354 -0
  277. package/out/cli/commands/deploy.js +223 -0
  278. package/out/cli/commands/gateway.js +160 -0
  279. package/out/cli/commands/pack.js +220 -0
  280. package/out/cli/commands/probe-custom-engine.js +82 -0
  281. package/out/cli/commands/serve.js +583 -0
  282. package/out/cli/commands/store.js +919 -0
  283. package/out/cli/credential-context.js +113 -0
  284. package/out/cli/deploy-init.js +2087 -0
  285. package/out/cli/deps.js +238 -0
  286. package/out/cli/index.js +390 -0
  287. package/out/cli/pack-loader.js +376 -0
  288. package/out/cli/run-custom-engine-runner.js +318 -0
  289. package/out/cli/runtime.js +409 -0
  290. package/out/cli/serve-hardening.js +325 -0
  291. package/out/cli/state-sync.js +135 -0
  292. package/out/cli/utils.js +220 -0
  293. package/out/cli/vscode-shim.js +86 -0
  294. package/out/engines/base.js +159 -0
  295. package/out/engines/claude.js +1215 -0
  296. package/out/engines/codex.js +759 -0
  297. package/out/engines/custom.js +632 -0
  298. package/out/engines/customEngineIpc.js +95 -0
  299. package/out/engines/customEngineRunnerEmbed.js +107 -0
  300. package/out/engines/customRegistry.js +274 -0
  301. package/out/engines/embeddedCustomEngineRunner.generated.js +9 -0
  302. package/out/engines/engineApiKey.js +138 -0
  303. package/out/engines/factory.js +45 -0
  304. package/out/engines/install.js +372 -0
  305. package/out/engines/persistentEngineWorker.js +320 -0
  306. package/out/engines/publish.js +341 -0
  307. package/out/engines/types.js +17 -0
  308. package/out/examples/run-headless-repl.js +291 -0
  309. package/out/examples/run-headless.js +187 -0
  310. package/out/extension.js +4472 -0
  311. package/out/gateway/bridge.js +805 -0
  312. package/out/gateway/cli-agent-id.js +36 -0
  313. package/out/gateway/client.js +303 -0
  314. package/out/gateway/client.test.js +395 -0
  315. package/out/gateway/gateway-scope-errors.js +14 -0
  316. package/out/gateway/gatewayBackup.js +76 -0
  317. package/out/gateway/gatewayRouteIndex.js +371 -0
  318. package/out/gateway/gatewaySessionStore.js +243 -0
  319. package/out/gateway/index.js +31 -0
  320. package/out/gateway/lifecycle.js +3056 -0
  321. package/out/gateway/openclaw-channel-routing.js +317 -0
  322. package/out/gateway/outboundHandles.js +373 -0
  323. package/out/gateway/outboundPaths.js +72 -0
  324. package/out/gateway/outboundSend.js +254 -0
  325. package/out/gateway/protocol.js +48 -0
  326. package/out/gateway/resetMappings.js +52 -0
  327. package/out/gateway/service.js +1244 -0
  328. package/out/gateway/sessionRouting.js +77 -0
  329. package/out/headless/index.js +505 -0
  330. package/out/headless/sync.js +473 -0
  331. package/out/mcp/publish.js +143 -0
  332. package/out/mcp.js +597 -0
  333. package/out/permissions.js +108 -0
  334. package/out/permissions.mapping.yaml +68 -0
  335. package/out/registry/index.js +332 -0
  336. package/out/remote/handleRemoteChat.js +122 -0
  337. package/out/remote/remotePackStatusBar.js +103 -0
  338. package/out/remote/serveClient.js +216 -0
  339. package/out/remoteSkills.js +191 -0
  340. package/out/runtime/index.js +32 -0
  341. package/out/runtime/interfaces/config-provider.js +32 -0
  342. package/out/runtime/interfaces/edit-tracker.js +22 -0
  343. package/out/runtime/interfaces/index.js +29 -0
  344. package/out/runtime/interfaces/logger.js +40 -0
  345. package/out/runtime/interfaces/permission-handler.js +66 -0
  346. package/out/runtime/interfaces/session-store.js +342 -0
  347. package/out/runtime/vscode/index.js +20 -0
  348. package/out/runtime/vscode/vscode-config-provider.js +77 -0
  349. package/out/runtime/vscode/vscode-edit-tracker.js +120 -0
  350. package/out/runtime/vscode/vscode-logger.js +30 -0
  351. package/out/runtime/vscode/vscode-permission-handler.js +72 -0
  352. package/out/runtime/vscode/vscode-session-store.js +22 -0
  353. package/out/scripts/agentide-litellm.ps1 +501 -0
  354. package/out/scripts/agentide-litellm.sh +700 -0
  355. package/out/scripts/agentide_litellm_hooks_v1.py +604 -0
  356. package/out/scripts/analytics/agent-fleet-widget.js +954 -0
  357. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +160 -0
  358. package/out/scripts/analytics/default-seed-scripts.json +12 -0
  359. package/out/scripts/analytics/duckdbLocator.js +29 -0
  360. package/out/scripts/analytics/fleet-local-adapter.js +246 -0
  361. package/out/scripts/analytics/fleet-workflow.sh +263 -0
  362. package/out/scripts/analytics/generate-default-charts.py +685 -0
  363. package/out/scripts/analytics/hotspot-analysis-widget.js +909 -0
  364. package/out/scripts/analytics/index-traces.py +1394 -0
  365. package/out/scripts/analytics/install-analytics.sh +286 -0
  366. package/out/scripts/analytics/live-fleet-4-collector.cjs +315 -0
  367. package/out/scripts/analytics/live-fleet-4-daemon.mjs +134 -0
  368. package/out/scripts/analytics/live-fleet-4-widget.cjs +433 -0
  369. package/out/scripts/analytics/otel-collector.yaml +55 -0
  370. package/out/scripts/analytics/query-analytics.ps1 +396 -0
  371. package/out/scripts/analytics/query-analytics.sh +2551 -0
  372. package/out/scripts/analytics/session-comparison-widget.js +1468 -0
  373. package/out/scripts/analytics/session-timeline-widget.js +2137 -0
  374. package/out/scripts/analytics/setup-analytics.ps1 +432 -0
  375. package/out/scripts/analytics/setup-analytics.sh +854 -0
  376. package/out/scripts/analytics/start-analytics.ps1 +169 -0
  377. package/out/scripts/analytics/start-analytics.sh +323 -0
  378. package/out/scripts/analytics/token-cost-flamegraph-widget.js +954 -0
  379. package/out/scripts/analytics/token-cost-trend-widget.js +1062 -0
  380. package/out/scripts/analytics/workflows-collector.cjs +315 -0
  381. package/out/scripts/analytics/workflows-daemon.mjs +186 -0
  382. package/out/scripts/analytics/workflows-widget.cjs +433 -0
  383. package/out/scripts/analytics/wow-model-widget.js +191 -0
  384. package/out/scripts/gateway/Dockerfile.serve +45 -0
  385. package/out/scripts/gateway/docker-serve.sh +248 -0
  386. package/out/scripts/gateway/setup-gateway-server.ps1 +953 -0
  387. package/out/scripts/gateway/setup-gateway-server.sh +935 -0
  388. package/out/scripts/gateway/setup-gateway.ps1 +1813 -0
  389. package/out/scripts/gateway/setup-gateway.sh +1541 -0
  390. package/out/scripts/setup-cli.ps1 +86 -0
  391. package/out/scripts/setup-cli.sh +117 -0
  392. package/out/scripts/tools-manifest.json +83 -0
  393. package/out/scripts/vendor-tools.mjs +234 -0
  394. package/out/session.js +272 -0
  395. package/out/shared/agentEngines.js +170 -0
  396. package/out/shared/index.js +109 -0
  397. package/out/shared/models.js +235 -0
  398. package/out/shared/providers.js +202 -0
  399. package/out/skills/publish.js +178 -0
  400. package/out/spotlight/install.js +339 -0
  401. package/out/spotlight/publish.js +544 -0
  402. package/out/spotlight/yaml.js +31 -0
  403. package/out/store/github/setup.js +235 -0
  404. package/out/store/github/templates/build-index.ts +339 -0
  405. package/out/store/github/templates/build-index.yml +97 -0
  406. package/out/store/github/templates/package.json +11 -0
  407. package/out/store/index.js +755 -0
  408. package/out/store/oramaIndex.js +884 -0
  409. package/out/store/shared.js +241 -0
  410. package/out/sync.js +1827 -0
  411. package/out/terminalLogger.js +358 -0
  412. package/out/test/agentEnvFile.test.js +97 -0
  413. package/out/test/agentYaml.test.js +373 -0
  414. package/out/test/agentpack.test.js +639 -0
  415. package/out/test/analytics-lifecycle.test.js +174 -0
  416. package/out/test/analytics.test.js +524 -0
  417. package/out/test/auth-bootstrapApply.test.js +102 -0
  418. package/out/test/auth-broker.test.js +434 -0
  419. package/out/test/auth-callback.test.js +62 -0
  420. package/out/test/branding.test.js +229 -0
  421. package/out/test/cli-agent-id.test.js +55 -0
  422. package/out/test/credential-context.test.js +105 -0
  423. package/out/test/customEngineInstall.test.js +100 -0
  424. package/out/test/customRegistry.test.js +122 -0
  425. package/out/test/deploy-init.test.js +532 -0
  426. package/out/test/deps-auto-install.test.js +81 -0
  427. package/out/test/engine-integration.test.js +297 -0
  428. package/out/test/enginePaths.test.js +195 -0
  429. package/out/test/enginePublish.test.js +82 -0
  430. package/out/test/fleetDisplayUtils.test.js +96 -0
  431. package/out/test/gateway-client.test.js +381 -0
  432. package/out/test/gateway-lifecycle.test.js +608 -0
  433. package/out/test/gateway-outbound.test.js +531 -0
  434. package/out/test/gateway-reset-mappings.test.js +221 -0
  435. package/out/test/gateway-route-index.test.js +235 -0
  436. package/out/test/gateway-session-routing.test.js +47 -0
  437. package/out/test/gateway-session-store.test.js +216 -0
  438. package/out/test/github.test.js +232 -0
  439. package/out/test/headless-sync.test.js +279 -0
  440. package/out/test/helpers/manifest-fixtures.js +27 -0
  441. package/out/test/mcp-obo.test.js +82 -0
  442. package/out/test/mcp.test.js +307 -0
  443. package/out/test/openclaw-channel-routing.test.js +191 -0
  444. package/out/test/pathSafety.test.js +63 -0
  445. package/out/test/permissions.test.js +175 -0
  446. package/out/test/persistent-engine-worker.test.js +220 -0
  447. package/out/test/refreshPolicy.test.js +60 -0
  448. package/out/test/registry.test.js +351 -0
  449. package/out/test/runtime.test.js +349 -0
  450. package/out/test/scheduledFleetStorage.test.js +72 -0
  451. package/out/test/serve-hardening.test.js +192 -0
  452. package/out/test/serveClient.test.js +173 -0
  453. package/out/test/sessionJsonlFleet.test.js +78 -0
  454. package/out/test/shared.test.js +173 -0
  455. package/out/test/skills-mcp.test.js +299 -0
  456. package/out/test/spotlightPublish.test.js +299 -0
  457. package/out/test/spotlightYaml.test.js +58 -0
  458. package/out/test/state-sync-peer.test.js +74 -0
  459. package/out/test/store.test.js +331 -0
  460. package/out/test/stores.test.js +504 -0
  461. package/out/test/sync-switch.test.js +251 -0
  462. package/out/test/teamViewDiscovery.test.js +90 -0
  463. package/out/test/toolPath.test.js +120 -0
  464. package/out/test/utils.test.js +308 -0
  465. package/out/test/viewsConfigMerge.test.js +180 -0
  466. package/out/test/windows-command-line.test.js +75 -0
  467. package/out/utils/agentEnvFile.js +131 -0
  468. package/out/utils/binary.js +175 -0
  469. package/out/utils/index.js +37 -0
  470. package/out/utils/network.js +74 -0
  471. package/out/utils/pathSafety.js +60 -0
  472. package/out/utils/paths.js +116 -0
  473. package/out/utils/pid.js +180 -0
  474. package/out/utils/pythonRunner.js +258 -0
  475. package/out/utils/resolveNodeBinary.js +168 -0
  476. package/out/utils/toolPath.js +83 -0
  477. package/out/utils/windowsCommandLine.js +29 -0
  478. package/out/utils.js +915 -0
  479. package/out/workspaceFolders.js +59 -0
  480. package/package.json +41 -0
@@ -0,0 +1,2087 @@
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.AGENTHIPPO_CLI_NPM_VERSION_FALLBACK = exports.AGENTHIPPO_CLI_NPM_PACKAGE = exports.DEPLOY_PUBLIC_HOSTNAME = exports.DEFAULT_DEPLOY_PUBLIC_HOSTNAME = exports.DEPLOY_TARGETS = void 0;
40
+ exports.isDeployTarget = isDeployTarget;
41
+ exports.defaultRuntimeImage = defaultRuntimeImage;
42
+ exports.serveCmdArgs = serveCmdArgs;
43
+ exports.resolveAgentHippoCliNpmVersion = resolveAgentHippoCliNpmVersion;
44
+ exports.resolveDeployPublicHostname = resolveDeployPublicHostname;
45
+ exports.scanEnvRefs = scanEnvRefs;
46
+ exports.renderEnvExample = renderEnvExample;
47
+ exports.renderAuthEnvExample = renderAuthEnvExample;
48
+ exports.renderTarget = renderTarget;
49
+ /**
50
+ * `agenthippo deploy init` — generator for platform-native deploy artifacts
51
+ * (serve-v3 §A4 / P2). We *generate templates into the pack repo*; we never run
52
+ * `docker build` or call a cloud. The customer's security team reads a 5-line
53
+ * Dockerfile and their CI/CLI does the rest.
54
+ *
55
+ * Tiers (serve-v3 §3): vm = T0.5/T1, compose = T2, cloudrun/cloudflare =
56
+ * T1-generic, databricks-app/spcs = T1-native.
57
+ *
58
+ * Deployment model (org IdP vs generated staging): see enterprise-deploy/README.md in the repo root.
59
+ * `deploy init` only writes templates; the customer runs scripts in deploy-out/ to deploy.
60
+ *
61
+ * This module is pure (string in, string out) apart from `scanEnvRefs`, which
62
+ * reads the pack to make its secret surface self-documenting. Kept separate
63
+ * from the command so it is unit-testable without fs or a cloud.
64
+ */
65
+ const fs = __importStar(require("fs"));
66
+ const path = __importStar(require("path"));
67
+ const serve_hardening_1 = require("./serve-hardening");
68
+ exports.DEPLOY_TARGETS = ['vm', 'compose', 'cloudrun', 'databricks-app', 'spcs', 'cloudflare'];
69
+ function isDeployTarget(value) {
70
+ return exports.DEPLOY_TARGETS.includes(value);
71
+ }
72
+ /** Default runtime image for a pack engine (cursor needs the /engines layer). */
73
+ function defaultRuntimeImage(engine) {
74
+ return engine.trim().toLowerCase() === 'cursor'
75
+ ? 'ghcr.io/agenthippo/runtime:latest-cursor'
76
+ : 'ghcr.io/agenthippo/runtime:latest';
77
+ }
78
+ /** `serve` argv baked into generated Dockerfiles (matches agent.yaml engine/model). */
79
+ function serveCmdArgs(ctx, opts) {
80
+ const args = [
81
+ 'serve',
82
+ '--agent',
83
+ '/app/pack',
84
+ '--workspace',
85
+ '/state/workspace',
86
+ '--state-dir',
87
+ '/state',
88
+ '--port',
89
+ String(opts?.port ?? ctx.port),
90
+ ];
91
+ const engine = ctx.engine?.trim().toLowerCase();
92
+ if (engine && !['claude', 'codex'].includes(engine)) {
93
+ args.push('--engine', engine);
94
+ }
95
+ const model = ctx.model?.trim();
96
+ if (model) {
97
+ args.push('--model', model);
98
+ }
99
+ if (opts?.wwwDir) {
100
+ args.push('--www-dir', opts.wwwDir);
101
+ }
102
+ return args;
103
+ }
104
+ const TEXT_EXT = new Set(['.json', '.yaml', '.yml', '.md', '.env', '.example', '.txt', '.mcp']);
105
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'deploy', '.agent-hippo', 'dist', 'out']);
106
+ /** Default public hostname when `--hostname` is not passed to `deploy init`. */
107
+ exports.DEFAULT_DEPLOY_PUBLIC_HOSTNAME = 'auth.agenthippo.ai';
108
+ /** @deprecated Use {@link DEFAULT_DEPLOY_PUBLIC_HOSTNAME}. */
109
+ exports.DEPLOY_PUBLIC_HOSTNAME = exports.DEFAULT_DEPLOY_PUBLIC_HOSTNAME;
110
+ /** npm package for Databricks Apps source deploy (org: agenthippo-ai). */
111
+ exports.AGENTHIPPO_CLI_NPM_PACKAGE = '@agenthippo-ai/cli';
112
+ /** Fallback when monorepo package.json is not present (customer runs published CLI). */
113
+ exports.AGENTHIPPO_CLI_NPM_VERSION_FALLBACK = 'latest';
114
+ /**
115
+ * Pin @agenthippo-ai/cli to the monorepo release version when `deploy init` runs from
116
+ * a dev checkout; otherwise fall back to `latest`.
117
+ */
118
+ function resolveAgentHippoCliNpmVersion() {
119
+ try {
120
+ const pkgPath = path.join(__dirname, '..', '..', '..', '..', 'package.json');
121
+ if (fs.existsSync(pkgPath)) {
122
+ const raw = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
123
+ const v = raw.version?.trim();
124
+ if (v) {
125
+ return v;
126
+ }
127
+ }
128
+ }
129
+ catch {
130
+ // ignore
131
+ }
132
+ return exports.AGENTHIPPO_CLI_NPM_VERSION_FALLBACK;
133
+ }
134
+ function resolveDeployPublicHostname(value) {
135
+ const trimmed = value?.trim();
136
+ return trimmed || exports.DEFAULT_DEPLOY_PUBLIC_HOSTNAME;
137
+ }
138
+ /**
139
+ * Recursively scan a pack for `${env:VAR}` references and return the sorted,
140
+ * de-duplicated variable names. This is what makes `.env.example` honest: the
141
+ * pack's secret surface is whatever it actually interpolates.
142
+ */
143
+ function scanEnvRefs(dir) {
144
+ const found = new Set();
145
+ const re = /\$\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g;
146
+ const walk = (current) => {
147
+ let entries;
148
+ try {
149
+ entries = fs.readdirSync(current, { withFileTypes: true });
150
+ }
151
+ catch {
152
+ return;
153
+ }
154
+ for (const entry of entries) {
155
+ const full = path.join(current, entry.name);
156
+ if (entry.isDirectory()) {
157
+ if (!SKIP_DIRS.has(entry.name)) {
158
+ walk(full);
159
+ }
160
+ continue;
161
+ }
162
+ // `.mcp.json` → ext `.json`; also accept files with a meaningful name even if extensionless config.
163
+ const ext = path.extname(entry.name).toLowerCase();
164
+ if (!TEXT_EXT.has(ext)) {
165
+ continue;
166
+ }
167
+ let text;
168
+ try {
169
+ text = fs.readFileSync(full, 'utf8');
170
+ }
171
+ catch {
172
+ continue;
173
+ }
174
+ for (const m of text.matchAll(re)) {
175
+ found.add(m[1]);
176
+ }
177
+ }
178
+ };
179
+ walk(dir);
180
+ return [...found].sort();
181
+ }
182
+ /**
183
+ * Render `.env.example`. Always seeds the two serve needs (a bearer secret and
184
+ * a model credential) as comments, then lists every scanned pack variable.
185
+ */
186
+ function renderEnvExample(vars, opts) {
187
+ const isCursor = opts?.engine?.trim().toLowerCase() === 'cursor';
188
+ const hostname = resolveDeployPublicHostname(opts?.hostname);
189
+ const lines = [
190
+ '# Generated by `agenthippo deploy init`. Copy to .env and fill in.',
191
+ ];
192
+ if (isCursor) {
193
+ lines.push('# Cursor engine (pack default model: auto):', 'CURSOR_API_KEY= # https://cursor.com/settings → API keys');
194
+ }
195
+ else {
196
+ lines.push('# Model credential — pick ONE path (serve-v3 §10.2):', '# ANTHROPIC_API_KEY= # direct Anthropic', '# CLAUDE_CODE_USE_BEDROCK=1 # existing AWS Bedrock entitlement (no new key)', '# CLAUDE_CODE_USE_VERTEX=1 # existing GCP Vertex entitlement (no new key)', 'ANTHROPIC_API_KEY=');
197
+ }
198
+ if (vars.length > 0) {
199
+ lines.push('', '# Pack-referenced secrets (scanned from ${env:...}):');
200
+ for (const v of vars) {
201
+ lines.push(`${v}=`);
202
+ }
203
+ }
204
+ if (opts?.edge) {
205
+ lines.push('', '# Public hostname — set once; Caddyfile reads {$AGENTHIPPO_PUBLIC_HOSTNAME}.', `AGENTHIPPO_PUBLIC_HOSTNAME=${hostname}`, '', '# Edge auth secrets (JWT signing key, Google OAuth client/cookie) live in .env.auth,', '# which is loaded ONLY by the auth-broker and oauth2-proxy services — never the agent.', '# Keeping the token-signing key out of the agent container limits blast radius if the', '# agent (which runs model-driven tool calls) is compromised. Copy .env.auth.example.', '', '# Cloudflare Tunnel (free — no Access seats), config-file mode in compose:', '# cloudflared tunnel create <name> → save the JSON as deploy/tunnel-credentials.json (gitignored)', '# set the tunnel id in deploy/cloudflared.yml; cloudflared tunnel route dns <name> <hostname>', '# No token/env needed — the credentials JSON is mounted into the cloudflared container.');
206
+ }
207
+ return lines.join('\n') + '\n';
208
+ }
209
+ /**
210
+ * Render `.env.auth.example` — the edge auth secrets (JWT signing key, Google OAuth
211
+ * client/cookie secrets, team bootstrap config). Loaded ONLY by the auth-broker and
212
+ * oauth2-proxy services (see docker-compose.yml), never the agent: the agent has no use
213
+ * for the token-signing key, so keeping it out of that container limits the blast radius
214
+ * of a compromised (model-driven) agent — it can no longer forge tokens for any identity.
215
+ */
216
+ function renderAuthEnvExample(opts) {
217
+ const hostname = resolveDeployPublicHostname(opts?.hostname);
218
+ if (opts?.fed) {
219
+ const idp = (opts.idpUrl || 'https://auth.agenthippo.ai').replace(/\/+$/, '');
220
+ const cookieDomain = opts.cookieDomain || '.agenthippo.ai';
221
+ return [
222
+ '# Generated by `agenthippo deploy init --ingress edge-fed`. Copy to .env.auth and fill in.',
223
+ '# Federated edge (A′): NO Google OAuth client here. Google login happens once at the',
224
+ `# central IdP (${idp}); this agent only VERIFIES the shared ${cookieDomain} AgentHippo JWT.`,
225
+ '# Loaded only by auth-broker. Never given to the agent (limits token-forgery blast radius).',
226
+ '',
227
+ '# AgentHippo JWT signing key — MUST match the central IdP (auth.agenthippo.ai).',
228
+ '# Copy it from there (or the GCP secret it stores) so tokens issued by the IdP verify here.',
229
+ 'AGENTHIPPO_AUTH_JWT_SECRET= # copy from auth.agenthippo.ai (openssl rand -hex 32 to generate one)',
230
+ `AGENTHIPPO_AUTH_PUBLIC_URL=https://${hostname}`,
231
+ 'AGENTHIPPO_AUTH_ALLOWED_DOMAINS=agenthippo.ai',
232
+ `AGENTHIPPO_AUTH_COOKIE_DOMAIN=${cookieDomain} # scope the shared cookie to all subdomains`,
233
+ `AGENTHIPPO_AUTH_IDP_URL=${idp} # where browsers are sent to log in`,
234
+ 'AGENTHIPPO_AUTH_COOKIE_NAME=ah_token',
235
+ '',
236
+ '# Team bootstrap (store + LiteLLM routing) — same as the IdP if you want parity.',
237
+ 'AGENTHIPPO_TEAM_ID=team_default',
238
+ 'AGENTHIPPO_TEAM_NAME=Default Team',
239
+ 'AGENTHIPPO_TEAM_STORE_REPO=agenthippoai/agenthippo-store',
240
+ 'AGENTHIPPO_TEAM_STORE_INDEX_URL=https://agenthippoai.github.io/agenthippo-store',
241
+ 'AGENTHIPPO_TEAM_GITHUB_ORG=agenthippoai',
242
+ 'AGENTHIPPO_TEAM_LITELLM_BASE_URL=http://127.0.0.1:4000',
243
+ 'AGENTHIPPO_TEAM_LITELLM_API_KEY=',
244
+ '',
245
+ '# No OAUTH2_PROXY_* here — this agent does not talk to Google. The one oauth2-proxy +',
246
+ '# one Google OAuth client live only at the IdP.',
247
+ ].join('\n') + '\n';
248
+ }
249
+ return [
250
+ '# Generated by `agenthippo deploy init`. Copy to .env.auth and fill in.',
251
+ '# Loaded only by auth-broker + oauth2-proxy (docker-compose.yml). Never given to the agent.',
252
+ '',
253
+ '# Google SSO (oauth2-proxy). GCP → OAuth client (Web):',
254
+ `# Authorized origins: https://${hostname}`,
255
+ `# Redirect URI: https://${hostname}/oauth2/callback`,
256
+ 'OAUTH2_PROXY_CLIENT_ID=',
257
+ 'OAUTH2_PROXY_CLIENT_SECRET=',
258
+ 'OAUTH2_PROXY_COOKIE_SECRET= # openssl rand -base64 32 | head -c 32',
259
+ 'OAUTH2_PROXY_PROVIDER=google',
260
+ 'OAUTH2_PROXY_EMAIL_DOMAINS=example.com',
261
+ 'OAUTH2_PROXY_REVERSE_PROXY=true',
262
+ `OAUTH2_PROXY_REDIRECT_URL=https://${hostname}/oauth2/callback`,
263
+ '# Federated SSO (A′) — set when this edge is the central IdP for *.agenthippo.ai agents:',
264
+ 'OAUTH2_PROXY_COOKIE_DOMAINS=.agenthippo.ai # scope the oauth2-proxy session cookie to all subdomains',
265
+ '',
266
+ '# IDE auth broker (team bootstrap for store + LiteLLM routing)',
267
+ 'AGENTHIPPO_AUTH_JWT_SECRET= # openssl rand -hex 32',
268
+ 'AGENTHIPPO_AUTH_PUBLIC_URL=https://' + hostname,
269
+ 'AGENTHIPPO_AUTH_ALLOWED_DOMAINS=example.com',
270
+ 'AGENTHIPPO_AUTH_COOKIE_NAME=ah_token # federated cookie holding the AgentHippo JWT',
271
+ 'AGENTHIPPO_AUTH_COOKIE_DOMAIN=.agenthippo.ai # set so /oauth/browser-login stamps a cross-subdomain cookie',
272
+ 'AGENTHIPPO_TEAM_ID=team_default',
273
+ 'AGENTHIPPO_TEAM_NAME=Default Team',
274
+ 'AGENTHIPPO_TEAM_STORE_REPO=agenthippoai/agenthippo-store',
275
+ 'AGENTHIPPO_TEAM_STORE_INDEX_URL=https://agenthippoai.github.io/agenthippo-store',
276
+ 'AGENTHIPPO_TEAM_GITHUB_ORG=agenthippoai',
277
+ 'AGENTHIPPO_TEAM_LITELLM_BASE_URL=http://127.0.0.1:4000',
278
+ 'AGENTHIPPO_TEAM_LITELLM_API_KEY=',
279
+ ].join('\n') + '\n';
280
+ }
281
+ /** Auth broker sidecar for IDE sign-in + team bootstrap API. */
282
+ function renderAuthBrokerDockerfile() {
283
+ return [
284
+ '# Auth broker for IDE sign-in + team bootstrap API.',
285
+ '# Build context = pack root: docker build -f deploy/Dockerfile.auth-broker -t <pack>-auth-broker:local .',
286
+ 'FROM ghcr.io/agenthippo/runtime:local-cursor',
287
+ 'ENV AGENTHIPPO_AUTH_PORT=9780',
288
+ 'ENV AGENTHIPPO_AUTH_DATA_DIR=/data',
289
+ 'EXPOSE 9780',
290
+ 'VOLUME ["/data"]',
291
+ 'CMD ["auth-broker"]',
292
+ '',
293
+ ].join('\n');
294
+ }
295
+ /** .dockerignore emitted alongside every image-building target's Dockerfile.
296
+ * Keeps skills/, references/, scripts/ (required at runtime) and drops only VCS,
297
+ * cache, and build cruft. node_modules is excluded so the linux/amd64 image
298
+ * reinstalls fresh deps via the Dockerfile skill-deps step — dev-machine
299
+ * node_modules may be wrong-arch (arm64/darwin) for the image. */
300
+ function renderDockerignore() {
301
+ return [
302
+ '# Generated by `agenthippo deploy init`. Keeps skills/, references/, scripts/',
303
+ '# (needed at runtime) and drops only VCS/cache/build cruft. node_modules is',
304
+ '# excluded so the Dockerfile reinstalls fresh linux/amd64 deps for skills.',
305
+ '.git',
306
+ '.gitignore',
307
+ '.gitattributes',
308
+ '**/node_modules',
309
+ '**/__pycache__',
310
+ '**/.pytest_cache',
311
+ '**/.mypy_cache',
312
+ '**/.ruff_cache',
313
+ '**/.venv',
314
+ '**/venv',
315
+ '**/*.log',
316
+ '**/dist',
317
+ '**/build',
318
+ '**/.next',
319
+ '**/.turbo',
320
+ '**/.cache',
321
+ '.DS_Store',
322
+ '**/.DS_Store',
323
+ '.idea',
324
+ '.vscode',
325
+ '*.swp',
326
+ '# Secrets anywhere in the pack (defensive; deploy/ secrets are also covered below).',
327
+ '**/.env',
328
+ '**/*credentials*.json',
329
+ '# deploy/ holds build/deploy artifacts + secrets; only deploy/www/ ships at runtime.',
330
+ 'deploy/*',
331
+ '!deploy/www',
332
+ '!deploy/www/**',
333
+ '',
334
+ ].join('\n');
335
+ }
336
+ /** Build-time install of skill dependencies (skills/** and scripts/**). Skills that
337
+ * ship their own requirements.txt / package.json need their libs present at runtime,
338
+ * but AGENTHIPPO_NO_AUTO_INSTALL=1 skips runtime installs — so we install at BUILD
339
+ * time. Pure no-op when no dep files are present (keeps minimal packs lean). pip3 is
340
+ * ensured only when a requirements.txt exists (the runtime base ships python3 but not
341
+ * necessarily pip). Runs as root, then drops back to the non-root agent user. */
342
+ function skillDepsInstallLines() {
343
+ return [
344
+ '# Skill dependencies — installed at BUILD time so skills that ship their own',
345
+ '# requirements.txt / package.json have their libs present at runtime',
346
+ '# (AGENTHIPPO_NO_AUTO_INSTALL=1 skips runtime installs). No-op when none exist.',
347
+ 'USER root',
348
+ 'RUN set -e; \\',
349
+ ' py_reqs=$(find /app/pack/skills /app/pack/scripts -type f -name requirements.txt -not -path "*/node_modules/*" 2>/dev/null || true); \\',
350
+ ' if [ -n "$py_reqs" ]; then \\',
351
+ ' pip3 --version 2>/dev/null || (apt-get update && apt-get install -y --no-install-recommends python3-pip && rm -rf /var/lib/apt/lists/*); \\',
352
+ ' echo "$py_reqs" | while IFS= read -r f; do [ -n "$f" ] && pip3 install --no-cache-dir --break-system-packages -r "$f" || true; done; \\',
353
+ ' fi; \\',
354
+ ' pkg_json=$(find /app/pack/skills /app/pack/scripts -type f -name package.json -not -path "*/node_modules/*" 2>/dev/null || true); \\',
355
+ ' if [ -n "$pkg_json" ]; then \\',
356
+ ' echo "$pkg_json" | while IFS= read -r f; do [ -n "$f" ] && (cd "$(dirname "$f")" && npm install --omit=dev --no-audit --no-fund || true) || true; done; \\',
357
+ ' fi',
358
+ 'USER agent',
359
+ ];
360
+ }
361
+ /** Cloud Run / compose agent env: bind on all interfaces inside the container network. */
362
+ function agentServeHostEnvYaml(indent = ' ') {
363
+ return `${indent}- { name: ${serve_hardening_1.SERVE_HOST_ENV}, value: ${serve_hardening_1.CONTAINER_SERVE_HOST} }`;
364
+ }
365
+ /** The Dockerfile shared by image-based targets (serve-v3 §A2). */
366
+ function sharedDockerfile(ctx, opts) {
367
+ const cmd = serveCmdArgs(ctx, opts).map((a) => `"${a}"`).join(',');
368
+ return [
369
+ `# Generated by \`agenthippo deploy init\`. Build context = pack root:`,
370
+ `# docker build -f deploy/Dockerfile -t ${ctx.packName}:local .`,
371
+ `FROM ${ctx.image}`,
372
+ `COPY . /app/pack/`,
373
+ ...skillDepsInstallLines(),
374
+ `ENV AGENTHIPPO_NO_AUTO_INSTALL=1`,
375
+ `ENV ${serve_hardening_1.SERVE_HOST_ENV}=${serve_hardening_1.CONTAINER_SERVE_HOST}`,
376
+ `EXPOSE ${ctx.port}`,
377
+ // Runtime base image sets ENTRYPOINT ["agenthippo"]; CMD args append to it.
378
+ `CMD [${cmd}]`,
379
+ '',
380
+ ].join('\n');
381
+ }
382
+ /** Minimal post-SSO landing page (static chat UI → /v1/chat). */
383
+ function renderLandingPage(packName) {
384
+ return [
385
+ '<!DOCTYPE html>',
386
+ '<html lang="en">',
387
+ '<head>',
388
+ ' <meta charset="utf-8" />',
389
+ ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
390
+ ` <title>${packName}</title>`,
391
+ ' <style>',
392
+ ' :root { color-scheme: light dark; font-family: system-ui, sans-serif; }',
393
+ ' body { max-width: 720px; margin: 2rem auto; padding: 0 1rem; line-height: 1.5; }',
394
+ ' h1 { font-size: 1.4rem; margin-bottom: 0.25rem; }',
395
+ ' .sub { opacity: 0.7; font-size: 0.9rem; margin-bottom: 1.5rem; }',
396
+ ' #log { border: 1px solid #8884; border-radius: 8px; min-height: 200px; padding: 1rem; white-space: pre-wrap; margin-bottom: 1rem; }',
397
+ ' form { display: flex; gap: 0.5rem; }',
398
+ ' input[type=text] { flex: 1; padding: 0.6rem 0.75rem; border-radius: 6px; border: 1px solid #8886; font: inherit; }',
399
+ ' button { padding: 0.6rem 1rem; border-radius: 6px; border: 0; background: #2563eb; color: #fff; font: inherit; cursor: pointer; }',
400
+ ' button:disabled { opacity: 0.6; cursor: wait; }',
401
+ ' </style>',
402
+ '</head>',
403
+ '<body>',
404
+ ` <h1>${packName}</h1>`,
405
+ ' <p class="sub">Signed in via Google SSO. Chat uses the agent API behind this page.</p>',
406
+ ' <div id="log">Ready. Send a message below.</div>',
407
+ ' <form id="chat">',
408
+ ' <input id="msg" type="text" placeholder="Ask the agent…" autocomplete="off" required />',
409
+ ' <button type="submit">Send</button>',
410
+ ' </form>',
411
+ ' <script>',
412
+ ' const log = document.getElementById("log");',
413
+ ' const form = document.getElementById("chat");',
414
+ ' const input = document.getElementById("msg");',
415
+ ' // Stable per-browser session id so turns resume the same conversation (multi-turn).',
416
+ ' const sessionId = (self.crypto && crypto.randomUUID) ? crypto.randomUUID() : ("web-" + Date.now().toString(36) + Math.random().toString(36).slice(2));',
417
+ ' function append(text) {',
418
+ ' log.textContent += (log.textContent.endsWith("\\n") || log.textContent === "Ready. Send a message below." ? "" : "\\n") + text;',
419
+ ' }',
420
+ ' form.addEventListener("submit", async (e) => {',
421
+ ' e.preventDefault();',
422
+ ' const message = input.value.trim();',
423
+ ' if (!message) return;',
424
+ ' input.value = "";',
425
+ ' form.querySelector("button").disabled = true;',
426
+ ' append("\\nYou: " + message + "\\nAgent: ");',
427
+ ' try {',
428
+ ' const body = { message, session_id: sessionId };',
429
+ ' const res = await fetch("/v1/chat", {',
430
+ ' method: "POST",',
431
+ ' headers: { "Content-Type": "application/json" },',
432
+ ' body: JSON.stringify(body),',
433
+ ' });',
434
+ ' const data = await res.json();',
435
+ ' if (!res.ok) throw new Error(data.error || res.statusText);',
436
+ ' append(data.text || data.response || JSON.stringify(data));',
437
+ ' } catch (err) {',
438
+ ' append("[error] " + (err.message || err));',
439
+ ' } finally {',
440
+ ' form.querySelector("button").disabled = false;',
441
+ ' input.focus();',
442
+ ' }',
443
+ ' });',
444
+ ' input.focus();',
445
+ ' </script>',
446
+ '</body>',
447
+ '</html>',
448
+ '',
449
+ ].join('\n');
450
+ }
451
+ /** Caddy config for the `edge` compose profile (oauth2-proxy + agent on Docker network). */
452
+ function renderCaddyfile(ctx) {
453
+ return [
454
+ '# SSO edge + Cloudflare Tunnel. Set AGENTHIPPO_PUBLIC_HOSTNAME in .env (Caddy reads it below).',
455
+ '# TLS terminates at Cloudflare; origin listens on HTTP only.',
456
+ '{',
457
+ ' auto_https off',
458
+ '}',
459
+ '',
460
+ 'http://{$AGENTHIPPO_PUBLIC_HOSTNAME} {',
461
+ ' # Federated SSO (A′): browsers from *.agenthippo.ai subdomains land here to log in.',
462
+ ' # Gated behind oauth2-proxy (Google) → auth-broker mints + sets the shared .agenthippo.ai',
463
+ ' # cookie (ah_token), then redirects back to the agent subdomain (`return_to`). Must come',
464
+ ' # before the generic /oauth/* handle so it wins for this exact path.',
465
+ ' handle /oauth/browser-login {',
466
+ ' forward_auth oauth2-proxy:4180 {',
467
+ ' uri /oauth2/auth',
468
+ ' copy_headers X-Auth-Request-Email',
469
+ ' header_up X-Forwarded-Proto https',
470
+ ' header_up X-Forwarded-Host {http.request.host}',
471
+ ' @unauth status 401',
472
+ ' handle_response @unauth {',
473
+ ' redir * "/oauth2/sign_in?rd=/oauth/browser-login{http.request.uri.query}"',
474
+ ' }',
475
+ ' }',
476
+ ' reverse_proxy auth-broker:9780 {',
477
+ ' header_up X-Forwarded-Proto https',
478
+ ' header_up X-Forwarded-Host {http.request.host}',
479
+ ' }',
480
+ ' }',
481
+ ' handle /oauth/* {',
482
+ ' reverse_proxy auth-broker:9780 {',
483
+ ' header_up X-Forwarded-Proto https',
484
+ ' header_up X-Forwarded-Host {http.request.host}',
485
+ ' }',
486
+ ' }',
487
+ ' handle /api/ide/* {',
488
+ ' reverse_proxy auth-broker:9780 {',
489
+ ' header_up X-Forwarded-Proto https',
490
+ ' header_up X-Forwarded-Host {http.request.host}',
491
+ ' }',
492
+ ' }',
493
+ ' handle /oauth2/* {',
494
+ ' reverse_proxy oauth2-proxy:4180 {',
495
+ ' header_up X-Forwarded-Proto https',
496
+ ' header_up X-Forwarded-Host {http.request.host}',
497
+ ' }',
498
+ ' }',
499
+ ' @health path /health /ready',
500
+ ' handle @health {',
501
+ ` reverse_proxy agent:${ctx.port}`,
502
+ ' }',
503
+ ' # API clients (IDE) with a Bearer token: validate via auth-broker, copy identity header.',
504
+ ' @apiWithAuth {',
505
+ ' path /v1/*',
506
+ ' header Authorization *',
507
+ ' }',
508
+ ' handle @apiWithAuth {',
509
+ ' forward_auth auth-broker:9780 {',
510
+ ' uri /api/ide/me',
511
+ ' copy_headers X-Agent-User',
512
+ ' header_up X-Forwarded-Proto https',
513
+ ' header_up X-Forwarded-Host {http.request.host}',
514
+ ' @unauth status 401',
515
+ ' handle_response @unauth {',
516
+ ' respond "Unauthorized" 401',
517
+ ' }',
518
+ ' }',
519
+ ` reverse_proxy agent:${ctx.port}`,
520
+ ' }',
521
+ ' # Browser clients: SSO via oauth2-proxy, identity injected as X-Agent-User.',
522
+ ' handle {',
523
+ ' forward_auth oauth2-proxy:4180 {',
524
+ ' uri /oauth2/auth',
525
+ ' copy_headers X-Auth-Request-Email>X-Agent-User',
526
+ ' header_up X-Forwarded-Proto https',
527
+ ' header_up X-Forwarded-Host {http.request.host}',
528
+ ' @unauth status 401',
529
+ ' handle_response @unauth {',
530
+ ' redir * "/oauth2/sign_in?rd={http.request.uri.path}{http.request.uri.query}"',
531
+ ' }',
532
+ ' }',
533
+ ' @api path /v1/*',
534
+ ' handle @api {',
535
+ ` reverse_proxy agent:${ctx.port}`,
536
+ ' }',
537
+ ' root * /srv/www',
538
+ ' file_server',
539
+ ' }',
540
+ '}',
541
+ '',
542
+ ].join('\n');
543
+ }
544
+ /** cloudflared config-file mode (optional). Prefer CLOUDFLARE_TUNNEL_TOKEN + compose `tunnel` profile. */
545
+ function renderCloudflaredYml(hostname) {
546
+ return [
547
+ '# Cloudflare Tunnel — run as the compose `cloudflared` service (config-file mode).',
548
+ '# One-time setup:',
549
+ '# cloudflared tunnel create <name> # creates the tunnel + a credentials JSON',
550
+ '# cp <that-creds>.json deploy/tunnel-credentials.json # mounted into the container (gitignore it!)',
551
+ `# cloudflared tunnel route dns <name> ${hostname}`,
552
+ '# set the tunnel id below, then: docker compose --profile edge --profile tunnel up -d',
553
+ 'tunnel: <TUNNEL_ID>',
554
+ 'credentials-file: /etc/cloudflared/credentials.json # mounted from deploy/tunnel-credentials.json',
555
+ 'ingress:',
556
+ ` - hostname: ${hostname} # must match AGENTHIPPO_PUBLIC_HOSTNAME in .env`,
557
+ ' service: http://caddy:80 # cloudflared container → Caddy over the compose network',
558
+ ' - service: http_status:404',
559
+ '',
560
+ ].join('\n');
561
+ }
562
+ /** Shared `agent` service block for vm/compose docker-compose.yml (build context = pack root). */
563
+ function renderAgentComposeService(ctx, extraLines = []) {
564
+ return [
565
+ ' agent:',
566
+ ' build:',
567
+ ' context: ..',
568
+ ' dockerfile: deploy/Dockerfile',
569
+ ' env_file:',
570
+ ' - path: .env',
571
+ ' required: false',
572
+ ...extraLines,
573
+ ' volumes: ["agenthippo-state:/state"]',
574
+ ` expose: ["${ctx.port}"]`,
575
+ ' # No host port — edge reaches agent on the Docker network only.',
576
+ ' # T0.5a VPN: bash deploy/run.sh binds 127.0.0.1:' + ctx.port + ' via docker run -p.',
577
+ '',
578
+ ];
579
+ }
580
+ /** oauth2-proxy + Caddy services for `--profile edge`. */
581
+ function renderEdgeComposeServices() {
582
+ return [
583
+ ' oauth2-proxy:',
584
+ ' profiles: ["edge"]',
585
+ ' image: quay.io/oauth2-proxy/oauth2-proxy:v7.7.1',
586
+ ' env_file:',
587
+ ' - path: .env',
588
+ ' required: false',
589
+ ' - path: .env.auth # OAuth client/cookie secrets — not shared with the agent',
590
+ ' required: false',
591
+ ' environment:',
592
+ ' OAUTH2_PROXY_HTTP_ADDRESS: 0.0.0.0:4180',
593
+ ' OAUTH2_PROXY_UPSTREAMS: file:///dev/null',
594
+ ' OAUTH2_PROXY_SET_XAUTHREQUEST: "true"',
595
+ ' OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER: "true"',
596
+ ' OAUTH2_PROXY_SKIP_PROVIDER_BUTTON: "true"',
597
+ ' OAUTH2_PROXY_REVERSE_PROXY: "true"',
598
+ ' expose: ["4180"]',
599
+ '',
600
+ ' auth-broker:',
601
+ ' profiles: ["edge"]',
602
+ ' build:',
603
+ ' context: ..',
604
+ ' dockerfile: deploy/Dockerfile.auth-broker',
605
+ ' env_file:',
606
+ ' - path: .env',
607
+ ' required: false',
608
+ ' - path: .env.auth # JWT signing key + team config — not shared with the agent',
609
+ ' required: false',
610
+ ' environment:',
611
+ ' AGENTHIPPO_AUTH_PORT: "9780"',
612
+ ' AGENTHIPPO_AUTH_DATA_DIR: /data',
613
+ ' user: "0:0" # named volume /data is root-owned on first create',
614
+ ' volumes: ["auth-broker-data:/data"]',
615
+ ' expose: ["9780"]',
616
+ '',
617
+ ' caddy:',
618
+ ' profiles: ["edge"]',
619
+ ' image: caddy:2.8-alpine',
620
+ ' env_file:',
621
+ ' - path: .env',
622
+ ' required: false',
623
+ ' ports: ["80:80", "443:443"] # use 80:80 not 127.0.0.1:80 — breaks vmnetd on some Docker Desktop builds',
624
+ ' volumes: ["./Caddyfile:/etc/caddy/Caddyfile:ro", "./www:/srv/www:ro"]',
625
+ ' depends_on: [oauth2-proxy, agent, auth-broker]',
626
+ '',
627
+ ];
628
+ }
629
+ /** cloudflared in compose — outbound tunnel to Cloudflare (free; no Access). */
630
+ function renderTunnelComposeServices() {
631
+ return [
632
+ ' cloudflared:',
633
+ ' profiles: ["tunnel"]',
634
+ ' image: cloudflare/cloudflared:2024.12.2',
635
+ ' restart: unless-stopped',
636
+ ' # Config-file mode: routing lives in deploy/cloudflared.yml (GitOps), credentials',
637
+ ' # in deploy/tunnel-credentials.json (gitignored secret from `cloudflared tunnel create`).',
638
+ ' command: tunnel --config /etc/cloudflared/config.yml run',
639
+ ' volumes:',
640
+ ' - ./cloudflared.yml:/etc/cloudflared/config.yml:ro',
641
+ ' - ./tunnel-credentials.json:/etc/cloudflared/credentials.json:ro',
642
+ ' depends_on: [caddy]',
643
+ '',
644
+ ];
645
+ }
646
+ function renderEdgeTunnelUpScript(hostname) {
647
+ return [
648
+ '#!/usr/bin/env bash',
649
+ '# T0.5b — Cloudflare Tunnel + Google SSO (no Access seats). Run from pack root.',
650
+ 'set -euo pipefail',
651
+ 'cd "$(dirname "$0")"',
652
+ 'docker compose --profile edge --profile tunnel up -d --build',
653
+ 'echo "Public URL: https://' + hostname + ' (after DNS + tunnel route configured)"',
654
+ '',
655
+ ].join('\n');
656
+ }
657
+ function vmArtifacts(ctx) {
658
+ const hostname = ctx.publicHostname;
659
+ const run = [
660
+ '#!/usr/bin/env bash',
661
+ '# T0.5a — VPN + localhost only (no SSO edge). Binds 127.0.0.1 so LAN cannot bypass the proxy.',
662
+ 'set -euo pipefail',
663
+ ...anchorToStagingRoot(),
664
+ `IMAGE="${ctx.packName}:local"`,
665
+ 'docker build -f deploy/Dockerfile -t "$IMAGE" .',
666
+ 'docker rm -f agenthippo-agent 2>/dev/null || true',
667
+ 'docker run -d --name agenthippo-agent --restart unless-stopped \\',
668
+ ' --env-file deploy/.env \\',
669
+ ` -v agenthippo-state:/state -p 127.0.0.1:${ctx.port}:${ctx.port} "$IMAGE"`,
670
+ `echo "agent on 127.0.0.1:${ctx.port} — health: curl localhost:${ctx.port}/health"`,
671
+ '',
672
+ ].join('\n');
673
+ const service = [
674
+ '# systemd user unit — survives logout (run: loginctl enable-linger $USER).',
675
+ '# VPN path: cp deploy/agenthippo.service ~/.config/systemd/user/',
676
+ '# SSO path: use agenthippo-edge.service (compose --profile edge)',
677
+ '# install: systemctl --user enable --now agenthippo',
678
+ '[Unit]',
679
+ 'Description=AgentHippo agent (serve)',
680
+ 'After=network-online.target docker.service',
681
+ '',
682
+ '[Service]',
683
+ `WorkingDirectory=%h/${ctx.packName}`,
684
+ 'ExecStart=/usr/bin/env bash deploy/run.sh',
685
+ 'Restart=always',
686
+ 'RestartSec=5',
687
+ '',
688
+ '[Install]',
689
+ 'WantedBy=default.target',
690
+ '',
691
+ ].join('\n');
692
+ const edgeService = [
693
+ '# systemd user unit — Cloudflare Tunnel + Google SSO + agent.',
694
+ '# install: cp deploy/agenthippo-edge.service ~/.config/systemd/user/',
695
+ '# loginctl enable-linger $USER && systemctl --user enable --now agenthippo-edge',
696
+ '[Unit]',
697
+ 'Description=AgentHippo agent (Tunnel + Google SSO)',
698
+ 'After=network-online.target docker.service',
699
+ '',
700
+ '[Service]',
701
+ `WorkingDirectory=%h/${ctx.packName}/deploy`,
702
+ 'ExecStart=/usr/bin/env docker compose -f docker-compose.yml --profile edge --profile tunnel up --build',
703
+ 'ExecStop=/usr/bin/env docker compose -f docker-compose.yml --profile edge --profile tunnel down',
704
+ 'Restart=always',
705
+ 'RestartSec=5',
706
+ '',
707
+ '[Install]',
708
+ 'WantedBy=default.target',
709
+ '',
710
+ ].join('\n');
711
+ const vmCompose = [
712
+ '# T0.5 workbench/VM. Paths:',
713
+ '# T0.5a VPN: bash deploy/run.sh',
714
+ '# T0.5b remote: bash deploy/run-edge-tunnel.sh (Tunnel + Google SSO — recommended WFH)',
715
+ '# edge only: docker compose --profile edge up -d (SSO on :443, no tunnel)',
716
+ 'services:',
717
+ ...renderAgentComposeService(ctx),
718
+ ...renderEdgeComposeServices(),
719
+ ...renderTunnelComposeServices(),
720
+ 'volumes:',
721
+ ' agenthippo-state:',
722
+ ' auth-broker-data:',
723
+ '',
724
+ ].join('\n');
725
+ return {
726
+ 'Dockerfile': sharedDockerfile(ctx),
727
+ 'Dockerfile.auth-broker': renderAuthBrokerDockerfile(),
728
+ 'docker-compose.yml': vmCompose,
729
+ 'run.sh': run,
730
+ 'run-edge-tunnel.sh': renderEdgeTunnelUpScript(hostname),
731
+ 'agenthippo.service': service,
732
+ 'agenthippo-edge.service': edgeService,
733
+ 'Caddyfile': renderCaddyfile(ctx),
734
+ 'www/index.html': renderLandingPage(ctx.packName),
735
+ 'cloudflared.yml': renderCloudflaredYml(hostname),
736
+ 'README.md': [
737
+ `# Deploy: ${ctx.packName} — T0.5 workbench/VM`,
738
+ '',
739
+ '`cp deploy/.env.example deploy/.env` and fill in, then pick a path:',
740
+ '',
741
+ '**T0.5a — VPN only (simplest internal pilot)**',
742
+ '```bash',
743
+ 'bash deploy/run.sh',
744
+ '# curl http://127.0.0.1:' + ctx.port + '/v1/chat -d \'{"message":"hi"}\' ...',
745
+ '```',
746
+ '',
747
+ '**T0.5b — Cloudflare Tunnel + Google SSO (recommended for WFH, no Access seats)**',
748
+ '```bash',
749
+ 'agenthippo deploy init . --target vm --force # from pack root; review deploy/',
750
+ 'cp deploy/.env.example deploy/.env # agent: model key + public hostname',
751
+ 'cp deploy/.env.auth.example deploy/.env.auth # broker/oauth2-proxy: JWT + OAuth secrets (never loaded by the agent)',
752
+ '# 1. Google Cloud → OAuth Web client for https://' + hostname,
753
+ '# Authorized origin + redirect: https://' + hostname + '/oauth2/callback',
754
+ '# 2. cloudflared tunnel create <name>',
755
+ '# cp ~/.cloudflared/<tunnel-id>.json deploy/tunnel-credentials.json # gitignore; secret',
756
+ '# set tunnel: <tunnel-id> in deploy/cloudflared.yml',
757
+ '# cloudflared tunnel route dns <name> ' + hostname,
758
+ '# 3. Fill deploy/.env (hostname, model key) and deploy/.env.auth (OAUTH2_PROXY_*, AGENTHIPPO_AUTH_JWT_SECRET)',
759
+ 'cd deploy && docker compose --profile edge --profile tunnel up -d --build',
760
+ '# or from pack root: bash deploy/run-edge-tunnel.sh',
761
+ '# Users open https://' + hostname + ' → Google login → static chat UI → /v1/chat',
762
+ '```',
763
+ 'Tunnel routing is **config-file mode** (`deploy/cloudflared.yml` → `http://caddy:80` on the compose network), not the Cloudflare dashboard public-hostname UI alone.',
764
+ 'Optional: `agenthippo-edge.service` + `loginctl enable-linger $USER` for persistence.',
765
+ '',
766
+ '**Edge only (no tunnel)** — `docker compose --profile edge up -d` on LAN/VPN.',
767
+ '',
768
+ 'Identity: Google → oauth2-proxy → `X-Agent-User` → serve (`www/index.html` sends a per-tab `session_id` for multi-turn).',
769
+ 'Per-user *data* scoping needs Design B (§11.1) in the app layer — generic `serve` alone does not filter warehouse rows.',
770
+ '',
771
+ '**Do not commit** `deploy/.env`, `deploy/.env.auth`, or `deploy/tunnel-credentials.json`.',
772
+ '',
773
+ ].join('\n'),
774
+ };
775
+ }
776
+ /** Compose-tuned OTEL collector config: same pipeline as the canonical Spotlight
777
+ * collector, but bound to 0.0.0.0 so a sibling container (litellm) can reach it,
778
+ * and writing JSONL to the shared traces volume DuckDB/query-analytics.sh read. */
779
+ const OTEL_COLLECTOR_COMPOSE_YAML = [
780
+ '# Spotlight OTEL collector (compose-tuned: 0.0.0.0 bind). Writes JSONL to',
781
+ '# ${AGENT_HOME_DIR}/analytics/traces/ — query with query-analytics.sh (DuckDB).',
782
+ 'receivers:',
783
+ ' otlp:',
784
+ ' protocols:',
785
+ ' http: { endpoint: 0.0.0.0:4318 }',
786
+ ' grpc: { endpoint: 0.0.0.0:4317 }',
787
+ 'processors:',
788
+ ' batch: { timeout: 500ms, send_batch_size: 10 }',
789
+ ' resource:',
790
+ ' attributes:',
791
+ ' - { key: service.name, value: agentide-litellm, action: upsert }',
792
+ 'exporters:',
793
+ ' file/jsonl:',
794
+ ' path: ${AGENT_HOME_DIR}/analytics/traces/traces.jsonl',
795
+ ' rotation: { max_megabytes: 100, max_days: 30, max_backups: 10 }',
796
+ 'service:',
797
+ ' pipelines:',
798
+ ' traces: { receivers: [otlp], processors: [batch, resource], exporters: [file/jsonl] }',
799
+ '',
800
+ ].join('\n');
801
+ function composeArtifacts(ctx) {
802
+ const channels = ctx.gatewayChannels?.trim();
803
+ const hostname = ctx.publicHostname;
804
+ // Agent Anywhere: override the agent command to also run the gateway bridge,
805
+ // and add the OpenClaw gateway service (channels profile).
806
+ const agentGateway = channels
807
+ ? [
808
+ ' command:',
809
+ ` ["serve","--agent","/app/pack","--workspace","/state/workspace",`,
810
+ ` "--state-dir","/state","--port","${ctx.port}",`,
811
+ ` "--gateway-bridge","--bind","${channels}"]`,
812
+ ]
813
+ : [];
814
+ const channelsService = channels
815
+ ? [
816
+ '',
817
+ ' # Agent Anywhere gateway (OpenClaw) — Slack/Telegram bridge.',
818
+ ' # Build: docker build -f extensions/agentide/scripts/Dockerfile.gateway -t this-tag .',
819
+ ' # NOTE: the gateway runs in secured auth mode — the agent bridge needs',
820
+ ' # OPENCLAW_GATEWAY_TOKEN (or a paired device) to connect. Set it in .env.',
821
+ ' openclaw-gateway:',
822
+ ' profiles: ["channels"]',
823
+ ' image: ghcr.io/agenthippo/openclaw-gateway:2026.06',
824
+ ' env_file:',
825
+ ' - path: .env',
826
+ ' required: false',
827
+ ' volumes: ["openclaw-config:/root/.openclaw"]',
828
+ ' expose: ["18789"]',
829
+ ]
830
+ : [];
831
+ const agentExtra = [
832
+ ' environment:',
833
+ ' # Routed through LiteLLM only when the gateway profile is up; harmless otherwise.',
834
+ ' AGENTHIPPO_LITELLM_BASE_URL: http://litellm:4443',
835
+ ...(channels ? [' OPENCLAW_GATEWAY_URL: ws://openclaw-gateway:18789'] : []),
836
+ ...agentGateway,
837
+ ];
838
+ const compose = [
839
+ '# T2 team/fleet stack (serve-v3 §8). Profiles:',
840
+ '# default = agent only (Docker network; no host port — use deploy/run.sh for T0.5a VPN)',
841
+ '# --profile edge = + Caddy + oauth2-proxy (Google SSO)',
842
+ '# --profile tunnel = + cloudflared (with edge; route hostname → http://caddy:80)',
843
+ '# --profile gateway = + LiteLLM + Spotlight OTEL collector',
844
+ '# --profile channels = + OpenClaw gateway (Agent Anywhere)',
845
+ '# e.g.: docker compose -f deploy/docker-compose.yml --profile edge --profile tunnel up -d',
846
+ 'services:',
847
+ ...renderAgentComposeService(ctx, agentExtra),
848
+ ...renderEdgeComposeServices(),
849
+ ...renderTunnelComposeServices(),
850
+ ' # LiteLLM gateway — self-contained image bakes the Spotlight config + OTEL hooks',
851
+ ' # (build: docker build -f extensions/agentide/scripts/Dockerfile.litellm -t this-tag .)',
852
+ ' litellm:',
853
+ ' profiles: ["gateway"]',
854
+ ' image: ghcr.io/agenthippo/litellm-gateway:2026.06',
855
+ ' env_file:',
856
+ ' - path: .env',
857
+ ' required: false',
858
+ ' environment:',
859
+ ' OTEL_EXPORTER: otlp_http',
860
+ ' OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318',
861
+ ' expose: ["4443"]',
862
+ ' depends_on: [otel-collector]',
863
+ '',
864
+ ' # Spotlight collector → JSONL traces volume (query with query-analytics.sh / DuckDB).',
865
+ ' otel-collector:',
866
+ ' profiles: ["gateway"]',
867
+ ' image: otel/opentelemetry-collector-contrib:0.118.0',
868
+ ' command: ["--config=/etc/otel-collector.yaml"]',
869
+ ' # Root so the collector can create /data/analytics on a fresh named volume',
870
+ ' # (the contrib image runs non-root; the volume root is root-owned).',
871
+ ' user: "0:0"',
872
+ ' environment: { AGENT_HOME_DIR: /data }',
873
+ ' volumes:',
874
+ ' - ./otel-collector.yaml:/etc/otel-collector.yaml:ro',
875
+ ' - spotlight-traces:/data',
876
+ ...channelsService,
877
+ '',
878
+ 'volumes:',
879
+ ' agenthippo-state:',
880
+ ' auth-broker-data:',
881
+ ' spotlight-traces:',
882
+ ...(channels ? [' openclaw-config:'] : []),
883
+ '',
884
+ ].join('\n');
885
+ return {
886
+ 'Dockerfile': sharedDockerfile(ctx),
887
+ 'Dockerfile.auth-broker': renderAuthBrokerDockerfile(),
888
+ 'docker-compose.yml': compose,
889
+ 'Caddyfile': renderCaddyfile(ctx),
890
+ 'www/index.html': renderLandingPage(ctx.packName),
891
+ 'cloudflared.yml': renderCloudflaredYml(hostname),
892
+ 'run-edge-tunnel.sh': renderEdgeTunnelUpScript(hostname),
893
+ 'otel-collector.yaml': OTEL_COLLECTOR_COMPOSE_YAML,
894
+ 'README.md': [
895
+ `# Deploy: ${ctx.packName} — T2 team/fleet`,
896
+ '',
897
+ '- Agent only (VPN): `docker compose -f deploy/docker-compose.yml up -d`',
898
+ '- + Tunnel + Google SSO: `bash deploy/run-edge-tunnel.sh` (see vm README T0.5b).',
899
+ '- + LiteLLM gateway + Spotlight: `--profile gateway`.',
900
+ '- + Agent Anywhere: `--profile channels` + gateway env vars in `.env`.',
901
+ '',
902
+ 'Profiles compose: e.g. `--profile gateway --profile edge --profile tunnel up -d`.',
903
+ 'LiteLLM must speak Anthropic API format — our gateway image does.',
904
+ '',
905
+ ].join('\n'),
906
+ };
907
+ }
908
+ /**
909
+ * Cloud Run multi-container (sidecars) service — agent + LiteLLM + Spotlight OTEL collector,
910
+ * the Cloud Run analogue of the compose `--profile gateway` stack. Sidecars share the pod's
911
+ * localhost, so the agent routes to LiteLLM on 127.0.0.1:4443 and LiteLLM exports OTLP to the
912
+ * collector on 127.0.0.1:4318; the collector writes JSONL to the GCS-backed /state volume
913
+ * (query with query-analytics.sh / DuckDB). Deployed via `gcloud run services replace`.
914
+ *
915
+ * Placeholders (${SERVICE} ${IMAGE} ${STATE_BUCKET} ${LITELLM_IMAGE}) are substituted by the
916
+ * deploy script — kept as a checked-in template so the wiring is reviewable in git.
917
+ */
918
+ /**
919
+ * The env var the LiteLLM gateway reads for a model's provider key — must match the
920
+ * `os.environ/<NAME>` reference in the gateway's model map (e.g. gpt-5.5 → OPENAI_API_KEY).
921
+ */
922
+ function litellmProviderKeyEnv(model) {
923
+ const m = (model || '').toLowerCase();
924
+ if (m.includes('codex')) {
925
+ return 'OPENAI_CODEX_API_KEY';
926
+ }
927
+ if (m.includes('gpt') || m.includes('openai')) {
928
+ return 'OPENAI_API_KEY';
929
+ }
930
+ if (m.includes('claude') || m.includes('anthropic')) {
931
+ return 'ANTHROPIC_API_KEY';
932
+ }
933
+ return 'OPENAI_API_KEY';
934
+ }
935
+ function renderCloudrunServiceYaml(ctx) {
936
+ const providerKeyEnv = litellmProviderKeyEnv(ctx.model);
937
+ return [
938
+ '# Cloud Run multi-container: agent (ingress) + LiteLLM + Spotlight OTEL collector.',
939
+ '# Generated by `agenthippo deploy init`. Deploy with `bash deploy/cloudrun-deploy.sh`',
940
+ '# (it substitutes ${...} and runs `gcloud run services replace`). Traces land in the GCS',
941
+ '# /state volume at /state/analytics/traces — query with query-analytics.sh (DuckDB).',
942
+ 'apiVersion: serving.knative.dev/v1',
943
+ 'kind: Service',
944
+ 'metadata:',
945
+ ' name: ${SERVICE}',
946
+ 'spec:',
947
+ ' template:',
948
+ ' metadata:',
949
+ ' annotations:',
950
+ ' # Sidecars need CPU between requests (the collector flushes async); no scale-to-zero throttle.',
951
+ ' run.googleapis.com/cpu-throttling: "false"',
952
+ ' run.googleapis.com/startup-cpu-boost: "true"',
953
+ ' # Start order: collector → litellm → agent, so traces are never dropped on cold start.',
954
+ " run.googleapis.com/container-dependencies: '{\"litellm\":[\"otel-collector\"],\"agent\":[\"litellm\"]}'",
955
+ ' autoscaling.knative.dev/maxScale: "1"',
956
+ // An inbound channel socket can't scale to zero — pin one always-on instance.
957
+ ...(ctx.gatewayChannels?.trim() ? [' autoscaling.knative.dev/minScale: "1"'] : []),
958
+ ' spec:',
959
+ ' timeoutSeconds: 3600',
960
+ ' containers:',
961
+ ' # Ingress container — the ONLY one with a port. Routes the engine through LiteLLM.',
962
+ ' - name: agent',
963
+ ' image: ${IMAGE}',
964
+ ' ports:',
965
+ ' - name: http1',
966
+ ` containerPort: ${ctx.port}`,
967
+ ' env:',
968
+ ' - { name: AGENTHIPPO_HOME, value: /state }',
969
+ ' - { name: AGENTHIPPO_NO_AUTO_INSTALL, value: "1" }',
970
+ agentServeHostEnvYaml(),
971
+ ' - { name: AGENTHIPPO_LITELLM_BASE_URL, value: http://localhost:4443 }',
972
+ ' - name: AGENTHIPPO_LITELLM_API_KEY',
973
+ ' valueFrom:',
974
+ ' secretKeyRef: { name: ${SERVICE}-litellm-key, key: latest }',
975
+ ' volumeMounts:',
976
+ ' - { name: state, mountPath: /state }',
977
+ ' # LiteLLM gateway sidecar — exports every call to the collector (Spotlight cost/latency/tokens).',
978
+ ' - name: litellm',
979
+ ' image: ${LITELLM_IMAGE}',
980
+ ' env:',
981
+ ' - { name: OTEL_EXPORTER, value: otlp_http }',
982
+ ' - { name: OTEL_EXPORTER_OTLP_ENDPOINT, value: http://localhost:4318 }',
983
+ ' - name: LITELLM_MASTER_KEY',
984
+ ' valueFrom:',
985
+ ' secretKeyRef: { name: ${SERVICE}-litellm-key, key: latest }',
986
+ ` # Upstream provider key for ${ctx.model || 'this model'} (matches the gateway's os.environ/${providerKeyEnv} ref).`,
987
+ ' # Other models need their own key var — rename to match your model map.',
988
+ ` - name: ${providerKeyEnv}`,
989
+ ' valueFrom:',
990
+ ' secretKeyRef: { name: ${SERVICE}-provider-key, key: latest }',
991
+ ' # Spotlight OTEL collector sidecar — writes JSONL traces into the shared /state volume.',
992
+ ' - name: otel-collector',
993
+ ' image: otel/opentelemetry-collector-contrib:0.118.0',
994
+ ' args: ["--config=/etc/otel/config.yaml"]',
995
+ ' env:',
996
+ ' - { name: AGENT_HOME_DIR, value: /state }',
997
+ ' volumeMounts:',
998
+ ' - { name: otel-config, mountPath: /etc/otel, readOnly: true }',
999
+ ' - { name: state, mountPath: /state }',
1000
+ ' volumes:',
1001
+ ' - name: state',
1002
+ ' csi:',
1003
+ ' driver: gcsfuse.run.googleapis.com',
1004
+ ' volumeAttributes:',
1005
+ ' bucketName: ${STATE_BUCKET}',
1006
+ ' - name: otel-config',
1007
+ ' secret:',
1008
+ ' secretName: ${SERVICE}-otel-config',
1009
+ ' items:',
1010
+ ' - { key: latest, path: config.yaml }',
1011
+ '',
1012
+ ].join('\n');
1013
+ }
1014
+ /** Anchor a generated deploy script to the staging root (parent of `deploy/`). The script
1015
+ * lives at <staging>/deploy/<script>.sh; `docker build -f deploy/Dockerfile .` and the
1016
+ * `deploy/service.yaml` / `deploy/.env` relative paths must resolve against the staging
1017
+ * root, so the script works no matter which cwd it is invoked from. (compose-based scripts
1018
+ * don't need this — `build: context: ..` in docker-compose.yml resolves relative to the
1019
+ * compose file itself.) */
1020
+ function anchorToStagingRoot() {
1021
+ return [
1022
+ '# Anchor to the staging root (parent of deploy/) — this script runs from any cwd.',
1023
+ 'cd "$(dirname "$0")/.."',
1024
+ ];
1025
+ }
1026
+ /** IAP finalization (corp SSO). `--iap` is beta-track only on `gcloud run`, and public
1027
+ * access on an existing service is controlled via the run.invoker IAM binding (there
1028
+ * is no `--no-allow-unauthenticated` flag on `services update`). So: enable IAP on the
1029
+ * beta track, strip any allUsers invoker binding (keeps the service private), and grant
1030
+ * IAP-secured Web User to each principal in IAP_MEMBERS (space-separated). Used by both
1031
+ * the Spotlight deploy (`services replace`) and the T1-generic deploy (`run deploy`). */
1032
+ function iapFinalizeLines() {
1033
+ return [
1034
+ '# Ingress: corp SSO via IAP (forwards X-Goog-Authenticated-User-Email → user_id).',
1035
+ '# --iap is beta-track only on `gcloud run`; public access on an existing service is',
1036
+ '# controlled via the run.invoker IAM binding (no --no-allow-unauthenticated on',
1037
+ '# `services update`). So: enable IAP on the beta track, strip any allUsers invoker',
1038
+ '# binding (keeps the service private), and grant IAP-secured Web User to each',
1039
+ '# principal in IAP_MEMBERS (space-separated, e.g.',
1040
+ "# IAP_MEMBERS='user:you@corp.com serviceAccount:sa@project.iam.gserviceaccount.com'",
1041
+ '# ). Omit IAP_MEMBERS to rely on bindings you manage out-of-band.',
1042
+ 'if [[ "${ENABLE_IAP:-1}" == "1" ]]; then',
1043
+ ' gcloud beta run services update "$SERVICE" --region "$REGION" --iap',
1044
+ ' gcloud run services remove-iam-policy-binding "$SERVICE" --region "$REGION" \\',
1045
+ ' --member=allUsers --role=roles/run.invoker >/dev/null 2>&1 || true',
1046
+ ' for m in ${IAP_MEMBERS:-}; do',
1047
+ ' gcloud beta iap web add-iam-policy-binding \\',
1048
+ ' --resource-type=cloud-run --service="$SERVICE" --region="$REGION" \\',
1049
+ ' --member="$m" --role=roles/iap.httpsResourceAccessor',
1050
+ ' done',
1051
+ 'fi',
1052
+ ];
1053
+ }
1054
+ /** Deploy script for the Cloud Run Spotlight (multi-container) stack. */
1055
+ function renderCloudrunSpotlightDeploy(ctx) {
1056
+ return [
1057
+ '#!/usr/bin/env bash',
1058
+ `# Cloud Run + Spotlight: agent + LiteLLM + OTEL collector sidecars (engine: ${ctx.engine}${ctx.model ? `, model ${ctx.model}` : ''}).`,
1059
+ '# Traces persist in the GCS /state bucket; query with query-analytics.sh (DuckDB).',
1060
+ 'set -euo pipefail',
1061
+ ...anchorToStagingRoot(),
1062
+ `SERVICE="${ctx.packName}"`,
1063
+ 'REGION="${REGION:-us-central1}"',
1064
+ 'PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project)}"',
1065
+ 'REPO="${ARTIFACT_REPO:-agenthippo}"',
1066
+ 'IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/${SERVICE}:latest"',
1067
+ 'LITELLM_IMAGE="${LITELLM_IMAGE:-ghcr.io/agenthippo/litellm-gateway:2026.06}"',
1068
+ 'STATE_BUCKET="${STATE_BUCKET:?set STATE_BUCKET=your-gcs-bucket (gcloud storage buckets create gs://...)}"',
1069
+ '# LITELLM_KEY = the proxy key the agent presents to LiteLLM (and LiteLLM master key).',
1070
+ 'LITELLM_KEY="${LITELLM_KEY:?set LITELLM_KEY=<litellm proxy key>}"',
1071
+ `# PROVIDER_KEY = upstream key the gateway uses for ${ctx.model || 'your model'} (gateway reads ${litellmProviderKeyEnv(ctx.model)}).`,
1072
+ 'PROVIDER_KEY="${PROVIDER_KEY:?set PROVIDER_KEY=<upstream provider key>}"',
1073
+ '',
1074
+ 'gcloud artifacts repositories describe "$REPO" --location="$REGION" 2>/dev/null \\',
1075
+ ' || gcloud artifacts repositories create "$REPO" --repository-format=docker --location="$REGION"',
1076
+ 'gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet',
1077
+ '',
1078
+ '# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/agenthippo/runtime:latest',
1079
+ '# tag was retired; AR has runtime:latest / :latest-cursor). No-op if the base already targets AR.',
1080
+ 'DF_TMP="$(mktemp)"',
1081
+ 'sed "s|FROM ghcr.io/agenthippo/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
1082
+ 'docker build --pull --platform linux/amd64 -f "$DF_TMP" -t "$IMAGE" .',
1083
+ 'docker push "$IMAGE"',
1084
+ '',
1085
+ '# Secrets: Spotlight collector config + the LiteLLM proxy key.',
1086
+ 'ensure_secret() { gcloud secrets describe "$1" >/dev/null 2>&1 || gcloud secrets create "$1" --replication-policy=automatic; }',
1087
+ 'ensure_secret "${SERVICE}-otel-config"; gcloud secrets versions add "${SERVICE}-otel-config" --data-file=deploy/otel-collector.yaml',
1088
+ 'ensure_secret "${SERVICE}-litellm-key"; printf %s "$LITELLM_KEY" | gcloud secrets versions add "${SERVICE}-litellm-key" --data-file=-',
1089
+ 'ensure_secret "${SERVICE}-provider-key"; printf %s "$PROVIDER_KEY" | gcloud secrets versions add "${SERVICE}-provider-key" --data-file=-',
1090
+ '',
1091
+ '# Render the multi-container template for this project (sed — no gettext dependency) and deploy.',
1092
+ 'RENDERED="$(mktemp)"',
1093
+ 'sed -e "s|\\${SERVICE}|${SERVICE}|g" -e "s|\\${IMAGE}|${IMAGE}|g" \\',
1094
+ ' -e "s|\\${LITELLM_IMAGE}|${LITELLM_IMAGE}|g" -e "s|\\${STATE_BUCKET}|${STATE_BUCKET}|g" \\',
1095
+ ' deploy/service.yaml > "$RENDERED"',
1096
+ 'gcloud run services replace "$RENDERED" --region "$REGION"',
1097
+ '',
1098
+ ...iapFinalizeLines(),
1099
+ '',
1100
+ '# Grant the runtime SA access to the secrets if needed:',
1101
+ '# SA="$(gcloud run services describe "$SERVICE" --region "$REGION" --format=\'value(spec.template.spec.serviceAccountName)\')"',
1102
+ '# for s in "${SERVICE}-otel-config" "${SERVICE}-litellm-key"; do gcloud secrets add-iam-policy-binding "$s" --member="serviceAccount:$SA" --role=roles/secretmanager.secretAccessor; done',
1103
+ '',
1104
+ ].join('\n');
1105
+ }
1106
+ /**
1107
+ * Option B — AgentHippo edge on Cloud Run. Caddy is the ingress (the only container with a
1108
+ * port) and fronts the agent with the SAME SSO flow the IDE already uses for `remote-vm`:
1109
+ * - Browser → oauth2-proxy (Google SSO) → X-Agent-User → agent
1110
+ * - IDE/API → auth-broker validates the AgentHippo Bearer JWT → X-Agent-User → agent
1111
+ * No IAP, so the IDE points `spec.remote.url` straight at the public hostname and the existing
1112
+ * `ensureSignedIn` + `buildServeAuthHeaders` path works unchanged.
1113
+ *
1114
+ * Cloud Run multi-container shares the pod's network namespace, so every sidecar is reached
1115
+ * via `localhost:<port>` (NOT docker-DNS names like the compose edge). Cloud Run terminates TLS
1116
+ * at the edge, so Caddy listens on HTTP `:{$PORT}` (`auto_https off`). The agent is NOT the
1117
+ * ingress here, so it listens on a distinct internal port (EDGE_AGENT_PORT) to avoid clashing
1118
+ * with Caddy on $PORT.
1119
+ */
1120
+ const EDGE_AGENT_PORT = 8090;
1121
+ /** Caddyfile for the Cloud Run edge — HTTP on $PORT (Cloud Run terminates TLS), localhost upstreams. */
1122
+ function renderCloudrunEdgeCaddyfile() {
1123
+ return [
1124
+ '# Cloud Run edge ingress. Cloud Run terminates TLS; Caddy listens on HTTP :{$PORT}.',
1125
+ '# Sidecars share the pod\'s localhost → upstreams are 127.0.0.1:<port> (not docker-DNS names).',
1126
+ '{',
1127
+ ' auto_https off',
1128
+ '}',
1129
+ '',
1130
+ ':{$PORT} {',
1131
+ ' # Auth-broker: IDE sign-in callback + JWT validation endpoint.',
1132
+ ' handle /oauth/* {',
1133
+ ' reverse_proxy localhost:9780 {',
1134
+ ' header_up X-Forwarded-Proto https',
1135
+ ' header_up X-Forwarded-Host {http.request.host}',
1136
+ ' }',
1137
+ ' }',
1138
+ ' handle /api/ide/* {',
1139
+ ' reverse_proxy localhost:9780 {',
1140
+ ' header_up X-Forwarded-Proto https',
1141
+ ' header_up X-Forwarded-Host {http.request.host}',
1142
+ ' }',
1143
+ ' }',
1144
+ ' # oauth2-proxy: Google SSO callback + sign-in.',
1145
+ ' handle /oauth2/* {',
1146
+ ' reverse_proxy localhost:4180 {',
1147
+ ' header_up X-Forwarded-Proto https',
1148
+ ' header_up X-Forwarded-Host {http.request.host}',
1149
+ ' }',
1150
+ ' }',
1151
+ ' @health path /health /ready',
1152
+ ' handle @health {',
1153
+ ` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
1154
+ ' }',
1155
+ ' # IDE / API clients with a Bearer token: validate via auth-broker, copy identity header.',
1156
+ ' @apiWithAuth {',
1157
+ ' path /v1/*',
1158
+ ' header Authorization *',
1159
+ ' }',
1160
+ ' handle @apiWithAuth {',
1161
+ ' forward_auth localhost:9780 {',
1162
+ ' uri /api/ide/me',
1163
+ ' copy_headers X-Agent-User',
1164
+ ' header_up X-Forwarded-Proto https',
1165
+ ' header_up X-Forwarded-Host {http.request.host}',
1166
+ ' @unauth status 401',
1167
+ ' handle_response @unauth {',
1168
+ ' respond "Unauthorized" 401',
1169
+ ' }',
1170
+ ' }',
1171
+ ` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
1172
+ ' }',
1173
+ ' # Browser clients: SSO via oauth2-proxy, identity injected as X-Agent-User, then proxy',
1174
+ ' # to the agent (which serves the www landing page at / and the API at /v1/*).',
1175
+ ' handle {',
1176
+ ' forward_auth localhost:4180 {',
1177
+ ' uri /oauth2/auth',
1178
+ ' copy_headers X-Auth-Request-Email>X-Agent-User',
1179
+ ' header_up X-Forwarded-Proto https',
1180
+ ' header_up X-Forwarded-Host {http.request.host}',
1181
+ ' @unauth status 401',
1182
+ ' handle_response @unauth {',
1183
+ ' redir * "/oauth2/sign_in?rd={http.request.uri.path}{http.request.uri.query}"',
1184
+ ' }',
1185
+ ' }',
1186
+ ` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
1187
+ ' }',
1188
+ '}',
1189
+ '',
1190
+ ].join('\n');
1191
+ }
1192
+ /** Cloud Run multi-container service behind the AgentHippo edge (Option B). Caddy is ingress. */
1193
+ function renderCloudrunEdgeServiceYaml(ctx) {
1194
+ const isCursor = ctx.engine.trim().toLowerCase() === 'cursor';
1195
+ const providerKeyEnv = litellmProviderKeyEnv(ctx.model);
1196
+ const agentArgs = serveCmdArgs(ctx, { wwwDir: '/app/pack/deploy/www', port: EDGE_AGENT_PORT })
1197
+ .map((a) => `"${a.replace(/"/g, '\\"')}"`).join(', ');
1198
+ // Start order: sidecars before caddy (ingress) so Caddy's first request finds everyone up.
1199
+ const deps = isCursor
1200
+ ? '{"caddy":["agent","oauth2-proxy","auth-broker"]}'
1201
+ : '{"caddy":["agent","oauth2-proxy","auth-broker"],"agent":["litellm"],"litellm":["otel-collector"]}';
1202
+ const lines = [
1203
+ '# Cloud Run + AgentHippo edge (Option B): Caddy (ingress) + oauth2-proxy + auth-broker',
1204
+ '# + agent (+ LiteLLM + OTEL collector for Spotlight). No IAP — Caddy gates via SSO,',
1205
+ '# so the IDE hits the public hostname directly with its AgentHippo Bearer token.',
1206
+ '# Generated by `agenthippo deploy init --target cloudrun --ingress edge`.',
1207
+ 'apiVersion: serving.knative.dev/v1',
1208
+ 'kind: Service',
1209
+ 'metadata:',
1210
+ ' name: ${SERVICE}',
1211
+ 'spec:',
1212
+ ' template:',
1213
+ ' metadata:',
1214
+ ' annotations:',
1215
+ ' run.googleapis.com/cpu-throttling: "false"',
1216
+ ' run.googleapis.com/startup-cpu-boost: "true"',
1217
+ ` run.googleapis.com/container-dependencies: '${deps}'`,
1218
+ ' autoscaling.knative.dev/maxScale: "1"',
1219
+ ...(ctx.gatewayChannels?.trim() ? [' autoscaling.knative.dev/minScale: "1"'] : []),
1220
+ ' spec:',
1221
+ ' timeoutSeconds: 3600',
1222
+ ' containers:',
1223
+ ' # Ingress — the ONLY container with a port. SSO edge for browsers + IDE Bearer.',
1224
+ ' - name: caddy',
1225
+ ' image: caddy:2.8-alpine',
1226
+ ' ports:',
1227
+ ' - name: http1',
1228
+ ` containerPort: ${ctx.port}`,
1229
+ ' volumeMounts:',
1230
+ ' - { name: caddy-config, mountPath: /etc/caddy, readOnly: true }',
1231
+ ' # Auth broker — validates IDE Bearer JWTs (/api/ide/me) + IDE sign-in (/oauth/*).',
1232
+ ' # Reuses the agent runtime image (the `agenthippo` CLI ships `auth-broker`).',
1233
+ ' - name: auth-broker',
1234
+ ' image: ${IMAGE}',
1235
+ ' args: ["auth-broker"]',
1236
+ ' env:',
1237
+ ' - { name: AGENTHIPPO_AUTH_PORT, value: "9780" }',
1238
+ ' - { name: AGENTHIPPO_AUTH_DATA_DIR, value: /data }',
1239
+ ' - { name: AGENTHIPPO_AUTH_PUBLIC_URL, value: https://${PUBLIC_HOSTNAME} }',
1240
+ ' - { name: AGENTHIPPO_AUTH_ALLOWED_DOMAINS, value: ${ALLOWED_DOMAINS} }',
1241
+ ' - name: AGENTHIPPO_AUTH_JWT_SECRET',
1242
+ ' valueFrom:',
1243
+ ' secretKeyRef: { name: ${SERVICE}-auth-jwt-secret, key: latest }',
1244
+ ' # oauth2-proxy — Google SSO for browser clients (provider=google).',
1245
+ ' - name: oauth2-proxy',
1246
+ ' image: quay.io/oauth2-proxy/oauth2-proxy:v7.7.1',
1247
+ ' env:',
1248
+ ' - { name: OAUTH2_PROXY_HTTP_ADDRESS, value: 0.0.0.0:4180 }',
1249
+ ' - { name: OAUTH2_PROXY_UPSTREAMS, value: file:///dev/null }',
1250
+ ' - { name: OAUTH2_PROXY_SET_XAUTHREQUEST, value: "true" }',
1251
+ ' - { name: OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER, value: "true" }',
1252
+ ' - { name: OAUTH2_PROXY_SKIP_PROVIDER_BUTTON, value: "true" }',
1253
+ ' - { name: OAUTH2_PROXY_REVERSE_PROXY, value: "true" }',
1254
+ ' - { name: OAUTH2_PROXY_PROVIDER, value: google }',
1255
+ ' - { name: OAUTH2_PROXY_EMAIL_DOMAINS, value: ${ALLOWED_DOMAINS} }',
1256
+ ' - { name: OAUTH2_PROXY_REDIRECT_URL, value: https://${PUBLIC_HOSTNAME}/oauth2/callback }',
1257
+ ' - name: OAUTH2_PROXY_CLIENT_ID',
1258
+ ' valueFrom:',
1259
+ ' secretKeyRef: { name: ${SERVICE}-oauth-client-id, key: latest }',
1260
+ ' - name: OAUTH2_PROXY_CLIENT_SECRET',
1261
+ ' valueFrom:',
1262
+ ' secretKeyRef: { name: ${SERVICE}-oauth-client-secret, key: latest }',
1263
+ ' - name: OAUTH2_PROXY_COOKIE_SECRET',
1264
+ ' valueFrom:',
1265
+ ' secretKeyRef: { name: ${SERVICE}-oauth-cookie-secret, key: latest }',
1266
+ ' # Agent — serves www + /v1/* on localhost (NOT ingress). Routed through LiteLLM.',
1267
+ ' - name: agent',
1268
+ ' image: ${IMAGE}',
1269
+ ` args: [${agentArgs}]`,
1270
+ ' env:',
1271
+ ' - { name: AGENTHIPPO_HOME, value: /state }',
1272
+ ' - { name: AGENTHIPPO_NO_AUTO_INSTALL, value: "1" }',
1273
+ agentServeHostEnvYaml(),
1274
+ ...(isCursor ? [
1275
+ ' - { name: AGENTHIPPO_CUSTOM_ENGINES_DIR, value: /engines }',
1276
+ ' - name: CURSOR_API_KEY',
1277
+ ' valueFrom:',
1278
+ ' secretKeyRef: { name: ${SERVICE}-provider-key, key: latest }',
1279
+ ] : [
1280
+ ' - { name: AGENTHIPPO_LITELLM_BASE_URL, value: http://localhost:4443 }',
1281
+ ' - name: AGENTHIPPO_LITELLM_API_KEY',
1282
+ ' valueFrom:',
1283
+ ' secretKeyRef: { name: ${SERVICE}-litellm-key, key: latest }',
1284
+ ]),
1285
+ ' volumeMounts:',
1286
+ ' - { name: state, mountPath: /state }',
1287
+ ...startupProbeLines(EDGE_AGENT_PORT, true),
1288
+ ...resourceLimits('2000m', '2Gi'),
1289
+ ];
1290
+ if (!isCursor) {
1291
+ lines.push(' # LiteLLM gateway sidecar — exports every call to the collector (Spotlight).', ' - name: litellm', ' image: ${LITELLM_IMAGE}', ' env:', ' - { name: OTEL_EXPORTER, value: otlp_http }', ' - { name: OTEL_EXPORTER_OTLP_ENDPOINT, value: http://localhost:4318 }', ' - name: LITELLM_MASTER_KEY', ' valueFrom:', ' secretKeyRef: { name: ${SERVICE}-litellm-key, key: latest }', ` # Upstream provider key for ${ctx.model || 'this model'} (matches the gateway's os.environ/${providerKeyEnv} ref).`, ` - name: ${providerKeyEnv}`, ' valueFrom:', ' secretKeyRef: { name: ${SERVICE}-provider-key, key: latest }', ...startupProbeLines(4443), ...resourceLimits('1000m', '1Gi'), ' # Spotlight OTEL collector sidecar — writes JSONL traces into the shared /state volume.', ' - name: otel-collector', ' image: otel/opentelemetry-collector-contrib:0.118.0', ' args: ["--config=/etc/otel/config.yaml"]', ' env:', ' - { name: AGENT_HOME_DIR, value: /state }', ' volumeMounts:', ' - { name: otel-config, mountPath: /etc/otel, readOnly: true }', ' - { name: state, mountPath: /state }', ...startupProbeLines(4318), ...resourceLimits('1000m', '512Mi'));
1292
+ }
1293
+ lines.push(' volumes:', ' - name: state', ' csi:', ' driver: gcsfuse.run.googleapis.com', ' volumeAttributes:', ' bucketName: ${STATE_BUCKET}', ' - name: caddy-config', ' secret:', ' secretName: ${SERVICE}-caddyfile', ' items:', ' - { key: latest, path: Caddyfile }');
1294
+ if (!isCursor) {
1295
+ lines.push(' - name: otel-config', ' secret:', ' secretName: ${SERVICE}-otel-config', ' items:', ' - { key: latest, path: config.yaml }');
1296
+ }
1297
+ lines.push('');
1298
+ return lines.join('\n');
1299
+ }
1300
+ /** Deploy script for the Cloud Run edge (Option B): build, upsert secrets, replace, make public. */
1301
+ function renderCloudrunEdgeDeploy(ctx) {
1302
+ const isCursor = ctx.engine.trim().toLowerCase() === 'cursor';
1303
+ const modelNote = ctx.model?.trim() ? `, model ${ctx.model.trim()}` : '';
1304
+ const lines = [
1305
+ '#!/usr/bin/env bash',
1306
+ `# Cloud Run + AgentHippo edge (Option B): Caddy + oauth2-proxy + auth-broker front the agent.`,
1307
+ `# Engine: ${ctx.engine}${modelNote}. No IAP — Caddy gates via SSO (Google for browsers, AgentHippo JWT for IDE).`,
1308
+ '# The IDE points spec.remote.url straight at https://$PUBLIC_HOSTNAME (same flow as remote-vm).',
1309
+ 'set -euo pipefail',
1310
+ ...anchorToStagingRoot(),
1311
+ `SERVICE="${ctx.packName}"`,
1312
+ 'REGION="${REGION:-us-central1}"',
1313
+ 'PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project)}"',
1314
+ 'REPO="${ARTIFACT_REPO:-agenthippo}"',
1315
+ 'IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/${SERVICE}:latest"',
1316
+ 'LITELLM_IMAGE="${LITELLM_IMAGE:-ghcr.io/agenthippo/litellm-gateway:2026.06}"',
1317
+ '# Public hostname (custom domain mapped to the service) + the Google Workspace domain allowed by SSO.',
1318
+ 'PUBLIC_HOSTNAME="${PUBLIC_HOSTNAME:?set PUBLIC_HOSTNAME=your.edge.hostname (gcloud run domain-mappings create --service $SERVICE --domain ...)}"',
1319
+ 'ALLOWED_DOMAINS="${ALLOWED_DOMAINS:?set ALLOWED_DOMAINS=yourworkspace.com (oauth2-proxy email domain + broker allowed domains)}"',
1320
+ 'STATE_BUCKET="${STATE_BUCKET:?set STATE_BUCKET=your-gcs-bucket (gcloud storage buckets create gs://...)}"',
1321
+ '# Edge secrets (from .env.auth): Google OAuth client + cookie secret + AgentHippo JWT signing key.',
1322
+ 'OAUTH_CLIENT_ID="${OAUTH_CLIENT_ID:?set OAUTH_CLIENT_ID=<google oauth web client id>}"',
1323
+ 'OAUTH_CLIENT_SECRET="${OAUTH_CLIENT_SECRET:?set OAUTH_CLIENT_SECRET=<google oauth web client secret>}"',
1324
+ 'OAUTH_COOKIE_SECRET="${OAUTH_COOKIE_SECRET:?set OAUTH_COOKIE_SECRET=$(openssl rand -base64 32 | head -c 32)}"',
1325
+ 'AUTH_JWT_SECRET="${AUTH_JWT_SECRET:?set AUTH_JWT_SECRET=$(openssl rand -hex 32)}"',
1326
+ ...(isCursor ? [
1327
+ 'PROVIDER_KEY="${PROVIDER_KEY:?set PROVIDER_KEY=<cursor api key>}"',
1328
+ ] : [
1329
+ '# LiteLLM proxy key + the upstream provider key the gateway uses for the model.',
1330
+ 'LITELLM_KEY="${LITELLM_KEY:?set LITELLM_KEY=<litellm proxy key>}"',
1331
+ 'PROVIDER_KEY="${PROVIDER_KEY:?set PROVIDER_KEY=<upstream provider key>}"',
1332
+ ]),
1333
+ '',
1334
+ 'gcloud artifacts repositories describe "$REPO" --location="$REGION" 2>/dev/null \\',
1335
+ ' || gcloud artifacts repositories create "$REPO" --repository-format=docker --location="$REGION"',
1336
+ 'gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet',
1337
+ '',
1338
+ '# One agent image serves both the agent and the auth-broker containers (CLI ships `auth-broker`).',
1339
+ '# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/agenthippo/runtime:latest',
1340
+ '# tag was retired; AR has runtime:latest / :latest-cursor). No-op if the base already targets AR.',
1341
+ 'DF_TMP="$(mktemp)"',
1342
+ 'sed "s|FROM ghcr.io/agenthippo/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
1343
+ 'docker build --pull --platform linux/amd64 -f "$DF_TMP" -t "$IMAGE" .',
1344
+ 'docker push "$IMAGE"',
1345
+ '',
1346
+ '# Secrets: edge config + provider/litellm keys.',
1347
+ 'ensure_secret() { gcloud secrets describe "$1" >/dev/null 2>&1 || gcloud secrets create "$1" --replication-policy=automatic; }',
1348
+ 'ensure_secret "${SERVICE}-caddyfile"; gcloud secrets versions add "${SERVICE}-caddyfile" --data-file=deploy/Caddyfile',
1349
+ 'ensure_secret "${SERVICE}-auth-jwt-secret"; printf %s "$AUTH_JWT_SECRET" | gcloud secrets versions add "${SERVICE}-auth-jwt-secret" --data-file=-',
1350
+ 'ensure_secret "${SERVICE}-oauth-client-id"; printf %s "$OAUTH_CLIENT_ID" | gcloud secrets versions add "${SERVICE}-oauth-client-id" --data-file=-',
1351
+ 'ensure_secret "${SERVICE}-oauth-client-secret"; printf %s "$OAUTH_CLIENT_SECRET" | gcloud secrets versions add "${SERVICE}-oauth-client-secret" --data-file=-',
1352
+ 'ensure_secret "${SERVICE}-oauth-cookie-secret"; printf %s "$OAUTH_COOKIE_SECRET" | gcloud secrets versions add "${SERVICE}-oauth-cookie-secret" --data-file=-',
1353
+ 'ensure_secret "${SERVICE}-provider-key"; printf %s "$PROVIDER_KEY" | gcloud secrets versions add "${SERVICE}-provider-key" --data-file=-',
1354
+ ];
1355
+ if (!isCursor) {
1356
+ lines.push('ensure_secret "${SERVICE}-otel-config"; gcloud secrets versions add "${SERVICE}-otel-config" --data-file=deploy/otel-collector.yaml', 'ensure_secret "${SERVICE}-litellm-key"; printf %s "$LITELLM_KEY" | gcloud secrets versions add "${SERVICE}-litellm-key" --data-file=-');
1357
+ }
1358
+ lines.push('', '# Render the multi-container template for this project (sed — no gettext dependency) and deploy.', 'RENDERED="$(mktemp)"', 'sed -e "s|\\${SERVICE}|${SERVICE}|g" -e "s|\\${IMAGE}|${IMAGE}|g" \\', ' -e "s|\\${LITELLM_IMAGE}|${LITELLM_IMAGE}|g" -e "s|\\${STATE_BUCKET}|${STATE_BUCKET}|g" \\', ' -e "s|\\${PUBLIC_HOSTNAME}|${PUBLIC_HOSTNAME}|g" -e "s|\\${ALLOWED_DOMAINS}|${ALLOWED_DOMAINS}|g" \\', ' deploy/service.yaml > "$RENDERED"', 'gcloud run services replace "$RENDERED" --region "$REGION"', '', '# No IAP: Caddy is the gate, so Cloud Run itself must allow public ingress (allUsers invoker).', '# Without this, Cloud Run 403s before Caddy ever sees the request to run SSO.', 'gcloud run services add-iam-policy-binding "$SERVICE" --region "$REGION" \\', ' --member=allUsers --role=roles/run.invoker >/dev/null 2>&1 || true', '', '# Map the custom domain (once): gcloud run domain-mappings create --service "$SERVICE" --region "$REGION" --domain "$PUBLIC_HOSTNAME"', '# Then add the CNAME Cloud Run prints to your DNS. Google OAuth Web client must list', '# https://$PUBLIC_HOSTNAME as an authorized origin + https://$PUBLIC_HOSTNAME/oauth2/callback as a redirect.', '', '# Grant the runtime SA access to the secrets if needed:', '# SA="$(gcloud run services describe "$SERVICE" --region "$REGION" --format=\'value(spec.template.spec.serviceAccountName)\')"', '# for s in "${SERVICE}-caddyfile" "${SERVICE}-auth-jwt-secret" "${SERVICE}-oauth-client-id" "${SERVICE}-oauth-client-secret" "${SERVICE}-oauth-cookie-secret" "${SERVICE}-provider-key"; do', '# gcloud secrets add-iam-policy-binding "$s" --member="serviceAccount:$SA" --role=roles/secretmanager.secretAccessor', '# done', '');
1359
+ return lines.join('\n');
1360
+ }
1361
+ function cloudrunArtifacts(ctx) {
1362
+ const inbound = Boolean(ctx.gatewayChannels?.trim()); // 1b: holds an always-listening channel socket
1363
+ const isCursor = ctx.engine.trim().toLowerCase() === 'cursor';
1364
+ // Option B: AgentHippo edge on Cloud Run (Caddy + auth-broker + oauth2-proxy ingress).
1365
+ // Same SSO flow the IDE already uses for remote-vm — point spec.remote.url at the public
1366
+ // hostname and the existing ensureSignedIn + Bearer path works. No IAP, no IDE changes.
1367
+ if (ctx.ingress === 'edge') {
1368
+ return cloudrunEdgeArtifacts(ctx, inbound, isCursor);
1369
+ }
1370
+ // A′: federated edge — Caddy + auth-broker only (no oauth2-proxy). Google login stays at the
1371
+ // central IdP; this agent verifies the shared .agenthippo.ai JWT. Scales to N agents with zero
1372
+ // per-service OAuth client config. No IAP, no IDE changes.
1373
+ if (ctx.ingress === 'edge-fed') {
1374
+ return cloudrunFedArtifacts(ctx, inbound, isCursor);
1375
+ }
1376
+ // Model-routed engines (claude/codex) get the Spotlight multi-container stack: the agent
1377
+ // calls a LiteLLM sidecar (so a `litellm/*` model resolves in-pod) and the OTEL collector
1378
+ // lands cost/latency/token traces in the GCS /state volume. This also resolves the
1379
+ // CLAUDE_CODE_USE_VERTEX-vs-LiteLLM contradiction — provider routing now lives on the gateway.
1380
+ // Cursor talks to its own API (not through LiteLLM), so it keeps the lean single-container path.
1381
+ if (!isCursor) {
1382
+ return {
1383
+ 'Dockerfile': sharedDockerfile(ctx, { wwwDir: '/app/pack/deploy/www' }),
1384
+ 'www/index.html': renderLandingPage(ctx.packName),
1385
+ 'service.yaml': renderCloudrunServiceYaml(ctx),
1386
+ 'otel-collector.yaml': OTEL_COLLECTOR_COMPOSE_YAML,
1387
+ 'cloudrun-deploy.sh': renderCloudrunSpotlightDeploy(ctx),
1388
+ 'README.md': [
1389
+ `# Deploy: ${ctx.packName} — Cloud Run + Spotlight (multi-container)`,
1390
+ '',
1391
+ `Engine: **${ctx.engine}**${ctx.model ? `, model: **${ctx.model}**` : ''}. Runtime base: \`${ctx.image}\`.`,
1392
+ '',
1393
+ 'Three sidecars share the pod\'s localhost (Cloud Run multi-container):',
1394
+ '- **agent** (ingress) → routes the engine to `http://localhost:4443`',
1395
+ '- **litellm** gateway → exports OTLP to `http://localhost:4318`',
1396
+ '- **otel-collector** → writes JSONL traces to the GCS `/state` volume (`/state/analytics/traces`).',
1397
+ '',
1398
+ '```bash',
1399
+ 'gcloud storage buckets create gs://<your-state-bucket> # once',
1400
+ 'export STATE_BUCKET=<your-state-bucket> LITELLM_KEY=<litellm proxy key>',
1401
+ 'bash deploy/cloudrun-deploy.sh # builds linux/amd64, creates secrets, `gcloud run services replace service.yaml`',
1402
+ '```',
1403
+ '',
1404
+ '`service.yaml` is the multi-container template (checked in so the wiring is reviewable);',
1405
+ 'the deploy script substitutes `${SERVICE} ${IMAGE} ${STATE_BUCKET} ${LITELLM_IMAGE}`.',
1406
+ 'Provider creds (Anthropic key / Vertex ADC / OpenAI …) go on the **litellm** sidecar, not the agent.',
1407
+ '',
1408
+ inbound
1409
+ ? '**Inbound channel (1b):** `minScale: 1` pins one always-on instance (the socket can\'t scale to zero).'
1410
+ : '**No inbound channel (1a):** scales to zero; traces + sessions persist on the GCS `/state` volume.',
1411
+ '',
1412
+ '**Spotlight:** once traffic flows, query the GCS-backed traces with `query-analytics.sh` (DuckDB) against `/state/analytics/traces`.',
1413
+ '',
1414
+ '**Lean alternative (no Spotlight):** a single-container deploy that calls a provider directly',
1415
+ '(or an external LiteLLM) — set `AGENTHIPPO_LITELLM_BASE_URL` to your endpoint and drop the sidecars.',
1416
+ '',
1417
+ ].join('\n'),
1418
+ };
1419
+ }
1420
+ // 1a (no inbound channel): scale-to-zero OK. 1b (inbound socket): pin a single,
1421
+ // always-on instance with CPU always allocated so the socket survives.
1422
+ const instanceFlags = inbound
1423
+ ? ' --min-instances 1 --max-instances 1 --no-cpu-throttling \\'
1424
+ : ' --min-instances 0 --max-instances 1 \\';
1425
+ const runEnvVars = isCursor
1426
+ ? 'AGENTHIPPO_NO_AUTO_INSTALL=1,AGENTHIPPO_HOME=/state,AGENTHIPPO_CUSTOM_ENGINES_DIR=/engines'
1427
+ : 'CLAUDE_CODE_USE_VERTEX=1,AGENTHIPPO_NO_AUTO_INSTALL=1,AGENTHIPPO_HOME=/state';
1428
+ const runSecrets = isCursor
1429
+ ? 'CURSOR_API_KEY=agenthippo-cursor-key:latest'
1430
+ : undefined;
1431
+ const modelNote = ctx.model?.trim() ? `, model ${ctx.model.trim()}` : '';
1432
+ const deploy = [
1433
+ '#!/usr/bin/env bash',
1434
+ `# T1-generic: Cloud Run with built-in IAP (corp SSO, no load balancer).`,
1435
+ `# Engine: ${ctx.engine}${modelNote}. IAP forwards X-Goog-Authenticated-User-Email → user_id.`,
1436
+ 'set -euo pipefail',
1437
+ ...anchorToStagingRoot(),
1438
+ `SERVICE="${ctx.packName}"`,
1439
+ 'REGION="${REGION:-us-central1}"',
1440
+ 'PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project)}"',
1441
+ 'REPO="${ARTIFACT_REPO:-agenthippo}"',
1442
+ 'IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/${SERVICE}:latest"',
1443
+ '# Q1: durable state. Cloud Run local disk is ephemeral (wiped on scale-to-zero),',
1444
+ '# so mount a GCS bucket at /state — sessions/provider-sessions/analytics survive.',
1445
+ 'STATE_BUCKET="${STATE_BUCKET:?set STATE_BUCKET=your-gcs-bucket (gcloud storage buckets create gs://...)}"',
1446
+ '',
1447
+ 'gcloud artifacts repositories describe "$REPO" --location="$REGION" 2>/dev/null \\',
1448
+ ' || gcloud artifacts repositories create "$REPO" --repository-format=docker --location="$REGION"',
1449
+ 'gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet',
1450
+ '',
1451
+ '# Build from pack root; Dockerfile is deploy/Dockerfile (not pack root — gcloud --source . misses it).',
1452
+ '# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/agenthippo/runtime:latest',
1453
+ '# tag was retired; AR has runtime:latest / :latest-cursor). No-op if the base already targets AR.',
1454
+ 'DF_TMP="$(mktemp)"',
1455
+ 'sed "s|FROM ghcr.io/agenthippo/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
1456
+ 'docker build --pull --platform linux/amd64 -f "$DF_TMP" -t "$IMAGE" .',
1457
+ 'docker push "$IMAGE"',
1458
+ '',
1459
+ '# Deploy private (no --allow-unauthenticated). IAP + role grants are applied below on the beta track.',
1460
+ 'gcloud run deploy "$SERVICE" \\',
1461
+ ' --image "$IMAGE" \\',
1462
+ ' --no-allow-unauthenticated \\',
1463
+ ` --port ${ctx.port} \\`,
1464
+ instanceFlags,
1465
+ ' --timeout 3600 \\',
1466
+ ' --add-volume name=state,type=cloud-storage,bucket="$STATE_BUCKET" \\',
1467
+ ' --add-volume-mount volume=state,mount-path=/state \\',
1468
+ ` --set-env-vars ${runEnvVars} \\`,
1469
+ ...(runSecrets ? [` --set-secrets ${runSecrets} \\`] : []),
1470
+ ' --region "$REGION"',
1471
+ '',
1472
+ ...iapFinalizeLines(),
1473
+ '',
1474
+ '# Notes:',
1475
+ inbound
1476
+ ? '# - INBOUND channel (1b): pinned min=max=1 + CPU-always-on so the socket stays alive (can\'t scale to zero).'
1477
+ : '# - No inbound channel (1a): scales to zero; state on the GCS volume survives the instance dying.',
1478
+ '# - GCS-FUSE adds latency on session read/write vs local disk; fine for chat, watch for heavy I/O.',
1479
+ isCursor
1480
+ ? '# - Cursor engine: CURSOR_API_KEY secret + runtime:latest-cursor base (AGENTHIPPO_CUSTOM_ENGINES_DIR=/engines).'
1481
+ : '# - Vertex path = keyless Claude on your existing GCP entitlement; swap for ANTHROPIC_API_KEY if preferred.',
1482
+ '# - First deploy without IAP org setup: ENABLE_IAP=0 bash deploy/cloudrun-deploy.sh',
1483
+ '',
1484
+ ].join('\n');
1485
+ const secretSteps = isCursor
1486
+ ? [
1487
+ '2. `gcloud secrets create agenthippo-cursor-key --data-file=-` (paste your `CURSOR_API_KEY`).',
1488
+ '3. Grant Cloud Run access: `gcloud secrets add-iam-policy-binding agenthippo-cursor-key --member=serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com --role=roles/secretmanager.secretAccessor`.',
1489
+ '4. `bash deploy/cloudrun-deploy.sh` — builds linux/amd64, pushes to Artifact Registry, deploys with GCS /state.',
1490
+ ]
1491
+ : [
1492
+ '2. `bash deploy/cloudrun-deploy.sh` — builds linux/amd64, pushes to Artifact Registry, deploys with `--iap` + GCS /state.',
1493
+ ];
1494
+ return {
1495
+ 'Dockerfile': sharedDockerfile(ctx, { wwwDir: '/app/pack/deploy/www' }),
1496
+ 'www/index.html': renderLandingPage(ctx.packName),
1497
+ 'cloudrun-deploy.sh': deploy,
1498
+ 'README.md': [
1499
+ `# Deploy: ${ctx.packName} — Cloud Run (T1-generic)`,
1500
+ '',
1501
+ `Engine: **${ctx.engine}**${ctx.model ? `, model: **${ctx.model}**` : ''}. Runtime base: \`${ctx.image}\`.`,
1502
+ '',
1503
+ '1. `gcloud storage buckets create gs://<your-state-bucket>` then `export STATE_BUCKET=<your-state-bucket>`.',
1504
+ ...secretSteps,
1505
+ '',
1506
+ 'SSO + ingress: built-in IAP (`X-Goog-Authenticated-User-Email`). **Q1:** `/state` is a mounted GCS bucket.',
1507
+ ctx.gatewayChannels ? '**Inbound channel (1b):** pinned single always-on instance (no scale-to-zero).' : '**No inbound channel (1a):** scales to zero; state persists on GCS.',
1508
+ '',
1509
+ '**Browser:** open the service URL → Google login → chat UI at `/` (same as vm `deploy/www/index.html`).',
1510
+ '',
1511
+ '**Verify** (smoke test via local proxy — bypasses IAP auth for direct testing):',
1512
+ '```bash',
1513
+ `gcloud run services proxy ${ctx.packName} --region "\${REGION:-us-central1}" &`,
1514
+ 'PROXY=http://127.0.0.1:8080',
1515
+ 'curl -s "$PROXY/ready"',
1516
+ 'curl -s -XPOST "$PROXY/v1/chat" \\',
1517
+ ' -H "X-Goog-Authenticated-User-Email: accounts.google.com:you@corp.com" \\',
1518
+ ' -H "Content-Type: application/json" \\',
1519
+ ' -d \'{"message":"Reply with exactly: CLOUDRUN_OK","session_id":"cr-1"}\'',
1520
+ '```',
1521
+ '',
1522
+ ].join('\n'),
1523
+ };
1524
+ }
1525
+ /** Option B artifacts: Cloud Run behind the AgentHippo edge (Caddy + auth-broker + oauth2-proxy). */
1526
+ function cloudrunEdgeArtifacts(ctx, inbound, isCursor) {
1527
+ const sidecars = isCursor
1528
+ ? 'Caddy (ingress) + oauth2-proxy + auth-broker + agent.'
1529
+ : 'Caddy (ingress) + oauth2-proxy + auth-broker + agent + LiteLLM + OTEL collector (Spotlight).';
1530
+ const files = {
1531
+ 'Dockerfile': sharedDockerfile(ctx, { wwwDir: '/app/pack/deploy/www' }),
1532
+ 'www/index.html': renderLandingPage(ctx.packName),
1533
+ 'Caddyfile': renderCloudrunEdgeCaddyfile(),
1534
+ 'service.yaml': renderCloudrunEdgeServiceYaml(ctx),
1535
+ 'cloudrun-deploy.sh': renderCloudrunEdgeDeploy(ctx),
1536
+ };
1537
+ if (!isCursor) {
1538
+ files['otel-collector.yaml'] = OTEL_COLLECTOR_COMPOSE_YAML;
1539
+ }
1540
+ files['README.md'] = [
1541
+ `# Deploy: ${ctx.packName} — Cloud Run + AgentHippo edge (Option B)`,
1542
+ '',
1543
+ `Engine: **${ctx.engine}**${ctx.model ? `, model: **${ctx.model}**` : ''}. Runtime base: \`${ctx.image}\`.`,
1544
+ '',
1545
+ `${sidecars} Sidecars share the pod\'s localhost (Cloud Run multi-container):`,
1546
+ '- **caddy** (ingress, the only port) → SSO edge: oauth2-proxy for browsers, auth-broker for IDE Bearer',
1547
+ `- **agent** → serves www + /v1/* on localhost:${EDGE_AGENT_PORT} (NOT ingress)`,
1548
+ '- **auth-broker** → validates IDE AgentHippo JWTs (`/api/ide/me`) + IDE sign-in (`/oauth/*`)',
1549
+ '- **oauth2-proxy** → Google SSO for browser clients (`provider=google`)',
1550
+ ...(isCursor ? [] : [
1551
+ '- **litellm** gateway → exports OTLP to `http://localhost:4318`',
1552
+ '- **otel-collector** → writes JSONL traces to the GCS `/state` volume',
1553
+ ]),
1554
+ '',
1555
+ '**No IAP** — Caddy is the gate, so the IDE points `spec.remote.url` straight at',
1556
+ '`https://$PUBLIC_HOSTNAME` and the existing AgentHippo SSO flow works (same as `remote-vm`).',
1557
+ 'Cloud Run itself is public (`allUsers` invoker) so traffic can reach Caddy to be authed.',
1558
+ '',
1559
+ '```bash',
1560
+ 'gcloud storage buckets create gs://<state-bucket> # once',
1561
+ 'cp deploy/.env.example deploy/.env && cp deploy/.env.auth.example deploy/.env.auth # fill both',
1562
+ 'export PUBLIC_HOSTNAME=<your.edge.hostname> ALLOWED_DOMAINS=<yourworkspace.com>',
1563
+ 'export OAUTH_CLIENT_ID=… OAUTH_CLIENT_SECRET=… OAUTH_COOKIE_SECRET=… AUTH_JWT_SECRET=…',
1564
+ ...(isCursor ? ['export PROVIDER_KEY=<cursor api key>'] : ['export LITELLM_KEY=… PROVIDER_KEY=… STATE_BUCKET=<state-bucket>']),
1565
+ 'bash deploy/cloudrun-deploy.sh # builds linux/amd64, upserts secrets, `gcloud run services replace`',
1566
+ 'gcloud run domain-mappings create --service "$SERVICE" --region "${REGION:-us-central1}" --domain "$PUBLIC_HOSTNAME" # once',
1567
+ '```',
1568
+ '',
1569
+ 'Google OAuth Web client (for oauth2-proxy): authorized origin `https://$PUBLIC_HOSTNAME`,',
1570
+ 'redirect `https://$PUBLIC_HOSTNAME/oauth2/callback`. Restrict to your Workspace domain via',
1571
+ '`ALLOWED_DOMAINS` (oauth2-proxy `email_domains` + broker `allowed_domains`).',
1572
+ '',
1573
+ '`service.yaml` placeholders (`${SERVICE} ${IMAGE} ${LITELLM_IMAGE} ${STATE_BUCKET} ${PUBLIC_HOSTNAME} ${ALLOWED_DOMAINS}`)',
1574
+ 'are substituted by the deploy script. The Caddyfile is stored in Secret Manager (`${SERVICE}-caddyfile`).',
1575
+ '',
1576
+ inbound
1577
+ ? '**Inbound channel (1b):** `minScale: 1` pins one always-on instance (the socket can\'t scale to zero).'
1578
+ : '**No inbound channel (1a):** scales to zero; traces + sessions persist on the GCS `/state` volume.',
1579
+ '',
1580
+ ].join('\n');
1581
+ return files;
1582
+ }
1583
+ /**
1584
+ * A′ — Federated edge Caddyfile. No oauth2-proxy: Google login happens once at the central IdP
1585
+ * (`ctx.idpUrl`); this agent only verifies the shared AgentHippo JWT (Bearer for IDE, cookie for
1586
+ * browser). On a browser 401, redirect to the IdP's `/oauth/browser-login` to stamp the shared
1587
+ * `.agenthippo.ai` cookie, then return.
1588
+ */
1589
+ function renderCloudrunFedCaddyfile(ctx) {
1590
+ const idp = (ctx.idpUrl || '').trim().replace(/\/+$/, '');
1591
+ const loginRedirect = idp
1592
+ ? ` redir * "${idp}/oauth/browser-login?return_to=https://{http.request.host}{http.request.uri}" 302`
1593
+ : ' respond "Unauthorized" 401';
1594
+ return [
1595
+ '# Cloud Run federated edge (A′). Cloud Run terminates TLS; Caddy listens on HTTP :{$PORT}.',
1596
+ `# No oauth2-proxy — Google login happens once at the central IdP (${idp || '<IDP_URL>'});`,
1597
+ '# this agent only verifies the shared .agenthippo.ai AgentHippo JWT (Bearer for IDE, cookie for browser).',
1598
+ '{',
1599
+ ' auto_https off',
1600
+ '}',
1601
+ '',
1602
+ ':{$PORT} {',
1603
+ ' # Auth-broker: IDE bootstrap API + JWT validation (/api/ide/me is also used by forward_auth).',
1604
+ ' handle /api/ide/* {',
1605
+ ' reverse_proxy localhost:9780 {',
1606
+ ' header_up X-Forwarded-Proto https',
1607
+ ' header_up X-Forwarded-Host {http.request.host}',
1608
+ ' }',
1609
+ ' }',
1610
+ ' @health path /health /ready',
1611
+ ' handle @health {',
1612
+ ` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
1613
+ ' }',
1614
+ ' # IDE / API clients with a Bearer token: validate via auth-broker, copy identity header.',
1615
+ ' @apiWithAuth {',
1616
+ ' path /v1/*',
1617
+ ' header Authorization *',
1618
+ ' }',
1619
+ ' handle @apiWithAuth {',
1620
+ ' forward_auth localhost:9780 {',
1621
+ ' uri /api/ide/me',
1622
+ ' copy_headers X-Agent-User',
1623
+ ' header_up X-Forwarded-Proto https',
1624
+ ' header_up X-Forwarded-Host {http.request.host}',
1625
+ ' @unauth status 401',
1626
+ ' handle_response @unauth {',
1627
+ ' respond "Unauthorized" 401',
1628
+ ' }',
1629
+ ' }',
1630
+ ` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
1631
+ ' }',
1632
+ ' # Browser clients: the shared .agenthippo.ai cookie is verified by auth-broker. No cookie',
1633
+ ' # → redirect to the central IdP to log in (it stamps the cookie), then return.',
1634
+ ' handle {',
1635
+ ' forward_auth localhost:9780 {',
1636
+ ' uri /api/ide/me',
1637
+ ' copy_headers X-Agent-User',
1638
+ ' header_up X-Forwarded-Proto https',
1639
+ ' header_up X-Forwarded-Host {http.request.host}',
1640
+ ' @unauth status 401',
1641
+ ' handle_response @unauth {',
1642
+ loginRedirect,
1643
+ ' }',
1644
+ ' }',
1645
+ ` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
1646
+ ' }',
1647
+ '}',
1648
+ '',
1649
+ ].join('\n');
1650
+ }
1651
+ /** Cloud Run requires a startupProbe on any container that is a `depends_on` target, so the
1652
+ * platform knows when the dependency is ready. `slow` = one generous 240s attempt (agent, which
1653
+ * may warm an engine); otherwise 30×5s polls (sidecars that start fast). */
1654
+ function startupProbeLines(port, slow = false) {
1655
+ if (slow) {
1656
+ // Slow starter (agent): check every 5s but tolerate up to 240s total. A single
1657
+ // failureThreshold:1/periodSeconds:240 probe would only check ONCE at 240s, leaving
1658
+ // the instance "no available instance" for the whole cold start and aborting requests.
1659
+ return [
1660
+ ' startupProbe:',
1661
+ ' failureThreshold: 48',
1662
+ ' periodSeconds: 5',
1663
+ ' tcpSocket:',
1664
+ ` port: ${port}`,
1665
+ ' timeoutSeconds: 3',
1666
+ ];
1667
+ }
1668
+ return [
1669
+ ' startupProbe:',
1670
+ ' failureThreshold: 30',
1671
+ ' periodSeconds: 5',
1672
+ ' tcpSocket:',
1673
+ ` port: ${port}`,
1674
+ ' timeoutSeconds: 3',
1675
+ ];
1676
+ }
1677
+ /** Per-container CPU/memory limits. Cloud Run multi-container shares one instance whose total
1678
+ * memory = sum of container limits, so the streaming agent + Python LiteLLM need real headroom
1679
+ * (defaults of 256Mi each OOM-kill the instance mid-request → 503 "Service Unavailable"). */
1680
+ function resourceLimits(cpu, memory) {
1681
+ return [
1682
+ ' resources:',
1683
+ ' limits:',
1684
+ ` cpu: ${cpu}`,
1685
+ ` memory: ${memory}`,
1686
+ ];
1687
+ }
1688
+ /** A′ — Federated edge multi-container service. Caddy (ingress) + auth-broker + agent
1689
+ * (+ LiteLLM + OTEL for Spotlight). No oauth2-proxy container; in-memory emptyDir state. */
1690
+ function renderCloudrunFedServiceYaml(ctx) {
1691
+ const isCursor = ctx.engine.trim().toLowerCase() === 'cursor';
1692
+ const providerKeyEnv = litellmProviderKeyEnv(ctx.model);
1693
+ const agentArgs = serveCmdArgs(ctx, { wwwDir: '/app/pack/deploy/www', port: EDGE_AGENT_PORT })
1694
+ .map((a) => `"${a.replace(/"/g, '\\"')}"`).join(', ');
1695
+ const cookieDomain = (ctx.cookieDomain || '.agenthippo.ai').trim();
1696
+ const idpUrl = (ctx.idpUrl || 'https://auth.agenthippo.ai').trim().replace(/\/+$/, '');
1697
+ const deps = isCursor
1698
+ ? '{"caddy":["agent","auth-broker"]}'
1699
+ : '{"caddy":["agent","auth-broker"],"agent":["litellm"],"litellm":["otel-collector"]}';
1700
+ const lines = [
1701
+ '# Cloud Run + AgentHippo federated edge (A′): Caddy (ingress) + auth-broker + agent',
1702
+ '# (+ LiteLLM + OTEL collector for Spotlight). No oauth2-proxy — Google login happens once',
1703
+ '# at the central IdP; this agent verifies the shared .agenthippo.ai AgentHippo JWT.',
1704
+ '# Generated by `agenthippo deploy init --target cloudrun --ingress edge-fed`.',
1705
+ 'apiVersion: serving.knative.dev/v1',
1706
+ 'kind: Service',
1707
+ 'metadata:',
1708
+ ' name: ${SERVICE}',
1709
+ 'spec:',
1710
+ ' template:',
1711
+ ' metadata:',
1712
+ ' annotations:',
1713
+ ' run.googleapis.com/cpu-throttling: "false"',
1714
+ ' run.googleapis.com/startup-cpu-boost: "true"',
1715
+ ` run.googleapis.com/container-dependencies: '${deps}'`,
1716
+ ' autoscaling.knative.dev/maxScale: "1"',
1717
+ ...(ctx.gatewayChannels?.trim() ? [' autoscaling.knative.dev/minScale: "1"'] : []),
1718
+ ' spec:',
1719
+ ' timeoutSeconds: 3600',
1720
+ ' containers:',
1721
+ ' # Ingress — the ONLY container with a port. Verifies the shared JWT (no Google here).',
1722
+ ' - name: caddy',
1723
+ ' image: caddy:2.8-alpine',
1724
+ ' ports:',
1725
+ ' - name: http1',
1726
+ ` containerPort: ${ctx.port}`,
1727
+ ' volumeMounts:',
1728
+ ' - { name: caddy-config, mountPath: /etc/caddy, readOnly: true }',
1729
+ ...resourceLimits('1000m', '512Mi'),
1730
+ ' # Auth broker — verifies IDE Bearer JWTs AND the federated .agenthippo.ai cookie (/api/ide/me).',
1731
+ ' # Same shared AGENTHIPPO_AUTH_JWT_SECRET as the central IdP so its tokens verify here.',
1732
+ ' - name: auth-broker',
1733
+ ' image: ${IMAGE}',
1734
+ ' args: ["auth-broker"]',
1735
+ ' env:',
1736
+ ' - { name: AGENTHIPPO_AUTH_PORT, value: "9780" }',
1737
+ ' - { name: AGENTHIPPO_AUTH_DATA_DIR, value: /data }',
1738
+ ' - { name: AGENTHIPPO_AUTH_PUBLIC_URL, value: https://${PUBLIC_HOSTNAME} }',
1739
+ ' - { name: AGENTHIPPO_AUTH_ALLOWED_DOMAINS, value: ${ALLOWED_DOMAINS} }',
1740
+ ` - { name: AGENTHIPPO_AUTH_COOKIE_DOMAIN, value: "${cookieDomain}" }`,
1741
+ ` - { name: AGENTHIPPO_AUTH_IDP_URL, value: "${idpUrl}" }`,
1742
+ ' - { name: AGENTHIPPO_AUTH_COOKIE_NAME, value: "ah_token" }',
1743
+ ' - name: AGENTHIPPO_AUTH_JWT_SECRET',
1744
+ ' valueFrom:',
1745
+ ' secretKeyRef: { name: ${SERVICE}-auth-jwt-secret, key: latest }',
1746
+ ...startupProbeLines(9780),
1747
+ ...resourceLimits('1000m', '512Mi'),
1748
+ ' # Agent — serves www + /v1/* on localhost (NOT ingress). Routed through LiteLLM.',
1749
+ ' - name: agent',
1750
+ ' image: ${IMAGE}',
1751
+ ` args: [${agentArgs}]`,
1752
+ ' env:',
1753
+ ' - { name: AGENTHIPPO_HOME, value: /state }',
1754
+ ' - { name: AGENTHIPPO_NO_AUTO_INSTALL, value: "1" }',
1755
+ agentServeHostEnvYaml(),
1756
+ ...(isCursor ? [
1757
+ ' - { name: AGENTHIPPO_CUSTOM_ENGINES_DIR, value: /engines }',
1758
+ ' - name: CURSOR_API_KEY',
1759
+ ' valueFrom:',
1760
+ ' secretKeyRef: { name: ${SERVICE}-provider-key, key: latest }',
1761
+ ] : [
1762
+ ' - { name: AGENTHIPPO_LITELLM_BASE_URL, value: http://localhost:4443 }',
1763
+ ' - name: AGENTHIPPO_LITELLM_API_KEY',
1764
+ ' valueFrom:',
1765
+ ' secretKeyRef: { name: ${SERVICE}-litellm-key, key: latest }',
1766
+ ]),
1767
+ ' volumeMounts:',
1768
+ ' - { name: state, mountPath: /state }',
1769
+ ...startupProbeLines(EDGE_AGENT_PORT, true),
1770
+ ...resourceLimits('2000m', '2Gi'),
1771
+ ];
1772
+ if (!isCursor) {
1773
+ lines.push(' # LiteLLM gateway sidecar — exports every call to the collector (Spotlight).', ' - name: litellm', ' image: ${LITELLM_IMAGE}', ' env:', ' - { name: OTEL_EXPORTER, value: otlp_http }', ' - { name: OTEL_EXPORTER_OTLP_ENDPOINT, value: http://localhost:4318 }', ' - name: LITELLM_MASTER_KEY', ' valueFrom:', ' secretKeyRef: { name: ${SERVICE}-litellm-key, key: latest }', ` # Upstream provider key for ${ctx.model || 'this model'} (matches the gateway's os.environ/${providerKeyEnv} ref).`, ` - name: ${providerKeyEnv}`, ' valueFrom:', ' secretKeyRef: { name: ${SERVICE}-provider-key, key: latest }', ...startupProbeLines(4443), ...resourceLimits('1000m', '1Gi'), ' # Spotlight OTEL collector sidecar — writes JSONL traces into the shared /state volume.', ' - name: otel-collector', ' image: otel/opentelemetry-collector-contrib:0.118.0', ' args: ["--config=/etc/otel/config.yaml"]', ' env:', ' - { name: AGENT_HOME_DIR, value: /state }', ' volumeMounts:', ' - { name: otel-config, mountPath: /etc/otel, readOnly: true }', ' - { name: state, mountPath: /state }', ...startupProbeLines(4318), ...resourceLimits('1000m', '512Mi'));
1774
+ }
1775
+ lines.push(' volumes:', ' # In-memory state (matches the working spotlight deploy — no GCS SA perms needed).', ' - name: state', ' emptyDir:', ' medium: Memory', ' sizeLimit: 1Gi', ' - name: caddy-config', ' secret:', ' secretName: ${SERVICE}-caddyfile', ' items:', ' - { key: latest, path: Caddyfile }');
1776
+ if (!isCursor) {
1777
+ lines.push(' - name: otel-config', ' secret:', ' secretName: ${SERVICE}-otel-config', ' items:', ' - { key: latest, path: config.yaml }');
1778
+ }
1779
+ lines.push('');
1780
+ return lines.join('\n');
1781
+ }
1782
+ /** Deploy script for the federated edge (A′): build, upsert secrets (no OAuth), replace, make
1783
+ * public, map the subdomain. No IAP; no Google OAuth client. */
1784
+ function renderCloudrunFedDeploy(ctx) {
1785
+ const isCursor = ctx.engine.trim().toLowerCase() === 'cursor';
1786
+ const modelNote = ctx.model?.trim() ? `, model ${ctx.model.trim()}` : '';
1787
+ const lines = [
1788
+ '#!/usr/bin/env bash',
1789
+ '# Cloud Run + AgentHippo federated edge (A′): Caddy + auth-broker front the agent.',
1790
+ `# Engine: ${ctx.engine}${modelNote}. No oauth2-proxy, no per-service Google OAuth client —`,
1791
+ '# Google login happens once at the central IdP (auth.agenthippo.ai); this agent verifies',
1792
+ '# the shared .agenthippo.ai AgentHippo JWT (Bearer for IDE, cookie for browser). No IAP.',
1793
+ '# The IDE points spec.remote.url straight at https://$PUBLIC_HOSTNAME (same flow as remote-vm).',
1794
+ 'set -euo pipefail',
1795
+ ...anchorToStagingRoot(),
1796
+ `SERVICE="${ctx.packName}"`,
1797
+ 'REGION="${REGION:-us-central1}"',
1798
+ 'PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project)}"',
1799
+ 'REPO="${ARTIFACT_REPO:-agenthippo}"',
1800
+ 'IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/${SERVICE}:latest"',
1801
+ 'LITELLM_IMAGE="${LITELLM_IMAGE:-ghcr.io/agenthippo/litellm-gateway:2026.06}"',
1802
+ '# Public hostname = a *.agenthippo.ai subdomain mapped to this service (one-time wildcard DNS).',
1803
+ 'PUBLIC_HOSTNAME="${PUBLIC_HOSTNAME:?set PUBLIC_HOSTNAME=<subdomain>.agenthippo.ai}"',
1804
+ 'ALLOWED_DOMAINS="${ALLOWED_DOMAINS:-agenthippo.ai}"',
1805
+ '# Shared AgentHippo JWT signing key — MUST match the central IdP (auth.agenthippo.ai).',
1806
+ '# Copy it from the IdP (or the GCP secret it stores) so tokens issued there verify here.',
1807
+ 'AUTH_JWT_SECRET="${AUTH_JWT_SECRET:?set AUTH_JWT_SECRET=<copy from auth.agenthippo.ai>}"',
1808
+ ...(isCursor ? [
1809
+ 'PROVIDER_KEY="${PROVIDER_KEY:?set PROVIDER_KEY=<cursor api key>}"',
1810
+ ] : [
1811
+ '# LiteLLM proxy key + the upstream provider key the gateway uses for the model.',
1812
+ 'LITELLM_KEY="${LITELLM_KEY:?set LITELLM_KEY=<litellm proxy key>}"',
1813
+ 'PROVIDER_KEY="${PROVIDER_KEY:?set PROVIDER_KEY=<upstream provider key>}"',
1814
+ ]),
1815
+ '',
1816
+ 'gcloud artifacts repositories describe "$REPO" --location="$REGION" 2>/dev/null \\',
1817
+ ' || gcloud artifacts repositories create "$REPO" --repository-format=docker --location="$REGION"',
1818
+ 'gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet',
1819
+ '',
1820
+ '# One agent image serves both the agent and the auth-broker containers (CLI ships `auth-broker`).',
1821
+ '# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/agenthippo/runtime:latest',
1822
+ '# tag was retired; AR has runtime:latest / :latest-cursor). No-op if the base already targets AR.',
1823
+ 'DF_TMP="$(mktemp)"',
1824
+ 'sed "s|FROM ghcr.io/agenthippo/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
1825
+ 'docker build --pull --platform linux/amd64 -f "$DF_TMP" -t "$IMAGE" .',
1826
+ 'docker push "$IMAGE"',
1827
+ '',
1828
+ '# Secrets: caddyfile + shared JWT secret + provider/litellm keys. NO OAuth client secrets.',
1829
+ 'ensure_secret() { gcloud secrets describe "$1" >/dev/null 2>&1 || gcloud secrets create "$1" --replication-policy=automatic; }',
1830
+ 'ensure_secret "${SERVICE}-caddyfile"; gcloud secrets versions add "${SERVICE}-caddyfile" --data-file=deploy/Caddyfile',
1831
+ 'ensure_secret "${SERVICE}-auth-jwt-secret"; printf %s "$AUTH_JWT_SECRET" | gcloud secrets versions add "${SERVICE}-auth-jwt-secret" --data-file=-',
1832
+ 'ensure_secret "${SERVICE}-provider-key"; printf %s "$PROVIDER_KEY" | gcloud secrets versions add "${SERVICE}-provider-key" --data-file=-',
1833
+ ];
1834
+ if (!isCursor) {
1835
+ lines.push('ensure_secret "${SERVICE}-otel-config"; gcloud secrets versions add "${SERVICE}-otel-config" --data-file=deploy/otel-collector.yaml', 'ensure_secret "${SERVICE}-litellm-key"; printf %s "$LITELLM_KEY" | gcloud secrets versions add "${SERVICE}-litellm-key" --data-file=-');
1836
+ }
1837
+ lines.push('', '# Render the multi-container template (sed — no gettext) and deploy.', 'RENDERED="$(mktemp)"', 'sed -e "s|\\${SERVICE}|${SERVICE}|g" -e "s|\\${IMAGE}|${IMAGE}|g" \\', ' -e "s|\\${LITELLM_IMAGE}|${LITELLM_IMAGE}|g" \\', ' -e "s|\\${PUBLIC_HOSTNAME}|${PUBLIC_HOSTNAME}|g" -e "s|\\${ALLOWED_DOMAINS}|${ALLOWED_DOMAINS}|g" \\', ' deploy/service.yaml > "$RENDERED"', 'gcloud run services replace "$RENDERED" --region "$REGION"', '', '# No IAP: Caddy is the gate, so Cloud Run must allow public ingress (allUsers invoker).', 'gcloud run services add-iam-policy-binding "$SERVICE" --region "$REGION" \\', ' --member=allUsers --role=roles/run.invoker >/dev/null 2>&1 || true', '', '# Map the *.agenthippo.ai subdomain (idempotent). Requires one-time wildcard DNS', '# *.agenthippo.ai → Cloud Run (gctrl.run.app). No per-subdomain DNS, no Google OAuth edit.', '# Skip for *.run.app default URLs (no custom mapping needed there).', 'if [[ "$PUBLIC_HOSTNAME" != *.run.app ]]; then \\', ' gcloud beta run domain-mappings describe "$PUBLIC_HOSTNAME" --region "$REGION" >/dev/null 2>&1 \\', ' || gcloud beta run domain-mappings create --service "$SERVICE" --region "$REGION" --domain "$PUBLIC_HOSTNAME"; \\', 'fi', '', '# Grant the runtime SA access to the secrets if needed:', '# SA="$(gcloud run services describe "$SERVICE" --region "$REGION" --format=\'value(spec.template.spec.serviceAccountName)\')"', '# for s in caddyfile auth-jwt-secret provider-key; do', '# gcloud secrets add-iam-policy-binding "${SERVICE}-$s" --member="serviceAccount:$SA" --role=roles/secretmanager.secretAccessor', '# done', '');
1838
+ return lines.join('\n');
1839
+ }
1840
+ /** A′ artifacts: Cloud Run behind the federated AgentHippo edge (Caddy + auth-broker; no oauth2-proxy). */
1841
+ function cloudrunFedArtifacts(ctx, inbound, isCursor) {
1842
+ const sidecars = isCursor
1843
+ ? 'Caddy (ingress) + auth-broker + agent.'
1844
+ : 'Caddy (ingress) + auth-broker + agent + LiteLLM + OTEL collector (Spotlight).';
1845
+ const idp = (ctx.idpUrl || 'https://auth.agenthippo.ai').trim().replace(/\/+$/, '');
1846
+ const files = {
1847
+ 'Dockerfile': sharedDockerfile(ctx, { wwwDir: '/app/pack/deploy/www' }),
1848
+ 'www/index.html': renderLandingPage(ctx.packName),
1849
+ 'Caddyfile': renderCloudrunFedCaddyfile(ctx),
1850
+ 'service.yaml': renderCloudrunFedServiceYaml(ctx),
1851
+ 'cloudrun-deploy.sh': renderCloudrunFedDeploy(ctx),
1852
+ };
1853
+ if (!isCursor) {
1854
+ files['otel-collector.yaml'] = OTEL_COLLECTOR_COMPOSE_YAML;
1855
+ }
1856
+ files['README.md'] = [
1857
+ `# Deploy: ${ctx.packName} — Cloud Run + AgentHippo federated edge (A′)`,
1858
+ '',
1859
+ `Engine: **${ctx.engine}**${ctx.model ? `, model: **${ctx.model}**` : ''}. Runtime base: \`${ctx.image}\`.`,
1860
+ '',
1861
+ `${sidecars} Sidecars share the pod\'s localhost (Cloud Run multi-container):`,
1862
+ '- **caddy** (ingress, the only port) → verifies the shared AgentHippo JWT (Bearer for IDE, cookie for browser)',
1863
+ `- **agent** → serves www + /v1/* on localhost:${EDGE_AGENT_PORT} (NOT ingress)`,
1864
+ '- **auth-broker** → validates the shared `.agenthippo.ai` JWT via `/api/ide/me` (same secret as the IdP)',
1865
+ ...(isCursor ? [] : [
1866
+ '- **litellm** gateway → exports OTLP to `http://localhost:4318`',
1867
+ '- **otel-collector** → writes JSONL traces to the in-memory `/state` volume',
1868
+ ]),
1869
+ '',
1870
+ '**No oauth2-proxy, no per-service Google OAuth client.** Google login happens once at the',
1871
+ `central IdP (${idp}); this agent only verifies the resulting shared \`.agenthippo.ai\` JWT.`,
1872
+ 'The IDE points `spec.remote.url` at `https://$PUBLIC_HOSTNAME` and the existing AgentHippo',
1873
+ 'SSO flow works (same as `remote-vm`). Browser visits with no cookie are redirected to the',
1874
+ 'IdP to log in, then return. Cloud Run is public (`allUsers` invoker) so traffic reaches Caddy.',
1875
+ '',
1876
+ '```bash',
1877
+ 'cp deploy/.env.example deploy/.env && cp deploy/.env.auth.example deploy/.env.auth # fill both',
1878
+ 'export PUBLIC_HOSTNAME=<subdomain>.agenthippo.ai # e.g. cs.agenthippo.ai',
1879
+ 'export AUTH_JWT_SECRET=<copy from auth.agenthippo.ai> # shared signing key',
1880
+ ...(isCursor ? ['export PROVIDER_KEY=<cursor api key>'] : ['export LITELLM_KEY=… PROVIDER_KEY=…']),
1881
+ 'bash deploy/cloudrun-deploy.sh # builds linux/amd64, upserts secrets, `gcloud run services replace`, maps the subdomain',
1882
+ '```',
1883
+ '',
1884
+ '**One-time setup (not per agent):** wildcard DNS `*.agenthippo.ai` → Cloud Run (`gctrl.run.app`),',
1885
+ `and the IdP (${idp}) must run the \`/oauth/browser-login\` route (oauth2-proxy forward_auth →`,
1886
+ 'auth-broker sets the `ah_token` cookie on `.agenthippo.ai`). After that, every new agent is',
1887
+ 'just: `deploy init --ingress edge-fed` + this script — zero Console/OAuth work.',
1888
+ '',
1889
+ '`service.yaml` placeholders (`${SERVICE} ${IMAGE} ${LITELLM_IMAGE} ${PUBLIC_HOSTNAME} ${ALLOWED_DOMAINS}`)',
1890
+ 'are substituted by the deploy script. The Caddyfile is stored in Secret Manager (`${SERVICE}-caddyfile`).',
1891
+ '`AGENTHIPPO_AUTH_COOKIE_DOMAIN` / `AGENTHIPPO_AUTH_IDP_URL` are baked in at generation time.',
1892
+ '',
1893
+ inbound
1894
+ ? '**Inbound channel (1b):** `minScale: 1` pins one always-on instance (the socket can\'t scale to zero).'
1895
+ : '**No inbound channel (1a):** scales to zero; in-memory `/state` is wiped on cold start (use the `edge` mode + GCS bucket if you need durable state).',
1896
+ '',
1897
+ ].join('\n');
1898
+ return files;
1899
+ }
1900
+ function databricksAppArtifacts(ctx) {
1901
+ // Databricks Apps take SOURCE, not images — the platform builds it. serve-v3 §5.
1902
+ const appYaml = [
1903
+ '# Databricks App (serverless source app). Databricks runs `npm ci` then start.',
1904
+ 'command: ["node", "start.js"]',
1905
+ 'env:',
1906
+ ` - name: PORT`,
1907
+ ` value: "${ctx.port}"`,
1908
+ 'user_authorization:',
1909
+ ' # OBO: Databricks forwards X-Forwarded-Access-Token (the asking user\'s',
1910
+ ' # downscoped token) so SQL runs as that user → Unity Catalog enforces',
1911
+ ' # per-user grants. serve-v3 §11.1.',
1912
+ ' scopes: ["sql"]',
1913
+ '',
1914
+ ].join('\n');
1915
+ const pkg = JSON.stringify({
1916
+ name: ctx.packName,
1917
+ private: true,
1918
+ dependencies: {
1919
+ [exports.AGENTHIPPO_CLI_NPM_PACKAGE]: resolveAgentHippoCliNpmVersion(),
1920
+ '@anthropic-ai/claude-code': 'latest',
1921
+ },
1922
+ }, null, 2) + '\n';
1923
+ const start = [
1924
+ "// Databricks App entrypoint: exec serve against the bundled pack.",
1925
+ "const { spawn } = require('node:child_process');",
1926
+ "const port = process.env.PORT || '" + ctx.port + "';",
1927
+ "const child = spawn('agenthippo', [",
1928
+ " 'serve', '--agent', './pack',",
1929
+ " '--workspace', '/tmp/agenthippo/workspace',",
1930
+ " '--state-dir', '/tmp/agenthippo/state',",
1931
+ " '--port', port,",
1932
+ "], { stdio: 'inherit', env: { ...process.env, AGENTHIPPO_SERVE_HOST: '0.0.0.0' } });",
1933
+ "child.on('exit', (code) => process.exit(code ?? 0));",
1934
+ '',
1935
+ ].join('\n');
1936
+ return {
1937
+ 'app.yaml': appYaml,
1938
+ 'package.json': pkg,
1939
+ 'start.js': start,
1940
+ 'README.md': [
1941
+ `# Deploy: ${ctx.packName} — Databricks App (T1-native, the wedge)`,
1942
+ '',
1943
+ '```bash',
1944
+ 'cp -r . ./databricks-app/pack # bundle the pack next to start.js',
1945
+ 'databricks apps deploy ' + ctx.packName,
1946
+ '```',
1947
+ '',
1948
+ '**Per-user permissions:** workspace SSO authenticates the user; OBO forwards',
1949
+ '`X-Forwarded-Access-Token`, and the SQL tool authenticates to the warehouse as',
1950
+ 'that user, so Unity Catalog row filters apply automatically (serve-v3 §11.1).',
1951
+ '',
1952
+ '> ⚠️ The OBO *credential* seam (per-turn token → SQL tool) is the load-bearing',
1953
+ '> spike (serve-v3 §13.2). Without it serve queries as one app identity. Identity',
1954
+ '> attribution (`user_id`) works today; per-user *data* enforcement needs the spike.',
1955
+ '',
1956
+ ].join('\n'),
1957
+ };
1958
+ }
1959
+ function spcsArtifacts(ctx) {
1960
+ const spec = [
1961
+ '# Snowpark Container Services spec. Push the runtime image to your account',
1962
+ '# registry, then run deploy.sql in a worksheet. serve-v3 §6.',
1963
+ 'spec:',
1964
+ ' containers:',
1965
+ ` - name: ${ctx.packName}`,
1966
+ ` image: /<db>/<schema>/<repo>/${ctx.packName}:latest`,
1967
+ ' env:',
1968
+ ' AGENTHIPPO_NO_AUTO_INSTALL: "1"',
1969
+ ' AGENTHIPPO_HOME: /state',
1970
+ ' readinessProbe:',
1971
+ ` port: ${ctx.port}`,
1972
+ ' path: /ready',
1973
+ ' # Q1: durable state on a block volume — survives compute-pool suspend/resume.',
1974
+ ' volumeMounts:',
1975
+ ' - name: state',
1976
+ ' mountPath: /state',
1977
+ ' volumes:',
1978
+ ' - name: state',
1979
+ ' source: block',
1980
+ ' size: 10Gi',
1981
+ ' endpoints:',
1982
+ ` - name: api`,
1983
+ ` port: ${ctx.port}`,
1984
+ ' public: true',
1985
+ '',
1986
+ ].join('\n');
1987
+ const sql = [
1988
+ '-- Run in a Snowflake worksheet. SPCS injects an OAuth session token at',
1989
+ '-- /snowflake/session/token → the agent queries as the service role (no key-pair).',
1990
+ 'CREATE COMPUTE POOL IF NOT EXISTS agenthippo_pool',
1991
+ ' MIN_NODES = 1 MAX_NODES = 1 INSTANCE_FAMILY = CPU_X64_S AUTO_SUSPEND_SECS = 600;',
1992
+ '',
1993
+ '-- Only egress: the model API. Everything else stays inside Snowflake.',
1994
+ 'CREATE OR REPLACE NETWORK RULE anthropic_rule',
1995
+ " MODE = EGRESS TYPE = HOST_PORT VALUE_LIST = ('api.anthropic.com:443');",
1996
+ 'CREATE OR REPLACE EXTERNAL ACCESS INTEGRATION anthropic_eai',
1997
+ ' ALLOWED_NETWORK_RULES = (anthropic_rule) ENABLED = TRUE;',
1998
+ '',
1999
+ `CREATE SERVICE ${ctx.packName}`,
2000
+ ' IN COMPUTE POOL agenthippo_pool',
2001
+ ` FROM SPECIFICATION_FILE = 'spec.yaml'`,
2002
+ ' EXTERNAL_ACCESS_INTEGRATIONS = (anthropic_eai);',
2003
+ '',
2004
+ ].join('\n');
2005
+ return {
2006
+ 'Dockerfile': sharedDockerfile(ctx),
2007
+ 'spec.yaml': spec,
2008
+ 'deploy.sql': sql,
2009
+ 'README.md': [
2010
+ `# Deploy: ${ctx.packName} — Snowflake SPCS (T1-native)`,
2011
+ '',
2012
+ '1. Build + push: `docker build -f deploy/Dockerfile -t <account-registry>/.../' + ctx.packName + ':latest . && docker push ...`',
2013
+ '2. Upload `spec.yaml` to a stage, then run `deploy.sql` in a worksheet.',
2014
+ '',
2015
+ 'Zero Snowflake credentials: SPCS injects a session token; the agent queries as the',
2016
+ 'service role, so RBAC is the permission system. Security review = "one EAI to api.anthropic.com".',
2017
+ '',
2018
+ ].join('\n'),
2019
+ };
2020
+ }
2021
+ function cloudflareArtifacts(ctx) {
2022
+ const wrangler = JSON.stringify({
2023
+ name: ctx.packName,
2024
+ compatibility_date: '2026-05-01',
2025
+ containers: [
2026
+ {
2027
+ class_name: 'Agent',
2028
+ image: './deploy/Dockerfile',
2029
+ max_instances: 1,
2030
+ instance_type: { vcpu: 2, memory_mib: 6144, disk_mb: 8192 },
2031
+ },
2032
+ ],
2033
+ durable_objects: { bindings: [{ name: 'AGENT', class_name: 'Agent' }] },
2034
+ migrations: [{ tag: 'v1', new_sqlite_classes: ['Agent'] }],
2035
+ }, null, 2) + '\n';
2036
+ return {
2037
+ 'Dockerfile': sharedDockerfile(ctx),
2038
+ 'wrangler.jsonc': wrangler,
2039
+ 'README.md': [
2040
+ `# Deploy: ${ctx.packName} — Cloudflare Container (T1-generic)`,
2041
+ '',
2042
+ '`wrangler deploy` builds the Dockerfile and ships the container (scale-to-zero).',
2043
+ 'Front it with a Worker that does Cloudflare Access (SSO) and sets `X-Agent-User`,',
2044
+ 'then proxies to the container binding. For the *external/unauthenticated* case',
2045
+ '(customer support), use the full reference at `examples/deploy/external-support/`.',
2046
+ '',
2047
+ ].join('\n'),
2048
+ };
2049
+ }
2050
+ const RENDERERS = {
2051
+ vm: vmArtifacts,
2052
+ compose: composeArtifacts,
2053
+ cloudrun: cloudrunArtifacts,
2054
+ 'databricks-app': databricksAppArtifacts,
2055
+ spcs: spcsArtifacts,
2056
+ cloudflare: cloudflareArtifacts,
2057
+ };
2058
+ /**
2059
+ * Render every artifact for a target (relative path → file content), always
2060
+ * including a scanned `.env.example`. Pure: no fs writes.
2061
+ */
2062
+ function renderTarget(target, ctx) {
2063
+ const files = RENDERERS[target](ctx);
2064
+ const fed = target === 'cloudrun' && ctx.ingress === 'edge-fed';
2065
+ const edge = target === 'vm' || target === 'compose' || (target === 'cloudrun' && ctx.ingress === 'edge') || fed;
2066
+ // .dockerignore only alongside a Dockerfile (image-based targets). databricks-app
2067
+ // is source-based (platform builds it) and ships no Dockerfile.
2068
+ const hasDockerfile = Object.keys(files).some((k) => k === 'Dockerfile' || k.endsWith('/Dockerfile'));
2069
+ return {
2070
+ ...files,
2071
+ ...(hasDockerfile ? { '.dockerignore': renderDockerignore() } : {}),
2072
+ '.env.example': renderEnvExample(ctx.envVars, {
2073
+ edge,
2074
+ engine: ctx.engine,
2075
+ hostname: ctx.publicHostname,
2076
+ }),
2077
+ // Edge auth secrets are split out so the agent container never loads them.
2078
+ ...(edge ? {
2079
+ '.env.auth.example': renderAuthEnvExample({
2080
+ hostname: ctx.publicHostname,
2081
+ fed,
2082
+ idpUrl: ctx.idpUrl,
2083
+ cookieDomain: ctx.cookieDomain,
2084
+ }),
2085
+ } : {}),
2086
+ };
2087
+ }