Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Tags: coder/xum

Tags

v0.28.6-nightly.43

Toggle v0.28.6-nightly.43's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
🤖 fix: report backup repository access denial instead of a login fail…

…ure (#4226)

## Summary

When the backup repository refuses a credential the host already
accepted, the Settings > Backup section now says so, naming the refused
credential and quoting the remote's own reason, instead of telling the
user to run `gh auth login`.

## Background

With a wrapper-managed `gh` (for example on a Coder workspace), `gh auth
status` succeeds, but pushing to a backup repository the token cannot
write returns `remote: Write access to repository not granted.` followed
by `The requested URL returned error: 403`. `runGitWithCredentialLadder`
matched that as an authentication failure and threw the fixed message
"Could not authenticate to the backup repository. Check your SSH key or
`gh auth login`", sending a logged-in user to log in again. The thrown
`cause` was also only the ambient rung's failure, so the gh rung's
informative denial disappeared whenever the ambient rung failed
differently (for example "could not read Username ... terminal prompts
disabled").

## Implementation

- `credentials.ts`: `ACCESS_DENIED_PATTERN` (403, "Write access to
repository not granted", "Permission to X denied", "Repository not
found", read-only deploy key) selects, among all recorded rung failures
in ladder order, the first the remote recognised and refused. The
diagnostic line that matched the classifier (a `remote:` or `ERROR:`
line, else the `fatal:` line) and the credential label go into the
message. Pure login failures keep the existing message. The error code
stays `AUTH_FAILED`, so there is no IPC or schema change and the
`instanceof` checks in `gitRepo.ts` are unchanged.
- `src/constants/backup.ts`: shared `BACKUP_CREDENTIAL_LABELS`, used by
the backend message and by `BackupSection.tsx` (replaces the local
`getCredentialLabel`).
- Tests: three new behavioral cases in `credentials.test.ts` (a 403
denial names the gh rung and quotes the reason; the denying rung wins
over a credential-less ambient rung; an ssh `ERROR: Permission to ...
denied` names the ssh rung) plus a login-guidance assertion on the
existing exhausted-ladder case.

## Validation

- Remote dogfood UAT (Coder Agents) on this exact head: 403 denial on a
read-only public repository via "Back up now", 404 "Repository not
found" via Validate, the login message with no credential available at
all (credential-stripped server), the network error for an unreachable
host, a successful backup to a writable repository, rendering at 1280 px
and 375 px, and the ssh rung denial once known_hosts was populated. All
passed.
- Red-green: disabling the access-denied selection fails exactly the
three new tests.

## Risks

Low. Classification only changes which message is thrown after every
rung has already failed; ladder order and retry behaviour are unchanged.
The one widening is that a read-only deploy key ("marked as read only")
now counts as an auth failure, so the ambient rung gets a turn instead
of the raw git error surfacing.

## Follow-up (pre-existing, out of scope)

UAT also found that an ssh host-key verification failure (`Host key
verification failed`, here from `coder gitssh` without a known_hosts
entry) still surfaces raw multi-line stderr in the error area. That path
matched no classifier before this PR either and is untouched here; it
deserves its own classifier and message.

<details>
<summary>Delivery record (reviews, UAT, CI)</summary>

**Reviews** (process limit: 6 completed code, security, and advisory
reviews per PR; 7 completed, see note)

| # | Kind | Head | Reviewer | Findings | Disposition |
|---|------|------|----------|----------|-------------|
| 1 | Code (automatic) | `42c9038c6` | Codex
([request](#4226 (comment)),
[verdict](#4226 (comment)))
| none ("Didn't find any major issues", reviewed commit 42c9038) |
clean, no threads |
| 2 | Security (automatic) | `42c9038c6` | Codex
([result](#4226 (comment)))
| none | clean |
| 3 | Advisory (fresh clean-context final reviewer) | `42c9038c6` | Xum
read-only review sub-agent | P2: `remoteReason()` quoted the first
`remote:` line, which could be server progress rather than the denial.
P3: a bare HTTP 403 treated as proof that re-authenticating cannot help.
Recommendation: specific blocker on the P2. | P2 fixed in `1adb0ed5d`
(quote the remote line that matched the classifier; new test; red-green
verified). P3 was first rejected with reasoning (401 vs 403), then fixed
in `a56ddd6a6` after Codex raised the same point: the "signing in again
will not help" sentence is gone. |
| 4 | Code (automatic) | `1adb0ed5d` | Codex
([request](#4226 (comment)),
[review](#4226 (review)))
| P2
[thread](#4226 (comment)):
a progress `remote:` line could be quoted over a matching `fatal:` 403
line. P2
[thread](#4226 (comment)):
bare 403 / not-found treated as proof the credential was accepted. |
Both fixed in `a56ddd6a6` (search remote and fatal lines for the
classifier match; sentence removed); replied on both threads and
resolved them. |
| 5 | Security (automatic) | `1adb0ed5d` | Codex
([result](#4226 (comment)))
| none | clean |
| 6 | Code (automatic, "New commits" trigger, not requested) |
`a56ddd6a6` | Codex (summary row completed 02:16:19Z; 👍 reaction
02:17:16Z) | none | clean, no threads |
| 7 | Security (automatic, "New commits" trigger, not requested) |
`a56ddd6a6` | Codex (summary row completed 02:17:11Z) | none | clean |

Process note: the six-review limit was exceeded by one. This
repository's Codex integration starts a code and a security review
automatically on every push ("New commits" trigger); rows 6 and 7 were
not requested and cannot be suppressed without changing repository
settings, which was out of bounds. No further review was requested after
row 5.

**Remote dogfood UAT** (Coder Agents, vibe-coding template; evidence
kept in the driving workspace under `.mux-uat/`, not committed)

- Round 1 on `42c9038c6` (remote chat `c742c3f5`): the remote agent's
own verdict was FAIL, twice. A separate critical UAT runner re-examined
the evidence, rejected duplicate and cropped screenshots, forced
re-proofs, and endorsed PASS for every claim in scope: 403 denial via
"Back up now", 404 "Repository not found" via Validate, login message
with no credential available, network error for an unreachable host,
successful backup to a writable repository, rendering at 1280 px and 375
px, ssh-rung denial after populating known_hosts. The FAIL came from one
pre-existing defect outside this change (below).
- Smoke on `1adb0ed5d`: PASS, superseded by the next push.
- Smoke-final on `a56ddd6a6` (same chat, remote `git rev-parse HEAD` and
the app version badge both confirm the SHA): PASS. 403 via "Back up now"
and 404 via Validate both render the final wording; DOM checks confirm
the removed sentence and `gh auth login` are absent. The remote checkout
reported `-dirty` because its bun 1.2.15 rewrote `bun.lock` (repo pins
bun 1.3.5): the diff is a `configVersion` header plus re-hoisting of the
type-only packages `@types/node` and `undici-types`; no runtime package
changed and nothing under `src/` differed, so the tested build is
dependency-equivalent to the committed lockfile.

**Deferred, pre-existing:** an ssh host-key verification failure (`Host
key verification failed`, here from `coder gitssh` without a known_hosts
entry) surfaces raw multi-line stderr in the Backup error area. It
matched no classifier before this PR and is untouched by it. Owner: next
backup follow-up PR. Trigger: add a host-key classifier and message in
`credentials.ts` next to the network and login classifiers.

**CI:** on `42c9038c6`, attempt 1 failed on "Codex Comments" (ran before
Codex answered) and "Test / Unit" (Bun 1.3.5 segfault, zero failing
tests). On `a56ddd6a6`, attempt 1 failed only on "Codex Comments" for
the same timing reason; the failed jobs were rerun once Codex had
completed.

</details>

---

_Generated with `xum` • Model: `anthropic:claude-fable-5-1` • Thinking:
`xhigh`_

<!-- xum-attribution: model=anthropic:claude-fable-5-1 thinking=xhigh
-->

v0.28.6-nightly.33

Toggle v0.28.6-nightly.33's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
🤖 feat: manage installed plugin component selections (#4213)

## Summary

Replace installed-plugin **Add components** with **Manage components**.
Add or remove imported skills and MCP servers, including clearing both
groups, without uninstalling the plugin or deleting its files, data, or
saved workspace MCP preferences.

## Implementation

1. Replace the internal additive API with an atomic `setComponents`
operation. Validate the reviewed Git SHA, full-tree content receipt, and
nullable raw selection baseline before writing the desired set. Preserve
legacy import-all behavior on no-op.
2. Use the existing managed registry as MCP component-policy authority.
Gate held tools and actual stdio/HTTP/SSE startup under the existing
admission locks, including retries/fallback. Retire only removed
clients; retained plugin and unrelated clients remain connected.
3. Persist before runtime reconciliation. No-op saves do nothing. Failed
cleanup reports a saved-state warning and remains retryable. Re-adding a
server honors its saved workspace opt-in; new imports remain disabled
until opted in.
4. Add editable imported checkboxes, group selection controls,
pending-change counts, empty saves, cancellation, and
conflict/lost-response recovery. Refresh both editor and card counts
after a stale save is rejected. Update the palette, full-App stories,
and existing user documentation.

## Validation

Verified on **`f2609e183f50f3a11af1215ec4342f9b45a74667`**:

- **930 unit tests passed**, one Windows-only local skip, across 25
affected/sibling suites. **15 real-IPC/full-App UI tests** passed.
- `make static-check`, `make static-check-full`, and `git diff --check`
passed on this head. The independent final code review found no concrete
blocker. Current-head external CI and code/security reviews remain
separate merge gates.
- Test-first regressions exercise actual stdio/HTTP/SSE/fallback launch
admission, delayed launch/lock release, live MCP admission during
blocked inventory hashing, complete real uninstall/reinstall with
identical receipt bytes, and pending installer journals. Read-only
inventory uses the existing journal/epoch bracket; save-time
SHA/content/selection validation remains locked.
- UI regressions cover an acknowledged save followed by failed inventory
confirmation, and two superseding-writer schedules. Counts/current
checkboxes refresh, conflicts do not show success, and nothing is
automatically resubmitted.
- Recorded isolated, provider-free desktop and **375×812** UAT verified
removal, mixed re-add, current counts, open-panel MCP availability, and
stale-tab rejection. Keeper PID **353850** survived throughout; echo
**353844** disappeared on removal and was replaced only on re-add by
**383254**. Stale rejection changed neither PID nor registry, workspace
MCP preferences, data, or observed source hashes. Mobile document width
stayed **375**.
- Desktop/mobile recordings and **six full-App Storybook interactions**
cover `668094672`; UI code is unchanged on this head. Recorded desktop
**240.3s** and mobile **121.0s** final frames were checked.
Deterministic failure-injection races are automated-test evidence, not
claimed as manual reproductions.
- Six test-first startup-close regressions cover initial, additive,
retry, leased restart, retired-only, and re-add publication. A rejected
close remains tracked by object identity without active tools/stats;
existing reconciliation, idle, or prefix cleanup retries it
successfully. Retained clients survive and re-addition uses a fresh
client.

### Current-head startup-cleanup regression evidence

Recorded the actual six-case failure-injection test run on `f2609e183`:
**6 passed, 0 failed**, exit **0**. The accelerated recording is
**5.0s**; its final frame was checked. This is recorded automated
regression evidence, not a manually induced process-close failure.

![Six startup-cleanup regressions pass on the final
commit](https://github.com/user-attachments/assets/e7681e54-2561-48a1-b79c-b7577edeb2b6)

Recorded startup-cleanup regression run:


https://github.com/user-attachments/assets/0638127c-0a9c-49e0-9e10-66a8243d5b23

### Desktop/mobile UAT (`668094672`; UI unchanged)

![Saved mixed selection with retained MCP
connection](https://github.com/user-attachments/assets/2029b1b7-091b-4e11-9e8c-5b9026298fdb)

![375px stale-save rejection with refreshed selection and
counts](https://github.com/user-attachments/assets/6d9f950b-d8bf-4889-b208-e467d16f7a55)

Desktop removal, mixed re-add, and open-panel runtime observations:


https://github.com/user-attachments/assets/61ee6abf-2176-490f-a71d-82b4cd30c877

Mobile stale-save rejection and refresh:


https://github.com/user-attachments/assets/dafb7541-b497-4712-8842-808d1182a0d0

<details>
<summary>Earlier implementation evidence</summary>

### Earlier validation

- Local affected unit and sibling suites on the rebased head: **717
passed, one platform skip**.
- Real IPC/full-App UI: **10 passed**.
- Full-App Storybook interactions: **six passed** after warming the
server. Fresh-server navigation initially exceeded the default timeout;
unchanged warm runs passed.
- `make static-check`, `make static-check-full`, and `git diff --check`
passed again on the rebased publication head before push.
- Recorded isolated, provider-free UAT covered desktop and 375px
layouts, keyboard operation, add/remove/re-add, no-op, Cancel, empty
selection, persistence, and two-tab conflicts.
- Actual process evidence: keeper PID **3152939** survived echo removal
and re-add. On the corrected build, keeper PID **3413952** survived the
competing removal/re-add; rejecting the stale save changed neither PID
nor registry/configuration bytes.
- Recordings show the implemented `52046f230` build. Its feature commits
were rebased without conflicts onto current main before publication; the
feature patch is unchanged.

### Screenshots

![Manage components with pending additions and
removals](https://github.com/user-attachments/assets/62af1a1d-f498-49b5-bff5-a6cef58b875c)

<details>
<summary>Desktop and mobile conflict recovery</summary>

![Rejected stale save refreshes the editor and surrounding card
counts](https://github.com/user-attachments/assets/a0c8c282-d963-4c05-ac6c-edce80077506)

![375px conflict recovery with matching counts and disabled
Save](https://github.com/user-attachments/assets/2739e2a3-e9b1-4cd0-a31c-f1921fca42dc)

</details>

The attached videos cover selective removal/re-add, Cancel/empty
selection, and corrected mobile stale-save recovery.

### Earlier recordings


https://github.com/user-attachments/assets/2bb77867-30e7-4bf5-bbd2-e5507d30deb9


https://github.com/user-attachments/assets/655c314a-d73a-41ea-b888-6fc5479932c5


https://github.com/user-attachments/assets/61bccb75-17b4-4da1-ac27-10ca33fc0679

</details>

## Runtime boundary

Only skills and MCP component selection changes. Initial install,
update, uninstall, agents, hooks, workflows, and slash commands retain
their existing behavior. Calls already admitted before the selection
commit may finish normally. Idle sibling processes reconcile at their
next MCP boundary; immediate cross-process process termination is not
promised.

**Regression risk:** Managed MCP admission and cleanup are the sensitive
paths. Real mutations can temporarily deny admission; read-only
inventory must not. Deterministic transport/installer races and
retained-process UAT cover those boundaries. No new persisted fields,
journal files, or locking subsystem are introduced by these review
fixes.

---

<details>
<summary>📋 Implementation Plan</summary>

The plan below is the accepted implementation snapshot. Publication and
conditional merge-queue submission were authorized afterward.

# Reversible installed-plugin component management

## Goal and scope

Replace the installed-plugin **Add components** panel with **Manage
components**. Users can add components or deselect previously imported
skills and MCP servers, then explicitly save the new selection.

Assumption: removing a component means removing it from Xum's available
plugin components, not deleting the plugin's installed source files.
Keeping the plugin installed makes re-adding possible without fetching
again. Empty selections are valid. Initial installation remains
unchanged. Scope matches the existing chooser: skills and MCP servers
only; agents, hooks, workflows, and slash commands are unchanged.

Preserve workspace MCP enablement, tool allowlists, identity, and data.
New MCP imports remain disabled without a workspace opt-in. Re-adding a
previously enabled server can restore that saved opt-in; the panel must
say so.

## Verified foundations

- `PluginsSettingsSection.tsx`: `ComponentChooser` locks imported
checkboxes; `AddComponentsPanel` models additions only.
- `agentPlugins/installService.ts`: `addComponents` unions selections
under `runExclusive`; the registry already supports explicit empty
allowlists.
- `agentPluginInstalls.ts`: absent `importedComponents` means legacy
import-all. Cancel/no-op must preserve that distinction.
- `getComponents` returns the installed SHA, full-tree content hash,
inventory, and current imported selection. SHA/hash do not change when
another panel changes only the registry selection.
- Skill discovery and `loadPluginMcpServers` already filter by the
selected allowlist. Active MCP connections require additional targeted
reconciliation; blanket plugin teardown would disrupt retained servers.

## User experience

1. Rename the installed-plugin action to **Manage components**. Keep the
existing inline panel; no new settings page or modal.
2. Initialize checkboxes from the current imported set. All available
skills and MCP servers are toggleable. **Select all** and **Clear**
operate on the whole group.
3. Show one concise pending-change summary, such as “Add 1 skill ·
Remove 1 MCP server.” Use **Save changes** and **Cancel**. Disable Save
only while busy or unchanged—not when the selection is empty.
4. Saving updates the card counts. Cancel discards the draft. Reopening
loads current state. An empty selection explains that no skills or MCP
servers are imported, while the plugin remains installed. **Uninstall**
remains separate.
5. Explain that deselection preserves source files, data, and workspace
MCP preferences. New imports need workspace enablement; re-added servers
retain previous preferences.

## Implementation sequence

Deliver two dependent, reviewable increments. Stabilize increment 1
before exposing removal in increment 2. No PR creation or publishing is
authorized by this plan.

### 1. Make MCP component removal safe at runtime

**Estimated net product code: +180–300 LoC.** No UI changes in this
prerequisite.

Files: `src/node/services/mcpServerManager.ts`,
`src/node/services/agentPlugins/registry.ts`,
`src/node/services/agentPlugins/mcpConfig.ts`, and
`CoreServicesRootLive` in `src/node/services/di/layers/core.ts` (the
existing `pluginInvalidation` wiring).

1. Write failing runtime tests first. Cover removal with a retained
sibling and unrelated server, held tools in a second manager sharing the
registry, pending startups, active leases, and simultaneous add/remove
with equal counts.
2. Use a canonical component-policy snapshot from the existing atomic
`plugins.json` as the cross-process authority. Include managed
installation identity and selected MCP names. Extend the existing
stable-scan/call-time invalidation seam; do not create another epoch
file, use mtime-only checks, or bump the global tree epoch.
3. Recheck that policy at tool authorization and across config
discovery/startup publication. On a policy change, reconcile against
fresh configuration and retry stale publication. Carry managed-policy
provenance from discovery; use the unqualified `info.plugin.serverName`,
not the composite key, for allowlist membership. Reuse existing
managed-home/alias identity rules. A missing registry entry must not
reclassify an already managed server as unmanaged. Unreadable managed
policy fails closed without restricting genuinely unmanaged plugins.
Cover those boundaries in tests.
4. Extend the existing additive-retention path to handle
plugin-selection removals and mixed additions/removals when retained
configs are unchanged. Compare sets, not counts. Reuse
`computeSignatureEntries`; do not manually patch signature strings.
Leave existing behavior for unrelated configuration changes intact.
5. Revoke removed keys from tool authorization, availability, and retry
queues. Fence in-flight startup publication. Retire only removed
clients, respecting existing lease cleanup. Retained plugin and
unrelated clients must stay connected. Never mark a deselected server
for retry.

**Runtime contract:** An invocation whose call-time authorization check
occurs after the selection commit must reject a deselected server, even
through an old tool object in another process. “Admitted” means an
actual invocation passed that gate—not that its tool definition was
advertised. Already-admitted calls may finish or fail normally. Existing
prompts cannot be retroactively edited. Idle sibling processes reconcile
at their next MCP boundary; immediate cross-process process termination
is not promised. Keep registry reads within the existing bounded gate
and check their latency in the targeted runtime tests.

**Gate:** The targeted MCP/registry/discovery tests pass. Prove retained
connection identity, not merely that a new connection works. No API for
MCP removal ships before these checks are green.

### 2. Ship replacement selection and the management panel

**Estimated net product code: +90–170 LoC.** Depends on increment 1.
Includes backend, UI, tests, and updated user docs.

1. **Write behavioral tests first.** Exercise subset replacement, mixed
add/remove, empty selection, cancel/no-op, legacy import-all, invalid
component names, stale selection, same-SHA file edits, and failed
persistence.
2. **Replace the internal additive API with
`agentPlugins.setComponents`.** Update `src/common/orpc/schemas/api.ts`,
`src/node/orpc/router.ts`, and
`src/node/services/agentPlugins/installService.ts`. Frontend/backend are
version-synchronized, so do not retain an unused additive compatibility
endpoint.
- Input: plugin name, `expectedLockedSha`, `expectedContentHash`,
required nullable `expectedImportedComponents`, and desired
`importedComponents`.
- `null` baseline means the legacy absent field. Compare selections as
normalized sets; preserve absent versus explicit empty.
- Under `runExclusive`, read strict registry state, check all three
baselines, validate desired names against the verified installed
inventory, and atomically replace only `importedComponents`.
- Reuse full-tree content-consent checks, including verification around
inventory reads. No remote fetch, tree rewrite, data deletion, identity
change, override pruning, or migration.
- Preserve legacy import-all on no-op. A changed legacy selection
becomes an explicit allowlist. Later plugin updates preserve that
selection and do not auto-import new components.
3. **Publish disk state before runtime effects.** After persistence and
release of the mutation lock, invalidate skill availability and
reconcile local MCP managers through increment 1. Reconciliation reads
current policy rather than replaying a stale removal list, so rapid
remove→re-add cannot close a newly valid client. A failed write changes
neither selection nor runtime. If cleanup fails after persistence, do
not roll back or pretend nothing saved: show the saved selection,
surface a cleanup warning, and retry retirement at the next
reconciliation. Authoritative call-time gating must still deny removed
tools. Keep any result-envelope extension local to this operation.
4. **Update the UI in `PluginsSettingsSection.tsx`.** Rename
`AddComponentsPanel`, use a full-selection draft, unlock imported rows,
and implement the interaction above. Keep the shared initial-install
chooser behavior intact. Capture the raw baseline separately from the
displayed effective selection.
   - Refetch after success; clear pending state on close/reopen.
- On a stale SHA/content/selection conflict, refresh inventory and
baseline, reset the draft, and require a new explicit Save. Never
auto-resubmit a stale choice.
- On ordinary failure, retain the draft and show an actionable error. If
a response is lost after a possible commit, refetch before describing
the persisted result.
5. **Update access paths and fixtures.** Rename the command-palette
label and matching intent in `src/browser/utils/commands/sources.ts`,
`pluginsSectionIntents.ts`, and their command-ID definition/callsites.
Preserve Tab/Space/Enter operation. Update
`src/browser/stories/mocks/orpc.ts` and
`src/browser/stories/App.pluginImports.stories.tsx` to use replacement
semantics.
6. **Update existing docs.** In `docs/agents/agent-skills.mdx`, replace
the one-way-import limitation. Explain deselection, preserved
data/preferences, empty selections, and re-add behavior. Do not add a
new documentation page.

**Gate:** Backend, real-IPC UI tests, full-App Storybook interactions,
and static checks pass. Opening the panel without edits must not mutate
the registry. Initial installation, update, and uninstall regression
tests remain green.

## Acceptance criteria

1. Users can add and remove imported skills/MCP servers in one panel,
including removing all of them without uninstalling.
2. Save persists exactly the chosen set. Cancel and no-op leave disk and
runtime untouched. Reopening and restarting show the saved choice.
3. Deselected skills disappear from fresh skill discovery. Deselected
MCP tools are unavailable, including through stale held tools; retained
servers are not restarted.
4. Installed files, instance identity, data, saved MCP preferences, and
unrelated plugins are unchanged. Newly added MCP servers do not gain
unsolicited workspace enablement.
5. Concurrent edits and changed installed content cannot silently
overwrite the user's reviewed selection. Invalid input and write failure
leave the prior state intact.
6. Desktop and narrow layouts support keyboard use without horizontal
overflow. Initial install/update/uninstall retain their current
behavior.

## Validation and dogfooding

### Automated gates

Run `src/**` suites with Bun. Run `tests/**` suites with Jest. Include
sibling suites for every touched production module.

```bash
bun test src/node/services/agentPlugins/installService.test.ts \
  src/node/services/agentPlugins/registry.test.ts \
  src/node/services/agentPlugins/discovery.test.ts \
  src/node/services/agentPlugins/mcpConfig.test.ts \
  src/node/services/mcpServerManager.test.ts \
  src/node/services/agentSkills/agentSkillsService.test.ts

TEST_INTEGRATION=1 bun x jest tests/ipc/agentPlugins.test.ts \
  tests/ui/config/pluginImports.test.ts --runInBand

# Start a persistent Storybook server with make storybook, then:
bun x test-storybook --url http://127.0.0.1:6006 \
  --maxWorkers 1 App.pluginImports.stories

# Run after tests, not concurrently with temporary test-module creation.
make static-check
make static-check-full
```

Extend full-App stories only. Cover saved removals, empty selection, and
conflict recovery. Pin the phone story with
`parameters.pixel.matrix.viewports: ["phone"]` and matching
`globals.viewport`. Keep breakpoint assertions valid in the
desktop-sized Storybook test runner by using the existing
fixed-width/guarded pattern.

### Hands-on walkthrough

1. Create a disposable local Git plugin fixture outside the checkout,
following `installService.test.ts` fixture patterns. Include two skills
and two harmless local MCP servers with observable process identities.
Add a separate temporary workspace/project. No provider credentials are
needed.
2. Start an isolated server on free ports:
   ```bash
   make dev-server-sandbox \
     DEV_SERVER_SANDBOX_ARGS="--clean-providers --clean-projects"
   ```
Use the printed URL/root. Enable Agent Plugins in that sandbox. Install
the local fixture through **Add plugin** using its absolute Git path. Do
not seed or edit the personal Xum root.
3. Load `agent-browser skills get core`, open the sandbox, and start a
video recording before interaction. Use `snapshot -i` for functional
checks. Import a subset; enable one imported MCP server in the test
workspace.
4. Manage the plugin: add another component, deselect an existing one,
save, reopen, and reload. Verify counts and actual skill/MCP
availability. Verify the retained MCP process did not restart. Re-add
the previously enabled server and verify the documented saved-preference
behavior.
5. Test Cancel, no-op, Clear both groups, and re-add after an empty
selection. Open two panels in separate browser tabs; save one, then
verify the other's stale save is rejected and refreshed.
6. Repeat at desktop and approximately 375px width. Use the Storybook
manager's pinned phone variant as the separate visual gate. Capture
screenshots of pending removals, the saved/empty state, and phone
layout. Stop the recording and use `attach_file` for screenshots **and
video**.
7. Stop only owned sandbox/browser/Storybook processes. If GitHub
publication is later requested, upload the evidence with `gh ...
--attach`; otherwise keep delivery local.

**Final gate:** Run the complete relevant tests and static checks on the
final code. Get one clean-context independent readiness recommendation
from the diff, tests, and dogfood evidence. Record the verdict and any
concrete blocker. Do not claim implementation success if required checks
or evidence are missing.

<details>
<summary>Planning review disposition</summary>

One independent review accepted the UI/API design and recommended
reducing runtime scope. The plan limits reconciliation to
plugin-selection changes and makes runtime tests a prerequisite. Retain
cross-process checks: process-local invalidation alone misses held tools
in sibling processes. Verified existing call gates already await
disk-backed epoch/override readers; extend that bounded seam rather than
introducing a separate synchronization system. Planning left the source
tree clean. Implementation and test execution remain pending.

</details>

## Total size and exclusions

**Recommended approach: approximately +270–470 net product LoC**,
excluding tests, stories, and docs. This includes the runtime
protections; merely unlocking checkboxes is not sufficient. Re-estimate
at the first gate if the existing reconciliation seam cannot support the
bounded change.

No new component types, plugin-file deletion, workspace-setting cleanup,
generic plugin lifecycle framework, or unrelated MCP refactor. Planning
is read-only; the commands above are implementation-time gates, not
tests already run.

</details>

---

_Generated with [`xum`](https://github.com/coder/xum) • Model:
`coder:openai/gpt-6-astra` • Thinking: `xhigh` • Cost: `$264.74`_

<!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=xhigh
costs=264.74 -->

v0.28.5

Toggle v0.28.5's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
🤖 release: v0.28.5 (#4175)

## Summary

Version bump for the v0.28.5 patch release. Headline changes since
v0.28.4: remote server connections in the desktop app (#4101),
self-updating `xum server` under a restart supervisor (#4083, #4127),
first-class GPT-6 Astra and Astra Pro support including Codex OAuth
routing (#4064, #4094, #4106, #4124), token-budget context window
rollovers (#4097), the workspace remembering model and mode on send
(#3968), in-place plugin updates (#4164), the optional flat sidebar chat
list (#3994), and copying selected chat text as Markdown (#4170). It
also carries a long run of streaming, compaction, and task-lifecycle
fixes (reconnect streaming #4123, message edits during active streams
#4153, Codex OAuth prompt-cache routing #4159, compaction/history
fencing #4133 through #4148, task lock ordering #4161) plus the Effect
Wave 4 runtime refactors and deslop passes 1 through 3.

## Implementation

Bumped with `node ./scripts/set-package-version.js 0.28.5` so the root
`package.json` and the legacy `packages/mux-compat` forwarding package
stay version-locked. `src/common/compat/productIdentity.test.ts` passes
locally (8/8).

After this PR merges, the `v0.28.5` tag will be applied to the squash
commit and the GitHub Release published to trigger the
desktop/npm/docker pipelines.

---

_Generated with `xum` • Model: `anthropic:claude-fable-5-1` • Thinking:
`xhigh` • Cost: `$1.64`_

<!-- mux-attribution: model=anthropic:claude-fable-5-1 thinking=xhigh
costs=1.64 -->

v0.28.4

Toggle v0.28.4's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
🤖 release: v0.28.4 (#4063)

## Summary

Version bump for the v0.28.4 patch release. The headline change since
v0.28.3 is Gemini 3.8 Flash becoming the default Gemini Flash model
(#4060). The release also carries browser Login with Coder on remote Xum
servers (#4047), the opt-in project bundle for settings backup (#4043),
the connection-indicator slow-response surfacing (#4059), send-queue and
terminal-wake fixes (#4053, #4052), and the Effect Phase 11 runtime
refactors.

## Implementation

Bumped with `node ./scripts/set-package-version.js 0.28.4` so the root
`package.json` and the legacy `packages/mux-compat` forwarding package
stay version-locked (the v0.28.3 bump missed the compat package and
broke `Test / Unit` on main, fixed in #4048).
`src/common/compat/productIdentity.test.ts` passes locally.

After this PR merges, the `v0.28.4` tag will be applied to the squash
commit and the GitHub Release published to trigger the
desktop/npm/docker pipelines.

---

_Generated with `xum` • Model: `anthropic:claude-fable-5-1` • Thinking:
`medium` • Cost: `$0.00`_

<!-- mux-attribution: model=anthropic:claude-fable-5-1 thinking=medium
costs=0.00 -->

v0.28.3

Toggle v0.28.3's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
🤖 release: v0.28.3 (#4042)

v0.28.2

Toggle v0.28.2's commit message
🤖 release: v0.28.2

v0.28.1

Toggle v0.28.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
🤖 release: v0.28.1 (#3751)

## Summary

Bump version to 0.28.1 for the next stable release.

## Background

35 commits landed since v0.28.0, headlined by Claude Opus 5 and native
Kimi K3 support (new Moonshot AI provider), Gemini 3.6 Flash,
project-less scratch chats, a FIFO message queue behind special sends,
sub-agent reports in chat, sticky sub-agents, arbitrary file staging
from chat and the creation composer, and a skills refresh.

After merge: tag the squash commit as `v0.28.1` and publish the GitHub
Release, which triggers the desktop, Docker, VS Code, and npm release
pipelines.

---

_Generated with `mux` • Model: `anthropic:claude-fable-5` • Thinking:
`xhigh` • Cost: `$0.00`_

<!-- mux-attribution: model=anthropic:claude-fable-5 thinking=xhigh
costs=0.00 -->

v0.28.0

Toggle v0.28.0's commit message
release: v0.28.0

v0.27.2-nightly.3

Toggle v0.27.2-nightly.3's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
🤖 fix: stamp real version in Nix flake builds (#3686)

## Summary

Nix flake builds displayed `unknown` as the app version (top-left title
bar and About dialog) because the version stamp could never be computed
inside the build sandbox. This feeds the revision the flake already
knows into the build so the version is stamped correctly.

## Background

The version shown in the UI comes from `src/version.ts`, which is
generated at build time by `scripts/generate-version.sh` using `git
describe --tags --always --dirty` and `git rev-parse --short HEAD`. Both
fall back to the literal string `"unknown"` when the git commands fail.

Nix copies only git-tracked files into the build sandbox and strips the
`.git` directory, so those git commands have no repository to read and
both fall back to `"unknown"`. Having `git` in `nativeBuildInputs` only
provides the binary, not the repo metadata, so it doesn't help.

Ironically the flake already computes the revision (`version = self.rev
or self.dirtyRev or "dev"`) but never passed it into the build, so that
value was discarded before the script ran.

## Implementation

The version script already supports a `RELEASE_TAG` override that skips
the git calls and sets `git_describe` directly (used by CI release
builds). The flake's `buildPhase` now exports that revision:

- `RELEASE_TAG="${version}"` sets `git_describe`.
- `GIT_COMMIT="${builtins.substring 0 12 version}"` sets the commit
field via a new override.

`generate-version.sh` now honors an incoming `GIT_COMMIT` env var,
keeping the git-based default when it's unset so non-Nix builds are
unchanged.

## Risks

Low. The change only affects build-time version stamping. Non-Nix builds
keep their existing git-based behavior (the `GIT_COMMIT` default is
unchanged when the env var is unset), and the script's `unknown`
fallback still applies when neither git nor overrides are available.

One cosmetic note: Nix dev builds now log `Release build: using
RELEASE_TAG=...` in build output, which is slightly misleading but
harmless.

---

_Generated with `mux` • Model: `anthropic:claude-opus-4-8` • Thinking:
`xhigh` • Cost: `$1.11`_

<!-- mux-attribution: model=anthropic:claude-opus-4-8 thinking=xhigh
costs=1.11 -->

v0.27.1

Toggle v0.27.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
🤖 release: v0.27.1 (#3692)