# Server update buttons silently do nothing for connections paired before the environment:maintain split · Issue \#17034 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/issues/17034)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: backlog

Priority: 0

## Description

[Discussion and activity](https://git.cafe/pingdotgg/t3code/issues/17034)

---

### What happened

> After updating the client on my macbook, I got the notification to update the server, but clicking it does nothing and the server seems to be out of date. Installing the latest nightly after a new update (2787) didn't fix it. My server is stuck at 2761.

The macOS desktop app (2787) connects to a remote Linux server (2761) that runs as the systemd background service. There are two update entry points, and neither works:

- The composer banner **"Server update available…"** shows an **Update** button that is disabled and does not react to clicks.
- **Settings → Connections → "Update all"** is enabled and reacts to the click, but does nothing. There is no toast, no error and no progress.

There is no indication of why. The server's 2752 → 2761 update, triggered from the same Mac the day before, worked.

### Diagnosis

The client blocks the update because the connection's session lacks `environment:maintain`. That is intended. The bug is that the UI does not say so: one button is disabled with no explanation, and the other is enabled but does nothing.

1. The Mac's saved connection uses a bearer session paired on 2026-09-28 by 0.0.43-nightly.20260927.2344, before #9786 split out the granular permissions. The server's `auth_sessions` row for it has these scopes:
   `["orchestration:read","orchestration:operate","terminal:operate","review:write","relay:read"]`.
   The row is labelled "MacBook Air", client version 2787, and its `last_connected_at` matches the websocket that carried the clicks.
2. 28ce9d1c (#9786) is in v0.0.46-nightly.20261007.2761 and not in v0.0.46-nightly.20261006.2752. Since 2761, `/api/auth/session` returns `permissions` equal to the stored scopes, so `sessionGrantsScope(session, "environment:maintain")` (`packages/contracts/src/auth.ts:181-192`) takes the `permissions` branch at `:186` and returns false. The legacy `orchestration:operate → environment:maintain` fallback only applies when `permissions` is absent. This is why the 2752 → 2761 update worked and every update after it does not.
3. **Composer banner / per-row button** (`apps/web/src/components/ServerUpdateAction.tsx`): `canUpdate = canUpdateServer(sessionState)` at `:226`, then `disabled={manualCommand === null && !canUpdate}` at `:333`. `manualCommand` is null because `selfUpdate === "boot-service"`. The button is greyed out with no reason given.
4. **"Update all"**: `savedServerUpdateTargets` (`apps/web/src/components/settings/ConnectionsSettings.tsx:2148-2181`) filters on version mismatch, enabled, connected, not desktop-local and a non-null `selfUpdate`, but **not on the session's permissions**. `ServerUpdatesAction` (`ServerUpdateAction.tsx:125-159`) therefore counts the server as eligible and keeps the button enabled. On click, `useServerUpdate` (`:70-76`) runs `if (!canUpdateServer(...) || pending) return;` before calling `server.updateServer`, so nothing is sent and nothing is shown.
5. Server side: nothing reaches it. The server would also reject the call, since `RpcAuthorization.ts:77-79` requires `environment:maintain` for `server.updateServer` and `server.updateServerWithProgress`. The background service, launcher and runtime download were not involved. Running `t3 update` by hand on the server succeeded right away.

`PermissionUpdateNotice.tsx` shows a one-time "Permissions have changed for …" toast for legacy-only sessions. Its dismissal is stored in localStorage, and after that the update UI gives no hint that re-pairing is needed.

The same code is unchanged on `main` as of 2026-10-08. `ServerUpdateAction.tsx` was last touched by 365aa879 (#10298).

Relevant files at the 2787 tag:
- `apps/web/src/components/ServerUpdateAction.tsx`
- `apps/web/src/components/settings/ConnectionsSettings.tsx`
- `packages/contracts/src/auth.ts`

### Steps to reproduce

1. Pair a client with a remote server on a build from before #9786 (for example 0.0.43 nightly), using a one-time pairing link (bearer session). The session gets the old five-scope set.
2. Update the server to a build that includes #9786 (2761 or later) and the client to a newer build, so the client shows "Server update available".
3. Composer banner: the **Update** button is disabled and gives no reason.
4. Settings → Connections: **Update all** is enabled. Click it. Nothing happens, no toast appears, and the server trace shows no `ws.rpc.server.updateServer*` span. The only traffic is `server.reportClientActivity` from the click.

Expected: either the session can still update its server, or both buttons explain that the connection needs re-pairing for `environment:maintain`. "Update all" should not count targets it will skip, and should not skip them silently.

### Version

Client: 0.0.46-nightly.20261007.2787 (macOS desktop). Server: 0.0.46-nightly.20261007.2761, systemd user service with launcher 0.0.46-nightly.20261003.2610.

### Environment

Server: CachyOS Linux x64 (kernel 7.2), Node v26.8.2, systemd user service. Client: macOS desktop app (Electron 44.4.2) over a direct Tailscale route.

### Evidence

```shell
# Clicks at about 10:12:33-38 JST: server trace shows only activity pings on the open /ws, and no update RPC
10:12:33 ws.rpc.server.reportClientActivity {"rpc.transport":"websocket","rpc.method":"server.reportClientActivity"}
10:12:34 ws.rpc.server.reportClientActivity ...
10:12:38 ws.rpc.server.reportClientActivity ...
$ grep -c -E 'updateServer|self_update|SelfUpdate' server.trace.ndjson*   # all retained files
0

# Session the Mac is using (auth_sessions, read-only)
subject=one-time-token  method=bearer-access-token  label="MacBook Air"  surface=desktop
client_app_version=0.0.46-nightly.20261007.2787  issued_at=2026-09-28T10:46:07Z  last_connected_at=2026-10-08T01:05:03Z
scopes=["orchestration:read","orchestration:operate","terminal:operate","review:write","relay:read"]

# The /api/auth/session request from 2787 at 10:05:03 went over the direct Tailscale route with a bearer header (no DPoP)
http.server GET /api/auth/session host=<tailnet-host>.ts.net:3773 ua=T3Code(Nightly)/0.0.46-nightly.20261007.2787

# Last successful UI-driven update (server was 2752, before #9786)
[15:59:17.731] INFO: Server update prepared; handing off to the service launcher.
    targetVersion: '0.0.46-nightly.20261007.2761'
```

### Related issues

- #13072: Tailscale-paired remote where "every so often the update server option in the local t3-code UI is unable to update the server". This is likely the same mechanism for any pre-#9786 pairing, but that issue is mainly about connections dropping and needing re-pairing, so this one is filed separately.
- #16804: the same class of problem (sessions missing a newly split permission, `providers:manage`), but on mobile and for provider auth.
- #15835: update guidance hidden for incompatible environments. That is a different gate.

### Fix applied or workaround

Ran `npx t3@nightly update --yes` on the server, which moved it to 0.0.46-nightly.20261008.2801 and restarted the service cleanly. The Mac was deliberately left on its old pairing so the bug can still be reproduced on the next version mismatch. The permanent workaround is to re-pair the connection with a fresh link (`t3 auth pairing create` / `t3 pair`), which issues `AuthStandardClientScopes` including `environment:maintain`.

### Filed by

Claude Code (claude-opus-5-5) via `t3 triage`

