@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/cli/state-sync.js
CHANGED
|
@@ -2,37 +2,93 @@
|
|
|
2
2
|
/*---------------------------------------------------------------------------------------------
|
|
3
3
|
* Copyright (c) AgentHippo.ai. All rights reserved.
|
|
4
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
|
+
})();
|
|
5
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
39
|
exports.isAwsCliAvailable = isAwsCliAvailable;
|
|
7
40
|
exports.resolveS3StateConfig = resolveS3StateConfig;
|
|
41
|
+
exports.resolveRestoreBootMode = resolveRestoreBootMode;
|
|
42
|
+
exports.safeSessionId = safeSessionId;
|
|
43
|
+
exports.sessionRemoteUri = sessionRemoteUri;
|
|
44
|
+
exports.shardBelongsToSession = shardBelongsToSession;
|
|
45
|
+
exports.hasLocalSessionState = hasLocalSessionState;
|
|
46
|
+
exports.listSessionShardFiles = listSessionShardFiles;
|
|
47
|
+
exports.stageSessionPushDir = stageSessionPushDir;
|
|
48
|
+
exports.mergeSessionStagingIntoState = mergeSessionStagingIntoState;
|
|
8
49
|
exports.restoreState = restoreState;
|
|
9
50
|
exports.pushState = pushState;
|
|
51
|
+
exports.pushSessionState = pushSessionState;
|
|
52
|
+
exports.ensureSessionLocal = ensureSessionLocal;
|
|
53
|
+
exports.clearRestoreAttemptCache = clearRestoreAttemptCache;
|
|
10
54
|
exports.makeStatePusher = makeStatePusher;
|
|
55
|
+
exports.providerSessionShardName = providerSessionShardName;
|
|
11
56
|
/**
|
|
12
|
-
* Object-store state sync (serve-v3 Q1
|
|
57
|
+
* Object-store state sync (serve-v3 Q1 + serve-v4 P1.2).
|
|
13
58
|
*
|
|
14
|
-
* Persists the durable state root
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* fleet replica can reload an affinity-owned session.
|
|
59
|
+
* Persists the durable state root to an S3-compatible object store so sessions
|
|
60
|
+
* survive scale-to-zero / a fresh instance (Cloudflare R2, MinIO, GCS, AWS S3)
|
|
61
|
+
* and so a sticky-routed fleet can restore-on-miss after resharding.
|
|
18
62
|
*
|
|
19
|
-
* Strategy
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* the
|
|
63
|
+
* Strategy:
|
|
64
|
+
* - Per-session async-delta push (debounced, off the response path) →
|
|
65
|
+
* s3://…/sessions/<sessionId>/
|
|
66
|
+
* - Restore-on-miss before a turn when the session is not local
|
|
67
|
+
* - Optional boot restore of the whole prefix (`AGENTHIPPO_STATE_RESTORE_BOOT=all`)
|
|
68
|
+
* - SIGTERM flush of pending session pushes
|
|
69
|
+
*
|
|
70
|
+
* Uses `aws s3 sync` (S3-compatible via --endpoint-url). No SDK in the binary.
|
|
71
|
+
* Last-write-wins per session prefix — no CAS. Mid-reshard may lose one turn of
|
|
72
|
+
* context; sticky routing makes that rare.
|
|
23
73
|
*
|
|
24
74
|
* Config (no-op unless AGENTHIPPO_STATE_S3_URI is set):
|
|
25
|
-
* AGENTHIPPO_STATE_S3_URI
|
|
26
|
-
* AGENTHIPPO_STATE_S3_ENDPOINT
|
|
27
|
-
*
|
|
75
|
+
* AGENTHIPPO_STATE_S3_URI s3://bucket/prefix
|
|
76
|
+
* AGENTHIPPO_STATE_S3_ENDPOINT http://minio:9000 (non-AWS S3)
|
|
77
|
+
* AGENTHIPPO_STATE_RESTORE_BOOT all|none (default all for single-instance compat;
|
|
78
|
+
* multi-replica templates set none)
|
|
79
|
+
* AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_REGION
|
|
28
80
|
*/
|
|
81
|
+
const crypto_1 = require("crypto");
|
|
29
82
|
const child_process_1 = require("child_process");
|
|
83
|
+
const fsp = __importStar(require("fs/promises"));
|
|
84
|
+
const path = __importStar(require("path"));
|
|
30
85
|
const util_1 = require("util");
|
|
31
86
|
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
87
|
+
/** In-process cache: skip repeat S3 pulls for the same session on this replica. */
|
|
88
|
+
const restoreAttempted = new Set();
|
|
32
89
|
/**
|
|
33
90
|
* Verify the `aws` CLI is callable. State sync shells out to it, so when S3 is
|
|
34
91
|
* configured a missing binary means silent data loss — callers should fail fast.
|
|
35
|
-
* Resolves true when available, false otherwise.
|
|
36
92
|
*/
|
|
37
93
|
async function isAwsCliAvailable() {
|
|
38
94
|
try {
|
|
@@ -54,6 +110,27 @@ function resolveS3StateConfig(env = process.env) {
|
|
|
54
110
|
endpoint: env.AGENTHIPPO_STATE_S3_ENDPOINT?.trim() || undefined,
|
|
55
111
|
};
|
|
56
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Boot restore mode. Default `all` preserves single-instance behavior.
|
|
115
|
+
* Multi-replica templates set `none` and rely on restore-on-miss (P1.2).
|
|
116
|
+
*/
|
|
117
|
+
function resolveRestoreBootMode(env = process.env) {
|
|
118
|
+
const raw = env.AGENTHIPPO_STATE_RESTORE_BOOT?.trim().toLowerCase();
|
|
119
|
+
if (raw === 'none') {
|
|
120
|
+
return 'none';
|
|
121
|
+
}
|
|
122
|
+
return 'all';
|
|
123
|
+
}
|
|
124
|
+
/** Sanitize a client session id for use as an S3 path segment / local dir name. */
|
|
125
|
+
function safeSessionId(sessionId) {
|
|
126
|
+
const trimmed = sessionId.trim();
|
|
127
|
+
const safe = trimmed.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '_').replace(/\.\./g, '_').slice(0, 160);
|
|
128
|
+
return safe || 'session';
|
|
129
|
+
}
|
|
130
|
+
/** Remote prefix for one session: s3://bucket/prefix/sessions/<id> */
|
|
131
|
+
function sessionRemoteUri(cfg, sessionId) {
|
|
132
|
+
return `${cfg.uri}/sessions/${safeSessionId(sessionId)}`;
|
|
133
|
+
}
|
|
57
134
|
function syncArgs(cfg, from, to) {
|
|
58
135
|
const args = ['s3', 'sync', from, to, '--no-progress'];
|
|
59
136
|
if (cfg.endpoint) {
|
|
@@ -61,6 +138,127 @@ function syncArgs(cfg, from, to) {
|
|
|
61
138
|
}
|
|
62
139
|
return args;
|
|
63
140
|
}
|
|
141
|
+
function attemptKey(stateDir, sessionId) {
|
|
142
|
+
return `${path.resolve(stateDir)}\0${sessionId}`;
|
|
143
|
+
}
|
|
144
|
+
/** Conversation key format: engine:chatSessionId:pack:signature */
|
|
145
|
+
function shardBelongsToSession(shardKey, chatSessionId, value) {
|
|
146
|
+
if (value?.chatSessionId && value.chatSessionId === chatSessionId) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
const parts = shardKey.split(':');
|
|
150
|
+
return parts.length >= 2 && parts[1] === chatSessionId;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* True when this replica already has provider-session state for the chat session.
|
|
154
|
+
* Used to skip restore-on-miss on the hot path.
|
|
155
|
+
*/
|
|
156
|
+
async function hasLocalSessionState(stateDir, sessionId) {
|
|
157
|
+
const dir = path.join(stateDir, 'provider-sessions');
|
|
158
|
+
let names;
|
|
159
|
+
try {
|
|
160
|
+
names = await fsp.readdir(dir);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
for (const name of names) {
|
|
166
|
+
if (!name.endsWith('.json')) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const raw = await fsp.readFile(path.join(dir, name), 'utf8');
|
|
171
|
+
const parsed = JSON.parse(raw);
|
|
172
|
+
if (typeof parsed.key === 'string' && shardBelongsToSession(parsed.key, sessionId, parsed.value)) {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// skip unreadable
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
/** Collect provider-session shard filenames that belong to this chat session. */
|
|
183
|
+
async function listSessionShardFiles(stateDir, sessionId) {
|
|
184
|
+
const dir = path.join(stateDir, 'provider-sessions');
|
|
185
|
+
let names;
|
|
186
|
+
try {
|
|
187
|
+
names = await fsp.readdir(dir);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
const out = [];
|
|
193
|
+
for (const name of names) {
|
|
194
|
+
if (!name.endsWith('.json')) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const raw = await fsp.readFile(path.join(dir, name), 'utf8');
|
|
199
|
+
const parsed = JSON.parse(raw);
|
|
200
|
+
if (typeof parsed.key === 'string' && shardBelongsToSession(parsed.key, sessionId, parsed.value)) {
|
|
201
|
+
out.push(name);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// skip
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
async function pathExists(p) {
|
|
211
|
+
try {
|
|
212
|
+
await fsp.access(p);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Build a local staging tree for session S: provider-sessions/<shards for S> only.
|
|
221
|
+
*
|
|
222
|
+
* `<stateDir>/agents/<agentId>/{claude,codex}` (the SDK's own on-disk session/config
|
|
223
|
+
* cache — see agentpack/paths.ts) is deliberately NOT included here. That directory is
|
|
224
|
+
* shared by every session/user of the same pack on this replica, keyed only by agentId —
|
|
225
|
+
* never session-scoped. Treating it as part of one session's push/restore let one
|
|
226
|
+
* session's stale snapshot force-overwrite the live SDK cache for every other
|
|
227
|
+
* concurrently active session on the replica (fs.cp force:true in the old
|
|
228
|
+
* mergeSessionStagingIntoState). It's local-disk-only now, same as before object-store
|
|
229
|
+
* sync existed: it lives and dies with its replica. A resume target missing after a
|
|
230
|
+
* replica change already falls back to a fresh SDK session — the same path every
|
|
231
|
+
* session's true first turn on any replica already takes — so this doesn't introduce a
|
|
232
|
+
* new failure mode, it just stops the wrong-session-data clobber.
|
|
233
|
+
*/
|
|
234
|
+
async function stageSessionPushDir(stateDir, sessionId, stagingDir) {
|
|
235
|
+
await fsp.rm(stagingDir, { recursive: true, force: true });
|
|
236
|
+
await fsp.mkdir(stagingDir, { recursive: true });
|
|
237
|
+
const shards = await listSessionShardFiles(stateDir, sessionId);
|
|
238
|
+
const psDest = path.join(stagingDir, 'provider-sessions');
|
|
239
|
+
if (shards.length > 0) {
|
|
240
|
+
await fsp.mkdir(psDest, { recursive: true });
|
|
241
|
+
for (const name of shards) {
|
|
242
|
+
await fsp.copyFile(path.join(stateDir, 'provider-sessions', name), path.join(psDest, name));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return shards.length > 0;
|
|
246
|
+
}
|
|
247
|
+
/** Merge a restored staging tree back into stateDir (provider-session shards only — see
|
|
248
|
+
* stageSessionPushDir for why the SDK config cache is intentionally excluded). */
|
|
249
|
+
async function mergeSessionStagingIntoState(stagingDir, stateDir) {
|
|
250
|
+
const psSrc = path.join(stagingDir, 'provider-sessions');
|
|
251
|
+
if (await pathExists(psSrc)) {
|
|
252
|
+
const psDest = path.join(stateDir, 'provider-sessions');
|
|
253
|
+
await fsp.mkdir(psDest, { recursive: true });
|
|
254
|
+
for (const name of await fsp.readdir(psSrc)) {
|
|
255
|
+
if (!name.endsWith('.json')) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
await fsp.copyFile(path.join(psSrc, name), path.join(psDest, name));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
64
262
|
/** Download state from the bucket into stateDir on boot. An empty bucket is a no-op. */
|
|
65
263
|
async function restoreState(cfg, stateDir, log) {
|
|
66
264
|
try {
|
|
@@ -68,46 +266,130 @@ async function restoreState(cfg, stateDir, log) {
|
|
|
68
266
|
log?.(`State restored from ${cfg.uri}`);
|
|
69
267
|
}
|
|
70
268
|
catch (err) {
|
|
71
|
-
// First boot / empty bucket / transient — don't block startup.
|
|
72
269
|
log?.(`State restore skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
73
270
|
}
|
|
74
271
|
}
|
|
75
|
-
/** Upload changed files from stateDir to the bucket (delta). */
|
|
272
|
+
/** Upload changed files from stateDir to the bucket (full-tree delta — legacy / flush fallback). */
|
|
76
273
|
async function pushState(cfg, stateDir, log) {
|
|
77
274
|
try {
|
|
78
275
|
await execFileAsync('aws', syncArgs(cfg, stateDir, cfg.uri));
|
|
79
276
|
}
|
|
80
277
|
catch (err) {
|
|
81
|
-
// A failed push means sessions aren't durable — surface it unconditionally,
|
|
82
|
-
// not just under --verbose, so the operator notices state isn't persisting.
|
|
83
278
|
console.error(`[state-sync] push to ${cfg.uri} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
84
279
|
log?.(`State push failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
85
280
|
}
|
|
86
281
|
}
|
|
282
|
+
/** Push one session's scoped tree to s3://…/sessions/<id>/. */
|
|
283
|
+
async function pushSessionState(cfg, stateDir, sessionId, log) {
|
|
284
|
+
const safe = safeSessionId(sessionId);
|
|
285
|
+
const staging = path.join(stateDir, '.push-staging', safe);
|
|
286
|
+
try {
|
|
287
|
+
const hasContent = await stageSessionPushDir(stateDir, sessionId, staging);
|
|
288
|
+
if (!hasContent) {
|
|
289
|
+
log?.(`State push skipped for session ${safe}: nothing local`);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const remote = sessionRemoteUri(cfg, sessionId);
|
|
293
|
+
await execFileAsync('aws', syncArgs(cfg, staging, remote));
|
|
294
|
+
log?.(`State pushed session ${safe} → ${remote}`);
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
console.error(`[state-sync] push session ${safe} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
298
|
+
log?.(`State push failed for ${safe}: ${err instanceof Error ? err.message : String(err)}`);
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
await fsp.rm(staging, { recursive: true, force: true }).catch(() => undefined);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* On a request whose session_id has no local provider-session state, pull
|
|
306
|
+
* s3://…/sessions/<id>/ before runner.run(). No-ops if already local or already
|
|
307
|
+
* attempted this process (avoids re-pulling brand-new sessions every turn).
|
|
308
|
+
*/
|
|
309
|
+
async function ensureSessionLocal(cfg, stateDir, sessionId, log) {
|
|
310
|
+
const key = attemptKey(stateDir, sessionId);
|
|
311
|
+
if (restoreAttempted.has(key)) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (await hasLocalSessionState(stateDir, sessionId)) {
|
|
315
|
+
restoreAttempted.add(key);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const safe = safeSessionId(sessionId);
|
|
319
|
+
const staging = path.join(stateDir, '.restore-staging', safe);
|
|
320
|
+
try {
|
|
321
|
+
await fsp.rm(staging, { recursive: true, force: true });
|
|
322
|
+
await fsp.mkdir(staging, { recursive: true });
|
|
323
|
+
const remote = sessionRemoteUri(cfg, sessionId);
|
|
324
|
+
await execFileAsync('aws', syncArgs(cfg, remote, staging));
|
|
325
|
+
await mergeSessionStagingIntoState(staging, stateDir);
|
|
326
|
+
log?.(`State restored session ${safe} from ${remote}`);
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
// Empty prefix / first turn / transient — continue; runner creates fresh state.
|
|
330
|
+
log?.(`State restore-on-miss skipped for ${safe}: ${err instanceof Error ? err.message : String(err)}`);
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
restoreAttempted.add(key);
|
|
334
|
+
await fsp.rm(staging, { recursive: true, force: true }).catch(() => undefined);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/** Test helper — clear the restore-attempted cache. */
|
|
338
|
+
function clearRestoreAttemptCache() {
|
|
339
|
+
restoreAttempted.clear();
|
|
340
|
+
}
|
|
87
341
|
/**
|
|
88
|
-
* Debounced, non-overlapping pusher.
|
|
89
|
-
*
|
|
342
|
+
* Debounced, non-overlapping per-session pusher.
|
|
343
|
+
* `schedule(sessionId)` after each turn; `flush()` awaits pending pushes (SIGTERM).
|
|
90
344
|
*/
|
|
91
345
|
function makeStatePusher(cfg, stateDir, log, waitMs = 1000) {
|
|
92
346
|
let timer = null;
|
|
93
347
|
let running = false;
|
|
94
|
-
let pending = false;
|
|
95
348
|
let inflightPromise = null;
|
|
349
|
+
const pendingSessions = new Set();
|
|
350
|
+
let pendingFull = false;
|
|
96
351
|
const runNow = () => {
|
|
97
352
|
running = true;
|
|
98
|
-
|
|
99
|
-
|
|
353
|
+
const sessions = [...pendingSessions];
|
|
354
|
+
const doFull = pendingFull;
|
|
355
|
+
pendingSessions.clear();
|
|
356
|
+
pendingFull = false;
|
|
357
|
+
inflightPromise = (async () => {
|
|
358
|
+
if (doFull) {
|
|
359
|
+
await pushState(cfg, stateDir, log);
|
|
360
|
+
}
|
|
361
|
+
for (const id of sessions) {
|
|
362
|
+
await pushSessionState(cfg, stateDir, id, log);
|
|
363
|
+
}
|
|
364
|
+
})()
|
|
365
|
+
.catch((err) => {
|
|
366
|
+
console.error(`[state-sync] push batch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
367
|
+
})
|
|
368
|
+
.finally(() => {
|
|
100
369
|
running = false;
|
|
101
370
|
inflightPromise = null;
|
|
102
|
-
if (
|
|
103
|
-
schedule()
|
|
371
|
+
if (pendingSessions.size > 0 || pendingFull) {
|
|
372
|
+
// Re-arm debounce without treating this as a legacy full-push schedule().
|
|
373
|
+
if (!timer) {
|
|
374
|
+
timer = setTimeout(() => {
|
|
375
|
+
timer = null;
|
|
376
|
+
void runNow();
|
|
377
|
+
}, waitMs);
|
|
378
|
+
timer.unref?.();
|
|
379
|
+
}
|
|
104
380
|
}
|
|
105
381
|
});
|
|
106
382
|
return inflightPromise;
|
|
107
383
|
};
|
|
108
|
-
const schedule = () => {
|
|
384
|
+
const schedule = (sessionId) => {
|
|
385
|
+
if (sessionId?.trim()) {
|
|
386
|
+
pendingSessions.add(sessionId.trim());
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
// Legacy no-arg call → full-tree push (boot-compat / callers that omit id).
|
|
390
|
+
pendingFull = true;
|
|
391
|
+
}
|
|
109
392
|
if (running) {
|
|
110
|
-
pending = true;
|
|
111
393
|
return;
|
|
112
394
|
}
|
|
113
395
|
if (timer) {
|
|
@@ -124,12 +406,19 @@ function makeStatePusher(cfg, stateDir, log, waitMs = 1000) {
|
|
|
124
406
|
clearTimeout(timer);
|
|
125
407
|
timer = null;
|
|
126
408
|
}
|
|
127
|
-
// Wait for any in-flight push to complete before the final push,
|
|
128
|
-
// so SIGTERM never runs two concurrent aws s3 sync processes.
|
|
129
409
|
if (inflightPromise) {
|
|
130
410
|
await inflightPromise;
|
|
131
411
|
}
|
|
132
|
-
|
|
412
|
+
if (pendingSessions.size > 0 || pendingFull) {
|
|
413
|
+
await runNow();
|
|
414
|
+
if (inflightPromise) {
|
|
415
|
+
await inflightPromise;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
133
418
|
};
|
|
134
419
|
return { schedule, flush };
|
|
135
420
|
}
|
|
421
|
+
/** Stable hash helper exposed for tests (matches DirectorySessionStore naming). */
|
|
422
|
+
function providerSessionShardName(conversationKey) {
|
|
423
|
+
return `${(0, crypto_1.createHash)('sha256').update(conversationKey).digest('hex').slice(0, 40)}.json`;
|
|
424
|
+
}
|
package/out/engines/claude.js
CHANGED
|
@@ -43,6 +43,8 @@ const base_1 = require("./base");
|
|
|
43
43
|
const utils_1 = require("../utils");
|
|
44
44
|
const permissions_1 = require("../permissions");
|
|
45
45
|
const mcp_1 = require("../mcp");
|
|
46
|
+
const data_plane_1 = require("../cli/data-plane");
|
|
47
|
+
const runtime_1 = require("../cli/runtime");
|
|
46
48
|
const session_1 = require("../session");
|
|
47
49
|
const agentpack_1 = require("../agentpack");
|
|
48
50
|
const shared_1 = require("../shared");
|
|
@@ -241,7 +243,8 @@ class ClaudeEngine {
|
|
|
241
243
|
// Edit tracker from runtime context (optional)
|
|
242
244
|
const editTracker = runtime.editTracker;
|
|
243
245
|
// Resolve API routing (LiteLLM proxy vs direct Anthropic)
|
|
244
|
-
|
|
246
|
+
// claude routes to Anthropic when direct (or the LiteLLM proxy when configured) — never OpenAI.
|
|
247
|
+
const { apiKey, baseUrl, useLiteLLM } = (0, utils_1.resolveApiRouting)(options, this.config, 'anthropic');
|
|
245
248
|
if (!apiKey) {
|
|
246
249
|
throw new Error('Claude API key not provided');
|
|
247
250
|
}
|
|
@@ -267,6 +270,11 @@ class ClaudeEngine {
|
|
|
267
270
|
'DEBUG_CLAUDE_AGENT_SDK': '1',
|
|
268
271
|
'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC': '1',
|
|
269
272
|
};
|
|
273
|
+
// Egress broker active ⇒ the gateway token stays broker-side; scrub it from the child
|
|
274
|
+
// env so it never reaches the model (secure-obo-design §7.6 / §7.4 gap 1).
|
|
275
|
+
if (claudeEnv[data_plane_1.EGRESS_URL_ENV]) {
|
|
276
|
+
delete claudeEnv[runtime_1.OPENCLAW_GATEWAY_TOKEN_ENV];
|
|
277
|
+
}
|
|
270
278
|
const isSetupTokenAuth = (0, shared_1.isAnthropicSetupToken)(apiKey);
|
|
271
279
|
if (isSetupTokenAuth) {
|
|
272
280
|
const normalizedSetupToken = (0, shared_1.normalizeAnthropicSetupToken)(apiKey);
|
|
@@ -541,8 +549,10 @@ class ClaudeEngine {
|
|
|
541
549
|
logger.debug(`[AgentIDE] SessionStart hook called! event=${input.hook_event_name}`);
|
|
542
550
|
return CONTINUE;
|
|
543
551
|
};
|
|
544
|
-
// Load MCP servers for the active agent and pass directly to SDK
|
|
545
|
-
|
|
552
|
+
// Load MCP servers for the active agent and pass directly to SDK. claudeEnv is the
|
|
553
|
+
// per-turn child env — URL servers resolve their bearerTokenEnv (turn nonce) from it,
|
|
554
|
+
// in memory, so no per-turn value ever lands in a shared config file.
|
|
555
|
+
const mcpServers = await this.loadMcpServersForClaude(logger, options.workspaceRoot, agentId, claudeEnv);
|
|
546
556
|
// Determine cwd and settingSources
|
|
547
557
|
// cwd is ALWAYS workspaceRoot - active agent content has been synced to workspace's .claude/
|
|
548
558
|
// Agent packs: settingSources is 'project' only (no user ~/.claude/ mixing)
|
|
@@ -1191,9 +1201,11 @@ class ClaudeEngine {
|
|
|
1191
1201
|
* - Default agent: uses bundled MCP servers from extension
|
|
1192
1202
|
* - Pack agents: uses pack's mcp/ directory
|
|
1193
1203
|
*
|
|
1194
|
-
* The SDK accepts mcpServers directly in query options
|
|
1204
|
+
* The SDK accepts mcpServers directly in query options — including HTTP Streamable
|
|
1205
|
+
* servers (`{type:'http', url, headers}`). `turnEnv` is the per-turn child env; a URL
|
|
1206
|
+
* server's `bearerTokenEnv` (e.g. AGENTHIPPO_TURN_NONCE) resolves against it in memory.
|
|
1195
1207
|
*/
|
|
1196
|
-
async loadMcpServersForClaude(logger, workspaceRoot, agentId = 'default') {
|
|
1208
|
+
async loadMcpServersForClaude(logger, workspaceRoot, agentId = 'default', turnEnv) {
|
|
1197
1209
|
const basePromptsDir = this.getBundledBasePromptsDir();
|
|
1198
1210
|
const servers = await (0, mcp_1.loadMcpServers)(agentId, workspaceRoot, basePromptsDir);
|
|
1199
1211
|
if (servers.length === 0) {
|
|
@@ -1201,6 +1213,15 @@ class ClaudeEngine {
|
|
|
1201
1213
|
}
|
|
1202
1214
|
const mcpServers = {};
|
|
1203
1215
|
for (const s of servers) {
|
|
1216
|
+
if (s.url) {
|
|
1217
|
+
const headers = (0, mcp_1.resolveHttpMcpHeaders)(s, turnEnv ?? process.env);
|
|
1218
|
+
mcpServers[s.name] = {
|
|
1219
|
+
type: 'http',
|
|
1220
|
+
url: s.url,
|
|
1221
|
+
...(headers ? { headers } : {}),
|
|
1222
|
+
};
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1204
1225
|
mcpServers[s.name] = {
|
|
1205
1226
|
command: s.command,
|
|
1206
1227
|
args: s.args,
|
package/out/engines/codex.js
CHANGED
|
@@ -46,6 +46,9 @@ const utils_1 = require("../utils");
|
|
|
46
46
|
const permissions_1 = require("../permissions");
|
|
47
47
|
const session_1 = require("../session");
|
|
48
48
|
const agentpack_1 = require("../agentpack");
|
|
49
|
+
const credential_context_1 = require("../cli/credential-context");
|
|
50
|
+
const data_plane_1 = require("../cli/data-plane");
|
|
51
|
+
const runtime_1 = require("../cli/runtime");
|
|
49
52
|
// Note: HTTP edit server removed - file changes are now handled directly via file_change events
|
|
50
53
|
// -----------------------------------------------------------------------------
|
|
51
54
|
// Constants
|
|
@@ -63,7 +66,7 @@ const AGENTIDE_CODEX_SOURCE_ENV_VAR = 'AGENTIDE_ANALYTICS_SOURCE';
|
|
|
63
66
|
const AGENTIDE_CODEX_ENGINE_ENV_VAR = 'AGENTIDE_ANALYTICS_ENGINE';
|
|
64
67
|
const AGENTIDE_CODEX_REQUEST_ID_ENV_VAR = 'AGENTIDE_ANALYTICS_REQUEST_ID';
|
|
65
68
|
// Bump when we change generated config semantics so long-lived sessions regenerate config.toml.
|
|
66
|
-
const AGENTIDE_CODEX_CONFIG_VERSION =
|
|
69
|
+
const AGENTIDE_CODEX_CONFIG_VERSION = 20; // v20: default_tools_approval_mode "auto"→"approve" ("auto" is invalid on codex ≥0.131 and cancels every headless MCP tool call)
|
|
67
70
|
// Per-process runtime context for agents (not analytics): lets the agent locate its per-session context export.
|
|
68
71
|
const AGENTIDE_CONTEXT_EXPORT_PATH_ENV_VAR = 'AGENTIDE_CONTEXT_EXPORT_PATH';
|
|
69
72
|
const AGENTIDE_TERMINAL_LOG_PATH_ENV_VAR = 'AGENTIDE_TERMINAL_LOG_PATH';
|
|
@@ -100,7 +103,8 @@ class CodexEngine {
|
|
|
100
103
|
const threadModelId = options.modelId;
|
|
101
104
|
// Resolve API routing (LiteLLM proxy vs direct OpenAI)
|
|
102
105
|
// Use openaiBaseUrl which has overrideOpenAIBaseUrl applied
|
|
103
|
-
|
|
106
|
+
// codex routes to OpenAI when direct (or the LiteLLM proxy when configured) — never Anthropic.
|
|
107
|
+
const routing = (0, utils_1.resolveApiRouting)(options, this.config, 'openai');
|
|
104
108
|
const apiKey = routing.apiKey;
|
|
105
109
|
const baseUrl = routing.openaiBaseUrl; // Use OpenAI-specific URL
|
|
106
110
|
const useLiteLLM = routing.useLiteLLM;
|
|
@@ -126,6 +130,13 @@ class CodexEngine {
|
|
|
126
130
|
...(runtime.cliEnvPatch ?? {}),
|
|
127
131
|
OPENAI_LOG: 'debug',
|
|
128
132
|
};
|
|
133
|
+
// When serve runs the egress broker, the OpenClaw gateway token must never enter the
|
|
134
|
+
// agent's trust domain (secure-obo-design §7.6 / §7.4 gap 1). Scrub it from the child env
|
|
135
|
+
// at the spawn boundary — belt-and-suspenders with serve's boot-time process.env strip,
|
|
136
|
+
// so a token in process.env (e.g. captured before the strip) still cannot reach the model.
|
|
137
|
+
if (codexEnv[data_plane_1.EGRESS_URL_ENV]) {
|
|
138
|
+
delete codexEnv[runtime_1.OPENCLAW_GATEWAY_TOKEN_ENV];
|
|
139
|
+
}
|
|
129
140
|
// Always expose the VS Code chat session id to the agent process so it can locate:
|
|
130
141
|
// .agentide/sessions/<sessionId>/context.md
|
|
131
142
|
const rawChatSessionId = sessionId?.trim();
|
|
@@ -141,8 +152,12 @@ class CodexEngine {
|
|
|
141
152
|
// Note: workingDirectory is ALWAYS workspaceRoot - active agent content is synced to workspace's .codex/
|
|
142
153
|
const agentId = options.agentPackId ?? 'default';
|
|
143
154
|
const runtimePaths = (0, agentpack_1.resolveRuntimePaths)(options.workspaceRoot, this.config.storagePath, agentId, options.agentPackVersion);
|
|
144
|
-
// Use agent-isolated CODEX_HOME (for session/state isolation)
|
|
145
|
-
|
|
155
|
+
// Use agent-isolated CODEX_HOME (for session/state isolation). In multi-tenant serve,
|
|
156
|
+
// additionally key it by the verified request user: config.toml is rewritten per turn,
|
|
157
|
+
// and a shared per-agent home lets concurrent turns of different users race it
|
|
158
|
+
// (secure-obo-design §1 defect 1 / §8 Phase 0). No credential frame (IDE,
|
|
159
|
+
// single-tenant CLI) ⇒ unchanged per-agent path.
|
|
160
|
+
const codexHome = (0, agentpack_1.userScopedEngineHome)(runtimePaths.codexHome, (0, credential_context_1.currentCredential)()?.userId);
|
|
146
161
|
codexEnv['CODEX_HOME'] = codexHome;
|
|
147
162
|
await fs_1.promises.mkdir(codexHome, { recursive: true });
|
|
148
163
|
// Always set AGENT_ID for agent to know its identity
|
|
@@ -601,7 +616,11 @@ class CodexEngine {
|
|
|
601
616
|
// Codex CLI requires wire_api=responses (chat wire was removed in newer Codex releases).
|
|
602
617
|
const wireApi = 'responses';
|
|
603
618
|
const routingType = useLiteLLM ? 'litellm' : (customBaseUrl ? 'custom' : 'openai');
|
|
604
|
-
|
|
619
|
+
// No per-turn component in this signature: config.toml must be byte-identical across
|
|
620
|
+
// turns (per-turn auth rides the process env via bearer_token_env_var, never the file),
|
|
621
|
+
// so concurrent same-home turns write identical bytes and the old cross-turn race is
|
|
622
|
+
// gone by construction (secure-obo-design §3.2).
|
|
623
|
+
const signature = `${AGENTIDE_CODEX_CONFIG_VERSION}:${routingType}:${customBaseUrl ?? ''}:${wireApi}:${agentId}:${codexInstructionsFile ?? ''}`;
|
|
605
624
|
if (this.lastWrittenConfigSignature === signature) {
|
|
606
625
|
return;
|
|
607
626
|
}
|
package/out/engines/custom.js
CHANGED
|
@@ -38,6 +38,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.CustomEngine = void 0;
|
|
40
40
|
exports.resolveCustomEngineLoadStrategy = resolveCustomEngineLoadStrategy;
|
|
41
|
+
exports.deliverModelRoutingEnv = deliverModelRoutingEnv;
|
|
42
|
+
exports.buildRunnerChildEnv = buildRunnerChildEnv;
|
|
41
43
|
exports.probeCustomEngineImport = probeCustomEngineImport;
|
|
42
44
|
const node_child_process_1 = require("node:child_process");
|
|
43
45
|
const node_readline_1 = require("node:readline");
|
|
@@ -83,10 +85,45 @@ function prependNodePath(env, engineDir) {
|
|
|
83
85
|
? `${engineNodeModules}${path.delimiter}${existing}`
|
|
84
86
|
: engineNodeModules;
|
|
85
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Deliver the per-turn model-routing values into the env vars a manifest declares
|
|
90
|
+
* (`model.apiKeyEnvVar` / `baseUrlEnvVar` / `openaiBaseUrlEnvVar`).
|
|
91
|
+
*
|
|
92
|
+
* NON-CLOBBERING: a var already set in the host's own ambient env is left alone. Two reasons:
|
|
93
|
+
* (1) it preserves the effective behavior deployments already relied on — the old
|
|
94
|
+
* `{...turn.env, ...process.env}` child-env spread accidentally let ambient values win,
|
|
95
|
+
* and operators' engine-owned keys (e.g. a real CURSOR_API_KEY vs a stale manifest that
|
|
96
|
+
* wrongly declared it as a delivery slot) must keep winning after that spread is fixed;
|
|
97
|
+
* (2) it makes the manifest contract safe by construction: mis-declaring an operator-owned
|
|
98
|
+
* credential as a delivery slot can shadow nothing.
|
|
99
|
+
* The flip side is documented on CustomEngineModelConfig: ambient-exporting a genuine
|
|
100
|
+
* delivery var (e.g. LITELLM_API_KEY in serve's shell) shadows per-turn rotation.
|
|
101
|
+
*/
|
|
102
|
+
function deliverModelRoutingEnv(env, manifest, routing, ambientEnv, logger) {
|
|
103
|
+
const deliver = (envVar, value, label) => {
|
|
104
|
+
if (!envVar || !value) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (ambientEnv[envVar]?.trim()) {
|
|
108
|
+
logger?.info(`[AgentIDE] Custom engine "${manifest.id}": ${envVar} already set in ambient env — keeping the operator's value, skipping per-turn ${label} delivery`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
env[envVar] = value;
|
|
112
|
+
};
|
|
113
|
+
deliver(manifest.model?.apiKeyEnvVar, routing.apiKey, 'API key');
|
|
114
|
+
deliver(manifest.model?.baseUrlEnvVar, routing.baseUrl, 'base URL');
|
|
115
|
+
deliver(manifest.model?.openaiBaseUrlEnvVar, routing.openaiBaseUrl, 'OpenAI base URL');
|
|
116
|
+
}
|
|
86
117
|
function buildRunnerChildEnv(manifest, turn, probeImport) {
|
|
87
118
|
const childEnv = {
|
|
88
|
-
|
|
119
|
+
// Per-turn values WIN over the host's ambient env. turn.env starts from a spread of
|
|
120
|
+
// process.env, so for undeclared vars this is identical either way; the difference is
|
|
121
|
+
// deliberate per-turn state (AGENTHIPPO_* control vars, the data-plane turn nonce,
|
|
122
|
+
// session paths, manifest-declared deliveries) which must never be reverted by a
|
|
123
|
+
// same-named var in the host's own environment. Ambient-vs-delivery precedence for
|
|
124
|
+
// manifest credential vars is decided earlier, in deliverModelRoutingEnv (non-clobber).
|
|
89
125
|
...process.env,
|
|
126
|
+
...turn.env,
|
|
90
127
|
AGENTHIPPO_ENGINE_ENTRY: manifest.entryPath,
|
|
91
128
|
AGENTHIPPO_MESSAGE: turn.message,
|
|
92
129
|
AGENTHIPPO_WORKSPACE_ROOT: turn.workspaceRoot,
|
|
@@ -466,7 +503,10 @@ class CustomEngine {
|
|
|
466
503
|
const attachmentsDir = path.join(workspaceSessionDir, 'attachments');
|
|
467
504
|
const nativeSessionId = this.activeSessions.get(conversationKey)?.nativeSessionId ??
|
|
468
505
|
(0, base_1.getPersistedSessionId)(`custom:${manifest.id}`, conversationKey);
|
|
469
|
-
|
|
506
|
+
// A custom engine isn't pinned to one vendor — infer its direct provider from the model
|
|
507
|
+
// (e.g. openclaw's litellm/* routes through the proxy anyway; a claude-* model → Anthropic).
|
|
508
|
+
// The engine's manifest still decides which env var receives the resolved key.
|
|
509
|
+
const routing = (0, utils_1.resolveApiRouting)(options, this.config, (0, utils_1.inferModelProvider)(options.modelId), runtime.configProvider);
|
|
470
510
|
const routingApiKey = routing.apiKey?.trim() || undefined;
|
|
471
511
|
const routingBaseUrl = routing.baseUrl?.trim() || undefined;
|
|
472
512
|
const routingOpenAIBaseUrl = routing.openaiBaseUrl?.trim() || undefined;
|
|
@@ -532,15 +572,7 @@ class CustomEngine {
|
|
|
532
572
|
if (manifest.homeEnvVar) {
|
|
533
573
|
env[manifest.homeEnvVar] = engineHomeDir;
|
|
534
574
|
}
|
|
535
|
-
|
|
536
|
-
env[manifest.model.apiKeyEnvVar] = routingApiKey;
|
|
537
|
-
}
|
|
538
|
-
if (manifest.model?.baseUrlEnvVar && routingBaseUrl) {
|
|
539
|
-
env[manifest.model.baseUrlEnvVar] = routingBaseUrl;
|
|
540
|
-
}
|
|
541
|
-
if (manifest.model?.openaiBaseUrlEnvVar && routingOpenAIBaseUrl) {
|
|
542
|
-
env[manifest.model.openaiBaseUrlEnvVar] = routingOpenAIBaseUrl;
|
|
543
|
-
}
|
|
575
|
+
deliverModelRoutingEnv(env, manifest, { apiKey: routingApiKey, baseUrl: routingBaseUrl, openaiBaseUrl: routingOpenAIBaseUrl }, process.env, runtime.logger);
|
|
544
576
|
if ((routingBaseUrl && (0, utils_1.applyLocalTlsBypass)(env, routingBaseUrl))
|
|
545
577
|
|| (routingOpenAIBaseUrl && routingOpenAIBaseUrl !== routingBaseUrl && (0, utils_1.applyLocalTlsBypass)(env, routingOpenAIBaseUrl))) {
|
|
546
578
|
runtime.logger.info(`[AgentIDE] Disabling TLS verification for local HTTPS (${manifest.id})`);
|
|
@@ -550,7 +582,9 @@ class CustomEngine {
|
|
|
550
582
|
: (routingBaseUrl ? 'custom provider' : 'native provider');
|
|
551
583
|
runtime.logger.info(`[AgentIDE] Custom engine "${manifest.id}" routing: ${routingDescription}, baseUrl=${routingBaseUrl || 'default'}, model=${modelId}`);
|
|
552
584
|
const resolvedMcp = await (0, mcp_1.loadMcpServers)(agentId, options.workspaceRoot, this.getBundledBasePromptsDir());
|
|
553
|
-
|
|
585
|
+
// `env` is this turn's child env — URL servers resolve bearerTokenEnv (turn nonce)
|
|
586
|
+
// from it in memory, so per-turn auth never lands in a shared file.
|
|
587
|
+
const mcpServers = (0, mcp_1.formatInlineMcpServers)(resolvedMcp, env);
|
|
554
588
|
if (resolvedMcp.length > 0) {
|
|
555
589
|
runtime.logger.debug(`[AgentIDE] Custom engine "${manifest.id}" MCP (${agentId}): ${resolvedMcp.map(s => s.name).join(', ')}`);
|
|
556
590
|
}
|