@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.
- package/out/agentpack/index.js +3 -1
- package/out/agentpack/paths.js +25 -2
- package/out/analytics/dashboard/localFleetAdapter.js +82 -2
- package/out/analytics/dashboard/provider.js +13 -2
- package/out/analytics/dashboard/webview/panel.js +3 -2
- package/out/analytics/identity.js +0 -0
- package/out/analytics/service.js +29 -25
- package/out/analytics/sessionEvents.js +100 -24
- package/out/auth/authBaseUrl.js +59 -0
- package/out/auth/bootstrapApply.js +9 -10
- package/out/auth/brokerClient.js +48 -14
- package/out/auth/provider.js +8 -21
- package/out/auth/sessionStore.js +3 -40
- package/out/auth/signInFlow.js +2 -1
- package/out/auth-broker/config.js +50 -5
- package/out/auth-broker/server.js +126 -26
- 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/conformance.js +249 -0
- package/out/cli/commands/data-plane.js +151 -0
- package/out/cli/commands/deploy.js +1 -0
- package/out/cli/commands/gateway.js +4 -0
- package/out/cli/commands/pack.js +182 -3
- package/out/cli/commands/sandbox.js +351 -0
- package/out/cli/commands/serve.js +1145 -130
- package/out/cli/commands/store.js +12 -4
- package/out/cli/credential-context.js +17 -0
- package/out/cli/data-plane/gateway.js +28 -5
- package/out/cli/data-plane/index.js +18 -2
- package/out/cli/data-plane/registry.js +9 -0
- package/out/cli/data-plane/remote.js +106 -0
- package/out/cli/data-plane/sidecar.js +225 -0
- package/out/cli/deploy/layers/identity.js +88 -4
- package/out/cli/deploy-init.js +89 -24
- package/out/cli/deps.js +106 -2
- package/out/cli/idp-init.js +35 -0
- package/out/cli/index.js +171 -17
- package/out/cli/pack-install-ref.js +15 -0
- package/out/cli/pack-loader.js +8 -1
- package/out/cli/pack-runtime.js +61 -0
- package/out/cli/runtime.js +35 -1
- package/out/cli/serve-hardening.js +260 -14
- package/out/cli/utils.js +12 -1
- package/out/engines/base.js +32 -3
- package/out/engines/claude.js +33 -15
- package/out/engines/codex.js +111 -27
- package/out/engines/custom.js +15 -1
- package/out/evidence/sessionShip.js +643 -0
- package/out/extension.js +11 -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/secretsPreflight.js +90 -0
- package/out/gateway/service.js +7 -2
- package/out/headless/index.js +25 -2
- package/out/live-e2e/mcp-demo/server.mjs +32 -0
- package/out/mcp.js +30 -0
- package/out/pack/caps.js +271 -0
- package/out/pack/governor.js +326 -0
- package/out/pack/lease-schema.js +273 -0
- package/out/pack/lease.js +682 -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 +141 -0
- package/out/runner/protocol.js +128 -0
- package/out/runner/remoteEngine.js +166 -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 +572 -0
- package/out/sandbox/processDriver.js +318 -0
- package/out/sandbox/sandboxRunner.js +303 -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 +266 -21
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +98 -132
- package/out/scripts/analytics/setup-analytics.ps1 +45 -14
- package/out/scripts/analytics/setup-analytics.sh +106 -15
- package/out/scripts/analytics/start-analytics.sh +86 -14
- package/out/scripts/gateway/setup-gateway.sh +72 -32
- package/out/shared/models.js +11 -110
- 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 +162 -23
- 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 +59 -9
- package/out/test/branding.test.js +13 -0
- package/out/test/caps-governor.test.js +460 -0
- package/out/test/codex-new-file-survival.test.js +119 -0
- package/out/test/configuration-scopes.test.js +131 -0
- package/out/test/data-plane-engine-wiring.test.js +32 -0
- package/out/test/data-plane.test.js +380 -0
- package/out/test/deploy-init.test.js +47 -1
- 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 +222 -0
- package/out/test/headless-sync.test.js +22 -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 +224 -0
- package/out/test/lease.test.js +114 -0
- package/out/test/localFleetAdapter.test.js +91 -0
- package/out/test/mcp.test.js +27 -1
- 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 +406 -0
- package/out/test/sandbox-runner.test.js +343 -0
- package/out/test/sandbox-scope.test.js +449 -0
- package/out/test/serve-cron-acceptance.test.js +370 -0
- package/out/test/serve-gateway-turn.test.js +122 -0
- package/out/test/serve-hardening.test.js +227 -15
- package/out/test/serveClient.test.js +6 -5
- package/out/test/sessionShip.test.js +618 -0
- package/out/test/spotlightPublish.test.js +28 -0
- 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 +46 -9
- package/package.json +1 -1
|
@@ -36,11 +36,16 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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.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.REQUIRE_TENANT_ISOLATION_ENV = exports.IDENTITY_PROXY_SECRET_ENV = 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.SERVICE_USER_ENV = exports.DEFAULT_MAX_BODY_BYTES = void 0;
|
|
40
40
|
exports.resolveStateDir = resolveStateDir;
|
|
41
41
|
exports.resolveAnalyticsSessionsDir = resolveAnalyticsSessionsDir;
|
|
42
|
+
exports.resolveServiceIdentity = resolveServiceIdentity;
|
|
43
|
+
exports.isLoopbackHost = isLoopbackHost;
|
|
42
44
|
exports.resolveBindHost = resolveBindHost;
|
|
43
45
|
exports.clientIp = clientIp;
|
|
46
|
+
exports.hasTrustedProxyProof = hasTrustedProxyProof;
|
|
47
|
+
exports.identityHeaderAdvice = identityHeaderAdvice;
|
|
48
|
+
exports.tenantIsolationAdvice = tenantIsolationAdvice;
|
|
44
49
|
exports.resolveUserId = resolveUserId;
|
|
45
50
|
exports.resolveDataToken = resolveDataToken;
|
|
46
51
|
exports.peerToUserId = peerToUserId;
|
|
@@ -59,12 +64,15 @@ exports.readBodyWithCap = readBodyWithCap;
|
|
|
59
64
|
*
|
|
60
65
|
* SECURITY CONTRACT: serve trusts the identity and data-token headers below unconditionally.
|
|
61
66
|
* It MUST sit behind a proxy that (a) authenticates the caller and sets the identity header,
|
|
62
|
-
* and (b) STRIPS any client-supplied copies of these headers on the way in.
|
|
67
|
+
* and (b) STRIPS any client-supplied copies of these headers on the way in. Deployments where
|
|
68
|
+
* other workloads share the network with serve (compose, sidecar platforms) can additionally
|
|
69
|
+
* require proof that a request came through that proxy — see IDENTITY_PROXY_SECRET_ENV.
|
|
63
70
|
*/
|
|
64
71
|
const os = __importStar(require("os"));
|
|
65
72
|
const path = __importStar(require("path"));
|
|
66
73
|
const crypto = __importStar(require("crypto"));
|
|
67
74
|
const paths_1 = require("../utils/paths");
|
|
75
|
+
const config_1 = require("../auth-broker/config");
|
|
68
76
|
/** Default cap on a single request body (1 MiB). Chat messages are small; large bodies are abuse or bugs. */
|
|
69
77
|
exports.DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
70
78
|
/**
|
|
@@ -107,8 +115,63 @@ function resolveAnalyticsSessionsDir(stateDir, stateDirEphemeral, env = process.
|
|
|
107
115
|
}
|
|
108
116
|
return path.join((0, paths_1.getAgentideHome)(), 'analytics', 'sessions');
|
|
109
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Env: the identity an UNATTENDED turn runs as — a cron- or channel-fired run with no chat peer
|
|
120
|
+
* (PLAN/agent-identity.md §5). Deployment configuration, deliberately not a pack field: it names
|
|
121
|
+
* an identity that exists only in the operator's environment and must match their role map.
|
|
122
|
+
*/
|
|
123
|
+
exports.SERVICE_USER_ENV = 'AGENTHIPPO_SERVICE_USER';
|
|
124
|
+
/** Longest identity we will carry. Generous; the point is to reject a pasted file, not to be tight. */
|
|
125
|
+
const MAX_SERVICE_USER_LEN = 256;
|
|
126
|
+
/**
|
|
127
|
+
* Validate the service identity at boot.
|
|
128
|
+
*
|
|
129
|
+
* **Shape, not vocabulary.** An earlier draft of the design required `^svc:[a-z0-9-]+$` and would
|
|
130
|
+
* have refused to boot on the two handles an enterprise operator is most likely to bind: a SPIFFE
|
|
131
|
+
* ID (`spiffe://corp/ns/agents/invoice`) and a Microsoft Entra agent identity object id. Both are
|
|
132
|
+
* exactly the federated principals §8 wants this to become, so the validator checks that the value
|
|
133
|
+
* is a usable single token and warns — rather than refuses — when it carries no recognisable
|
|
134
|
+
* namespace.
|
|
135
|
+
*
|
|
136
|
+
* Malformed IS a boot refusal though: the alternative is a fleet that looks configured and runs
|
|
137
|
+
* every scheduled job anonymously.
|
|
138
|
+
*/
|
|
139
|
+
function resolveServiceIdentity(env = process.env) {
|
|
140
|
+
const raw = env[exports.SERVICE_USER_ENV];
|
|
141
|
+
if (raw === undefined) {
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
const identity = raw.trim();
|
|
145
|
+
if (!identity) {
|
|
146
|
+
return { error: `${exports.SERVICE_USER_ENV} is set but empty. Unset it, or give it the identity unattended runs should act as.` };
|
|
147
|
+
}
|
|
148
|
+
if (/\s/.test(identity)) {
|
|
149
|
+
return { error: `${exports.SERVICE_USER_ENV} must not contain whitespace, got ${JSON.stringify(raw)}.` };
|
|
150
|
+
}
|
|
151
|
+
if (identity.length > MAX_SERVICE_USER_LEN) {
|
|
152
|
+
return { error: `${exports.SERVICE_USER_ENV} is ${identity.length} chars; the maximum is ${MAX_SERVICE_USER_LEN}.` };
|
|
153
|
+
}
|
|
154
|
+
// Recognised shapes: the local convention, a SPIFFE ID, or a bare GUID (Entra object id).
|
|
155
|
+
const namespaced = /^svc:/.test(identity)
|
|
156
|
+
|| /^spiffe:\/\//.test(identity)
|
|
157
|
+
|| /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identity);
|
|
158
|
+
if (!namespaced) {
|
|
159
|
+
return {
|
|
160
|
+
identity,
|
|
161
|
+
warning: `${exports.SERVICE_USER_ENV}="${identity}" carries no namespace marker (svc: / spiffe:// / GUID). `
|
|
162
|
+
+ 'It will be used verbatim and must be mapped in the connector role map, exactly like a human identity. '
|
|
163
|
+
+ 'If it can collide with a real user id, prefix it with `svc:`.',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return { identity };
|
|
167
|
+
}
|
|
110
168
|
/** Loopback default — safe everywhere a human runs `serve` locally. */
|
|
111
169
|
exports.DEFAULT_BIND_HOST = '127.0.0.1';
|
|
170
|
+
/** True when a bind address is reachable only from this machine. */
|
|
171
|
+
function isLoopbackHost(host) {
|
|
172
|
+
const h = host.trim().toLowerCase().replace(/^\[|\]$/g, '');
|
|
173
|
+
return h === 'localhost' || h === '::1' || /^127\./.test(h);
|
|
174
|
+
}
|
|
112
175
|
/** Env var for the listen address (used by generated deploy Dockerfiles). */
|
|
113
176
|
exports.SERVE_HOST_ENV = 'AGENTHIPPO_SERVE_HOST';
|
|
114
177
|
/** Bind address baked into container deploy artifacts (Docker / Cloud Run sidecars). */
|
|
@@ -204,15 +267,177 @@ class RateLimiter {
|
|
|
204
267
|
}
|
|
205
268
|
}
|
|
206
269
|
exports.RateLimiter = RateLimiter;
|
|
270
|
+
/**
|
|
271
|
+
* Identity headers trusted when NOTHING is pinned.
|
|
272
|
+
*
|
|
273
|
+
* Every entry here is a header that exactly one platform sets AND owns the request path for, so
|
|
274
|
+
* a client-supplied copy cannot reach serve without that platform having been bypassed entirely.
|
|
275
|
+
*
|
|
276
|
+
* `x-agent-user` is deliberately NOT in this list, and this is a breaking change for anything
|
|
277
|
+
* that relied on it being trusted by default. It is AgentHippo's own canonical header — the name
|
|
278
|
+
* an arbitrary client is most likely to guess — it belongs to no platform that strips it, and it
|
|
279
|
+
* used to sit FIRST, so it beat every platform header that follows. The consequence on any
|
|
280
|
+
* deployment that had not pinned `AGENTHIPPO_IDENTITY_HEADER`: any HTTP client could send
|
|
281
|
+
* `x-agent-user: victim@example.com` and be namespaced into that user's session state, attach to
|
|
282
|
+
* their conversation, and be issued a data-plane turn nonce scoped to them. Cloud Run IAP makes
|
|
283
|
+
* this concrete — it strips `X-Goog-*` and forwards a client-supplied `x-agent-user` untouched.
|
|
284
|
+
*
|
|
285
|
+
* It remains perfectly valid as a PINNED header (`AGENTHIPPO_IDENTITY_HEADER=x-agent-user`),
|
|
286
|
+
* which is what a deployment whose proxy really does set and strip it should use — including the
|
|
287
|
+
* Caddy `forward_auth` flow, where `auth-broker/server.ts` sets `X-Agent-User` on `/api/ide/me`
|
|
288
|
+
* so Caddy can copy a VALIDATED identity upstream. That direction is the correct and only source
|
|
289
|
+
* of the header, and it is unaffected: pinning is exactly the declaration that it is trustworthy.
|
|
290
|
+
*/
|
|
207
291
|
const HEADER_ALIASES = [
|
|
208
|
-
'x-agent-user', // AgentHippo canonical (set by our Worker / generic proxy)
|
|
209
292
|
'x-forwarded-email', // Databricks Apps
|
|
210
293
|
'x-forwarded-preferred-username', // Databricks Apps
|
|
211
294
|
'x-goog-authenticated-user-email', // Cloud Run IAP (prefixed value, stripped below)
|
|
212
295
|
'cf-access-authenticated-user-email', // Cloudflare Access
|
|
213
296
|
];
|
|
297
|
+
/**
|
|
298
|
+
* Headers that are only ever trusted when explicitly pinned.
|
|
299
|
+
*
|
|
300
|
+
* Kept as a named list rather than deleted, because "why is my identity header ignored?" needs an
|
|
301
|
+
* answer, and `identityHeaderAdvice` gives one that names the env var to set.
|
|
302
|
+
*/
|
|
303
|
+
exports.PIN_ONLY_IDENTITY_HEADERS = ['x-agent-user'];
|
|
214
304
|
/** Env var: pin the single identity header the deploy layer actually sets (and strips). */
|
|
215
305
|
exports.IDENTITY_HEADER_ENV = 'AGENTHIPPO_IDENTITY_HEADER';
|
|
306
|
+
/**
|
|
307
|
+
* Env var: shared secret proving the identity header was set by the trusted proxy, not by
|
|
308
|
+
* something else that can reach serve's port.
|
|
309
|
+
*
|
|
310
|
+
* Pinning declares WHICH header carries identity; it says nothing about WHO may assert it. On a
|
|
311
|
+
* shared network (compose, sidecar platforms where every container shares localhost) any
|
|
312
|
+
* neighbour that can dial serve can send the pinned header and be namespaced as that user —
|
|
313
|
+
* network topology cannot express "the agent may dial the gateway but not the reverse". When
|
|
314
|
+
* this is set, serve honours identity headers only on requests that also carry the matching
|
|
315
|
+
* `x-agenthippo-proxy-secret` (the same proof-of-proxy contract the auth-broker enforces for
|
|
316
|
+
* `X-Auth-Request-Email`); without proof the request runs anonymous, same as an unpinned
|
|
317
|
+
* header. Opt-in: unset keeps today's behaviour, because platform-header deployments (IAP,
|
|
318
|
+
* Databricks Apps) have an unforgeable header and no proxy hop that could attach a secret.
|
|
319
|
+
*/
|
|
320
|
+
exports.IDENTITY_PROXY_SECRET_ENV = 'AGENTHIPPO_IDENTITY_PROXY_SECRET';
|
|
321
|
+
/**
|
|
322
|
+
* Proof that an identity header was set by the trusted proxy and not by a client: the request
|
|
323
|
+
* carries `x-agenthippo-proxy-secret` matching the configured secret. Shared with the
|
|
324
|
+
* auth-broker, which applies the same contract to `X-Auth-Request-Email` (it lives here rather
|
|
325
|
+
* than in the broker because the broker already imports from this module).
|
|
326
|
+
*/
|
|
327
|
+
function hasTrustedProxyProof(headers, expectedSecret) {
|
|
328
|
+
if (!expectedSecret) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
const raw = headers[config_1.AUTH_PROXY_SECRET_HEADER];
|
|
332
|
+
const presented = typeof raw === 'string' ? raw : Array.isArray(raw) ? String(raw[0] ?? '') : '';
|
|
333
|
+
if (!presented) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
// Hash both sides first: timingSafeEqual throws on a length mismatch, which would itself leak
|
|
337
|
+
// the secret's length.
|
|
338
|
+
const a = crypto.createHash('sha256').update(presented).digest();
|
|
339
|
+
const b = crypto.createHash('sha256').update(expectedSecret).digest();
|
|
340
|
+
return crypto.timingSafeEqual(a, b);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Boot-time advice about identity trust. Returns the lines serve must print, loudest first.
|
|
344
|
+
*
|
|
345
|
+
* Not a refusal. Refusing to boot without a pin would stop deployments that are configured
|
|
346
|
+
* correctly today (Databricks Apps, Cloudflare Access) and whose platform header is genuinely
|
|
347
|
+
* unforgeable — a fail-closed default that mostly punishes the compliant is not a security win.
|
|
348
|
+
* What was actually dangerous was `x-agent-user` being trusted by default, and that is now gone.
|
|
349
|
+
*/
|
|
350
|
+
function identityHeaderAdvice(env = process.env, bindHost) {
|
|
351
|
+
const pinned = env[exports.IDENTITY_HEADER_ENV]?.trim();
|
|
352
|
+
if (pinned) {
|
|
353
|
+
return [];
|
|
354
|
+
}
|
|
355
|
+
// Severity follows reachability. On loopback nothing off-box can reach the port, so the same
|
|
356
|
+
// paragraph every local run is warning fatigue — and fatigue is why the message gets ignored
|
|
357
|
+
// on the deploy where it decides whether strangers can name themselves as another user.
|
|
358
|
+
const host = bindHost?.trim();
|
|
359
|
+
if (host && isLoopbackHost(host)) {
|
|
360
|
+
return [
|
|
361
|
+
`${exports.IDENTITY_HEADER_ENV} is not set. Fine for this loopback bind; pin it before exposing ` +
|
|
362
|
+
`serve on a network interface.`,
|
|
363
|
+
];
|
|
364
|
+
}
|
|
365
|
+
return [
|
|
366
|
+
...(host
|
|
367
|
+
? [`serve is bound to ${host} — reachable beyond this machine — with no ${exports.IDENTITY_HEADER_ENV} pinned. ` +
|
|
368
|
+
`Anyone who can reach this port is trusted to state who they are.`]
|
|
369
|
+
: []),
|
|
370
|
+
`${exports.IDENTITY_HEADER_ENV} is not set, so identity falls back to the platform header aliases ` +
|
|
371
|
+
`(${HEADER_ALIASES.join(', ')}). That is only safe when your proxy sets AND STRIPS every one of them. ` +
|
|
372
|
+
`Pin the single header your deploy layer really controls, e.g. ${exports.IDENTITY_HEADER_ENV}=x-goog-authenticated-user-email.`,
|
|
373
|
+
`NOTE: ${exports.PIN_ONLY_IDENTITY_HEADERS.join(', ')} is NO LONGER trusted by default — it is not stripped by any ` +
|
|
374
|
+
`platform, so an arbitrary client could send it and be namespaced as another user. If your proxy does set it ` +
|
|
375
|
+
`(the Caddy forward_auth / auth-broker flow does), pin it: ${exports.IDENTITY_HEADER_ENV}=x-agent-user.`,
|
|
376
|
+
];
|
|
377
|
+
}
|
|
378
|
+
/** Env var: turn the tenant-isolation advice below into a boot refusal. */
|
|
379
|
+
exports.REQUIRE_TENANT_ISOLATION_ENV = 'AGENTHIPPO_REQUIRE_TENANT_ISOLATION';
|
|
380
|
+
/** True when `child` is `parent` or sits beneath it. */
|
|
381
|
+
function isWithin(child, parent) {
|
|
382
|
+
const c = path.resolve(child);
|
|
383
|
+
const p = path.resolve(parent);
|
|
384
|
+
return c === p || c.startsWith(p + path.sep);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Boot-time advice about what a second user of this agent can read off the disk.
|
|
388
|
+
*
|
|
389
|
+
* The condition this exists for: a deployment that PINS an identity header has declared it serves
|
|
390
|
+
* more than one person, and a tier-0 (`--sandbox=process`) or sandbox-less serve gives every one
|
|
391
|
+
* of them the same uid, the same workspace and the same state dir. Per-user namespacing —
|
|
392
|
+
* `userScopedEngineHome`'s `users/<hash>`, user-namespaced session keys, per-session analytics
|
|
393
|
+
* JSONL — is naming, not access control: a same-uid process reads its neighbours' names by
|
|
394
|
+
* listing the directory. What sits there is the engines' own session files, i.e. the plaintext of
|
|
395
|
+
* every other user's conversation, including whatever rows the OBO connector correctly scoped to
|
|
396
|
+
* them on the way in. The data path stays isolated and the artifact path undoes it.
|
|
397
|
+
*
|
|
398
|
+
* The nesting check is the sharper half. `--workspace=/state/workspace --state-dir=/state` is the
|
|
399
|
+
* shape the samples shipped, and it puts the artifact store one `..` above the agent's own cwd —
|
|
400
|
+
* so reaching it takes no traversal trick, just a relative path. Unrelated subtrees
|
|
401
|
+
* (`--workspace=/work --state-dir=/state`) do not make a same-uid process unable to read /state;
|
|
402
|
+
* they stop it being the obvious next directory.
|
|
403
|
+
*
|
|
404
|
+
* Warn, not refuse, by default — the shipped samples are in exactly this shape and a fatal check
|
|
405
|
+
* would brick them on upgrade, which is the same trade `identityHeaderAdvice` documents. An
|
|
406
|
+
* operator who has finished the migration sets REQUIRE_TENANT_ISOLATION_ENV and gets the refusal.
|
|
407
|
+
*/
|
|
408
|
+
function tenantIsolationAdvice(input, env = process.env) {
|
|
409
|
+
// Unpinned means serve cannot tell one caller from another at all; identityHeaderAdvice owns
|
|
410
|
+
// that case and saying both at once buries the one the operator can act on.
|
|
411
|
+
if (!env[exports.IDENTITY_HEADER_ENV]?.trim()) {
|
|
412
|
+
return { level: 'ok', lines: [] };
|
|
413
|
+
}
|
|
414
|
+
const perSessionFilesystem = input.sandbox === 'container' && !input.sharedWorkspace;
|
|
415
|
+
if (perSessionFilesystem) {
|
|
416
|
+
return { level: 'ok', lines: [] };
|
|
417
|
+
}
|
|
418
|
+
const tier = input.sandbox === 'process' ? 'a tier-0 (--sandbox=process) sandbox' : 'no sandbox';
|
|
419
|
+
const lines = [
|
|
420
|
+
`${exports.IDENTITY_HEADER_ENV} is pinned — so this serve has more than one user — but it runs with ` +
|
|
421
|
+
`${tier}, which shares one uid, one workspace (${input.workspace ?? 'unset'}) and one state dir ` +
|
|
422
|
+
`(${input.stateDir}) across all of them. Any user can read every other user's engine session ` +
|
|
423
|
+
`files, which hold their conversations verbatim. Per-user paths are namespacing, not isolation.`,
|
|
424
|
+
`The boundary is --sandbox=container (per-session workspace and state, without ` +
|
|
425
|
+
`--shared-workspace) or one serve per tenant. Until then, restrict the pack's tool surface so ` +
|
|
426
|
+
`the agent has no way to read arbitrary files.`,
|
|
427
|
+
];
|
|
428
|
+
if (input.workspace && isWithin(input.workspace, input.stateDir)) {
|
|
429
|
+
lines.push(`--workspace ${input.workspace} sits INSIDE --state-dir ${input.stateDir}: every other ` +
|
|
430
|
+
`user's sessions, analytics and evidence spool are one '..' above the agent's own cwd. ` +
|
|
431
|
+
`Put them in unrelated subtrees (e.g. --workspace=/work --state-dir=/state).`);
|
|
432
|
+
}
|
|
433
|
+
else if (input.workspace && isWithin(input.stateDir, input.workspace)) {
|
|
434
|
+
lines.push(`--state-dir ${input.stateDir} sits INSIDE --workspace ${input.workspace}: every other ` +
|
|
435
|
+
`user's sessions, analytics and evidence spool are inside the directory the agent is ` +
|
|
436
|
+
`pointed at. Put them in unrelated subtrees (e.g. --workspace=/work --state-dir=/state).`);
|
|
437
|
+
}
|
|
438
|
+
const strict = /^(1|true|yes|on)$/i.test(env[exports.REQUIRE_TENANT_ISOLATION_ENV]?.trim() ?? '');
|
|
439
|
+
return { level: strict ? 'refuse' : 'warn', lines };
|
|
440
|
+
}
|
|
216
441
|
/**
|
|
217
442
|
* Resolve the end-user identity for a request.
|
|
218
443
|
*
|
|
@@ -222,7 +447,10 @@ exports.IDENTITY_HEADER_ENV = 'AGENTHIPPO_IDENTITY_HEADER';
|
|
|
222
447
|
* that pins a header has declared it has a real proxy-verified identity, so an
|
|
223
448
|
* unauthenticated caller must not be able to name themselves via the client-
|
|
224
449
|
* controlled body. Body fallback is dev/internal convenience for the *unpinned*
|
|
225
|
-
* case only (e.g. direct curl against a local serve with no identity proxy)
|
|
450
|
+
* case only (e.g. direct curl against a local serve with no identity proxy) —
|
|
451
|
+
* and it must be OPTED INTO via AGENTHIPPO_ALLOW_BODY_IDENTITY=1 (serve-v4 §10
|
|
452
|
+
* hole 3). Before the gate it was safe "by convention": any deployment that
|
|
453
|
+
* forgot to pin a header silently let every caller name an arbitrary victim.
|
|
226
454
|
*
|
|
227
455
|
* The alias-list default (used when unpinned) is only safe when the proxy strips
|
|
228
456
|
* or overwrites ALL of the aliases; platforms that strip only their own header
|
|
@@ -231,14 +459,22 @@ exports.IDENTITY_HEADER_ENV = 'AGENTHIPPO_IDENTITY_HEADER';
|
|
|
231
459
|
* artifacts set it (e.g. cloudrun-deploy.sh pins `x-goog-authenticated-user-email`)
|
|
232
460
|
* so an authenticated caller cannot impersonate another user.
|
|
233
461
|
*
|
|
234
|
-
* Resolves identity for `analyticsUserId` attribution
|
|
235
|
-
* credential context (per-user data scoping)
|
|
236
|
-
*
|
|
462
|
+
* Resolves identity for `analyticsUserId` attribution, the request-scoped
|
|
463
|
+
* credential context (per-user data scoping), and the user-namespaced session
|
|
464
|
+
* key (resolveServeSessionKey folds this identity into every session key).
|
|
237
465
|
*
|
|
238
466
|
* Per-turn *data credentials* (OBO token, e.g. X-Forwarded-Access-Token) are
|
|
239
467
|
* handled by resolveDataToken — not read here.
|
|
240
468
|
*/
|
|
241
469
|
function resolveUserId(headers, body, env = process.env) {
|
|
470
|
+
// Proof-of-proxy gate (IDENTITY_PROXY_SECRET_ENV): when configured, no identity — header OR
|
|
471
|
+
// body — is honoured without the matching secret header. A caller that fails this check is
|
|
472
|
+
// by definition not the trusted proxy, so the request runs anonymous rather than letting a
|
|
473
|
+
// network neighbour (or a dev body fallback) name a victim.
|
|
474
|
+
const proxySecret = env[exports.IDENTITY_PROXY_SECRET_ENV]?.trim();
|
|
475
|
+
if (proxySecret && !hasTrustedProxyProof(headers, proxySecret)) {
|
|
476
|
+
return undefined;
|
|
477
|
+
}
|
|
242
478
|
const pinned = env[exports.IDENTITY_HEADER_ENV]?.trim().toLowerCase();
|
|
243
479
|
const names = pinned ? [pinned] : HEADER_ALIASES;
|
|
244
480
|
for (const name of names) {
|
|
@@ -257,6 +493,10 @@ function resolveUserId(headers, body, env = process.env) {
|
|
|
257
493
|
// access, gcloud run services proxy bypassing IAP) name an arbitrary victim.
|
|
258
494
|
return undefined;
|
|
259
495
|
}
|
|
496
|
+
// Unpinned body identity is an explicit opt-in, never a default (§10 hole 3).
|
|
497
|
+
if (env.AGENTHIPPO_ALLOW_BODY_IDENTITY?.trim() !== '1') {
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
260
500
|
const fromBody = body?.user_id?.trim();
|
|
261
501
|
return fromBody || undefined;
|
|
262
502
|
}
|
|
@@ -315,20 +555,26 @@ function userNamespaceHash(userId) {
|
|
|
315
555
|
* cross-user attachment structurally impossible without changing client behavior.
|
|
316
556
|
*
|
|
317
557
|
* Fail-closed rule: when the deployment pins an identity header (it declared a real
|
|
318
|
-
* proxy-verified identity exists)
|
|
319
|
-
*
|
|
558
|
+
* proxy-verified identity exists), any turn without a verified user is rejected — with or
|
|
559
|
+
* without a session_id. Rejecting only named sessions would leave every identity-less
|
|
560
|
+
* caller sharing the durable `anon-unknown` conversation state. Unpinned deployments
|
|
320
561
|
* (local dev, no identity boundary at all) keep today's raw-key behavior.
|
|
321
562
|
*/
|
|
322
|
-
function resolveServeSessionKey(sessionId, userId, env = process.env) {
|
|
563
|
+
function resolveServeSessionKey(sessionId, userId, scope = 'session', env = process.env) {
|
|
323
564
|
const sid = sessionId?.trim();
|
|
324
565
|
const uid = userId?.trim();
|
|
325
566
|
if (uid) {
|
|
326
|
-
|
|
567
|
+
const sessionKey = `u${userNamespaceHash(uid)}--${sid || 'default'}`;
|
|
568
|
+
return { sessionKey, sandboxKey: scope === 'user' ? `u${userNamespaceHash(uid)}` : sessionKey };
|
|
327
569
|
}
|
|
328
|
-
if (
|
|
329
|
-
return { sessionKey: '', rejected: exports.SESSION_WITHOUT_USER_REASON };
|
|
570
|
+
if (env[exports.IDENTITY_HEADER_ENV]?.trim()) {
|
|
571
|
+
return { sessionKey: '', sandboxKey: '', rejected: exports.SESSION_WITHOUT_USER_REASON };
|
|
330
572
|
}
|
|
331
|
-
|
|
573
|
+
// No verified user, so there is no user to scope a sandbox TO. `user` scope deliberately does
|
|
574
|
+
// not apply here: merging unidentified callers would put two strangers in one sandbox on the
|
|
575
|
+
// strength of a fact nobody established. Local dev keeps one sandbox per session.
|
|
576
|
+
const sessionKey = sid || 'anon-unknown';
|
|
577
|
+
return { sessionKey, sandboxKey: sessionKey };
|
|
332
578
|
}
|
|
333
579
|
/** Build CORS headers. Serve is always behind a proxy; CORS is wide-open here. */
|
|
334
580
|
function corsHeaders() {
|
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,14 +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");
|
|
46
|
-
const data_plane_1 = require("../cli/data-plane");
|
|
47
47
|
const runtime_1 = require("../cli/runtime");
|
|
48
48
|
const session_1 = require("../session");
|
|
49
49
|
const agentpack_1 = require("../agentpack");
|
|
50
|
+
const credential_context_1 = require("../cli/credential-context");
|
|
50
51
|
const shared_1 = require("../shared");
|
|
51
52
|
// File tools we track for edit diffs via hooks (Set for O(1) lookup)
|
|
52
53
|
// Includes Claude SDK tools, Computer Use tools, and common variations
|
|
@@ -261,6 +262,11 @@ class ClaudeEngine {
|
|
|
261
262
|
source: 'agenthippo',
|
|
262
263
|
engine: 'claude',
|
|
263
264
|
requestId: options.requestId,
|
|
265
|
+
policyVersion: options.policyVersion,
|
|
266
|
+
packDigest: options.packDigest,
|
|
267
|
+
instanceId: options.instanceId,
|
|
268
|
+
engineVersion: options.engineVersion,
|
|
269
|
+
tier: options.tier,
|
|
264
270
|
});
|
|
265
271
|
// Build environment for Claude SDK.
|
|
266
272
|
// Setup-token auth must use Claude's OAuth token env var instead of ANTHROPIC_API_KEY.
|
|
@@ -270,11 +276,13 @@ class ClaudeEngine {
|
|
|
270
276
|
'DEBUG_CLAUDE_AGENT_SDK': '1',
|
|
271
277
|
'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC': '1',
|
|
272
278
|
};
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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'];
|
|
278
286
|
const isSetupTokenAuth = (0, shared_1.isAnthropicSetupToken)(apiKey);
|
|
279
287
|
if (isSetupTokenAuth) {
|
|
280
288
|
const normalizedSetupToken = (0, shared_1.normalizeAnthropicSetupToken)(apiKey);
|
|
@@ -303,8 +311,13 @@ class ClaudeEngine {
|
|
|
303
311
|
// Note: cwd is ALWAYS workspaceRoot - active agent content is synced to workspace's .claude/.codex
|
|
304
312
|
const agentId = options.agentPackId ?? 'default';
|
|
305
313
|
const runtimePaths = (0, agentpack_1.resolveRuntimePaths)(options.workspaceRoot, this.config.storagePath, agentId, options.agentPackVersion);
|
|
306
|
-
// Claude config directory (isolated per agent for session/state isolation)
|
|
307
|
-
|
|
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.userScopedEngineHomeFromKey)(runtimePaths.claudeConfigDir, (0, credential_context_1.resolveEngineHomeUserKey)(options.engineHomeUserKey));
|
|
308
321
|
claudeEnv['CLAUDE_CONFIG_DIR'] = claudeConfigDir;
|
|
309
322
|
await fs_1.promises.mkdir(claudeConfigDir, { recursive: true });
|
|
310
323
|
// Always set AGENT_ID for agent to know its identity
|
|
@@ -549,9 +562,10 @@ class ClaudeEngine {
|
|
|
549
562
|
logger.debug(`[AgentIDE] SessionStart hook called! event=${input.hook_event_name}`);
|
|
550
563
|
return CONTINUE;
|
|
551
564
|
};
|
|
552
|
-
// Load MCP servers for the active agent and pass directly to SDK.
|
|
553
|
-
//
|
|
554
|
-
//
|
|
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.
|
|
555
569
|
const mcpServers = await this.loadMcpServersForClaude(logger, options.workspaceRoot, agentId, claudeEnv);
|
|
556
570
|
// Determine cwd and settingSources
|
|
557
571
|
// cwd is ALWAYS workspaceRoot - active agent content has been synced to workspace's .claude/
|
|
@@ -1068,7 +1082,7 @@ class ClaudeEngine {
|
|
|
1068
1082
|
logger.info(`[AgentIDE] Claude thinking summary: startBlocks=${thinkingStartBlockCount}, redactedBlocks=${redactedThinkingBlockCount}, deltas=${thinkingDeltaCount}, chars=${thinkingCharCount}`);
|
|
1069
1083
|
logger.info(`[AgentIDE] Claude tool summary: toolBlocks=${toolUseBlockCount}, toolEmits=${toolUseEmitCount}, toolProgressMsgs=${toolProgressCount}, inputJsonDeltas=${inputJsonDeltaCount}`);
|
|
1070
1084
|
if (thinkingStartBlockCount === 0 && thinkingDeltaCount === 0 && redactedThinkingBlockCount === 0) {
|
|
1071
|
-
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.`);
|
|
1072
1086
|
}
|
|
1073
1087
|
if (toolUseBlockCount === 0 && toolProgressCount === 0) {
|
|
1074
1088
|
logger.debug('[AgentIDE] Claude emitted no tool usage events. If tools were used, ensure includePartialMessages=true and parse tool_use blocks.');
|
|
@@ -1202,8 +1216,12 @@ class ClaudeEngine {
|
|
|
1202
1216
|
* - Pack agents: uses pack's mcp/ directory
|
|
1203
1217
|
*
|
|
1204
1218
|
* The SDK accepts mcpServers directly in query options — including HTTP Streamable
|
|
1205
|
-
* servers (`{type:'http', url, headers}`).
|
|
1206
|
-
*
|
|
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.
|
|
1207
1225
|
*/
|
|
1208
1226
|
async loadMcpServersForClaude(logger, workspaceRoot, agentId = 'default', turnEnv) {
|
|
1209
1227
|
const basePromptsDir = this.getBundledBasePromptsDir();
|
|
@@ -1214,7 +1232,7 @@ class ClaudeEngine {
|
|
|
1214
1232
|
const mcpServers = {};
|
|
1215
1233
|
for (const s of servers) {
|
|
1216
1234
|
if (s.url) {
|
|
1217
|
-
const headers = (0, mcp_1.
|
|
1235
|
+
const headers = (0, mcp_1.referenceHttpMcpHeaders)(s, turnEnv ?? process.env);
|
|
1218
1236
|
mcpServers[s.name] = {
|
|
1219
1237
|
type: 'http',
|
|
1220
1238
|
url: s.url,
|