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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.20.42",
3
+ "version": "0.20.43",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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: process.env,
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 telemetryPath = path.join(attemptDir, "telemetry.jsonl");
454
- const codexArgs = [
455
- "codex",
456
- ...(options.mode === "direct" ? ["--agent", options.agent] : []),
457
- "exec",
458
- "--json",
459
- ...options.extraArgs,
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
- let summary = null;
485
- let parseError = null;
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
- summary,
500
- jsonValid: parseError === null,
501
- parseError,
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 = 0;
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 >= options.attempts) return;
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
- await Promise.all(Array.from({ length: options.concurrency }, () => worker()));
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 runWorkers(options, sessionDir);
585
- const aggregate = {
586
- schema: PROFILE_SCHEMA,
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
- tenantId: options.expectedTenant,
590
- sloClass: options.sloClass,
591
- mode: options.mode,
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
- throw new Error(`cohort aborted (${aggregate.abort?.failureClass || "incomplete"}); partial aggregate retained`);
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: [NATIVE_AGENT_ANSWER_TOOL, NATIVE_AGENT_RESUME_TOOL].map((tool) => nativeToolName(tool)),
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 ? entries.filter(([key]) => !transient.has(key)) : entries),
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, { directCodeMode = true } = {}) {
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([IMPEL_MANAGED_MCP_ENV, ...IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES]);
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 = [NATIVE_AGENT_ANSWER_TOOL, NATIVE_AGENT_RESUME_TOOL]
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([IMPEL_MANAGED_MCP_ENV, ...IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES]);
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, null, {
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, null, { directCodeMode });
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
@@ -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)",
@@ -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
- nativeSpawnInvocation,
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
- ({ selector: codexAgentSelector, argv: nativeArgv } = parseCodexAgentLaunchArguments(argv));
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 = { client: "claude", root: profile.configDir, label: "Impel isolated Claude (impel claude)" };
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 = { client: "codex", root: profile.codexHome, label: "Impel isolated Codex (impel codex)" };
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),
@@ -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
- return { [IMPEL_MANAGED_MCP_ENV]: "1", ...telemetry };
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 = {}) {