@agenthippo-ai/agenthippo 0.1.1 → 1.110.9

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 (158) hide show
  1. package/out/agentpack/index.js +2 -1
  2. package/out/agentpack/paths.js +23 -0
  3. package/out/analytics/dashboard/fleetDisplayUtils.js +3 -0
  4. package/out/analytics/dashboard/provider.js +13 -2
  5. package/out/analytics/dashboard/webview/panel.js +293 -82
  6. package/out/analytics/identity.js +0 -0
  7. package/out/analytics/service.js +23 -25
  8. package/out/analytics/sessionEvents.js +109 -22
  9. package/out/auth/authBaseUrl.js +59 -0
  10. package/out/auth/bootstrapApply.js +9 -10
  11. package/out/auth/brokerClient.js +49 -15
  12. package/out/auth/onboardingPrompt.js +112 -0
  13. package/out/auth/provider.js +9 -22
  14. package/out/auth/sessionStore.js +3 -40
  15. package/out/auth/signInFlow.js +2 -1
  16. package/out/auth-broker/config.js +54 -4
  17. package/out/auth-broker/server.js +152 -20
  18. package/out/auth-broker/store.js +25 -24
  19. package/out/authority/warrant.js +206 -0
  20. package/out/branding.js +27 -1
  21. package/out/cli/commands/ask.js +3 -0
  22. package/out/cli/commands/chat.js +3 -0
  23. package/out/cli/commands/conformance.js +249 -0
  24. package/out/cli/commands/data-plane.js +67 -0
  25. package/out/cli/commands/deploy.js +50 -10
  26. package/out/cli/commands/gateway.js +58 -0
  27. package/out/cli/commands/idp.js +85 -0
  28. package/out/cli/commands/pack.js +182 -3
  29. package/out/cli/commands/sandbox.js +332 -0
  30. package/out/cli/commands/serve.js +1165 -82
  31. package/out/cli/commands/store.js +12 -4
  32. package/out/cli/credential-context.js +42 -80
  33. package/out/cli/data-plane/broker.js +78 -0
  34. package/out/cli/data-plane/connector-host.js +121 -0
  35. package/out/cli/data-plane/egress.js +189 -0
  36. package/out/cli/data-plane/gateway.js +218 -0
  37. package/out/cli/data-plane/index.js +152 -0
  38. package/out/cli/data-plane/registry.js +89 -0
  39. package/out/cli/data-plane/remote.js +106 -0
  40. package/out/cli/data-plane/sidecar.js +161 -0
  41. package/out/cli/deploy/blueprints/registry.js +84 -0
  42. package/out/cli/deploy/layers/identity.js +172 -0
  43. package/out/cli/deploy-init.js +1182 -347
  44. package/out/cli/deps.js +106 -2
  45. package/out/cli/idp-init.js +338 -0
  46. package/out/cli/index.js +192 -23
  47. package/out/cli/pack-install-ref.js +15 -0
  48. package/out/cli/pack-loader.js +45 -3
  49. package/out/cli/pack-runtime.js +61 -0
  50. package/out/cli/runtime.js +44 -3
  51. package/out/cli/serve-hardening.js +139 -3
  52. package/out/cli/state-sync.js +16 -15
  53. package/out/cli/utils.js +12 -1
  54. package/out/engines/base.js +32 -3
  55. package/out/engines/claude.js +47 -8
  56. package/out/engines/codex.js +125 -22
  57. package/out/engines/custom.js +54 -12
  58. package/out/evidence/sessionShip.js +610 -0
  59. package/out/extension.js +13 -2
  60. package/out/gateway/bridge.js +40 -2
  61. package/out/gateway/client.js +34 -1
  62. package/out/gateway/installSource.js +68 -0
  63. package/out/gateway/lifecycle.js +185 -51
  64. package/out/gateway/outboundPaths.js +4 -1
  65. package/out/gateway/outboundSend.js +6 -6
  66. package/out/gateway/secretsPreflight.js +90 -0
  67. package/out/gateway/service.js +7 -2
  68. package/out/headless/index.js +40 -9
  69. package/out/live-e2e/mcp-demo/server.mjs +32 -0
  70. package/out/live-probe/connector.mjs +8 -0
  71. package/out/live-probe/env-report.mjs +8 -0
  72. package/out/mcp.js +115 -67
  73. package/out/pack/caps.js +271 -0
  74. package/out/pack/governor.js +309 -0
  75. package/out/pack/lease-schema.js +270 -0
  76. package/out/pack/lease.js +640 -0
  77. package/out/pack/reconcile.js +217 -0
  78. package/out/pack/signature.js +839 -0
  79. package/out/remote/handleRemoteChat.js +44 -3
  80. package/out/remote/remoteTrust.js +277 -0
  81. package/out/remote/serveClient.js +52 -5
  82. package/out/runner/host.js +131 -0
  83. package/out/runner/protocol.js +108 -0
  84. package/out/runner/remoteEngine.js +130 -0
  85. package/out/runtime/interfaces/edit-tracker.js +2 -0
  86. package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
  87. package/out/sandbox/containerDriver.js +570 -0
  88. package/out/sandbox/processDriver.js +315 -0
  89. package/out/sandbox/sandboxRunner.js +191 -0
  90. package/out/sandbox/support.js +301 -0
  91. package/out/sandbox/types.js +55 -0
  92. package/out/scripts/agentide-litellm.sh +67 -5
  93. package/out/scripts/agentide_litellm_hooks_v1.py +182 -21
  94. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +94 -118
  95. package/out/scripts/analytics/setup-analytics.ps1 +37 -13
  96. package/out/scripts/analytics/setup-analytics.sh +93 -13
  97. package/out/scripts/analytics/start-analytics.sh +86 -14
  98. package/out/scripts/gateway/setup-gateway.sh +72 -32
  99. package/out/shared/models.js +12 -111
  100. package/out/shared/providers.js +2 -2
  101. package/out/spotlight/install.js +7 -1
  102. package/out/store/index.js +41 -10
  103. package/out/sync.js +3 -1
  104. package/out/test/analytics.test.js +198 -34
  105. package/out/test/auth-baseurl.test.js +132 -0
  106. package/out/test/auth-bootstrapApply.test.js +27 -6
  107. package/out/test/auth-broker-hardening.test.js +227 -0
  108. package/out/test/auth-broker.test.js +112 -9
  109. package/out/test/blueprint-registry.test.js +105 -0
  110. package/out/test/branding.test.js +13 -0
  111. package/out/test/caps-governor.test.js +455 -0
  112. package/out/test/codex-new-file-survival.test.js +119 -0
  113. package/out/test/configuration-scopes.test.js +131 -0
  114. package/out/test/credential-context.test.js +23 -28
  115. package/out/test/custom-env-layering.test.js +151 -0
  116. package/out/test/data-plane-engine-wiring.test.js +250 -0
  117. package/out/test/data-plane.test.js +311 -0
  118. package/out/test/deploy-init.test.js +295 -4
  119. package/out/test/egress-broker.test.js +172 -0
  120. package/out/test/fleetDisplayUtils.test.js +4 -0
  121. package/out/test/gateway-client.test.js +28 -0
  122. package/out/test/gateway-install-source.test.js +111 -0
  123. package/out/test/gateway-lifecycle.test.js +1 -1
  124. package/out/test/gateway-outbound.test.js +7 -0
  125. package/out/test/gateway-secrets-preflight.test.js +127 -0
  126. package/out/test/gateway-session-routing.test.js +18 -0
  127. package/out/test/governor-hardening.test.js +217 -0
  128. package/out/test/headless-sync.test.js +22 -0
  129. package/out/test/idp-init.test.js +151 -0
  130. package/out/test/lease-signature.test.js +357 -0
  131. package/out/test/lease-trust-root.test.js +273 -0
  132. package/out/test/lease-validation.test.js +169 -0
  133. package/out/test/lease.test.js +114 -0
  134. package/out/test/mcp-obo.test.js +52 -27
  135. package/out/test/mcp.test.js +69 -3
  136. package/out/test/model-routing.test.js +133 -0
  137. package/out/test/pack-admission.test.js +216 -0
  138. package/out/test/pack-digest-parity.test.js +140 -0
  139. package/out/test/pack-reconcile.test.js +183 -0
  140. package/out/test/pack-signature-artifact.test.js +103 -0
  141. package/out/test/pack-signature.test.js +176 -0
  142. package/out/test/pack-trust-root.test.js +202 -0
  143. package/out/test/remote-trust.test.js +242 -0
  144. package/out/test/runner-protocol.test.js +249 -0
  145. package/out/test/sandbox-driver.test.js +553 -0
  146. package/out/test/sandbox-hardening.test.js +395 -0
  147. package/out/test/sandbox-runner.test.js +343 -0
  148. package/out/test/serve-gateway-turn.test.js +267 -0
  149. package/out/test/serve-hardening.test.js +125 -6
  150. package/out/test/serveClient.test.js +38 -5
  151. package/out/test/sessionShip.test.js +560 -0
  152. package/out/test/spotlightPublish.test.js +28 -0
  153. package/out/test/state-sync-peer.test.js +18 -5
  154. package/out/test/store-install-safe.test.js +69 -0
  155. package/out/test/tls-bypass.test.js +90 -0
  156. package/out/test/warrant.test.js +138 -0
  157. package/out/utils.js +73 -20
  158. package/package.json +1 -1
@@ -36,13 +36,18 @@ var __importStar = (this && this.__importStar) || (function () {
36
36
  };
37
37
  })();
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.TurnGate = exports.TURN_GATE_BUSY_MESSAGE = exports.TURN_GATE_RETRY_AFTER_SECONDS = exports.TURN_QUEUE_ENV = exports.MAX_CONCURRENT_TURNS_ENV = exports.TurnLimiter = exports.IDENTITY_HEADER_ENV = exports.RateLimiter = exports.XFF_TRUSTED_HOPS_ENV = exports.CONTAINER_SERVE_HOST = exports.SERVE_HOST_ENV = exports.DEFAULT_BIND_HOST = exports.DEFAULT_MAX_BODY_BYTES = void 0;
39
+ exports.TurnGate = exports.TURN_LIMIT_BUSY_MESSAGE = exports.RATE_LIMIT_BUSY_MESSAGE = exports.TURN_GATE_BUSY_MESSAGE = exports.TURN_GATE_RETRY_AFTER_SECONDS = exports.TURN_QUEUE_ENV = exports.MAX_CONCURRENT_TURNS_ENV = exports.TurnLimiter = exports.SESSION_WITHOUT_USER_REASON = exports.IDENTITY_HEADER_ENV = exports.PIN_ONLY_IDENTITY_HEADERS = exports.RateLimiter = exports.XFF_TRUSTED_HOPS_ENV = exports.CONTAINER_SERVE_HOST = exports.SERVE_HOST_ENV = exports.DEFAULT_BIND_HOST = exports.DEFAULT_MAX_BODY_BYTES = void 0;
40
40
  exports.resolveStateDir = resolveStateDir;
41
+ exports.resolveAnalyticsSessionsDir = resolveAnalyticsSessionsDir;
42
+ exports.isLoopbackHost = isLoopbackHost;
41
43
  exports.resolveBindHost = resolveBindHost;
42
44
  exports.clientIp = clientIp;
45
+ exports.identityHeaderAdvice = identityHeaderAdvice;
43
46
  exports.resolveUserId = resolveUserId;
44
47
  exports.resolveDataToken = resolveDataToken;
45
48
  exports.peerToUserId = peerToUserId;
49
+ exports.userNamespaceHash = userNamespaceHash;
50
+ exports.resolveServeSessionKey = resolveServeSessionKey;
46
51
  exports.corsHeaders = corsHeaders;
47
52
  exports.createTurnGateFromEnv = createTurnGateFromEnv;
48
53
  exports.createRejectionWarner = createRejectionWarner;
@@ -60,6 +65,8 @@ exports.readBodyWithCap = readBodyWithCap;
60
65
  */
61
66
  const os = __importStar(require("os"));
62
67
  const path = __importStar(require("path"));
68
+ const crypto = __importStar(require("crypto"));
69
+ const paths_1 = require("../utils/paths");
63
70
  /** Default cap on a single request body (1 MiB). Chat messages are small; large bodies are abuse or bugs. */
64
71
  exports.DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
65
72
  /**
@@ -82,8 +89,33 @@ function resolveStateDir(stateDirOpt, env = process.env) {
82
89
  }
83
90
  return { dir: path.join(os.tmpdir(), 'agenthippo-server'), ephemeral: true };
84
91
  }
92
+ /**
93
+ * Where serve's Layer 2 analytics (tool-call/warrant JSONL) should live. Precedence:
94
+ * 1. AGENT_HOME_DIR / AGENTHIPPO_HOME set → getAgentideHome()/analytics/sessions — the
95
+ * exact location sessionJsonlFleet.ts and query-analytics.sh already scan by default,
96
+ * so this data stays discoverable to Spotlight/DuckDB.
97
+ * 2. Neither set, but the resolved state dir is non-ephemeral (an explicit --state-dir
98
+ * was given) → <stateDir>/analytics/sessions. Not the canonical scan location, but
99
+ * still durable — never regress below whatever durability the caller already
100
+ * established just to be discoverable.
101
+ * 3. Neither set and state is ephemeral (no --state-dir, no home-dir env var at all) →
102
+ * falls through to getAgentideHome()'s own os.homedir() default, matching the
103
+ * already-correct IDE-local behavior — nothing durable was configured either way.
104
+ */
105
+ function resolveAnalyticsSessionsDir(stateDir, stateDirEphemeral, env = process.env) {
106
+ const homeOverride = env.AGENT_HOME_DIR?.trim() || env.AGENTHIPPO_HOME?.trim();
107
+ if (!homeOverride && !stateDirEphemeral) {
108
+ return path.join(stateDir, 'analytics', 'sessions');
109
+ }
110
+ return path.join((0, paths_1.getAgentideHome)(), 'analytics', 'sessions');
111
+ }
85
112
  /** Loopback default — safe everywhere a human runs `serve` locally. */
86
113
  exports.DEFAULT_BIND_HOST = '127.0.0.1';
114
+ /** True when a bind address is reachable only from this machine. */
115
+ function isLoopbackHost(host) {
116
+ const h = host.trim().toLowerCase().replace(/^\[|\]$/g, '');
117
+ return h === 'localhost' || h === '::1' || /^127\./.test(h);
118
+ }
87
119
  /** Env var for the listen address (used by generated deploy Dockerfiles). */
88
120
  exports.SERVE_HOST_ENV = 'AGENTHIPPO_SERVE_HOST';
89
121
  /** Bind address baked into container deploy artifacts (Docker / Cloud Run sidecars). */
@@ -179,15 +211,78 @@ class RateLimiter {
179
211
  }
180
212
  }
181
213
  exports.RateLimiter = RateLimiter;
214
+ /**
215
+ * Identity headers trusted when NOTHING is pinned.
216
+ *
217
+ * Every entry here is a header that exactly one platform sets AND owns the request path for, so
218
+ * a client-supplied copy cannot reach serve without that platform having been bypassed entirely.
219
+ *
220
+ * `x-agent-user` is deliberately NOT in this list, and this is a breaking change for anything
221
+ * that relied on it being trusted by default. It is AgentHippo's own canonical header — the name
222
+ * an arbitrary client is most likely to guess — it belongs to no platform that strips it, and it
223
+ * used to sit FIRST, so it beat every platform header that follows. The consequence on any
224
+ * deployment that had not pinned `AGENTHIPPO_IDENTITY_HEADER`: any HTTP client could send
225
+ * `x-agent-user: victim@example.com` and be namespaced into that user's session state, attach to
226
+ * their conversation, and be issued a data-plane turn nonce scoped to them. Cloud Run IAP makes
227
+ * this concrete — it strips `X-Goog-*` and forwards a client-supplied `x-agent-user` untouched.
228
+ *
229
+ * It remains perfectly valid as a PINNED header (`AGENTHIPPO_IDENTITY_HEADER=x-agent-user`),
230
+ * which is what a deployment whose proxy really does set and strip it should use — including the
231
+ * Caddy `forward_auth` flow, where `auth-broker/server.ts` sets `X-Agent-User` on `/api/ide/me`
232
+ * so Caddy can copy a VALIDATED identity upstream. That direction is the correct and only source
233
+ * of the header, and it is unaffected: pinning is exactly the declaration that it is trustworthy.
234
+ */
182
235
  const HEADER_ALIASES = [
183
- 'x-agent-user', // AgentHippo canonical (set by our Worker / generic proxy)
184
236
  'x-forwarded-email', // Databricks Apps
185
237
  'x-forwarded-preferred-username', // Databricks Apps
186
238
  'x-goog-authenticated-user-email', // Cloud Run IAP (prefixed value, stripped below)
187
239
  'cf-access-authenticated-user-email', // Cloudflare Access
188
240
  ];
241
+ /**
242
+ * Headers that are only ever trusted when explicitly pinned.
243
+ *
244
+ * Kept as a named list rather than deleted, because "why is my identity header ignored?" needs an
245
+ * answer, and `identityHeaderAdvice` gives one that names the env var to set.
246
+ */
247
+ exports.PIN_ONLY_IDENTITY_HEADERS = ['x-agent-user'];
189
248
  /** Env var: pin the single identity header the deploy layer actually sets (and strips). */
190
249
  exports.IDENTITY_HEADER_ENV = 'AGENTHIPPO_IDENTITY_HEADER';
250
+ /**
251
+ * Boot-time advice about identity trust. Returns the lines serve must print, loudest first.
252
+ *
253
+ * Not a refusal. Refusing to boot without a pin would stop deployments that are configured
254
+ * correctly today (Databricks Apps, Cloudflare Access) and whose platform header is genuinely
255
+ * unforgeable — a fail-closed default that mostly punishes the compliant is not a security win.
256
+ * What was actually dangerous was `x-agent-user` being trusted by default, and that is now gone.
257
+ */
258
+ function identityHeaderAdvice(env = process.env, bindHost) {
259
+ const pinned = env[exports.IDENTITY_HEADER_ENV]?.trim();
260
+ if (pinned) {
261
+ return [];
262
+ }
263
+ // Severity follows reachability. On loopback nothing off-box can reach the port, so the same
264
+ // paragraph every local run is warning fatigue — and fatigue is why the message gets ignored
265
+ // on the deploy where it decides whether strangers can name themselves as another user.
266
+ const host = bindHost?.trim();
267
+ if (host && isLoopbackHost(host)) {
268
+ return [
269
+ `${exports.IDENTITY_HEADER_ENV} is not set. Fine for this loopback bind; pin it before exposing ` +
270
+ `serve on a network interface.`,
271
+ ];
272
+ }
273
+ return [
274
+ ...(host
275
+ ? [`serve is bound to ${host} — reachable beyond this machine — with no ${exports.IDENTITY_HEADER_ENV} pinned. ` +
276
+ `Anyone who can reach this port is trusted to state who they are.`]
277
+ : []),
278
+ `${exports.IDENTITY_HEADER_ENV} is not set, so identity falls back to the platform header aliases ` +
279
+ `(${HEADER_ALIASES.join(', ')}). That is only safe when your proxy sets AND STRIPS every one of them. ` +
280
+ `Pin the single header your deploy layer really controls, e.g. ${exports.IDENTITY_HEADER_ENV}=x-goog-authenticated-user-email.`,
281
+ `NOTE: ${exports.PIN_ONLY_IDENTITY_HEADERS.join(', ')} is NO LONGER trusted by default — it is not stripped by any ` +
282
+ `platform, so an arbitrary client could send it and be namespaced as another user. If your proxy does set it ` +
283
+ `(the Caddy forward_auth / auth-broker flow does), pin it: ${exports.IDENTITY_HEADER_ENV}=x-agent-user.`,
284
+ ];
285
+ }
191
286
  /**
192
287
  * Resolve the end-user identity for a request.
193
288
  *
@@ -197,7 +292,10 @@ exports.IDENTITY_HEADER_ENV = 'AGENTHIPPO_IDENTITY_HEADER';
197
292
  * that pins a header has declared it has a real proxy-verified identity, so an
198
293
  * unauthenticated caller must not be able to name themselves via the client-
199
294
  * controlled body. Body fallback is dev/internal convenience for the *unpinned*
200
- * case only (e.g. direct curl against a local serve with no identity proxy).
295
+ * case only (e.g. direct curl against a local serve with no identity proxy) —
296
+ * and it must be OPTED INTO via AGENTHIPPO_ALLOW_BODY_IDENTITY=1 (serve-v4 §10
297
+ * hole 3). Before the gate it was safe "by convention": any deployment that
298
+ * forgot to pin a header silently let every caller name an arbitrary victim.
201
299
  *
202
300
  * The alias-list default (used when unpinned) is only safe when the proxy strips
203
301
  * or overwrites ALL of the aliases; platforms that strip only their own header
@@ -232,6 +330,10 @@ function resolveUserId(headers, body, env = process.env) {
232
330
  // access, gcloud run services proxy bypassing IAP) name an arbitrary victim.
233
331
  return undefined;
234
332
  }
333
+ // Unpinned body identity is an explicit opt-in, never a default (§10 hole 3).
334
+ if (env.AGENTHIPPO_ALLOW_BODY_IDENTITY?.trim() !== '1') {
335
+ return undefined;
336
+ }
235
337
  const fromBody = body?.user_id?.trim();
236
338
  return fromBody || undefined;
237
339
  }
@@ -275,6 +377,36 @@ function peerToUserId(peer) {
275
377
  const channel = peer.channel?.trim();
276
378
  return channel ? `${channel}:${id}` : id;
277
379
  }
380
+ /** Stable reason code when a session_id is presented without a verified user on a pinned-identity deployment. */
381
+ exports.SESSION_WITHOUT_USER_REASON = 'session_without_verified_user';
382
+ /** Filesystem/S3-safe per-user namespace segment (user ids are emails/subjects — never raw path segments). */
383
+ function userNamespaceHash(userId) {
384
+ return crypto.createHash('sha256').update(userId).digest('hex').slice(0, 16);
385
+ }
386
+ /**
387
+ * Namespace the session key by the verified user (secure-obo-design §3.4 / §8 Phase 0).
388
+ *
389
+ * `session_id` is client-supplied; un-namespaced, any authenticated user who guesses another
390
+ * user's session_id attaches to that session's conversation state (which holds the other
391
+ * user's row-filtered query results). Prefixing the key with a hash of the verified user makes
392
+ * cross-user attachment structurally impossible without changing client behavior.
393
+ *
394
+ * Fail-closed rule: when the deployment pins an identity header (it declared a real
395
+ * proxy-verified identity exists) and a request carries a session_id but no verified user,
396
+ * reject — never let an unauthenticated caller name a session. Unpinned deployments
397
+ * (local dev, no identity boundary at all) keep today's raw-key behavior.
398
+ */
399
+ function resolveServeSessionKey(sessionId, userId, env = process.env) {
400
+ const sid = sessionId?.trim();
401
+ const uid = userId?.trim();
402
+ if (uid) {
403
+ return { sessionKey: `u${userNamespaceHash(uid)}--${sid || 'default'}` };
404
+ }
405
+ if (sid && env[exports.IDENTITY_HEADER_ENV]?.trim()) {
406
+ return { sessionKey: '', rejected: exports.SESSION_WITHOUT_USER_REASON };
407
+ }
408
+ return { sessionKey: sid || 'anon-unknown' };
409
+ }
278
410
  /** Build CORS headers. Serve is always behind a proxy; CORS is wide-open here. */
279
411
  function corsHeaders() {
280
412
  return {
@@ -320,6 +452,10 @@ exports.TURN_QUEUE_ENV = 'AGENTHIPPO_TURN_QUEUE';
320
452
  exports.TURN_GATE_RETRY_AFTER_SECONDS = 15;
321
453
  /** User-visible capacity message for gateway channels (Slack/Telegram can't receive HTTP 429). */
322
454
  exports.TURN_GATE_BUSY_MESSAGE = 'At capacity — please retry in a moment';
455
+ /** User-visible message for gateway channels when the per-identity RateLimiter rejects. */
456
+ exports.RATE_LIMIT_BUSY_MESSAGE = 'Rate limit exceeded — please retry in a moment';
457
+ /** User-visible message for gateway channels when the per-session TurnLimiter rejects. */
458
+ exports.TURN_LIMIT_BUSY_MESSAGE = 'Per-session turn limit reached';
323
459
  /**
324
460
  * Process-wide concurrency gate for engine turns (semaphore + bounded FIFO).
325
461
  *
@@ -216,17 +216,20 @@ async function pathExists(p) {
216
216
  return false;
217
217
  }
218
218
  }
219
- async function copyDirIfExists(src, dest) {
220
- if (!(await pathExists(src))) {
221
- return;
222
- }
223
- await fsp.mkdir(dest, { recursive: true });
224
- await fsp.cp(src, dest, { recursive: true, force: true });
225
- }
226
219
  /**
227
- * Build a local staging tree for session S:
228
- * provider-sessions/<shards for S>
229
- * agents/ (replica snapshot — sticky-by-user keeps this scoped)
220
+ * Build a local staging tree for session S: provider-sessions/<shards for S> only.
221
+ *
222
+ * `<stateDir>/agents/<agentId>/{claude,codex}` (the SDK's own on-disk session/config
223
+ * cache — see agentpack/paths.ts) is deliberately NOT included here. That directory is
224
+ * shared by every session/user of the same pack on this replica, keyed only by agentId —
225
+ * never session-scoped. Treating it as part of one session's push/restore let one
226
+ * session's stale snapshot force-overwrite the live SDK cache for every other
227
+ * concurrently active session on the replica (fs.cp force:true in the old
228
+ * mergeSessionStagingIntoState). It's local-disk-only now, same as before object-store
229
+ * sync existed: it lives and dies with its replica. A resume target missing after a
230
+ * replica change already falls back to a fresh SDK session — the same path every
231
+ * session's true first turn on any replica already takes — so this doesn't introduce a
232
+ * new failure mode, it just stops the wrong-session-data clobber.
230
233
  */
231
234
  async function stageSessionPushDir(stateDir, sessionId, stagingDir) {
232
235
  await fsp.rm(stagingDir, { recursive: true, force: true });
@@ -239,11 +242,10 @@ async function stageSessionPushDir(stateDir, sessionId, stagingDir) {
239
242
  await fsp.copyFile(path.join(stateDir, 'provider-sessions', name), path.join(psDest, name));
240
243
  }
241
244
  }
242
- await copyDirIfExists(path.join(stateDir, 'agents'), path.join(stagingDir, 'agents'));
243
- const hasAgents = await pathExists(path.join(stagingDir, 'agents'));
244
- return shards.length > 0 || hasAgents;
245
+ return shards.length > 0;
245
246
  }
246
- /** Merge a restored staging tree back into stateDir. */
247
+ /** Merge a restored staging tree back into stateDir (provider-session shards only — see
248
+ * stageSessionPushDir for why the SDK config cache is intentionally excluded). */
247
249
  async function mergeSessionStagingIntoState(stagingDir, stateDir) {
248
250
  const psSrc = path.join(stagingDir, 'provider-sessions');
249
251
  if (await pathExists(psSrc)) {
@@ -256,7 +258,6 @@ async function mergeSessionStagingIntoState(stagingDir, stateDir) {
256
258
  await fsp.copyFile(path.join(psSrc, name), path.join(psDest, name));
257
259
  }
258
260
  }
259
- await copyDirIfExists(path.join(stagingDir, 'agents'), path.join(stateDir, 'agents'));
260
261
  }
261
262
  /** Download state from the bucket into stateDir on boot. An empty bucket is a no-op. */
262
263
  async function restoreState(cfg, stateDir, log) {
package/out/cli/utils.js CHANGED
@@ -77,6 +77,13 @@ function findClaudeCodePath() {
77
77
  if (depsPath) {
78
78
  return depsPath;
79
79
  }
80
+ // 2b. Our OWN dependency tree — the case where the runtime ships as a node app with the
81
+ // SDK already in node_modules. Without this, such a deployment fails with "Claude Code CLI
82
+ // not found" while the SDK sits right there (hit on the Hanoi VM, 2026-07-27).
83
+ const ownCli = (0, deps_1.resolveFromOwnDeps)('@anthropic-ai/claude-agent-sdk', 'cli.js');
84
+ if (ownCli) {
85
+ return ownCli;
86
+ }
80
87
  // 3. Check bundled location (next to binary) - legacy fallback
81
88
  const binaryDir = getBinaryDir();
82
89
  const bundledPath = path.join(binaryDir, 'claude-sdk', 'cli.js');
@@ -131,6 +138,10 @@ function findCodexPath() {
131
138
  return process.env.CODEX_PATH;
132
139
  }
133
140
  // 2. Check npm-installed location (.agenthippo-deps/node_modules)
141
+ const ownCodex = (0, deps_1.resolveFromOwnDeps)('@openai/codex', 'bin/codex.js');
142
+ if (ownCodex) {
143
+ return ownCodex;
144
+ }
134
145
  const depsPath = (0, deps_1.getCodexSdkBinaryPath)();
135
146
  if (depsPath) {
136
147
  return depsPath;
@@ -211,7 +222,7 @@ exports.COLORS = {
211
222
  red: '\x1b[31m',
212
223
  yellow: '\x1b[33m',
213
224
  };
214
- exports.SUGGESTED_LITELLM_SMOKE_TEST_COMMAND = 'AGENTHIPPO_LITELLM_BASE_URL="http://localhost:4000" AGENTHIPPO_LITELLM_API_KEY="dummy" AGENTHIPPO_DEFAULT_MODEL="litellm/gpt-5.3-codex" AGENTHIPPO_DEFAULT_ENGINE="codex" agenthippo ask "Reply with exactly: CLI_TEST_OK"';
225
+ exports.SUGGESTED_LITELLM_SMOKE_TEST_COMMAND = 'AGENTHIPPO_LITELLM_BASE_URL="http://localhost:4000" AGENTHIPPO_LITELLM_API_KEY="dummy" AGENTHIPPO_DEFAULT_MODEL="litellm/gpt-5.5" AGENTHIPPO_DEFAULT_ENGINE="codex" agenthippo ask "Reply with exactly: CLI_TEST_OK"';
215
226
  function getLiteLLMQuickstartHintLines() {
216
227
  return [
217
228
  'Try local LiteLLM quickstart:',
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  /*---------------------------------------------------------------------------------------------
3
3
  * Copyright (c) AgentHippo.ai. All rights reserved.
4
+ * Licensed under the MIT License. See LICENSE.txt in the project root for license information.
4
5
  *--------------------------------------------------------------------------------------------*/
5
6
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
7
  if (k2 === undefined) k2 = k;
@@ -42,6 +43,7 @@ exports.generateRequestId = generateRequestId;
42
43
  exports.persistSessionIfNeeded = persistSessionIfNeeded;
43
44
  exports.touchSessionIfNeeded = touchSessionIfNeeded;
44
45
  exports.getPersistedSessionId = getPersistedSessionId;
46
+ const identity_1 = require("../analytics/identity");
45
47
  const session_1 = require("../session");
46
48
  const path = __importStar(require("path"));
47
49
  /**
@@ -54,16 +56,26 @@ const path = __importStar(require("path"));
54
56
  * Langfuse passthrough: langfuse_session_id / langfuse_trace_user_id headers
55
57
  * are included so users who add Langfuse as a LiteLLM callback get session
56
58
  * grouping automatically.
59
+ *
60
+ * IDENTITY: the user id is a salted hash (see analytics/identity.ts). It used to be the raw
61
+ * value — usually an email — sent on two headers to a proxy that logs every request and forwards
62
+ * to a third-party observability backend. The hash is stable, so grouping and per-user analytics
63
+ * still work; it is just no longer a directory of who works here. Set
64
+ * AGENTHIPPO_EVIDENCE_RAW_IDS=1 to send the raw value again.
65
+ *
66
+ * The provider never sees either form: `additional_drop_params: ["user"]` in the LiteLLM config
67
+ * strips the end-user field before dispatch.
57
68
  */
58
69
  function buildLiteLLMMetadataHeaders(useLiteLLM, metadata) {
59
70
  if (!useLiteLLM) {
60
71
  return [];
61
72
  }
62
73
  const headers = [];
63
- if (metadata.analyticsUserId?.trim()) {
64
- headers.push(`x-litellm-metadata-user-id: ${metadata.analyticsUserId.trim()}`);
74
+ const userId = (0, identity_1.hashIdentity)(metadata.analyticsUserId);
75
+ if (userId) {
76
+ headers.push(`x-litellm-metadata-user-id: ${userId}`);
65
77
  // Langfuse passthrough: maps to Langfuse trace user_id when Langfuse callback is enabled
66
- headers.push(`langfuse_trace_user_id: ${metadata.analyticsUserId.trim()}`);
78
+ headers.push(`langfuse_trace_user_id: ${userId}`);
67
79
  }
68
80
  if (metadata.sessionId?.trim()) {
69
81
  headers.push(`x-litellm-metadata-session-id: ${metadata.sessionId.trim()}`);
@@ -88,6 +100,23 @@ function buildLiteLLMMetadataHeaders(useLiteLLM, metadata) {
88
100
  if (metadata.requestId?.trim()) {
89
101
  headers.push(`x-litellm-metadata-request-id: ${metadata.requestId.trim()}`);
90
102
  }
103
+ // Provenance stamp (PLAN/compliance.md §3.1): lands in requester_metadata on every OTEL
104
+ // span, which is where the evidence index reads policy_version / pack_digest from.
105
+ if (metadata.policyVersion?.trim()) {
106
+ headers.push(`x-litellm-metadata-policy-version: ${metadata.policyVersion.trim()}`);
107
+ }
108
+ if (metadata.packDigest?.trim()) {
109
+ headers.push(`x-litellm-metadata-pack-digest: ${metadata.packDigest.trim()}`);
110
+ }
111
+ if (metadata.engineVersion?.trim()) {
112
+ headers.push(`x-litellm-metadata-engine-version: ${metadata.engineVersion.trim()}`);
113
+ }
114
+ if (metadata.tier !== undefined) {
115
+ headers.push(`x-litellm-metadata-tier: ${metadata.tier}`);
116
+ }
117
+ if (metadata.instanceId?.trim()) {
118
+ headers.push(`x-litellm-metadata-instance-id: ${metadata.instanceId.trim()}`);
119
+ }
91
120
  return headers;
92
121
  }
93
122
  /**
@@ -39,12 +39,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.ClaudeEngine = void 0;
40
40
  const fs_1 = require("fs");
41
41
  const path = __importStar(require("path"));
42
+ const branding_1 = require("../branding");
42
43
  const base_1 = require("./base");
43
44
  const utils_1 = require("../utils");
44
45
  const permissions_1 = require("../permissions");
45
46
  const mcp_1 = require("../mcp");
47
+ const runtime_1 = require("../cli/runtime");
46
48
  const session_1 = require("../session");
47
49
  const agentpack_1 = require("../agentpack");
50
+ const credential_context_1 = require("../cli/credential-context");
48
51
  const shared_1 = require("../shared");
49
52
  // File tools we track for edit diffs via hooks (Set for O(1) lookup)
50
53
  // Includes Claude SDK tools, Computer Use tools, and common variations
@@ -241,7 +244,8 @@ class ClaudeEngine {
241
244
  // Edit tracker from runtime context (optional)
242
245
  const editTracker = runtime.editTracker;
243
246
  // Resolve API routing (LiteLLM proxy vs direct Anthropic)
244
- const { apiKey, baseUrl, useLiteLLM } = (0, utils_1.resolveApiRouting)(options, this.config.litellmApiKey, this.config.litellmBaseUrl);
247
+ // claude routes to Anthropic when direct (or the LiteLLM proxy when configured) — never OpenAI.
248
+ const { apiKey, baseUrl, useLiteLLM } = (0, utils_1.resolveApiRouting)(options, this.config, 'anthropic');
245
249
  if (!apiKey) {
246
250
  throw new Error('Claude API key not provided');
247
251
  }
@@ -258,6 +262,11 @@ class ClaudeEngine {
258
262
  source: 'agenthippo',
259
263
  engine: 'claude',
260
264
  requestId: options.requestId,
265
+ policyVersion: options.policyVersion,
266
+ packDigest: options.packDigest,
267
+ instanceId: options.instanceId,
268
+ engineVersion: options.engineVersion,
269
+ tier: options.tier,
261
270
  });
262
271
  // Build environment for Claude SDK.
263
272
  // Setup-token auth must use Claude's OAuth token env var instead of ANTHROPIC_API_KEY.
@@ -267,6 +276,13 @@ class ClaudeEngine {
267
276
  'DEBUG_CLAUDE_AGENT_SDK': '1',
268
277
  'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC': '1',
269
278
  };
279
+ // The gateway token stays broker-side; scrub it from the child env so it never reaches
280
+ // the model (secure-obo-design §7.6 / §7.4 gap 1). UNCONDITIONALLY (serve-v4 §10 hole 2):
281
+ // gating this on the egress broker let a deployment without --gateway-bridge re-inherit
282
+ // the token, including via buildCliEnvPatch's bake from the host's ~/.openclaw config.
283
+ delete claudeEnv[runtime_1.OPENCLAW_GATEWAY_TOKEN_ENV];
284
+ // Data-plane sidecar control token: never the engine's to hold (secure-obo-design §7.6).
285
+ delete claudeEnv['AGENTHIPPO_DATA_PLANE_TOKEN'];
270
286
  const isSetupTokenAuth = (0, shared_1.isAnthropicSetupToken)(apiKey);
271
287
  if (isSetupTokenAuth) {
272
288
  const normalizedSetupToken = (0, shared_1.normalizeAnthropicSetupToken)(apiKey);
@@ -295,8 +311,13 @@ class ClaudeEngine {
295
311
  // Note: cwd is ALWAYS workspaceRoot - active agent content is synced to workspace's .claude/.codex
296
312
  const agentId = options.agentPackId ?? 'default';
297
313
  const runtimePaths = (0, agentpack_1.resolveRuntimePaths)(options.workspaceRoot, this.config.storagePath, agentId, options.agentPackVersion);
298
- // Claude config directory (isolated per agent for session/state isolation)
299
- const claudeConfigDir = runtimePaths.claudeConfigDir;
314
+ // Claude config directory (isolated per agent for session/state isolation). In
315
+ // multi-tenant serve, additionally key it by the verified request user — the same
316
+ // §8 Phase 0 scoping codex.ts applies to CODEX_HOME. A shared per-agent dir lets
317
+ // concurrent turns of different users race each other's per-turn credential state
318
+ // (the agentpack/paths.ts race); per-user scoping makes any remaining race
319
+ // same-user. No credential frame (IDE, single-tenant CLI) ⇒ unchanged path.
320
+ const claudeConfigDir = (0, agentpack_1.userScopedEngineHome)(runtimePaths.claudeConfigDir, (0, credential_context_1.currentCredential)()?.userId);
300
321
  claudeEnv['CLAUDE_CONFIG_DIR'] = claudeConfigDir;
301
322
  await fs_1.promises.mkdir(claudeConfigDir, { recursive: true });
302
323
  // Always set AGENT_ID for agent to know its identity
@@ -541,8 +562,11 @@ class ClaudeEngine {
541
562
  logger.debug(`[AgentIDE] SessionStart hook called! event=${input.hook_event_name}`);
542
563
  return CONTINUE;
543
564
  };
544
- // Load MCP servers for the active agent and pass directly to SDK
545
- const mcpServers = await this.loadMcpServersForClaude(logger, options.workspaceRoot, agentId);
565
+ // Load MCP servers for the active agent and pass directly to SDK. URL servers carry a
566
+ // `${NAME}` bearer reference the CLI expands from claudeEnv at connect time, so the
567
+ // turn nonce rides neither a shared config file nor the SDK's --mcp-config argv —
568
+ // claudeEnv is passed for the rare non-expandable-name fallback only.
569
+ const mcpServers = await this.loadMcpServersForClaude(logger, options.workspaceRoot, agentId, claudeEnv);
546
570
  // Determine cwd and settingSources
547
571
  // cwd is ALWAYS workspaceRoot - active agent content has been synced to workspace's .claude/
548
572
  // Agent packs: settingSources is 'project' only (no user ~/.claude/ mixing)
@@ -1058,7 +1082,7 @@ class ClaudeEngine {
1058
1082
  logger.info(`[AgentIDE] Claude thinking summary: startBlocks=${thinkingStartBlockCount}, redactedBlocks=${redactedThinkingBlockCount}, deltas=${thinkingDeltaCount}, chars=${thinkingCharCount}`);
1059
1083
  logger.info(`[AgentIDE] Claude tool summary: toolBlocks=${toolUseBlockCount}, toolEmits=${toolUseEmitCount}, toolProgressMsgs=${toolProgressCount}, inputJsonDeltas=${inputJsonDeltaCount}`);
1060
1084
  if (thinkingStartBlockCount === 0 && thinkingDeltaCount === 0 && redactedThinkingBlockCount === 0) {
1061
- logger.debug('[AgentIDE] Claude emitted no thinking blocks/deltas. Ensure model supports extended thinking and set agentide.claude.maxThinkingTokens >= 1024.');
1085
+ logger.debug(`[AgentIDE] Claude emitted no thinking blocks/deltas. Ensure model supports extended thinking and set ${(0, branding_1.toSettingsDisplayKey)('agentide.claude.maxThinkingTokens')} >= 1024.`);
1062
1086
  }
1063
1087
  if (toolUseBlockCount === 0 && toolProgressCount === 0) {
1064
1088
  logger.debug('[AgentIDE] Claude emitted no tool usage events. If tools were used, ensure includePartialMessages=true and parse tool_use blocks.');
@@ -1191,9 +1215,15 @@ class ClaudeEngine {
1191
1215
  * - Default agent: uses bundled MCP servers from extension
1192
1216
  * - Pack agents: uses pack's mcp/ directory
1193
1217
  *
1194
- * The SDK accepts mcpServers directly in query options.
1218
+ * The SDK accepts mcpServers directly in query options — including HTTP Streamable
1219
+ * servers (`{type:'http', url, headers}`). A URL server's `bearerTokenEnv` (e.g.
1220
+ * AGENTHIPPO_TURN_NONCE) becomes a `${NAME}` reference the CLI expands from its own env,
1221
+ * NOT a resolved header: the SDK serializes mcpServers into `--mcp-config` argv, and a
1222
+ * resolved bearer there is world-readable via /proc/<pid>/cmdline (see
1223
+ * referenceHttpMcpHeaders). `turnEnv` is the per-turn child env, used only for the
1224
+ * fallback when a name is outside the CLI's expansion grammar.
1195
1225
  */
1196
- async loadMcpServersForClaude(logger, workspaceRoot, agentId = 'default') {
1226
+ async loadMcpServersForClaude(logger, workspaceRoot, agentId = 'default', turnEnv) {
1197
1227
  const basePromptsDir = this.getBundledBasePromptsDir();
1198
1228
  const servers = await (0, mcp_1.loadMcpServers)(agentId, workspaceRoot, basePromptsDir);
1199
1229
  if (servers.length === 0) {
@@ -1201,6 +1231,15 @@ class ClaudeEngine {
1201
1231
  }
1202
1232
  const mcpServers = {};
1203
1233
  for (const s of servers) {
1234
+ if (s.url) {
1235
+ const headers = (0, mcp_1.referenceHttpMcpHeaders)(s, turnEnv ?? process.env);
1236
+ mcpServers[s.name] = {
1237
+ type: 'http',
1238
+ url: s.url,
1239
+ ...(headers ? { headers } : {}),
1240
+ };
1241
+ continue;
1242
+ }
1204
1243
  mcpServers[s.name] = {
1205
1244
  command: s.command,
1206
1245
  args: s.args,