@agenthippo-ai/agenthippo 0.2.3-dataplane → 1.110.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/agentpack/index.js +3 -1
- package/out/agentpack/paths.js +25 -2
- package/out/analytics/dashboard/localFleetAdapter.js +82 -2
- package/out/analytics/dashboard/provider.js +13 -2
- package/out/analytics/dashboard/webview/panel.js +3 -2
- package/out/analytics/identity.js +0 -0
- package/out/analytics/service.js +29 -25
- package/out/analytics/sessionEvents.js +100 -24
- package/out/auth/authBaseUrl.js +59 -0
- package/out/auth/bootstrapApply.js +9 -10
- package/out/auth/brokerClient.js +48 -14
- package/out/auth/provider.js +8 -21
- package/out/auth/sessionStore.js +3 -40
- package/out/auth/signInFlow.js +2 -1
- package/out/auth-broker/config.js +50 -5
- package/out/auth-broker/server.js +126 -26
- package/out/auth-broker/store.js +25 -24
- package/out/authority/warrant.js +206 -0
- package/out/branding.js +27 -1
- package/out/cli/commands/conformance.js +249 -0
- package/out/cli/commands/data-plane.js +151 -0
- package/out/cli/commands/deploy.js +1 -0
- package/out/cli/commands/gateway.js +4 -0
- package/out/cli/commands/pack.js +182 -3
- package/out/cli/commands/sandbox.js +351 -0
- package/out/cli/commands/serve.js +1145 -130
- package/out/cli/commands/store.js +12 -4
- package/out/cli/credential-context.js +17 -0
- package/out/cli/data-plane/gateway.js +28 -5
- package/out/cli/data-plane/index.js +18 -2
- package/out/cli/data-plane/registry.js +9 -0
- package/out/cli/data-plane/remote.js +106 -0
- package/out/cli/data-plane/sidecar.js +225 -0
- package/out/cli/deploy/layers/identity.js +88 -4
- package/out/cli/deploy-init.js +89 -24
- package/out/cli/deps.js +106 -2
- package/out/cli/idp-init.js +35 -0
- package/out/cli/index.js +171 -17
- package/out/cli/pack-install-ref.js +15 -0
- package/out/cli/pack-loader.js +8 -1
- package/out/cli/pack-runtime.js +61 -0
- package/out/cli/runtime.js +35 -1
- package/out/cli/serve-hardening.js +260 -14
- package/out/cli/utils.js +12 -1
- package/out/engines/base.js +32 -3
- package/out/engines/claude.js +33 -15
- package/out/engines/codex.js +111 -27
- package/out/engines/custom.js +15 -1
- package/out/evidence/sessionShip.js +643 -0
- package/out/extension.js +11 -2
- package/out/gateway/bridge.js +40 -2
- package/out/gateway/client.js +34 -1
- package/out/gateway/installSource.js +68 -0
- package/out/gateway/lifecycle.js +185 -51
- package/out/gateway/outboundPaths.js +4 -1
- package/out/gateway/secretsPreflight.js +90 -0
- package/out/gateway/service.js +7 -2
- package/out/headless/index.js +25 -2
- package/out/live-e2e/mcp-demo/server.mjs +32 -0
- package/out/mcp.js +30 -0
- package/out/pack/caps.js +271 -0
- package/out/pack/governor.js +326 -0
- package/out/pack/lease-schema.js +273 -0
- package/out/pack/lease.js +682 -0
- package/out/pack/reconcile.js +217 -0
- package/out/pack/signature.js +839 -0
- package/out/remote/handleRemoteChat.js +44 -3
- package/out/remote/remoteTrust.js +277 -0
- package/out/remote/serveClient.js +52 -5
- package/out/runner/host.js +141 -0
- package/out/runner/protocol.js +128 -0
- package/out/runner/remoteEngine.js +166 -0
- package/out/runtime/interfaces/edit-tracker.js +2 -0
- package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
- package/out/sandbox/containerDriver.js +572 -0
- package/out/sandbox/processDriver.js +318 -0
- package/out/sandbox/sandboxRunner.js +303 -0
- package/out/sandbox/support.js +301 -0
- package/out/sandbox/types.js +55 -0
- package/out/scripts/agentide-litellm.sh +67 -5
- package/out/scripts/agentide_litellm_hooks_v1.py +266 -21
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +98 -132
- package/out/scripts/analytics/setup-analytics.ps1 +45 -14
- package/out/scripts/analytics/setup-analytics.sh +106 -15
- package/out/scripts/analytics/start-analytics.sh +86 -14
- package/out/scripts/gateway/setup-gateway.sh +72 -32
- package/out/shared/models.js +11 -110
- package/out/spotlight/install.js +7 -1
- package/out/store/index.js +41 -10
- package/out/sync.js +3 -1
- package/out/test/analytics.test.js +162 -23
- package/out/test/auth-baseurl.test.js +132 -0
- package/out/test/auth-bootstrapApply.test.js +27 -6
- package/out/test/auth-broker-hardening.test.js +227 -0
- package/out/test/auth-broker.test.js +59 -9
- package/out/test/branding.test.js +13 -0
- package/out/test/caps-governor.test.js +460 -0
- package/out/test/codex-new-file-survival.test.js +119 -0
- package/out/test/configuration-scopes.test.js +131 -0
- package/out/test/data-plane-engine-wiring.test.js +32 -0
- package/out/test/data-plane.test.js +380 -0
- package/out/test/deploy-init.test.js +47 -1
- package/out/test/gateway-client.test.js +28 -0
- package/out/test/gateway-install-source.test.js +111 -0
- package/out/test/gateway-lifecycle.test.js +1 -1
- package/out/test/gateway-outbound.test.js +7 -0
- package/out/test/gateway-secrets-preflight.test.js +127 -0
- package/out/test/gateway-session-routing.test.js +18 -0
- package/out/test/governor-hardening.test.js +222 -0
- package/out/test/headless-sync.test.js +22 -0
- package/out/test/lease-signature.test.js +357 -0
- package/out/test/lease-trust-root.test.js +273 -0
- package/out/test/lease-validation.test.js +224 -0
- package/out/test/lease.test.js +114 -0
- package/out/test/localFleetAdapter.test.js +91 -0
- package/out/test/mcp.test.js +27 -1
- package/out/test/pack-admission.test.js +216 -0
- package/out/test/pack-digest-parity.test.js +140 -0
- package/out/test/pack-reconcile.test.js +183 -0
- package/out/test/pack-signature-artifact.test.js +103 -0
- package/out/test/pack-signature.test.js +176 -0
- package/out/test/pack-trust-root.test.js +202 -0
- package/out/test/remote-trust.test.js +242 -0
- package/out/test/runner-protocol.test.js +249 -0
- package/out/test/sandbox-driver.test.js +553 -0
- package/out/test/sandbox-hardening.test.js +406 -0
- package/out/test/sandbox-runner.test.js +343 -0
- package/out/test/sandbox-scope.test.js +449 -0
- package/out/test/serve-cron-acceptance.test.js +370 -0
- package/out/test/serve-gateway-turn.test.js +122 -0
- package/out/test/serve-hardening.test.js +227 -15
- package/out/test/serveClient.test.js +6 -5
- package/out/test/sessionShip.test.js +618 -0
- package/out/test/spotlightPublish.test.js +28 -0
- package/out/test/store-install-safe.test.js +69 -0
- package/out/test/tls-bypass.test.js +90 -0
- package/out/test/warrant.test.js +138 -0
- package/out/utils.js +46 -9
- package/package.json +1 -1
package/out/shared/models.js
CHANGED
|
@@ -30,72 +30,15 @@ exports.BUILTIN_MODELS = [
|
|
|
30
30
|
contextWindow: 200000,
|
|
31
31
|
maxOutputTokens: 8192,
|
|
32
32
|
capabilities: ['chat', 'vision', 'function-calling', 'code'],
|
|
33
|
-
|
|
33
|
+
// The picker must say where the prompt goes: this route leaves the machine through
|
|
34
|
+
// AgentHippo's servers on the account JWT, NOT through any API key the user configured.
|
|
35
|
+
description: 'Managed gateway — routed through AgentHippo using your account, not your own API keys',
|
|
34
36
|
recommended: true,
|
|
35
37
|
},
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
id: 'gpt-4.1-nano',
|
|
41
|
-
name: 'GPT-4.1 Nano',
|
|
42
|
-
provider: 'openai',
|
|
43
|
-
contextWindow: 128000,
|
|
44
|
-
maxOutputTokens: 2048,
|
|
45
|
-
capabilities: ['chat', 'vision', 'function-calling', 'code'],
|
|
46
|
-
description: 'Lightweight GPT-4.1 option for quick completions',
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
id: 'gpt-5.3-codex',
|
|
50
|
-
name: 'GPT-5.3 Codex (ChatGPT)',
|
|
51
|
-
provider: 'openai-codex',
|
|
52
|
-
contextWindow: 256000,
|
|
53
|
-
maxOutputTokens: 8192,
|
|
54
|
-
capabilities: ['chat', 'code', 'function-calling'],
|
|
55
|
-
description: 'Codex model authenticated via ChatGPT OAuth subscription',
|
|
56
|
-
recommended: true,
|
|
57
|
-
},
|
|
58
|
-
// ===========================================
|
|
59
|
-
// LiteLLM Proxy Models
|
|
60
|
-
// These route through your LiteLLM deployment
|
|
61
|
-
// ===========================================
|
|
62
|
-
{
|
|
63
|
-
id: 'litellm/gpt-4.1-nano',
|
|
64
|
-
name: 'LiteLLM GPT-4.1 Nano',
|
|
65
|
-
provider: 'litellm',
|
|
66
|
-
contextWindow: 128000,
|
|
67
|
-
maxOutputTokens: 2048,
|
|
68
|
-
capabilities: ['chat', 'vision', 'function-calling', 'code'],
|
|
69
|
-
description: 'GPT-4.1 Nano via LiteLLM proxy',
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
id: 'litellm/gpt-5-nano',
|
|
73
|
-
name: 'LiteLLM GPT-5 Nano',
|
|
74
|
-
provider: 'litellm',
|
|
75
|
-
contextWindow: 256000,
|
|
76
|
-
maxOutputTokens: 8192,
|
|
77
|
-
capabilities: ['chat', 'vision', 'function-calling', 'code'],
|
|
78
|
-
description: 'GPT-5 Nano via LiteLLM proxy',
|
|
79
|
-
},
|
|
80
|
-
// Claude Models: Claude Code will auto translate models into dated aliases
|
|
81
|
-
{
|
|
82
|
-
id: 'claude-sonnet-4-5-20250929',
|
|
83
|
-
name: 'Claude Sonnet 4.5 (SDK alias)',
|
|
84
|
-
provider: 'litellm',
|
|
85
|
-
contextWindow: 200000,
|
|
86
|
-
maxOutputTokens: 8192,
|
|
87
|
-
capabilities: ['chat', 'vision', 'artifacts', 'code'],
|
|
88
|
-
description: 'Compatibility alias used by Claude Agent SDK (maps to Claude 3.5 Sonnet)',
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
id: 'claude-haiku-4-5-20251001',
|
|
92
|
-
name: 'Claude Haiku 4.5 20251001',
|
|
93
|
-
provider: 'litellm',
|
|
94
|
-
contextWindow: 200000,
|
|
95
|
-
maxOutputTokens: 8192,
|
|
96
|
-
capabilities: ['chat', 'vision', 'artifacts', 'code'],
|
|
97
|
-
description: 'Compatibility alias used by Claude Agent SDK (maps to Claude 3 Haiku)',
|
|
98
|
-
},
|
|
38
|
+
// LiteLLM models are NOT hardcoded here.
|
|
39
|
+
// They come from two dynamic sources:
|
|
40
|
+
// 1. Auto-discovery via LiteLLM's /model/info endpoint (require explicit enablement)
|
|
41
|
+
// 2. User-defined custom models in agentide.models.custom
|
|
99
42
|
];
|
|
100
43
|
// -----------------------------------------------------------------------------
|
|
101
44
|
// Registry Key Functions
|
|
@@ -178,58 +121,16 @@ function getActiveModels() {
|
|
|
178
121
|
// Model Resolution (for CLI)
|
|
179
122
|
// -----------------------------------------------------------------------------
|
|
180
123
|
/**
|
|
181
|
-
*
|
|
182
|
-
* Maps short names to full SDK-compatible model IDs.
|
|
183
|
-
*/
|
|
184
|
-
const MODEL_ALIASES = {
|
|
185
|
-
// Claude aliases
|
|
186
|
-
'claude-sonnet-4': 'claude-sonnet-4-5-20250929',
|
|
187
|
-
'claude-sonnet': 'claude-sonnet-4-5-20250929',
|
|
188
|
-
'sonnet': 'claude-sonnet-4-5-20250929',
|
|
189
|
-
'claude-haiku-4': 'claude-haiku-4-5-20251001',
|
|
190
|
-
'claude-haiku': 'claude-haiku-4-5-20251001',
|
|
191
|
-
'haiku': 'claude-haiku-4-5-20251001',
|
|
192
|
-
'claude-opus-4': 'claude-opus-4-20250514',
|
|
193
|
-
'claude-opus': 'claude-opus-4-20250514',
|
|
194
|
-
'opus': 'claude-opus-4-20250514',
|
|
195
|
-
// OpenAI aliases (these work as-is with SDK, but include for consistency)
|
|
196
|
-
'gpt-4o': 'gpt-4o',
|
|
197
|
-
'gpt-4': 'gpt-4o',
|
|
198
|
-
'o3': 'o3',
|
|
199
|
-
'o1': 'o1',
|
|
200
|
-
};
|
|
201
|
-
/**
|
|
202
|
-
* Resolve a model alias to a full model ID.
|
|
124
|
+
* Normalize a model id for CLI/headless use.
|
|
203
125
|
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* 2. Check if it's a known alias
|
|
207
|
-
* 3. Check if any model ID starts with the input (prefix match)
|
|
208
|
-
* 4. Return as-is (let SDK handle it)
|
|
209
|
-
*
|
|
210
|
-
* @param input - User-provided model name (e.g., 'claude-sonnet-4', 'sonnet')
|
|
211
|
-
* @returns Resolved model ID suitable for SDK
|
|
126
|
+
* Only canonicalizes ids that match a built-in catalog entry (currently `ah-auto`).
|
|
127
|
+
* All other ids are passed through unchanged for LiteLLM, pack specs, or provider SDKs.
|
|
212
128
|
*/
|
|
213
129
|
function resolveModelAlias(input) {
|
|
214
130
|
if (!input) {
|
|
215
131
|
return input;
|
|
216
132
|
}
|
|
217
133
|
const normalized = input.toLowerCase().trim();
|
|
218
|
-
// 1. Exact match in BUILTIN_MODELS
|
|
219
134
|
const exactMatch = exports.BUILTIN_MODELS.find(m => m.id.toLowerCase() === normalized);
|
|
220
|
-
|
|
221
|
-
return exactMatch.id;
|
|
222
|
-
}
|
|
223
|
-
// 2. Known alias
|
|
224
|
-
const aliasMatch = MODEL_ALIASES[normalized];
|
|
225
|
-
if (aliasMatch) {
|
|
226
|
-
return aliasMatch;
|
|
227
|
-
}
|
|
228
|
-
// 3. Prefix match (e.g., 'claude-sonnet-4' matches 'claude-sonnet-4-5-20250929')
|
|
229
|
-
const prefixMatch = exports.BUILTIN_MODELS.find(m => m.id.toLowerCase().startsWith(normalized));
|
|
230
|
-
if (prefixMatch) {
|
|
231
|
-
return prefixMatch.id;
|
|
232
|
-
}
|
|
233
|
-
// 4. Return as-is
|
|
234
|
-
return input;
|
|
135
|
+
return exactMatch?.id ?? input;
|
|
235
136
|
}
|
package/out/spotlight/install.js
CHANGED
|
@@ -194,7 +194,13 @@ async function installSpotlightViewFromFiles(files, options) {
|
|
|
194
194
|
if (!INSTALLABLE_ROOT_FILES.has(relPath) && !INSTALLABLE_PREFIXES.some(prefix => relPath.startsWith(prefix))) {
|
|
195
195
|
continue;
|
|
196
196
|
}
|
|
197
|
-
|
|
197
|
+
// The prefix check above is on the raw string — "scripts/../../x" passes it. Resolve and
|
|
198
|
+
// require containment so a store bundle can never write outside its own install dir.
|
|
199
|
+
const resolvedInstallDir = path.resolve(installDir);
|
|
200
|
+
const destination = path.resolve(resolvedInstallDir, relPath);
|
|
201
|
+
if (!destination.startsWith(resolvedInstallDir + path.sep)) {
|
|
202
|
+
throw new Error(`Store bundle entry "${relPath}" escapes the install directory`);
|
|
203
|
+
}
|
|
198
204
|
await fs_1.promises.mkdir(path.dirname(destination), { recursive: true });
|
|
199
205
|
await fs_1.promises.writeFile(destination, content);
|
|
200
206
|
}
|
package/out/store/index.js
CHANGED
|
@@ -25,6 +25,7 @@ exports.detectArtifactType = detectArtifactType;
|
|
|
25
25
|
exports.isStoreUrl = isStoreUrl;
|
|
26
26
|
exports.listStoreIndex = listStoreIndex;
|
|
27
27
|
const utils_1 = require("../utils");
|
|
28
|
+
const branding_1 = require("../branding");
|
|
28
29
|
const github_1 = require("../agentpack/github");
|
|
29
30
|
// Import shared types and utilities for local use
|
|
30
31
|
const shared_1 = require("./shared");
|
|
@@ -158,28 +159,58 @@ async function getLatestStoreVersion(type, slug, includePrerelease = false) {
|
|
|
158
159
|
const zlib_1 = require("zlib");
|
|
159
160
|
/**
|
|
160
161
|
* Parse a tar archive into a map of path → content.
|
|
162
|
+
*
|
|
163
|
+
* Long paths are the whole difficulty here. The ustar `name` field is 100 bytes, and anything
|
|
164
|
+
* longer is carried one of two other ways:
|
|
165
|
+
*
|
|
166
|
+
* - ustar `prefix` (offset 345, 155 bytes): the real path is `prefix + "/" + name`.
|
|
167
|
+
* - PAX extended header (typeflag 'x') or GNU LongName ('L'): a pseudo-entry whose BODY holds
|
|
168
|
+
* the path for the entry that follows.
|
|
169
|
+
*
|
|
170
|
+
* Reading only the `name` field silently DROPPED every such file — a store install of a pack with
|
|
171
|
+
* deep skill directories quietly produced an incomplete pack, which is worse than failing, because
|
|
172
|
+
* nothing anywhere reported a problem. Found when a signed pack verified cryptographically but its
|
|
173
|
+
* digest did not match: the download was missing two files the signature covered.
|
|
161
174
|
*/
|
|
162
175
|
function parseTar(data) {
|
|
163
176
|
const files = new Map();
|
|
164
177
|
let offset = 0;
|
|
178
|
+
/** Set by a PAX/GNU pseudo-entry, consumed by the next real entry. */
|
|
179
|
+
let pendingLongName;
|
|
180
|
+
const str = (buf) => buf.toString('utf-8').replace(/\0/g, '').trim();
|
|
165
181
|
while (offset < data.length - 512) {
|
|
166
182
|
const header = data.subarray(offset, offset + 512);
|
|
167
183
|
if (header.every(b => b === 0)) {
|
|
168
184
|
break;
|
|
169
185
|
}
|
|
170
|
-
const name = header.subarray(0, 100)
|
|
171
|
-
const
|
|
186
|
+
const name = str(header.subarray(0, 100));
|
|
187
|
+
const prefix = str(header.subarray(345, 500));
|
|
188
|
+
const sizeOctal = str(header.subarray(124, 136));
|
|
172
189
|
const typeFlag = header[156];
|
|
173
|
-
|
|
174
|
-
|
|
190
|
+
const size = parseInt(sizeOctal, 8) || 0;
|
|
191
|
+
const body = data.subarray(offset + 512, offset + 512 + size);
|
|
192
|
+
const advance = 512 + Math.ceil(size / 512) * 512;
|
|
193
|
+
// 'x' (120) PAX extended header, 'L' (76) GNU LongName: the path for the NEXT entry.
|
|
194
|
+
if (typeFlag === 120 || typeFlag === 103) {
|
|
195
|
+
// PAX records are "<len> <key>=<value>\n"; we only care about path.
|
|
196
|
+
const match = body.toString('utf-8').match(/\d+ path=([^\n]+)\n/);
|
|
197
|
+
if (match) {
|
|
198
|
+
pendingLongName = match[1];
|
|
199
|
+
}
|
|
200
|
+
offset += advance;
|
|
175
201
|
continue;
|
|
176
202
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
203
|
+
if (typeFlag === 76) {
|
|
204
|
+
pendingLongName = str(body);
|
|
205
|
+
offset += advance;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const fullName = pendingLongName ?? (prefix ? `${prefix}/${name}` : name);
|
|
209
|
+
pendingLongName = undefined;
|
|
210
|
+
if (fullName && (typeFlag === 0 || typeFlag === 48)) {
|
|
211
|
+
files.set(fullName, Buffer.from(body));
|
|
181
212
|
}
|
|
182
|
-
offset +=
|
|
213
|
+
offset += advance;
|
|
183
214
|
}
|
|
184
215
|
return files;
|
|
185
216
|
}
|
|
@@ -422,7 +453,7 @@ async function ensureFork() {
|
|
|
422
453
|
if (!sourceRepo) {
|
|
423
454
|
throw new Error(`Store repo not found: ${exports.STORE_CONFIG.owner}/${exports.STORE_CONFIG.repo}\n\n` +
|
|
424
455
|
`If using a custom store, ensure the repo exists and is accessible.\n` +
|
|
425
|
-
`Setting: agentide.storeRepo`);
|
|
456
|
+
`Setting: ${(0, branding_1.toSettingsDisplayKey)('agentide.storeRepo')}`);
|
|
426
457
|
}
|
|
427
458
|
// Create fork
|
|
428
459
|
const response = await fetch(`https://api.github.com/repos/${exports.STORE_CONFIG.owner}/${exports.STORE_CONFIG.repo}/forks`, {
|
package/out/sync.js
CHANGED
|
@@ -962,7 +962,9 @@ class SyncService {
|
|
|
962
962
|
const roots = Array.from(new Set([workspaceRoot, ...additionalWorkspaceRoots].map(r => r.trim()).filter(Boolean))).filter(r => r !== os.homedir());
|
|
963
963
|
for (const root of roots) {
|
|
964
964
|
// Ensure git is initialized and root .gitignore has AgentIDE entries
|
|
965
|
-
await (0, utils_1.ensureWorkspaceGitAndGitignore)(root, outputChannel
|
|
965
|
+
await (0, utils_1.ensureWorkspaceGitAndGitignore)(root, outputChannel, {
|
|
966
|
+
extraSearchPath: getConfiguredAdditionalEngineDir(),
|
|
967
|
+
});
|
|
966
968
|
const agentideDir = this.getAgentideProjectDir(root);
|
|
967
969
|
await fs_1.promises.mkdir(agentideDir, { recursive: true });
|
|
968
970
|
await this.ensureAgentideGitignore(root, outputChannel);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/*---------------------------------------------------------------------------------------------
|
|
3
3
|
* Copyright (c) AgentHippo.ai. All rights reserved.
|
|
4
|
+
* Licensed under the MIT License. See LICENSE.txt in the project root for license information.
|
|
4
5
|
*--------------------------------------------------------------------------------------------*/
|
|
5
6
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
7
|
if (k2 === undefined) k2 = k;
|
|
@@ -43,6 +44,28 @@ const os = __importStar(require("os"));
|
|
|
43
44
|
const fs_1 = require("fs");
|
|
44
45
|
const base_1 = require("../engines/base");
|
|
45
46
|
const sessionEvents_1 = require("../analytics/sessionEvents");
|
|
47
|
+
const identity_1 = require("../analytics/identity");
|
|
48
|
+
/**
|
|
49
|
+
* Run `fn` with prompt/tool content capture opted in.
|
|
50
|
+
*
|
|
51
|
+
* Content capture is off by default now (PLAN/compliance-req.md §6), so the tests that assert on
|
|
52
|
+
* previews have to say out loud that they are testing the opt-in path.
|
|
53
|
+
*/
|
|
54
|
+
async function withContentCapture(fn) {
|
|
55
|
+
const previous = process.env[identity_1.CONTENT_CAPTURE_ENV];
|
|
56
|
+
process.env[identity_1.CONTENT_CAPTURE_ENV] = '1';
|
|
57
|
+
try {
|
|
58
|
+
return await fn();
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
if (previous === undefined) {
|
|
62
|
+
delete process.env[identity_1.CONTENT_CAPTURE_ENV];
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
process.env[identity_1.CONTENT_CAPTURE_ENV] = previous;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
46
69
|
async function createUniqueTempDir(prefix) {
|
|
47
70
|
return fs_1.promises.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
48
71
|
}
|
|
@@ -96,9 +119,17 @@ suite('Analytics - buildLiteLLMMetadataHeaders', () => {
|
|
|
96
119
|
const headers = (0, base_1.buildLiteLLMMetadataHeaders)(true, { sessionId: 'sess-123' });
|
|
97
120
|
assert.ok(headers.some(h => h === 'x-litellm-metadata-session-id: sess-123'), `Missing session_id header. Headers: ${JSON.stringify(headers)}`);
|
|
98
121
|
});
|
|
99
|
-
test('includes user_id header', () => {
|
|
122
|
+
test('includes user_id header, hashed', () => {
|
|
100
123
|
const headers = (0, base_1.buildLiteLLMMetadataHeaders)(true, { analyticsUserId: 'user-42' });
|
|
101
|
-
assert.ok(headers.some(h => h ===
|
|
124
|
+
assert.ok(headers.some(h => h === `x-litellm-metadata-user-id: ${(0, identity_1.hashIdentity)('user-42')}`), `Missing user_id header. Headers: ${JSON.stringify(headers)}`);
|
|
125
|
+
});
|
|
126
|
+
test('the raw user id never leaves the process on a header', () => {
|
|
127
|
+
// The proxy logs every request and may forward to a third-party observability backend, so
|
|
128
|
+
// a raw address here is a directory of who works at the customer.
|
|
129
|
+
const headers = (0, base_1.buildLiteLLMMetadataHeaders)(true, { analyticsUserId: 'priya@meridian.com' });
|
|
130
|
+
assert.ok(!headers.some(h => h.includes('priya@meridian.com')), JSON.stringify(headers));
|
|
131
|
+
assert.ok(headers.some(h => h.startsWith('x-litellm-metadata-user-id: anon_')));
|
|
132
|
+
assert.ok(headers.some(h => h.startsWith('langfuse_trace_user_id: anon_')));
|
|
102
133
|
});
|
|
103
134
|
test('includes project_id header', () => {
|
|
104
135
|
const headers = (0, base_1.buildLiteLLMMetadataHeaders)(true, { analyticsProjectId: 'agentide-vscode' });
|
|
@@ -130,7 +161,7 @@ suite('Analytics - buildLiteLLMMetadataHeaders', () => {
|
|
|
130
161
|
analyticsUserId: 'user-1',
|
|
131
162
|
});
|
|
132
163
|
assert.ok(headers.some(h => h === 'langfuse_session_id: sess-1'), 'Should include langfuse_session_id for Langfuse callback compatibility');
|
|
133
|
-
assert.ok(headers.some(h => h ===
|
|
164
|
+
assert.ok(headers.some(h => h === `langfuse_trace_user_id: ${(0, identity_1.hashIdentity)('user-1')}`), 'Should include langfuse_trace_user_id for Langfuse callback compatibility');
|
|
134
165
|
});
|
|
135
166
|
// --- Use Case: AgentHippo pack (Mode A) — fully automatic ---
|
|
136
167
|
test('Mode A: AgentHippo pack produces all 10 headers (8 analytics + 2 langfuse)', () => {
|
|
@@ -376,8 +407,31 @@ suite('Analytics - ObservableStreamEmitter', () => {
|
|
|
376
407
|
assert.strictEqual(event.event_type, 'tool_start');
|
|
377
408
|
assert.strictEqual(event.payload.toolName, 'Bash');
|
|
378
409
|
assert.strictEqual(event.payload.callId, 'call-1');
|
|
379
|
-
assert.strictEqual(event.payload.inputPreview, 'ls -la');
|
|
380
410
|
assert.strictEqual(event.request_id, 'req-1');
|
|
411
|
+
// Tool arguments are content: digest by default, no preview.
|
|
412
|
+
assert.strictEqual(event.payload.inputPreview, undefined);
|
|
413
|
+
assert.strictEqual(event.payload.input_length, 6);
|
|
414
|
+
assert.strictEqual(typeof event.payload.input_sha256, 'string');
|
|
415
|
+
});
|
|
416
|
+
test('tool input preview appears only under the content-capture opt-in', async () => {
|
|
417
|
+
await withContentCapture(async () => {
|
|
418
|
+
const emitter = (0, sessionEvents_1.wrapEmitterWithObservability)(innerEmitter, writer, 'sess-optin', 'req-1');
|
|
419
|
+
await emitter.toolStart('Bash', 'ls -la', 'call-1');
|
|
420
|
+
});
|
|
421
|
+
const content = await fs_1.promises.readFile(path.join(testDir, 'sess-optin.jsonl'), 'utf8');
|
|
422
|
+
const event = JSON.parse(content.trim());
|
|
423
|
+
assert.strictEqual(event.payload.inputPreview, 'ls -la');
|
|
424
|
+
// The digest is present in both modes, so a query written against it never breaks.
|
|
425
|
+
assert.strictEqual(typeof event.payload.input_sha256, 'string');
|
|
426
|
+
});
|
|
427
|
+
test('tool result content is a digest, not a preview, by default', async () => {
|
|
428
|
+
const emitter = (0, sessionEvents_1.wrapEmitterWithObservability)(innerEmitter, writer, 'sess-nores', 'req-1');
|
|
429
|
+
await emitter.toolEnd('Read', 'call-1', 'CUSTOMER SECRET', false);
|
|
430
|
+
const content = await fs_1.promises.readFile(path.join(testDir, 'sess-nores.jsonl'), 'utf8');
|
|
431
|
+
assert.ok(!content.includes('CUSTOMER SECRET'), 'raw tool output must not reach the JSONL');
|
|
432
|
+
const event = JSON.parse(content.trim());
|
|
433
|
+
assert.strictEqual(event.payload.resultPreview, undefined);
|
|
434
|
+
assert.strictEqual(event.payload.result_length, 15);
|
|
381
435
|
});
|
|
382
436
|
test('toolEnd writes session event AND passes through', async () => {
|
|
383
437
|
const emitter = (0, sessionEvents_1.wrapEmitterWithObservability)(innerEmitter, writer, 'sess-1', 'req-1');
|
|
@@ -426,21 +480,28 @@ suite('Analytics - ObservableStreamEmitter', () => {
|
|
|
426
480
|
assert.strictEqual(event.event_type, 'turn_end');
|
|
427
481
|
assert.strictEqual(event.payload.success, true);
|
|
428
482
|
});
|
|
429
|
-
test('truncates long tool input to 500 chars', async () => {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
483
|
+
test('truncates long tool input to 500 chars (content-capture opt-in)', async () => {
|
|
484
|
+
await withContentCapture(async () => {
|
|
485
|
+
const emitter = (0, sessionEvents_1.wrapEmitterWithObservability)(innerEmitter, writer, 'sess-1', 'req-1');
|
|
486
|
+
const longInput = 'x'.repeat(1000);
|
|
487
|
+
await emitter.toolStart('Read', longInput, 'call-1');
|
|
488
|
+
});
|
|
433
489
|
const content = await fs_1.promises.readFile(path.join(testDir, 'sess-1.jsonl'), 'utf8');
|
|
434
490
|
const event = JSON.parse(content.trim());
|
|
435
491
|
assert.strictEqual(event.payload.inputPreview.length, 500);
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
await
|
|
492
|
+
// The digest covers the WHOLE input, not the truncated preview.
|
|
493
|
+
assert.strictEqual(event.payload.input_length, 1000);
|
|
494
|
+
});
|
|
495
|
+
test('truncates long tool result to 500 chars (content-capture opt-in)', async () => {
|
|
496
|
+
await withContentCapture(async () => {
|
|
497
|
+
const emitter = (0, sessionEvents_1.wrapEmitterWithObservability)(innerEmitter, writer, 'sess-1', 'req-1');
|
|
498
|
+
const longResult = 'y'.repeat(1000);
|
|
499
|
+
await emitter.toolEnd('Read', 'call-1', longResult, false);
|
|
500
|
+
});
|
|
441
501
|
const content = await fs_1.promises.readFile(path.join(testDir, 'sess-1.jsonl'), 'utf8');
|
|
442
502
|
const event = JSON.parse(content.trim());
|
|
443
503
|
assert.strictEqual(event.payload.resultPreview.length, 500);
|
|
504
|
+
assert.strictEqual(event.payload.result_length, 1000);
|
|
444
505
|
});
|
|
445
506
|
// --- Use Case: Full agent request lifecycle ---
|
|
446
507
|
test('Use Case: full request lifecycle produces correct event sequence', async () => {
|
|
@@ -555,14 +616,24 @@ suite('Analytics - ObservableStreamEmitter records the acting user (the warrant)
|
|
|
555
616
|
const content = await fs_1.promises.readFile(path.join(testDir, `${sessionId}.jsonl`), 'utf8');
|
|
556
617
|
return content.trim().split('\n').map((l) => JSON.parse(l));
|
|
557
618
|
}
|
|
558
|
-
test('toolStart/toolEnd record the userId passed at wrap', async () => {
|
|
619
|
+
test('toolStart/toolEnd record the userId passed at wrap, as a salted hash', async () => {
|
|
559
620
|
const emitter = (0, sessionEvents_1.wrapEmitterWithObservability)(innerEmitter, writer, 'sess-warrant-1', 'req-1', 'priya@meridian.com');
|
|
560
621
|
await emitter.toolStart('Bash', 'echo hi', 'c1');
|
|
561
622
|
await emitter.toolEnd('Bash', 'c1', 'hi', false);
|
|
562
623
|
const events = await readEvents('sess-warrant-1');
|
|
563
624
|
assert.strictEqual(events.length, 2);
|
|
564
|
-
|
|
565
|
-
assert.strictEqual(events[
|
|
625
|
+
const expected = (0, identity_1.hashIdentity)('priya@meridian.com');
|
|
626
|
+
assert.strictEqual(events[0].payload.userId, expected);
|
|
627
|
+
assert.strictEqual(events[1].payload.userId, expected);
|
|
628
|
+
// Attribution is preserved; the address itself is not.
|
|
629
|
+
assert.ok(String(expected).startsWith('anon_'));
|
|
630
|
+
});
|
|
631
|
+
test('the raw address never reaches the evidence file', async () => {
|
|
632
|
+
const emitter = (0, sessionEvents_1.wrapEmitterWithObservability)(innerEmitter, writer, 'sess-warrant-raw', 'req-1', 'priya@meridian.com');
|
|
633
|
+
await emitter.emitTurnStart();
|
|
634
|
+
const raw = await fs_1.promises.readFile(path.join(testDir, 'sess-warrant-raw.jsonl'), 'utf8');
|
|
635
|
+
assert.ok(!raw.includes('priya@meridian.com'), 'raw user id must not be written to evidence');
|
|
636
|
+
assert.ok(!raw.includes('meridian.com'), 'not even the domain');
|
|
566
637
|
});
|
|
567
638
|
test('toolStart records no userId when wrap omitted it (IDE-local usage)', async () => {
|
|
568
639
|
const emitter = (0, sessionEvents_1.wrapEmitterWithObservability)(innerEmitter, writer, 'sess-warrant-2', 'req-1');
|
|
@@ -582,8 +653,9 @@ suite('Analytics - ObservableStreamEmitter records the acting user (the warrant)
|
|
|
582
653
|
]);
|
|
583
654
|
const events = await readEvents('sess-warrant-3');
|
|
584
655
|
const byCallId = Object.fromEntries(events.map((e) => [e.payload.callId, e.payload.userId]));
|
|
585
|
-
assert.strictEqual(byCallId['a1'], 'alice@acme.com');
|
|
586
|
-
assert.strictEqual(byCallId['b1'], 'bob@acme.com');
|
|
656
|
+
assert.strictEqual(byCallId['a1'], (0, identity_1.hashIdentity)('alice@acme.com'));
|
|
657
|
+
assert.strictEqual(byCallId['b1'], (0, identity_1.hashIdentity)('bob@acme.com'));
|
|
658
|
+
assert.notStrictEqual(byCallId['a1'], byCallId['b1'], 'distinct users must stay distinguishable');
|
|
587
659
|
});
|
|
588
660
|
test('toolStart/toolEnd keep wrap-time userId even when called outside any credential scope', async () => {
|
|
589
661
|
// Simulates the custom-engine persistent-worker path: wrap captures identity on the
|
|
@@ -594,8 +666,8 @@ suite('Analytics - ObservableStreamEmitter records the acting user (the warrant)
|
|
|
594
666
|
await emitter.toolStart('Bash', 'echo hi', 'c1');
|
|
595
667
|
await emitter.toolEnd('Bash', 'c1', 'hi', false);
|
|
596
668
|
const events = await readEvents('sess-warrant-4');
|
|
597
|
-
assert.strictEqual(events[0].payload.userId, 'alice@acme.com');
|
|
598
|
-
assert.strictEqual(events[1].payload.userId, 'alice@acme.com');
|
|
669
|
+
assert.strictEqual(events[0].payload.userId, (0, identity_1.hashIdentity)('alice@acme.com'));
|
|
670
|
+
assert.strictEqual(events[1].payload.userId, (0, identity_1.hashIdentity)('alice@acme.com'));
|
|
599
671
|
});
|
|
600
672
|
test('error/turn_start/turn_end also carry the wrap-time userId, not just tool events', async () => {
|
|
601
673
|
// A turn that errors before any tool call (or has no tool calls at all) must still
|
|
@@ -607,11 +679,78 @@ suite('Analytics - ObservableStreamEmitter records the acting user (the warrant)
|
|
|
607
679
|
await emitter.emitTurnEnd(false);
|
|
608
680
|
const events = await readEvents('sess-warrant-5');
|
|
609
681
|
assert.strictEqual(events.length, 3);
|
|
682
|
+
const bob = (0, identity_1.hashIdentity)('bob@acme.com');
|
|
610
683
|
assert.strictEqual(events[0].event_type, 'turn_start');
|
|
611
|
-
assert.strictEqual(events[0].payload.userId,
|
|
684
|
+
assert.strictEqual(events[0].payload.userId, bob);
|
|
612
685
|
assert.strictEqual(events[1].event_type, 'error');
|
|
613
|
-
assert.strictEqual(events[1].payload.userId,
|
|
686
|
+
assert.strictEqual(events[1].payload.userId, bob);
|
|
614
687
|
assert.strictEqual(events[2].event_type, 'turn_end');
|
|
615
|
-
assert.strictEqual(events[2].payload.userId,
|
|
688
|
+
assert.strictEqual(events[2].payload.userId, bob);
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
// =============================================================================
|
|
692
|
+
// Evidence-write failures must be LOUD (compliance fix 2a)
|
|
693
|
+
//
|
|
694
|
+
// `append()` used to swallow every error in an empty catch block. A full disk, a read-only
|
|
695
|
+
// volume, a bad mount — all of them produced a turn that ran with no record of it, and nothing
|
|
696
|
+
// anywhere said so. An absent record is the failure mode that matters (SOC 2 CC7.2 / R-13), so
|
|
697
|
+
// these tests exist to keep the silence from coming back.
|
|
698
|
+
// =============================================================================
|
|
699
|
+
suite('Analytics - evidence write failures are surfaced, never swallowed', () => {
|
|
700
|
+
let blockedDir;
|
|
701
|
+
let errors;
|
|
702
|
+
let originalError;
|
|
703
|
+
setup(async () => {
|
|
704
|
+
(0, sessionEvents_1.resetSessionEventWriterStatsForTest)();
|
|
705
|
+
// A FILE where the writer expects a directory: mkdir fails, so every append fails.
|
|
706
|
+
const root = await createUniqueTempDir('agentide-evidence-fail-');
|
|
707
|
+
blockedDir = path.join(root, 'not-a-dir');
|
|
708
|
+
await fs_1.promises.writeFile(blockedDir, 'this is a file, not a directory');
|
|
709
|
+
errors = [];
|
|
710
|
+
originalError = console.error;
|
|
711
|
+
console.error = (msg) => { errors.push(String(msg)); };
|
|
712
|
+
});
|
|
713
|
+
teardown(() => {
|
|
714
|
+
console.error = originalError;
|
|
715
|
+
delete process.env.AGENTHIPPO_EVIDENCE_REQUIRED;
|
|
716
|
+
(0, sessionEvents_1.resetSessionEventWriterStatsForTest)();
|
|
717
|
+
});
|
|
718
|
+
test('a failed append logs at error level and counts the loss', async () => {
|
|
719
|
+
const writer = new sessionEvents_1.SessionEventWriter(blockedDir);
|
|
720
|
+
await writer.append('sess-x', 'req-1', 'tool_start', { toolName: 'Bash' });
|
|
721
|
+
assert.strictEqual(writer.getStats().failed, 1, 'per-writer counter');
|
|
722
|
+
assert.strictEqual(writer.getStats().written, 0);
|
|
723
|
+
assert.strictEqual((0, sessionEvents_1.getSessionEventWriterStats)().failed, 1, 'process-wide counter for /health');
|
|
724
|
+
assert.ok(errors.some((e) => e.includes('LOST session event')), `no error logged: ${JSON.stringify(errors)}`);
|
|
725
|
+
assert.ok(errors.some((e) => e.includes('tool_start')), 'the lost event type is named');
|
|
726
|
+
});
|
|
727
|
+
test('the last failure is retained for /health output', async () => {
|
|
728
|
+
const writer = new sessionEvents_1.SessionEventWriter(blockedDir);
|
|
729
|
+
await writer.append('sess-x', 'req-1', 'turn_end', { success: true });
|
|
730
|
+
const stats = (0, sessionEvents_1.getSessionEventWriterStats)();
|
|
731
|
+
assert.strictEqual(stats.lastError?.eventType, 'turn_end');
|
|
732
|
+
assert.ok((stats.lastError?.at ?? 0) > 0);
|
|
733
|
+
assert.ok((stats.lastError?.message ?? '').length > 0);
|
|
734
|
+
});
|
|
735
|
+
test('AGENTHIPPO_EVIDENCE_REQUIRED makes the turn fail rather than run unrecorded', async () => {
|
|
736
|
+
process.env.AGENTHIPPO_EVIDENCE_REQUIRED = '1';
|
|
737
|
+
const writer = new sessionEvents_1.SessionEventWriter(blockedDir);
|
|
738
|
+
await assert.rejects(() => writer.append('sess-x', 'req-1', 'tool_start', {}), /evidence write failed for tool_start/);
|
|
739
|
+
assert.strictEqual((0, sessionEvents_1.getSessionEventWriterStats)().failed, 1);
|
|
740
|
+
});
|
|
741
|
+
test('without the flag the agent keeps running, but the loss is still counted', async () => {
|
|
742
|
+
const writer = new sessionEvents_1.SessionEventWriter(blockedDir);
|
|
743
|
+
await writer.append('sess-x', 'req-1', 'tool_start', {});
|
|
744
|
+
await writer.append('sess-x', 'req-1', 'tool_end', {});
|
|
745
|
+
assert.strictEqual((0, sessionEvents_1.getSessionEventWriterStats)().failed, 2);
|
|
746
|
+
});
|
|
747
|
+
test('successful writes are counted too, so /health can show a ratio', async () => {
|
|
748
|
+
const dir = await createUniqueTempDir('agentide-evidence-ok-');
|
|
749
|
+
const writer = new sessionEvents_1.SessionEventWriter(dir);
|
|
750
|
+
await writer.append('sess-ok', 'req-1', 'turn_start', {});
|
|
751
|
+
assert.strictEqual(writer.getStats().written, 1);
|
|
752
|
+
assert.strictEqual(writer.getStats().failed, 0);
|
|
753
|
+
assert.strictEqual((0, sessionEvents_1.getSessionEventWriterStats)().written, 1);
|
|
754
|
+
await removeDirWithRetries(dir);
|
|
616
755
|
});
|
|
617
756
|
});
|