@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.
Files changed (158) hide show
  1. package/out/agentpack/index.js +2 -1
  2. package/out/agentpack/paths.js +23 -0
  3. package/out/analytics/dashboard/fleetDisplayUtils.js +3 -0
  4. package/out/analytics/dashboard/provider.js +13 -2
  5. package/out/analytics/dashboard/webview/panel.js +293 -82
  6. package/out/analytics/identity.js +0 -0
  7. package/out/analytics/service.js +23 -25
  8. package/out/analytics/sessionEvents.js +109 -22
  9. package/out/auth/authBaseUrl.js +59 -0
  10. package/out/auth/bootstrapApply.js +9 -10
  11. package/out/auth/brokerClient.js +49 -15
  12. package/out/auth/onboardingPrompt.js +112 -0
  13. package/out/auth/provider.js +9 -22
  14. package/out/auth/sessionStore.js +3 -40
  15. package/out/auth/signInFlow.js +2 -1
  16. package/out/auth-broker/config.js +54 -4
  17. package/out/auth-broker/server.js +152 -20
  18. package/out/auth-broker/store.js +25 -24
  19. package/out/authority/warrant.js +206 -0
  20. package/out/branding.js +27 -1
  21. package/out/cli/commands/ask.js +3 -0
  22. package/out/cli/commands/chat.js +3 -0
  23. package/out/cli/commands/conformance.js +249 -0
  24. package/out/cli/commands/data-plane.js +67 -0
  25. package/out/cli/commands/deploy.js +50 -10
  26. package/out/cli/commands/gateway.js +58 -0
  27. package/out/cli/commands/idp.js +85 -0
  28. package/out/cli/commands/pack.js +182 -3
  29. package/out/cli/commands/sandbox.js +332 -0
  30. package/out/cli/commands/serve.js +1165 -82
  31. package/out/cli/commands/store.js +12 -4
  32. package/out/cli/credential-context.js +42 -80
  33. package/out/cli/data-plane/broker.js +78 -0
  34. package/out/cli/data-plane/connector-host.js +121 -0
  35. package/out/cli/data-plane/egress.js +189 -0
  36. package/out/cli/data-plane/gateway.js +218 -0
  37. package/out/cli/data-plane/index.js +152 -0
  38. package/out/cli/data-plane/registry.js +89 -0
  39. package/out/cli/data-plane/remote.js +106 -0
  40. package/out/cli/data-plane/sidecar.js +161 -0
  41. package/out/cli/deploy/blueprints/registry.js +84 -0
  42. package/out/cli/deploy/layers/identity.js +172 -0
  43. package/out/cli/deploy-init.js +1182 -347
  44. package/out/cli/deps.js +106 -2
  45. package/out/cli/idp-init.js +338 -0
  46. package/out/cli/index.js +192 -23
  47. package/out/cli/pack-install-ref.js +15 -0
  48. package/out/cli/pack-loader.js +45 -3
  49. package/out/cli/pack-runtime.js +61 -0
  50. package/out/cli/runtime.js +44 -3
  51. package/out/cli/serve-hardening.js +139 -3
  52. package/out/cli/state-sync.js +16 -15
  53. package/out/cli/utils.js +12 -1
  54. package/out/engines/base.js +32 -3
  55. package/out/engines/claude.js +47 -8
  56. package/out/engines/codex.js +125 -22
  57. package/out/engines/custom.js +54 -12
  58. package/out/evidence/sessionShip.js +610 -0
  59. package/out/extension.js +13 -2
  60. package/out/gateway/bridge.js +40 -2
  61. package/out/gateway/client.js +34 -1
  62. package/out/gateway/installSource.js +68 -0
  63. package/out/gateway/lifecycle.js +185 -51
  64. package/out/gateway/outboundPaths.js +4 -1
  65. package/out/gateway/outboundSend.js +6 -6
  66. package/out/gateway/secretsPreflight.js +90 -0
  67. package/out/gateway/service.js +7 -2
  68. package/out/headless/index.js +40 -9
  69. package/out/live-e2e/mcp-demo/server.mjs +32 -0
  70. package/out/live-probe/connector.mjs +8 -0
  71. package/out/live-probe/env-report.mjs +8 -0
  72. package/out/mcp.js +115 -67
  73. package/out/pack/caps.js +271 -0
  74. package/out/pack/governor.js +309 -0
  75. package/out/pack/lease-schema.js +270 -0
  76. package/out/pack/lease.js +640 -0
  77. package/out/pack/reconcile.js +217 -0
  78. package/out/pack/signature.js +839 -0
  79. package/out/remote/handleRemoteChat.js +44 -3
  80. package/out/remote/remoteTrust.js +277 -0
  81. package/out/remote/serveClient.js +52 -5
  82. package/out/runner/host.js +131 -0
  83. package/out/runner/protocol.js +108 -0
  84. package/out/runner/remoteEngine.js +130 -0
  85. package/out/runtime/interfaces/edit-tracker.js +2 -0
  86. package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
  87. package/out/sandbox/containerDriver.js +570 -0
  88. package/out/sandbox/processDriver.js +315 -0
  89. package/out/sandbox/sandboxRunner.js +191 -0
  90. package/out/sandbox/support.js +301 -0
  91. package/out/sandbox/types.js +55 -0
  92. package/out/scripts/agentide-litellm.sh +67 -5
  93. package/out/scripts/agentide_litellm_hooks_v1.py +182 -21
  94. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +94 -118
  95. package/out/scripts/analytics/setup-analytics.ps1 +37 -13
  96. package/out/scripts/analytics/setup-analytics.sh +93 -13
  97. package/out/scripts/analytics/start-analytics.sh +86 -14
  98. package/out/scripts/gateway/setup-gateway.sh +72 -32
  99. package/out/shared/models.js +12 -111
  100. package/out/shared/providers.js +2 -2
  101. package/out/spotlight/install.js +7 -1
  102. package/out/store/index.js +41 -10
  103. package/out/sync.js +3 -1
  104. package/out/test/analytics.test.js +198 -34
  105. package/out/test/auth-baseurl.test.js +132 -0
  106. package/out/test/auth-bootstrapApply.test.js +27 -6
  107. package/out/test/auth-broker-hardening.test.js +227 -0
  108. package/out/test/auth-broker.test.js +112 -9
  109. package/out/test/blueprint-registry.test.js +105 -0
  110. package/out/test/branding.test.js +13 -0
  111. package/out/test/caps-governor.test.js +455 -0
  112. package/out/test/codex-new-file-survival.test.js +119 -0
  113. package/out/test/configuration-scopes.test.js +131 -0
  114. package/out/test/credential-context.test.js +23 -28
  115. package/out/test/custom-env-layering.test.js +151 -0
  116. package/out/test/data-plane-engine-wiring.test.js +250 -0
  117. package/out/test/data-plane.test.js +311 -0
  118. package/out/test/deploy-init.test.js +295 -4
  119. package/out/test/egress-broker.test.js +172 -0
  120. package/out/test/fleetDisplayUtils.test.js +4 -0
  121. package/out/test/gateway-client.test.js +28 -0
  122. package/out/test/gateway-install-source.test.js +111 -0
  123. package/out/test/gateway-lifecycle.test.js +1 -1
  124. package/out/test/gateway-outbound.test.js +7 -0
  125. package/out/test/gateway-secrets-preflight.test.js +127 -0
  126. package/out/test/gateway-session-routing.test.js +18 -0
  127. package/out/test/governor-hardening.test.js +217 -0
  128. package/out/test/headless-sync.test.js +22 -0
  129. package/out/test/idp-init.test.js +151 -0
  130. package/out/test/lease-signature.test.js +357 -0
  131. package/out/test/lease-trust-root.test.js +273 -0
  132. package/out/test/lease-validation.test.js +169 -0
  133. package/out/test/lease.test.js +114 -0
  134. package/out/test/mcp-obo.test.js +52 -27
  135. package/out/test/mcp.test.js +69 -3
  136. package/out/test/model-routing.test.js +133 -0
  137. package/out/test/pack-admission.test.js +216 -0
  138. package/out/test/pack-digest-parity.test.js +140 -0
  139. package/out/test/pack-reconcile.test.js +183 -0
  140. package/out/test/pack-signature-artifact.test.js +103 -0
  141. package/out/test/pack-signature.test.js +176 -0
  142. package/out/test/pack-trust-root.test.js +202 -0
  143. package/out/test/remote-trust.test.js +242 -0
  144. package/out/test/runner-protocol.test.js +249 -0
  145. package/out/test/sandbox-driver.test.js +553 -0
  146. package/out/test/sandbox-hardening.test.js +395 -0
  147. package/out/test/sandbox-runner.test.js +343 -0
  148. package/out/test/serve-gateway-turn.test.js +267 -0
  149. package/out/test/serve-hardening.test.js +125 -6
  150. package/out/test/serveClient.test.js +38 -5
  151. package/out/test/sessionShip.test.js +560 -0
  152. package/out/test/spotlightPublish.test.js +28 -0
  153. package/out/test/state-sync-peer.test.js +18 -5
  154. package/out/test/store-install-safe.test.js +69 -0
  155. package/out/test/tls-bypass.test.js +90 -0
  156. package/out/test/warrant.test.js +138 -0
  157. package/out/utils.js +73 -20
  158. package/package.json +1 -1
@@ -0,0 +1,227 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) AgentHippo.ai. All rights reserved.
4
+ *
5
+ * The broker mints tokens. These tests pin the three things that stopped it from minting them for
6
+ * whoever asked: a bind that is not every interface, a proof that the identity header came from
7
+ * the trusted proxy, and a redirect_uri allow-list that admits only our own clients.
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 server_1 = require("../auth-broker/server");
46
+ const config_1 = require("../auth-broker/config");
47
+ const types_1 = require("../auth/types");
48
+ const pkce_1 = require("../auth-broker/pkce");
49
+ function prodConfig(over = {}) {
50
+ return {
51
+ port: 0,
52
+ publicBaseUrl: 'https://auth.example.com',
53
+ jwtSecret: 'a-real-secret-not-the-default',
54
+ accessTokenTtlSec: 3600,
55
+ refreshTokenTtlSec: 86400,
56
+ allowedEmailDomains: ['example.com'],
57
+ devMode: false,
58
+ rateLimitPerMin: 0,
59
+ logPii: false,
60
+ cookieName: 'ah_token',
61
+ cookieDomain: '.example.com',
62
+ idpBaseUrl: '',
63
+ proxySecret: 'proxy-shared-secret',
64
+ trustProxyHeadersUnverified: false,
65
+ teamConfigured: true,
66
+ team: {
67
+ id: 'team_test',
68
+ name: 'Test Team',
69
+ storeRepo: 'acme/agent-store',
70
+ storeIndexUrl: 'https://acme.github.io/agent-store',
71
+ githubOrg: 'acme',
72
+ gatewayUrl: 'https://gateway.example.com/v1',
73
+ },
74
+ ...over,
75
+ };
76
+ }
77
+ suite('auth-broker bind address', () => {
78
+ test('defaults to loopback, not every interface', () => {
79
+ const decision = (0, config_1.resolveAuthBrokerBind)({});
80
+ assert.strictEqual(decision.address, config_1.DEFAULT_AUTH_BIND);
81
+ assert.strictEqual(decision.address, '127.0.0.1');
82
+ assert.strictEqual(decision.isPublic, false);
83
+ });
84
+ test('an explicit loopback bind needs no extra flag', () => {
85
+ const decision = (0, config_1.resolveAuthBrokerBind)({ AGENTHIPPO_AUTH_BIND: '::1' });
86
+ assert.strictEqual(decision.isPublic, false);
87
+ });
88
+ test('a public bind is REFUSED unless separately opted into', () => {
89
+ assert.throws(() => (0, config_1.resolveAuthBrokerBind)({ AGENTHIPPO_AUTH_BIND: '0.0.0.0' }), /Refusing to bind/);
90
+ assert.throws(() => (0, config_1.resolveAuthBrokerBind)({ AGENTHIPPO_AUTH_BIND: '10.0.0.5' }), /AGENTHIPPO_AUTH_ALLOW_PUBLIC_BIND/);
91
+ });
92
+ test('the opt-in works and reports that the bind is public', () => {
93
+ const decision = (0, config_1.resolveAuthBrokerBind)({
94
+ AGENTHIPPO_AUTH_BIND: '0.0.0.0',
95
+ AGENTHIPPO_AUTH_ALLOW_PUBLIC_BIND: '1',
96
+ });
97
+ assert.strictEqual(decision.address, '0.0.0.0');
98
+ assert.strictEqual(decision.isPublic, true);
99
+ });
100
+ });
101
+ suite('auth-broker trusted-proxy proof', () => {
102
+ test('a matching secret passes', () => {
103
+ assert.strictEqual((0, server_1.hasTrustedProxyProof)({ [config_1.AUTH_PROXY_SECRET_HEADER]: 'shhh' }, 'shhh'), true);
104
+ });
105
+ test('a missing, empty, or wrong secret fails', () => {
106
+ assert.strictEqual((0, server_1.hasTrustedProxyProof)({}, 'shhh'), false);
107
+ assert.strictEqual((0, server_1.hasTrustedProxyProof)({ [config_1.AUTH_PROXY_SECRET_HEADER]: '' }, 'shhh'), false);
108
+ assert.strictEqual((0, server_1.hasTrustedProxyProof)({ [config_1.AUTH_PROXY_SECRET_HEADER]: 'nope' }, 'shhh'), false);
109
+ });
110
+ test('an unconfigured expected secret can never be satisfied', () => {
111
+ // Fail closed: "no secret configured" must not mean "any secret matches".
112
+ assert.strictEqual((0, server_1.hasTrustedProxyProof)({ [config_1.AUTH_PROXY_SECRET_HEADER]: 'anything' }, ''), false);
113
+ });
114
+ test('a length mismatch does not throw (it would leak the secret length)', () => {
115
+ assert.strictEqual((0, server_1.hasTrustedProxyProof)({ [config_1.AUTH_PROXY_SECRET_HEADER]: 'x' }, 'a-much-longer-secret'), false);
116
+ });
117
+ });
118
+ suite('auth-broker boot gate', () => {
119
+ test('refuses to boot in production without a proxy secret', () => {
120
+ assert.throws(() => (0, server_1.createAuthBrokerServer)(prodConfig({ proxySecret: '' })), /AGENTHIPPO_AUTH_PROXY_SECRET must be set/);
121
+ });
122
+ test('the explicit unverified opt-in allows boot', () => {
123
+ const broker = (0, server_1.createAuthBrokerServer)(prodConfig({ proxySecret: '', trustProxyHeadersUnverified: true }));
124
+ assert.ok(broker.server);
125
+ });
126
+ test('the pre-existing default-secret gates are untouched', () => {
127
+ assert.throws(() => (0, server_1.createAuthBrokerServer)(prodConfig({ jwtSecret: config_1.DEFAULT_JWT_SECRET })), /AGENTHIPPO_AUTH_JWT_SECRET must be set/);
128
+ });
129
+ test('dev mode is still gated on the explicit flag, not NODE_ENV', () => {
130
+ const asTest = (0, config_1.loadAuthBrokerConfig)({ NODE_ENV: 'test' });
131
+ assert.strictEqual(asTest.devMode, false);
132
+ const explicit = (0, config_1.loadAuthBrokerConfig)({ AGENTHIPPO_AUTH_DEV: '1' });
133
+ assert.strictEqual(explicit.devMode, true);
134
+ });
135
+ });
136
+ suite('auth-broker forged X-Auth-Request-Email', () => {
137
+ async function listen(config) {
138
+ const broker = (0, server_1.createAuthBrokerServer)(config);
139
+ await new Promise((resolve) => broker.server.listen(0, '127.0.0.1', resolve));
140
+ const address = broker.server.address();
141
+ if (!address || typeof address === 'string') {
142
+ throw new Error('bad listen address');
143
+ }
144
+ return { baseUrl: `http://127.0.0.1:${address.port}`, close: () => broker.close() };
145
+ }
146
+ function authorizeUrl(baseUrl) {
147
+ const challenge = (0, pkce_1.codeChallengeFromVerifier)((0, pkce_1.generateCodeVerifier)());
148
+ return `${baseUrl}/oauth/authorize?client_id=${types_1.AUTH_CLIENT_ID}`
149
+ + `&redirect_uri=${encodeURIComponent('http://127.0.0.1:54123/callback')}`
150
+ + `&code_challenge=${challenge}&code_challenge_method=S256&state=x`;
151
+ }
152
+ test('an unproven email header gets 401, not an authorization code', async () => {
153
+ const server = await listen(prodConfig());
154
+ try {
155
+ const res = await fetch(authorizeUrl(server.baseUrl), {
156
+ redirect: 'manual',
157
+ headers: { 'X-Auth-Request-Email': 'ceo@example.com' },
158
+ });
159
+ assert.strictEqual(res.status, 401);
160
+ assert.strictEqual(res.headers.get('location'), null, 'no auth code was handed out');
161
+ const body = await res.json();
162
+ assert.ok(body.error_description?.includes(config_1.AUTH_PROXY_SECRET_HEADER));
163
+ }
164
+ finally {
165
+ await server.close();
166
+ }
167
+ });
168
+ test('the same request WITH the proxy proof succeeds', async () => {
169
+ const server = await listen(prodConfig());
170
+ try {
171
+ const res = await fetch(authorizeUrl(server.baseUrl), {
172
+ redirect: 'manual',
173
+ headers: {
174
+ 'X-Auth-Request-Email': 'user@example.com',
175
+ [config_1.AUTH_PROXY_SECRET_HEADER]: 'proxy-shared-secret',
176
+ },
177
+ });
178
+ assert.strictEqual(res.status, 302);
179
+ const location = res.headers.get('location') || '';
180
+ assert.ok(new URL(location).searchParams.get('code'), 'a code is issued to the proven proxy');
181
+ }
182
+ finally {
183
+ await server.close();
184
+ }
185
+ });
186
+ test('/oauth/browser-login is gated the same way', async () => {
187
+ const server = await listen(prodConfig());
188
+ try {
189
+ const res = await fetch(`${server.baseUrl}/oauth/browser-login`, {
190
+ redirect: 'manual',
191
+ headers: { 'X-Auth-Request-Email': 'ceo@example.com' },
192
+ });
193
+ assert.strictEqual(res.status, 401);
194
+ assert.strictEqual(res.headers.get('set-cookie'), null, 'no session cookie was minted');
195
+ }
196
+ finally {
197
+ await server.close();
198
+ }
199
+ });
200
+ });
201
+ suite('auth-broker redirect_uri allow-list', () => {
202
+ test('accepts the shapes real clients send', () => {
203
+ assert.ok((0, server_1.isAllowedRedirectUri)('http://127.0.0.1:54123/callback'));
204
+ assert.ok((0, server_1.isAllowedRedirectUri)('http://localhost:61999/callback'));
205
+ assert.ok((0, server_1.isAllowedRedirectUri)('vscode://agenthippo.agenthippo/auth/callback'));
206
+ });
207
+ test('rejects an attacker-controlled destination', () => {
208
+ assert.ok(!(0, server_1.isAllowedRedirectUri)('https://attacker.example/collect'));
209
+ assert.ok(!(0, server_1.isAllowedRedirectUri)('http://attacker.example/callback'));
210
+ assert.ok(!(0, server_1.isAllowedRedirectUri)('http://127.0.0.1.attacker.example:8080/callback'));
211
+ });
212
+ test('rejects a permitted shape carrying a rider', () => {
213
+ // A query or fragment makes the destination different from the one bound to the code.
214
+ assert.ok(!(0, server_1.isAllowedRedirectUri)('http://127.0.0.1:54123/callback?next=https://attacker.example'));
215
+ assert.ok(!(0, server_1.isAllowedRedirectUri)('http://127.0.0.1:54123/callback#x'));
216
+ assert.ok(!(0, server_1.isAllowedRedirectUri)('http://user:pass@127.0.0.1:54123/callback'));
217
+ });
218
+ test('rejects a privileged or absent loopback port', () => {
219
+ // The loopback server binds an OS-assigned ephemeral port; port 80 or 22 is not it.
220
+ assert.ok(!(0, server_1.isAllowedRedirectUri)('http://127.0.0.1/callback'));
221
+ assert.ok(!(0, server_1.isAllowedRedirectUri)('http://127.0.0.1:22/callback'));
222
+ });
223
+ test('rejects a wrong path and a malformed URI', () => {
224
+ assert.ok(!(0, server_1.isAllowedRedirectUri)('http://127.0.0.1:54123/other'));
225
+ assert.ok(!(0, server_1.isAllowedRedirectUri)('not a uri'));
226
+ });
227
+ });
@@ -56,14 +56,16 @@ function testConfig(port) {
56
56
  cookieName: 'ah_token',
57
57
  cookieDomain: '.agenthippo.ai',
58
58
  idpBaseUrl: 'http://127.0.0.1:0',
59
+ proxySecret: '',
60
+ trustProxyHeadersUnverified: false,
61
+ teamConfigured: true,
59
62
  team: {
60
63
  id: 'team_test',
61
64
  name: 'Test Team',
62
65
  storeRepo: 'acme/agent-store',
63
66
  storeIndexUrl: 'https://acme.github.io/agent-store',
64
67
  githubOrg: 'acme',
65
- litellmBaseUrl: 'http://127.0.0.1:4000',
66
- litellmApiKey: 'ahk_test_key',
68
+ gatewayUrl: 'http://127.0.0.1:3456/v1',
67
69
  },
68
70
  };
69
71
  }
@@ -120,7 +122,9 @@ suite('AuthBroker', () => {
120
122
  assert.strictEqual(bootstrapRes.status, 200);
121
123
  const bootstrap = await bootstrapRes.json();
122
124
  assert.strictEqual(bootstrap.store.repo, 'acme/agent-store');
123
- assert.strictEqual(bootstrap.routing.litellmBaseUrl, 'http://127.0.0.1:4000');
125
+ assert.strictEqual(bootstrap.routing.inferenceGatewayUrl, 'http://127.0.0.1:3456/v1');
126
+ // The bootstrap carries no routing credential — the account JWT is the credential.
127
+ assert.strictEqual(bootstrap.routing.litellmApiKey, undefined);
124
128
  assert.strictEqual(bootstrap.team.id, 'team_test');
125
129
  const refreshRes = await postForm(`${baseUrl}/oauth/token`, {
126
130
  grant_type: 'refresh_token',
@@ -163,7 +167,7 @@ suite('AuthBroker', () => {
163
167
  await broker.close();
164
168
  }
165
169
  });
166
- test('bootstrap refresh re-issues routing credentials', async () => {
170
+ test('bootstrap refresh endpoint is gone — routing carries no credential to re-issue', async () => {
167
171
  const broker = (0, server_js_1.createAuthBrokerServer)(testConfig(0));
168
172
  await new Promise((resolve) => broker.server.listen(0, '127.0.0.1', resolve));
169
173
  const address = broker.server.address();
@@ -198,11 +202,57 @@ suite('AuthBroker', () => {
198
202
  method: 'POST',
199
203
  headers: { Authorization: `Bearer ${tokens.access_token}` },
200
204
  });
201
- assert.strictEqual(refreshRes.status, 200);
202
- const body = await refreshRes.json();
203
- assert.strictEqual(body.routing.litellmApiKey, 'ahk_test_key');
204
- // Static team key — the broker must not fabricate an expiry it can't enforce.
205
- assert.strictEqual(body.routing.litellmApiKeyExpiresAt, undefined);
205
+ assert.strictEqual(refreshRes.status, 404);
206
+ }
207
+ finally {
208
+ await broker.close();
209
+ }
210
+ });
211
+ test('solo deployment omits team in bootstrap and team_id in JWT', async () => {
212
+ const broker = (0, server_js_1.createAuthBrokerServer)({
213
+ ...testConfig(0),
214
+ teamConfigured: false,
215
+ });
216
+ await new Promise((resolve) => broker.server.listen(0, '127.0.0.1', resolve));
217
+ const address = broker.server.address();
218
+ if (!address || typeof address === 'string') {
219
+ throw new Error('bad listen address');
220
+ }
221
+ const baseUrl = `http://127.0.0.1:${address.port}`;
222
+ try {
223
+ const verifier = (0, pkce_js_1.generateCodeVerifier)();
224
+ const challenge = (0, pkce_js_1.codeChallengeFromVerifier)(verifier);
225
+ const redirectUri = 'agenthippo://auth/callback';
226
+ const devRes = await fetch(`${baseUrl}/oauth/dev/login`, {
227
+ method: 'POST',
228
+ headers: { 'Content-Type': 'application/json' },
229
+ body: JSON.stringify({
230
+ email: 'solo@agenthippo.ai',
231
+ client_id: types_js_1.AUTH_CLIENT_ID,
232
+ redirect_uri: redirectUri,
233
+ code_challenge: challenge,
234
+ }),
235
+ });
236
+ const devJson = await devRes.json();
237
+ const tokenRes = await postForm(`${baseUrl}/oauth/token`, {
238
+ grant_type: 'authorization_code',
239
+ client_id: types_js_1.AUTH_CLIENT_ID,
240
+ code: devJson.code,
241
+ redirect_uri: redirectUri,
242
+ code_verifier: verifier,
243
+ });
244
+ assert.strictEqual(tokenRes.status, 200);
245
+ const tokens = await tokenRes.json();
246
+ const payload = (0, jwt_js_1.verifyJwt)(tokens.access_token, 'test-jwt-secret');
247
+ assert.ok(payload);
248
+ assert.strictEqual(payload.team_id, undefined);
249
+ const bootstrapRes = await fetch(`${baseUrl}/api/ide/bootstrap`, {
250
+ headers: { Authorization: `Bearer ${tokens.access_token}` },
251
+ });
252
+ assert.strictEqual(bootstrapRes.status, 200);
253
+ const bootstrap = await bootstrapRes.json();
254
+ assert.strictEqual(bootstrap.team, undefined);
255
+ assert.deepStrictEqual(bootstrap.policy.teamManagedSettings, []);
206
256
  }
207
257
  finally {
208
258
  await broker.close();
@@ -435,4 +485,57 @@ suite('AuthBroker', () => {
435
485
  await broker.close();
436
486
  }
437
487
  });
488
+ test('authorize rejects a redirect_uri outside the loopback/IDE-callback allow-list', async () => {
489
+ const broker = (0, server_js_1.createAuthBrokerServer)(testConfig(0));
490
+ await new Promise((resolve) => broker.server.listen(0, '127.0.0.1', resolve));
491
+ const address = broker.server.address();
492
+ if (!address || typeof address === 'string') {
493
+ throw new Error('bad listen address');
494
+ }
495
+ const baseUrl = `http://127.0.0.1:${address.port}`;
496
+ try {
497
+ const challenge = (0, pkce_js_1.codeChallengeFromVerifier)((0, pkce_js_1.generateCodeVerifier)());
498
+ // An attacker-controlled destination — must never receive an auth code (open-redirect /
499
+ // code-theft guard). Real clients only ever send the loopback or IDE-scheme callback.
500
+ const url = `${baseUrl}/oauth/authorize?client_id=${types_js_1.AUTH_CLIENT_ID}`
501
+ + `&redirect_uri=${encodeURIComponent('https://attacker.example/collect')}`
502
+ + `&code_challenge=${challenge}&code_challenge_method=S256&state=x&login_hint=dev@agenthippo.ai`;
503
+ const res = await fetch(url, { redirect: 'manual' });
504
+ assert.strictEqual(res.status, 400);
505
+ const body = await res.json();
506
+ assert.strictEqual(body.error, 'invalid_request');
507
+ }
508
+ finally {
509
+ await broker.close();
510
+ }
511
+ });
512
+ test('authorize accepts the loopback and IDE-scheme redirect_uri shapes real clients send', async () => {
513
+ const broker = (0, server_js_1.createAuthBrokerServer)(testConfig(0));
514
+ await new Promise((resolve) => broker.server.listen(0, '127.0.0.1', resolve));
515
+ const address = broker.server.address();
516
+ if (!address || typeof address === 'string') {
517
+ throw new Error('bad listen address');
518
+ }
519
+ const baseUrl = `http://127.0.0.1:${address.port}`;
520
+ try {
521
+ const redirectUris = [
522
+ 'http://127.0.0.1:54123/callback', // CLI/IDE loopback callback (any OS-assigned port)
523
+ 'vscode://agenthippo.agenthippo/auth/callback', // IDE URI-scheme callback
524
+ ];
525
+ for (const redirectUri of redirectUris) {
526
+ const challenge = (0, pkce_js_1.codeChallengeFromVerifier)((0, pkce_js_1.generateCodeVerifier)());
527
+ const url = `${baseUrl}/oauth/authorize?client_id=${types_js_1.AUTH_CLIENT_ID}`
528
+ + `&redirect_uri=${encodeURIComponent(redirectUri)}`
529
+ + `&code_challenge=${challenge}&code_challenge_method=S256&state=x&login_hint=dev@agenthippo.ai`;
530
+ const res = await fetch(url, { redirect: 'manual' });
531
+ assert.strictEqual(res.status, 302, `expected redirect for ${redirectUri}`);
532
+ const location = res.headers.get('location') || '';
533
+ assert.ok(location.startsWith(redirectUri), `location "${location}" should start with "${redirectUri}"`);
534
+ assert.ok(new URL(location).searchParams.get('code'), 'redirect carries an auth code');
535
+ }
536
+ }
537
+ finally {
538
+ await broker.close();
539
+ }
540
+ });
438
541
  });
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Blueprint Registry Test Suite (PLAN/deploy.md §3, §7 d7-blueprint-registry)
4
+ *
5
+ * Pure resolver tests: a blueprint (+ optional platform) must resolve to exactly the
6
+ * --target/--ingress pair deploy-init.ts already renders correctly — these tests never
7
+ * call deployCommand, so they can assert on the error paths too without a process.exit risk.
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 registry_1 = require("../cli/deploy/blueprints/registry");
46
+ suite('blueprint registry: isBlueprintId', () => {
47
+ test('recognizes every declared blueprint id', () => {
48
+ for (const id of registry_1.BLUEPRINT_IDS) {
49
+ assert.ok((0, registry_1.isBlueprintId)(id));
50
+ }
51
+ });
52
+ test('rejects unknown ids', () => {
53
+ assert.ok(!(0, registry_1.isBlueprintId)('enterprise'));
54
+ assert.ok(!(0, registry_1.isBlueprintId)(''));
55
+ });
56
+ });
57
+ suite('blueprint registry: resolveBlueprint', () => {
58
+ test('sandbox resolves to vm with no ingress', () => {
59
+ assert.deepStrictEqual((0, registry_1.resolveBlueprint)('sandbox'), { target: 'vm' });
60
+ });
61
+ test('regulated-air-gapped resolves to vm with no ingress', () => {
62
+ assert.deepStrictEqual((0, registry_1.resolveBlueprint)('regulated-air-gapped'), { target: 'vm' });
63
+ });
64
+ test('prospect-demo defaults to vm with edge ingress; compose is also supported', () => {
65
+ assert.deepStrictEqual((0, registry_1.resolveBlueprint)('prospect-demo'), { target: 'vm', ingress: 'edge' });
66
+ assert.deepStrictEqual((0, registry_1.resolveBlueprint)('prospect-demo', 'compose'), { target: 'compose', ingress: 'edge' });
67
+ });
68
+ test('enterprise-fleet always resolves to edge-fed, on any of its supported platforms', () => {
69
+ assert.deepStrictEqual((0, registry_1.resolveBlueprint)('enterprise-fleet'), { target: 'cloudrun', ingress: 'edge-fed' });
70
+ assert.deepStrictEqual((0, registry_1.resolveBlueprint)('enterprise-fleet', 'vm'), { target: 'vm', ingress: 'edge-fed' });
71
+ assert.deepStrictEqual((0, registry_1.resolveBlueprint)('enterprise-fleet', 'compose'), { target: 'compose', ingress: 'edge-fed' });
72
+ });
73
+ test('team-pilot rides platform-native identity where one exists', () => {
74
+ assert.deepStrictEqual((0, registry_1.resolveBlueprint)('team-pilot', 'cloudrun'), { target: 'cloudrun', ingress: 'iap' });
75
+ assert.deepStrictEqual((0, registry_1.resolveBlueprint)('team-pilot', 'databricks-app'), { target: 'databricks-app' });
76
+ assert.deepStrictEqual((0, registry_1.resolveBlueprint)('team-pilot', 'spcs'), { target: 'spcs' });
77
+ assert.deepStrictEqual((0, registry_1.resolveBlueprint)('team-pilot', 'cloudflare'), { target: 'cloudflare' });
78
+ });
79
+ test('team-pilot falls back to a self-issued edge on vm/compose/aws-ecs/azure-container-apps (no platform-native option there)', () => {
80
+ assert.deepStrictEqual((0, registry_1.resolveBlueprint)('team-pilot', 'vm'), { target: 'vm', ingress: 'edge' });
81
+ assert.deepStrictEqual((0, registry_1.resolveBlueprint)('team-pilot', 'compose'), { target: 'compose', ingress: 'edge' });
82
+ assert.deepStrictEqual((0, registry_1.resolveBlueprint)('team-pilot', 'aws-ecs'), { target: 'aws-ecs', ingress: 'edge' });
83
+ assert.deepStrictEqual((0, registry_1.resolveBlueprint)('team-pilot', 'azure-container-apps'), { target: 'azure-container-apps', ingress: 'edge' });
84
+ });
85
+ test('enterprise-fleet supports aws-ecs/azure-container-apps too', () => {
86
+ assert.deepStrictEqual((0, registry_1.resolveBlueprint)('enterprise-fleet', 'aws-ecs'), { target: 'aws-ecs', ingress: 'edge-fed' });
87
+ assert.deepStrictEqual((0, registry_1.resolveBlueprint)('enterprise-fleet', 'azure-container-apps'), { target: 'azure-container-apps', ingress: 'edge-fed' });
88
+ });
89
+ test('team-pilot with no --platform defaults to its first supported target (cloudrun, iap)', () => {
90
+ assert.deepStrictEqual((0, registry_1.resolveBlueprint)('team-pilot'), { target: 'cloudrun', ingress: 'iap' });
91
+ });
92
+ test('throws when --platform is outside the blueprint\'s supported set', () => {
93
+ assert.throws(() => (0, registry_1.resolveBlueprint)('sandbox', 'cloudrun'), /does not support --platform cloudrun/);
94
+ assert.throws(() => (0, registry_1.resolveBlueprint)('regulated-air-gapped', 'databricks-app'), /Supported: vm/);
95
+ });
96
+ });
97
+ suite('blueprint registry: BLUEPRINTS catalog', () => {
98
+ test('every blueprint declares at least one supported target and a description', () => {
99
+ for (const id of registry_1.BLUEPRINT_IDS) {
100
+ const def = registry_1.BLUEPRINTS[id];
101
+ assert.ok(def.targets.length > 0, `${id} must support at least one platform`);
102
+ assert.ok(def.description.trim().length > 0, `${id} must have a description`);
103
+ }
104
+ });
105
+ });
@@ -143,6 +143,19 @@ suite('brandify', () => {
143
143
  });
144
144
  });
145
145
  // =============================================================================
146
+ // toSettingsDisplayKey Tests
147
+ // =============================================================================
148
+ suite('toSettingsDisplayKey', () => {
149
+ test('maps agentide.* keys to agenthippoai.* for user-facing copy', () => {
150
+ assert.strictEqual((0, branding_1.toSettingsDisplayKey)('agentide.anywhere.installSource'), 'agenthippoai.anywhere.installSource');
151
+ assert.strictEqual((0, branding_1.toSettingsDisplayKey)('agentide.cli.path'), 'agenthippoai.cli.path');
152
+ assert.strictEqual((0, branding_1.toSettingsDisplayKey)('agentide.providers.custom.acme.baseUrl'), 'agenthippoai.providers.custom.acme.baseUrl');
153
+ });
154
+ test('leaves non-agentide keys unchanged', () => {
155
+ assert.strictEqual((0, branding_1.toSettingsDisplayKey)('editor.fontSize'), 'editor.fontSize');
156
+ });
157
+ });
158
+ // =============================================================================
146
159
  // copyDirRecursiveWithTransform Tests
147
160
  // =============================================================================
148
161
  suite('copyDirRecursiveWithTransform', () => {