@agenthippo-ai/agenthippo 0.2.3-dataplane → 1.110.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/out/agentpack/index.js +3 -1
  2. package/out/agentpack/paths.js +25 -2
  3. package/out/analytics/dashboard/localFleetAdapter.js +82 -2
  4. package/out/analytics/dashboard/provider.js +13 -2
  5. package/out/analytics/dashboard/webview/panel.js +3 -2
  6. package/out/analytics/identity.js +0 -0
  7. package/out/analytics/service.js +29 -25
  8. package/out/analytics/sessionEvents.js +100 -24
  9. package/out/auth/authBaseUrl.js +59 -0
  10. package/out/auth/bootstrapApply.js +9 -10
  11. package/out/auth/brokerClient.js +48 -14
  12. package/out/auth/provider.js +8 -21
  13. package/out/auth/sessionStore.js +3 -40
  14. package/out/auth/signInFlow.js +2 -1
  15. package/out/auth-broker/config.js +50 -5
  16. package/out/auth-broker/server.js +126 -26
  17. package/out/auth-broker/store.js +25 -24
  18. package/out/authority/warrant.js +206 -0
  19. package/out/branding.js +27 -1
  20. package/out/cli/commands/conformance.js +249 -0
  21. package/out/cli/commands/data-plane.js +151 -0
  22. package/out/cli/commands/deploy.js +1 -0
  23. package/out/cli/commands/gateway.js +4 -0
  24. package/out/cli/commands/pack.js +182 -3
  25. package/out/cli/commands/sandbox.js +351 -0
  26. package/out/cli/commands/serve.js +1145 -130
  27. package/out/cli/commands/store.js +12 -4
  28. package/out/cli/credential-context.js +17 -0
  29. package/out/cli/data-plane/gateway.js +28 -5
  30. package/out/cli/data-plane/index.js +18 -2
  31. package/out/cli/data-plane/registry.js +9 -0
  32. package/out/cli/data-plane/remote.js +106 -0
  33. package/out/cli/data-plane/sidecar.js +225 -0
  34. package/out/cli/deploy/layers/identity.js +88 -4
  35. package/out/cli/deploy-init.js +89 -24
  36. package/out/cli/deps.js +106 -2
  37. package/out/cli/idp-init.js +35 -0
  38. package/out/cli/index.js +171 -17
  39. package/out/cli/pack-install-ref.js +15 -0
  40. package/out/cli/pack-loader.js +8 -1
  41. package/out/cli/pack-runtime.js +61 -0
  42. package/out/cli/runtime.js +35 -1
  43. package/out/cli/serve-hardening.js +260 -14
  44. package/out/cli/utils.js +12 -1
  45. package/out/engines/base.js +32 -3
  46. package/out/engines/claude.js +33 -15
  47. package/out/engines/codex.js +111 -27
  48. package/out/engines/custom.js +15 -1
  49. package/out/evidence/sessionShip.js +643 -0
  50. package/out/extension.js +11 -2
  51. package/out/gateway/bridge.js +40 -2
  52. package/out/gateway/client.js +34 -1
  53. package/out/gateway/installSource.js +68 -0
  54. package/out/gateway/lifecycle.js +185 -51
  55. package/out/gateway/outboundPaths.js +4 -1
  56. package/out/gateway/secretsPreflight.js +90 -0
  57. package/out/gateway/service.js +7 -2
  58. package/out/headless/index.js +25 -2
  59. package/out/live-e2e/mcp-demo/server.mjs +32 -0
  60. package/out/mcp.js +30 -0
  61. package/out/pack/caps.js +271 -0
  62. package/out/pack/governor.js +326 -0
  63. package/out/pack/lease-schema.js +273 -0
  64. package/out/pack/lease.js +682 -0
  65. package/out/pack/reconcile.js +217 -0
  66. package/out/pack/signature.js +839 -0
  67. package/out/remote/handleRemoteChat.js +44 -3
  68. package/out/remote/remoteTrust.js +277 -0
  69. package/out/remote/serveClient.js +52 -5
  70. package/out/runner/host.js +141 -0
  71. package/out/runner/protocol.js +128 -0
  72. package/out/runner/remoteEngine.js +166 -0
  73. package/out/runtime/interfaces/edit-tracker.js +2 -0
  74. package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
  75. package/out/sandbox/containerDriver.js +572 -0
  76. package/out/sandbox/processDriver.js +318 -0
  77. package/out/sandbox/sandboxRunner.js +303 -0
  78. package/out/sandbox/support.js +301 -0
  79. package/out/sandbox/types.js +55 -0
  80. package/out/scripts/agentide-litellm.sh +67 -5
  81. package/out/scripts/agentide_litellm_hooks_v1.py +266 -21
  82. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +98 -132
  83. package/out/scripts/analytics/setup-analytics.ps1 +45 -14
  84. package/out/scripts/analytics/setup-analytics.sh +106 -15
  85. package/out/scripts/analytics/start-analytics.sh +86 -14
  86. package/out/scripts/gateway/setup-gateway.sh +72 -32
  87. package/out/shared/models.js +11 -110
  88. package/out/spotlight/install.js +7 -1
  89. package/out/store/index.js +41 -10
  90. package/out/sync.js +3 -1
  91. package/out/test/analytics.test.js +162 -23
  92. package/out/test/auth-baseurl.test.js +132 -0
  93. package/out/test/auth-bootstrapApply.test.js +27 -6
  94. package/out/test/auth-broker-hardening.test.js +227 -0
  95. package/out/test/auth-broker.test.js +59 -9
  96. package/out/test/branding.test.js +13 -0
  97. package/out/test/caps-governor.test.js +460 -0
  98. package/out/test/codex-new-file-survival.test.js +119 -0
  99. package/out/test/configuration-scopes.test.js +131 -0
  100. package/out/test/data-plane-engine-wiring.test.js +32 -0
  101. package/out/test/data-plane.test.js +380 -0
  102. package/out/test/deploy-init.test.js +47 -1
  103. package/out/test/gateway-client.test.js +28 -0
  104. package/out/test/gateway-install-source.test.js +111 -0
  105. package/out/test/gateway-lifecycle.test.js +1 -1
  106. package/out/test/gateway-outbound.test.js +7 -0
  107. package/out/test/gateway-secrets-preflight.test.js +127 -0
  108. package/out/test/gateway-session-routing.test.js +18 -0
  109. package/out/test/governor-hardening.test.js +222 -0
  110. package/out/test/headless-sync.test.js +22 -0
  111. package/out/test/lease-signature.test.js +357 -0
  112. package/out/test/lease-trust-root.test.js +273 -0
  113. package/out/test/lease-validation.test.js +224 -0
  114. package/out/test/lease.test.js +114 -0
  115. package/out/test/localFleetAdapter.test.js +91 -0
  116. package/out/test/mcp.test.js +27 -1
  117. package/out/test/pack-admission.test.js +216 -0
  118. package/out/test/pack-digest-parity.test.js +140 -0
  119. package/out/test/pack-reconcile.test.js +183 -0
  120. package/out/test/pack-signature-artifact.test.js +103 -0
  121. package/out/test/pack-signature.test.js +176 -0
  122. package/out/test/pack-trust-root.test.js +202 -0
  123. package/out/test/remote-trust.test.js +242 -0
  124. package/out/test/runner-protocol.test.js +249 -0
  125. package/out/test/sandbox-driver.test.js +553 -0
  126. package/out/test/sandbox-hardening.test.js +406 -0
  127. package/out/test/sandbox-runner.test.js +343 -0
  128. package/out/test/sandbox-scope.test.js +449 -0
  129. package/out/test/serve-cron-acceptance.test.js +370 -0
  130. package/out/test/serve-gateway-turn.test.js +122 -0
  131. package/out/test/serve-hardening.test.js +227 -15
  132. package/out/test/serveClient.test.js +6 -5
  133. package/out/test/sessionShip.test.js +618 -0
  134. package/out/test/spotlightPublish.test.js +28 -0
  135. package/out/test/store-install-safe.test.js +69 -0
  136. package/out/test/tls-bypass.test.js +90 -0
  137. package/out/test/warrant.test.js +138 -0
  138. package/out/utils.js +46 -9
  139. package/package.json +1 -1
@@ -19,7 +19,6 @@ async function applyIdeBootstrap(bootstrap, target) {
19
19
  const skipped = [];
20
20
  const teamManaged = bootstrap.policy.teamManagedSettings ?? [];
21
21
  const storeConfig = target.getConfiguration('agentide');
22
- const litellmConfig = target.getConfiguration('agentide.providers.litellm');
23
22
  if (shouldApplySetting(target.globalStateGet(exports.TEAM_MANAGED_STORE_KEY), teamManaged, 'store')) {
24
23
  await storeConfig.update('storeRepo', bootstrap.store.repo, target.ConfigurationTarget.Global);
25
24
  await storeConfig.update('store.indexUrl', bootstrap.store.indexUrl, target.ConfigurationTarget.Global);
@@ -29,22 +28,19 @@ async function applyIdeBootstrap(bootstrap, target) {
29
28
  else {
30
29
  skipped.push('store');
31
30
  }
31
+ // Routing configures the `agenthippo` provider slot ONLY. The `litellm` slot stays user-owned
32
+ // (a developer's local Spotlight proxy) — sign-in must never overwrite it. No credential is
33
+ // written here: the account JWT is synced to the provider by the auth provider's timer, and
34
+ // the bootstrap carries no routing key at all.
32
35
  if (shouldApplySetting(target.globalStateGet(exports.TEAM_MANAGED_ROUTING_KEY), teamManaged, 'routing')) {
33
- await litellmConfig.update('baseUrl', bootstrap.routing.litellmBaseUrl, target.ConfigurationTarget.Global);
34
- if (target.setParticipantApiKey) {
35
- await target.setParticipantApiKey({
36
- provider: 'litellm',
37
- apiKey: bootstrap.routing.litellmApiKey,
38
- baseUrl: bootstrap.routing.litellmBaseUrl,
39
- });
40
- }
36
+ await storeConfig.update('AgentHippoModelGateway.url', bootstrap.routing.inferenceGatewayUrl, target.ConfigurationTarget.Global);
41
37
  await target.globalStateUpdate(exports.TEAM_MANAGED_ROUTING_KEY, teamManaged.includes('routing'));
42
38
  applied.push('routing');
43
39
  }
44
40
  else {
45
41
  skipped.push('routing');
46
42
  }
47
- await target.globalStateUpdate(exports.TEAM_ID_KEY, bootstrap.team.id);
43
+ await target.globalStateUpdate(exports.TEAM_ID_KEY, bootstrap.team?.id);
48
44
  await target.globalStateUpdate(exports.TEAM_LAST_BOOTSTRAP_KEY, new Date().toISOString());
49
45
  await target.globalStateUpdate(exports.TEAM_BOOTSTRAP_CACHE_KEY, bootstrap);
50
46
  return { applied, skipped };
@@ -59,6 +55,9 @@ async function revertTeamManagedBootstrap(target) {
59
55
  await storeConfig.update('store.indexUrl', 'https://agenthippoai.github.io/agenthippo-store', target.ConfigurationTarget.Global);
60
56
  }
61
57
  if (routingManaged) {
58
+ await storeConfig.update('AgentHippoModelGateway.url', undefined, target.ConfigurationTarget.Global);
59
+ // Clean up state written by pre-separation installs, where team routing overwrote the
60
+ // user-owned litellm slot with the team base URL and a shared key.
62
61
  await litellmConfig.update('baseUrl', '', target.ConfigurationTarget.Global);
63
62
  if (target.setParticipantApiKey) {
64
63
  await target.setParticipantApiKey({ provider: 'litellm', apiKey: '', baseUrl: '' });
@@ -3,21 +3,66 @@
3
3
  * Copyright (c) AgentHippo.ai. All rights reserved.
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DEFAULT_AUTH_BROKER_BASE_URL = void 0;
7
+ exports.isLoopbackAuthHost = isLoopbackAuthHost;
8
+ exports.assertSafeAuthBaseUrl = assertSafeAuthBaseUrl;
6
9
  exports.getAuthBrokerBaseUrl = getAuthBrokerBaseUrl;
7
10
  exports.devLoginForTests = devLoginForTests;
8
11
  exports.exchangeAuthorizationCode = exchangeAuthorizationCode;
9
12
  exports.refreshAccessToken = refreshAccessToken;
10
- exports.fetchBootstrapRefresh = fetchBootstrapRefresh;
11
13
  exports.fetchBootstrap = fetchBootstrap;
12
14
  exports.fetchMe = fetchMe;
13
15
  exports.buildAuthorizeUrl = buildAuthorizeUrl;
14
16
  exports.completeDevPkceLogin = completeDevPkceLogin;
15
17
  const types_js_1 = require("./types.js");
16
18
  const pkce_js_1 = require("./pkce.js");
19
+ exports.DEFAULT_AUTH_BROKER_BASE_URL = 'https://auth.agenthippo.ai';
20
+ /** Hostnames for which plain `http://` is accepted — an operator running a broker locally. */
21
+ const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
22
+ function isLoopbackAuthHost(hostname) {
23
+ return LOOPBACK_HOSTNAMES.has(hostname.toLowerCase());
24
+ }
25
+ /**
26
+ * Refuse an auth base URL that cannot safely carry a bearer token.
27
+ *
28
+ * Every request built from this value carries either the PKCE authorization code or the user's
29
+ * AgentHippo access token (`/oauth/token`, `/api/ide/bootstrap`, `/api/ide/me`), and the bootstrap
30
+ * response is written straight into the user's model-routing settings. A cleartext or non-http(s)
31
+ * endpoint therefore leaks the credential on the wire; anything but https is refused unless the
32
+ * host is loopback, which is the local-development case and never crosses a network.
33
+ *
34
+ * Returns the normalized (trailing-slash-stripped) URL, or throws with an actionable message.
35
+ */
36
+ function assertSafeAuthBaseUrl(value) {
37
+ const trimmed = value.trim();
38
+ let url;
39
+ try {
40
+ url = new URL(trimmed);
41
+ }
42
+ catch {
43
+ throw new Error(`AgentHippo auth base URL "${trimmed}" is not a valid URL. ` +
44
+ `Fix "agentide.auth.baseUrl" (or AGENTHIPPO_AUTH_BASE_URL), or clear it to use ${exports.DEFAULT_AUTH_BROKER_BASE_URL}.`);
45
+ }
46
+ if (url.protocol === 'https:') {
47
+ return trimmed.replace(/\/+$/, '');
48
+ }
49
+ if (url.protocol === 'http:' && isLoopbackAuthHost(url.hostname)) {
50
+ return trimmed.replace(/\/+$/, '');
51
+ }
52
+ throw new Error(`Refusing to use AgentHippo auth base URL "${trimmed}": it must be https:// ` +
53
+ `(plain http:// is accepted only for loopback hosts such as http://127.0.0.1:9780). ` +
54
+ `Your AgentHippo access token is sent to this host, so it is never sent over cleartext. ` +
55
+ `Fix "agentide.auth.baseUrl" (or AGENTHIPPO_AUTH_BASE_URL), or clear it to use ${exports.DEFAULT_AUTH_BROKER_BASE_URL}.`);
56
+ }
57
+ /**
58
+ * Resolve the auth broker base URL. `configured` must already have been filtered for scope by the
59
+ * caller (see `auth/authBaseUrl.ts` — a workspace-scoped value is dropped before it gets here);
60
+ * this function is the second gate and enforces the transport requirement.
61
+ */
17
62
  function getAuthBrokerBaseUrl(configured) {
18
63
  const fromEnv = process.env.AGENTHIPPO_AUTH_BASE_URL?.trim();
19
- const value = configured?.trim() || fromEnv || 'https://auth.agenthippo.ai';
20
- return value.replace(/\/+$/, '');
64
+ const value = configured?.trim() || fromEnv || exports.DEFAULT_AUTH_BROKER_BASE_URL;
65
+ return assertSafeAuthBaseUrl(value);
21
66
  }
22
67
  async function devLoginForTests(baseUrl, email, redirectUri, codeChallenge) {
23
68
  const res = await fetch(`${baseUrl}/oauth/dev/login`, {
@@ -73,17 +118,6 @@ async function refreshAccessToken(baseUrl, refreshToken) {
73
118
  }
74
119
  return res.json();
75
120
  }
76
- async function fetchBootstrapRefresh(baseUrl, accessToken) {
77
- const res = await fetch(`${baseUrl}/api/ide/bootstrap/refresh`, {
78
- method: 'POST',
79
- headers: { Authorization: `Bearer ${accessToken}` },
80
- });
81
- if (!res.ok) {
82
- const text = await res.text();
83
- throw new Error(`bootstrap refresh failed: ${res.status} ${text}`.trim());
84
- }
85
- return res.json();
86
- }
87
121
  async function fetchBootstrap(baseUrl, accessToken) {
88
122
  const res = await fetch(`${baseUrl}/api/ide/bootstrap`, {
89
123
  headers: { Authorization: `Bearer ${accessToken}` },
@@ -41,7 +41,7 @@ exports.registerAgentHippoAuth = registerAgentHippoAuth;
41
41
  exports.refreshBootstrapIfSignedIn = refreshBootstrapIfSignedIn;
42
42
  exports.startAgentHippoTokenRefreshTimer = startAgentHippoTokenRefreshTimer;
43
43
  const vscode = __importStar(require("vscode"));
44
- const brokerClient_js_1 = require("./brokerClient.js");
44
+ const authBaseUrl_js_1 = require("./authBaseUrl.js");
45
45
  const bootstrapApply_js_1 = require("./bootstrapApply.js");
46
46
  const types_js_1 = require("./types.js");
47
47
  const signInFlow_js_1 = require("./signInFlow.js");
@@ -111,7 +111,7 @@ class AgentHippoAuthProvider {
111
111
  const stored = await (0, sessionStore_js_1.getStoredAuthSession)(this.context.secrets);
112
112
  const removedSession = stored ? (0, sessionStore_js_1.storedSessionToVsCode)(stored) : undefined;
113
113
  if (stored) {
114
- const baseUrl = (0, brokerClient_js_1.getAuthBrokerBaseUrl)(vscode.workspace.getConfiguration('agentide').get('auth.baseUrl'));
114
+ const baseUrl = (0, authBaseUrl_js_1.resolveAuthBrokerBaseUrl)(vscode.workspace.getConfiguration('agentide'), (m) => console.warn(m));
115
115
  try {
116
116
  await fetch(`${baseUrl}/api/ide/signout`, {
117
117
  method: 'POST',
@@ -184,20 +184,17 @@ async function refreshBootstrapIfSignedIn(context) {
184
184
  void syncAgentHippoModelGatewayToken(token.accessToken);
185
185
  }
186
186
  /**
187
- * Refresh cadence for the model-gateway JWT and LiteLLM routing key. The access token TTL is
188
- * ~1h; getAgentHippoAccessToken refreshes it inside a 5-min pre-expiry window, so a 60s tick
189
- * always lands inside that window with buffer and re-syncs the agenthippo provider key before
190
- * the old JWT expires (fixes stale-key 401s on long sessions). Routing credentials are refreshed
191
- * less often (every ~5 min) so a server-side rotation of the team LiteLLM key is picked up
192
- * without re-sign-in. Both syncs are diff-gated so we don't churn SecretStorage / settings /
193
- * onDidChangeKeys every tick when nothing changed.
187
+ * Refresh cadence for the model-gateway JWT. The access token TTL is ~1h;
188
+ * getAgentHippoAccessToken refreshes it inside a 5-min pre-expiry window, so a 60s tick always
189
+ * lands inside that window with buffer and re-syncs the agenthippo provider key before the old
190
+ * JWT expires (fixes stale-key 401s on long sessions). The sync is diff-gated so we don't churn
191
+ * SecretStorage / onDidChangeKeys every tick when nothing changed. There is no routing-key
192
+ * refresh: the bootstrap carries no routing credential — the JWT is the credential.
194
193
  */
195
194
  const TOKEN_REFRESH_INTERVAL_MS = 60_000;
196
- const ROUTING_REFRESH_EVERY_N_TICKS = 5;
197
195
  function startAgentHippoTokenRefreshTimer(context) {
198
196
  let lastSyncedAccessToken = '';
199
197
  let running = false;
200
- let tick = 0;
201
198
  const handle = setInterval(() => {
202
199
  // Guard against overlapping ticks: a stalled fetch shouldn't let the next tick stack up.
203
200
  if (running) {
@@ -219,16 +216,6 @@ function startAgentHippoTokenRefreshTimer(context) {
219
216
  lastSyncedAccessToken = token.accessToken;
220
217
  }
221
218
  }
222
- // Routing key rotation is rare; refresh infrequently and let the helper skip the
223
- // settings write when the key hasn't changed.
224
- if (++tick % ROUTING_REFRESH_EVERY_N_TICKS === 0) {
225
- try {
226
- await (0, sessionStore_js_1.refreshRoutingCredentialsForContext)(context, token.accessToken);
227
- }
228
- catch {
229
- // best-effort routing refresh; next cycle retries
230
- }
231
- }
232
219
  }
233
220
  finally {
234
221
  running = false;
@@ -39,12 +39,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.getStoredAuthSession = getStoredAuthSession;
40
40
  exports.persistAuthSession = persistAuthSession;
41
41
  exports.applyIdeBootstrapForContext = applyIdeBootstrapForContext;
42
- exports.refreshRoutingCredentialsForContext = refreshRoutingCredentialsForContext;
43
42
  exports.getAgentHippoAccessToken = getAgentHippoAccessToken;
44
43
  exports.clearAuthSession = clearAuthSession;
45
44
  exports.storedSessionToVsCode = storedSessionToVsCode;
46
45
  const vscode = __importStar(require("vscode"));
47
46
  const brokerClient_js_1 = require("./brokerClient.js");
47
+ const authBaseUrl_js_1 = require("./authBaseUrl.js");
48
48
  const bootstrapApply_js_1 = require("./bootstrapApply.js");
49
49
  const SESSION_SECRET_KEY = 'agenthippo.auth.session';
50
50
  /**
@@ -94,7 +94,7 @@ async function persistAuthSession(secrets, tokens, bootstrap, scopes) {
94
94
  return toVsCodeSession(session);
95
95
  }
96
96
  async function applyIdeBootstrapForContext(context, accessToken) {
97
- const baseUrl = (0, brokerClient_js_1.getAuthBrokerBaseUrl)(vscode.workspace.getConfiguration('agentide').get('auth.baseUrl'));
97
+ const baseUrl = (0, authBaseUrl_js_1.resolveAuthBrokerBaseUrl)(vscode.workspace.getConfiguration('agentide'), (m) => console.warn(m));
98
98
  const bootstrap = await (0, brokerClient_js_1.fetchBootstrap)(baseUrl, accessToken);
99
99
  await (0, bootstrapApply_js_1.applyIdeBootstrap)(bootstrap, {
100
100
  getConfiguration: (section) => vscode.workspace.getConfiguration(section),
@@ -110,43 +110,6 @@ async function applyIdeBootstrapForContext(context, accessToken) {
110
110
  });
111
111
  return bootstrap;
112
112
  }
113
- /**
114
- * Refresh only the LiteLLM routing credentials (lighter than re-fetching the full bootstrap)
115
- * and re-apply them, merging into the last cached bootstrap. Handles server-side rotation of
116
- * the team routing key without requiring a re-sign-in. No-op if no bootstrap has been applied.
117
- */
118
- async function refreshRoutingCredentialsForContext(context, accessToken) {
119
- const cached = context.globalState.get(bootstrapApply_js_1.TEAM_BOOTSTRAP_CACHE_KEY);
120
- if (!cached) {
121
- return;
122
- }
123
- const baseUrl = (0, brokerClient_js_1.getAuthBrokerBaseUrl)(vscode.workspace.getConfiguration('agentide').get('auth.baseUrl'));
124
- const refreshed = await (0, brokerClient_js_1.fetchBootstrapRefresh)(baseUrl, accessToken);
125
- // Skip the settings write when nothing rotated: applyIdeBootstrap re-writes store + routing
126
- // and setAPIKey fires onDidChangeKeys on every call, so re-applying an unchanged key every
127
- // few minutes would churn listeners and SecretStorage.
128
- const keyChanged = refreshed.routing.litellmApiKey !== cached.routing.litellmApiKey;
129
- const baseUrlChanged = refreshed.routing.litellmBaseUrl !== cached.routing.litellmBaseUrl;
130
- if (!keyChanged && !baseUrlChanged) {
131
- return;
132
- }
133
- const merged = {
134
- ...cached,
135
- routing: { ...cached.routing, ...refreshed.routing },
136
- };
137
- await (0, bootstrapApply_js_1.applyIdeBootstrap)(merged, {
138
- getConfiguration: (section) => vscode.workspace.getConfiguration(section),
139
- ConfigurationTarget: vscode.ConfigurationTarget,
140
- setParticipantApiKey: async (args) => {
141
- await vscode.commands.executeCommand('agentide.setParticipantApiKey', args);
142
- await vscode.commands.executeCommand('agentide.auth.applyBootstrapKeys', args);
143
- },
144
- globalStateUpdate: async (key, value) => {
145
- await context.globalState.update(key, value);
146
- },
147
- globalStateGet: (key) => context.globalState.get(key),
148
- });
149
- }
150
113
  async function getAgentHippoAccessToken(context) {
151
114
  const stored = await getStoredAuthSession(context.secrets);
152
115
  if (!stored) {
@@ -157,7 +120,7 @@ async function getAgentHippoAccessToken(context) {
157
120
  }
158
121
  // Near expiry — refresh exactly once across concurrent callers.
159
122
  if (!inFlightRefresh) {
160
- const baseUrl = (0, brokerClient_js_1.getAuthBrokerBaseUrl)(vscode.workspace.getConfiguration('agentide').get('auth.baseUrl'));
123
+ const baseUrl = (0, authBaseUrl_js_1.resolveAuthBrokerBaseUrl)(vscode.workspace.getConfiguration('agentide'), (m) => console.warn(m));
161
124
  const refreshing = stored;
162
125
  inFlightRefresh = (async () => {
163
126
  try {
@@ -40,6 +40,7 @@ exports.runAgentHippoSignInFlow = runAgentHippoSignInFlow;
40
40
  exports.ensureAgentHippoSignedIn = ensureAgentHippoSignedIn;
41
41
  const vscode = __importStar(require("vscode"));
42
42
  const brokerClient_js_1 = require("./brokerClient.js");
43
+ const authBaseUrl_js_1 = require("./authBaseUrl.js");
43
44
  const pkce_js_1 = require("./pkce.js");
44
45
  const callbackUriParse_js_1 = require("./callbackUriParse.js");
45
46
  const types_js_1 = require("./types.js");
@@ -77,7 +78,7 @@ function extractRedirectUriFromText(text) {
77
78
  * Falls back to pasted redirect URL when loopback does not fire (remote/SSH/headless).
78
79
  */
79
80
  async function runAgentHippoSignInFlow(context, options) {
80
- const baseUrl = (0, brokerClient_js_1.getAuthBrokerBaseUrl)(vscode.workspace.getConfiguration('agentide').get('auth.baseUrl'));
81
+ const baseUrl = (0, authBaseUrl_js_1.resolveAuthBrokerBaseUrl)(vscode.workspace.getConfiguration('agentide'), (m) => console.warn(m));
81
82
  const scopes = options?.scopes ?? [...types_js_1.AUTH_SCOPES];
82
83
  const loopback = new loopbackAuthServer_js_1.AgentHippoLoopbackAuthServer();
83
84
  const verifier = (0, pkce_js_1.generateCodeVerifier)();
@@ -3,7 +3,8 @@
3
3
  * Copyright (c) AgentHippo.ai. All rights reserved.
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.DEFAULT_LITELLM_API_KEY = exports.DEFAULT_JWT_SECRET = void 0;
6
+ exports.DEFAULT_AUTH_BIND = exports.AUTH_PROXY_SECRET_HEADER = exports.DEFAULT_JWT_SECRET = void 0;
7
+ exports.resolveAuthBrokerBind = resolveAuthBrokerBind;
7
8
  exports.loadAuthBrokerConfig = loadAuthBrokerConfig;
8
9
  exports.emailDomainAllowed = emailDomainAllowed;
9
10
  function parseDomains(raw) {
@@ -14,8 +15,50 @@ function parseDomains(raw) {
14
15
  }
15
16
  /** Well-known default secret used in dev/test. Never acceptable in production. */
16
17
  exports.DEFAULT_JWT_SECRET = 'dev-agenthippo-auth-secret-change-me';
17
- /** Well-known default LiteLLM team key used in dev/test. Never acceptable in production. */
18
- exports.DEFAULT_LITELLM_API_KEY = 'ahk_team_dev_key';
18
+ /**
19
+ * Header the trusted proxy sets to prove it is the trusted proxy.
20
+ *
21
+ * `X-Auth-Request-Email` is set by oauth2-proxy AFTER a real Google login, but nothing about the
22
+ * header itself proves it came from oauth2-proxy — anything that can reach the broker's port can
23
+ * send it. The proxy therefore also sets this header from a secret only it and the broker know.
24
+ */
25
+ exports.AUTH_PROXY_SECRET_HEADER = 'x-agenthippo-proxy-secret';
26
+ /**
27
+ * Where the broker listens. 127.0.0.1 by default: this port mints tokens, and every intended caller
28
+ * (the Caddy/oauth2-proxy sidecar) is same-host. Binding a routable interface is an explicit,
29
+ * separately-flagged decision — see `resolveAuthBrokerBind`.
30
+ */
31
+ exports.DEFAULT_AUTH_BIND = '127.0.0.1';
32
+ const LOOPBACK_BINDS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]']);
33
+ /**
34
+ * Resolve (and gate) the bind address.
35
+ *
36
+ * The old default was `0.0.0.0`, which put a token-minting endpoint on every interface: reach
37
+ * :9780 directly, forge `X-Auth-Request-Email`, get an auth code for any address, and exchange it
38
+ * at the ungated `/oauth/token`. Loopback is now the default, and a public bind must be asked for
39
+ * TWICE — the address plus `AGENTHIPPO_AUTH_ALLOW_PUBLIC_BIND=1` — so it can never happen by
40
+ * inheriting a stray env var from a base image.
41
+ */
42
+ function resolveAuthBrokerBind(env = process.env) {
43
+ const requested = env.AGENTHIPPO_AUTH_BIND?.trim() || exports.DEFAULT_AUTH_BIND;
44
+ const isPublic = !LOOPBACK_BINDS.has(requested.toLowerCase());
45
+ if (!isPublic) {
46
+ return { address: requested, isPublic: false };
47
+ }
48
+ if (env.AGENTHIPPO_AUTH_ALLOW_PUBLIC_BIND !== '1') {
49
+ throw new Error(`Refusing to bind the auth broker to "${requested}". This port issues authorization codes ` +
50
+ `and access tokens, and a directly reachable broker can be asked to mint a token for an ` +
51
+ `arbitrary email by forging X-Auth-Request-Email.\n` +
52
+ ` - Same-host proxy (recommended): bind 127.0.0.1 and run Caddy/oauth2-proxy alongside it.\n` +
53
+ ` - SEPARATE CONTAINER for the proxy (the compose/Cloud Run layout, where Caddy dials\n` +
54
+ ` "auth-broker:9780"): the broker must listen on the container's network interface, so set\n` +
55
+ ` BOTH AGENTHIPPO_AUTH_BIND=${requested} AND AGENTHIPPO_AUTH_ALLOW_PUBLIC_BIND=1,\n` +
56
+ ` and make sure the port is not published outside the private network.\n` +
57
+ `In either case set AGENTHIPPO_AUTH_PROXY_SECRET (and have the proxy send it) so a forged ` +
58
+ `X-Auth-Request-Email is rejected — that is what actually stops the token-minting attack.`);
59
+ }
60
+ return { address: requested, isPublic: true };
61
+ }
19
62
  function loadAuthBrokerConfig(env = process.env) {
20
63
  const jwtSecret = env.AGENTHIPPO_AUTH_JWT_SECRET?.trim() || exports.DEFAULT_JWT_SECRET;
21
64
  const publicBaseUrl = (env.AGENTHIPPO_AUTH_PUBLIC_URL?.trim() || `http://127.0.0.1:${env.AGENTHIPPO_AUTH_PORT?.trim() || '9780'}`).replace(/\/+$/, '');
@@ -38,15 +81,17 @@ function loadAuthBrokerConfig(env = process.env) {
38
81
  cookieName: env.AGENTHIPPO_AUTH_COOKIE_NAME?.trim() || 'ah_token',
39
82
  cookieDomain: env.AGENTHIPPO_AUTH_COOKIE_DOMAIN?.trim() || '',
40
83
  idpBaseUrl: (env.AGENTHIPPO_AUTH_IDP_URL?.trim() || '').replace(/\/+$/, ''),
84
+ proxySecret: env.AGENTHIPPO_AUTH_PROXY_SECRET?.trim() || '',
85
+ trustProxyHeadersUnverified: env.AGENTHIPPO_AUTH_TRUST_PROXY_UNVERIFIED === '1',
41
86
  dataDir: env.AGENTHIPPO_AUTH_DATA_DIR?.trim() || undefined,
87
+ teamConfigured: Boolean(env.AGENTHIPPO_TEAM_ID?.trim()),
42
88
  team: {
43
89
  id: env.AGENTHIPPO_TEAM_ID?.trim() || 'team_default',
44
90
  name: env.AGENTHIPPO_TEAM_NAME?.trim() || 'Default Team',
45
91
  storeRepo,
46
92
  storeIndexUrl: env.AGENTHIPPO_TEAM_STORE_INDEX_URL?.trim() || `https://${storeOwner}.github.io/${storeName}`,
47
93
  githubOrg: env.AGENTHIPPO_TEAM_GITHUB_ORG?.trim() || storeOwner,
48
- litellmBaseUrl: env.AGENTHIPPO_TEAM_LITELLM_BASE_URL?.trim() || 'http://127.0.0.1:4000',
49
- litellmApiKey: env.AGENTHIPPO_TEAM_LITELLM_API_KEY?.trim() || exports.DEFAULT_LITELLM_API_KEY,
94
+ gatewayUrl: env.AGENTHIPPO_TEAM_GATEWAY_URL?.trim() || 'https://gateway.agenthippo.ai/v1',
50
95
  },
51
96
  };
52
97
  }
@@ -36,6 +36,8 @@ var __importStar = (this && this.__importStar) || (function () {
36
36
  };
37
37
  })();
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.hasTrustedProxyProof = void 0;
40
+ exports.isAllowedRedirectUri = isAllowedRedirectUri;
39
41
  exports.createAuthBrokerServer = createAuthBrokerServer;
40
42
  exports.startAuthBrokerServer = startAuthBrokerServer;
41
43
  const http = __importStar(require("http"));
@@ -65,11 +67,49 @@ function isAllowedRedirectUri(redirectUri) {
65
67
  catch {
66
68
  return false;
67
69
  }
70
+ // No embedded credentials, no query, no fragment. A permitted shape with a `?next=…` rider or a
71
+ // `user:pass@` prefix is a different destination wearing an allowed hostname; the exchange binds
72
+ // the code to this exact string, so anything that isn't the literal callback is refused here.
73
+ if (target.username || target.password || target.search || target.hash) {
74
+ return false;
75
+ }
68
76
  if (target.protocol === 'http:') {
69
- return (target.hostname === '127.0.0.1' || target.hostname === 'localhost') && target.pathname === '/callback';
77
+ // The loopback callback is the real IDE/CLI flow (loopbackAuthServer.ts binds an OS-assigned
78
+ // port on 127.0.0.1), so the port cannot be pinned. The port is not what makes this safe:
79
+ // the code is single-use, bound to THIS redirect_uri at issue time and verified against it at
80
+ // /oauth/token, and PKCE binds it to a verifier only the initiating client holds. An attacker
81
+ // who forges a loopback redirect_uri still receives a code they cannot exchange.
82
+ // `localhost` is accepted for parity with the client, but nothing else resolves here.
83
+ if (target.hostname !== '127.0.0.1' && target.hostname !== 'localhost') {
84
+ return false;
85
+ }
86
+ if (target.pathname !== '/callback') {
87
+ return false;
88
+ }
89
+ // A bare `http://127.0.0.1/callback` (port 80) is not something the loopback server ever
90
+ // asks for — it needs a bindable ephemeral port — and port 80 is the one loopback port a
91
+ // non-root process cannot claim, so refuse it rather than leave the shape open.
92
+ return target.port !== '' && Number(target.port) > 1024;
93
+ }
94
+ if (target.protocol === 'https:') {
95
+ // No https redirect target is a client of this broker: the IDE uses the editor URI scheme
96
+ // and the CLI uses loopback. Allowing https here would make "redirect to my server" a
97
+ // one-line manifest away.
98
+ return false;
70
99
  }
71
100
  return (0, callbackUriParse_js_1.isAuthCallbackParts)(target.host, target.pathname);
72
101
  }
102
+ /**
103
+ * Does this request carry the trusted proxy's proof?
104
+ *
105
+ * `X-Auth-Request-Email` is only meaningful when it was set by our oauth2-proxy sidecar after a real
106
+ * Google login. Nothing about the header says so, so the proxy also stamps a shared secret which is
107
+ * checked constant-time, so a mismatched secret cannot be recovered byte by byte. The check lives in
108
+ * serve-hardening (serve applies the same contract to its identity header via
109
+ * AGENTHIPPO_IDENTITY_PROXY_SECRET); re-exported here for the broker's existing consumers.
110
+ */
111
+ var serve_hardening_js_2 = require("../cli/serve-hardening.js");
112
+ Object.defineProperty(exports, "hasTrustedProxyProof", { enumerable: true, get: function () { return serve_hardening_js_2.hasTrustedProxyProof; } });
73
113
  function sendJson(res, status, body) {
74
114
  res.writeHead(status, { 'Content-Type': 'application/json' });
75
115
  res.end(JSON.stringify(body));
@@ -161,11 +201,27 @@ function createAuthBrokerServer(config) {
161
201
  throw new Error('AGENTHIPPO_AUTH_JWT_SECRET must be set to a strong secret in production. ' +
162
202
  'Generate one with: openssl rand -hex 32');
163
203
  }
164
- // Fail-closed: a known-default LiteLLM key would hand every team member the same
165
- // publicly-known credential if the operator forgot to set the real one.
166
- if (!resolved.devMode && resolved.team.litellmApiKey === config_js_1.DEFAULT_LITELLM_API_KEY) {
167
- throw new Error('AGENTHIPPO_TEAM_LITELLM_API_KEY must be set in production. ' +
168
- 'The default dev key must not be issued to real users via bootstrap.');
204
+ // Fail-closed: without a proxy proof, `X-Auth-Request-Email` is a self-asserted claim. Anything
205
+ // that can reach this port could send `X-Auth-Request-Email: ceo@example.com` with a loopback
206
+ // redirect_uri to /oauth/authorize and exchange the resulting code at the ungated /oauth/token —
207
+ // a full account takeover with no credential. Same posture as the JWT/LiteLLM defaults above:
208
+ // refuse to boot rather than run a broker whose identity input is unauthenticated.
209
+ if (!resolved.devMode && !resolved.proxySecret && !resolved.trustProxyHeadersUnverified) {
210
+ throw new Error('AGENTHIPPO_AUTH_PROXY_SECRET must be set in production. The broker trusts ' +
211
+ 'X-Auth-Request-Email from the oauth2-proxy/Caddy forward_auth sidecar; without a shared ' +
212
+ 'secret proving the header came from that sidecar, any caller that reaches this port can ' +
213
+ 'mint a token for an arbitrary email.\n' +
214
+ ' 1. Generate one: openssl rand -hex 32\n' +
215
+ ` 2. Set it here: AGENTHIPPO_AUTH_PROXY_SECRET=<secret>\n` +
216
+ ` 3. Have the proxy send it, e.g. in the Caddyfile:\n` +
217
+ ` header_up ${config_js_1.AUTH_PROXY_SECRET_HEADER} {env.AGENTHIPPO_AUTH_PROXY_SECRET}\n` +
218
+ 'If the broker is genuinely unreachable except through the proxy and you accept the risk, ' +
219
+ 'set AGENTHIPPO_AUTH_TRUST_PROXY_UNVERIFIED=1.');
220
+ }
221
+ if (!resolved.devMode && !resolved.proxySecret && resolved.trustProxyHeadersUnverified) {
222
+ console.warn('[auth-broker] WARNING: AGENTHIPPO_AUTH_TRUST_PROXY_UNVERIFIED=1 — X-Auth-Request-Email is ' +
223
+ 'accepted without proof. Anything that can reach this port can mint a token for any email. ' +
224
+ 'Set AGENTHIPPO_AUTH_PROXY_SECRET to close this.');
169
225
  }
170
226
  // Warn when no domain restriction is configured: any SSO-authenticated email can sign in.
171
227
  if (!resolved.devMode && resolved.allowedEmailDomains.length === 0) {
@@ -229,11 +285,12 @@ function createAuthBrokerServer(config) {
229
285
  // IdP's own broker still hits getUser for its locally-created users, so this fallback is
230
286
  // a no-op there.
231
287
  if (!user) {
288
+ const teamIdFromToken = typeof payload.team_id === 'string' && payload.team_id ? payload.team_id : undefined;
232
289
  user = {
233
290
  id: String(payload.sub),
234
291
  email: String(payload.email),
235
292
  displayName: typeof payload.name === 'string' && payload.name ? payload.name : String(payload.email),
236
- teamId: typeof payload.team_id === 'string' && payload.team_id ? payload.team_id : resolved.team.id,
293
+ teamId: teamIdFromToken ?? (resolved.teamConfigured ? resolved.team.id : undefined),
237
294
  };
238
295
  }
239
296
  if (payload.exp) {
@@ -253,6 +310,33 @@ function createAuthBrokerServer(config) {
253
310
  }
254
311
  return user;
255
312
  }
313
+ /**
314
+ * Read the SSO-asserted email, but only if the request proved it came from the trusted proxy.
315
+ * Returns `undefined` when there is no email, and `null` when an email was present but the proof
316
+ * was missing/wrong — those are different failures and the second one is worth auditing loudly.
317
+ */
318
+ const proxyAssertedEmail = (req) => {
319
+ const email = (req.headers['x-auth-request-email'] || '').trim().toLowerCase();
320
+ if (!email) {
321
+ return undefined;
322
+ }
323
+ // devMode and the explicit unverified opt-in are the only ways past the proof.
324
+ if (resolved.devMode || (!resolved.proxySecret && resolved.trustProxyHeadersUnverified)) {
325
+ return email;
326
+ }
327
+ if (!(0, serve_hardening_js_1.hasTrustedProxyProof)(req.headers, resolved.proxySecret)) {
328
+ return null;
329
+ }
330
+ return email;
331
+ };
332
+ const rejectForgedProxyHeader = (res, ip, pathname) => {
333
+ audit('proxy_header.unverified', { ip, path: pathname });
334
+ sendJson(res, 401, {
335
+ error: 'unauthorized',
336
+ error_description: `X-Auth-Request-Email was present without a valid ${config_js_1.AUTH_PROXY_SECRET_HEADER} header. ` +
337
+ 'Only the trusted reverse proxy may assert an identity to this broker.',
338
+ });
339
+ };
256
340
  const server = http.createServer(async (req, res) => {
257
341
  try {
258
342
  const host = req.headers.host || `127.0.0.1:${resolved.port}`;
@@ -308,8 +392,14 @@ function createAuthBrokerServer(config) {
308
392
  }
309
393
  // Caddy forward_auth (via oauth2-proxy) sets X-Auth-Request-Email after the user
310
394
  // logs in with Google. The authorize endpoint is gated behind forward_auth in the
311
- // Caddyfile so this header is always present when the request reaches the broker.
312
- const userEmail = (req.headers['x-auth-request-email'] || '').trim().toLowerCase();
395
+ // Caddyfile so this header is always present when the request reaches the broker —
396
+ // but a caller that reaches the broker DIRECTLY can set it too, which is why the
397
+ // proxy also has to prove who it is.
398
+ const userEmail = proxyAssertedEmail(req);
399
+ if (userEmail === null) {
400
+ rejectForgedProxyHeader(res, ip, pathname);
401
+ return;
402
+ }
313
403
  if (!userEmail) {
314
404
  sendJson(res, 401, { error: 'unauthorized', error_description: 'No authenticated user (missing X-Auth-Request-Email)' });
315
405
  return;
@@ -335,7 +425,11 @@ function createAuthBrokerServer(config) {
335
425
  // (same as /oauth/authorize) — X-Auth-Request-Email is only set once Google login
336
426
  // completes. Federated agent services never hit this path (they have no oauth2-proxy),
337
427
  // so it 401s there, which is correct.
338
- const userEmail = (req.headers['x-auth-request-email'] || '').trim().toLowerCase();
428
+ const userEmail = proxyAssertedEmail(req);
429
+ if (userEmail === null) {
430
+ rejectForgedProxyHeader(res, ip, pathname);
431
+ return;
432
+ }
339
433
  if (!userEmail) {
340
434
  sendJson(res, 401, { error: 'unauthorized', error_description: 'No authenticated user (missing X-Auth-Request-Email)' });
341
435
  return;
@@ -508,6 +602,21 @@ function createAuthBrokerServer(config) {
508
602
  // Set X-Agent-User so Caddy's forward_auth can copy the validated identity
509
603
  // into the upstream request without serve needing its own JWT validation.
510
604
  res.setHeader('X-Agent-User', user.email || user.id);
605
+ // …and the ASSERTION alongside the identifier (PLAN/agent-identity.md §8.5 G1).
606
+ // The identifier alone is all the edge forwarded until now, so an edge-registered
607
+ // turn carried no `inboundToken`: `passthrough` failed closed on every tool call
608
+ // and an RFC 8693 `exchange` provider had no subject_token. This is the same
609
+ // token this request just verified — the browser (cookie) lane has no
610
+ // Authorization header for the edge to relay, so it has to come from here.
611
+ //
612
+ // Not a new exposure: whatever presented this token — Bearer header or cookie —
613
+ // is already on the request Caddy proxies to the agent. What changes is that the
614
+ // connector sidecar can now reach it, which is the whole point.
615
+ const presented = (0, jwt_js_1.extractBearerToken)(req.headers.authorization)
616
+ || extractCookieToken(req.headers.cookie, resolved.cookieName);
617
+ if (presented) {
618
+ res.setHeader('X-Agent-User-Assertion', presented);
619
+ }
511
620
  sendJson(res, 200, store.buildMe(user));
512
621
  return;
513
622
  }
@@ -519,20 +628,6 @@ function createAuthBrokerServer(config) {
519
628
  sendJson(res, 200, store.buildBootstrap(user));
520
629
  return;
521
630
  }
522
- if (req.method === 'POST' && pathname === '/api/ide/bootstrap/refresh') {
523
- if (!user) {
524
- sendJson(res, 401, { error: 'unauthorized' });
525
- return;
526
- }
527
- const routing = store.refreshRoutingCredentials();
528
- sendJson(res, 200, {
529
- routing: {
530
- litellmBaseUrl: resolved.team.litellmBaseUrl,
531
- ...routing,
532
- },
533
- });
534
- return;
535
- }
536
631
  if (req.method === 'POST' && pathname === '/api/ide/signout') {
537
632
  if (user) {
538
633
  store.revokeRefreshTokensForUser(user.id);
@@ -572,9 +667,14 @@ function createAuthBrokerServer(config) {
572
667
  }
573
668
  async function startAuthBrokerServer(config) {
574
669
  const broker = createAuthBrokerServer(config);
575
- const bind = process.env.AGENTHIPPO_AUTH_BIND?.trim() || '0.0.0.0';
670
+ const bind = (0, config_js_1.resolveAuthBrokerBind)();
671
+ if (bind.isPublic) {
672
+ console.warn(`[auth-broker] WARNING: listening on ${bind.address}:${broker.config.port} — this ` +
673
+ `token-issuing endpoint is reachable from off-host (AGENTHIPPO_AUTH_ALLOW_PUBLIC_BIND=1). ` +
674
+ `Ensure only the reverse proxy can reach it and that AGENTHIPPO_AUTH_PROXY_SECRET is set.`);
675
+ }
576
676
  await new Promise((resolve, reject) => {
577
- broker.server.listen(broker.config.port, bind, () => resolve());
677
+ broker.server.listen(broker.config.port, bind.address, () => resolve());
578
678
  broker.server.once('error', reject);
579
679
  });
580
680
  return broker;