@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.
- package/out/agentpack/index.js +2 -1
- package/out/agentpack/paths.js +23 -0
- package/out/analytics/dashboard/fleetDisplayUtils.js +3 -0
- package/out/analytics/dashboard/provider.js +13 -2
- package/out/analytics/dashboard/webview/panel.js +293 -82
- package/out/analytics/identity.js +0 -0
- package/out/analytics/service.js +23 -25
- package/out/analytics/sessionEvents.js +109 -22
- package/out/auth/authBaseUrl.js +59 -0
- package/out/auth/bootstrapApply.js +9 -10
- package/out/auth/brokerClient.js +49 -15
- package/out/auth/onboardingPrompt.js +112 -0
- package/out/auth/provider.js +9 -22
- package/out/auth/sessionStore.js +3 -40
- package/out/auth/signInFlow.js +2 -1
- package/out/auth-broker/config.js +54 -4
- package/out/auth-broker/server.js +152 -20
- package/out/auth-broker/store.js +25 -24
- package/out/authority/warrant.js +206 -0
- package/out/branding.js +27 -1
- package/out/cli/commands/ask.js +3 -0
- package/out/cli/commands/chat.js +3 -0
- package/out/cli/commands/conformance.js +249 -0
- package/out/cli/commands/data-plane.js +67 -0
- package/out/cli/commands/deploy.js +50 -10
- package/out/cli/commands/gateway.js +58 -0
- package/out/cli/commands/idp.js +85 -0
- package/out/cli/commands/pack.js +182 -3
- package/out/cli/commands/sandbox.js +332 -0
- package/out/cli/commands/serve.js +1165 -82
- package/out/cli/commands/store.js +12 -4
- package/out/cli/credential-context.js +42 -80
- package/out/cli/data-plane/broker.js +78 -0
- package/out/cli/data-plane/connector-host.js +121 -0
- package/out/cli/data-plane/egress.js +189 -0
- package/out/cli/data-plane/gateway.js +218 -0
- package/out/cli/data-plane/index.js +152 -0
- package/out/cli/data-plane/registry.js +89 -0
- package/out/cli/data-plane/remote.js +106 -0
- package/out/cli/data-plane/sidecar.js +161 -0
- package/out/cli/deploy/blueprints/registry.js +84 -0
- package/out/cli/deploy/layers/identity.js +172 -0
- package/out/cli/deploy-init.js +1182 -347
- package/out/cli/deps.js +106 -2
- package/out/cli/idp-init.js +338 -0
- package/out/cli/index.js +192 -23
- package/out/cli/pack-install-ref.js +15 -0
- package/out/cli/pack-loader.js +45 -3
- package/out/cli/pack-runtime.js +61 -0
- package/out/cli/runtime.js +44 -3
- package/out/cli/serve-hardening.js +139 -3
- package/out/cli/state-sync.js +16 -15
- package/out/cli/utils.js +12 -1
- package/out/engines/base.js +32 -3
- package/out/engines/claude.js +47 -8
- package/out/engines/codex.js +125 -22
- package/out/engines/custom.js +54 -12
- package/out/evidence/sessionShip.js +610 -0
- package/out/extension.js +13 -2
- package/out/gateway/bridge.js +40 -2
- package/out/gateway/client.js +34 -1
- package/out/gateway/installSource.js +68 -0
- package/out/gateway/lifecycle.js +185 -51
- package/out/gateway/outboundPaths.js +4 -1
- package/out/gateway/outboundSend.js +6 -6
- package/out/gateway/secretsPreflight.js +90 -0
- package/out/gateway/service.js +7 -2
- package/out/headless/index.js +40 -9
- package/out/live-e2e/mcp-demo/server.mjs +32 -0
- package/out/live-probe/connector.mjs +8 -0
- package/out/live-probe/env-report.mjs +8 -0
- package/out/mcp.js +115 -67
- package/out/pack/caps.js +271 -0
- package/out/pack/governor.js +309 -0
- package/out/pack/lease-schema.js +270 -0
- package/out/pack/lease.js +640 -0
- package/out/pack/reconcile.js +217 -0
- package/out/pack/signature.js +839 -0
- package/out/remote/handleRemoteChat.js +44 -3
- package/out/remote/remoteTrust.js +277 -0
- package/out/remote/serveClient.js +52 -5
- package/out/runner/host.js +131 -0
- package/out/runner/protocol.js +108 -0
- package/out/runner/remoteEngine.js +130 -0
- package/out/runtime/interfaces/edit-tracker.js +2 -0
- package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
- package/out/sandbox/containerDriver.js +570 -0
- package/out/sandbox/processDriver.js +315 -0
- package/out/sandbox/sandboxRunner.js +191 -0
- package/out/sandbox/support.js +301 -0
- package/out/sandbox/types.js +55 -0
- package/out/scripts/agentide-litellm.sh +67 -5
- package/out/scripts/agentide_litellm_hooks_v1.py +182 -21
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +94 -118
- package/out/scripts/analytics/setup-analytics.ps1 +37 -13
- package/out/scripts/analytics/setup-analytics.sh +93 -13
- package/out/scripts/analytics/start-analytics.sh +86 -14
- package/out/scripts/gateway/setup-gateway.sh +72 -32
- package/out/shared/models.js +12 -111
- package/out/shared/providers.js +2 -2
- package/out/spotlight/install.js +7 -1
- package/out/store/index.js +41 -10
- package/out/sync.js +3 -1
- package/out/test/analytics.test.js +198 -34
- package/out/test/auth-baseurl.test.js +132 -0
- package/out/test/auth-bootstrapApply.test.js +27 -6
- package/out/test/auth-broker-hardening.test.js +227 -0
- package/out/test/auth-broker.test.js +112 -9
- package/out/test/blueprint-registry.test.js +105 -0
- package/out/test/branding.test.js +13 -0
- package/out/test/caps-governor.test.js +455 -0
- package/out/test/codex-new-file-survival.test.js +119 -0
- package/out/test/configuration-scopes.test.js +131 -0
- package/out/test/credential-context.test.js +23 -28
- package/out/test/custom-env-layering.test.js +151 -0
- package/out/test/data-plane-engine-wiring.test.js +250 -0
- package/out/test/data-plane.test.js +311 -0
- package/out/test/deploy-init.test.js +295 -4
- package/out/test/egress-broker.test.js +172 -0
- package/out/test/fleetDisplayUtils.test.js +4 -0
- package/out/test/gateway-client.test.js +28 -0
- package/out/test/gateway-install-source.test.js +111 -0
- package/out/test/gateway-lifecycle.test.js +1 -1
- package/out/test/gateway-outbound.test.js +7 -0
- package/out/test/gateway-secrets-preflight.test.js +127 -0
- package/out/test/gateway-session-routing.test.js +18 -0
- package/out/test/governor-hardening.test.js +217 -0
- package/out/test/headless-sync.test.js +22 -0
- package/out/test/idp-init.test.js +151 -0
- package/out/test/lease-signature.test.js +357 -0
- package/out/test/lease-trust-root.test.js +273 -0
- package/out/test/lease-validation.test.js +169 -0
- package/out/test/lease.test.js +114 -0
- package/out/test/mcp-obo.test.js +52 -27
- package/out/test/mcp.test.js +69 -3
- package/out/test/model-routing.test.js +133 -0
- package/out/test/pack-admission.test.js +216 -0
- package/out/test/pack-digest-parity.test.js +140 -0
- package/out/test/pack-reconcile.test.js +183 -0
- package/out/test/pack-signature-artifact.test.js +103 -0
- package/out/test/pack-signature.test.js +176 -0
- package/out/test/pack-trust-root.test.js +202 -0
- package/out/test/remote-trust.test.js +242 -0
- package/out/test/runner-protocol.test.js +249 -0
- package/out/test/sandbox-driver.test.js +553 -0
- package/out/test/sandbox-hardening.test.js +395 -0
- package/out/test/sandbox-runner.test.js +343 -0
- package/out/test/serve-gateway-turn.test.js +267 -0
- package/out/test/serve-hardening.test.js +125 -6
- package/out/test/serveClient.test.js +38 -5
- package/out/test/sessionShip.test.js +560 -0
- package/out/test/spotlightPublish.test.js +28 -0
- package/out/test/state-sync-peer.test.js +18 -5
- package/out/test/store-install-safe.test.js +69 -0
- package/out/test/tls-bypass.test.js +90 -0
- package/out/test/warrant.test.js +138 -0
- package/out/utils.js +73 -20
- 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
|
*
|
package/out/cli/state-sync.js
CHANGED
|
@@ -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
|
-
*
|
|
229
|
-
*
|
|
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
|
-
|
|
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.
|
|
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:',
|
package/out/engines/base.js
CHANGED
|
@@ -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
|
-
|
|
64
|
-
|
|
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: ${
|
|
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
|
/**
|
package/out/engines/claude.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|