impel-cli 0.20.56 → 0.20.58
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 +58 -0
- package/package.json +1 -1
- package/src/apps.js +6 -104
- package/src/cli.js +13 -0
- package/src/cliProfiles.js +28 -6
- package/src/commands/apps.js +43 -0
- package/src/commands/converge.js +2 -0
- package/src/commands/doctor.js +41 -3
- package/src/commands/launch.js +16 -2
- package/src/commands/models.js +160 -0
- package/src/commands/provisionVendorClis.js +92 -0
- package/src/commands/sessions.js +56 -3
- package/src/commands/setup.js +2 -0
- package/src/commands/tasks.js +35 -3
- package/src/commands/token.js +11 -1
- package/src/extension/index.js +2 -0
- package/src/gatewayModels.js +58 -0
- package/src/macSetup.js +31 -9
- package/src/managedProfileVersion.js +6 -1
- package/src/modelCatalog.js +109 -1
- package/src/modelSync.js +197 -0
- package/src/modelsManifest.js +132 -0
- package/src/platformSetup.js +7 -4
- package/src/provisioning.js +3 -1
- package/src/runtimeBrand.js +1 -1
- package/src/updates.js +10 -0
- package/src/windowsSetup.js +30 -5
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,4 +1,62 @@
|
|
|
1
1
|
# Release notes
|
|
2
|
+
## 0.20.58 — Provision vendor CLIs on managed enrollment
|
|
3
|
+
|
|
4
|
+
- Ensures freshly enrolled managed devices receive the reviewed Claude Code and Codex CLI binaries without requiring a personal PAT or interactive vendor login.
|
|
5
|
+
- Keeps provisioning isolated from personal Claude/Codex profiles and reports missing vendor runtimes for follow-up convergence.
|
|
6
|
+
|
|
7
|
+
## 0.20.57 — Live tenant model catalogs for `impel codex` (`impel models sync`)
|
|
8
|
+
|
|
9
|
+
- Adds `impel models sync|list`: the isolated `impel codex` Codex picker now
|
|
10
|
+
follows the tenant's live gateway catalog (`GET /v1/models`) instead of
|
|
11
|
+
being frozen at CLI release time. `sync` rewrites the tenant `models.json`
|
|
12
|
+
atomically (with `--stale-only` TTL gating); `list` shows the picker
|
|
13
|
+
contents, whether they came from a gateway sync or the built-in offline
|
|
14
|
+
floor, and how stale they are.
|
|
15
|
+
- Launches never block on the network: `ensureImpelCodexProfile` stays
|
|
16
|
+
synchronous and keeps writing the built-in catalog as the offline floor,
|
|
17
|
+
while an async, never-throwing `syncCodexModelsSafe` layers the gateway
|
|
18
|
+
catalog after it — at launch, from the detached 6h stale-only app refresh,
|
|
19
|
+
from codex session hooks, and from the `impel token` heartbeat Codex calls
|
|
20
|
+
every ~5 minutes. A valid synced catalog survives relaunches
|
|
21
|
+
(stale-beats-broken); a failed sync keeps the last good file.
|
|
22
|
+
- Records catalog provenance in `models-manifest.json`
|
|
23
|
+
(`{schemaVersion, syncedAt, catalogVersion, digest}` plus tenant/gateway/
|
|
24
|
+
experiment bindings): tampered bytes or changed bindings fall back to the
|
|
25
|
+
floor, and `impel doctor` reports CLI catalog staleness per tenant.
|
|
26
|
+
- Cross-app models (hidden experiment) now reach the isolated CLI: with
|
|
27
|
+
`experimental.crossAppModels` enabled, `impel codex` routes through the
|
|
28
|
+
experimental OpenAI-compatible gateway path and projects tenant Claude
|
|
29
|
+
models into the Codex picker — fail-closed on each model's verified
|
|
30
|
+
`client_capabilities.codex`, exactly like the desktop app.
|
|
31
|
+
- Moves the Claude→Codex projection (`foreignClaudeCodexCatalogEntry`) and
|
|
32
|
+
the gateway catalog fetch (`fetchGatewayModels`) into leaf modules shared
|
|
33
|
+
by the CLI and desktop writers (re-exported from `src/apps.js`), so the
|
|
34
|
+
two surfaces can never fork the projection again. Gateway JSON is still
|
|
35
|
+
reconstructed field-by-field, never written through.
|
|
36
|
+
- Bumps `CURRENT_CONFIG_VERSION` to 45 so existing installs take the slow
|
|
37
|
+
rewrite path once.
|
|
38
|
+
- Release mechanics: the released-fixture corpus for 0.20.57 was re-captured
|
|
39
|
+
on the final release tree (after the projection-convergence merge added
|
|
40
|
+
`additional_speed_tiers` to the shared Codex projection) and is
|
|
41
|
+
byte-identical to the earlier capture — vendor model catalogs
|
|
42
|
+
(`models.json`), the only artifacts that projection change touches, are
|
|
43
|
+
excluded from the corpus by design (offline determinism), so no corpus
|
|
44
|
+
bytes moved.
|
|
45
|
+
- Skipped the green requirement on the `Pinned ChatGPT/Codex Store install`
|
|
46
|
+
vendor-contract job because the Microsoft Store now fulfills
|
|
47
|
+
`OpenAI.Codex 26.818.5229.0` while the reviewed pin tracks `26.818.2441.0`
|
|
48
|
+
(upstream Store drift; the pin bump is tracked as its own follow-up). The
|
|
49
|
+
`Pinned Claude MSIX install and replacement` job is required green on the
|
|
50
|
+
release SHA as usual; the release gate is bypassed only via the documented
|
|
51
|
+
`workflow_dispatch` + `skip_release_gate` escape hatch with a written
|
|
52
|
+
reason naming that drift, after manually verifying every other required
|
|
53
|
+
check green on the tagged SHA.
|
|
54
|
+
- Skipped the prerelease soak (testing-protocol §4.2): 0.20.57 ships stable
|
|
55
|
+
without a `next`-channel soak.
|
|
56
|
+
- Local `pnpm test` on the release machine shows two known environmental
|
|
57
|
+
failures (launch.test.js / managed-apps.test.js, caused by this machine's
|
|
58
|
+
MSC-enrolled managed apps); they reproduce on clean `main` and all six CI
|
|
59
|
+
`Node {18,24}` legs run the same suite green.
|
|
2
60
|
|
|
3
61
|
## 0.20.56 — Enforce reviewed Windows vendor CLIs, follow the Store pin, harden convergence
|
|
4
62
|
|
package/package.json
CHANGED
package/src/apps.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
secureManagedCodexHome,
|
|
12
12
|
} from "./codexSecurity.js";
|
|
13
13
|
import { normalizeTenantId } from "./tenants.js";
|
|
14
|
-
import { codexCatalogEntry } from "./modelCatalog.js";
|
|
14
|
+
import { codexCatalogEntry, foreignClaudeCodexCatalogEntry } from "./modelCatalog.js";
|
|
15
15
|
import {
|
|
16
16
|
IMPEL_CLI_ENTRYPOINT,
|
|
17
17
|
IMPEL_TASKS_MCP_SERVER_NAME,
|
|
@@ -234,7 +234,11 @@ const LEGACY_TRAPPED_CLAUDE_NAME = new RegExp(`^${escapedDisplayPrefix} Claude(
|
|
|
234
234
|
// modelCatalog.js (a leaf module) so cliProfiles.js can consume them
|
|
235
235
|
// without importing this module's launch-command cycle. Re-exported here
|
|
236
236
|
// for the many existing consumers.
|
|
237
|
-
|
|
237
|
+
// Re-exported from their leaf homes so existing importers keep working;
|
|
238
|
+
// modelCatalog.js and gatewayModels.js must never import from apps.js (the
|
|
239
|
+
// leaf constraint behind the 0.20.39 fork regression).
|
|
240
|
+
export { FALLBACK_MODELS, foreignClaudeCodexCatalogEntry, managedCodexCliCatalog } from "./modelCatalog.js";
|
|
241
|
+
export { fetchGatewayModels } from "./gatewayModels.js";
|
|
238
242
|
|
|
239
243
|
export function normalizeAppTarget(value) {
|
|
240
244
|
if (value == null || value === "all") return ["claude", "chatgpt"];
|
|
@@ -841,33 +845,6 @@ export function migrateLegacyClaudeAppSessions(userData, homeDir = os.homedir())
|
|
|
841
845
|
return copied;
|
|
842
846
|
}
|
|
843
847
|
|
|
844
|
-
export async function fetchGatewayModels(config, fetchImpl = fetch, { allowEmpty = false } = {}) {
|
|
845
|
-
const controller = new AbortController();
|
|
846
|
-
const timeout = setTimeout(() => controller.abort(), 20000);
|
|
847
|
-
try {
|
|
848
|
-
const response = await fetchImpl(`${config.gatewayUrl}/v1/models`, {
|
|
849
|
-
headers: { authorization: `Bearer ${config.pat}` },
|
|
850
|
-
signal: controller.signal,
|
|
851
|
-
});
|
|
852
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
853
|
-
const payload = await response.json();
|
|
854
|
-
if (config.tenantId && payload?.org_id !== config.tenantId) {
|
|
855
|
-
throw new Error("gateway model catalog tenant did not match the selected tenant");
|
|
856
|
-
}
|
|
857
|
-
if (config.productAccess && payload?.product_access !== config.productAccess) {
|
|
858
|
-
throw new Error("gateway model catalog product access did not match the live entitlement");
|
|
859
|
-
}
|
|
860
|
-
if (!Array.isArray(payload?.data)) throw new Error("response has no model data array");
|
|
861
|
-
const models = payload.data.filter(isGatewayModel);
|
|
862
|
-
if (models.length === 0 && !(allowEmpty && isExplicitNoSeatCatalog(payload))) {
|
|
863
|
-
throw new Error("gateway returned no supported models");
|
|
864
|
-
}
|
|
865
|
-
return { models, source: "gateway", version: payload.version ?? null };
|
|
866
|
-
} finally {
|
|
867
|
-
clearTimeout(timeout);
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
|
|
871
848
|
export function installManagedAppFiles({
|
|
872
849
|
config,
|
|
873
850
|
targets,
|
|
@@ -1555,66 +1532,6 @@ export function projectClaudeDesktopModels(models) {
|
|
|
1555
1532
|
});
|
|
1556
1533
|
}
|
|
1557
1534
|
|
|
1558
|
-
export function foreignClaudeCodexCatalogEntry(model, index) {
|
|
1559
|
-
const capability = model.client_capabilities?.codex;
|
|
1560
|
-
const levels = capability?.supported_reasoning_levels;
|
|
1561
|
-
const tiers = capability?.service_tiers;
|
|
1562
|
-
const modalities = capability?.input_modalities;
|
|
1563
|
-
const valid = capability && typeof capability === "object"
|
|
1564
|
-
&& Number.isInteger(capability.context_window) && capability.context_window > 0
|
|
1565
|
-
&& Number.isInteger(capability.max_context_window) && capability.max_context_window >= capability.context_window
|
|
1566
|
-
&& Array.isArray(levels) && levels.every((level) => (
|
|
1567
|
-
level && typeof level.effort === "string" && typeof level.description === "string"
|
|
1568
|
-
))
|
|
1569
|
-
&& Array.isArray(tiers) && tiers.every((tier) => (
|
|
1570
|
-
tier && typeof tier.id === "string" && typeof tier.name === "string" && typeof tier.description === "string"
|
|
1571
|
-
))
|
|
1572
|
-
&& Array.isArray(modalities) && modalities.every((value) => typeof value === "string")
|
|
1573
|
-
&& ["supports_parallel_tool_calls", "supports_reasoning_summaries", "supports_verbosity", "supports_search_tool", "supports_image_detail_original"]
|
|
1574
|
-
.every((key) => typeof capability[key] === "boolean")
|
|
1575
|
-
&& (capability.default_reasoning_level == null
|
|
1576
|
-
|| capability.default_reasoning_level === ""
|
|
1577
|
-
|| levels.some((level) => level.effort === capability.default_reasoning_level))
|
|
1578
|
-
&& (capability.apply_patch_tool_type == null || typeof capability.apply_patch_tool_type === "string")
|
|
1579
|
-
&& (capability.web_search_tool_type == null || typeof capability.web_search_tool_type === "string");
|
|
1580
|
-
if (!valid) {
|
|
1581
|
-
console.warn(`impel: omitting model ${redactSecretText(model.id)} from Impel ChatGPT: missing verified Codex capabilities`);
|
|
1582
|
-
return null;
|
|
1583
|
-
}
|
|
1584
|
-
return {
|
|
1585
|
-
slug: model.id,
|
|
1586
|
-
display_name: model.display_name || model.id,
|
|
1587
|
-
description: model.description || `Claude model available through the Impel gateway (${model.id}).`,
|
|
1588
|
-
...(capability.default_reasoning_level ? { default_reasoning_level: capability.default_reasoning_level } : {}),
|
|
1589
|
-
supported_reasoning_levels: levels,
|
|
1590
|
-
service_tiers: tiers,
|
|
1591
|
-
shell_type: "shell_command",
|
|
1592
|
-
visibility: "list",
|
|
1593
|
-
supported_in_api: true,
|
|
1594
|
-
priority: Number.isInteger(model.priority) ? model.priority : index + 1,
|
|
1595
|
-
base_instructions: "You are an AI coding agent. Follow repository instructions, collaborate with the user, make scoped changes, and verify your work.",
|
|
1596
|
-
include_skills_usage_instructions: false,
|
|
1597
|
-
supports_reasoning_summaries: capability.supports_reasoning_summaries,
|
|
1598
|
-
support_verbosity: capability.supports_verbosity,
|
|
1599
|
-
...(capability.supports_reasoning_summaries ? { default_reasoning_summary: "none" } : {}),
|
|
1600
|
-
...(capability.supports_verbosity ? { default_verbosity: "low" } : {}),
|
|
1601
|
-
...(capability.apply_patch_tool_type ? { apply_patch_tool_type: capability.apply_patch_tool_type } : {}),
|
|
1602
|
-
...(capability.web_search_tool_type ? { web_search_tool_type: capability.web_search_tool_type } : {}),
|
|
1603
|
-
truncation_policy: { mode: "tokens", limit: 10000 },
|
|
1604
|
-
context_window: capability.context_window,
|
|
1605
|
-
max_context_window: capability.max_context_window,
|
|
1606
|
-
effective_context_window_percent: 95,
|
|
1607
|
-
experimental_supported_tools: [],
|
|
1608
|
-
input_modalities: modalities,
|
|
1609
|
-
supports_parallel_tool_calls: capability.supports_parallel_tool_calls,
|
|
1610
|
-
supports_image_detail_original: capability.supports_image_detail_original,
|
|
1611
|
-
supports_search_tool: capability.supports_search_tool,
|
|
1612
|
-
use_responses_lite: false,
|
|
1613
|
-
tool_mode: null,
|
|
1614
|
-
multi_agent_version: null,
|
|
1615
|
-
};
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
1535
|
function readVendorCodexModels(vendorPath) {
|
|
1619
1536
|
const binary = vendorPath && path.join(vendorPath, "Contents", "Resources", "codex");
|
|
1620
1537
|
if (!binary || !fs.existsSync(binary)) return new Map();
|
|
@@ -3354,21 +3271,6 @@ export function replaceDirectory(destination, staging, options = {}) {
|
|
|
3354
3271
|
));
|
|
3355
3272
|
}
|
|
3356
3273
|
|
|
3357
|
-
function isGatewayModel(model) {
|
|
3358
|
-
return model && typeof model.id === "string" && (model.provider === "claude" || model.provider === "codex");
|
|
3359
|
-
}
|
|
3360
|
-
|
|
3361
|
-
function isExplicitNoSeatCatalog(payload) {
|
|
3362
|
-
const statuses = payload?.provider_status;
|
|
3363
|
-
return Boolean(statuses
|
|
3364
|
-
&& typeof statuses === "object"
|
|
3365
|
-
&& !Array.isArray(statuses)
|
|
3366
|
-
&& ["claude", "codex"].every((provider) => (
|
|
3367
|
-
statuses[provider]?.state === "no_seat"
|
|
3368
|
-
&& statuses[provider]?.routable === false
|
|
3369
|
-
)));
|
|
3370
|
-
}
|
|
3371
|
-
|
|
3372
3274
|
function writeAtomic(target, contents, mode) {
|
|
3373
3275
|
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
3374
3276
|
if (fs.existsSync(target) && fs.readFileSync(target, "utf8") === contents) {
|
package/src/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import { cmdApps } from "./commands/apps.js";
|
|
|
11
11
|
import { cmdMcp } from "./commands/mcp.js";
|
|
12
12
|
import { cmdLaunch } from "./commands/launch.js";
|
|
13
13
|
import { cmdSkills } from "./commands/skills.js";
|
|
14
|
+
import { cmdModels } from "./commands/models.js";
|
|
14
15
|
import { cmdAgents } from "./commands/agents.js";
|
|
15
16
|
import { cmdTenant } from "./commands/tenant.js";
|
|
16
17
|
import { cmdDoctor } from "./commands/doctor.js";
|
|
@@ -21,6 +22,7 @@ import { cmdUpdate } from "./commands/update.js";
|
|
|
21
22
|
import { cmdNuke } from "./commands/nuke.js";
|
|
22
23
|
import { cmdExperimental } from "./commands/experimental.js";
|
|
23
24
|
import { cmdConverge } from "./commands/converge.js";
|
|
25
|
+
import { cmdProvisionVendorClis } from "./commands/provisionVendorClis.js";
|
|
24
26
|
import { cmdRemote } from "./commands/remote.js";
|
|
25
27
|
import { cmdNative } from "./commands/native.js";
|
|
26
28
|
import { ensureMacDeveloperTools } from "./macDeveloperTools.js";
|
|
@@ -68,6 +70,7 @@ Account:
|
|
|
68
70
|
Diagnostics:
|
|
69
71
|
impel status Authentication, current tenant, and local readiness
|
|
70
72
|
impel doctor [--tenant <org>|--all-tenants] Synthetic provider, routing, and latency checks
|
|
73
|
+
impel models list|sync Inspect or refresh the isolated Codex model catalog
|
|
71
74
|
impel report --message "<text>" Send a bug report; you approve the exact payload
|
|
72
75
|
|
|
73
76
|
Reset:
|
|
@@ -183,6 +186,10 @@ async function dispatch(cmd, rest) {
|
|
|
183
186
|
case "skill":
|
|
184
187
|
return cmdSkills(rest);
|
|
185
188
|
|
|
189
|
+
case "models":
|
|
190
|
+
case "model":
|
|
191
|
+
return cmdModels(rest);
|
|
192
|
+
|
|
186
193
|
case "agents":
|
|
187
194
|
case "agent":
|
|
188
195
|
return cmdAgents(rest);
|
|
@@ -219,6 +226,12 @@ async function dispatch(cmd, rest) {
|
|
|
219
226
|
case "_converge":
|
|
220
227
|
return cmdConverge(rest);
|
|
221
228
|
|
|
229
|
+
// Hidden, unauthenticated vendor-CLI provisioning, called by the managed
|
|
230
|
+
// enrollment bootstrap. A managed device has no PAT, so it cannot reach
|
|
231
|
+
// `impel setup` — and without this nothing ever installs Claude Code.
|
|
232
|
+
case "_provision-vendor-clis":
|
|
233
|
+
return cmdProvisionVendorClis(rest);
|
|
234
|
+
|
|
222
235
|
// Hidden OS-shell launcher. Setup/update guarantees the app is ready.
|
|
223
236
|
case "_app-launch": {
|
|
224
237
|
const [target = "all", ...args] = rest;
|
package/src/cliProfiles.js
CHANGED
|
@@ -22,6 +22,7 @@ import { renameWithWindowsRetry } from "./windowsFs.js";
|
|
|
22
22
|
import { ensureClaudeSessionHooks, ensureCodexSessionHooks } from "./sessionHooks.js";
|
|
23
23
|
import { RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
24
24
|
import { managedCodexCliCatalog } from "./modelCatalog.js";
|
|
25
|
+
import { codexModelsManifestPath, syncedCodexCatalogMatches } from "./modelsManifest.js";
|
|
25
26
|
|
|
26
27
|
export const IMPEL_CLI_PROFILES_DIR = path.join(CONFIG_DIR, "cli");
|
|
27
28
|
export const CODEX_GATEWAY_TOKEN_ENV = `${RUNTIME_BRAND.cli.providerId
|
|
@@ -200,14 +201,19 @@ function splitTomlPreamble(text) {
|
|
|
200
201
|
return { preamble: text.slice(0, table.index), rest: text.slice(table.index) };
|
|
201
202
|
}
|
|
202
203
|
|
|
203
|
-
function codexManagedBlock(gatewayUrl, tenantId) {
|
|
204
|
+
function codexManagedBlock(gatewayUrl, tenantId, { crossAppModels = false } = {}) {
|
|
204
205
|
const providerId = RUNTIME_BRAND.cli.providerId;
|
|
205
206
|
const lines = [
|
|
206
207
|
CODEX_START_MARK,
|
|
207
208
|
`# Generated for \`${RUNTIME_BRAND.cli.command} codex\`. Other profile settings outside this block are preserved.`,
|
|
208
209
|
`[model_providers.${providerId}]`,
|
|
209
210
|
`name = ${JSON.stringify(`${RUNTIME_BRAND.product.displayName} Gateway`)}`,
|
|
210
|
-
|
|
211
|
+
// The experimental OpenAI-compatible route can dispatch both provider
|
|
212
|
+
// families (the desktop ChatGPT profile's cross-app path); the default
|
|
213
|
+
// route is the byte-preserving Codex passthrough.
|
|
214
|
+
`base_url = ${JSON.stringify(crossAppModels
|
|
215
|
+
? `${gatewayUrl}/experimental/openai/v1`
|
|
216
|
+
: impelCodexBaseUrl(gatewayUrl))}`,
|
|
211
217
|
'wire_api = "responses"',
|
|
212
218
|
`env_key = ${JSON.stringify(CODEX_GATEWAY_TOKEN_ENV)}`,
|
|
213
219
|
];
|
|
@@ -233,7 +239,7 @@ function codexManagedBlock(gatewayUrl, tenantId) {
|
|
|
233
239
|
return lines.join("\n");
|
|
234
240
|
}
|
|
235
241
|
|
|
236
|
-
export function ensureImpelCodexProfile(gatewayUrl, tenantId) {
|
|
242
|
+
export function ensureImpelCodexProfile(gatewayUrl, tenantId, { crossAppModels = false } = {}) {
|
|
237
243
|
secureAllManagedCodexHomes({ cliRoot: IMPEL_CLI_PROFILES_DIR });
|
|
238
244
|
const { codexHome, codexCatalog } = tenantCliProfilePaths(tenantId);
|
|
239
245
|
secureManagedCodexHome(codexHome);
|
|
@@ -269,14 +275,30 @@ export function ensureImpelCodexProfile(gatewayUrl, tenantId) {
|
|
|
269
275
|
: `${nextPreamble.trimEnd()}\n${catalogLine}\n`;
|
|
270
276
|
nextPreamble = nextPreamble.replace(/^bypass_hook_trust[ \t]*=[ \t]*(?:true|false)[ \t]*\n?/gm, "");
|
|
271
277
|
const restText = rest.trim();
|
|
272
|
-
const next = [nextPreamble.trimEnd(), codexManagedBlock(gatewayUrl, tenantId), restText]
|
|
278
|
+
const next = [nextPreamble.trimEnd(), codexManagedBlock(gatewayUrl, tenantId, { crossAppModels }), restText]
|
|
273
279
|
.filter(Boolean)
|
|
274
280
|
.join("\n\n")
|
|
275
281
|
.concat("\n");
|
|
276
282
|
|
|
277
283
|
writePrivateFile(configPath, hardenManagedCodexToml(next, configPath));
|
|
278
|
-
|
|
284
|
+
// Launch layering (D13), synchronous half: the FALLBACK_MODELS projection
|
|
285
|
+
// is the offline floor. A catalog that models-manifest.json proves is
|
|
286
|
+
// exactly what the last gateway sync wrote for THESE bindings (tenant,
|
|
287
|
+
// gateway, cross-app state) is kept even when its TTL has lapsed —
|
|
288
|
+
// stale-beats-broken — and syncCodexModelsSafe refreshes it afterwards
|
|
289
|
+
// without ever blocking this writer on the network. Anything else (first
|
|
290
|
+
// run, tampered bytes, changed bindings) is reset to the floor, and the
|
|
291
|
+
// stale manifest is removed so the TTL machinery sees the floor as stale.
|
|
292
|
+
const keepSyncedCatalog = syncedCodexCatalogMatches(codexHome, codexCatalog, {
|
|
293
|
+
tenantId: normalizeTenantId(tenantId),
|
|
294
|
+
gatewayUrl,
|
|
295
|
+
crossAppModels,
|
|
296
|
+
});
|
|
297
|
+
if (!keepSyncedCatalog) {
|
|
298
|
+
writePrivateFile(codexCatalog, `${JSON.stringify(managedCodexCliCatalog(), null, 2)}\n`);
|
|
299
|
+
fs.rmSync(codexModelsManifestPath(codexHome), { force: true });
|
|
300
|
+
}
|
|
279
301
|
if (RUNTIME_BRAND.features.sessions) ensureCodexSessionHooks(codexHome, tenantId, "codex_cli");
|
|
280
302
|
secureManagedCodexHome(codexHome);
|
|
281
|
-
return { codexHome, configPath, catalogPath: codexCatalog };
|
|
303
|
+
return { codexHome, configPath, catalogPath: codexCatalog, syncedCatalog: keepSyncedCatalog };
|
|
282
304
|
}
|
package/src/commands/apps.js
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
import { resolveSkillCredential } from "../skillBundle.js";
|
|
46
46
|
import { resolveSkillsGateway, syncSkillsSafe } from "../skills.js";
|
|
47
47
|
import { syncAgentProfilesSafe } from "../agents.js";
|
|
48
|
+
import { syncCodexModelsSafe } from "../modelSync.js";
|
|
48
49
|
import { secureManagedCodexHome } from "../codexSecurity.js";
|
|
49
50
|
import { impelCliInvocation } from "../selfInvocation.js";
|
|
50
51
|
import { createProgressLogger, withProgress } from "../progress.js";
|
|
@@ -749,6 +750,28 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
749
750
|
|
|
750
751
|
try {
|
|
751
752
|
if (action === "open" && RUNTIME_BRAND.cli.packageName === "impel-cli") maybePrintUpdateNotice();
|
|
753
|
+
// Windows mirror of refreshApps' CLI-catalog leg: the detached
|
|
754
|
+
// `app refresh --stale-only` slot also converges the isolated
|
|
755
|
+
// `impel codex` model catalog (D13). Runs before the desktop manifest
|
|
756
|
+
// fast-path below because the CLI catalog has its own TTL manifest.
|
|
757
|
+
if (action === "refresh") {
|
|
758
|
+
const stored = loadConfig();
|
|
759
|
+
if (stored?.pat) {
|
|
760
|
+
const cliTenantId = flags.tenant
|
|
761
|
+
? normalizeTenantId(flags.tenant)
|
|
762
|
+
: stored.tenantId || null;
|
|
763
|
+
if (cliTenantId) {
|
|
764
|
+
await (overrides.syncCliModels || syncCodexModelsSafe)({
|
|
765
|
+
gatewayUrl: normalizeGatewayUrl(stored.gatewayUrl || resolveDefaultGateway()),
|
|
766
|
+
credential: tenantCredential(stored.pat, cliTenantId),
|
|
767
|
+
tenantId: cliTenantId,
|
|
768
|
+
crossAppModels: crossAppModelsEnabled(stored),
|
|
769
|
+
staleOnly: Boolean(flags["stale-only"]),
|
|
770
|
+
logger: { warn: () => {} },
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
752
775
|
// Background token-helper refreshes pass --stale-only; honor the manifest
|
|
753
776
|
// TTL here like the darwin refresh path does, so every vendor token call
|
|
754
777
|
// does not become a full catalog fetch + profile rewrite (and its child
|
|
@@ -1355,11 +1378,31 @@ async function refreshApps(targets, { staleOnly = false, tenantId = null } = {},
|
|
|
1355
1378
|
installFiles: installManagedAppFiles,
|
|
1356
1379
|
syncSkills: syncSkillsSafe,
|
|
1357
1380
|
syncAgents: syncAgentProfilesSafe,
|
|
1381
|
+
syncCliModels: syncCodexModelsSafe,
|
|
1358
1382
|
secureCodexHome: secureManagedCodexHome,
|
|
1359
1383
|
...overrides,
|
|
1360
1384
|
};
|
|
1361
1385
|
const stored = io.loadStoredConfig();
|
|
1362
1386
|
if (!stored?.pat) return;
|
|
1387
|
+
// The isolated `impel codex` catalog rides the same detached refresh slot
|
|
1388
|
+
// (D13). Independent of the desktop legs below: a CLI-only machine has no
|
|
1389
|
+
// installed app bundles yet still needs its Codex picker converging.
|
|
1390
|
+
// syncCodexModelsSafe skips uninitialized profiles, honors the 6h manifest
|
|
1391
|
+
// TTL under --stale-only, never throws, and keeps the last good catalog on
|
|
1392
|
+
// failure.
|
|
1393
|
+
{
|
|
1394
|
+
const cliTenantId = tenantId ? normalizeTenantId(tenantId) : stored.tenantId || null;
|
|
1395
|
+
if (cliTenantId) {
|
|
1396
|
+
await io.syncCliModels({
|
|
1397
|
+
gatewayUrl: normalizeGatewayUrl(stored.gatewayUrl || resolveDefaultGateway()),
|
|
1398
|
+
credential: tenantCredential(stored.pat, cliTenantId),
|
|
1399
|
+
tenantId: cliTenantId,
|
|
1400
|
+
crossAppModels: crossAppModelsEnabled(stored),
|
|
1401
|
+
staleOnly,
|
|
1402
|
+
logger: { warn: () => {} },
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1363
1406
|
const requestedTenantId = tenantId
|
|
1364
1407
|
? normalizeTenantId(tenantId)
|
|
1365
1408
|
: stored.tenantId || null;
|
package/src/commands/converge.js
CHANGED
|
@@ -173,6 +173,7 @@ export async function cmdConverge(argv = [], overrides = {}) {
|
|
|
173
173
|
credential: resolveSkillCredential({ pat: config.pat, tenantId: selected.id }),
|
|
174
174
|
platform: io.platform,
|
|
175
175
|
skipInstall: true,
|
|
176
|
+
crossAppModels: config.experimental?.crossAppModels === true,
|
|
176
177
|
});
|
|
177
178
|
let prepared = inspected;
|
|
178
179
|
if (inspected.missingAfter.length && !inspectOnly && !skipClis) {
|
|
@@ -206,6 +207,7 @@ export async function cmdConverge(argv = [], overrides = {}) {
|
|
|
206
207
|
platform: io.platform,
|
|
207
208
|
skipInstall: false,
|
|
208
209
|
installTools,
|
|
210
|
+
crossAppModels: config.experimental?.crossAppModels === true,
|
|
209
211
|
});
|
|
210
212
|
}
|
|
211
213
|
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { parseFlags } from "../args.js";
|
|
2
|
-
import { loadConfig, normalizeGatewayUrl, resolveDefaultGateway } from "../config.js";
|
|
2
|
+
import { crossAppModelsEnabled, loadConfig, normalizeGatewayUrl, resolveDefaultGateway } from "../config.js";
|
|
3
3
|
import {
|
|
4
4
|
DEFAULT_TTFT_BUDGET_MS,
|
|
5
5
|
DOCTOR_PROVIDERS,
|
|
6
6
|
probeTenant,
|
|
7
7
|
recordSucceeded,
|
|
8
8
|
} from "../doctor.js";
|
|
9
|
+
import { codexCatalogStatus } from "../modelSync.js";
|
|
9
10
|
import {
|
|
10
11
|
assertProviderScopes,
|
|
11
12
|
fetchTenants,
|
|
@@ -85,12 +86,43 @@ function doctorGatewayUrl(value) {
|
|
|
85
86
|
return parsed.origin;
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Local, offline staleness of the isolated `impel codex` model catalog
|
|
91
|
+
* (models.json + models-manifest.json). Reported beside the live gateway
|
|
92
|
+
* catalog probe so a device stuck on the offline floor or a months-old sync
|
|
93
|
+
* is visible from the same diagnosis surface that proves the gateway works.
|
|
94
|
+
*/
|
|
95
|
+
function cliModelCatalogReport(tenantId, { gatewayUrl, crossAppModels }) {
|
|
96
|
+
try {
|
|
97
|
+
const status = codexCatalogStatus(tenantId, { gatewayUrl, crossAppModels });
|
|
98
|
+
return {
|
|
99
|
+
state: status.catalogError ?? (status.source === "floor" ? "floor" : "synced"),
|
|
100
|
+
syncedAt: status.syncedAt,
|
|
101
|
+
catalogVersion: status.manifest?.catalogVersion ?? null,
|
|
102
|
+
fresh: status.fresh,
|
|
103
|
+
models: status.models.length,
|
|
104
|
+
};
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return { state: "error", error: String(error?.message || error), fresh: false, models: 0 };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function cliModelCatalogLine(local) {
|
|
111
|
+
if (local.state === "missing") return "not initialized (run `impel codex` once)";
|
|
112
|
+
if (local.state === "floor") return "offline floor (never synced; run `impel models sync`)";
|
|
113
|
+
if (local.state === "synced") {
|
|
114
|
+
return `gateway sync ${local.syncedAt ?? "unknown"} (${local.fresh ? "fresh" : "stale"}, ${local.models} models)`;
|
|
115
|
+
}
|
|
116
|
+
return `${local.state}${local.error ? ` (${local.error})` : ""}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
88
119
|
function printHuman(report) {
|
|
89
120
|
for (const tenant of report.tenants) {
|
|
90
121
|
console.log(`Tenant: ${tenant.tenantId}`);
|
|
91
122
|
console.log(`Access: ${productAccessLabel(tenant.productAccess)}`);
|
|
92
123
|
console.log(`Catalog: ${tenant.catalog.status ?? "network error"} (${tenant.catalog.models} ready models, ${tenant.catalog.durationMs ?? "?"}ms)`);
|
|
93
124
|
if (tenant.catalog.error) console.log(` ERROR ${tenant.catalog.error}`);
|
|
125
|
+
if (tenant.cliModelCatalog) console.log(`CLI models: ${cliModelCatalogLine(tenant.cliModelCatalog)}`);
|
|
94
126
|
for (const record of tenant.records) {
|
|
95
127
|
const ok = recordSucceeded(record);
|
|
96
128
|
const latency = record.ttftMs === null ? "TTFT n/a" : `TTFT ${record.ttftMs}ms, total ${record.totalMs}ms`;
|
|
@@ -194,7 +226,7 @@ export async function cmdDoctor(argv) {
|
|
|
194
226
|
|
|
195
227
|
const tenantReports = [];
|
|
196
228
|
for (const tenant of tenants) {
|
|
197
|
-
|
|
229
|
+
const tenantReport = await probeTenant({
|
|
198
230
|
config: doctorConfig,
|
|
199
231
|
tenantId: tenant.id,
|
|
200
232
|
productAccess: listing.productAccess,
|
|
@@ -203,7 +235,13 @@ export async function cmdDoctor(argv) {
|
|
|
203
235
|
timeoutMs,
|
|
204
236
|
ttftBudgets,
|
|
205
237
|
strictLatency: Boolean(flags["strict-latency"]),
|
|
206
|
-
})
|
|
238
|
+
});
|
|
239
|
+
// Offline and advisory: catalog staleness never flips the probe verdict.
|
|
240
|
+
tenantReport.cliModelCatalog = cliModelCatalogReport(tenant.id, {
|
|
241
|
+
gatewayUrl,
|
|
242
|
+
crossAppModels: crossAppModelsEnabled(config),
|
|
243
|
+
});
|
|
244
|
+
tenantReports.push(tenantReport);
|
|
207
245
|
}
|
|
208
246
|
const report = {
|
|
209
247
|
generatedAt: new Date().toISOString(),
|
package/src/commands/launch.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
saveConfig,
|
|
21
21
|
} from "../config.js";
|
|
22
22
|
import { impelClaudeBaseUrl, impelCrossAppClaudeBaseUrl } from "../claudeSetup.js";
|
|
23
|
+
import { syncCodexModelsSafe } from "../modelSync.js";
|
|
23
24
|
import { parentVerbatimRelayAppendix } from "../verbatimRelay.js";
|
|
24
25
|
import { withGitEnvironment } from "../skills.js";
|
|
25
26
|
import { assertProviderScopes, ensureTenantSelection, tenantCredential } from "../tenants.js";
|
|
@@ -501,7 +502,7 @@ export async function cmdLaunch(tool, argv) {
|
|
|
501
502
|
if (RUNTIME_BRAND.cli.packageName === "impel-cli") maybePrintUpdateNotice();
|
|
502
503
|
|
|
503
504
|
const gatewayUrl = normalizeGatewayUrl(config.gatewayUrl || resolveDefaultGateway());
|
|
504
|
-
const crossAppModels =
|
|
505
|
+
const crossAppModels = crossAppModelsEnabled(config);
|
|
505
506
|
let tenantId;
|
|
506
507
|
try {
|
|
507
508
|
// PAT scopes are immutable. Refresh legacy configs once, then use the
|
|
@@ -565,7 +566,7 @@ export async function cmdLaunch(tool, argv) {
|
|
|
565
566
|
? `${environment.ANTHROPIC_CUSTOM_HEADERS}\n${versionHeader}`
|
|
566
567
|
: versionHeader;
|
|
567
568
|
} else if (tool === "codex") {
|
|
568
|
-
const profile = ensureImpelCodexProfile(gatewayUrl, tenantId);
|
|
569
|
+
const profile = ensureImpelCodexProfile(gatewayUrl, tenantId, { crossAppModels });
|
|
569
570
|
agentProfile = {
|
|
570
571
|
client: "codex",
|
|
571
572
|
root: profile.codexHome,
|
|
@@ -575,6 +576,19 @@ export async function cmdLaunch(tool, argv) {
|
|
|
575
576
|
deleteEnvironmentKeys(environment, CODEX_DIRECT_AUTH_ENV);
|
|
576
577
|
environment.CODEX_HOME = profile.codexHome;
|
|
577
578
|
environment[CODEX_GATEWAY_TOKEN_ENV] = gatewayCredential;
|
|
579
|
+
// Launch layering (D13): the synchronous writer above guaranteed a valid
|
|
580
|
+
// catalog (offline floor, or the intact last sync). Refresh it from the
|
|
581
|
+
// gateway at most every six hours; a network/catalog failure keeps the
|
|
582
|
+
// last good file and never blocks this launch. Codex reads
|
|
583
|
+
// model_catalog_json at startup, so a fresh sync lands next launch.
|
|
584
|
+
await syncCodexModelsSafe({
|
|
585
|
+
gatewayUrl,
|
|
586
|
+
credential: gatewayCredential,
|
|
587
|
+
tenantId,
|
|
588
|
+
crossAppModels,
|
|
589
|
+
staleOnly: true,
|
|
590
|
+
logger: agentSyncLogger(nativeArgv),
|
|
591
|
+
});
|
|
578
592
|
} else {
|
|
579
593
|
throw new Error(`unsupported CLI launcher: ${tool}`);
|
|
580
594
|
}
|