impel-cli 0.20.45 → 0.20.46-beta.2

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
@@ -384,7 +384,7 @@ impel tasks delete IMP-123 --yes
384
384
  Use `--json` when exact machine-readable fields matter. Gateway-only members
385
385
  cannot use task CRUD.
386
386
 
387
- ### Native Tasks MCP App
387
+ ### Native Tasks board and MCP tools
388
388
 
389
389
  Setup and update register a second tenant-bound MCP server named
390
390
  `impel-tasks` in Impel-managed Claude and Codex/ChatGPT profiles. It is separate
@@ -404,14 +404,20 @@ control-plane HTTP bearer credential, and fixes tenant selection in the
404
404
  transport header. Generated Claude/Codex MCP entries contain neither the PAT
405
405
  nor a tenant-derived credential.
406
406
 
407
- - The pinned Impel Claude Desktop renders the standard MCP Apps task board when
408
- its host supports that protocol.
409
- - The exact pinned Impel Codex desktop profile enables its reviewed
410
- `enable_mcp_apps` contract so `show_tasks` can render the same board. The
411
- vendor-pin test fails closed if the embedded Codex binary stops accepting it.
407
+ - Managed Claude and ChatGPT/Codex apps on macOS add one persistent Tasks
408
+ destination. Windows keeps the tenant-bound headless `impel-tasks` tools but
409
+ does not inject the board into the signed Store/MSIX apps: their verified
410
+ Electron archives cannot be patched without invalidating vendor signatures.
411
+ It loads the authenticated canonical Impel `/tasks` application directly in
412
+ a sandboxed, tenant-isolated web view; a fresh profile uses the existing
413
+ Board default.
414
+ - The trusted native process obtains the selected tenant credential through a
415
+ private one-shot pipe, exchanges the PAT for a bounded HttpOnly web session,
416
+ and then loads `/tasks` without an Authorization header. The web renderer has
417
+ no preload, Node integration, credential API, or IPC bridge.
412
418
  - Claude Code and Codex CLI profiles remain headless and receive complete task
413
- tools plus text results; the desktop-only renderer flag is never written to
414
- those profiles.
419
+ tools plus text results. Desktop Codex no longer enables the MCP Apps
420
+ renderer; the headless `impel-tasks` server remains for model/tool access.
415
421
  - Profiles preserve unrelated MCP servers. If a user-authored server already
416
422
  owns the `impel-tasks` name, setup/update fails with remediation instead of
417
423
  overwriting it.
@@ -525,15 +531,148 @@ Main paths:
525
531
  ~/.config/impel/config.json
526
532
  ~/.config/impel/cli/tenants/<tenant>/
527
533
  ~/.config/impel/apps/tenants/<tenant>/
534
+ ~/.config/impel/update-check.json
535
+ ~/.config/impel/flags.json
536
+ ~/.config/impel/telemetry-notice.json
537
+ ~/.config/impel/telemetry/
538
+ ~/.config/impel/reports/
528
539
  ```
529
540
 
530
541
  The config directory is owner-only and the config file is written with mode
531
542
  `0600` where supported. Generated client profiles use tenant-bound helper
532
543
  invocations; PAT material is not embedded into shell entries.
533
544
 
545
+ `flags.json` caches server-evaluated feature flags for the current principal.
546
+ `telemetry/` holds queued analytics events waiting to be sent, plus their
547
+ send state; it is empty unless analytics are on. `reports/` holds bug reports
548
+ that could not be delivered, kept so they are never lost, bounded to the last
549
+ 50 and nothing older than 30 days; its hidden `.last-auto-report.json` records
550
+ what this machine last reported automatically, so the same failure is not
551
+ reported twice in a day. Everything above lives under one directory, so
552
+ `impel nuke` erases all of it.
553
+
534
554
  Losing membership does not delete local tenant data. A future cleanup feature
535
555
  must remain explicit and destructive-state aware.
536
556
 
557
+ ## Privacy
558
+
559
+ Analytics are off unless you turn them on: nothing in the first table below is
560
+ sent until `telemetry.enabled` is `true` in `config.json`, which only
561
+ `impel setup --analytics on` writes.
562
+
563
+ Failure reports are the one exception and are described in their own section
564
+ further down. When `impel setup` or `impel update` runs out of ways to fix a
565
+ failure, the CLI files a bug report about that failure whether or not analytics
566
+ are on. That is deliberate: a broken install cannot be fixed by people who never
567
+ hear about it, and the machines least likely to opt in are the ones failing.
568
+
569
+ **Usage analytics** (`cli_command_run`, one per command, sent when enabled):
570
+
571
+ | Field | Example | Notes |
572
+ |---|---|---|
573
+ | `command` | `claude` | The command name only — never its arguments |
574
+ | `outcome` | `success` | `success`, `failure`, or `refused` |
575
+ | `durationBucket` | `lt_5s` | A coarse bucket, not a duration |
576
+ | `cliVersion` | `0.20.41` | |
577
+ | `channel` | `latest` | npm release channel |
578
+ | `platform` | `darwin` | `darwin`, `linux`, `win32`, or `unknown` |
579
+
580
+ **Bug reports** (`cli_bug_report`, one per delivered report): the same
581
+ `cliVersion`, `channel`, and `platform`, plus whether a message was attached,
582
+ whether a task was filed, and if not, why.
583
+
584
+ Events are identified by the account your PAT resolves to, stamped by the
585
+ server. Nothing else is collected: no command arguments, file contents,
586
+ prompts, session content, environment variables, paths, or credentials. Events
587
+ are validated against a closed allowlist before they reach disk, and anything
588
+ that still looks like a secret after redaction is dropped locally and reported
589
+ by `impel status`.
590
+
591
+ Events are queued on disk and sent by a detached background process, so no
592
+ command ever waits on the network. Analytics never initialize inside a managed
593
+ desktop app profile or a non-interactive surface.
594
+
595
+ **Turning analytics off.** Any one of these is enough, and each overrides a
596
+ stored `yes`:
597
+
598
+ ```sh
599
+ IMPEL_DISABLE_TELEMETRY=1 impel claude # also accepts true/yes
600
+ DO_NOT_TRACK=1 impel claude
601
+ ```
602
+
603
+ Any non-empty `CI` value suppresses analytics as well. To change the stored
604
+ answer, run `impel setup --analytics off`. To erase everything already queued,
605
+ run `impel nuke`.
606
+
607
+ ## Automatic failure reports
608
+
609
+ When `impel setup` or `impel update` fails in a way it cannot recover from, the
610
+ CLI files a bug report about that failure and prints one line saying so, with
611
+ the report's id and how to turn the behavior off. It does not ask first.
612
+
613
+ **What triggers one.** Only a terminal failure of `impel setup` or `impel
614
+ update` — a failure that recovery already tried and could not repair. A failure
615
+ that recovery fixes is not a failure, and reports nothing. No other command
616
+ reports itself, and a successful run never reports.
617
+
618
+ **What is in it.** The same fields `impel report` sends, assembled from what the
619
+ command already had in hand:
620
+
621
+ | Field | Example | Notes |
622
+ |---|---|---|
623
+ | `message` | `The npm-verified global impel-cli update failed.` | Written by the CLI, not by you |
624
+ | `cliVersion` | `0.20.41` | |
625
+ | `nodeVersion` | `v22.11.0` | |
626
+ | `platform` | `darwin` | |
627
+ | `architecture` | `arm64` | |
628
+ | `channel` | `latest` | npm release channel |
629
+ | `command` | `npm install --global impel-cli` | The command that failed, when there was one |
630
+ | `exitCode` | `1` | |
631
+ | `errorCode` | `install_failed` | |
632
+ | `stderr` | | That command's output, redacted |
633
+ | `diagnostics` | `{ "step": "install.impel_cli", "reportId": "…" }` | The failing step, the report id, and a handful of scalars naming what the run was doing |
634
+
635
+ Credentials, home directory paths, and email addresses are removed before
636
+ anything leaves the machine, by the same redaction the manual command uses.
637
+ Nothing else is collected: no arguments beyond the failing command line, no file
638
+ contents, no prompts, no session content, no environment variables.
639
+
640
+ **Turning it off.** Either of these, and only these:
641
+
642
+ ```sh
643
+ IMPEL_DO_NOT_TRACK=1 impel setup
644
+ IMPEL_CI=1 impel setup
645
+ ```
646
+
647
+ Bare `DO_NOT_TRACK` and bare `CI` **do not** suppress failure reports, though
648
+ they still suppress analytics. The two surfaces have different contracts on
649
+ purpose: `CI` is set by every CI runner on earth and says nothing about whether
650
+ its operators want to hear that installs are broken there, and deciding that for
651
+ them is how a broken install stays broken. Suppressing a failure report takes
652
+ the branded name, typed on purpose.
653
+
654
+ The consequence is worth stating plainly: a GitHub Actions job sets `CI=true`,
655
+ not `IMPEL_CI`, so a repeatedly failing `impel setup` in an unmodified CI
656
+ pipeline files a report every run until someone sets `IMPEL_CI=1`.
657
+
658
+ Reports are also suppressed when the CLI is a rebranded distribution, when it
659
+ runs inside a managed desktop app profile, and when there is no PAT to attribute
660
+ the report to.
661
+
662
+ **The same failure is not reported twice in a day.** The report id is a
663
+ fingerprint of the report's own contents, so an identical failure produces an
664
+ identical id; `~/.config/impel/reports/.last-auto-report.json` records what was
665
+ reported and when, and a repeat inside 24 hours is dropped silently.
666
+
667
+ **Delivery is best effort and never blocks.** Two retries against an unreachable
668
+ control plane, then the report is written to `~/.config/impel/reports/` like any
669
+ other undelivered report. A reporting failure never changes the exit code of the
670
+ command that failed, and never adds a second error message to the one you
671
+ already got.
672
+
673
+ `impel report` remains a message you choose to send: neither switch above
674
+ disables it, and it works whether or not analytics are on.
675
+
537
676
  ## Public command surface
538
677
 
539
678
  ```text
@@ -548,6 +687,7 @@ impel tasks ...
548
687
  impel pat ...
549
688
  impel status
550
689
  impel doctor
690
+ impel report
551
691
  ```
552
692
 
553
693
  Profile helpers, content synchronization, desktop maintenance, and transport
package/RELEASE_NOTES.md CHANGED
@@ -1,5 +1,64 @@
1
1
  # Release notes
2
2
 
3
+ ## 0.20.46-beta.2 — Reload reconciled apps safely
4
+
5
+ - Closes every running managed macOS app before rewriting its profile and
6
+ desktop Tasks preloads, then reopens only the managed launchers that the
7
+ reconcile run actually closed. Current bundles still avoid an unnecessary
8
+ rebuild.
9
+ - Allows the production Tasks board to load GitHub-hosted member avatars while
10
+ keeping desktop web-view egress restricted to exact reviewed origins.
11
+ - States the signed-platform boundary explicitly: persistent in-app Tasks
12
+ navigation is a macOS feature; Windows retains tenant-bound headless Tasks
13
+ tools because modifying Store/MSIX Electron archives would invalidate the
14
+ verified vendor signatures.
15
+ - Gives Claude doctor probes enough output budget for reasoning-capable models
16
+ and reports non-terminal provider stop reasons such as token exhaustion
17
+ directly instead of presenting them as an unexplained missing acknowledgement.
18
+ - Preserves the `0.20.46-beta.1` released fixture and captures this prerelease as
19
+ a separate upgrade corpus without advancing the managed config or bundle
20
+ fingerprints.
21
+
22
+ ## 0.20.46-beta.1 — Embed the canonical Tasks board
23
+
24
+ - Replaces the duplicate desktop Tasks renderer with the authenticated
25
+ `www.useimpel.com/tasks` application in a sandboxed, tenant-isolated web
26
+ view. A fresh profile opens the existing Board default.
27
+ - Exchanges the tenant-bound Tasks PAT for an eight-hour HttpOnly web session
28
+ through a one-shot, header-only bootstrap. The PAT never enters a URL,
29
+ renderer, IPC bridge, profile, launch argument, or log.
30
+ - Removes the desktop-only Codex MCP Apps renderer flag while retaining the
31
+ tenant-bound headless Tasks MCP tools for CLI/model compatibility.
32
+ - Keeps the v1 Tasks widget available on the control plane for already-installed
33
+ desktop clients; its source and resource are retired only after the beta and
34
+ stable rollout cutoff is explicitly approved.
35
+ - Retains the telemetry, remote flags, and failure-reporting capabilities from
36
+ `0.20.46-beta.0`; beta users do not lose that prerelease functionality.
37
+ - Keeps a selected board visible when its host loses focus, and confines the
38
+ native Tasks fallback to the primary vendor window so first-run auxiliary
39
+ windows cannot cover or stall the board.
40
+ - Targets npm's existing `next` dist-tag. Promote the same
41
+ reviewed tree to stable `0.20.46` only after the session endpoint is deployed
42
+ and the isolated pinned-app launch smoke passes.
43
+
44
+ ## 0.20.46 — Restore exact ChatGPT Store compatibility
45
+
46
+ - Updates the coordinated reviewed ChatGPT pin to desktop `26.803.81509`,
47
+ embedded Codex `0.147.0-alpha.6.6`, and Windows Store package
48
+ `26.803.10989.0`, restoring managed installation while keeping artifact,
49
+ signature, runtime, and version checks exact and fail-closed.
50
+ - Retries gateway model-catalog body-read timeouts as readiness failures while
51
+ preserving malformed-catalog failures and one shared bounded retry policy.
52
+ - Prevents cooperating verbatim-relay hosts from adding external sources on
53
+ every managed relay surface, including the default Claude slash command.
54
+ - Preserves the original vendor-install failure and the untouched sibling app
55
+ during targeted recovery, instead of replacing useful diagnostics or
56
+ disturbing an otherwise healthy managed app.
57
+ - Accepts the control plane's `cancelled` task progress value in `impel tasks`
58
+ validation and command help.
59
+ - Carries the exact-pin compatibility repair through the `0.20.46` prerelease
60
+ train before stable promotion.
61
+
3
62
  ## 0.20.41 — Windows onboarding: converge over an already-installed Store ChatGPT
4
63
 
5
64
  - Accepts winget's UPDATE_NOT_APPLICABLE result during setup as well as
@@ -1,6 +1,6 @@
1
1
  # Native-agent host capability matrix
2
2
 
3
- Validated on 2026-08-09 with isolated temporary homes and tenant profiles. The
3
+ Validated on 2026-08-11 with isolated temporary homes and tenant profiles. The
4
4
  matrix is intentionally pinned: a client upgrade must be re-qualified before it
5
5
  is treated as an eager host.
6
6
 
@@ -9,7 +9,7 @@ is treated as an eager host.
9
9
  | Claude Code | 2.1.220 | Yes, agent frontmatter `tools` | Eager; optional single-thread selection | The bound run/answer, resume, and recovery tools are direct child tools. `impel claude --agent` verifies the exact managed adapter before enabling 2.1.220's bypass-permission opt-in and mode. |
10
10
  | Claude Desktop | 1.26832.0 (embedded Code 2.1.222) | Yes, agent frontmatter `tools` | Eager | Same direct-tool contract as Claude Code. |
11
11
  | Codex CLI through `impel codex` | 0.147.0 | Yes, with `features.code_mode.direct_only_tool_namespaces = ["mcp__impel_agent"]` | Eager; optional single-thread profile | Existing `@Agent` children call the fixed MCP namespace directly. `impel codex --agent` also removes the parent spawn/wait/relay thread. |
12
- | ChatGPT desktop Codex | 26.803.41515 (embedded Codex 0.147.0-alpha.6.5) | Yes, with the same exact native namespace | Eager child, compatible parent | Keeps the app's supported `@Agent` parent/child route, but the child no longer needs code cells or code-mode waits. |
12
+ | ChatGPT desktop Codex | 26.803.81509 (embedded Codex 0.147.0-alpha.6.6) | Yes, with the same exact native namespace | Eager child, compatible parent | Keeps the app's supported `@Agent` parent/child route, but the child no longer needs code cells or code-mode waits. |
13
13
 
14
14
  ## Qualified contract
15
15
 
@@ -48,9 +48,9 @@ is treated as an eager host.
48
48
  and re-qualified rather than receiving unknown configuration.
49
49
  - `mcp__impel_agent` is the native tool namespace. The server id
50
50
  `impel_agent` is not equivalent and does not qualify as direct exposure.
51
- - Generated Codex fixed-binding adapters use the ultra-fast Codex Spark model
52
- at low reasoning for the thin tool-selection and terminal-relay turns, with
53
- unrelated local host, plugin, remote-plugin, and skill-catalog context
51
+ - Generated Codex fixed-binding adapters use the Code Mode-compatible Luna
52
+ model at low reasoning for the thin tool-selection and terminal-relay turns,
53
+ with unrelated local host, plugin, remote-plugin, and skill-catalog context
54
54
  disabled. The selected Eve agent, not this wrapper model, still owns the
55
55
  substantive task.
56
56
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.20.45",
3
+ "version": "0.20.46-beta.2",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
package/src/agents.js CHANGED
@@ -65,14 +65,14 @@ export const NATIVE_AGENT_RESUME_TOOL = "resume_native_agent_run";
65
65
  export const NATIVE_AGENT_RECOVER_TOOL = "recover_native_agent_runs";
66
66
  export const NATIVE_AGENT_CONTINUATION_SCHEMA = "impel.native-agent-continuation.v1";
67
67
  export const MANAGED_AGENT_MCP_SERVER = "impel_agent";
68
- export const MANAGED_AGENT_MANIFEST_VERSION = 24;
68
+ export const MANAGED_AGENT_MANIFEST_VERSION = 25;
69
69
  export const IMPEL_NATIVE_PARENT_DIRECT_ENV = "IMPEL_NATIVE_PARENT_DIRECT";
70
70
  export const IMPEL_NATIVE_SLASH_COMMANDS_ENV = "IMPEL_NATIVE_SLASH_COMMANDS";
71
71
 
72
72
  // The host model only selects the fixed MCP tool and faithfully returns its
73
- // result. Spark minimizes those transport-only turns while the selected Eve
74
- // agent remains responsible for the substantive work.
75
- const CODEX_AGENT_ADAPTER_MODEL = "gpt-5.3-codex-spark";
73
+ // result. Luna advertises the code-mode contract required by the direct-only
74
+ // adapter while the selected Eve agent remains responsible for substantive work.
75
+ const CODEX_AGENT_ADAPTER_MODEL = "gpt-5.6-luna";
76
76
 
77
77
  const NATIVE_AGENT_TOOL_NAMES = [
78
78
  NATIVE_AGENT_RUN_TOOL,
package/src/apps.js CHANGED
@@ -6,7 +6,6 @@ import { spawnSync } from "node:child_process";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import {
8
8
  enableManagedCodexDesktopCodeModeHost,
9
- enableManagedCodexDesktopMcpApps,
10
9
  hardenManagedCodexToml,
11
10
  secureAllManagedCodexHomes,
12
11
  secureManagedCodexHome,
@@ -29,7 +28,6 @@ import { CURRENT_CONFIG_VERSION } from "./managedProfileVersion.js";
29
28
  import { IMPEL_CODEX_PARENT_DELEGATION_INSTRUCTIONS } from "./commands/launch.js";
30
29
  import {
31
30
  desktopTasksAssetPaths,
32
- desktopTasksHostHtml,
33
31
  desktopTasksMainPreload,
34
32
  desktopTasksViewPreload,
35
33
  } from "./desktopTasks.js";
@@ -107,8 +105,8 @@ export const PINNED_VENDOR_APPS = Object.freeze({
107
105
  }),
108
106
  }),
109
107
  chatgpt: Object.freeze({
110
- version: "26.803.41515",
111
- codexVersion: "0.147.0-alpha.6.5",
108
+ version: "26.803.81509",
109
+ codexVersion: "0.147.0-alpha.6.6",
112
110
  bundleName: "ChatGPT.app",
113
111
  windows: Object.freeze({
114
112
  storeProductId: "9PLM9XGG6VKS",
@@ -116,20 +114,20 @@ export const PINNED_VENDOR_APPS = Object.freeze({
116
114
  // The Microsoft Store manifest identifies this exact reviewed build.
117
115
  // Keep one Windows package version so the manifest contract and local
118
116
  // AppX validation cannot silently drift apart again.
119
- packageVersion: "26.803.5235.0",
120
- codexVersion: "0.147.0-alpha.6.5",
117
+ packageVersion: "26.803.10989.0",
118
+ codexVersion: "0.147.0-alpha.6.6",
121
119
  publisherId: "2p2nqsd0c76g0",
122
120
  executable: "app\\ChatGPT.exe",
123
121
  updateManifestUrl: "https://persistent.oaistatic.com/codex-app-prod/windows-store-update.json",
124
122
  }),
125
123
  downloads: Object.freeze({
126
124
  arm64: Object.freeze({
127
- url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-arm64-26.803.41515.zip",
128
- sha256: "8abd46bf063bc27cbadcbc2863007ac44365b13a23ede17ceaee81fb9eeaeb9a",
125
+ url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-arm64-26.803.81509.zip",
126
+ sha256: "34c7e629678ad5d63a639ec694a5b83b85f4e1bd7c21ee2abcfefbb041095b9c",
129
127
  }),
130
128
  x64: Object.freeze({
131
- url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-x64-26.803.41515.zip",
132
- sha256: "87239a3dd12e2761de515ee78a0b73c02ac907c34c1f01a68dd6f093cf433fb1",
129
+ url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-x64-26.803.81509.zip",
130
+ sha256: "c9d5a14803c3138a77be5e891a4944c04ff421645150043dd3eddd0bf34eda97",
133
131
  }),
134
132
  }),
135
133
  }),
@@ -511,6 +509,38 @@ export function blockingAppNames(targets, { tenantId = null, tenantName = null }
511
509
  return [...new Set(names)];
512
510
  }
513
511
 
512
+ function blockingAppEntries(targets, {
513
+ tenantId = null,
514
+ tenantName = null,
515
+ homeDir = os.homedir(),
516
+ bundleTargets = targets,
517
+ } = {}) {
518
+ const bundleTargetSet = new Set(bundleTargets);
519
+ const paths = appPaths(homeDir, tenantId, { tenantName });
520
+ const legacyPaths = tenantId ? appPaths(homeDir) : null;
521
+ const entries = [];
522
+ for (const target of targets) {
523
+ if (bundleTargetSet.has(target)) {
524
+ for (const root of vendorSearchRoots(homeDir, target)) {
525
+ for (const bundleName of APP_DEFINITIONS[target].names) {
526
+ entries.push({
527
+ name: bundleName.replace(/\.app$/u, ""),
528
+ appPath: path.join(root, bundleName),
529
+ });
530
+ }
531
+ }
532
+ if (legacyPaths) {
533
+ entries.push({ name: APP_DEFINITIONS[target].label, appPath: legacyPaths[target].launcher });
534
+ }
535
+ }
536
+ entries.push({
537
+ name: managedLauncherName(target, { tenantId, tenantName }),
538
+ appPath: paths[target].launcher,
539
+ });
540
+ }
541
+ return [...new Map(entries.map((entry) => [entry.appPath, entry])).values()];
542
+ }
543
+
514
544
  /**
515
545
  * pgrep/pkill -f pattern for one app's processes. Anchored on
516
546
  * "/<Name>.app/.../Contents/MacOS/" so "Claude" never matches the
@@ -522,6 +552,14 @@ export function appProcessPattern(name) {
522
552
  return `/${escaped}\\.app/(.*/)?Contents/MacOS/`;
523
553
  }
524
554
 
555
+ /** Anchored process pattern for one exact outer app bundle. */
556
+ export function appBundleProcessPattern(appPath) {
557
+ // App bundles and process argv paths are macOS/POSIX paths even when this
558
+ // pure helper is exercised by the cross-platform test matrix.
559
+ const escaped = path.posix.resolve(appPath).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
560
+ return `^${escaped}/(.*/)?Contents/MacOS/`;
561
+ }
562
+
525
563
  const QUIT_POLL_MS = 500;
526
564
 
527
565
  /**
@@ -535,39 +573,46 @@ const QUIT_POLL_MS = 500;
535
573
  export async function quitBlockingApps(targets, {
536
574
  tenantId = null,
537
575
  tenantName = null,
576
+ homeDir = os.homedir(),
577
+ bundleTargets = targets,
538
578
  log = console.log,
539
- isRunning = (name) =>
540
- spawnSync("/usr/bin/pgrep", ["-f", appProcessPattern(name)], { stdio: "ignore" }).status === 0,
541
- requestQuit = (name) =>
579
+ isRunning = (_name, appPath) =>
580
+ spawnSync("/usr/bin/pgrep", ["-f", appBundleProcessPattern(appPath)], { stdio: "ignore" }).status === 0,
581
+ requestQuit = (_name, appPath) =>
542
582
  // osascript `tell` would LAUNCH a non-running app, so callers must gate on
543
583
  // isRunning first; the timeout covers apps stuck on a save/confirm dialog.
544
- spawnSync("/usr/bin/osascript", ["-e", `tell application ${JSON.stringify(name)} to quit`], {
584
+ spawnSync("/usr/bin/osascript", ["-e", `tell application ${JSON.stringify(appPath)} to quit`], {
545
585
  stdio: "ignore",
546
586
  timeout: 8_000,
547
587
  }),
548
- forceQuit = (name) =>
549
- spawnSync("/usr/bin/pkill", ["-f", appProcessPattern(name)], { stdio: "ignore" }),
588
+ forceQuit = (_name, appPath) =>
589
+ spawnSync("/usr/bin/pkill", ["-f", appBundleProcessPattern(appPath)], { stdio: "ignore" }),
550
590
  wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
551
591
  quitWaitMs = 10_000,
552
592
  killWaitMs = 5_000,
553
593
  } = {}) {
554
- async function goneWithin(name, budgetMs) {
594
+ async function goneWithin(name, appPath, budgetMs) {
555
595
  for (let waited = 0; waited <= budgetMs; waited += QUIT_POLL_MS) {
556
- if (!isRunning(name)) return true;
596
+ if (!isRunning(name, appPath)) return true;
557
597
  await wait(QUIT_POLL_MS);
558
598
  }
559
- return !isRunning(name);
599
+ return !isRunning(name, appPath);
560
600
  }
561
601
 
562
602
  const closed = [];
563
- for (const name of blockingAppNames(targets, { tenantId, tenantName })) {
564
- if (!isRunning(name)) continue;
603
+ for (const { name, appPath } of blockingAppEntries(targets, {
604
+ tenantId,
605
+ tenantName,
606
+ homeDir,
607
+ bundleTargets,
608
+ })) {
609
+ if (!isRunning(name, appPath)) continue;
565
610
  log(`${name} is running; closing it before touching app bundles…`);
566
- requestQuit(name);
567
- if (await goneWithin(name, quitWaitMs)) { closed.push(name); continue; }
611
+ requestQuit(name, appPath);
612
+ if (await goneWithin(name, appPath, quitWaitMs)) { closed.push(name); continue; }
568
613
  log(`${name} did not quit gracefully; terminating it…`);
569
- forceQuit(name);
570
- if (await goneWithin(name, killWaitMs)) { closed.push(name); continue; }
614
+ forceQuit(name, appPath);
615
+ if (await goneWithin(name, appPath, killWaitMs)) { closed.push(name); continue; }
571
616
  throw new Error(`${name} is still running; close it manually, then re-run`);
572
617
  }
573
618
  return closed;
@@ -615,11 +660,14 @@ export function appPaths(homeDir = os.homedir(), tenantId = null, {
615
660
  };
616
661
  }
617
662
 
618
- function writeDesktopTasksProfileAssets(root) {
663
+ function writeDesktopTasksProfileAssets(root, appUrl) {
619
664
  const assets = desktopTasksAssetPaths(root);
620
- writeAtomic(assets.mainPreload, desktopTasksMainPreload(), 0o600);
665
+ writeAtomic(assets.mainPreload, desktopTasksMainPreload(appUrl), 0o600);
621
666
  writeAtomic(assets.viewPreload, desktopTasksViewPreload(), 0o600);
622
- writeAtomic(assets.hostHtml, desktopTasksHostHtml(), 0o600);
667
+ // This exact path is Impel-owned. Remove the former local MCP Apps host when
668
+ // a profile is regenerated; old released clients keep the immutable server
669
+ // resource during the rollout window.
670
+ fs.rmSync(assets.hostHtml, { force: true });
623
671
  return assets;
624
672
  }
625
673
 
@@ -828,7 +876,10 @@ export function installManagedAppFiles({
828
876
 
829
877
  const installed = [];
830
878
  for (const target of targets) {
831
- writeDesktopTasksProfileAssets(paths[target].root);
879
+ writeDesktopTasksProfileAssets(
880
+ paths[target].root,
881
+ config.appUrl || RUNTIME_BRAND.controlPlane.defaultOrigin,
882
+ );
832
883
  // Non-bundle targets are the background-refresh / fast-open path: configs,
833
884
  // token helper, catalog, and manifest only. Bundle swaps require the app
834
885
  // to be closed (see quitBlockingApps), so those paths never attempt one.
@@ -1444,7 +1495,7 @@ function writeChatGPTConfig(
1444
1495
  writeAtomic(
1445
1496
  configPath,
1446
1497
  enableManagedCodexDesktopCodeModeHost(
1447
- enableManagedCodexDesktopMcpApps(hardenedToml, configPath),
1498
+ hardenedToml,
1448
1499
  configPath,
1449
1500
  ),
1450
1501
  0o600,
@@ -1603,11 +1654,9 @@ export function managedChatGPTConfigDrifted(paths) {
1603
1654
  // surface the vendor settings writer rewrites. code_mode_host is
1604
1655
  // load-bearing: without it pinned Codex fails closed on the code_mode_only
1605
1656
  // default model, so a dropped key must self-heal as drift rather than wait
1606
- // out the refresh TTL. enable_mcp_apps only degrades rendering but rides
1607
- // the same check. Configs from generations before the keys existed are
1657
+ // out the refresh TTL. Configs from generations before the key existed are
1608
1658
  // merely stale (the manifest version bump rewrites them), never drift —
1609
- // require the keys only once this install's manifest proves they were
1610
- // written.
1659
+ // require it only once this install's manifest proves it was written.
1611
1660
  let manifestConfigVersion = 0;
1612
1661
  try {
1613
1662
  manifestConfigVersion = Number(JSON.parse(
@@ -1618,8 +1667,7 @@ export function managedChatGPTConfigDrifted(paths) {
1618
1667
  }
1619
1668
  const featuresTable = current.match(/^\[features\]\n(?:[^[\n][^\n]*\n|\n)*/mu)?.[0] || "";
1620
1669
  const featureKeysDropped = manifestConfigVersion >= 35
1621
- && (!/^code_mode_host = true$/mu.test(featuresTable)
1622
- || !/^enable_mcp_apps = true$/mu.test(featuresTable));
1670
+ && !/^code_mode_host = true$/mu.test(featuresTable);
1623
1671
  return !current.includes(CHATGPT_CONFIG_START)
1624
1672
  || readTopLevelTomlString(current, "model_provider") !== RUNTIME_BRAND.cli.providerId
1625
1673
  || !readTopLevelTomlString(current, "chatgpt_base_url")