@agenthippo-ai/agenthippo 0.2.3-dataplane → 1.110.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/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 +23 -25
- package/out/analytics/sessionEvents.js +98 -23
- 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 +122 -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 +67 -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 +332 -0
- package/out/cli/commands/serve.js +875 -52
- package/out/cli/commands/store.js +12 -4
- package/out/cli/data-plane/gateway.js +28 -5
- package/out/cli/data-plane/index.js +17 -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 +161 -0
- package/out/cli/deploy/layers/identity.js +85 -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 +155 -16
- 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 +80 -3
- 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 +110 -26
- package/out/engines/custom.js +8 -0
- package/out/evidence/sessionShip.js +610 -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 +17 -1
- 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 +309 -0
- package/out/pack/lease-schema.js +270 -0
- package/out/pack/lease.js +640 -0
- package/out/pack/reconcile.js +217 -0
- package/out/pack/signature.js +839 -0
- package/out/remote/handleRemoteChat.js +44 -3
- package/out/remote/remoteTrust.js +277 -0
- package/out/remote/serveClient.js +52 -5
- package/out/runner/host.js +131 -0
- package/out/runner/protocol.js +108 -0
- package/out/runner/remoteEngine.js +130 -0
- package/out/runtime/interfaces/edit-tracker.js +2 -0
- package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
- package/out/sandbox/containerDriver.js +570 -0
- package/out/sandbox/processDriver.js +315 -0
- package/out/sandbox/sandboxRunner.js +191 -0
- package/out/sandbox/support.js +301 -0
- package/out/sandbox/types.js +55 -0
- package/out/scripts/agentide-litellm.sh +67 -5
- package/out/scripts/agentide_litellm_hooks_v1.py +182 -21
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +94 -132
- package/out/scripts/analytics/setup-analytics.ps1 +37 -13
- package/out/scripts/analytics/setup-analytics.sh +93 -13
- package/out/scripts/analytics/start-analytics.sh +86 -14
- package/out/scripts/gateway/setup-gateway.sh +72 -32
- package/out/shared/models.js +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 +455 -0
- package/out/test/codex-new-file-survival.test.js +119 -0
- package/out/test/configuration-scopes.test.js +131 -0
- package/out/test/data-plane.test.js +67 -0
- package/out/test/deploy-init.test.js +40 -0
- package/out/test/gateway-client.test.js +28 -0
- package/out/test/gateway-install-source.test.js +111 -0
- package/out/test/gateway-lifecycle.test.js +1 -1
- package/out/test/gateway-outbound.test.js +7 -0
- package/out/test/gateway-secrets-preflight.test.js +127 -0
- package/out/test/gateway-session-routing.test.js +18 -0
- package/out/test/governor-hardening.test.js +217 -0
- package/out/test/headless-sync.test.js +22 -0
- package/out/test/lease-signature.test.js +357 -0
- package/out/test/lease-trust-root.test.js +273 -0
- package/out/test/lease-validation.test.js +169 -0
- package/out/test/lease.test.js +114 -0
- package/out/test/mcp.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 +395 -0
- package/out/test/sandbox-runner.test.js +343 -0
- package/out/test/serve-hardening.test.js +45 -6
- package/out/test/serveClient.test.js +6 -5
- package/out/test/sessionShip.test.js +560 -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
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.isAllowedRedirectUri = isAllowedRedirectUri;
|
|
40
|
+
exports.hasTrustedProxyProof = hasTrustedProxyProof;
|
|
39
41
|
exports.createAuthBrokerServer = createAuthBrokerServer;
|
|
40
42
|
exports.startAuthBrokerServer = startAuthBrokerServer;
|
|
41
43
|
const http = __importStar(require("http"));
|
|
@@ -65,11 +67,60 @@ 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 and this
|
|
107
|
+
* is where it is checked — constant-time, so a mismatched secret cannot be recovered byte by byte.
|
|
108
|
+
*/
|
|
109
|
+
function hasTrustedProxyProof(headers, expectedSecret) {
|
|
110
|
+
if (!expectedSecret) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
const raw = headers[config_js_1.AUTH_PROXY_SECRET_HEADER];
|
|
114
|
+
const presented = typeof raw === 'string' ? raw : Array.isArray(raw) ? String(raw[0] ?? '') : '';
|
|
115
|
+
if (!presented) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
// Hash both sides first: timingSafeEqual throws on a length mismatch, which would itself leak
|
|
119
|
+
// the secret's length.
|
|
120
|
+
const a = crypto.createHash('sha256').update(presented).digest();
|
|
121
|
+
const b = crypto.createHash('sha256').update(expectedSecret).digest();
|
|
122
|
+
return crypto.timingSafeEqual(a, b);
|
|
123
|
+
}
|
|
73
124
|
function sendJson(res, status, body) {
|
|
74
125
|
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
75
126
|
res.end(JSON.stringify(body));
|
|
@@ -161,11 +212,27 @@ function createAuthBrokerServer(config) {
|
|
|
161
212
|
throw new Error('AGENTHIPPO_AUTH_JWT_SECRET must be set to a strong secret in production. ' +
|
|
162
213
|
'Generate one with: openssl rand -hex 32');
|
|
163
214
|
}
|
|
164
|
-
// Fail-closed: a
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
215
|
+
// Fail-closed: without a proxy proof, `X-Auth-Request-Email` is a self-asserted claim. Anything
|
|
216
|
+
// that can reach this port could send `X-Auth-Request-Email: ceo@example.com` with a loopback
|
|
217
|
+
// redirect_uri to /oauth/authorize and exchange the resulting code at the ungated /oauth/token —
|
|
218
|
+
// a full account takeover with no credential. Same posture as the JWT/LiteLLM defaults above:
|
|
219
|
+
// refuse to boot rather than run a broker whose identity input is unauthenticated.
|
|
220
|
+
if (!resolved.devMode && !resolved.proxySecret && !resolved.trustProxyHeadersUnverified) {
|
|
221
|
+
throw new Error('AGENTHIPPO_AUTH_PROXY_SECRET must be set in production. The broker trusts ' +
|
|
222
|
+
'X-Auth-Request-Email from the oauth2-proxy/Caddy forward_auth sidecar; without a shared ' +
|
|
223
|
+
'secret proving the header came from that sidecar, any caller that reaches this port can ' +
|
|
224
|
+
'mint a token for an arbitrary email.\n' +
|
|
225
|
+
' 1. Generate one: openssl rand -hex 32\n' +
|
|
226
|
+
` 2. Set it here: AGENTHIPPO_AUTH_PROXY_SECRET=<secret>\n` +
|
|
227
|
+
` 3. Have the proxy send it, e.g. in the Caddyfile:\n` +
|
|
228
|
+
` header_up ${config_js_1.AUTH_PROXY_SECRET_HEADER} {env.AGENTHIPPO_AUTH_PROXY_SECRET}\n` +
|
|
229
|
+
'If the broker is genuinely unreachable except through the proxy and you accept the risk, ' +
|
|
230
|
+
'set AGENTHIPPO_AUTH_TRUST_PROXY_UNVERIFIED=1.');
|
|
231
|
+
}
|
|
232
|
+
if (!resolved.devMode && !resolved.proxySecret && resolved.trustProxyHeadersUnverified) {
|
|
233
|
+
console.warn('[auth-broker] WARNING: AGENTHIPPO_AUTH_TRUST_PROXY_UNVERIFIED=1 — X-Auth-Request-Email is ' +
|
|
234
|
+
'accepted without proof. Anything that can reach this port can mint a token for any email. ' +
|
|
235
|
+
'Set AGENTHIPPO_AUTH_PROXY_SECRET to close this.');
|
|
169
236
|
}
|
|
170
237
|
// Warn when no domain restriction is configured: any SSO-authenticated email can sign in.
|
|
171
238
|
if (!resolved.devMode && resolved.allowedEmailDomains.length === 0) {
|
|
@@ -229,11 +296,12 @@ function createAuthBrokerServer(config) {
|
|
|
229
296
|
// IdP's own broker still hits getUser for its locally-created users, so this fallback is
|
|
230
297
|
// a no-op there.
|
|
231
298
|
if (!user) {
|
|
299
|
+
const teamIdFromToken = typeof payload.team_id === 'string' && payload.team_id ? payload.team_id : undefined;
|
|
232
300
|
user = {
|
|
233
301
|
id: String(payload.sub),
|
|
234
302
|
email: String(payload.email),
|
|
235
303
|
displayName: typeof payload.name === 'string' && payload.name ? payload.name : String(payload.email),
|
|
236
|
-
teamId:
|
|
304
|
+
teamId: teamIdFromToken ?? (resolved.teamConfigured ? resolved.team.id : undefined),
|
|
237
305
|
};
|
|
238
306
|
}
|
|
239
307
|
if (payload.exp) {
|
|
@@ -253,6 +321,33 @@ function createAuthBrokerServer(config) {
|
|
|
253
321
|
}
|
|
254
322
|
return user;
|
|
255
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* Read the SSO-asserted email, but only if the request proved it came from the trusted proxy.
|
|
326
|
+
* Returns `undefined` when there is no email, and `null` when an email was present but the proof
|
|
327
|
+
* was missing/wrong — those are different failures and the second one is worth auditing loudly.
|
|
328
|
+
*/
|
|
329
|
+
const proxyAssertedEmail = (req) => {
|
|
330
|
+
const email = (req.headers['x-auth-request-email'] || '').trim().toLowerCase();
|
|
331
|
+
if (!email) {
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
334
|
+
// devMode and the explicit unverified opt-in are the only ways past the proof.
|
|
335
|
+
if (resolved.devMode || (!resolved.proxySecret && resolved.trustProxyHeadersUnverified)) {
|
|
336
|
+
return email;
|
|
337
|
+
}
|
|
338
|
+
if (!hasTrustedProxyProof(req.headers, resolved.proxySecret)) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
return email;
|
|
342
|
+
};
|
|
343
|
+
const rejectForgedProxyHeader = (res, ip, pathname) => {
|
|
344
|
+
audit('proxy_header.unverified', { ip, path: pathname });
|
|
345
|
+
sendJson(res, 401, {
|
|
346
|
+
error: 'unauthorized',
|
|
347
|
+
error_description: `X-Auth-Request-Email was present without a valid ${config_js_1.AUTH_PROXY_SECRET_HEADER} header. ` +
|
|
348
|
+
'Only the trusted reverse proxy may assert an identity to this broker.',
|
|
349
|
+
});
|
|
350
|
+
};
|
|
256
351
|
const server = http.createServer(async (req, res) => {
|
|
257
352
|
try {
|
|
258
353
|
const host = req.headers.host || `127.0.0.1:${resolved.port}`;
|
|
@@ -308,8 +403,14 @@ function createAuthBrokerServer(config) {
|
|
|
308
403
|
}
|
|
309
404
|
// Caddy forward_auth (via oauth2-proxy) sets X-Auth-Request-Email after the user
|
|
310
405
|
// 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
|
-
|
|
406
|
+
// Caddyfile so this header is always present when the request reaches the broker —
|
|
407
|
+
// but a caller that reaches the broker DIRECTLY can set it too, which is why the
|
|
408
|
+
// proxy also has to prove who it is.
|
|
409
|
+
const userEmail = proxyAssertedEmail(req);
|
|
410
|
+
if (userEmail === null) {
|
|
411
|
+
rejectForgedProxyHeader(res, ip, pathname);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
313
414
|
if (!userEmail) {
|
|
314
415
|
sendJson(res, 401, { error: 'unauthorized', error_description: 'No authenticated user (missing X-Auth-Request-Email)' });
|
|
315
416
|
return;
|
|
@@ -335,7 +436,11 @@ function createAuthBrokerServer(config) {
|
|
|
335
436
|
// (same as /oauth/authorize) — X-Auth-Request-Email is only set once Google login
|
|
336
437
|
// completes. Federated agent services never hit this path (they have no oauth2-proxy),
|
|
337
438
|
// so it 401s there, which is correct.
|
|
338
|
-
const userEmail = (req
|
|
439
|
+
const userEmail = proxyAssertedEmail(req);
|
|
440
|
+
if (userEmail === null) {
|
|
441
|
+
rejectForgedProxyHeader(res, ip, pathname);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
339
444
|
if (!userEmail) {
|
|
340
445
|
sendJson(res, 401, { error: 'unauthorized', error_description: 'No authenticated user (missing X-Auth-Request-Email)' });
|
|
341
446
|
return;
|
|
@@ -519,20 +624,6 @@ function createAuthBrokerServer(config) {
|
|
|
519
624
|
sendJson(res, 200, store.buildBootstrap(user));
|
|
520
625
|
return;
|
|
521
626
|
}
|
|
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
627
|
if (req.method === 'POST' && pathname === '/api/ide/signout') {
|
|
537
628
|
if (user) {
|
|
538
629
|
store.revokeRefreshTokensForUser(user.id);
|
|
@@ -572,9 +663,14 @@ function createAuthBrokerServer(config) {
|
|
|
572
663
|
}
|
|
573
664
|
async function startAuthBrokerServer(config) {
|
|
574
665
|
const broker = createAuthBrokerServer(config);
|
|
575
|
-
const bind =
|
|
666
|
+
const bind = (0, config_js_1.resolveAuthBrokerBind)();
|
|
667
|
+
if (bind.isPublic) {
|
|
668
|
+
console.warn(`[auth-broker] WARNING: listening on ${bind.address}:${broker.config.port} — this ` +
|
|
669
|
+
`token-issuing endpoint is reachable from off-host (AGENTHIPPO_AUTH_ALLOW_PUBLIC_BIND=1). ` +
|
|
670
|
+
`Ensure only the reverse proxy can reach it and that AGENTHIPPO_AUTH_PROXY_SECRET is set.`);
|
|
671
|
+
}
|
|
576
672
|
await new Promise((resolve, reject) => {
|
|
577
|
-
broker.server.listen(broker.config.port, bind, () => resolve());
|
|
673
|
+
broker.server.listen(broker.config.port, bind.address, () => resolve());
|
|
578
674
|
broker.server.once('error', reject);
|
|
579
675
|
});
|
|
580
676
|
return broker;
|
package/out/auth-broker/store.js
CHANGED
|
@@ -136,7 +136,7 @@ class AuthBrokerStore {
|
|
|
136
136
|
id: `usr_${crypto.randomBytes(8).toString('hex')}`,
|
|
137
137
|
email: normalized,
|
|
138
138
|
displayName: displayName?.trim() || normalized.split('@')[0] || normalized,
|
|
139
|
-
teamId: this.config.team.id,
|
|
139
|
+
...(this.config.teamConfigured ? { teamId: this.config.team.id } : {}),
|
|
140
140
|
};
|
|
141
141
|
this.users.set(user.id, user);
|
|
142
142
|
this.persist();
|
|
@@ -210,7 +210,7 @@ class AuthBrokerStore {
|
|
|
210
210
|
sub: user.id,
|
|
211
211
|
email: user.email,
|
|
212
212
|
name: user.displayName,
|
|
213
|
-
team_id: user.teamId,
|
|
213
|
+
...(user.teamId ? { team_id: user.teamId } : {}),
|
|
214
214
|
scope,
|
|
215
215
|
}, this.config.jwtSecret, this.config.accessTokenTtlSec);
|
|
216
216
|
const refreshToken = this.createRefreshToken(user.id, clientId);
|
|
@@ -221,6 +221,9 @@ class AuthBrokerStore {
|
|
|
221
221
|
};
|
|
222
222
|
}
|
|
223
223
|
buildBootstrap(user) {
|
|
224
|
+
// No AGENTHIPPO_TEAM_ID ⇒ solo deployment: no team object, and nothing is team-managed —
|
|
225
|
+
// store/routing still apply as defaults but are not locked against the user changing them.
|
|
226
|
+
const teamConfigured = this.config.teamConfigured;
|
|
224
227
|
return {
|
|
225
228
|
version: 1,
|
|
226
229
|
user: {
|
|
@@ -228,34 +231,28 @@ class AuthBrokerStore {
|
|
|
228
231
|
email: user.email,
|
|
229
232
|
displayName: user.displayName,
|
|
230
233
|
},
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
234
|
+
...(teamConfigured
|
|
235
|
+
? {
|
|
236
|
+
team: {
|
|
237
|
+
id: this.config.team.id,
|
|
238
|
+
name: this.config.team.name,
|
|
239
|
+
role: 'member',
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
: {}),
|
|
236
243
|
store: {
|
|
237
244
|
repo: this.config.team.storeRepo,
|
|
238
245
|
indexUrl: this.config.team.storeIndexUrl,
|
|
239
246
|
githubOrg: this.config.team.githubOrg,
|
|
240
247
|
},
|
|
241
248
|
routing: {
|
|
242
|
-
|
|
243
|
-
litellmApiKey: this.config.team.litellmApiKey,
|
|
244
|
-
// No litellmApiKeyExpiresAt: the team key is static — a fabricated expiry
|
|
245
|
-
// would only pretend rotation exists. Per-user virtual keys are the upgrade path.
|
|
249
|
+
inferenceGatewayUrl: this.config.team.gatewayUrl,
|
|
246
250
|
},
|
|
247
251
|
policy: {
|
|
248
|
-
|
|
249
|
-
teamManagedSettings: ['store', 'routing'],
|
|
252
|
+
teamManagedSettings: teamConfigured ? ['store', 'routing'] : [],
|
|
250
253
|
},
|
|
251
254
|
};
|
|
252
255
|
}
|
|
253
|
-
refreshRoutingCredentials() {
|
|
254
|
-
// The team key is static (no rotation backend yet), so no expiry is reported.
|
|
255
|
-
return {
|
|
256
|
-
litellmApiKey: this.config.team.litellmApiKey,
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
256
|
buildMe(user) {
|
|
260
257
|
return {
|
|
261
258
|
user: {
|
|
@@ -263,11 +260,15 @@ class AuthBrokerStore {
|
|
|
263
260
|
email: user.email,
|
|
264
261
|
displayName: user.displayName,
|
|
265
262
|
},
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
263
|
+
...(this.config.teamConfigured
|
|
264
|
+
? {
|
|
265
|
+
team: {
|
|
266
|
+
id: this.config.team.id,
|
|
267
|
+
name: this.config.team.name,
|
|
268
|
+
role: 'member',
|
|
269
|
+
},
|
|
270
|
+
}
|
|
271
|
+
: {}),
|
|
271
272
|
};
|
|
272
273
|
}
|
|
273
274
|
}
|