@agenthippo-ai/agenthippo 0.1.1 → 1.110.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) 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/provider.js +13 -2
  5. package/out/analytics/dashboard/webview/panel.js +293 -82
  6. package/out/analytics/identity.js +0 -0
  7. package/out/analytics/service.js +23 -25
  8. package/out/analytics/sessionEvents.js +109 -22
  9. package/out/auth/authBaseUrl.js +59 -0
  10. package/out/auth/bootstrapApply.js +9 -10
  11. package/out/auth/brokerClient.js +49 -15
  12. package/out/auth/onboardingPrompt.js +112 -0
  13. package/out/auth/provider.js +9 -22
  14. package/out/auth/sessionStore.js +3 -40
  15. package/out/auth/signInFlow.js +2 -1
  16. package/out/auth-broker/config.js +54 -4
  17. package/out/auth-broker/server.js +152 -20
  18. package/out/auth-broker/store.js +25 -24
  19. package/out/authority/warrant.js +206 -0
  20. package/out/branding.js +27 -1
  21. package/out/cli/commands/ask.js +3 -0
  22. package/out/cli/commands/chat.js +3 -0
  23. package/out/cli/commands/conformance.js +249 -0
  24. package/out/cli/commands/data-plane.js +67 -0
  25. package/out/cli/commands/deploy.js +50 -10
  26. package/out/cli/commands/gateway.js +58 -0
  27. package/out/cli/commands/idp.js +85 -0
  28. package/out/cli/commands/pack.js +182 -3
  29. package/out/cli/commands/sandbox.js +332 -0
  30. package/out/cli/commands/serve.js +1165 -82
  31. package/out/cli/commands/store.js +12 -4
  32. package/out/cli/credential-context.js +42 -80
  33. package/out/cli/data-plane/broker.js +78 -0
  34. package/out/cli/data-plane/connector-host.js +121 -0
  35. package/out/cli/data-plane/egress.js +189 -0
  36. package/out/cli/data-plane/gateway.js +218 -0
  37. package/out/cli/data-plane/index.js +152 -0
  38. package/out/cli/data-plane/registry.js +89 -0
  39. package/out/cli/data-plane/remote.js +106 -0
  40. package/out/cli/data-plane/sidecar.js +161 -0
  41. package/out/cli/deploy/blueprints/registry.js +84 -0
  42. package/out/cli/deploy/layers/identity.js +172 -0
  43. package/out/cli/deploy-init.js +1182 -347
  44. package/out/cli/deps.js +106 -2
  45. package/out/cli/idp-init.js +338 -0
  46. package/out/cli/index.js +192 -23
  47. package/out/cli/pack-install-ref.js +15 -0
  48. package/out/cli/pack-loader.js +45 -3
  49. package/out/cli/pack-runtime.js +61 -0
  50. package/out/cli/runtime.js +44 -3
  51. package/out/cli/serve-hardening.js +139 -3
  52. package/out/cli/state-sync.js +16 -15
  53. package/out/cli/utils.js +12 -1
  54. package/out/engines/base.js +32 -3
  55. package/out/engines/claude.js +47 -8
  56. package/out/engines/codex.js +125 -22
  57. package/out/engines/custom.js +54 -12
  58. package/out/evidence/sessionShip.js +610 -0
  59. package/out/extension.js +13 -2
  60. package/out/gateway/bridge.js +40 -2
  61. package/out/gateway/client.js +34 -1
  62. package/out/gateway/installSource.js +68 -0
  63. package/out/gateway/lifecycle.js +185 -51
  64. package/out/gateway/outboundPaths.js +4 -1
  65. package/out/gateway/outboundSend.js +6 -6
  66. package/out/gateway/secretsPreflight.js +90 -0
  67. package/out/gateway/service.js +7 -2
  68. package/out/headless/index.js +40 -9
  69. package/out/live-e2e/mcp-demo/server.mjs +32 -0
  70. package/out/live-probe/connector.mjs +8 -0
  71. package/out/live-probe/env-report.mjs +8 -0
  72. package/out/mcp.js +115 -67
  73. package/out/pack/caps.js +271 -0
  74. package/out/pack/governor.js +309 -0
  75. package/out/pack/lease-schema.js +270 -0
  76. package/out/pack/lease.js +640 -0
  77. package/out/pack/reconcile.js +217 -0
  78. package/out/pack/signature.js +839 -0
  79. package/out/remote/handleRemoteChat.js +44 -3
  80. package/out/remote/remoteTrust.js +277 -0
  81. package/out/remote/serveClient.js +52 -5
  82. package/out/runner/host.js +131 -0
  83. package/out/runner/protocol.js +108 -0
  84. package/out/runner/remoteEngine.js +130 -0
  85. package/out/runtime/interfaces/edit-tracker.js +2 -0
  86. package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
  87. package/out/sandbox/containerDriver.js +570 -0
  88. package/out/sandbox/processDriver.js +315 -0
  89. package/out/sandbox/sandboxRunner.js +191 -0
  90. package/out/sandbox/support.js +301 -0
  91. package/out/sandbox/types.js +55 -0
  92. package/out/scripts/agentide-litellm.sh +67 -5
  93. package/out/scripts/agentide_litellm_hooks_v1.py +182 -21
  94. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +94 -118
  95. package/out/scripts/analytics/setup-analytics.ps1 +37 -13
  96. package/out/scripts/analytics/setup-analytics.sh +93 -13
  97. package/out/scripts/analytics/start-analytics.sh +86 -14
  98. package/out/scripts/gateway/setup-gateway.sh +72 -32
  99. package/out/shared/models.js +12 -111
  100. package/out/shared/providers.js +2 -2
  101. package/out/spotlight/install.js +7 -1
  102. package/out/store/index.js +41 -10
  103. package/out/sync.js +3 -1
  104. package/out/test/analytics.test.js +198 -34
  105. package/out/test/auth-baseurl.test.js +132 -0
  106. package/out/test/auth-bootstrapApply.test.js +27 -6
  107. package/out/test/auth-broker-hardening.test.js +227 -0
  108. package/out/test/auth-broker.test.js +112 -9
  109. package/out/test/blueprint-registry.test.js +105 -0
  110. package/out/test/branding.test.js +13 -0
  111. package/out/test/caps-governor.test.js +455 -0
  112. package/out/test/codex-new-file-survival.test.js +119 -0
  113. package/out/test/configuration-scopes.test.js +131 -0
  114. package/out/test/credential-context.test.js +23 -28
  115. package/out/test/custom-env-layering.test.js +151 -0
  116. package/out/test/data-plane-engine-wiring.test.js +250 -0
  117. package/out/test/data-plane.test.js +311 -0
  118. package/out/test/deploy-init.test.js +295 -4
  119. package/out/test/egress-broker.test.js +172 -0
  120. package/out/test/fleetDisplayUtils.test.js +4 -0
  121. package/out/test/gateway-client.test.js +28 -0
  122. package/out/test/gateway-install-source.test.js +111 -0
  123. package/out/test/gateway-lifecycle.test.js +1 -1
  124. package/out/test/gateway-outbound.test.js +7 -0
  125. package/out/test/gateway-secrets-preflight.test.js +127 -0
  126. package/out/test/gateway-session-routing.test.js +18 -0
  127. package/out/test/governor-hardening.test.js +217 -0
  128. package/out/test/headless-sync.test.js +22 -0
  129. package/out/test/idp-init.test.js +151 -0
  130. package/out/test/lease-signature.test.js +357 -0
  131. package/out/test/lease-trust-root.test.js +273 -0
  132. package/out/test/lease-validation.test.js +169 -0
  133. package/out/test/lease.test.js +114 -0
  134. package/out/test/mcp-obo.test.js +52 -27
  135. package/out/test/mcp.test.js +69 -3
  136. package/out/test/model-routing.test.js +133 -0
  137. package/out/test/pack-admission.test.js +216 -0
  138. package/out/test/pack-digest-parity.test.js +140 -0
  139. package/out/test/pack-reconcile.test.js +183 -0
  140. package/out/test/pack-signature-artifact.test.js +103 -0
  141. package/out/test/pack-signature.test.js +176 -0
  142. package/out/test/pack-trust-root.test.js +202 -0
  143. package/out/test/remote-trust.test.js +242 -0
  144. package/out/test/runner-protocol.test.js +249 -0
  145. package/out/test/sandbox-driver.test.js +553 -0
  146. package/out/test/sandbox-hardening.test.js +395 -0
  147. package/out/test/sandbox-runner.test.js +343 -0
  148. package/out/test/serve-gateway-turn.test.js +267 -0
  149. package/out/test/serve-hardening.test.js +125 -6
  150. package/out/test/serveClient.test.js +38 -5
  151. package/out/test/sessionShip.test.js +560 -0
  152. package/out/test/spotlightPublish.test.js +28 -0
  153. package/out/test/state-sync-peer.test.js +18 -5
  154. package/out/test/store-install-safe.test.js +69 -0
  155. package/out/test/tls-bypass.test.js +90 -0
  156. package/out/test/warrant.test.js +138 -0
  157. package/out/utils.js +73 -20
  158. package/package.json +1 -1
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) AgentHippo.ai. All rights reserved.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.parseStrictJsonConfig = parseStrictJsonConfig;
7
+ exports.collectMissingChannelEnvSecrets = collectMissingChannelEnvSecrets;
8
+ exports.disableChannelsInConfigText = disableChannelsInConfigText;
9
+ const MAX_WALK_DEPTH = 8;
10
+ function isRecord(value) {
11
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
12
+ }
13
+ /**
14
+ * Parse config content as strict JSON, or undefined when it does not parse.
15
+ *
16
+ * OpenClaw accepts JSON5 (the starter template has comments). A JSON5 file cannot be
17
+ * round-tripped through JSON.parse/stringify without losing comments, so callers must
18
+ * treat `undefined` as "hands off — detect nothing, rewrite nothing".
19
+ */
20
+ function parseStrictJsonConfig(content) {
21
+ try {
22
+ return JSON.parse(content);
23
+ }
24
+ catch {
25
+ return undefined;
26
+ }
27
+ }
28
+ function collectEnvRefs(value, out, depth) {
29
+ if (depth > MAX_WALK_DEPTH) {
30
+ return;
31
+ }
32
+ if (Array.isArray(value)) {
33
+ for (const item of value) {
34
+ collectEnvRefs(item, out, depth + 1);
35
+ }
36
+ return;
37
+ }
38
+ if (!isRecord(value)) {
39
+ return;
40
+ }
41
+ if (value.source === 'env' && typeof value.id === 'string' && value.id.trim()) {
42
+ out.add(value.id.trim());
43
+ return;
44
+ }
45
+ for (const child of Object.values(value)) {
46
+ collectEnvRefs(child, out, depth + 1);
47
+ }
48
+ }
49
+ /**
50
+ * List enabled channels whose env-sourced secret refs resolve to nothing in `env`.
51
+ *
52
+ * A channel counts as enabled unless it says `enabled: false` — a configured channel is
53
+ * intent, and the gateway will try (and fail) to start it either way. Literal token
54
+ * strings are not env refs and are never reported.
55
+ */
56
+ function collectMissingChannelEnvSecrets(config, env) {
57
+ if (!isRecord(config) || !isRecord(config.channels)) {
58
+ return [];
59
+ }
60
+ const result = [];
61
+ for (const [name, channel] of Object.entries(config.channels)) {
62
+ if (!isRecord(channel) || channel.enabled === false) {
63
+ continue;
64
+ }
65
+ const refs = new Set();
66
+ collectEnvRefs(channel, refs, 0);
67
+ const missing = [...refs].filter((id) => !env[id]?.trim()).sort();
68
+ if (missing.length) {
69
+ result.push({ channel: name, envVars: missing });
70
+ }
71
+ }
72
+ return result;
73
+ }
74
+ /**
75
+ * Return config text with the named channels set to `enabled: false`, or undefined when
76
+ * the content is not strict JSON (rewriting JSON5 would destroy comments).
77
+ */
78
+ function disableChannelsInConfigText(content, channels) {
79
+ const config = parseStrictJsonConfig(content);
80
+ if (!isRecord(config) || !isRecord(config.channels)) {
81
+ return undefined;
82
+ }
83
+ for (const name of channels) {
84
+ const channel = config.channels[name];
85
+ if (isRecord(channel)) {
86
+ channel.enabled = false;
87
+ }
88
+ }
89
+ return `${JSON.stringify(config, null, 2)}\n`;
90
+ }
@@ -46,10 +46,12 @@ exports.GatewayService = exports.GatewaySessionBindingError = void 0;
46
46
  * - Session routing: disk-backed route index + per-session gateway-session.json
47
47
  * - Publish: user connects an existing IDE session to the gateway
48
48
  */
49
+ const crypto_1 = require("crypto");
49
50
  const os = __importStar(require("os"));
50
51
  const path = __importStar(require("path"));
51
52
  const vscode = __importStar(require("vscode"));
52
53
  const scheduledFleetStorage_js_1 = require("../analytics/dashboard/scheduledFleetStorage.js");
54
+ const branding_js_1 = require("../branding.js");
53
55
  const bridge_js_1 = require("./bridge.js");
54
56
  const gateway_scope_errors_js_1 = require("./gateway-scope-errors.js");
55
57
  const gatewayRouteIndex_js_1 = require("./gatewayRouteIndex.js");
@@ -769,7 +771,10 @@ class GatewayService {
769
771
  */
770
772
  createSessionAwareChatUIHandler(agentId, packName) {
771
773
  return async (message, sessionKey, peerInfo, emitter, runContext) => {
772
- const gatewayRequestToken = `gw-${Date.now()}-${Math.random().toString(36).slice(2)}`;
774
+ // Unguessable: this token is a bearer capability for the run's output stream, and
775
+ // `getGatewayEmitter` is a destructive first-reader-wins read. A predictable value
776
+ // lets any chat message beginning with `[gw:...]` steal or drop another run's stream.
777
+ const gatewayRequestToken = `gw-${(0, crypto_1.randomUUID)()}`;
773
778
  GatewayService.gatewayEmitters.set(gatewayRequestToken, emitter);
774
779
  const markedMessage = `[gw:${gatewayRequestToken}]${message}`;
775
780
  try {
@@ -983,7 +988,7 @@ class GatewayService {
983
988
  const tokenRaw = (gwLive.get("token") ?? "").trim() || (this.config.token ?? "").trim();
984
989
  if (!url) {
985
990
  if (!silent) {
986
- vscode.window.showWarningMessage("Gateway URL not configured. Set agentide.gateway.url in settings.");
991
+ vscode.window.showWarningMessage(`Gateway URL not configured. Set ${(0, branding_js_1.toSettingsDisplayKey)("agentide.gateway.url")} in settings.`);
987
992
  }
988
993
  return;
989
994
  }
@@ -175,6 +175,8 @@ const session_1 = require("../session");
175
175
  const sync_1 = require("./sync");
176
176
  const sessionEvents_1 = require("../analytics/sessionEvents");
177
177
  const runtime_1 = require("../cli/runtime");
178
+ const credential_context_1 = require("../cli/credential-context");
179
+ const paths_1 = require("../utils/paths");
178
180
  function resolveHeadlessAnalyticsUserId(runOverride, configOverride) {
179
181
  const fromRun = runOverride?.trim();
180
182
  if (fromRun) {
@@ -223,13 +225,18 @@ function createHeadlessRunner(config) {
223
225
  // the legacy single file at provider-sessions.json on first load.
224
226
  (0, session_1.initSessionStoreWithStore)(new interfaces_1.DirectorySessionStore(path.join(config.storagePath, 'provider-sessions')));
225
227
  }
226
- // Build engine config
228
+ // Per-destination credentials. When a caller supplies the split keys (serve), use them so
229
+ // each engine picks the key for the endpoint it dials. Otherwise fall back to the single
230
+ // `apiKey` as the LiteLLM key (back-compat: eval/CLI runners configured one key).
231
+ const hasSplitKeys = Boolean(config.litellmApiKey || config.openaiApiKey || config.anthropicApiKey);
227
232
  const engineConfig = {
228
233
  storagePath: config.storagePath,
229
234
  codexHome: undefined,
230
235
  claudeConfigDir: undefined,
231
- litellmApiKey: config.apiKey,
236
+ litellmApiKey: config.litellmApiKey ?? (hasSplitKeys ? undefined : config.apiKey),
232
237
  litellmBaseUrl: config.baseUrl,
238
+ openaiApiKey: config.openaiApiKey,
239
+ anthropicApiKey: config.anthropicApiKey,
233
240
  claudeCodeExecutablePath: config.claudeCodeExecutablePath,
234
241
  codexExecutablePath: config.codexExecutablePath,
235
242
  };
@@ -283,11 +290,12 @@ function createHeadlessRunner(config) {
283
290
  }
284
291
  return customEngine;
285
292
  };
286
- // Shared session event writer for Layer 2 analytics. Rooted under storagePath (not the
287
- // os.homedir() default) so tool-call/warrant events land on the same durable volume as
288
- // session state — a container's home directory is ephemeral, storagePath honors
289
- // AGENTHIPPO_HOME / --state-dir and is what actually survives a restart or scale event.
290
- const sessionEventWriter = new sessionEvents_1.SessionEventWriter(path.join(config.storagePath, 'analytics', 'sessions'));
293
+ // Shared session event writer for Layer 2 analytics. Defaults to getAgentideHome() — the
294
+ // exact directory sessionJsonlFleet.ts and query-analytics.sh already scan, so tool-call/
295
+ // warrant events stay discoverable — but honors an explicit override (config.analyticsSessionsDir)
296
+ // so callers with their own state-dir precedence (serve.ts) don't regress durability when
297
+ // no AGENTHIPPO_HOME-style env var is set but a durable --state-dir was given explicitly.
298
+ const sessionEventWriter = new sessionEvents_1.SessionEventWriter(config.analyticsSessionsDir ?? path.join((0, paths_1.getAgentideHome)(), 'analytics', 'sessions'));
291
299
  return {
292
300
  async run(options) {
293
301
  const result = {
@@ -339,8 +347,21 @@ function createHeadlessRunner(config) {
339
347
  const requestId = (0, base_1.generateRequestId)();
340
348
  // Wrap emitter with observability (Layer 2 session events)
341
349
  // Session events are written to ~/.agent-hippo/analytics/sessions/<session_id>.jsonl
350
+ // Capture warrant userId here (inside serve's runWithCredential continuation), not
351
+ // later at tool emit — custom-engine IPC callbacks can lose/stick ambient ALS.
342
352
  const sessionId = options.sessionId ?? `headless-${Date.now()}`;
343
- const observableEmitter = (0, sessionEvents_1.wrapEmitterWithObservability)(baseEmitter, sessionEventWriter, sessionId, requestId);
353
+ const provenance = config.provenance?.();
354
+ const observableEmitter = (0, sessionEvents_1.wrapEmitterWithObservability)(baseEmitter, sessionEventWriter, sessionId, requestId, (0, credential_context_1.currentCredential)()?.userId, provenance && {
355
+ policy_version: provenance.policyVersion,
356
+ pack_digest: provenance.packDigest,
357
+ engine_version: provenance.engineVersion,
358
+ tier: provenance.tier,
359
+ // Layer 2 must be joinable to an instance ON ITS OWN. Without this the session
360
+ // lane could only be attributed by going through Layer 1 (the OTEL span), which
361
+ // defeats the point of self-stamping: with a shared collector spool, the
362
+ // shipper's INSTANCE_ID cannot know which instance actually ran a turn.
363
+ instance_id: provenance.instanceId,
364
+ });
344
365
  const requestOptions = {
345
366
  modelId: options.modelId,
346
367
  engineId: options.engineId,
@@ -348,11 +369,20 @@ function createHeadlessRunner(config) {
348
369
  additionalDirectories: options.additionalDirectories,
349
370
  sessionId,
350
371
  analyticsUserId: resolveHeadlessAnalyticsUserId(options.analyticsUserId, config.analyticsUserId),
351
- apiKey: config.apiKey,
372
+ // With split keys (serve), leave options.apiKey UNSET so resolveApiRouting selects
373
+ // the key for the engine's destination — passing the collapsed key here would let
374
+ // it win over the correct provider key (the original bug). Back-compat single-key
375
+ // callers keep options.apiKey.
376
+ apiKey: hasSplitKeys ? undefined : config.apiKey,
352
377
  baseUrl: config.baseUrl,
353
378
  useLiteLLM: config.useLiteLLM,
354
379
  agentPackId: packId,
355
380
  agentPackVersion: config.packVersion,
381
+ policyVersion: provenance?.policyVersion,
382
+ packDigest: provenance?.packDigest,
383
+ engineVersion: provenance?.engineVersion,
384
+ tier: provenance?.tier,
385
+ instanceId: provenance?.instanceId,
356
386
  permissions: options.permissions,
357
387
  requestId, // Same requestId used by ObservableStreamEmitter for Layer 1/2 correlation
358
388
  };
@@ -367,6 +397,7 @@ function createHeadlessRunner(config) {
367
397
  chatSessionId: sessionId,
368
398
  gatewaySessionKey: options.gatewaySessionKey,
369
399
  }),
400
+ ...(options.turnEnv ?? {}),
370
401
  },
371
402
  };
372
403
  try {
@@ -0,0 +1,32 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
+
5
+ const BALANCES = {
6
+ 'alice@corp.com': 4242,
7
+ 'bob@corp.com': 99,
8
+ };
9
+ const TURN = 'ai.agenthippo/turn';
10
+
11
+ const server = new Server({ name: 'e2e-demo', version: '0.0.1' }, { capabilities: { tools: {} } });
12
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
13
+ tools: [{
14
+ name: 'get_my_balance',
15
+ description: 'Return the signed-in user balance. No args.',
16
+ inputSchema: { type: 'object', properties: {} },
17
+ }],
18
+ }));
19
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
20
+ const turn = (req.params._meta ?? {})[TURN] ?? null;
21
+ const userId = typeof turn?.userId === 'string' ? turn.userId.trim() : '';
22
+ if (!userId) {
23
+ return { isError: true, content: [{ type: 'text', text: 'fail-closed: no verified user' }] };
24
+ }
25
+ const bal = BALANCES[userId];
26
+ if (bal === undefined) {
27
+ return { isError: true, content: [{ type: 'text', text: `fail-closed: no balance for ${userId}` }] };
28
+ }
29
+ console.error(`[e2e-connector] ${userId} → ${bal}`);
30
+ return { content: [{ type: 'text', text: JSON.stringify({ userId, balance: bal }) }] };
31
+ });
32
+ await server.connect(new StdioServerTransport());
@@ -0,0 +1,8 @@
1
+
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
+ const server = new Server({ name: 'probe', version: '0.0.1' }, { capabilities: { tools: {} } });
6
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [{ name: 'probe_tool', description: 'x', inputSchema: { type: 'object' } }] }));
7
+ server.setRequestHandler(CallToolRequestSchema, async () => ({ content: [{ type: 'text', text: 'MAGIC=zebra42' }] }));
8
+ await server.connect(new StdioServerTransport());
@@ -0,0 +1,8 @@
1
+
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
+ const server = new Server({ name: 'env-report', version: '0.0.1' }, { capabilities: { tools: {} } });
6
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [{ name: 'report_env', description: 'reports a probe env var', inputSchema: { type: 'object' } }] }));
7
+ server.setRequestHandler(CallToolRequestSchema, async () => ({ content: [{ type: 'text', text: 'PROBE_SEEN=' + (process.env.AGENTHIPPO_PROBE_ENV || 'ABSENT') }] }));
8
+ await server.connect(new StdioServerTransport());
package/out/mcp.js CHANGED
@@ -36,14 +36,15 @@ var __importStar = (this && this.__importStar) || (function () {
36
36
  };
37
37
  })();
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.AGENTHIPPO_MCP_SERVERS_ENV = exports.AGENTHIPPO_DATA_TOKEN_FILE_ENV = void 0;
39
+ exports.AGENTHIPPO_MCP_SERVERS_ENV = exports.ALLOW_INLINE_DATA_TOKEN_ENV = void 0;
40
40
  exports.loadMcpServers = loadMcpServers;
41
41
  exports.resolveMcpEnvVar = resolveMcpEnvVar;
42
- exports.mcpConfigCacheSignatureSuffix = mcpConfigCacheSignatureSuffix;
43
42
  exports.resolveMcpServerEnv = resolveMcpServerEnv;
44
43
  exports.generateCodexMcpConfig = generateCodexMcpConfig;
45
44
  exports.getDefaultMcpConfig = getDefaultMcpConfig;
46
45
  exports.loadBundledMcpServers = loadBundledMcpServers;
46
+ exports.resolveHttpMcpHeaders = resolveHttpMcpHeaders;
47
+ exports.referenceHttpMcpHeaders = referenceHttpMcpHeaders;
47
48
  exports.formatInlineMcpServers = formatInlineMcpServers;
48
49
  exports.serializeInlineMcpServers = serializeInlineMcpServers;
49
50
  exports.parseInlineMcpServers = parseInlineMcpServers;
@@ -64,11 +65,16 @@ const MCP_CONFIG_FILE = '.mcp.json';
64
65
  /** MCP servers subdirectory in base-prompts */
65
66
  const MCP_SERVERS_DIR = 'mcp-servers';
66
67
  /**
67
- * Env var passed to stdio MCP child processes: path to the per-turn data credential
68
- * file (OBO token, etc.). MCP servers read this file on each tool call — spawn env
69
- * is fixed at process start and cannot use AsyncLocalStorage.
68
+ * Explicit local-dev opt-in: allow the per-turn data credential to be resolved INLINE into
69
+ * MCP child env / `${credential:dataToken}` placeholders. Off by default — in any real
70
+ * deployment the credential must stay in the broker and reach tools only via the loopback
71
+ * data-plane gateway + turn nonce (secure-obo-design §2). Without this flag, a credential-
72
+ * backed value resolves to nothing and the pack's connector fails closed with its own error.
70
73
  */
71
- exports.AGENTHIPPO_DATA_TOKEN_FILE_ENV = 'AGENTHIPPO_DATA_TOKEN_FILE';
74
+ exports.ALLOW_INLINE_DATA_TOKEN_ENV = 'AGENTHIPPO_ALLOW_INLINE_DATA_TOKEN';
75
+ function inlineDataTokenAllowed() {
76
+ return /^(1|true)$/i.test(process.env[exports.ALLOW_INLINE_DATA_TOKEN_ENV]?.trim() ?? '');
77
+ }
72
78
  const ENV_PLACEHOLDER = /\$\{env:(\w+)\}/g;
73
79
  const CREDENTIAL_PLACEHOLDER = /\$\{credential:(\w+)\}/g;
74
80
  // -----------------------------------------------------------------------------
@@ -146,14 +152,23 @@ async function resolveServerEntry(name, entry, mcpDir, bundledMcpDir, envSources
146
152
  }
147
153
  // HTTP Streamable MCP server — no subprocess needed
148
154
  if (entry.url) {
155
+ // `${env:VAR}` in the URL resolves against the serve process env (e.g. the
156
+ // boot-stable AGENTHIPPO_DATA_MCP_URL of the loopback data-plane gateway).
157
+ const url = entry.url.replace(ENV_PLACEHOLDER, (_, varName) => resolvePlainEnvVar(varName, envSources)).trim();
158
+ if (!url) {
159
+ // Placeholder didn't resolve (e.g. gateway-backed entry loaded outside serve):
160
+ // drop the server rather than hand engines a broken empty-URL entry.
161
+ return null;
162
+ }
149
163
  return {
150
164
  name,
151
165
  label: entry.label ?? name,
152
166
  description: entry.description,
153
167
  command: '',
154
168
  args: [],
155
- url: entry.url,
169
+ url,
156
170
  headers: resolveHeaders(entry.headers, envSources),
171
+ bearerTokenEnv: entry.bearerTokenEnv?.trim() || undefined,
157
172
  };
158
173
  }
159
174
  // Custom stdio server - resolve relative paths from mcpDir
@@ -255,89 +270,63 @@ function resolveArgs(args, serverDir) {
255
270
  /**
256
271
  * Resolve a single ${env:VAR} reference for MCP child processes.
257
272
  *
258
- * The turn's OBO data token is exposed two ways:
273
+ * The turn's OBO data token is exposed two ways, BOTH gated behind the explicit
274
+ * local-dev opt-in {@link ALLOW_INLINE_DATA_TOKEN_ENV}:
259
275
  * - `${credential:dataToken}` placeholder (pack-author-facing; handled in resolveEnv).
260
- * - A configurable set of env-var aliases (default `DATABRICKS_TOKEN`) for packs that
261
- * reference the token via `${env:VAR}` directly. Override the alias list with the
262
- * `AGENTHIPPO_DATA_TOKEN_ENV_VARS` env var (comma-separated) — keeps this helper
263
- * vendor-neutral instead of hardcoding one provider's token name.
276
+ * - A configurable set of env-var aliases for packs that reference the token via
277
+ * `${env:VAR}` directly (`AGENTHIPPO_DATA_TOKEN_ENV_VARS`, comma-separated; empty
278
+ * by default — inline aliasing must be declared, never ambient).
264
279
  *
265
- * When a turn-scoped data token file exists, the token is NOT baked into spawn env —
266
- * MCP servers read it from AGENTHIPPO_DATA_TOKEN_FILE on each tool call instead.
280
+ * In a real deployment neither fires: the credential stays in the broker and tools reach
281
+ * it via the loopback data-plane gateway + turn nonce (secure-obo-design §3).
267
282
  */
268
283
  function resolveMcpEnvVar(varName, envSources = {}) {
269
- const cred = (0, credential_context_1.currentCredential)();
270
- const tokenFile = cred?.dataTokenFile;
271
284
  const aliases = getDataTokenEnvAliases();
272
- const isDataTokenVar = varName === 'dataToken' || aliases.includes(varName);
273
- if (tokenFile && isDataTokenVar) {
274
- // Token lives in the per-turn file (AGENTHIPPO_DATA_TOKEN_FILE); never bake the OBO token
275
- // into spawn env. The explicit ${credential:dataToken} placeholder resolves to empty
276
- // (AgentHippo-aware servers read the file). A plain ${env:ALIAS} reference falls through to
277
- // any operator-provided ambient value so a stock server that reads the var directly still
278
- // works, instead of being clobbered to an empty string.
279
- if (varName === 'dataToken') {
280
- return '';
281
- }
282
- return process.env[varName] ?? envSources[varName] ?? '';
283
- }
284
285
  if (varName === 'dataToken') {
285
- return cred?.dataToken?.trim() ?? '';
286
+ const tok = (0, credential_context_1.currentCredential)()?.dataToken?.trim();
287
+ return tok && inlineDataTokenAllowed() ? tok : '';
286
288
  }
287
- if (cred?.dataToken?.trim() && aliases.includes(varName)) {
288
- return cred.dataToken.trim();
289
+ if (aliases.includes(varName)) {
290
+ const tok = (0, credential_context_1.currentCredential)()?.dataToken?.trim();
291
+ if (tok && inlineDataTokenAllowed()) {
292
+ return tok;
293
+ }
289
294
  }
290
295
  return process.env[varName] ?? envSources[varName] ?? '';
291
296
  }
292
297
  /**
293
298
  * Env-var names that resolve to the turn's OBO data token when a pack references
294
- * `${env:VAR}` directly. Defaults to `DATABRICKS_TOKEN`; override at deploy time via
295
- * `AGENTHIPPO_DATA_TOKEN_ENV_VARS=VAR1,VAR2` (empty value → no aliases).
299
+ * `${env:VAR}` directly. Empty by default — set `AGENTHIPPO_DATA_TOKEN_ENV_VARS=VAR1,VAR2`
300
+ * (together with the inline opt-in) to enable. The old implicit `DATABRICKS_TOKEN` default
301
+ * was removed with the disk token path: inline credential exposure is now always explicit.
296
302
  */
297
303
  function getDataTokenEnvAliases() {
298
304
  const raw = process.env.AGENTHIPPO_DATA_TOKEN_ENV_VARS;
299
305
  if (raw !== undefined) {
300
306
  return raw.split(',').map((v) => v.trim()).filter(Boolean);
301
307
  }
302
- return ['DATABRICKS_TOKEN'];
308
+ return [];
303
309
  }
304
310
  function envValueUsesCredentialDataToken(value) {
305
311
  return /\$\{credential:dataToken\}/.test(value);
306
312
  }
307
- /** Suffix for Codex config.toml cache invalidation when MCP credential scope changes. */
308
- function mcpConfigCacheSignatureSuffix() {
309
- const cred = (0, credential_context_1.currentCredential)();
310
- const aliases = getDataTokenEnvAliases();
311
- if (cred?.dataTokenFile) {
312
- return `file:${cred.dataTokenFile}:aliases=${aliases.join(',')}`;
313
- }
314
- if (cred?.dataToken?.trim()) {
315
- return `inline-token:aliases=${aliases.join(',')}`;
316
- }
317
- return `no-token:aliases=${aliases.join(',')}`;
318
- }
319
313
  function resolveEnv(env, envSources = {}) {
320
314
  if (!env) {
321
315
  return undefined;
322
316
  }
323
317
  const resolved = {};
324
- const tokenFile = (0, credential_context_1.currentCredential)()?.dataTokenFile;
325
- if (tokenFile) {
326
- resolved[exports.AGENTHIPPO_DATA_TOKEN_FILE_ENV] = tokenFile;
327
- }
328
318
  for (const [key, value] of Object.entries(env)) {
329
319
  CREDENTIAL_PLACEHOLDER.lastIndex = 0;
330
320
  if (envValueUsesCredentialDataToken(value)) {
331
- // Credential-backed secret. When a per-turn token file exists, the key is intentionally
332
- // NOT set here: the MCP server reads the token from AGENTHIPPO_DATA_TOKEN_FILE
333
- // (injected above) on each tool call, so baking it into spawn env would be redundant
334
- // and would leak the turn-scoped token into the long-lived child process env.
335
- // Prefer the OBO token file, then inline token, then env fallback.
336
- if (tokenFile) {
337
- // AGENTHIPPO_DATA_TOKEN_FILE already injected above; don't bake the token here.
338
- }
339
- else if ((0, credential_context_1.currentCredential)()?.dataToken?.trim()) {
340
- resolved[key] = (0, credential_context_1.currentCredential)().dataToken.trim();
321
+ // Credential-backed secret. Baked into spawn env ONLY behind the explicit
322
+ // local-dev opt-in — the supported transport is the data-plane gateway, where
323
+ // the credential never leaves the broker process (secure-obo-design §2).
324
+ const tok = (0, credential_context_1.currentCredential)()?.dataToken?.trim();
325
+ if (tok) {
326
+ if (inlineDataTokenAllowed()) {
327
+ resolved[key] = tok;
328
+ }
329
+ // else: intentionally unset — the connector fails closed with its own error.
341
330
  }
342
331
  else {
343
332
  // No OBO credential (local dev / non-serve context): fall back to env var.
@@ -436,12 +425,14 @@ function generateCodexMcpConfig(servers, options) {
436
425
  if (servers.length === 0) {
437
426
  return '';
438
427
  }
439
- // `default_tools_approval_mode = "auto"` is the documented Codex 0.122+ per-server key
440
- // (values: auto/prompt/approve) that lets MCP tools run without an interactive prompt.
428
+ // `default_tools_approval_mode = "approve"` is the Codex per-server key that lets MCP
429
+ // tools run without an interactive prompt. The value enum is prompt|approve — "auto" is
430
+ // NOT valid: Codex ≥0.131 silently ignores it, defaults the server to `prompt`, and a
431
+ // headless `codex exec` turn then auto-Cancels the approval elicitation, surfacing as
432
+ // "user cancelled MCP tool call" on every tool call (verified live against 0.131.0).
441
433
  // Only emit it in headless/serve auto-approve contexts — the interactive IDE path leaves
442
- // it unset so users get Codex's normal approval prompts. The previous `approval_mode =
443
- // "never"` was an invalid key/value and was silently ignored by the Codex CLI.
444
- const approvalLine = options?.autoApprove ? 'default_tools_approval_mode = "auto"' : undefined;
434
+ // it unset so users get Codex's normal approval prompts.
435
+ const approvalLine = options?.autoApprove ? 'default_tools_approval_mode = "approve"' : undefined;
445
436
  const sections = servers.map(s => {
446
437
  // HTTP Streamable MCP server — codex connects via URL, no subprocess
447
438
  if (s.url) {
@@ -449,6 +440,13 @@ function generateCodexMcpConfig(servers, options) {
449
440
  `[mcp_servers.${s.name}]`,
450
441
  `url = ${JSON.stringify(s.url)}`,
451
442
  ];
443
+ if (s.bearerTokenEnv) {
444
+ // The auth token is read from the codex PROCESS env at request time
445
+ // (per-spawn codexEnv carries the turn nonce) — the config file itself
446
+ // stays byte-identical across turns, which is what removes the
447
+ // cross-user config.toml race (secure-obo-design §3.2).
448
+ urlLines.push(`bearer_token_env_var = ${JSON.stringify(s.bearerTokenEnv)}`);
449
+ }
452
450
  if (s.headers && Object.keys(s.headers).length > 0) {
453
451
  const headerEntries = Object.entries(s.headers)
454
452
  .map(([k, v]) => `${JSON.stringify(k)} = ${JSON.stringify(v)}`)
@@ -541,6 +539,52 @@ async function loadBundledMcpServers(basePromptsDir) {
541
539
  }
542
540
  /** Env var carrying JSON-serialized {@link InlineMcpServers} into custom engine workers. */
543
541
  exports.AGENTHIPPO_MCP_SERVERS_ENV = 'AGENTHIPPO_MCP_SERVERS';
542
+ /**
543
+ * Resolve the final HTTP headers for a URL-based MCP server, applying `bearerTokenEnv`
544
+ * against the ENGINE'S per-turn env (process.env + per-spawn patch). Engines that build
545
+ * their MCP config in memory per turn (claude, custom) call this with the same env they
546
+ * hand the engine subprocess, so the turn nonce lands in the request header without ever
547
+ * touching a shared file.
548
+ */
549
+ function resolveHttpMcpHeaders(server, turnEnv) {
550
+ const headers = { ...(server.headers ?? {}) };
551
+ if (server.bearerTokenEnv) {
552
+ const token = turnEnv[server.bearerTokenEnv]?.trim();
553
+ if (token) {
554
+ headers['Authorization'] = `Bearer ${token}`;
555
+ }
556
+ }
557
+ return Object.keys(headers).length > 0 ? headers : undefined;
558
+ }
559
+ /**
560
+ * The variable names the Claude CLI's `${NAME}` config expansion accepts. A `bearerTokenEnv`
561
+ * outside this shape would ship a header the CLI sends verbatim, so it falls back to in-memory
562
+ * resolution instead.
563
+ */
564
+ const CLI_EXPANDABLE_ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
565
+ /**
566
+ * Claude-engine variant of {@link resolveHttpMcpHeaders}: emit a `${NAME}` REFERENCE and let the
567
+ * CLI expand it from its own environment at connect time, instead of resolving the secret here.
568
+ *
569
+ * The Agent SDK serializes the whole mcpServers object into `--mcp-config <json>` argv, and
570
+ * /proc/<pid>/cmdline is world-readable — no ptrace gate, unlike environ — so a resolved
571
+ * Authorization header there is visible to every uid on the host and to execve-argv audit
572
+ * tooling. The reference keeps the turn nonce at environ-level exposure, which the engine child
573
+ * already carries for the egress CLI. Expansion verified live on CLI 2.1.220, including the
574
+ * failure mode this relies on: an UNSET name passes through literally and the data gateway
575
+ * rejects it — fail closed, never fail open.
576
+ */
577
+ function referenceHttpMcpHeaders(server, turnEnv) {
578
+ const name = server.bearerTokenEnv;
579
+ if (name && !CLI_EXPANDABLE_ENV_NAME.test(name)) {
580
+ return resolveHttpMcpHeaders(server, turnEnv);
581
+ }
582
+ const headers = { ...(server.headers ?? {}) };
583
+ if (name) {
584
+ headers['Authorization'] = `Bearer \${${name}}`;
585
+ }
586
+ return Object.keys(headers).length > 0 ? headers : undefined;
587
+ }
544
588
  /**
545
589
  * Convert resolved pack MCP servers to the inline record shape engines consume.
546
590
  *
@@ -548,15 +592,19 @@ exports.AGENTHIPPO_MCP_SERVERS_ENV = 'AGENTHIPPO_MCP_SERVERS';
548
592
  * `{ type: 'http', url, headers? }` — the Cursor SDK (`@cursor/sdk` McpServerConfig) accepts
549
593
  * this shape inline, so remote MCP servers are available to cursor/custom engines, not just
550
594
  * to the Codex engine (which gets them via config.toml).
595
+ *
596
+ * `turnEnv` (when provided) resolves each URL server's `bearerTokenEnv` into a concrete
597
+ * Authorization header — pass the same env the engine subprocess will receive.
551
598
  */
552
- function formatInlineMcpServers(servers) {
599
+ function formatInlineMcpServers(servers, turnEnv) {
553
600
  const mcpServers = {};
554
601
  for (const server of servers) {
555
602
  if (server.url) {
603
+ const headers = resolveHttpMcpHeaders(server, turnEnv ?? process.env);
556
604
  mcpServers[server.name] = {
557
605
  type: 'http',
558
606
  url: server.url,
559
- ...(server.headers && Object.keys(server.headers).length > 0 ? { headers: server.headers } : {}),
607
+ ...(headers ? { headers } : {}),
560
608
  };
561
609
  continue;
562
610
  }