@agenthippo-ai/agenthippo 1.110.9 → 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/service.js +6 -0
- package/out/analytics/sessionEvents.js +4 -3
- package/out/auth-broker/server.js +23 -19
- package/out/cli/commands/data-plane.js +84 -0
- package/out/cli/commands/sandbox.js +28 -9
- package/out/cli/commands/serve.js +289 -97
- package/out/cli/credential-context.js +17 -0
- package/out/cli/data-plane/index.js +2 -1
- package/out/cli/data-plane/sidecar.js +66 -2
- package/out/cli/deploy/layers/identity.js +5 -2
- package/out/cli/deploy-init.js +4 -4
- package/out/cli/index.js +16 -1
- package/out/cli/serve-hardening.js +181 -12
- package/out/engines/claude.js +1 -1
- package/out/engines/codex.js +1 -1
- package/out/engines/custom.js +7 -1
- package/out/evidence/sessionShip.js +50 -17
- package/out/headless/index.js +8 -1
- package/out/pack/governor.js +32 -15
- package/out/pack/lease-schema.js +4 -1
- package/out/pack/lease.js +42 -0
- package/out/runner/host.js +24 -14
- package/out/runner/protocol.js +24 -4
- package/out/runner/remoteEngine.js +53 -17
- package/out/sandbox/containerDriver.js +4 -2
- package/out/sandbox/processDriver.js +4 -1
- package/out/sandbox/sandboxRunner.js +148 -36
- package/out/scripts/agentide_litellm_hooks_v1.py +85 -1
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +4 -0
- package/out/scripts/analytics/setup-analytics.ps1 +8 -1
- package/out/scripts/analytics/setup-analytics.sh +13 -2
- package/out/test/caps-governor.test.js +6 -1
- package/out/test/data-plane-engine-wiring.test.js +32 -0
- package/out/test/data-plane.test.js +313 -0
- package/out/test/deploy-init.test.js +7 -1
- package/out/test/governor-hardening.test.js +6 -1
- package/out/test/lease-validation.test.js +55 -0
- package/out/test/localFleetAdapter.test.js +91 -0
- package/out/test/sandbox-driver.test.js +2 -2
- package/out/test/sandbox-hardening.test.js +12 -1
- package/out/test/sandbox-runner.test.js +1 -1
- 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 +182 -9
- package/out/test/sessionShip.test.js +58 -0
- package/package.json +1 -1
|
@@ -36,6 +36,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.ASSERTION_HEADER = void 0;
|
|
39
40
|
exports.startDataPlaneSidecar = startDataPlaneSidecar;
|
|
40
41
|
/**
|
|
41
42
|
* Data-plane SIDECAR (secure-obo-design §7.6 / aperture-thin) — the connector's own trust
|
|
@@ -68,6 +69,24 @@ const crypto_1 = require("crypto");
|
|
|
68
69
|
const index_1 = require("./index");
|
|
69
70
|
const serve_hardening_1 = require("../serve-hardening");
|
|
70
71
|
const gateway_1 = require("./gateway");
|
|
72
|
+
/**
|
|
73
|
+
* Header carrying the EDGE-verified user ASSERTION (agent-identity.md §8.5 G1) — the signed
|
|
74
|
+
* artifact, as opposed to `X-Agent-User` which is only the identifier. It becomes the turn's
|
|
75
|
+
* `inboundToken`, which is what `passthrough` hands the backend and what an RFC 8693 `exchange`
|
|
76
|
+
* provider presents as its `subject_token`.
|
|
77
|
+
*
|
|
78
|
+
* Set by the trusted edge on the turn-registration hop only, so it never reaches the agent
|
|
79
|
+
* container. A client-supplied copy must be stripped at the edge, exactly as for the nonce.
|
|
80
|
+
*/
|
|
81
|
+
exports.ASSERTION_HEADER = 'x-agent-user-assertion';
|
|
82
|
+
function headerValue(req, name) {
|
|
83
|
+
const v = req.headers[name];
|
|
84
|
+
return (Array.isArray(v) ? v[0] : v)?.trim() || undefined;
|
|
85
|
+
}
|
|
86
|
+
/** Audit-safe fingerprint of a bearer credential — never the credential. */
|
|
87
|
+
function sha256Prefix(value) {
|
|
88
|
+
return (0, crypto_1.createHash)('sha256').update(value).digest('hex').slice(0, 12);
|
|
89
|
+
}
|
|
71
90
|
function bearer(header) {
|
|
72
91
|
return header?.match(/^Bearer\s+(\S+)$/i)?.[1];
|
|
73
92
|
}
|
|
@@ -104,8 +123,20 @@ async function startDataPlaneSidecar(opts) {
|
|
|
104
123
|
opts.audit?.('unauthorized', 'sidecar_edge_reject', { reason: 'missing_user' });
|
|
105
124
|
return reject(400, 'X-Agent-User required');
|
|
106
125
|
}
|
|
107
|
-
|
|
108
|
-
|
|
126
|
+
// The user ASSERTION, not just the identifier (agent-identity.md §8.5 G1). The edge
|
|
127
|
+
// forwarded only X-Agent-User until now, so `registerTurnRaw` got no inboundToken and
|
|
128
|
+
// edge mode silently supported the `identity` provider alone: `passthrough` failed
|
|
129
|
+
// closed on every call, and an RFC 8693 `exchange` provider had no subject_token to
|
|
130
|
+
// exchange. Trusted exactly like X-Agent-User — only the in-network edge can reach
|
|
131
|
+
// this port, and the edge overwrites any client-supplied copy.
|
|
132
|
+
const inboundToken = headerValue(req, exports.ASSERTION_HEADER)?.replace(/^Bearer\s+/i, '').trim() || undefined;
|
|
133
|
+
const { nonce } = plane.registerTurnRaw({ userId, sessionKey: userId, inboundToken });
|
|
134
|
+
// Never the assertion itself: it is a bearer credential, and these rows ship to the
|
|
135
|
+
// evidence server. A hash prefix still proves which assertion a turn used.
|
|
136
|
+
opts.audit?.(userId, 'sidecar_edge_register', {
|
|
137
|
+
userId,
|
|
138
|
+
assertion: inboundToken ? `sha256:${sha256Prefix(inboundToken)}` : 'none',
|
|
139
|
+
});
|
|
109
140
|
res.writeHead(200, { 'Content-Type': 'application/json', [gateway_1.TURN_NONCE_HEADER]: nonce });
|
|
110
141
|
return res.end(JSON.stringify({ ok: true }));
|
|
111
142
|
}
|
|
@@ -121,6 +152,30 @@ async function startDataPlaneSidecar(opts) {
|
|
|
121
152
|
if (!userId) {
|
|
122
153
|
return reject(400, 'userId required');
|
|
123
154
|
}
|
|
155
|
+
// Which agent SAYS it is asking. Recorded, never trusted: the register token is
|
|
156
|
+
// shared, so this claim is only as good as whoever holds it. `binding: 'asserted'`
|
|
157
|
+
// travels with it so nobody reading evidence later mistakes it for a verified fact
|
|
158
|
+
// (PLAN/agent-identity.md §4.4 — the successor is warrant verification, F3).
|
|
159
|
+
const claimed = body.agent?.pack?.trim();
|
|
160
|
+
// One connector is meant to serve one agent. When it does not, this is where it
|
|
161
|
+
// becomes visible — and visibility is the whole deliverable, since the sidecar
|
|
162
|
+
// cannot safely refuse on an unverifiable claim.
|
|
163
|
+
const mismatch = Boolean(claimed && opts.agent?.pack && claimed !== opts.agent.pack);
|
|
164
|
+
if (mismatch) {
|
|
165
|
+
// console.warn, not logger.warn: the logger is a no-op without --verbose, and two
|
|
166
|
+
// agents sharing one connector is the exact condition E2's agent bound rests on
|
|
167
|
+
// NOT happening. It must be unmissable in the default deployment, not a debug line.
|
|
168
|
+
console.warn(`Warning: turn registration claims agent "${claimed}" but this connector loaded ` +
|
|
169
|
+
`"${opts.agent?.pack}". One connector is meant to serve one agent ` +
|
|
170
|
+
`(PLAN/agent-identity.md §4.4) — both now share this connector's base credential, ` +
|
|
171
|
+
`so neither is bounded by the job it was deployed for.`);
|
|
172
|
+
}
|
|
173
|
+
opts.audit?.((body.sessionKey ?? '').trim() || userId, 'sidecar_register', {
|
|
174
|
+
userId,
|
|
175
|
+
claimedAgent: claimed ?? 'none',
|
|
176
|
+
binding: 'asserted',
|
|
177
|
+
...(mismatch ? { agentMismatch: true, loadedAgent: opts.agent?.pack } : {}),
|
|
178
|
+
});
|
|
124
179
|
const { nonce, release } = plane.registerTurnRaw({
|
|
125
180
|
userId, sessionKey: (body.sessionKey ?? '').trim() || userId,
|
|
126
181
|
inboundToken: body.inboundToken, egressStoreKey: body.egressStoreKey,
|
|
@@ -153,6 +208,15 @@ async function startDataPlaneSidecar(opts) {
|
|
|
153
208
|
const controlAddress = control.address();
|
|
154
209
|
const controlPort = controlAddress && typeof controlAddress !== 'string' ? controlAddress.port : opts.controlPort;
|
|
155
210
|
opts.logger?.info(`[Sidecar] gateway ${plane.url} | control :${controlPort} (bound ${host})`);
|
|
211
|
+
// A `passthrough` provider has nothing of its own: with no inbound token it fails closed on
|
|
212
|
+
// EVERY call (broker.ts). Before the assertion header existed, an edge-registered turn never
|
|
213
|
+
// carried one, so this combination looked healthy at boot and then denied every single tool
|
|
214
|
+
// call at runtime. Say it once, at boot, where an operator can act on it.
|
|
215
|
+
if ((opts.config.provider ?? 'passthrough').trim().toLowerCase() === 'passthrough') {
|
|
216
|
+
console.warn(`Warning: provider=passthrough needs a per-user token on every turn. Edge-registered turns ` +
|
|
217
|
+
`supply it via the ${exports.ASSERTION_HEADER} header — if your edge does not forward it, every tool ` +
|
|
218
|
+
`call will fail closed. See PLAN/agent-identity.md §8.5 G1.`);
|
|
219
|
+
}
|
|
156
220
|
return {
|
|
157
221
|
gatewayUrl: plane.url,
|
|
158
222
|
controlPort,
|
|
@@ -34,6 +34,9 @@ function renderIdentityLayer(opts) {
|
|
|
34
34
|
? [
|
|
35
35
|
' # Turn registration — after identity: X-Agent-User (set above) names the user the',
|
|
36
36
|
' # sidecar mints the nonce for. Only POST chat turns match; others pass through.',
|
|
37
|
+
' # X-Agent-User-Assertion (also set above) rides along as the turn\'s inboundToken:',
|
|
38
|
+
' # without it `passthrough` fails closed on every call and an RFC 8693 `exchange`',
|
|
39
|
+
' # provider has no subject_token. See PLAN/agent-identity.md §8.5 G1.',
|
|
37
40
|
` forward_auth @turnStart ${turn.connectorAddr} {`,
|
|
38
41
|
' uri /edge/turn',
|
|
39
42
|
' copy_headers X-Agent-Turn-Nonce',
|
|
@@ -54,7 +57,7 @@ function renderIdentityLayer(opts) {
|
|
|
54
57
|
: [...identityGate, ...body];
|
|
55
58
|
const lines = [];
|
|
56
59
|
if (turn) {
|
|
57
|
-
lines.push(' # Hygiene: never let a client-supplied nonce header through (copy_headers overwrites it', ' # on chat paths anyway, and a forged nonce fails closed at the gateway — belt-and-braces).', ' request_header -X-Agent-Turn-Nonce');
|
|
60
|
+
lines.push(' # Hygiene: never let a client-supplied nonce header through (copy_headers overwrites it', ' # on chat paths anyway, and a forged nonce fails closed at the gateway — belt-and-braces).', ' request_header -X-Agent-Turn-Nonce', ' # Same for the user assertion: it is set from the verified /api/ide/me response below,', ' # never from the client. A forged one would become the turn\'s inboundToken.', ' request_header -X-Agent-User-Assertion');
|
|
58
61
|
}
|
|
59
62
|
if (mode === 'self-issued') {
|
|
60
63
|
lines.push(' # IDE PKCE sign-in: broker requires X-Auth-Request-Email from oauth2-proxy. Must come', ' # before the generic /oauth/* handle so authorize is gated (not dumped straight to broker).', ' handle /oauth/authorize {', ` forward_auth ${oauth2ProxyAddr} {`, ' uri /oauth2/auth', ' copy_headers X-Auth-Request-Email', ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' @unauth status 401', ' handle_response @unauth {', ' redir * "/oauth2/sign_in?rd={http.request.uri}"', ' }', ' }', ` reverse_proxy ${authBrokerAddr} {`, ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ...BROKER_PROXY_SECRET_LINES, ' }', ' }', ' # Federated SSO (A′): browsers from other subdomains land here to stamp the shared cookie.', ' # Same oauth2-proxy gate as authorize; must precede the generic /oauth/* handle.', ' handle /oauth/browser-login {', ` forward_auth ${oauth2ProxyAddr} {`, ' uri /oauth2/auth', ' copy_headers X-Auth-Request-Email', ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' @unauth status 401', ' handle_response @unauth {', ' redir * "/oauth2/sign_in?rd={http.request.uri}"', ' }', ' }', ` reverse_proxy ${authBrokerAddr} {`, ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ...BROKER_PROXY_SECRET_LINES, ' }', ' }', ' # Auth-broker: token exchange + other /oauth/* routes that must stay ungated.', ' handle /oauth/* {', ` reverse_proxy ${authBrokerAddr} {`, ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' }', ' }');
|
|
@@ -70,7 +73,7 @@ function renderIdentityLayer(opts) {
|
|
|
70
73
|
lines.push(' # API clients (IDE) with a Bearer token: validate via auth-broker, copy identity header.', ' @apiWithAuth {', ' path /v1/*', ' header Authorization *', ' }', ' handle @apiWithAuth {', ...gated([
|
|
71
74
|
` forward_auth ${authBrokerAddr} {`,
|
|
72
75
|
' uri /api/ide/me',
|
|
73
|
-
|
|
76
|
+
` copy_headers X-Agent-User${turn ? ' X-Agent-User-Assertion' : ''}`,
|
|
74
77
|
' header_up X-Forwarded-Proto https',
|
|
75
78
|
' header_up X-Forwarded-Host {http.request.host}',
|
|
76
79
|
' @unauth status 401',
|
package/out/cli/deploy-init.js
CHANGED
|
@@ -1082,7 +1082,7 @@ function composeArtifacts(ctx) {
|
|
|
1082
1082
|
' # (build: docker build -f extensions/agentide/scripts/Dockerfile.litellm -t this-tag .)',
|
|
1083
1083
|
' litellm:',
|
|
1084
1084
|
' profiles: ["gateway"]',
|
|
1085
|
-
` image: ${agenthippoImage('litellm
|
|
1085
|
+
` image: ${agenthippoImage('agenthippo-litellm')}`,
|
|
1086
1086
|
' env_file:',
|
|
1087
1087
|
' - path: .env',
|
|
1088
1088
|
' required: false',
|
|
@@ -1405,7 +1405,7 @@ function renderCloudrunSpotlightDeploy(ctx) {
|
|
|
1405
1405
|
'PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project)}"',
|
|
1406
1406
|
'REPO="${ARTIFACT_REPO:-agenthippo}"',
|
|
1407
1407
|
'IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/${SERVICE}:latest"',
|
|
1408
|
-
`LITELLM_IMAGE="\${LITELLM_IMAGE:-${agenthippoImage('litellm
|
|
1408
|
+
`LITELLM_IMAGE="\${LITELLM_IMAGE:-${agenthippoImage('agenthippo-litellm')}}"`,
|
|
1409
1409
|
'STATE_BUCKET="${STATE_BUCKET:?set STATE_BUCKET=your-gcs-bucket (gcloud storage buckets create gs://...)}"',
|
|
1410
1410
|
'# LITELLM_KEY = the proxy key the agent presents to LiteLLM (and LiteLLM master key).',
|
|
1411
1411
|
'LITELLM_KEY="${LITELLM_KEY:?set LITELLM_KEY=<litellm proxy key>}"',
|
|
@@ -1592,7 +1592,7 @@ function renderCloudrunEdgeDeploy(ctx) {
|
|
|
1592
1592
|
'PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project)}"',
|
|
1593
1593
|
'REPO="${ARTIFACT_REPO:-agenthippo}"',
|
|
1594
1594
|
'IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/${SERVICE}:latest"',
|
|
1595
|
-
`LITELLM_IMAGE="\${LITELLM_IMAGE:-${agenthippoImage('litellm
|
|
1595
|
+
`LITELLM_IMAGE="\${LITELLM_IMAGE:-${agenthippoImage('agenthippo-litellm')}}"`,
|
|
1596
1596
|
'# Public hostname (custom domain mapped to the service) + the Google Workspace domain allowed by SSO.',
|
|
1597
1597
|
'PUBLIC_HOSTNAME="${PUBLIC_HOSTNAME:?set PUBLIC_HOSTNAME=your.edge.hostname (gcloud run domain-mappings create --service $SERVICE --domain ...)}"',
|
|
1598
1598
|
'ALLOWED_DOMAINS="${ALLOWED_DOMAINS:?set ALLOWED_DOMAINS=yourworkspace.com (oauth2-proxy email domain + broker allowed domains)}"',
|
|
@@ -2157,7 +2157,7 @@ function renderCloudrunFedDeploy(ctx) {
|
|
|
2157
2157
|
'PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project)}"',
|
|
2158
2158
|
'REPO="${ARTIFACT_REPO:-agenthippo}"',
|
|
2159
2159
|
'IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/${SERVICE}:latest"',
|
|
2160
|
-
`LITELLM_IMAGE="\${LITELLM_IMAGE:-${agenthippoImage('litellm
|
|
2160
|
+
`LITELLM_IMAGE="\${LITELLM_IMAGE:-${agenthippoImage('agenthippo-litellm')}}"`,
|
|
2161
2161
|
'# Public hostname = a *.agenthippo.ai subdomain mapped to this service (one-time wildcard DNS).',
|
|
2162
2162
|
'PUBLIC_HOSTNAME="${PUBLIC_HOSTNAME:?set PUBLIC_HOSTNAME=<subdomain>.agenthippo.ai}"',
|
|
2163
2163
|
'ALLOWED_DOMAINS="${ALLOWED_DOMAINS:-agenthippo.ai}"',
|
package/out/cli/index.js
CHANGED
|
@@ -107,7 +107,16 @@ function resolveMaxConcurrentSandboxes(options) {
|
|
|
107
107
|
}
|
|
108
108
|
return undefined;
|
|
109
109
|
}
|
|
110
|
-
|
|
110
|
+
/** Single source: extensions/agentide/package.json (bundled into the compiled CLI by Bun). */
|
|
111
|
+
function resolveCliVersion() {
|
|
112
|
+
try {
|
|
113
|
+
return require('../../package.json').version ?? 'dev';
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return 'dev';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const VERSION = resolveCliVersion();
|
|
111
120
|
const CLI_DESCRIPTION = [
|
|
112
121
|
`${branding_1.AGENTIDE_BRAND_NAME} CLI - Run agent packs from the command line`,
|
|
113
122
|
'',
|
|
@@ -244,6 +253,7 @@ program
|
|
|
244
253
|
.option('--www-dir <path>', 'Serve deploy/www/index.html at GET / (static browser chat UI)')
|
|
245
254
|
.option('--sandbox <mode>', 'Run each session\'s engine in a sandbox: process (tier 0, no boundary) or container (tier 2, one disposable container per session with only its declared routes). Unset = in-process (default, unchanged)')
|
|
246
255
|
.option('--sandbox-image <ref>', '--sandbox container: sandbox image pinned BY DIGEST (image@sha256:...)')
|
|
256
|
+
.option('--sandbox-scope <scope>', '--sandbox: what one sandbox holds — session (default, one per conversation) or user (one per verified user on this instance, so their sessions share a container, workspace, TTL and kill). Unidentified callers get one each either way')
|
|
247
257
|
.option('--pull-packs', 'Fetch the version the lease pins from the store instead of only refusing others. Always verifies the signature; a fetch or verification failure keeps serving the current version')
|
|
248
258
|
.option('--require-signed-pack', 'Refuse to start a sandbox unless the pack is signed by a trusted key (the lease may also require this; neither side can waive the other). Now the DEFAULT — kept so existing invocations still mean something')
|
|
249
259
|
.option('--allow-unsigned-pack', 'LOCAL DEVELOPMENT ONLY: run a pack this instance cannot verify. Signature checking is required by default; this disables it and warns loudly on every boot. A lease that sets requireSignedPack still overrides it')
|
|
@@ -266,6 +276,9 @@ program
|
|
|
266
276
|
await (0, serve_1.serveCommand)({
|
|
267
277
|
sandbox: options.sandbox === 'container' ? 'container' : options.sandbox === 'process' ? 'process' : undefined,
|
|
268
278
|
sandboxImage: options.sandboxImage,
|
|
279
|
+
// Anything but an explicit `user` means session scope: an unrecognised value must not
|
|
280
|
+
// quietly widen what a sandbox holds.
|
|
281
|
+
sandboxScope: options.sandboxScope === 'user' ? 'user' : 'session',
|
|
269
282
|
requireSignedPack: Boolean(options.requireSignedPack),
|
|
270
283
|
allowUnsignedPack: Boolean(options.allowUnsignedPack),
|
|
271
284
|
requireSignedLease: Boolean(options.requireSignedLease),
|
|
@@ -352,6 +365,7 @@ program
|
|
|
352
365
|
.option('--gateway-port <port>', 'Port the engine dials (nonce-auth MCP)', '9090')
|
|
353
366
|
.option('--control-port <port>', 'Port serve dials to register turns (bearer-auth)', '9091')
|
|
354
367
|
.option('--host <address>', 'Bind address (default 0.0.0.0 for the pod network)')
|
|
368
|
+
.option('--state-dir <path>', 'Where data-plane audit events are written. Same precedence as serve: --state-dir, then AGENTHIPPO_HOME, then tmpdir')
|
|
355
369
|
.action(async (options) => {
|
|
356
370
|
const verbose = program.opts().verbose;
|
|
357
371
|
await (0, deps_1.ensureDeps)(verbose);
|
|
@@ -361,6 +375,7 @@ program
|
|
|
361
375
|
gatewayPort: parseInt(options.gatewayPort, 10),
|
|
362
376
|
controlPort: parseInt(options.controlPort, 10),
|
|
363
377
|
host: options.host,
|
|
378
|
+
stateDir: options.stateDir,
|
|
364
379
|
verbose,
|
|
365
380
|
});
|
|
366
381
|
});
|
|
@@ -36,13 +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.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;
|
|
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;
|
|
42
43
|
exports.isLoopbackHost = isLoopbackHost;
|
|
43
44
|
exports.resolveBindHost = resolveBindHost;
|
|
44
45
|
exports.clientIp = clientIp;
|
|
46
|
+
exports.hasTrustedProxyProof = hasTrustedProxyProof;
|
|
45
47
|
exports.identityHeaderAdvice = identityHeaderAdvice;
|
|
48
|
+
exports.tenantIsolationAdvice = tenantIsolationAdvice;
|
|
46
49
|
exports.resolveUserId = resolveUserId;
|
|
47
50
|
exports.resolveDataToken = resolveDataToken;
|
|
48
51
|
exports.peerToUserId = peerToUserId;
|
|
@@ -61,12 +64,15 @@ exports.readBodyWithCap = readBodyWithCap;
|
|
|
61
64
|
*
|
|
62
65
|
* SECURITY CONTRACT: serve trusts the identity and data-token headers below unconditionally.
|
|
63
66
|
* It MUST sit behind a proxy that (a) authenticates the caller and sets the identity header,
|
|
64
|
-
* 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.
|
|
65
70
|
*/
|
|
66
71
|
const os = __importStar(require("os"));
|
|
67
72
|
const path = __importStar(require("path"));
|
|
68
73
|
const crypto = __importStar(require("crypto"));
|
|
69
74
|
const paths_1 = require("../utils/paths");
|
|
75
|
+
const config_1 = require("../auth-broker/config");
|
|
70
76
|
/** Default cap on a single request body (1 MiB). Chat messages are small; large bodies are abuse or bugs. */
|
|
71
77
|
exports.DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
72
78
|
/**
|
|
@@ -109,6 +115,56 @@ function resolveAnalyticsSessionsDir(stateDir, stateDirEphemeral, env = process.
|
|
|
109
115
|
}
|
|
110
116
|
return path.join((0, paths_1.getAgentideHome)(), 'analytics', 'sessions');
|
|
111
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
|
+
}
|
|
112
168
|
/** Loopback default — safe everywhere a human runs `serve` locally. */
|
|
113
169
|
exports.DEFAULT_BIND_HOST = '127.0.0.1';
|
|
114
170
|
/** True when a bind address is reachable only from this machine. */
|
|
@@ -247,6 +303,42 @@ const HEADER_ALIASES = [
|
|
|
247
303
|
exports.PIN_ONLY_IDENTITY_HEADERS = ['x-agent-user'];
|
|
248
304
|
/** Env var: pin the single identity header the deploy layer actually sets (and strips). */
|
|
249
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
|
+
}
|
|
250
342
|
/**
|
|
251
343
|
* Boot-time advice about identity trust. Returns the lines serve must print, loudest first.
|
|
252
344
|
*
|
|
@@ -283,6 +375,69 @@ function identityHeaderAdvice(env = process.env, bindHost) {
|
|
|
283
375
|
`(the Caddy forward_auth / auth-broker flow does), pin it: ${exports.IDENTITY_HEADER_ENV}=x-agent-user.`,
|
|
284
376
|
];
|
|
285
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
|
+
}
|
|
286
441
|
/**
|
|
287
442
|
* Resolve the end-user identity for a request.
|
|
288
443
|
*
|
|
@@ -304,14 +459,22 @@ function identityHeaderAdvice(env = process.env, bindHost) {
|
|
|
304
459
|
* artifacts set it (e.g. cloudrun-deploy.sh pins `x-goog-authenticated-user-email`)
|
|
305
460
|
* so an authenticated caller cannot impersonate another user.
|
|
306
461
|
*
|
|
307
|
-
* Resolves identity for `analyticsUserId` attribution
|
|
308
|
-
* credential context (per-user data scoping)
|
|
309
|
-
*
|
|
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).
|
|
310
465
|
*
|
|
311
466
|
* Per-turn *data credentials* (OBO token, e.g. X-Forwarded-Access-Token) are
|
|
312
467
|
* handled by resolveDataToken — not read here.
|
|
313
468
|
*/
|
|
314
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
|
+
}
|
|
315
478
|
const pinned = env[exports.IDENTITY_HEADER_ENV]?.trim().toLowerCase();
|
|
316
479
|
const names = pinned ? [pinned] : HEADER_ALIASES;
|
|
317
480
|
for (const name of names) {
|
|
@@ -392,20 +555,26 @@ function userNamespaceHash(userId) {
|
|
|
392
555
|
* cross-user attachment structurally impossible without changing client behavior.
|
|
393
556
|
*
|
|
394
557
|
* Fail-closed rule: when the deployment pins an identity header (it declared a real
|
|
395
|
-
* proxy-verified identity exists)
|
|
396
|
-
*
|
|
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
|
|
397
561
|
* (local dev, no identity boundary at all) keep today's raw-key behavior.
|
|
398
562
|
*/
|
|
399
|
-
function resolveServeSessionKey(sessionId, userId, env = process.env) {
|
|
563
|
+
function resolveServeSessionKey(sessionId, userId, scope = 'session', env = process.env) {
|
|
400
564
|
const sid = sessionId?.trim();
|
|
401
565
|
const uid = userId?.trim();
|
|
402
566
|
if (uid) {
|
|
403
|
-
|
|
567
|
+
const sessionKey = `u${userNamespaceHash(uid)}--${sid || 'default'}`;
|
|
568
|
+
return { sessionKey, sandboxKey: scope === 'user' ? `u${userNamespaceHash(uid)}` : sessionKey };
|
|
404
569
|
}
|
|
405
|
-
if (
|
|
406
|
-
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 };
|
|
407
572
|
}
|
|
408
|
-
|
|
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 };
|
|
409
578
|
}
|
|
410
579
|
/** Build CORS headers. Serve is always behind a proxy; CORS is wide-open here. */
|
|
411
580
|
function corsHeaders() {
|
package/out/engines/claude.js
CHANGED
|
@@ -317,7 +317,7 @@ class ClaudeEngine {
|
|
|
317
317
|
// concurrent turns of different users race each other's per-turn credential state
|
|
318
318
|
// (the agentpack/paths.ts race); per-user scoping makes any remaining race
|
|
319
319
|
// same-user. No credential frame (IDE, single-tenant CLI) ⇒ unchanged path.
|
|
320
|
-
const claudeConfigDir = (0, agentpack_1.
|
|
320
|
+
const claudeConfigDir = (0, agentpack_1.userScopedEngineHomeFromKey)(runtimePaths.claudeConfigDir, (0, credential_context_1.resolveEngineHomeUserKey)(options.engineHomeUserKey));
|
|
321
321
|
claudeEnv['CLAUDE_CONFIG_DIR'] = claudeConfigDir;
|
|
322
322
|
await fs_1.promises.mkdir(claudeConfigDir, { recursive: true });
|
|
323
323
|
// Always set AGENT_ID for agent to know its identity
|
package/out/engines/codex.js
CHANGED
|
@@ -207,7 +207,7 @@ class CodexEngine {
|
|
|
207
207
|
// and a shared per-agent home lets concurrent turns of different users race it
|
|
208
208
|
// (secure-obo-design §1 defect 1 / §8 Phase 0). No credential frame (IDE,
|
|
209
209
|
// single-tenant CLI) ⇒ unchanged per-agent path.
|
|
210
|
-
const codexHome = (0, agentpack_1.
|
|
210
|
+
const codexHome = (0, agentpack_1.userScopedEngineHomeFromKey)(runtimePaths.codexHome, (0, credential_context_1.resolveEngineHomeUserKey)(options.engineHomeUserKey));
|
|
211
211
|
codexEnv['CODEX_HOME'] = codexHome;
|
|
212
212
|
await fs_1.promises.mkdir(codexHome, { recursive: true });
|
|
213
213
|
// Always set AGENT_ID for agent to know its identity
|
package/out/engines/custom.js
CHANGED
|
@@ -50,6 +50,7 @@ const base_1 = require("./base");
|
|
|
50
50
|
const runtime_1 = require("../cli/runtime");
|
|
51
51
|
const customRegistry_1 = require("./customRegistry");
|
|
52
52
|
const paths_1 = require("../agentpack/paths");
|
|
53
|
+
const credential_context_1 = require("../cli/credential-context");
|
|
53
54
|
const utils_1 = require("../utils");
|
|
54
55
|
const resolveNodeBinary_1 = require("../utils/resolveNodeBinary");
|
|
55
56
|
const customEngineIpc_1 = require("./customEngineIpc");
|
|
@@ -494,7 +495,12 @@ class CustomEngine {
|
|
|
494
495
|
const customBaseDir = this.config.customEnginesStoragePath
|
|
495
496
|
?? path.join(this.config.storagePath, paths_1.AGENT_CONSTANTS.AGENTS_DIR, agentId, 'custom');
|
|
496
497
|
const engineBaseDir = path.join(customBaseDir, manifest.id);
|
|
497
|
-
|
|
498
|
+
// Per-user, on the same rule claude and codex use. This engine had NO user scoping at all
|
|
499
|
+
// — in serve or in a sandbox — so on a multi-user deployment every custom engine's home
|
|
500
|
+
// (its config, caches, and whatever the engine chooses to persist) was one shared
|
|
501
|
+
// directory. `home` is the one that needed it: engineSessionDir is already per session.
|
|
502
|
+
// No verified user ⇒ unchanged path, so the IDE and single-tenant CLI are unaffected.
|
|
503
|
+
const engineHomeDir = (0, paths_1.userScopedEngineHomeFromKey)(path.join(engineBaseDir, 'home'), (0, credential_context_1.resolveEngineHomeUserKey)(options.engineHomeUserKey));
|
|
498
504
|
const engineSessionDir = path.join(engineBaseDir, paths_1.AGENT_CONSTANTS.SESSIONS_DIR, sessionDirName);
|
|
499
505
|
await fs_1.promises.mkdir(engineHomeDir, { recursive: true });
|
|
500
506
|
await fs_1.promises.mkdir(engineSessionDir, { recursive: true });
|