impel-cli 0.20.38 → 0.20.40
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/README.md +4 -3
- package/RELEASE_NOTES.md +36 -0
- package/bin/impel.js +10 -1
- package/docs/native-agent-host-capability-matrix.md +4 -3
- package/package.json +1 -1
- package/scripts/capture-release-fixtures.mjs +56 -0
- package/scripts/clean-machine/README.md +51 -0
- package/scripts/clean-machine/run-clean-windows.sh +115 -0
- package/scripts/clean-machine/userdata.ps1.template +102 -0
- package/scripts/regen-managed-artifacts.mjs +475 -0
- package/scripts/verify-vendor-pins.mjs +207 -0
- package/src/agents.js +47 -15
- package/src/apps.js +270 -82
- package/src/cliProfiles.js +1 -1
- package/src/codexSecurity.js +21 -0
- package/src/commands/apps.js +25 -2
- package/src/commands/converge.js +17 -1
- package/src/commands/mcp.js +1 -1
- package/src/commands/sessions.js +44 -11
- package/src/commands/setup.js +17 -1
- package/src/commands/token.js +23 -3
- package/src/commands/update.js +144 -16
- package/src/installRecovery/engine.js +45 -0
- package/src/macSetup.js +26 -8
- package/src/modelCatalog.js +103 -0
- package/src/nativeAgentTransport.js +13 -4
- package/src/platformSetup.js +9 -3
- package/src/vendorCliBinaries.js +36 -7
- package/src/verbatimRelay.js +9 -1
- package/src/windowsSetup.js +20 -0
- package/src/codexCliCatalog.js +0 -56
package/src/agents.js
CHANGED
|
@@ -56,7 +56,7 @@ export const NATIVE_AGENT_RESUME_TOOL = "resume_native_agent_run";
|
|
|
56
56
|
export const NATIVE_AGENT_RECOVER_TOOL = "recover_native_agent_runs";
|
|
57
57
|
export const NATIVE_AGENT_CONTINUATION_SCHEMA = "impel.native-agent-continuation.v1";
|
|
58
58
|
export const MANAGED_AGENT_MCP_SERVER = "impel_agent";
|
|
59
|
-
export const MANAGED_AGENT_MANIFEST_VERSION =
|
|
59
|
+
export const MANAGED_AGENT_MANIFEST_VERSION = 22;
|
|
60
60
|
|
|
61
61
|
// The host model only selects the fixed MCP tool and faithfully returns its
|
|
62
62
|
// result. Spark minimizes those transport-only turns while the selected Eve
|
|
@@ -623,7 +623,7 @@ function claudeAdapterInstructions(tenantId, agent) {
|
|
|
623
623
|
`Confirm that the request fits the synchronized capabilities ${JSON.stringify(agent.capabilities)} and none of the exclusions ${JSON.stringify(agent.exclusions)}.${contextRequirement}`,
|
|
624
624
|
sideEffectInstruction,
|
|
625
625
|
`Call ${nativeToolName(NATIVE_AGENT_ANSWER_TOOL)} exactly once with question set to the complete assigned task, optional context set to one string containing all supplied context, and contextKeys naming the fields present in that string.`,
|
|
626
|
-
`If the bounded answer returns an object whose schema is exactly ${JSON.stringify(NATIVE_AGENT_CONTINUATION_SCHEMA)}, pass that complete object unchanged as the handle to ${nativeToolName(NATIVE_AGENT_RESUME_TOOL)} until terminal. Never change its schema
|
|
626
|
+
`If the bounded answer returns an object whose schema is exactly ${JSON.stringify(NATIVE_AGENT_CONTINUATION_SCHEMA)}, pass that complete object unchanged as the handle to ${nativeToolName(NATIVE_AGENT_RESUME_TOOL)} until terminal. Never change its schema, add handle metadata, or fabricate a continuation from any other field. If the answer call is cancelled or fails before returning any continuation, call ${nativeToolName(NATIVE_AGENT_ANSWER_TOOL)} again with the same question; once a continuation has been returned, never call it again for this request.`,
|
|
627
627
|
completionGuidance,
|
|
628
628
|
].join(" ");
|
|
629
629
|
}
|
|
@@ -673,7 +673,7 @@ function codexAdapterInstructions(tenantId, agent) {
|
|
|
673
673
|
`Confirm that the assigned request fits the cataloged capabilities ${JSON.stringify(agent.capabilities)} and none of the exclusions ${JSON.stringify(agent.exclusions)} before answering.${contextRequirement}`,
|
|
674
674
|
sideEffectInstruction,
|
|
675
675
|
`Call ${nativeToolName(NATIVE_AGENT_ANSWER_TOOL)} exactly once with question set to the complete assigned task, optional context set to one string containing all supplied context, and contextKeys naming the fields present in that string.`,
|
|
676
|
-
`If the bounded answer returns an object whose schema is exactly ${JSON.stringify(NATIVE_AGENT_CONTINUATION_SCHEMA)}, pass that complete object unchanged as the handle to ${nativeToolName(NATIVE_AGENT_RESUME_TOOL)} until terminal. Never change its schema
|
|
676
|
+
`If the bounded answer returns an object whose schema is exactly ${JSON.stringify(NATIVE_AGENT_CONTINUATION_SCHEMA)}, pass that complete object unchanged as the handle to ${nativeToolName(NATIVE_AGENT_RESUME_TOOL)} until terminal. Never change its schema, add handle metadata, or fabricate a continuation from any other field. If the answer call is cancelled or fails before returning any continuation, call ${nativeToolName(NATIVE_AGENT_ANSWER_TOOL)} again with the same question; once a continuation has been returned, never call it again for this request.`,
|
|
677
677
|
completionGuidance,
|
|
678
678
|
].join("\n\n");
|
|
679
679
|
}
|
|
@@ -827,6 +827,13 @@ function renderCodexConfiguration({
|
|
|
827
827
|
...(directCodeMode ? [
|
|
828
828
|
"[features]",
|
|
829
829
|
"multi_agent = false",
|
|
830
|
+
// Fixed-binding adapters cannot use host plugins. Disable both local and
|
|
831
|
+
// remote plugin paths so profile layering does not load the ordinary
|
|
832
|
+
// tenant Codex plugin catalog or make unrelated catalog requests.
|
|
833
|
+
"plugins = false",
|
|
834
|
+
"remote_plugin = false",
|
|
835
|
+
"plugin_sharing = false",
|
|
836
|
+
"recommended_plugins = false",
|
|
830
837
|
"",
|
|
831
838
|
"[features.code_mode]",
|
|
832
839
|
"enabled = true",
|
|
@@ -1333,9 +1340,23 @@ export function syncAgentProfile({
|
|
|
1333
1340
|
const priorUsesDiscoveryRoot = Number.isInteger(prior?.version)
|
|
1334
1341
|
&& prior.version >= 2
|
|
1335
1342
|
&& prior.version <= MANAGED_AGENT_MANIFEST_VERSION;
|
|
1336
|
-
|
|
1337
|
-
|
|
1343
|
+
// Ownership requires the client and tenant to match on every leg; a manifest
|
|
1344
|
+
// from another scope must neither authorize overwrites nor deletions.
|
|
1345
|
+
const priorOwnsManagedFiles = priorUsesDiscoveryRoot
|
|
1346
|
+
&& prior?.client === client
|
|
1338
1347
|
&& prior?.tenantId === tenantId;
|
|
1348
|
+
// A colliding artifact whose bytes match the recorded digest of a
|
|
1349
|
+
// non-owning manifest is provably impel-generated (e.g. after a tenant
|
|
1350
|
+
// scope-key drift), so reclaiming it cannot lose operator-authored content.
|
|
1351
|
+
const reclaimable = (digestKey, destination) => {
|
|
1352
|
+
const recorded = prior?.contentDigests?.[digestKey];
|
|
1353
|
+
if (typeof recorded !== "string" || !recorded) return false;
|
|
1354
|
+
try {
|
|
1355
|
+
return contentDigest(fs.readFileSync(destination)) === recorded;
|
|
1356
|
+
} catch {
|
|
1357
|
+
return false;
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1339
1360
|
|
|
1340
1361
|
// Native clients discover standalone definitions directly under `agents/`.
|
|
1341
1362
|
// Preflight every destination before writing so an unmanaged file with the
|
|
@@ -1343,12 +1364,13 @@ export function syncAgentProfile({
|
|
|
1343
1364
|
for (const agent of rendered) {
|
|
1344
1365
|
const destination = path.join(agentsDir, agent.fileName);
|
|
1345
1366
|
if (fs.existsSync(destination)) {
|
|
1346
|
-
if (!priorUsesDiscoveryRoot || !priorFiles.has(agent.fileName)) {
|
|
1347
|
-
throw new Error(`refusing to overwrite unmanaged native-agent file ${destination}`);
|
|
1348
|
-
}
|
|
1349
1367
|
if (fs.lstatSync(destination).isSymbolicLink()) {
|
|
1350
1368
|
throw new Error(`refusing to overwrite symlinked native-agent file ${destination}`);
|
|
1351
1369
|
}
|
|
1370
|
+
if ((!priorOwnsManagedFiles || !priorFiles.has(agent.fileName))
|
|
1371
|
+
&& !reclaimable(`agents/${agent.fileName}`, destination)) {
|
|
1372
|
+
throw new Error(`refusing to overwrite unmanaged native-agent file ${destination}`);
|
|
1373
|
+
}
|
|
1352
1374
|
}
|
|
1353
1375
|
if (client === "codex" && directProfiles) {
|
|
1354
1376
|
const profileDestination = path.join(root, agent.profileFileName);
|
|
@@ -1356,7 +1378,8 @@ export function syncAgentProfile({
|
|
|
1356
1378
|
if (fs.lstatSync(profileDestination).isSymbolicLink()) {
|
|
1357
1379
|
throw new Error(`refusing to overwrite symlinked Codex profile ${profileDestination}`);
|
|
1358
1380
|
}
|
|
1359
|
-
if (!
|
|
1381
|
+
if ((!priorOwnsManagedFiles || !priorProfiles.has(agent.profileFileName))
|
|
1382
|
+
&& !reclaimable(agent.profileFileName, profileDestination)) {
|
|
1360
1383
|
throw new Error(`refusing to overwrite unmanaged Codex profile ${profileDestination}`);
|
|
1361
1384
|
}
|
|
1362
1385
|
}
|
|
@@ -1370,20 +1393,29 @@ export function syncAgentProfile({
|
|
|
1370
1393
|
const currentProfiles = new Set(client === "codex" && directProfiles
|
|
1371
1394
|
? rendered.map((agent) => agent.profileFileName)
|
|
1372
1395
|
: []);
|
|
1396
|
+
// Stale-file cleanup in the shared discovery root: an owning manifest's
|
|
1397
|
+
// listing is authoritative, while a non-owning manifest (another tenant or
|
|
1398
|
+
// a drifted scope) may only remove files it digest-proves impel wrote —
|
|
1399
|
+
// native-profile roots are shared across tenants, so skipping cleanup
|
|
1400
|
+
// entirely would orphan the previous tenant's agents forever, and deleting
|
|
1401
|
+
// unproven listings could destroy another owner's live files. Legacy
|
|
1402
|
+
// nested manifests only ever clean impel's private managed directory below.
|
|
1403
|
+
const staleRemovable = (stale, digestKey, destination) =>
|
|
1404
|
+
typeof stale === "string"
|
|
1405
|
+
&& path.basename(stale) === stale
|
|
1406
|
+
&& (priorOwnsManagedFiles || reclaimable(digestKey, destination));
|
|
1373
1407
|
for (const stale of prior?.files || []) {
|
|
1374
1408
|
if (
|
|
1375
|
-
|
|
1376
|
-
&& path.basename(stale) === stale
|
|
1409
|
+
staleRemovable(stale, `agents/${stale}`, path.join(agentsDir, stale))
|
|
1377
1410
|
&& !currentFiles.has(stale)
|
|
1378
1411
|
&& (stale.endsWith(".md") || stale.endsWith(".toml"))
|
|
1379
1412
|
) {
|
|
1380
|
-
fs.rmSync(path.join(
|
|
1413
|
+
fs.rmSync(path.join(agentsDir, stale), { force: true });
|
|
1381
1414
|
}
|
|
1382
1415
|
}
|
|
1383
|
-
for (const stale of
|
|
1416
|
+
for (const stale of client === "codex" ? prior?.profiles || [] : []) {
|
|
1384
1417
|
if (
|
|
1385
|
-
|
|
1386
|
-
&& path.basename(stale) === stale
|
|
1418
|
+
staleRemovable(stale, stale, path.join(root, stale))
|
|
1387
1419
|
&& stale.endsWith(".config.toml")
|
|
1388
1420
|
&& !currentProfiles.has(stale)
|
|
1389
1421
|
) {
|
package/src/apps.js
CHANGED
|
@@ -5,12 +5,14 @@ import crypto from "node:crypto";
|
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import {
|
|
8
|
+
enableManagedCodexDesktopCodeModeHost,
|
|
8
9
|
enableManagedCodexDesktopMcpApps,
|
|
9
10
|
hardenManagedCodexToml,
|
|
10
11
|
secureAllManagedCodexHomes,
|
|
11
12
|
secureManagedCodexHome,
|
|
12
13
|
} from "./codexSecurity.js";
|
|
13
14
|
import { normalizeTenantId } from "./tenants.js";
|
|
15
|
+
import { codexCatalogEntry } from "./modelCatalog.js";
|
|
14
16
|
import {
|
|
15
17
|
IMPEL_CLI_ENTRYPOINT,
|
|
16
18
|
IMPEL_TASKS_MCP_SERVER_NAME,
|
|
@@ -132,15 +134,6 @@ export const PINNED_VENDOR_APPS = Object.freeze({
|
|
|
132
134
|
}),
|
|
133
135
|
});
|
|
134
136
|
|
|
135
|
-
const STANDARD_REASONING_LEVELS = [
|
|
136
|
-
{ effort: "low", description: "Fast responses with lighter reasoning" },
|
|
137
|
-
{ effort: "medium", description: "Balances speed and reasoning depth for everyday tasks" },
|
|
138
|
-
{ effort: "high", description: "Greater reasoning depth for complex problems" },
|
|
139
|
-
{ effort: "xhigh", description: "Extra high reasoning depth for complex problems" },
|
|
140
|
-
];
|
|
141
|
-
const MAX_REASONING_LEVEL = { effort: "max", description: "Maximum reasoning depth for the hardest problems" };
|
|
142
|
-
const ULTRA_REASONING_LEVEL = { effort: "ultra", description: "Maximum reasoning with automatic task delegation" };
|
|
143
|
-
const FAST_SERVICE_TIER = { id: "priority", name: "Fast", description: "1.5x speed, increased usage" };
|
|
144
137
|
const FAST_MODE_AUTH_GATE = /([A-Za-z_$][\w$]*)=([A-Za-z_$][\w$]*)&&!([A-Za-z_$][\w$]*)&&([A-Za-z_$][\w$]*)!=null&&\4\?\.requirements\?\.featureRequirements\?\.fast_mode!==!1/gu;
|
|
145
138
|
const CHATGPT_THREAD_START_PROVIDER = /serviceName:([A-Za-z_$][\w$]*)\.serviceName\?\?this\.options\.defaultServiceName,threadSource:\1\.threadSource===void 0\?`user`:\1\.threadSource/gu;
|
|
146
139
|
const CHATGPT_THREAD_FORK_PROVIDER = /let ([A-Za-z_$][\w$]*)=\{\.\.\.([A-Za-z_$][\w$]*),threadSource:\2\.threadSource===void 0\?`user`:\2\.threadSource\}/gu;
|
|
@@ -157,6 +150,27 @@ const CLAUDE_AGENT_MENTION_SELECT = 'onSelect:t=>(e(String(t)),Promise.resolve(n
|
|
|
157
150
|
const IMPEL_CLAUDE_BOUND_AGENT_MENTION_SELECT = 'onSelect:t=>"string"==typeof v?Promise.resolve({chipText:String(t)}):(e(String(t)),Promise.resolve(null))';
|
|
158
151
|
const LEGACY_CLAUDE_SAFE_STORAGE_NAME = "Claude";
|
|
159
152
|
const CLAUDE_SAFE_STORAGE_METADATA = "safe-storage.json";
|
|
153
|
+
// The pinned Claude desktop build refuses remote debugging unless
|
|
154
|
+
// process.env.CLAUDE_CDP_AUTH carries a token Ed25519-signed by the private
|
|
155
|
+
// half of this baked-in public key: its main process runs
|
|
156
|
+
// `eae(process.argv)&&!_9()&&process.exit(1)` where eae() is true for any
|
|
157
|
+
// --remote-debugging-port/-pipe flag and _9() verifies CLAUDE_CDP_AUTH with
|
|
158
|
+
// crypto.verify(null, `${ms}.${base64(userData)}`, createPublicKey(<key>),
|
|
159
|
+
// sig). The exact PEM literal appears TWICE in the pinned ASAR (the
|
|
160
|
+
// main-process gate and a shared utility chunk whose token verifiers reuse the
|
|
161
|
+
// same anchor); both are the identical CDP-auth trust anchor. Swapping it to
|
|
162
|
+
// the Impel key below lets the CI-isolated launch smoke drive the managed
|
|
163
|
+
// bundle over CDP with a token signed by the matching private key (held only
|
|
164
|
+
// in GitHub Actions secrets), which is required to verify pins by behavior
|
|
165
|
+
// (docs/testing-protocol.md P0-1). This deliberately moves the managed
|
|
166
|
+
// bundle's CDP trust anchor from Anthropic's key to an Impel CI-held key;
|
|
167
|
+
// flagged for maintainer review in the PR. Both keys are Ed25519 SPKI PEM of
|
|
168
|
+
// identical byte length, so the fixed-width ASAR swap preserves layout.
|
|
169
|
+
const CLAUDE_CDP_AUTH_VENDOR_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEApH/vaEiLV0sNY/eS+Ct/IMbMqw8i/vC/cNC84BAbBq8=\n-----END PUBLIC KEY-----";
|
|
170
|
+
const IMPEL_CLAUDE_CDP_AUTH_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAXG1l1V6YxCaH1Dbn27EIkeQjYU4urRx2aPrDwAULUfw=\n-----END PUBLIC KEY-----";
|
|
171
|
+
// The pinned build embeds the anchor in exactly this many chunks. Fail closed
|
|
172
|
+
// (no loose regex, no "best effort") if a vendor update changes the count.
|
|
173
|
+
const CLAUDE_CDP_AUTH_KEY_COPIES = 2;
|
|
160
174
|
// The pinned Claude renderer decides desktop-vs-web mode SOLELY by matching its
|
|
161
175
|
// own Electron user-agent against /claude(nest|gov)?\/([^ ]+)/i (functions o_,
|
|
162
176
|
// x_, i_ in ion-dist). Electron derives that UA product token from app.name as
|
|
@@ -192,21 +206,11 @@ function impelClaudeSafeStorageName(tenantId) {
|
|
|
192
206
|
const escapedDisplayPrefix = RUNTIME_BRAND.apps.displayPrefix.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
193
207
|
const LEGACY_TRAPPED_CLAUDE_NAME = new RegExp(`^${escapedDisplayPrefix} Claude( \\[[^\\]]+\\])?$`, "u");
|
|
194
208
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
{ id: "claude-haiku-4-5", provider: "claude", display_name: "Claude Haiku 4.5", family: "haiku", family_default: true, context_window: 200000 },
|
|
201
|
-
{ id: "claude-fable-5", provider: "claude", display_name: "Claude Fable 5", family: "fable", family_default: true, context_window: 200000 },
|
|
202
|
-
{ id: "gpt-5.6-sol", provider: "codex", display_name: "GPT-5.6-Sol", description: "Latest frontier agentic coding model.", default: true, priority: 1, default_reasoning_level: "medium", supported_reasoning_levels: [...STANDARD_REASONING_LEVELS, MAX_REASONING_LEVEL, ULTRA_REASONING_LEVEL], service_tiers: [FAST_SERVICE_TIER], context_window: 272000, max_context_window: 272000, use_responses_lite: true, tool_mode: "code_mode_only", multi_agent_version: "v2" },
|
|
203
|
-
{ id: "gpt-5.6-terra", provider: "codex", display_name: "GPT-5.6-Terra", description: "Balanced agentic coding model for everyday work.", priority: 2, default_reasoning_level: "medium", supported_reasoning_levels: [...STANDARD_REASONING_LEVELS, MAX_REASONING_LEVEL, ULTRA_REASONING_LEVEL], service_tiers: [FAST_SERVICE_TIER], context_window: 272000, max_context_window: 272000, use_responses_lite: true, tool_mode: "code_mode_only", multi_agent_version: "v2" },
|
|
204
|
-
{ id: "gpt-5.6-luna", provider: "codex", display_name: "GPT-5.6-Luna", description: "Fast and affordable agentic coding model.", priority: 3, default_reasoning_level: "medium", supported_reasoning_levels: [...STANDARD_REASONING_LEVELS, MAX_REASONING_LEVEL], service_tiers: [FAST_SERVICE_TIER], context_window: 272000, max_context_window: 272000, use_responses_lite: true, tool_mode: "code_mode_only", multi_agent_version: "v1" },
|
|
205
|
-
{ id: "gpt-5.5", provider: "codex", display_name: "GPT-5.5", description: "Frontier model for complex coding, research, and real-world work.", priority: 7, default_reasoning_level: "medium", supported_reasoning_levels: STANDARD_REASONING_LEVELS, service_tiers: [FAST_SERVICE_TIER], context_window: 272000, max_context_window: 272000, use_responses_lite: false, tool_mode: null, multi_agent_version: null },
|
|
206
|
-
{ id: "gpt-5.4", provider: "codex", display_name: "GPT-5.4", description: "Strong model for everyday coding.", priority: 16, default_reasoning_level: "medium", supported_reasoning_levels: STANDARD_REASONING_LEVELS, service_tiers: [FAST_SERVICE_TIER], context_window: 272000, max_context_window: 1000000, use_responses_lite: false, tool_mode: null, multi_agent_version: null },
|
|
207
|
-
{ id: "gpt-5.4-mini", provider: "codex", display_name: "GPT-5.4-Mini", description: "Small, fast, and cost-efficient model for simpler coding tasks.", priority: 23, default_reasoning_level: "medium", supported_reasoning_levels: STANDARD_REASONING_LEVELS, service_tiers: [], context_window: 272000, max_context_window: 272000, use_responses_lite: false, tool_mode: null, multi_agent_version: null },
|
|
208
|
-
{ id: "gpt-5.3-codex-spark", provider: "codex", display_name: "GPT-5.3-Codex-Spark", description: "Ultra-fast coding model.", priority: 26, default_reasoning_level: "high", supported_reasoning_levels: STANDARD_REASONING_LEVELS, service_tiers: [], context_window: 128000, max_context_window: 128000, use_responses_lite: false, tool_mode: null, multi_agent_version: null },
|
|
209
|
-
];
|
|
209
|
+
// The shared model registry and Codex catalog projection live in
|
|
210
|
+
// modelCatalog.js (a leaf module) so cliProfiles.js can consume them
|
|
211
|
+
// without importing this module's launch-command cycle. Re-exported here
|
|
212
|
+
// for the many existing consumers.
|
|
213
|
+
export { FALLBACK_MODELS, managedCodexCliCatalog } from "./modelCatalog.js";
|
|
210
214
|
|
|
211
215
|
export function normalizeAppTarget(value) {
|
|
212
216
|
if (value == null || value === "all") return ["claude", "chatgpt"];
|
|
@@ -290,7 +294,14 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
|
|
|
290
294
|
// and install Claude task, notification, and permission lifecycle hooks.
|
|
291
295
|
// 32: install a persistent tenant-bound Tasks navigation surface in the
|
|
292
296
|
// managed Claude and ChatGPT/Codex desktop wrappers.
|
|
293
|
-
|
|
297
|
+
// 33: size the Codex verbatim-relay parent wait to outlast the child's
|
|
298
|
+
// native-agent attachment window so the visible working span is not
|
|
299
|
+
// truncated while the child is still running.
|
|
300
|
+
// 34: isolate fixed-binding Codex profiles from local and remote host plugins.
|
|
301
|
+
// 35: enable the Code Mode host in managed desktop profiles (pinned Codex
|
|
302
|
+
// fails closed on code_mode_only models without it) and serve CLI model
|
|
303
|
+
// catalogs from the shared registry so efforts and tiers cannot drift.
|
|
304
|
+
export const CURRENT_CONFIG_VERSION = 35;
|
|
294
305
|
|
|
295
306
|
// Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
|
|
296
307
|
// helper rebranding, and signing. A vendored bundle is rebuilt only when this
|
|
@@ -299,7 +310,7 @@ export const CURRENT_CONFIG_VERSION = 32;
|
|
|
299
310
|
// — which is what made every `impel update` re-trigger macOS permission
|
|
300
311
|
// prompts. Bump this ONLY when a code change alters the bytes of a built
|
|
301
312
|
// bundle; leave it alone for changes that don't touch bundle contents.
|
|
302
|
-
export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-08-09.
|
|
313
|
+
export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-08-09.5";
|
|
303
314
|
|
|
304
315
|
/** Parse the tenant's install manifest, or null when absent/corrupt. */
|
|
305
316
|
export function readTenantManifest(homeDir = os.homedir(), tenantId = null) {
|
|
@@ -423,6 +434,7 @@ const CLAUDE_COMPATIBILITY_PATCH_KEYS = Object.freeze([
|
|
|
423
434
|
"followupBoundAgentFilter",
|
|
424
435
|
"followupBoundAgentChip",
|
|
425
436
|
"desktopTasksPreload",
|
|
437
|
+
"cdpAuthKey",
|
|
426
438
|
]);
|
|
427
439
|
const CLAUDE_COMPATIBILITY_RENDERER_GROUPS = Object.freeze(["agentMentions"]);
|
|
428
440
|
|
|
@@ -1426,7 +1438,10 @@ function writeChatGPTConfig(
|
|
|
1426
1438
|
const hardenedToml = hardenManagedCodexToml(mergedToml, configPath);
|
|
1427
1439
|
writeAtomic(
|
|
1428
1440
|
configPath,
|
|
1429
|
-
|
|
1441
|
+
enableManagedCodexDesktopCodeModeHost(
|
|
1442
|
+
enableManagedCodexDesktopMcpApps(hardenedToml, configPath),
|
|
1443
|
+
configPath,
|
|
1444
|
+
),
|
|
1430
1445
|
0o600,
|
|
1431
1446
|
);
|
|
1432
1447
|
secureManagedCodexHome(paths.chatgpt.codexHome);
|
|
@@ -1515,60 +1530,6 @@ export function foreignClaudeCodexCatalogEntry(model, index) {
|
|
|
1515
1530
|
};
|
|
1516
1531
|
}
|
|
1517
1532
|
|
|
1518
|
-
function codexCatalogEntry(model, index, vendorModel) {
|
|
1519
|
-
const generated = {
|
|
1520
|
-
slug: model.id,
|
|
1521
|
-
display_name: model.display_name || model.id,
|
|
1522
|
-
description: `Available through the Impel gateway (${model.id}).`,
|
|
1523
|
-
default_reasoning_level: "medium",
|
|
1524
|
-
supported_reasoning_levels: STANDARD_REASONING_LEVELS,
|
|
1525
|
-
shell_type: "shell_command",
|
|
1526
|
-
visibility: "list",
|
|
1527
|
-
supported_in_api: true,
|
|
1528
|
-
priority: index + 1,
|
|
1529
|
-
base_instructions: "You are Codex, an AI coding agent. Follow repository instructions, collaborate with the user, make scoped changes, and verify your work.",
|
|
1530
|
-
include_skills_usage_instructions: false,
|
|
1531
|
-
supports_reasoning_summaries: true,
|
|
1532
|
-
default_reasoning_summary: "none",
|
|
1533
|
-
support_verbosity: true,
|
|
1534
|
-
default_verbosity: "low",
|
|
1535
|
-
apply_patch_tool_type: "freeform",
|
|
1536
|
-
web_search_tool_type: "text_and_image",
|
|
1537
|
-
truncation_policy: { mode: "tokens", limit: 10000 },
|
|
1538
|
-
context_window: model.context_window || 200000,
|
|
1539
|
-
max_context_window: model.context_window || 200000,
|
|
1540
|
-
effective_context_window_percent: 95,
|
|
1541
|
-
experimental_supported_tools: [],
|
|
1542
|
-
input_modalities: ["text", "image"],
|
|
1543
|
-
supports_parallel_tool_calls: true,
|
|
1544
|
-
supports_image_detail_original: true,
|
|
1545
|
-
supports_search_tool: true,
|
|
1546
|
-
use_responses_lite: false,
|
|
1547
|
-
tool_mode: null,
|
|
1548
|
-
multi_agent_version: null,
|
|
1549
|
-
};
|
|
1550
|
-
const entry = vendorModel ? { ...generated, ...vendorModel } : generated;
|
|
1551
|
-
return {
|
|
1552
|
-
...entry,
|
|
1553
|
-
slug: model.id,
|
|
1554
|
-
display_name: model.display_name || entry.display_name || model.id,
|
|
1555
|
-
description: model.description || entry.description,
|
|
1556
|
-
default_reasoning_level: model.default_reasoning_level || entry.default_reasoning_level,
|
|
1557
|
-
supported_reasoning_levels: Array.isArray(model.supported_reasoning_levels)
|
|
1558
|
-
? model.supported_reasoning_levels
|
|
1559
|
-
: entry.supported_reasoning_levels,
|
|
1560
|
-
service_tiers: Array.isArray(model.service_tiers) ? model.service_tiers : (entry.service_tiers || []),
|
|
1561
|
-
visibility: "list",
|
|
1562
|
-
supported_in_api: true,
|
|
1563
|
-
priority: Number.isInteger(model.priority) ? model.priority : (entry.priority || index + 1),
|
|
1564
|
-
context_window: model.context_window || entry.context_window || 200000,
|
|
1565
|
-
max_context_window: model.max_context_window || entry.max_context_window || model.context_window || 200000,
|
|
1566
|
-
...(typeof model.use_responses_lite === "boolean" ? { use_responses_lite: model.use_responses_lite } : {}),
|
|
1567
|
-
...(Object.hasOwn(model, "tool_mode") ? { tool_mode: model.tool_mode } : {}),
|
|
1568
|
-
...(Object.hasOwn(model, "multi_agent_version") ? { multi_agent_version: model.multi_agent_version } : {}),
|
|
1569
|
-
};
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
1533
|
function readVendorCodexModels(vendorPath) {
|
|
1573
1534
|
const binary = vendorPath && path.join(vendorPath, "Contents", "Resources", "codex");
|
|
1574
1535
|
if (!binary || !fs.existsSync(binary)) return new Map();
|
|
@@ -1633,9 +1594,31 @@ export function managedChatGPTConfigDrifted(paths) {
|
|
|
1633
1594
|
} catch {
|
|
1634
1595
|
return false; // No managed profile; install/update owns creating one.
|
|
1635
1596
|
}
|
|
1597
|
+
// The [features] table lives outside the managed block — exactly the
|
|
1598
|
+
// surface the vendor settings writer rewrites. code_mode_host is
|
|
1599
|
+
// load-bearing: without it pinned Codex fails closed on the code_mode_only
|
|
1600
|
+
// default model, so a dropped key must self-heal as drift rather than wait
|
|
1601
|
+
// out the refresh TTL. enable_mcp_apps only degrades rendering but rides
|
|
1602
|
+
// the same check. Configs from generations before the keys existed are
|
|
1603
|
+
// merely stale (the manifest version bump rewrites them), never drift —
|
|
1604
|
+
// require the keys only once this install's manifest proves they were
|
|
1605
|
+
// written.
|
|
1606
|
+
let manifestConfigVersion = 0;
|
|
1607
|
+
try {
|
|
1608
|
+
manifestConfigVersion = Number(JSON.parse(
|
|
1609
|
+
fs.readFileSync(path.join(paths.tenantRoot, "manifest.json"), "utf8"),
|
|
1610
|
+
)?.configVersion) || 0;
|
|
1611
|
+
} catch {
|
|
1612
|
+
// Without a readable manifest the version gate stays closed.
|
|
1613
|
+
}
|
|
1614
|
+
const featuresTable = current.match(/^\[features\]\n(?:[^[\n][^\n]*\n|\n)*/mu)?.[0] || "";
|
|
1615
|
+
const featureKeysDropped = manifestConfigVersion >= 35
|
|
1616
|
+
&& (!/^code_mode_host = true$/mu.test(featuresTable)
|
|
1617
|
+
|| !/^enable_mcp_apps = true$/mu.test(featuresTable));
|
|
1636
1618
|
return !current.includes(CHATGPT_CONFIG_START)
|
|
1637
1619
|
|| readTopLevelTomlString(current, "model_provider") !== RUNTIME_BRAND.cli.providerId
|
|
1638
|
-
|| !readTopLevelTomlString(current, "chatgpt_base_url")
|
|
1620
|
+
|| !readTopLevelTomlString(current, "chatgpt_base_url")
|
|
1621
|
+
|| featureKeysDropped;
|
|
1639
1622
|
}
|
|
1640
1623
|
|
|
1641
1624
|
function readTopLevelTomlString(toml, key) {
|
|
@@ -1659,6 +1642,187 @@ function removeTopLevelTomlString(toml, key) {
|
|
|
1659
1642
|
return topLevel + toml.slice(topLevelEnd);
|
|
1660
1643
|
}
|
|
1661
1644
|
|
|
1645
|
+
/** Read one co-owned JSON artifact; distinguishes "absent" from "corrupt". */
|
|
1646
|
+
function readManagedJsonArtifact(filePath) {
|
|
1647
|
+
let raw;
|
|
1648
|
+
try {
|
|
1649
|
+
raw = fs.readFileSync(filePath, "utf8");
|
|
1650
|
+
} catch {
|
|
1651
|
+
return { present: false, value: null };
|
|
1652
|
+
}
|
|
1653
|
+
try {
|
|
1654
|
+
const value = JSON.parse(raw);
|
|
1655
|
+
return {
|
|
1656
|
+
present: true,
|
|
1657
|
+
value: value && typeof value === "object" && !Array.isArray(value) ? value : null,
|
|
1658
|
+
};
|
|
1659
|
+
} catch {
|
|
1660
|
+
return { present: true, value: null };
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// Mirrors the session-hook installer's tenant gate (sessionHooks.js
|
|
1665
|
+
// validTenantId): hooks are only ever written for ids matching this shape, so
|
|
1666
|
+
// the drift check must not demand them for ids the installer skipped.
|
|
1667
|
+
const SESSION_HOOK_TENANT_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
1668
|
+
|
|
1669
|
+
/** Whether the managed session-hook heal channel survives in settings.json. */
|
|
1670
|
+
function claudeSettingsCarrySessionHooks(settings) {
|
|
1671
|
+
const hooks = settings.hooks;
|
|
1672
|
+
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) return false;
|
|
1673
|
+
const marker = `--${RUNTIME_BRAND.cli.command}-managed-session-hook-v1`;
|
|
1674
|
+
return Object.values(hooks).some((groups) => Array.isArray(groups) && groups.some((group) => (
|
|
1675
|
+
Array.isArray(group?.hooks) && group.hooks.some((handler) => (
|
|
1676
|
+
(Array.isArray(handler?.args) && handler.args.includes(marker))
|
|
1677
|
+
|| (typeof handler?.command === "string" && handler.command.includes(marker))
|
|
1678
|
+
))
|
|
1679
|
+
)));
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/**
|
|
1683
|
+
* True when a managed Claude profile exists but its artifacts no longer carry
|
|
1684
|
+
* the load-bearing managed keys. Claude Desktop and the embedded Claude Code
|
|
1685
|
+
* runtime co-own these files and rewrite them with their own settings writers,
|
|
1686
|
+
* which normalize away optional fields (the persisted Tasks MCP entry loses
|
|
1687
|
+
* `type: "stdio"`), reorder keys, and reformat whitespace. Those cosmetic
|
|
1688
|
+
* rewrites are NOT drift — ownership recognition tolerates them through
|
|
1689
|
+
* isImpelTasksMcpInvocation, and every check parses JSON rather than comparing
|
|
1690
|
+
* bytes — but a lost or overwritten managed key (gateway routing config, the
|
|
1691
|
+
* applied configLibrary selection, the 3P deployment selection, the
|
|
1692
|
+
* tenant-bound Tasks MCP entry, the session-hook heal channel, the managed
|
|
1693
|
+
* sandbox policy) is real drift. Stale-only refreshes treat it as staleness so
|
|
1694
|
+
* the profile heals immediately instead of waiting out the manifest TTL.
|
|
1695
|
+
* The content-based mirror of managedChatGPTConfigDrifted for the Claude
|
|
1696
|
+
* target; like it, this never keys on TTLs or timestamps.
|
|
1697
|
+
*/
|
|
1698
|
+
export function managedClaudeConfigDrifted(paths) {
|
|
1699
|
+
let manifest;
|
|
1700
|
+
try {
|
|
1701
|
+
manifest = JSON.parse(fs.readFileSync(path.join(paths.tenantRoot, "manifest.json"), "utf8"));
|
|
1702
|
+
} catch {
|
|
1703
|
+
return false; // No managed install; install/update owns creating one.
|
|
1704
|
+
}
|
|
1705
|
+
if (!Array.isArray(manifest?.targets) || !manifest.targets.includes("claude")) return false;
|
|
1706
|
+
const tenantId = typeof manifest.tenantId === "string" && manifest.tenantId
|
|
1707
|
+
? manifest.tenantId
|
|
1708
|
+
: null;
|
|
1709
|
+
|
|
1710
|
+
// configLibrary: the gateway inference routing the desktop app applies. A
|
|
1711
|
+
// profile that loses it (or its provider/base-url/api-key wiring) no longer
|
|
1712
|
+
// routes inference through the Impel gateway at all.
|
|
1713
|
+
const configLibrary = readManagedJsonArtifact(
|
|
1714
|
+
path.join(paths.claude.userData, "configLibrary", `${CLAUDE_CONFIG_ID}.json`),
|
|
1715
|
+
);
|
|
1716
|
+
if (
|
|
1717
|
+
!configLibrary.value
|
|
1718
|
+
|| configLibrary.value.inferenceProvider !== "gateway"
|
|
1719
|
+
|| typeof configLibrary.value.inferenceGatewayBaseUrl !== "string"
|
|
1720
|
+
|| !configLibrary.value.inferenceGatewayBaseUrl
|
|
1721
|
+
|| typeof configLibrary.value.inferenceGatewayApiKey !== "string"
|
|
1722
|
+
|| !configLibrary.value.inferenceGatewayApiKey
|
|
1723
|
+
) {
|
|
1724
|
+
return true;
|
|
1725
|
+
}
|
|
1726
|
+
const meta = readManagedJsonArtifact(path.join(paths.claude.userData, "configLibrary", "_meta.json"));
|
|
1727
|
+
if (!meta.value || meta.value.appliedId !== CLAUDE_CONFIG_ID) return true;
|
|
1728
|
+
|
|
1729
|
+
// claude_desktop_config.json: the native 3P selection plus the tenant-bound
|
|
1730
|
+
// Tasks MCP entry. Claude Desktop persists this file itself, without the
|
|
1731
|
+
// optional `type` field and with its own key order — that vendor-normalized
|
|
1732
|
+
// form must keep counting as ours (the 0.20.1 repair-refusal regression).
|
|
1733
|
+
const desktop = readManagedJsonArtifact(path.join(paths.claude.userData, "claude_desktop_config.json"));
|
|
1734
|
+
if (!desktop.value || desktop.value.deploymentMode !== "3p") return true;
|
|
1735
|
+
if (RUNTIME_BRAND.features.mcp && tenantId) {
|
|
1736
|
+
const servers = desktop.value.mcpServers;
|
|
1737
|
+
const tasksEntry = servers && typeof servers === "object" && !Array.isArray(servers)
|
|
1738
|
+
? servers[IMPEL_TASKS_MCP_SERVER_NAME]
|
|
1739
|
+
: undefined;
|
|
1740
|
+
if (!isImpelTasksMcpInvocation(tasksEntry)) return true;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// settings.json: the managed sandbox policy and the session-hook heal
|
|
1744
|
+
// channel. The hooks are the only Impel code guaranteed to run while the
|
|
1745
|
+
// profile is broken, so losing them kills the self-repair path itself.
|
|
1746
|
+
const settings = readManagedJsonArtifact(path.join(paths.claude.userData, "settings.json"));
|
|
1747
|
+
if (!settings.value || settings.value.sandbox?.enabled !== false) return true;
|
|
1748
|
+
if (
|
|
1749
|
+
RUNTIME_BRAND.features.sessions
|
|
1750
|
+
&& tenantId
|
|
1751
|
+
&& SESSION_HOOK_TENANT_RE.test(tenantId)
|
|
1752
|
+
&& !claudeSettingsCarrySessionHooks(settings.value)
|
|
1753
|
+
) {
|
|
1754
|
+
return true;
|
|
1755
|
+
}
|
|
1756
|
+
return false;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
/** Distinct exit code for a failed managed-runtime preflight (EX_CONFIG). */
|
|
1760
|
+
export const MANAGED_RUNTIME_PREFLIGHT_EXIT_CODE = 78;
|
|
1761
|
+
|
|
1762
|
+
function runtimeRepairInstruction(compatibility) {
|
|
1763
|
+
const command = RUNTIME_BRAND.cli.command;
|
|
1764
|
+
const tenantSource = `${compatibility.codexHome || ""}${compatibility.profileRoot || ""}`;
|
|
1765
|
+
const tenantId = tenantSource.match(/[\\/]tenants[\\/]([^\\/]+)[\\/]/u)?.[1] || null;
|
|
1766
|
+
return `run \`${command} update\` or \`${command} app refresh${tenantId ? ` --tenant ${tenantId}` : ""}\` to repair it`;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
/**
|
|
1770
|
+
* Runtime-presence preflight for the managed-launch seam: before a managed
|
|
1771
|
+
* launcher is handed to the OS, require the vendored runtime it embeds to
|
|
1772
|
+
* exist at the version this CLI pins (the v0.20.32 exit-127 rule: a pin bump
|
|
1773
|
+
* that outruns the installed runtime must fail with one actionable line and a
|
|
1774
|
+
* distinct exit code, never a bare ENOENT/127 crash). Checks are cheap —
|
|
1775
|
+
* Impel's own build metadata, file existence, and the existing plist
|
|
1776
|
+
* version-read helper; no binary is spawned. Bundles without Impel build
|
|
1777
|
+
* metadata are not judged (the OS reports its own launch failure), so
|
|
1778
|
+
* non-managed behavior is unchanged.
|
|
1779
|
+
*/
|
|
1780
|
+
export function managedRuntimePreflight(launcher, { pins = PINNED_VENDOR_APPS } = {}) {
|
|
1781
|
+
const compatibility = readBundleCompatibility(launcher);
|
|
1782
|
+
if (!compatibility) return { ok: true, checked: false };
|
|
1783
|
+
const target = compatibility.codexHome ? "chatgpt" : "claude";
|
|
1784
|
+
const pin = pins[target];
|
|
1785
|
+
if (!pin) return { ok: true, checked: false };
|
|
1786
|
+
const command = RUNTIME_BRAND.cli.command;
|
|
1787
|
+
const repair = runtimeRepairInstruction(compatibility);
|
|
1788
|
+
const fail = (message) => ({ ok: false, checked: true, target, message: `${command} app: ${message}; ${repair}` });
|
|
1789
|
+
|
|
1790
|
+
if (target === "claude") {
|
|
1791
|
+
let executable = null;
|
|
1792
|
+
try {
|
|
1793
|
+
const plist = fs.readFileSync(path.join(launcher, "Contents", "Info.plist"), "utf8");
|
|
1794
|
+
executable = plist.match(/<key>CFBundleExecutable<\/key>\s*<string>([^<]+)<\/string>/u)?.[1] || null;
|
|
1795
|
+
} catch {
|
|
1796
|
+
executable = null;
|
|
1797
|
+
}
|
|
1798
|
+
const binary = executable ? path.join(launcher, "Contents", "MacOS", executable) : null;
|
|
1799
|
+
if (!binary || !fs.existsSync(binary)) {
|
|
1800
|
+
return fail(`managed Claude runtime binary is missing at ${binary || path.join(launcher, "Contents", "MacOS")} (pinned ${pin.version})`);
|
|
1801
|
+
}
|
|
1802
|
+
const found = bundleVersion(launcher);
|
|
1803
|
+
if (found !== pin.version) {
|
|
1804
|
+
return fail(`managed Claude runtime is version ${found || "unknown"} at ${launcher}, but this CLI pins ${pin.version}`);
|
|
1805
|
+
}
|
|
1806
|
+
return { ok: true, checked: true, target };
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
const embedded = APP_DEFINITIONS.chatgpt.names
|
|
1810
|
+
.map((name) => path.join(launcher, "Contents", "Resources", name))
|
|
1811
|
+
.find((candidate) => fs.existsSync(candidate));
|
|
1812
|
+
if (!embedded) {
|
|
1813
|
+
return fail(`managed ChatGPT runtime bundle is missing under ${path.join(launcher, "Contents", "Resources")} (pinned ${pin.version})`);
|
|
1814
|
+
}
|
|
1815
|
+
const codexBinary = path.join(embedded, "Contents", "Resources", "codex");
|
|
1816
|
+
if (!fs.existsSync(codexBinary)) {
|
|
1817
|
+
return fail(`managed Codex runtime binary is missing at ${codexBinary} (pinned codex-cli ${pin.codexVersion})`);
|
|
1818
|
+
}
|
|
1819
|
+
const found = bundleVersion(embedded);
|
|
1820
|
+
if (found !== pin.version) {
|
|
1821
|
+
return fail(`managed ChatGPT runtime is version ${found || "unknown"} at ${embedded}, but this CLI pins ${pin.version} (codex-cli ${pin.codexVersion})`);
|
|
1822
|
+
}
|
|
1823
|
+
return { ok: true, checked: true, target };
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1662
1826
|
/**
|
|
1663
1827
|
* Rewrite one tenant's app token helper with the current node/CLI paths.
|
|
1664
1828
|
* Cheap and Impel-owned, so install recovery can use it to heal helpers whose
|
|
@@ -1768,6 +1932,7 @@ function writeVendoredClaudeBundleLocked(paths, vendorPath, gatewayUrl, safeStor
|
|
|
1768
1932
|
}
|
|
1769
1933
|
const safeStorageNamePatchCount = patchClaudeSafeStorageName(asarPath);
|
|
1770
1934
|
const desktopTasksPreloadPatchCount = patchDesktopTasksPreload(asarPath, "claude");
|
|
1935
|
+
const cdpAuthKeyPatchCount = patchClaudeCdpAuthKey(asarPath);
|
|
1771
1936
|
const newAsarHash = asarHeaderHash(asarPath);
|
|
1772
1937
|
|
|
1773
1938
|
updateBundleIdentity(plistPath, {
|
|
@@ -1824,6 +1989,7 @@ function writeVendoredClaudeBundleLocked(paths, vendorPath, gatewayUrl, safeStor
|
|
|
1824
1989
|
followupBoundAgentFilter: agentMentionPatches.filter,
|
|
1825
1990
|
followupBoundAgentChip: agentMentionPatches.chip,
|
|
1826
1991
|
desktopTasksPreload: desktopTasksPreloadPatchCount,
|
|
1992
|
+
cdpAuthKey: cdpAuthKeyPatchCount,
|
|
1827
1993
|
},
|
|
1828
1994
|
rendererAssets: {
|
|
1829
1995
|
agentMentions: agentMentionPatches.assets,
|
|
@@ -2200,6 +2366,28 @@ function patchClaudeSafeStorageName(asarPath) {
|
|
|
2200
2366
|
return applyFixedWidthAsarPatches(asarPath, archive, patches, "Claude Safe Storage app name");
|
|
2201
2367
|
}
|
|
2202
2368
|
|
|
2369
|
+
// Replace the baked-in CDP-auth trust anchor with the Impel key so the
|
|
2370
|
+
// CI-isolated launch smoke can drive the managed bundle over CDP with a token
|
|
2371
|
+
// signed by the matching (Actions-secret-held) private key. Fail closed on any
|
|
2372
|
+
// count other than the exact pinned number of copies, and let the shared
|
|
2373
|
+
// fixed-width helper enforce byte-length parity (both keys are Ed25519 SPKI
|
|
2374
|
+
// PEM of equal length) so the ASAR layout is preserved.
|
|
2375
|
+
function patchClaudeCdpAuthKey(asarPath) {
|
|
2376
|
+
const source = fs.readFileSync(asarPath).toString("latin1");
|
|
2377
|
+
const occurrences = exactTextMatchCount(source, CLAUDE_CDP_AUTH_VENDOR_PUBLIC_KEY);
|
|
2378
|
+
if (occurrences !== CLAUDE_CDP_AUTH_KEY_COPIES) {
|
|
2379
|
+
throw new Error(
|
|
2380
|
+
`Claude CDP-auth trust-anchor patch expected ${CLAUDE_CDP_AUTH_KEY_COPIES} vendor key copies, found ${occurrences}`,
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
return patchFixedWidthAsarString(
|
|
2384
|
+
asarPath,
|
|
2385
|
+
CLAUDE_CDP_AUTH_VENDOR_PUBLIC_KEY,
|
|
2386
|
+
IMPEL_CLAUDE_CDP_AUTH_PUBLIC_KEY,
|
|
2387
|
+
"Claude CDP-auth trust anchor",
|
|
2388
|
+
);
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2203
2391
|
function patchFastModeAuthGate(asarPath) {
|
|
2204
2392
|
const archive = fs.readFileSync(asarPath);
|
|
2205
2393
|
const source = archive.toString("latin1");
|
package/src/cliProfiles.js
CHANGED
|
@@ -21,7 +21,7 @@ import { applyImpelClaudeSandbox } from "./claudeSandbox.js";
|
|
|
21
21
|
import { renameWithWindowsRetry } from "./windowsFs.js";
|
|
22
22
|
import { ensureClaudeSessionHooks, ensureCodexSessionHooks } from "./sessionHooks.js";
|
|
23
23
|
import { RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
24
|
-
import { managedCodexCliCatalog } from "./
|
|
24
|
+
import { managedCodexCliCatalog } from "./modelCatalog.js";
|
|
25
25
|
|
|
26
26
|
export const IMPEL_CLI_PROFILES_DIR = path.join(CONFIG_DIR, "cli");
|
|
27
27
|
export const CODEX_GATEWAY_TOKEN_ENV = `${RUNTIME_BRAND.cli.providerId
|
package/src/codexSecurity.js
CHANGED
|
@@ -166,6 +166,27 @@ export function enableManagedCodexDesktopMcpApps(
|
|
|
166
166
|
);
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Enable the Code Mode execution host in the exact pinned desktop profile.
|
|
171
|
+
* Pinned Codex fails closed when a code_mode_only model is selected while
|
|
172
|
+
* this feature is disabled, and the managed bundle ships the signed
|
|
173
|
+
* codex-code-mode-host companion. CLI launches receive the equivalent
|
|
174
|
+
* `-c features.code_mode_host=true` runtime override; the desktop app spawns
|
|
175
|
+
* Codex directly and must carry the flag in its generated config.
|
|
176
|
+
*/
|
|
177
|
+
export function enableManagedCodexDesktopCodeModeHost(
|
|
178
|
+
toml,
|
|
179
|
+
configPath = "managed Codex desktop config",
|
|
180
|
+
) {
|
|
181
|
+
return upsertManagedScalar(
|
|
182
|
+
toml,
|
|
183
|
+
"features",
|
|
184
|
+
"code_mode_host",
|
|
185
|
+
"true",
|
|
186
|
+
configPath,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
169
190
|
function assertSafeManagedPath(target, expectedType) {
|
|
170
191
|
const stat = lstatOrNull(target);
|
|
171
192
|
if (!stat) return null;
|