Skip to content

fix(console): the OAuth consent screen explains metadata:write and never shows an unknown scope bare - #26659

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-9l8r78
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-9l8r78

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread
Fixes #26587
Clause-②: yes
Metadata-protocol: none

维护者速读

  • 改了什么: Console 的 OAuth 授权同意页(/oauth/consent)现在把 metadata:write 讲成一句话:「以草稿形式构建和修改此应用的元数据(不超出您自己的权限),由人来发布」,十种语言都有。页面不认识的 scope 显示为「无法识别的权限:xxx」,不再裸露原始字符串。
  • 为什么改: feat(mcp): bridge a host-injected authoring ToolRegistry over HTTP behind a metadata:write OAuth scope #26550 上线了 metadata:write,它是唯一能让 AI 客户端改动环境元数据的授权,却是同意页上唯一没有解释、只显示原始字符串的一项;用户在不知道授予了什么的情况下点「授权」。
  • 风险与回滚: 低。只改同意页的文案渲染和语言包两个新键;授权流程、提交内容不变。四个 MCP scope 的英文句子改为直接取自 @objectstack/spec 的 MCP_OAUTH_SCOPE_CONSENT_COPY,与服务端声明一致。回滚即 revert 本 PR。
  • 先看: ui/apps/console/src/pages/auth/OAuthConsentPage.tsx 的 describeAgentScope(对 McpOauthScope 穷尽的 switch)与 default 分支;ui/apps/console/src/pages/auth/__tests__/OAuthConsentPage.mcp-scopes-26587.test.tsx。
  • 两分钟验证: pnpm --dir ui exec vitest run apps/console/src/pages/auth/__tests__/OAuthConsentPage.mcp-scopes-26587.test.tsx,预期 12 个用例全绿(英文逐句等于 spec 文案,其余九种语言均不出现原始 scope,未知 scope 显示为「无法识别的权限」)。
  • 需要你做的: 无,席位落地。

What changes

@object-ui/console — OAuthConsentPage. The consent list used to map the three older agent scopes by hand and send everything else to default: return scope, so metadata:write (added by #26550) reached the user as its raw spelling. Now:

  • describeScope routes every member of MCP_OAUTH_SCOPES to describeAgentScope, a switch that is exhaustive over McpOauthScope with no default. Each case reads its own pack key and passes MCP_OAUTH_SCOPE_CONSENT_COPY[scope] (from @objectstack/spec/ai) as the defaultValue. A scope the spec adds without a case here fails the Console type-check instead of rendering unexplained.
  • The default branch renders oauth.consent.scope.unrecognised ("Unrecognised permission: SCOPE"), per the triage ruling (comment 6438780502): "An unknown scope renders as an explicit "unrecognised permission", ⛔ never as a bare string."

@object-ui/i18n — all ten locale packs. Two keys under oauth.consent.scope: metadataWrite (the en value is byte-identical to the spec's sentence) and unrecognised (one {{scope}} placeholder). The en comment above the block now says where the agent-scope English comes from. TranslationKeys = typeof en widens by these two keys, hence Clause-②: yes.

docs/qa/platform-checklist/areas/ai.json — ai.mcp-http-surface, revision 6 (the second item the card carries; a devx file). Main at c739de7 still said "three" scopes. The acceptance[5] clause and its verify now name the four scopes the 403 body and the WWW-Authenticate scope list carry (packages/runtime/src/domains/mcp.ts joins MCP_OAUTH_SCOPES), steps[5] says "four", and the variants matrix gains the metadata:write row (the host-injected authoring tools, served only to an OAuth grant naming the scope, per packages/mcp/src/mcp-http-tools.ts). History row added.

Changeset: .changeset/26587-consent-metadata-write.md, patch for @object-ui/i18n and @object-ui/console.

Tests

  • New: ui/apps/console/src/pages/auth/__tests__/OAuthConsentPage.mcp-scopes-26587.test.tsx. It walks MCP_OAUTH_SCOPES, so a scope the spec adds is a subject the moment it lands. In en the rows equal MCP_OAUTH_SCOPE_CONSENT_COPY in order. In each of the nine other built-in languages (catalogue loaded before the instance, so a missing key cannot hide behind the en fallback), no row contains the raw scope and no row equals the English sentence. An unknown scope renders through the unrecognised pack value in en, and in zh as a translated line that still names the scope.
  • Updated: the CONTROL case of OAuthConsentPage.agent-scopes-12853.test.tsx pinned the old raw-string behaviour; it now asserts the unrecognised line.

Gates

Local subset at 3f2b32c8: the 41 commands node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives for this diff, each run with its exit code captured before any pipe, all 41 exit 0. --ran reconciliation: "41 derived famil(ies) accounted for — 41 run, 0 NOT-MEASURED (a DERIVED zero — all 41 recorded an exit code and none of them is 3)". The type-check lanes, the artifact rosters and the whole-tree families are CI's; their check-runs on this PR are the reading.

The 41 derived commands and their exit codes
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-adr-links.mjs :: exit 0
node scripts/check-adr-links.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/check-sdui-manifest.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:platform-checklist :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0

Also run, outside the derivation (ui gates no root workflow runs today): node scripts/check-i18n-call-site-keys.mjs exit 0 (every call-site key resolves, interpolation parity holds), node scripts/check-i18n-en-drift.mjs exit 0 ("No en value changed in this range"), node scripts/check-new-cross-file-line-citations.mjs exit 0 ("0 new citation(s)").

Acceptance notes

  • The claim's file surface said "one new consent key"; the card's route also asks for a translated "unrecognised permission" line, so the packs gain two keys (metadataWrite, unrecognised).
  • The zh / ja / … sentences for metadataWrite follow each pack's existing pattern for the other agent scopes (the permission bound in parentheses) and add the "a person publishes" clause; they are hand translations, not machine-checked.
  • The Console now imports @objectstack/spec/ai from an eagerly loaded page. The constants live in a spec chunk that @objectstack/spec/system (already in the Console's dependency set) also reaches; the eager-closure byte cost was not measured locally and is left to CI's performance budget.
  • ai.json: besides the clause the card names, steps[5] ("NONE of the three MCP scopes") and the variants matrix were also stale for the same reason and are corrected in the same revision.

Verification

All readings at 3f2b32c8.

Tests
  • cd ui && pnpm exec vitest run apps/console/src/pages/auth/__tests__/OAuthConsentPage.mcp-scopes-26587.test.tsx apps/console/src/pages/auth/__tests__/OAuthConsentPage.agent-scopes-12853.test.tsx apps/console/src/pages/auth/__tests__/OAuthConsentPage.public-client-12301.test.tsx --maxWorkers=1 (under os-verify-lock.sh): Test Files 3 passed (3), Tests 23 passed (23).
  • cd ui && pnpm exec vitest run packages/i18n/ --maxWorkers=1: Test Files 89 passed (89), Tests 1373 passed, 13 skipped (all-locales-key-parity among them).
  • pnpm --dir ui exec vitest run scripts/ (derived): Test Files 178 passed, 2 skipped; Tests 5065 passed, 2 skipped.
Type-check

pnpm exec turbo run type-check --filter=@object-ui/i18n --filter=@object-ui/console --concurrency=2: Tasks 71 successful, 71 total, 0 cached (the two packages' type-check, which covers their tests, plus the ^build closure). VERDICT command-exit 0.

Ablations (one-shot, through scripts/ablation-replace.mjs, restored to HEAD each time)
  • Drop the zh pack's metadataWrite line: "anchor 1 -> 0, blob 4a8ff35cd28f -> 0d88d403f28e"; the walk goes red on the zh case only ("Tests 1 failed | 11 passed (12)"); restore: "blob == HEAD (4a8ff35cd28f) and git diff HEAD is empty".
  • Send metadata:write past the agent-scope routing in describeScope (it falls to the unrecognised branch): "anchor 1 -> 0"; the en walk and all nine language cases go red ("Tests 10 failed | 2 passed (12)"); restore: "blob == HEAD (935b2c4ded21) and git diff HEAD is empty".

Generated by Claude Code

…nces an unknown scope as unrecognised

The agent-scope sentences come from @objectstack/spec's
MCP_OAUTH_SCOPE_CONSENT_COPY through an exhaustive switch over
McpOauthScope, and every locale pack gains metadataWrite and unrecognised.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jUZATdbPBDkvjTyX8D
…et for the consent screen

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jUZATdbPBDkvjTyX8D
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 3f2b32c84b1f9650ff2d7e2ad979055a579728f7
Local-runs: none

Face: declaration. The face is owed only because of the Clause-②: yes line in the PR body and the changeset. The committed tree has no published schema (packages/spec/src/** is untouched) and no governed path: check-governed-merges --pr 26659 reports 0 of 15 paths, ≤ 3000 lines. There is no narrowing line.

① Derived judgments

  • @object-ui/i18n public surface widens by two members. All ten packs (ar de en es fr ja ko pt ru zh) gain oauth.consent.scope.metadataWrite and oauth.consent.scope.unrecognised, so TranslationKeys (typeof en) gains two members. Nothing is removed or renamed. A host reading an existing key is unaffected. A host that types its own full pack against TranslationKeys must add the two rows. That is the same reach as every earlier widening in this package's CHANGELOG. Correct.
  • unrecognised takes a {{scope}} placeholder in every pack. The diff carries the placeholder in all ten rows, and the call site passes scope. Correct.
  • @object-ui/console consent screen. The screen now imports MCP_OAUTH_SCOPES, MCP_OAUTH_SCOPE_CONSENT_COPY, the four scope constants and McpOauthScope from @objectstack/spec/ai. All of them are exported on main (packages/spec/src/ai/mcp.zod.ts, re-exported by ai/index.ts), and console already depends on @objectstack/spec (workspace:^).
    • describeAgentScope switches over McpOauthScope with no default, so a fifth spec scope with no case fails to compile. That is the drift guard the card asked for.
    • The default text is the spec's copy, so the server-side sentence and the screen's fallback cannot diverge.
    • A scope outside the known set renders as "Unrecognised permission: …" instead of the bare string.
    • No exported runtime contract changes: OAuthConsentPage keeps its signature, and the helpers are module-private.
    • Correct.
  • QA checklist (docs/qa/platform-checklist/areas/ai.json rev 6). "All four scopes" and the four-scope WWW-Authenticate list match packages/runtime/src/domains/mcp.ts, which joins MCP_OAUTH_SCOPES into the 403 body and header. The metadata:write variants row cites registerAuthoringTools in packages/mcp/src/mcp-server-runtime.ts, which exists. This is a prose face, not part of the contract. Correct.

② Semver level

The changeset declares patch for @object-ui/i18n and @object-ui/console. That matches this package's precedent: a widening-only key addition ships as patch, and only a removal (narrowing) is minor under objectui's version policy, as the CHANGELOG records for timeline.bucket.overdue. Consistent.

③ Boundary flags

The dev raised four deviations. Each is answered below.

  • Two pack keys instead of the one the claim named: accepted. The unrecognised key is what keeps an unknown scope from rendering bare, and it falls inside the claimed locales/*.ts surface.
  • ai.json steps[5] and the variants row were also fixed: accepted. They follow the same fourth scope in the same claimed file.
  • check-closing-target-claim UNDETERMINED because of the claim's Branch-line parenthetical: a reader artefact, not a defect. The claim (6440101598) names claude/project-thread-9l8r78, which is this PR's head ref, and the body's first line is Fixes #26587.
  • The eager-bundle cost of the @objectstack/spec/ai import was not measured: not blocking. This is the first /ai subpath import in console source. Console already imports the package root and six other subpaths (api, contracts, data, shared, system, ui). The new import brings in constants and a type. Build & E2E UI at this head decides whether the build holds. The seat measured no bundle size.
  • PR user.login is objectstack-fleet[bot], so there is no outside author to name.

Implemented-by: claude/project-thread-9l8r78
Reviewed-by: session_01QsR2jTfvjNTaR29ehLk26T

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · seat domain:ui#4 · session_01QsR2jTfvjNTaR29ehLk26T · 2026-10-27T12:06Z · head 3f2b32c84b1f9650ff2d7e2ad979055a579728f7

  1. Shape: draft, base main, first line Fixes #26587. Clause-②: yes and Metadata-protocol: none match the claim (6440101598), in the body and in the changeset. Files: 15, all inside the claimed surface: OAuthConsentPage.tsx and two tests, the ten ui/packages/i18n/src/locales/*.ts, docs/qa/platform-checklist/areas/ai.json, and one root changeset. No content/docs/releases/.
  2. Premise, measured: the seat read the diff. On main, describeScope's default returned the bare scope, so metadata:write rendered raw. After the change, every MCP_OAUTH_SCOPES member goes through describeAgentScope, which is exhaustive over McpOauthScope. An unknown scope renders as oauth.consent.scope.unrecognised, as triage ruled (6438780502). The spec exports it reads (MCP_OAUTH_SCOPE_CONSENT_COPY, the four scope constants) are on main in packages/spec/src/ai/mcp.zod.ts.
  3. Gates, pinned to the head: check-runs at 3f2b32c8: 54 total, 47 success, 7 skipped, 0 failed. Type Check UI, Lint UI, Test UI (16 shards), Build & E2E UI, Check Changeset and Governed Surface Queue Guard are all success. check-governed-merges --pr 26659: not governed (0 of 15 paths), 261 changed lines ≤ 3000. mergeable_state: clean.
  4. Accepted deviations: the four in the report are answered one by one in the contract review (6441453047, ③): two pack keys instead of one (the unrecognised key is the triage ruling), ai.json steps[5] and the variants row (same file, same fourth scope), check-closing-target-claim UNDETERMINED (a reader artefact on the claim's Branch parenthetical; the claim names this head ref), and bundle cost not measured (constants and a type; the build is green).
  5. Contract review: Clause-②: yes alone puts this on the declaration face, so a TIER_DEFAULT review by this seat is owed. Its record is in place: ## Contract review at this head, Served-tier: TIER_DEFAULT, Local-runs: none, PASS (6441453047). No needs:contract-review marker is owed.
  6. Prose face, checked sentence by sentence against the diff: the changeset's two bullets match hunks: the consent sentences come from MCP_OAUTH_SCOPE_CONSENT_COPY through an exhaustive switch; an unknown scope reads "Unrecognised permission: SCOPE"; every pack gains the two keys; TranslationKeys widens by two. One nit stands, and is not a defect: the PR body attributes the metadata:write variants row to packages/mcp/src/mcp-http-tools.ts, while ai.json cites registerAuthoringTools in mcp-server-runtime.ts. Both files exist on main, and the checklist row, the shipped one, cites the function that registers the tools.

Landing: the seat marks the PR ready and arms auto-merge. On MERGED, Fixes #26587 closes the card.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 12:07
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 27, 2026 12:07
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit 8222fa8 Oct 27, 2026
55 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-9l8r78 branch October 27, 2026 12:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

console(oauth consent): the metadata:write scope from #26550 is shown to the user as the raw string, not as a sentence of what they are granting

2 participants