From eeb820da8ca47981e4b8e3268504281fde22d7b2 Mon Sep 17 00:00:00 2001 From: LiYao <2113239898x@gmail.com> Date: Wed, 30 Sep 2026 10:45:09 +0800 Subject: [PATCH 1/3] ci(release): make macOS signing opt-in for unsigned fork builds A tag push forced MACOS_SIGN_RELEASE to true, so the release pipeline always demanded the five Apple Developer secrets. This fork owns none of them, and the guard in "Require macOS signing and notarization secrets" additionally pins APPLE_TEAM_ID to the upstream maintainer's team (DUV63RKYTW) while CSC_NAME is hardcoded to their certificate common name, so no certificate obtained by this repository can satisfy the check. Both macOS matrix legs then failed, the build job failed with them, and the publish job was skipped by needs, leaving a release workflow that could not produce a Release on this fork. Signing is now opt-in: a tag push builds unsigned installers, and setting the repository variable MACOS_SIGN_RELEASE=true restores the sign-always behaviour once this repository owns a Developer ID certificate. workflow_dispatch keeps honouring its own sign_macos input, so the existing debug lane is unchanged. Unsigned macOS artifacts lose only the Developer ID seal and the notarization ticket; installer layout, artifact names, the updater feeds, and the publish job are all independent of signing. --- .github/workflows/release.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f81dc0e370..04a92254df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -86,8 +86,16 @@ jobs: name: Build ${{ matrix.name }} needs: verify env: - # Tag pushes always sign. workflow_dispatch signs unless explicitly disabled. - MACOS_SIGN_RELEASE: ${{ github.event_name != 'workflow_dispatch' || inputs.sign_macos == true }} + # Signing is opt-in on a fork. A tag push must not force signing: this + # repository owns no Apple Developer secrets, and the assertion in + # "Require macOS signing and notarization secrets" additionally pins + # APPLE_TEAM_ID to the upstream maintainer's team, so no locally obtained + # certificate can satisfy it. An unsigned tag build still publishes every + # installer; only the Developer ID seal and the notarization ticket are + # absent. Set the repository variable MACOS_SIGN_RELEASE=true to restore + # upstream's sign-always behaviour once this repository owns a + # Developer ID certificate. + MACOS_SIGN_RELEASE: ${{ (github.event_name == 'workflow_dispatch' && inputs.sign_macos == true) || vars.MACOS_SIGN_RELEASE == 'true' }} strategy: fail-fast: false matrix: From c7278ecdd5ab4981998f39c1ca3ab53f4a0d9e97 Mon Sep 17 00:00:00 2001 From: LiYao <2113239898x@gmail.com> Date: Wed, 30 Sep 2026 10:52:36 +0800 Subject: [PATCH 2/3] test(release): assert macOS signing is opt-in instead of tag-forced MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ci-workflow contract test pinned the old MACOS_SIGN_RELEASE expression, in which a tag push always signs. That guard is exactly what the release workflow change had to relax, so leaving it would have kept the pipeline red: the verify job runs the unit tests before any build job, and its failure skips publish. The test now pins the new contract instead: the opt-in expression must be present, the tag-forced form must be absent, and the existing assertions covering both macOS lanes are unchanged. Verified in both directions — it passes against the new expression and fails when the workflow is reverted to the old one, so the guard is not vacuous. --- apps/desktop/test/ci-workflow.test.mjs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/desktop/test/ci-workflow.test.mjs b/apps/desktop/test/ci-workflow.test.mjs index b1124653fd..adbe18b1f4 100644 --- a/apps/desktop/test/ci-workflow.test.mjs +++ b/apps/desktop/test/ci-workflow.test.mjs @@ -208,16 +208,26 @@ test("release matrix packages both native macOS architectures", () => { assert.match(releaseWorkflowSource, /Merge macOS updater metadata[\s\S]*?ruby/); }); -test("macOS release signing is required on tag pushes", () => { +test("macOS release signing is opt-in and workflow_dispatch still honours sign_macos", () => { assert.match( releaseWorkflowSource, /workflow_dispatch:\s+inputs:\s+sign_macos:[\s\S]*?default:\s*true[\s\S]*?type:\s*boolean/, ); + // A tag push must not force signing: a fork owns no Apple Developer + // secrets, and the guard below pins APPLE_TEAM_ID to the upstream + // maintainer's team, so no locally obtained certificate can satisfy it. + // Signing is opted into with the repository variable, and the manual + // dispatch lane keeps its own sign_macos input. assert.ok( releaseWorkflowSource.includes( - "MACOS_SIGN_RELEASE: ${{ github.event_name != 'workflow_dispatch' || inputs.sign_macos == true }}", + "MACOS_SIGN_RELEASE: ${{ (github.event_name == 'workflow_dispatch' && inputs.sign_macos == true) || vars.MACOS_SIGN_RELEASE == 'true' }}", ), ); + assert.doesNotMatch( + releaseWorkflowSource, + /MACOS_SIGN_RELEASE: \$\{\{ github\.event_name != 'workflow_dispatch'/, + "a tag push no longer forces macOS signing", + ); const unsignedBlock = releaseWorkflowSource.match( /- name: Package unsigned macOS installer[\s\S]*?(?=\n - name:)/, From 7a0160387f4ac1978cc2edb93a2b4df38243c13e Mon Sep 17 00:00:00 2001 From: LiYao <2113239898x@gmail.com> Date: Wed, 30 Sep 2026 11:42:35 +0800 Subject: [PATCH 3/3] feat(settings): give vendor accounts the enable switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A vendor subscription account had no enable switch on its service row, so the only way to stop an account's models from being offered was to remove the account and sign in again. Every layer below the row already treated a disabled provider the same way it treats an API service: `providers.list` with `includeDisabled=false` drops it from the model picker, session launch fails with `PROVIDER_DISABLED`, and the settings list still shows the row with a disabled badge so it can be turned back on. Only the UI entry point was missing, which made the account look like it could not be switched off. The switch decision moves into a pure `serviceRowToggle` helper next to the row's other rules, so which kinds get a switch — and which get a locked one — is covered by tests instead of by a condition in JSX. A plugin-declared row still has no switch, because the plugin owns that row and refreshes it from its manifest on every load; that is now stated in the spec rather than only in a comment. The source-text assertion in settings-general.test.mjs followed the old condition and is repointed at the helper call; the behavior it stood for is asserted directly in service-row-status.test.mjs. fixes #930 --- .../src/components/settings/ServiceRow.tsx | 10 +++--- .../components/settings/service-row-status.ts | 20 ++++++++++++ apps/desktop/test/service-row-status.test.mjs | 32 +++++++++++++++++++ apps/desktop/test/settings-general.test.mjs | 9 ++++-- .../03-runtime/11-provider-model-system.md | 7 +++- .../03-runtime/11-provider-model-system.md | 5 ++- 6 files changed, 72 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/components/settings/ServiceRow.tsx b/apps/desktop/src/components/settings/ServiceRow.tsx index ab786d18c1..32bcbaab56 100644 --- a/apps/desktop/src/components/settings/ServiceRow.tsx +++ b/apps/desktop/src/components/settings/ServiceRow.tsx @@ -16,9 +16,9 @@ import { CapabilityRowMenu, type CapabilityMenuItem } from "./AgentCapabilityLay import { ServiceMonogram } from "./ServiceMonogram"; import { serviceRowBadges, - serviceRowKind, serviceRowMeta, serviceRowTitle, + serviceRowToggle, } from "./service-row-status"; import type { AccountEntry } from "./useVendorAccounts"; @@ -70,10 +70,10 @@ export function ServiceRow({ reorderEvents, }: ServiceRowProps) { const { t } = useTranslation(); - const kind = serviceRowKind(provider); const title = serviceRowTitle(provider, entry, t); const badges = serviceRowBadges(provider, { isDefault, entry }, t); const meta = serviceRowMeta(provider, t); + const toggle = serviceRowToggle(provider, busy); // Read at pointerdown, before the outside press has closed the menu. const menuWasOpen = useRef(false); const { onPointerDown, onClickCapture, onKeyDown, ...reorderAttributes } = reorderEvents; @@ -167,13 +167,11 @@ export function ServiceRow({