@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
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*---------------------------------------------------------------------------------------------
|
|
3
|
+
* Data-plane gateway integration tests (secure-obo-design §3, §5, §9)
|
|
4
|
+
*
|
|
5
|
+
* Boots the real thing end to end: TurnRegistry + CredentialBroker + a real connector
|
|
6
|
+
* subprocess (stdio MCP over a private pipe) + the loopback streamable-HTTP gateway —
|
|
7
|
+
* then drives it with the MCP SDK client exactly like an engine would.
|
|
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 fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
|
|
48
|
+
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
49
|
+
const data_plane_1 = require("../cli/data-plane");
|
|
50
|
+
// The fixture connector must live INSIDE the extension tree so its `import` of
|
|
51
|
+
// @modelcontextprotocol/sdk resolves by walking up to extensions/agentide/node_modules.
|
|
52
|
+
const FIXTURE_DIR = path.join(__dirname, 'data-plane-fixture');
|
|
53
|
+
const CONNECTOR_PATH = path.join(FIXTURE_DIR, 'connector.mjs');
|
|
54
|
+
const CONNECTOR_SOURCE = `
|
|
55
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
56
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
57
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
58
|
+
|
|
59
|
+
const server = new Server({ name: 'test-connector', version: '0.0.1' }, { capabilities: { tools: {} } });
|
|
60
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
61
|
+
tools: [{
|
|
62
|
+
name: 'whoami',
|
|
63
|
+
description: 'echo the broker-supplied turn context',
|
|
64
|
+
inputSchema: { type: 'object', properties: { probe: { type: 'string' } } },
|
|
65
|
+
}],
|
|
66
|
+
}));
|
|
67
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
68
|
+
const turn = (req.params._meta ?? {})['ai.agenthippo/turn'] ?? null;
|
|
69
|
+
const probe = (req.params.arguments ?? {}).probe ?? '';
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
72
|
+
userId: turn ? turn.userId : null,
|
|
73
|
+
credential: turn ? turn.credential : null,
|
|
74
|
+
envContainsProbe: probe ? JSON.stringify(process.env).includes(probe) : false,
|
|
75
|
+
}) }],
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
await server.connect(new StdioServerTransport());
|
|
79
|
+
`;
|
|
80
|
+
function mcpClient(url, nonce) {
|
|
81
|
+
const transport = new streamableHttp_js_1.StreamableHTTPClientTransport(new URL(url), {
|
|
82
|
+
requestInit: { headers: { Authorization: `Bearer ${nonce}` } },
|
|
83
|
+
});
|
|
84
|
+
const client = new index_js_1.Client({ name: 'test-engine', version: '0.0.1' });
|
|
85
|
+
return { client, transport };
|
|
86
|
+
}
|
|
87
|
+
async function callWhoami(url, nonce, probe) {
|
|
88
|
+
const { client, transport } = mcpClient(url, nonce);
|
|
89
|
+
try {
|
|
90
|
+
await client.connect(transport);
|
|
91
|
+
const res = await client.callTool({ name: 'whoami', arguments: probe ? { probe } : {} });
|
|
92
|
+
const content = res.content[0];
|
|
93
|
+
return JSON.parse(content.text);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
await client.close().catch(() => undefined);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
suite('data-plane: gateway + registry + broker end to end', function () {
|
|
100
|
+
this.timeout(30_000);
|
|
101
|
+
let plane;
|
|
102
|
+
suiteSetup(async () => {
|
|
103
|
+
fs.mkdirSync(FIXTURE_DIR, { recursive: true });
|
|
104
|
+
fs.writeFileSync(CONNECTOR_PATH, CONNECTOR_SOURCE, 'utf8');
|
|
105
|
+
plane = await data_plane_1.DataPlane.start({ connectorPath: CONNECTOR_PATH, provider: 'passthrough' });
|
|
106
|
+
});
|
|
107
|
+
suiteTeardown(async () => {
|
|
108
|
+
await plane?.close();
|
|
109
|
+
fs.rmSync(FIXTURE_DIR, { recursive: true, force: true });
|
|
110
|
+
});
|
|
111
|
+
test('gateway binds loopback only', () => {
|
|
112
|
+
assert.ok(plane.url.startsWith('http://127.0.0.1:'), plane.url);
|
|
113
|
+
});
|
|
114
|
+
test('a live nonce lists and calls tools as the registered user, credential selected server-side', async () => {
|
|
115
|
+
const turn = plane.registerTurn({ userId: 'alice@corp.com', sessionKey: 's1', inboundToken: 'alice-obo-token' });
|
|
116
|
+
try {
|
|
117
|
+
const { client, transport } = mcpClient(plane.url, turn.nonce);
|
|
118
|
+
await client.connect(transport);
|
|
119
|
+
const tools = await client.listTools();
|
|
120
|
+
assert.deepStrictEqual(tools.tools.map((t) => t.name), ['whoami']);
|
|
121
|
+
await client.close();
|
|
122
|
+
const who = await callWhoami(plane.url, turn.nonce);
|
|
123
|
+
assert.strictEqual(who.userId, 'alice@corp.com');
|
|
124
|
+
assert.strictEqual(who.credential, 'alice-obo-token', 'the broker must hand the connector the turn user\'s credential');
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
turn.release();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
test('the credential never enters the connector process env — it rides only the per-call pipe', async () => {
|
|
131
|
+
const turn = plane.registerTurn({ userId: 'alice@corp.com', sessionKey: 's1', inboundToken: 'alice-obo-token' });
|
|
132
|
+
try {
|
|
133
|
+
const who = await callWhoami(plane.url, turn.nonce, 'alice-obo-token');
|
|
134
|
+
assert.strictEqual(who.envContainsProbe, false, 'raw credential must not appear anywhere in the connector env');
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
turn.release();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
test('two concurrent turns each resolve their OWN user + credential (the §9.1 concurrency property)', async () => {
|
|
141
|
+
const alice = plane.registerTurn({ userId: 'alice@corp.com', sessionKey: 'sa', inboundToken: 'tok-alice' });
|
|
142
|
+
const bob = plane.registerTurn({ userId: 'bob@corp.com', sessionKey: 'sb', inboundToken: 'tok-bob' });
|
|
143
|
+
try {
|
|
144
|
+
const [a, b] = await Promise.all([
|
|
145
|
+
callWhoami(plane.url, alice.nonce),
|
|
146
|
+
callWhoami(plane.url, bob.nonce),
|
|
147
|
+
]);
|
|
148
|
+
assert.strictEqual(a.userId, 'alice@corp.com');
|
|
149
|
+
assert.strictEqual(a.credential, 'tok-alice');
|
|
150
|
+
assert.strictEqual(b.userId, 'bob@corp.com');
|
|
151
|
+
assert.strictEqual(b.credential, 'tok-bob');
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
alice.release();
|
|
155
|
+
bob.release();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
test('missing / unknown nonce → 401, fail closed', async () => {
|
|
159
|
+
await assert.rejects(() => callWhoami(plane.url, 'not-a-real-nonce'), /401|Unauthorized/i);
|
|
160
|
+
});
|
|
161
|
+
test('a released (turn-ended) nonce → 401 — a stale engine fails closed (§3.2)', async () => {
|
|
162
|
+
const turn = plane.registerTurn({ userId: 'alice@corp.com', sessionKey: 's1', inboundToken: 't' });
|
|
163
|
+
const nonce = turn.nonce;
|
|
164
|
+
const who = await callWhoami(plane.url, nonce);
|
|
165
|
+
assert.strictEqual(who.userId, 'alice@corp.com');
|
|
166
|
+
turn.release();
|
|
167
|
+
await assert.rejects(() => callWhoami(plane.url, nonce), /401|Unauthorized/i);
|
|
168
|
+
});
|
|
169
|
+
test('an expired deadline → 401 even if release() was never called (stranded-nonce backstop)', async () => {
|
|
170
|
+
const { nonce } = plane.registry.register({
|
|
171
|
+
userId: 'alice@corp.com',
|
|
172
|
+
sessionKey: 's1',
|
|
173
|
+
inboundToken: 't',
|
|
174
|
+
deadline: Date.now() - 1,
|
|
175
|
+
});
|
|
176
|
+
await assert.rejects(() => callWhoami(plane.url, nonce), /401|Unauthorized/i);
|
|
177
|
+
});
|
|
178
|
+
test('passthrough provider with no inbound token → the TOOL CALL errors, fail closed, turn itself unaffected', async () => {
|
|
179
|
+
const turn = plane.registerTurn({ userId: 'peer:telegram:123', sessionKey: 's2' });
|
|
180
|
+
try {
|
|
181
|
+
const { client, transport } = mcpClient(plane.url, turn.nonce);
|
|
182
|
+
await client.connect(transport);
|
|
183
|
+
const res = await client.callTool({ name: 'whoami', arguments: {} });
|
|
184
|
+
assert.strictEqual(res.isError, true, 'no inbound credential must fail the call closed');
|
|
185
|
+
const text = res.content[0].text;
|
|
186
|
+
assert.match(text, /fail-closed/);
|
|
187
|
+
await client.close();
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
turn.release();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
test('a token-less turn fails closed even RIGHT AFTER the same user ran a turn WITH a token (no cache bridging)', async () => {
|
|
194
|
+
// Passthrough gets the platform token on every legitimate request; a broker cache
|
|
195
|
+
// that outlives the turn would let broken forwarding silently reuse the previous
|
|
196
|
+
// token for the cache TTL — the exact silent-fallback failure the provider forbids.
|
|
197
|
+
const withToken = plane.registerTurn({ userId: 'u@corp.com', sessionKey: 's3', inboundToken: 'tok-1' });
|
|
198
|
+
try {
|
|
199
|
+
const ok = await callWhoami(plane.url, withToken.nonce);
|
|
200
|
+
assert.strictEqual(ok.credential, 'tok-1');
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
withToken.release();
|
|
204
|
+
}
|
|
205
|
+
const withoutToken = plane.registerTurn({ userId: 'u@corp.com', sessionKey: 's3' });
|
|
206
|
+
try {
|
|
207
|
+
const { client, transport } = mcpClient(plane.url, withoutToken.nonce);
|
|
208
|
+
await client.connect(transport);
|
|
209
|
+
const res = await client.callTool({ name: 'whoami', arguments: {} });
|
|
210
|
+
assert.strictEqual(res.isError, true, 'the cached tok-1 must not serve a token-less turn');
|
|
211
|
+
await client.close();
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
withoutToken.release();
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
suite('data-plane: TurnRegistry unit properties', () => {
|
|
219
|
+
test('raw nonces are never map keys — lookups go through sha256', () => {
|
|
220
|
+
const reg = new data_plane_1.TurnRegistry();
|
|
221
|
+
const { nonce } = reg.register({ userId: 'u', sessionKey: 's' });
|
|
222
|
+
assert.ok(reg.resolve(nonce));
|
|
223
|
+
// resolve by the HASH must fail — only the raw nonce authenticates.
|
|
224
|
+
assert.strictEqual(reg.resolve((0, data_plane_1.hashNonce)(nonce)), undefined);
|
|
225
|
+
});
|
|
226
|
+
test('release is idempotent and deletes exactly its own entry', () => {
|
|
227
|
+
const reg = new data_plane_1.TurnRegistry();
|
|
228
|
+
const a = reg.register({ userId: 'a', sessionKey: 's' });
|
|
229
|
+
const b = reg.register({ userId: 'b', sessionKey: 's' });
|
|
230
|
+
a.release();
|
|
231
|
+
a.release();
|
|
232
|
+
assert.strictEqual(reg.resolve(a.nonce), undefined);
|
|
233
|
+
assert.strictEqual(reg.resolve(b.nonce)?.userId, 'b');
|
|
234
|
+
b.release();
|
|
235
|
+
assert.strictEqual(reg.size, 0);
|
|
236
|
+
});
|
|
237
|
+
test('expired entries are pruned on register', () => {
|
|
238
|
+
const reg = new data_plane_1.TurnRegistry();
|
|
239
|
+
reg.register({ userId: 'a', sessionKey: 's', deadline: Date.now() - 1 });
|
|
240
|
+
assert.strictEqual(reg.size, 1);
|
|
241
|
+
reg.register({ userId: 'b', sessionKey: 's' });
|
|
242
|
+
assert.strictEqual(reg.size, 1, 'the expired entry must have been pruned');
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
suite('data-plane: sidecar EDGE registration (P1 fix — no forging authority in the agent container)', function () {
|
|
246
|
+
this.timeout(30_000);
|
|
247
|
+
const EDGE_TOKEN = 'edge-only-secret';
|
|
248
|
+
let sidecar;
|
|
249
|
+
let controlBase;
|
|
250
|
+
suiteSetup(async () => {
|
|
251
|
+
fs.mkdirSync(FIXTURE_DIR, { recursive: true });
|
|
252
|
+
fs.writeFileSync(CONNECTOR_PATH, CONNECTOR_SOURCE, 'utf8');
|
|
253
|
+
sidecar = await (0, data_plane_1.startDataPlaneSidecar)({
|
|
254
|
+
config: { connectorPath: CONNECTOR_PATH, provider: 'identity' },
|
|
255
|
+
registerToken: EDGE_TOKEN,
|
|
256
|
+
gatewayPort: 0,
|
|
257
|
+
controlPort: 0,
|
|
258
|
+
bindHost: '127.0.0.1',
|
|
259
|
+
});
|
|
260
|
+
controlBase = `http://127.0.0.1:${sidecar.controlPort}`;
|
|
261
|
+
});
|
|
262
|
+
suiteTeardown(async () => {
|
|
263
|
+
await sidecar?.close();
|
|
264
|
+
fs.rmSync(FIXTURE_DIR, { recursive: true, force: true });
|
|
265
|
+
});
|
|
266
|
+
async function edgeRegister(user, token = EDGE_TOKEN) {
|
|
267
|
+
// Shaped like Caddy forward_auth: GET, no body, verified identity in X-Agent-User.
|
|
268
|
+
return fetch(`${controlBase}/edge/turn`, {
|
|
269
|
+
headers: { Authorization: `Bearer ${token}`, 'X-Agent-User': user },
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
test('the edge mints a nonce for the verified user; the nonce works at the gateway as that user', async () => {
|
|
273
|
+
const res = await edgeRegister('alice@corp.com');
|
|
274
|
+
assert.strictEqual(res.status, 200);
|
|
275
|
+
const nonce = res.headers.get(data_plane_1.TURN_NONCE_HEADER);
|
|
276
|
+
assert.ok(nonce, 'nonce must ride the response header for Caddy copy_headers');
|
|
277
|
+
const who = await callWhoami(sidecar.gatewayUrl, nonce);
|
|
278
|
+
assert.strictEqual(who.userId, 'alice@corp.com');
|
|
279
|
+
});
|
|
280
|
+
test('edge registration without the edge token → 401 (the engine can route here but cannot forge)', async () => {
|
|
281
|
+
const res = await edgeRegister('victim@corp.com', 'stolen-or-missing');
|
|
282
|
+
assert.strictEqual(res.status, 401);
|
|
283
|
+
});
|
|
284
|
+
test('edge registration without a verified identity header → 400, fail closed', async () => {
|
|
285
|
+
const res = await fetch(`${controlBase}/edge/turn`, { headers: { Authorization: `Bearer ${EDGE_TOKEN}` } });
|
|
286
|
+
assert.strictEqual(res.status, 400);
|
|
287
|
+
});
|
|
288
|
+
test('serve releases an adopted turn via the gateway using ONLY the nonce; the nonce then fails closed', async () => {
|
|
289
|
+
const res = await edgeRegister('bob@corp.com');
|
|
290
|
+
const nonce = res.headers.get(data_plane_1.TURN_NONCE_HEADER);
|
|
291
|
+
const edge = new data_plane_1.EdgeDataPlane({ gatewayUrl: sidecar.gatewayUrl });
|
|
292
|
+
const turn = edge.adoptTurn(nonce);
|
|
293
|
+
assert.strictEqual(turn.env.AGENTHIPPO_TURN_NONCE, nonce);
|
|
294
|
+
assert.strictEqual(turn.env.AGENTHIPPO_DATA_MCP_URL, sidecar.gatewayUrl);
|
|
295
|
+
const who = await callWhoami(sidecar.gatewayUrl, nonce);
|
|
296
|
+
assert.strictEqual(who.userId, 'bob@corp.com');
|
|
297
|
+
await turn.release();
|
|
298
|
+
await assert.rejects(() => callWhoami(sidecar.gatewayUrl, nonce), /401|Unauthorized/i);
|
|
299
|
+
});
|
|
300
|
+
test('release with a bogus nonce is a harmless no-op (204) and touches no other turn', async () => {
|
|
301
|
+
const res = await edgeRegister('carol@corp.com');
|
|
302
|
+
const nonce = res.headers.get(data_plane_1.TURN_NONCE_HEADER);
|
|
303
|
+
const del = await fetch(new URL('/turn', sidecar.gatewayUrl), {
|
|
304
|
+
method: 'DELETE',
|
|
305
|
+
headers: { Authorization: 'Bearer not-a-real-nonce' },
|
|
306
|
+
});
|
|
307
|
+
assert.strictEqual(del.status, 204);
|
|
308
|
+
const who = await callWhoami(sidecar.gatewayUrl, nonce);
|
|
309
|
+
assert.strictEqual(who.userId, 'carol@corp.com', 'an unrelated release must not end a live turn');
|
|
310
|
+
});
|
|
311
|
+
});
|