impel-cli 0.20.59 → 0.20.61

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/RELEASE_NOTES.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Release notes
2
2
 
3
+ ## 0.20.61 — Silence the Codex apps connector at startup
4
+
5
+ - Passes `features.apps=false` to tenant Codex CLI sessions so Codex no longer
6
+ contributes its hosted `codex_apps` connector, which handshaked against the
7
+ gateway passthrough without a credential and printed an HTTP 401 "MCP startup
8
+ incomplete" warning on every `impel codex` launch. The 0.20.59 mitigation set
9
+ `enable_mcp_apps=false`, which only governs MCP-UI rendering and left the
10
+ connector enabled.
11
+
12
+ ## 0.20.60 — Fix managed Codex launch discovery
13
+
14
+ - Fixes `impel codex` failing with "not authenticated and no enrolled managed
15
+ application is installed" on devices enrolled through the signed runner:
16
+ discovery now maps `codex` to ChatGPT descriptors like the legacy and
17
+ Windows paths, preferring a standalone Codex enrollment for the same tenant.
18
+ - Fix Codex CLI hook trust fingerprints to include the cross-platform command
19
+ definition. This keeps managed session hooks trusted after Codex upgrades
20
+ instead of repeatedly showing startup review warnings.
21
+
3
22
  ## 0.20.59 — Stabilize Codex CLI startup
4
23
 
5
24
  - Disables the optional standalone Code Mode host for headless Codex CLI
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.20.59",
3
+ "version": "0.20.61",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,7 +19,7 @@ import {
19
19
  } from "./selfInvocation.js";
20
20
  import { applyImpelClaudeSandbox } from "./claudeSandbox.js";
21
21
  import { renameWithWindowsRetry } from "./windowsFs.js";
22
- import { ensureClaudeSessionHooks, ensureCodexSessionHooks } from "./sessionHooks.js";
22
+ import { ensureClaudeSessionHooks, removeManagedCodexSessionHooks } from "./sessionHooks.js";
23
23
  import { RUNTIME_BRAND } from "./runtimeBrand.js";
24
24
  import { managedCodexCliCatalog } from "./modelCatalog.js";
25
25
  import { codexModelsManifestPath, syncedCodexCatalogMatches } from "./modelsManifest.js";
@@ -298,7 +298,12 @@ export function ensureImpelCodexProfile(gatewayUrl, tenantId, { crossAppModels =
298
298
  writePrivateFile(codexCatalog, `${JSON.stringify(managedCodexCliCatalog(), null, 2)}\n`);
299
299
  fs.rmSync(codexModelsManifestPath(codexHome), { force: true });
300
300
  }
301
- if (RUNTIME_BRAND.features.sessions) ensureCodexSessionHooks(codexHome, tenantId, "codex_cli");
301
+ // Codex CLI profiles intentionally do not install lifecycle hooks. Hooks
302
+ // are a desktop-session integration and Codex now reviews every user-layer
303
+ // hook at startup (including plugin hooks), producing noisy prompts and
304
+ // making the effective hook count depend on the current project. Telemetry
305
+ // for CLI sessions is collected through the token heartbeat instead.
306
+ removeManagedCodexSessionHooks(codexHome);
302
307
  secureManagedCodexHome(codexHome);
303
308
  return { codexHome, configPath, catalogPath: codexCatalog, syncedCatalog: keepSyncedCatalog };
304
309
  }
@@ -162,13 +162,24 @@ export function enableManagedCodexDesktopCodeModeHost(
162
162
  toml,
163
163
  configPath = "managed Codex desktop config",
164
164
  ) {
165
- return upsertManagedScalar(
165
+ const withCodeModeHost = upsertManagedScalar(
166
166
  toml,
167
167
  "features",
168
168
  "code_mode_host",
169
169
  "true",
170
170
  configPath,
171
171
  );
172
+ // Managed desktop profiles do not ship the native Codex Apps companion and
173
+ // must not eagerly initialize the vendor `codex_apps` MCP client. That
174
+ // client expects OpenAI account credentials, while Impel routes inference
175
+ // through the gateway and intentionally has no native account token.
176
+ return upsertManagedScalar(
177
+ withCodeModeHost,
178
+ "features",
179
+ "enable_mcp_apps",
180
+ "false",
181
+ configPath,
182
+ );
172
183
  }
173
184
 
174
185
  function assertSafeManagedPath(target, expectedType) {
@@ -85,10 +85,16 @@ const IMPEL_CODEX_RUNTIME_OVERRIDES = [
85
85
  // launcher path for tenant CLI sessions so a missing helper cannot abort
86
86
  // startup. Desktop bundles that include the helper enable it explicitly.
87
87
  "features.code_mode_host=false",
88
- // Headless CLI sessions do not render MCP Apps. Disabling the connector
89
- // avoids an eager codex_apps initialize against the vendor gateway, which
90
- // otherwise fails with HTTP 401 when no native account credential exists.
88
+ // Codex contributes the hosted codex_apps connector whenever the stable
89
+ // `apps` feature is on. It handshakes against chatgpt_base_url with no
90
+ // gateway credential, so headless tenant sessions get an HTTP 401 at
91
+ // startup. `enable_mcp_apps` only governs MCP-UI rendering and does not
92
+ // gate the connector; keep it off as well since the CLI never renders apps.
93
+ "features.apps=false",
91
94
  "features.enable_mcp_apps=false",
95
+ // CLI profiles do not load desktop/plugin lifecycle bundles. Plugin hooks
96
+ // are user-layer code and newer Codex versions review every one at startup.
97
+ "features.plugins=false",
92
98
  ];
93
99
 
94
100
  const IMPEL_CODEX_UNRESTRICTED_ARGUMENT = "--dangerously-bypass-approvals-and-sandbox";
@@ -176,10 +176,6 @@ export async function cmdSessions(argv) {
176
176
  });
177
177
  if (flags.provider === "codex") {
178
178
  maybeRepairManagedCodexApp(flags.tenant);
179
- // Same guaranteed-to-run channel, second artifact: keep the isolated
180
- // CLI Codex model catalog converging from codex session events too
181
- // (TTL + heartbeat-locked, detached `models sync --stale-only`).
182
- maybeSyncCliModels(flags.tenant);
183
179
  }
184
180
  // The Claude mirror of the codex repair above: Claude Desktop rewrites
185
181
  // its co-owned profile files too, and its session hooks are the only
@@ -79,7 +79,7 @@ function legacyMacCandidates(tool, home, applicationsRoot) {
79
79
  return candidates;
80
80
  }
81
81
 
82
- function runnerEnvelopeCandidate(envelopePath, launcher, tool, home) {
82
+ function runnerEnvelopeCandidate(envelopePath, launcher, applicationId, home) {
83
83
  if (!regularFile(envelopePath) || !regularFile(launcher)) return null;
84
84
  let envelope;
85
85
  try {
@@ -95,11 +95,11 @@ function runnerEnvelopeCandidate(envelopePath, launcher, tool, home) {
95
95
  envelope?.schemaVersion !== 1
96
96
  || payload?.kind !== "com.useimpel.managed-application-descriptor"
97
97
  || payload?.runtimeContract?.entrypoint !== "engine/main.mjs"
98
- || payload?.application?.id !== tool
99
- || payload?.application?.adapter !== tool
98
+ || payload?.application?.id !== applicationId
99
+ || payload?.application?.adapter !== applicationId
100
100
  || !TENANT.test(tenantId || "")
101
101
  || String(tenantId).includes("..")
102
- || payload?.logicalApplicationId !== `${tenantId}.${tool}`
102
+ || payload?.logicalApplicationId !== `${tenantId}.${applicationId}`
103
103
  ) return null;
104
104
  const credential = path.join(home, ".config", "impel", "apps", "credentials", `${tenantId}.json`);
105
105
  if (!regularFile(credential)) return null;
@@ -120,25 +120,28 @@ function managedRunnerMacCandidates(tool, home, descriptorRoot, runnerLauncher)
120
120
  } catch {
121
121
  return [];
122
122
  }
123
- const candidates = [];
124
- for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
125
- if (!entry.isDirectory() || !entry.name.endsWith(`.${tool}`)) continue;
126
- const directory = path.join(descriptorRoot, entry.name);
127
- let envelopes;
128
- try {
129
- envelopes = fs.readdirSync(directory, { withFileTypes: true });
130
- } catch {
131
- continue;
132
- }
133
- for (const envelope of envelopes.sort((left, right) => left.name.localeCompare(right.name))) {
134
- if (!envelope.isFile() || !envelope.name.endsWith(".json")) continue;
135
- const candidate = runnerEnvelopeCandidate(path.join(directory, envelope.name), runnerLauncher, tool, home);
136
- if (!candidate || candidate.logicalApplicationId !== entry.name) continue;
137
- candidates.push(candidate);
138
- break;
123
+ entries.sort((left, right) => left.name.localeCompare(right.name));
124
+ const byTenant = new Map();
125
+ for (const applicationId of APPLICATIONS_BY_TOOL[tool]) {
126
+ for (const entry of entries) {
127
+ if (!entry.isDirectory() || !entry.name.endsWith(`.${applicationId}`)) continue;
128
+ const directory = path.join(descriptorRoot, entry.name);
129
+ let envelopes;
130
+ try {
131
+ envelopes = fs.readdirSync(directory, { withFileTypes: true });
132
+ } catch {
133
+ continue;
134
+ }
135
+ for (const envelope of envelopes.sort((left, right) => left.name.localeCompare(right.name))) {
136
+ if (!envelope.isFile() || !envelope.name.endsWith(".json")) continue;
137
+ const candidate = runnerEnvelopeCandidate(path.join(directory, envelope.name), runnerLauncher, applicationId, home);
138
+ if (!candidate || candidate.logicalApplicationId !== entry.name) continue;
139
+ if (!byTenant.has(candidate.tenantId)) byTenant.set(candidate.tenantId, candidate);
140
+ break;
141
+ }
139
142
  }
140
143
  }
141
- return candidates;
144
+ return [...byTenant.values()];
142
145
  }
143
146
 
144
147
  function macCandidates(tool, home, applicationsRoot, descriptorRoot, runnerLauncher) {
@@ -14,4 +14,4 @@
14
14
  // reasserting the FALLBACK_MODELS floor, records provenance in
15
15
  // models-manifest.json, and (with cross-app models enabled) routes the CLI
16
16
  // provider through the experimental OpenAI-compatible gateway path.
17
- export const CURRENT_CONFIG_VERSION = 45;
17
+ export const CURRENT_CONFIG_VERSION = 46;
@@ -246,3 +246,27 @@ export function ensureCodexSessionHooks(codexHome, tenantId, surface = "codex_cl
246
246
  replaceManagedTrustBlock(configPath, trustEntries);
247
247
  return { hooksPath, configPath, events: [...CODEX_SESSION_EVENTS], trustEntries };
248
248
  }
249
+
250
+ /** Remove Impel-owned CLI hooks while preserving user-authored hooks. */
251
+ export function removeManagedCodexSessionHooks(codexHome) {
252
+ const hooksPath = path.join(codexHome, "hooks.json");
253
+ if (!fs.existsSync(hooksPath)) return;
254
+ const document = readJsonObject(hooksPath);
255
+ const hooks = document.hooks && typeof document.hooks === "object" && !Array.isArray(document.hooks)
256
+ ? { ...document.hooks } : {};
257
+ for (const event of Object.keys(hooks)) {
258
+ hooks[event] = Array.isArray(hooks[event])
259
+ ? hooks[event].map((group) => {
260
+ if (!group || typeof group !== "object" || !Array.isArray(group.hooks)) return group;
261
+ const retained = group.hooks.filter((handler) => !handlerIsManaged(handler));
262
+ return retained.length ? { ...group, hooks: retained } : null;
263
+ }).filter(Boolean)
264
+ : hooks[event];
265
+ if (Array.isArray(hooks[event]) && hooks[event].length === 0) delete hooks[event];
266
+ }
267
+ if (Object.keys(hooks).length) document.hooks = hooks;
268
+ else delete document.hooks;
269
+ writePrivateJson(hooksPath, document);
270
+ const configPath = path.join(codexHome, "config.toml");
271
+ if (fs.existsSync(configPath)) replaceManagedTrustBlock(configPath, []);
272
+ }