impel-cli 0.20.13 → 0.20.15

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
@@ -16,6 +16,7 @@ import {
16
16
  redactSecretText,
17
17
  } from "./config.js";
18
18
  import {
19
+ IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES,
19
20
  IMPEL_NATIVE_AGENT_MCP_TARGET,
20
21
  impelNativeAgentMcpInvocation,
21
22
  } from "./selfInvocation.js";
@@ -51,7 +52,7 @@ export const NATIVE_AGENT_RUN_TOOL = "run_native_agent";
51
52
  export const NATIVE_AGENT_RESUME_TOOL = "resume_native_agent_run";
52
53
  export const NATIVE_AGENT_RECOVER_TOOL = "recover_native_agent_runs";
53
54
  export const MANAGED_AGENT_MCP_SERVER = "impel_agent";
54
- export const MANAGED_AGENT_MANIFEST_VERSION = 9;
55
+ export const MANAGED_AGENT_MANIFEST_VERSION = 10;
55
56
 
56
57
  const NATIVE_AGENT_TOOL_NAMES = [
57
58
  NATIVE_AGENT_RUN_TOOL,
@@ -631,8 +632,12 @@ function claudeAdapterInstructions(tenantId, agent) {
631
632
  ].join(" ");
632
633
  }
633
634
 
635
+ export function nativeToolNamespace(serverName = MANAGED_AGENT_MCP_SERVER) {
636
+ return `mcp__${serverName}`;
637
+ }
638
+
634
639
  function nativeToolName(toolName) {
635
- return `mcp__${MANAGED_AGENT_MCP_SERVER}__${toolName}`;
640
+ return `${nativeToolNamespace()}__${toolName}`;
636
641
  }
637
642
 
638
643
  function codexAdapterInstructions(tenantId, agent) {
@@ -732,23 +737,46 @@ function renderClaudeAgent({ tenantId, agent, name, invocation, recoveryOnly = f
732
737
  return lines.join("\n");
733
738
  }
734
739
 
735
- function renderCodexAgent({ tenantId, agent, name, invocation, recoveryOnly = false }) {
740
+ function codexInvocationEnvironment(invocation, { durableProfile = false } = {}) {
741
+ const entries = Object.entries(invocation.env || {});
742
+ if (!durableProfile) return entries;
743
+ const transient = new Set(IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES);
744
+ return entries.filter(([key]) => !transient.has(key));
745
+ }
746
+
747
+ function renderCodexConfiguration({
748
+ tenantId,
749
+ agent,
750
+ name,
751
+ invocation,
752
+ recoveryOnly = false,
753
+ durableProfile = false,
754
+ directCodeMode = true,
755
+ }) {
736
756
  const description = recoveryOnly
737
757
  ? `Recovery-only custom agent for pending runs from retired Impel binding ${agent.agentId} in tenant ${tenantId}.`
738
758
  : customAgentDescription(tenantId, agent);
739
759
  const toolNames = recoveryOnly ? NATIVE_AGENT_RECOVERY_TOOL_NAMES : nativeAgentToolNames(agent);
740
760
  const eager = eagerNativeAgentTransportEnabled();
741
- const envEntries = Object.entries(invocation.env || {})
761
+ const envEntries = codexInvocationEnvironment(invocation, { durableProfile })
742
762
  .map(([key, value]) => `${JSON.stringify(key)} = ${JSON.stringify(value)}`)
743
763
  .join(", ");
744
764
  const lines = [
745
- `name = ${JSON.stringify(name)}`,
746
- `description = ${JSON.stringify(description)}`,
765
+ ...(!durableProfile ? [
766
+ `name = ${JSON.stringify(name)}`,
767
+ `description = ${JSON.stringify(description)}`,
768
+ ] : []),
747
769
  'model = "gpt-5.6-luna"',
748
770
  'model_reasoning_effort = "low"',
749
771
  'sandbox_mode = "read-only"',
750
772
  `developer_instructions = ${JSON.stringify(recoveryOnly ? retiredAdapterInstructions(tenantId, agent) : codexAdapterInstructions(tenantId, agent))}`,
751
773
  "",
774
+ ...(directCodeMode ? [
775
+ "[features.code_mode]",
776
+ "enabled = true",
777
+ `direct_only_tool_namespaces = [${JSON.stringify(nativeToolNamespace())}]`,
778
+ "",
779
+ ] : []),
752
780
  `[mcp_servers.${MANAGED_AGENT_MCP_SERVER}]`,
753
781
  `command = ${JSON.stringify(invocation.command)}`,
754
782
  `args = [${invocation.args.map((argument) => JSON.stringify(argument)).join(", ")}]`,
@@ -764,6 +792,14 @@ function renderCodexAgent({ tenantId, agent, name, invocation, recoveryOnly = fa
764
792
  return lines.join("\n");
765
793
  }
766
794
 
795
+ function renderCodexAgent(options) {
796
+ return renderCodexConfiguration(options);
797
+ }
798
+
799
+ function renderCodexProfile(options) {
800
+ return renderCodexConfiguration({ ...options, durableProfile: true });
801
+ }
802
+
767
803
  function boundNativeAgentInvocation(tenantId, agent, invocation, {
768
804
  policyFingerprint = nativeAgentPolicyFingerprint(agent),
769
805
  mode = usesDirectAnswer(agent) ? "answer" : "durable",
@@ -794,7 +830,7 @@ function boundNativeAgentInvocation(tenantId, agent, invocation, {
794
830
  };
795
831
  }
796
832
 
797
- export function renderManagedAgents(client, tenantId, agents, invocation = null) {
833
+ export function renderManagedAgents(client, tenantId, agents, invocation = null, { directCodeMode = true } = {}) {
798
834
  if (client !== "claude" && client !== "codex") throw new Error(`unknown agent client ${client}`);
799
835
  const normalizedTenant = normalizeTenantId(tenantId);
800
836
  const fileStems = generatedAgentFileStems(normalizedTenant, agents);
@@ -811,20 +847,34 @@ export function renderManagedAgents(client, tenantId, agents, invocation = null)
811
847
  const boundInvocation = boundNativeAgentInvocation(normalizedTenant, agent, invocation);
812
848
  const contents = client === "claude"
813
849
  ? renderClaudeAgent({ tenantId: normalizedTenant, agent, name, invocation: boundInvocation })
814
- : renderCodexAgent({ tenantId: normalizedTenant, agent, name, invocation: boundInvocation });
850
+ : renderCodexAgent({ tenantId: normalizedTenant, agent, name, invocation: boundInvocation, directCodeMode });
851
+ const profileName = client === "codex" ? fileStem : null;
815
852
  return {
816
853
  agentId: agent.agentId,
854
+ title: agent.title,
855
+ sideEffects: agent.sideEffects,
817
856
  scopeParam: agent.scopeParam,
818
857
  policyFingerprint: nativeAgentPolicyFingerprint(agent),
819
858
  retired: false,
820
859
  name,
821
860
  fileName: `${fileStem}${extension}`,
822
861
  contents,
862
+ ...(profileName ? {
863
+ profileName,
864
+ profileFileName: `${profileName}.config.toml`,
865
+ profileContents: renderCodexProfile({
866
+ tenantId: normalizedTenant,
867
+ agent,
868
+ name,
869
+ invocation: boundInvocation,
870
+ directCodeMode,
871
+ }),
872
+ } : {}),
823
873
  };
824
874
  });
825
875
  }
826
876
 
827
- function renderRetiredManagedAgents(client, tenantId, bindings, active, invocation = null) {
877
+ function renderRetiredManagedAgents(client, tenantId, bindings, active, invocation = null, { directCodeMode = true } = {}) {
828
878
  const usedNames = new Set(active.map(({ name }) => name));
829
879
  const usedFiles = new Set(active.map(({ fileName }) => fileName));
830
880
  return bindings.map((binding) => {
@@ -865,15 +915,30 @@ function renderRetiredManagedAgents(client, tenantId, bindings, active, invocati
865
915
  });
866
916
  const contents = client === "claude"
867
917
  ? renderClaudeAgent({ tenantId, agent, name, invocation: boundInvocation, recoveryOnly: true })
868
- : renderCodexAgent({ tenantId, agent, name, invocation: boundInvocation, recoveryOnly: true });
918
+ : renderCodexAgent({ tenantId, agent, name, invocation: boundInvocation, recoveryOnly: true, directCodeMode });
919
+ const profileName = client === "codex" ? path.basename(fileName, extension) : null;
869
920
  return {
870
921
  agentId: binding.agentId,
922
+ title: agent.title,
923
+ sideEffects: agent.sideEffects,
871
924
  scopeParam: binding.scopeParam,
872
925
  policyFingerprint: binding.policyFingerprint,
873
926
  retired: true,
874
927
  name,
875
928
  fileName,
876
929
  contents,
930
+ ...(profileName ? {
931
+ profileName,
932
+ profileFileName: `${profileName}.config.toml`,
933
+ profileContents: renderCodexProfile({
934
+ tenantId,
935
+ agent,
936
+ name,
937
+ invocation: boundInvocation,
938
+ recoveryOnly: true,
939
+ directCodeMode,
940
+ }),
941
+ } : {}),
877
942
  };
878
943
  });
879
944
  }
@@ -903,16 +968,112 @@ function readManifest(manifestPath) {
903
968
  }
904
969
  }
905
970
 
971
+ function managedManifestPath(root) {
972
+ return path.join(root, "agents", MANAGED_AGENT_DIRECTORY, MANAGED_AGENT_MANIFEST);
973
+ }
974
+
975
+ function contentDigest(contents) {
976
+ return crypto.createHash("sha256").update(contents, "utf8").digest("hex");
977
+ }
978
+
979
+ function managedArtifactIsCurrent(root, relativePath, expectedDigest) {
980
+ if (typeof relativePath !== "string" || !relativePath || path.isAbsolute(relativePath)) return false;
981
+ const segments = relativePath.split("/");
982
+ if (segments.some((segment) => !segment || segment === "." || segment === "..")) return false;
983
+ if (!/^[a-f0-9]{64}$/u.test(expectedDigest || "")) return false;
984
+ const artifactPath = path.join(root, ...segments);
985
+ try {
986
+ if (fs.lstatSync(artifactPath).isSymbolicLink()) return false;
987
+ return contentDigest(fs.readFileSync(artifactPath, "utf8")) === expectedDigest;
988
+ } catch {
989
+ return false;
990
+ }
991
+ }
992
+
993
+ function managedArtifactExists(root, relativePath, expectedDigest) {
994
+ if (typeof relativePath !== "string" || !relativePath || path.isAbsolute(relativePath)) return false;
995
+ const segments = relativePath.split("/");
996
+ if (segments.some((segment) => !segment || segment === "." || segment === "..")) return false;
997
+ if (!/^[a-f0-9]{64}$/u.test(expectedDigest || "")) return false;
998
+ try {
999
+ const stat = fs.lstatSync(path.join(root, ...segments));
1000
+ return stat.isFile() && !stat.isSymbolicLink();
1001
+ } catch {
1002
+ return false;
1003
+ }
1004
+ }
1005
+
1006
+ export function readManagedAgentManifest(root) {
1007
+ return readManifest(managedManifestPath(root));
1008
+ }
1009
+
1010
+ export function resolveManagedCodexAgentProfile(root, tenantId, selector) {
1011
+ const normalizedTenant = normalizeTenantId(tenantId);
1012
+ const normalizedSelector = boundedString(selector, "agent selector", { max: 512 });
1013
+ const manifest = readManagedAgentManifest(root);
1014
+ if (!manifest
1015
+ || manifest.version !== MANAGED_AGENT_MANIFEST_VERSION
1016
+ || manifest.client !== "codex"
1017
+ || manifest.tenantId !== normalizedTenant
1018
+ || !Array.isArray(manifest.agents)) {
1019
+ throw new Error("the managed Codex agent catalog is missing or stale; run `impel agents sync codex`");
1020
+ }
1021
+ const records = manifest.agents.filter((record) => record
1022
+ && typeof record === "object"
1023
+ && typeof record.profileName === "string"
1024
+ && typeof record.profileFileName === "string");
1025
+ const active = records.filter((record) => !record.retired);
1026
+ let matches = active.filter((record) => record.agentId === normalizedSelector);
1027
+ if (matches.length === 0) matches = active.filter((record) => record.title === normalizedSelector);
1028
+ if (matches.length === 0) matches = active.filter((record) => record.profileName === normalizedSelector);
1029
+ if (matches.length === 0 && records.some((record) => record.retired && [
1030
+ record.agentId,
1031
+ record.title,
1032
+ record.profileName,
1033
+ ].includes(normalizedSelector))) {
1034
+ throw new Error(`managed Codex agent ${JSON.stringify(normalizedSelector)} is retired and cannot start new work`);
1035
+ }
1036
+ if (matches.length !== 1) {
1037
+ const reason = matches.length > 1 ? "is ambiguous" : "was not found";
1038
+ throw new Error(`managed Codex agent ${JSON.stringify(normalizedSelector)} ${reason} for tenant ${JSON.stringify(normalizedTenant)}`);
1039
+ }
1040
+ const [record] = matches;
1041
+ if (path.basename(record.profileFileName) !== record.profileFileName
1042
+ || record.profileFileName !== `${record.profileName}.config.toml`) {
1043
+ throw new Error("the managed Codex agent profile mapping is invalid; run `impel agents sync codex`");
1044
+ }
1045
+ const relativePath = record.profileFileName;
1046
+ if (!managedArtifactIsCurrent(root, relativePath, manifest.contentDigests?.[relativePath])) {
1047
+ throw new Error("the managed Codex agent profile failed its integrity check; run `impel agents sync codex`");
1048
+ }
1049
+ return { ...record, path: path.join(root, relativePath) };
1050
+ }
1051
+
906
1052
  function profileIsFresh(profile, tenantId, now, ttlMs) {
907
- const manifest = readManifest(path.join(profile.root, "agents", MANAGED_AGENT_DIRECTORY, MANAGED_AGENT_MANIFEST));
1053
+ const manifest = readManagedAgentManifest(profile.root);
908
1054
  if (!manifest || manifest.version !== MANAGED_AGENT_MANIFEST_VERSION || manifest.tenantId !== tenantId) return false;
1055
+ if (!Array.isArray(manifest.files) || !manifest.contentDigests
1056
+ || typeof manifest.contentDigests !== "object"
1057
+ || Array.isArray(manifest.contentDigests)) return false;
1058
+ const expectedDirectProfiles = profile.client === "codex" && profile.directProfiles !== false;
1059
+ const expectedDirectCodeMode = profile.client === "codex" && profile.directCodeMode !== false;
1060
+ if (manifest.directProfiles !== expectedDirectProfiles
1061
+ || manifest.directCodeMode !== expectedDirectCodeMode) return false;
909
1062
  const syncedAt = Date.parse(manifest.syncedAt || "");
910
1063
  if (!Number.isFinite(syncedAt) || now - syncedAt >= ttlMs) return false;
911
- return manifest.files.every((fileName) =>
912
- typeof fileName === "string"
913
- && path.basename(fileName) === fileName
914
- && fs.existsSync(path.join(profile.root, "agents", fileName))
915
- );
1064
+ const artifacts = [
1065
+ ...manifest.files.map((fileName) => `agents/${fileName}`),
1066
+ ...(Array.isArray(manifest.profiles) ? manifest.profiles : []),
1067
+ ];
1068
+ // Renderer changes are invalidated by the manifest version/capability fields
1069
+ // above. Keep ordinary launch freshness O(number of files) metadata-only;
1070
+ // the explicitly selected direct profile gets a full SHA-256 verification
1071
+ // in resolveManagedCodexAgentProfile before Codex starts.
1072
+ return artifacts.every((relativePath) => managedArtifactExists(
1073
+ profile.root,
1074
+ relativePath,
1075
+ manifest.contentDigests?.[relativePath],
1076
+ ));
916
1077
  }
917
1078
 
918
1079
  export function syncAgentProfile({
@@ -921,6 +1082,8 @@ export function syncAgentProfile({
921
1082
  label,
922
1083
  tenantId,
923
1084
  agents,
1085
+ directProfiles = client === "codex",
1086
+ directCodeMode = client === "codex",
924
1087
  now = Date.now(),
925
1088
  nativeAgentRunsRoot = path.join(CONFIG_DIR, "native-agent-runs"),
926
1089
  }) {
@@ -934,7 +1097,7 @@ export function syncAgentProfile({
934
1097
  privateDirectory(managedDir);
935
1098
  const manifestPath = path.join(managedDir, MANAGED_AGENT_MANIFEST);
936
1099
  const prior = readManifest(manifestPath);
937
- const active = renderManagedAgents(client, tenantId, agents);
1100
+ const active = renderManagedAgents(client, tenantId, agents, null, { directCodeMode });
938
1101
  const activeBindings = new Set(active.map((agent) =>
939
1102
  `${agent.agentId}\0${agent.scopeParam}\0${agent.policyFingerprint}`
940
1103
  ));
@@ -942,16 +1105,22 @@ export function syncAgentProfile({
942
1105
  .filter((binding) => !activeBindings.has(
943
1106
  `${binding.agentId}\0${binding.scopeParam}\0${binding.policyFingerprint}`,
944
1107
  ));
945
- const retired = renderRetiredManagedAgents(client, tenantId, retiredBindings, active);
1108
+ const retired = renderRetiredManagedAgents(client, tenantId, retiredBindings, active, null, { directCodeMode });
946
1109
  const rendered = [...active, ...retired];
947
1110
  if (new Set(rendered.map(({ name }) => name)).size !== rendered.length
948
- || new Set(rendered.map(({ fileName }) => fileName)).size !== rendered.length) {
1111
+ || new Set(rendered.map(({ fileName }) => fileName)).size !== rendered.length
1112
+ || (client === "codex" && directProfiles
1113
+ && new Set(rendered.map(({ profileFileName }) => profileFileName)).size !== rendered.length)) {
949
1114
  throw new Error("generated native-agent destinations are not unique");
950
1115
  }
951
1116
  const priorFiles = new Set(prior?.files || []);
1117
+ const priorProfiles = new Set(prior?.profiles || []);
952
1118
  const priorUsesDiscoveryRoot = Number.isInteger(prior?.version)
953
1119
  && prior.version >= 2
954
1120
  && prior.version <= MANAGED_AGENT_MANIFEST_VERSION;
1121
+ const priorOwnsCodexProfiles = priorUsesDiscoveryRoot
1122
+ && prior?.client === "codex"
1123
+ && prior?.tenantId === tenantId;
955
1124
 
956
1125
  // Native clients discover standalone definitions directly under `agents/`.
957
1126
  // Preflight every destination before writing so an unmanaged file with the
@@ -966,11 +1135,26 @@ export function syncAgentProfile({
966
1135
  throw new Error(`refusing to overwrite symlinked native-agent file ${destination}`);
967
1136
  }
968
1137
  }
1138
+ if (client === "codex" && directProfiles) {
1139
+ const profileDestination = path.join(root, agent.profileFileName);
1140
+ if (fs.existsSync(profileDestination)) {
1141
+ if (fs.lstatSync(profileDestination).isSymbolicLink()) {
1142
+ throw new Error(`refusing to overwrite symlinked Codex profile ${profileDestination}`);
1143
+ }
1144
+ if (!priorOwnsCodexProfiles || !priorProfiles.has(agent.profileFileName)) {
1145
+ throw new Error(`refusing to overwrite unmanaged Codex profile ${profileDestination}`);
1146
+ }
1147
+ }
1148
+ }
969
1149
  }
970
1150
  for (const agent of rendered) {
971
1151
  atomicPrivateWrite(path.join(agentsDir, agent.fileName), agent.contents);
1152
+ if (client === "codex" && directProfiles) atomicPrivateWrite(path.join(root, agent.profileFileName), agent.profileContents);
972
1153
  }
973
1154
  const currentFiles = new Set(rendered.map((agent) => agent.fileName));
1155
+ const currentProfiles = new Set(client === "codex" && directProfiles
1156
+ ? rendered.map((agent) => agent.profileFileName)
1157
+ : []);
974
1158
  for (const stale of prior?.files || []) {
975
1159
  if (
976
1160
  typeof stale === "string"
@@ -981,6 +1165,16 @@ export function syncAgentProfile({
981
1165
  fs.rmSync(path.join(priorUsesDiscoveryRoot ? agentsDir : managedDir, stale), { force: true });
982
1166
  }
983
1167
  }
1168
+ for (const stale of priorOwnsCodexProfiles ? prior.profiles || [] : []) {
1169
+ if (
1170
+ typeof stale === "string"
1171
+ && path.basename(stale) === stale
1172
+ && stale.endsWith(".config.toml")
1173
+ && !currentProfiles.has(stale)
1174
+ ) {
1175
+ fs.rmSync(path.join(root, stale), { force: true });
1176
+ }
1177
+ }
984
1178
  if (!priorUsesDiscoveryRoot) {
985
1179
  for (const legacy of prior?.files || []) {
986
1180
  if (
@@ -992,18 +1186,34 @@ export function syncAgentProfile({
992
1186
  }
993
1187
  }
994
1188
  }
1189
+ const contentDigests = Object.fromEntries(rendered.flatMap((agent) => [
1190
+ [`agents/${agent.fileName}`, contentDigest(agent.contents)],
1191
+ ...(client === "codex" && directProfiles
1192
+ ? [[agent.profileFileName, contentDigest(agent.profileContents)]]
1193
+ : []),
1194
+ ]).sort(([left], [right]) => left.localeCompare(right)));
995
1195
  atomicPrivateWrite(manifestPath, `${JSON.stringify({
996
1196
  version: MANAGED_AGENT_MANIFEST_VERSION,
997
1197
  tenantId,
998
1198
  client,
1199
+ directProfiles: client === "codex" && directProfiles,
1200
+ directCodeMode: client === "codex" && directCodeMode,
999
1201
  syncedAt: new Date(now).toISOString(),
1000
1202
  files: [...currentFiles].sort(),
1203
+ profiles: [...currentProfiles].sort(),
1204
+ contentDigests,
1001
1205
  agents: rendered.map((agent) => ({
1002
1206
  agentId: agent.agentId,
1207
+ title: agent.title,
1208
+ sideEffects: agent.sideEffects,
1003
1209
  scopeParam: agent.scopeParam,
1004
1210
  policyFingerprint: agent.policyFingerprint,
1005
1211
  retired: agent.retired,
1006
1212
  name: agent.name,
1213
+ ...(client === "codex" && directProfiles ? {
1214
+ profileName: agent.profileName,
1215
+ profileFileName: agent.profileFileName,
1216
+ } : {}),
1007
1217
  })),
1008
1218
  }, null, 2)}\n`);
1009
1219
  return {
@@ -1014,6 +1224,7 @@ export function syncAgentProfile({
1014
1224
  count: active.length,
1015
1225
  recoveryCount: retired.length,
1016
1226
  files: [...currentFiles],
1227
+ profiles: [...currentProfiles],
1017
1228
  };
1018
1229
  }
1019
1230
 
package/src/apps.js CHANGED
@@ -108,8 +108,8 @@ export const PINNED_VENDOR_APPS = Object.freeze({
108
108
  // The Microsoft Store manifest identifies this exact reviewed build.
109
109
  // Keep one Windows package version so the manifest contract and local
110
110
  // AppX validation cannot silently drift apart again.
111
- packageVersion: "26.730.8199.0",
112
- codexVersion: "0.147.0-alpha.1.2",
111
+ packageVersion: "26.803.5235.0",
112
+ codexVersion: "0.147.0-alpha.6.5",
113
113
  publisherId: "2p2nqsd0c76g0",
114
114
  executable: "app\\ChatGPT.exe",
115
115
  updateManifestUrl: "https://persistent.oaistatic.com/codex-app-prod/windows-store-update.json",
@@ -279,7 +279,9 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
279
279
  // transport and remove model-controlled upstream start/read polling.
280
280
  // 29: add marker-aware custom-agent verbatim relay guidance to ChatGPT and
281
281
  // expose direct-answer native agents through their one-shot local MCP binding.
282
- export const CURRENT_CONFIG_VERSION = 29;
282
+ // 30: generate integrity-tracked top-level Codex profiles and direct code-mode
283
+ // namespaces for fixed native-agent bindings.
284
+ export const CURRENT_CONFIG_VERSION = 30;
283
285
 
284
286
  // Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
285
287
  // helper rebranding, and signing. A vendored bundle is rebuilt only when this
package/src/cli.js CHANGED
@@ -35,6 +35,7 @@ Get started:
35
35
  Work:
36
36
  impel claude [args...] Launch Claude Code with an isolated Impel profile
37
37
  impel codex [args...] Launch Codex with an isolated Impel profile
38
+ impel codex --agent <id|exact-title> ... Run one fixed tenant agent without a parent hop
38
39
  impel remote handoff|dispatch|handback Move or control provider-native sessions remotely
39
40
  impel remote status|viewer|proxy Inspect, control, or connect to a remote run
40
41
  impel tenant list List accessible organizations
@@ -38,6 +38,10 @@ export function managedAgentProfiles(client, options = {}) {
38
38
  environment,
39
39
  homeDir,
40
40
  }),
41
+ ...(client === "codex" ? {
42
+ directProfiles: profile.label !== "Codex CLI (native profile)",
43
+ directCodeMode: profile.label !== "Codex CLI (native profile)",
44
+ } : {}),
41
45
  }));
42
46
  }
43
47
 
@@ -6,7 +6,7 @@ import {
6
6
  ensureImpelClaudeProfile,
7
7
  ensureImpelCodexProfile,
8
8
  } from "../cliProfiles.js";
9
- import { syncAgentProfilesSafe } from "../agents.js";
9
+ import { resolveManagedCodexAgentProfile, syncAgentProfilesSafe } from "../agents.js";
10
10
  import {
11
11
  crossAppModelsEnabled,
12
12
  loadConfig,
@@ -77,7 +77,7 @@ const IMPEL_CODEX_RUNTIME_OVERRIDES = [
77
77
  "features.code_mode_host=false",
78
78
  ];
79
79
 
80
- export function impelLaunchArguments(tool, argv) {
80
+ export function impelLaunchArguments(tool, argv, { codexAgentProfile = null } = {}) {
81
81
  if (!RUNTIME_BRAND.features.agents) return [...argv];
82
82
  if (tool === "claude") {
83
83
  return ["--append-system-prompt", IMPEL_CLAUDE_PARENT_DELEGATION_INSTRUCTIONS, ...argv];
@@ -86,8 +86,10 @@ export function impelLaunchArguments(tool, argv) {
86
86
  // `-c` is a Codex global option, so it must precede subcommands such as
87
87
  // `exec`, `resume`, and `mcp`. JSON strings are valid TOML basic strings.
88
88
  return [
89
- "-c",
90
- `developer_instructions=${JSON.stringify(IMPEL_CODEX_PARENT_DELEGATION_INSTRUCTIONS)}`,
89
+ ...(codexAgentProfile ? ["--profile", codexAgentProfile] : [
90
+ "-c",
91
+ `developer_instructions=${JSON.stringify(IMPEL_CODEX_PARENT_DELEGATION_INSTRUCTIONS)}`,
92
+ ]),
91
93
  ...IMPEL_CODEX_RUNTIME_OVERRIDES.flatMap((override) => ["-c", override]),
92
94
  ...argv,
93
95
  ];
@@ -95,6 +97,91 @@ export function impelLaunchArguments(tool, argv) {
95
97
  throw new Error(`unsupported CLI launcher: ${tool}`);
96
98
  }
97
99
 
100
+ const CODEX_AGENT_LOCKED_OPTIONS = new Set([
101
+ "-c",
102
+ "--config",
103
+ "-m",
104
+ "--model",
105
+ "-p",
106
+ "--profile",
107
+ "-s",
108
+ "--sandbox",
109
+ "-a",
110
+ "--ask-for-approval",
111
+ "--enable",
112
+ "--disable",
113
+ "--full-auto",
114
+ "--approve-for-me",
115
+ "--dangerously-bypass-approvals-and-sandbox",
116
+ "--dangerously-bypass-hook-trust",
117
+ "--oss",
118
+ "--local-provider",
119
+ "--search",
120
+ ]);
121
+
122
+ function codexAgentLockedOption(argument) {
123
+ if (CODEX_AGENT_LOCKED_OPTIONS.has(argument)) return argument;
124
+ const withEquals = [...CODEX_AGENT_LOCKED_OPTIONS].find((option) =>
125
+ argument.startsWith(`${option}=`)
126
+ );
127
+ if (withEquals) return withEquals;
128
+ return ["-c", "-m", "-p", "-s", "-a"].find((option) =>
129
+ argument.length > option.length && argument.startsWith(option)
130
+ ) || null;
131
+ }
132
+
133
+ export function parseCodexAgentLaunchArguments(argv) {
134
+ const passthrough = [];
135
+ let selector = null;
136
+ let literal = false;
137
+ for (let index = 0; index < argv.length; index += 1) {
138
+ const argument = argv[index];
139
+ if (literal) {
140
+ passthrough.push(argument);
141
+ continue;
142
+ }
143
+ if (argument === "--") {
144
+ literal = true;
145
+ passthrough.push(argument);
146
+ continue;
147
+ }
148
+ if (argument === "--agent" || argument.startsWith("--agent=")) {
149
+ if (selector !== null) throw new Error("`--agent` may be specified only once");
150
+ const value = argument === "--agent" ? argv[index += 1] : argument.slice("--agent=".length);
151
+ if (typeof value !== "string" || !value.trim() || value.startsWith("-")) {
152
+ throw new Error("`--agent` requires an exact agent ID or title");
153
+ }
154
+ selector = value.trim();
155
+ continue;
156
+ }
157
+ passthrough.push(argument);
158
+ }
159
+ if (selector !== null) {
160
+ for (const argument of passthrough) {
161
+ if (argument === "--") break;
162
+ const locked = codexAgentLockedOption(argument);
163
+ if (locked) {
164
+ throw new Error(
165
+ `${locked} cannot be used with \`--agent\`; the managed profile locks model, instructions, tools, approvals, and sandbox policy`,
166
+ );
167
+ }
168
+ }
169
+ }
170
+ return { selector, argv: passthrough };
171
+ }
172
+
173
+ function codexJsonOutput(argv) {
174
+ return argv.some((argument) => argument === "--json" || argument.startsWith("--json="));
175
+ }
176
+
177
+ function agentSyncLogger(argv) {
178
+ if (!codexJsonOutput(argv)) return console;
179
+ return {
180
+ log: (...args) => console.error(...args),
181
+ warn: (...args) => console.error(...args),
182
+ };
183
+ }
184
+
98
185
  function deleteEnvironmentKeys(environment, keys) {
99
186
  for (const key of keys) delete environment[key];
100
187
  }
@@ -174,6 +261,17 @@ function runNativeCli(tool, argv, environment) {
174
261
  }
175
262
 
176
263
  export async function cmdLaunch(tool, argv) {
264
+ let nativeArgv = [...argv];
265
+ let codexAgentSelector = null;
266
+ if (tool === "codex") {
267
+ try {
268
+ ({ selector: codexAgentSelector, argv: nativeArgv } = parseCodexAgentLaunchArguments(argv));
269
+ } catch (error) {
270
+ console.error(`impel codex: ${error.message}`);
271
+ process.exitCode = 1;
272
+ return;
273
+ }
274
+ }
177
275
  const config = loadConfig();
178
276
  if (!config?.pat) {
179
277
  console.error(`impel ${tool}: not authenticated. Run \`impel auth\` first.`);
@@ -199,7 +297,7 @@ export async function cmdLaunch(tool, argv) {
199
297
  return;
200
298
  }
201
299
  const gatewayCredential = tenantCredential(config.pat, tenantId);
202
- if (!localInformationRequest(argv)) {
300
+ if (!localInformationRequest(nativeArgv)) {
203
301
  try {
204
302
  await assertLiveProviderReadiness({
205
303
  config,
@@ -252,13 +350,33 @@ export async function cmdLaunch(tool, argv) {
252
350
  credential: gatewayCredential,
253
351
  tenantId,
254
352
  staleOnly: true,
353
+ logger: agentSyncLogger(nativeArgv),
255
354
  });
256
355
  }
257
356
 
357
+ let codexAgentProfile = null;
358
+ if (codexAgentSelector !== null) {
359
+ try {
360
+ codexAgentProfile = resolveManagedCodexAgentProfile(
361
+ agentProfile.root,
362
+ tenantId,
363
+ codexAgentSelector,
364
+ ).profileName;
365
+ } catch (error) {
366
+ console.error(`impel codex: ${redactSecretText(error?.message || error)}`);
367
+ process.exitCode = 1;
368
+ return;
369
+ }
370
+ }
371
+
258
372
  // Both vendor CLIs shell out to `git` for plugin/marketplace operations; on
259
373
  // a fresh Windows machine the installed git (typically the Impel-managed
260
374
  // MinGit) sits off the inherited PATH, so repair it for the child session.
261
375
  const { env: launchEnvironment } = withGitEnvironment(environment, { baseEnvironment: environment });
262
- const exitCode = await runNativeCli(tool, impelLaunchArguments(tool, argv), launchEnvironment);
376
+ const exitCode = await runNativeCli(
377
+ tool,
378
+ impelLaunchArguments(tool, nativeArgv, { codexAgentProfile }),
379
+ launchEnvironment,
380
+ );
263
381
  if (exitCode !== 0) process.exitCode = exitCode;
264
382
  }