@agenthippo-ai/agenthippo 0.1.1 → 1.110.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/agentpack/index.js +2 -1
- package/out/agentpack/paths.js +23 -0
- package/out/analytics/dashboard/fleetDisplayUtils.js +3 -0
- package/out/analytics/dashboard/provider.js +13 -2
- package/out/analytics/dashboard/webview/panel.js +293 -82
- package/out/analytics/identity.js +0 -0
- package/out/analytics/service.js +23 -25
- package/out/analytics/sessionEvents.js +109 -22
- package/out/auth/authBaseUrl.js +59 -0
- package/out/auth/bootstrapApply.js +9 -10
- package/out/auth/brokerClient.js +49 -15
- package/out/auth/onboardingPrompt.js +112 -0
- package/out/auth/provider.js +9 -22
- package/out/auth/sessionStore.js +3 -40
- package/out/auth/signInFlow.js +2 -1
- package/out/auth-broker/config.js +54 -4
- package/out/auth-broker/server.js +152 -20
- 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/ask.js +3 -0
- package/out/cli/commands/chat.js +3 -0
- package/out/cli/commands/conformance.js +249 -0
- package/out/cli/commands/data-plane.js +67 -0
- package/out/cli/commands/deploy.js +50 -10
- package/out/cli/commands/gateway.js +58 -0
- package/out/cli/commands/idp.js +85 -0
- package/out/cli/commands/pack.js +182 -3
- package/out/cli/commands/sandbox.js +332 -0
- package/out/cli/commands/serve.js +1165 -82
- package/out/cli/commands/store.js +12 -4
- package/out/cli/credential-context.js +42 -80
- package/out/cli/data-plane/broker.js +78 -0
- package/out/cli/data-plane/connector-host.js +121 -0
- package/out/cli/data-plane/egress.js +189 -0
- package/out/cli/data-plane/gateway.js +218 -0
- package/out/cli/data-plane/index.js +152 -0
- package/out/cli/data-plane/registry.js +89 -0
- package/out/cli/data-plane/remote.js +106 -0
- package/out/cli/data-plane/sidecar.js +161 -0
- package/out/cli/deploy/blueprints/registry.js +84 -0
- package/out/cli/deploy/layers/identity.js +172 -0
- package/out/cli/deploy-init.js +1182 -347
- package/out/cli/deps.js +106 -2
- package/out/cli/idp-init.js +338 -0
- package/out/cli/index.js +192 -23
- package/out/cli/pack-install-ref.js +15 -0
- package/out/cli/pack-loader.js +45 -3
- package/out/cli/pack-runtime.js +61 -0
- package/out/cli/runtime.js +44 -3
- package/out/cli/serve-hardening.js +139 -3
- package/out/cli/state-sync.js +16 -15
- package/out/cli/utils.js +12 -1
- package/out/engines/base.js +32 -3
- package/out/engines/claude.js +47 -8
- package/out/engines/codex.js +125 -22
- package/out/engines/custom.js +54 -12
- package/out/evidence/sessionShip.js +610 -0
- package/out/extension.js +13 -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/outboundSend.js +6 -6
- package/out/gateway/secretsPreflight.js +90 -0
- package/out/gateway/service.js +7 -2
- package/out/headless/index.js +40 -9
- package/out/live-e2e/mcp-demo/server.mjs +32 -0
- package/out/live-probe/connector.mjs +8 -0
- package/out/live-probe/env-report.mjs +8 -0
- package/out/mcp.js +115 -67
- 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 -118
- 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 +12 -111
- package/out/shared/providers.js +2 -2
- 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 +198 -34
- 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 +112 -9
- package/out/test/blueprint-registry.test.js +105 -0
- 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/credential-context.test.js +23 -28
- package/out/test/custom-env-layering.test.js +151 -0
- package/out/test/data-plane-engine-wiring.test.js +250 -0
- package/out/test/data-plane.test.js +311 -0
- package/out/test/deploy-init.test.js +295 -4
- package/out/test/egress-broker.test.js +172 -0
- package/out/test/fleetDisplayUtils.test.js +4 -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/idp-init.test.js +151 -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-obo.test.js +52 -27
- package/out/test/mcp.test.js +69 -3
- package/out/test/model-routing.test.js +133 -0
- 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-gateway-turn.test.js +267 -0
- package/out/test/serve-hardening.test.js +125 -6
- package/out/test/serveClient.test.js +38 -5
- package/out/test/sessionShip.test.js +560 -0
- package/out/test/spotlightPublish.test.js +28 -0
- package/out/test/state-sync-peer.test.js +18 -5
- 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 +73 -20
- 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.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,6 +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';
|
|
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
|
+
}
|
|
17
62
|
function loadAuthBrokerConfig(env = process.env) {
|
|
18
63
|
const jwtSecret = env.AGENTHIPPO_AUTH_JWT_SECRET?.trim() || exports.DEFAULT_JWT_SECRET;
|
|
19
64
|
const publicBaseUrl = (env.AGENTHIPPO_AUTH_PUBLIC_URL?.trim() || `http://127.0.0.1:${env.AGENTHIPPO_AUTH_PORT?.trim() || '9780'}`).replace(/\/+$/, '');
|
|
@@ -27,21 +72,26 @@ function loadAuthBrokerConfig(env = process.env) {
|
|
|
27
72
|
accessTokenTtlSec: Number(env.AGENTHIPPO_AUTH_ACCESS_TTL_SEC || 3600),
|
|
28
73
|
refreshTokenTtlSec: Number(env.AGENTHIPPO_AUTH_REFRESH_TTL_SEC || 30 * 24 * 3600),
|
|
29
74
|
allowedEmailDomains: parseDomains(env.AGENTHIPPO_AUTH_ALLOWED_DOMAINS),
|
|
30
|
-
|
|
75
|
+
// Deliberately not gated on NODE_ENV=test: that value is too easy to inherit by accident
|
|
76
|
+
// (test runners, base images, wrapper scripts) and this flag unlocks an unauthenticated
|
|
77
|
+
// login bypass (/oauth/dev/login, login_hint auto-auth) — require the explicit opt-in.
|
|
78
|
+
devMode: env.AGENTHIPPO_AUTH_DEV === '1',
|
|
31
79
|
rateLimitPerMin: env.AGENTHIPPO_AUTH_RATE_LIMIT !== undefined ? Number(env.AGENTHIPPO_AUTH_RATE_LIMIT) : 60,
|
|
32
80
|
logPii: env.AGENTHIPPO_AUTH_LOG_PII === '1',
|
|
33
81
|
cookieName: env.AGENTHIPPO_AUTH_COOKIE_NAME?.trim() || 'ah_token',
|
|
34
82
|
cookieDomain: env.AGENTHIPPO_AUTH_COOKIE_DOMAIN?.trim() || '',
|
|
35
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',
|
|
36
86
|
dataDir: env.AGENTHIPPO_AUTH_DATA_DIR?.trim() || undefined,
|
|
87
|
+
teamConfigured: Boolean(env.AGENTHIPPO_TEAM_ID?.trim()),
|
|
37
88
|
team: {
|
|
38
89
|
id: env.AGENTHIPPO_TEAM_ID?.trim() || 'team_default',
|
|
39
90
|
name: env.AGENTHIPPO_TEAM_NAME?.trim() || 'Default Team',
|
|
40
91
|
storeRepo,
|
|
41
92
|
storeIndexUrl: env.AGENTHIPPO_TEAM_STORE_INDEX_URL?.trim() || `https://${storeOwner}.github.io/${storeName}`,
|
|
42
93
|
githubOrg: env.AGENTHIPPO_TEAM_GITHUB_ORG?.trim() || storeOwner,
|
|
43
|
-
|
|
44
|
-
litellmApiKey: env.AGENTHIPPO_TEAM_LITELLM_API_KEY?.trim() || 'ahk_team_dev_key',
|
|
94
|
+
gatewayUrl: env.AGENTHIPPO_TEAM_GATEWAY_URL?.trim() || 'https://gateway.agenthippo.ai/v1',
|
|
45
95
|
},
|
|
46
96
|
};
|
|
47
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"));
|
|
@@ -47,7 +49,78 @@ const store_js_1 = require("./store.js");
|
|
|
47
49
|
const pkce_js_1 = require("./pkce.js");
|
|
48
50
|
const jwt_js_1 = require("./jwt.js");
|
|
49
51
|
const types_js_1 = require("../auth/types.js");
|
|
52
|
+
const callbackUriParse_js_1 = require("../auth/callbackUriParse.js");
|
|
50
53
|
const serve_hardening_js_1 = require("../cli/serve-hardening.js");
|
|
54
|
+
/**
|
|
55
|
+
* Restrict `redirect_uri` to the shapes our own clients actually send — the CLI/IDE loopback
|
|
56
|
+
* callback (127.0.0.1/localhost, any OS-assigned port, see loopbackAuthServer.ts) and the IDE
|
|
57
|
+
* URI-scheme callback (any editor scheme, fixed authority+path, see callbackUri.ts). Without
|
|
58
|
+
* this, `/oauth/authorize` would hand a real auth code to whatever `redirect_uri` a caller
|
|
59
|
+
* supplies — an attacker can send a victim (who has an active SSO session) a link with their
|
|
60
|
+
* own redirect_uri and code_challenge and steal the resulting code via their own server.
|
|
61
|
+
*/
|
|
62
|
+
function isAllowedRedirectUri(redirectUri) {
|
|
63
|
+
let target;
|
|
64
|
+
try {
|
|
65
|
+
target = new url_1.URL(redirectUri);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
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
|
+
}
|
|
76
|
+
if (target.protocol === 'http:') {
|
|
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;
|
|
99
|
+
}
|
|
100
|
+
return (0, callbackUriParse_js_1.isAuthCallbackParts)(target.host, target.pathname);
|
|
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
|
+
}
|
|
51
124
|
function sendJson(res, status, body) {
|
|
52
125
|
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
53
126
|
res.end(JSON.stringify(body));
|
|
@@ -114,6 +187,9 @@ function extractCookieToken(cookieHeader, name) {
|
|
|
114
187
|
function safeReturnTo(rawReturnTo, cookieDomain, fallback) {
|
|
115
188
|
try {
|
|
116
189
|
const target = new url_1.URL(rawReturnTo);
|
|
190
|
+
if (target.protocol !== 'http:' && target.protocol !== 'https:') {
|
|
191
|
+
return fallback;
|
|
192
|
+
}
|
|
117
193
|
if (cookieDomain) {
|
|
118
194
|
const parent = cookieDomain.replace(/^\./, '').toLowerCase();
|
|
119
195
|
if (target.hostname.toLowerCase() === parent || target.hostname.toLowerCase().endsWith('.' + parent)) {
|
|
@@ -136,6 +212,28 @@ function createAuthBrokerServer(config) {
|
|
|
136
212
|
throw new Error('AGENTHIPPO_AUTH_JWT_SECRET must be set to a strong secret in production. ' +
|
|
137
213
|
'Generate one with: openssl rand -hex 32');
|
|
138
214
|
}
|
|
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.');
|
|
236
|
+
}
|
|
139
237
|
// Warn when no domain restriction is configured: any SSO-authenticated email can sign in.
|
|
140
238
|
if (!resolved.devMode && resolved.allowedEmailDomains.length === 0) {
|
|
141
239
|
console.warn('[auth-broker] Warning: AGENTHIPPO_AUTH_ALLOWED_DOMAINS is not set — ' +
|
|
@@ -198,11 +296,12 @@ function createAuthBrokerServer(config) {
|
|
|
198
296
|
// IdP's own broker still hits getUser for its locally-created users, so this fallback is
|
|
199
297
|
// a no-op there.
|
|
200
298
|
if (!user) {
|
|
299
|
+
const teamIdFromToken = typeof payload.team_id === 'string' && payload.team_id ? payload.team_id : undefined;
|
|
201
300
|
user = {
|
|
202
301
|
id: String(payload.sub),
|
|
203
302
|
email: String(payload.email),
|
|
204
303
|
displayName: typeof payload.name === 'string' && payload.name ? payload.name : String(payload.email),
|
|
205
|
-
teamId:
|
|
304
|
+
teamId: teamIdFromToken ?? (resolved.teamConfigured ? resolved.team.id : undefined),
|
|
206
305
|
};
|
|
207
306
|
}
|
|
208
307
|
if (payload.exp) {
|
|
@@ -222,6 +321,33 @@ function createAuthBrokerServer(config) {
|
|
|
222
321
|
}
|
|
223
322
|
return user;
|
|
224
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
|
+
};
|
|
225
351
|
const server = http.createServer(async (req, res) => {
|
|
226
352
|
try {
|
|
227
353
|
const host = req.headers.host || `127.0.0.1:${resolved.port}`;
|
|
@@ -256,6 +382,11 @@ function createAuthBrokerServer(config) {
|
|
|
256
382
|
sendJson(res, 400, { error: 'invalid_request' });
|
|
257
383
|
return;
|
|
258
384
|
}
|
|
385
|
+
if (!isAllowedRedirectUri(redirectUri)) {
|
|
386
|
+
audit('authorize.invalid_redirect_uri', { client_id: clientId });
|
|
387
|
+
sendJson(res, 400, { error: 'invalid_request', error_description: 'redirect_uri not allowed' });
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
259
390
|
// Dev mode with explicit login_hint: auto-authenticate (backward compat for tests).
|
|
260
391
|
if (resolved.devMode && loginHint) {
|
|
261
392
|
const user = store.upsertUser(loginHint);
|
|
@@ -272,8 +403,14 @@ function createAuthBrokerServer(config) {
|
|
|
272
403
|
}
|
|
273
404
|
// Caddy forward_auth (via oauth2-proxy) sets X-Auth-Request-Email after the user
|
|
274
405
|
// logs in with Google. The authorize endpoint is gated behind forward_auth in the
|
|
275
|
-
// Caddyfile so this header is always present when the request reaches the broker
|
|
276
|
-
|
|
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
|
+
}
|
|
277
414
|
if (!userEmail) {
|
|
278
415
|
sendJson(res, 401, { error: 'unauthorized', error_description: 'No authenticated user (missing X-Auth-Request-Email)' });
|
|
279
416
|
return;
|
|
@@ -299,7 +436,11 @@ function createAuthBrokerServer(config) {
|
|
|
299
436
|
// (same as /oauth/authorize) — X-Auth-Request-Email is only set once Google login
|
|
300
437
|
// completes. Federated agent services never hit this path (they have no oauth2-proxy),
|
|
301
438
|
// so it 401s there, which is correct.
|
|
302
|
-
const userEmail = (req
|
|
439
|
+
const userEmail = proxyAssertedEmail(req);
|
|
440
|
+
if (userEmail === null) {
|
|
441
|
+
rejectForgedProxyHeader(res, ip, pathname);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
303
444
|
if (!userEmail) {
|
|
304
445
|
sendJson(res, 401, { error: 'unauthorized', error_description: 'No authenticated user (missing X-Auth-Request-Email)' });
|
|
305
446
|
return;
|
|
@@ -483,20 +624,6 @@ function createAuthBrokerServer(config) {
|
|
|
483
624
|
sendJson(res, 200, store.buildBootstrap(user));
|
|
484
625
|
return;
|
|
485
626
|
}
|
|
486
|
-
if (req.method === 'POST' && pathname === '/api/ide/bootstrap/refresh') {
|
|
487
|
-
if (!user) {
|
|
488
|
-
sendJson(res, 401, { error: 'unauthorized' });
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
const routing = store.refreshRoutingCredentials();
|
|
492
|
-
sendJson(res, 200, {
|
|
493
|
-
routing: {
|
|
494
|
-
litellmBaseUrl: resolved.team.litellmBaseUrl,
|
|
495
|
-
...routing,
|
|
496
|
-
},
|
|
497
|
-
});
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
627
|
if (req.method === 'POST' && pathname === '/api/ide/signout') {
|
|
501
628
|
if (user) {
|
|
502
629
|
store.revokeRefreshTokensForUser(user.id);
|
|
@@ -536,9 +663,14 @@ function createAuthBrokerServer(config) {
|
|
|
536
663
|
}
|
|
537
664
|
async function startAuthBrokerServer(config) {
|
|
538
665
|
const broker = createAuthBrokerServer(config);
|
|
539
|
-
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
|
+
}
|
|
540
672
|
await new Promise((resolve, reject) => {
|
|
541
|
-
broker.server.listen(broker.config.port, bind, () => resolve());
|
|
673
|
+
broker.server.listen(broker.config.port, bind.address, () => resolve());
|
|
542
674
|
broker.server.once('error', reject);
|
|
543
675
|
});
|
|
544
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
|
}
|