@agenthippo-ai/agenthippo 0.1.0 → 0.2.3-dataplane

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/out/agentpack/index.js +2 -1
  2. package/out/agentpack/paths.js +23 -0
  3. package/out/analytics/dashboard/fleetDisplayUtils.js +3 -0
  4. package/out/analytics/dashboard/webview/panel.js +290 -80
  5. package/out/analytics/sessionEvents.js +21 -4
  6. package/out/auth/brokerClient.js +1 -1
  7. package/out/auth/onboardingPrompt.js +112 -0
  8. package/out/auth/provider.js +1 -1
  9. package/out/auth-broker/config.js +8 -3
  10. package/out/auth-broker/pkce.js +3 -1
  11. package/out/auth-broker/server.js +43 -1
  12. package/out/auth-broker/store.js +6 -5
  13. package/out/cli/commands/ask.js +3 -0
  14. package/out/cli/commands/chat.js +3 -0
  15. package/out/cli/commands/deploy.js +49 -10
  16. package/out/cli/commands/gateway.js +54 -0
  17. package/out/cli/commands/idp.js +85 -0
  18. package/out/cli/commands/serve.js +401 -49
  19. package/out/cli/credential-context.js +47 -69
  20. package/out/cli/data-plane/broker.js +78 -0
  21. package/out/cli/data-plane/connector-host.js +121 -0
  22. package/out/cli/data-plane/egress.js +189 -0
  23. package/out/cli/data-plane/gateway.js +195 -0
  24. package/out/cli/data-plane/index.js +137 -0
  25. package/out/cli/data-plane/registry.js +80 -0
  26. package/out/cli/deploy/blueprints/registry.js +84 -0
  27. package/out/cli/deploy/layers/identity.js +91 -0
  28. package/out/cli/deploy-init.js +1156 -285
  29. package/out/cli/idp-init.js +303 -0
  30. package/out/cli/index.js +46 -8
  31. package/out/cli/pack-loader.js +37 -2
  32. package/out/cli/runtime.js +10 -3
  33. package/out/cli/serve-hardening.js +217 -12
  34. package/out/cli/state-sync.js +318 -29
  35. package/out/engines/claude.js +26 -5
  36. package/out/engines/codex.js +24 -5
  37. package/out/engines/custom.js +46 -12
  38. package/out/extension.js +2 -0
  39. package/out/gateway/outboundSend.js +6 -6
  40. package/out/headless/index.js +24 -6
  41. package/out/live-probe/connector.mjs +8 -0
  42. package/out/live-probe/env-report.mjs +8 -0
  43. package/out/mcp.js +88 -61
  44. package/out/remote/handleRemoteChat.js +7 -1
  45. package/out/remote/serveClient.js +27 -13
  46. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +14 -0
  47. package/out/shared/models.js +1 -1
  48. package/out/shared/providers.js +5 -2
  49. package/out/test/analytics.test.js +93 -0
  50. package/out/test/auth-broker.test.js +59 -2
  51. package/out/test/blueprint-registry.test.js +105 -0
  52. package/out/test/credential-context.test.js +55 -0
  53. package/out/test/custom-env-layering.test.js +151 -0
  54. package/out/test/data-plane-engine-wiring.test.js +250 -0
  55. package/out/test/data-plane.test.js +244 -0
  56. package/out/test/deploy-init.test.js +287 -5
  57. package/out/test/egress-broker.test.js +172 -0
  58. package/out/test/fleetDisplayUtils.test.js +4 -0
  59. package/out/test/idp-init.test.js +151 -0
  60. package/out/test/mcp-obo.test.js +52 -27
  61. package/out/test/mcp.test.js +60 -4
  62. package/out/test/model-routing.test.js +133 -0
  63. package/out/test/serve-gateway-turn.test.js +267 -0
  64. package/out/test/serve-hardening.test.js +187 -0
  65. package/out/test/serveClient.test.js +37 -2
  66. package/out/test/state-sync-peer.test.js +83 -1
  67. package/out/utils.js +27 -11
  68. package/package.json +3 -4
@@ -0,0 +1,195 @@
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.TURN_NONCE_ENV = exports.DATA_MCP_URL_ENV = void 0;
40
+ exports.startDataGateway = startDataGateway;
41
+ /**
42
+ * Data-plane MCP gateway — a streamable-HTTP MCP server inside the serve (broker) process,
43
+ * bound to 127.0.0.1 (secure-obo-design §3.1/§3.2).
44
+ *
45
+ * Every request must present `Authorization: Bearer <turn nonce>`; the nonce resolves in
46
+ * the TurnRegistry to a verified {userId, sessionKey}. The credential is then selected
47
+ * SERVER-SIDE from the broker at call time (§5 step 7) — no parameter in the MCP request
48
+ * can name a user or a credential. Unknown/expired nonce ⇒ 401, fail closed.
49
+ *
50
+ * What nonce theft buys is documented in §3.3: loopback-only, turn-scoped, tool-surface-
51
+ * only access as the user the model was already acting for.
52
+ */
53
+ const http = __importStar(require("http"));
54
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
55
+ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
56
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
57
+ const serve_hardening_1 = require("../serve-hardening");
58
+ const registry_1 = require("./registry");
59
+ /** Loopback URL of the data-plane gateway; set per turn into the engine child env. */
60
+ exports.DATA_MCP_URL_ENV = 'AGENTHIPPO_DATA_MCP_URL';
61
+ /** The turn capability. Per-spawn env only; never written to any shared file. */
62
+ exports.TURN_NONCE_ENV = 'AGENTHIPPO_TURN_NONCE';
63
+ function bearerToken(header) {
64
+ const m = header?.match(/^Bearer\s+(\S+)$/i);
65
+ return m?.[1];
66
+ }
67
+ /** sha256 prefix for audit lines — never log a raw nonce (secure-obo-design §11). */
68
+ function noncePrefix(nonce) {
69
+ return nonce ? (0, registry_1.hashNonce)(nonce).slice(0, 12) : 'none';
70
+ }
71
+ async function startDataGateway(opts) {
72
+ const server = http.createServer(async (req, res) => {
73
+ try {
74
+ const urlPath = req.url?.split('?')[0] ?? '';
75
+ if (urlPath !== '/mcp') {
76
+ res.writeHead(404, { 'Content-Type': 'application/json' });
77
+ res.end(JSON.stringify({ error: 'not found' }));
78
+ return;
79
+ }
80
+ if (req.method !== 'POST') {
81
+ // Stateless transport: one POST per JSON-RPC exchange; no GET stream, no sessions.
82
+ res.writeHead(405, { 'Content-Type': 'application/json', 'Allow': 'POST' });
83
+ res.end(JSON.stringify({ error: 'method not allowed' }));
84
+ return;
85
+ }
86
+ const nonce = bearerToken(req.headers.authorization);
87
+ const turn = nonce ? opts.registry.resolve(nonce) : undefined;
88
+ if (!turn) {
89
+ // Expired / unknown / missing — a stale engine subprocess fails CLOSED here
90
+ // instead of ever using a stale user's credential (§3.2).
91
+ opts.audit?.('unauthorized', 'nonce_reject', {
92
+ reason: nonce ? 'unknown_or_expired' : 'missing',
93
+ nonce_sha256_prefix: noncePrefix(nonce),
94
+ });
95
+ res.writeHead(401, { 'Content-Type': 'application/json' });
96
+ res.end(JSON.stringify({
97
+ jsonrpc: '2.0',
98
+ error: { code: -32001, message: 'Unauthorized: missing, unknown, or expired turn nonce' },
99
+ id: null,
100
+ }));
101
+ return;
102
+ }
103
+ opts.logger?.info(`[DataPlane] authorized request user=${turn.userId} session=${turn.sessionKey}`);
104
+ let body;
105
+ try {
106
+ body = JSON.parse(await (0, serve_hardening_1.readBodyWithCap)(req));
107
+ }
108
+ catch {
109
+ res.writeHead(400, { 'Content-Type': 'application/json' });
110
+ res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32700, message: 'Parse error' }, id: null }));
111
+ return;
112
+ }
113
+ // Fresh stateless server+transport per request (SDK stateless pattern) so a request
114
+ // carries exactly its own turn's authority and nothing outlives the response.
115
+ const mcp = buildTurnScopedMcpServer(opts, turn);
116
+ const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
117
+ res.on('close', () => {
118
+ void transport.close();
119
+ void mcp.close();
120
+ });
121
+ await mcp.connect(transport);
122
+ await transport.handleRequest(req, res, body);
123
+ }
124
+ catch (err) {
125
+ opts.logger?.warn(`[DataPlane] gateway error: ${err instanceof Error ? err.message : String(err)}`);
126
+ if (!res.headersSent) {
127
+ res.writeHead(500, { 'Content-Type': 'application/json' });
128
+ }
129
+ res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal error' }, id: null }));
130
+ }
131
+ });
132
+ await new Promise((resolve, reject) => {
133
+ server.once('error', reject);
134
+ // Loopback ONLY — the gateway must never be reachable off-box.
135
+ server.listen(0, '127.0.0.1', () => resolve());
136
+ });
137
+ const address = server.address();
138
+ if (!address || typeof address === 'string') {
139
+ throw new Error('Data gateway failed to bind a loopback port');
140
+ }
141
+ const url = `http://127.0.0.1:${address.port}/mcp`;
142
+ opts.logger?.info(`[DataPlane] gateway listening at ${url}`);
143
+ return {
144
+ url,
145
+ close: () => new Promise((resolve) => server.close(() => resolve())),
146
+ };
147
+ }
148
+ function buildTurnScopedMcpServer(opts, turn) {
149
+ const server = new index_js_1.Server({ name: 'agenthippo-data-gateway', version: '1.0.0' }, { capabilities: { tools: {} } });
150
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
151
+ tools: await opts.connector.listTools(),
152
+ }));
153
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
154
+ const toolName = request.params.name;
155
+ // Server-side credential selection at call time (§5 step 7): the registry entry's
156
+ // verified userId picks the credential — never anything in the MCP request.
157
+ let secretRef;
158
+ try {
159
+ const credential = await opts.broker.acquire(turn.userId, turn.inboundToken);
160
+ secretRef = credential.secretRef;
161
+ opts.audit?.(turn.sessionKey, 'tool_authorize', { tool: toolName, userId: turn.userId, provider: opts.broker.provider.name });
162
+ }
163
+ catch (err) {
164
+ const message = err instanceof Error ? err.message : String(err);
165
+ opts.audit?.(turn.sessionKey, 'tool_deny', { tool: toolName, userId: turn.userId, reason: 'acquire_failed', error: message });
166
+ return {
167
+ content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `credential acquire failed (fail-closed): ${message}` }) }],
168
+ isError: true,
169
+ };
170
+ }
171
+ try {
172
+ return await opts.connector.callTool(toolName, request.params.arguments ?? {}, { userId: turn.userId, credential: secretRef });
173
+ }
174
+ catch (err) {
175
+ // Mid-turn credential expiry: drop the cache, re-acquire once, retry once (§5).
176
+ const message = err instanceof Error ? err.message : String(err);
177
+ if (/401|unauthoriz|expired/i.test(message)) {
178
+ opts.broker.invalidate(turn.userId);
179
+ try {
180
+ const retryCred = await opts.broker.acquire(turn.userId, turn.inboundToken);
181
+ return await opts.connector.callTool(toolName, request.params.arguments ?? {}, { userId: turn.userId, credential: retryCred.secretRef });
182
+ }
183
+ catch {
184
+ // fall through to the original error below
185
+ }
186
+ }
187
+ opts.audit?.(turn.sessionKey, 'tool_deny', { tool: toolName, userId: turn.userId, reason: 'call_failed', error: message });
188
+ return {
189
+ content: [{ type: 'text', text: JSON.stringify({ ok: false, error: message }) }],
190
+ isError: true,
191
+ };
192
+ }
193
+ });
194
+ return server;
195
+ }
@@ -0,0 +1,137 @@
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.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;
40
+ exports.resolveDataPlaneConfig = resolveDataPlaneConfig;
41
+ /**
42
+ * DataPlane — orchestration of the credential broker + data-plane gateway
43
+ * (secure-obo-design §3): TurnRegistry + CredentialBroker + broker-owned connector
44
+ * subprocess + loopback streamable-HTTP MCP gateway.
45
+ *
46
+ * serve starts one DataPlane at boot when the pack declares `spec.dataPlane`, then
47
+ * registers each verified turn to get `{ AGENTHIPPO_TURN_NONCE }` for the engine's
48
+ * per-spawn env. The pack's MCP config points at `${env:AGENTHIPPO_DATA_MCP_URL}` with
49
+ * `bearerTokenEnv: AGENTHIPPO_TURN_NONCE` — turn-invariant bytes, per-turn authority.
50
+ */
51
+ const path = __importStar(require("path"));
52
+ const registry_1 = require("./registry");
53
+ const broker_1 = require("./broker");
54
+ const connector_host_1 = require("./connector-host");
55
+ const gateway_1 = require("./gateway");
56
+ var registry_2 = require("./registry");
57
+ Object.defineProperty(exports, "TurnRegistry", { enumerable: true, get: function () { return registry_2.TurnRegistry; } });
58
+ Object.defineProperty(exports, "hashNonce", { enumerable: true, get: function () { return registry_2.hashNonce; } });
59
+ Object.defineProperty(exports, "DEFAULT_TURN_DEADLINE_MS", { enumerable: true, get: function () { return registry_2.DEFAULT_TURN_DEADLINE_MS; } });
60
+ var broker_2 = require("./broker");
61
+ Object.defineProperty(exports, "CredentialBroker", { enumerable: true, get: function () { return broker_2.CredentialBroker; } });
62
+ Object.defineProperty(exports, "resolveProvider", { enumerable: true, get: function () { return broker_2.resolveProvider; } });
63
+ Object.defineProperty(exports, "passthroughProvider", { enumerable: true, get: function () { return broker_2.passthroughProvider; } });
64
+ Object.defineProperty(exports, "identityProvider", { enumerable: true, get: function () { return broker_2.identityProvider; } });
65
+ var connector_host_2 = require("./connector-host");
66
+ Object.defineProperty(exports, "ConnectorHost", { enumerable: true, get: function () { return connector_host_2.ConnectorHost; } });
67
+ Object.defineProperty(exports, "TURN_META_KEY", { enumerable: true, get: function () { return connector_host_2.TURN_META_KEY; } });
68
+ var gateway_2 = require("./gateway");
69
+ Object.defineProperty(exports, "startDataGateway", { enumerable: true, get: function () { return gateway_2.startDataGateway; } });
70
+ Object.defineProperty(exports, "DATA_MCP_URL_ENV", { enumerable: true, get: function () { return gateway_2.DATA_MCP_URL_ENV; } });
71
+ Object.defineProperty(exports, "TURN_NONCE_ENV", { enumerable: true, get: function () { return gateway_2.TURN_NONCE_ENV; } });
72
+ var egress_1 = require("./egress");
73
+ Object.defineProperty(exports, "startEgressBroker", { enumerable: true, get: function () { return egress_1.startEgressBroker; } });
74
+ Object.defineProperty(exports, "EGRESS_URL_ENV", { enumerable: true, get: function () { return egress_1.EGRESS_URL_ENV; } });
75
+ /**
76
+ * Resolve the pack's data-plane declaration. The connector path resolves against the PACK
77
+ * SOURCE directory (operator-authored, broker trust domain) — not the agent-readable
78
+ * workspace copy.
79
+ */
80
+ function resolveDataPlaneConfig(spec, packPath) {
81
+ const dp = spec?.dataPlane;
82
+ const connector = dp?.connector?.trim();
83
+ if (!connector) {
84
+ return undefined;
85
+ }
86
+ return {
87
+ connectorPath: path.isAbsolute(connector) ? connector : path.join(packPath, connector),
88
+ provider: dp?.provider?.trim() || undefined,
89
+ };
90
+ }
91
+ class DataPlane {
92
+ url;
93
+ registry;
94
+ broker;
95
+ connector;
96
+ gateway;
97
+ constructor(url, registry, broker, connector, gateway) {
98
+ this.url = url;
99
+ this.registry = registry;
100
+ this.broker = broker;
101
+ this.connector = connector;
102
+ this.gateway = gateway;
103
+ }
104
+ static async start(cfg, opts) {
105
+ // Reuse serve's shared registry when given, so one nonce grants BOTH data and egress
106
+ // capabilities (secure-obo-design §7.6 step 2). Standalone (probe/tests) get their own.
107
+ const registry = opts?.registry ?? new registry_1.TurnRegistry();
108
+ const broker = new broker_1.CredentialBroker((0, broker_1.resolveProvider)(cfg.provider));
109
+ const connector = await connector_host_1.ConnectorHost.spawn({ connectorPath: cfg.connectorPath, logger: opts?.logger });
110
+ try {
111
+ const gateway = await (0, gateway_1.startDataGateway)({ registry, broker, connector, logger: opts?.logger, audit: opts?.audit });
112
+ return new DataPlane(gateway.url, registry, broker, connector, gateway);
113
+ }
114
+ catch (err) {
115
+ await connector.close();
116
+ throw err;
117
+ }
118
+ }
119
+ /**
120
+ * Register a verified turn. Returns the per-spawn env for the engine and the release()
121
+ * to call in the turn's `finally`. The gateway URL itself is boot-stable and rides
122
+ * process.env (see serve), keeping codex's config.toml turn-invariant.
123
+ */
124
+ registerTurn(reg) {
125
+ const { nonce, release } = this.registry.register(reg);
126
+ return {
127
+ nonce,
128
+ env: { [gateway_1.TURN_NONCE_ENV]: nonce, [gateway_1.DATA_MCP_URL_ENV]: this.url },
129
+ release,
130
+ };
131
+ }
132
+ async close() {
133
+ await this.gateway.close();
134
+ await this.connector.close();
135
+ }
136
+ }
137
+ exports.DataPlane = DataPlane;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) AgentHippo.ai. All rights reserved.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TurnRegistry = exports.DEFAULT_TURN_DEADLINE_MS = void 0;
7
+ exports.hashNonce = hashNonce;
8
+ /**
9
+ * TurnRegistry — the per-turn capability table of the data-plane gateway
10
+ * (secure-obo-design §3.1). Replaces the on-disk token file entirely.
11
+ *
12
+ * Per turn: mint a random 128-bit nonce, store the turn's verified identity under
13
+ * `sha256(nonce)` (the raw nonce is never stored or used as a lookup key, so lookups leak
14
+ * no timing signal about live nonces), hand the nonce to the engine via per-spawn env only,
15
+ * and delete the entry in the same `finally` that ends the turn. `deadline` is the backstop
16
+ * for a stranded entry — a leaked nonce dies on its own.
17
+ *
18
+ * The registry holds NO data credential: the gateway selects the credential server-side at
19
+ * call time from the broker's per-user cache (§5 step 7) — nothing a client sends can name
20
+ * a user or a credential.
21
+ */
22
+ const crypto_1 = require("crypto");
23
+ /** Backstop lifetime for a turn's nonce. Turns are seconds-to-minutes; 30 min is generous. */
24
+ exports.DEFAULT_TURN_DEADLINE_MS = 30 * 60_000;
25
+ function hashNonce(nonce) {
26
+ return (0, crypto_1.createHash)('sha256').update(nonce).digest('hex');
27
+ }
28
+ class TurnRegistry {
29
+ entries = new Map();
30
+ /**
31
+ * Mint a nonce and register the turn. Call `release()` in the turn's `finally`.
32
+ */
33
+ register(reg) {
34
+ this.prune();
35
+ // Never derived from anything — pure randomness (secure-obo-design §3.1).
36
+ const nonce = (0, crypto_1.randomBytes)(16).toString('hex');
37
+ const key = hashNonce(nonce);
38
+ this.entries.set(key, {
39
+ userId: reg.userId,
40
+ sessionKey: reg.sessionKey,
41
+ inboundToken: reg.inboundToken,
42
+ egressStoreKey: reg.egressStoreKey,
43
+ deadline: reg.deadline ?? Date.now() + exports.DEFAULT_TURN_DEADLINE_MS,
44
+ });
45
+ let released = false;
46
+ return {
47
+ nonce,
48
+ release: () => {
49
+ if (!released) {
50
+ released = true;
51
+ this.entries.delete(key);
52
+ }
53
+ },
54
+ };
55
+ }
56
+ /** Resolve a presented nonce to its turn, enforcing the deadline. Fail-closed: unknown/expired → undefined. */
57
+ resolve(nonce, now = Date.now()) {
58
+ const entry = this.entries.get(hashNonce(nonce));
59
+ if (!entry) {
60
+ return undefined;
61
+ }
62
+ if (now > entry.deadline) {
63
+ this.entries.delete(hashNonce(nonce));
64
+ return undefined;
65
+ }
66
+ return entry;
67
+ }
68
+ /** Drop expired entries (called on every register; cheap at realistic turn counts). */
69
+ prune(now = Date.now()) {
70
+ for (const [key, entry] of this.entries) {
71
+ if (now > entry.deadline) {
72
+ this.entries.delete(key);
73
+ }
74
+ }
75
+ }
76
+ get size() {
77
+ return this.entries.size;
78
+ }
79
+ }
80
+ exports.TurnRegistry = TurnRegistry;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) AgentHippo.ai. All rights reserved.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.BLUEPRINTS = exports.BLUEPRINT_IDS = void 0;
7
+ exports.isBlueprintId = isBlueprintId;
8
+ exports.resolveBlueprint = resolveBlueprint;
9
+ exports.BLUEPRINT_IDS = [
10
+ 'sandbox',
11
+ 'prospect-demo',
12
+ 'team-pilot',
13
+ 'regulated-air-gapped',
14
+ 'enterprise-fleet',
15
+ ];
16
+ function isBlueprintId(value) {
17
+ return exports.BLUEPRINT_IDS.includes(value);
18
+ }
19
+ exports.BLUEPRINTS = {
20
+ sandbox: {
21
+ id: 'sandbox',
22
+ description: 'Internal test, no external users — no edge, no SSO, no host port beyond localhost.',
23
+ targets: ['vm'],
24
+ },
25
+ 'prospect-demo': {
26
+ id: 'prospect-demo',
27
+ description: 'Throwaway public demo for a prospect — Cloudflare Tunnel + a throwaway Google OAuth client.',
28
+ targets: ['vm', 'compose'],
29
+ },
30
+ 'team-pilot': {
31
+ id: 'team-pilot',
32
+ description: 'Single-agent pilot. Rides the customer platform\'s own identity when one exists (GCP IAP, ' +
33
+ 'Databricks workspace SSO, Snowflake network, Cloudflare Access); falls back to a self-issued edge ' +
34
+ '(own Caddy + oauth2-proxy + Google client) on vm/compose/aws-ecs/azure-container-apps, which have ' +
35
+ 'no platform-native option yet.',
36
+ targets: ['cloudrun', 'databricks-app', 'spcs', 'cloudflare', 'vm', 'compose', 'aws-ecs', 'azure-container-apps'],
37
+ },
38
+ 'regulated-air-gapped': {
39
+ id: 'regulated-air-gapped',
40
+ description: 'VPN/localhost only, no tunnel, no external telemetry — the highest-scrutiny buyer profile. ' +
41
+ 'Currently generator-identical to sandbox (PLAN/deploy.md §2 gap); the distinction is buyer framing, ' +
42
+ 'not yet enforced posture.',
43
+ targets: ['vm'],
44
+ },
45
+ 'enterprise-fleet': {
46
+ id: 'enterprise-fleet',
47
+ description: 'Federated identity against a central IdP (enterprise-deploy/idp/) for a growing fleet of agents. ' +
48
+ 'Same-host colocation (no edge of its own) is available for vm/compose via the raw ' +
49
+ '`--ingress edge-fed --colocate` flag — not yet encoded as a blueprint variant.',
50
+ targets: ['cloudrun', 'vm', 'compose', 'aws-ecs', 'azure-container-apps'],
51
+ },
52
+ };
53
+ /**
54
+ * Resolve a blueprint (+ optional platform override) to the `--target`/`--ingress` pair
55
+ * `deploy-init.ts` already knows how to render. Throws on a platform not in the blueprint's
56
+ * supported set so a typo doesn't silently produce the wrong deployment shape.
57
+ */
58
+ function resolveBlueprint(id, platform) {
59
+ const def = exports.BLUEPRINTS[id];
60
+ const target = platform ?? def.targets[0];
61
+ if (!def.targets.includes(target)) {
62
+ throw new Error(`Blueprint "${id}" does not support --platform ${target}. ` +
63
+ `Supported: ${def.targets.join(', ')}.`);
64
+ }
65
+ switch (id) {
66
+ case 'sandbox':
67
+ case 'regulated-air-gapped':
68
+ return { target };
69
+ case 'prospect-demo':
70
+ return { target, ingress: 'edge' };
71
+ case 'enterprise-fleet':
72
+ return { target, ingress: 'edge-fed' };
73
+ case 'team-pilot':
74
+ // Platform-native identity where the platform actually has one; vm/compose have
75
+ // none, so they fall back to the self-issued edge (own Caddy + oauth2-proxy).
76
+ if (target === 'cloudrun') {
77
+ return { target, ingress: 'iap' };
78
+ }
79
+ if (target === 'databricks-app' || target === 'spcs' || target === 'cloudflare') {
80
+ return { target };
81
+ }
82
+ return { target, ingress: 'edge' };
83
+ }
84
+ }
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) AgentHippo.ai. All rights reserved.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.EDGE_AGENT_PORT = void 0;
7
+ exports.renderIdentityLayer = renderIdentityLayer;
8
+ exports.renderSidecarEdgeCaddyfile = renderSidecarEdgeCaddyfile;
9
+ exports.renderSidecarFedCaddyfile = renderSidecarFedCaddyfile;
10
+ function renderIdentityLayer(opts) {
11
+ const { mode, authBrokerAddr, healthProxy, agentProxy } = opts;
12
+ const oauth2ProxyAddr = opts.oauth2ProxyAddr ?? '';
13
+ const catchallBody = opts.catchallBody ?? agentProxy;
14
+ const lines = [];
15
+ 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}', ' }', ' }');
17
+ }
18
+ lines.push(' handle /api/ide/* {', ` reverse_proxy ${authBrokerAddr} {`, ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' }', ' }');
19
+ if (mode === 'self-issued') {
20
+ 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
+ }
22
+ lines.push(' @health path /health /ready', ' handle @health {', ...healthProxy, ' }', ' # API clients (IDE) with a Bearer token: validate via auth-broker, copy identity header.', ' @apiWithAuth {', ' path /v1/*', ' header Authorization *', ' }', ' handle @apiWithAuth {', ` forward_auth ${authBrokerAddr} {`, ' uri /api/ide/me', ' copy_headers X-Agent-User', ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' @unauth status 401', ' handle_response @unauth {', ' respond "Unauthorized" 401', ' }', ' }', ...agentProxy, ' }');
23
+ if (mode === 'self-issued') {
24
+ lines.push(' # Browser clients: SSO via oauth2-proxy, identity injected as X-Agent-User.', ' handle {', ` forward_auth ${oauth2ProxyAddr} {`, ' uri /oauth2/auth', ' copy_headers X-Auth-Request-Email>X-Agent-User', ' 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}"', ' }', ' }', ...catchallBody, ' }');
25
+ }
26
+ else {
27
+ const idp = opts.idpUrl?.trim() || '';
28
+ const loginRedirect = idp
29
+ ? ` redir * "${idp}/oauth/browser-login?return_to=https://{http.request.host}{http.request.uri}" 302`
30
+ : ' 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 {', ` forward_auth ${authBrokerAddr} {`, ' uri /api/ide/me', ' copy_headers X-Agent-User', ' header_up X-Forwarded-Proto https', ' header_up X-Forwarded-Host {http.request.host}', ' @unauth status 401', ' handle_response @unauth {', loginRedirect, ' }', ' }', ...catchallBody, ' }');
32
+ }
33
+ return lines;
34
+ }
35
+ /**
36
+ * "Sidecar" platforms — Cloud Run, ECS Fargate (`awsvpc` mode), Azure Container Apps (one
37
+ * replica) — all share one task's network namespace across containers and terminate TLS in
38
+ * front, so Caddy always listens on plain HTTP `:{$PORT}` and reaches every sidecar via
39
+ * `localhost:<port>` (never docker-DNS names). The agent isn't the ingress on these platforms,
40
+ * so it listens on a distinct internal port to avoid clashing with Caddy on `$PORT`.
41
+ */
42
+ exports.EDGE_AGENT_PORT = 8090;
43
+ /** Caddyfile for a sidecar platform's self-issued edge (own oauth2-proxy + Google client). */
44
+ function renderSidecarEdgeCaddyfile() {
45
+ return [
46
+ '# Sidecar-platform edge ingress. TLS terminates in front; Caddy listens on HTTP :{$PORT}.',
47
+ '# Containers share one task/pod\'s localhost → upstreams are 127.0.0.1:<port> (not docker-DNS names).',
48
+ '{',
49
+ ' auto_https off',
50
+ '}',
51
+ '',
52
+ ':{$PORT} {',
53
+ ...renderIdentityLayer({
54
+ mode: 'self-issued',
55
+ oauth2ProxyAddr: 'localhost:4180',
56
+ authBrokerAddr: 'localhost:9780',
57
+ healthProxy: [` reverse_proxy localhost:${exports.EDGE_AGENT_PORT}`],
58
+ agentProxy: [` reverse_proxy localhost:${exports.EDGE_AGENT_PORT}`],
59
+ }),
60
+ '}',
61
+ '',
62
+ ].join('\n');
63
+ }
64
+ /**
65
+ * A′ — federated sidecar-platform edge. No oauth2-proxy: Google login happens once at the
66
+ * central IdP (`idpUrl`); this agent only verifies the shared AgentHippo JWT (Bearer for IDE,
67
+ * cookie for browser). On a browser 401, redirect to the IdP's `/oauth/browser-login` to stamp
68
+ * the shared cookie, then return.
69
+ */
70
+ function renderSidecarFedCaddyfile(idpUrl) {
71
+ const idp = (idpUrl || '').trim().replace(/\/+$/, '');
72
+ return [
73
+ '# Sidecar-platform federated edge (A′). TLS terminates in front; Caddy listens on HTTP :{$PORT}.',
74
+ `# No oauth2-proxy — Google login happens once at the central IdP (${idp || '<IDP_URL>'});`,
75
+ '# this agent only verifies the shared AgentHippo JWT (Bearer for IDE, cookie for browser).',
76
+ '{',
77
+ ' auto_https off',
78
+ '}',
79
+ '',
80
+ ':{$PORT} {',
81
+ ...renderIdentityLayer({
82
+ mode: 'federated',
83
+ authBrokerAddr: 'localhost:9780',
84
+ idpUrl: idp,
85
+ healthProxy: [` reverse_proxy localhost:${exports.EDGE_AGENT_PORT}`],
86
+ agentProxy: [` reverse_proxy localhost:${exports.EDGE_AGENT_PORT}`],
87
+ }),
88
+ '}',
89
+ '',
90
+ ].join('\n');
91
+ }