@agenthippo-ai/agenthippo 0.2.3-dataplane → 1.110.11

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 (139) hide show
  1. package/out/agentpack/index.js +3 -1
  2. package/out/agentpack/paths.js +25 -2
  3. package/out/analytics/dashboard/localFleetAdapter.js +82 -2
  4. package/out/analytics/dashboard/provider.js +13 -2
  5. package/out/analytics/dashboard/webview/panel.js +3 -2
  6. package/out/analytics/identity.js +0 -0
  7. package/out/analytics/service.js +29 -25
  8. package/out/analytics/sessionEvents.js +100 -24
  9. package/out/auth/authBaseUrl.js +59 -0
  10. package/out/auth/bootstrapApply.js +9 -10
  11. package/out/auth/brokerClient.js +48 -14
  12. package/out/auth/provider.js +8 -21
  13. package/out/auth/sessionStore.js +3 -40
  14. package/out/auth/signInFlow.js +2 -1
  15. package/out/auth-broker/config.js +50 -5
  16. package/out/auth-broker/server.js +126 -26
  17. package/out/auth-broker/store.js +25 -24
  18. package/out/authority/warrant.js +206 -0
  19. package/out/branding.js +27 -1
  20. package/out/cli/commands/conformance.js +249 -0
  21. package/out/cli/commands/data-plane.js +151 -0
  22. package/out/cli/commands/deploy.js +1 -0
  23. package/out/cli/commands/gateway.js +4 -0
  24. package/out/cli/commands/pack.js +182 -3
  25. package/out/cli/commands/sandbox.js +351 -0
  26. package/out/cli/commands/serve.js +1145 -130
  27. package/out/cli/commands/store.js +12 -4
  28. package/out/cli/credential-context.js +17 -0
  29. package/out/cli/data-plane/gateway.js +28 -5
  30. package/out/cli/data-plane/index.js +18 -2
  31. package/out/cli/data-plane/registry.js +9 -0
  32. package/out/cli/data-plane/remote.js +106 -0
  33. package/out/cli/data-plane/sidecar.js +225 -0
  34. package/out/cli/deploy/layers/identity.js +88 -4
  35. package/out/cli/deploy-init.js +89 -24
  36. package/out/cli/deps.js +106 -2
  37. package/out/cli/idp-init.js +35 -0
  38. package/out/cli/index.js +171 -17
  39. package/out/cli/pack-install-ref.js +15 -0
  40. package/out/cli/pack-loader.js +8 -1
  41. package/out/cli/pack-runtime.js +61 -0
  42. package/out/cli/runtime.js +35 -1
  43. package/out/cli/serve-hardening.js +260 -14
  44. package/out/cli/utils.js +12 -1
  45. package/out/engines/base.js +32 -3
  46. package/out/engines/claude.js +33 -15
  47. package/out/engines/codex.js +111 -27
  48. package/out/engines/custom.js +15 -1
  49. package/out/evidence/sessionShip.js +643 -0
  50. package/out/extension.js +11 -2
  51. package/out/gateway/bridge.js +40 -2
  52. package/out/gateway/client.js +34 -1
  53. package/out/gateway/installSource.js +68 -0
  54. package/out/gateway/lifecycle.js +185 -51
  55. package/out/gateway/outboundPaths.js +4 -1
  56. package/out/gateway/secretsPreflight.js +90 -0
  57. package/out/gateway/service.js +7 -2
  58. package/out/headless/index.js +25 -2
  59. package/out/live-e2e/mcp-demo/server.mjs +32 -0
  60. package/out/mcp.js +30 -0
  61. package/out/pack/caps.js +271 -0
  62. package/out/pack/governor.js +326 -0
  63. package/out/pack/lease-schema.js +273 -0
  64. package/out/pack/lease.js +682 -0
  65. package/out/pack/reconcile.js +217 -0
  66. package/out/pack/signature.js +839 -0
  67. package/out/remote/handleRemoteChat.js +44 -3
  68. package/out/remote/remoteTrust.js +277 -0
  69. package/out/remote/serveClient.js +52 -5
  70. package/out/runner/host.js +141 -0
  71. package/out/runner/protocol.js +128 -0
  72. package/out/runner/remoteEngine.js +166 -0
  73. package/out/runtime/interfaces/edit-tracker.js +2 -0
  74. package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
  75. package/out/sandbox/containerDriver.js +572 -0
  76. package/out/sandbox/processDriver.js +318 -0
  77. package/out/sandbox/sandboxRunner.js +303 -0
  78. package/out/sandbox/support.js +301 -0
  79. package/out/sandbox/types.js +55 -0
  80. package/out/scripts/agentide-litellm.sh +67 -5
  81. package/out/scripts/agentide_litellm_hooks_v1.py +266 -21
  82. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +98 -132
  83. package/out/scripts/analytics/setup-analytics.ps1 +45 -14
  84. package/out/scripts/analytics/setup-analytics.sh +106 -15
  85. package/out/scripts/analytics/start-analytics.sh +86 -14
  86. package/out/scripts/gateway/setup-gateway.sh +72 -32
  87. package/out/shared/models.js +11 -110
  88. package/out/spotlight/install.js +7 -1
  89. package/out/store/index.js +41 -10
  90. package/out/sync.js +3 -1
  91. package/out/test/analytics.test.js +162 -23
  92. package/out/test/auth-baseurl.test.js +132 -0
  93. package/out/test/auth-bootstrapApply.test.js +27 -6
  94. package/out/test/auth-broker-hardening.test.js +227 -0
  95. package/out/test/auth-broker.test.js +59 -9
  96. package/out/test/branding.test.js +13 -0
  97. package/out/test/caps-governor.test.js +460 -0
  98. package/out/test/codex-new-file-survival.test.js +119 -0
  99. package/out/test/configuration-scopes.test.js +131 -0
  100. package/out/test/data-plane-engine-wiring.test.js +32 -0
  101. package/out/test/data-plane.test.js +380 -0
  102. package/out/test/deploy-init.test.js +47 -1
  103. package/out/test/gateway-client.test.js +28 -0
  104. package/out/test/gateway-install-source.test.js +111 -0
  105. package/out/test/gateway-lifecycle.test.js +1 -1
  106. package/out/test/gateway-outbound.test.js +7 -0
  107. package/out/test/gateway-secrets-preflight.test.js +127 -0
  108. package/out/test/gateway-session-routing.test.js +18 -0
  109. package/out/test/governor-hardening.test.js +222 -0
  110. package/out/test/headless-sync.test.js +22 -0
  111. package/out/test/lease-signature.test.js +357 -0
  112. package/out/test/lease-trust-root.test.js +273 -0
  113. package/out/test/lease-validation.test.js +224 -0
  114. package/out/test/lease.test.js +114 -0
  115. package/out/test/localFleetAdapter.test.js +91 -0
  116. package/out/test/mcp.test.js +27 -1
  117. package/out/test/pack-admission.test.js +216 -0
  118. package/out/test/pack-digest-parity.test.js +140 -0
  119. package/out/test/pack-reconcile.test.js +183 -0
  120. package/out/test/pack-signature-artifact.test.js +103 -0
  121. package/out/test/pack-signature.test.js +176 -0
  122. package/out/test/pack-trust-root.test.js +202 -0
  123. package/out/test/remote-trust.test.js +242 -0
  124. package/out/test/runner-protocol.test.js +249 -0
  125. package/out/test/sandbox-driver.test.js +553 -0
  126. package/out/test/sandbox-hardening.test.js +406 -0
  127. package/out/test/sandbox-runner.test.js +343 -0
  128. package/out/test/sandbox-scope.test.js +449 -0
  129. package/out/test/serve-cron-acceptance.test.js +370 -0
  130. package/out/test/serve-gateway-turn.test.js +122 -0
  131. package/out/test/serve-hardening.test.js +227 -15
  132. package/out/test/serveClient.test.js +6 -5
  133. package/out/test/sessionShip.test.js +618 -0
  134. package/out/test/spotlightPublish.test.js +28 -0
  135. package/out/test/store-install-safe.test.js +69 -0
  136. package/out/test/tls-bypass.test.js +90 -0
  137. package/out/test/warrant.test.js +138 -0
  138. package/out/utils.js +46 -9
  139. package/package.json +1 -1
@@ -7,28 +7,112 @@ exports.EDGE_AGENT_PORT = void 0;
7
7
  exports.renderIdentityLayer = renderIdentityLayer;
8
8
  exports.renderSidecarEdgeCaddyfile = renderSidecarEdgeCaddyfile;
9
9
  exports.renderSidecarFedCaddyfile = renderSidecarFedCaddyfile;
10
+ /**
11
+ * Proof that `X-Auth-Request-Email` was set by THIS Caddy and not by a client.
12
+ *
13
+ * The broker now 401s an `X-Auth-Request-Email` that arrives without a matching
14
+ * `x-agenthippo-proxy-secret` (constant-time compared), because the header is otherwise a bearer
15
+ * assertion of identity that anything able to reach the broker's port could make. Only the routes
16
+ * that actually forward a verified email upstream carry it — adding it elsewhere would spread the
17
+ * secret across hops that have no use for it.
18
+ *
19
+ * `{env.…}` rather than `{$…}`: a runtime placeholder, so the secret is never baked into the
20
+ * rendered config on disk. Unset ⇒ empty ⇒ the broker refuses, which is the intended direction.
21
+ */
22
+ const BROKER_PROXY_SECRET_LINES = [
23
+ ' # Proves this hop is the trusted proxy — the broker 401s X-Auth-Request-Email without it.',
24
+ ' header_up x-agenthippo-proxy-secret {env.AGENTHIPPO_AUTH_PROXY_SECRET}',
25
+ ];
10
26
  function renderIdentityLayer(opts) {
11
27
  const { mode, authBrokerAddr, healthProxy, agentProxy } = opts;
12
28
  const oauth2ProxyAddr = opts.oauth2ProxyAddr ?? '';
13
29
  const catchallBody = opts.catchallBody ?? agentProxy;
30
+ const turn = opts.turnRegistration;
31
+ // The turn-registration hop, inserted AFTER the identity forward_auth (which sets
32
+ // X-Agent-User — the user the sidecar mints the nonce for). Only POST chat turns match.
33
+ const turnHop = turn
34
+ ? [
35
+ ' # Turn registration — after identity: X-Agent-User (set above) names the user the',
36
+ ' # sidecar mints the nonce for. Only POST chat turns match; others pass through.',
37
+ ' # X-Agent-User-Assertion (also set above) rides along as the turn\'s inboundToken:',
38
+ ' # without it `passthrough` fails closed on every call and an RFC 8693 `exchange`',
39
+ ' # provider has no subject_token. See PLAN/agent-identity.md §8.5 G1.',
40
+ ` forward_auth @turnStart ${turn.connectorAddr} {`,
41
+ ' uri /edge/turn',
42
+ ' copy_headers X-Agent-Turn-Nonce',
43
+ ` header_up Authorization "Bearer {$${turn.tokenEnvVar}}"`,
44
+ ' }',
45
+ ]
46
+ : [];
47
+ // NOTE: two forward_auth in one handle block do NOT run in source order — Caddy sorts by
48
+ // its directive table, so the turn-registration hop could fire before identity is
49
+ // established (the connector then gets no X-Agent-User → 400). Wrapping the sequence in
50
+ // `route {}` forces source order: identity first (unauth short-circuits), then turn
51
+ // registration, then proxy.
52
+ // Re-indent wrapped directives one level so the generated file stays readable — operators
53
+ // copy these snippets by hand onto the IdP host.
54
+ const indent = (lines) => lines.map((l) => (l.trim() ? ` ${l}` : l));
55
+ const gated = (identityGate, body) => turn
56
+ ? [' route {', ...indent(identityGate), ...turnHop, ...indent(body), ' }']
57
+ : [...identityGate, ...body];
14
58
  const lines = [];
59
+ if (turn) {
60
+ lines.push(' # Hygiene: never let a client-supplied nonce header through (copy_headers overwrites it', ' # on chat paths anyway, and a forged nonce fails closed at the gateway — belt-and-braces).', ' request_header -X-Agent-Turn-Nonce', ' # Same for the user assertion: it is set from the verified /api/ide/me response below,', ' # never from the client. A forged one would become the turn\'s inboundToken.', ' request_header -X-Agent-User-Assertion');
61
+ }
15
62
  if (mode === 'self-issued') {
16
- lines.push(' # IDE PKCE sign-in: broker requires X-Auth-Request-Email from oauth2-proxy. Must come', ' # before the generic /oauth/* handle so authorize is gated (not dumped straight to broker).', ' handle /oauth/authorize {', ` forward_auth ${oauth2ProxyAddr} {`, ' uri /oauth2/auth', ' copy_headers X-Auth-Request-Email', ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' @unauth status 401', ' handle_response @unauth {', ' redir * "/oauth2/sign_in?rd={http.request.uri}"', ' }', ' }', ` reverse_proxy ${authBrokerAddr} {`, ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' }', ' }', ' # Federated SSO (A′): browsers from other subdomains land here to stamp the shared cookie.', ' # Same oauth2-proxy gate as authorize; must precede the generic /oauth/* handle.', ' handle /oauth/browser-login {', ` forward_auth ${oauth2ProxyAddr} {`, ' uri /oauth2/auth', ' copy_headers X-Auth-Request-Email', ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' @unauth status 401', ' handle_response @unauth {', ' redir * "/oauth2/sign_in?rd={http.request.uri}"', ' }', ' }', ` reverse_proxy ${authBrokerAddr} {`, ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' }', ' }', ' # Auth-broker: token exchange + other /oauth/* routes that must stay ungated.', ' handle /oauth/* {', ` reverse_proxy ${authBrokerAddr} {`, ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' }', ' }');
63
+ lines.push(' # IDE PKCE sign-in: broker requires X-Auth-Request-Email from oauth2-proxy. Must come', ' # before the generic /oauth/* handle so authorize is gated (not dumped straight to broker).', ' handle /oauth/authorize {', ` forward_auth ${oauth2ProxyAddr} {`, ' uri /oauth2/auth', ' copy_headers X-Auth-Request-Email', ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' @unauth status 401', ' handle_response @unauth {', ' redir * "/oauth2/sign_in?rd={http.request.uri}"', ' }', ' }', ` reverse_proxy ${authBrokerAddr} {`, ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ...BROKER_PROXY_SECRET_LINES, ' }', ' }', ' # Federated SSO (A′): browsers from other subdomains land here to stamp the shared cookie.', ' # Same oauth2-proxy gate as authorize; must precede the generic /oauth/* handle.', ' handle /oauth/browser-login {', ` forward_auth ${oauth2ProxyAddr} {`, ' uri /oauth2/auth', ' copy_headers X-Auth-Request-Email', ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' @unauth status 401', ' handle_response @unauth {', ' redir * "/oauth2/sign_in?rd={http.request.uri}"', ' }', ' }', ` reverse_proxy ${authBrokerAddr} {`, ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ...BROKER_PROXY_SECRET_LINES, ' }', ' }', ' # Auth-broker: token exchange + other /oauth/* routes that must stay ungated.', ' handle /oauth/* {', ` reverse_proxy ${authBrokerAddr} {`, ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' }', ' }');
17
64
  }
18
65
  lines.push(' handle /api/ide/* {', ` reverse_proxy ${authBrokerAddr} {`, ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' }', ' }');
19
66
  if (mode === 'self-issued') {
20
67
  lines.push(' # oauth2-proxy: Google SSO callback + sign-in.', ' handle /oauth2/* {', ` reverse_proxy ${oauth2ProxyAddr} {`, ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' }', ' }');
21
68
  }
22
- lines.push(' @health path /health /ready', ' handle @health {', ...healthProxy, ' }', ' # API clients (IDE) with a Bearer token: validate via auth-broker, copy identity header.', ' @apiWithAuth {', ' path /v1/*', ' header Authorization *', ' }', ' handle @apiWithAuth {', ` forward_auth ${authBrokerAddr} {`, ' uri /api/ide/me', ' copy_headers X-Agent-User', ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' @unauth status 401', ' handle_response @unauth {', ' respond "Unauthorized" 401', ' }', ' }', ...agentProxy, ' }');
69
+ lines.push(' @health path /health /ready', ' handle @health {', ...healthProxy, ' }');
70
+ if (turn) {
71
+ lines.push(' # Chat-turn matcher for edge turn registration (P1 fix): only POST chat turns register.', ' @turnStart {', ' method POST', ' path /v1/chat /v1/chat/stream', ' }');
72
+ }
73
+ lines.push(' # API clients (IDE) with a Bearer token: validate via auth-broker, copy identity header.', ' @apiWithAuth {', ' path /v1/*', ' header Authorization *', ' }', ' handle @apiWithAuth {', ...gated([
74
+ ` forward_auth ${authBrokerAddr} {`,
75
+ ' uri /api/ide/me',
76
+ ` copy_headers X-Agent-User${turn ? ' X-Agent-User-Assertion' : ''}`,
77
+ ' header_up X-Forwarded-Proto https',
78
+ ' header_up X-Forwarded-Host {http.request.host}',
79
+ ' @unauth status 401',
80
+ ' handle_response @unauth {',
81
+ ' respond "Unauthorized" 401',
82
+ ' }',
83
+ ' }',
84
+ ], agentProxy), ' }');
23
85
  if (mode === 'self-issued') {
24
- lines.push(' # Browser clients: SSO via oauth2-proxy, identity injected as X-Agent-User.', ' handle {', ` forward_auth ${oauth2ProxyAddr} {`, ' uri /oauth2/auth', ' copy_headers X-Auth-Request-Email>X-Agent-User', ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' @unauth status 401', ' handle_response @unauth {', ' redir * "/oauth2/sign_in?rd={http.request.uri}"', ' }', ' }', ...catchallBody, ' }');
86
+ lines.push(' # Browser clients: SSO via oauth2-proxy, identity injected as X-Agent-User.', ' handle {', ...gated([
87
+ ` forward_auth ${oauth2ProxyAddr} {`,
88
+ ' uri /oauth2/auth',
89
+ ' copy_headers X-Auth-Request-Email>X-Agent-User',
90
+ ' header_up X-Forwarded-Proto https',
91
+ ' header_up X-Forwarded-Host {http.request.host}',
92
+ ' @unauth status 401',
93
+ ' handle_response @unauth {',
94
+ ' redir * "/oauth2/sign_in?rd={http.request.uri}"',
95
+ ' }',
96
+ ' }',
97
+ ], catchallBody), ' }');
25
98
  }
26
99
  else {
27
100
  const idp = opts.idpUrl?.trim() || '';
28
101
  const loginRedirect = idp
29
102
  ? ` redir * "${idp}/oauth/browser-login?return_to=https://{http.request.host}{http.request.uri}" 302`
30
103
  : ' respond "Unauthorized" 401';
31
- lines.push(' # Browser clients: the shared .agenthippo.ai cookie is verified by auth-broker. No cookie', ' # → redirect to the central IdP to log in (it stamps the cookie), then return.', ' handle {', ` forward_auth ${authBrokerAddr} {`, ' uri /api/ide/me', ' copy_headers X-Agent-User', ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' @unauth status 401', ' handle_response @unauth {', loginRedirect, ' }', ' }', ...catchallBody, ' }');
104
+ lines.push(' # Browser clients: the shared .agenthippo.ai cookie is verified by auth-broker. No cookie', ' # → redirect to the central IdP to log in (it stamps the cookie), then return.', ' handle {', ...gated([
105
+ ` forward_auth ${authBrokerAddr} {`,
106
+ ' uri /api/ide/me',
107
+ ' copy_headers X-Agent-User',
108
+ ' header_up X-Forwarded-Proto https',
109
+ ' header_up X-Forwarded-Host {http.request.host}',
110
+ ' @unauth status 401',
111
+ ' handle_response @unauth {',
112
+ loginRedirect,
113
+ ' }',
114
+ ' }',
115
+ ], catchallBody), ' }');
32
116
  }
33
117
  return lines;
34
118
  }
@@ -36,8 +36,9 @@ var __importStar = (this && this.__importStar) || (function () {
36
36
  };
37
37
  })();
38
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;
39
+ exports.AGENTHIPPO_CLI_NPM_VERSION_FALLBACK = exports.AGENTHIPPO_CLI_NPM_PACKAGE = exports.DEPLOY_PUBLIC_HOSTNAME = exports.DEFAULT_DEPLOY_PUBLIC_HOSTNAME = exports.CLAUDE_CODE_NPM_VERSION = exports.AGENTHIPPO_IMAGE_TAG = exports.AGENTHIPPO_IMAGE_REGISTRY = exports.DEPLOY_TARGETS = void 0;
40
40
  exports.isDeployTarget = isDeployTarget;
41
+ exports.agenthippoImage = agenthippoImage;
41
42
  exports.defaultRuntimeImage = defaultRuntimeImage;
42
43
  exports.packAttributionBody = packAttributionBody;
43
44
  exports.packAttributionComment = packAttributionComment;
@@ -50,6 +51,7 @@ exports.renderEnvExample = renderEnvExample;
50
51
  exports.renderAuthEnvExample = renderAuthEnvExample;
51
52
  exports.renderAuthBrokerDockerfile = renderAuthBrokerDockerfile;
52
53
  exports.isSecretLikeBasename = isSecretLikeBasename;
54
+ exports.dataPlaneTokenEnvVar = dataPlaneTokenEnvVar;
53
55
  exports.renderTarget = renderTarget;
54
56
  /**
55
57
  * `agenthippo deploy init` — generator for platform-native deploy artifacts
@@ -75,11 +77,40 @@ exports.DEPLOY_TARGETS = ['vm', 'compose', 'cloudrun', 'databricks-app', 'spcs',
75
77
  function isDeployTarget(value) {
76
78
  return exports.DEPLOY_TARGETS.includes(value);
77
79
  }
80
+ /** GHCR namespace for every AgentHippo-published image. Canonical — `agenthippoai`,
81
+ * matching what scripts/release/publish-runtime-image.sh actually pushes to.
82
+ * (`ghcr.io/agenthippo/` — no trailing "ai" — is a dead namespace; do not reintroduce it.) */
83
+ exports.AGENTHIPPO_IMAGE_REGISTRY = 'ghcr.io/agenthippoai';
84
+ /**
85
+ * The ONE dated tag every generated artifact pins to.
86
+ *
87
+ * Generated customer artifacts must never say `:latest`: a floating tag silently
88
+ * re-points a customer's deployment at a different image, which is exactly what the
89
+ * dated-tag policy in scripts/release/publish-runtime-image.sh exists to prevent.
90
+ *
91
+ * MUST match the `--version` used by scripts/release/publish-runtime-image.sh for the
92
+ * currently promoted release (scheme: `YYYY.MM` or `YYYY.MM.DD` — see
93
+ * scripts/release/README.md § Versioning). Bump here and in the publish run together.
94
+ */
95
+ exports.AGENTHIPPO_IMAGE_TAG = '2026.07';
96
+ /**
97
+ * Claude Code CLI version pinned into generated customer artifacts.
98
+ *
99
+ * `"latest"` here meant every customer install resolved a different version, so two
100
+ * deploys of the same pack were not the same software. Keep this equal to the
101
+ * `CLAUDE_CODE_VERSION` build ARG in extensions/agentide/scripts/Dockerfile.runtime,
102
+ * so the source-based (Databricks) path and the container path ship the same engine.
103
+ */
104
+ exports.CLAUDE_CODE_NPM_VERSION = '2.1.220';
105
+ /** `ghcr.io/agenthippoai/<name>:<dated tag>` — the only way to name a published image. */
106
+ function agenthippoImage(name, variant) {
107
+ return `${exports.AGENTHIPPO_IMAGE_REGISTRY}/${name}:${exports.AGENTHIPPO_IMAGE_TAG}${variant ? `-${variant}` : ''}`;
108
+ }
78
109
  /** Default runtime image for a pack engine (cursor needs the /engines layer). */
79
110
  function defaultRuntimeImage(engine) {
80
111
  return engine.trim().toLowerCase() === 'cursor'
81
- ? 'ghcr.io/agenthippoai/runtime:latest-cursor'
82
- : 'ghcr.io/agenthippoai/runtime:latest';
112
+ ? agenthippoImage('runtime', 'cursor')
113
+ : agenthippoImage('runtime');
83
114
  }
84
115
  /** Provenance line for generated deploy artifacts (pack-standard WS2). */
85
116
  function packAttributionBody(ctx) {
@@ -285,14 +316,15 @@ function renderAuthEnvExample(opts) {
285
316
  `AGENTHIPPO_AUTH_IDP_URL=${idp} # where browsers are sent to log in`,
286
317
  'AGENTHIPPO_AUTH_COOKIE_NAME=ah_token',
287
318
  '',
288
- '# Team bootstrap (store + LiteLLM routing) — same as the IdP if you want parity.',
319
+ '# Team bootstrap (store + gateway routing) — same as the IdP if you want parity.',
289
320
  'AGENTHIPPO_TEAM_ID=team_default',
290
321
  'AGENTHIPPO_TEAM_NAME=Default Team',
291
322
  'AGENTHIPPO_TEAM_STORE_REPO=agenthippoai/agenthippo-store',
292
323
  'AGENTHIPPO_TEAM_STORE_INDEX_URL=https://agenthippoai.github.io/agenthippo-store',
293
324
  'AGENTHIPPO_TEAM_GITHUB_ORG=agenthippoai',
294
- 'AGENTHIPPO_TEAM_LITELLM_BASE_URL=http://127.0.0.1:4000',
295
- 'AGENTHIPPO_TEAM_LITELLM_API_KEY=',
325
+ '# An agenthippo-gateway deployment (JWT lane) — NOT bare LiteLLM. No key: the account',
326
+ '# JWT is the routing credential.',
327
+ 'AGENTHIPPO_TEAM_GATEWAY_URL=https://gateway.agenthippo.ai/v1',
296
328
  '',
297
329
  '# No OAUTH2_PROXY_* here — this agent does not talk to Google. The one oauth2-proxy +',
298
330
  '# one Google OAuth client live only at the IdP.',
@@ -328,8 +360,9 @@ function renderAuthEnvExample(opts) {
328
360
  'AGENTHIPPO_TEAM_STORE_REPO=agenthippoai/agenthippo-store',
329
361
  'AGENTHIPPO_TEAM_STORE_INDEX_URL=https://agenthippoai.github.io/agenthippo-store',
330
362
  'AGENTHIPPO_TEAM_GITHUB_ORG=agenthippoai',
331
- 'AGENTHIPPO_TEAM_LITELLM_BASE_URL=http://127.0.0.1:4000',
332
- 'AGENTHIPPO_TEAM_LITELLM_API_KEY=',
363
+ '# An agenthippo-gateway deployment (JWT lane) — NOT bare LiteLLM. No key: the account',
364
+ '# JWT is the routing credential.',
365
+ 'AGENTHIPPO_TEAM_GATEWAY_URL=https://gateway.agenthippo.ai/v1',
333
366
  ].join('\n') + '\n';
334
367
  }
335
368
  /** Auth broker sidecar for IDE sign-in + team bootstrap API. */
@@ -337,7 +370,7 @@ function renderAuthBrokerDockerfile() {
337
370
  return [
338
371
  '# Auth broker for IDE sign-in + team bootstrap API.',
339
372
  '# Build context = pack root: docker build -f deploy/Dockerfile.auth-broker -t <pack>-auth-broker:local .',
340
- 'FROM ghcr.io/agenthippoai/runtime:latest',
373
+ `FROM ${agenthippoImage('runtime')}`,
341
374
  'ENV AGENTHIPPO_AUTH_PORT=9780',
342
375
  'ENV AGENTHIPPO_AUTH_DATA_DIR=/data',
343
376
  'EXPOSE 9780',
@@ -392,6 +425,10 @@ function renderDockerignore() {
392
425
  '# Secrets anywhere in the pack (defensive; deploy/ secrets are also covered below).',
393
426
  '# Keep in sync with isSecretLikeBasename() above.',
394
427
  '**/.env',
428
+ // `**/.env` alone does NOT match `.env.auth` / `.env.local` / `.env.production`.
429
+ // `.env.auth` carries AGENTHIPPO_AUTH_JWT_SECRET and OAUTH2_PROXY_* — baking it
430
+ // into a published layer leaks fleet-wide token-forgery material.
431
+ '**/.env.*',
395
432
  '**/*credentials*',
396
433
  '**/*.pem',
397
434
  '**/*.key',
@@ -404,6 +441,9 @@ function renderDockerignore() {
404
441
  '**/id_dsa',
405
442
  '**/id_ecdsa',
406
443
  '**/id_ed25519',
444
+ '# …but *.example templates are safe and are what the README tells customers to copy.',
445
+ '!**/.env.example',
446
+ '!**/.env.*.example',
407
447
  '# deploy/ holds build/deploy artifacts + secrets; only deploy/www/ ships at runtime.',
408
448
  'deploy/*',
409
449
  '!deploy/www',
@@ -721,9 +761,16 @@ function renderEdgeTunnelUpScript(hostname) {
721
761
  * predictable container name) plus a Caddy route snippet for the operator to drop into the
722
762
  * IdP's `agents/` import directory (see enterprise-deploy/idp/Caddyfile + agents/README.md).
723
763
  */
764
+ /** Env var (in the IdP/edge stack's env) holding the connector sidecar's register token —
765
+ * matches the connector's own AGENTHIPPO_DATA_PLANE_TOKEN. Convention proven by the
766
+ * dynamodb-analyst overlay: pack `dynamodb-analyst` → `DYNAMODB_ANALYST_DATA_PLANE_TOKEN`. */
767
+ function dataPlaneTokenEnvVar(packName) {
768
+ return packName.toUpperCase().replace(/[^A-Z0-9]/g, '_') + '_DATA_PLANE_TOKEN';
769
+ }
724
770
  function renderColocatedArtifacts(ctx) {
725
771
  const idp = (ctx.idpUrl || '').trim().replace(/\/+$/, '');
726
772
  const containerName = `${ctx.packName}-agent`;
773
+ const turnTokenEnvVar = dataPlaneTokenEnvVar(ctx.packName);
727
774
  const compose = [
728
775
  '# Colocated agent (PLAN/deploy.md §4 state 4) — no Caddy/oauth2-proxy/auth-broker of its',
729
776
  '# own. Joins the org IdP\'s docker network so its already-running Caddy can reach this',
@@ -768,6 +815,9 @@ function renderColocatedArtifacts(ctx) {
768
815
  idpUrl: idp,
769
816
  healthProxy: [` reverse_proxy ${containerName}:${ctx.port}`],
770
817
  agentProxy: [` reverse_proxy ${containerName}:${ctx.port}`],
818
+ ...(ctx.edgeTurnConnector
819
+ ? { turnRegistration: { connectorAddr: ctx.edgeTurnConnector, tokenEnvVar: turnTokenEnvVar } }
820
+ : {}),
771
821
  }),
772
822
  '}',
773
823
  '',
@@ -791,6 +841,16 @@ function renderColocatedArtifacts(ctx) {
791
841
  '',
792
842
  '# Wire the route into the IdP\'s Caddy (one-time per agent):',
793
843
  `cp deploy/agent-route.caddy <idp-host>:enterprise-deploy/idp/agents/${ctx.packName}.caddy`,
844
+ ...(ctx.edgeTurnConnector
845
+ ? [
846
+ '',
847
+ '# EDGE turn registration (P1 fix): the route registers each chat turn with the connector',
848
+ `# sidecar (${ctx.edgeTurnConnector}). Set the register token in the IdP stack's env —`,
849
+ `# same value as the connector's AGENTHIPPO_DATA_PLANE_TOKEN — before reloading:`,
850
+ `# echo "${turnTokenEnvVar}=<token>" >> enterprise-deploy/idp/.env`,
851
+ '# The agent container never holds this token (that is the point).',
852
+ ]
853
+ : []),
794
854
  'docker compose -f enterprise-deploy/idp/docker-compose.yml exec caddy \\',
795
855
  ' caddy reload --config /etc/caddy/Caddyfile',
796
856
  '```',
@@ -991,7 +1051,7 @@ function composeArtifacts(ctx) {
991
1051
  ' # OPENCLAW_GATEWAY_TOKEN (or a paired device) to connect. Set it in .env.',
992
1052
  ' openclaw-gateway:',
993
1053
  ' profiles: ["channels"]',
994
- ' image: ghcr.io/agenthippoai/openclaw-gateway:2026.06',
1054
+ ` image: ${agenthippoImage('openclaw-gateway')}`,
995
1055
  ' env_file:',
996
1056
  ' - path: .env',
997
1057
  ' required: false',
@@ -1022,13 +1082,18 @@ function composeArtifacts(ctx) {
1022
1082
  ' # (build: docker build -f extensions/agentide/scripts/Dockerfile.litellm -t this-tag .)',
1023
1083
  ' litellm:',
1024
1084
  ' profiles: ["gateway"]',
1025
- ' image: ghcr.io/agenthippoai/litellm-gateway:2026.06',
1085
+ ` image: ${agenthippoImage('agenthippo-litellm')}`,
1026
1086
  ' env_file:',
1027
1087
  ' - path: .env',
1028
1088
  ' required: false',
1029
1089
  ' environment:',
1030
1090
  ' OTEL_EXPORTER: otlp_http',
1031
1091
  ' OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318',
1092
+ ' # The image defaults to --host 127.0.0.1 (safe when someone runs it bare).',
1093
+ ' # Here it must answer the agent container over the compose network, so bind',
1094
+ ' # 0.0.0.0 — and keep `expose:` rather than `ports:` so it is never reachable',
1095
+ ' # from the host. Requires general_settings.master_key in the baked config.',
1096
+ ' command: ["--config", "/config/litellm.config.yaml", "--port", "4443", "--host", "0.0.0.0"]',
1032
1097
  ' expose: ["4443"]',
1033
1098
  ' depends_on: [otel-collector]',
1034
1099
  '',
@@ -1340,7 +1405,7 @@ function renderCloudrunSpotlightDeploy(ctx) {
1340
1405
  'PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project)}"',
1341
1406
  'REPO="${ARTIFACT_REPO:-agenthippo}"',
1342
1407
  'IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/${SERVICE}:latest"',
1343
- 'LITELLM_IMAGE="${LITELLM_IMAGE:-ghcr.io/agenthippoai/litellm-gateway:2026.07}"',
1408
+ `LITELLM_IMAGE="\${LITELLM_IMAGE:-${agenthippoImage('agenthippo-litellm')}}"`,
1344
1409
  'STATE_BUCKET="${STATE_BUCKET:?set STATE_BUCKET=your-gcs-bucket (gcloud storage buckets create gs://...)}"',
1345
1410
  '# LITELLM_KEY = the proxy key the agent presents to LiteLLM (and LiteLLM master key).',
1346
1411
  'LITELLM_KEY="${LITELLM_KEY:?set LITELLM_KEY=<litellm proxy key>}"',
@@ -1351,8 +1416,8 @@ function renderCloudrunSpotlightDeploy(ctx) {
1351
1416
  ' || gcloud artifacts repositories create "$REPO" --repository-format=docker --location="$REGION"',
1352
1417
  'gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet',
1353
1418
  '',
1354
- '# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/agenthippoai/runtime:latest',
1355
- '# tag was retired; AR has runtime:latest / :latest-cursor). No-op if the base already targets AR.',
1419
+ '# Rewrite the runtime base to this project Artifact Registry. Customer artifacts pin a',
1420
+ '# dated ghcr.io/agenthippoai/runtime tag; mirror that exact tag into AR. No-op if already AR.',
1356
1421
  'DF_TMP="$(mktemp)"',
1357
1422
  'sed "s|FROM ghcr.io/agenthippoai/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
1358
1423
  'docker build --pull --platform linux/amd64 -f "$DF_TMP" -t "$IMAGE" .',
@@ -1527,7 +1592,7 @@ function renderCloudrunEdgeDeploy(ctx) {
1527
1592
  'PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project)}"',
1528
1593
  'REPO="${ARTIFACT_REPO:-agenthippo}"',
1529
1594
  'IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/${SERVICE}:latest"',
1530
- 'LITELLM_IMAGE="${LITELLM_IMAGE:-ghcr.io/agenthippoai/litellm-gateway:2026.07}"',
1595
+ `LITELLM_IMAGE="\${LITELLM_IMAGE:-${agenthippoImage('agenthippo-litellm')}}"`,
1531
1596
  '# Public hostname (custom domain mapped to the service) + the Google Workspace domain allowed by SSO.',
1532
1597
  'PUBLIC_HOSTNAME="${PUBLIC_HOSTNAME:?set PUBLIC_HOSTNAME=your.edge.hostname (gcloud run domain-mappings create --service $SERVICE --domain ...)}"',
1533
1598
  'ALLOWED_DOMAINS="${ALLOWED_DOMAINS:?set ALLOWED_DOMAINS=yourworkspace.com (oauth2-proxy email domain + broker allowed domains)}"',
@@ -1550,8 +1615,8 @@ function renderCloudrunEdgeDeploy(ctx) {
1550
1615
  'gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet',
1551
1616
  '',
1552
1617
  '# One agent image serves both the agent and the auth-broker containers (CLI ships `auth-broker`).',
1553
- '# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/agenthippoai/runtime:latest',
1554
- '# tag was retired; AR has runtime:latest / :latest-cursor). No-op if the base already targets AR.',
1618
+ '# Rewrite the runtime base to this project Artifact Registry. Customer artifacts pin a',
1619
+ '# dated ghcr.io/agenthippoai/runtime tag; mirror that exact tag into AR. No-op if already AR.',
1555
1620
  'DF_TMP="$(mktemp)"',
1556
1621
  'sed "s|FROM ghcr.io/agenthippoai/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
1557
1622
  'docker build --pull --platform linux/amd64 -f "$DF_TMP" -t "$IMAGE" .',
@@ -1664,8 +1729,8 @@ function cloudrunArtifacts(ctx) {
1664
1729
  'gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet',
1665
1730
  '',
1666
1731
  '# Build from pack root; Dockerfile is deploy/Dockerfile (not pack root — gcloud --source . misses it).',
1667
- '# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/agenthippoai/runtime:latest',
1668
- '# tag was retired; AR has runtime:latest / :latest-cursor). No-op if the base already targets AR.',
1732
+ '# Rewrite the runtime base to this project Artifact Registry. Customer artifacts pin a',
1733
+ '# dated ghcr.io/agenthippoai/runtime tag; mirror that exact tag into AR. No-op if already AR.',
1669
1734
  'DF_TMP="$(mktemp)"',
1670
1735
  'sed "s|FROM ghcr.io/agenthippoai/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
1671
1736
  'docker build --pull --platform linux/amd64 -f "$DF_TMP" -t "$IMAGE" .',
@@ -1700,7 +1765,7 @@ function cloudrunArtifacts(ctx) {
1700
1765
  : '# - No inbound channel (1a): scales to zero; state on the GCS volume survives the instance dying.',
1701
1766
  '# - GCS-FUSE adds latency on session read/write vs local disk; fine for chat, watch for heavy I/O.',
1702
1767
  isCursor
1703
- ? '# - Cursor engine: CURSOR_API_KEY secret + runtime:latest-cursor base (AGENTHIPPO_CUSTOM_ENGINES_DIR=/engines).'
1768
+ ? `# - Cursor engine: CURSOR_API_KEY secret + ${agenthippoImage('runtime', 'cursor')} base (AGENTHIPPO_CUSTOM_ENGINES_DIR=/engines).`
1704
1769
  : '# - Vertex path = keyless Claude on your existing GCP entitlement; swap for ANTHROPIC_API_KEY if preferred.',
1705
1770
  '# - First deploy without IAP org setup: ENABLE_IAP=0 bash deploy/cloudrun-deploy.sh',
1706
1771
  '',
@@ -2092,7 +2157,7 @@ function renderCloudrunFedDeploy(ctx) {
2092
2157
  'PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project)}"',
2093
2158
  'REPO="${ARTIFACT_REPO:-agenthippo}"',
2094
2159
  'IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/${SERVICE}:latest"',
2095
- 'LITELLM_IMAGE="${LITELLM_IMAGE:-ghcr.io/agenthippoai/litellm-gateway:2026.07}"',
2160
+ `LITELLM_IMAGE="\${LITELLM_IMAGE:-${agenthippoImage('agenthippo-litellm')}}"`,
2096
2161
  '# Public hostname = a *.agenthippo.ai subdomain mapped to this service (one-time wildcard DNS).',
2097
2162
  'PUBLIC_HOSTNAME="${PUBLIC_HOSTNAME:?set PUBLIC_HOSTNAME=<subdomain>.agenthippo.ai}"',
2098
2163
  'ALLOWED_DOMAINS="${ALLOWED_DOMAINS:-agenthippo.ai}"',
@@ -2116,8 +2181,8 @@ function renderCloudrunFedDeploy(ctx) {
2116
2181
  'gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet',
2117
2182
  '',
2118
2183
  '# One agent image serves both the agent and the auth-broker containers (CLI ships `auth-broker`).',
2119
- '# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/agenthippoai/runtime:latest',
2120
- '# tag was retired; AR has runtime:latest / :latest-cursor). No-op if the base already targets AR.',
2184
+ '# Rewrite the runtime base to this project Artifact Registry. Customer artifacts pin a',
2185
+ '# dated ghcr.io/agenthippoai/runtime tag; mirror that exact tag into AR. No-op if already AR.',
2121
2186
  'DF_TMP="$(mktemp)"',
2122
2187
  'sed "s|FROM ghcr.io/agenthippoai/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
2123
2188
  'docker build --pull --platform linux/amd64 -f "$DF_TMP" -t "$IMAGE" .',
@@ -2229,7 +2294,7 @@ function databricksAppArtifacts(ctx) {
2229
2294
  private: true,
2230
2295
  dependencies: {
2231
2296
  [exports.AGENTHIPPO_CLI_NPM_PACKAGE]: resolveAgentHippoCliNpmVersion(),
2232
- '@anthropic-ai/claude-code': 'latest',
2297
+ '@anthropic-ai/claude-code': exports.CLAUDE_CODE_NPM_VERSION,
2233
2298
  },
2234
2299
  }, null, 2) + '\n';
2235
2300
  const start = [
package/out/cli/deps.js CHANGED
@@ -37,12 +37,14 @@ var __importStar = (this && this.__importStar) || (function () {
37
37
  })();
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.getDepsDir = getDepsDir;
40
+ exports.resolveFromOwnDeps = resolveFromOwnDeps;
40
41
  exports.areDepsInstalled = areDepsInstalled;
41
42
  exports.installDeps = installDeps;
42
43
  exports.isAutoInstallDisabled = isAutoInstallDisabled;
43
44
  exports.ensureDeps = ensureDeps;
44
45
  exports.getClaudeSdkCliPath = getClaudeSdkCliPath;
45
46
  exports.getCodexSdkBinaryPath = getCodexSdkBinaryPath;
47
+ exports.installedEngineVersion = installedEngineVersion;
46
48
  /**
47
49
  * CLI Dependency Management
48
50
  *
@@ -50,6 +52,7 @@ exports.getCodexSdkBinaryPath = getCodexSdkBinaryPath;
50
52
  * This lets npm/bun handle platform-specific binaries automatically.
51
53
  */
52
54
  const fs = __importStar(require("fs"));
55
+ const os = __importStar(require("os"));
53
56
  const path = __importStar(require("path"));
54
57
  const child_process_1 = require("child_process");
55
58
  const utils_1 = require("./utils");
@@ -84,11 +87,60 @@ function resolveEngineDeps() {
84
87
  // Dependencies required for the engines (resolved once at startup).
85
88
  const ENGINE_DEPS = resolveEngineDeps();
86
89
  /**
87
- * Get the deps directory (next to the binary)
90
+ * Where engine SDKs are installed.
91
+ *
92
+ * Normally beside the binary — a self-contained install that survives `$HOME` changes. But
93
+ * `process.execPath` is the *interpreter* when the CLI runs as `node out/cli/index.js`, which
94
+ * on a locked-down host means `/usr/bin`, and the first run dies with
95
+ * `EACCES: mkdir '/usr/bin/.agenthippo-deps'` (observed on Amazon Linux, 2026-07-27). A
96
+ * runtime must not need write access to the system bin dir, so fall back to the agent home.
88
97
  */
89
98
  function getDepsDir() {
90
99
  const binaryDir = path.dirname(process.execPath);
91
- return path.join(binaryDir, branding_1.AGENTIDE_DEPS_DIR_NAME);
100
+ const beside = path.join(binaryDir, branding_1.AGENTIDE_DEPS_DIR_NAME);
101
+ if (fs.existsSync(beside)) {
102
+ return beside;
103
+ }
104
+ try {
105
+ fs.accessSync(binaryDir, fs.constants.W_OK);
106
+ return beside;
107
+ }
108
+ catch {
109
+ // Not writable (system node, read-only image, non-root user): use the agent home.
110
+ return path.join(os.homedir(), branding_1.AGENTIDE_DIR_NAME, branding_1.AGENTIDE_DEPS_DIR_NAME);
111
+ }
112
+ }
113
+ /**
114
+ * Engine SDKs resolved from the CLI's OWN dependency tree.
115
+ *
116
+ * When the runtime ships as a node app (`node out/cli/index.js` beside a `node_modules/`) the
117
+ * SDKs are already installed as ordinary dependencies — there is nothing to fetch, and asking
118
+ * the user to set CLAUDE_CODE_PATH by hand for a dependency we declare ourselves is a
119
+ * papercut that costs a deployment. `require.resolve` finds them wherever the tree lives.
120
+ */
121
+ function resolveFromOwnDeps(pkgName, relativeFile) {
122
+ // A node_modules walk, deliberately, rather than `require.resolve`. Neither SDK can be
123
+ // resolved by specifier: @anthropic-ai/claude-agent-sdk publishes an `exports` map that
124
+ // omits ./cli.js (deep resolve → ERR_PACKAGE_PATH_NOT_EXPORTED) and @openai/codex declares
125
+ // no main/exports at all, only a `bin` (bare resolve → MODULE_NOT_FOUND). Both were
126
+ // verified against the shipped versions on 2026-07-27. Walking for the file we actually
127
+ // want is immune to how a package chooses to advertise itself.
128
+ let dir = __dirname;
129
+ for (let hops = 0; hops < 8; hops++) {
130
+ const candidate = path.join(dir, 'node_modules', ...pkgName.split('/'), ...relativeFile.split('/'));
131
+ try {
132
+ if (fs.existsSync(candidate)) {
133
+ return candidate;
134
+ }
135
+ }
136
+ catch { /* keep walking */ }
137
+ const parent = path.dirname(dir);
138
+ if (parent === dir) {
139
+ break;
140
+ }
141
+ dir = parent;
142
+ }
143
+ return undefined;
92
144
  }
93
145
  /**
94
146
  * Check if engine dependencies are installed
@@ -183,6 +235,14 @@ async function ensureDeps(verbose = false) {
183
235
  if (areDepsInstalled()) {
184
236
  return;
185
237
  }
238
+ // Already satisfied by our own dependency tree (node-app runtime): nothing to install, and
239
+ // attempting it would fail on a read-only or non-writable install dir for no benefit.
240
+ if (resolveFromOwnDeps('@anthropic-ai/claude-agent-sdk', 'cli.js') && resolveFromOwnDeps('@openai/codex', 'bin/codex.js')) {
241
+ if (verbose) {
242
+ console.error(`${utils_1.COLORS.dim}Engine SDKs resolved from the CLI's own node_modules — skipping install.${utils_1.COLORS.reset}`);
243
+ }
244
+ return;
245
+ }
186
246
  if (isAutoInstallDisabled()) {
187
247
  if (verbose) {
188
248
  console.error(`${utils_1.COLORS.dim}AGENTHIPPO_NO_AUTO_INSTALL set — skipping engine dependency install (expecting a self-contained image).${utils_1.COLORS.reset}`);
@@ -236,3 +296,47 @@ function getCodexPlatform() {
236
296
  }
237
297
  return undefined;
238
298
  }
299
+ /**
300
+ * The engine version actually installed next to this binary.
301
+ *
302
+ * Reported on the lease poll so the console can answer "what is the fleet really running?" —
303
+ * pack version alone cannot explain a behaviour change that came from an engine upgrade, and the
304
+ * CLI is NOT engine-version agnostic (see AGENTIDE_CODEX_CONFIG_VERSION: config we write is only
305
+ * valid on codex >= 0.131, and getting it wrong silently cancels headless tool calls).
306
+ *
307
+ * Best effort by design: a deployment that vendors its engines elsewhere reports nothing rather
308
+ * than guessing, and the console shows a dash. Never throws — this is on the boot path.
309
+ *
310
+ * Expect nothing under a CONTAINER sandbox: there the engine runs inside the sandbox image, not
311
+ * beside serve, so serve genuinely does not know its version and the image digest is what
312
+ * identifies that build. Reporting serve's own (absent) copy would be a guess about someone
313
+ * else's filesystem. Process sandboxes and in-process runs share this binary's deps, so those
314
+ * report truthfully.
315
+ */
316
+ function installedEngineVersion(engine) {
317
+ const pkg = engine === 'codex' ? path.join('@openai', 'codex')
318
+ : engine === 'claude' ? path.join('@anthropic-ai', 'claude-agent-sdk')
319
+ : undefined;
320
+ if (!pkg) {
321
+ return undefined;
322
+ }
323
+ const candidates = [
324
+ path.join(getDepsDir(), 'node_modules', pkg, 'package.json'),
325
+ // Runtime images install engines globally rather than beside the binary.
326
+ path.join('/usr/local/lib/node_modules', pkg, 'package.json'),
327
+ path.join('/usr/lib/node_modules', pkg, 'package.json'),
328
+ ];
329
+ for (const candidate of candidates) {
330
+ try {
331
+ if (!fs.existsSync(candidate)) {
332
+ continue;
333
+ }
334
+ const parsed = JSON.parse(fs.readFileSync(candidate, 'utf8'));
335
+ if (parsed.version) {
336
+ return parsed.version;
337
+ }
338
+ }
339
+ catch { /* unreadable or malformed — try the next candidate */ }
340
+ }
341
+ return undefined;
342
+ }
@@ -78,6 +78,18 @@ function renderIdpComposeYaml() {
78
78
  ' environment:',
79
79
  ' AGENTHIPPO_AUTH_PORT: "9780"',
80
80
  ' AGENTHIPPO_AUTH_DATA_DIR: /data',
81
+ ' # The broker now defaults to binding 127.0.0.1 and REFUSES a public bind unless told',
82
+ ' # otherwise. Here it must listen on the container interface, because Caddy reaches it',
83
+ ' # as `auth-broker:9780` over the compose network — a loopback bind makes it unreachable.',
84
+ ' # Safe: the port is `expose`d, never published to the host, so the only thing that can',
85
+ ' # dial it is another service on this network.',
86
+ ' AGENTHIPPO_AUTH_BIND: "0.0.0.0"',
87
+ ' AGENTHIPPO_AUTH_ALLOW_PUBLIC_BIND: "1"',
88
+ ' # Shared secret proving an X-Auth-Request-Email came from THIS Caddy. Without it the',
89
+ ' # broker treats that header as unverified and 401s the forward_auth-gated routes.',
90
+ ' # No default on purpose: compose FAILS to start rather than silently running with an',
91
+ ' # empty secret on both sides, which would accept every caller. Put it in ./.env.',
92
+ ' AGENTHIPPO_AUTH_PROXY_SECRET: "${AGENTHIPPO_AUTH_PROXY_SECRET:?set AGENTHIPPO_AUTH_PROXY_SECRET in .env (openssl rand -hex 32)}"',
81
93
  ' # Proxy hops in front of the broker: 1 = Caddy only (edge profile), 2 = Cloudflare',
82
94
  ' # Tunnel → Caddy (run.sh sets 2; at 1 the rate limiter would key every tunnel',
83
95
  ' # client on cloudflared\'s IP — one shared bucket).',
@@ -101,6 +113,11 @@ function renderIdpComposeYaml() {
101
113
  ' ports:',
102
114
  ' - "${AGENTHIPPO_CADDY_BIND:-0.0.0.0}:${AGENTHIPPO_CADDY_HTTP_PORT:-80}:80"',
103
115
  ' - "${AGENTHIPPO_CADDY_BIND:-0.0.0.0}:${AGENTHIPPO_CADDY_HTTPS_PORT:-443}:443"',
116
+ ' environment:',
117
+ ' # Caddy stamps this on the two broker hops that forward a verified X-Auth-Request-Email',
118
+ ' # (/oauth/authorize, /oauth/browser-login). Same value the broker checks; same fail-closed',
119
+ ' # rule — compose refuses to start rather than send an empty secret.',
120
+ ' AGENTHIPPO_AUTH_PROXY_SECRET: "${AGENTHIPPO_AUTH_PROXY_SECRET:?set AGENTHIPPO_AUTH_PROXY_SECRET in .env (openssl rand -hex 32)}"',
104
121
  ' volumes:',
105
122
  ' - ./Caddyfile:/etc/caddy/Caddyfile:ro',
106
123
  ' # Colocated agents (deploy init --colocate) drop a route snippet here — see Caddyfile.',
@@ -179,6 +196,15 @@ function renderIdpArtifacts(ctx) {
179
196
  '',
180
197
  `AGENTHIPPO_PUBLIC_HOSTNAME=${ctx.hostname}`,
181
198
  '',
199
+ '# REQUIRED. Shared secret between Caddy and auth-broker, proving that an',
200
+ '# X-Auth-Request-Email reached the broker through this Caddy rather than from a client',
201
+ '# that merely found the port. The broker 401s that header without a matching secret, and',
202
+ '# docker compose refuses to start if this is unset — an empty secret on both sides would',
203
+ '# accept everyone. It lives HERE (not .env.auth) because Caddy needs it too, and Caddy',
204
+ '# must not be handed the OAuth client secret or the JWT signing key.',
205
+ '# openssl rand -hex 32',
206
+ 'AGENTHIPPO_AUTH_PROXY_SECRET=',
207
+ '',
182
208
  '# Proxy hops / Caddy bind (optional; run.sh sets XFF hops=2 for tunnel)',
183
209
  '# AGENTHIPPO_XFF_TRUSTED_HOPS=1',
184
210
  '# AGENTHIPPO_CADDY_BIND=0.0.0.0',
@@ -276,8 +302,17 @@ function renderIdpArtifacts(ctx) {
276
302
  '```bash',
277
303
  'cp .env.example ~/.agent-hippo/.env',
278
304
  'cp .env.auth.example ~/.agent-hippo/.env.auth # fill in; never put AGENTHIPPO_AUTH_JWT_SECRET in .env',
305
+ '',
306
+ '# REQUIRED: the Caddy↔broker proxy secret. It must be visible to `docker compose`',
307
+ '# ITSELF (compose interpolates it into both services), so it goes in ./.env here —',
308
+ '# not only in ~/.agent-hippo/.env, which is read by containers but not by compose.',
309
+ 'echo "AGENTHIPPO_AUTH_PROXY_SECRET=$(openssl rand -hex 32)" >> .env',
279
310
  '```',
280
311
  '',
312
+ 'Without it `docker compose up` fails immediately with a message naming the variable. That',
313
+ 'is deliberate: the broker refuses an `X-Auth-Request-Email` that arrives without a matching',
314
+ '`x-agenthippo-proxy-secret`, so an empty secret on both sides would accept every caller.',
315
+ '',
281
316
  '**Cloudflare Tunnel** (no public ports to manage yourself):',
282
317
  '',
283
318
  '```bash',