Repository navigation
fix(service-settings,cloud-connection)!: Branding settings drive runtime-config under single; theme_mode and support_email retire; namespace withheld under walled postures - #24959
Conversation
… pins Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y Co-authored-by: Claude <noreply@anthropic.com>
…tings per request Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y Co-authored-by: Claude <noreply@anthropic.com>
…anding-runtime-config
📓 Docs Drift CheckThis PR changes 4 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 24 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 53cd2975c4b9183ede7e36bc69dd2d05d4fc1824 && git checkout 53cd2975c4b9183ede7e36bc69dd2d05d4fc1824
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f32d795ce511c0644a0bc2b110710f8a181d87bb d616d4d873375890e054174bafb82ff0dc53aa8d && git checkout -B drift-repro f32d795ce511c0644a0bc2b110710f8a181d87bb && git merge --no-ff d616d4d873375890e054174bafb82ff0dc53aa8d
node scripts/docs-audit/affected-docs.mjs --json f32d795ce511c0644a0bc2b110710f8a181d87bb
|
席位核对REWORK (patch round 1) · #24766 · The delivered half matches the ruling:
Accepted deviations:
The seat's answers to the three open questions are its implementation calls, not product questions:
Before ACCEPT the seat owes a contract-review-tier review of the narrowing: the settings accept set, and the namespace withheld under a wall. Generated by Claude Code |
…anding-runtime-config
… the brand only where branding is offered Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y Co-authored-by: Claude <noreply@anthropic.com>
… (TS2345 in check:test-typecheck) Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y Co-authored-by: Claude <noreply@anthropic.com>
…nding Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y Co-authored-by: Claude <noreply@anthropic.com>
…osition (Branding now travels with service-settings) Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Read-only: the card (body and ten comments), the PR body, its file list, the net diff against ① Derived judgmentsNamed RIGHT, each against the committed tree:
Named WRONG:
② Semver level
③ Boundary flags
What a PASS needs: the isolation dogfood suite re-pointed off Implemented-by: VERDICT: FAIL |
…anding-runtime-config
…ompany.legal_name, offered under a wall Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y Co-authored-by: Claude <noreply@anthropic.com>
…WORKSPACE_NAME; Setup docs name service-settings for Branding Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y Co-authored-by: Claude <noreply@anthropic.com>
…nvironment setting; the ADR-0087 reasoning says why the ledger carries nothing Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: A fresh record on the new head, over the full floor. Read-only: the card (body and twelve comments), the PR body as it now stands, its file list, the net diff against ① Derived judgmentsNamed RIGHT, each against the committed tree at
Named WRONG: none on this head. ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
席位核对ACCEPT · #24766 ·
|
Fixes #24766
Clause-②: no (narrowing)
What this implements
The maintainer's ruling B′ (
6293793948) with its amendment (6293825075), under the standing rule 「甲」 (#60156292005854).Under
single: runtime-config'sbrandingblock reads the Branding settings, per request.RuntimeConfigPlugin(packages/cloud-connection) reads thebrandingnamespace on everyGET /api/v1/runtime/config(and the legacy alias), so a save shows on the console's and the sign-in page's next load:productNamebranding.workspace_nameOS_PRODUCT_NAME, thenObjectOSlogoUrlbranding.logo_urlOS_LOGO_URLbrandColorbranding.accent_colorOS_BRAND_COLORsourcebutdefault: saved in Setup, or pinned by the settings layer's ownOS_BRANDING_*variable. That is the same ruleplugin-authalready applies toworkspace_name, so the two readers of that key agree. The manifest default (ObjectStack) is never served on this door.productShortName,pwaDescriptionandpwaThemeColorfollow the served name and colour unless configured themselves. Before, they were derived once at construction from the deployment values.brandColorkeeps its one meaning: the console's browser and installed-app theme colour (objectuigetPwaThemeColor), beside each app's ownAppBrandingcolours. No precedence rule is added.warnper run of failures. The door never answers 500 for it.brandingblock keeps its shape. No exported type or option changed:RuntimeConfigPluginConfiggained no member (JSDoc only).theme_modeandsupport_emailretire, manifest version 1 → 2, in #21970's shape.branding.manifest.ts, the four translation bundles and the README follow.accent_coloralso loses its#6366f1default: the page showed it as a colour the console never used, since an unset key is now served as the deployment's colour.workspace_name's auth readers keep working.plugin-authstill reads the key for sign-in mail, the authenticator issuer and MCP sign-in; its binding now runs only where the namespace is offered (see the patch-round section below). The key's description states both effects: "The product name in the console, on the sign-in page and in the browser title, and in sign-in emails, the authenticator app and MCP sign-in."Under
group/isolatedthe server does not serve thebrandingnamespace.SettingsServicePluginholds a manifest inUNWALLED_ONLY_NAMESPACES(today onlybranding) back atinit(). It registers that manifest from itskernel:readyhook only when the posture in force puts up no organization wall. It reads the posture the way its own door and service already read it (resolveAdmissionTenancyPosture). It decides atkernel:ready, not atinit(): duringinit()thetenancyservice may not be registered yet, andisolatedis in force only onceorg-scopinghas registered. The registry's own answer then covers every door:GET /api/settingsdoes not listbranding;GETandPUT /api/settings/brandingare refused404 UNKNOWN_NAMESPACEby the existing unknown-namespace path, never a silent 200;OS_*branding is served as before. No organization is resolved before sign-in.A posture that cannot be read withholds the namespace and logs one
warnnaming it: offering is the one direction a guess must not take. Notenancyservice, or a host with no async registry, offers it, which is how every lean kernel has always read the posture.settings-service.ts(claimed by epic #15194) is not touched.Walled deployments, the auth product name. With
brandingwithheld,OS_BRANDING_WORKSPACE_NAME, and an organization-lessbranding.workspace_namerow, no longer name sign-in mail, the authenticator issuer or MCP sign-in; theOS_APP_NAMEchain does. Operators move the value toOS_APP_NAME. This is stated in the service-settings and plugin-auth changesets.The retired-key refusal under 「甲」: confirmed
A save naming a retired key gets the loud refusal through the version step:
PUT /api/settings/brandingnamingtheme_modeorsupport_emailanswers400 UNKNOWN_KEYwithdetails: { namespace: 'branding', key }, and the refusal covers the batch (alogo_urlsent beside it does not land). Pinned insettings-routes.test.tswith stored rows seeded, and ablation-verified below. So the fallback (a warning in 18.x, with the refusal filed for 19.0) is not needed.Clause-② measured
no (narrowing). Published types:@objectstack/service-settingsexports the same symbols with the same types; only the value ofbrandingSettingsManifestchanged.SettingsServicePluginOptionsandRuntimeConfigPluginConfigchanged in JSDoc only, andRuntimeConfigPluginchanged only in private members. The servedruntime/configshape is unchanged. Narrowed accept sets, three:branding.theme_mode/branding.support_emailon every posture (now400 UNKNOWN_KEY);brandingnamespace (now404 UNKNOWN_NAMESPACE);OS_BRANDING_WORKSPACE_NAMEoverride and any organization-lessworkspace_namerow, which no longer name the auth product name.The ruling's "wiring the three keys is a widening" is the capability sense, which is why
@objectstack/cloud-connectiontakes aminor. No published surface widens in the Clause-② sense.Cross-lane in this PR:
packages/qa/dogfood(domain:cli) for the fixture re-point, andcontent/docs/ui/setup-app.mdx(domain:devx) for the Configuration row.content/docs/releases/is untouched.The Setup entry and the auth binding (patch round 1)
SettingsServicePlugincontributesnav_settings_brandingthrough themanifestservice from thekernel:readystep that offers thebrandingnamespace, and only then. It sits in Configuration at priority 110, after platform-objects' entries and before service-messaging's; the pattern is@objectstack/mcp'sconnect-ui.ts(cloud ADR-0009).SETUP_NAV_CONTRIBUTIONSdrops the static line.check-app-nav-i18n.mjsboots the plugin throughkernel:readyin a new roster entry, so the id stays in that gate's population: setup still 53 merged ids. Labels stay keyedapps.setup.navigation.nav_settings_branding.branding.workspace_nameonly where the settings service lists thebrandingnamespace, the runtime-config door's test. A walled boot no longer logsAuth: failed to apply branding.workspace_name: No settings manifest registered for namespace 'branding'., and the configured app name stays in force.packages/cli/test/connect-agent-both-halves-wire.pin.test.tscounts 31 Setup ids, exactly: that harness composesSETUP_NAV_CONTRIBUTIONSand the MCP bundle only, and the Branding entry now travels with service-settings. It also assertsnav_settings_brandingis absent there.@objectstack/platform-objectsminor (24766-setup-branding-entry) and@objectstack/plugin-authminor (24766-auth-brand-binding) join the two above.Clause-②: no (narrowing)is unchanged for the PR.Tests (head
0c71c4190, after mergingorigin/main644e4b2c5)pnpm --filter @objectstack/service-settings exec vitest run --maxWorkers=2: 56 files, 924 tests passed.pnpm --filter @objectstack/cloud-connection exec vitest run --maxWorkers=2: 53 files, 625 tests passed.typecheckfor both (tsc --noEmit, plus-p tsconfig.test.jsonfor cloud-connection): exit 0.--listFilesshows both programs compile every new and edited test file.init()tokernel:ready):plugin-auth'sauth-settings-ordering.pin,auth-settings-seeded-boot.pinanddev-admin-seed-workspace-timezonetests, 3 files and 12 tests passed.plugin-authaliases@objectstack/service-settingsto source.turbo run build --filter=@objectstack/cloud-connection... --filter=@objectstack/service-settings..., 34 tasks).New pins:
service-settings/src/settings-branding-posture.pin.test.tsboots the realSettingsServicePluginsequence withtenancyon a realObjectKernel, and drives the realregisterSettingsRoutesover the service it built:groupandisolated: absent from the listing (the other namespaces are still listed);PUTandGETrefused404 UNKNOWN_NAMESPACE;single, with no tenancy service, and on a host with no async registry: listed, saved, read back;service-settings/src/settings-routes.test.ts: the two retired keys. Not offered and not resolved, even with a stored row.PUTrefused400 UNKNOWN_KEYby name with the whole batch. A stored row never blocks a live save and is kept.service-settings/src/manifests/branding.manifest.test.ts: version 2; exactlyworkspace_name/accent_color/logo_urlinidentity/appearance; no retired key; no default onaccent_color/logo_url.cloud-connection/src/runtime-config-branding-settings.test.ts:single: a saved logo URL is the one served, and an unset one falls back toOS_LOGO_URL; the same for the name (never the manifest default) and the colour;source: envcounts as set; a failing read serves the env branding with one warning;group: the setting is not served and the door never callsgetMany;settings-service.test.ts: the env 来源的 settings 值绕过 manifest 的 options 表校验 —— #5094 在写入 API 上堵住的洞,在 OS_* 覆盖这一侧原样敞开 #5204 env-override pins usedbranding.theme_modeas their example select. They now uselocalization.locale, also a domain-less select, and pin the same behaviour, as fix(service-settings)!: retire date_format, time_format, number_format and first_day_of_week from the Localization manifest (#21958) #21970 re-pointed its pins.Ablations (each negative pin, put back, seen red, restored)
Each used
scripts/ablation-replace.mjsin WRAP mode, with a literal anchor that must hit and restore proven by blob. Every restore ended "blob == HEAD andgit diff HEADis empty". The test subjects resolve by relative import tosrc, so nodistrebuild was involved.if (posture !== undefined && postureEnforcesWall(posture))becameif (false && …), bloba282064cb1d8→5e3180ac9016): 6 red insettings-branding-posture.pin.test.ts. They are listing / PUT / GET undergroupandisolated:expected [ 'company', 'localization', …(2) ] to not include 'branding'andexpected 200 to be 404. Restored toa282064cb1d8.listManifests().find(…)replaced by a constant manifest, blob1cedb39aa721→88ff9db94c1d): the walled-posture pin went red,expected { productName: 'Tenant Org', …(6) } to match object { productName: 'Env Product', …(2) }. Restored to1cedb39aa721.if (!entry || entry.source === 'default') continue;becameif (!entry) continue;, blob →f429c41343c8): 3 red,expected 'ObjectStack' to be 'Env Product'(and… 'Host Product'). Restored to1cedb39aa721.theme_modeput back into the manifest (blob6e63f4cce79b→85c21c5efa98): 7 red. The route pins answeredexpected 'SETTINGS_VALIDATION' to be 'UNKNOWN_KEY'andtheme_mode must not be offered, the manifest pins failed, and the zh-CN / ja-JP / es-ES coverage reportedkey:theme_modemissing. Restored to6e63f4cce79b.1cedb39aa721→bff4ef12d8d3): the per-request pin went red,expected 'Env Product' to be 'Renamed'. Restored to1cedb39aa721.Patch round 1 (head
e6522a957)service-settings: full suite 56 files, 929 passed;settings-branding-posture.pinre-run ate6522a957: 15 passed.plugin-auth: full suite 173 files, 3319 passed, 10 skipped (before the test-only typing fix);auth-plugin.test.tsre-run ate6522a957: 112 passed;auth-settings-ordering.pin,auth-settings-seeded-boot.pin,dev-admin-seed-workspace-timezonepassed.platform-objects: full suite 82 files, 1232 passed.cliunit project, the files that compose the Setup nav: 4 files, 70 tests passed (connect-agent-both-halves-wire.pin,platform-page-i18n-parity,adr-0048-app-split,format.server-ready-dev-admin-audience).service-settings,plugin-auth(withcheck:test-typecheck),platform-objects(withcheck:test-typecheck),cloud-connectionandcli(withcheck:test-typecheck).settings-branding-posture.pinfolds the realSETUP_APP+SETUP_NAV_CONTRIBUTIONSplus what the boot registered through the realSchemaRegistry: undergroupandisolatednonav_settings_brandingin the merged Setup tree (controlsnav_settings_company,nav_settings_localizationpresent); undersingle, with no tenancy service, and with no async registry, exactly once ingroup_configuration, afternav_settings_feature_flags.auth-plugin.test.ts: walled pin (namespace not listed: no branding read, no subscription, no warn naming branding,setAppNamenever called) and a real-SettingsServicepin (no branding manifest: unbound and silent; registered with a savedAcme Single:setAppName('Acme Single')).scripts/ablation-replace.mjsWRAP; every restore "blob == HEAD andgit diff HEADis empty"):9f496bd8c738.SETUP_NAV_CONTRIBUTIONS: 5 red (walled postures:to not include nav_settings_branding; the three offering postures:to have a length of 1 but got 2). Restored toad98264e484d.groupandisolated). Restored to171c958f5702.kernel:ready:check-app-nav-i18nexits 1, "landed NO setup navigation id". Restored to23f2ff594226.Patch round 2 (head
d616d4d87)packages/qa/dogfood/test/settings-organization-isolation.dogfood.test.tsnow usescompany.legal_nameas its tenant fixture. Run for real under theisolatedposture it boots (its guard leg asserts postureisolated, degradedfalse): 6 passed.e6522a957and run on the same code gives 4 failed, 2 passed. Three failures carry{"code":"UNKNOWN_NAMESPACE","message":"No settings manifest registered for namespace 'branding'."}(expected 404 to be 200twice,expected 404 to be 400), and the data-API positive control failsexpected false to be true. Restored: blob531e8f552e34== HEAD.settings-config-change-audit,settings-audit-global-rung-siblingandaudit-log-parent-read-gate, 3 files, 16 tests passed.check:test-typecheckOK), service-settings, platform-objects (check:test-typecheckOK), cloud-connection.Gates
dispatch-gates --commandsat the final headd616d4d87derived 106 commands; the 29 new ones come from the docs page and the dogfood path. Each ran with its exit code captured before any pipe: 104 exited 0.check:skill-examples(spec) andcheck:dual-build-cjs-loadsexited 3 (PREREQUISITE NOT MET: missing dists) and re-ran exit 0 after building their closures ("262 prose examples type-check across 3 surface(s)"; "112 published require entry point(s) across 69 package(s) load").--ranwith exit codes: "106 derived famil(ies) accounted for — 106 run, 0 NOT-MEASURED (a DERIVED zero — all 106 recorded an exit code and none of them is 3)".d616d4d87, each exit 0:check-adr-0087-registration --base origin/main("1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … not-required (no-migration-prescription)"),check-changeset-no-major,check-empty-changeset, and the adr-0087--self-test(548 assertions).acaf3231b: a literal FROM/TO migration label tripped the ADR-0087 prescription detector. Reproduced locally (exit 1), reworded as the environment fix it is, andsuccessond616d4d87.check:startup-registry-verdict,check:settings-bind-windowandcheck:init-service-contractexit 0 at0c71c4190;pnpm check:app-nav-i18nexit 0 ate6522a957(setup: 53 merged nav ids).Acceptance notes (not filed)
docs/qa/platform-checklist/areas/platform-core.json,runtime-config-boot-readstep 1 (logoUrl/brandColoronly when configured by the plugin config or env vars): a saved Branding setting now serves them too, undersingle. Its namespace count is asingle-posture count; under a walled posturebrandingis not served. QA text drift, not edited. Carrier: none.packages/qa/dogfood/test/settings-organization-isolation.dogfood.test.tsusescompany.legal_nameas its tenant fixture, becausebrandingis not offered under theisolatedposture it boots.packages/qa/dogfood/test/anonymous-door-sweep.classification.tsclassifiesruntime/configas "no tenant data". Undersinglethe door now serves the one organization's saved branding, which the ruling defines as the deployment's. Under a wall it serves none. The wording still reads true by the ruling's definition. Carrier: none.packages/platform-objects/src/apps/translations/setup-nav-dead-key-tombstone.test.tskeepsCONDITIONAL_SETUP_NAV_IDS = ['nav_sso_providers'], the ids whose labels must stay although no single composition contributes them.nav_settings_brandingis now such an id (contributed only undersingle) and could join it. Carrier: none.packages/qa/dogfood/test/platform-app-object-entry-views.test.tssays its roster mirrorscheck-app-nav-i18n's CONTRIBUTORS. The new service-settings entry contributes oneurlitem, which that file (object-entry views) has nothing to judge, so its verdict is unchanged. Carrier: none.env-registry), the branding read is the host kernel's own settings, process-wide, exactly asOS_PRODUCT_NAMEwas. Per-environment branding was never served here and still is not. Carrier: none.workspace_name's manifest defaultObjectStackstill differs from the console's built-inObjectOS. That is the two-defaults reading docs(deployment): the environment-variables page gives OS_PRODUCT_NAME a default the runtime does not use and omits six branding variables the runtime reads #24887 carries for the operator docs. Carrier: docs(deployment): the environment-variables page gives OS_PRODUCT_NAME a default the runtime does not use and omits six branding variables the runtime reads #24887.Generated by Claude Code