@agenthippo-ai/agenthippo 0.1.0 → 0.2.3-dataplane
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/webview/panel.js +290 -80
- package/out/analytics/sessionEvents.js +21 -4
- package/out/auth/brokerClient.js +1 -1
- package/out/auth/onboardingPrompt.js +112 -0
- package/out/auth/provider.js +1 -1
- package/out/auth-broker/config.js +8 -3
- package/out/auth-broker/pkce.js +3 -1
- package/out/auth-broker/server.js +43 -1
- package/out/auth-broker/store.js +6 -5
- package/out/cli/commands/ask.js +3 -0
- package/out/cli/commands/chat.js +3 -0
- package/out/cli/commands/deploy.js +49 -10
- package/out/cli/commands/gateway.js +54 -0
- package/out/cli/commands/idp.js +85 -0
- package/out/cli/commands/serve.js +401 -49
- package/out/cli/credential-context.js +47 -69
- 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 +195 -0
- package/out/cli/data-plane/index.js +137 -0
- package/out/cli/data-plane/registry.js +80 -0
- package/out/cli/deploy/blueprints/registry.js +84 -0
- package/out/cli/deploy/layers/identity.js +91 -0
- package/out/cli/deploy-init.js +1156 -285
- package/out/cli/idp-init.js +303 -0
- package/out/cli/index.js +46 -8
- package/out/cli/pack-loader.js +37 -2
- package/out/cli/runtime.js +10 -3
- package/out/cli/serve-hardening.js +217 -12
- package/out/cli/state-sync.js +318 -29
- package/out/engines/claude.js +26 -5
- package/out/engines/codex.js +24 -5
- package/out/engines/custom.js +46 -12
- package/out/extension.js +2 -0
- package/out/gateway/outboundSend.js +6 -6
- package/out/headless/index.js +24 -6
- package/out/live-probe/connector.mjs +8 -0
- package/out/live-probe/env-report.mjs +8 -0
- package/out/mcp.js +88 -61
- package/out/remote/handleRemoteChat.js +7 -1
- package/out/remote/serveClient.js +27 -13
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +14 -0
- package/out/shared/models.js +1 -1
- package/out/shared/providers.js +5 -2
- package/out/test/analytics.test.js +93 -0
- package/out/test/auth-broker.test.js +59 -2
- package/out/test/blueprint-registry.test.js +105 -0
- package/out/test/credential-context.test.js +55 -0
- 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 +244 -0
- package/out/test/deploy-init.test.js +287 -5
- package/out/test/egress-broker.test.js +172 -0
- package/out/test/fleetDisplayUtils.test.js +4 -0
- package/out/test/idp-init.test.js +151 -0
- package/out/test/mcp-obo.test.js +52 -27
- package/out/test/mcp.test.js +60 -4
- package/out/test/model-routing.test.js +133 -0
- package/out/test/serve-gateway-turn.test.js +267 -0
- package/out/test/serve-hardening.test.js +187 -0
- package/out/test/serveClient.test.js +37 -2
- package/out/test/state-sync-peer.test.js +83 -1
- package/out/utils.js +27 -11
- package/package.json +3 -4
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*---------------------------------------------------------------------------------------------
|
|
3
|
+
* IdP Init Test Suite (PLAN/deploy.md — "customer 0")
|
|
4
|
+
*
|
|
5
|
+
* Verifies `agenthippo idp init` generates a self-contained stack: no repo dependency in the
|
|
6
|
+
* auth-broker image, identity routes reused verbatim from renderIdentityLayer (no drift risk),
|
|
7
|
+
* and valid YAML/bash throughout.
|
|
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 os = __importStar(require("os"));
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
const child_process_1 = require("child_process");
|
|
49
|
+
const yaml = __importStar(require("js-yaml"));
|
|
50
|
+
const idp_init_1 = require("../cli/idp-init");
|
|
51
|
+
const idp_1 = require("../cli/commands/idp");
|
|
52
|
+
suite('idp-init: renderIdpArtifacts', () => {
|
|
53
|
+
test('generates a self-contained stack — no monorepo build context', () => {
|
|
54
|
+
const files = (0, idp_init_1.renderIdpArtifacts)({ hostname: 'auth.corp.example' });
|
|
55
|
+
assert.ok(files['Dockerfile.auth-broker'].includes('FROM ghcr.io/agenthippoai/runtime'), 'auth-broker image must come from the published runtime image, not repo build artifacts');
|
|
56
|
+
assert.ok(!files['Dockerfile.auth-broker'].includes('extensions/agentide/dist'), 'must not reference this repo\'s local build output — a customer never has it');
|
|
57
|
+
assert.ok(files['docker-compose.yml'].includes('dockerfile: Dockerfile.auth-broker')
|
|
58
|
+
&& files['docker-compose.yml'].includes('context: .'), 'auth-broker builds from the generated dir itself, not a monorepo path');
|
|
59
|
+
});
|
|
60
|
+
test('docker-compose.yml is valid YAML with the expected services and networks', () => {
|
|
61
|
+
const files = (0, idp_init_1.renderIdpArtifacts)({ hostname: 'auth.corp.example' });
|
|
62
|
+
const compose = yaml.load(files['docker-compose.yml']);
|
|
63
|
+
assert.deepStrictEqual(Object.keys(compose.services), ['oauth2-proxy', 'auth-broker', 'caddy', 'cloudflared']);
|
|
64
|
+
assert.ok(!('agent' in compose.services), 'the IdP runs no agent');
|
|
65
|
+
assert.deepStrictEqual(Object.keys(compose.networks), ['default', 'colocate']);
|
|
66
|
+
assert.ok(files['docker-compose.yml'].includes('name: agenthippo-idp-colocate'));
|
|
67
|
+
});
|
|
68
|
+
test('Caddyfile reuses renderIdentityLayer verbatim — self-issued, no agent backend', () => {
|
|
69
|
+
const files = (0, idp_init_1.renderIdpArtifacts)({ hostname: 'auth.corp.example' });
|
|
70
|
+
const caddy = files['Caddyfile'];
|
|
71
|
+
assert.ok(caddy.includes('import agents/*.caddy'), 'colocated agents import in');
|
|
72
|
+
assert.ok(caddy.includes('handle /oauth/authorize {') && caddy.includes('handle /oauth/browser-login {'));
|
|
73
|
+
assert.ok(caddy.includes('forward_auth oauth2-proxy:4180'), 'self-issued: gates via oauth2-proxy');
|
|
74
|
+
assert.ok(caddy.includes('reverse_proxy auth-broker:9780'), 'health checks the broker, not an agent');
|
|
75
|
+
assert.ok(caddy.includes('respond "not_found" 404'), 'no /v1/* API here — Bearer requests 404, not silently passed through');
|
|
76
|
+
assert.ok(caddy.includes('respond "AgentHippo IdP" 200'), 'browser catch-all, past the SSO gate');
|
|
77
|
+
assert.ok(caddy.includes('http://{$AGENTHIPPO_PUBLIC_HOSTNAME} {') && !caddy.includes('https://{$AGENTHIPPO_PUBLIC_HOSTNAME}'), 'plain HTTP — auto_https is off, TLS terminates in front');
|
|
78
|
+
});
|
|
79
|
+
test('.env.auth.example carries the shared JWT secret + Google OAuth client, no fed vars', () => {
|
|
80
|
+
const files = (0, idp_init_1.renderIdpArtifacts)({ hostname: 'auth.corp.example' });
|
|
81
|
+
const auth = files['.env.auth.example'];
|
|
82
|
+
assert.ok(auth.includes('AGENTHIPPO_AUTH_JWT_SECRET') && auth.includes('OAUTH2_PROXY_CLIENT_ID'));
|
|
83
|
+
assert.ok(!auth.includes('AGENTHIPPO_AUTH_IDP_URL'), 'the IdP is not itself federated against anything');
|
|
84
|
+
});
|
|
85
|
+
test('cookie domain defaults to the hostname\'s own parent domain when not given explicitly', () => {
|
|
86
|
+
const files = (0, idp_init_1.renderIdpArtifacts)({ hostname: 'auth.corp.example' });
|
|
87
|
+
assert.ok(files['.env.auth.example'].includes('AGENTHIPPO_AUTH_COOKIE_DOMAIN=.corp.example'));
|
|
88
|
+
});
|
|
89
|
+
test('explicit cookieDomain overrides the derived default', () => {
|
|
90
|
+
const files = (0, idp_init_1.renderIdpArtifacts)({ hostname: 'auth.eu.corp.example', cookieDomain: '.corp.example' });
|
|
91
|
+
assert.ok(files['.env.auth.example'].includes('AGENTHIPPO_AUTH_COOKIE_DOMAIN=.corp.example'));
|
|
92
|
+
});
|
|
93
|
+
test('run.sh is valid bash and reflects the hostname', () => {
|
|
94
|
+
const files = (0, idp_init_1.renderIdpArtifacts)({ hostname: 'auth.corp.example' });
|
|
95
|
+
assert.doesNotThrow(() => (0, child_process_1.execFileSync)('bash', ['-n', '-c', files['run.sh']]));
|
|
96
|
+
assert.ok(files['run.sh'].includes('auth.corp.example'));
|
|
97
|
+
});
|
|
98
|
+
// Bare metal / BYO nginx or ALB (e.g. Hetzner): run.sh must not hard-require a Cloudflare
|
|
99
|
+
// Tunnel, and Caddy's host port must be reconfigurable when 80/443 are already taken by the
|
|
100
|
+
// operator's own reverse proxy.
|
|
101
|
+
test('run.sh branches on an explicit tunnel-credentials path argument, not a fixed file location', () => {
|
|
102
|
+
const sh = (0, idp_init_1.renderIdpArtifacts)({ hostname: 'auth.corp.example' })['run.sh'];
|
|
103
|
+
assert.ok(sh.includes('if [[ $# -ge 1 ]]; then'), 'branches on whether an argument was passed');
|
|
104
|
+
assert.ok(!sh.includes('-f "$HOME/.agent-hippo/tunnel-credentials.json"'), 'must not infer tunnel mode from a fixed default file path');
|
|
105
|
+
assert.ok(sh.includes('AGENTHIPPO_TUNNEL_CREDENTIALS="$1"'), 'the argument becomes the credentials path');
|
|
106
|
+
assert.ok(sh.includes('docker compose --profile edge --profile tunnel up -d --build'));
|
|
107
|
+
assert.ok(sh.includes('docker compose --profile edge up -d --build'), 'no-argument branch: --profile edge only, no --profile tunnel');
|
|
108
|
+
});
|
|
109
|
+
test('run.sh fails loudly if a given tunnel-credentials path doesn\'t exist', () => {
|
|
110
|
+
const sh = (0, idp_init_1.renderIdpArtifacts)({ hostname: 'auth.corp.example' })['run.sh'];
|
|
111
|
+
assert.ok(sh.includes('[[ -f "$AGENTHIPPO_TUNNEL_CREDENTIALS" ]] || {'));
|
|
112
|
+
assert.ok(sh.includes('exit 1'));
|
|
113
|
+
});
|
|
114
|
+
test('docker-compose.yml wires the cloudflared credentials mount to AGENTHIPPO_TUNNEL_CREDENTIALS', () => {
|
|
115
|
+
const compose = (0, idp_init_1.renderIdpArtifacts)({ hostname: 'auth.corp.example' })['docker-compose.yml'];
|
|
116
|
+
assert.ok(compose.includes('${AGENTHIPPO_TUNNEL_CREDENTIALS:-${HOME}/.agent-hippo/tunnel-credentials.json}'));
|
|
117
|
+
});
|
|
118
|
+
test('Caddy host ports are configurable via AGENTHIPPO_CADDY_HTTP_PORT/_HTTPS_PORT (Hetzner: 80/443 may already be taken)', () => {
|
|
119
|
+
const compose = (0, idp_init_1.renderIdpArtifacts)({ hostname: 'auth.corp.example' })['docker-compose.yml'];
|
|
120
|
+
assert.ok(compose.includes('${AGENTHIPPO_CADDY_HTTP_PORT:-80}:80'));
|
|
121
|
+
assert.ok(compose.includes('${AGENTHIPPO_CADDY_HTTPS_PORT:-443}:443'));
|
|
122
|
+
});
|
|
123
|
+
test('README documents both the Cloudflare Tunnel path and the bare-metal/BYO-nginx path', () => {
|
|
124
|
+
const readme = (0, idp_init_1.renderIdpArtifacts)({ hostname: 'auth.corp.example' })['README.md'];
|
|
125
|
+
assert.ok(readme.includes('Cloudflare Tunnel'));
|
|
126
|
+
assert.ok(readme.includes('Bare metal') || readme.includes('BYO nginx'));
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
suite('idp-init: idpCommand writes files to disk', () => {
|
|
130
|
+
let outDir;
|
|
131
|
+
setup(() => {
|
|
132
|
+
outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ah-idp-'));
|
|
133
|
+
});
|
|
134
|
+
teardown(() => {
|
|
135
|
+
fs.rmSync(outDir, { recursive: true, force: true });
|
|
136
|
+
});
|
|
137
|
+
test('writes every generated file under --out', async () => {
|
|
138
|
+
await (0, idp_1.idpCommand)('init', { hostname: 'auth.corp.example', out: outDir });
|
|
139
|
+
for (const rel of ['docker-compose.yml', 'Dockerfile.auth-broker', 'Caddyfile', '.env.example', '.env.auth.example', 'run.sh', 'agents/README.md', 'README.md']) {
|
|
140
|
+
assert.ok(fs.existsSync(path.join(outDir, rel)), `${rel} should be written`);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
test('refuses to overwrite existing files without --force', async () => {
|
|
144
|
+
await (0, idp_1.idpCommand)('init', { hostname: 'auth.corp.example', out: outDir });
|
|
145
|
+
fs.writeFileSync(path.join(outDir, 'README.md'), 'hand-edited\n');
|
|
146
|
+
await (0, idp_1.idpCommand)('init', { hostname: 'auth.corp.example', out: outDir });
|
|
147
|
+
assert.strictEqual(fs.readFileSync(path.join(outDir, 'README.md'), 'utf8'), 'hand-edited\n');
|
|
148
|
+
await (0, idp_1.idpCommand)('init', { hostname: 'auth.corp.example', out: outDir, force: true });
|
|
149
|
+
assert.notStrictEqual(fs.readFileSync(path.join(outDir, 'README.md'), 'utf8'), 'hand-edited\n');
|
|
150
|
+
});
|
|
151
|
+
});
|
package/out/test/mcp-obo.test.js
CHANGED
|
@@ -34,49 +34,74 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
const assert = __importStar(require("assert"));
|
|
37
|
-
const node_fs_1 = require("node:fs");
|
|
38
|
-
const node_os_1 = require("node:os");
|
|
39
|
-
const node_path_1 = require("node:path");
|
|
40
37
|
const credential_context_1 = require("../cli/credential-context");
|
|
41
38
|
const mcp_1 = require("../mcp");
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
/**
|
|
40
|
+
* The per-turn OBO credential must never reach MCP child env implicitly: the supported
|
|
41
|
+
* transport is the loopback data-plane gateway + turn nonce (secure-obo-design §2/§3).
|
|
42
|
+
* Inline resolution exists only behind the explicit local-dev opt-in.
|
|
43
|
+
*/
|
|
44
|
+
suite('mcp: OBO token in MCP env (secure-obo-design §3.4 — inline is opt-in only)', () => {
|
|
45
|
+
const ENV_KEYS = ['DATABRICKS_TOKEN', 'AGENTHIPPO_DATA_TOKEN_ENV_VARS', mcp_1.ALLOW_INLINE_DATA_TOKEN_ENV];
|
|
46
|
+
const saved = {};
|
|
45
47
|
setup(() => {
|
|
46
|
-
|
|
48
|
+
for (const k of ENV_KEYS) {
|
|
49
|
+
saved[k] = process.env[k];
|
|
50
|
+
delete process.env[k];
|
|
51
|
+
}
|
|
47
52
|
});
|
|
48
53
|
teardown(() => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
for (const k of ENV_KEYS) {
|
|
55
|
+
if (saved[k] === undefined) {
|
|
56
|
+
delete process.env[k];
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
process.env[k] = saved[k];
|
|
60
|
+
}
|
|
54
61
|
}
|
|
55
|
-
(0, node_fs_1.rmSync)(tokenDir, { recursive: true, force: true });
|
|
56
62
|
});
|
|
57
|
-
test('static PAT when no credential scope', () => {
|
|
63
|
+
test('static PAT resolves from plain env when no credential scope', () => {
|
|
58
64
|
process.env.DATABRICKS_TOKEN = 'pat-static';
|
|
59
65
|
assert.strictEqual((0, mcp_1.resolveMcpEnvVar)('DATABRICKS_TOKEN'), 'pat-static');
|
|
60
66
|
});
|
|
61
|
-
test('OBO token
|
|
62
|
-
process.env.
|
|
67
|
+
test('WITHOUT the opt-in, a live OBO token never bakes into MCP child env', async () => {
|
|
68
|
+
process.env.AGENTHIPPO_DATA_TOKEN_ENV_VARS = 'DATABRICKS_TOKEN';
|
|
63
69
|
await (0, credential_context_1.runWithCredential)({ userId: 'u@x.com', dataToken: 'obo-live' }, async () => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
await (0, credential_context_1.runWithCredential)({ userId: 'u@x.com', dataToken: 'obo-file-token' }, async () => {
|
|
70
|
+
// alias reference falls back to ambient env (unset here → empty), never the credential
|
|
71
|
+
assert.strictEqual((0, mcp_1.resolveMcpEnvVar)('DATABRICKS_TOKEN'), '');
|
|
72
|
+
// explicit placeholder resolves to nothing → the connector fails closed
|
|
73
|
+
assert.strictEqual((0, mcp_1.resolveMcpEnvVar)('dataToken'), '');
|
|
69
74
|
const resolved = (0, mcp_1.resolveMcpServerEnv)({
|
|
70
75
|
DATABRICKS_HOST: 'dbc.example.com',
|
|
71
76
|
DATABRICKS_TOKEN: '${credential:dataToken}',
|
|
72
77
|
});
|
|
73
78
|
assert.ok(resolved);
|
|
74
79
|
assert.strictEqual(resolved.DATABRICKS_HOST, 'dbc.example.com');
|
|
75
|
-
assert.strictEqual(resolved.DATABRICKS_TOKEN, undefined);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
assert.strictEqual(resolved.DATABRICKS_TOKEN, undefined, 'credential must not appear in spawn env without the opt-in');
|
|
81
|
+
assert.ok(!JSON.stringify(resolved).includes('obo-live'), 'the raw token must not leak anywhere in the resolved env');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
test('WITH the explicit local-dev opt-in, inline resolution works (alias + placeholder)', async () => {
|
|
85
|
+
process.env[mcp_1.ALLOW_INLINE_DATA_TOKEN_ENV] = '1';
|
|
86
|
+
process.env.AGENTHIPPO_DATA_TOKEN_ENV_VARS = 'DATABRICKS_TOKEN';
|
|
87
|
+
process.env.DATABRICKS_TOKEN = 'pat-static';
|
|
88
|
+
await (0, credential_context_1.runWithCredential)({ userId: 'u@x.com', dataToken: 'obo-live' }, async () => {
|
|
89
|
+
assert.strictEqual((0, mcp_1.resolveMcpEnvVar)('DATABRICKS_TOKEN'), 'obo-live', 'OBO token overrides static PAT when opted in');
|
|
90
|
+
const resolved = (0, mcp_1.resolveMcpServerEnv)({ DATABRICKS_TOKEN: '${credential:dataToken}' });
|
|
91
|
+
assert.strictEqual(resolved.DATABRICKS_TOKEN, 'obo-live');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
test('alias list is empty by default — DATABRICKS_TOKEN is no longer an implicit credential alias', async () => {
|
|
95
|
+
process.env[mcp_1.ALLOW_INLINE_DATA_TOKEN_ENV] = '1';
|
|
96
|
+
process.env.DATABRICKS_TOKEN = 'pat-static';
|
|
97
|
+
await (0, credential_context_1.runWithCredential)({ userId: 'u@x.com', dataToken: 'obo-live' }, async () => {
|
|
98
|
+
// No AGENTHIPPO_DATA_TOKEN_ENV_VARS declared → the plain env var wins even opted-in.
|
|
99
|
+
assert.strictEqual((0, mcp_1.resolveMcpEnvVar)('DATABRICKS_TOKEN'), 'pat-static');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
test('no credential scope + placeholder → falls back to ambient env for local dev', () => {
|
|
103
|
+
process.env.DATABRICKS_TOKEN = 'pat-static';
|
|
104
|
+
const resolved = (0, mcp_1.resolveMcpServerEnv)({ DATABRICKS_TOKEN: '${credential:dataToken}' });
|
|
105
|
+
assert.strictEqual(resolved.DATABRICKS_TOKEN, 'pat-static');
|
|
81
106
|
});
|
|
82
107
|
});
|
package/out/test/mcp.test.js
CHANGED
|
@@ -43,6 +43,7 @@ const os = __importStar(require("os"));
|
|
|
43
43
|
const fs_1 = require("fs");
|
|
44
44
|
const mcp_1 = require("../mcp");
|
|
45
45
|
const branding_1 = require("../branding");
|
|
46
|
+
const credential_context_1 = require("../cli/credential-context");
|
|
46
47
|
suite('MCP Server Loading', () => {
|
|
47
48
|
let testDir;
|
|
48
49
|
const getAgentMcpDir = (agentId) => path.join(testDir, branding_1.AGENTIDE_DIR_NAME, 'agents', agentId, 'mcp');
|
|
@@ -225,20 +226,39 @@ suite('MCP Server Loading', () => {
|
|
|
225
226
|
assert.ok(result.includes('[mcp_servers.test-server]'));
|
|
226
227
|
assert.ok(result.includes('command = "node"'));
|
|
227
228
|
assert.ok(result.includes('args = ["/path/to/server.js"]'));
|
|
228
|
-
assert.ok(result.includes('API_KEY = "secret"'));
|
|
229
|
+
assert.ok(result.includes('"API_KEY" = "secret"'));
|
|
229
230
|
// No approval line by default — interactive IDE keeps Codex's normal prompts.
|
|
230
231
|
assert.ok(!result.includes('default_tools_approval_mode'));
|
|
231
232
|
});
|
|
232
|
-
test('
|
|
233
|
+
test('env keys are quoted so names with dots/spaces stay valid TOML', () => {
|
|
234
|
+
const servers = [
|
|
235
|
+
{
|
|
236
|
+
name: 'test-server',
|
|
237
|
+
label: 'Test Server',
|
|
238
|
+
command: 'node',
|
|
239
|
+
args: ['s.js'],
|
|
240
|
+
env: { 'my.token': 'a', 'NPM CONFIG': 'b' },
|
|
241
|
+
},
|
|
242
|
+
];
|
|
243
|
+
const result = (0, mcp_1.generateCodexMcpConfig)(servers);
|
|
244
|
+
assert.ok(result.includes('"my.token" = "a"'));
|
|
245
|
+
assert.ok(result.includes('"NPM CONFIG" = "b"'));
|
|
246
|
+
// A bare (unquoted) dotted key would be misparsed as a nested table.
|
|
247
|
+
assert.ok(!result.includes('my.token = '));
|
|
248
|
+
});
|
|
249
|
+
test('autoApprove emits default_tools_approval_mode = "approve" per server', () => {
|
|
233
250
|
const servers = [
|
|
234
251
|
{ name: 'stdio-srv', label: 's', command: 'node', args: ['s.js'] },
|
|
235
252
|
{ name: 'http-srv', label: 'h', command: '', args: [], url: 'https://mcp.example.com/sse' },
|
|
236
253
|
];
|
|
237
254
|
const result = (0, mcp_1.generateCodexMcpConfig)(servers, { autoApprove: true });
|
|
238
|
-
assert.ok(result.includes('default_tools_approval_mode = "
|
|
255
|
+
assert.ok(result.includes('default_tools_approval_mode = "approve"'));
|
|
239
256
|
assert.ok(result.includes('url = "https://mcp.example.com/sse"'));
|
|
240
|
-
// Invalid legacy key/value must never be emitted.
|
|
257
|
+
// Invalid legacy key/value must never be emitted. "auto" is not a valid enum value
|
|
258
|
+
// either — Codex ≥0.131 ignores it, falls back to `prompt`, and headless exec then
|
|
259
|
+
// cancels every MCP tool call ("user cancelled MCP tool call").
|
|
241
260
|
assert.ok(!result.includes('approval_mode = "never"'));
|
|
261
|
+
assert.ok(!result.includes('"auto"'));
|
|
242
262
|
});
|
|
243
263
|
test('generateCodexMcpConfig emits http_headers for URL servers', () => {
|
|
244
264
|
const servers = [
|
|
@@ -255,6 +275,27 @@ suite('MCP Server Loading', () => {
|
|
|
255
275
|
assert.ok(result.includes('url = "https://mcp.example.com/sse"'));
|
|
256
276
|
assert.ok(result.includes('http_headers = { "Authorization" = "Bearer abc", "X-Custom" = "val" }'));
|
|
257
277
|
});
|
|
278
|
+
test('bearerTokenEnv emits bearer_token_env_var and NEVER a per-turn value — config.toml stays turn-invariant (secure-obo-design §3.2)', async () => {
|
|
279
|
+
const servers = [
|
|
280
|
+
{
|
|
281
|
+
name: 'data',
|
|
282
|
+
label: 'data',
|
|
283
|
+
command: '',
|
|
284
|
+
args: [],
|
|
285
|
+
url: 'http://127.0.0.1:39999/mcp',
|
|
286
|
+
bearerTokenEnv: 'AGENTHIPPO_TURN_NONCE',
|
|
287
|
+
},
|
|
288
|
+
];
|
|
289
|
+
const outsideScope = (0, mcp_1.generateCodexMcpConfig)(servers);
|
|
290
|
+
assert.ok(outsideScope.includes('bearer_token_env_var = "AGENTHIPPO_TURN_NONCE"'));
|
|
291
|
+
assert.ok(!outsideScope.includes('http_headers'), 'no resolved header may carry the nonce into the file');
|
|
292
|
+
// Byte-identical no matter which user's credential scope is ambient — this is the
|
|
293
|
+
// exact property that removes the cross-user config.toml race (defect 1).
|
|
294
|
+
const inAlice = await (0, credential_context_1.runWithCredential)({ userId: 'alice@corp.com', dataToken: 'tok-a' }, async () => (0, mcp_1.generateCodexMcpConfig)(servers));
|
|
295
|
+
const inBob = await (0, credential_context_1.runWithCredential)({ userId: 'bob@corp.com', dataToken: 'tok-b' }, async () => (0, mcp_1.generateCodexMcpConfig)(servers));
|
|
296
|
+
assert.strictEqual(inAlice, outsideScope);
|
|
297
|
+
assert.strictEqual(inBob, outsideScope);
|
|
298
|
+
});
|
|
258
299
|
});
|
|
259
300
|
suite('inline MCP helpers', () => {
|
|
260
301
|
test('formatInlineMcpServers maps resolved servers by name', () => {
|
|
@@ -290,6 +331,21 @@ suite('MCP Server Loading', () => {
|
|
|
290
331
|
remote: { type: 'http', url: 'https://mcp.example.com/sse', headers: { Authorization: 'Bearer abc' } },
|
|
291
332
|
});
|
|
292
333
|
});
|
|
334
|
+
test('bearerTokenEnv resolves against the PER-TURN env into an Authorization header (claude/custom path)', () => {
|
|
335
|
+
const server = {
|
|
336
|
+
name: 'data', label: 'data', command: '', args: [],
|
|
337
|
+
url: 'http://127.0.0.1:39999/mcp', bearerTokenEnv: 'AGENTHIPPO_TURN_NONCE',
|
|
338
|
+
};
|
|
339
|
+
const inline = (0, mcp_1.formatInlineMcpServers)([server], { AGENTHIPPO_TURN_NONCE: 'nonce-123' });
|
|
340
|
+
assert.deepStrictEqual(inline, {
|
|
341
|
+
data: { type: 'http', url: 'http://127.0.0.1:39999/mcp', headers: { Authorization: 'Bearer nonce-123' } },
|
|
342
|
+
});
|
|
343
|
+
// No nonce in the turn env (e.g. IDE) → no Authorization header, entry still valid.
|
|
344
|
+
const withoutNonce = (0, mcp_1.formatInlineMcpServers)([server], {});
|
|
345
|
+
assert.deepStrictEqual(withoutNonce, {
|
|
346
|
+
data: { type: 'http', url: 'http://127.0.0.1:39999/mcp' },
|
|
347
|
+
});
|
|
348
|
+
});
|
|
293
349
|
test('serialize and parse round-trip inline MCP', () => {
|
|
294
350
|
const inline = (0, mcp_1.formatInlineMcpServers)([
|
|
295
351
|
{ name: 'echo', label: 'echo', command: 'echo', args: ['hi'] },
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*---------------------------------------------------------------------------------------------
|
|
3
|
+
* Model routing / credential selection tests.
|
|
4
|
+
*
|
|
5
|
+
* Guards the fix that replaced the provider-blind precedence
|
|
6
|
+
* (apiKey = litellm || anthropic || openai) with destination-based selection: the credential
|
|
7
|
+
* is chosen by the endpoint the engine dials, so a codex turn never authenticates to OpenAI
|
|
8
|
+
* with an Anthropic key (the Databricks 401), and a claude turn never uses the OpenAI key.
|
|
9
|
+
*--------------------------------------------------------------------------------------------*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
require("mocha");
|
|
45
|
+
const assert = __importStar(require("assert"));
|
|
46
|
+
const utils_1 = require("../utils");
|
|
47
|
+
const pack_loader_1 = require("../cli/pack-loader");
|
|
48
|
+
const OPENAI = 'sk-openai-xxx';
|
|
49
|
+
const ANTHROPIC = 'sk-ant-yyy';
|
|
50
|
+
const LITELLM = 'sk-litellm-zzz';
|
|
51
|
+
function opts(over = {}) {
|
|
52
|
+
return { workspaceRoot: '/ws', ...over };
|
|
53
|
+
}
|
|
54
|
+
suite('resolveApiRouting: direct path selects the ENGINE\'s provider key (no collapse)', () => {
|
|
55
|
+
// The exact Databricks scenario: both direct keys present, no proxy configured.
|
|
56
|
+
const bothKeys = { openaiApiKey: OPENAI, anthropicApiKey: ANTHROPIC };
|
|
57
|
+
test('codex (openai) picks the OpenAI key even when an Anthropic key is also present', () => {
|
|
58
|
+
const r = (0, utils_1.resolveApiRouting)(opts({ useLiteLLM: false }), bothKeys, 'openai');
|
|
59
|
+
assert.strictEqual(r.apiKey, OPENAI);
|
|
60
|
+
assert.strictEqual(r.useLiteLLM, false);
|
|
61
|
+
});
|
|
62
|
+
test('claude (anthropic) picks the Anthropic key even when an OpenAI key is also present', () => {
|
|
63
|
+
const r = (0, utils_1.resolveApiRouting)(opts({ useLiteLLM: false }), bothKeys, 'anthropic');
|
|
64
|
+
assert.strictEqual(r.apiKey, ANTHROPIC);
|
|
65
|
+
});
|
|
66
|
+
test('the codex→OpenAI-with-Anthropic-key 401 is impossible now', () => {
|
|
67
|
+
const r = (0, utils_1.resolveApiRouting)(opts({ useLiteLLM: false }), bothKeys, 'openai');
|
|
68
|
+
assert.notStrictEqual(r.apiKey, ANTHROPIC, 'codex must never receive the Anthropic key');
|
|
69
|
+
});
|
|
70
|
+
test('missing provider key → empty (fail visibly), never the other provider\'s key', () => {
|
|
71
|
+
const onlyAnthropic = { anthropicApiKey: ANTHROPIC };
|
|
72
|
+
const r = (0, utils_1.resolveApiRouting)(opts({ useLiteLLM: false }), onlyAnthropic, 'openai');
|
|
73
|
+
assert.strictEqual(r.apiKey, '', 'codex with only an Anthropic key gets no key, not the wrong one');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
suite('resolveApiRouting: LiteLLM path uses the proxy key for every engine', () => {
|
|
77
|
+
const creds = { litellmApiKey: LITELLM, litellmBaseUrl: 'http://proxy:4000', openaiApiKey: OPENAI, anthropicApiKey: ANTHROPIC };
|
|
78
|
+
test('codex through the proxy uses the LiteLLM key + base URL', () => {
|
|
79
|
+
const r = (0, utils_1.resolveApiRouting)(opts({ useLiteLLM: true }), creds, 'openai');
|
|
80
|
+
assert.strictEqual(r.apiKey, LITELLM);
|
|
81
|
+
assert.strictEqual(r.baseUrl, 'http://proxy:4000');
|
|
82
|
+
assert.strictEqual(r.useLiteLLM, true);
|
|
83
|
+
});
|
|
84
|
+
test('claude through the proxy also uses the LiteLLM key (one key routes all)', () => {
|
|
85
|
+
const r = (0, utils_1.resolveApiRouting)(opts({ useLiteLLM: true }), creds, 'anthropic');
|
|
86
|
+
assert.strictEqual(r.apiKey, LITELLM);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
suite('resolveApiRouting: explicit options.apiKey (IDE single key) still wins', () => {
|
|
90
|
+
test('IDE explicit key overrides the provider fallback on the direct path', () => {
|
|
91
|
+
const r = (0, utils_1.resolveApiRouting)(opts({ useLiteLLM: false, apiKey: 'sk-ide-explicit' }), { openaiApiKey: OPENAI }, 'openai');
|
|
92
|
+
assert.strictEqual(r.apiKey, 'sk-ide-explicit');
|
|
93
|
+
});
|
|
94
|
+
test('IDE with only an explicit key and no creds still works (back-compat)', () => {
|
|
95
|
+
const r = (0, utils_1.resolveApiRouting)(opts({ useLiteLLM: false, apiKey: 'sk-ide-explicit' }), {}, 'anthropic');
|
|
96
|
+
assert.strictEqual(r.apiKey, 'sk-ide-explicit');
|
|
97
|
+
});
|
|
98
|
+
test('proxy path keeps the sk-dummy fallback when nothing is configured', () => {
|
|
99
|
+
const r = (0, utils_1.resolveApiRouting)(opts({ useLiteLLM: true }), {}, 'openai');
|
|
100
|
+
assert.strictEqual(r.apiKey, 'sk-dummy');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
suite('inferModelProvider: custom engines pick provider from the model id', () => {
|
|
104
|
+
test('claude/anthropic model ids → anthropic', () => {
|
|
105
|
+
assert.strictEqual((0, utils_1.inferModelProvider)('claude-sonnet-4-5'), 'anthropic');
|
|
106
|
+
assert.strictEqual((0, utils_1.inferModelProvider)('litellm/claude-haiku-4-5'), 'anthropic');
|
|
107
|
+
assert.strictEqual((0, utils_1.inferModelProvider)('anthropic/claude-opus'), 'anthropic');
|
|
108
|
+
});
|
|
109
|
+
test('everything else → openai (the safe default for gpt/codex/unknown)', () => {
|
|
110
|
+
assert.strictEqual((0, utils_1.inferModelProvider)('gpt-5.5'), 'openai');
|
|
111
|
+
assert.strictEqual((0, utils_1.inferModelProvider)('litellm/gpt-5.4-nano'), 'openai');
|
|
112
|
+
assert.strictEqual((0, utils_1.inferModelProvider)(undefined), 'openai');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
suite('hasCredentialForEngine: engine-aware boot check', () => {
|
|
116
|
+
const base = { apiKey: undefined, baseUrl: undefined, useLiteLLM: false, litellmApiKey: undefined, openaiApiKey: undefined, anthropicApiKey: undefined };
|
|
117
|
+
test('codex needs an OpenAI key; an Anthropic-only env fails the check', () => {
|
|
118
|
+
assert.strictEqual((0, pack_loader_1.hasCredentialForEngine)('codex', { ...base, anthropicApiKey: ANTHROPIC }), false);
|
|
119
|
+
assert.strictEqual((0, pack_loader_1.hasCredentialForEngine)('codex', { ...base, openaiApiKey: OPENAI }), true);
|
|
120
|
+
});
|
|
121
|
+
test('claude needs an Anthropic key; an OpenAI-only env fails the check', () => {
|
|
122
|
+
assert.strictEqual((0, pack_loader_1.hasCredentialForEngine)('claude', { ...base, openaiApiKey: OPENAI }), false);
|
|
123
|
+
assert.strictEqual((0, pack_loader_1.hasCredentialForEngine)('claude', { ...base, anthropicApiKey: ANTHROPIC }), true);
|
|
124
|
+
});
|
|
125
|
+
test('a configured LiteLLM proxy satisfies every engine', () => {
|
|
126
|
+
const proxy = { ...base, useLiteLLM: true, litellmApiKey: LITELLM, baseUrl: 'http://proxy:4000' };
|
|
127
|
+
assert.strictEqual((0, pack_loader_1.hasCredentialForEngine)('codex', proxy), true);
|
|
128
|
+
assert.strictEqual((0, pack_loader_1.hasCredentialForEngine)('claude', proxy), true);
|
|
129
|
+
});
|
|
130
|
+
test('custom engines are not gated here (they authenticate via their own manifest env var)', () => {
|
|
131
|
+
assert.strictEqual((0, pack_loader_1.hasCredentialForEngine)('openclaw', base), true);
|
|
132
|
+
});
|
|
133
|
+
});
|