impel-cli 0.20.39 → 0.20.41
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 +42 -0
- package/package.json +1 -1
- package/src/apps.js +38 -81
- package/src/cliProfiles.js +1 -1
- package/src/codexSecurity.js +21 -0
- package/src/commands/apps.js +4 -2
- package/src/commands/converge.js +17 -1
- package/src/commands/setup.js +17 -1
- package/src/commands/status.js +9 -4
- package/src/commands/update.js +144 -16
- package/src/installRecovery/engine.js +45 -0
- package/src/macSetup.js +17 -0
- package/src/modelCatalog.js +103 -0
- package/src/platformSetup.js +9 -3
- package/src/windowsApps.js +122 -10
- package/src/windowsSetup.js +20 -0
- package/src/codexCliCatalog.js +0 -56
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Release notes
|
|
2
2
|
|
|
3
|
+
## 0.20.41 — Windows onboarding: converge over an already-installed Store ChatGPT
|
|
4
|
+
|
|
5
|
+
- Accepts winget's UPDATE_NOT_APPLICABLE result during setup as well as
|
|
6
|
+
update, gated by the pin-exact package probe, ending the unconvergeable
|
|
7
|
+
first-run failure loop on machines where the ChatGPT Store app already
|
|
8
|
+
exists (and, downstream, the managed sign-in loop it caused).
|
|
9
|
+
- Registers a machine-wide staged Store package for the current user
|
|
10
|
+
(download-free, no elevation) when winget sees it but the per-user probe
|
|
11
|
+
does not, still failing closed through the pin-exact probe.
|
|
12
|
+
- Resolves the app-open path strictly through the pinned finder so an
|
|
13
|
+
off-pin Store build can never be staged as the managed app.
|
|
14
|
+
- Names installed-vs-pinned versions in vendor-install diagnostics with a
|
|
15
|
+
measured direction, adds the 0x8A15002B winget hint, and makes Windows
|
|
16
|
+
status require the staged managed app rather than config presence alone.
|
|
17
|
+
|
|
18
|
+
## 0.20.40 — Field-report fixes: launcher drift, sync ownership, Codex capability, one-run update
|
|
19
|
+
|
|
20
|
+
- Survives Claude auto-update drift: `impel claude` resolves the exact reviewed
|
|
21
|
+
release from the installer's side-by-side layout instead of exiting 127 when
|
|
22
|
+
the vendor updater moves the shared symlink; setup/update detection shares
|
|
23
|
+
the same resolution, ending the reinstall loop.
|
|
24
|
+
- Makes `impel agents sync` ownership symmetric across Claude and Codex with
|
|
25
|
+
digest-proven reclaim: manifest scope drift self-heals, operator-edited
|
|
26
|
+
files still refuse fail-closed, non-owning manifests can no longer delete
|
|
27
|
+
another tenant's files, and shared roots converge on tenant switch.
|
|
28
|
+
- Unstrands answer-mode adapters: retrying the answer call is permitted only
|
|
29
|
+
before a continuation exists, continuation ids tolerate case drift, and the
|
|
30
|
+
invalid-handle error now says what to pass instead.
|
|
31
|
+
- Restores Codex capability in the managed app and CLI: the generated desktop
|
|
32
|
+
profile enables the Code Mode host (pinned Codex fails closed on
|
|
33
|
+
code_mode_only models without it, hiding the 5.6 family with its max/ultra
|
|
34
|
+
efforts and Fast tier), the CLI model catalog is served from the same
|
|
35
|
+
registry as the desktop (max/ultra/Fast restored), and a dropped
|
|
36
|
+
code_mode_host key now self-heals as drift on current installs.
|
|
37
|
+
- Sizes the Codex verbatim-relay parent wait from the child's attachment
|
|
38
|
+
window so visible working time is no longer truncated, and direct answers
|
|
39
|
+
emit progress notifications.
|
|
40
|
+
- Converges `impel update` in one run: npm-prefix skew hands convergence to
|
|
41
|
+
the verified freshly installed entrypoint instead of aborting after a
|
|
42
|
+
partial update, and install-recovery re-probes live state so completed
|
|
43
|
+
steps are never re-issued.
|
|
44
|
+
|
|
3
45
|
## 0.20.39 — Isolate fixed Codex agent adapters
|
|
4
46
|
|
|
5
47
|
- Disables local and remote plugin loading, locally built plugin sharing, and
|
package/package.json
CHANGED
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;
|
|
@@ -213,21 +206,11 @@ function impelClaudeSafeStorageName(tenantId) {
|
|
|
213
206
|
const escapedDisplayPrefix = RUNTIME_BRAND.apps.displayPrefix.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
214
207
|
const LEGACY_TRAPPED_CLAUDE_NAME = new RegExp(`^${escapedDisplayPrefix} Claude( \\[[^\\]]+\\])?$`, "u");
|
|
215
208
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
{ id: "claude-haiku-4-5", provider: "claude", display_name: "Claude Haiku 4.5", family: "haiku", family_default: true, context_window: 200000 },
|
|
222
|
-
{ id: "claude-fable-5", provider: "claude", display_name: "Claude Fable 5", family: "fable", family_default: true, context_window: 200000 },
|
|
223
|
-
{ 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" },
|
|
224
|
-
{ 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" },
|
|
225
|
-
{ 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" },
|
|
226
|
-
{ 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 },
|
|
227
|
-
{ 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 },
|
|
228
|
-
{ 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 },
|
|
229
|
-
{ 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 },
|
|
230
|
-
];
|
|
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";
|
|
231
214
|
|
|
232
215
|
export function normalizeAppTarget(value) {
|
|
233
216
|
if (value == null || value === "all") return ["claude", "chatgpt"];
|
|
@@ -315,7 +298,10 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
|
|
|
315
298
|
// native-agent attachment window so the visible working span is not
|
|
316
299
|
// truncated while the child is still running.
|
|
317
300
|
// 34: isolate fixed-binding Codex profiles from local and remote host plugins.
|
|
318
|
-
|
|
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;
|
|
319
305
|
|
|
320
306
|
// Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
|
|
321
307
|
// helper rebranding, and signing. A vendored bundle is rebuilt only when this
|
|
@@ -1452,7 +1438,10 @@ function writeChatGPTConfig(
|
|
|
1452
1438
|
const hardenedToml = hardenManagedCodexToml(mergedToml, configPath);
|
|
1453
1439
|
writeAtomic(
|
|
1454
1440
|
configPath,
|
|
1455
|
-
|
|
1441
|
+
enableManagedCodexDesktopCodeModeHost(
|
|
1442
|
+
enableManagedCodexDesktopMcpApps(hardenedToml, configPath),
|
|
1443
|
+
configPath,
|
|
1444
|
+
),
|
|
1456
1445
|
0o600,
|
|
1457
1446
|
);
|
|
1458
1447
|
secureManagedCodexHome(paths.chatgpt.codexHome);
|
|
@@ -1541,60 +1530,6 @@ export function foreignClaudeCodexCatalogEntry(model, index) {
|
|
|
1541
1530
|
};
|
|
1542
1531
|
}
|
|
1543
1532
|
|
|
1544
|
-
function codexCatalogEntry(model, index, vendorModel) {
|
|
1545
|
-
const generated = {
|
|
1546
|
-
slug: model.id,
|
|
1547
|
-
display_name: model.display_name || model.id,
|
|
1548
|
-
description: `Available through the Impel gateway (${model.id}).`,
|
|
1549
|
-
default_reasoning_level: "medium",
|
|
1550
|
-
supported_reasoning_levels: STANDARD_REASONING_LEVELS,
|
|
1551
|
-
shell_type: "shell_command",
|
|
1552
|
-
visibility: "list",
|
|
1553
|
-
supported_in_api: true,
|
|
1554
|
-
priority: index + 1,
|
|
1555
|
-
base_instructions: "You are Codex, an AI coding agent. Follow repository instructions, collaborate with the user, make scoped changes, and verify your work.",
|
|
1556
|
-
include_skills_usage_instructions: false,
|
|
1557
|
-
supports_reasoning_summaries: true,
|
|
1558
|
-
default_reasoning_summary: "none",
|
|
1559
|
-
support_verbosity: true,
|
|
1560
|
-
default_verbosity: "low",
|
|
1561
|
-
apply_patch_tool_type: "freeform",
|
|
1562
|
-
web_search_tool_type: "text_and_image",
|
|
1563
|
-
truncation_policy: { mode: "tokens", limit: 10000 },
|
|
1564
|
-
context_window: model.context_window || 200000,
|
|
1565
|
-
max_context_window: model.context_window || 200000,
|
|
1566
|
-
effective_context_window_percent: 95,
|
|
1567
|
-
experimental_supported_tools: [],
|
|
1568
|
-
input_modalities: ["text", "image"],
|
|
1569
|
-
supports_parallel_tool_calls: true,
|
|
1570
|
-
supports_image_detail_original: true,
|
|
1571
|
-
supports_search_tool: true,
|
|
1572
|
-
use_responses_lite: false,
|
|
1573
|
-
tool_mode: null,
|
|
1574
|
-
multi_agent_version: null,
|
|
1575
|
-
};
|
|
1576
|
-
const entry = vendorModel ? { ...generated, ...vendorModel } : generated;
|
|
1577
|
-
return {
|
|
1578
|
-
...entry,
|
|
1579
|
-
slug: model.id,
|
|
1580
|
-
display_name: model.display_name || entry.display_name || model.id,
|
|
1581
|
-
description: model.description || entry.description,
|
|
1582
|
-
default_reasoning_level: model.default_reasoning_level || entry.default_reasoning_level,
|
|
1583
|
-
supported_reasoning_levels: Array.isArray(model.supported_reasoning_levels)
|
|
1584
|
-
? model.supported_reasoning_levels
|
|
1585
|
-
: entry.supported_reasoning_levels,
|
|
1586
|
-
service_tiers: Array.isArray(model.service_tiers) ? model.service_tiers : (entry.service_tiers || []),
|
|
1587
|
-
visibility: "list",
|
|
1588
|
-
supported_in_api: true,
|
|
1589
|
-
priority: Number.isInteger(model.priority) ? model.priority : (entry.priority || index + 1),
|
|
1590
|
-
context_window: model.context_window || entry.context_window || 200000,
|
|
1591
|
-
max_context_window: model.max_context_window || entry.max_context_window || model.context_window || 200000,
|
|
1592
|
-
...(typeof model.use_responses_lite === "boolean" ? { use_responses_lite: model.use_responses_lite } : {}),
|
|
1593
|
-
...(Object.hasOwn(model, "tool_mode") ? { tool_mode: model.tool_mode } : {}),
|
|
1594
|
-
...(Object.hasOwn(model, "multi_agent_version") ? { multi_agent_version: model.multi_agent_version } : {}),
|
|
1595
|
-
};
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
1533
|
function readVendorCodexModels(vendorPath) {
|
|
1599
1534
|
const binary = vendorPath && path.join(vendorPath, "Contents", "Resources", "codex");
|
|
1600
1535
|
if (!binary || !fs.existsSync(binary)) return new Map();
|
|
@@ -1659,9 +1594,31 @@ export function managedChatGPTConfigDrifted(paths) {
|
|
|
1659
1594
|
} catch {
|
|
1660
1595
|
return false; // No managed profile; install/update owns creating one.
|
|
1661
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));
|
|
1662
1618
|
return !current.includes(CHATGPT_CONFIG_START)
|
|
1663
1619
|
|| readTopLevelTomlString(current, "model_provider") !== RUNTIME_BRAND.cli.providerId
|
|
1664
|
-
|| !readTopLevelTomlString(current, "chatgpt_base_url")
|
|
1620
|
+
|| !readTopLevelTomlString(current, "chatgpt_base_url")
|
|
1621
|
+
|| featureKeysDropped;
|
|
1665
1622
|
}
|
|
1666
1623
|
|
|
1667
1624
|
function readTopLevelTomlString(toml, key) {
|
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;
|
package/src/commands/apps.js
CHANGED
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
findWindowsChatGPTApp,
|
|
56
56
|
findManagedWindowsChatGPTApp,
|
|
57
57
|
launchWindowsClaudeApp,
|
|
58
|
+
pinnedWindowsChatGPTApp,
|
|
58
59
|
launchWindowsChatGPTApp,
|
|
59
60
|
removeManagedWindowsChatGPTApp,
|
|
60
61
|
stageWindowsChatGPTApp,
|
|
@@ -280,6 +281,7 @@ const WINGET_EXIT_HINTS = new Map([
|
|
|
280
281
|
[0x8A150014, "no package matched the winget query"],
|
|
281
282
|
[0x8A15001B, "the Microsoft Store client is blocked by policy"],
|
|
282
283
|
[0x8A15001C, "the Microsoft Store app is blocked by policy"],
|
|
284
|
+
[0x8A15002B, "a Store package is already installed but does not match the reviewed pin — its version likely moved past the pinned release"],
|
|
283
285
|
]);
|
|
284
286
|
|
|
285
287
|
function windowsProcessFailure(result) {
|
|
@@ -380,7 +382,7 @@ export async function reconcileWindowsTenantApps({
|
|
|
380
382
|
findClaudeApp: findWindowsClaudeApp,
|
|
381
383
|
ensureClaudeApp: ensureWindowsClaudeApp,
|
|
382
384
|
claudeUserData: windowsClaudeUserData,
|
|
383
|
-
findChatGPTApp:
|
|
385
|
+
findChatGPTApp: (environment) => pinnedWindowsChatGPTApp(environment),
|
|
384
386
|
ensureChatGPTApp: ensureWindowsChatGPTApp,
|
|
385
387
|
findManagedChatGPTApp: findManagedWindowsChatGPTApp,
|
|
386
388
|
stageChatGPTApp: stageWindowsChatGPTApp,
|
|
@@ -664,7 +666,7 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
664
666
|
ensureClaudeApp: ensureWindowsClaudeApp,
|
|
665
667
|
launchClaudeApp: launchWindowsClaudeApp,
|
|
666
668
|
claudeUserData: windowsClaudeUserData,
|
|
667
|
-
findChatGPTApp:
|
|
669
|
+
findChatGPTApp: (environment) => pinnedWindowsChatGPTApp(environment),
|
|
668
670
|
ensureChatGPTApp: ensureWindowsChatGPTApp,
|
|
669
671
|
findManagedChatGPTApp: findManagedWindowsChatGPTApp,
|
|
670
672
|
launchChatGPTApp: launchWindowsChatGPTApp,
|
package/src/commands/converge.js
CHANGED
|
@@ -214,7 +214,23 @@ export async function cmdConverge(argv = [], overrides = {}) {
|
|
|
214
214
|
goals: [{
|
|
215
215
|
id: "shared-vendor-clis-ready",
|
|
216
216
|
description: "Shared vendor CLI prerequisites are installed and discoverable",
|
|
217
|
-
|
|
217
|
+
// Live probe, not a session snapshot: a vendor CLI installed outside
|
|
218
|
+
// this process (for example by the user in another terminal while the
|
|
219
|
+
// recovery session runs) must count as complete the moment the goal
|
|
220
|
+
// is evaluated, so recovery never re-issues an already-finished step.
|
|
221
|
+
run: async () => {
|
|
222
|
+
if (sharedReady) return true;
|
|
223
|
+
const prepared = await io.preparePlatformClis({
|
|
224
|
+
gatewayUrl: config.gatewayUrl,
|
|
225
|
+
tenantId: selected.id,
|
|
226
|
+
platform: io.platform,
|
|
227
|
+
skipInstall: true,
|
|
228
|
+
// Goals re-run before every system-risk step: the probe must be
|
|
229
|
+
// pure discovery — no installs, profile writes, or skill syncs.
|
|
230
|
+
inspectOnly: true,
|
|
231
|
+
});
|
|
232
|
+
return prepared.missingAfter.length === 0;
|
|
233
|
+
},
|
|
218
234
|
}],
|
|
219
235
|
explicit: true,
|
|
220
236
|
noRecovery: false,
|
package/src/commands/setup.js
CHANGED
|
@@ -370,7 +370,23 @@ export async function cmdSetup(argv, overrides = {}) {
|
|
|
370
370
|
goals: [{
|
|
371
371
|
id: "shared-vendor-clis-ready",
|
|
372
372
|
description: "Shared vendor CLI prerequisites are installed and discoverable",
|
|
373
|
-
|
|
373
|
+
// Live probe, not a session snapshot: a vendor CLI installed outside
|
|
374
|
+
// this process (for example by the user in another terminal while the
|
|
375
|
+
// recovery session runs) must count as complete the moment the goal
|
|
376
|
+
// is evaluated, so recovery never re-issues an already-finished step.
|
|
377
|
+
run: async () => {
|
|
378
|
+
if (sharedReady) return true;
|
|
379
|
+
const prepared = await io.preparePlatformClis({
|
|
380
|
+
gatewayUrl,
|
|
381
|
+
tenantId: selected.id,
|
|
382
|
+
platform: io.platform,
|
|
383
|
+
skipInstall: true,
|
|
384
|
+
// Goals re-run before every system-risk step: the probe must be
|
|
385
|
+
// pure discovery — no installs, profile writes, or skill syncs.
|
|
386
|
+
inspectOnly: true,
|
|
387
|
+
});
|
|
388
|
+
return prepared.missingAfter.length === 0;
|
|
389
|
+
},
|
|
374
390
|
}],
|
|
375
391
|
explicit: Boolean(flags.repair),
|
|
376
392
|
noRecovery: false,
|
package/src/commands/status.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
productAccessLabel,
|
|
16
16
|
} from "../tenants.js";
|
|
17
17
|
import { installedVersion, maybePrintUpdateNotice } from "../updates.js";
|
|
18
|
-
import { windowsClaudeUserData } from "../windowsApps.js";
|
|
18
|
+
import { findManagedWindowsChatGPTApp, windowsClaudeUserData } from "../windowsApps.js";
|
|
19
19
|
import { RUNTIME_BRAND } from "../runtimeBrand.js";
|
|
20
20
|
import { cachedProviderReadiness, providerAvailable } from "../providerReadiness.js";
|
|
21
21
|
|
|
@@ -52,11 +52,16 @@ export function desktopReadiness(tenant, {
|
|
|
52
52
|
states[client] = { app: "unsupported", shell: "unsupported" };
|
|
53
53
|
continue;
|
|
54
54
|
}
|
|
55
|
+
// A generated config alone proves nothing launchable on Windows: when the
|
|
56
|
+
// vendor ensure failed, config.toml can exist while no managed app was
|
|
57
|
+
// ever staged — reporting "ready" then hides the exact failure the user
|
|
58
|
+
// is debugging (IMP-474). Require the staged binary too.
|
|
55
59
|
const appReady = platform === "darwin"
|
|
56
60
|
? existsSync(paths[product].launcher)
|
|
57
|
-
:
|
|
58
|
-
? path.join(paths.claude.userData, "configLibrary", `${CLAUDE_CONFIG_ID}.json`)
|
|
59
|
-
: path.join(paths.chatgpt.codexHome, "config.toml"))
|
|
61
|
+
: product === "claude"
|
|
62
|
+
? existsSync(path.join(paths.claude.userData, "configLibrary", `${CLAUDE_CONFIG_ID}.json`))
|
|
63
|
+
: existsSync(path.join(paths.chatgpt.codexHome, "config.toml"))
|
|
64
|
+
&& Boolean(findManagedWindowsChatGPTApp(paths.root, { isFile: existsSync }));
|
|
60
65
|
let shellReady = appReady && registered.has(product);
|
|
61
66
|
if (platform === "win32" && appData) {
|
|
62
67
|
const shortcutPath = path.win32.join(
|
package/src/commands/update.js
CHANGED
|
@@ -10,7 +10,7 @@ import { loadConfig, redactSecretText } from "../config.js";
|
|
|
10
10
|
import { nativeCommandInvocation } from "../nativeProcess.js";
|
|
11
11
|
import { withProgress } from "../progress.js";
|
|
12
12
|
import { runInstallRecovery } from "../installRecovery/engine.js";
|
|
13
|
-
import { brandedText, RUNTIME_BRAND } from "../runtimeBrand.js";
|
|
13
|
+
import { brandedEnvironmentName, brandedText, RUNTIME_BRAND } from "../runtimeBrand.js";
|
|
14
14
|
import { IMPEL_CLI_ENTRYPOINT } from "../selfInvocation.js";
|
|
15
15
|
import {
|
|
16
16
|
fetchRemoteVersion,
|
|
@@ -18,12 +18,20 @@ import {
|
|
|
18
18
|
isNewerVersion,
|
|
19
19
|
refreshUpdateCache,
|
|
20
20
|
updateInstallSpec,
|
|
21
|
+
updatePackage,
|
|
21
22
|
updateTagForVersion,
|
|
22
23
|
writeUpdateCache,
|
|
23
24
|
} from "../updates.js";
|
|
24
25
|
|
|
25
26
|
const CLI_BIN = IMPEL_CLI_ENTRYPOINT;
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Loop guard for the fresh-build continuation: `_converge` children run with
|
|
30
|
+
* this marker so a nested `impel update` inside the cascade can never start
|
|
31
|
+
* another self-update (and thus another cascade) recursively.
|
|
32
|
+
*/
|
|
33
|
+
export const UPDATE_CASCADE_ENV = brandedEnvironmentName("UPDATE_CASCADE");
|
|
34
|
+
|
|
27
35
|
/**
|
|
28
36
|
* The version now on disk at the package that owns CLI_BIN, read FRESH (never
|
|
29
37
|
* from this process's cached module graph). After `npm install -g` this is the
|
|
@@ -39,6 +47,28 @@ export function postInstallCliVersion() {
|
|
|
39
47
|
}
|
|
40
48
|
}
|
|
41
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Whether the build at CLI_BIN actually RUNS and reports the expected
|
|
52
|
+
* version. A bare package.json read is not proof after a failed
|
|
53
|
+
* `npm install -g`: the global replace is non-atomic (notably on Windows),
|
|
54
|
+
* so a half-written package can carry the new manifest with broken code.
|
|
55
|
+
*/
|
|
56
|
+
export function postInstallCliRunnable(expectedVersion, { spawn = spawnSync, execPath = process.execPath } = {}) {
|
|
57
|
+
if (typeof expectedVersion !== "string" || !expectedVersion.trim()) return false;
|
|
58
|
+
try {
|
|
59
|
+
const run = spawn(execPath, [CLI_BIN, "--version"], {
|
|
60
|
+
encoding: "utf8",
|
|
61
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
62
|
+
timeout: 15_000,
|
|
63
|
+
windowsHide: true,
|
|
64
|
+
});
|
|
65
|
+
if (run.status !== 0 || run.error) return false;
|
|
66
|
+
return String(run.stdout || "").trim() === expectedVersion.trim();
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
42
72
|
const HELP = brandedText(`impel update - update everything Impel in one command
|
|
43
73
|
|
|
44
74
|
Reinstalls impel-cli from npm, then uses the new build to discover every
|
|
@@ -101,11 +131,67 @@ function defaultSelfUpdate(spec) {
|
|
|
101
131
|
return installUpdateFromRegistry(spec);
|
|
102
132
|
}
|
|
103
133
|
|
|
134
|
+
/**
|
|
135
|
+
* After `npm install -g` exits 0 but the RUNNING CLI's own package still
|
|
136
|
+
* resolves the old version (npm-prefix skew: nvm/Homebrew node transitions,
|
|
137
|
+
* npm 11 dangling prepare-script symlinks), locate the build npm actually
|
|
138
|
+
* installed so the cascade can run under it. Fail-closed: the entry point
|
|
139
|
+
* must live inside the global root of the same `npm` the installer ran, and
|
|
140
|
+
* its package.json must resolve exactly the version npm reported for the
|
|
141
|
+
* update tag; anything else returns null and the caller keeps the hard error.
|
|
142
|
+
*/
|
|
143
|
+
export function resolveFreshGlobalEntrypoint(targetVersion, dependencies = {}) {
|
|
144
|
+
const io = {
|
|
145
|
+
spawnSync,
|
|
146
|
+
platform: process.platform,
|
|
147
|
+
environment: process.env,
|
|
148
|
+
readFileSync: fs.readFileSync,
|
|
149
|
+
existsSync: fs.existsSync,
|
|
150
|
+
...dependencies,
|
|
151
|
+
};
|
|
152
|
+
if (typeof targetVersion !== "string" || !targetVersion.trim()) return null;
|
|
153
|
+
try {
|
|
154
|
+
const invocation = nativeCommandInvocation("npm", ["root", "-g"], io.environment, io.platform);
|
|
155
|
+
const run = io.spawnSync(invocation.command, invocation.args, {
|
|
156
|
+
encoding: "utf8",
|
|
157
|
+
env: io.environment,
|
|
158
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
159
|
+
timeout: 15_000,
|
|
160
|
+
windowsHide: true,
|
|
161
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
162
|
+
});
|
|
163
|
+
if (run.status !== 0 || run.error) return null;
|
|
164
|
+
const globalRoot = String(run.stdout || "").trim();
|
|
165
|
+
if (!globalRoot || !path.isAbsolute(globalRoot)) return null;
|
|
166
|
+
// Containment is enforced at every hop: the package directory must stay
|
|
167
|
+
// inside the npm global root (an env-supplied package name cannot
|
|
168
|
+
// traverse out), the declared bin must stay inside the package, and the
|
|
169
|
+
// real (symlink-resolved) entry point must still live under the real
|
|
170
|
+
// global root. Anything else fails closed to the caller's hard error.
|
|
171
|
+
const packageRoot = path.resolve(path.join(globalRoot, ...updatePackage().split("/")));
|
|
172
|
+
if (!packageRoot.startsWith(path.resolve(globalRoot) + path.sep)) return null;
|
|
173
|
+
const pkg = JSON.parse(io.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
174
|
+
if (pkg?.version !== targetVersion) return null;
|
|
175
|
+
const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.[RUNTIME_BRAND.cli.command];
|
|
176
|
+
if (typeof bin !== "string" || !bin.trim()) return null;
|
|
177
|
+
const entrypoint = path.resolve(packageRoot, bin);
|
|
178
|
+
if (!entrypoint.startsWith(packageRoot + path.sep)) return null;
|
|
179
|
+
if (!io.existsSync(entrypoint)) return null;
|
|
180
|
+
const realEntrypoint = (io.realpathSync || fs.realpathSync)(entrypoint);
|
|
181
|
+
const realRoot = (io.realpathSync || fs.realpathSync)(globalRoot);
|
|
182
|
+
if (!realEntrypoint.startsWith(realRoot + path.sep)) return null;
|
|
183
|
+
return entrypoint;
|
|
184
|
+
} catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
104
189
|
// The cascading steps re-execute the (freshly installed) CLI binary so the
|
|
105
190
|
// NEW code performs them, not the process that started the update. On
|
|
106
191
|
// Windows this is also what refreshes the stable %LOCALAPPDATA% entry point:
|
|
107
192
|
// bin/impel.js rewrites it at startup, so the shim is only ever updated by a
|
|
108
|
-
// build
|
|
193
|
+
// build verified either by the postInstallCliVersion guard below or by
|
|
194
|
+
// resolveFreshGlobalEntrypoint's fail-closed skew resolution.
|
|
109
195
|
export function defaultRunConvergence({
|
|
110
196
|
skipApps = false,
|
|
111
197
|
skipClis = false,
|
|
@@ -113,6 +199,7 @@ export function defaultRunConvergence({
|
|
|
113
199
|
spawn = spawnSync,
|
|
114
200
|
execPath = process.execPath,
|
|
115
201
|
cliBin = CLI_BIN,
|
|
202
|
+
environment = process.env,
|
|
116
203
|
} = {}) {
|
|
117
204
|
const args = [cliBin, "_converge"];
|
|
118
205
|
if (skipApps) args.push("--skip-apps");
|
|
@@ -120,6 +207,7 @@ export function defaultRunConvergence({
|
|
|
120
207
|
if (noRecovery) args.push("--no-recovery");
|
|
121
208
|
const result = spawn(execPath, args, {
|
|
122
209
|
stdio: "inherit",
|
|
210
|
+
env: { ...environment, [UPDATE_CASCADE_ENV]: "1" },
|
|
123
211
|
});
|
|
124
212
|
return result.status === 0;
|
|
125
213
|
}
|
|
@@ -161,9 +249,12 @@ export async function cmdUpdate(argv, overrides = {}) {
|
|
|
161
249
|
runSkillsSync: defaultRunSkillsSync,
|
|
162
250
|
runAgentsSync: defaultRunAgentsSync,
|
|
163
251
|
platform: process.platform,
|
|
252
|
+
environment: process.env,
|
|
164
253
|
progress: withProgress,
|
|
165
254
|
recoverInstall: runInstallRecovery,
|
|
166
255
|
postInstallVersion: postInstallCliVersion,
|
|
256
|
+
postInstallRunnable: postInstallCliRunnable,
|
|
257
|
+
resolveFreshInstall: resolveFreshGlobalEntrypoint,
|
|
167
258
|
loadConfig,
|
|
168
259
|
...overrides,
|
|
169
260
|
};
|
|
@@ -225,10 +316,22 @@ export async function cmdUpdate(argv, overrides = {}) {
|
|
|
225
316
|
}
|
|
226
317
|
|
|
227
318
|
// ── CLI ────────────────────────────────────────────────────────────────
|
|
319
|
+
let cascadeEntrypoint = null;
|
|
228
320
|
if (!remote) {
|
|
229
321
|
console.warn("CLI: npm update check unavailable; keeping the installed build.");
|
|
230
322
|
} else if (upToDate) {
|
|
231
323
|
console.log("CLI: already up to date.");
|
|
324
|
+
} else if (io.environment?.[UPDATE_CASCADE_ENV] === "1") {
|
|
325
|
+
// Loop guard: a self-update must never start from inside its own
|
|
326
|
+
// fresh-build continuation, or a buggy cascade could respawn forever.
|
|
327
|
+
// Degrade rather than abort: an inherited marker (e.g. a terminal opened
|
|
328
|
+
// under a converge child) must not permanently brick `impel update` —
|
|
329
|
+
// convergence still runs; only the self-update is withheld.
|
|
330
|
+
console.warn(
|
|
331
|
+
`impel update: skipping the self-update because ${UPDATE_CASCADE_ENV} is set `
|
|
332
|
+
+ "(update cascade in progress, or inherited from one). Convergence continues; "
|
|
333
|
+
+ `for a self-update, unset ${UPDATE_CASCADE_ENV} or open a fresh terminal.`,
|
|
334
|
+
);
|
|
232
335
|
} else {
|
|
233
336
|
console.log(`CLI: installing the npm ${updateTag} build…`);
|
|
234
337
|
if (!await io.progress(`Installing the npm ${updateTag} impel-cli build`, () => io.selfUpdate(installSpec))) {
|
|
@@ -240,8 +343,11 @@ export async function cmdUpdate(argv, overrides = {}) {
|
|
|
240
343
|
const config = io.loadConfig();
|
|
241
344
|
let recovered = false;
|
|
242
345
|
if (config?.pat && config?.tenantId) {
|
|
243
|
-
// The goal
|
|
244
|
-
//
|
|
346
|
+
// The goal passes on the most recent reviewed retry of the npm
|
|
347
|
+
// install OR on a live disk probe of the package that owns the
|
|
348
|
+
// running CLI: when the user completes the printed manual
|
|
349
|
+
// `npm install --global …` in another terminal mid-recovery, the goal
|
|
350
|
+
// must observe that completion instead of re-issuing the same step.
|
|
245
351
|
const updateState = { ok: false };
|
|
246
352
|
try {
|
|
247
353
|
const recovery = await io.recoverInstall(
|
|
@@ -259,7 +365,10 @@ export async function cmdUpdate(argv, overrides = {}) {
|
|
|
259
365
|
{
|
|
260
366
|
id: "cli-update",
|
|
261
367
|
description: `The global ${RUNTIME_BRAND.cli.packageName} npm update completed successfully`,
|
|
262
|
-
run: () => updateState.ok === true
|
|
368
|
+
run: () => updateState.ok === true
|
|
369
|
+
|| (remote != null
|
|
370
|
+
&& io.postInstallVersion() === remote
|
|
371
|
+
&& io.postInstallRunnable(remote)),
|
|
263
372
|
},
|
|
264
373
|
],
|
|
265
374
|
explicit: Boolean(flags.repair),
|
|
@@ -296,19 +405,35 @@ export async function cmdUpdate(argv, overrides = {}) {
|
|
|
296
405
|
console.log("CLI: recovery verified the update path.");
|
|
297
406
|
}
|
|
298
407
|
// npm exiting 0 is NOT proof the RUNNING install was updated: with multiple
|
|
299
|
-
// Node installs / npm prefixes (
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
408
|
+
// Node installs / npm prefixes (nvm/Homebrew node transitions on macOS,
|
|
409
|
+
// prefix skew on Windows), the install can land in a different global
|
|
410
|
+
// prefix while CLI_BIN — the path the cascade re-executes — still holds
|
|
411
|
+
// the old build. Cascading old code that believes an update is still
|
|
412
|
+
// pending is how unbounded respawn storms start, so the cascade may only
|
|
413
|
+
// ever run a build that provably resolves the target version. Probe the
|
|
414
|
+
// version at CLI_BIN fresh from disk; on skew, locate and verify the
|
|
415
|
+
// build npm actually installed and hand the continuation to THAT binary.
|
|
416
|
+
// (The previous behavior — abort with exit 1 — left the machine
|
|
417
|
+
// half-updated: CLI updated in the other prefix, no tenant/app/skills
|
|
418
|
+
// convergence, and a rerun from the same terminal repeated the abort via
|
|
419
|
+
// the shell's stale command hash. That is the field-reported "run
|
|
420
|
+
// `impel update` twice, sometimes in separate terminals".)
|
|
304
421
|
const postInstall = io.postInstallVersion();
|
|
305
422
|
if (remote && postInstall !== remote) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
423
|
+
cascadeEntrypoint = io.resolveFreshInstall(remote);
|
|
424
|
+
if (!cascadeEntrypoint) {
|
|
425
|
+
console.error(`impel update: npm reported success but the running CLI still resolves v${postInstall ?? "?"} (expected v${remote}), and the freshly installed build could not be located and verified.`);
|
|
426
|
+
console.error(` Running CLI: ${CLI_BIN}`);
|
|
427
|
+
console.error(" This usually means the `impel` on PATH belongs to a different npm prefix than `npm prefix -g`.");
|
|
428
|
+
console.error(` Fix: run \`npm prefix -g\`, confirm it owns the \`impel\` shim on PATH, then \`npm install --global ${installSpec}\` there.`);
|
|
429
|
+
process.exitCode = 1;
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
console.warn(`impel update: the running CLI at ${CLI_BIN} still resolves v${postInstall ?? "?"}; continuing with the freshly installed v${remote}.`);
|
|
433
|
+
console.warn(` Fresh build: ${cascadeEntrypoint}`);
|
|
434
|
+
console.warn(io.platform === "win32"
|
|
435
|
+
? " Your current shell may still launch the old install. Open a new terminal, and align the `impel` on PATH with `npm prefix -g`."
|
|
436
|
+
: " Your current shell may still launch the old install. Run `hash -r` (zsh/bash) or open a new terminal, and align the `impel` on PATH with `npm prefix -g`.");
|
|
312
437
|
}
|
|
313
438
|
console.log(`CLI: updated${remote ? ` to v${remote}` : ""}.`);
|
|
314
439
|
}
|
|
@@ -318,6 +443,9 @@ export async function cmdUpdate(argv, overrides = {}) {
|
|
|
318
443
|
skipApps: Boolean(flags["skip-apps"]),
|
|
319
444
|
skipClis: Boolean(flags["skip-clis"]),
|
|
320
445
|
noRecovery: Boolean(flags["no-recovery"]),
|
|
446
|
+
// On npm-prefix skew the continuation must re-exec the verified fresh
|
|
447
|
+
// build; everywhere else CLI_BIN already holds the new bytes.
|
|
448
|
+
...(cascadeEntrypoint ? { cliBin: cascadeEntrypoint } : {}),
|
|
321
449
|
};
|
|
322
450
|
if (!await io.runConvergence(convergenceArgs)) {
|
|
323
451
|
console.error("impel update: tenant convergence failed; rerun `impel update` after addressing the reported issue.");
|
|
@@ -358,6 +358,27 @@ export async function runInstallRecovery(options, overrides = {}) {
|
|
|
358
358
|
continue;
|
|
359
359
|
}
|
|
360
360
|
}
|
|
361
|
+
// A blocked report gets one live goal pass before it stands: when
|
|
362
|
+
// every health check succeeds (a skipped step, or the user
|
|
363
|
+
// finishing the work in another terminal), the machine is healthy
|
|
364
|
+
// regardless of what the model concluded — mirroring the
|
|
365
|
+
// budget-exhaustion upgrade below.
|
|
366
|
+
if (status === "blocked") {
|
|
367
|
+
goalReport = await runGoals(session.goals);
|
|
368
|
+
if (goalReport.passed) {
|
|
369
|
+
finished = {
|
|
370
|
+
status: "fixed",
|
|
371
|
+
summary: "Every goal health check passes; the reported blocker is no longer present.",
|
|
372
|
+
userAction: null,
|
|
373
|
+
};
|
|
374
|
+
toolResults.push({
|
|
375
|
+
type: "tool_result",
|
|
376
|
+
tool_use_id: call.id,
|
|
377
|
+
content: "Outcome recorded.",
|
|
378
|
+
});
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
361
382
|
finished = {
|
|
362
383
|
status,
|
|
363
384
|
summary: String(call.input?.summary || "").slice(0, 2_000),
|
|
@@ -371,6 +392,24 @@ export async function runInstallRecovery(options, overrides = {}) {
|
|
|
371
392
|
break;
|
|
372
393
|
}
|
|
373
394
|
|
|
395
|
+
// Live re-probe before any system-risk step: when every goal health
|
|
396
|
+
// check already passes, the state this step would produce is already
|
|
397
|
+
// present on the machine (typically because the user completed the
|
|
398
|
+
// instruction in another terminal mid-session). Skip the step instead
|
|
399
|
+
// of re-prompting for an installer the environment no longer needs.
|
|
400
|
+
if (installRecoveryToolRisk(call.name) === "system" && session.goals.length) {
|
|
401
|
+
const live = await runGoals(session.goals);
|
|
402
|
+
if (live.passed) {
|
|
403
|
+
io.log(`Install recovery: skipping ${call.name} — every goal health check already passes.`);
|
|
404
|
+
toolResults.push({
|
|
405
|
+
type: "tool_result",
|
|
406
|
+
tool_use_id: call.id,
|
|
407
|
+
content: "Skipped: every goal health check now passes — this step is already satisfied on this machine. Verify with check_health and finish with report_outcome.",
|
|
408
|
+
});
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
374
413
|
const result = await executeCall(session, call.name, call.input);
|
|
375
414
|
if (result.outcome === "declined" && installRecoveryToolRisk(call.name) === "system") {
|
|
376
415
|
declinedSystemCalls += 1;
|
|
@@ -397,6 +436,12 @@ export async function runInstallRecovery(options, overrides = {}) {
|
|
|
397
436
|
}
|
|
398
437
|
|
|
399
438
|
if (declinedSystemCalls >= 2) {
|
|
439
|
+
// Same live upgrade as the blocked and budget paths: declining a
|
|
440
|
+
// repair the machine no longer needs must not read as a failure.
|
|
441
|
+
goalReport = await runGoals(session.goals);
|
|
442
|
+
if (goalReport.passed) {
|
|
443
|
+
return terminalReturn(session, "fixed", "The installation passes all health checks.");
|
|
444
|
+
}
|
|
400
445
|
return terminalReturn(
|
|
401
446
|
session,
|
|
402
447
|
"aborted",
|
package/src/macSetup.js
CHANGED
|
@@ -172,6 +172,7 @@ export async function prepareMacClis({
|
|
|
172
172
|
gatewayUrl,
|
|
173
173
|
tenantId,
|
|
174
174
|
skipInstall = false,
|
|
175
|
+
inspectOnly = false,
|
|
175
176
|
installTools = ["claude", "codex"],
|
|
176
177
|
} = {}, dependencies = {}) {
|
|
177
178
|
const io = {
|
|
@@ -194,6 +195,22 @@ export async function prepareMacClis({
|
|
|
194
195
|
if (requested.size !== installTools.length || [...requested].some((tool) => !io.installers[tool])) {
|
|
195
196
|
throw new Error("installTools must contain unique supported macOS CLI names");
|
|
196
197
|
}
|
|
198
|
+
// A pure discovery probe: recovery goals and health checks re-evaluate
|
|
199
|
+
// this repeatedly, so it must never install, write profiles, or sync
|
|
200
|
+
// skills — only report what is currently discoverable.
|
|
201
|
+
if (inspectOnly) {
|
|
202
|
+
return {
|
|
203
|
+
binaries: before,
|
|
204
|
+
missingBefore,
|
|
205
|
+
missingAfter: [...missingBefore],
|
|
206
|
+
installAttempted: false,
|
|
207
|
+
installSucceeded: null,
|
|
208
|
+
installFailure: null,
|
|
209
|
+
installations: {},
|
|
210
|
+
installCommands: {},
|
|
211
|
+
profiles: null,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
197
214
|
|
|
198
215
|
const installations = {};
|
|
199
216
|
if (missingBefore.length && !skipInstall) {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// The single model registry shared by every managed Codex surface. This
|
|
2
|
+
// module must stay a leaf (importing only other leaf modules such as
|
|
3
|
+
// runtimeBrand.js) so both the desktop app generation in apps.js and the CLI
|
|
4
|
+
// tenant profiles in cliProfiles.js can consume it without an import cycle —
|
|
5
|
+
// the 0.20.39 regression happened precisely because the CLI carried its own
|
|
6
|
+
// hand-frozen copy of this data.
|
|
7
|
+
import { RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
8
|
+
|
|
9
|
+
const STANDARD_REASONING_LEVELS = [
|
|
10
|
+
{ effort: "low", description: "Fast responses with lighter reasoning" },
|
|
11
|
+
{ effort: "medium", description: "Balances speed and reasoning depth for everyday tasks" },
|
|
12
|
+
{ effort: "high", description: "Greater reasoning depth for complex problems" },
|
|
13
|
+
{ effort: "xhigh", description: "Extra high reasoning depth for complex problems" },
|
|
14
|
+
];
|
|
15
|
+
const MAX_REASONING_LEVEL = { effort: "max", description: "Maximum reasoning depth for the hardest problems" };
|
|
16
|
+
const ULTRA_REASONING_LEVEL = { effort: "ultra", description: "Maximum reasoning with automatic task delegation" };
|
|
17
|
+
const FAST_SERVICE_TIER = { id: "priority", name: "Fast", description: "1.5x speed, increased usage" };
|
|
18
|
+
|
|
19
|
+
export const FALLBACK_MODELS = [
|
|
20
|
+
{ id: "claude-opus-5", provider: "claude", display_name: "Claude Opus 5", family: "opus", family_default: true, default: true, supports_1m: true, context_window: 1000000 },
|
|
21
|
+
{ id: "claude-opus-4-8", provider: "claude", display_name: "Claude Opus 4.8", family: "opus", supports_1m: true, context_window: 1000000 },
|
|
22
|
+
{ id: "claude-sonnet-5", provider: "claude", display_name: "Claude Sonnet 5", family: "sonnet", family_default: true, context_window: 200000 },
|
|
23
|
+
{ id: "claude-sonnet-4-6", provider: "claude", display_name: "Claude Sonnet 4.6", family: "sonnet", context_window: 200000 },
|
|
24
|
+
{ id: "claude-haiku-4-5", provider: "claude", display_name: "Claude Haiku 4.5", family: "haiku", family_default: true, context_window: 200000 },
|
|
25
|
+
{ id: "claude-fable-5", provider: "claude", display_name: "Claude Fable 5", family: "fable", family_default: true, context_window: 200000 },
|
|
26
|
+
{ 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" },
|
|
27
|
+
{ 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" },
|
|
28
|
+
{ 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" },
|
|
29
|
+
{ 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 },
|
|
30
|
+
{ 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 },
|
|
31
|
+
{ 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 },
|
|
32
|
+
{ 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 },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export function codexCatalogEntry(model, index, vendorModel) {
|
|
36
|
+
const generated = {
|
|
37
|
+
slug: model.id,
|
|
38
|
+
display_name: model.display_name || model.id,
|
|
39
|
+
description: `Available through the Impel gateway (${model.id}).`,
|
|
40
|
+
default_reasoning_level: "medium",
|
|
41
|
+
supported_reasoning_levels: STANDARD_REASONING_LEVELS,
|
|
42
|
+
shell_type: "shell_command",
|
|
43
|
+
visibility: "list",
|
|
44
|
+
supported_in_api: true,
|
|
45
|
+
priority: index + 1,
|
|
46
|
+
base_instructions: "You are Codex, an AI coding agent. Follow repository instructions, collaborate with the user, make scoped changes, and verify your work.",
|
|
47
|
+
include_skills_usage_instructions: false,
|
|
48
|
+
supports_reasoning_summaries: true,
|
|
49
|
+
default_reasoning_summary: "none",
|
|
50
|
+
support_verbosity: true,
|
|
51
|
+
default_verbosity: "low",
|
|
52
|
+
apply_patch_tool_type: "freeform",
|
|
53
|
+
web_search_tool_type: "text_and_image",
|
|
54
|
+
truncation_policy: { mode: "tokens", limit: 10000 },
|
|
55
|
+
context_window: model.context_window || 200000,
|
|
56
|
+
max_context_window: model.context_window || 200000,
|
|
57
|
+
effective_context_window_percent: 95,
|
|
58
|
+
experimental_supported_tools: [],
|
|
59
|
+
input_modalities: ["text", "image"],
|
|
60
|
+
supports_parallel_tool_calls: true,
|
|
61
|
+
supports_image_detail_original: true,
|
|
62
|
+
supports_search_tool: true,
|
|
63
|
+
use_responses_lite: false,
|
|
64
|
+
tool_mode: null,
|
|
65
|
+
multi_agent_version: null,
|
|
66
|
+
};
|
|
67
|
+
const entry = vendorModel ? { ...generated, ...vendorModel } : generated;
|
|
68
|
+
return {
|
|
69
|
+
...entry,
|
|
70
|
+
slug: model.id,
|
|
71
|
+
display_name: model.display_name || entry.display_name || model.id,
|
|
72
|
+
description: model.description || entry.description,
|
|
73
|
+
default_reasoning_level: model.default_reasoning_level || entry.default_reasoning_level,
|
|
74
|
+
supported_reasoning_levels: Array.isArray(model.supported_reasoning_levels)
|
|
75
|
+
? model.supported_reasoning_levels
|
|
76
|
+
: entry.supported_reasoning_levels,
|
|
77
|
+
service_tiers: Array.isArray(model.service_tiers) ? model.service_tiers : (entry.service_tiers || []),
|
|
78
|
+
visibility: "list",
|
|
79
|
+
supported_in_api: true,
|
|
80
|
+
priority: Number.isInteger(model.priority) ? model.priority : (entry.priority || index + 1),
|
|
81
|
+
context_window: model.context_window || entry.context_window || 200000,
|
|
82
|
+
max_context_window: model.max_context_window || entry.max_context_window || model.context_window || 200000,
|
|
83
|
+
...(typeof model.use_responses_lite === "boolean" ? { use_responses_lite: model.use_responses_lite } : {}),
|
|
84
|
+
...(Object.hasOwn(model, "tool_mode") ? { tool_mode: model.tool_mode } : {}),
|
|
85
|
+
...(Object.hasOwn(model, "multi_agent_version") ? { multi_agent_version: model.multi_agent_version } : {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The managed CLI Codex catalog, projected from the same model registry the
|
|
91
|
+
* desktop app uses so reasoning efforts, service tiers, and tool modes can
|
|
92
|
+
* never drift between the two surfaces. CLI profiles pin this file to keep
|
|
93
|
+
* launches offline; vendor enrichment stays a desktop-only concern.
|
|
94
|
+
*/
|
|
95
|
+
export function managedCodexCliCatalog(now = new Date()) {
|
|
96
|
+
return {
|
|
97
|
+
fetched_at: now.toISOString(),
|
|
98
|
+
client_version: `${RUNTIME_BRAND.cli.command}-managed-2`,
|
|
99
|
+
models: FALLBACK_MODELS
|
|
100
|
+
.filter((model) => model.provider === "codex")
|
|
101
|
+
.map((model, index) => codexCatalogEntry(model, index)),
|
|
102
|
+
};
|
|
103
|
+
}
|
package/src/platformSetup.js
CHANGED
|
@@ -15,10 +15,8 @@ export async function preparePlatformClis(options = {}, dependencies = {}) {
|
|
|
15
15
|
claude: find("claude", environment, platform),
|
|
16
16
|
codex: find("codex", environment, platform),
|
|
17
17
|
};
|
|
18
|
-
const claudeProfile = (dependencies.ensureClaudeProfile || ensureImpelClaudeProfile)(options.gatewayUrl, options.tenantId);
|
|
19
|
-
const codexProfile = (dependencies.ensureCodexProfile || ensureImpelCodexProfile)(options.gatewayUrl, options.tenantId);
|
|
20
18
|
const missingAfter = Object.entries(binaries).filter(([, binary]) => !binary).map(([tool]) => tool);
|
|
21
|
-
|
|
19
|
+
const result = {
|
|
22
20
|
binaries,
|
|
23
21
|
missingBefore: [...missingAfter],
|
|
24
22
|
missingAfter,
|
|
@@ -27,6 +25,14 @@ export async function preparePlatformClis(options = {}, dependencies = {}) {
|
|
|
27
25
|
installFailure: null,
|
|
28
26
|
installations: {},
|
|
29
27
|
installCommands: {},
|
|
28
|
+
profiles: null,
|
|
29
|
+
};
|
|
30
|
+
// Discovery-only probes must not write profiles either.
|
|
31
|
+
if (options.inspectOnly) return result;
|
|
32
|
+
const claudeProfile = (dependencies.ensureClaudeProfile || ensureImpelClaudeProfile)(options.gatewayUrl, options.tenantId);
|
|
33
|
+
const codexProfile = (dependencies.ensureCodexProfile || ensureImpelCodexProfile)(options.gatewayUrl, options.tenantId);
|
|
34
|
+
return {
|
|
35
|
+
...result,
|
|
30
36
|
profiles: { claude: claudeProfile.configDir, codex: codexProfile.codexHome },
|
|
31
37
|
};
|
|
32
38
|
}
|
package/src/windowsApps.js
CHANGED
|
@@ -90,6 +90,71 @@ function installedMsixChatGPT(environment, run = spawnSync, pin = null) {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Register the already-staged ChatGPT Store package for the current user.
|
|
95
|
+
* winget resolves packages machine-wide, so on a machine where another user
|
|
96
|
+
* (or provisioning) installed the app, `winget install` reports
|
|
97
|
+
* UPDATE_NOT_APPLICABLE while the current user's Get-AppxPackage sees
|
|
98
|
+
* nothing. Add-AppxPackage -RegisterByFamilyName registers the existing
|
|
99
|
+
* staged bits for this user without downloading or elevating.
|
|
100
|
+
*/
|
|
101
|
+
export function registerProvisionedMsixChatGPT(environment, run = spawnSync) {
|
|
102
|
+
const familyName = `${WINDOWS_CHATGPT_PACKAGE_NAME}_${WINDOWS_CHATGPT_PUBLISHER_ID}`;
|
|
103
|
+
try {
|
|
104
|
+
const result = run("powershell.exe", [
|
|
105
|
+
"-NoProfile",
|
|
106
|
+
"-NonInteractive",
|
|
107
|
+
"-Command",
|
|
108
|
+
`Add-AppxPackage -RegisterByFamilyName -MainPackage '${familyName}' -ErrorAction Stop`,
|
|
109
|
+
], {
|
|
110
|
+
encoding: "utf8",
|
|
111
|
+
env: windowsPowerShellEnvironment(environment),
|
|
112
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
113
|
+
timeout: 120_000,
|
|
114
|
+
windowsHide: true,
|
|
115
|
+
});
|
|
116
|
+
return result?.status === 0 && !result?.error;
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function compareDottedVersions(left, right) {
|
|
123
|
+
const parse = (value) => String(value).split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
124
|
+
const a = parse(left);
|
|
125
|
+
const b = parse(right);
|
|
126
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
|
127
|
+
const delta = (a[index] || 0) - (b[index] || 0);
|
|
128
|
+
if (delta !== 0) return delta;
|
|
129
|
+
}
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The version string of whatever ChatGPT Store package is installed,
|
|
135
|
+
* regardless of the reviewed pin. Diagnostic-only: it lets a pin mismatch
|
|
136
|
+
* name the actual installed version instead of a bare winget HRESULT.
|
|
137
|
+
*/
|
|
138
|
+
export function installedMsixChatGPTVersion(environment, run = spawnSync) {
|
|
139
|
+
const script = [
|
|
140
|
+
`$package = Get-AppxPackage -Name '${WINDOWS_CHATGPT_PACKAGE_NAME}' -ErrorAction SilentlyContinue`,
|
|
141
|
+
`$package = $package | Where-Object { $_.PublisherId -ceq '${WINDOWS_CHATGPT_PUBLISHER_ID}' } | Sort-Object Version -Descending | Select-Object -First 1`,
|
|
142
|
+
"if ($package) { $package.Version.ToString() }",
|
|
143
|
+
].join("; ");
|
|
144
|
+
try {
|
|
145
|
+
const result = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
146
|
+
encoding: "utf8",
|
|
147
|
+
env: windowsPowerShellEnvironment(environment),
|
|
148
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
149
|
+
timeout: 15_000,
|
|
150
|
+
windowsHide: true,
|
|
151
|
+
});
|
|
152
|
+
return result?.status === 0 ? String(result.stdout || "").trim() || null : null;
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
93
158
|
/** Candidate paths for Anthropic's signed per-user Windows desktop install. */
|
|
94
159
|
export function windowsClaudeAppCandidates(environment = process.env, dependencies = {}) {
|
|
95
160
|
const io = { readDirectory: fs.readdirSync, ...dependencies };
|
|
@@ -148,7 +213,7 @@ export function findWindowsChatGPTApp(environment = process.env, dependencies =
|
|
|
148
213
|
return msix && exists(msix) ? path.win32.normalize(msix) : null;
|
|
149
214
|
}
|
|
150
215
|
|
|
151
|
-
function pinnedWindowsChatGPTApp(environment = process.env, dependencies = {}) {
|
|
216
|
+
export function pinnedWindowsChatGPTApp(environment = process.env, dependencies = {}) {
|
|
152
217
|
const exists = dependencies.isFile || isFile;
|
|
153
218
|
const pin = dependencies.pin || WINDOWS_CHATGPT_PIN;
|
|
154
219
|
const msix = (dependencies.queryMsix || installedMsixChatGPT)(environment, dependencies.run, pin);
|
|
@@ -434,16 +499,36 @@ export function ensureWindowsChatGPTApp({ update = false } = {}, dependencies =
|
|
|
434
499
|
result = { status: null, error };
|
|
435
500
|
}
|
|
436
501
|
let resultBinary = io.findPinned(io.environment, { pin: io.pin, run: io.run });
|
|
437
|
-
|
|
438
|
-
// with no newer Store release. That is a healthy idempotent update result,
|
|
439
|
-
// not an installation failure (APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE).
|
|
440
|
-
const alreadyUpToDate = Boolean(
|
|
441
|
-
shouldUpdate
|
|
442
|
-
&& resultBinary
|
|
443
|
-
&& !result?.error
|
|
502
|
+
const noApplicableUpgrade = !result?.error
|
|
444
503
|
&& Number.isInteger(result?.status)
|
|
445
|
-
&& (result.status >>> 0) === WINDOWS_WINGET_UPDATE_NOT_APPLICABLE
|
|
446
|
-
|
|
504
|
+
&& (result.status >>> 0) === WINDOWS_WINGET_UPDATE_NOT_APPLICABLE;
|
|
505
|
+
// winget resolves machine-wide: when it reports the package installed but
|
|
506
|
+
// the current user's probe sees nothing, the staged bits are registered to
|
|
507
|
+
// another user or provisioning. Registering them for this user is a local,
|
|
508
|
+
// download-free repair; the pin-exact probe afterwards keeps it fail-closed.
|
|
509
|
+
let registeredOffPin = false;
|
|
510
|
+
if (noApplicableUpgrade && !resultBinary) {
|
|
511
|
+
io.logger.log("ChatGPT/Codex: attempting to register the machine-wide Microsoft Store package for the current user…");
|
|
512
|
+
if ((io.registerProvisioned || registerProvisionedMsixChatGPT)(io.environment, io.run)) {
|
|
513
|
+
resultBinary = io.findPinned(io.environment, { pin: io.pin, run: io.run });
|
|
514
|
+
if (resultBinary) {
|
|
515
|
+
io.logger.log("ChatGPT/Codex: registered the already-staged Microsoft Store package for the current user.");
|
|
516
|
+
} else {
|
|
517
|
+
registeredOffPin = true;
|
|
518
|
+
}
|
|
519
|
+
} else {
|
|
520
|
+
io.logger.log("ChatGPT/Codex: the Store package could not be registered for the current user.");
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// winget uses an error HRESULT when it finds an installed package with no
|
|
524
|
+
// newer Store release (APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE). That
|
|
525
|
+
// is a healthy idempotent result whenever the pin-exact re-query proves the
|
|
526
|
+
// reviewed package is present — during setup/install as much as update:
|
|
527
|
+
// `winget install` reports the same HRESULT for an already-installed
|
|
528
|
+
// package, and treating it as failure strands first-run machines in an
|
|
529
|
+
// unconvergeable recovery loop. Acceptance stays fail-closed through the
|
|
530
|
+
// pin-exact probe below.
|
|
531
|
+
const alreadyUpToDate = Boolean(resultBinary && noApplicableUpgrade);
|
|
447
532
|
if (alreadyUpToDate) {
|
|
448
533
|
io.logger.log(`ChatGPT/Codex: Microsoft Store package ${io.pin.packageVersion} already up to date.`);
|
|
449
534
|
}
|
|
@@ -456,6 +541,33 @@ export function ensureWindowsChatGPTApp({ update = false } = {}, dependencies =
|
|
|
456
541
|
),
|
|
457
542
|
};
|
|
458
543
|
}
|
|
544
|
+
// A no-applicable-upgrade result over a package that fails the pin-exact
|
|
545
|
+
// probe means the Store carries a different (usually newer) build the
|
|
546
|
+
// Store cannot downgrade. Name both versions so the operator learns this
|
|
547
|
+
// needs a CLI release with an updated ChatGPT pin, not another retry.
|
|
548
|
+
if (!commandSucceeded && !resultBinary && noApplicableUpgrade) {
|
|
549
|
+
const installedVersion = (io.installedVersion || installedMsixChatGPTVersion)(io.environment, io.run);
|
|
550
|
+
if (installedVersion) {
|
|
551
|
+
const newer = compareDottedVersions(installedVersion, io.pin.packageVersion) > 0;
|
|
552
|
+
result = {
|
|
553
|
+
...result,
|
|
554
|
+
error: new Error(
|
|
555
|
+
`installed ChatGPT Store package ${installedVersion} does not match the reviewed pin ${io.pin.packageVersion}; `
|
|
556
|
+
+ (newer
|
|
557
|
+
? "the Microsoft Store cannot downgrade, so this needs a CLI release with an updated ChatGPT pin"
|
|
558
|
+
: "the Store has not fulfilled the pinned build on this machine yet — retry once the Store catches up"),
|
|
559
|
+
),
|
|
560
|
+
};
|
|
561
|
+
} else if (registeredOffPin) {
|
|
562
|
+
result = {
|
|
563
|
+
...result,
|
|
564
|
+
error: new Error(
|
|
565
|
+
"a machine-wide ChatGPT Store package was registered for the current user but does not match "
|
|
566
|
+
+ `the reviewed pin ${io.pin.packageVersion}; the managed app stays uninstalled until a matching build is available`,
|
|
567
|
+
),
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
}
|
|
459
571
|
const succeeded = commandSucceeded && Boolean(resultBinary);
|
|
460
572
|
if (!succeeded) resultBinary = null;
|
|
461
573
|
return {
|
package/src/windowsSetup.js
CHANGED
|
@@ -121,6 +121,7 @@ export async function prepareWindowsClis({
|
|
|
121
121
|
gatewayUrl,
|
|
122
122
|
tenantId,
|
|
123
123
|
skipInstall = false,
|
|
124
|
+
inspectOnly = false,
|
|
124
125
|
installTools = ["claude", "codex"],
|
|
125
126
|
} = {}, dependencies = {}) {
|
|
126
127
|
const io = {
|
|
@@ -149,6 +150,25 @@ export async function prepareWindowsClis({
|
|
|
149
150
|
throw new Error("installTools must contain unique supported Windows CLI names");
|
|
150
151
|
}
|
|
151
152
|
|
|
153
|
+
// A pure discovery probe: recovery goals and health checks re-evaluate
|
|
154
|
+
// this repeatedly, so it must never download git, install anything, write
|
|
155
|
+
// profiles, or sync skills — only report what is currently discoverable.
|
|
156
|
+
// This short-circuit MUST stay ahead of the git provisioning below, which
|
|
157
|
+
// performs an unprompted network install when git is absent.
|
|
158
|
+
if (inspectOnly) {
|
|
159
|
+
return {
|
|
160
|
+
binaries: before,
|
|
161
|
+
missingBefore,
|
|
162
|
+
missingAfter: [...missingBefore],
|
|
163
|
+
installAttempted: false,
|
|
164
|
+
installSucceeded: null,
|
|
165
|
+
installFailure: null,
|
|
166
|
+
installations: {},
|
|
167
|
+
installCommands: {},
|
|
168
|
+
profiles: null,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
152
172
|
// Both vendor CLIs need `git` to install the skills marketplace, and the
|
|
153
173
|
// target machines are brand new — provision the Impel-managed MinGit before
|
|
154
174
|
// any skill sync, on every setup/update pass (idempotent once installed).
|
package/src/codexCliCatalog.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
const LEVELS = ["low", "medium", "high", "xhigh"].map((effort) => ({
|
|
2
|
-
effort,
|
|
3
|
-
description: `${effort} reasoning effort`,
|
|
4
|
-
}));
|
|
5
|
-
|
|
6
|
-
const MODELS = [
|
|
7
|
-
["gpt-5.6-sol", "GPT-5.6-Sol", 1, "medium", "code_mode_only", "v2"],
|
|
8
|
-
["gpt-5.6-terra", "GPT-5.6-Terra", 2, "medium", "code_mode_only", "v2"],
|
|
9
|
-
["gpt-5.6-luna", "GPT-5.6-Luna", 3, "medium", "code_mode_only", "v1"],
|
|
10
|
-
["gpt-5.5", "GPT-5.5", 7, "medium", null, null],
|
|
11
|
-
["gpt-5.4", "GPT-5.4", 16, "medium", null, null],
|
|
12
|
-
["gpt-5.4-mini", "GPT-5.4-Mini", 23, "medium", null, null],
|
|
13
|
-
["gpt-5.3-codex-spark", "GPT-5.3-Codex-Spark", 26, "high", null, null],
|
|
14
|
-
];
|
|
15
|
-
|
|
16
|
-
// Reviewed static projection of the exact pinned Codex 0.146 catalog. Keeping
|
|
17
|
-
// it process-local prevents the client from issuing unsupported online model
|
|
18
|
-
// refreshes during every managed launch.
|
|
19
|
-
export function managedCodexCliCatalog(now = new Date()) {
|
|
20
|
-
return {
|
|
21
|
-
fetched_at: now.toISOString(),
|
|
22
|
-
client_version: "impel-managed-codex-0.146",
|
|
23
|
-
models: MODELS.map(([slug, displayName, priority, effort, toolMode, multiAgentVersion]) => ({
|
|
24
|
-
slug,
|
|
25
|
-
display_name: displayName,
|
|
26
|
-
description: `Available through the Impel gateway (${slug}).`,
|
|
27
|
-
default_reasoning_level: effort,
|
|
28
|
-
supported_reasoning_levels: LEVELS,
|
|
29
|
-
service_tiers: [],
|
|
30
|
-
shell_type: "shell_command",
|
|
31
|
-
visibility: "list",
|
|
32
|
-
supported_in_api: true,
|
|
33
|
-
priority,
|
|
34
|
-
base_instructions: "You are Codex, an AI coding agent. Follow repository instructions and verify your work.",
|
|
35
|
-
include_skills_usage_instructions: false,
|
|
36
|
-
supports_reasoning_summaries: true,
|
|
37
|
-
default_reasoning_summary: "none",
|
|
38
|
-
support_verbosity: true,
|
|
39
|
-
default_verbosity: "low",
|
|
40
|
-
apply_patch_tool_type: "freeform",
|
|
41
|
-
web_search_tool_type: "text_and_image",
|
|
42
|
-
truncation_policy: { mode: "tokens", limit: 10_000 },
|
|
43
|
-
context_window: slug === "gpt-5.3-codex-spark" ? 128_000 : 272_000,
|
|
44
|
-
max_context_window: slug === "gpt-5.4" ? 1_000_000 : (slug === "gpt-5.3-codex-spark" ? 128_000 : 272_000),
|
|
45
|
-
effective_context_window_percent: 95,
|
|
46
|
-
experimental_supported_tools: [],
|
|
47
|
-
input_modalities: ["text", "image"],
|
|
48
|
-
supports_parallel_tool_calls: true,
|
|
49
|
-
supports_image_detail_original: true,
|
|
50
|
-
supports_search_tool: true,
|
|
51
|
-
use_responses_lite: slug.startsWith("gpt-5.6-"),
|
|
52
|
-
tool_mode: toolMode,
|
|
53
|
-
multi_agent_version: multiAgentVersion,
|
|
54
|
-
})),
|
|
55
|
-
};
|
|
56
|
-
}
|