impel-cli 0.18.14-beta.0 → 0.18.14-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -286,16 +286,10 @@ compatibility for one release, but no command can enable native routing.
286
286
  - Run `impel.cmd setup` or `impel.cmd update` from PowerShell so restricted
287
287
  script policy cannot select the npm-generated `impel.ps1` shim. Impel does
288
288
  not modify persistent PowerShell execution policy.
289
- - Claude Chat and Code remain available through the per-user managed profile.
290
- Cowork is enabled only when a read-only check confirms Virtual Machine
291
- Platform, the HCS services and `vfpext` driver, nested virtualization when
292
- applicable, and machine-wide provisioning of the exact pinned Claude MSIX.
293
- - When those Cowork prerequisites are missing or cannot be read, Impel hides
294
- the Cowork tab and reports the reason and remediation in setup, update, and
295
- status. Impel never enables Windows features, restarts Windows, provisions a
296
- machine-wide package, or elevates itself.
297
- - Machine-wide deployment requirements are documented by Anthropic in
298
- [Deploy Claude Desktop for Windows](https://support.claude.com/en/articles/12622703-deploy-claude-desktop-for-windows).
289
+ - Claude Chat, Code, and Cowork remain enabled through the per-user managed
290
+ profile. Impel does not infer Cowork runtime support during setup or update.
291
+ - If Cowork reports a problem, open Claude Desktop's **Help > Troubleshooting >
292
+ Show Logs** and use the vendor diagnostics for that exact runtime failure.
299
293
  - Claude Desktop is downloaded from Anthropic as the exact x64 or arm64 MSIX
300
294
  pinned by this CLI. Its SHA-256, Authenticode signature, package identity,
301
295
  version, architecture, and required layout are checked before installation.
@@ -379,6 +373,15 @@ run a real package manager, or modify native personal profiles.
379
373
  CI runs on Ubuntu, macOS, and Windows with supported Node versions. See
380
374
  [RELEASING.md](RELEASING.md) for the npm release process.
381
375
 
376
+ The pinned macOS CLI installer contract follows the official Claude and Codex
377
+ installer redirects with the production downloader, then verifies their
378
+ reviewed final origins, size, shell-script marker, and SHA-256 without executing
379
+ either script or touching a local profile. Run it explicitly with:
380
+
381
+ ```sh
382
+ IMPEL_VALIDATE_PINNED_MAC_CLI_INSTALLERS=1 node --test test/mac-installer-contract.test.js
383
+ ```
384
+
382
385
  The separate Windows vendor contract runs only on a disposable GitHub-hosted
383
386
  Windows VM. It installs the exact newer Claude fixture that preceded the
384
387
  current compatibility pin, invokes the production downloader and installer to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.18.14-beta.0",
3
+ "version": "0.18.14-beta.1",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
package/src/apps.js CHANGED
@@ -57,7 +57,6 @@ export const PINNED_VENDOR_APPS = Object.freeze({
57
57
  packageName: "Claude",
58
58
  packageVersion: "1.24012.9.0",
59
59
  publisher: 'CN="Anthropic, PBC", O="Anthropic, PBC", L=San Francisco, S=California, C=US, SERIALNUMBER=4860621, OID.2.5.4.15=Private Organization, OID.1.3.6.1.4.1.311.60.2.1.2=Delaware, OID.1.3.6.1.4.1.311.60.2.1.3=US',
60
- publisherId: "pzs8sxrjxfjjc",
61
60
  executable: "app\\Claude.exe",
62
61
  downloads: Object.freeze({
63
62
  arm64: Object.freeze({
@@ -235,7 +234,9 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
235
234
  // enterprise machines do not enter the administrator-only setup flow.
236
235
  // 24: thread the inspected Windows Cowork host capability into Claude config
237
236
  // and the tenant manifest so unsupported hosts keep Chat and Code usable.
238
- export const CURRENT_CONFIG_VERSION = 24;
237
+ // 25: remove the unproven Windows Cowork capability gate from beta.0 profiles
238
+ // and restore the reviewed always-enabled managed policy.
239
+ export const CURRENT_CONFIG_VERSION = 25;
239
240
 
240
241
  // Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
241
242
  // helper rebranding, and signing. A vendored bundle is rebuilt only when this
@@ -713,7 +714,6 @@ export function installManagedAppFiles({
713
714
  writeTokenHelperFile = true,
714
715
  claudeUserData = null,
715
716
  chatgptInvocations = null,
716
- windowsCoworkCapability = null,
717
717
  generatedAt = new Date().toISOString(),
718
718
  }) {
719
719
  // true = rebuild every target's bundle; false = configs only; an array
@@ -772,7 +772,7 @@ export function installManagedAppFiles({
772
772
  migrateLegacyClaudeAppSessions(paths.claude.userData, homeDir);
773
773
  writeClaudeNative3PSelection(paths);
774
774
  writeClaudeCodeSettings(paths);
775
- writeClaudeConfig(paths, config, models, windowsCoworkCapability);
775
+ writeClaudeConfig(paths, config, models);
776
776
  if (RUNTIME_BRAND.features.sessions) ensureClaudeSessionHooks(paths.claude.userData, config.tenantId, "claude_desktop");
777
777
  }
778
778
  else {
@@ -793,33 +793,8 @@ export function installManagedAppFiles({
793
793
  });
794
794
  }
795
795
 
796
- const manifestCoworkCapability = windowsCoworkCapability
797
- ? {
798
- schemaVersion: 1,
799
- status: ["available", "unavailable", "unknown"].includes(windowsCoworkCapability.status)
800
- ? windowsCoworkCapability.status
801
- : "unknown",
802
- enabled: windowsCoworkCapability.enabled === true,
803
- checks: {
804
- virtualMachinePlatform: windowsCoworkCapability.checks?.virtualMachinePlatform ?? "unknown",
805
- restartPending: windowsCoworkCapability.checks?.restartPending ?? null,
806
- vmcompute: windowsCoworkCapability.checks?.vmcompute ?? "unknown",
807
- hns: windowsCoworkCapability.checks?.hns ?? "unknown",
808
- vfpext: windowsCoworkCapability.checks?.vfpext ?? "unknown",
809
- machineProvisioned: windowsCoworkCapability.checks?.machineProvisioned ?? null,
810
- virtualMachine: windowsCoworkCapability.checks?.virtualMachine ?? null,
811
- nestedVirtualization: windowsCoworkCapability.checks?.nestedVirtualization ?? null,
812
- },
813
- reasonCodes: Array.isArray(windowsCoworkCapability.reasonCodes)
814
- ? [...windowsCoworkCapability.reasonCodes]
815
- : ["inspector-malformed-output"],
816
- remediation: Array.isArray(windowsCoworkCapability.remediation)
817
- ? [...windowsCoworkCapability.remediation]
818
- : [],
819
- }
820
- : null;
821
796
  writeAtomic(path.join(paths.tenantRoot, "manifest.json"), JSON.stringify({
822
- schemaVersion: 5,
797
+ schemaVersion: 6,
823
798
  configVersion: CURRENT_CONFIG_VERSION,
824
799
  modelCatalogVersion: 3,
825
800
  gatewayUrl: config.gatewayUrl,
@@ -832,7 +807,6 @@ export function installManagedAppFiles({
832
807
  models,
833
808
  config,
834
809
  ),
835
- ...(manifestCoworkCapability ? { capabilities: { claudeCowork: manifestCoworkCapability } } : {}),
836
810
  models: models.map((model) => model.id),
837
811
  updatedAt: generatedAt,
838
812
  }, null, 2) + "\n", 0o600);
@@ -1059,7 +1033,7 @@ export function removeManagedApps(targets, {
1059
1033
  }
1060
1034
  }
1061
1035
 
1062
- function writeClaudeConfig(paths, config, models, windowsCoworkCapability = null) {
1036
+ function writeClaudeConfig(paths, config, models) {
1063
1037
  const dir = path.join(paths.claude.userData, "configLibrary");
1064
1038
  const experimental = crossAppModelsEnabled(config);
1065
1039
  const claudeModels = experimental
@@ -1074,13 +1048,10 @@ function writeClaudeConfig(paths, config, models, windowsCoworkCapability = null
1074
1048
  inferenceGatewayAuthScheme: "bearer",
1075
1049
  modelDiscoveryEnabled: false,
1076
1050
  inferenceModels: claudeModels,
1077
- // These are Anthropic's native managed 3P surface controls. Windows
1078
- // enables Cowork only after a separate read-only host inspection proves
1079
- // the VMP/HCS/machine-provisioning prerequisites. Other platforms and
1080
- // legacy callers without a Windows observation retain the reviewed true.
1081
- coworkTabEnabled: windowsCoworkCapability
1082
- ? windowsCoworkCapability.enabled === true
1083
- : true,
1051
+ // These are Anthropic's native managed 3P surface controls. Cowork and
1052
+ // Code default to enabled in the pinned build, but state them explicitly
1053
+ // so an older generated profile cannot leave either surface disabled.
1054
+ coworkTabEnabled: true,
1084
1055
  isClaudeCodeForDesktopEnabled: true,
1085
1056
  // Chat is the only opt-in surface in the pinned build.
1086
1057
  chatTabEnabled: true,
@@ -60,11 +60,6 @@ import {
60
60
  windowsManagedChatGPTRoot,
61
61
  } from "../windowsApps.js";
62
62
  import { RUNTIME_BRAND } from "../runtimeBrand.js";
63
- import {
64
- formatWindowsClaudeCoworkCapability,
65
- inspectWindowsClaudeCoworkCapability,
66
- unknownWindowsClaudeCoworkCapability,
67
- } from "../windowsCapabilities.js";
68
63
 
69
64
  const CLAUDE_KEYCHAIN_NOTICE = "Claude Keychain: enter your Mac login password and choose Always Allow on the first prompt for this tenant; Allow is temporary.";
70
65
 
@@ -324,7 +319,6 @@ function configureWindowsApps(config, targets, vendorPaths, catalog, io) {
324
319
  writeTokenHelperFile: false,
325
320
  claudeUserData: userData,
326
321
  chatgptInvocations: { auth, mcp },
327
- windowsCoworkCapability: io.windowsCoworkCapability || null,
328
322
  });
329
323
  return { catalog, userData };
330
324
  }
@@ -359,10 +353,7 @@ export async function reconcileWindowsTenantApps({
359
353
  confirmVendorInstall = async () => true,
360
354
  homeDir = os.homedir(),
361
355
  environment = process.env,
362
- windowsCoworkCapability = null,
363
356
  } = {}, overrides = {}) {
364
- const effectiveCoworkCapability = windowsCoworkCapability
365
- || unknownWindowsClaudeCoworkCapability();
366
357
  const io = {
367
358
  findClaudeApp: findWindowsClaudeApp,
368
359
  ensureClaudeApp: ensureWindowsClaudeApp,
@@ -444,7 +435,6 @@ export async function reconcileWindowsTenantApps({
444
435
  ...io,
445
436
  homeDir,
446
437
  environment,
447
- windowsCoworkCapability: effectiveCoworkCapability,
448
438
  });
449
439
  }
450
440
  const agentTargets = [];
@@ -638,7 +628,6 @@ export async function cmdWindowsApps(argv, overrides = {}) {
638
628
  syncSkills: syncSkillsSafe,
639
629
  syncAgents: syncAgentProfilesSafe,
640
630
  selectedConfig: selectedAppConfig,
641
- inspectCoworkCapability: inspectWindowsClaudeCoworkCapability,
642
631
  ...overrides,
643
632
  };
644
633
  // Preserve the original Windows-Claude override names used by embedders and
@@ -667,10 +656,6 @@ export async function cmdWindowsApps(argv, overrides = {}) {
667
656
  console.log(` profile: ${fs.existsSync(claudeConfigPath) ? paths.claude.userData : "not installed"}`);
668
657
  console.log(` vendor: ${io.findClaudeApp(io.environment) || "not installed"}`);
669
658
  console.log(" open: impel app open claude");
670
- const manifest = localTenantId ? readTenantManifest(io.homeDir, localTenantId) : null;
671
- for (const line of formatWindowsClaudeCoworkCapability(manifest?.capabilities?.claudeCowork)) {
672
- console.log(` ${line}`);
673
- }
674
659
  }
675
660
  if (targets.includes("chatgpt")) {
676
661
  console.log("Impel ChatGPT:");
@@ -792,13 +777,8 @@ export async function cmdWindowsApps(argv, overrides = {}) {
792
777
  managedChatGPT = io.stageChatGPTApp(vendorPaths.chatgpt, actionPaths.root);
793
778
  }
794
779
 
795
- const windowsCoworkCapability = io.inspectCoworkCapability({
796
- environment: io.environment,
797
- platform: "win32",
798
- });
799
780
  const { userData } = configureWindowsApps(config, actionTargets, vendorPaths, catalog, {
800
781
  ...io,
801
- windowsCoworkCapability,
802
782
  });
803
783
  const tenantPaths = appPaths(io.homeDir, config.tenantId, {
804
784
  claudeUserData: userData,
@@ -17,7 +17,6 @@ import { installedVersion, maybePrintUpdateNotice } from "../updates.js";
17
17
  import { windowsClaudeUserData } from "../windowsApps.js";
18
18
  import { RUNTIME_BRAND } from "../runtimeBrand.js";
19
19
  import { cachedProviderReadiness, providerAvailable } from "../providerReadiness.js";
20
- import { formatWindowsClaudeCoworkCapability } from "../windowsCapabilities.js";
21
20
 
22
21
  function readShellManifest(paths) {
23
22
  try {
@@ -38,7 +37,6 @@ export function desktopReadiness(tenant, {
38
37
  return {
39
38
  claude: { app: "unavailable", shell: "unavailable" },
40
39
  codex: { app: "unavailable", shell: "unavailable" },
41
- cowork: null,
42
40
  };
43
41
  }
44
42
  const claudeUserData = platform === "win32" ? windowsClaudeUserData(environment, tenant.id) : null;
@@ -72,7 +70,6 @@ export function desktopReadiness(tenant, {
72
70
  shell: shellReady ? "ready" : "missing",
73
71
  };
74
72
  }
75
- states.cowork = manifest?.capabilities?.claudeCowork || null;
76
73
  return states;
77
74
  }
78
75
 
@@ -128,8 +125,6 @@ export async function cmdStatus(overrides = {}) {
128
125
  };
129
126
  console.log("Tenant readiness:");
130
127
  let incomplete = false;
131
- let currentCowork = null;
132
- let fallbackCowork = null;
133
128
  for (const tenant of [...selected.tenants].sort((left, right) => left.id.localeCompare(right.id))) {
134
129
  const profiles = io.cliReadiness(tenant.id);
135
130
  const desktop = io.desktopReadiness(tenant, overrides);
@@ -150,15 +145,12 @@ export async function cmdStatus(overrides = {}) {
150
145
  });
151
146
  const states = [claudeCli, codexCli, desktop.claude.app, desktop.codex.app, desktop.claude.shell, desktop.codex.shell];
152
147
  if (states.includes("missing")) incomplete = true;
153
- fallbackCowork ||= desktop.cowork || null;
154
- if (tenant.id === selected.tenantId) currentCowork = desktop.cowork || null;
155
148
  console.log(
156
149
  ` ${tenant.id}${tenant.id === selected.tenantId ? " (current)" : ""}: `
157
150
  + `Claude CLI ${claudeCli}, Codex CLI ${codexCli}, `
158
151
  + `Claude app ${desktop.claude.app}, ChatGPT app ${desktop.codex.app}`,
159
152
  );
160
153
  }
161
- for (const line of formatWindowsClaudeCoworkCapability(currentCowork || fallbackCowork)) console.log(line);
162
154
  if (incomplete) {
163
155
  console.log(RUNTIME_BRAND.tenant.defaultId
164
156
  ? `Repair or finish missing tenant surfaces with: ${RUNTIME_BRAND.cli.command} setup`
package/src/macSetup.js CHANGED
@@ -20,13 +20,21 @@ const MAC_SIGNING_TEAMS = Object.freeze({ claude: "Q6L2SF6YDW", codex: "2DC432GL
20
20
  export const MAC_CLI_INSTALLERS = Object.freeze({
21
21
  claude: Object.freeze({
22
22
  url: "https://claude.ai/install.sh",
23
- sha256: "b3f79015b54c751440a6488f07b1b64f9088742b9052bc1bd356d13108320d2a",
23
+ trustedOrigins: Object.freeze([
24
+ "https://claude.ai",
25
+ "https://downloads.claude.ai",
26
+ ]),
27
+ sha256: "cde4f1702d3b1695f92b73d26888364e17bca476e17f0fd676484c951d36c125",
24
28
  command: "curl -fsSL https://claude.ai/install.sh | bash -s stable",
25
29
  args: Object.freeze(["stable"]),
26
30
  environment: Object.freeze({}),
27
31
  }),
28
32
  codex: Object.freeze({
29
33
  url: "https://chatgpt.com/codex/install.sh",
34
+ trustedOrigins: Object.freeze([
35
+ "https://chatgpt.com",
36
+ "https://releases.openai.com",
37
+ ]),
30
38
  sha256: "1154e9daf713aacd1534efca8042bfd6665ad24bc1d1dfd86b8f439fe60a7a5d",
31
39
  command: "curl -fsSL https://chatgpt.com/codex/install.sh | CODEX_NON_INTERACTIVE=1 CODEX_RELEASE=0.144.6 sh",
32
40
  args: Object.freeze([]),
@@ -75,7 +83,10 @@ function detectMacClis(find, environment, verify) {
75
83
  return { claude: detect("claude"), codex: detect("codex") };
76
84
  }
77
85
 
78
- async function downloadPinnedInstaller(tool, { fetchImpl, fsImpl, installers }) {
86
+ export async function fetchVerifiedMacCliInstaller(tool, {
87
+ fetchImpl = fetch,
88
+ installers = MAC_CLI_INSTALLERS,
89
+ } = {}) {
79
90
  const installer = installers[tool];
80
91
  if (!installer) throw new Error(`unsupported macOS CLI installer: ${tool}`);
81
92
  const controller = new AbortController();
@@ -87,9 +98,14 @@ async function downloadPinnedInstaller(tool, { fetchImpl, fsImpl, installers })
87
98
  clearTimeout(timeout);
88
99
  }
89
100
  if (!response.ok) throw new Error(`official ${tool} installer returned HTTP ${response.status}`);
90
- const finalUrl = new URL(response.url || installer.url);
91
- if (finalUrl.protocol !== "https:" || !["claude.ai", "chatgpt.com"].includes(finalUrl.hostname)) {
92
- throw new Error(`official ${tool} installer redirected to an untrusted origin`);
101
+ let finalUrl;
102
+ try {
103
+ finalUrl = new URL(response.url || installer.url);
104
+ } catch {
105
+ throw new Error(`official ${tool} installer returned an invalid final URL`);
106
+ }
107
+ if (finalUrl.protocol !== "https:" || !installer.trustedOrigins?.includes(finalUrl.origin)) {
108
+ throw new Error(`official ${tool} installer redirected to untrusted origin ${finalUrl.origin}`);
93
109
  }
94
110
  const bytes = Buffer.from(await response.arrayBuffer());
95
111
  if (bytes.length === 0 || bytes.length > MAX_INSTALLER_BYTES) {
@@ -101,6 +117,11 @@ async function downloadPinnedInstaller(tool, { fetchImpl, fsImpl, installers })
101
117
  }
102
118
  const source = bytes.toString("utf8");
103
119
  if (!source.startsWith("#!")) throw new Error(`official ${tool} installer was not a shell script`);
120
+ return { bytes, finalUrl: finalUrl.href, sha256: digest, status: response.status };
121
+ }
122
+
123
+ async function downloadPinnedInstaller(tool, { fetchImpl, fsImpl, installers }) {
124
+ const { bytes } = await fetchVerifiedMacCliInstaller(tool, { fetchImpl, installers });
104
125
  const directory = fsImpl.mkdtempSync(path.join(os.tmpdir(), `impel-${tool}-installer-`));
105
126
  const script = path.join(directory, "install.sh");
106
127
  fsImpl.writeFileSync(script, bytes, { mode: 0o700 });
@@ -17,10 +17,6 @@ import {
17
17
  } from "./tenants.js";
18
18
  import { reconcileTenantApps } from "./commands/apps.js";
19
19
  import { RUNTIME_BRAND } from "./runtimeBrand.js";
20
- import {
21
- formatWindowsClaudeCoworkCapability,
22
- inspectWindowsClaudeCoworkCapability,
23
- } from "./windowsCapabilities.js";
24
20
 
25
21
  export function selectDefaultTenant(listing, { requested = null, currentTenantId = null } = {}) {
26
22
  const normalized = requested ? normalizeTenantId(requested) : null;
@@ -189,7 +185,6 @@ export async function reconcileAllTenants({
189
185
  reconcileApps: reconcileTenantApps,
190
186
  registerShellEntries: registerTenantShellEntries,
191
187
  knownTenantIds: locallyKnownTenantIds,
192
- inspectCoworkCapability: inspectWindowsClaudeCoworkCapability,
193
188
  ...overrides,
194
189
  };
195
190
  // Dependency-injection compatibility for older embedders/tests. Production
@@ -213,9 +208,6 @@ export async function reconcileAllTenants({
213
208
  claude: io.findBinary("claude", environment, platform),
214
209
  codex: io.findBinary("codex", environment, platform),
215
210
  };
216
- const windowsCoworkCapability = includeApps && platform === "win32"
217
- ? io.inspectCoworkCapability({ environment, platform })
218
- : null;
219
211
  const results = new Map(liveTenants.map((tenant) => [tenant.id, {
220
212
  tenantId: tenant.id,
221
213
  tenantName: tenant.name,
@@ -270,7 +262,6 @@ export async function reconcileAllTenants({
270
262
  homeDir,
271
263
  environment,
272
264
  confirmVendorInstall,
273
- windowsCoworkCapability,
274
265
  }, overrides.appOverrides || {});
275
266
  supported = new Set(installed.targets || []);
276
267
  const failedApps = installed.failed || [];
@@ -342,7 +333,6 @@ export async function reconcileAllTenants({
342
333
  selectedTenantId: config.tenantId,
343
334
  tenants: values,
344
335
  inaccessible,
345
- ...(windowsCoworkCapability ? { capabilities: { claudeCowork: windowsCoworkCapability } } : {}),
346
336
  passed: values.every((result) => (
347
337
  (result.cli === "ready" || result.cli === "unavailable")
348
338
  && (result.apps === "ready" || result.apps === "unavailable")
@@ -376,7 +366,6 @@ export function printReconciliationSummary(report, log = console.log) {
376
366
  if (report.inaccessible.length) {
377
367
  log(` Retained local data for inaccessible tenants: ${report.inaccessible.join(", ")}`);
378
368
  }
379
- for (const line of formatWindowsClaudeCoworkCapability(report.capabilities?.claudeCowork)) log(line);
380
369
  }
381
370
 
382
371
  export function tenantCliPathsReady(tenantId, existsSync = fs.existsSync) {
@@ -1,334 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import crypto from "node:crypto";
3
-
4
- import { PINNED_VENDOR_APPS } from "./apps.js";
5
- import { environmentValue, nativeCommandInvocation } from "./nativeProcess.js";
6
-
7
- export const WINDOWS_COWORK_REMEDIATION = Object.freeze({
8
- enableVirtualMachinePlatform: "Enable the Windows Virtual Machine Platform optional feature, then restart Windows.",
9
- restartWindows: "Restart Windows to finish Virtual Machine Platform and service provisioning.",
10
- restoreHcs: "Ask an administrator or endpoint manager to restore the vmcompute, hns, and vfpext Windows components.",
11
- provisionClaude: "Ask an administrator or endpoint manager to machine-provision the exact pinned Claude MSIX with Add-AppxProvisionedPackage.",
12
- enableNestedVirtualization: "Enable nested virtualization for this VM, or use a physical Windows host for Claude Cowork.",
13
- inspectHost: "Ask an administrator to verify Virtual Machine Platform, HCS services, vfpext, and machine-wide Claude provisioning.",
14
- });
15
-
16
- const WINDOWS_PACKAGE_BASE32 = "0123456789abcdefghjkmnpqrstvwxyz";
17
-
18
- /** Derive the stable Package Family Name publisher id from the exact DN. */
19
- export function windowsPackagePublisherId(publisher) {
20
- const hash = crypto.createHash("sha256").update(Buffer.from(String(publisher), "utf16le")).digest();
21
- let bits = "";
22
- for (const byte of hash.subarray(0, 8)) bits += byte.toString(2).padStart(8, "0");
23
- bits += "0";
24
- let publisherId = "";
25
- for (let offset = 0; offset < 65; offset += 5) {
26
- publisherId += WINDOWS_PACKAGE_BASE32[Number.parseInt(bits.slice(offset, offset + 5), 2)];
27
- }
28
- return publisherId;
29
- }
30
-
31
- export function windowsPackageFullName({ name, version, architecture, publisherId }) {
32
- return `${name}_${version}_${String(architecture).toLowerCase()}__${publisherId}`;
33
- }
34
-
35
- export function windowsProvisionedPackageIdentityMatches(candidate, expected) {
36
- if (!candidate || !expected) return false;
37
- const expectedFullName = windowsPackageFullName(expected);
38
- return candidate.displayName === expected.name
39
- && candidate.version === expected.version
40
- && String(candidate.architecture).toLowerCase() === String(expected.architecture).toLowerCase()
41
- && candidate.packageName === expectedFullName;
42
- }
43
-
44
- // Fixed, read-only host observation. Expected package metadata is passed only
45
- // through fixed environment keys; no tenant, user, or path is interpolated
46
- // into PowerShell source.
47
- export const WINDOWS_COWORK_INSPECTION_SCRIPT = String.raw`
48
- $ErrorActionPreference = 'Stop'
49
- function Read-ServiceState([string]$Name) {
50
- try {
51
- $service = Get-Service -Name $Name -ErrorAction SilentlyContinue
52
- if ($null -eq $service) { return 'missing' }
53
- if ($service.Status -eq [System.ServiceProcess.ServiceControllerStatus]::Running) { return 'present-running' }
54
- return 'present-stopped'
55
- } catch { return 'unknown' }
56
- }
57
- function Read-DriverState([string]$Name) {
58
- try {
59
- $driver = Get-CimInstance -ClassName Win32_SystemDriver -Filter "Name='$Name'" -ErrorAction SilentlyContinue
60
- if ($null -eq $driver) { return 'missing' }
61
- if ($driver.State -eq 'Running') { return 'present-running' }
62
- return 'present-stopped'
63
- } catch { return 'unknown' }
64
- }
65
- $vmp = 'unknown'
66
- try {
67
- $feature = Get-CimInstance -ClassName Win32_OptionalFeature -Filter "Name='VirtualMachinePlatform'" -ErrorAction Stop
68
- if ($null -eq $feature) { $vmp = 'absent' }
69
- elseif ($feature.InstallState -eq 1) { $vmp = 'enabled' }
70
- elseif ($feature.InstallState -eq 2) { $vmp = 'disabled' }
71
- elseif ($feature.InstallState -eq 3) { $vmp = 'absent' }
72
- } catch { $vmp = 'unknown' }
73
- $restartPending = $null
74
- try {
75
- $cbs = Test-Path -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending'
76
- $wu = Test-Path -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired'
77
- $restartPending = [bool]($cbs -or $wu)
78
- } catch { $restartPending = $null }
79
- $machineProvisioned = $null
80
- try {
81
- $expectedName = $env:IMPEL_COWORK_EXPECTED_PACKAGE_NAME
82
- $expectedVersion = $env:IMPEL_COWORK_EXPECTED_PACKAGE_VERSION
83
- $expectedArchitecture = $env:IMPEL_COWORK_EXPECTED_PACKAGE_ARCHITECTURE
84
- $expectedPublisherId = $env:IMPEL_COWORK_EXPECTED_PACKAGE_PUBLISHER_ID
85
- $expectedPackageName = $expectedName + '_' + $expectedVersion + '_' + $expectedArchitecture.ToLowerInvariant() + '__' + $expectedPublisherId
86
- $packages = Get-AppxProvisionedPackage -Online -ErrorAction Stop
87
- $exact = $packages | Where-Object {
88
- $displayMatches = ([string]$_.DisplayName -ceq $expectedName)
89
- $versionMatches = ([string]$_.Version -ceq $expectedVersion)
90
- $architectureMatches = ([string]$_.Architecture -ieq $expectedArchitecture)
91
- $packageNameMatches = ([string]$_.PackageName -ceq $expectedPackageName)
92
- $displayMatches -and $versionMatches -and $architectureMatches -and $packageNameMatches
93
- } | Select-Object -First 1
94
- $machineProvisioned = [bool]($null -ne $exact)
95
- } catch { $machineProvisioned = $null }
96
- $virtualMachine = $null
97
- $nestedVirtualization = $null
98
- try {
99
- $computer = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop
100
- $identity = (([string]$computer.Manufacturer) + ' ' + ([string]$computer.Model))
101
- $virtualMachine = [bool]($identity -match 'Virtual|VMware|KVM|HVM|Hyper-V|Parallels|Xen')
102
- if ($virtualMachine) {
103
- $processors = @(Get-CimInstance -ClassName Win32_Processor -ErrorAction Stop)
104
- $nestedVirtualization = [bool]($processors.Count -gt 0 -and ($processors | Where-Object {
105
- -not ($_.VirtualizationFirmwareEnabled -and $_.VMMonitorModeExtensions)
106
- }).Count -eq 0)
107
- } else {
108
- $nestedVirtualization = $true
109
- }
110
- } catch {
111
- $virtualMachine = $null
112
- $nestedVirtualization = $null
113
- }
114
- [ordered]@{
115
- schemaVersion = 1
116
- virtualMachinePlatform = $vmp
117
- restartPending = $restartPending
118
- vmcompute = Read-ServiceState 'vmcompute'
119
- hns = Read-ServiceState 'hns'
120
- vfpext = Read-DriverState 'vfpext'
121
- machineProvisioned = $machineProvisioned
122
- virtualMachine = $virtualMachine
123
- nestedVirtualization = $nestedVirtualization
124
- } | ConvertTo-Json -Compress -Depth 3
125
- `.trim();
126
-
127
- const VMP_STATES = new Set(["enabled", "disabled", "absent", "unknown"]);
128
- const COMPONENT_STATES = new Set(["present-running", "present-stopped", "missing", "unknown"]);
129
-
130
- function emptyChecks() {
131
- return {
132
- virtualMachinePlatform: "unknown",
133
- restartPending: null,
134
- vmcompute: "unknown",
135
- hns: "unknown",
136
- vfpext: "unknown",
137
- machineProvisioned: null,
138
- virtualMachine: null,
139
- nestedVirtualization: null,
140
- };
141
- }
142
-
143
- function normalizedFailure(reasonCode) {
144
- return {
145
- schemaVersion: 1,
146
- status: "unknown",
147
- enabled: false,
148
- checks: emptyChecks(),
149
- reasonCodes: [reasonCode],
150
- remediation: [WINDOWS_COWORK_REMEDIATION.inspectHost],
151
- };
152
- }
153
-
154
- export function unknownWindowsClaudeCoworkCapability(reasonCode = "inspector-not-run") {
155
- return normalizedFailure(reasonCode);
156
- }
157
-
158
- function nullableBoolean(value) {
159
- return value === null || typeof value === "boolean";
160
- }
161
-
162
- export function normalizeWindowsClaudeCoworkCapability(observation) {
163
- if (
164
- !observation
165
- || observation.schemaVersion !== 1
166
- || !VMP_STATES.has(observation.virtualMachinePlatform)
167
- || !COMPONENT_STATES.has(observation.vmcompute)
168
- || !COMPONENT_STATES.has(observation.hns)
169
- || !COMPONENT_STATES.has(observation.vfpext)
170
- || !nullableBoolean(observation.restartPending)
171
- || !nullableBoolean(observation.machineProvisioned)
172
- || !nullableBoolean(observation.virtualMachine)
173
- || !nullableBoolean(observation.nestedVirtualization)
174
- ) return normalizedFailure("inspector-malformed-output");
175
-
176
- const checks = {
177
- virtualMachinePlatform: observation.virtualMachinePlatform,
178
- restartPending: observation.restartPending,
179
- vmcompute: observation.vmcompute,
180
- hns: observation.hns,
181
- vfpext: observation.vfpext,
182
- machineProvisioned: observation.machineProvisioned,
183
- virtualMachine: observation.virtualMachine,
184
- nestedVirtualization: observation.nestedVirtualization,
185
- };
186
- const reasonCodes = [];
187
- const knownUnavailable = [];
188
- const unknown = [];
189
- const addKnown = (reason) => {
190
- reasonCodes.push(reason);
191
- knownUnavailable.push(reason);
192
- };
193
- const addUnknown = (reason) => {
194
- reasonCodes.push(reason);
195
- unknown.push(reason);
196
- };
197
-
198
- if (checks.virtualMachinePlatform === "disabled") addKnown("virtual-machine-platform-disabled");
199
- else if (checks.virtualMachinePlatform === "absent") addKnown("virtual-machine-platform-absent");
200
- else if (checks.virtualMachinePlatform === "unknown") addUnknown("virtual-machine-platform-unknown");
201
-
202
- if (checks.virtualMachinePlatform === "enabled") {
203
- for (const component of ["vmcompute", "hns", "vfpext"]) {
204
- if (checks[component] === "missing") addKnown(`${component}-missing`);
205
- else if (checks[component] === "unknown") addUnknown(`${component}-unknown`);
206
- }
207
- }
208
- const componentsReady = [checks.vmcompute, checks.hns, checks.vfpext]
209
- .every((state) => state.startsWith("present-"));
210
- if (checks.restartPending === true && (
211
- checks.virtualMachinePlatform !== "enabled" || !componentsReady
212
- )) addKnown("restart-required");
213
- else if (checks.restartPending === null && (
214
- checks.virtualMachinePlatform !== "enabled" || !componentsReady
215
- )) addUnknown("restart-state-unknown");
216
-
217
- if (checks.machineProvisioned === false) addKnown("claude-machine-provisioning-missing");
218
- else if (checks.machineProvisioned === null) addUnknown("claude-machine-provisioning-unknown");
219
-
220
- if (checks.virtualMachine === null) addUnknown("virtualization-environment-unknown");
221
- else if (checks.virtualMachine && checks.nestedVirtualization === false) addKnown("nested-virtualization-unavailable");
222
- else if (checks.virtualMachine && checks.nestedVirtualization === null) addUnknown("nested-virtualization-unknown");
223
-
224
- const enabled = checks.virtualMachinePlatform === "enabled"
225
- && componentsReady
226
- && checks.machineProvisioned === true
227
- && checks.virtualMachine !== null
228
- && (!checks.virtualMachine || checks.nestedVirtualization === true);
229
- if (enabled) {
230
- return {
231
- schemaVersion: 1,
232
- status: "available",
233
- enabled: true,
234
- checks,
235
- reasonCodes: [],
236
- remediation: [],
237
- };
238
- }
239
-
240
- const remediation = [];
241
- const addRemediation = (message) => {
242
- if (!remediation.includes(message)) remediation.push(message);
243
- };
244
- if (reasonCodes.some((reason) => reason.startsWith("virtual-machine-platform-"))) {
245
- addRemediation(WINDOWS_COWORK_REMEDIATION.enableVirtualMachinePlatform);
246
- }
247
- if (reasonCodes.includes("restart-required")) addRemediation(WINDOWS_COWORK_REMEDIATION.restartWindows);
248
- if (reasonCodes.some((reason) => /^(?:vmcompute|hns|vfpext)-missing$/u.test(reason))) {
249
- addRemediation(WINDOWS_COWORK_REMEDIATION.restoreHcs);
250
- }
251
- if (reasonCodes.includes("claude-machine-provisioning-missing")) {
252
- addRemediation(WINDOWS_COWORK_REMEDIATION.provisionClaude);
253
- }
254
- if (reasonCodes.includes("nested-virtualization-unavailable")) {
255
- addRemediation(WINDOWS_COWORK_REMEDIATION.enableNestedVirtualization);
256
- }
257
- if (unknown.length) addRemediation(WINDOWS_COWORK_REMEDIATION.inspectHost);
258
-
259
- return {
260
- schemaVersion: 1,
261
- status: knownUnavailable.length ? "unavailable" : "unknown",
262
- enabled: false,
263
- checks,
264
- reasonCodes,
265
- remediation,
266
- };
267
- }
268
-
269
- export function inspectWindowsClaudeCoworkCapability({
270
- environment = process.env,
271
- platform = process.platform,
272
- run = spawnSync,
273
- architecture = process.arch,
274
- } = {}) {
275
- if (platform !== "win32") return normalizedFailure("inspector-not-windows");
276
- const expectedArchitecture = /arm64/iu.test(
277
- environmentValue(environment, "PROCESSOR_ARCHITEW6432")
278
- || environmentValue(environment, "PROCESSOR_ARCHITECTURE")
279
- || architecture,
280
- ) ? "arm64" : "x64";
281
- const pin = PINNED_VENDOR_APPS.claude.windows;
282
- if (windowsPackagePublisherId(pin.publisher) !== pin.publisherId) {
283
- return normalizedFailure("inspector-pinned-publisher-invalid");
284
- }
285
- let invocation;
286
- try {
287
- invocation = nativeCommandInvocation("powershell", [
288
- "-NoLogo",
289
- "-NoProfile",
290
- "-NonInteractive",
291
- "-ExecutionPolicy",
292
- "Bypass",
293
- "-Command",
294
- WINDOWS_COWORK_INSPECTION_SCRIPT,
295
- ], environment, "win32");
296
- } catch {
297
- return normalizedFailure("inspector-powershell-failed");
298
- }
299
- let result;
300
- try {
301
- result = run(invocation.command, invocation.args, {
302
- encoding: "utf8",
303
- env: {
304
- ...environment,
305
- IMPEL_COWORK_EXPECTED_PACKAGE_NAME: pin.packageName,
306
- IMPEL_COWORK_EXPECTED_PACKAGE_VERSION: pin.packageVersion,
307
- IMPEL_COWORK_EXPECTED_PACKAGE_ARCHITECTURE: expectedArchitecture,
308
- IMPEL_COWORK_EXPECTED_PACKAGE_PUBLISHER_ID: pin.publisherId,
309
- },
310
- stdio: ["ignore", "pipe", "pipe"],
311
- windowsHide: true,
312
- windowsVerbatimArguments: invocation.windowsVerbatimArguments,
313
- });
314
- } catch {
315
- return normalizedFailure("inspector-powershell-failed");
316
- }
317
- if (result?.error || result?.status !== 0) return normalizedFailure("inspector-powershell-failed");
318
- try {
319
- return normalizeWindowsClaudeCoworkCapability(JSON.parse(String(result.stdout || "").trim()));
320
- } catch {
321
- return normalizedFailure("inspector-malformed-output");
322
- }
323
- }
324
-
325
- export function formatWindowsClaudeCoworkCapability(capability) {
326
- if (!capability || capability.enabled === true) return [];
327
- const reasons = Array.isArray(capability.reasonCodes) && capability.reasonCodes.length
328
- ? capability.reasonCodes.join(", ")
329
- : "host-prerequisites-unknown";
330
- return [
331
- `Claude Cowork ${capability.status || "unknown"} (${reasons}). Chat and Code remain available.`,
332
- ...(Array.isArray(capability.remediation) ? capability.remediation.map((message) => ` ${message}`) : []),
333
- ];
334
- }