@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.
- package/out/agentpack/index.js +2 -1
- package/out/agentpack/paths.js +23 -0
- package/out/analytics/dashboard/fleetDisplayUtils.js +3 -0
- package/out/analytics/dashboard/webview/panel.js +290 -80
- package/out/analytics/sessionEvents.js +21 -4
- package/out/auth/brokerClient.js +1 -1
- package/out/auth/onboardingPrompt.js +112 -0
- package/out/auth/provider.js +1 -1
- package/out/auth-broker/config.js +8 -3
- package/out/auth-broker/pkce.js +3 -1
- package/out/auth-broker/server.js +43 -1
- package/out/auth-broker/store.js +6 -5
- package/out/cli/commands/ask.js +3 -0
- package/out/cli/commands/chat.js +3 -0
- package/out/cli/commands/deploy.js +49 -10
- package/out/cli/commands/gateway.js +54 -0
- package/out/cli/commands/idp.js +85 -0
- package/out/cli/commands/serve.js +401 -49
- package/out/cli/credential-context.js +47 -69
- package/out/cli/data-plane/broker.js +78 -0
- package/out/cli/data-plane/connector-host.js +121 -0
- package/out/cli/data-plane/egress.js +189 -0
- package/out/cli/data-plane/gateway.js +195 -0
- package/out/cli/data-plane/index.js +137 -0
- package/out/cli/data-plane/registry.js +80 -0
- package/out/cli/deploy/blueprints/registry.js +84 -0
- package/out/cli/deploy/layers/identity.js +91 -0
- package/out/cli/deploy-init.js +1156 -285
- package/out/cli/idp-init.js +303 -0
- package/out/cli/index.js +46 -8
- package/out/cli/pack-loader.js +37 -2
- package/out/cli/runtime.js +10 -3
- package/out/cli/serve-hardening.js +217 -12
- package/out/cli/state-sync.js +318 -29
- package/out/engines/claude.js +26 -5
- package/out/engines/codex.js +24 -5
- package/out/engines/custom.js +46 -12
- package/out/extension.js +2 -0
- package/out/gateway/outboundSend.js +6 -6
- package/out/headless/index.js +24 -6
- package/out/live-probe/connector.mjs +8 -0
- package/out/live-probe/env-report.mjs +8 -0
- package/out/mcp.js +88 -61
- package/out/remote/handleRemoteChat.js +7 -1
- package/out/remote/serveClient.js +27 -13
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +14 -0
- package/out/shared/models.js +1 -1
- package/out/shared/providers.js +5 -2
- package/out/test/analytics.test.js +93 -0
- package/out/test/auth-broker.test.js +59 -2
- package/out/test/blueprint-registry.test.js +105 -0
- package/out/test/credential-context.test.js +55 -0
- package/out/test/custom-env-layering.test.js +151 -0
- package/out/test/data-plane-engine-wiring.test.js +250 -0
- package/out/test/data-plane.test.js +244 -0
- package/out/test/deploy-init.test.js +287 -5
- package/out/test/egress-broker.test.js +172 -0
- package/out/test/fleetDisplayUtils.test.js +4 -0
- package/out/test/idp-init.test.js +151 -0
- package/out/test/mcp-obo.test.js +52 -27
- package/out/test/mcp.test.js +60 -4
- package/out/test/model-routing.test.js +133 -0
- package/out/test/serve-gateway-turn.test.js +267 -0
- package/out/test/serve-hardening.test.js +187 -0
- package/out/test/serveClient.test.js +37 -2
- package/out/test/state-sync-peer.test.js +83 -1
- package/out/utils.js +27 -11
- package/package.json +3 -4
package/out/extension.js
CHANGED
|
@@ -68,6 +68,7 @@ const serveClient_js_1 = require("./remote/serveClient.js");
|
|
|
68
68
|
const remotePackStatusBar_js_1 = require("./remote/remotePackStatusBar.js");
|
|
69
69
|
const provider_js_1 = require("./auth/provider.js");
|
|
70
70
|
const signInFlow_js_1 = require("./auth/signInFlow.js");
|
|
71
|
+
const onboardingPrompt_js_1 = require("./auth/onboardingPrompt.js");
|
|
71
72
|
const githubStoreGate_js_1 = require("./auth/githubStoreGate.js");
|
|
72
73
|
const sessionEvents_1 = require("./analytics/sessionEvents");
|
|
73
74
|
const base_1 = require("./engines/base");
|
|
@@ -323,6 +324,7 @@ async function activate(context) {
|
|
|
323
324
|
(0, provider_js_1.registerAgentHippoAuth)(context);
|
|
324
325
|
void (0, provider_js_1.refreshBootstrapIfSignedIn)(context);
|
|
325
326
|
context.subscriptions.push((0, provider_js_1.startAgentHippoTokenRefreshTimer)(context));
|
|
327
|
+
(0, onboardingPrompt_js_1.scheduleSignInOnboardingPrompt)(context);
|
|
326
328
|
// Register commands
|
|
327
329
|
registerCommands(context, _state);
|
|
328
330
|
const remotePackStatusBar = new remotePackStatusBar_js_1.RemotePackStatusBar(syncService, () => getWorkspaceRoot(outputChannel).workspaceRoot, outputChannel);
|
|
@@ -172,12 +172,12 @@ function buildGatewaySendParams(handle, message, media) {
|
|
|
172
172
|
function buildGatewayRpcCommand(handle, message, media) {
|
|
173
173
|
return [exports.GATEWAY_OUTBOUND_RPC_METHOD, JSON.stringify(buildGatewaySendParams(handle, message, media))];
|
|
174
174
|
}
|
|
175
|
-
async function sendViaGatewayRpc(handle, message, media) {
|
|
175
|
+
async function sendViaGatewayRpc(handle, message, media, explicitToken) {
|
|
176
176
|
const gatewayUrl = process.env.OPENCLAW_GATEWAY_URL?.trim() || `ws://127.0.0.1:${DEFAULT_GATEWAY_PORT}`;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
// Prefer the broker-supplied token; the env re-population that used to happen here is gone
|
|
178
|
+
// deliberately — under the egress broker it would re-leak the token into process.env, and
|
|
179
|
+
// every engine child spreads process.env (secure-obo-design §7.6).
|
|
180
|
+
const token = explicitToken?.trim() || (0, runtime_js_1.readOpenClawGatewayToken)();
|
|
181
181
|
const { client, clientInfo } = (0, client_js_1.createEphemeralGatewayClient)(gatewayUrl, token);
|
|
182
182
|
await client.connect(clientInfo);
|
|
183
183
|
try {
|
|
@@ -218,7 +218,7 @@ async function sendGatewayOutboundMessage(handle, options) {
|
|
|
218
218
|
const cliCommand = [cli, ...cliArgs];
|
|
219
219
|
const rpcCommand = buildGatewayRpcCommand(handle, options.message, media);
|
|
220
220
|
try {
|
|
221
|
-
const rpc = await sendViaGatewayRpc(handle, options.message, media);
|
|
221
|
+
const rpc = await sendViaGatewayRpc(handle, options.message, media, options.token);
|
|
222
222
|
return {
|
|
223
223
|
ok: true,
|
|
224
224
|
via: 'gateway-rpc',
|
package/out/headless/index.js
CHANGED
|
@@ -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
|
-
//
|
|
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,8 +290,12 @@ function createHeadlessRunner(config) {
|
|
|
283
290
|
}
|
|
284
291
|
return customEngine;
|
|
285
292
|
};
|
|
286
|
-
// Shared session event writer for Layer 2 analytics
|
|
287
|
-
|
|
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'));
|
|
288
299
|
return {
|
|
289
300
|
async run(options) {
|
|
290
301
|
const result = {
|
|
@@ -336,8 +347,10 @@ function createHeadlessRunner(config) {
|
|
|
336
347
|
const requestId = (0, base_1.generateRequestId)();
|
|
337
348
|
// Wrap emitter with observability (Layer 2 session events)
|
|
338
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.
|
|
339
352
|
const sessionId = options.sessionId ?? `headless-${Date.now()}`;
|
|
340
|
-
const observableEmitter = (0, sessionEvents_1.wrapEmitterWithObservability)(baseEmitter, sessionEventWriter, sessionId, requestId);
|
|
353
|
+
const observableEmitter = (0, sessionEvents_1.wrapEmitterWithObservability)(baseEmitter, sessionEventWriter, sessionId, requestId, (0, credential_context_1.currentCredential)()?.userId);
|
|
341
354
|
const requestOptions = {
|
|
342
355
|
modelId: options.modelId,
|
|
343
356
|
engineId: options.engineId,
|
|
@@ -345,7 +358,11 @@ function createHeadlessRunner(config) {
|
|
|
345
358
|
additionalDirectories: options.additionalDirectories,
|
|
346
359
|
sessionId,
|
|
347
360
|
analyticsUserId: resolveHeadlessAnalyticsUserId(options.analyticsUserId, config.analyticsUserId),
|
|
348
|
-
|
|
361
|
+
// With split keys (serve), leave options.apiKey UNSET so resolveApiRouting selects
|
|
362
|
+
// the key for the engine's destination — passing the collapsed key here would let
|
|
363
|
+
// it win over the correct provider key (the original bug). Back-compat single-key
|
|
364
|
+
// callers keep options.apiKey.
|
|
365
|
+
apiKey: hasSplitKeys ? undefined : config.apiKey,
|
|
349
366
|
baseUrl: config.baseUrl,
|
|
350
367
|
useLiteLLM: config.useLiteLLM,
|
|
351
368
|
agentPackId: packId,
|
|
@@ -364,6 +381,7 @@ function createHeadlessRunner(config) {
|
|
|
364
381
|
chatSessionId: sessionId,
|
|
365
382
|
gatewaySessionKey: options.gatewaySessionKey,
|
|
366
383
|
}),
|
|
384
|
+
...(options.turnEnv ?? {}),
|
|
367
385
|
},
|
|
368
386
|
};
|
|
369
387
|
try {
|
|
@@ -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,14 @@ 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.
|
|
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
47
|
exports.formatInlineMcpServers = formatInlineMcpServers;
|
|
48
48
|
exports.serializeInlineMcpServers = serializeInlineMcpServers;
|
|
49
49
|
exports.parseInlineMcpServers = parseInlineMcpServers;
|
|
@@ -64,11 +64,16 @@ const MCP_CONFIG_FILE = '.mcp.json';
|
|
|
64
64
|
/** MCP servers subdirectory in base-prompts */
|
|
65
65
|
const MCP_SERVERS_DIR = 'mcp-servers';
|
|
66
66
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
67
|
+
* Explicit local-dev opt-in: allow the per-turn data credential to be resolved INLINE into
|
|
68
|
+
* MCP child env / `${credential:dataToken}` placeholders. Off by default — in any real
|
|
69
|
+
* deployment the credential must stay in the broker and reach tools only via the loopback
|
|
70
|
+
* data-plane gateway + turn nonce (secure-obo-design §2). Without this flag, a credential-
|
|
71
|
+
* backed value resolves to nothing and the pack's connector fails closed with its own error.
|
|
70
72
|
*/
|
|
71
|
-
exports.
|
|
73
|
+
exports.ALLOW_INLINE_DATA_TOKEN_ENV = 'AGENTHIPPO_ALLOW_INLINE_DATA_TOKEN';
|
|
74
|
+
function inlineDataTokenAllowed() {
|
|
75
|
+
return /^(1|true)$/i.test(process.env[exports.ALLOW_INLINE_DATA_TOKEN_ENV]?.trim() ?? '');
|
|
76
|
+
}
|
|
72
77
|
const ENV_PLACEHOLDER = /\$\{env:(\w+)\}/g;
|
|
73
78
|
const CREDENTIAL_PLACEHOLDER = /\$\{credential:(\w+)\}/g;
|
|
74
79
|
// -----------------------------------------------------------------------------
|
|
@@ -146,14 +151,23 @@ async function resolveServerEntry(name, entry, mcpDir, bundledMcpDir, envSources
|
|
|
146
151
|
}
|
|
147
152
|
// HTTP Streamable MCP server — no subprocess needed
|
|
148
153
|
if (entry.url) {
|
|
154
|
+
// `${env:VAR}` in the URL resolves against the serve process env (e.g. the
|
|
155
|
+
// boot-stable AGENTHIPPO_DATA_MCP_URL of the loopback data-plane gateway).
|
|
156
|
+
const url = entry.url.replace(ENV_PLACEHOLDER, (_, varName) => resolvePlainEnvVar(varName, envSources)).trim();
|
|
157
|
+
if (!url) {
|
|
158
|
+
// Placeholder didn't resolve (e.g. gateway-backed entry loaded outside serve):
|
|
159
|
+
// drop the server rather than hand engines a broken empty-URL entry.
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
149
162
|
return {
|
|
150
163
|
name,
|
|
151
164
|
label: entry.label ?? name,
|
|
152
165
|
description: entry.description,
|
|
153
166
|
command: '',
|
|
154
167
|
args: [],
|
|
155
|
-
url
|
|
168
|
+
url,
|
|
156
169
|
headers: resolveHeaders(entry.headers, envSources),
|
|
170
|
+
bearerTokenEnv: entry.bearerTokenEnv?.trim() || undefined,
|
|
157
171
|
};
|
|
158
172
|
}
|
|
159
173
|
// Custom stdio server - resolve relative paths from mcpDir
|
|
@@ -255,82 +269,63 @@ function resolveArgs(args, serverDir) {
|
|
|
255
269
|
/**
|
|
256
270
|
* Resolve a single ${env:VAR} reference for MCP child processes.
|
|
257
271
|
*
|
|
258
|
-
* The turn's OBO data token is exposed two ways
|
|
272
|
+
* The turn's OBO data token is exposed two ways, BOTH gated behind the explicit
|
|
273
|
+
* local-dev opt-in {@link ALLOW_INLINE_DATA_TOKEN_ENV}:
|
|
259
274
|
* - `${credential:dataToken}` placeholder (pack-author-facing; handled in resolveEnv).
|
|
260
|
-
* - A configurable set of env-var aliases
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
* vendor-neutral instead of hardcoding one provider's token name.
|
|
275
|
+
* - A configurable set of env-var aliases for packs that reference the token via
|
|
276
|
+
* `${env:VAR}` directly (`AGENTHIPPO_DATA_TOKEN_ENV_VARS`, comma-separated; empty
|
|
277
|
+
* by default — inline aliasing must be declared, never ambient).
|
|
264
278
|
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
279
|
+
* In a real deployment neither fires: the credential stays in the broker and tools reach
|
|
280
|
+
* it via the loopback data-plane gateway + turn nonce (secure-obo-design §3).
|
|
267
281
|
*/
|
|
268
282
|
function resolveMcpEnvVar(varName, envSources = {}) {
|
|
269
|
-
const cred = (0, credential_context_1.currentCredential)();
|
|
270
|
-
const tokenFile = cred?.dataTokenFile;
|
|
271
283
|
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); don't bake into spawn env.
|
|
275
|
-
return '';
|
|
276
|
-
}
|
|
277
284
|
if (varName === 'dataToken') {
|
|
278
|
-
|
|
285
|
+
const tok = (0, credential_context_1.currentCredential)()?.dataToken?.trim();
|
|
286
|
+
return tok && inlineDataTokenAllowed() ? tok : '';
|
|
279
287
|
}
|
|
280
|
-
if (
|
|
281
|
-
|
|
288
|
+
if (aliases.includes(varName)) {
|
|
289
|
+
const tok = (0, credential_context_1.currentCredential)()?.dataToken?.trim();
|
|
290
|
+
if (tok && inlineDataTokenAllowed()) {
|
|
291
|
+
return tok;
|
|
292
|
+
}
|
|
282
293
|
}
|
|
283
294
|
return process.env[varName] ?? envSources[varName] ?? '';
|
|
284
295
|
}
|
|
285
296
|
/**
|
|
286
297
|
* Env-var names that resolve to the turn's OBO data token when a pack references
|
|
287
|
-
* `${env:VAR}` directly.
|
|
288
|
-
*
|
|
298
|
+
* `${env:VAR}` directly. Empty by default — set `AGENTHIPPO_DATA_TOKEN_ENV_VARS=VAR1,VAR2`
|
|
299
|
+
* (together with the inline opt-in) to enable. The old implicit `DATABRICKS_TOKEN` default
|
|
300
|
+
* was removed with the disk token path: inline credential exposure is now always explicit.
|
|
289
301
|
*/
|
|
290
302
|
function getDataTokenEnvAliases() {
|
|
291
303
|
const raw = process.env.AGENTHIPPO_DATA_TOKEN_ENV_VARS;
|
|
292
304
|
if (raw !== undefined) {
|
|
293
305
|
return raw.split(',').map((v) => v.trim()).filter(Boolean);
|
|
294
306
|
}
|
|
295
|
-
return [
|
|
307
|
+
return [];
|
|
296
308
|
}
|
|
297
309
|
function envValueUsesCredentialDataToken(value) {
|
|
298
310
|
return /\$\{credential:dataToken\}/.test(value);
|
|
299
311
|
}
|
|
300
|
-
/** Suffix for Codex config.toml cache invalidation when MCP credential scope changes. */
|
|
301
|
-
function mcpConfigCacheSignatureSuffix() {
|
|
302
|
-
const cred = (0, credential_context_1.currentCredential)();
|
|
303
|
-
const aliases = getDataTokenEnvAliases();
|
|
304
|
-
if (cred?.dataTokenFile) {
|
|
305
|
-
return `file:${cred.dataTokenFile}:aliases=${aliases.join(',')}`;
|
|
306
|
-
}
|
|
307
|
-
if (cred?.dataToken?.trim()) {
|
|
308
|
-
return `inline-token:aliases=${aliases.join(',')}`;
|
|
309
|
-
}
|
|
310
|
-
return `no-token:aliases=${aliases.join(',')}`;
|
|
311
|
-
}
|
|
312
312
|
function resolveEnv(env, envSources = {}) {
|
|
313
313
|
if (!env) {
|
|
314
314
|
return undefined;
|
|
315
315
|
}
|
|
316
316
|
const resolved = {};
|
|
317
|
-
const tokenFile = (0, credential_context_1.currentCredential)()?.dataTokenFile;
|
|
318
|
-
if (tokenFile) {
|
|
319
|
-
resolved[exports.AGENTHIPPO_DATA_TOKEN_FILE_ENV] = tokenFile;
|
|
320
|
-
}
|
|
321
317
|
for (const [key, value] of Object.entries(env)) {
|
|
322
318
|
CREDENTIAL_PLACEHOLDER.lastIndex = 0;
|
|
323
319
|
if (envValueUsesCredentialDataToken(value)) {
|
|
324
|
-
// Credential-backed secret.
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
resolved[key] = (0, credential_context_1.currentCredential)().dataToken.trim();
|
|
320
|
+
// Credential-backed secret. Baked into spawn env ONLY behind the explicit
|
|
321
|
+
// local-dev opt-in — the supported transport is the data-plane gateway, where
|
|
322
|
+
// the credential never leaves the broker process (secure-obo-design §2).
|
|
323
|
+
const tok = (0, credential_context_1.currentCredential)()?.dataToken?.trim();
|
|
324
|
+
if (tok) {
|
|
325
|
+
if (inlineDataTokenAllowed()) {
|
|
326
|
+
resolved[key] = tok;
|
|
327
|
+
}
|
|
328
|
+
// else: intentionally unset — the connector fails closed with its own error.
|
|
334
329
|
}
|
|
335
330
|
else {
|
|
336
331
|
// No OBO credential (local dev / non-serve context): fall back to env var.
|
|
@@ -429,12 +424,14 @@ function generateCodexMcpConfig(servers, options) {
|
|
|
429
424
|
if (servers.length === 0) {
|
|
430
425
|
return '';
|
|
431
426
|
}
|
|
432
|
-
// `default_tools_approval_mode = "
|
|
433
|
-
//
|
|
427
|
+
// `default_tools_approval_mode = "approve"` is the Codex per-server key that lets MCP
|
|
428
|
+
// tools run without an interactive prompt. The value enum is prompt|approve — "auto" is
|
|
429
|
+
// NOT valid: Codex ≥0.131 silently ignores it, defaults the server to `prompt`, and a
|
|
430
|
+
// headless `codex exec` turn then auto-Cancels the approval elicitation, surfacing as
|
|
431
|
+
// "user cancelled MCP tool call" on every tool call (verified live against 0.131.0).
|
|
434
432
|
// Only emit it in headless/serve auto-approve contexts — the interactive IDE path leaves
|
|
435
|
-
// it unset so users get Codex's normal approval prompts.
|
|
436
|
-
|
|
437
|
-
const approvalLine = options?.autoApprove ? 'default_tools_approval_mode = "auto"' : undefined;
|
|
433
|
+
// it unset so users get Codex's normal approval prompts.
|
|
434
|
+
const approvalLine = options?.autoApprove ? 'default_tools_approval_mode = "approve"' : undefined;
|
|
438
435
|
const sections = servers.map(s => {
|
|
439
436
|
// HTTP Streamable MCP server — codex connects via URL, no subprocess
|
|
440
437
|
if (s.url) {
|
|
@@ -442,6 +439,13 @@ function generateCodexMcpConfig(servers, options) {
|
|
|
442
439
|
`[mcp_servers.${s.name}]`,
|
|
443
440
|
`url = ${JSON.stringify(s.url)}`,
|
|
444
441
|
];
|
|
442
|
+
if (s.bearerTokenEnv) {
|
|
443
|
+
// The auth token is read from the codex PROCESS env at request time
|
|
444
|
+
// (per-spawn codexEnv carries the turn nonce) — the config file itself
|
|
445
|
+
// stays byte-identical across turns, which is what removes the
|
|
446
|
+
// cross-user config.toml race (secure-obo-design §3.2).
|
|
447
|
+
urlLines.push(`bearer_token_env_var = ${JSON.stringify(s.bearerTokenEnv)}`);
|
|
448
|
+
}
|
|
445
449
|
if (s.headers && Object.keys(s.headers).length > 0) {
|
|
446
450
|
const headerEntries = Object.entries(s.headers)
|
|
447
451
|
.map(([k, v]) => `${JSON.stringify(k)} = ${JSON.stringify(v)}`)
|
|
@@ -465,7 +469,9 @@ function generateCodexMcpConfig(servers, options) {
|
|
|
465
469
|
// Use separate [mcp_servers.name.env] section — codex binary requires this form
|
|
466
470
|
lines.push('', `[mcp_servers.${s.name}.env]`);
|
|
467
471
|
for (const [k, v] of Object.entries(s.env)) {
|
|
468
|
-
|
|
472
|
+
// Quote the key: env-var names may contain '.', spaces, etc. that are not valid
|
|
473
|
+
// TOML bare keys (a bare `my.token` would parse as a nested table).
|
|
474
|
+
lines.push(`${JSON.stringify(k)} = ${JSON.stringify(v)}`);
|
|
469
475
|
}
|
|
470
476
|
}
|
|
471
477
|
return lines.join('\n');
|
|
@@ -532,6 +538,23 @@ async function loadBundledMcpServers(basePromptsDir) {
|
|
|
532
538
|
}
|
|
533
539
|
/** Env var carrying JSON-serialized {@link InlineMcpServers} into custom engine workers. */
|
|
534
540
|
exports.AGENTHIPPO_MCP_SERVERS_ENV = 'AGENTHIPPO_MCP_SERVERS';
|
|
541
|
+
/**
|
|
542
|
+
* Resolve the final HTTP headers for a URL-based MCP server, applying `bearerTokenEnv`
|
|
543
|
+
* against the ENGINE'S per-turn env (process.env + per-spawn patch). Engines that build
|
|
544
|
+
* their MCP config in memory per turn (claude, custom) call this with the same env they
|
|
545
|
+
* hand the engine subprocess, so the turn nonce lands in the request header without ever
|
|
546
|
+
* touching a shared file.
|
|
547
|
+
*/
|
|
548
|
+
function resolveHttpMcpHeaders(server, turnEnv) {
|
|
549
|
+
const headers = { ...(server.headers ?? {}) };
|
|
550
|
+
if (server.bearerTokenEnv) {
|
|
551
|
+
const token = turnEnv[server.bearerTokenEnv]?.trim();
|
|
552
|
+
if (token) {
|
|
553
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return Object.keys(headers).length > 0 ? headers : undefined;
|
|
557
|
+
}
|
|
535
558
|
/**
|
|
536
559
|
* Convert resolved pack MCP servers to the inline record shape engines consume.
|
|
537
560
|
*
|
|
@@ -539,15 +562,19 @@ exports.AGENTHIPPO_MCP_SERVERS_ENV = 'AGENTHIPPO_MCP_SERVERS';
|
|
|
539
562
|
* `{ type: 'http', url, headers? }` — the Cursor SDK (`@cursor/sdk` McpServerConfig) accepts
|
|
540
563
|
* this shape inline, so remote MCP servers are available to cursor/custom engines, not just
|
|
541
564
|
* to the Codex engine (which gets them via config.toml).
|
|
565
|
+
*
|
|
566
|
+
* `turnEnv` (when provided) resolves each URL server's `bearerTokenEnv` into a concrete
|
|
567
|
+
* Authorization header — pass the same env the engine subprocess will receive.
|
|
542
568
|
*/
|
|
543
|
-
function formatInlineMcpServers(servers) {
|
|
569
|
+
function formatInlineMcpServers(servers, turnEnv) {
|
|
544
570
|
const mcpServers = {};
|
|
545
571
|
for (const server of servers) {
|
|
546
572
|
if (server.url) {
|
|
573
|
+
const headers = resolveHttpMcpHeaders(server, turnEnv ?? process.env);
|
|
547
574
|
mcpServers[server.name] = {
|
|
548
575
|
type: 'http',
|
|
549
576
|
url: server.url,
|
|
550
|
-
...(
|
|
577
|
+
...(headers ? { headers } : {}),
|
|
551
578
|
};
|
|
552
579
|
continue;
|
|
553
580
|
}
|
|
@@ -96,13 +96,19 @@ async function handleRemoteServeChat(deps, args) {
|
|
|
96
96
|
try {
|
|
97
97
|
syncService.incrementActiveAgentTurns();
|
|
98
98
|
await observableEmitter.emitTurnStart();
|
|
99
|
-
await (0, serveClient_js_1.streamRemoteServeChat)({
|
|
99
|
+
const result = await (0, serveClient_js_1.streamRemoteServeChat)({
|
|
100
100
|
baseUrl: remoteUrl,
|
|
101
101
|
message,
|
|
102
102
|
sessionId: remoteSessionId,
|
|
103
103
|
signal: abortController.signal,
|
|
104
104
|
auth,
|
|
105
105
|
}, observableEmitter);
|
|
106
|
+
if (!result.ok) {
|
|
107
|
+
// streamRemoteServeChat already surfaced the failure to the emitter (error + done);
|
|
108
|
+
// record the turn as failed and propagate to the caller without re-emitting.
|
|
109
|
+
await observableEmitter.emitTurnEnd(false);
|
|
110
|
+
return { error: result.error ?? 'Remote serve failed' };
|
|
111
|
+
}
|
|
106
112
|
await observableEmitter.emitTurnEnd(true);
|
|
107
113
|
return { streaming: true };
|
|
108
114
|
}
|
|
@@ -77,7 +77,7 @@ async function streamRemoteServeChat(options, emitter) {
|
|
|
77
77
|
const message = err instanceof Error ? err.message : 'Remote serve connection failed';
|
|
78
78
|
await emitter.error(message);
|
|
79
79
|
await emitter.done();
|
|
80
|
-
return;
|
|
80
|
+
return { ok: false, error: message };
|
|
81
81
|
}
|
|
82
82
|
if (!response.ok) {
|
|
83
83
|
clearTimers();
|
|
@@ -93,29 +93,39 @@ async function streamRemoteServeChat(options, emitter) {
|
|
|
93
93
|
}
|
|
94
94
|
await emitter.error(errorMessage);
|
|
95
95
|
await emitter.done();
|
|
96
|
-
return;
|
|
96
|
+
return { ok: false, error: errorMessage };
|
|
97
97
|
}
|
|
98
98
|
if (!response.body) {
|
|
99
99
|
clearTimers();
|
|
100
|
-
|
|
100
|
+
const message = 'Remote serve returned empty body';
|
|
101
|
+
await emitter.error(message);
|
|
101
102
|
await emitter.done();
|
|
102
|
-
return;
|
|
103
|
+
return { ok: false, error: message };
|
|
103
104
|
}
|
|
104
105
|
try {
|
|
105
|
-
await consumeServeSse(response.body, emitter, resetIdle);
|
|
106
|
+
const streamError = await consumeServeSse(response.body, emitter, resetIdle);
|
|
107
|
+
return streamError ? { ok: false, error: streamError } : { ok: true };
|
|
106
108
|
}
|
|
107
109
|
finally {
|
|
108
110
|
clearTimers();
|
|
109
111
|
}
|
|
110
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Consume the SSE stream. Returns the message of an `error` event if one was seen
|
|
115
|
+
* (the turn failed even though the stream ended cleanly), otherwise undefined.
|
|
116
|
+
*/
|
|
111
117
|
async function consumeServeSse(body, emitter, resetIdle) {
|
|
112
118
|
const reader = body.getReader();
|
|
113
119
|
const decoder = new TextDecoder();
|
|
114
120
|
let buffer = '';
|
|
115
121
|
let doneEmitted = false;
|
|
122
|
+
let streamError;
|
|
116
123
|
const markDone = () => {
|
|
117
124
|
doneEmitted = true;
|
|
118
125
|
};
|
|
126
|
+
const markError = (message) => {
|
|
127
|
+
streamError = message;
|
|
128
|
+
};
|
|
119
129
|
try {
|
|
120
130
|
while (true) {
|
|
121
131
|
const { value, done } = await reader.read();
|
|
@@ -125,24 +135,25 @@ async function consumeServeSse(body, emitter, resetIdle) {
|
|
|
125
135
|
// Any data on the wire means the turn is progressing — push the idle window out.
|
|
126
136
|
resetIdle();
|
|
127
137
|
buffer += decoder.decode(value, { stream: true });
|
|
128
|
-
buffer = await drainServeSseBuffer(buffer, emitter, markDone);
|
|
138
|
+
buffer = await drainServeSseBuffer(buffer, emitter, markDone, markError);
|
|
129
139
|
if (doneEmitted) {
|
|
130
|
-
return;
|
|
140
|
+
return streamError;
|
|
131
141
|
}
|
|
132
142
|
}
|
|
133
143
|
buffer += decoder.decode();
|
|
134
144
|
if (buffer.trim()) {
|
|
135
|
-
await drainServeSseBuffer(`${buffer}\n`, emitter, markDone);
|
|
145
|
+
await drainServeSseBuffer(`${buffer}\n`, emitter, markDone, markError);
|
|
136
146
|
}
|
|
137
147
|
if (!doneEmitted) {
|
|
138
148
|
await emitter.done();
|
|
139
149
|
}
|
|
150
|
+
return streamError;
|
|
140
151
|
}
|
|
141
152
|
finally {
|
|
142
153
|
reader.releaseLock();
|
|
143
154
|
}
|
|
144
155
|
}
|
|
145
|
-
async function drainServeSseBuffer(buffer, emitter, markDone) {
|
|
156
|
+
async function drainServeSseBuffer(buffer, emitter, markDone, markError) {
|
|
146
157
|
const lines = buffer.split('\n');
|
|
147
158
|
const remainder = lines.pop() ?? '';
|
|
148
159
|
for (const line of lines) {
|
|
@@ -160,7 +171,7 @@ async function drainServeSseBuffer(buffer, emitter, markDone) {
|
|
|
160
171
|
catch {
|
|
161
172
|
continue;
|
|
162
173
|
}
|
|
163
|
-
const finished = await dispatchServeEvent(event, emitter);
|
|
174
|
+
const finished = await dispatchServeEvent(event, emitter, markError);
|
|
164
175
|
if (finished) {
|
|
165
176
|
markDone();
|
|
166
177
|
return '';
|
|
@@ -168,7 +179,7 @@ async function drainServeSseBuffer(buffer, emitter, markDone) {
|
|
|
168
179
|
}
|
|
169
180
|
return remainder;
|
|
170
181
|
}
|
|
171
|
-
async function dispatchServeEvent(event, emitter) {
|
|
182
|
+
async function dispatchServeEvent(event, emitter, markError) {
|
|
172
183
|
const type = typeof event.type === 'string' ? event.type : '';
|
|
173
184
|
switch (type) {
|
|
174
185
|
case 'thinking':
|
|
@@ -204,9 +215,12 @@ async function dispatchServeEvent(event, emitter) {
|
|
|
204
215
|
}
|
|
205
216
|
return false;
|
|
206
217
|
}
|
|
207
|
-
case 'error':
|
|
208
|
-
|
|
218
|
+
case 'error': {
|
|
219
|
+
const message = typeof event.message === 'string' ? event.message : 'Remote serve error';
|
|
220
|
+
markError(message);
|
|
221
|
+
await emitter.error(message);
|
|
209
222
|
return false;
|
|
223
|
+
}
|
|
210
224
|
case 'done':
|
|
211
225
|
await emitter.done();
|
|
212
226
|
return true;
|
|
@@ -158,3 +158,17 @@ model_list:
|
|
|
158
158
|
api_base: https://api.openai.com/v1
|
|
159
159
|
model_info:
|
|
160
160
|
upstream_provider: "openai"
|
|
161
|
+
|
|
162
|
+
# Fleet / Cloud Run: ah-auto resolves via the central AgentHippo model gateway (same as
|
|
163
|
+
# colocated agents with AGENTHIPPO_LITELLM_BASE_URL=https://gateway.agenthippo.ai/v1).
|
|
164
|
+
# Set AGENTHIPPO_GATEWAY_KEY to an ahgw_… service key (create-gateway-key on the gateway host).
|
|
165
|
+
- model_name: ah-auto
|
|
166
|
+
litellm_params:
|
|
167
|
+
# openai/ prefix required — bare `ah-auto` fails LiteLLM router deployment (no provider).
|
|
168
|
+
model: openai/ah-auto
|
|
169
|
+
api_base: https://gateway.agenthippo.ai/v1
|
|
170
|
+
api_key: os.environ/AGENTHIPPO_GATEWAY_KEY
|
|
171
|
+
drop_params: true
|
|
172
|
+
additional_drop_params: ["user", "max_output_tokens", "reasoning.effort", "reasoning_effort"]
|
|
173
|
+
model_info:
|
|
174
|
+
upstream_provider: "agenthippo-gateway"
|
package/out/shared/models.js
CHANGED
package/out/shared/providers.js
CHANGED
|
@@ -73,12 +73,13 @@ exports.PROVIDERS = {
|
|
|
73
73
|
},
|
|
74
74
|
litellm: {
|
|
75
75
|
id: 'litellm',
|
|
76
|
-
displayName: 'LiteLLM
|
|
76
|
+
displayName: 'LiteLLM',
|
|
77
77
|
endpointSettingKey: 'agentide.providers.litellm.baseUrl',
|
|
78
78
|
requiresApiKey: true,
|
|
79
79
|
description: 'local adapter for agents to work with Spotlight and all models from OpenAI, Anthropic, Ollama etc.',
|
|
80
80
|
showInAddApiKeyUI: true,
|
|
81
81
|
isGateway: true, // Can route to any backend
|
|
82
|
+
deferMissingApiKey: true, // base-url configured; key may be empty in dev
|
|
82
83
|
},
|
|
83
84
|
ollama: {
|
|
84
85
|
id: 'ollama',
|
|
@@ -95,13 +96,15 @@ exports.PROVIDERS = {
|
|
|
95
96
|
agenthippo: {
|
|
96
97
|
id: 'agenthippo',
|
|
97
98
|
displayName: 'AgentHippo Gateway',
|
|
98
|
-
defaultEndpoint: 'https://
|
|
99
|
+
defaultEndpoint: 'https://gateway.agenthippo.ai/v1',
|
|
99
100
|
endpointSettingKey: 'agentide.AgentHippoModelGateway.url',
|
|
100
101
|
requiresApiKey: true,
|
|
101
102
|
description: 'AgentHippo managed gateway — uses account JWT, picks model automatically',
|
|
102
103
|
showInAddApiKeyUI: false,
|
|
103
104
|
isGateway: true,
|
|
104
105
|
staticHeaders: { 'x-agenthippo-source': 'chat-ui' },
|
|
106
|
+
credentialRefreshCommand: 'agentide.auth.refreshToken', // refresh stale sign-in JWT on 401
|
|
107
|
+
deferMissingApiKey: true, // key is the sign-in JWT; route branch prompts sign-in
|
|
105
108
|
},
|
|
106
109
|
};
|
|
107
110
|
// -----------------------------------------------------------------------------
|