impel-cli 0.20.42 → 0.20.44

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/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,
@@ -86,8 +88,11 @@ function eagerNativeAgentTransportEnabled() {
86
88
  return process.env.IMPEL_NATIVE_EAGER_TRANSPORT !== "0";
87
89
  }
88
90
 
89
- function enabledProfileFlag(environment, name) {
90
- return ["1", "true"].includes(String(environment?.[name] || "").toLowerCase());
91
+ function enabledProfileFlag(environment, name, { defaultEnabled = false } = {}) {
92
+ const configured = String(environment?.[name] || "").toLowerCase();
93
+ if (["1", "true"].includes(configured)) return true;
94
+ if (["0", "false"].includes(configured)) return false;
95
+ return defaultEnabled;
91
96
  }
92
97
 
93
98
  export function nativeParentDirectEnabled(environment = process.env) {
@@ -95,7 +100,11 @@ export function nativeParentDirectEnabled(environment = process.env) {
95
100
  }
96
101
 
97
102
  export function nativeSlashCommandsEnabled(environment = process.env) {
98
- return enabledProfileFlag(environment, IMPEL_NATIVE_SLASH_COMMANDS_ENV);
103
+ // This surface is additive: the relay agent remains installed. Keep the
104
+ // explicit false literals as a symmetric rollback for managed profiles.
105
+ return enabledProfileFlag(environment, IMPEL_NATIVE_SLASH_COMMANDS_ENV, {
106
+ defaultEnabled: true,
107
+ });
99
108
  }
100
109
  const MAX_RETIRED_AGENT_BINDINGS = 50;
101
110
  const MAX_NATIVE_AGENT_STATE_BYTES = 512 * 1024;
@@ -670,6 +679,116 @@ function nativeToolName(toolName) {
670
679
  return `${nativeToolNamespace()}__${toolName}`;
671
680
  }
672
681
 
682
+ const CLAUDE_PARENT_DIRECT_TOOL_PERMISSIONS = Object.freeze([
683
+ NATIVE_AGENT_ANSWER_TOOL,
684
+ NATIVE_AGENT_RESUME_TOOL,
685
+ ].map((tool) => nativeToolName(tool)));
686
+
687
+ function parentDirectPermissionTools(renderedAgents) {
688
+ const trusted = new Set(CLAUDE_PARENT_DIRECT_TOOL_PERMISSIONS);
689
+ const tools = [...new Set(renderedAgents.flatMap((agent) =>
690
+ agent.parentLaunchDefinition?.tools || []
691
+ ))];
692
+ if (!tools.every((tool) => trusted.has(tool))) {
693
+ throw new Error("the managed Claude parent-direct permission grant is invalid");
694
+ }
695
+ return tools;
696
+ }
697
+
698
+ function priorParentDirectPermissionGrants(prior, ownsManagedProfile) {
699
+ const grants = ownsManagedProfile ? prior?.nativeParentDirectPermissionGrants : null;
700
+ const trusted = new Set(CLAUDE_PARENT_DIRECT_TOOL_PERMISSIONS);
701
+ if (!Array.isArray(grants)
702
+ || new Set(grants).size !== grants.length
703
+ || !grants.every((tool) => trusted.has(tool))) return [];
704
+ return [...grants];
705
+ }
706
+
707
+ function readClaudeSettingsForPermissions(settingsPath) {
708
+ let raw;
709
+ try {
710
+ const stat = fs.lstatSync(settingsPath);
711
+ if (stat.isSymbolicLink() || !stat.isFile()) {
712
+ throw new Error(`refusing to update unsafe Claude settings path ${settingsPath}`);
713
+ }
714
+ raw = fs.readFileSync(settingsPath, "utf8");
715
+ } catch (error) {
716
+ if (error?.code === "ENOENT") return { settings: {} };
717
+ throw error;
718
+ }
719
+ try {
720
+ const settings = raw.trim() ? JSON.parse(raw) : {};
721
+ if (!settings || typeof settings !== "object" || Array.isArray(settings)) throw new Error();
722
+ return { settings };
723
+ } catch {
724
+ throw new Error(`${settingsPath} exists but isn't a valid JSON object. Fix or remove it, then re-run.`);
725
+ }
726
+ }
727
+
728
+ function syncClaudeParentDirectPermissions(root, desiredTools, priorGrants) {
729
+ if (desiredTools.length === 0 && priorGrants.length === 0) return [];
730
+
731
+ const settingsPath = path.join(root, "settings.json");
732
+ const { settings } = readClaudeSettingsForPermissions(settingsPath);
733
+ const permissionsExisted = Object.hasOwn(settings, "permissions");
734
+ if (permissionsExisted && (
735
+ !settings.permissions
736
+ || typeof settings.permissions !== "object"
737
+ || Array.isArray(settings.permissions)
738
+ )) {
739
+ throw new Error(`${settingsPath} has an invalid permissions value. Fix or remove it, then re-run.`);
740
+ }
741
+ const permissions = permissionsExisted ? { ...settings.permissions } : {};
742
+ if (Object.hasOwn(permissions, "allow") && (
743
+ !Array.isArray(permissions.allow)
744
+ || !permissions.allow.every((rule) => typeof rule === "string")
745
+ )) {
746
+ throw new Error(`${settingsPath} has an invalid permissions.allow value. Fix or remove it, then re-run.`);
747
+ }
748
+
749
+ const desired = new Set(desiredTools);
750
+ const previouslyManaged = new Set(priorGrants);
751
+ const allow = Array.isArray(permissions.allow) ? [...permissions.allow] : [];
752
+ const nextAllow = allow.filter((rule) => !previouslyManaged.has(rule) || desired.has(rule));
753
+ const managedGrants = [];
754
+ for (const tool of desiredTools) {
755
+ if (previouslyManaged.has(tool)) {
756
+ managedGrants.push(tool);
757
+ if (!nextAllow.includes(tool)) nextAllow.push(tool);
758
+ } else if (!nextAllow.includes(tool)) {
759
+ managedGrants.push(tool);
760
+ nextAllow.push(tool);
761
+ }
762
+ }
763
+
764
+ // permissions.allow is Claude's ordinary pre-approval surface. Record only
765
+ // entries this sync actually adds, so turning the experiment back off can
766
+ // remove them without claiming any operator-authored permission rule.
767
+ if (JSON.stringify(allow) !== JSON.stringify(nextAllow)) {
768
+ settings.permissions = { ...permissions, allow: nextAllow };
769
+ atomicPrivateWrite(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
770
+ }
771
+ return managedGrants;
772
+ }
773
+
774
+ function claudeParentDirectPermissionsCurrent(root, manifest) {
775
+ try {
776
+ const desiredTools = parentDirectPermissionTools(
777
+ Array.isArray(manifest.agents) ? manifest.agents.map((record) => ({
778
+ parentLaunchDefinition: record?.launchMode === "parent-direct"
779
+ ? record.parentLaunchDefinition
780
+ : null,
781
+ })) : [],
782
+ );
783
+ if (desiredTools.length === 0) return true;
784
+ const { settings } = readClaudeSettingsForPermissions(path.join(root, "settings.json"));
785
+ const allow = settings.permissions?.allow;
786
+ return Array.isArray(allow) && desiredTools.every((tool) => allow.includes(tool));
787
+ } catch {
788
+ return false;
789
+ }
790
+ }
791
+
673
792
  function codexAdapterInstructions(tenantId, agent) {
674
793
  const contextRequirement = agent.requiredContext.length
675
794
  ? ` Required context keys are ${JSON.stringify(agent.requiredContext)}; if any are absent, ask for them before starting the run.`
@@ -814,7 +933,7 @@ function claudeParentDirectDefinition({ tenantId, agent, invocation }) {
814
933
  return {
815
934
  prompt: claudeParentDirectInstructions(tenantId, agent),
816
935
  permissionMode: "bypassPermissions",
817
- tools: [NATIVE_AGENT_ANSWER_TOOL, NATIVE_AGENT_RESUME_TOOL].map((tool) => nativeToolName(tool)),
936
+ tools: [...CLAUDE_PARENT_DIRECT_TOOL_PERMISSIONS],
818
937
  mcpServers: [{
819
938
  [MANAGED_AGENT_MCP_SERVER]: {
820
939
  type: "stdio",
@@ -826,11 +945,35 @@ function claudeParentDirectDefinition({ tenantId, agent, invocation }) {
826
945
  };
827
946
  }
828
947
 
948
+ function nativeAgentTelemetryEnvironment(environment = process.env) {
949
+ return Object.fromEntries(IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES.flatMap((name) => {
950
+ const value = environment?.[name];
951
+ return typeof value === "string" && value.length > 0 ? [[name, value]] : [];
952
+ }));
953
+ }
954
+
955
+ function telemetryEnvironmentMatches(recorded, expected) {
956
+ const normalizedRecorded = recorded === undefined ? {} : recorded;
957
+ return normalizedRecorded
958
+ && typeof normalizedRecorded === "object"
959
+ && !Array.isArray(normalizedRecorded)
960
+ && JSON.stringify(normalizedRecorded) === JSON.stringify(expected);
961
+ }
962
+
829
963
  function codexInvocationEnvironment(invocation, { durableProfile = false } = {}) {
830
964
  const entries = Object.entries(invocation.env || {});
831
965
  const transient = new Set(IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES);
966
+ // Ordinary durable profiles must never retain a one-off telemetry path.
967
+ // The native Codex profiler is different: it generates a throwaway profile
968
+ // for the whole benchmark cohort and needs Codex's explicit MCP environment
969
+ // map to carry that cohort path into the server process. The isolated root is
970
+ // removed after the cohort, so benchmark-tagged telemetry cannot linger in
971
+ // an operator's managed profile.
972
+ const isolatedBenchmark = nativeBenchmarkHeaderValue(invocation.env || {}) !== null;
832
973
  return [
833
- ...(durableProfile ? entries.filter(([key]) => !transient.has(key)) : entries),
974
+ ...(durableProfile && !isolatedBenchmark
975
+ ? entries.filter(([key]) => !transient.has(key))
976
+ : entries),
834
977
  [IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_ENV, String(CODEX_NATIVE_AGENT_ATTACHMENT_WINDOW_MS)],
835
978
  ];
836
979
  }
@@ -914,6 +1057,7 @@ function boundNativeAgentInvocation(tenantId, agent, invocation, {
914
1057
  policyFingerprint = nativeAgentPolicyFingerprint(agent),
915
1058
  mode = usesDirectAnswer(agent) ? "answer" : "durable",
916
1059
  answerToolAttribution = false,
1060
+ environment = process.env,
917
1061
  } = {}) {
918
1062
  const bound = !invocation
919
1063
  ? impelNativeAgentMcpInvocation({
@@ -922,7 +1066,7 @@ function boundNativeAgentInvocation(tenantId, agent, invocation, {
922
1066
  scopeParam: agent.scopeParam,
923
1067
  policyFingerprint,
924
1068
  mode,
925
- })
1069
+ }, { environment })
926
1070
  : (() => {
927
1071
  const mcpIndex = invocation.args?.lastIndexOf("mcp") ?? -1;
928
1072
  if (mcpIndex < 0) throw new Error("native-agent MCP invocation has no mcp command");
@@ -948,6 +1092,7 @@ function boundNativeAgentInvocation(tenantId, agent, invocation, {
948
1092
  export function renderManagedAgents(client, tenantId, agents, invocation = null, {
949
1093
  directCodeMode = true,
950
1094
  nativeParentDirect = false,
1095
+ environment = process.env,
951
1096
  } = {}) {
952
1097
  if (client !== "claude" && client !== "codex") throw new Error(`unknown agent client ${client}`);
953
1098
  const normalizedTenant = normalizeTenantId(tenantId);
@@ -965,6 +1110,7 @@ export function renderManagedAgents(client, tenantId, agents, invocation = null,
965
1110
  const parentDirect = client === "claude" && nativeParentDirect && usesDirectAnswer(agent);
966
1111
  const boundInvocation = boundNativeAgentInvocation(normalizedTenant, agent, invocation, {
967
1112
  answerToolAttribution: parentDirect,
1113
+ environment,
968
1114
  });
969
1115
  const claudeRendered = client === "claude" && !parentDirect
970
1116
  ? renderClaudeAgent({ tenantId: normalizedTenant, agent, name, invocation: boundInvocation })
@@ -1059,7 +1205,10 @@ export function renderManagedClaudeSlashCommands(tenantId, agents, invocation =
1059
1205
  });
1060
1206
  }
1061
1207
 
1062
- function renderRetiredManagedAgents(client, tenantId, bindings, active, invocation = null, { directCodeMode = true } = {}) {
1208
+ function renderRetiredManagedAgents(client, tenantId, bindings, active, invocation = null, {
1209
+ directCodeMode = true,
1210
+ environment = process.env,
1211
+ } = {}) {
1063
1212
  const usedNames = new Set(active.map(({ name }) => name));
1064
1213
  const usedFiles = new Set(active.map(({ fileName }) => fileName));
1065
1214
  return bindings.map((binding) => {
@@ -1097,6 +1246,7 @@ function renderRetiredManagedAgents(client, tenantId, bindings, active, invocati
1097
1246
  const boundInvocation = boundNativeAgentInvocation(tenantId, agent, invocation, {
1098
1247
  policyFingerprint: binding.policyFingerprint,
1099
1248
  mode: "recovery",
1249
+ environment,
1100
1250
  });
1101
1251
  const claudeRendered = client === "claude"
1102
1252
  ? renderClaudeAgent({ tenantId, agent, name, invocation: boundInvocation, recoveryOnly: true })
@@ -1233,7 +1383,11 @@ function validateClaudeLaunchDefinition(record, tenantId) {
1233
1383
  throw new Error("the managed Claude launch definition is invalid; run `impel agents sync claude`");
1234
1384
  }
1235
1385
  const server = definition.mcpServers[0][MANAGED_AGENT_MCP_SERVER];
1236
- const allowedEnvironment = new Set([IMPEL_MANAGED_MCP_ENV, ...IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES]);
1386
+ const allowedEnvironment = new Set([
1387
+ IMPEL_MANAGED_MCP_ENV,
1388
+ ...IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES,
1389
+ IMPEL_NATIVE_BENCHMARK_ENV,
1390
+ ]);
1237
1391
  if (!exactObjectKeys(server, ["type", "command", "args", "env"])
1238
1392
  || server.type !== "stdio"
1239
1393
  || typeof server.command !== "string"
@@ -1274,8 +1428,7 @@ function validateClaudeLaunchDefinition(record, tenantId) {
1274
1428
 
1275
1429
  function validateClaudeParentLaunchDefinition(record, tenantId) {
1276
1430
  const definition = record.parentLaunchDefinition;
1277
- const expectedTools = [NATIVE_AGENT_ANSWER_TOOL, NATIVE_AGENT_RESUME_TOOL]
1278
- .map((name) => nativeToolName(name));
1431
+ const expectedTools = CLAUDE_PARENT_DIRECT_TOOL_PERMISSIONS;
1279
1432
  if (!exactObjectKeys(definition, ["prompt", "permissionMode", "tools", "mcpServers"])
1280
1433
  || definition.prompt !== claudeParentDirectInstructions(tenantId, record)
1281
1434
  || definition.permissionMode !== "bypassPermissions"
@@ -1286,7 +1439,11 @@ function validateClaudeParentLaunchDefinition(record, tenantId) {
1286
1439
  throw new Error("the managed Claude parent-direct definition is invalid; run `impel agents sync claude`");
1287
1440
  }
1288
1441
  const server = definition.mcpServers[0][MANAGED_AGENT_MCP_SERVER];
1289
- const allowedEnvironment = new Set([IMPEL_MANAGED_MCP_ENV, ...IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES]);
1442
+ const allowedEnvironment = new Set([
1443
+ IMPEL_MANAGED_MCP_ENV,
1444
+ ...IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES,
1445
+ IMPEL_NATIVE_BENCHMARK_ENV,
1446
+ ]);
1290
1447
  if (!exactObjectKeys(server, ["type", "command", "args", "env"])
1291
1448
  || server.type !== "stdio"
1292
1449
  || typeof server.command !== "string"
@@ -1519,24 +1676,39 @@ function profileIsFresh(profile, tenantId, now, ttlMs) {
1519
1676
  if (!Array.isArray(manifest.files) || !manifest.contentDigests
1520
1677
  || typeof manifest.contentDigests !== "object"
1521
1678
  || Array.isArray(manifest.contentDigests)) return false;
1679
+ const profileEnvironment = profile.environment ?? process.env;
1522
1680
  const expectedDirectProfiles = profile.client === "codex" && profile.directProfiles !== false;
1523
1681
  const expectedDirectCodeMode = profile.client === "codex" && profile.directCodeMode !== false;
1524
1682
  const expectedNativeParentDirect = profile.client === "claude"
1525
- && (profile.nativeParentDirect ?? nativeParentDirectEnabled());
1683
+ && (profile.nativeParentDirect ?? nativeParentDirectEnabled(profileEnvironment));
1526
1684
  const expectedNativeSlashCommands = profile.client === "claude"
1527
- && (profile.nativeSlashCommands ?? nativeSlashCommandsEnabled());
1685
+ && (profile.nativeSlashCommands ?? nativeSlashCommandsEnabled(profileEnvironment));
1528
1686
  const expectedNativeIntercept = profile.client === "claude"
1529
- && (profile.nativeIntercept ?? nativeInterceptEnabled());
1687
+ && (profile.nativeIntercept ?? nativeInterceptEnabled(profileEnvironment));
1530
1688
  const expectedNativeInterceptTimeoutMs = expectedNativeIntercept
1531
- ? (profile.nativeInterceptTimeoutMs ?? nativeInterceptTimeoutMs())
1689
+ ? (profile.nativeInterceptTimeoutMs ?? nativeInterceptTimeoutMs(profileEnvironment))
1532
1690
  : null;
1691
+ const expectedTelemetryEnvironment = nativeAgentTelemetryEnvironment(
1692
+ profileEnvironment,
1693
+ );
1694
+ const expectedBenchmark = nativeBenchmarkHeaderValue(
1695
+ profileEnvironment,
1696
+ ) !== null;
1533
1697
  if (manifest.directProfiles !== expectedDirectProfiles
1534
1698
  || manifest.directCodeMode !== expectedDirectCodeMode
1535
1699
  || Boolean(manifest.nativeParentDirect) !== expectedNativeParentDirect
1536
1700
  || Boolean(manifest.nativeSlashCommands) !== expectedNativeSlashCommands
1537
1701
  || Boolean(manifest.nativeIntercept) !== expectedNativeIntercept
1702
+ || !telemetryEnvironmentMatches(
1703
+ manifest.nativeAgentTelemetryEnvironment,
1704
+ expectedTelemetryEnvironment,
1705
+ )
1706
+ || Boolean(manifest.nativeBenchmark) !== expectedBenchmark
1538
1707
  || (expectedNativeIntercept
1539
1708
  && manifest.nativeInterceptTimeoutMs !== expectedNativeInterceptTimeoutMs)) return false;
1709
+ if (expectedNativeParentDirect && !claudeParentDirectPermissionsCurrent(profile.root, manifest)) {
1710
+ return false;
1711
+ }
1540
1712
  const syncedAt = Date.parse(manifest.syncedAt || "");
1541
1713
  if (!Number.isFinite(syncedAt) || now - syncedAt >= ttlMs) return false;
1542
1714
  const artifacts = [
@@ -1565,10 +1737,14 @@ export function syncAgentProfile({
1565
1737
  agents,
1566
1738
  directProfiles = client === "codex",
1567
1739
  directCodeMode = client === "codex",
1568
- nativeParentDirect = client === "claude" && nativeParentDirectEnabled(),
1569
- nativeSlashCommands = client === "claude" && nativeSlashCommandsEnabled(),
1570
- nativeIntercept = client === "claude" && nativeInterceptEnabled(),
1571
- nativeInterceptTimeoutMs: interceptTimeoutMs = nativeIntercept ? nativeInterceptTimeoutMs() : null,
1740
+ environment = process.env,
1741
+ nativeParentDirect = client === "claude" && nativeParentDirectEnabled(environment),
1742
+ nativeSlashCommands = client === "claude" && nativeSlashCommandsEnabled(environment),
1743
+ nativeIntercept = client === "claude" && nativeInterceptEnabled(environment),
1744
+ nativeInterceptTimeoutMs: interceptTimeoutMs = nativeIntercept
1745
+ ? nativeInterceptTimeoutMs(environment)
1746
+ : null,
1747
+ invocation = null,
1572
1748
  now = Date.now(),
1573
1749
  nativeAgentRunsRoot = path.join(CONFIG_DIR, "native-agent-runs"),
1574
1750
  }) {
@@ -1588,9 +1764,10 @@ export function syncAgentProfile({
1588
1764
  && fs.lstatSync(commandsDir).isSymbolicLink()) {
1589
1765
  throw new Error(`refusing to use symlinked native-agent path ${commandsDir}`);
1590
1766
  }
1591
- const active = renderManagedAgents(client, tenantId, agents, null, {
1767
+ const active = renderManagedAgents(client, tenantId, agents, invocation, {
1592
1768
  directCodeMode,
1593
1769
  nativeParentDirect,
1770
+ environment,
1594
1771
  });
1595
1772
  const activeBindings = new Set(active.map((agent) =>
1596
1773
  `${agent.agentId}\0${agent.scopeParam}\0${agent.policyFingerprint}`
@@ -1599,7 +1776,10 @@ export function syncAgentProfile({
1599
1776
  .filter((binding) => !activeBindings.has(
1600
1777
  `${binding.agentId}\0${binding.scopeParam}\0${binding.policyFingerprint}`,
1601
1778
  ));
1602
- const retired = renderRetiredManagedAgents(client, tenantId, retiredBindings, active, null, { directCodeMode });
1779
+ const retired = renderRetiredManagedAgents(client, tenantId, retiredBindings, active, invocation, {
1780
+ directCodeMode,
1781
+ environment,
1782
+ });
1603
1783
  const rendered = [...active, ...retired];
1604
1784
  const commands = client === "claude" && nativeSlashCommands
1605
1785
  ? renderManagedClaudeSlashCommands(tenantId, agents)
@@ -1679,6 +1859,13 @@ export function syncAgentProfile({
1679
1859
  }
1680
1860
  }
1681
1861
  }
1862
+ const parentDirectPermissions = client === "claude"
1863
+ ? syncClaudeParentDirectPermissions(
1864
+ root,
1865
+ parentDirectPermissionTools(rendered),
1866
+ priorParentDirectPermissionGrants(prior, priorOwnsManagedFiles),
1867
+ )
1868
+ : null;
1682
1869
  for (const agent of rendered) {
1683
1870
  if (agent.fileName) atomicPrivateWrite(path.join(agentsDir, agent.fileName), agent.contents);
1684
1871
  if (client === "codex" && directProfiles) atomicPrivateWrite(path.join(root, agent.profileFileName), agent.profileContents);
@@ -1755,6 +1942,8 @@ export function syncAgentProfile({
1755
1942
  `commands/${command.fileName}`,
1756
1943
  contentDigest(command.contents),
1757
1944
  ])).sort(([left], [right]) => left.localeCompare(right)));
1945
+ const telemetryEnvironment = nativeAgentTelemetryEnvironment(environment);
1946
+ const benchmark = nativeBenchmarkHeaderValue(environment);
1758
1947
  atomicPrivateWrite(manifestPath, `${JSON.stringify({
1759
1948
  version: MANAGED_AGENT_MANIFEST_VERSION,
1760
1949
  tenantId,
@@ -1762,11 +1951,18 @@ export function syncAgentProfile({
1762
1951
  directProfiles: client === "codex" && directProfiles,
1763
1952
  directCodeMode: client === "codex" && directCodeMode,
1764
1953
  ...(client === "claude" && nativeParentDirect ? { nativeParentDirect: true } : {}),
1954
+ ...(client === "claude" && parentDirectPermissions.length > 0 ? {
1955
+ nativeParentDirectPermissionGrants: parentDirectPermissions,
1956
+ } : {}),
1765
1957
  ...(client === "claude" && nativeSlashCommands ? { nativeSlashCommands: true } : {}),
1766
1958
  ...(client === "claude" && nativeIntercept ? {
1767
1959
  nativeIntercept: true,
1768
1960
  nativeInterceptTimeoutMs: interceptTimeoutMs,
1769
1961
  } : {}),
1962
+ ...(Object.keys(telemetryEnvironment).length > 0 ? {
1963
+ nativeAgentTelemetryEnvironment: telemetryEnvironment,
1964
+ } : {}),
1965
+ ...(benchmark ? { nativeBenchmark: true } : {}),
1770
1966
  syncedAt: new Date(now).toISOString(),
1771
1967
  files: [...currentFiles].sort(),
1772
1968
  ...(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;
@@ -1,4 +1,4 @@
1
1
  // Fleet generation shared by managed-profile writers and upstream clients.
2
2
  // Keep this isolated from apps.js so latency-sensitive transports do not load
3
3
  // desktop bundle machinery just to identify their managed config contract.
4
- export const CURRENT_CONFIG_VERSION = 35;
4
+ export const CURRENT_CONFIG_VERSION = 36;
@@ -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 = {}) {