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 +19 -0
- package/package.json +1 -1
- package/src/cliProfiles.js +7 -2
- package/src/codexSecurity.js +12 -1
- package/src/commands/launch.js +9 -3
- package/src/commands/sessions.js +0 -4
- package/src/managedApps.js +24 -21
- package/src/managedProfileVersion.js +1 -1
- package/src/sessionHooks.js +24 -0
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
package/src/cliProfiles.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
}
|
package/src/codexSecurity.js
CHANGED
|
@@ -162,13 +162,24 @@ export function enableManagedCodexDesktopCodeModeHost(
|
|
|
162
162
|
toml,
|
|
163
163
|
configPath = "managed Codex desktop config",
|
|
164
164
|
) {
|
|
165
|
-
|
|
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) {
|
package/src/commands/launch.js
CHANGED
|
@@ -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
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
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";
|
package/src/commands/sessions.js
CHANGED
|
@@ -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
|
package/src/managedApps.js
CHANGED
|
@@ -79,7 +79,7 @@ function legacyMacCandidates(tool, home, applicationsRoot) {
|
|
|
79
79
|
return candidates;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
function runnerEnvelopeCandidate(envelopePath, launcher,
|
|
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 !==
|
|
99
|
-
|| payload?.application?.adapter !==
|
|
98
|
+
|| payload?.application?.id !== applicationId
|
|
99
|
+
|| payload?.application?.adapter !== applicationId
|
|
100
100
|
|| !TENANT.test(tenantId || "")
|
|
101
101
|
|| String(tenantId).includes("..")
|
|
102
|
-
|| payload?.logicalApplicationId !== `${tenantId}.${
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
envelopes
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
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 =
|
|
17
|
+
export const CURRENT_CONFIG_VERSION = 46;
|
package/src/sessionHooks.js
CHANGED
|
@@ -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
|
+
}
|