Skip to content

feat: allow admins to override the app update url from a system file - #3181

Merged
abose merged 4 commits into
mainfrom
ai
Sep 6, 2026
Merged

feat: allow admins to override the app update url from a system file#3181
abose merged 4 commits into
mainfrom
ai

Conversation

@abose

@abose abose commented Sep 6, 2026

Copy link
Copy Markdown
Member

Why

The auto updater always fetched its manifest from the url baked into the build, so there was no way to point a prod or staging build at a local update feed to test the update flow.

What

An optional phoenix_override_config.json in a machine wide folder that only admin/root can write to overrides brackets.config keys.

OS Path
Windows C:\Program Files\Phoenix Code Control\phoenix_override_config.json
macOS /Library/Application Support/Phoenix Code Control/phoenix_override_config.json
Linux /etc/phoenix-code-control/phoenix_override_config.json
{
    "app_update_url": "http://localhost:8000/.../localUpdateManifest.json",
    "app_update_linux_installer_url": "http://localhost:8000/.../linux-local-update-override-script.sh"
}

Two keys, because app_update_url alone is not enough on linux. Windows and mac download and install the artifact named in the manifest, so overriding the manifest is sufficient there. Linux installs nothing from the manifest — it pipes an installer script into bash and that script decides what to install. Overriding only the manifest therefore produced the worst outcome: it reported a fake version and then installed the real production build. Both are overridable now, so every shipped platform can be tested end to end.

The installer url is taken as given, no sanitising. Whatever it points at is piped into bash anyway, so validating the url string would buy nothing — it can only come from our own https constant or from a file that only root can create.

Why this is safe

The folder being admin owned is the whole security model: a standard user, an extension or a downloaded project cannot create the file. Reads need no elevation and raise no OS prompt on any platform — C:\Program Files and /etc are world readable, and system /Library/Application Support is not TCC protected (that only covers ~/Documents, ~/Desktop etc). Same trust model as the existing AI control file.

Only keys in OVERRIDABLE_KEYS are honoured; everything else in the file is ignored.

Not breaking the update path

Updates are critical, so the override cannot degrade them:

  • the config read is raced against a timeout. It is awaited at quit time, when the node process may already be gone, and a read that never settled would hang the quit rather than merely the update.
  • the read is warmed at boot, because a window that only inherits an already scheduled update never runs an update check and would otherwise first touch the disk at quit time.
  • any failure at all reading the override falls back to the shipped default and the update proceeds as normal.
  • with no override file present the resolved urls are exactly the previous hardcoded constants.

Testing

Verified end to end on linux/electron:

  • with the file in place the updater fetched its manifest from localhost and the dialog rendered the served release notes — Update available: {"version":"99.9.9", ...} Detected platform: linux-x64
  • a key not in OVERRIDABLE_KEYS was dropped, and brackets.config was left untouched
  • removing the file fell back to the baked in url; with no file, getOverrides() returns {} in 0 ms
  • the file was created after the app booted, so the read was cold, and it succeeded with no permission prompt
  • the exact quit time command, wget -qO- "$UPDATE_URL" | bash -s -- --upgrade, runs the sample installer and exits 0

Reference samples

src/extensionsIntegrated/appUpdater/unit-tests/ holds three samples that point at each other over the dev server, so they work as-is with no setup:

  • localOverride.json — copy to the admin folder as phoenix_override_config.json
  • localUpdateManifest.json — fake manifest, version 99.9.9 so it always wins
  • linux-local-update-override-script.sh — fake installer, just prints update done

sh is added to the PWA cache's disallowed extension list, since the manifest builder throws on extensions it has not been told about and shell scripts are never fetched by the app.

Notes for review

  • The tauri linux path (main.js) is left hardcoded since linux ships as electron. If that ever changes, note it goes through __TAURI__.shell.Command, whose argument regex lives in phoenix-desktop's tauri.conf.json.
  • Boot update checks are throttled to 24h via PH_LAST_UPDATE_CHECK_TIME, so after dropping in an override file you usually need Help → Check for Updates to see it take effect.
  • The samples currently ship in dist. The unit-tests exclusion in the gulpfile is specific to phoenix-pro, which is dropped from prod for an unrelated packaging reason. Happy to add a generic !src/**/unit-tests/**/* if we want that convention to be real.

Related

Companion comment only PR in phoenix-pro marks the older Phoenix AI Control folder as legacy and points new machine wide settings here.

https://claude.ai/code/session_01UfWRCpcBZbfDd143DV1CxP

The auto updater always fetched its manifest from the url baked into the
build, so there was no way to point a prod or staging build at a local
update feed for testing.

Read an optional phoenix_override_config.json from a machine wide folder
that only admin/root can write to, and prefer its app_update_url over
brackets.config. The folder being admin owned is what makes the file
trustworthy- a standard user, an extension or a project cannot forge it,
so this cannot be used to redirect the updater at an attacker's server.

  windows: C:\Program Files\Phoenix Code Control\phoenix_override_config.json
  mac    : /Library/Application Support/Phoenix Code Control/phoenix_override_config.json
  linux  : /etc/phoenix-code-control/phoenix_override_config.json

Only keys in OVERRIDABLE_KEYS are honoured, everything else in the file is
ignored. Reads fail open- a missing or malformed file just means no
override. Desktop only, the file is never read in the browser.

Verified end to end on linux/electron: with the file in place the updater
fetched its manifest from localhost and showed the served release notes,
a non allowlisted key was dropped, and removing the file fell back to the
baked in url.
The marker is what makes a core module part of the public extension API
docs, which is easy to add by reflex to something meant to stay internal.
Overriding app_update_url alone was not enough to genuinely test an update
on linux. Windows and mac download and install the artifact named in the
manifest, but linux installs nothing from it- it pipes an installer script
into bash and that script decides what to install. So detection used the
overridden manifest while the install still fetched the production
installer, which reported a fake version and then installed the real build.

Add app_update_linux_installer_url to the allowlist and route the linux
install through it, so every shipped platform can now be tested end to end.

The url is taken as given. Whatever it points at is piped into bash anyway,
so sanitising the url would buy nothing- it can only come from our own
https constant or from a file that only root can create.

Two guards so this cannot hurt the update path:
- the config read is raced against a timeout. It is awaited at quit time,
  when the node process may already be gone, and a read that never settled
  would hang the quit rather than just the update.
- the read is warmed at boot, because a window that only inherits an
  already scheduled update never runs an update check and would otherwise
  first touch the disk at quit time.
Any failure reading the override falls back to the shipped default and the
update proceeds as normal.

Also add reference samples under appUpdater/unit-tests: an override config,
a fake update manifest and a fake installer that just prints "update done".
They point at each other over the dev server so they work as-is.
The cache manifest builder throws on any extension it has not been told
about, and appUpdater/unit-tests now carries a sample .sh installer.
Shell scripts are never fetched by the app, so they belong in the
disallowed list rather than the cache.
@sonarqubecloud

sonarqubecloud Bot commented Sep 6, 2026

Copy link
Copy Markdown

@abose
abose merged commit 0596c53 into main Sep 6, 2026
11 of 21 checks passed
@abose
abose deleted the ai branch September 6, 2026 15:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant