@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.
Files changed (50) hide show
  1. package/out/agentpack/index.js +3 -1
  2. package/out/agentpack/paths.js +25 -2
  3. package/out/analytics/dashboard/localFleetAdapter.js +82 -2
  4. package/out/analytics/service.js +6 -0
  5. package/out/analytics/sessionEvents.js +4 -3
  6. package/out/auth-broker/server.js +23 -19
  7. package/out/cli/commands/data-plane.js +84 -0
  8. package/out/cli/commands/sandbox.js +28 -9
  9. package/out/cli/commands/serve.js +289 -97
  10. package/out/cli/credential-context.js +17 -0
  11. package/out/cli/data-plane/index.js +2 -1
  12. package/out/cli/data-plane/sidecar.js +66 -2
  13. package/out/cli/deploy/layers/identity.js +5 -2
  14. package/out/cli/deploy-init.js +4 -4
  15. package/out/cli/index.js +16 -1
  16. package/out/cli/serve-hardening.js +181 -12
  17. package/out/engines/claude.js +1 -1
  18. package/out/engines/codex.js +1 -1
  19. package/out/engines/custom.js +7 -1
  20. package/out/evidence/sessionShip.js +50 -17
  21. package/out/headless/index.js +8 -1
  22. package/out/pack/governor.js +32 -15
  23. package/out/pack/lease-schema.js +4 -1
  24. package/out/pack/lease.js +42 -0
  25. package/out/runner/host.js +24 -14
  26. package/out/runner/protocol.js +24 -4
  27. package/out/runner/remoteEngine.js +53 -17
  28. package/out/sandbox/containerDriver.js +4 -2
  29. package/out/sandbox/processDriver.js +4 -1
  30. package/out/sandbox/sandboxRunner.js +148 -36
  31. package/out/scripts/agentide_litellm_hooks_v1.py +85 -1
  32. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +4 -0
  33. package/out/scripts/analytics/setup-analytics.ps1 +8 -1
  34. package/out/scripts/analytics/setup-analytics.sh +13 -2
  35. package/out/test/caps-governor.test.js +6 -1
  36. package/out/test/data-plane-engine-wiring.test.js +32 -0
  37. package/out/test/data-plane.test.js +313 -0
  38. package/out/test/deploy-init.test.js +7 -1
  39. package/out/test/governor-hardening.test.js +6 -1
  40. package/out/test/lease-validation.test.js +55 -0
  41. package/out/test/localFleetAdapter.test.js +91 -0
  42. package/out/test/sandbox-driver.test.js +2 -2
  43. package/out/test/sandbox-hardening.test.js +12 -1
  44. package/out/test/sandbox-runner.test.js +1 -1
  45. package/out/test/sandbox-scope.test.js +449 -0
  46. package/out/test/serve-cron-acceptance.test.js +370 -0
  47. package/out/test/serve-gateway-turn.test.js +122 -0
  48. package/out/test/serve-hardening.test.js +182 -9
  49. package/out/test/sessionShip.test.js +58 -0
  50. package/package.json +1 -1
@@ -3,7 +3,7 @@
3
3
  * Copyright (c) AgentHippo.ai. All rights reserved.
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.checkGitHubUpdate = exports.installFromGitHub = exports.validateForPublish = exports.publishToGitHub = exports.getGitHubUser = exports.isGitHubAuthenticated = exports.getGitHubToken = exports.formatGitHubSource = exports.parseGitHubSource = exports.resetDefaultAgent = exports.duplicateAgent = exports.updateFromUserLibrary = exports.checkForUpdate = exports.readAgentSource = exports.promoteToUserLibrary = exports.hydrateFromUserLibrary = exports.isAgentInUserLibrary = exports.isWorkspaceAgentAvailable = exports.listUserLibraryAgents = exports.updateManifestName = exports.parseAgentManifest = exports.loadAgentManifest = exports.stringifyYaml = exports.parseYaml = exports.AgentPackManager = exports.getUserAgentCurrentPath = exports.getUserAgentVersionDir = exports.getUserAgentRoot = exports.getUserAgentsDir = exports.getAgentideHome = exports.parseAgentIdFromPath = exports.getSessionsDir = exports.getAgentMcpDir = exports.getAgentRulesDir = exports.getAgentSkillsDir = exports.getAgentContextPath = exports.getAgentAgentsMdPath = exports.getAgentManifestPath = exports.isDefaultAgent = exports.userScopedEngineHome = exports.resolveRuntimePaths = exports.resolveAgentRuntimePaths = exports.getAgentRoot = exports.getAgentsDir = exports.getAgentideDir = exports.AGENT_CONSTANTS = void 0;
6
+ exports.checkGitHubUpdate = exports.installFromGitHub = exports.validateForPublish = exports.publishToGitHub = exports.getGitHubUser = exports.isGitHubAuthenticated = exports.getGitHubToken = exports.formatGitHubSource = exports.parseGitHubSource = exports.resetDefaultAgent = exports.duplicateAgent = exports.updateFromUserLibrary = exports.checkForUpdate = exports.readAgentSource = exports.promoteToUserLibrary = exports.hydrateFromUserLibrary = exports.isAgentInUserLibrary = exports.isWorkspaceAgentAvailable = exports.listUserLibraryAgents = exports.updateManifestName = exports.parseAgentManifest = exports.loadAgentManifest = exports.stringifyYaml = exports.parseYaml = exports.AgentPackManager = exports.getUserAgentCurrentPath = exports.getUserAgentVersionDir = exports.getUserAgentRoot = exports.getUserAgentsDir = exports.getAgentideHome = exports.parseAgentIdFromPath = exports.getSessionsDir = exports.getAgentMcpDir = exports.getAgentRulesDir = exports.getAgentSkillsDir = exports.getAgentContextPath = exports.getAgentAgentsMdPath = exports.getAgentManifestPath = exports.isDefaultAgent = exports.engineHomeUserKey = exports.userScopedEngineHomeFromKey = exports.userScopedEngineHome = exports.resolveRuntimePaths = exports.resolveAgentRuntimePaths = exports.getAgentRoot = exports.getAgentsDir = exports.getAgentideDir = exports.AGENT_CONSTANTS = void 0;
7
7
  // Path utilities
8
8
  var paths_1 = require("./paths");
9
9
  Object.defineProperty(exports, "AGENT_CONSTANTS", { enumerable: true, get: function () { return paths_1.AGENT_CONSTANTS; } });
@@ -13,6 +13,8 @@ Object.defineProperty(exports, "getAgentRoot", { enumerable: true, get: function
13
13
  Object.defineProperty(exports, "resolveAgentRuntimePaths", { enumerable: true, get: function () { return paths_1.resolveAgentRuntimePaths; } });
14
14
  Object.defineProperty(exports, "resolveRuntimePaths", { enumerable: true, get: function () { return paths_1.resolveRuntimePaths; } });
15
15
  Object.defineProperty(exports, "userScopedEngineHome", { enumerable: true, get: function () { return paths_1.userScopedEngineHome; } });
16
+ Object.defineProperty(exports, "userScopedEngineHomeFromKey", { enumerable: true, get: function () { return paths_1.userScopedEngineHomeFromKey; } });
17
+ Object.defineProperty(exports, "engineHomeUserKey", { enumerable: true, get: function () { return paths_1.engineHomeUserKey; } });
16
18
  Object.defineProperty(exports, "isDefaultAgent", { enumerable: true, get: function () { return paths_1.isDefaultAgent; } });
17
19
  Object.defineProperty(exports, "getAgentManifestPath", { enumerable: true, get: function () { return paths_1.getAgentManifestPath; } });
18
20
  Object.defineProperty(exports, "getAgentAgentsMdPath", { enumerable: true, get: function () { return paths_1.getAgentAgentsMdPath; } });
@@ -52,6 +52,8 @@ exports.getAgentRoot = getAgentRoot;
52
52
  exports.resolveAgentRuntimePaths = resolveAgentRuntimePaths;
53
53
  exports.resolveRuntimePaths = resolveRuntimePaths;
54
54
  exports.userScopedEngineHome = userScopedEngineHome;
55
+ exports.engineHomeUserKey = engineHomeUserKey;
56
+ exports.userScopedEngineHomeFromKey = userScopedEngineHomeFromKey;
55
57
  exports.isDefaultAgent = isDefaultAgent;
56
58
  exports.getAgentManifestPath = getAgentManifestPath;
57
59
  exports.getAgentAgentsMdPath = getAgentAgentsMdPath;
@@ -227,12 +229,33 @@ function resolveRuntimePaths(workspaceRoot, storageUri, agentId, agentVersion) {
227
229
  * The user id is hashed — it is an email/subject, never safe as a raw path segment.
228
230
  */
229
231
  function userScopedEngineHome(baseHome, verifiedUserId) {
232
+ return userScopedEngineHomeFromKey(baseHome, engineHomeUserKey(verifiedUserId));
233
+ }
234
+ /**
235
+ * The opaque per-user directory segment, derived from a verified identity.
236
+ *
237
+ * Split out from `userScopedEngineHome` so it can cross a process boundary. A sandbox child has
238
+ * no `runWithCredential` frame — the credential deliberately never leaves broker memory — so
239
+ * before this existed every engine inside a tier-0 sandbox resolved the UNSCOPED base dir and
240
+ * every user's transcripts landed in one directory. The key is safe to send where the identity is
241
+ * not: it is a one-way hash, it authorizes nothing, and the child only ever uses it as a path
242
+ * segment.
243
+ */
244
+ function engineHomeUserKey(verifiedUserId) {
230
245
  const uid = verifiedUserId?.trim();
231
246
  if (!uid) {
247
+ return undefined;
248
+ }
249
+ return crypto.createHash('sha256').update(uid).digest('hex').slice(0, 16);
250
+ }
251
+ /** `userScopedEngineHome` for a caller that already holds the key (i.e. inside a sandbox). */
252
+ function userScopedEngineHomeFromKey(baseHome, userKey) {
253
+ // A key from the wire is a path segment, so it is checked rather than trusted: anything but
254
+ // the hex this function produces would let a caller aim an engine home anywhere on disk.
255
+ if (!userKey || !/^[0-9a-f]{16}$/.test(userKey)) {
232
256
  return baseHome;
233
257
  }
234
- const hash = crypto.createHash('sha256').update(uid).digest('hex').slice(0, 16);
235
- return path.join(baseHome, 'users', hash);
258
+ return path.join(baseHome, 'users', userKey);
236
259
  }
237
260
  /**
238
261
  * Check if an agent is the default workspace agent.
@@ -36,12 +36,14 @@ var __importStar = (this && this.__importStar) || (function () {
36
36
  };
37
37
  })();
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.mergeSessionJsonlIntoFleetRows = mergeSessionJsonlIntoFleetRows;
39
40
  exports.getScheduledFleetRequests = getScheduledFleetRequests;
40
41
  exports.createLocalFleetAdapter = createLocalFleetAdapter;
41
42
  const vscode = __importStar(require("vscode"));
42
43
  const fleetTypes_1 = require("./fleetTypes");
43
44
  const fleetDisplayUtils_1 = require("./fleetDisplayUtils");
44
45
  const fleetRowDefaults_1 = require("./fleetRowDefaults");
46
+ const sessionJsonlFleet_1 = require("./sessionJsonlFleet");
45
47
  const scheduledFleetStorage_1 = require("./scheduledFleetStorage");
46
48
  const LOCAL_SOURCE_ID = 'local';
47
49
  const FUTURE_START_SCHEDULED_MS = 60_000;
@@ -101,12 +103,87 @@ function toRow(sessionId, agentId, startedAt, endedAt, status, taskTitle, model,
101
103
  disconnectCapable: false,
102
104
  });
103
105
  }
106
+ function scheduleSnapshotByChatSessionId(scheduled) {
107
+ const map = new Map();
108
+ for (const item of scheduled) {
109
+ const sessionId = String(item.sessionId ?? '').trim();
110
+ if (!sessionId) {
111
+ continue;
112
+ }
113
+ const existing = map.get(sessionId);
114
+ if (!existing || item.runAt >= existing.runAt) {
115
+ map.set(sessionId, item);
116
+ }
117
+ }
118
+ return map;
119
+ }
120
+ function applyJsonlHintToRow(row, hint) {
121
+ row.turns = Math.max(row.turns, hint.turns);
122
+ if (hint.startedAt) {
123
+ row.startedAt = hint.startedAt;
124
+ }
125
+ if (hint.endedAt) {
126
+ row.endedAt = hint.endedAt;
127
+ }
128
+ // JSONL turn events are authoritative when traces are missing or still show in_progress.
129
+ if (hint.turns > 0 || hint.status === 'done') {
130
+ row.status = hint.status;
131
+ }
132
+ else if (row.status === 'in_progress' && hint.status !== 'in_progress') {
133
+ row.status = hint.status;
134
+ }
135
+ }
136
+ function jsonlHintToRow(hint, schedule) {
137
+ const messagePreview = schedule?.messageText
138
+ ? (schedule.messageText.length > 80
139
+ ? `${schedule.messageText.slice(0, 77)}...`
140
+ : schedule.messageText)
141
+ : hint.sessionId;
142
+ return toRow(hint.sessionId, schedule?.agentId, hint.startedAt, hint.endedAt, hint.status, messagePreview, schedule?.model ?? 'unknown', schedule?.engine ?? (0, fleetDisplayUtils_1.inferFleetEngineId)(schedule?.model), hint.turns, 0);
143
+ }
144
+ /** Drop schedule:* cards when the linked chat session is live or finished. */
145
+ function dropScheduleRowsSupersededByChatSession(sessionsById) {
146
+ for (const key of [...sessionsById.keys()]) {
147
+ if (!key.startsWith('schedule:')) {
148
+ continue;
149
+ }
150
+ const scheduleRow = sessionsById.get(key);
151
+ if (!scheduleRow) {
152
+ continue;
153
+ }
154
+ const linked = String(scheduleRow.channelTarget ?? '').trim();
155
+ if (!linked) {
156
+ continue;
157
+ }
158
+ const chatRow = sessionsById.get(linked);
159
+ if (chatRow && chatRow.status !== 'scheduled') {
160
+ sessionsById.delete(key);
161
+ }
162
+ }
163
+ }
164
+ /** @internal Exported for unit tests. */
165
+ function mergeSessionJsonlIntoFleetRows(sessionsById, scheduleByChatSessionId, filters, sessionsDir) {
166
+ for (const hint of (0, sessionJsonlFleet_1.scanSessionJsonlFleetHints)(sessionsDir)) {
167
+ const schedule = scheduleByChatSessionId.get(hint.sessionId);
168
+ if (filters.agentId && schedule?.agentId && schedule.agentId !== filters.agentId) {
169
+ continue;
170
+ }
171
+ const existing = sessionsById.get(hint.sessionId);
172
+ if (existing) {
173
+ applyJsonlHintToRow(existing, hint);
174
+ continue;
175
+ }
176
+ sessionsById.set(hint.sessionId, jsonlHintToRow(hint, schedule));
177
+ }
178
+ dropScheduleRowsSupersededByChatSession(sessionsById);
179
+ }
104
180
  /** Returns scheduled chat requests for fleet merge (used by orchestrator for formal local script). */
105
181
  async function getScheduledFleetRequests() {
106
182
  return (0, scheduledFleetStorage_1.loadScheduledFleetRequests)();
107
183
  }
108
184
  /**
109
- * Local fleet source adapter: trace-derived sessions + scheduled chat requests, merged and status-normalized.
185
+ * Local fleet source adapter: trace-derived sessions, session JSONL hints, and scheduled chat
186
+ * requests, merged and status-normalized.
110
187
  */
111
188
  function createLocalFleetAdapter(queryRunner) {
112
189
  return {
@@ -120,7 +197,9 @@ function createLocalFleetAdapter(queryRunner) {
120
197
  // Trace DB may be missing; continue with scheduled only.
121
198
  }
122
199
  const scheduledRequests = await getScheduledFleetRequests();
123
- const scheduleHintsByChatSessionId = (0, fleetDisplayUtils_1.buildScheduleHintsByChatSessionId)(await (0, scheduledFleetStorage_1.loadScheduledFleetEnrichmentHints)());
200
+ const scheduleEnrichment = await (0, scheduledFleetStorage_1.loadScheduledFleetEnrichmentHints)();
201
+ const scheduleByChatSessionId = scheduleSnapshotByChatSessionId(scheduleEnrichment);
202
+ const scheduleHintsByChatSessionId = (0, fleetDisplayUtils_1.buildScheduleHintsByChatSessionId)(scheduleEnrichment);
124
203
  const sessionsById = new Map();
125
204
  for (const session of sessions) {
126
205
  const status = normalizeStatus(session);
@@ -153,6 +232,7 @@ function createLocalFleetAdapter(queryRunner) {
153
232
  });
154
233
  sessionsById.set(scheduleRowKey, scheduleRow);
155
234
  }
235
+ mergeSessionJsonlIntoFleetRows(sessionsById, scheduleByChatSessionId, filters);
156
236
  const sessionIdsForTitles = [...new Set(Array.from(sessionsById.values()).flatMap(row => {
157
237
  const ids = [];
158
238
  if (row.sessionId && !row.sessionId.startsWith('schedule:')) {
@@ -920,6 +920,12 @@ class AnalyticsService {
920
920
  ? ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', scriptPath, ...args]
921
921
  : [scriptPath, ...args];
922
922
  const env = { ...process.env, AGENT_HOME_DIR: (0, index_1.getAgentideHome)() };
923
+ // IDE-launched proxies default to content capture ON so Spotlight can show prompts and
924
+ // correlate sessions; serve/gateway deployments launch LiteLLM elsewhere and keep the
925
+ // metadata-only default. An explicit AGENTHIPPO_EVIDENCE_CONTENT_CAPTURE=0 still wins.
926
+ if (!env.AGENTHIPPO_EVIDENCE_CONTENT_CAPTURE) {
927
+ env.AGENTHIPPO_EVIDENCE_CONTENT_CAPTURE = '1';
928
+ }
923
929
  if (opts.configPath) {
924
930
  env.LITELLM_CONFIG_PATH = opts.configPath;
925
931
  }
@@ -37,9 +37,10 @@ var __importStar = (this && this.__importStar) || (function () {
37
37
  };
38
38
  })();
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
- exports.ObservableStreamEmitter = exports.SessionEventWriter = void 0;
40
+ exports.ObservableStreamEmitter = exports.SessionEventWriter = exports.EVIDENCE_REQUIRED_ENV = void 0;
41
41
  exports.getSessionEventWriterStats = getSessionEventWriterStats;
42
42
  exports.resetSessionEventWriterStatsForTest = resetSessionEventWriterStatsForTest;
43
+ exports.evidenceRequired = evidenceRequired;
43
44
  exports.wrapEmitterWithObservability = wrapEmitterWithObservability;
44
45
  /**
45
46
  * Session Event Writer & Observable Stream Emitter
@@ -84,9 +85,9 @@ function resetSessionEventWriterStatsForTest() {
84
85
  * fails instead of proceeding unrecorded. Off by default — an IDE user should not lose a turn
85
86
  * because a disk was full — but a regulated deployment wants the opposite trade.
86
87
  */
87
- const EVIDENCE_REQUIRED_ENV = 'AGENTHIPPO_EVIDENCE_REQUIRED';
88
+ exports.EVIDENCE_REQUIRED_ENV = 'AGENTHIPPO_EVIDENCE_REQUIRED';
88
89
  function evidenceRequired(env = process.env) {
89
- const v = (env[EVIDENCE_REQUIRED_ENV] ?? '').trim().toLowerCase();
90
+ const v = (env[exports.EVIDENCE_REQUIRED_ENV] ?? '').trim().toLowerCase();
90
91
  return v === '1' || v === 'true' || v === 'yes' || v === 'on';
91
92
  }
92
93
  class SessionEventWriter {
@@ -36,8 +36,8 @@ var __importStar = (this && this.__importStar) || (function () {
36
36
  };
37
37
  })();
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.hasTrustedProxyProof = void 0;
39
40
  exports.isAllowedRedirectUri = isAllowedRedirectUri;
40
- exports.hasTrustedProxyProof = hasTrustedProxyProof;
41
41
  exports.createAuthBrokerServer = createAuthBrokerServer;
42
42
  exports.startAuthBrokerServer = startAuthBrokerServer;
43
43
  const http = __importStar(require("http"));
@@ -103,24 +103,13 @@ function isAllowedRedirectUri(redirectUri) {
103
103
  * Does this request carry the trusted proxy's proof?
104
104
  *
105
105
  * `X-Auth-Request-Email` is only meaningful when it was set by our oauth2-proxy sidecar after a real
106
- * Google login. Nothing about the header says so, so the proxy also stamps a shared secret and this
107
- * is where it is checked — constant-time, so a mismatched secret cannot be recovered byte by byte.
106
+ * Google login. Nothing about the header says so, so the proxy also stamps a shared secret which is
107
+ * checked constant-time, so a mismatched secret cannot be recovered byte by byte. The check lives in
108
+ * serve-hardening (serve applies the same contract to its identity header via
109
+ * AGENTHIPPO_IDENTITY_PROXY_SECRET); re-exported here for the broker's existing consumers.
108
110
  */
109
- function hasTrustedProxyProof(headers, expectedSecret) {
110
- if (!expectedSecret) {
111
- return false;
112
- }
113
- const raw = headers[config_js_1.AUTH_PROXY_SECRET_HEADER];
114
- const presented = typeof raw === 'string' ? raw : Array.isArray(raw) ? String(raw[0] ?? '') : '';
115
- if (!presented) {
116
- return false;
117
- }
118
- // Hash both sides first: timingSafeEqual throws on a length mismatch, which would itself leak
119
- // the secret's length.
120
- const a = crypto.createHash('sha256').update(presented).digest();
121
- const b = crypto.createHash('sha256').update(expectedSecret).digest();
122
- return crypto.timingSafeEqual(a, b);
123
- }
111
+ var serve_hardening_js_2 = require("../cli/serve-hardening.js");
112
+ Object.defineProperty(exports, "hasTrustedProxyProof", { enumerable: true, get: function () { return serve_hardening_js_2.hasTrustedProxyProof; } });
124
113
  function sendJson(res, status, body) {
125
114
  res.writeHead(status, { 'Content-Type': 'application/json' });
126
115
  res.end(JSON.stringify(body));
@@ -335,7 +324,7 @@ function createAuthBrokerServer(config) {
335
324
  if (resolved.devMode || (!resolved.proxySecret && resolved.trustProxyHeadersUnverified)) {
336
325
  return email;
337
326
  }
338
- if (!hasTrustedProxyProof(req.headers, resolved.proxySecret)) {
327
+ if (!(0, serve_hardening_js_1.hasTrustedProxyProof)(req.headers, resolved.proxySecret)) {
339
328
  return null;
340
329
  }
341
330
  return email;
@@ -613,6 +602,21 @@ function createAuthBrokerServer(config) {
613
602
  // Set X-Agent-User so Caddy's forward_auth can copy the validated identity
614
603
  // into the upstream request without serve needing its own JWT validation.
615
604
  res.setHeader('X-Agent-User', user.email || user.id);
605
+ // …and the ASSERTION alongside the identifier (PLAN/agent-identity.md §8.5 G1).
606
+ // The identifier alone is all the edge forwarded until now, so an edge-registered
607
+ // turn carried no `inboundToken`: `passthrough` failed closed on every tool call
608
+ // and an RFC 8693 `exchange` provider had no subject_token. This is the same
609
+ // token this request just verified — the browser (cookie) lane has no
610
+ // Authorization header for the edge to relay, so it has to come from here.
611
+ //
612
+ // Not a new exposure: whatever presented this token — Bearer header or cookie —
613
+ // is already on the request Caddy proxies to the agent. What changes is that the
614
+ // connector sidecar can now reach it, which is the whole point.
615
+ const presented = (0, jwt_js_1.extractBearerToken)(req.headers.authorization)
616
+ || extractCookieToken(req.headers.cookie, resolved.cookieName);
617
+ if (presented) {
618
+ res.setHeader('X-Agent-User-Assertion', presented);
619
+ }
616
620
  sendJson(res, 200, store.buildMe(user));
617
621
  return;
618
622
  }
@@ -3,6 +3,7 @@
3
3
  * Copyright (c) AgentHippo.ai. All rights reserved.
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createDataPlaneAuditSink = createDataPlaneAuditSink;
6
7
  exports.dataPlaneSidecarCommand = dataPlaneSidecarCommand;
7
8
  /**
8
9
  * `agenthippo data-plane` — run the connector as a SIDECAR (secure-obo-design §7.6).
@@ -12,10 +13,70 @@ exports.dataPlaneSidecarCommand = dataPlaneSidecarCommand;
12
13
  * agent container's `serve` registers turns with this sidecar's authenticated control API and
13
14
  * never sees the base credential.
14
15
  */
16
+ const fs_1 = require("fs");
15
17
  const pack_loader_1 = require("../pack-loader");
16
18
  const data_plane_1 = require("../data-plane");
19
+ const serve_hardening_1 = require("../serve-hardening");
20
+ const sessionEvents_1 = require("../../analytics/sessionEvents");
21
+ const identity_1 = require("../../analytics/identity");
22
+ const base_1 = require("../../engines/base");
17
23
  const headless_1 = require("../../headless");
18
24
  const utils_1 = require("../utils");
25
+ /**
26
+ * Build the sidecar's audit sink.
27
+ *
28
+ * Until this existed the sidecar passed no `audit` to `startDataPlaneSidecar`, and since every
29
+ * emit site in the data plane is `opts.audit?.(…)` the entire trust-boundary record —
30
+ * `tool_authorize`, `tool_deny`, `sidecar_register_reject`, `sidecar_edge_register` — was
31
+ * discarded in sidecar and edge mode: the two shapes the blueprints actually deploy. The
32
+ * "which user touched which rows" claim had no writer exactly where it was being made.
33
+ *
34
+ * `ok: false` means the directory could not be prepared. The caller decides what that costs,
35
+ * using the existing AGENTHIPPO_EVIDENCE_REQUIRED contract rather than a new knob.
36
+ */
37
+ async function createDataPlaneAuditSink(stateDirOpt,
38
+ /**
39
+ * The pack THIS sidecar loaded — stamped onto every row so evidence answers "via which agent",
40
+ * not only "for which user" (PLAN/agent-identity.md §1 failure 3).
41
+ *
42
+ * There is no digest here on purpose: the sidecar does not verify a signature (`pack-loader.ts`
43
+ * has no signature code), so claiming one would dress an unverified read as an attestation.
44
+ * What serve ASSERTS about the pack arrives separately, per turn, via the register payload —
45
+ * and the two being different is the §4.4 detection.
46
+ */
47
+ agent) {
48
+ const { dir: stateDir, ephemeral } = (0, serve_hardening_1.resolveStateDir)(stateDirOpt);
49
+ const auditDir = (0, serve_hardening_1.resolveAnalyticsSessionsDir)(stateDir, ephemeral);
50
+ let ok = true;
51
+ let error;
52
+ // Preflight, rather than discovering it on the first denied tool call.
53
+ try {
54
+ await fs_1.promises.mkdir(auditDir, { recursive: true });
55
+ await fs_1.promises.access(auditDir);
56
+ }
57
+ catch (err) {
58
+ ok = false;
59
+ error = err instanceof Error ? err.message : String(err);
60
+ }
61
+ const writer = new sessionEvents_1.SessionEventWriter(auditDir);
62
+ const audit = (sessionKey, event, fields) => {
63
+ // Pseudonymise identity before it lands, matching the tool-event lane
64
+ // (ObservableStreamEmitter → hashIdentity). These records ship wholesale to the evidence
65
+ // server, so a raw address here leaves the machine; hashIdentity honours the operator's
66
+ // AGENTHIPPO_EVIDENCE_RAW_IDS / _ID_SALT choices so this lane makes the same trade as
67
+ // every other producer rather than a private one.
68
+ const rawUser = typeof fields.userId === 'string' ? fields.userId : undefined;
69
+ const anonUser = (0, identity_1.hashIdentity)(rawUser);
70
+ const safeFields = anonUser ? { ...fields, userId: anonUser } : fields;
71
+ // In EDGE mode the sidecar registers with `sessionKey: userId` (sidecar.ts:97), so the
72
+ // session key IS the identity and would otherwise become the evidence FILENAME —
73
+ // `alice@corp.com.jsonl`. Hash it the same way when, and only when, it is exactly the
74
+ // identity; a genuine session key is left alone so it still joins across lanes.
75
+ const safeSession = anonUser && sessionKey === rawUser ? anonUser : sessionKey;
76
+ void writer.append(safeSession, (0, base_1.generateRequestId)(), event, agent ? { ...safeFields, agent: { ...agent, digest: null } } : safeFields);
77
+ };
78
+ return { audit, auditDir, ephemeral, ok, error };
79
+ }
19
80
  async function dataPlaneSidecarCommand(options) {
20
81
  const registerToken = process.env.AGENTHIPPO_DATA_PLANE_TOKEN?.trim();
21
82
  if (!registerToken) {
@@ -38,6 +99,23 @@ async function dataPlaneSidecarCommand(options) {
38
99
  return;
39
100
  }
40
101
  const logger = options.verbose ? new headless_1.ConsoleLogger('[Sidecar]') : new headless_1.NoOpLogger();
102
+ const { audit: auditEvent, auditDir, ephemeral, ok: auditOk, error: auditError } = await createDataPlaneAuditSink(options.stateDir, {
103
+ pack: pack.manifest.metadata.name,
104
+ version: pack.manifest.metadata.version ?? '0.0.0',
105
+ });
106
+ if (!auditOk) {
107
+ const detail = `${auditDir}: ${auditError}`;
108
+ // The existing AGENTHIPPO_EVIDENCE_REQUIRED contract decides what an unwritable sink
109
+ // costs — no new knob, and it already means "a hole in the record must fail the turn".
110
+ if ((0, sessionEvents_1.evidenceRequired)()) {
111
+ console.error(`${utils_1.COLORS.red}Error: data-plane audit directory is not writable${utils_1.COLORS.reset} — ${detail}`);
112
+ console.error(`AGENTHIPPO_EVIDENCE_REQUIRED is set, so refusing to run an unauditable data plane. Unset it to downgrade this to a warning.`);
113
+ process.exit(1);
114
+ return;
115
+ }
116
+ console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} data-plane audit directory is not writable — ${detail}`);
117
+ console.warn(' Tool authorizations and denials will NOT be recorded. Set --state-dir or AGENTHIPPO_HOME to a writable path.');
118
+ }
41
119
  let sidecar;
42
120
  try {
43
121
  sidecar = await (0, data_plane_1.startDataPlaneSidecar)({
@@ -47,6 +125,11 @@ async function dataPlaneSidecarCommand(options) {
47
125
  controlPort: options.controlPort,
48
126
  bindHost: options.host ?? '0.0.0.0',
49
127
  logger,
128
+ audit: auditEvent,
129
+ agent: {
130
+ pack: pack.manifest.metadata.name,
131
+ version: pack.manifest.metadata.version ?? '0.0.0',
132
+ },
50
133
  });
51
134
  }
52
135
  catch (err) {
@@ -57,6 +140,7 @@ async function dataPlaneSidecarCommand(options) {
57
140
  console.log(`OK: Data-plane sidecar (provider=${cfg.provider ?? 'passthrough'}) — connector: ${cfg.connectorPath}`);
58
141
  console.log(` gateway (engine dials, nonce-auth): ${sidecar.gatewayUrl}`);
59
142
  console.log(` control (edge/serve registers, bearer): :${sidecar.controlPort}`);
143
+ console.log(` audit (tool_authorize / tool_deny / register events): ${auditDir}${ephemeral ? ' (EPHEMERAL — lost on restart; set AGENTHIPPO_HOME or --state-dir)' : ''}`);
60
144
  console.log(' Base credentials live here only — never in the agent container.');
61
145
  console.log(' Edge mode (P1): give the register token to the IdP edge only; serve runs with AGENTHIPPO_DATA_PLANE_EDGE=1 and no token.');
62
146
  const shutdown = () => { void sidecar.close().finally(() => process.exit(0)); };
@@ -84,8 +84,8 @@ function isForeignWorkspace(wireRoot, sandboxWorkspace) {
84
84
  }
85
85
  /**
86
86
  * Adapts the headless runner to `AgentEngine` so `startRunnerHost` can drive it. The model
87
- * credential is *per-run*: the sandbox rebuilds its runner if the token changes (rotation), and
88
- * never persists a provider key of its own.
87
+ * credential is *per-run* — it rides each frame rather than the runner — and this process never
88
+ * persists a provider key of its own.
89
89
  */
90
90
  class SandboxEngine {
91
91
  defaults;
@@ -101,7 +101,12 @@ class SandboxEngine {
101
101
  this.defaults = defaults;
102
102
  }
103
103
  ensureRunner(opts) {
104
- const key = `${opts.baseUrl ?? ''}|${opts.modelToken ?? ''}`;
104
+ // Keyed on baseUrl ALONE. The warrant is re-minted every turn (runner/remoteEngine.ts), so
105
+ // including it rebuilt — and disposed — the runner on every single turn. Harmless while a
106
+ // sandbox held one session and turns arrived one at a time; with several of a user's
107
+ // sessions here, that dispose lands in the middle of another session's stream. The token
108
+ // now travels per run instead (`modelToken` below).
109
+ const key = opts.baseUrl ?? '';
105
110
  if (this.runner && this.builtWith === key) {
106
111
  return this.runner;
107
112
  }
@@ -117,6 +122,8 @@ class SandboxEngine {
117
122
  workspaceRoot: this.defaults.workspace,
118
123
  credentials: {
119
124
  baseUrl: opts.baseUrl,
125
+ // The warrant this runner was BUILT with, kept only as the fallback for a frame
126
+ // that carries none; every turn normally supplies its own.
120
127
  litellmApiKey: opts.modelToken,
121
128
  useLiteLLM: Boolean(opts.baseUrl),
122
129
  },
@@ -136,10 +143,15 @@ class SandboxEngine {
136
143
  }
137
144
  try {
138
145
  fs.mkdirSync(workspaceRoot, { recursive: true });
139
- // A sandbox hosts exactly one session, so its working directory IS that session's
140
- // workspace. Without the chdir the engine inherits the sandbox process's cwd and a tool
141
- // that writes a RELATIVE path (codex runs `sh -lc 'printf ... > proof.txt'`) lands the
142
- // file outside the workspace — it exits 0, the agent reports success, work is lost.
146
+ // Without the chdir the engine inherits the sandbox process's cwd and a tool that writes
147
+ // a RELATIVE path (codex runs `sh -lc 'printf ... > proof.txt'`) lands the file outside
148
+ // the workspace — it exits 0, the agent reports success, work is lost.
149
+ //
150
+ // INVARIANT: workspace scope must never be narrower than sandbox scope. chdir is
151
+ // process-global; concurrent turns in one sandbox are only safe when every session
152
+ // resolves to the same workspaceRoot (serve's sandboxFsFor must key writable mounts at
153
+ // sandbox scope, not session scope). The chdir is then idempotent. Per-session workspaces
154
+ // inside a shared sandbox would race here — stream demuxing would not fix it.
143
155
  process.chdir(workspaceRoot);
144
156
  }
145
157
  catch (err) {
@@ -165,11 +177,17 @@ class SandboxEngine {
165
177
  workspaceRoot,
166
178
  additionalDirectories: wire.additionalDirectories,
167
179
  sessionId: wire.sessionId,
180
+ // Per-turn warrant, not the one this runner was built with — see ensureRunner.
181
+ modelToken: wire.modelToken,
168
182
  permissions: perms,
169
183
  // Parity with serve's in-process runner: attribution for evidence, the per-turn
170
184
  // data-plane nonce (without which a sandbox cannot reach the data gateway at all), and
171
185
  // the bound gateway session.
172
186
  analyticsUserId: wire.analyticsUserId,
187
+ // The credential does not cross into this process by design, so the per-user engine
188
+ // home key arrives pre-computed on the frame. Absent ⇒ unscoped, which is correct for
189
+ // a single-tenant run and is what every multi-user sandbox turn used to get.
190
+ engineHomeUserKey: wire.engineHomeUserKey,
173
191
  turnEnv: wire.turnEnv,
174
192
  gatewaySessionKey: wire.gatewaySessionKey,
175
193
  emitter,
@@ -185,8 +203,9 @@ class SandboxEngine {
185
203
  dispose() { this.runner?.dispose(); }
186
204
  }
187
205
  /**
188
- * A sandbox hosts ONE session for its whole life, so an engine-side async error must not take the
189
- * process down — that would lose the session, not just the turn. Vendor SDKs reject outside the
206
+ * A sandbox outlives every turn it runs — and under user scope holds several of one person's
207
+ * sessions — so an engine-side async error must not take the process down: that would lose every
208
+ * conversation here, not just the turn. Vendor SDKs reject outside the
190
209
  * `engine.stream()` await chain (observed: the Claude SDK's `streamInput` rejecting after its
191
210
  * child process dies), which Node treats as fatal by default. Log and stay up; the turn's own
192
211
  * catch still emits `failed`, and serve can always destroy the sandbox if it wants it gone.