@agenthippo-ai/agenthippo 0.2.3-dataplane → 1.110.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/out/agentpack/index.js +3 -1
  2. package/out/agentpack/paths.js +25 -2
  3. package/out/analytics/dashboard/localFleetAdapter.js +82 -2
  4. package/out/analytics/dashboard/provider.js +13 -2
  5. package/out/analytics/dashboard/webview/panel.js +3 -2
  6. package/out/analytics/identity.js +0 -0
  7. package/out/analytics/service.js +29 -25
  8. package/out/analytics/sessionEvents.js +100 -24
  9. package/out/auth/authBaseUrl.js +59 -0
  10. package/out/auth/bootstrapApply.js +9 -10
  11. package/out/auth/brokerClient.js +48 -14
  12. package/out/auth/provider.js +8 -21
  13. package/out/auth/sessionStore.js +3 -40
  14. package/out/auth/signInFlow.js +2 -1
  15. package/out/auth-broker/config.js +50 -5
  16. package/out/auth-broker/server.js +126 -26
  17. package/out/auth-broker/store.js +25 -24
  18. package/out/authority/warrant.js +206 -0
  19. package/out/branding.js +27 -1
  20. package/out/cli/commands/conformance.js +249 -0
  21. package/out/cli/commands/data-plane.js +151 -0
  22. package/out/cli/commands/deploy.js +1 -0
  23. package/out/cli/commands/gateway.js +4 -0
  24. package/out/cli/commands/pack.js +182 -3
  25. package/out/cli/commands/sandbox.js +351 -0
  26. package/out/cli/commands/serve.js +1145 -130
  27. package/out/cli/commands/store.js +12 -4
  28. package/out/cli/credential-context.js +17 -0
  29. package/out/cli/data-plane/gateway.js +28 -5
  30. package/out/cli/data-plane/index.js +18 -2
  31. package/out/cli/data-plane/registry.js +9 -0
  32. package/out/cli/data-plane/remote.js +106 -0
  33. package/out/cli/data-plane/sidecar.js +225 -0
  34. package/out/cli/deploy/layers/identity.js +88 -4
  35. package/out/cli/deploy-init.js +89 -24
  36. package/out/cli/deps.js +106 -2
  37. package/out/cli/idp-init.js +35 -0
  38. package/out/cli/index.js +171 -17
  39. package/out/cli/pack-install-ref.js +15 -0
  40. package/out/cli/pack-loader.js +8 -1
  41. package/out/cli/pack-runtime.js +61 -0
  42. package/out/cli/runtime.js +35 -1
  43. package/out/cli/serve-hardening.js +260 -14
  44. package/out/cli/utils.js +12 -1
  45. package/out/engines/base.js +32 -3
  46. package/out/engines/claude.js +33 -15
  47. package/out/engines/codex.js +111 -27
  48. package/out/engines/custom.js +15 -1
  49. package/out/evidence/sessionShip.js +643 -0
  50. package/out/extension.js +11 -2
  51. package/out/gateway/bridge.js +40 -2
  52. package/out/gateway/client.js +34 -1
  53. package/out/gateway/installSource.js +68 -0
  54. package/out/gateway/lifecycle.js +185 -51
  55. package/out/gateway/outboundPaths.js +4 -1
  56. package/out/gateway/secretsPreflight.js +90 -0
  57. package/out/gateway/service.js +7 -2
  58. package/out/headless/index.js +25 -2
  59. package/out/live-e2e/mcp-demo/server.mjs +32 -0
  60. package/out/mcp.js +30 -0
  61. package/out/pack/caps.js +271 -0
  62. package/out/pack/governor.js +326 -0
  63. package/out/pack/lease-schema.js +273 -0
  64. package/out/pack/lease.js +682 -0
  65. package/out/pack/reconcile.js +217 -0
  66. package/out/pack/signature.js +839 -0
  67. package/out/remote/handleRemoteChat.js +44 -3
  68. package/out/remote/remoteTrust.js +277 -0
  69. package/out/remote/serveClient.js +52 -5
  70. package/out/runner/host.js +141 -0
  71. package/out/runner/protocol.js +128 -0
  72. package/out/runner/remoteEngine.js +166 -0
  73. package/out/runtime/interfaces/edit-tracker.js +2 -0
  74. package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
  75. package/out/sandbox/containerDriver.js +572 -0
  76. package/out/sandbox/processDriver.js +318 -0
  77. package/out/sandbox/sandboxRunner.js +303 -0
  78. package/out/sandbox/support.js +301 -0
  79. package/out/sandbox/types.js +55 -0
  80. package/out/scripts/agentide-litellm.sh +67 -5
  81. package/out/scripts/agentide_litellm_hooks_v1.py +266 -21
  82. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +98 -132
  83. package/out/scripts/analytics/setup-analytics.ps1 +45 -14
  84. package/out/scripts/analytics/setup-analytics.sh +106 -15
  85. package/out/scripts/analytics/start-analytics.sh +86 -14
  86. package/out/scripts/gateway/setup-gateway.sh +72 -32
  87. package/out/shared/models.js +11 -110
  88. package/out/spotlight/install.js +7 -1
  89. package/out/store/index.js +41 -10
  90. package/out/sync.js +3 -1
  91. package/out/test/analytics.test.js +162 -23
  92. package/out/test/auth-baseurl.test.js +132 -0
  93. package/out/test/auth-bootstrapApply.test.js +27 -6
  94. package/out/test/auth-broker-hardening.test.js +227 -0
  95. package/out/test/auth-broker.test.js +59 -9
  96. package/out/test/branding.test.js +13 -0
  97. package/out/test/caps-governor.test.js +460 -0
  98. package/out/test/codex-new-file-survival.test.js +119 -0
  99. package/out/test/configuration-scopes.test.js +131 -0
  100. package/out/test/data-plane-engine-wiring.test.js +32 -0
  101. package/out/test/data-plane.test.js +380 -0
  102. package/out/test/deploy-init.test.js +47 -1
  103. package/out/test/gateway-client.test.js +28 -0
  104. package/out/test/gateway-install-source.test.js +111 -0
  105. package/out/test/gateway-lifecycle.test.js +1 -1
  106. package/out/test/gateway-outbound.test.js +7 -0
  107. package/out/test/gateway-secrets-preflight.test.js +127 -0
  108. package/out/test/gateway-session-routing.test.js +18 -0
  109. package/out/test/governor-hardening.test.js +222 -0
  110. package/out/test/headless-sync.test.js +22 -0
  111. package/out/test/lease-signature.test.js +357 -0
  112. package/out/test/lease-trust-root.test.js +273 -0
  113. package/out/test/lease-validation.test.js +224 -0
  114. package/out/test/lease.test.js +114 -0
  115. package/out/test/localFleetAdapter.test.js +91 -0
  116. package/out/test/mcp.test.js +27 -1
  117. package/out/test/pack-admission.test.js +216 -0
  118. package/out/test/pack-digest-parity.test.js +140 -0
  119. package/out/test/pack-reconcile.test.js +183 -0
  120. package/out/test/pack-signature-artifact.test.js +103 -0
  121. package/out/test/pack-signature.test.js +176 -0
  122. package/out/test/pack-trust-root.test.js +202 -0
  123. package/out/test/remote-trust.test.js +242 -0
  124. package/out/test/runner-protocol.test.js +249 -0
  125. package/out/test/sandbox-driver.test.js +553 -0
  126. package/out/test/sandbox-hardening.test.js +406 -0
  127. package/out/test/sandbox-runner.test.js +343 -0
  128. package/out/test/sandbox-scope.test.js +449 -0
  129. package/out/test/serve-cron-acceptance.test.js +370 -0
  130. package/out/test/serve-gateway-turn.test.js +122 -0
  131. package/out/test/serve-hardening.test.js +227 -15
  132. package/out/test/serveClient.test.js +6 -5
  133. package/out/test/sessionShip.test.js +618 -0
  134. package/out/test/spotlightPublish.test.js +28 -0
  135. package/out/test/store-install-safe.test.js +69 -0
  136. package/out/test/tls-bypass.test.js +90 -0
  137. package/out/test/warrant.test.js +138 -0
  138. package/out/utils.js +46 -9
  139. 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 = path.join(os.homedir(), branding_1.AGENTIDE_DIR_NAME, 'agents', slug, targetVersion);
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 = path.join(installDir, file.path);
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
- // For packs, create current.txt
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
  }
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.ON_DENIED_TIMEOUT_MS = exports.WARRANT_DENIED_REASON = void 0;
7
7
  exports.runWithCredential = runWithCredential;
8
8
  exports.currentCredential = currentCredential;
9
+ exports.resolveEngineHomeUserKey = resolveEngineHomeUserKey;
9
10
  exports.requireUserId = requireUserId;
10
11
  /**
11
12
  * Request-scoped credential context — the "Design B" seam from serve-v3 §11.1.
@@ -23,6 +24,7 @@ exports.requireUserId = requireUserId;
23
24
  * (secure-obo-design §2/§3 — the agent gets a capability, not the credential).
24
25
  */
25
26
  const async_hooks_1 = require("async_hooks");
27
+ const paths_1 = require("../agentpack/paths");
26
28
  /** Stable, queryable reason code for the one case runWithCredential rejects. */
27
29
  exports.WARRANT_DENIED_REASON = 'token_without_verified_user';
28
30
  /** Default bound on `onDenied` so a hung audit write cannot stall admission forever. */
@@ -78,6 +80,21 @@ async function runWithCredential(cred, fn, options) {
78
80
  function currentCredential() {
79
81
  return storage.getStore();
80
82
  }
83
+ /**
84
+ * The per-user engine-home segment for this turn, from whichever source actually has it.
85
+ *
86
+ * Two processes need the same answer and only one of them can read the credential. In serve the
87
+ * frame is authoritative. In a sandbox child there is no frame — that is the design, the
88
+ * credential never crosses — so the key arrives pre-computed on the run frame and is used as-is.
89
+ * The frame wins where both exist, so a child cannot widen its own scope by sending a key.
90
+ *
91
+ * Every engine must go through this rather than reading the credential directly: doing the latter
92
+ * is what silently unscoped every sandboxed turn, and the failure was invisible because the
93
+ * unscoped path is a perfectly good directory that simply holds everyone's data.
94
+ */
95
+ function resolveEngineHomeUserKey(fromRunFrame) {
96
+ return (0, paths_1.engineHomeUserKey)(storage.getStore()?.userId) ?? fromRunFrame?.trim() ?? undefined;
97
+ }
81
98
  /**
82
99
  * Fail-closed accessor for the acting user id. Throws when there is no
83
100
  * authenticated user in scope, so a tool can never silently run unscoped.
@@ -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
- // Loopback ONLY — the gateway must never be reachable off-box.
135
- server.listen(0, '127.0.0.1', () => resolve());
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 loopback port');
162
+ throw new Error('Data gateway failed to bind a port');
140
163
  }
141
- const url = `http://127.0.0.1:${address.port}/mcp`;
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.ASSERTION_HEADER = 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,18 @@ 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
+ Object.defineProperty(exports, "ASSERTION_HEADER", { enumerable: true, get: function () { return sidecar_1.ASSERTION_HEADER; } });
79
+ var remote_1 = require("./remote");
80
+ Object.defineProperty(exports, "RemoteDataPlane", { enumerable: true, get: function () { return remote_1.RemoteDataPlane; } });
81
+ Object.defineProperty(exports, "resolveRemoteDataPlane", { enumerable: true, get: function () { return remote_1.resolveRemoteDataPlane; } });
82
+ Object.defineProperty(exports, "EdgeDataPlane", { enumerable: true, get: function () { return remote_1.EdgeDataPlane; } });
83
+ Object.defineProperty(exports, "resolveEdgeDataPlane", { enumerable: true, get: function () { return remote_1.resolveEdgeDataPlane; } });
75
84
  /**
76
85
  * Resolve the pack's data-plane declaration. The connector path resolves against the PACK
77
86
  * SOURCE directory (operator-authored, broker trust domain) — not the agent-readable
@@ -108,7 +117,10 @@ class DataPlane {
108
117
  const broker = new broker_1.CredentialBroker((0, broker_1.resolveProvider)(cfg.provider));
109
118
  const connector = await connector_host_1.ConnectorHost.spawn({ connectorPath: cfg.connectorPath, logger: opts?.logger });
110
119
  try {
111
- const gateway = await (0, gateway_1.startDataGateway)({ registry, broker, connector, logger: opts?.logger, audit: opts?.audit });
120
+ const gateway = await (0, gateway_1.startDataGateway)({
121
+ registry, broker, connector, logger: opts?.logger, audit: opts?.audit,
122
+ host: opts?.gateway?.host, port: opts?.gateway?.port, advertiseHost: opts?.gateway?.advertiseHost,
123
+ });
112
124
  return new DataPlane(gateway.url, registry, broker, connector, gateway);
113
125
  }
114
126
  catch (err) {
@@ -116,6 +128,10 @@ class DataPlane {
116
128
  throw err;
117
129
  }
118
130
  }
131
+ /** Register a turn directly in the local registry (used by the sidecar control API). */
132
+ registerTurnRaw(reg) {
133
+ return this.registry.register(reg);
134
+ }
119
135
  /**
120
136
  * Register a verified turn. Returns the per-spawn env for the engine and the release()
121
137
  * 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,225 @@
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.ASSERTION_HEADER = void 0;
40
+ exports.startDataPlaneSidecar = startDataPlaneSidecar;
41
+ /**
42
+ * Data-plane SIDECAR (secure-obo-design §7.6 / aperture-thin) — the connector's own trust
43
+ * domain, a SEPARATE container from the engine/agent.
44
+ *
45
+ * It runs the full data plane (TurnRegistry + gateway + connector) and holds the backend BASE
46
+ * credential (AWS keys, Postgres pool login, …). The agent container gets NONE of it: it only
47
+ * (a) registers verified turns via this authenticated control API and (b) points the engine at
48
+ * the gateway URL. Because the sidecar is a different container (different PID namespace), the
49
+ * engine cannot read the base credential from `/proc/<pid>/environ`, and because the sidecar
50
+ * resolves the nonce → userId server-side, the engine cannot forge another user's identity.
51
+ *
52
+ * Control API (loopback/pod-network, bearer-authenticated):
53
+ * POST /turns { userId, sessionKey, inboundToken?, egressStoreKey? } → { nonce }
54
+ * DELETE /turns/:nonce → 204
55
+ * GET /edge/turn (forward_auth from the trusted EDGE; X-Agent-User header)
56
+ * → 200 + `X-Agent-Turn-Nonce` response header
57
+ * The gateway (nonce-authenticated MCP) is a separate port the engine dials directly.
58
+ *
59
+ * EDGE-registration mode (P1 fix, obo-inprogress §2): the bearer secret is held by the IdP
60
+ * Caddy — NOT the agent container. Caddy registers each chat turn via `forward_auth` against
61
+ * `/edge/turn` (identity from the X-Agent-User header its auth-broker hop just verified) and
62
+ * copies the returned nonce header onto the request it proxies to serve. serve then has no
63
+ * registration authority at all: a compromised engine reading serve's `/proc/<pid>/environ`
64
+ * finds no token that can mint a nonce for an arbitrary userId. Release rides the gateway's
65
+ * nonce-authenticated `DELETE /turn`; the registry deadline is the backstop.
66
+ */
67
+ const http = __importStar(require("http"));
68
+ const crypto_1 = require("crypto");
69
+ const index_1 = require("./index");
70
+ const serve_hardening_1 = require("../serve-hardening");
71
+ const gateway_1 = require("./gateway");
72
+ /**
73
+ * Header carrying the EDGE-verified user ASSERTION (agent-identity.md §8.5 G1) — the signed
74
+ * artifact, as opposed to `X-Agent-User` which is only the identifier. It becomes the turn's
75
+ * `inboundToken`, which is what `passthrough` hands the backend and what an RFC 8693 `exchange`
76
+ * provider presents as its `subject_token`.
77
+ *
78
+ * Set by the trusted edge on the turn-registration hop only, so it never reaches the agent
79
+ * container. A client-supplied copy must be stripped at the edge, exactly as for the nonce.
80
+ */
81
+ exports.ASSERTION_HEADER = 'x-agent-user-assertion';
82
+ function headerValue(req, name) {
83
+ const v = req.headers[name];
84
+ return (Array.isArray(v) ? v[0] : v)?.trim() || undefined;
85
+ }
86
+ /** Audit-safe fingerprint of a bearer credential — never the credential. */
87
+ function sha256Prefix(value) {
88
+ return (0, crypto_1.createHash)('sha256').update(value).digest('hex').slice(0, 12);
89
+ }
90
+ function bearer(header) {
91
+ return header?.match(/^Bearer\s+(\S+)$/i)?.[1];
92
+ }
93
+ function tokenEq(a, b) {
94
+ const x = Buffer.from(a);
95
+ const y = Buffer.from(b);
96
+ return x.length === y.length && (0, crypto_1.timingSafeEqual)(x, y);
97
+ }
98
+ async function startDataPlaneSidecar(opts) {
99
+ const host = opts.bindHost ?? '0.0.0.0';
100
+ // The gateway binds `host` on a fixed port so the engine can be told the URL at deploy time.
101
+ const plane = await index_1.DataPlane.start(opts.config, {
102
+ logger: opts.logger,
103
+ audit: opts.audit,
104
+ gateway: { host, port: opts.gatewayPort, advertiseHost: host === '0.0.0.0' ? undefined : host },
105
+ });
106
+ const releases = new Map();
107
+ const control = http.createServer(async (req, res) => {
108
+ const reject = (code, msg) => { res.writeHead(code, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: msg })); };
109
+ try {
110
+ const tok = bearer(req.headers.authorization);
111
+ if (!tok || !tokenEq(tok, opts.registerToken)) {
112
+ opts.audit?.('unauthorized', 'sidecar_register_reject', { reason: tok ? 'bad_token' : 'missing_token' });
113
+ return reject(401, 'unauthorized');
114
+ }
115
+ const path = req.url?.split('?')[0] ?? '';
116
+ // Edge registration (Caddy forward_auth sends GET, no body): identity comes from the
117
+ // X-Agent-User header the edge's preceding auth-broker hop verified and set. The nonce
118
+ // rides a response header so `copy_headers` can lift it onto the proxied request.
119
+ // sessionKey degrades to userId here — the edge never sees the body's session_id.
120
+ if ((req.method === 'GET' || req.method === 'POST') && path === '/edge/turn') {
121
+ const userId = (Array.isArray(req.headers['x-agent-user']) ? req.headers['x-agent-user'][0] : req.headers['x-agent-user'])?.trim();
122
+ if (!userId) {
123
+ opts.audit?.('unauthorized', 'sidecar_edge_reject', { reason: 'missing_user' });
124
+ return reject(400, 'X-Agent-User required');
125
+ }
126
+ // The user ASSERTION, not just the identifier (agent-identity.md §8.5 G1). The edge
127
+ // forwarded only X-Agent-User until now, so `registerTurnRaw` got no inboundToken and
128
+ // edge mode silently supported the `identity` provider alone: `passthrough` failed
129
+ // closed on every call, and an RFC 8693 `exchange` provider had no subject_token to
130
+ // exchange. Trusted exactly like X-Agent-User — only the in-network edge can reach
131
+ // this port, and the edge overwrites any client-supplied copy.
132
+ const inboundToken = headerValue(req, exports.ASSERTION_HEADER)?.replace(/^Bearer\s+/i, '').trim() || undefined;
133
+ const { nonce } = plane.registerTurnRaw({ userId, sessionKey: userId, inboundToken });
134
+ // Never the assertion itself: it is a bearer credential, and these rows ship to the
135
+ // evidence server. A hash prefix still proves which assertion a turn used.
136
+ opts.audit?.(userId, 'sidecar_edge_register', {
137
+ userId,
138
+ assertion: inboundToken ? `sha256:${sha256Prefix(inboundToken)}` : 'none',
139
+ });
140
+ res.writeHead(200, { 'Content-Type': 'application/json', [gateway_1.TURN_NONCE_HEADER]: nonce });
141
+ return res.end(JSON.stringify({ ok: true }));
142
+ }
143
+ if (req.method === 'POST' && path === '/turns') {
144
+ let body;
145
+ try {
146
+ body = JSON.parse(await (0, serve_hardening_1.readBodyWithCap)(req));
147
+ }
148
+ catch {
149
+ return reject(400, 'bad json');
150
+ }
151
+ const userId = (body.userId ?? '').trim();
152
+ if (!userId) {
153
+ return reject(400, 'userId required');
154
+ }
155
+ // Which agent SAYS it is asking. Recorded, never trusted: the register token is
156
+ // shared, so this claim is only as good as whoever holds it. `binding: 'asserted'`
157
+ // travels with it so nobody reading evidence later mistakes it for a verified fact
158
+ // (PLAN/agent-identity.md §4.4 — the successor is warrant verification, F3).
159
+ const claimed = body.agent?.pack?.trim();
160
+ // One connector is meant to serve one agent. When it does not, this is where it
161
+ // becomes visible — and visibility is the whole deliverable, since the sidecar
162
+ // cannot safely refuse on an unverifiable claim.
163
+ const mismatch = Boolean(claimed && opts.agent?.pack && claimed !== opts.agent.pack);
164
+ if (mismatch) {
165
+ // console.warn, not logger.warn: the logger is a no-op without --verbose, and two
166
+ // agents sharing one connector is the exact condition E2's agent bound rests on
167
+ // NOT happening. It must be unmissable in the default deployment, not a debug line.
168
+ console.warn(`Warning: turn registration claims agent "${claimed}" but this connector loaded ` +
169
+ `"${opts.agent?.pack}". One connector is meant to serve one agent ` +
170
+ `(PLAN/agent-identity.md §4.4) — both now share this connector's base credential, ` +
171
+ `so neither is bounded by the job it was deployed for.`);
172
+ }
173
+ opts.audit?.((body.sessionKey ?? '').trim() || userId, 'sidecar_register', {
174
+ userId,
175
+ claimedAgent: claimed ?? 'none',
176
+ binding: 'asserted',
177
+ ...(mismatch ? { agentMismatch: true, loadedAgent: opts.agent?.pack } : {}),
178
+ });
179
+ const { nonce, release } = plane.registerTurnRaw({
180
+ userId, sessionKey: (body.sessionKey ?? '').trim() || userId,
181
+ inboundToken: body.inboundToken, egressStoreKey: body.egressStoreKey,
182
+ });
183
+ releases.set(nonce, release);
184
+ res.writeHead(200, { 'Content-Type': 'application/json' });
185
+ return res.end(JSON.stringify({ nonce }));
186
+ }
187
+ if (req.method === 'DELETE' && path.startsWith('/turns/')) {
188
+ const nonce = decodeURIComponent(path.slice('/turns/'.length));
189
+ releases.get(nonce)?.();
190
+ releases.delete(nonce);
191
+ res.writeHead(204);
192
+ return res.end();
193
+ }
194
+ reject(404, 'not found');
195
+ }
196
+ catch (err) {
197
+ opts.logger?.warn(`[Sidecar] control error: ${err instanceof Error ? err.message : String(err)}`);
198
+ if (!res.headersSent) {
199
+ res.writeHead(500, { 'Content-Type': 'application/json' });
200
+ }
201
+ res.end(JSON.stringify({ error: 'internal' }));
202
+ }
203
+ });
204
+ await new Promise((resolve, reject) => {
205
+ control.once('error', reject);
206
+ control.listen(opts.controlPort, host, () => resolve());
207
+ });
208
+ const controlAddress = control.address();
209
+ const controlPort = controlAddress && typeof controlAddress !== 'string' ? controlAddress.port : opts.controlPort;
210
+ opts.logger?.info(`[Sidecar] gateway ${plane.url} | control :${controlPort} (bound ${host})`);
211
+ // A `passthrough` provider has nothing of its own: with no inbound token it fails closed on
212
+ // EVERY call (broker.ts). Before the assertion header existed, an edge-registered turn never
213
+ // carried one, so this combination looked healthy at boot and then denied every single tool
214
+ // call at runtime. Say it once, at boot, where an operator can act on it.
215
+ if ((opts.config.provider ?? 'passthrough').trim().toLowerCase() === 'passthrough') {
216
+ console.warn(`Warning: provider=passthrough needs a per-user token on every turn. Edge-registered turns ` +
217
+ `supply it via the ${exports.ASSERTION_HEADER} header — if your edge does not forward it, every tool ` +
218
+ `call will fail closed. See PLAN/agent-identity.md §8.5 G1.`);
219
+ }
220
+ return {
221
+ gatewayUrl: plane.url,
222
+ controlPort,
223
+ close: async () => { await new Promise((r) => control.close(() => r())); await plane.close(); },
224
+ };
225
+ }