@agenthippo-ai/agenthippo 0.1.1 → 1.110.9
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 +2 -1
- package/out/agentpack/paths.js +23 -0
- package/out/analytics/dashboard/fleetDisplayUtils.js +3 -0
- package/out/analytics/dashboard/provider.js +13 -2
- package/out/analytics/dashboard/webview/panel.js +293 -82
- package/out/analytics/identity.js +0 -0
- package/out/analytics/service.js +23 -25
- package/out/analytics/sessionEvents.js +109 -22
- package/out/auth/authBaseUrl.js +59 -0
- package/out/auth/bootstrapApply.js +9 -10
- package/out/auth/brokerClient.js +49 -15
- package/out/auth/onboardingPrompt.js +112 -0
- package/out/auth/provider.js +9 -22
- package/out/auth/sessionStore.js +3 -40
- package/out/auth/signInFlow.js +2 -1
- package/out/auth-broker/config.js +54 -4
- package/out/auth-broker/server.js +152 -20
- 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/ask.js +3 -0
- package/out/cli/commands/chat.js +3 -0
- package/out/cli/commands/conformance.js +249 -0
- package/out/cli/commands/data-plane.js +67 -0
- package/out/cli/commands/deploy.js +50 -10
- package/out/cli/commands/gateway.js +58 -0
- package/out/cli/commands/idp.js +85 -0
- package/out/cli/commands/pack.js +182 -3
- package/out/cli/commands/sandbox.js +332 -0
- package/out/cli/commands/serve.js +1165 -82
- package/out/cli/commands/store.js +12 -4
- package/out/cli/credential-context.js +42 -80
- package/out/cli/data-plane/broker.js +78 -0
- package/out/cli/data-plane/connector-host.js +121 -0
- package/out/cli/data-plane/egress.js +189 -0
- package/out/cli/data-plane/gateway.js +218 -0
- package/out/cli/data-plane/index.js +152 -0
- package/out/cli/data-plane/registry.js +89 -0
- package/out/cli/data-plane/remote.js +106 -0
- package/out/cli/data-plane/sidecar.js +161 -0
- package/out/cli/deploy/blueprints/registry.js +84 -0
- package/out/cli/deploy/layers/identity.js +172 -0
- package/out/cli/deploy-init.js +1182 -347
- package/out/cli/deps.js +106 -2
- package/out/cli/idp-init.js +338 -0
- package/out/cli/index.js +192 -23
- package/out/cli/pack-install-ref.js +15 -0
- package/out/cli/pack-loader.js +45 -3
- package/out/cli/pack-runtime.js +61 -0
- package/out/cli/runtime.js +44 -3
- package/out/cli/serve-hardening.js +139 -3
- package/out/cli/state-sync.js +16 -15
- package/out/cli/utils.js +12 -1
- package/out/engines/base.js +32 -3
- package/out/engines/claude.js +47 -8
- package/out/engines/codex.js +125 -22
- package/out/engines/custom.js +54 -12
- package/out/evidence/sessionShip.js +610 -0
- package/out/extension.js +13 -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/outboundSend.js +6 -6
- package/out/gateway/secretsPreflight.js +90 -0
- package/out/gateway/service.js +7 -2
- package/out/headless/index.js +40 -9
- package/out/live-e2e/mcp-demo/server.mjs +32 -0
- package/out/live-probe/connector.mjs +8 -0
- package/out/live-probe/env-report.mjs +8 -0
- package/out/mcp.js +115 -67
- package/out/pack/caps.js +271 -0
- package/out/pack/governor.js +309 -0
- package/out/pack/lease-schema.js +270 -0
- package/out/pack/lease.js +640 -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 +131 -0
- package/out/runner/protocol.js +108 -0
- package/out/runner/remoteEngine.js +130 -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 +570 -0
- package/out/sandbox/processDriver.js +315 -0
- package/out/sandbox/sandboxRunner.js +191 -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 +182 -21
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +94 -118
- package/out/scripts/analytics/setup-analytics.ps1 +37 -13
- package/out/scripts/analytics/setup-analytics.sh +93 -13
- package/out/scripts/analytics/start-analytics.sh +86 -14
- package/out/scripts/gateway/setup-gateway.sh +72 -32
- package/out/shared/models.js +12 -111
- package/out/shared/providers.js +2 -2
- 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 +198 -34
- 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 +112 -9
- package/out/test/blueprint-registry.test.js +105 -0
- package/out/test/branding.test.js +13 -0
- package/out/test/caps-governor.test.js +455 -0
- package/out/test/codex-new-file-survival.test.js +119 -0
- package/out/test/configuration-scopes.test.js +131 -0
- package/out/test/credential-context.test.js +23 -28
- package/out/test/custom-env-layering.test.js +151 -0
- package/out/test/data-plane-engine-wiring.test.js +250 -0
- package/out/test/data-plane.test.js +311 -0
- package/out/test/deploy-init.test.js +295 -4
- package/out/test/egress-broker.test.js +172 -0
- package/out/test/fleetDisplayUtils.test.js +4 -0
- 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 +217 -0
- package/out/test/headless-sync.test.js +22 -0
- package/out/test/idp-init.test.js +151 -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 +169 -0
- package/out/test/lease.test.js +114 -0
- package/out/test/mcp-obo.test.js +52 -27
- package/out/test/mcp.test.js +69 -3
- package/out/test/model-routing.test.js +133 -0
- 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 +395 -0
- package/out/test/sandbox-runner.test.js +343 -0
- package/out/test/serve-gateway-turn.test.js +267 -0
- package/out/test/serve-hardening.test.js +125 -6
- package/out/test/serveClient.test.js +38 -5
- package/out/test/sessionShip.test.js +560 -0
- package/out/test/spotlightPublish.test.js +28 -0
- package/out/test/state-sync-peer.test.js +18 -5
- 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 +73 -20
- package/package.json +1 -1
package/out/shared/providers.js
CHANGED
|
@@ -73,7 +73,7 @@ exports.PROVIDERS = {
|
|
|
73
73
|
},
|
|
74
74
|
litellm: {
|
|
75
75
|
id: 'litellm',
|
|
76
|
-
displayName: 'LiteLLM
|
|
76
|
+
displayName: 'LiteLLM',
|
|
77
77
|
endpointSettingKey: 'agentide.providers.litellm.baseUrl',
|
|
78
78
|
requiresApiKey: true,
|
|
79
79
|
description: 'local adapter for agents to work with Spotlight and all models from OpenAI, Anthropic, Ollama etc.',
|
|
@@ -96,7 +96,7 @@ exports.PROVIDERS = {
|
|
|
96
96
|
agenthippo: {
|
|
97
97
|
id: 'agenthippo',
|
|
98
98
|
displayName: 'AgentHippo Gateway',
|
|
99
|
-
defaultEndpoint: 'https://
|
|
99
|
+
defaultEndpoint: 'https://gateway.agenthippo.ai/v1',
|
|
100
100
|
endpointSettingKey: 'agentide.AgentHippoModelGateway.url',
|
|
101
101
|
requiresApiKey: true,
|
|
102
102
|
description: 'AgentHippo managed gateway — uses account JWT, picks model automatically',
|
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,7 +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");
|
|
46
|
-
const
|
|
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
|
+
}
|
|
47
69
|
async function createUniqueTempDir(prefix) {
|
|
48
70
|
return fs_1.promises.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
49
71
|
}
|
|
@@ -97,9 +119,17 @@ suite('Analytics - buildLiteLLMMetadataHeaders', () => {
|
|
|
97
119
|
const headers = (0, base_1.buildLiteLLMMetadataHeaders)(true, { sessionId: 'sess-123' });
|
|
98
120
|
assert.ok(headers.some(h => h === 'x-litellm-metadata-session-id: sess-123'), `Missing session_id header. Headers: ${JSON.stringify(headers)}`);
|
|
99
121
|
});
|
|
100
|
-
test('includes user_id header', () => {
|
|
122
|
+
test('includes user_id header, hashed', () => {
|
|
101
123
|
const headers = (0, base_1.buildLiteLLMMetadataHeaders)(true, { analyticsUserId: 'user-42' });
|
|
102
|
-
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_')));
|
|
103
133
|
});
|
|
104
134
|
test('includes project_id header', () => {
|
|
105
135
|
const headers = (0, base_1.buildLiteLLMMetadataHeaders)(true, { analyticsProjectId: 'agentide-vscode' });
|
|
@@ -131,7 +161,7 @@ suite('Analytics - buildLiteLLMMetadataHeaders', () => {
|
|
|
131
161
|
analyticsUserId: 'user-1',
|
|
132
162
|
});
|
|
133
163
|
assert.ok(headers.some(h => h === 'langfuse_session_id: sess-1'), 'Should include langfuse_session_id for Langfuse callback compatibility');
|
|
134
|
-
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');
|
|
135
165
|
});
|
|
136
166
|
// --- Use Case: AgentHippo pack (Mode A) — fully automatic ---
|
|
137
167
|
test('Mode A: AgentHippo pack produces all 10 headers (8 analytics + 2 langfuse)', () => {
|
|
@@ -377,8 +407,31 @@ suite('Analytics - ObservableStreamEmitter', () => {
|
|
|
377
407
|
assert.strictEqual(event.event_type, 'tool_start');
|
|
378
408
|
assert.strictEqual(event.payload.toolName, 'Bash');
|
|
379
409
|
assert.strictEqual(event.payload.callId, 'call-1');
|
|
380
|
-
assert.strictEqual(event.payload.inputPreview, 'ls -la');
|
|
381
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);
|
|
382
435
|
});
|
|
383
436
|
test('toolEnd writes session event AND passes through', async () => {
|
|
384
437
|
const emitter = (0, sessionEvents_1.wrapEmitterWithObservability)(innerEmitter, writer, 'sess-1', 'req-1');
|
|
@@ -427,21 +480,28 @@ suite('Analytics - ObservableStreamEmitter', () => {
|
|
|
427
480
|
assert.strictEqual(event.event_type, 'turn_end');
|
|
428
481
|
assert.strictEqual(event.payload.success, true);
|
|
429
482
|
});
|
|
430
|
-
test('truncates long tool input to 500 chars', async () => {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
+
});
|
|
434
489
|
const content = await fs_1.promises.readFile(path.join(testDir, 'sess-1.jsonl'), 'utf8');
|
|
435
490
|
const event = JSON.parse(content.trim());
|
|
436
491
|
assert.strictEqual(event.payload.inputPreview.length, 500);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
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
|
+
});
|
|
442
501
|
const content = await fs_1.promises.readFile(path.join(testDir, 'sess-1.jsonl'), 'utf8');
|
|
443
502
|
const event = JSON.parse(content.trim());
|
|
444
503
|
assert.strictEqual(event.payload.resultPreview.length, 500);
|
|
504
|
+
assert.strictEqual(event.payload.result_length, 1000);
|
|
445
505
|
});
|
|
446
506
|
// --- Use Case: Full agent request lifecycle ---
|
|
447
507
|
test('Use Case: full request lifecycle produces correct event sequence', async () => {
|
|
@@ -524,7 +584,7 @@ suite('Analytics - ObservableStreamEmitter', () => {
|
|
|
524
584
|
});
|
|
525
585
|
});
|
|
526
586
|
// =============================================================================
|
|
527
|
-
// ObservableStreamEmitter
|
|
587
|
+
// ObservableStreamEmitter warrant audit trail (constructor-captured userId)
|
|
528
588
|
// =============================================================================
|
|
529
589
|
suite('Analytics - ObservableStreamEmitter records the acting user (the warrant)', () => {
|
|
530
590
|
let testDir;
|
|
@@ -556,37 +616,141 @@ suite('Analytics - ObservableStreamEmitter records the acting user (the warrant)
|
|
|
556
616
|
const content = await fs_1.promises.readFile(path.join(testDir, `${sessionId}.jsonl`), 'utf8');
|
|
557
617
|
return content.trim().split('\n').map((l) => JSON.parse(l));
|
|
558
618
|
}
|
|
559
|
-
test('toolStart/toolEnd record userId
|
|
560
|
-
const emitter = (0, sessionEvents_1.wrapEmitterWithObservability)(innerEmitter, writer, 'sess-warrant-1', 'req-1');
|
|
561
|
-
await (
|
|
562
|
-
|
|
563
|
-
await emitter.toolEnd('Bash', 'c1', 'hi', false);
|
|
564
|
-
});
|
|
619
|
+
test('toolStart/toolEnd record the userId passed at wrap, as a salted hash', async () => {
|
|
620
|
+
const emitter = (0, sessionEvents_1.wrapEmitterWithObservability)(innerEmitter, writer, 'sess-warrant-1', 'req-1', 'priya@meridian.com');
|
|
621
|
+
await emitter.toolStart('Bash', 'echo hi', 'c1');
|
|
622
|
+
await emitter.toolEnd('Bash', 'c1', 'hi', false);
|
|
565
623
|
const events = await readEvents('sess-warrant-1');
|
|
566
624
|
assert.strictEqual(events.length, 2);
|
|
567
|
-
|
|
568
|
-
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');
|
|
569
637
|
});
|
|
570
|
-
test('toolStart records no userId
|
|
638
|
+
test('toolStart records no userId when wrap omitted it (IDE-local usage)', async () => {
|
|
571
639
|
const emitter = (0, sessionEvents_1.wrapEmitterWithObservability)(innerEmitter, writer, 'sess-warrant-2', 'req-1');
|
|
572
640
|
await emitter.toolStart('Bash', 'echo hi', 'c1');
|
|
573
641
|
const events = await readEvents('sess-warrant-2');
|
|
574
642
|
assert.strictEqual(events[0].payload.userId, undefined);
|
|
575
643
|
});
|
|
576
|
-
test('two
|
|
577
|
-
const
|
|
644
|
+
test('two emitters never attribute a tool call to the wrong user', async () => {
|
|
645
|
+
const alice = (0, sessionEvents_1.wrapEmitterWithObservability)(innerEmitter, writer, 'sess-warrant-3', 'req-a', 'alice@acme.com');
|
|
646
|
+
const bob = (0, sessionEvents_1.wrapEmitterWithObservability)(innerEmitter, writer, 'sess-warrant-3', 'req-b', 'bob@acme.com');
|
|
578
647
|
await Promise.all([
|
|
579
|
-
(
|
|
648
|
+
(async () => {
|
|
580
649
|
await new Promise((r) => setTimeout(r, 5));
|
|
581
|
-
await
|
|
582
|
-
}),
|
|
583
|
-
(
|
|
584
|
-
await emitter.toolStart('Bash', 'b', 'b1');
|
|
585
|
-
}),
|
|
650
|
+
await alice.toolStart('Bash', 'a', 'a1');
|
|
651
|
+
})(),
|
|
652
|
+
bob.toolStart('Bash', 'b', 'b1'),
|
|
586
653
|
]);
|
|
587
654
|
const events = await readEvents('sess-warrant-3');
|
|
588
655
|
const byCallId = Object.fromEntries(events.map((e) => [e.payload.callId, e.payload.userId]));
|
|
589
|
-
assert.strictEqual(byCallId['a1'], 'alice@acme.com');
|
|
590
|
-
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');
|
|
659
|
+
});
|
|
660
|
+
test('toolStart/toolEnd keep wrap-time userId even when called outside any credential scope', async () => {
|
|
661
|
+
// Simulates the custom-engine persistent-worker path: wrap captures identity on the
|
|
662
|
+
// request continuation, then tool events fire later on a detached IPC callback with
|
|
663
|
+
// no ambient runWithCredential (or a sticky wrong one). Attribution must not depend
|
|
664
|
+
// on ambient AsyncLocalStorage at emit time.
|
|
665
|
+
const emitter = (0, sessionEvents_1.wrapEmitterWithObservability)(innerEmitter, writer, 'sess-warrant-4', 'req-1', 'alice@acme.com');
|
|
666
|
+
await emitter.toolStart('Bash', 'echo hi', 'c1');
|
|
667
|
+
await emitter.toolEnd('Bash', 'c1', 'hi', false);
|
|
668
|
+
const events = await readEvents('sess-warrant-4');
|
|
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'));
|
|
671
|
+
});
|
|
672
|
+
test('error/turn_start/turn_end also carry the wrap-time userId, not just tool events', async () => {
|
|
673
|
+
// A turn that errors before any tool call (or has no tool calls at all) must still
|
|
674
|
+
// carry identity attribution — the mechanism was already on the instance, it just
|
|
675
|
+
// wasn't threaded into these three payloads.
|
|
676
|
+
const emitter = (0, sessionEvents_1.wrapEmitterWithObservability)(innerEmitter, writer, 'sess-warrant-5', 'req-1', 'bob@acme.com');
|
|
677
|
+
await emitter.emitTurnStart();
|
|
678
|
+
await emitter.error('boom');
|
|
679
|
+
await emitter.emitTurnEnd(false);
|
|
680
|
+
const events = await readEvents('sess-warrant-5');
|
|
681
|
+
assert.strictEqual(events.length, 3);
|
|
682
|
+
const bob = (0, identity_1.hashIdentity)('bob@acme.com');
|
|
683
|
+
assert.strictEqual(events[0].event_type, 'turn_start');
|
|
684
|
+
assert.strictEqual(events[0].payload.userId, bob);
|
|
685
|
+
assert.strictEqual(events[1].event_type, 'error');
|
|
686
|
+
assert.strictEqual(events[1].payload.userId, bob);
|
|
687
|
+
assert.strictEqual(events[2].event_type, 'turn_end');
|
|
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);
|
|
591
755
|
});
|
|
592
756
|
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*---------------------------------------------------------------------------------------------
|
|
3
|
+
* Copyright (c) AgentHippo.ai. All rights reserved.
|
|
4
|
+
*
|
|
5
|
+
* The auth base URL is the endpoint that receives the PKCE code, then the user's AgentHippo
|
|
6
|
+
* access token, and whose bootstrap response is written into the user's model-routing settings.
|
|
7
|
+
* These tests pin the two gates that keep a workspace from steering it.
|
|
8
|
+
*--------------------------------------------------------------------------------------------*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
require("mocha");
|
|
44
|
+
const assert = __importStar(require("assert"));
|
|
45
|
+
const brokerClient_1 = require("../auth/brokerClient");
|
|
46
|
+
const authBaseUrl_1 = require("../auth/authBaseUrl");
|
|
47
|
+
suite('auth base URL — transport gate', () => {
|
|
48
|
+
test('accepts https and strips trailing slashes', () => {
|
|
49
|
+
assert.strictEqual((0, brokerClient_1.assertSafeAuthBaseUrl)('https://auth.agenthippo.ai/'), 'https://auth.agenthippo.ai');
|
|
50
|
+
assert.strictEqual((0, brokerClient_1.assertSafeAuthBaseUrl)('https://auth.example.com/base//'), 'https://auth.example.com/base');
|
|
51
|
+
});
|
|
52
|
+
test('accepts plain http only for loopback (local broker development)', () => {
|
|
53
|
+
assert.strictEqual((0, brokerClient_1.assertSafeAuthBaseUrl)('http://127.0.0.1:9780'), 'http://127.0.0.1:9780');
|
|
54
|
+
assert.strictEqual((0, brokerClient_1.assertSafeAuthBaseUrl)('http://localhost:9780'), 'http://localhost:9780');
|
|
55
|
+
});
|
|
56
|
+
test('refuses cleartext http to a remote host — the bearer token would be on the wire', () => {
|
|
57
|
+
assert.throws(() => (0, brokerClient_1.assertSafeAuthBaseUrl)('http://attacker.example'), /must be https/);
|
|
58
|
+
});
|
|
59
|
+
test('refuses a hostname that merely CONTAINS a loopback name', () => {
|
|
60
|
+
assert.throws(() => (0, brokerClient_1.assertSafeAuthBaseUrl)('http://localhost.attacker.tld'), /must be https/);
|
|
61
|
+
});
|
|
62
|
+
test('refuses a non-http scheme and a malformed URL', () => {
|
|
63
|
+
assert.throws(() => (0, brokerClient_1.assertSafeAuthBaseUrl)('file:///etc/passwd'), /must be https/);
|
|
64
|
+
assert.throws(() => (0, brokerClient_1.assertSafeAuthBaseUrl)('not a url'), /not a valid URL/);
|
|
65
|
+
});
|
|
66
|
+
test('falls back to the product default when nothing is configured', () => {
|
|
67
|
+
const previous = process.env.AGENTHIPPO_AUTH_BASE_URL;
|
|
68
|
+
delete process.env.AGENTHIPPO_AUTH_BASE_URL;
|
|
69
|
+
try {
|
|
70
|
+
assert.strictEqual((0, brokerClient_1.getAuthBrokerBaseUrl)(undefined), brokerClient_1.DEFAULT_AUTH_BROKER_BASE_URL);
|
|
71
|
+
assert.strictEqual((0, brokerClient_1.getAuthBrokerBaseUrl)(' '), brokerClient_1.DEFAULT_AUTH_BROKER_BASE_URL);
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
if (previous === undefined) {
|
|
75
|
+
delete process.env.AGENTHIPPO_AUTH_BASE_URL;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
process.env.AGENTHIPPO_AUTH_BASE_URL = previous;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
test('a bad configured value throws rather than silently falling back', () => {
|
|
83
|
+
assert.throws(() => (0, brokerClient_1.getAuthBrokerBaseUrl)('http://evil.example'), /Refusing to use/);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
suite('auth base URL — scope gate', () => {
|
|
87
|
+
test('a workspace value is ignored in favour of the user value', () => {
|
|
88
|
+
const picked = (0, authBaseUrl_1.pickTrustedAuthBaseUrl)({
|
|
89
|
+
defaultValue: 'https://auth.agenthippo.ai',
|
|
90
|
+
globalValue: 'https://auth.corp.example',
|
|
91
|
+
workspaceValue: 'https://attacker.example',
|
|
92
|
+
});
|
|
93
|
+
assert.strictEqual(picked.value, 'https://auth.corp.example');
|
|
94
|
+
assert.strictEqual(picked.ignoredWorkspaceValue, 'https://attacker.example');
|
|
95
|
+
});
|
|
96
|
+
test('a workspace-FOLDER value is ignored too', () => {
|
|
97
|
+
const picked = (0, authBaseUrl_1.pickTrustedAuthBaseUrl)({
|
|
98
|
+
defaultValue: 'https://auth.agenthippo.ai',
|
|
99
|
+
workspaceFolderValue: 'https://attacker.example',
|
|
100
|
+
});
|
|
101
|
+
assert.strictEqual(picked.value, 'https://auth.agenthippo.ai');
|
|
102
|
+
assert.strictEqual(picked.ignoredWorkspaceValue, 'https://attacker.example');
|
|
103
|
+
});
|
|
104
|
+
test('a remote user value wins over a local one (VS Code precedence)', () => {
|
|
105
|
+
const picked = (0, authBaseUrl_1.pickTrustedAuthBaseUrl)({
|
|
106
|
+
defaultValue: 'https://auth.agenthippo.ai',
|
|
107
|
+
globalValue: 'https://a.example',
|
|
108
|
+
globalLocalValue: 'https://b.example',
|
|
109
|
+
globalRemoteValue: 'https://c.example',
|
|
110
|
+
});
|
|
111
|
+
assert.strictEqual(picked.value, 'https://c.example');
|
|
112
|
+
});
|
|
113
|
+
test('a workspace value identical to the trusted value is not reported as an override', () => {
|
|
114
|
+
const picked = (0, authBaseUrl_1.pickTrustedAuthBaseUrl)({
|
|
115
|
+
defaultValue: 'https://auth.agenthippo.ai',
|
|
116
|
+
workspaceValue: 'https://auth.agenthippo.ai',
|
|
117
|
+
});
|
|
118
|
+
assert.strictEqual(picked.ignoredWorkspaceValue, undefined);
|
|
119
|
+
});
|
|
120
|
+
test('the attack end-to-end: a repo setting agentide.auth.baseUrl never reaches the network layer', () => {
|
|
121
|
+
const warnings = [];
|
|
122
|
+
const resolved = (0, authBaseUrl_1.resolveAuthBrokerBaseUrl)({
|
|
123
|
+
inspect: () => ({
|
|
124
|
+
defaultValue: 'https://auth.agenthippo.ai',
|
|
125
|
+
workspaceValue: 'https://attacker.example',
|
|
126
|
+
}),
|
|
127
|
+
}, (m) => warnings.push(m));
|
|
128
|
+
assert.strictEqual(resolved, 'https://auth.agenthippo.ai');
|
|
129
|
+
assert.strictEqual(warnings.length, 1);
|
|
130
|
+
assert.ok(warnings[0].includes('attacker.example'), 'the ignored value is named in the warning');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -78,25 +78,46 @@ const sampleBootstrap = {
|
|
|
78
78
|
user: { id: 'usr_1', email: 'you@acme.com', displayName: 'You' },
|
|
79
79
|
team: { id: 'team_acme', name: 'Acme', role: 'member' },
|
|
80
80
|
store: { repo: 'acme/store', indexUrl: 'https://acme.github.io/store', githubOrg: 'acme' },
|
|
81
|
-
routing: {
|
|
82
|
-
policy: {
|
|
81
|
+
routing: { inferenceGatewayUrl: 'https://gw.acme/v1' },
|
|
82
|
+
policy: { teamManagedSettings: ['store', 'routing'] },
|
|
83
|
+
};
|
|
84
|
+
const soloBootstrap = {
|
|
85
|
+
version: 1,
|
|
86
|
+
user: { id: 'usr_2', email: 'solo@example.com', displayName: 'Solo' },
|
|
87
|
+
store: { repo: 'acme/store', indexUrl: 'https://acme.github.io/store', githubOrg: 'acme' },
|
|
88
|
+
routing: { inferenceGatewayUrl: 'https://gw.acme/v1' },
|
|
89
|
+
policy: { teamManagedSettings: [] },
|
|
83
90
|
};
|
|
84
91
|
suite('BootstrapApply', () => {
|
|
85
|
-
test('applyIdeBootstrap writes store and routing
|
|
92
|
+
test('applyIdeBootstrap writes store and gateway routing; the litellm slot stays untouched', async () => {
|
|
86
93
|
const { target, settings, global, getApiKeyCalls } = createMockTarget();
|
|
94
|
+
settings.set('agentide.providers.litellm.baseUrl', 'http://127.0.0.1:4000');
|
|
87
95
|
const result = await (0, bootstrapApply_js_1.applyIdeBootstrap)(sampleBootstrap, target);
|
|
88
96
|
assert.deepStrictEqual(result.applied, ['store', 'routing']);
|
|
89
97
|
assert.strictEqual(settings.get('agentide.storeRepo'), 'acme/store');
|
|
90
|
-
assert.strictEqual(settings.get('agentide.
|
|
91
|
-
|
|
98
|
+
assert.strictEqual(settings.get('agentide.AgentHippoModelGateway.url'), 'https://gw.acme/v1');
|
|
99
|
+
// Slot split: sign-in must not overwrite the user-owned litellm provider or install a key.
|
|
100
|
+
assert.strictEqual(settings.get('agentide.providers.litellm.baseUrl'), 'http://127.0.0.1:4000');
|
|
101
|
+
assert.strictEqual(getApiKeyCalls(), 0);
|
|
92
102
|
assert.strictEqual(global.get('agentide.team.id'), 'team_acme');
|
|
93
103
|
});
|
|
104
|
+
test('a solo bootstrap (no team) applies settings without locking them', async () => {
|
|
105
|
+
const { target, settings, global } = createMockTarget();
|
|
106
|
+
const result = await (0, bootstrapApply_js_1.applyIdeBootstrap)(soloBootstrap, target);
|
|
107
|
+
// Settings still apply as defaults…
|
|
108
|
+
assert.deepStrictEqual(result.applied, ['store', 'routing']);
|
|
109
|
+
assert.strictEqual(settings.get('agentide.AgentHippoModelGateway.url'), 'https://gw.acme/v1');
|
|
110
|
+
// …but nothing is marked team-managed and no team identity is recorded.
|
|
111
|
+
assert.strictEqual(global.get('agentide.teamManaged.store'), false);
|
|
112
|
+
assert.strictEqual(global.get('agentide.teamManaged.routing'), false);
|
|
113
|
+
assert.strictEqual(global.get('agentide.team.id'), undefined);
|
|
114
|
+
});
|
|
94
115
|
test('revertTeamManagedBootstrap restores defaults', async () => {
|
|
95
116
|
const { target, settings, global } = createMockTarget();
|
|
96
117
|
await (0, bootstrapApply_js_1.applyIdeBootstrap)(sampleBootstrap, target);
|
|
97
118
|
await (0, bootstrapApply_js_1.revertTeamManagedBootstrap)(target);
|
|
98
119
|
assert.strictEqual(settings.get('agentide.storeRepo'), 'agenthippoai/agenthippo-store');
|
|
99
|
-
assert.strictEqual(settings.get('agentide.
|
|
120
|
+
assert.strictEqual(settings.get('agentide.AgentHippoModelGateway.url'), undefined);
|
|
100
121
|
assert.strictEqual(global.get('agentide.team.id'), undefined);
|
|
101
122
|
});
|
|
102
123
|
});
|