@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.
- package/out/agentpack/index.js +3 -1
- package/out/agentpack/paths.js +25 -2
- package/out/analytics/dashboard/localFleetAdapter.js +82 -2
- package/out/analytics/dashboard/provider.js +13 -2
- package/out/analytics/dashboard/webview/panel.js +3 -2
- package/out/analytics/identity.js +0 -0
- package/out/analytics/service.js +29 -25
- package/out/analytics/sessionEvents.js +100 -24
- package/out/auth/authBaseUrl.js +59 -0
- package/out/auth/bootstrapApply.js +9 -10
- package/out/auth/brokerClient.js +48 -14
- package/out/auth/provider.js +8 -21
- package/out/auth/sessionStore.js +3 -40
- package/out/auth/signInFlow.js +2 -1
- package/out/auth-broker/config.js +50 -5
- package/out/auth-broker/server.js +126 -26
- package/out/auth-broker/store.js +25 -24
- package/out/authority/warrant.js +206 -0
- package/out/branding.js +27 -1
- package/out/cli/commands/conformance.js +249 -0
- package/out/cli/commands/data-plane.js +151 -0
- package/out/cli/commands/deploy.js +1 -0
- package/out/cli/commands/gateway.js +4 -0
- package/out/cli/commands/pack.js +182 -3
- package/out/cli/commands/sandbox.js +351 -0
- package/out/cli/commands/serve.js +1145 -130
- package/out/cli/commands/store.js +12 -4
- package/out/cli/credential-context.js +17 -0
- package/out/cli/data-plane/gateway.js +28 -5
- package/out/cli/data-plane/index.js +18 -2
- package/out/cli/data-plane/registry.js +9 -0
- package/out/cli/data-plane/remote.js +106 -0
- package/out/cli/data-plane/sidecar.js +225 -0
- package/out/cli/deploy/layers/identity.js +88 -4
- package/out/cli/deploy-init.js +89 -24
- package/out/cli/deps.js +106 -2
- package/out/cli/idp-init.js +35 -0
- package/out/cli/index.js +171 -17
- package/out/cli/pack-install-ref.js +15 -0
- package/out/cli/pack-loader.js +8 -1
- package/out/cli/pack-runtime.js +61 -0
- package/out/cli/runtime.js +35 -1
- package/out/cli/serve-hardening.js +260 -14
- package/out/cli/utils.js +12 -1
- package/out/engines/base.js +32 -3
- package/out/engines/claude.js +33 -15
- package/out/engines/codex.js +111 -27
- package/out/engines/custom.js +15 -1
- package/out/evidence/sessionShip.js +643 -0
- package/out/extension.js +11 -2
- package/out/gateway/bridge.js +40 -2
- package/out/gateway/client.js +34 -1
- package/out/gateway/installSource.js +68 -0
- package/out/gateway/lifecycle.js +185 -51
- package/out/gateway/outboundPaths.js +4 -1
- package/out/gateway/secretsPreflight.js +90 -0
- package/out/gateway/service.js +7 -2
- package/out/headless/index.js +25 -2
- package/out/live-e2e/mcp-demo/server.mjs +32 -0
- package/out/mcp.js +30 -0
- package/out/pack/caps.js +271 -0
- package/out/pack/governor.js +326 -0
- package/out/pack/lease-schema.js +273 -0
- package/out/pack/lease.js +682 -0
- package/out/pack/reconcile.js +217 -0
- package/out/pack/signature.js +839 -0
- package/out/remote/handleRemoteChat.js +44 -3
- package/out/remote/remoteTrust.js +277 -0
- package/out/remote/serveClient.js +52 -5
- package/out/runner/host.js +141 -0
- package/out/runner/protocol.js +128 -0
- package/out/runner/remoteEngine.js +166 -0
- package/out/runtime/interfaces/edit-tracker.js +2 -0
- package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
- package/out/sandbox/containerDriver.js +572 -0
- package/out/sandbox/processDriver.js +318 -0
- package/out/sandbox/sandboxRunner.js +303 -0
- package/out/sandbox/support.js +301 -0
- package/out/sandbox/types.js +55 -0
- package/out/scripts/agentide-litellm.sh +67 -5
- package/out/scripts/agentide_litellm_hooks_v1.py +266 -21
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +98 -132
- package/out/scripts/analytics/setup-analytics.ps1 +45 -14
- package/out/scripts/analytics/setup-analytics.sh +106 -15
- package/out/scripts/analytics/start-analytics.sh +86 -14
- package/out/scripts/gateway/setup-gateway.sh +72 -32
- package/out/shared/models.js +11 -110
- package/out/spotlight/install.js +7 -1
- package/out/store/index.js +41 -10
- package/out/sync.js +3 -1
- package/out/test/analytics.test.js +162 -23
- package/out/test/auth-baseurl.test.js +132 -0
- package/out/test/auth-bootstrapApply.test.js +27 -6
- package/out/test/auth-broker-hardening.test.js +227 -0
- package/out/test/auth-broker.test.js +59 -9
- package/out/test/branding.test.js +13 -0
- package/out/test/caps-governor.test.js +460 -0
- package/out/test/codex-new-file-survival.test.js +119 -0
- package/out/test/configuration-scopes.test.js +131 -0
- package/out/test/data-plane-engine-wiring.test.js +32 -0
- package/out/test/data-plane.test.js +380 -0
- package/out/test/deploy-init.test.js +47 -1
- package/out/test/gateway-client.test.js +28 -0
- package/out/test/gateway-install-source.test.js +111 -0
- package/out/test/gateway-lifecycle.test.js +1 -1
- package/out/test/gateway-outbound.test.js +7 -0
- package/out/test/gateway-secrets-preflight.test.js +127 -0
- package/out/test/gateway-session-routing.test.js +18 -0
- package/out/test/governor-hardening.test.js +222 -0
- package/out/test/headless-sync.test.js +22 -0
- package/out/test/lease-signature.test.js +357 -0
- package/out/test/lease-trust-root.test.js +273 -0
- package/out/test/lease-validation.test.js +224 -0
- package/out/test/lease.test.js +114 -0
- package/out/test/localFleetAdapter.test.js +91 -0
- package/out/test/mcp.test.js +27 -1
- package/out/test/pack-admission.test.js +216 -0
- package/out/test/pack-digest-parity.test.js +140 -0
- package/out/test/pack-reconcile.test.js +183 -0
- package/out/test/pack-signature-artifact.test.js +103 -0
- package/out/test/pack-signature.test.js +176 -0
- package/out/test/pack-trust-root.test.js +202 -0
- package/out/test/remote-trust.test.js +242 -0
- package/out/test/runner-protocol.test.js +249 -0
- package/out/test/sandbox-driver.test.js +553 -0
- package/out/test/sandbox-hardening.test.js +406 -0
- package/out/test/sandbox-runner.test.js +343 -0
- package/out/test/sandbox-scope.test.js +449 -0
- package/out/test/serve-cron-acceptance.test.js +370 -0
- package/out/test/serve-gateway-turn.test.js +122 -0
- package/out/test/serve-hardening.test.js +227 -15
- package/out/test/serveClient.test.js +6 -5
- package/out/test/sessionShip.test.js +618 -0
- package/out/test/spotlightPublish.test.js +28 -0
- package/out/test/store-install-safe.test.js +69 -0
- package/out/test/tls-bypass.test.js +90 -0
- package/out/test/warrant.test.js +138 -0
- package/out/utils.js +46 -9
- 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
|
|
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
|
|
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: '' });
|
package/out/auth/brokerClient.js
CHANGED
|
@@ -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 ||
|
|
20
|
-
return value
|
|
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}` },
|
package/out/auth/provider.js
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
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;
|
package/out/auth/sessionStore.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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 {
|
package/out/auth/signInFlow.js
CHANGED
|
@@ -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,
|
|
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.
|
|
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
|
-
/**
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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;
|