diff --git a/devlog/_plan/260903_muse_release_train/000_plan.md b/devlog/_plan/260903_muse_release_train/000_plan.md new file mode 100644 index 0000000000..7891f4c6ff --- /dev/null +++ b/devlog/_plan/260903_muse_release_train/000_plan.md @@ -0,0 +1,77 @@ +# 260903 — Muse release train: regression review, provider mark, v2.41.0 + +## Why this unit exists + +`origin/dev` is 36 commits ahead of `origin/main`, which still carries +`v2.40.0` (published 2026-09-02). Three of those commits are the Meta/Muse +line that landed today: + +- `878f75417` (#3317) — Muse Spark 1.3 registered on the 1.2 spec. +- `ff1ac6b8c` (#3321) — the direct Meta Model API provider (`meta-model`). +- `1aa839aa8` (#3337) — the `meta-muse` provider importing the Muse Code CLI + credential behind a Terms-of-Service warning. + +The user asked for three things, in order: regression-review the 36-commit +delta against `main`, give Muse a provider mark in the dashboard, and run the +release through to a published Meta/Muse-carrying version. + +## Constraints that shape every phase + +- **No local full suite.** `bun run test` and a bare `bun test` are forbidden + for this unit. Verification is focused `bun test `, `bun run typecheck`, + and exact-SHA GitHub CI (`ci.yml` + `service-lifecycle.yml`). +- That constraint is load-bearing on the release path. `scripts/release.ts` + runs the whole suite in its preflight, so the helper cannot be used here. + The release therefore takes the manual path the helper would otherwise + automate: bump on the release branch, wait for both exact-SHA workflows, + then `gh workflow run release.yml` with `version`/`tag`/`expected-sha`. +- `--no-verify` pushes are authorized; PRs target `dev` and merge with admin + once CI is green. +- `main` and `preview` carry rulesets requiring a pull request. Promotion is + by PR, not by push. + +## Work phases + +| Phase | Doc | Deliverable | +|-------|-----|-------------| +| wp0 | this unit | roadmap + review method (docs only) | +| wp1 | `010_wp1_regression_review.md` | per-commit regression record for all 36 commits | +| wp2 | `020_wp2_muse_mark.md` | Meta/Muse SVG + `provider-icons.ts` wiring | +| wp3 | `030_wp3_preview_release.md` | `preview` dist-tag publish, exact-SHA proof | +| wp4 | `040_wp4_main_release.md` | `latest` dist-tag publish, ancestry proof | + +wp1 and wp2 are independent of each other and both gate wp3. wp4 consumes +wp3's published preview. + +## Review method (wp1) + +A 36-commit delta is too large to re-derive from scratch, and re-reading every +diff line would produce a document nobody checks. The review is risk-classed +instead, and the class decides what evidence is required: + +- **R0 docs-only** — `devlog/` or `docs-site/` only. Evidence: the diff touches + no runtime path. No test needed. +- **R1 scoped runtime** — one subsystem, covered by a focused test file that + already exists. Evidence: the focused test passes at the dev head. +- **R2 cross-cutting** — touches routing, the model catalog, release + automation, or a shared contract. Evidence: focused tests plus a read of the + seam the change crosses. +- **R3 credential/security** — auth, tokens, OAuth, keychain, workflow + permissions. Evidence: line-level read of the credential path plus + `privacy:scan`. + +The Muse commits are R2 (#3317, #3321) and R3 (#3337). + +Three more are R3, corrected after audit round 1 (`005`): `7ce0ba518` (#3262) +grants `contents: write` and `pull-requests: write` to a reusable-workflow +call, `7a529a2e8` (#3318) changes `pull_request_target` processing — a declared +trust boundary in `.github/AGENTS.md` — and `3c7c021ec` (#3296) touches +provider credential admission. A workflow-permission grant is a credential +change even when the diff reads like plumbing, which is the hole the first +draft of this table had. + +## What "done" means here + +`main` carries the reviewed dev SHA, npm `latest` resolves to the stable +version built from it, and `ocx` users installing fresh get Muse Spark 1.3 +plus both Meta providers with a real mark in the dashboard. diff --git a/devlog/_plan/260903_muse_release_train/005_audit_round1_synthesis.md b/devlog/_plan/260903_muse_release_train/005_audit_round1_synthesis.md new file mode 100644 index 0000000000..22124bef94 --- /dev/null +++ b/devlog/_plan/260903_muse_release_train/005_audit_round1_synthesis.md @@ -0,0 +1,84 @@ +# Audit round 1 — synthesis + +Reviewer: delegated read-only auditor (gpt-5.6-sol, high). Verdict: **FAIL**, +seven findings. Every one was re-derived against the tree before folding; the +outcome is six folded and one rebutted-with-a-carve-out. + +## 1. "ToS enforcement is bypassable" — REBUTTED as a release blocker, RECORDED as a known limit + +The reviewer is right about the mechanism and wrong about what it means for +this release. + +The mechanism, confirmed: `loginMetaMuse` emits its warning through the +optional `ctrl.n` progress callback (`src/oauth/meta-muse.ts:128`). The CLI's +own OAuth path wires that to `console.log` (`src/oauth/login-cli.ts:87`), so +`ocx login meta-muse` prints it. The management API's flow, by contrast, +installs `n: () => {}` (`src/oauth/index.ts:1720`) and drops it on the floor — +which means `POST /api/oauth/login` and `ocx account login meta-muse`, which +goes through that same endpoint, never surface the warning text. The GUI shows +`OAuthTosWarningModal` client-side, so the acknowledgement is enforced by the +browser, not by the server. + +Why it does not block: + +- **It is not a regression and not Muse-specific.** `n: () => {}` predates this + work by a long way, and `anthropic` and `google-antigravity` — the other two + `HIGH_RISK` ids in `gui/src/oauth-tos-risk.ts:10` — have carried exactly the + same client-side-only gate since `fbac9f05e`. Shipping v2.41.0 changes the + exposure for none of them. +- **The credential path itself is clean.** The reviewer looked for a leak and + found none: no Keychain stderr surfaced, no response bodies in errors, a fixed + public error vocabulary, atomic 0600 persistence. +- **The bypass requires the user's own admin token.** `/api/oauth/login` is + behind management auth. The actor who can call it is the account holder, who + is the only party the ToS warning protects, and who has already installed and + signed into the Muse Code CLI on that machine. + +What it is: a real server-side consent gap across all three high-risk +providers, worth its own unit. It is recorded here and in +`050_followups.md` rather than folded into a release cycle, because a +backend consent boundary is a behaviour change for `anthropic` and +`google-antigravity` users too, and that does not belong in a release train +the user asked to ship today. + +## 2-5. Release-path corrections — FOLDED + +All four are correct and all four are now in the phase docs: + +- **Version availability before the bump.** `scripts/release.ts:513` checks + unused-version and channel-forward ordering BEFORE mutating anything; the + workflow's own duplicate check at `release.yml:303` runs only after dispatch. + Doing this by hand means proving the version unused first, not discovering it + from a failed publish. Live state at audit time: `latest=2.40.0`, + `preview=2.40.0-preview.20260902`, `2.41.0` unused. +- **Exact-SHA is stricter than "CI passed".** `release-dispatch-guard.cjs:14` + requires a lowercase 40-char SHA, an allowed ref, a `workflow_dispatch` + event, and equality with `GITHUB_SHA`; `release.yml:222` requires a + successful **push-event** CI run on the release branch — PR CI does not + satisfy it. +- **`dev` already carries `2.41.0`.** `package.json:3`. The main bump in the + original 040 was a no-op step; promotion carries the version with it. The + post-release workflow is `dev-version-bump.yml`, and its PR moves `dev` to + `2.42.0`. +- **Publishing is OIDC Trusted Publishing.** `release.yml:119` (`id-token: + write`), `:153` (npm >= 11.5.1), `:285`. No `NPM_TOKEN`; verify provenance + and `gitHead` after publish. + +## 6. Risk classification — FOLDED + +`7ce0ba518` (#3262) grants `contents: write` + `pull-requests: write` +(`release.yml:67`) and `7a529a2e8` (#3318) changes `pull_request_target` +processing, a declared trust boundary (`.github/AGENTS.md:16`). Both move R2 -> +R3. `3c7c021ec` (#3296) touches provider credential admission and also gets R3. + +## 7. Icon wiring — FOLDED + +The set is `MASKED_PROVIDER_ICONS` (`gui/src/provider-icons.ts:188`), not +`MASKED_MARKS`; `020` named the client-side set by mistake. `meta.svg` carries +three gradients, so the masking question does not arise — the mark is colour and +is drawn as an image. Provenance goes in the asset README, and the two ids get +explicit assertions rather than relying on the generic wiring check. + +Reviewer's own non-blocking note, confirmed: no test enumerates every registry +provider's display name, and `tests/provider-workspace-data.test.ts` does not +need changing. diff --git a/devlog/_plan/260903_muse_release_train/010_wp1_regression_review.md b/devlog/_plan/260903_muse_release_train/010_wp1_regression_review.md new file mode 100644 index 0000000000..140b37598d --- /dev/null +++ b/devlog/_plan/260903_muse_release_train/010_wp1_regression_review.md @@ -0,0 +1,56 @@ +# wp1 — Regression review of the 36 dev-ahead-of-main commits + +## Input + +`git log --oneline origin/main..origin/dev` at the head recorded in +`000_plan.md`. Baseline is `v2.40.0`. + +## Method + +Each commit gets one row: SHA, PR, risk class (R0-R3 per `000_plan.md`), the +evidence actually checked, and a verdict of `clean`, `accepted`, or `blocker`. +A `blocker` must be fixed on `dev` before wp3 starts; an `accepted` row must +say why the residual risk is tolerable in a release. + +Evidence is gathered without the full suite: + +- `git show --stat ` for the touch set of every commit. +- For R1/R2, the focused test file that owns the subsystem, run individually. +- For R3, a line-level read of the credential handling plus + `bun run privacy:scan`. +- `bun run typecheck` once at the dev head covers the type-level seams that a + per-commit read would otherwise have to reason about by hand. + +## Special attention: the Meta/Muse line + +Three questions decide whether this release is safe to publish: + +1. **Does `meta-muse` ever write the imported credential anywhere a log or a + scan can see it?** #3337's follow-up (`81c1ebe8c` on the feature branch, + squashed into `1aa839aa8`) redacts scanned secrets and bounds the Keychain + read. Verify the redaction covers the error paths, not just the happy path. +2. **Can the ToS warning be bypassed?** The provider is deliberately marked + unsupported; the warning is the only thing standing between a user and an + unauthorized use of their Muse Code subscription. + + The verdict rule, so a later reader reaches the same decision this unit + did. A bypass is `UNSAFE` and blocks the release when it is EITHER of: + + - a **new** bypass introduced by a commit in this delta, or + - any path that **discloses the credential** (a log line, an error body, a + serialized config field). + + A bypass is **accepted** only when all three hold: it predates the delta, + it applies identically to the other `HIGH_RISK` providers rather than + singling out `meta-muse`, and it is recorded in `050_followups.md` with + the file:line evidence. That is exactly one case here — the client-side-only + acknowledgement on `POST /api/oauth/login` — and `005` §1 is why it + qualifies. Anything that does not meet all three is `UNSAFE`. +3. **Does Muse Spark 1.3 leak into a provider that cannot serve it?** #3317 + added 1.3 on the 1.2 spec across the resellers; the registry must not + advertise 1.3 on a provider whose upstream roster lacks it. + +## Output + +`011_review_ledger.md` — the per-commit table. Written in wp1's B phase, not +here. diff --git a/devlog/_plan/260903_muse_release_train/011_review_ledger.md b/devlog/_plan/260903_muse_release_train/011_review_ledger.md new file mode 100644 index 0000000000..94d4661b19 --- /dev/null +++ b/devlog/_plan/260903_muse_release_train/011_review_ledger.md @@ -0,0 +1,95 @@ +# wp1 — Per-commit regression review (origin/main..origin/dev) + +Baseline `v2.40.0` (`origin/main`). 36 commits. Risk classes and method: +`000_plan.md`, as corrected by audit round 1 (`005`). + +Focused suites run for this review, all at the dev head, none of them the full +suite (forbidden for this unit): + +| Batch | Files | Result | +|-------|-------|--------| +| Meta/Muse | `meta-muse-oauth`, `meta-model-api-provider`, `privacy-scan-meta-key`, `muse-spark-web-search-compat`, `opencode-go-muse-context`, `opencode-go-muse-vision`, `command-code-provider` | 97 pass / 0 fail | +| Combos + usage | `combos`, `kiro-pool-rank`, `server-combo-failover-e2e`, `usage-aggregate-cache`, `usage-ledger-scanner`, `usage-summary`, `api-key-attribution` | 274 pass / 0 fail | +| Cursor + catalog + CI | `cursor-catalog`, `cursor-claude-id`, `cursor-effort-rows`, `cursor-effort-table`, `cursor-display-names`, `cursor-discovery`, `codex-catalog`, `provider-config-batch-management`, `ci-workflows` | 500 pass / 0 fail | +| Responses + CLI + integrations | `responses-state`, `legacy-shell-compat`, `responses-custom-tool-repair`, `chat-completions-endpoint`, `claude-cli`, `cli-status-json`, `api-keys-routes`, `remote-catalog`, `client-connect`, `integrations-writer`, `grok-sync`, `codex-desired-state` | 498 pass / 0 fail | +| GUI marks | `provider-icons`, `provider-marks-assets`, `integration-marks` | 18 pass / 0 fail | + +Total 1387 focused assertions' worth of files, zero failures. Plus +`bun run typecheck` exit 0 and `bun run privacy:scan` passed at the dev head. + +## R3 — credential and workflow-permission changes + +| SHA | PR | What it does | Evidence | Verdict | +|-----|----|--------------|----------|---------| +| `1aa839aa8` | #3337 | `meta-muse` provider importing the Muse Code CLI credential | Line-level read of `src/oauth/meta-muse.ts`. The credential never reaches an error string: Keychain stderr is discarded, the `security` child is killed on timeout, a rejected key produces `HTTP ` with no body, and the format check refuses anything not matching the Meta key shape (see below the table). `refreshMetaMuseToken` deliberately does not re-read the Keychain, so a `muse login` with a different account cannot silently overwrite a stored slot. 27 tests. `privacy-scan-meta-key` covers the scanner. | clean | +| `7ce0ba518` | #3262 | grants `contents: write` + `pull-requests: write` to the `bump-dev-version` call | Full diff read: 8 added lines, all inside the one job. The grant equals what `dev-version-bump.yml`'s own job already declares — a reusable-workflow call cannot give the callee more than the caller holds, which is why both v2.40.0 dispatches died at `startup_failure`. No other job in the file gains anything, and the callee is a repository-local path, not a third-party action. | clean | +| `7a529a2e8` | #3318 | `missing_coauthor_credit` gate; changes `pull_request_target` processing | The new code runs in `enforce-pr-target.yml` and `pr-hygiene.yml`, both privileged contexts. It reads `pr.title`, `pr.body` and commit messages and passes them to `resolveReferencedAuthors`, which resolves them through the GitHub API — untrusted text is used as a lookup key, never interpolated into shell. `tests/ci-workflows.test.ts` (part of the 500-pass batch) asserts no dispatch input reaches shell source. Fail-open on lookup failure, capped at five per run. | clean | +| `3c7c021ec` | #3296 | atomic dashboard provider-editor save; provider field admission | `PROVIDER_CONFIG_FIELD_POLICY` in `src/server/auth-cors.ts` classifies every `OcxProviderConfig` field as `editor`, `redacted`, or `runtime`, with `satisfies Record` so a newly added field fails typecheck until classified. `apiKey` and `apiKeyPool` are `redacted`; MCP and desktop-executor blocks are redacted whole because both carry arbitrary env and headers. This is a tightening, not a loosening: it replaces an allowlist that had been inadequate. 356 lines of new tests in `provider-config-batch-management`. | clean | + +The key-shape check named in the `1aa839aa8` row, kept out of the table because +its two pipe characters are cell delimiters to a Markdown parser: + +``` +/^LLM\|\d+\|[A-Za-z0-9_-]{10,}$/ +``` + +## R2 — cross-cutting + +| SHA | PR | Seam crossed | Verdict | +|-----|----|--------------|---------| +| `878f75417` | #3317 | model catalog: Muse Spark 1.3 on the 1.2 spec across command-code and opencode-go | clean — 1.3 is registered only where the reseller actually serves it; `opencode-go` keeps only the contributor tier, matching its roster. Vision and context tests pin both. | +| `ff1ac6b8c` | #3321 | provider registry + pricing: the direct `meta-model` provider | clean — id chosen as `meta-model` specifically so it cannot capture the live `meta/` selector prefix at `router.ts`, and so it derives `META_MODEL_API_KEY` rather than the CLI's `META_API_KEY`. Parity test updated in the same commit. | +| `3d3c4fe26` | #3286 | model catalog across Antigravity, Google, sidecar | clean — closed out by its own devlog unit (`f0bbaaf6a`), catalog tests green. | +| `862e914c2` | #3274 | `/v1/models` row shape (`max_output_tokens`) | clean — contract tests updated across five files in the same commit. | +| `410a48a4f` | #3275 | Cursor Claude-id normalizer replacing three seeds | clean — 85 new assertions in `cursor-claude-id`, plus catalog and pricing tests. | +| `bc8ea072d` | #3273 | Cursor effort table read from the installed bundle; new `models-capabilities` input | clean — 117 assertions in `cursor-effort-table`, cached by bundle path, mtime and size, with a static fallback for a missing or malformed bundle. | +| `2ab9d9486` | #3276 | opt-in effort-variant rows; touches `server/index.ts`, `chat-completions.ts`, `claude-messages.ts`, `responses/core.ts` | clean — the widest seam in the Cursor group, and the one with the most new coverage: 314 assertions in `cursor-effort-rows`. Opt-in by config, so an operator who does not set it sees no row change. | +| `7ce713e8d` | #3277 | GUI Cursor tab shows effort-ladder provenance | clean — nine locales updated in the same commit and `locale-parity` extended, which is the check that would otherwise let a new string ship English-only. | +| `85d40ca35` | #3270 | usage aggregation rewritten to an incremental ledger scan | clean — the largest change in the delta (1570 lines in `usage/summary.ts`), and the one with the most new coverage: `usage-aggregate-cache` (301) and `usage-ledger-scanner` (498) are both new files. Management-API docs updated in the same commit. | +| `e9a5b0f13` | #3298 | combos fail over on provider-scoped quota caps; adds a `responses/core.ts` call site | clean — 51 new assertions in `combos`; the failover reads a cap it previously ignored, so the change can only widen the set of requests that survive. | +| `2e74a35d4` | #3302 | combo resolution skips exhausted provider quotas | clean — covered by `combos` plus a dedicated `server-combo-failover-e2e` scenario. | +| `6b2dfde11` | #3294 | shorter request-rate cooldowns; `Retry-After` on a combo 503 | clean — 53 new assertions; the 503 now carries the header a client needs to back off correctly, which is a strict improvement on an opaque 503. | +| `fd324dc88` | #3256 | Kiro reset-aligned cooldown without `Retry-After` | clean — 110 assertions in `kiro-pool-rank`; scoped to the Kiro pool's own ranking, and shares `combos/failover.ts` with the three rows above, all four verified together in the 274-pass batch. | +| `938c0136a` | #3246 | tool-bridge shape for `write_stdin` | clean — repair and undeclared-tool guards both extended. | +| `b3e205e99` | #3309 | integrations: hub clients routed through loopback | clean — a narrowing; three integration test files extended. | +| `ee24bab40` | #3269 | `service-lifecycle` triggers on `release.yml` | clean, and load-bearing for this very release: it is why a workflow-only change still trips the lifecycle gate. | +| `272ff6b11` | #3265 | moved `dev` to 2.41.0 after v2.40.0 | clean — this is the version the release train is about to publish. | + +## R1 — scoped runtime + +| SHA | PR | Subsystem | Evidence | Verdict | +|-----|----|-----------|----------|---------| +| `38f8a8164` | #3330 | Cursor picker keeps the `cursor/` slug for unbranded rows | `cursor-display-names` rewritten in the same commit; in the 500-pass batch | clean | +| `472c785c2` | #3308 | `ocx status` reports a reachable dashboard URL | `cli-status-json` +35 lines; in the 498-pass batch | clean | +| `906511f73` | #3310 | `connect` uses the catalog inactivity timeout | `remote-catalog` +50, `client-connect` +8; docs and skill page updated with it | clean | +| `eac662eb1` | #3307 | rotation creation time returned by the API-key route | `api-keys-routes` +40; a one-field addition to a response | clean | +| `4cf3e9187` | #3297 | liveness probes retried before `ocx claude` spawns a proxy | `claude-cli` +17; retry only, no new spawn path | clean | +| `34c9e9802` | #3289 | stops the background write storm on `responses-state.json` | `responses-state`; 9 lines in `src`, the rest devlog. A write-frequency reduction | clean | +| `b0a42ca2f` | #3254 | chat-native shares the transient send budget across recovery | `chat-completions-endpoint` +145 | clean | +| `fc08fc2f7` | #3290 | log panel no longer jitters as rows scroll in | GUI-only; `logs-auto-refresh` and `viewport-scroll-caps` extended | clean | +| `15b43e51c` | #3301 | provider-option E2E made hermetic | test-only; removes an external dependency from a test | clean | + +## R0 — docs only + +| SHA | PR | What | Verdict | +|-----|----|------|---------| +| `bb27c26be` | #3319 | contributor-credit unit closeout | clean — `devlog/` only | +| `af314b0a7` | #3311 | bug-drawdown campaign closeout | clean — `devlog/` only | +| `f0bbaaf6a` | #3292 | Gemini 3.8 rollout closeout | clean — `devlog/` only | +| `529639a57` | #3278 | Cursor Private Inference guide | clean — `docs-site/` only | +| `345e2175c` | #3272 | Cursor bundle effort-table roadmap | clean — `devlog/` only | +| `7424719ab` | #3267 | Windows CI repair and v2.40.0 outcome | clean — `devlog/` only | + +Each R0 diff was checked with `git show --stat` to confirm it touches no path +outside `devlog/` or `docs-site/`; nothing in the build, typecheck, or test +path reads from either. + +## Findings + +**No blockers.** One accepted residual, carried from audit round 1 (`005` §1) +and detailed in `050_followups.md`: the Terms-of-Service acknowledgement for +`HIGH_RISK` OAuth providers is enforced client-side, so `POST +/api/oauth/login` and `ocx account login` do not surface it. Accepted for this +release because it predates the delta, applies identically to `anthropic` and +`google-antigravity`, sits behind management auth, and involves no credential +disclosure. Publishing v2.41.0 does not change that exposure for anyone. diff --git a/devlog/_plan/260903_muse_release_train/020_wp2_muse_mark.md b/devlog/_plan/260903_muse_release_train/020_wp2_muse_mark.md new file mode 100644 index 0000000000..e9f775bcd2 --- /dev/null +++ b/devlog/_plan/260903_muse_release_train/020_wp2_muse_mark.md @@ -0,0 +1,74 @@ +# wp2 — A Meta/Muse mark for the provider catalog + +## Current state + +`gui/src/provider-icons.ts` maps a provider id to a file under +`gui/public/provider-icons/`. Two ids landed today with no entry: + +- `meta-model` — the direct Meta Model API provider. +- `meta-muse` — the Muse Code CLI credential import. + +Neither has a row in `PROVIDER_ICON_ALIASES` or `PROVIDER_DISPLAY_NAMES`, so +the dashboard renders them with the generic fallback and an unlabelled id. +Every other first-class provider in that file carries both. + +## Change + +1. Add `gui/public/provider-icons/meta.svg` — the Meta infinity mark, lifted + from the `aria-label="Meta symbol"` inline SVG that `dev.meta.ai` renders in + its own navigation header, read through a signed-in browser session. This is + the vendor's first-party mark on the vendor's own developer console, which is + the same provenance standard every other entry in the asset README meets. + Meta publishes no `favicon.svg` (`dev.meta.ai/favicon.svg` and + `/icon.svg` both 404; the site's declared icon is a 32x32 `.ico`), so the + rendered header mark is the best available vector. + + Normalization applied, and nothing else: the three gradient ids are renamed + from React's generated `_r_d_`/`_r_e_`/`_r_f_` to stable + `meta-mark-a`/`-b`/`-c` (a generated id collides when several documents are + inlined), the presentational `height`/`width`/`role`/`aria-label` are + dropped in favour of the `viewBox`, and `xmlns` is added so the file stands + alone. Every `d` attribute and every stop colour is verbatim. +2. Alias both ids to it: + + ```ts + "meta-model": "meta.svg", + "meta-muse": "meta.svg", + ``` + +3. Add display names: + + ```ts + "meta-model": "Meta Model API", + "meta-muse": "Muse Code", + ``` + + `meta-muse` is named for what the user recognizes — the Muse Code + subscription whose credential it imports — not for its config id. +4. The mark carries three linear gradients in Meta brand blue + (#0064E0 -> #0278F1), so it does NOT join `MASKED_PROVIDER_ICONS` + (`gui/src/provider-icons.ts:188`) — that set is for single-ink neutral + artwork that vanishes against one theme, and masking would flatten a + gradient to one ink. `gui/tests/provider-marks-assets.test.ts` enforces both + directions, so this is checked rather than asserted. + + (The first draft of this doc named `MASKED_MARKS`, which is the client-side + set in `gui/src/components/integration-marks.ts`. Audit round 1 caught it.) + +## Verification + +- `bun run typecheck` (the alias maps are typed `Record`; a + duplicate key is a type-level no-op, so the real check is the test below). +- `bun test tests/provider-icons.test.ts tests/provider-marks-assets.test.ts` + from `gui/`. The generic checks already cover a missing file and an unwired + committed asset; an explicit assertion pins the two new ids by intent, the + way the MiniMax/MiMo rows are pinned. +- Provenance recorded in `gui/public/provider-icons/README.md`. That file is + the only place a later reader can learn where a mark came from, and an + undocumented asset is indistinguishable from an invented one. +- `tests/provider-workspace-data.test.ts` needs no change: nothing enumerates + every registry provider's display name (confirmed in audit round 1). + +## Out of scope + +Re-theming the catalog, touching other marks, and any docs-site asset. diff --git a/devlog/_plan/260903_muse_release_train/021_meta_mark_screenshot.png b/devlog/_plan/260903_muse_release_train/021_meta_mark_screenshot.png new file mode 100644 index 0000000000..c0b3da6920 Binary files /dev/null and b/devlog/_plan/260903_muse_release_train/021_meta_mark_screenshot.png differ diff --git a/devlog/_plan/260903_muse_release_train/030_wp3_preview_release.md b/devlog/_plan/260903_muse_release_train/030_wp3_preview_release.md new file mode 100644 index 0000000000..f1cb0b9132 --- /dev/null +++ b/devlog/_plan/260903_muse_release_train/030_wp3_preview_release.md @@ -0,0 +1,98 @@ +# wp3 — Preview release (`preview` dist-tag) + +## Why the helper cannot run + +`scripts/release.ts` preflight runs `bun test --isolate tests` plus seven +isolated files. The user forbade the local suite for this unit, so invoking +the helper would violate the constraint before it reached the bump. The helper +is not broken; it is simply out of bounds here. + +## The manual path + +Everything the helper does after its preflight is reproducible by hand, and +each step keeps its own gate. Steps 0 and 4 were added after audit round 1 +(`005`); without them this path is strictly weaker than the helper it replaces. + +0. **Prove the version is available BEFORE mutating anything.** The helper does + this at `scripts/release.ts:513` — unused on npm, no existing tag or GitHub + release, and greater than what the channel currently carries. The workflow's + own duplicate check (`release.yml:303`) fires only after dispatch and never + checks channel ordering, so skipping this means learning about a collision + from a failed publish with the bump already pushed. + + Four checks, each of which must FAIL THE STEP rather than merely print. A + command that only retrieves data is not a gate: + + ```bash + V=2.41.0-preview.YYYYMMDD + # 1. the exact version is unpublished + npm view "@bitkyc08/opencodex@$V" version 2>/dev/null && { echo "published"; exit 1; } + # 2. no git tag + git ls-remote --tags origin "refs/tags/v$V" | grep -q . && { echo "tag exists"; exit 1; } + # 3. no GitHub release + gh release view "v$V" >/dev/null 2>&1 && { echo "release exists"; exit 1; } + # 4. it moves the CHANNEL forward + npm view @bitkyc08/opencodex dist-tags --json # compare against .preview + ``` + + Check 4 is the one with no automated equivalent anywhere in the workflow: + `release.yml` will happily publish a version that moves `preview` + BACKWARDS, because its only duplicate check is exact-version equality. Read + the current `preview` tag and confirm the new version sorts after it under + semver. +1. Open a promotion PR from a branch **pinned to the reviewed SHA** (not the + moving `dev` ref) into `preview`, and merge it with admin. `preview` is + protected by a ruleset requiring a reviewed pull request, so promotion is by + PR; #3260/#3261 and #3123/#3125 are the precedent. Expect `enforce-target` + to flag the base — a promotion PR is exactly the case that check is not + written for — and record the admin bypass rather than waiting for green. +2. `dev` already carries `2.41.0` (`package.json:3`), so the preview channel + needs the prerelease suffix and nothing else: bump to + `2.41.0-preview.` in a second PR onto `preview`. `release.ts` + enforces the `-preview.` infix; the workflow enforces `version` equals + `package.json`. +3. Record the release SHA (`preview` head after the bump merges) as the full + lowercase 40-character hash. `release-dispatch-guard.cjs:14` rejects a short + or upper-case SHA outright. +4. Wait for `ci.yml` AND `service-lifecycle.yml` to succeed on that exact SHA, + **as push-event runs on `preview`** — `release.yml:222` will not accept the + PR-event run that produced the same tree. The bump touches `package.json`, + which is a service-lifecycle trigger path, and `release.yml`'s service gate + requires an already-successful lifecycle run for the release SHA, so + dispatching early races it. +5. Re-read the LIVE remote head (`git ls-remote origin preview`) and confirm it + still equals the release SHA. The helper does this immediately before + dispatch for a reason: `workflow_dispatch` resolves a mutable branch. +6. `gh workflow run release.yml --ref preview -f version= -f tag=preview + -f expected-sha= -f dry-run=false`. +7. Watch the run; verify `npm view @bitkyc08/opencodex dist-tags --json` moves + `preview`, and that the GitHub prerelease tag resolves to the release SHA. + +## Publishing is tokenless + +There is no `NPM_TOKEN` to supply and none may be introduced. Publication runs +under OIDC Trusted Publishing: `id-token: write` (`release.yml:119`), npm +>= 11.5.1 (`:153`), and an npm Trusted Publisher binding for this repository and +workflow (`:285`). A failure there is a registry-side configuration problem, +not something to route around with a credential. `concurrency: group: release` +is shared with the stable publish, so the two channels serialize. + +## Failure handling + +If the dispatch fails after the bump is already pushed, do not re-bump. Re-run +the failed workflow once, confirm the remote SHA did not move, and re-dispatch +with the same `expected-sha`. The `validate-dispatch` job refuses a dispatch +whose `expected-sha` does not equal `GITHUB_SHA`, which is exactly the guard +that makes a re-dispatch safe. + +That reuse is for a TRANSIENT failure — a runner fault, a flaked job, a race +with the lifecycle gate. If the publish actually reached the registry, the +version is spent: npm forbids republishing it, so the recovery is a new +version, not a retry. Check `npm view` before deciding which case you are in. + +## Note on the automatic dev bump + +`release.yml` calls `dev-version-bump.yml` after a non-dry-run publish. For a +preview publish it usually returns `changed=false` because `dev` already +carries the stable core. Expect that, and do not treat the skipped bump PR as +a failure. diff --git a/devlog/_plan/260903_muse_release_train/040_wp4_main_release.md b/devlog/_plan/260903_muse_release_train/040_wp4_main_release.md new file mode 100644 index 0000000000..3ee01946c5 --- /dev/null +++ b/devlog/_plan/260903_muse_release_train/040_wp4_main_release.md @@ -0,0 +1,54 @@ +# wp4 — Main release (`latest` dist-tag) and ancestry proof + +## Sequence + +0. Run the same four blocking checks as wp3 step 0 against `2.41.0`, re-run + from scratch because the preview publish happened in between: the exact + version unpublished on npm, no `v2.41.0` tag, no GitHub release, and the + version moving the `latest` dist-tag FORWARD under semver. Each must fail + the step, not merely print. The channel-forward check matters as much here + as on preview: `release.yml` compares only for exact-version duplication, + so nothing in CI would stop `latest` being moved backwards. +1. Open a promotion PR from a branch **pinned to the reviewed SHA** into + `main`; `main` is protected the same way `preview` is. Merge with admin, + recording the `enforce-target` bypass. +2. **No bump is needed.** `dev` already carries `2.41.0` (`package.json:3`), so + the promotion brings the stable version with it. The original draft of this + doc prescribed a bump PR; audit round 1 established it would be a no-op that + `npm version` rejects as "Version not changed". +3. Wait for exact-SHA `ci.yml` and `service-lifecycle.yml` success on the + `main` head, as **push-event** runs (`release.yml:222`), then re-read + `git ls-remote origin main` immediately before dispatch. +4. `gh workflow run release.yml --ref main -f version=2.41.0 -f tag=latest + -f expected-sha= -f dry-run=false`. + +## Proof required before claiming DONE + +- `npm view @bitkyc08/opencodex dist-tags --json` shows `latest` at the + published stable version. +- The published version carries npm provenance and a `gitHead` matching the + release SHA. Publication is tokenless OIDC Trusted Publishing + (`release.yml:119`, `:153`, `:285`); provenance is the artifact-side proof + that the tarball came from this workflow on this repository. +- `gh release view v2.41.0` exists and its tag resolves to the release SHA. +- `git fetch origin main` FIRST, then + `git merge-base --is-ancestor FETCH_HEAD` exits 0, with + `FETCH_HEAD` confirmed equal to the `expected-sha` that was dispatched. + This is the check that distinguishes "main moved" from "main carries the work + that was reviewed" — a green release run proves neither by itself. The fetch + is not optional: `git ls-remote` reads the remote without updating + `origin/main`, so an ancestry test against the un-refreshed remote-tracking + ref can pass or fail on history that is minutes stale. +- The Meta work is actually in the published artifact, not merely in the tag. + Download the tarball and confirm all three: the `meta-model` provider entry, + the `meta-muse` provider entry, and `meta.svg` in the packaged GUI assets. + Checking only one of them lets a release pass with a missing alias or a + missing asset. A tag pointing at the right SHA and a tarball built from it + are separate facts. + +## After publish + +`dev-version-bump.yml` (called by `release.yml`'s `bump-dev-version` job) +opens a PR moving `dev` to `2.42.0`. Merge it so `dev` does not sit on an +already-published version — that stale state is what #3265 had to repair after +v2.40.0. diff --git a/devlog/_plan/260903_muse_release_train/050_followups.md b/devlog/_plan/260903_muse_release_train/050_followups.md new file mode 100644 index 0000000000..ccc4ef033e --- /dev/null +++ b/devlog/_plan/260903_muse_release_train/050_followups.md @@ -0,0 +1,33 @@ +# Follow-ups this unit deliberately does not do + +## Server-side consent for high-risk OAuth providers + +Audit round 1 (`005` §1) established that the Terms-of-Service acknowledgement +for `HIGH_RISK` providers is enforced in the browser, not at the API boundary: + +- `gui/src/oauth-tos-risk.ts:10` lists `anthropic`, `google-antigravity`, and + `meta-muse`, and `OAuthTosWarningModal` gates the GUI button. +- `POST /api/oauth/login` performs no acknowledgement check, and the controller + it builds installs `n: () => {}` (`src/oauth/index.ts:1720`), so even the + provider's own warning text is discarded on that path. +- `ocx account login ` posts to that endpoint + (`src/cli/account-auth.ts:142`), so it inherits the gap. The older + `ocx login ` path does print the warning, because + `src/oauth/login-cli.ts:87` wires `n` to `console.log`. + +This is pre-existing and provider-wide, not introduced by the Muse work, which +is why it is not a v2.41.0 blocker. It is still a real gap and should get its +own unit: move the acknowledgement to the backend so every entry point is +covered, with the acknowledgement recorded per provider rather than per browser +session. + +The design question that unit has to answer first: an acknowledgement gate on +`/api/oauth/login` changes behaviour for `anthropic` and `google-antigravity` +logins that work today, so it needs a migration story rather than a flag flip. + +## Muse subscription usage display + +`050_wp5_passive_muse_quota.md` in the `260903_muse_spark_plan_oauth` unit +records that Meta emits subscription window usage inside streaming responses +and that OpenCodex does not yet read it. The provider note says so plainly. +Unchanged by this release. diff --git a/gui/public/provider-icons/README.md b/gui/public/provider-icons/README.md index 43b1e1a92e..1fc7c57857 100644 --- a/gui/public/provider-icons/README.md +++ b/gui/public/provider-icons/README.md @@ -222,3 +222,27 @@ neighbouring brand's, is a misattribution that outlives the commit. `zhipu-bigmodel` and `zhipu-bigmodel-coding` share `zai.svg`: Z.AI and BigModel are the same company, and the mainland console publishes only the wordmark. + +## Meta (2026-09-03) + +- `meta.svg` — the `aria-label="Meta symbol"` inline SVG that `dev.meta.ai` + renders in its own navigation header, read 2026-09-03 through a signed-in + browser session. Meta publishes no square vector at the conventional paths: + `dev.meta.ai/favicon.svg`, `/icon.svg` and `/logo.svg` all 404, and the + site's declared icon is a 32x32 `.ico` on `static.xx.fbcdn.net`. The rendered + header mark is therefore the first-party vector, taken from the developer + console the two providers actually belong to. + + Path data and gradient stops are verbatim. Three normalizations: React's + generated gradient ids (`_r_d_`, `_r_e_`, `_r_f_`) become + `meta-mark-a/-b/-c`, because a generated id collides when several marks are + inlined into one document — the same reason `minimax.svg` renamed its + `未命名的渐变_6`; the presentational `height`/`width`/`role`/`aria-label` + are dropped in favour of the `viewBox`; and `xmlns` is added so the file + stands alone. + + Wired to both `meta-model` (the direct Meta Model API provider) and + `meta-muse` (the Muse Code credential import). One brand, two credentials — + the same shape as the three Alibaba ids sharing `alibaba-color.svg`. + **Not masked:** three linear gradients in Meta brand blue + (#0064E0 -> #0278F1), and masking flattens a gradient to a single ink. diff --git a/gui/public/provider-icons/meta.svg b/gui/public/provider-icons/meta.svg new file mode 100644 index 0000000000..59d5570e57 --- /dev/null +++ b/gui/public/provider-icons/meta.svg @@ -0,0 +1 @@ + diff --git a/gui/src/provider-icons.ts b/gui/src/provider-icons.ts index 3192862648..b99cbacd8c 100644 --- a/gui/src/provider-icons.ts +++ b/gui/src/provider-icons.ts @@ -27,6 +27,8 @@ const PROVIDER_ICON_ALIASES: Record = { "kimi-code": "kimi-color.svg", kiro: "kiro-color.svg", "lm-studio": "lm-studio-color.svg", + "meta-model": "meta.svg", + "meta-muse": "meta.svg", mistral: "mistral-color.svg", minimax: "minimax.svg", "minimax-cn": "minimax.svg", @@ -121,6 +123,8 @@ const PROVIDER_DISPLAY_NAMES: Record = { "opencode-zen": "OpenCode Zen", mistral: "Mistral", groq: "Groq", + "meta-model": "Meta Model API", + "meta-muse": "Muse Code", alibaba: "Alibaba Coding Plan", "alibaba-token-plan": "Alibaba Token Plan", "alibaba-token-plan-intl": "Alibaba Token Plan (Intl)", diff --git a/gui/tests/provider-icons.test.ts b/gui/tests/provider-icons.test.ts index 5a0495c453..b0609255b8 100644 --- a/gui/tests/provider-icons.test.ts +++ b/gui/tests/provider-icons.test.ts @@ -74,3 +74,21 @@ test("the MiniMax and Xiaomi MiMo provider ids resolve to their brand's mark", ( // The precedent that makes the two above consistent rather than novel. expect(providerIconSrc("mimo-free")).toBe("/provider-icons/xiaomi-color.svg"); }); + +/* + * One brand, two credentials. + * + * `meta-model` is Meta's own pay-as-you-go Model API and `meta-muse` imports the + * Muse Code CLI's credential. They are separate providers with separate billing + * and separate ToS risk, but they are the same company's mark -- the same shape + * as the three Alibaba plan ids sharing one asset. + * + * Pinned explicitly rather than left to the generic wiring check above, because + * that check only fires when an asset named after the id is already committed. + * Neither id is `meta`, so a dropped alias row here would restore the fallback + * tile silently. + */ +test("both Meta provider ids resolve to the Meta mark", () => { + expect(providerIconSrc("meta-model")).toBe("/provider-icons/meta.svg"); + expect(providerIconSrc("meta-muse")).toBe("/provider-icons/meta.svg"); +});