impel-cli 0.20.42 → 0.20.43
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/package.json +1 -1
- package/scripts/profile-native-codex.mjs +604 -57
- package/src/agents.js +196 -10
- package/src/cli.js +1 -0
- package/src/commands/agents.js +1 -0
- package/src/commands/launch.js +31 -7
- package/src/nativeAgentTransport.js +9 -2
- package/src/selfInvocation.js +20 -1
package/package.json
CHANGED
|
@@ -7,14 +7,22 @@ import os from "node:os";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
|
|
10
|
+
import {
|
|
11
|
+
MANAGED_AGENT_MCP_SERVER,
|
|
12
|
+
NATIVE_AGENT_ANSWER_TOOL,
|
|
13
|
+
nativeToolNamespace,
|
|
14
|
+
resolveManagedCodexAgentProfile,
|
|
15
|
+
} from "../src/agents.js";
|
|
10
16
|
import { nativeSpawnInvocation } from "../src/nativeProcess.js";
|
|
11
|
-
import { summarizeRawAttempt } from "./analyze-native-codex.mjs";
|
|
17
|
+
import { parseStrictJsonLines, summarizeRawAttempt } from "./analyze-native-codex.mjs";
|
|
12
18
|
|
|
13
19
|
const PROFILE_SCHEMA = "impel.native-codex-profile.v1";
|
|
14
20
|
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
15
21
|
const DEFAULT_GRACE_MS = 5_000;
|
|
16
22
|
const MAX_CAPTURE_BYTES = 256 * 1024 * 1024;
|
|
23
|
+
const MAX_CONFIG_BYTES = 1024 * 1024;
|
|
17
24
|
const TENANT_PREFLIGHT_RETRY_DELAYS_MS = [250, 750, 1_500];
|
|
25
|
+
const ISOLATED_PROFILE_PREFIX = "impel-native-codex-benchmark-";
|
|
18
26
|
|
|
19
27
|
function privateDirectory(directory) {
|
|
20
28
|
if (fs.existsSync(directory) && fs.lstatSync(directory).isSymbolicLink()) {
|
|
@@ -29,6 +37,16 @@ function privateWrite(filePath, contents) {
|
|
|
29
37
|
try { fs.chmodSync(filePath, 0o600); } catch { /* Best effort on Windows. */ }
|
|
30
38
|
}
|
|
31
39
|
|
|
40
|
+
function privateCopy(source, destination) {
|
|
41
|
+
const stat = fs.lstatSync(source);
|
|
42
|
+
if (!stat.isFile() || stat.size > MAX_CONFIG_BYTES) {
|
|
43
|
+
throw new Error("the selected Impel config must be a bounded regular file");
|
|
44
|
+
}
|
|
45
|
+
privateDirectory(path.dirname(destination));
|
|
46
|
+
fs.copyFileSync(source, destination, fs.constants.COPYFILE_EXCL);
|
|
47
|
+
try { fs.chmodSync(destination, 0o600); } catch { /* Best effort on Windows. */ }
|
|
48
|
+
}
|
|
49
|
+
|
|
32
50
|
function openPrivate(filePath) {
|
|
33
51
|
const descriptor = fs.openSync(filePath, "wx", 0o600);
|
|
34
52
|
try { fs.chmodSync(filePath, 0o600); } catch { /* Best effort on Windows. */ }
|
|
@@ -233,6 +251,350 @@ export async function assertExpectedTenant({ expectedTenant, impelBinary = "impe
|
|
|
233
251
|
return actual;
|
|
234
252
|
}
|
|
235
253
|
|
|
254
|
+
function selectedConfigPath(environment = process.env) {
|
|
255
|
+
const home = environment.HOME || environment.USERPROFILE || os.homedir();
|
|
256
|
+
const root = environment.IMPEL_CONFIG_DIR || path.join(home, ".config", "impel");
|
|
257
|
+
return path.join(root, "config.json");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function isolatedBenchmarkPaths(root, tenantId) {
|
|
261
|
+
const home = path.join(root, "home");
|
|
262
|
+
const configRoot = path.join(root, "config");
|
|
263
|
+
return {
|
|
264
|
+
root,
|
|
265
|
+
home,
|
|
266
|
+
configRoot,
|
|
267
|
+
codexHome: path.join(configRoot, "cli", "tenants", tenantId, "codex"),
|
|
268
|
+
nativeCodexHome: path.join(root, "native-codex"),
|
|
269
|
+
appHome: path.join(root, "apps"),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function isolatedBenchmarkEnvironment(options, paths, telemetryPath, baseEnvironment) {
|
|
274
|
+
return {
|
|
275
|
+
...baseEnvironment,
|
|
276
|
+
HOME: paths.home,
|
|
277
|
+
USERPROFILE: paths.home,
|
|
278
|
+
XDG_CONFIG_HOME: path.join(paths.home, ".config"),
|
|
279
|
+
IMPEL_CONFIG_DIR: paths.configRoot,
|
|
280
|
+
IMPEL_APP_HOME: paths.appHome,
|
|
281
|
+
// `impel codex` runs against the tenant's generated profile directory. An
|
|
282
|
+
// explicit CODEX_HOME overrides that derivation, so it must point at the
|
|
283
|
+
// isolated tenant profile; pointing it anywhere else starts Codex with no
|
|
284
|
+
// MCP servers, and the cohort silently measures model prose instead of
|
|
285
|
+
// agent latency.
|
|
286
|
+
CODEX_HOME: paths.codexHome,
|
|
287
|
+
IMPEL_SKIP_UPDATE_CHECK: "1",
|
|
288
|
+
IMPEL_NATIVE_AGENT_TELEMETRY_PATH: telemetryPath,
|
|
289
|
+
IMPEL_NATIVE_HOST: options.mode === "direct"
|
|
290
|
+
? "codex-direct-profile"
|
|
291
|
+
: "codex-compatible-agent",
|
|
292
|
+
IMPEL_NATIVE_HOST_BUILD: options.hostBuild,
|
|
293
|
+
IMPEL_NATIVE_BENCHMARK: "1",
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function profileEnvironmentEntry(contents, name, value) {
|
|
298
|
+
return contents.includes(`${JSON.stringify(name)} = ${JSON.stringify(value)}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function regularFileContents(filePath, missingMessage) {
|
|
302
|
+
try {
|
|
303
|
+
const stat = fs.lstatSync(filePath);
|
|
304
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error();
|
|
305
|
+
return fs.readFileSync(filePath, "utf8");
|
|
306
|
+
} catch {
|
|
307
|
+
throw new Error(missingMessage);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function tomlSection(contents, sectionName) {
|
|
312
|
+
const header = `[${sectionName}]`;
|
|
313
|
+
const lines = contents.split(/\r?\n/u);
|
|
314
|
+
const matches = lines.flatMap((line, index) => line.trim() === header ? [index] : []);
|
|
315
|
+
if (matches.length !== 1) return null;
|
|
316
|
+
const start = matches[0] + 1;
|
|
317
|
+
const relativeEnd = lines.slice(start).findIndex((line) => /^\s*\[/u.test(line));
|
|
318
|
+
return lines.slice(start, relativeEnd === -1 ? lines.length : start + relativeEnd).join("\n");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function tomlAssignment(section, name) {
|
|
322
|
+
if (section === null) return null;
|
|
323
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
324
|
+
const matches = [...section.matchAll(new RegExp(`^\\s*${escaped}\\s*=\\s*(.+?)\\s*$`, "gmu"))];
|
|
325
|
+
return matches.length === 1 ? matches[0][1] : null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function tomlStringList(value) {
|
|
329
|
+
if (typeof value !== "string" || !/^\s*\[.*\]\s*$/su.test(value)) return null;
|
|
330
|
+
try {
|
|
331
|
+
const parsed = JSON.parse(value);
|
|
332
|
+
return Array.isArray(parsed) && parsed.every((entry) => typeof entry === "string")
|
|
333
|
+
? parsed
|
|
334
|
+
: null;
|
|
335
|
+
} catch {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function assertSelectedProfileContract(contents, environment, policyFingerprint) {
|
|
341
|
+
const mcpSectionName = `mcp_servers.${MANAGED_AGENT_MCP_SERVER}`;
|
|
342
|
+
const mcpSection = tomlSection(contents, mcpSectionName);
|
|
343
|
+
if (mcpSection === null) {
|
|
344
|
+
throw new Error(`the isolated managed Codex agent profile has no [${mcpSectionName}] block`);
|
|
345
|
+
}
|
|
346
|
+
if (!tomlAssignment(mcpSection, "command") || !tomlAssignment(mcpSection, "args")) {
|
|
347
|
+
throw new Error("the isolated managed Codex agent profile has an incomplete MCP command binding");
|
|
348
|
+
}
|
|
349
|
+
if (tomlAssignment(mcpSection, "tool_timeout_sec") !== "120") {
|
|
350
|
+
throw new Error("the isolated managed Codex agent profile has no reviewed MCP tool timeout");
|
|
351
|
+
}
|
|
352
|
+
const enabledTools = tomlStringList(tomlAssignment(mcpSection, "enabled_tools"));
|
|
353
|
+
if (!enabledTools?.includes(NATIVE_AGENT_ANSWER_TOOL)) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
`the isolated managed Codex agent profile does not enable ${NATIVE_AGENT_ANSWER_TOOL}`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
const mcpEnvironment = tomlAssignment(mcpSection, "env");
|
|
359
|
+
for (const name of [
|
|
360
|
+
"IMPEL_NATIVE_AGENT_TELEMETRY_PATH",
|
|
361
|
+
"IMPEL_NATIVE_HOST",
|
|
362
|
+
"IMPEL_NATIVE_HOST_BUILD",
|
|
363
|
+
"IMPEL_NATIVE_BENCHMARK",
|
|
364
|
+
]) {
|
|
365
|
+
if (!profileEnvironmentEntry(mcpEnvironment || "", name, environment[name])) {
|
|
366
|
+
throw new Error(`the isolated managed Codex agent MCP env did not bake ${name}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const mcpArgs = tomlAssignment(mcpSection, "args") || "";
|
|
370
|
+
if (!mcpArgs.includes(JSON.stringify("--policy-fingerprint"))
|
|
371
|
+
|| !mcpArgs.includes(JSON.stringify(policyFingerprint))) {
|
|
372
|
+
throw new Error("the isolated managed Codex agent profile lost its policy fingerprint binding");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const codeModeSection = tomlSection(contents, "features.code_mode");
|
|
376
|
+
if (tomlAssignment(codeModeSection, "enabled") !== "true") {
|
|
377
|
+
throw new Error("the isolated managed Codex agent profile does not enable reviewed code mode");
|
|
378
|
+
}
|
|
379
|
+
const namespaces = tomlStringList(
|
|
380
|
+
tomlAssignment(codeModeSection, "direct_only_tool_namespaces"),
|
|
381
|
+
);
|
|
382
|
+
if (!namespaces?.includes(nativeToolNamespace())) {
|
|
383
|
+
throw new Error(
|
|
384
|
+
`the isolated managed Codex agent profile does not bind ${nativeToolNamespace()} as a direct-only namespace`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function verifyIsolatedBenchmarkProfile({
|
|
390
|
+
codexHome,
|
|
391
|
+
expectedTenant,
|
|
392
|
+
agent,
|
|
393
|
+
environment,
|
|
394
|
+
}) {
|
|
395
|
+
const manifestPath = path.join(codexHome, "agents", "impel-managed", ".manifest.json");
|
|
396
|
+
let manifest;
|
|
397
|
+
try {
|
|
398
|
+
const stat = fs.lstatSync(manifestPath);
|
|
399
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error();
|
|
400
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
401
|
+
} catch {
|
|
402
|
+
throw new Error("the isolated managed Codex agent manifest was not generated");
|
|
403
|
+
}
|
|
404
|
+
if (manifest?.client !== "codex"
|
|
405
|
+
|| manifest.tenantId !== expectedTenant
|
|
406
|
+
|| manifest.directProfiles !== true
|
|
407
|
+
|| manifest.directCodeMode !== true
|
|
408
|
+
|| manifest.nativeBenchmark !== true
|
|
409
|
+
|| !Array.isArray(manifest.files)
|
|
410
|
+
|| !Array.isArray(manifest.profiles)
|
|
411
|
+
|| !manifest.contentDigests
|
|
412
|
+
|| typeof manifest.contentDigests !== "object"
|
|
413
|
+
|| Array.isArray(manifest.contentDigests)
|
|
414
|
+
|| !manifest.nativeAgentTelemetryEnvironment
|
|
415
|
+
|| typeof manifest.nativeAgentTelemetryEnvironment !== "object"
|
|
416
|
+
|| Array.isArray(manifest.nativeAgentTelemetryEnvironment)) {
|
|
417
|
+
throw new Error("the isolated managed Codex agent manifest is missing its benchmark telemetry contract");
|
|
418
|
+
}
|
|
419
|
+
for (const name of [
|
|
420
|
+
"IMPEL_NATIVE_AGENT_TELEMETRY_PATH",
|
|
421
|
+
"IMPEL_NATIVE_HOST",
|
|
422
|
+
"IMPEL_NATIVE_HOST_BUILD",
|
|
423
|
+
]) {
|
|
424
|
+
if (manifest.nativeAgentTelemetryEnvironment[name] !== environment[name]) {
|
|
425
|
+
throw new Error(`the isolated managed Codex agent manifest did not retain ${name}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const matches = (Array.isArray(manifest.agents) ? manifest.agents : []).filter((record) =>
|
|
429
|
+
record
|
|
430
|
+
&& !record.retired
|
|
431
|
+
&& [record.agentId, record.title, record.profileName].includes(agent)
|
|
432
|
+
);
|
|
433
|
+
if (matches.length !== 1) {
|
|
434
|
+
throw new Error(`the isolated managed Codex agent ${JSON.stringify(agent)} was not generated exactly once`);
|
|
435
|
+
}
|
|
436
|
+
const [record] = matches;
|
|
437
|
+
if (!/^[a-f0-9]{64}$/u.test(record.policyFingerprint || "")) {
|
|
438
|
+
throw new Error("the isolated managed Codex agent manifest has no policy fingerprint");
|
|
439
|
+
}
|
|
440
|
+
let resolved;
|
|
441
|
+
try {
|
|
442
|
+
resolved = resolveManagedCodexAgentProfile(codexHome, expectedTenant, agent);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
throw new Error(`the isolated managed Codex agent profile failed its managed integrity check: ${error.message}`);
|
|
445
|
+
}
|
|
446
|
+
const profileFileName = resolved.profileFileName;
|
|
447
|
+
if (typeof profileFileName !== "string"
|
|
448
|
+
|| path.basename(profileFileName) !== profileFileName
|
|
449
|
+
|| !profileFileName.endsWith(".config.toml")) {
|
|
450
|
+
throw new Error("the isolated managed Codex agent profile mapping is invalid");
|
|
451
|
+
}
|
|
452
|
+
const profilePath = path.join(codexHome, profileFileName);
|
|
453
|
+
const contents = regularFileContents(
|
|
454
|
+
profilePath,
|
|
455
|
+
"the isolated managed Codex agent profile was not written as a regular file",
|
|
456
|
+
);
|
|
457
|
+
const definitionFileName = `${resolved.profileName}.toml`;
|
|
458
|
+
if (!manifest.files.includes(definitionFileName)) {
|
|
459
|
+
throw new Error("the isolated managed Codex agent definition is absent from its manifest");
|
|
460
|
+
}
|
|
461
|
+
const definitionPath = path.join(codexHome, "agents", definitionFileName);
|
|
462
|
+
const definitionContents = regularFileContents(
|
|
463
|
+
definitionPath,
|
|
464
|
+
"the isolated managed Codex agent definition was not written as a regular file",
|
|
465
|
+
);
|
|
466
|
+
if (manifest.contentDigests?.[`agents/${definitionFileName}`]
|
|
467
|
+
!== crypto.createHash("sha256").update(fs.readFileSync(definitionPath)).digest("hex")) {
|
|
468
|
+
throw new Error("the isolated managed Codex agent definition failed its managed integrity check");
|
|
469
|
+
}
|
|
470
|
+
assertSelectedProfileContract(contents, environment, record.policyFingerprint);
|
|
471
|
+
try {
|
|
472
|
+
assertSelectedProfileContract(definitionContents, environment, record.policyFingerprint);
|
|
473
|
+
} catch (error) {
|
|
474
|
+
throw new Error(error.message.replace("agent profile", "agent definition"));
|
|
475
|
+
}
|
|
476
|
+
return { manifestPath, profilePath, profileFileName, definitionPath };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function generateIsolatedBenchmarkProfile(options, isolation) {
|
|
480
|
+
// `managedSkillProfiles` only emits the "Impel isolated Codex" target when a
|
|
481
|
+
// tenant is selected AND its profile directory already exists, so a fresh
|
|
482
|
+
// isolated root would otherwise generate the native profile only — leaving
|
|
483
|
+
// Codex with no impel_agent MCP server and the cohort measuring model prose.
|
|
484
|
+
const select = impelInvocation(options.impelBinary, [
|
|
485
|
+
"tenant",
|
|
486
|
+
"use",
|
|
487
|
+
options.expectedTenant,
|
|
488
|
+
], isolation.environment);
|
|
489
|
+
const selected = await runCaptured(select.command, select.args, {
|
|
490
|
+
environment: isolation.environment,
|
|
491
|
+
});
|
|
492
|
+
if (selected.code !== 0) {
|
|
493
|
+
throw new Error(
|
|
494
|
+
`\`impel tenant use ${options.expectedTenant}\` exited with status ${selected.code ?? "unknown"}`,
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
fs.mkdirSync(isolation.codexHome, { recursive: true, mode: 0o700 });
|
|
498
|
+
|
|
499
|
+
const invocation = impelInvocation(options.impelBinary, [
|
|
500
|
+
"agents",
|
|
501
|
+
"sync",
|
|
502
|
+
"codex",
|
|
503
|
+
"--skip-apps",
|
|
504
|
+
], isolation.environment);
|
|
505
|
+
const result = await runCaptured(invocation.command, invocation.args, {
|
|
506
|
+
environment: isolation.environment,
|
|
507
|
+
});
|
|
508
|
+
if (result.code !== 0) {
|
|
509
|
+
throw new Error(`\`impel agents sync codex\` exited with status ${result.code ?? "unknown"}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Run a cohort with the CLI's existing IMPEL_CONFIG_DIR override pointed at a
|
|
515
|
+
* private temporary root. Only config.json is copied in for authentication;
|
|
516
|
+
* no managed profile from the operator's config root is read or written.
|
|
517
|
+
*/
|
|
518
|
+
export async function withIsolatedBenchmarkProfile(options, sessionDir, callback, {
|
|
519
|
+
baseEnvironment = process.env,
|
|
520
|
+
operatorConfigPath = selectedConfigPath(baseEnvironment),
|
|
521
|
+
prepareProfile = generateIsolatedBenchmarkProfile,
|
|
522
|
+
assertTenant = assertExpectedTenant,
|
|
523
|
+
stderr = process.stderr,
|
|
524
|
+
temporaryParent = os.tmpdir(),
|
|
525
|
+
} = {}) {
|
|
526
|
+
const root = fs.mkdtempSync(path.join(temporaryParent, ISOLATED_PROFILE_PREFIX));
|
|
527
|
+
const paths = isolatedBenchmarkPaths(root, options.expectedTenant);
|
|
528
|
+
const telemetryPath = path.join(sessionDir, "telemetry.jsonl");
|
|
529
|
+
const environment = isolatedBenchmarkEnvironment(
|
|
530
|
+
options,
|
|
531
|
+
paths,
|
|
532
|
+
telemetryPath,
|
|
533
|
+
baseEnvironment,
|
|
534
|
+
);
|
|
535
|
+
let removed = false;
|
|
536
|
+
const removeRoot = () => {
|
|
537
|
+
if (removed) return;
|
|
538
|
+
removed = true;
|
|
539
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
540
|
+
};
|
|
541
|
+
// Synchronous exit cleanup also covers Ctrl-C/termination paths where the
|
|
542
|
+
// event loop cannot continue far enough to unwind the async callback.
|
|
543
|
+
process.once("exit", removeRoot);
|
|
544
|
+
let prepared = false;
|
|
545
|
+
try {
|
|
546
|
+
privateDirectory(paths.home);
|
|
547
|
+
privateDirectory(paths.configRoot);
|
|
548
|
+
stderr.write(
|
|
549
|
+
`Using isolated benchmark profile ${paths.codexHome}; cohort telemetry: ${telemetryPath}\n`,
|
|
550
|
+
);
|
|
551
|
+
try {
|
|
552
|
+
privateCopy(operatorConfigPath, path.join(paths.configRoot, "config.json"));
|
|
553
|
+
// Explicit sync discovers initialized isolated CLI roots. Creating only
|
|
554
|
+
// this empty destination lets the production sync command generate the
|
|
555
|
+
// complete agent catalog without launching Codex or touching a live
|
|
556
|
+
// profile.
|
|
557
|
+
privateDirectory(paths.codexHome);
|
|
558
|
+
await assertTenant({
|
|
559
|
+
expectedTenant: options.expectedTenant,
|
|
560
|
+
impelBinary: options.impelBinary,
|
|
561
|
+
environment,
|
|
562
|
+
});
|
|
563
|
+
await prepareProfile(options, {
|
|
564
|
+
...paths,
|
|
565
|
+
telemetryPath,
|
|
566
|
+
environment,
|
|
567
|
+
});
|
|
568
|
+
const verified = verifyIsolatedBenchmarkProfile({
|
|
569
|
+
codexHome: paths.codexHome,
|
|
570
|
+
expectedTenant: options.expectedTenant,
|
|
571
|
+
agent: options.agent,
|
|
572
|
+
environment,
|
|
573
|
+
});
|
|
574
|
+
// Profile preparation must not contribute any events to the measured
|
|
575
|
+
// cohort. The generated profile keeps pointing at this same path.
|
|
576
|
+
fs.rmSync(telemetryPath, { force: true });
|
|
577
|
+
prepared = true;
|
|
578
|
+
return await callback({
|
|
579
|
+
...paths,
|
|
580
|
+
...verified,
|
|
581
|
+
telemetryPath,
|
|
582
|
+
environment,
|
|
583
|
+
});
|
|
584
|
+
} catch (error) {
|
|
585
|
+
if (prepared) throw error;
|
|
586
|
+
throw new Error(
|
|
587
|
+
`could not generate the isolated benchmark profile for tenant ${JSON.stringify(options.expectedTenant)}: `
|
|
588
|
+
+ `${error?.message || error}. Verify Impel auth, network access, the selected tenant, and `
|
|
589
|
+
+ "that `impel agents sync codex` can fetch this tenant's agent catalog.",
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
} finally {
|
|
593
|
+
process.off("exit", removeRoot);
|
|
594
|
+
removeRoot();
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
236
598
|
function listRollouts(root, startedAtMs, endedAtMs) {
|
|
237
599
|
if (!fs.existsSync(root)) return [];
|
|
238
600
|
const results = [];
|
|
@@ -439,35 +801,141 @@ export function safeAttemptRecord({ options, index, processResult, summary, json
|
|
|
439
801
|
};
|
|
440
802
|
}
|
|
441
803
|
|
|
804
|
+
export function profiledCodexAttemptOptions(options, telemetryPath, environment = process.env) {
|
|
805
|
+
return {
|
|
806
|
+
codexArgs: [
|
|
807
|
+
"codex",
|
|
808
|
+
"--benchmark",
|
|
809
|
+
...(options.mode === "direct" ? ["--agent", options.agent] : []),
|
|
810
|
+
"exec",
|
|
811
|
+
"--json",
|
|
812
|
+
...options.extraArgs,
|
|
813
|
+
"-",
|
|
814
|
+
],
|
|
815
|
+
environment: {
|
|
816
|
+
...environment,
|
|
817
|
+
IMPEL_NATIVE_AGENT_TELEMETRY_PATH: telemetryPath,
|
|
818
|
+
IMPEL_NATIVE_HOST: options.mode === "direct" ? "codex-direct-profile" : "codex-compatible-agent",
|
|
819
|
+
IMPEL_NATIVE_HOST_BUILD: options.hostBuild,
|
|
820
|
+
IMPEL_NATIVE_BENCHMARK: "1",
|
|
821
|
+
},
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function attemptTelemetryWindow(attempt, index) {
|
|
826
|
+
const startedAtMs = attempt?.processResult?.startedAtMs ?? Date.parse(attempt?.startedAt || "");
|
|
827
|
+
const endedAtMs = attempt?.processResult?.endedAtMs ?? Date.parse(attempt?.endedAt || "");
|
|
828
|
+
if (!Number.isFinite(startedAtMs) || !Number.isFinite(endedAtMs) || endedAtMs < startedAtMs) {
|
|
829
|
+
throw new Error(`attempt ${index + 1} has an invalid telemetry window`);
|
|
830
|
+
}
|
|
831
|
+
return { index, startedAtMs, endedAtMs };
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function eventTimestampMs(event) {
|
|
835
|
+
const value = Date.parse(event?.timestamp || "");
|
|
836
|
+
return Number.isFinite(value) ? value : null;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function windowOwner(windows, timestampMs) {
|
|
840
|
+
if (timestampMs === null) return null;
|
|
841
|
+
const candidates = windows.filter(({ startedAtMs, endedAtMs }) =>
|
|
842
|
+
timestampMs >= startedAtMs && timestampMs <= endedAtMs
|
|
843
|
+
);
|
|
844
|
+
if (candidates.length === 0) return null;
|
|
845
|
+
// Concurrent attempts can overlap. Prefer the attempt whose process start is
|
|
846
|
+
// closest to the event, then use input order as a stable tie breaker.
|
|
847
|
+
return candidates.sort((left, right) =>
|
|
848
|
+
(timestampMs - left.startedAtMs) - (timestampMs - right.startedAtMs)
|
|
849
|
+
|| left.index - right.index
|
|
850
|
+
)[0].index;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Attribute a shared cohort telemetry stream without duplicating events.
|
|
855
|
+
* A correlation's first timestamp anchors the whole correlation to one
|
|
856
|
+
* attempt; records without a correlation ID fall back to attempt windows.
|
|
857
|
+
*/
|
|
858
|
+
export function splitCohortTelemetryEvents(attempts, telemetryText) {
|
|
859
|
+
const windows = attempts.map(attemptTelemetryWindow);
|
|
860
|
+
const split = attempts.map(() => []);
|
|
861
|
+
const events = telemetryText.trim()
|
|
862
|
+
? parseStrictJsonLines(telemetryText, "native-agent cohort telemetry")
|
|
863
|
+
: [];
|
|
864
|
+
const correlationOwner = new Map();
|
|
865
|
+
for (const event of events) {
|
|
866
|
+
if (typeof event.correlationId !== "string" || !event.correlationId) continue;
|
|
867
|
+
if (!correlationOwner.has(event.correlationId)) {
|
|
868
|
+
correlationOwner.set(
|
|
869
|
+
event.correlationId,
|
|
870
|
+
windowOwner(windows, eventTimestampMs(event)),
|
|
871
|
+
);
|
|
872
|
+
} else if (correlationOwner.get(event.correlationId) === null) {
|
|
873
|
+
const owner = windowOwner(windows, eventTimestampMs(event));
|
|
874
|
+
if (owner !== null) correlationOwner.set(event.correlationId, owner);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
for (const event of events) {
|
|
878
|
+
const owner = typeof event.correlationId === "string" && event.correlationId
|
|
879
|
+
? correlationOwner.get(event.correlationId) ?? null
|
|
880
|
+
: windowOwner(windows, eventTimestampMs(event));
|
|
881
|
+
if (owner !== null) split[owner].push(event);
|
|
882
|
+
}
|
|
883
|
+
return split;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
export function finalizeProfileAttempts(attempts, telemetryText) {
|
|
887
|
+
const telemetryByAttempt = splitCohortTelemetryEvents(attempts, telemetryText);
|
|
888
|
+
return attempts.map((attempt, attemptIndex) => {
|
|
889
|
+
if (attempt?.profileAttempt !== true) return attempt;
|
|
890
|
+
const attributedTelemetryText = telemetryByAttempt[attemptIndex]
|
|
891
|
+
.map((event) => JSON.stringify(event))
|
|
892
|
+
.join("\n");
|
|
893
|
+
let summary = null;
|
|
894
|
+
let parseError = null;
|
|
895
|
+
try {
|
|
896
|
+
summary = summarizeRawAttempt({
|
|
897
|
+
stdoutText: attempt.stdoutText,
|
|
898
|
+
telemetryText: attributedTelemetryText,
|
|
899
|
+
rolloutTexts: attempt.rolloutTexts,
|
|
900
|
+
});
|
|
901
|
+
} catch (error) {
|
|
902
|
+
parseError = error;
|
|
903
|
+
}
|
|
904
|
+
const record = safeAttemptRecord({
|
|
905
|
+
options: attempt.options,
|
|
906
|
+
index: attempt.index,
|
|
907
|
+
processResult: attempt.processResult,
|
|
908
|
+
summary,
|
|
909
|
+
jsonValid: parseError === null,
|
|
910
|
+
parseError,
|
|
911
|
+
});
|
|
912
|
+
privateWrite(path.join(attempt.attemptDir, "record.json"), `${JSON.stringify(record, null, 2)}\n`);
|
|
913
|
+
return record;
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
|
|
442
917
|
async function profileAttempt(options, sessionDir, index) {
|
|
918
|
+
const environment = options.environment || process.env;
|
|
443
919
|
await assertExpectedTenant({
|
|
444
920
|
expectedTenant: options.expectedTenant,
|
|
445
921
|
impelBinary: options.impelBinary,
|
|
446
|
-
environment
|
|
922
|
+
environment,
|
|
447
923
|
});
|
|
448
924
|
const attemptName = `${options.cohort}-${String(index + 1 + options.attemptOffset).padStart(3, "0")}`;
|
|
449
925
|
const attemptDir = path.join(sessionDir, attemptName);
|
|
450
926
|
privateDirectory(attemptDir);
|
|
451
927
|
const stdoutPath = path.join(attemptDir, "stdout.jsonl");
|
|
452
928
|
const stderrPath = path.join(attemptDir, "stderr.log");
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
"-",
|
|
461
|
-
];
|
|
462
|
-
const invocation = impelInvocation(options.impelBinary, codexArgs, process.env);
|
|
929
|
+
const attemptOptions = profiledCodexAttemptOptions(
|
|
930
|
+
options,
|
|
931
|
+
options.telemetryPath,
|
|
932
|
+
environment,
|
|
933
|
+
);
|
|
934
|
+
const { codexArgs } = attemptOptions;
|
|
935
|
+
const invocation = impelInvocation(options.impelBinary, codexArgs, attemptOptions.environment);
|
|
463
936
|
const processResult = await runProfiledProcess({
|
|
464
937
|
...invocation,
|
|
465
|
-
environment:
|
|
466
|
-
...process.env,
|
|
467
|
-
IMPEL_NATIVE_AGENT_TELEMETRY_PATH: telemetryPath,
|
|
468
|
-
IMPEL_NATIVE_HOST: options.mode === "direct" ? "codex-direct-profile" : "codex-compatible-agent",
|
|
469
|
-
IMPEL_NATIVE_HOST_BUILD: options.hostBuild,
|
|
470
|
-
},
|
|
938
|
+
environment: attemptOptions.environment,
|
|
471
939
|
input: options.prompt,
|
|
472
940
|
stdoutPath,
|
|
473
941
|
stderrPath,
|
|
@@ -481,27 +949,15 @@ async function profileAttempt(options, sessionDir, index) {
|
|
|
481
949
|
stdoutText,
|
|
482
950
|
destination: path.join(attemptDir, "rollouts"),
|
|
483
951
|
});
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
try {
|
|
487
|
-
summary = summarizeRawAttempt({
|
|
488
|
-
stdoutText,
|
|
489
|
-
telemetryText: fs.existsSync(telemetryPath) ? fs.readFileSync(telemetryPath, "utf8") : "",
|
|
490
|
-
rolloutTexts: rolloutPaths.map((rolloutPath) => fs.readFileSync(rolloutPath, "utf8")),
|
|
491
|
-
});
|
|
492
|
-
} catch (error) {
|
|
493
|
-
parseError = error;
|
|
494
|
-
}
|
|
495
|
-
const record = safeAttemptRecord({
|
|
952
|
+
return {
|
|
953
|
+
profileAttempt: true,
|
|
496
954
|
options,
|
|
497
955
|
index,
|
|
956
|
+
attemptDir,
|
|
498
957
|
processResult,
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
});
|
|
503
|
-
privateWrite(path.join(attemptDir, "record.json"), `${JSON.stringify(record, null, 2)}\n`);
|
|
504
|
-
return record;
|
|
958
|
+
stdoutText,
|
|
959
|
+
rolloutTexts: rolloutPaths.map((rolloutPath) => fs.readFileSync(rolloutPath, "utf8")),
|
|
960
|
+
};
|
|
505
961
|
}
|
|
506
962
|
|
|
507
963
|
function profilerFailureClass(error) {
|
|
@@ -514,16 +970,20 @@ function profilerFailureClass(error) {
|
|
|
514
970
|
return "attempt-infrastructure";
|
|
515
971
|
}
|
|
516
972
|
|
|
517
|
-
export async function runWorkers(options, sessionDir, profileAttemptImpl = profileAttempt
|
|
973
|
+
export async function runWorkers(options, sessionDir, profileAttemptImpl = profileAttempt, {
|
|
974
|
+
startIndex = 0,
|
|
975
|
+
endIndex = options.attempts,
|
|
976
|
+
concurrency = options.concurrency,
|
|
977
|
+
} = {}) {
|
|
518
978
|
const results = new Array(options.attempts);
|
|
519
|
-
let next =
|
|
979
|
+
let next = startIndex;
|
|
520
980
|
let abort = null;
|
|
521
981
|
const worker = async () => {
|
|
522
982
|
for (;;) {
|
|
523
983
|
if (abort) return;
|
|
524
984
|
const index = next;
|
|
525
985
|
next += 1;
|
|
526
|
-
if (index >=
|
|
986
|
+
if (index >= endIndex) return;
|
|
527
987
|
try {
|
|
528
988
|
results[index] = await profileAttemptImpl(options, sessionDir, index);
|
|
529
989
|
} catch (error) {
|
|
@@ -534,10 +994,98 @@ export async function runWorkers(options, sessionDir, profileAttemptImpl = profi
|
|
|
534
994
|
}
|
|
535
995
|
}
|
|
536
996
|
};
|
|
537
|
-
|
|
997
|
+
const workerCount = Math.min(concurrency, Math.max(0, endIndex - startIndex));
|
|
998
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
538
999
|
return { attempts: results.filter(Boolean), abort };
|
|
539
1000
|
}
|
|
540
1001
|
|
|
1002
|
+
const MISSING_MANAGED_AGENT_MESSAGE =
|
|
1003
|
+
"the isolated profile did not expose the managed agent tool; the cohort would measure model prose, not agent latency";
|
|
1004
|
+
|
|
1005
|
+
export function firstAttemptIntegrityAbort(record) {
|
|
1006
|
+
const answerCalls = record?.toolCounts?.[NATIVE_AGENT_ANSWER_TOOL] || 0;
|
|
1007
|
+
const terminalResults = record?.terminalResults || 0;
|
|
1008
|
+
if (answerCalls > 0 && terminalResults > 0) return null;
|
|
1009
|
+
return {
|
|
1010
|
+
failureClass: "managed-agent-integrity",
|
|
1011
|
+
attemptId: record?.attemptId || null,
|
|
1012
|
+
message: `${MISSING_MANAGED_AGENT_MESSAGE} (first attempt: ${answerCalls} ${NATIVE_AGENT_ANSWER_TOOL} call${answerCalls === 1 ? "" : "s"}, ${terminalResults} terminal result${terminalResults === 1 ? "" : "s"}); aborting remaining attempts`,
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function readTelemetryText(telemetryPath) {
|
|
1017
|
+
return fs.existsSync(telemetryPath) ? fs.readFileSync(telemetryPath, "utf8") : "";
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* The first attempt is deliberately run alone. Until its rollout proves that
|
|
1022
|
+
* the fixed answer tool reached a terminal result, starting concurrent work
|
|
1023
|
+
* would risk collecting a whole cohort of plausible-looking model prose.
|
|
1024
|
+
*/
|
|
1025
|
+
export async function runGuardedCohort(options, sessionDir, {
|
|
1026
|
+
profileAttemptImpl = profileAttempt,
|
|
1027
|
+
readTelemetry = readTelemetryText,
|
|
1028
|
+
} = {}) {
|
|
1029
|
+
const first = await runWorkers(options, sessionDir, profileAttemptImpl, {
|
|
1030
|
+
startIndex: 0,
|
|
1031
|
+
endIndex: 1,
|
|
1032
|
+
concurrency: 1,
|
|
1033
|
+
});
|
|
1034
|
+
const firstRecords = finalizeProfileAttempts(
|
|
1035
|
+
first.attempts,
|
|
1036
|
+
readTelemetry(options.telemetryPath),
|
|
1037
|
+
);
|
|
1038
|
+
if (first.abort) return { attempts: firstRecords, abort: first.abort };
|
|
1039
|
+
const integrityAbort = firstAttemptIntegrityAbort(firstRecords[0]);
|
|
1040
|
+
if (integrityAbort) return { attempts: firstRecords, abort: integrityAbort };
|
|
1041
|
+
if (options.attempts === 1) return { attempts: firstRecords, abort: null };
|
|
1042
|
+
|
|
1043
|
+
const remaining = await runWorkers(options, sessionDir, profileAttemptImpl, {
|
|
1044
|
+
startIndex: 1,
|
|
1045
|
+
endIndex: options.attempts,
|
|
1046
|
+
concurrency: options.concurrency,
|
|
1047
|
+
});
|
|
1048
|
+
const remainingRecords = finalizeProfileAttempts(
|
|
1049
|
+
remaining.attempts,
|
|
1050
|
+
readTelemetry(options.telemetryPath),
|
|
1051
|
+
);
|
|
1052
|
+
return {
|
|
1053
|
+
attempts: [...firstRecords, ...remainingRecords],
|
|
1054
|
+
abort: remaining.abort,
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
export function buildProfileAggregate({ sessionId, generatedAt, options, result }) {
|
|
1059
|
+
const telemetryCaptured = result.attempts.some(
|
|
1060
|
+
(attempt) => (attempt.telemetryEventCount || 0) > 0,
|
|
1061
|
+
);
|
|
1062
|
+
return {
|
|
1063
|
+
schema: PROFILE_SCHEMA,
|
|
1064
|
+
sessionId,
|
|
1065
|
+
generatedAt,
|
|
1066
|
+
tenantId: options.expectedTenant,
|
|
1067
|
+
sloClass: options.sloClass,
|
|
1068
|
+
mode: options.mode,
|
|
1069
|
+
cohort: options.cohort,
|
|
1070
|
+
cliVersion: options.hostBuild,
|
|
1071
|
+
promptSha256: options.promptSha256,
|
|
1072
|
+
complete: result.abort === null && result.attempts.length === options.attempts,
|
|
1073
|
+
telemetryCaptured,
|
|
1074
|
+
expectedAttempts: options.attempts,
|
|
1075
|
+
abort: result.abort,
|
|
1076
|
+
attempts: result.attempts,
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
export function telemetryCaptureWarning(aggregate) {
|
|
1081
|
+
const terminalResults = aggregate.attempts.reduce(
|
|
1082
|
+
(total, attempt) => total + (attempt.terminalResults || 0),
|
|
1083
|
+
0,
|
|
1084
|
+
);
|
|
1085
|
+
if (!aggregate.complete || aggregate.telemetryCaptured || terminalResults === 0) return null;
|
|
1086
|
+
return "WARNING: the cohort completed with managed-agent terminal results but captured zero telemetry events; aggregate telemetryCaptured=false";
|
|
1087
|
+
}
|
|
1088
|
+
|
|
541
1089
|
async function restoreTenant(originalTenant, options) {
|
|
542
1090
|
let actual;
|
|
543
1091
|
try { actual = await currentTenant(options.impelBinary, process.env); } catch { return false; }
|
|
@@ -572,8 +1120,6 @@ async function main(argv) {
|
|
|
572
1120
|
throw new Error("could not establish the Impel CLI build under test");
|
|
573
1121
|
}
|
|
574
1122
|
options.hostBuild = version.stdout.trim();
|
|
575
|
-
const configRoot = process.env.IMPEL_CONFIG_DIR || path.join(os.homedir(), ".config", "impel");
|
|
576
|
-
options.rolloutRoot = path.join(configRoot, "cli", "tenants", options.expectedTenant, "codex", "sessions");
|
|
577
1123
|
const outputRoot = path.resolve(options.outputDir);
|
|
578
1124
|
privateDirectory(outputRoot);
|
|
579
1125
|
const sessionId = `codex-${new Date().toISOString().replace(/[:.]/gu, "-")}-${crypto.randomBytes(4).toString("hex")}`;
|
|
@@ -581,28 +1127,29 @@ async function main(argv) {
|
|
|
581
1127
|
privateDirectory(sessionDir);
|
|
582
1128
|
let restored = false;
|
|
583
1129
|
try {
|
|
584
|
-
const result = await
|
|
585
|
-
|
|
586
|
-
|
|
1130
|
+
const result = await withIsolatedBenchmarkProfile(options, sessionDir, async (isolation) => {
|
|
1131
|
+
options.environment = isolation.environment;
|
|
1132
|
+
options.telemetryPath = isolation.telemetryPath;
|
|
1133
|
+
options.rolloutRoot = path.join(isolation.codexHome, "sessions");
|
|
1134
|
+
return runGuardedCohort(options, sessionDir);
|
|
1135
|
+
});
|
|
1136
|
+
const aggregate = buildProfileAggregate({
|
|
587
1137
|
sessionId,
|
|
588
1138
|
generatedAt: new Date().toISOString(),
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
cohort: options.cohort,
|
|
593
|
-
cliVersion: options.hostBuild,
|
|
594
|
-
promptSha256: options.promptSha256,
|
|
595
|
-
complete: result.abort === null && result.attempts.length === options.attempts,
|
|
596
|
-
expectedAttempts: options.attempts,
|
|
597
|
-
abort: result.abort,
|
|
598
|
-
attempts: result.attempts,
|
|
599
|
-
};
|
|
1139
|
+
options,
|
|
1140
|
+
result,
|
|
1141
|
+
});
|
|
600
1142
|
const aggregatePath = path.join(sessionDir, "aggregate.json");
|
|
601
1143
|
privateWrite(aggregatePath, `${JSON.stringify(aggregate, null, 2)}\n`);
|
|
602
1144
|
process.stderr.write(`Private Codex profile written under ${sessionDir}\n`);
|
|
1145
|
+
const telemetryWarning = telemetryCaptureWarning(aggregate);
|
|
1146
|
+
if (telemetryWarning) process.stderr.write(`${telemetryWarning}\n`);
|
|
603
1147
|
process.stdout.write(`${JSON.stringify(aggregate)}\n`);
|
|
604
1148
|
if (!aggregate.complete) {
|
|
605
|
-
|
|
1149
|
+
const detail = aggregate.abort?.message ? `: ${aggregate.abort.message}` : "";
|
|
1150
|
+
throw new Error(
|
|
1151
|
+
`cohort aborted (${aggregate.abort?.failureClass || "incomplete"})${detail}; partial aggregate retained`,
|
|
1152
|
+
);
|
|
606
1153
|
}
|
|
607
1154
|
} finally {
|
|
608
1155
|
restored = await restoreTenant(originalTenant, options);
|
package/src/agents.js
CHANGED
|
@@ -21,8 +21,10 @@ import {
|
|
|
21
21
|
IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_ENV,
|
|
22
22
|
IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES,
|
|
23
23
|
IMPEL_NATIVE_AGENT_MCP_TARGET,
|
|
24
|
+
IMPEL_NATIVE_BENCHMARK_ENV,
|
|
24
25
|
impelCliInvocation,
|
|
25
26
|
impelNativeAgentMcpInvocation,
|
|
27
|
+
nativeBenchmarkHeaderValue,
|
|
26
28
|
} from "./selfInvocation.js";
|
|
27
29
|
import {
|
|
28
30
|
adapterAnswerFaithfulCompletionGuidance,
|
|
@@ -670,6 +672,116 @@ function nativeToolName(toolName) {
|
|
|
670
672
|
return `${nativeToolNamespace()}__${toolName}`;
|
|
671
673
|
}
|
|
672
674
|
|
|
675
|
+
const CLAUDE_PARENT_DIRECT_TOOL_PERMISSIONS = Object.freeze([
|
|
676
|
+
NATIVE_AGENT_ANSWER_TOOL,
|
|
677
|
+
NATIVE_AGENT_RESUME_TOOL,
|
|
678
|
+
].map((tool) => nativeToolName(tool)));
|
|
679
|
+
|
|
680
|
+
function parentDirectPermissionTools(renderedAgents) {
|
|
681
|
+
const trusted = new Set(CLAUDE_PARENT_DIRECT_TOOL_PERMISSIONS);
|
|
682
|
+
const tools = [...new Set(renderedAgents.flatMap((agent) =>
|
|
683
|
+
agent.parentLaunchDefinition?.tools || []
|
|
684
|
+
))];
|
|
685
|
+
if (!tools.every((tool) => trusted.has(tool))) {
|
|
686
|
+
throw new Error("the managed Claude parent-direct permission grant is invalid");
|
|
687
|
+
}
|
|
688
|
+
return tools;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function priorParentDirectPermissionGrants(prior, ownsManagedProfile) {
|
|
692
|
+
const grants = ownsManagedProfile ? prior?.nativeParentDirectPermissionGrants : null;
|
|
693
|
+
const trusted = new Set(CLAUDE_PARENT_DIRECT_TOOL_PERMISSIONS);
|
|
694
|
+
if (!Array.isArray(grants)
|
|
695
|
+
|| new Set(grants).size !== grants.length
|
|
696
|
+
|| !grants.every((tool) => trusted.has(tool))) return [];
|
|
697
|
+
return [...grants];
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function readClaudeSettingsForPermissions(settingsPath) {
|
|
701
|
+
let raw;
|
|
702
|
+
try {
|
|
703
|
+
const stat = fs.lstatSync(settingsPath);
|
|
704
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
705
|
+
throw new Error(`refusing to update unsafe Claude settings path ${settingsPath}`);
|
|
706
|
+
}
|
|
707
|
+
raw = fs.readFileSync(settingsPath, "utf8");
|
|
708
|
+
} catch (error) {
|
|
709
|
+
if (error?.code === "ENOENT") return { settings: {} };
|
|
710
|
+
throw error;
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
const settings = raw.trim() ? JSON.parse(raw) : {};
|
|
714
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) throw new Error();
|
|
715
|
+
return { settings };
|
|
716
|
+
} catch {
|
|
717
|
+
throw new Error(`${settingsPath} exists but isn't a valid JSON object. Fix or remove it, then re-run.`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function syncClaudeParentDirectPermissions(root, desiredTools, priorGrants) {
|
|
722
|
+
if (desiredTools.length === 0 && priorGrants.length === 0) return [];
|
|
723
|
+
|
|
724
|
+
const settingsPath = path.join(root, "settings.json");
|
|
725
|
+
const { settings } = readClaudeSettingsForPermissions(settingsPath);
|
|
726
|
+
const permissionsExisted = Object.hasOwn(settings, "permissions");
|
|
727
|
+
if (permissionsExisted && (
|
|
728
|
+
!settings.permissions
|
|
729
|
+
|| typeof settings.permissions !== "object"
|
|
730
|
+
|| Array.isArray(settings.permissions)
|
|
731
|
+
)) {
|
|
732
|
+
throw new Error(`${settingsPath} has an invalid permissions value. Fix or remove it, then re-run.`);
|
|
733
|
+
}
|
|
734
|
+
const permissions = permissionsExisted ? { ...settings.permissions } : {};
|
|
735
|
+
if (Object.hasOwn(permissions, "allow") && (
|
|
736
|
+
!Array.isArray(permissions.allow)
|
|
737
|
+
|| !permissions.allow.every((rule) => typeof rule === "string")
|
|
738
|
+
)) {
|
|
739
|
+
throw new Error(`${settingsPath} has an invalid permissions.allow value. Fix or remove it, then re-run.`);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const desired = new Set(desiredTools);
|
|
743
|
+
const previouslyManaged = new Set(priorGrants);
|
|
744
|
+
const allow = Array.isArray(permissions.allow) ? [...permissions.allow] : [];
|
|
745
|
+
const nextAllow = allow.filter((rule) => !previouslyManaged.has(rule) || desired.has(rule));
|
|
746
|
+
const managedGrants = [];
|
|
747
|
+
for (const tool of desiredTools) {
|
|
748
|
+
if (previouslyManaged.has(tool)) {
|
|
749
|
+
managedGrants.push(tool);
|
|
750
|
+
if (!nextAllow.includes(tool)) nextAllow.push(tool);
|
|
751
|
+
} else if (!nextAllow.includes(tool)) {
|
|
752
|
+
managedGrants.push(tool);
|
|
753
|
+
nextAllow.push(tool);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// permissions.allow is Claude's ordinary pre-approval surface. Record only
|
|
758
|
+
// entries this sync actually adds, so turning the experiment back off can
|
|
759
|
+
// remove them without claiming any operator-authored permission rule.
|
|
760
|
+
if (JSON.stringify(allow) !== JSON.stringify(nextAllow)) {
|
|
761
|
+
settings.permissions = { ...permissions, allow: nextAllow };
|
|
762
|
+
atomicPrivateWrite(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
|
|
763
|
+
}
|
|
764
|
+
return managedGrants;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function claudeParentDirectPermissionsCurrent(root, manifest) {
|
|
768
|
+
try {
|
|
769
|
+
const desiredTools = parentDirectPermissionTools(
|
|
770
|
+
Array.isArray(manifest.agents) ? manifest.agents.map((record) => ({
|
|
771
|
+
parentLaunchDefinition: record?.launchMode === "parent-direct"
|
|
772
|
+
? record.parentLaunchDefinition
|
|
773
|
+
: null,
|
|
774
|
+
})) : [],
|
|
775
|
+
);
|
|
776
|
+
if (desiredTools.length === 0) return true;
|
|
777
|
+
const { settings } = readClaudeSettingsForPermissions(path.join(root, "settings.json"));
|
|
778
|
+
const allow = settings.permissions?.allow;
|
|
779
|
+
return Array.isArray(allow) && desiredTools.every((tool) => allow.includes(tool));
|
|
780
|
+
} catch {
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
673
785
|
function codexAdapterInstructions(tenantId, agent) {
|
|
674
786
|
const contextRequirement = agent.requiredContext.length
|
|
675
787
|
? ` Required context keys are ${JSON.stringify(agent.requiredContext)}; if any are absent, ask for them before starting the run.`
|
|
@@ -814,7 +926,7 @@ function claudeParentDirectDefinition({ tenantId, agent, invocation }) {
|
|
|
814
926
|
return {
|
|
815
927
|
prompt: claudeParentDirectInstructions(tenantId, agent),
|
|
816
928
|
permissionMode: "bypassPermissions",
|
|
817
|
-
tools: [
|
|
929
|
+
tools: [...CLAUDE_PARENT_DIRECT_TOOL_PERMISSIONS],
|
|
818
930
|
mcpServers: [{
|
|
819
931
|
[MANAGED_AGENT_MCP_SERVER]: {
|
|
820
932
|
type: "stdio",
|
|
@@ -826,11 +938,35 @@ function claudeParentDirectDefinition({ tenantId, agent, invocation }) {
|
|
|
826
938
|
};
|
|
827
939
|
}
|
|
828
940
|
|
|
941
|
+
function nativeAgentTelemetryEnvironment(environment = process.env) {
|
|
942
|
+
return Object.fromEntries(IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES.flatMap((name) => {
|
|
943
|
+
const value = environment?.[name];
|
|
944
|
+
return typeof value === "string" && value.length > 0 ? [[name, value]] : [];
|
|
945
|
+
}));
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function telemetryEnvironmentMatches(recorded, expected) {
|
|
949
|
+
const normalizedRecorded = recorded === undefined ? {} : recorded;
|
|
950
|
+
return normalizedRecorded
|
|
951
|
+
&& typeof normalizedRecorded === "object"
|
|
952
|
+
&& !Array.isArray(normalizedRecorded)
|
|
953
|
+
&& JSON.stringify(normalizedRecorded) === JSON.stringify(expected);
|
|
954
|
+
}
|
|
955
|
+
|
|
829
956
|
function codexInvocationEnvironment(invocation, { durableProfile = false } = {}) {
|
|
830
957
|
const entries = Object.entries(invocation.env || {});
|
|
831
958
|
const transient = new Set(IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES);
|
|
959
|
+
// Ordinary durable profiles must never retain a one-off telemetry path.
|
|
960
|
+
// The native Codex profiler is different: it generates a throwaway profile
|
|
961
|
+
// for the whole benchmark cohort and needs Codex's explicit MCP environment
|
|
962
|
+
// map to carry that cohort path into the server process. The isolated root is
|
|
963
|
+
// removed after the cohort, so benchmark-tagged telemetry cannot linger in
|
|
964
|
+
// an operator's managed profile.
|
|
965
|
+
const isolatedBenchmark = nativeBenchmarkHeaderValue(invocation.env || {}) !== null;
|
|
832
966
|
return [
|
|
833
|
-
...(durableProfile
|
|
967
|
+
...(durableProfile && !isolatedBenchmark
|
|
968
|
+
? entries.filter(([key]) => !transient.has(key))
|
|
969
|
+
: entries),
|
|
834
970
|
[IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_ENV, String(CODEX_NATIVE_AGENT_ATTACHMENT_WINDOW_MS)],
|
|
835
971
|
];
|
|
836
972
|
}
|
|
@@ -914,6 +1050,7 @@ function boundNativeAgentInvocation(tenantId, agent, invocation, {
|
|
|
914
1050
|
policyFingerprint = nativeAgentPolicyFingerprint(agent),
|
|
915
1051
|
mode = usesDirectAnswer(agent) ? "answer" : "durable",
|
|
916
1052
|
answerToolAttribution = false,
|
|
1053
|
+
environment = process.env,
|
|
917
1054
|
} = {}) {
|
|
918
1055
|
const bound = !invocation
|
|
919
1056
|
? impelNativeAgentMcpInvocation({
|
|
@@ -922,7 +1059,7 @@ function boundNativeAgentInvocation(tenantId, agent, invocation, {
|
|
|
922
1059
|
scopeParam: agent.scopeParam,
|
|
923
1060
|
policyFingerprint,
|
|
924
1061
|
mode,
|
|
925
|
-
})
|
|
1062
|
+
}, { environment })
|
|
926
1063
|
: (() => {
|
|
927
1064
|
const mcpIndex = invocation.args?.lastIndexOf("mcp") ?? -1;
|
|
928
1065
|
if (mcpIndex < 0) throw new Error("native-agent MCP invocation has no mcp command");
|
|
@@ -948,6 +1085,7 @@ function boundNativeAgentInvocation(tenantId, agent, invocation, {
|
|
|
948
1085
|
export function renderManagedAgents(client, tenantId, agents, invocation = null, {
|
|
949
1086
|
directCodeMode = true,
|
|
950
1087
|
nativeParentDirect = false,
|
|
1088
|
+
environment = process.env,
|
|
951
1089
|
} = {}) {
|
|
952
1090
|
if (client !== "claude" && client !== "codex") throw new Error(`unknown agent client ${client}`);
|
|
953
1091
|
const normalizedTenant = normalizeTenantId(tenantId);
|
|
@@ -965,6 +1103,7 @@ export function renderManagedAgents(client, tenantId, agents, invocation = null,
|
|
|
965
1103
|
const parentDirect = client === "claude" && nativeParentDirect && usesDirectAnswer(agent);
|
|
966
1104
|
const boundInvocation = boundNativeAgentInvocation(normalizedTenant, agent, invocation, {
|
|
967
1105
|
answerToolAttribution: parentDirect,
|
|
1106
|
+
environment,
|
|
968
1107
|
});
|
|
969
1108
|
const claudeRendered = client === "claude" && !parentDirect
|
|
970
1109
|
? renderClaudeAgent({ tenantId: normalizedTenant, agent, name, invocation: boundInvocation })
|
|
@@ -1059,7 +1198,10 @@ export function renderManagedClaudeSlashCommands(tenantId, agents, invocation =
|
|
|
1059
1198
|
});
|
|
1060
1199
|
}
|
|
1061
1200
|
|
|
1062
|
-
function renderRetiredManagedAgents(client, tenantId, bindings, active, invocation = null, {
|
|
1201
|
+
function renderRetiredManagedAgents(client, tenantId, bindings, active, invocation = null, {
|
|
1202
|
+
directCodeMode = true,
|
|
1203
|
+
environment = process.env,
|
|
1204
|
+
} = {}) {
|
|
1063
1205
|
const usedNames = new Set(active.map(({ name }) => name));
|
|
1064
1206
|
const usedFiles = new Set(active.map(({ fileName }) => fileName));
|
|
1065
1207
|
return bindings.map((binding) => {
|
|
@@ -1097,6 +1239,7 @@ function renderRetiredManagedAgents(client, tenantId, bindings, active, invocati
|
|
|
1097
1239
|
const boundInvocation = boundNativeAgentInvocation(tenantId, agent, invocation, {
|
|
1098
1240
|
policyFingerprint: binding.policyFingerprint,
|
|
1099
1241
|
mode: "recovery",
|
|
1242
|
+
environment,
|
|
1100
1243
|
});
|
|
1101
1244
|
const claudeRendered = client === "claude"
|
|
1102
1245
|
? renderClaudeAgent({ tenantId, agent, name, invocation: boundInvocation, recoveryOnly: true })
|
|
@@ -1233,7 +1376,11 @@ function validateClaudeLaunchDefinition(record, tenantId) {
|
|
|
1233
1376
|
throw new Error("the managed Claude launch definition is invalid; run `impel agents sync claude`");
|
|
1234
1377
|
}
|
|
1235
1378
|
const server = definition.mcpServers[0][MANAGED_AGENT_MCP_SERVER];
|
|
1236
|
-
const allowedEnvironment = new Set([
|
|
1379
|
+
const allowedEnvironment = new Set([
|
|
1380
|
+
IMPEL_MANAGED_MCP_ENV,
|
|
1381
|
+
...IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES,
|
|
1382
|
+
IMPEL_NATIVE_BENCHMARK_ENV,
|
|
1383
|
+
]);
|
|
1237
1384
|
if (!exactObjectKeys(server, ["type", "command", "args", "env"])
|
|
1238
1385
|
|| server.type !== "stdio"
|
|
1239
1386
|
|| typeof server.command !== "string"
|
|
@@ -1274,8 +1421,7 @@ function validateClaudeLaunchDefinition(record, tenantId) {
|
|
|
1274
1421
|
|
|
1275
1422
|
function validateClaudeParentLaunchDefinition(record, tenantId) {
|
|
1276
1423
|
const definition = record.parentLaunchDefinition;
|
|
1277
|
-
const expectedTools =
|
|
1278
|
-
.map((name) => nativeToolName(name));
|
|
1424
|
+
const expectedTools = CLAUDE_PARENT_DIRECT_TOOL_PERMISSIONS;
|
|
1279
1425
|
if (!exactObjectKeys(definition, ["prompt", "permissionMode", "tools", "mcpServers"])
|
|
1280
1426
|
|| definition.prompt !== claudeParentDirectInstructions(tenantId, record)
|
|
1281
1427
|
|| definition.permissionMode !== "bypassPermissions"
|
|
@@ -1286,7 +1432,11 @@ function validateClaudeParentLaunchDefinition(record, tenantId) {
|
|
|
1286
1432
|
throw new Error("the managed Claude parent-direct definition is invalid; run `impel agents sync claude`");
|
|
1287
1433
|
}
|
|
1288
1434
|
const server = definition.mcpServers[0][MANAGED_AGENT_MCP_SERVER];
|
|
1289
|
-
const allowedEnvironment = new Set([
|
|
1435
|
+
const allowedEnvironment = new Set([
|
|
1436
|
+
IMPEL_MANAGED_MCP_ENV,
|
|
1437
|
+
...IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES,
|
|
1438
|
+
IMPEL_NATIVE_BENCHMARK_ENV,
|
|
1439
|
+
]);
|
|
1290
1440
|
if (!exactObjectKeys(server, ["type", "command", "args", "env"])
|
|
1291
1441
|
|| server.type !== "stdio"
|
|
1292
1442
|
|| typeof server.command !== "string"
|
|
@@ -1530,13 +1680,27 @@ function profileIsFresh(profile, tenantId, now, ttlMs) {
|
|
|
1530
1680
|
const expectedNativeInterceptTimeoutMs = expectedNativeIntercept
|
|
1531
1681
|
? (profile.nativeInterceptTimeoutMs ?? nativeInterceptTimeoutMs())
|
|
1532
1682
|
: null;
|
|
1683
|
+
const expectedTelemetryEnvironment = nativeAgentTelemetryEnvironment(
|
|
1684
|
+
profile.environment ?? process.env,
|
|
1685
|
+
);
|
|
1686
|
+
const expectedBenchmark = nativeBenchmarkHeaderValue(
|
|
1687
|
+
profile.environment ?? process.env,
|
|
1688
|
+
) !== null;
|
|
1533
1689
|
if (manifest.directProfiles !== expectedDirectProfiles
|
|
1534
1690
|
|| manifest.directCodeMode !== expectedDirectCodeMode
|
|
1535
1691
|
|| Boolean(manifest.nativeParentDirect) !== expectedNativeParentDirect
|
|
1536
1692
|
|| Boolean(manifest.nativeSlashCommands) !== expectedNativeSlashCommands
|
|
1537
1693
|
|| Boolean(manifest.nativeIntercept) !== expectedNativeIntercept
|
|
1694
|
+
|| !telemetryEnvironmentMatches(
|
|
1695
|
+
manifest.nativeAgentTelemetryEnvironment,
|
|
1696
|
+
expectedTelemetryEnvironment,
|
|
1697
|
+
)
|
|
1698
|
+
|| Boolean(manifest.nativeBenchmark) !== expectedBenchmark
|
|
1538
1699
|
|| (expectedNativeIntercept
|
|
1539
1700
|
&& manifest.nativeInterceptTimeoutMs !== expectedNativeInterceptTimeoutMs)) return false;
|
|
1701
|
+
if (expectedNativeParentDirect && !claudeParentDirectPermissionsCurrent(profile.root, manifest)) {
|
|
1702
|
+
return false;
|
|
1703
|
+
}
|
|
1540
1704
|
const syncedAt = Date.parse(manifest.syncedAt || "");
|
|
1541
1705
|
if (!Number.isFinite(syncedAt) || now - syncedAt >= ttlMs) return false;
|
|
1542
1706
|
const artifacts = [
|
|
@@ -1569,6 +1733,8 @@ export function syncAgentProfile({
|
|
|
1569
1733
|
nativeSlashCommands = client === "claude" && nativeSlashCommandsEnabled(),
|
|
1570
1734
|
nativeIntercept = client === "claude" && nativeInterceptEnabled(),
|
|
1571
1735
|
nativeInterceptTimeoutMs: interceptTimeoutMs = nativeIntercept ? nativeInterceptTimeoutMs() : null,
|
|
1736
|
+
environment = process.env,
|
|
1737
|
+
invocation = null,
|
|
1572
1738
|
now = Date.now(),
|
|
1573
1739
|
nativeAgentRunsRoot = path.join(CONFIG_DIR, "native-agent-runs"),
|
|
1574
1740
|
}) {
|
|
@@ -1588,9 +1754,10 @@ export function syncAgentProfile({
|
|
|
1588
1754
|
&& fs.lstatSync(commandsDir).isSymbolicLink()) {
|
|
1589
1755
|
throw new Error(`refusing to use symlinked native-agent path ${commandsDir}`);
|
|
1590
1756
|
}
|
|
1591
|
-
const active = renderManagedAgents(client, tenantId, agents,
|
|
1757
|
+
const active = renderManagedAgents(client, tenantId, agents, invocation, {
|
|
1592
1758
|
directCodeMode,
|
|
1593
1759
|
nativeParentDirect,
|
|
1760
|
+
environment,
|
|
1594
1761
|
});
|
|
1595
1762
|
const activeBindings = new Set(active.map((agent) =>
|
|
1596
1763
|
`${agent.agentId}\0${agent.scopeParam}\0${agent.policyFingerprint}`
|
|
@@ -1599,7 +1766,10 @@ export function syncAgentProfile({
|
|
|
1599
1766
|
.filter((binding) => !activeBindings.has(
|
|
1600
1767
|
`${binding.agentId}\0${binding.scopeParam}\0${binding.policyFingerprint}`,
|
|
1601
1768
|
));
|
|
1602
|
-
const retired = renderRetiredManagedAgents(client, tenantId, retiredBindings, active,
|
|
1769
|
+
const retired = renderRetiredManagedAgents(client, tenantId, retiredBindings, active, invocation, {
|
|
1770
|
+
directCodeMode,
|
|
1771
|
+
environment,
|
|
1772
|
+
});
|
|
1603
1773
|
const rendered = [...active, ...retired];
|
|
1604
1774
|
const commands = client === "claude" && nativeSlashCommands
|
|
1605
1775
|
? renderManagedClaudeSlashCommands(tenantId, agents)
|
|
@@ -1679,6 +1849,13 @@ export function syncAgentProfile({
|
|
|
1679
1849
|
}
|
|
1680
1850
|
}
|
|
1681
1851
|
}
|
|
1852
|
+
const parentDirectPermissions = client === "claude"
|
|
1853
|
+
? syncClaudeParentDirectPermissions(
|
|
1854
|
+
root,
|
|
1855
|
+
parentDirectPermissionTools(rendered),
|
|
1856
|
+
priorParentDirectPermissionGrants(prior, priorOwnsManagedFiles),
|
|
1857
|
+
)
|
|
1858
|
+
: null;
|
|
1682
1859
|
for (const agent of rendered) {
|
|
1683
1860
|
if (agent.fileName) atomicPrivateWrite(path.join(agentsDir, agent.fileName), agent.contents);
|
|
1684
1861
|
if (client === "codex" && directProfiles) atomicPrivateWrite(path.join(root, agent.profileFileName), agent.profileContents);
|
|
@@ -1755,6 +1932,8 @@ export function syncAgentProfile({
|
|
|
1755
1932
|
`commands/${command.fileName}`,
|
|
1756
1933
|
contentDigest(command.contents),
|
|
1757
1934
|
])).sort(([left], [right]) => left.localeCompare(right)));
|
|
1935
|
+
const telemetryEnvironment = nativeAgentTelemetryEnvironment(environment);
|
|
1936
|
+
const benchmark = nativeBenchmarkHeaderValue(environment);
|
|
1758
1937
|
atomicPrivateWrite(manifestPath, `${JSON.stringify({
|
|
1759
1938
|
version: MANAGED_AGENT_MANIFEST_VERSION,
|
|
1760
1939
|
tenantId,
|
|
@@ -1762,11 +1941,18 @@ export function syncAgentProfile({
|
|
|
1762
1941
|
directProfiles: client === "codex" && directProfiles,
|
|
1763
1942
|
directCodeMode: client === "codex" && directCodeMode,
|
|
1764
1943
|
...(client === "claude" && nativeParentDirect ? { nativeParentDirect: true } : {}),
|
|
1944
|
+
...(client === "claude" && parentDirectPermissions.length > 0 ? {
|
|
1945
|
+
nativeParentDirectPermissionGrants: parentDirectPermissions,
|
|
1946
|
+
} : {}),
|
|
1765
1947
|
...(client === "claude" && nativeSlashCommands ? { nativeSlashCommands: true } : {}),
|
|
1766
1948
|
...(client === "claude" && nativeIntercept ? {
|
|
1767
1949
|
nativeIntercept: true,
|
|
1768
1950
|
nativeInterceptTimeoutMs: interceptTimeoutMs,
|
|
1769
1951
|
} : {}),
|
|
1952
|
+
...(Object.keys(telemetryEnvironment).length > 0 ? {
|
|
1953
|
+
nativeAgentTelemetryEnvironment: telemetryEnvironment,
|
|
1954
|
+
} : {}),
|
|
1955
|
+
...(benchmark ? { nativeBenchmark: true } : {}),
|
|
1770
1956
|
syncedAt: new Date(now).toISOString(),
|
|
1771
1957
|
files: [...currentFiles].sort(),
|
|
1772
1958
|
...(client === "claude" && nativeSlashCommands ? { commands: [...currentCommands].sort() } : {}),
|
package/src/cli.js
CHANGED
|
@@ -38,6 +38,7 @@ Work:
|
|
|
38
38
|
impel claude --agent <id|exact-title> ... Run one fixed tenant agent without a parent hop
|
|
39
39
|
impel codex [args...] Launch Codex with an isolated Impel profile
|
|
40
40
|
impel codex --agent <id|exact-title> ... Run one fixed tenant agent without a parent hop
|
|
41
|
+
impel codex --benchmark ... Tag native-agent MCP calls as benchmark traffic
|
|
41
42
|
impel remote handoff|dispatch|handback Move or control provider-native sessions remotely
|
|
42
43
|
impel remote status|viewer|proxy Inspect, control, or connect to a remote run
|
|
43
44
|
impel tenant list List accessible organizations
|
package/src/commands/agents.js
CHANGED
|
@@ -38,6 +38,7 @@ export function managedAgentProfiles(client, options = {}) {
|
|
|
38
38
|
environment,
|
|
39
39
|
homeDir,
|
|
40
40
|
}),
|
|
41
|
+
environment,
|
|
41
42
|
...(client === "codex" ? {
|
|
42
43
|
directProfiles: profile.label !== "Codex CLI (native profile)",
|
|
43
44
|
directCodeMode: profile.label !== "Codex CLI (native profile)",
|
package/src/commands/launch.js
CHANGED
|
@@ -24,9 +24,8 @@ import { parentVerbatimRelayAppendix } from "../verbatimRelay.js";
|
|
|
24
24
|
import { withGitEnvironment } from "../skills.js";
|
|
25
25
|
import { assertProviderScopes, ensureTenantSelection, tenantCredential } from "../tenants.js";
|
|
26
26
|
import { maybePrintUpdateNotice } from "../updates.js";
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
} from "../nativeProcess.js";
|
|
27
|
+
import { nativeSpawnInvocation } from "../nativeProcess.js";
|
|
28
|
+
import { IMPEL_NATIVE_BENCHMARK_ENV } from "../selfInvocation.js";
|
|
30
29
|
import { resolveReviewedVendorCliBinary } from "../vendorCliBinaries.js";
|
|
31
30
|
import { PINNED_VENDOR_CLI_VERSIONS } from "../vendorCliVersions.js";
|
|
32
31
|
import { RUNTIME_BRAND } from "../runtimeBrand.js";
|
|
@@ -305,6 +304,7 @@ function codexAgentLockedOption(argument) {
|
|
|
305
304
|
export function parseCodexAgentLaunchArguments(argv) {
|
|
306
305
|
const passthrough = [];
|
|
307
306
|
let selector = null;
|
|
307
|
+
let benchmark = false;
|
|
308
308
|
let literal = false;
|
|
309
309
|
for (let index = 0; index < argv.length; index += 1) {
|
|
310
310
|
const argument = argv[index];
|
|
@@ -317,6 +317,14 @@ export function parseCodexAgentLaunchArguments(argv) {
|
|
|
317
317
|
passthrough.push(argument);
|
|
318
318
|
continue;
|
|
319
319
|
}
|
|
320
|
+
if (argument === "--benchmark") {
|
|
321
|
+
if (benchmark) throw new Error("`--benchmark` may be specified only once");
|
|
322
|
+
benchmark = true;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (argument.startsWith("--benchmark=")) {
|
|
326
|
+
throw new Error("`--benchmark` does not accept a value");
|
|
327
|
+
}
|
|
320
328
|
if (argument === "--agent" || argument.startsWith("--agent=")) {
|
|
321
329
|
if (selector !== null) throw new Error("`--agent` may be specified only once");
|
|
322
330
|
const value = argument === "--agent" ? argv[index += 1] : argument.slice("--agent=".length);
|
|
@@ -339,7 +347,7 @@ export function parseCodexAgentLaunchArguments(argv) {
|
|
|
339
347
|
}
|
|
340
348
|
}
|
|
341
349
|
}
|
|
342
|
-
return { selector, argv: passthrough };
|
|
350
|
+
return { selector, benchmark, argv: passthrough };
|
|
343
351
|
}
|
|
344
352
|
|
|
345
353
|
function codexJsonOutput(argv) {
|
|
@@ -436,11 +444,16 @@ export async function cmdLaunch(tool, argv) {
|
|
|
436
444
|
let nativeArgv = [...argv];
|
|
437
445
|
let claudeAgentSelector = null;
|
|
438
446
|
let codexAgentSelector = null;
|
|
447
|
+
let codexBenchmark = false;
|
|
439
448
|
if (tool === "claude" && RUNTIME_BRAND.features.agents) {
|
|
440
449
|
({ selector: claudeAgentSelector, argv: nativeArgv } = parseClaudeAgentLaunchArguments(argv));
|
|
441
450
|
} else if (tool === "codex") {
|
|
442
451
|
try {
|
|
443
|
-
({
|
|
452
|
+
({
|
|
453
|
+
selector: codexAgentSelector,
|
|
454
|
+
benchmark: codexBenchmark,
|
|
455
|
+
argv: nativeArgv,
|
|
456
|
+
} = parseCodexAgentLaunchArguments(argv));
|
|
444
457
|
} catch (error) {
|
|
445
458
|
console.error(`impel codex: ${error.message}`);
|
|
446
459
|
process.exitCode = 1;
|
|
@@ -489,12 +502,18 @@ export async function cmdLaunch(tool, argv) {
|
|
|
489
502
|
}
|
|
490
503
|
}
|
|
491
504
|
const environment = { ...process.env };
|
|
505
|
+
if (codexBenchmark) environment[IMPEL_NATIVE_BENCHMARK_ENV] = "1";
|
|
492
506
|
environment.IMPEL_TENANT_ID = tenantId;
|
|
493
507
|
let agentProfile;
|
|
494
508
|
|
|
495
509
|
if (tool === "claude") {
|
|
496
510
|
const profile = ensureImpelClaudeProfile(gatewayUrl, tenantId, { crossAppModels });
|
|
497
|
-
agentProfile = {
|
|
511
|
+
agentProfile = {
|
|
512
|
+
client: "claude",
|
|
513
|
+
root: profile.configDir,
|
|
514
|
+
label: "Impel isolated Claude (impel claude)",
|
|
515
|
+
environment,
|
|
516
|
+
};
|
|
498
517
|
deleteEnvironmentKeys(environment, CLAUDE_DIRECT_AUTH_ENV);
|
|
499
518
|
delete environment.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY;
|
|
500
519
|
environment.CLAUDE_CONFIG_DIR = profile.configDir;
|
|
@@ -508,7 +527,12 @@ export async function cmdLaunch(tool, argv) {
|
|
|
508
527
|
environment.ANTHROPIC_AUTH_TOKEN = gatewayCredential;
|
|
509
528
|
} else if (tool === "codex") {
|
|
510
529
|
const profile = ensureImpelCodexProfile(gatewayUrl, tenantId);
|
|
511
|
-
agentProfile = {
|
|
530
|
+
agentProfile = {
|
|
531
|
+
client: "codex",
|
|
532
|
+
root: profile.codexHome,
|
|
533
|
+
label: "Impel isolated Codex (impel codex)",
|
|
534
|
+
environment,
|
|
535
|
+
};
|
|
512
536
|
deleteEnvironmentKeys(environment, CODEX_DIRECT_AUTH_ENV);
|
|
513
537
|
environment.CODEX_HOME = profile.codexHome;
|
|
514
538
|
environment[CODEX_GATEWAY_TOKEN_ENV] = gatewayCredential;
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import { extractAnswerFinalText } from "./directAnswer.js";
|
|
26
26
|
import {
|
|
27
27
|
IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_ENV,
|
|
28
|
+
nativeBenchmarkHeaderValue,
|
|
28
29
|
} from "./selfInvocation.js";
|
|
29
30
|
import { normalizeTenantId } from "./tenants.js";
|
|
30
31
|
import { renameWithWindowsRetry } from "./windowsFs.js";
|
|
@@ -40,6 +41,7 @@ export {
|
|
|
40
41
|
export const NATIVE_AGENT_HANDLE_SCHEMA = "impel.native-agent-run.v1";
|
|
41
42
|
export const NATIVE_AGENT_RESULT_SCHEMA = "impel.native-agent-result.v1";
|
|
42
43
|
export const NATIVE_AGENT_RECOVERY_SCHEMA = "impel.native-agent-recovery.v1";
|
|
44
|
+
export const NATIVE_AGENT_BENCHMARK_HEADER = "X-Impel-Client-Benchmark";
|
|
43
45
|
export const NATIVE_AGENT_CLIENT_BUILD = `cli/${JSON.parse(
|
|
44
46
|
fs.readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8"),
|
|
45
47
|
).version}+manifest.v${CURRENT_CONFIG_VERSION}`;
|
|
@@ -52,8 +54,8 @@ const SAFE_FINGERPRINT_RE = /^[a-f0-9]{64}$/u;
|
|
|
52
54
|
const SAFE_INVOCATION_RE = /^[a-f0-9-]{36}$/u;
|
|
53
55
|
const DEFAULT_WAIT_SECONDS = 20;
|
|
54
56
|
const MAX_WAIT_SECONDS = 35;
|
|
55
|
-
const DEFAULT_ATTACHMENT_WINDOW_MS = 40_000;
|
|
56
|
-
const DEFAULT_UPSTREAM_TIMEOUT_MS = 42_000;
|
|
57
|
+
export const DEFAULT_ATTACHMENT_WINDOW_MS = 40_000;
|
|
58
|
+
export const DEFAULT_UPSTREAM_TIMEOUT_MS = 42_000;
|
|
57
59
|
const DEFAULT_ANSWER_HEDGE_MS = 38_000;
|
|
58
60
|
const DEFAULT_MAX_POLLS = 8;
|
|
59
61
|
const DEFAULT_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
@@ -742,6 +744,7 @@ export class NativeAgentUpstreamSession {
|
|
|
742
744
|
answerHedgeMs = nativeAgentAnswerHedgeMs(),
|
|
743
745
|
setTimeoutImpl = setTimeout,
|
|
744
746
|
clearTimeoutImpl = clearTimeout,
|
|
747
|
+
environment = process.env,
|
|
745
748
|
}) {
|
|
746
749
|
this.endpoint = `${normalizeGatewayUrl(gatewayUrl)}/mcp`;
|
|
747
750
|
this.credential = credential;
|
|
@@ -755,6 +758,7 @@ export class NativeAgentUpstreamSession {
|
|
|
755
758
|
this.answerHedgeMs = Math.max(0, answerHedgeMs);
|
|
756
759
|
this.setTimeoutImpl = setTimeoutImpl;
|
|
757
760
|
this.clearTimeoutImpl = clearTimeoutImpl;
|
|
761
|
+
this.benchmarkHeaderValue = nativeBenchmarkHeaderValue(environment);
|
|
758
762
|
this.sessionId = null;
|
|
759
763
|
this.nextId = 1;
|
|
760
764
|
}
|
|
@@ -785,6 +789,9 @@ export class NativeAgentUpstreamSession {
|
|
|
785
789
|
Accept: "application/json, text/event-stream",
|
|
786
790
|
"X-Impel-Client-Request-Id": correlationId,
|
|
787
791
|
"X-Impel-Client-Build": NATIVE_AGENT_CLIENT_BUILD,
|
|
792
|
+
...(this.benchmarkHeaderValue
|
|
793
|
+
? { [NATIVE_AGENT_BENCHMARK_HEADER]: this.benchmarkHeaderValue }
|
|
794
|
+
: {}),
|
|
788
795
|
...(this.sessionId ? { "Mcp-Session-Id": this.sessionId } : {}),
|
|
789
796
|
},
|
|
790
797
|
body: JSON.stringify(message),
|
package/src/selfInvocation.js
CHANGED
|
@@ -82,9 +82,23 @@ export const IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES = [
|
|
|
82
82
|
"IMPEL_NATIVE_HOST",
|
|
83
83
|
"IMPEL_NATIVE_HOST_BUILD",
|
|
84
84
|
];
|
|
85
|
+
export const IMPEL_NATIVE_BENCHMARK_ENV = "IMPEL_NATIVE_BENCHMARK";
|
|
85
86
|
export const IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_ENV = "IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_MS";
|
|
86
87
|
export const CODEX_NATIVE_AGENT_ATTACHMENT_WINDOW_MS = 100_000;
|
|
87
88
|
|
|
89
|
+
const SAFE_NATIVE_BENCHMARK_HEADER_VALUE = /^[\x21-\x7e]{1,128}$/u;
|
|
90
|
+
|
|
91
|
+
export function nativeBenchmarkHeaderValue(environment = process.env) {
|
|
92
|
+
const value = environment?.[IMPEL_NATIVE_BENCHMARK_ENV];
|
|
93
|
+
if (value === undefined) return null;
|
|
94
|
+
if (typeof value !== "string"
|
|
95
|
+
|| value !== "1"
|
|
96
|
+
|| !SAFE_NATIVE_BENCHMARK_HEADER_VALUE.test(value)) {
|
|
97
|
+
throw new Error(`${IMPEL_NATIVE_BENCHMARK_ENV} must be exactly 1 using visible ASCII`);
|
|
98
|
+
}
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
88
102
|
function managedMcpEnvironment(environment = process.env) {
|
|
89
103
|
const telemetry = Object.fromEntries(
|
|
90
104
|
IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES.flatMap((name) => {
|
|
@@ -92,7 +106,12 @@ function managedMcpEnvironment(environment = process.env) {
|
|
|
92
106
|
return typeof value === "string" && value.length > 0 ? [[name, value]] : [];
|
|
93
107
|
}),
|
|
94
108
|
);
|
|
95
|
-
|
|
109
|
+
const benchmark = nativeBenchmarkHeaderValue(environment);
|
|
110
|
+
return {
|
|
111
|
+
[IMPEL_MANAGED_MCP_ENV]: "1",
|
|
112
|
+
...telemetry,
|
|
113
|
+
...(benchmark ? { [IMPEL_NATIVE_BENCHMARK_ENV]: benchmark } : {}),
|
|
114
|
+
};
|
|
96
115
|
}
|
|
97
116
|
|
|
98
117
|
export function impelMcpInvocation(args = [], options = {}) {
|