Skip to content

refactor(platform): consolidate build-channel config into one table + fail closed - #385

Open
pnewsam wants to merge 2 commits into
stagingfrom
paul/developer-platform-channels
Open

refactor(platform): consolidate build-channel config into one table + fail closed#385
pnewsam wants to merge 2 commits into
stagingfrom
paul/developer-platform-channels

Conversation

@pnewsam

@pnewsam pnewsam commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What this does

Consolidates the build-"channel" configuration that had drifted across four files into a single source of truthsrc/main/channels.ts — and hardens build-kind resolution to fail closed.

Before, each concern resolved the build kind on its own:

  • cowork-home.ts → per-kind data home (~/.cowork-<kind>)
  • minds-urls.ts → MindsHub API host
  • server-source.ts / server-updater.ts → cowork-server ref + PyPI channel
  • gen-build-channel.mjs (CI) → baked env

Nothing tied them together, so they could — and during this branch's own rebase, demonstrably did — drift out of sync. This PR replaces those scattered switch (buildKind()) sites with one CHANNELS table that every layer reads and that CI validates against.

Contents

  • CHANNELS table (channels.ts) — one row per kind (dev / preview / stable / prod) mapping home dir, API host, env slug, and server ref. Both the runtime and CI derive from it.
  • Fail-closed resolution — a present-but-broken / unrecognized build-config.json now throws instead of silently falling back to prod (which would point a non-prod build at the production data home / API). A genuinely absent config still resolves to prod, so legacy releases keep working.
  • Build-time guardgen-build-channel.mjs validates the baked build_kindminds_api_url pairing; channel-origins.mjs is a plain-JS mirror of the table (build scripts can't import the TS) with a drift-guard unit test so the mirror can't silently go stale.
  • Per-channel isolation refinements — server binary, logs, and OTA UI cache keyed off the isolated home.

Context: overlap with already-merged work

The original "distinct envs / correct APIs / complete data isolation" goals were largely delivered independently while this PR was open — per-build data isolation (#312), build-kind → environment + PyPI channel (#461), renderer origin-derivation (#493). This PR is rescoped to what it still uniquely adds on top of that: the single-table consolidation, the fail-closed policy, and the drift guard.

Test plan

  • npm run typecheck (main + renderer + test) ✅
  • npm test — 629 passing ✅
  • npm run check:cowork-purity

🤖 Generated with Claude Code

pnewsam

This comment was marked as outdated.

pnewsam added a commit that referenced this pull request Jul 16, 2026
Address the three Major review comments on the channel model plus their
build-time mirror:

- normalizeBuildKind FAILS CLOSED on an explicitly present but
  unrecognized build kind (throws) instead of silently resolving to prod
  — a typo could otherwise point non-prod code at the production data
  home / API. An absent value still means prod (legacy builds).
  resolveBuildKind reads build-config.json in a guard that swallows only
  FILE errors, so a present-but-invalid value still aborts.
- checkChannelConsistency compares full API origins, not the env slug, so
  an unknown/mistyped host (e.g. https://example.com) is no longer
  indistinguishable from prod. An empty host stays the intentional prod
  default. gen-build-channel.mjs mirrors both changes at build time.
- minds-urls derives the main-process API host from the resolved channel
  (CHANNELS[buildKind()].apiHost) as its final fallback, so a clean
  `npm run dev` talks to the dev backend instead of silently falling back
  to prod while the renderer is on dev. prod is unchanged in every case.
- server-source sources the per-kind server ref from CHANNELS[kind]
  .serverRef (the table is now authoritative). This also fixes rename
  fallout: the old `kind === 'stable'` check no longer compiled, and a
  `staging` build would have fallen back to installing the server from
  main.
- buildKind() tolerates a missing electron `app` (tests/tooling) and
  resolves dev instead of throwing; prod is byte-for-byte identical.
- Stale-home migration failures (~/.cowork-stable → ~/.cowork-staging)
  are logged instead of silently swallowed, via a testable migrateHomeDir
  helper.

New/updated tests: channels (fail-closed + unknown-host), minds-urls
(clean-dev resolution), cowork-home (filesystem migration incl.
rename-failure), server-source (stable→staging).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@pnewsam
pnewsam force-pushed the paul/developer-platform-channels branch from 106200d to 8092821 Compare July 16, 2026 16:32
@pnewsam

This comment was marked as outdated.

pnewsam added a commit that referenced this pull request Jul 16, 2026
Address the three Major review comments on the channel model plus their
build-time mirror:

- normalizeBuildKind FAILS CLOSED on an explicitly present but
  unrecognized build kind (throws) instead of silently resolving to prod
  — a typo could otherwise point non-prod code at the production data
  home / API. An absent value still means prod (legacy builds).
  resolveBuildKind reads build-config.json in a guard that swallows only
  FILE errors, so a present-but-invalid value still aborts.
- checkChannelConsistency compares full API origins, not the env slug, so
  an unknown/mistyped host (e.g. https://example.com) is no longer
  indistinguishable from prod. An empty host stays the intentional prod
  default. gen-build-channel.mjs mirrors both changes at build time.
- minds-urls derives the main-process API host from the resolved channel
  (CHANNELS[buildKind()].apiHost) as its final fallback, so a clean
  `npm run dev` talks to the dev backend instead of silently falling back
  to prod while the renderer is on dev. prod is unchanged in every case.
- server-source sources the per-kind server ref from CHANNELS[kind]
  .serverRef (the table is now authoritative). This also fixes rename
  fallout: the old `kind === 'stable'` check no longer compiled, and a
  `staging` build would have fallen back to installing the server from
  main.
- buildKind() tolerates a missing electron `app` (tests/tooling) and
  resolves dev instead of throwing; prod is byte-for-byte identical.
- Stale-home migration failures (~/.cowork-stable → ~/.cowork-staging)
  are logged instead of silently swallowed, via a testable migrateHomeDir
  helper.

New/updated tests: channels (fail-closed + unknown-host), minds-urls
(clean-dev resolution), cowork-home (filesystem migration incl.
rename-failure), server-source (stable→staging).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@pnewsam
pnewsam force-pushed the paul/developer-platform-channels branch from 8092821 to ba6e6f4 Compare July 16, 2026 16:44
pnewsam

This comment was marked as outdated.

pnewsam

This comment was marked as outdated.

@hamishfagg hamishfagg left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approving devops changes

pnewsam added a commit that referenced this pull request Jul 21, 2026
Address the three Major review comments on the channel model plus their
build-time mirror:

- normalizeBuildKind FAILS CLOSED on an explicitly present but
  unrecognized build kind (throws) instead of silently resolving to prod
  — a typo could otherwise point non-prod code at the production data
  home / API. An absent value still means prod (legacy builds).
  resolveBuildKind reads build-config.json in a guard that swallows only
  FILE errors, so a present-but-invalid value still aborts.
- checkChannelConsistency compares full API origins, not the env slug, so
  an unknown/mistyped host (e.g. https://example.com) is no longer
  indistinguishable from prod. An empty host stays the intentional prod
  default. gen-build-channel.mjs mirrors both changes at build time.
- minds-urls derives the main-process API host from the resolved channel
  (CHANNELS[buildKind()].apiHost) as its final fallback, so a clean
  `npm run dev` talks to the dev backend instead of silently falling back
  to prod while the renderer is on dev. prod is unchanged in every case.
- server-source sources the per-kind server ref from CHANNELS[kind]
  .serverRef (the table is now authoritative). This also fixes rename
  fallout: the old `kind === 'stable'` check no longer compiled, and a
  `staging` build would have fallen back to installing the server from
  main.
- buildKind() tolerates a missing electron `app` (tests/tooling) and
  resolves dev instead of throwing; prod is byte-for-byte identical.
- Stale-home migration failures (~/.cowork-stable → ~/.cowork-staging)
  are logged instead of silently swallowed, via a testable migrateHomeDir
  helper.

New/updated tests: channels (fail-closed + unknown-host), minds-urls
(clean-dev resolution), cowork-home (filesystem migration incl.
rename-failure), server-source (stable→staging).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@pnewsam
pnewsam force-pushed the paul/developer-platform-channels branch from fe8cfc9 to ac55d19 Compare July 21, 2026 00:16
pnewsam added a commit that referenced this pull request Jul 28, 2026
Address the three Major review comments on the channel model plus their
build-time mirror:

- normalizeBuildKind FAILS CLOSED on an explicitly present but
  unrecognized build kind (throws) instead of silently resolving to prod
  — a typo could otherwise point non-prod code at the production data
  home / API. An absent value still means prod (legacy builds).
  resolveBuildKind reads build-config.json in a guard that swallows only
  FILE errors, so a present-but-invalid value still aborts.
- checkChannelConsistency compares full API origins, not the env slug, so
  an unknown/mistyped host (e.g. https://example.com) is no longer
  indistinguishable from prod. An empty host stays the intentional prod
  default. gen-build-channel.mjs mirrors both changes at build time.
- minds-urls derives the main-process API host from the resolved channel
  (CHANNELS[buildKind()].apiHost) as its final fallback, so a clean
  `npm run dev` talks to the dev backend instead of silently falling back
  to prod while the renderer is on dev. prod is unchanged in every case.
- server-source sources the per-kind server ref from CHANNELS[kind]
  .serverRef (the table is now authoritative). This also fixes rename
  fallout: the old `kind === 'stable'` check no longer compiled, and a
  `staging` build would have fallen back to installing the server from
  main.
- buildKind() tolerates a missing electron `app` (tests/tooling) and
  resolves dev instead of throwing; prod is byte-for-byte identical.
- Stale-home migration failures (~/.cowork-stable → ~/.cowork-staging)
  are logged instead of silently swallowed, via a testable migrateHomeDir
  helper.

New/updated tests: channels (fail-closed + unknown-host), minds-urls
(clean-dev resolution), cowork-home (filesystem migration incl.
rename-failure), server-source (stable→staging).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pnewsam added a commit that referenced this pull request Jul 28, 2026
… code

Rebasing #385 onto staging surfaced three references to the pre-rename
`stable` build kind in code that only exists on staging (added by #461's
PyPI flooring and #503's start-budget work, which the PR branched before):

- server-updater.ts `includePrereleases()` gated the rc pre-release stream
  on `kind === 'stable'`; after the rename buildKind() never returns that,
  so staging builds would silently stop tracking rc wheels (also a TS2367).
- server-process.test.ts mocked `./uv-paths` with `getCoworkServerBinary`,
  but the merged server-process.ts resolves the binary via the PR's
  `coworkServerBinCandidates()` — the stale mock broke 15 Windows-reap tests.
- minds-urls.workflows.test.ts `HOST_FOR_KIND` keyed the staging ring under
  `stable`, so the renamed `build_kind: staging` failed its host guard.

Behaviour-only alignment to the canonical `staging` kind; no logic change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pnewsam
pnewsam force-pushed the paul/developer-platform-channels branch from 51d6d1b to 98766c3 Compare July 28, 2026 18:45
pnewsam added a commit that referenced this pull request Jul 28, 2026
…rget (rescope)

Rescopes #385 to what it uniquely adds on top of today's staging — the
canonical CHANNELS table, fail-closed build-kind resolution, and the
build-script drift-guard — and backs out two decisions that the merged
#312/#461/#493 work made unnecessary or contentious:

- Keep the build kind named `stable` (not `staging`). staging already ships
  `stable` (workflows, ~/.cowork-stable home, docs); the rename was pure churn
  plus a data-home migration, for a naming-consistency win with no user-facing
  meaning. CHANNELS is simply keyed `stable`; env references (api.staging,
  envSlug 'staging', serverRef 'staging', the `staging` branch) are unchanged.
  Removes LEGACY_KIND_ALIASES and the ~/.cowork-stable→~/.cowork-staging
  migration (migrateStaleChannelHome / migrateHomeDir) with them.
- Keep `dev` builds pointed at the staging API (CHANNELS.dev.apiHost), matching
  staging's existing behaviour, instead of retargeting them at api.dev. The
  renderer mirror and its tests revert to staging's version as a result.

Kept from #385: the CHANNELS single source of truth (drift-proofing the
scattered build-kind switches that this very rebase caught drifting), the
fail-closed policy on a present-but-broken build kind, and the
channel-origins.mjs mirror + drift test. Full suite green (629 tests),
typecheck + cowork-purity clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pnewsam pnewsam changed the title feat(platform): coherent local build channels — distinct envs, correct APIs, complete data isolation refactor(platform): consolidate build-channel config into one table + fail closed Jul 28, 2026
@pnewsam
pnewsam force-pushed the paul/developer-platform-channels branch 3 times, most recently from 8788d47 to 4466efd Compare July 28, 2026 20:08
pnewsam and others added 2 commits July 28, 2026 13:13
…ble (fail closed)

The build "kind" (dev / preview / stable / prod) drives five things — the
isolated data home, the MindsHub API host, the env slug stamped on the server,
the default cowork-server ref, and the PyPI channel — and each was resolved by
its own `switch (buildKind())` in a different file, free to drift out of sync
(this branch's own rebase caught exactly that drift). Replace those scattered
sites with a single source of truth.

- channels.ts: the CHANNELS table (one row per kind → home dir, API host, env
  slug, server ref) plus normalizeBuildKind / checkChannelConsistency. Build-kind
  resolution now FAILS CLOSED: a present-but-broken or unrecognized kind throws
  instead of silently resolving to prod (which would point a non-prod build at
  the production data home and API). A genuinely absent config still resolves to
  prod, so legacy releases keep working.
- cowork-home, minds-urls, server-source, uv-paths, ui-updater, index,
  server-process, minds-auth: resolve their per-kind value through CHANNELS
  rather than a local switch.
- channel-origins.mjs (+ .d.mts): a plain-JS mirror of the table for build
  scripts that can't import the TS, guarded by a drift unit test in
  channels.test.ts so the mirror can't silently go stale.
- gen-build-channel.mjs: validates the baked build_kind ↔ minds_api_url pairing
  at build time against that same mirror.

The build kind stays named `stable` and `dev` keeps targeting the staging API:
the stable→staging rename and the dev→api.dev retarget are intentionally NOT
part of this change (staging already ships per-build data isolation, build-kind
→ environment resolution, and renderer origin-derivation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…odel

- build-macos-pkg.yml / build-windows-installer.yml pass COWORK_BUILD_KIND so
  gen-build-channel.mjs's build-time guard fails a build whose kind and
  minds_api_url target different environments (e.g. preview baked against prod).
- README documents the four build kinds and the OTA/build-kind gating.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pnewsam
pnewsam force-pushed the paul/developer-platform-channels branch from 4466efd to 6c9db65 Compare July 28, 2026 20:14
pnewsam added a commit that referenced this pull request Jul 28, 2026
…, dock icon

Give each non-prod build kind a distinct RUNTIME identity so build kinds on one
machine don't collide, building on the CHANNELS table from #385:

- channels.ts: add per-kind `appName` (the Electron app name → userData dir) and
  `iconName` (the window/dock icon). prod is frozen to `anton` / `icon.png` so
  shipped installs keep their data and look unchanged.
- app-identity.ts: a side-effect module imported FIRST in index.ts — calls
  app.setName for non-prod kinds only (before token-store reads userData at load)
  so localStorage, terms consent, and the token store isolate per channel. prod's
  app.name is never touched → byte-for-byte unchanged.
- keychain-service.ts: namespace the keychain service per channel
  (`cowork-oauth-<kind>`); prod keeps the historical `cowork-oauth`.
- app-icon.ts: pick the badged window/dock icon at runtime (the packaged bundle
  icon alone doesn't govern the running app's dock icon), falling back to the
  base icon.png if the asset is missing.

The `stable` kind targets the staging env (see #385), so its user-facing labels
read "Staging"; its invisible appId stays keyed to the kind.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants