@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
package/out/agentpack/index.js
CHANGED
|
@@ -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; } });
|
package/out/agentpack/paths.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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:')) {
|
package/out/analytics/service.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
107
|
-
*
|
|
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
|
-
|
|
110
|
-
|
|
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
|
|
88
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
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
|
|
189
|
-
*
|
|
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.
|