@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
|
@@ -37,6 +37,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.storeCommand = storeCommand;
|
|
40
|
+
exports.installFromStore = installFromStore;
|
|
40
41
|
exports.resolvePublishBundleMode = resolvePublishBundleMode;
|
|
41
42
|
/**
|
|
42
43
|
* Store Command - Search and install from the store
|
|
@@ -54,6 +55,7 @@ const path = __importStar(require("path"));
|
|
|
54
55
|
const os = __importStar(require("os"));
|
|
55
56
|
const zlib_1 = require("zlib");
|
|
56
57
|
const branding_1 = require("../../branding");
|
|
58
|
+
const reconcile_1 = require("../../pack/reconcile");
|
|
57
59
|
const index_1 = require("../../registry/index");
|
|
58
60
|
const shared_1 = require("../../store/shared");
|
|
59
61
|
const install_1 = require("../../engines/install");
|
|
@@ -361,7 +363,9 @@ async function installFromStore(options) {
|
|
|
361
363
|
installDir = path.join(workspace, branding_1.AGENTIDE_DIR_NAME, 'agents', agentId, 'skills', slug);
|
|
362
364
|
break;
|
|
363
365
|
case 'pack':
|
|
364
|
-
installDir =
|
|
366
|
+
installDir = options.installDir
|
|
367
|
+
? path.resolve(options.installDir)
|
|
368
|
+
: path.join(os.homedir(), branding_1.AGENTIDE_DIR_NAME, 'agents', slug, targetVersion);
|
|
365
369
|
break;
|
|
366
370
|
case 'mcp':
|
|
367
371
|
installDir = path.join(workspace, branding_1.AGENTIDE_DIR_NAME, 'agents', agentId, 'mcp', slug);
|
|
@@ -392,12 +396,16 @@ async function installFromStore(options) {
|
|
|
392
396
|
console.log(` Extracting ${files.length} files...`);
|
|
393
397
|
await fs.mkdir(installDir, { recursive: true });
|
|
394
398
|
for (const file of files) {
|
|
395
|
-
const filePath =
|
|
399
|
+
const filePath = (0, reconcile_1.safeJoin)(installDir, file.path);
|
|
400
|
+
if (!filePath) {
|
|
401
|
+
console.error(`Error: refusing install — artifact path escapes destination (${file.path})`);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
396
404
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
397
405
|
await fs.writeFile(filePath, file.content);
|
|
398
406
|
}
|
|
399
|
-
//
|
|
400
|
-
if (type === 'pack') {
|
|
407
|
+
// Default library layout tracks current.txt beside versioned dirs; a custom --to dir is flat.
|
|
408
|
+
if (type === 'pack' && !options.installDir) {
|
|
401
409
|
const packRoot = path.join(os.homedir(), branding_1.AGENTIDE_DIR_NAME, 'agents', slug);
|
|
402
410
|
await fs.writeFile(path.join(packRoot, 'current.txt'), targetVersion);
|
|
403
411
|
}
|
|
@@ -36,7 +36,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.TURN_NONCE_ENV = exports.DATA_MCP_URL_ENV = void 0;
|
|
39
|
+
exports.TURN_NONCE_HEADER = exports.TURN_NONCE_ENV = exports.DATA_MCP_URL_ENV = void 0;
|
|
40
40
|
exports.startDataGateway = startDataGateway;
|
|
41
41
|
/**
|
|
42
42
|
* Data-plane MCP gateway — a streamable-HTTP MCP server inside the serve (broker) process,
|
|
@@ -60,6 +60,14 @@ const registry_1 = require("./registry");
|
|
|
60
60
|
exports.DATA_MCP_URL_ENV = 'AGENTHIPPO_DATA_MCP_URL';
|
|
61
61
|
/** The turn capability. Per-spawn env only; never written to any shared file. */
|
|
62
62
|
exports.TURN_NONCE_ENV = 'AGENTHIPPO_TURN_NONCE';
|
|
63
|
+
/**
|
|
64
|
+
* Request header carrying an EDGE-registered turn nonce (P1 fix, obo-inprogress §2). The
|
|
65
|
+
* trusted edge (IdP Caddy) registers the turn with the sidecar via forward_auth and copies
|
|
66
|
+
* this header onto the request it proxies to serve. Trusted exactly like the pinned identity
|
|
67
|
+
* header: only the in-network edge can reach serve, and the edge overwrites any client-
|
|
68
|
+
* supplied value. A forged value fails closed at the gateway (unknown nonce ⇒ 401).
|
|
69
|
+
*/
|
|
70
|
+
exports.TURN_NONCE_HEADER = 'x-agent-turn-nonce';
|
|
63
71
|
function bearerToken(header) {
|
|
64
72
|
const m = header?.match(/^Bearer\s+(\S+)$/i);
|
|
65
73
|
return m?.[1];
|
|
@@ -72,6 +80,20 @@ async function startDataGateway(opts) {
|
|
|
72
80
|
const server = http.createServer(async (req, res) => {
|
|
73
81
|
try {
|
|
74
82
|
const urlPath = req.url?.split('?')[0] ?? '';
|
|
83
|
+
// Release-by-nonce (edge-registration mode): serve holds the nonce but no control-API
|
|
84
|
+
// authority, so it ends the turn here. Auth is the nonce itself — releasing is the one
|
|
85
|
+
// operation safe to grant any nonce holder (fail-closed: it only kills the capability
|
|
86
|
+
// early; the registry deadline remains the backstop when this is never called).
|
|
87
|
+
if (urlPath === '/turn' && req.method === 'DELETE') {
|
|
88
|
+
const nonce = bearerToken(req.headers.authorization);
|
|
89
|
+
if (nonce) {
|
|
90
|
+
const released = opts.registry.releaseByNonce(nonce);
|
|
91
|
+
opts.audit?.('edge', 'turn_release', { released, nonce_sha256_prefix: noncePrefix(nonce) });
|
|
92
|
+
}
|
|
93
|
+
res.writeHead(204);
|
|
94
|
+
res.end();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
75
97
|
if (urlPath !== '/mcp') {
|
|
76
98
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
77
99
|
res.end(JSON.stringify({ error: 'not found' }));
|
|
@@ -129,16 +151,17 @@ async function startDataGateway(opts) {
|
|
|
129
151
|
res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal error' }, id: null }));
|
|
130
152
|
}
|
|
131
153
|
});
|
|
154
|
+
const bindHost = opts.host ?? '127.0.0.1';
|
|
132
155
|
await new Promise((resolve, reject) => {
|
|
133
156
|
server.once('error', reject);
|
|
134
|
-
//
|
|
135
|
-
server.listen(0,
|
|
157
|
+
// Default loopback; a sidecar binds 0.0.0.0 for the private pod network only.
|
|
158
|
+
server.listen(opts.port ?? 0, bindHost, () => resolve());
|
|
136
159
|
});
|
|
137
160
|
const address = server.address();
|
|
138
161
|
if (!address || typeof address === 'string') {
|
|
139
|
-
throw new Error('Data gateway failed to bind a
|
|
162
|
+
throw new Error('Data gateway failed to bind a port');
|
|
140
163
|
}
|
|
141
|
-
const url = `http
|
|
164
|
+
const url = `http://${opts.advertiseHost ?? '127.0.0.1'}:${address.port}/mcp`;
|
|
142
165
|
opts.logger?.info(`[DataPlane] gateway listening at ${url}`);
|
|
143
166
|
return {
|
|
144
167
|
url,
|
|
@@ -36,7 +36,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.DataPlane = exports.EGRESS_URL_ENV = exports.startEgressBroker = exports.TURN_NONCE_ENV = exports.DATA_MCP_URL_ENV = exports.startDataGateway = exports.TURN_META_KEY = exports.ConnectorHost = exports.identityProvider = exports.passthroughProvider = exports.resolveProvider = exports.CredentialBroker = exports.DEFAULT_TURN_DEADLINE_MS = exports.hashNonce = exports.TurnRegistry = void 0;
|
|
39
|
+
exports.DataPlane = exports.resolveEdgeDataPlane = exports.EdgeDataPlane = exports.resolveRemoteDataPlane = exports.RemoteDataPlane = exports.startDataPlaneSidecar = exports.EGRESS_URL_ENV = exports.startEgressBroker = exports.TURN_NONCE_HEADER = exports.TURN_NONCE_ENV = exports.DATA_MCP_URL_ENV = exports.startDataGateway = exports.TURN_META_KEY = exports.ConnectorHost = exports.identityProvider = exports.passthroughProvider = exports.resolveProvider = exports.CredentialBroker = exports.DEFAULT_TURN_DEADLINE_MS = exports.hashNonce = exports.TurnRegistry = void 0;
|
|
40
40
|
exports.resolveDataPlaneConfig = resolveDataPlaneConfig;
|
|
41
41
|
/**
|
|
42
42
|
* DataPlane — orchestration of the credential broker + data-plane gateway
|
|
@@ -69,9 +69,17 @@ var gateway_2 = require("./gateway");
|
|
|
69
69
|
Object.defineProperty(exports, "startDataGateway", { enumerable: true, get: function () { return gateway_2.startDataGateway; } });
|
|
70
70
|
Object.defineProperty(exports, "DATA_MCP_URL_ENV", { enumerable: true, get: function () { return gateway_2.DATA_MCP_URL_ENV; } });
|
|
71
71
|
Object.defineProperty(exports, "TURN_NONCE_ENV", { enumerable: true, get: function () { return gateway_2.TURN_NONCE_ENV; } });
|
|
72
|
+
Object.defineProperty(exports, "TURN_NONCE_HEADER", { enumerable: true, get: function () { return gateway_2.TURN_NONCE_HEADER; } });
|
|
72
73
|
var egress_1 = require("./egress");
|
|
73
74
|
Object.defineProperty(exports, "startEgressBroker", { enumerable: true, get: function () { return egress_1.startEgressBroker; } });
|
|
74
75
|
Object.defineProperty(exports, "EGRESS_URL_ENV", { enumerable: true, get: function () { return egress_1.EGRESS_URL_ENV; } });
|
|
76
|
+
var sidecar_1 = require("./sidecar");
|
|
77
|
+
Object.defineProperty(exports, "startDataPlaneSidecar", { enumerable: true, get: function () { return sidecar_1.startDataPlaneSidecar; } });
|
|
78
|
+
var remote_1 = require("./remote");
|
|
79
|
+
Object.defineProperty(exports, "RemoteDataPlane", { enumerable: true, get: function () { return remote_1.RemoteDataPlane; } });
|
|
80
|
+
Object.defineProperty(exports, "resolveRemoteDataPlane", { enumerable: true, get: function () { return remote_1.resolveRemoteDataPlane; } });
|
|
81
|
+
Object.defineProperty(exports, "EdgeDataPlane", { enumerable: true, get: function () { return remote_1.EdgeDataPlane; } });
|
|
82
|
+
Object.defineProperty(exports, "resolveEdgeDataPlane", { enumerable: true, get: function () { return remote_1.resolveEdgeDataPlane; } });
|
|
75
83
|
/**
|
|
76
84
|
* Resolve the pack's data-plane declaration. The connector path resolves against the PACK
|
|
77
85
|
* SOURCE directory (operator-authored, broker trust domain) — not the agent-readable
|
|
@@ -108,7 +116,10 @@ class DataPlane {
|
|
|
108
116
|
const broker = new broker_1.CredentialBroker((0, broker_1.resolveProvider)(cfg.provider));
|
|
109
117
|
const connector = await connector_host_1.ConnectorHost.spawn({ connectorPath: cfg.connectorPath, logger: opts?.logger });
|
|
110
118
|
try {
|
|
111
|
-
const gateway = await (0, gateway_1.startDataGateway)({
|
|
119
|
+
const gateway = await (0, gateway_1.startDataGateway)({
|
|
120
|
+
registry, broker, connector, logger: opts?.logger, audit: opts?.audit,
|
|
121
|
+
host: opts?.gateway?.host, port: opts?.gateway?.port, advertiseHost: opts?.gateway?.advertiseHost,
|
|
122
|
+
});
|
|
112
123
|
return new DataPlane(gateway.url, registry, broker, connector, gateway);
|
|
113
124
|
}
|
|
114
125
|
catch (err) {
|
|
@@ -116,6 +127,10 @@ class DataPlane {
|
|
|
116
127
|
throw err;
|
|
117
128
|
}
|
|
118
129
|
}
|
|
130
|
+
/** Register a turn directly in the local registry (used by the sidecar control API). */
|
|
131
|
+
registerTurnRaw(reg) {
|
|
132
|
+
return this.registry.register(reg);
|
|
133
|
+
}
|
|
119
134
|
/**
|
|
120
135
|
* Register a verified turn. Returns the per-spawn env for the engine and the release()
|
|
121
136
|
* to call in the turn's `finally`. The gateway URL itself is boot-stable and rides
|
|
@@ -53,6 +53,15 @@ class TurnRegistry {
|
|
|
53
53
|
},
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Release a turn by its nonce (idempotent). Lets a holder of the nonce end its own turn —
|
|
58
|
+
* used by the gateway's release endpoint for EDGE-registered turns, where serve holds the
|
|
59
|
+
* nonce but no registration authority. Strictly fail-closed: the worst a stolen nonce can
|
|
60
|
+
* do here is end the capability early.
|
|
61
|
+
*/
|
|
62
|
+
releaseByNonce(nonce) {
|
|
63
|
+
return this.entries.delete(hashNonce(nonce));
|
|
64
|
+
}
|
|
56
65
|
/** Resolve a presented nonce to its turn, enforcing the deadline. Fail-closed: unknown/expired → undefined. */
|
|
57
66
|
resolve(nonce, now = Date.now()) {
|
|
58
67
|
const entry = this.entries.get(hashNonce(nonce));
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*---------------------------------------------------------------------------------------------
|
|
3
|
+
* Copyright (c) AgentHippo.ai. All rights reserved.
|
|
4
|
+
*--------------------------------------------------------------------------------------------*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RemoteDataPlane = exports.EdgeDataPlane = void 0;
|
|
7
|
+
exports.resolveEdgeDataPlane = resolveEdgeDataPlane;
|
|
8
|
+
exports.resolveRemoteDataPlane = resolveRemoteDataPlane;
|
|
9
|
+
/**
|
|
10
|
+
* RemoteDataPlane — serve's client to a data-plane SIDECAR (secure-obo-design §7.6).
|
|
11
|
+
*
|
|
12
|
+
* When the connector runs in its own container, serve (in the agent container) holds NO base
|
|
13
|
+
* credential. It registers each verified turn with the sidecar's authenticated control API and
|
|
14
|
+
* hands the engine the returned nonce plus the sidecar's gateway URL. The engine dials the
|
|
15
|
+
* sidecar gateway with the nonce; the sidecar resolves nonce → user and runs the connector.
|
|
16
|
+
*
|
|
17
|
+
* Presents the same `registerTurn()` shape as the in-process `DataPlane`, so serve's turn
|
|
18
|
+
* handling is identical whether the connector is in-process or a sidecar.
|
|
19
|
+
*/
|
|
20
|
+
const gateway_1 = require("./gateway");
|
|
21
|
+
/** Read the edge-mode wiring from env. Returns null unless explicitly enabled. */
|
|
22
|
+
function resolveEdgeDataPlane(env = process.env) {
|
|
23
|
+
const enabled = env.AGENTHIPPO_DATA_PLANE_EDGE?.trim();
|
|
24
|
+
const gatewayUrl = env.AGENTHIPPO_DATA_MCP_URL?.trim();
|
|
25
|
+
if (!enabled || enabled === '0' || enabled.toLowerCase() === 'false' || !gatewayUrl) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return { gatewayUrl };
|
|
29
|
+
}
|
|
30
|
+
class EdgeDataPlane {
|
|
31
|
+
cfg;
|
|
32
|
+
constructor(cfg) {
|
|
33
|
+
this.cfg = cfg;
|
|
34
|
+
}
|
|
35
|
+
/** The engine-facing gateway URL (boot-stable; set into process.env by serve like the local plane). */
|
|
36
|
+
get url() { return this.cfg.gatewayUrl; }
|
|
37
|
+
/**
|
|
38
|
+
* Adopt an edge-registered nonce for this turn. Release calls the gateway's
|
|
39
|
+
* nonce-authenticated `DELETE /turn` — the one operation a bare nonce may perform besides
|
|
40
|
+
* the tool surface, and strictly fail-closed (it only ends the capability early; the
|
|
41
|
+
* sidecar's registry deadline is the backstop when the call never lands).
|
|
42
|
+
*/
|
|
43
|
+
adoptTurn(nonce) {
|
|
44
|
+
return {
|
|
45
|
+
nonce,
|
|
46
|
+
env: { [gateway_1.TURN_NONCE_ENV]: nonce, [gateway_1.DATA_MCP_URL_ENV]: this.cfg.gatewayUrl },
|
|
47
|
+
release: async () => {
|
|
48
|
+
try {
|
|
49
|
+
await fetch(new URL('/turn', this.cfg.gatewayUrl), {
|
|
50
|
+
method: 'DELETE',
|
|
51
|
+
headers: { Authorization: `Bearer ${nonce}` },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch { /* best-effort release; the sidecar deadline is the backstop */ }
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.EdgeDataPlane = EdgeDataPlane;
|
|
60
|
+
/** Read the sidecar wiring from env (set at deploy on the agent container). Returns null if absent. */
|
|
61
|
+
function resolveRemoteDataPlane(env = process.env) {
|
|
62
|
+
const controlUrl = env.AGENTHIPPO_DATA_PLANE_CONTROL_URL?.trim();
|
|
63
|
+
const registerToken = env.AGENTHIPPO_DATA_PLANE_TOKEN?.trim();
|
|
64
|
+
const gatewayUrl = env.AGENTHIPPO_DATA_MCP_URL?.trim();
|
|
65
|
+
if (!controlUrl || !registerToken || !gatewayUrl) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return { controlUrl, registerToken, gatewayUrl };
|
|
69
|
+
}
|
|
70
|
+
class RemoteDataPlane {
|
|
71
|
+
cfg;
|
|
72
|
+
constructor(cfg) {
|
|
73
|
+
this.cfg = cfg;
|
|
74
|
+
}
|
|
75
|
+
/** The engine-facing gateway URL (boot-stable; set into process.env by serve like the local plane). */
|
|
76
|
+
get url() { return this.cfg.gatewayUrl; }
|
|
77
|
+
/**
|
|
78
|
+
* Register a turn with the sidecar. Returns the per-spawn env + release, matching DataPlane.
|
|
79
|
+
* The base credential never touches this process — only the nonce comes back.
|
|
80
|
+
*/
|
|
81
|
+
async registerTurn(reg) {
|
|
82
|
+
const res = await fetch(`${this.cfg.controlUrl.replace(/\/$/, '')}/turns`, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.cfg.registerToken}` },
|
|
85
|
+
body: JSON.stringify(reg),
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
throw new Error(`data-plane sidecar register failed: HTTP ${res.status}`);
|
|
89
|
+
}
|
|
90
|
+
const { nonce } = await res.json();
|
|
91
|
+
return {
|
|
92
|
+
nonce,
|
|
93
|
+
env: { [gateway_1.TURN_NONCE_ENV]: nonce, [gateway_1.DATA_MCP_URL_ENV]: this.cfg.gatewayUrl },
|
|
94
|
+
release: async () => {
|
|
95
|
+
try {
|
|
96
|
+
await fetch(`${this.cfg.controlUrl.replace(/\/$/, '')}/turns/${encodeURIComponent(nonce)}`, {
|
|
97
|
+
method: 'DELETE',
|
|
98
|
+
headers: { Authorization: `Bearer ${this.cfg.registerToken}` },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch { /* best-effort release; the sidecar deadline is the backstop */ }
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
exports.RemoteDataPlane = RemoteDataPlane;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*---------------------------------------------------------------------------------------------
|
|
3
|
+
* Copyright (c) AgentHippo.ai. All rights reserved.
|
|
4
|
+
*--------------------------------------------------------------------------------------------*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.startDataPlaneSidecar = startDataPlaneSidecar;
|
|
40
|
+
/**
|
|
41
|
+
* Data-plane SIDECAR (secure-obo-design §7.6 / aperture-thin) — the connector's own trust
|
|
42
|
+
* domain, a SEPARATE container from the engine/agent.
|
|
43
|
+
*
|
|
44
|
+
* It runs the full data plane (TurnRegistry + gateway + connector) and holds the backend BASE
|
|
45
|
+
* credential (AWS keys, Postgres pool login, …). The agent container gets NONE of it: it only
|
|
46
|
+
* (a) registers verified turns via this authenticated control API and (b) points the engine at
|
|
47
|
+
* the gateway URL. Because the sidecar is a different container (different PID namespace), the
|
|
48
|
+
* engine cannot read the base credential from `/proc/<pid>/environ`, and because the sidecar
|
|
49
|
+
* resolves the nonce → userId server-side, the engine cannot forge another user's identity.
|
|
50
|
+
*
|
|
51
|
+
* Control API (loopback/pod-network, bearer-authenticated):
|
|
52
|
+
* POST /turns { userId, sessionKey, inboundToken?, egressStoreKey? } → { nonce }
|
|
53
|
+
* DELETE /turns/:nonce → 204
|
|
54
|
+
* GET /edge/turn (forward_auth from the trusted EDGE; X-Agent-User header)
|
|
55
|
+
* → 200 + `X-Agent-Turn-Nonce` response header
|
|
56
|
+
* The gateway (nonce-authenticated MCP) is a separate port the engine dials directly.
|
|
57
|
+
*
|
|
58
|
+
* EDGE-registration mode (P1 fix, obo-inprogress §2): the bearer secret is held by the IdP
|
|
59
|
+
* Caddy — NOT the agent container. Caddy registers each chat turn via `forward_auth` against
|
|
60
|
+
* `/edge/turn` (identity from the X-Agent-User header its auth-broker hop just verified) and
|
|
61
|
+
* copies the returned nonce header onto the request it proxies to serve. serve then has no
|
|
62
|
+
* registration authority at all: a compromised engine reading serve's `/proc/<pid>/environ`
|
|
63
|
+
* finds no token that can mint a nonce for an arbitrary userId. Release rides the gateway's
|
|
64
|
+
* nonce-authenticated `DELETE /turn`; the registry deadline is the backstop.
|
|
65
|
+
*/
|
|
66
|
+
const http = __importStar(require("http"));
|
|
67
|
+
const crypto_1 = require("crypto");
|
|
68
|
+
const index_1 = require("./index");
|
|
69
|
+
const serve_hardening_1 = require("../serve-hardening");
|
|
70
|
+
const gateway_1 = require("./gateway");
|
|
71
|
+
function bearer(header) {
|
|
72
|
+
return header?.match(/^Bearer\s+(\S+)$/i)?.[1];
|
|
73
|
+
}
|
|
74
|
+
function tokenEq(a, b) {
|
|
75
|
+
const x = Buffer.from(a);
|
|
76
|
+
const y = Buffer.from(b);
|
|
77
|
+
return x.length === y.length && (0, crypto_1.timingSafeEqual)(x, y);
|
|
78
|
+
}
|
|
79
|
+
async function startDataPlaneSidecar(opts) {
|
|
80
|
+
const host = opts.bindHost ?? '0.0.0.0';
|
|
81
|
+
// The gateway binds `host` on a fixed port so the engine can be told the URL at deploy time.
|
|
82
|
+
const plane = await index_1.DataPlane.start(opts.config, {
|
|
83
|
+
logger: opts.logger,
|
|
84
|
+
audit: opts.audit,
|
|
85
|
+
gateway: { host, port: opts.gatewayPort, advertiseHost: host === '0.0.0.0' ? undefined : host },
|
|
86
|
+
});
|
|
87
|
+
const releases = new Map();
|
|
88
|
+
const control = http.createServer(async (req, res) => {
|
|
89
|
+
const reject = (code, msg) => { res.writeHead(code, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: msg })); };
|
|
90
|
+
try {
|
|
91
|
+
const tok = bearer(req.headers.authorization);
|
|
92
|
+
if (!tok || !tokenEq(tok, opts.registerToken)) {
|
|
93
|
+
opts.audit?.('unauthorized', 'sidecar_register_reject', { reason: tok ? 'bad_token' : 'missing_token' });
|
|
94
|
+
return reject(401, 'unauthorized');
|
|
95
|
+
}
|
|
96
|
+
const path = req.url?.split('?')[0] ?? '';
|
|
97
|
+
// Edge registration (Caddy forward_auth sends GET, no body): identity comes from the
|
|
98
|
+
// X-Agent-User header the edge's preceding auth-broker hop verified and set. The nonce
|
|
99
|
+
// rides a response header so `copy_headers` can lift it onto the proxied request.
|
|
100
|
+
// sessionKey degrades to userId here — the edge never sees the body's session_id.
|
|
101
|
+
if ((req.method === 'GET' || req.method === 'POST') && path === '/edge/turn') {
|
|
102
|
+
const userId = (Array.isArray(req.headers['x-agent-user']) ? req.headers['x-agent-user'][0] : req.headers['x-agent-user'])?.trim();
|
|
103
|
+
if (!userId) {
|
|
104
|
+
opts.audit?.('unauthorized', 'sidecar_edge_reject', { reason: 'missing_user' });
|
|
105
|
+
return reject(400, 'X-Agent-User required');
|
|
106
|
+
}
|
|
107
|
+
const { nonce } = plane.registerTurnRaw({ userId, sessionKey: userId });
|
|
108
|
+
opts.audit?.(userId, 'sidecar_edge_register', { userId });
|
|
109
|
+
res.writeHead(200, { 'Content-Type': 'application/json', [gateway_1.TURN_NONCE_HEADER]: nonce });
|
|
110
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
111
|
+
}
|
|
112
|
+
if (req.method === 'POST' && path === '/turns') {
|
|
113
|
+
let body;
|
|
114
|
+
try {
|
|
115
|
+
body = JSON.parse(await (0, serve_hardening_1.readBodyWithCap)(req));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return reject(400, 'bad json');
|
|
119
|
+
}
|
|
120
|
+
const userId = (body.userId ?? '').trim();
|
|
121
|
+
if (!userId) {
|
|
122
|
+
return reject(400, 'userId required');
|
|
123
|
+
}
|
|
124
|
+
const { nonce, release } = plane.registerTurnRaw({
|
|
125
|
+
userId, sessionKey: (body.sessionKey ?? '').trim() || userId,
|
|
126
|
+
inboundToken: body.inboundToken, egressStoreKey: body.egressStoreKey,
|
|
127
|
+
});
|
|
128
|
+
releases.set(nonce, release);
|
|
129
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
130
|
+
return res.end(JSON.stringify({ nonce }));
|
|
131
|
+
}
|
|
132
|
+
if (req.method === 'DELETE' && path.startsWith('/turns/')) {
|
|
133
|
+
const nonce = decodeURIComponent(path.slice('/turns/'.length));
|
|
134
|
+
releases.get(nonce)?.();
|
|
135
|
+
releases.delete(nonce);
|
|
136
|
+
res.writeHead(204);
|
|
137
|
+
return res.end();
|
|
138
|
+
}
|
|
139
|
+
reject(404, 'not found');
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
opts.logger?.warn(`[Sidecar] control error: ${err instanceof Error ? err.message : String(err)}`);
|
|
143
|
+
if (!res.headersSent) {
|
|
144
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
145
|
+
}
|
|
146
|
+
res.end(JSON.stringify({ error: 'internal' }));
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
await new Promise((resolve, reject) => {
|
|
150
|
+
control.once('error', reject);
|
|
151
|
+
control.listen(opts.controlPort, host, () => resolve());
|
|
152
|
+
});
|
|
153
|
+
const controlAddress = control.address();
|
|
154
|
+
const controlPort = controlAddress && typeof controlAddress !== 'string' ? controlAddress.port : opts.controlPort;
|
|
155
|
+
opts.logger?.info(`[Sidecar] gateway ${plane.url} | control :${controlPort} (bound ${host})`);
|
|
156
|
+
return {
|
|
157
|
+
gatewayUrl: plane.url,
|
|
158
|
+
controlPort,
|
|
159
|
+
close: async () => { await new Promise((r) => control.close(() => r())); await plane.close(); },
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -7,28 +7,109 @@ exports.EDGE_AGENT_PORT = void 0;
|
|
|
7
7
|
exports.renderIdentityLayer = renderIdentityLayer;
|
|
8
8
|
exports.renderSidecarEdgeCaddyfile = renderSidecarEdgeCaddyfile;
|
|
9
9
|
exports.renderSidecarFedCaddyfile = renderSidecarFedCaddyfile;
|
|
10
|
+
/**
|
|
11
|
+
* Proof that `X-Auth-Request-Email` was set by THIS Caddy and not by a client.
|
|
12
|
+
*
|
|
13
|
+
* The broker now 401s an `X-Auth-Request-Email` that arrives without a matching
|
|
14
|
+
* `x-agenthippo-proxy-secret` (constant-time compared), because the header is otherwise a bearer
|
|
15
|
+
* assertion of identity that anything able to reach the broker's port could make. Only the routes
|
|
16
|
+
* that actually forward a verified email upstream carry it — adding it elsewhere would spread the
|
|
17
|
+
* secret across hops that have no use for it.
|
|
18
|
+
*
|
|
19
|
+
* `{env.…}` rather than `{$…}`: a runtime placeholder, so the secret is never baked into the
|
|
20
|
+
* rendered config on disk. Unset ⇒ empty ⇒ the broker refuses, which is the intended direction.
|
|
21
|
+
*/
|
|
22
|
+
const BROKER_PROXY_SECRET_LINES = [
|
|
23
|
+
' # Proves this hop is the trusted proxy — the broker 401s X-Auth-Request-Email without it.',
|
|
24
|
+
' header_up x-agenthippo-proxy-secret {env.AGENTHIPPO_AUTH_PROXY_SECRET}',
|
|
25
|
+
];
|
|
10
26
|
function renderIdentityLayer(opts) {
|
|
11
27
|
const { mode, authBrokerAddr, healthProxy, agentProxy } = opts;
|
|
12
28
|
const oauth2ProxyAddr = opts.oauth2ProxyAddr ?? '';
|
|
13
29
|
const catchallBody = opts.catchallBody ?? agentProxy;
|
|
30
|
+
const turn = opts.turnRegistration;
|
|
31
|
+
// The turn-registration hop, inserted AFTER the identity forward_auth (which sets
|
|
32
|
+
// X-Agent-User — the user the sidecar mints the nonce for). Only POST chat turns match.
|
|
33
|
+
const turnHop = turn
|
|
34
|
+
? [
|
|
35
|
+
' # Turn registration — after identity: X-Agent-User (set above) names the user the',
|
|
36
|
+
' # sidecar mints the nonce for. Only POST chat turns match; others pass through.',
|
|
37
|
+
` forward_auth @turnStart ${turn.connectorAddr} {`,
|
|
38
|
+
' uri /edge/turn',
|
|
39
|
+
' copy_headers X-Agent-Turn-Nonce',
|
|
40
|
+
` header_up Authorization "Bearer {$${turn.tokenEnvVar}}"`,
|
|
41
|
+
' }',
|
|
42
|
+
]
|
|
43
|
+
: [];
|
|
44
|
+
// NOTE: two forward_auth in one handle block do NOT run in source order — Caddy sorts by
|
|
45
|
+
// its directive table, so the turn-registration hop could fire before identity is
|
|
46
|
+
// established (the connector then gets no X-Agent-User → 400). Wrapping the sequence in
|
|
47
|
+
// `route {}` forces source order: identity first (unauth short-circuits), then turn
|
|
48
|
+
// registration, then proxy.
|
|
49
|
+
// Re-indent wrapped directives one level so the generated file stays readable — operators
|
|
50
|
+
// copy these snippets by hand onto the IdP host.
|
|
51
|
+
const indent = (lines) => lines.map((l) => (l.trim() ? ` ${l}` : l));
|
|
52
|
+
const gated = (identityGate, body) => turn
|
|
53
|
+
? [' route {', ...indent(identityGate), ...turnHop, ...indent(body), ' }']
|
|
54
|
+
: [...identityGate, ...body];
|
|
14
55
|
const lines = [];
|
|
56
|
+
if (turn) {
|
|
57
|
+
lines.push(' # Hygiene: never let a client-supplied nonce header through (copy_headers overwrites it', ' # on chat paths anyway, and a forged nonce fails closed at the gateway — belt-and-braces).', ' request_header -X-Agent-Turn-Nonce');
|
|
58
|
+
}
|
|
15
59
|
if (mode === 'self-issued') {
|
|
16
|
-
lines.push(' # IDE PKCE sign-in: broker requires X-Auth-Request-Email from oauth2-proxy. Must come', ' # before the generic /oauth/* handle so authorize is gated (not dumped straight to broker).', ' handle /oauth/authorize {', ` forward_auth ${oauth2ProxyAddr} {`, ' uri /oauth2/auth', ' copy_headers X-Auth-Request-Email', ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' @unauth status 401', ' handle_response @unauth {', ' redir * "/oauth2/sign_in?rd={http.request.uri}"', ' }', ' }', ` reverse_proxy ${authBrokerAddr} {`, ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' }', ' }', ' # Federated SSO (A′): browsers from other subdomains land here to stamp the shared cookie.', ' # Same oauth2-proxy gate as authorize; must precede the generic /oauth/* handle.', ' handle /oauth/browser-login {', ` forward_auth ${oauth2ProxyAddr} {`, ' uri /oauth2/auth', ' copy_headers X-Auth-Request-Email', ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' @unauth status 401', ' handle_response @unauth {', ' redir * "/oauth2/sign_in?rd={http.request.uri}"', ' }', ' }', ` reverse_proxy ${authBrokerAddr} {`, ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' }', ' }', ' # Auth-broker: token exchange + other /oauth/* routes that must stay ungated.', ' handle /oauth/* {', ` reverse_proxy ${authBrokerAddr} {`, ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' }', ' }');
|
|
60
|
+
lines.push(' # IDE PKCE sign-in: broker requires X-Auth-Request-Email from oauth2-proxy. Must come', ' # before the generic /oauth/* handle so authorize is gated (not dumped straight to broker).', ' handle /oauth/authorize {', ` forward_auth ${oauth2ProxyAddr} {`, ' uri /oauth2/auth', ' copy_headers X-Auth-Request-Email', ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' @unauth status 401', ' handle_response @unauth {', ' redir * "/oauth2/sign_in?rd={http.request.uri}"', ' }', ' }', ` reverse_proxy ${authBrokerAddr} {`, ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ...BROKER_PROXY_SECRET_LINES, ' }', ' }', ' # Federated SSO (A′): browsers from other subdomains land here to stamp the shared cookie.', ' # Same oauth2-proxy gate as authorize; must precede the generic /oauth/* handle.', ' handle /oauth/browser-login {', ` forward_auth ${oauth2ProxyAddr} {`, ' uri /oauth2/auth', ' copy_headers X-Auth-Request-Email', ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' @unauth status 401', ' handle_response @unauth {', ' redir * "/oauth2/sign_in?rd={http.request.uri}"', ' }', ' }', ` reverse_proxy ${authBrokerAddr} {`, ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ...BROKER_PROXY_SECRET_LINES, ' }', ' }', ' # Auth-broker: token exchange + other /oauth/* routes that must stay ungated.', ' handle /oauth/* {', ` reverse_proxy ${authBrokerAddr} {`, ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' }', ' }');
|
|
17
61
|
}
|
|
18
62
|
lines.push(' handle /api/ide/* {', ` reverse_proxy ${authBrokerAddr} {`, ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' }', ' }');
|
|
19
63
|
if (mode === 'self-issued') {
|
|
20
64
|
lines.push(' # oauth2-proxy: Google SSO callback + sign-in.', ' handle /oauth2/* {', ` reverse_proxy ${oauth2ProxyAddr} {`, ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' }', ' }');
|
|
21
65
|
}
|
|
22
|
-
lines.push(' @health path /health /ready', ' handle @health {', ...healthProxy, ' }'
|
|
66
|
+
lines.push(' @health path /health /ready', ' handle @health {', ...healthProxy, ' }');
|
|
67
|
+
if (turn) {
|
|
68
|
+
lines.push(' # Chat-turn matcher for edge turn registration (P1 fix): only POST chat turns register.', ' @turnStart {', ' method POST', ' path /v1/chat /v1/chat/stream', ' }');
|
|
69
|
+
}
|
|
70
|
+
lines.push(' # API clients (IDE) with a Bearer token: validate via auth-broker, copy identity header.', ' @apiWithAuth {', ' path /v1/*', ' header Authorization *', ' }', ' handle @apiWithAuth {', ...gated([
|
|
71
|
+
` forward_auth ${authBrokerAddr} {`,
|
|
72
|
+
' uri /api/ide/me',
|
|
73
|
+
' copy_headers X-Agent-User',
|
|
74
|
+
' header_up X-Forwarded-Proto https',
|
|
75
|
+
' header_up X-Forwarded-Host {http.request.host}',
|
|
76
|
+
' @unauth status 401',
|
|
77
|
+
' handle_response @unauth {',
|
|
78
|
+
' respond "Unauthorized" 401',
|
|
79
|
+
' }',
|
|
80
|
+
' }',
|
|
81
|
+
], agentProxy), ' }');
|
|
23
82
|
if (mode === 'self-issued') {
|
|
24
|
-
lines.push(' # Browser clients: SSO via oauth2-proxy, identity injected as X-Agent-User.', ' handle {',
|
|
83
|
+
lines.push(' # Browser clients: SSO via oauth2-proxy, identity injected as X-Agent-User.', ' handle {', ...gated([
|
|
84
|
+
` forward_auth ${oauth2ProxyAddr} {`,
|
|
85
|
+
' uri /oauth2/auth',
|
|
86
|
+
' copy_headers X-Auth-Request-Email>X-Agent-User',
|
|
87
|
+
' header_up X-Forwarded-Proto https',
|
|
88
|
+
' header_up X-Forwarded-Host {http.request.host}',
|
|
89
|
+
' @unauth status 401',
|
|
90
|
+
' handle_response @unauth {',
|
|
91
|
+
' redir * "/oauth2/sign_in?rd={http.request.uri}"',
|
|
92
|
+
' }',
|
|
93
|
+
' }',
|
|
94
|
+
], catchallBody), ' }');
|
|
25
95
|
}
|
|
26
96
|
else {
|
|
27
97
|
const idp = opts.idpUrl?.trim() || '';
|
|
28
98
|
const loginRedirect = idp
|
|
29
99
|
? ` redir * "${idp}/oauth/browser-login?return_to=https://{http.request.host}{http.request.uri}" 302`
|
|
30
100
|
: ' respond "Unauthorized" 401';
|
|
31
|
-
lines.push(' # Browser clients: the shared .agenthippo.ai cookie is verified by auth-broker. No cookie', ' # → redirect to the central IdP to log in (it stamps the cookie), then return.', ' handle {',
|
|
101
|
+
lines.push(' # Browser clients: the shared .agenthippo.ai cookie is verified by auth-broker. No cookie', ' # → redirect to the central IdP to log in (it stamps the cookie), then return.', ' handle {', ...gated([
|
|
102
|
+
` forward_auth ${authBrokerAddr} {`,
|
|
103
|
+
' uri /api/ide/me',
|
|
104
|
+
' copy_headers X-Agent-User',
|
|
105
|
+
' header_up X-Forwarded-Proto https',
|
|
106
|
+
' header_up X-Forwarded-Host {http.request.host}',
|
|
107
|
+
' @unauth status 401',
|
|
108
|
+
' handle_response @unauth {',
|
|
109
|
+
loginRedirect,
|
|
110
|
+
' }',
|
|
111
|
+
' }',
|
|
112
|
+
], catchallBody), ' }');
|
|
32
113
|
}
|
|
33
114
|
return lines;
|
|
34
115
|
}
|