[Decision] May a user set their own sys_user.locale? — the ADR-0092 D2 self-service whitelist stays {name, image} after #13881 (column lands readonly, system-context writes only) #14787

Description

@claude

Filed by the domain:spec seat (session_017RbbUMnxkUnWhE4j94v8FE) from the contract review of PR #14775 (#13881, isolated reviewer verdict 5518923117, "Decisions to file for the maintainer" item 1). Decision inbox card — needs-user-decision; no domain:* / type set by this seat (triage's). Depends on PR #14775 landing (the column itself); nothing here blocks that landing.

背景

#13881 的裁决(2026-09-01,A 路线)给 sys_user 加了一等列 locale(PR #14775)。落地形状:该列 readonly,只有系统上下文(system context)能写 —— 不在 ADR-0092 D2 的用户自助编辑白名单 SYS_USER_PROFILE_EDIT_FIELDS = {name, image} 里,也不在 plugin-auth 的 MANAGED_EXTENSION_EDITABLE_FIELDS.sys_user 里;今天没有任何管理员界面写它。首个消费者 hotcrm#1185 可以在系统上下文里盖章,所以 PR 可以不等本决策先落地。剩下的问题是身份表写路径的安全边界,裁决没有覆盖,座位不能自裁。

带 re-check 命令的前提

  • 白名单今天是 {name, image}:git grep -n "SYS_USER_PROFILE_EDIT_FIELDS" origin/main -- packages/plugins/plugin-auth/src/sys-user-writable-fields.ts
  • 可编辑扩展字段表没有 sys_user 条目:git grep -n "MANAGED_EXTENSION_EDITABLE_FIELDS" origin/main -- packages/plugins/plugin-auth/src/managed-extension-fields.ts
  • 列已落地且 readonly(PR feat(spec,platform-objects,service-messaging,plugin-auth): sys_user.locale + per-recipient notification locale (#13881) #14775 合并后成立):git grep -n -A3 "locale: Field.text" origin/main -- packages/platform-objects/src/identity/sys-user.object.ts
  • better-auth 的 /update-user 走不到这列(它不是 better-auth 字段):git grep -n "additionalFields" origin/main -- packages/plugins/plugin-auth/src/auth-schema-config.ts(只有 invitation 模型有)

一句话问题

一个中文用户登录进来,想把自己的通知语言从英文改成中文 —— 今天没有任何地方让他自己改,只能等管理员或系统替他填。

选项 × 真实代价

选项做什么真实代价
A 保持现状readonly,只有系统上下文写(hotcrm 之类的应用在系统上下文里盖章)用户收到的通知语言由别人决定;平台自己的 Studio/Console 没有入口让用户改语言;每个应用都要自己造一条「系统替用户填语言」的通道
B 放开自助白名单加 locale({name, image, locale}),MANAGED_EXTENSION_EDITABLE_FIELDS.sys_userlocale,去掉 readonly,身份写守卫的 pin 同步翻转身份表多开一个用户可写字段(安全边界扩一格);用户填错格式由列的 BCP-47 形状校验响亮拒绝;objectui 侧还要一个「我的语言」表单项才真正可达(另开 ui 卡)

业务上:A = 像早期企业软件,语言是 IT 管理员配置的;B = 像 Salesforce / Slack 的个人设置页里的 Language 下拉,用户自己选。

四轴(从业务立场)

  • 实际业务需求:hotcrm 发布 4 种语言、16 个 notify 节点,zh-CN / ja-JP / es-ES 用户此前全收英文 —— 拉动已实测(hotcrm#1185)。但「谁来填这一列」在应用侧还是空的:A 下每个应用都得自己写一段系统上下文盖章逻辑;B 下用户第一次登录就能自己选。
  • 项目长远合理性(权重 ≥50%):用户自己的语言是主流平台的一等个人属性,长远终态就是用户自助;A 是过渡态,过渡态在创业阶段不留。B 不扩大契约(列已在),只是把已有的白名单机制多列一项,特例没有增生。
  • 防 AI 犯错:B 下 AI 写应用元数据时不需要为「怎么替用户填语言」发明系统上下文写路径(那正是 AI 最容易写错权限的地方);用户填错值由 LOCALE_TAG_SHAPE 校验响亮拒绝、回退到部署默认,不会静默死信。A 下 AI 会在应用侧造绕行。
  • 创业阶段不扩散:B 的改动是三行(两个白名单各一项 + 去 readonly)+ 一个 pin,不新增能力面;A 反而会让每个应用各自扩散一套盖章逻辑。

推荐 + 回退 + 置信缺口

  • 推荐 B(长远终态领起,实测拉动在,防错与不扩散同向)。安全边界扩一格是本卡进决策箱的唯一原因:身份表的用户可写集从 2 个字段变 3 个。
  • 回退 A:列保持只读,由应用在系统上下文里盖章;hotcrm 照常可用。
  • 本分析看不见什么:objectui 是否已有「个人设置」表单可以挂 locale(未测);cloud 仓是否有自己的用户语言设置面(未测,本容器不可达 objectstack-ai/cloud)。

裁后我会怎么执行(你不用管)

Refs

#13881(裁决 5494464459)· PR #14775(复审 5518923117)· hotcrm#1185 · #14641(邀请邮件梯级)· #14762(auth 短信/邮件读列)· ADR-0092 D2 / D4 · ADR-0105 D7

os-decision-facets

  • ① 长远合理性:用户自选语言是终态;B 只在既有白名单机制上加一项,不扩大契约、不增特例 —— 荐 B。
  • ② 实际业务拉动:已实测(hotcrm 4 语言 × 16 节点 × 0 可本地化);A 下每个应用要各造一条系统盖章路径才能填列。
  • ③ 防 AI 犯错:B 让 AI 写应用时不必发明权限绕行;错值由 BCP-47 形状校验响亮拒绝并回退部署默认。
  • ④ 创业阶段不扩散:B = 三行 + 一个 pin,无新能力面;A 会让盖章逻辑在应用间扩散。
  • 推荐:B(回退 A)。
  • 置信缺口:objectui 个人设置表单是否存在、cloud 仓是否另有用户语言面 —— 均未测。

Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
     blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    
    Skip to content

    [Decision] May a user set their own sys_user.locale? — the ADR-0092 D2 self-service whitelist stays {name, image} after #13881 (column lands readonly, system-context writes only) #14787

    Description

    @claude

    Filed by the domain:spec seat (session_017RbbUMnxkUnWhE4j94v8FE) from the contract review of PR #14775 (#13881, isolated reviewer verdict 5518923117, "Decisions to file for the maintainer" item 1). Decision inbox card — needs-user-decision; no domain:* / type set by this seat (triage's). Depends on PR #14775 landing (the column itself); nothing here blocks that landing.

    背景

    #13881 的裁决(2026-09-01,A 路线)给 sys_user 加了一等列 locale(PR #14775)。落地形状:该列 readonly,只有系统上下文(system context)能写 —— 不在 ADR-0092 D2 的用户自助编辑白名单 SYS_USER_PROFILE_EDIT_FIELDS = {name, image} 里,也不在 plugin-auth 的 MANAGED_EXTENSION_EDITABLE_FIELDS.sys_user 里;今天没有任何管理员界面写它。首个消费者 hotcrm#1185 可以在系统上下文里盖章,所以 PR 可以不等本决策先落地。剩下的问题是身份表写路径的安全边界,裁决没有覆盖,座位不能自裁。

    带 re-check 命令的前提

    • 白名单今天是 {name, image}:git grep -n "SYS_USER_PROFILE_EDIT_FIELDS" origin/main -- packages/plugins/plugin-auth/src/sys-user-writable-fields.ts
    • 可编辑扩展字段表没有 sys_user 条目:git grep -n "MANAGED_EXTENSION_EDITABLE_FIELDS" origin/main -- packages/plugins/plugin-auth/src/managed-extension-fields.ts
    • 列已落地且 readonly(PR feat(spec,platform-objects,service-messaging,plugin-auth): sys_user.locale + per-recipient notification locale (#13881) #14775 合并后成立):git grep -n -A3 "locale: Field.text" origin/main -- packages/platform-objects/src/identity/sys-user.object.ts
    • better-auth 的 /update-user 走不到这列(它不是 better-auth 字段):git grep -n "additionalFields" origin/main -- packages/plugins/plugin-auth/src/auth-schema-config.ts(只有 invitation 模型有)

    一句话问题

    一个中文用户登录进来,想把自己的通知语言从英文改成中文 —— 今天没有任何地方让他自己改,只能等管理员或系统替他填。

    选项 × 真实代价

    选项做什么真实代价
    A 保持现状readonly,只有系统上下文写(hotcrm 之类的应用在系统上下文里盖章)用户收到的通知语言由别人决定;平台自己的 Studio/Console 没有入口让用户改语言;每个应用都要自己造一条「系统替用户填语言」的通道
    B 放开自助白名单加 locale({name, image, locale}),MANAGED_EXTENSION_EDITABLE_FIELDS.sys_userlocale,去掉 readonly,身份写守卫的 pin 同步翻转身份表多开一个用户可写字段(安全边界扩一格);用户填错格式由列的 BCP-47 形状校验响亮拒绝;objectui 侧还要一个「我的语言」表单项才真正可达(另开 ui 卡)

    业务上:A = 像早期企业软件,语言是 IT 管理员配置的;B = 像 Salesforce / Slack 的个人设置页里的 Language 下拉,用户自己选。

    四轴(从业务立场)

    • 实际业务需求:hotcrm 发布 4 种语言、16 个 notify 节点,zh-CN / ja-JP / es-ES 用户此前全收英文 —— 拉动已实测(hotcrm#1185)。但「谁来填这一列」在应用侧还是空的:A 下每个应用都得自己写一段系统上下文盖章逻辑;B 下用户第一次登录就能自己选。
    • 项目长远合理性(权重 ≥50%):用户自己的语言是主流平台的一等个人属性,长远终态就是用户自助;A 是过渡态,过渡态在创业阶段不留。B 不扩大契约(列已在),只是把已有的白名单机制多列一项,特例没有增生。
    • 防 AI 犯错:B 下 AI 写应用元数据时不需要为「怎么替用户填语言」发明系统上下文写路径(那正是 AI 最容易写错权限的地方);用户填错值由 LOCALE_TAG_SHAPE 校验响亮拒绝、回退到部署默认,不会静默死信。A 下 AI 会在应用侧造绕行。
    • 创业阶段不扩散:B 的改动是三行(两个白名单各一项 + 去 readonly)+ 一个 pin,不新增能力面;A 反而会让每个应用各自扩散一套盖章逻辑。

    推荐 + 回退 + 置信缺口

    • 推荐 B(长远终态领起,实测拉动在,防错与不扩散同向)。安全边界扩一格是本卡进决策箱的唯一原因:身份表的用户可写集从 2 个字段变 3 个。
    • 回退 A:列保持只读,由应用在系统上下文里盖章;hotcrm 照常可用。
    • 本分析看不见什么:objectui 是否已有「个人设置」表单可以挂 locale(未测);cloud 仓是否有自己的用户语言设置面(未测,本容器不可达 objectstack-ai/cloud)。

    裁后我会怎么执行(你不用管)

    Refs

    #13881(裁决 5494464459)· PR #14775(复审 5518923117)· hotcrm#1185 · #14641(邀请邮件梯级)· #14762(auth 短信/邮件读列)· ADR-0092 D2 / D4 · ADR-0105 D7

    os-decision-facets

    • ① 长远合理性:用户自选语言是终态;B 只在既有白名单机制上加一项,不扩大契约、不增特例 —— 荐 B。
    • ② 实际业务拉动:已实测(hotcrm 4 语言 × 16 节点 × 0 可本地化);A 下每个应用要各造一条系统盖章路径才能填列。
    • ③ 防 AI 犯错:B 让 AI 写应用时不必发明权限绕行;错值由 BCP-47 形状校验响亮拒绝并回退部署默认。
    • ④ 创业阶段不扩散:B = 三行 + 一个 pin,无新能力面;A 会让盖章逻辑在应用间扩散。
    • 推荐:B(回退 A)。
    • 置信缺口:objectui 个人设置表单是否存在、cloud 仓是否另有用户语言面 —— 均未测。

    Generated by Claude Code

    Activity

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
      Skip to content

      [Decision] May a user set their own sys_user.locale? — the ADR-0092 D2 self-service whitelist stays {name, image} after #13881 (column lands readonly, system-context writes only) #14787

      Description

      @claude

      Filed by the domain:spec seat (session_017RbbUMnxkUnWhE4j94v8FE) from the contract review of PR #14775 (#13881, isolated reviewer verdict 5518923117, "Decisions to file for the maintainer" item 1). Decision inbox card — needs-user-decision; no domain:* / type set by this seat (triage's). Depends on PR #14775 landing (the column itself); nothing here blocks that landing.

      背景

      #13881 的裁决(2026-09-01,A 路线)给 sys_user 加了一等列 locale(PR #14775)。落地形状:该列 readonly,只有系统上下文(system context)能写 —— 不在 ADR-0092 D2 的用户自助编辑白名单 SYS_USER_PROFILE_EDIT_FIELDS = {name, image} 里,也不在 plugin-auth 的 MANAGED_EXTENSION_EDITABLE_FIELDS.sys_user 里;今天没有任何管理员界面写它。首个消费者 hotcrm#1185 可以在系统上下文里盖章,所以 PR 可以不等本决策先落地。剩下的问题是身份表写路径的安全边界,裁决没有覆盖,座位不能自裁。

      带 re-check 命令的前提

      • 白名单今天是 {name, image}:git grep -n "SYS_USER_PROFILE_EDIT_FIELDS" origin/main -- packages/plugins/plugin-auth/src/sys-user-writable-fields.ts
      • 可编辑扩展字段表没有 sys_user 条目:git grep -n "MANAGED_EXTENSION_EDITABLE_FIELDS" origin/main -- packages/plugins/plugin-auth/src/managed-extension-fields.ts
      • 列已落地且 readonly(PR feat(spec,platform-objects,service-messaging,plugin-auth): sys_user.locale + per-recipient notification locale (#13881) #14775 合并后成立):git grep -n -A3 "locale: Field.text" origin/main -- packages/platform-objects/src/identity/sys-user.object.ts
      • better-auth 的 /update-user 走不到这列(它不是 better-auth 字段):git grep -n "additionalFields" origin/main -- packages/plugins/plugin-auth/src/auth-schema-config.ts(只有 invitation 模型有)

      一句话问题

      一个中文用户登录进来,想把自己的通知语言从英文改成中文 —— 今天没有任何地方让他自己改,只能等管理员或系统替他填。

      选项 × 真实代价

      选项做什么真实代价
      A 保持现状readonly,只有系统上下文写(hotcrm 之类的应用在系统上下文里盖章)用户收到的通知语言由别人决定;平台自己的 Studio/Console 没有入口让用户改语言;每个应用都要自己造一条「系统替用户填语言」的通道
      B 放开自助白名单加 locale({name, image, locale}),MANAGED_EXTENSION_EDITABLE_FIELDS.sys_userlocale,去掉 readonly,身份写守卫的 pin 同步翻转身份表多开一个用户可写字段(安全边界扩一格);用户填错格式由列的 BCP-47 形状校验响亮拒绝;objectui 侧还要一个「我的语言」表单项才真正可达(另开 ui 卡)

      业务上:A = 像早期企业软件,语言是 IT 管理员配置的;B = 像 Salesforce / Slack 的个人设置页里的 Language 下拉,用户自己选。

      四轴(从业务立场)

      • 实际业务需求:hotcrm 发布 4 种语言、16 个 notify 节点,zh-CN / ja-JP / es-ES 用户此前全收英文 —— 拉动已实测(hotcrm#1185)。但「谁来填这一列」在应用侧还是空的:A 下每个应用都得自己写一段系统上下文盖章逻辑;B 下用户第一次登录就能自己选。
      • 项目长远合理性(权重 ≥50%):用户自己的语言是主流平台的一等个人属性,长远终态就是用户自助;A 是过渡态,过渡态在创业阶段不留。B 不扩大契约(列已在),只是把已有的白名单机制多列一项,特例没有增生。
      • 防 AI 犯错:B 下 AI 写应用元数据时不需要为「怎么替用户填语言」发明系统上下文写路径(那正是 AI 最容易写错权限的地方);用户填错值由 LOCALE_TAG_SHAPE 校验响亮拒绝、回退到部署默认,不会静默死信。A 下 AI 会在应用侧造绕行。
      • 创业阶段不扩散:B 的改动是三行(两个白名单各一项 + 去 readonly)+ 一个 pin,不新增能力面;A 反而会让每个应用各自扩散一套盖章逻辑。

      推荐 + 回退 + 置信缺口

      • 推荐 B(长远终态领起,实测拉动在,防错与不扩散同向)。安全边界扩一格是本卡进决策箱的唯一原因:身份表的用户可写集从 2 个字段变 3 个。
      • 回退 A:列保持只读,由应用在系统上下文里盖章;hotcrm 照常可用。
      • 本分析看不见什么:objectui 是否已有「个人设置」表单可以挂 locale(未测);cloud 仓是否有自己的用户语言设置面(未测,本容器不可达 objectstack-ai/cloud)。

      裁后我会怎么执行(你不用管)

      Refs

      #13881(裁决 5494464459)· PR #14775(复审 5518923117)· hotcrm#1185 · #14641(邀请邮件梯级)· #14762(auth 短信/邮件读列)· ADR-0092 D2 / D4 · ADR-0105 D7

      os-decision-facets

      • ① 长远合理性:用户自选语言是终态;B 只在既有白名单机制上加一项,不扩大契约、不增特例 —— 荐 B。
      • ② 实际业务拉动:已实测(hotcrm 4 语言 × 16 节点 × 0 可本地化);A 下每个应用要各造一条系统盖章路径才能填列。
      • ③ 防 AI 犯错:B 让 AI 写应用时不必发明权限绕行;错值由 BCP-47 形状校验响亮拒绝并回退部署默认。
      • ④ 创业阶段不扩散:B = 三行 + 一个 pin,无新能力面;A 会让盖章逻辑在应用间扩散。
      • 推荐:B(回退 A)。
      • 置信缺口:objectui 个人设置表单是否存在、cloud 仓是否另有用户语言面 —— 均未测。

      Generated by Claude Code

      Activity

      Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

      Metadata

      Metadata

      Assignees

      Type

      No type

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
        Skip to content

        [Decision] May a user set their own sys_user.locale? — the ADR-0092 D2 self-service whitelist stays {name, image} after #13881 (column lands readonly, system-context writes only) #14787

        Description

        @claude

        Filed by the domain:spec seat (session_017RbbUMnxkUnWhE4j94v8FE) from the contract review of PR #14775 (#13881, isolated reviewer verdict 5518923117, "Decisions to file for the maintainer" item 1). Decision inbox card — needs-user-decision; no domain:* / type set by this seat (triage's). Depends on PR #14775 landing (the column itself); nothing here blocks that landing.

        背景

        #13881 的裁决(2026-09-01,A 路线)给 sys_user 加了一等列 locale(PR #14775)。落地形状:该列 readonly,只有系统上下文(system context)能写 —— 不在 ADR-0092 D2 的用户自助编辑白名单 SYS_USER_PROFILE_EDIT_FIELDS = {name, image} 里,也不在 plugin-auth 的 MANAGED_EXTENSION_EDITABLE_FIELDS.sys_user 里;今天没有任何管理员界面写它。首个消费者 hotcrm#1185 可以在系统上下文里盖章,所以 PR 可以不等本决策先落地。剩下的问题是身份表写路径的安全边界,裁决没有覆盖,座位不能自裁。

        带 re-check 命令的前提

        • 白名单今天是 {name, image}:git grep -n "SYS_USER_PROFILE_EDIT_FIELDS" origin/main -- packages/plugins/plugin-auth/src/sys-user-writable-fields.ts
        • 可编辑扩展字段表没有 sys_user 条目:git grep -n "MANAGED_EXTENSION_EDITABLE_FIELDS" origin/main -- packages/plugins/plugin-auth/src/managed-extension-fields.ts
        • 列已落地且 readonly(PR feat(spec,platform-objects,service-messaging,plugin-auth): sys_user.locale + per-recipient notification locale (#13881) #14775 合并后成立):git grep -n -A3 "locale: Field.text" origin/main -- packages/platform-objects/src/identity/sys-user.object.ts
        • better-auth 的 /update-user 走不到这列(它不是 better-auth 字段):git grep -n "additionalFields" origin/main -- packages/plugins/plugin-auth/src/auth-schema-config.ts(只有 invitation 模型有)

        一句话问题

        一个中文用户登录进来,想把自己的通知语言从英文改成中文 —— 今天没有任何地方让他自己改,只能等管理员或系统替他填。

        选项 × 真实代价

        选项做什么真实代价
        A 保持现状readonly,只有系统上下文写(hotcrm 之类的应用在系统上下文里盖章)用户收到的通知语言由别人决定;平台自己的 Studio/Console 没有入口让用户改语言;每个应用都要自己造一条「系统替用户填语言」的通道
        B 放开自助白名单加 locale({name, image, locale}),MANAGED_EXTENSION_EDITABLE_FIELDS.sys_userlocale,去掉 readonly,身份写守卫的 pin 同步翻转身份表多开一个用户可写字段(安全边界扩一格);用户填错格式由列的 BCP-47 形状校验响亮拒绝;objectui 侧还要一个「我的语言」表单项才真正可达(另开 ui 卡)

        业务上:A = 像早期企业软件,语言是 IT 管理员配置的;B = 像 Salesforce / Slack 的个人设置页里的 Language 下拉,用户自己选。

        四轴(从业务立场)

        • 实际业务需求:hotcrm 发布 4 种语言、16 个 notify 节点,zh-CN / ja-JP / es-ES 用户此前全收英文 —— 拉动已实测(hotcrm#1185)。但「谁来填这一列」在应用侧还是空的:A 下每个应用都得自己写一段系统上下文盖章逻辑;B 下用户第一次登录就能自己选。
        • 项目长远合理性(权重 ≥50%):用户自己的语言是主流平台的一等个人属性,长远终态就是用户自助;A 是过渡态,过渡态在创业阶段不留。B 不扩大契约(列已在),只是把已有的白名单机制多列一项,特例没有增生。
        • 防 AI 犯错:B 下 AI 写应用元数据时不需要为「怎么替用户填语言」发明系统上下文写路径(那正是 AI 最容易写错权限的地方);用户填错值由 LOCALE_TAG_SHAPE 校验响亮拒绝、回退到部署默认,不会静默死信。A 下 AI 会在应用侧造绕行。
        • 创业阶段不扩散:B 的改动是三行(两个白名单各一项 + 去 readonly)+ 一个 pin,不新增能力面;A 反而会让每个应用各自扩散一套盖章逻辑。

        推荐 + 回退 + 置信缺口

        • 推荐 B(长远终态领起,实测拉动在,防错与不扩散同向)。安全边界扩一格是本卡进决策箱的唯一原因:身份表的用户可写集从 2 个字段变 3 个。
        • 回退 A:列保持只读,由应用在系统上下文里盖章;hotcrm 照常可用。
        • 本分析看不见什么:objectui 是否已有「个人设置」表单可以挂 locale(未测);cloud 仓是否有自己的用户语言设置面(未测,本容器不可达 objectstack-ai/cloud)。

        裁后我会怎么执行(你不用管)

        Refs

        #13881(裁决 5494464459)· PR #14775(复审 5518923117)· hotcrm#1185 · #14641(邀请邮件梯级)· #14762(auth 短信/邮件读列)· ADR-0092 D2 / D4 · ADR-0105 D7

        os-decision-facets

        • ① 长远合理性:用户自选语言是终态;B 只在既有白名单机制上加一项,不扩大契约、不增特例 —— 荐 B。
        • ② 实际业务拉动:已实测(hotcrm 4 语言 × 16 节点 × 0 可本地化);A 下每个应用要各造一条系统盖章路径才能填列。
        • ③ 防 AI 犯错:B 让 AI 写应用时不必发明权限绕行;错值由 BCP-47 形状校验响亮拒绝并回退部署默认。
        • ④ 创业阶段不扩散:B = 三行 + 一个 pin,无新能力面;A 会让盖章逻辑在应用间扩散。
        • 推荐:B(回退 A)。
        • 置信缺口:objectui 个人设置表单是否存在、cloud 仓是否另有用户语言面 —— 均未测。

        Generated by Claude Code

        Activity

        Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

        Metadata

        Metadata

        Assignees

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
          Skip to content

          [Decision] May a user set their own sys_user.locale? — the ADR-0092 D2 self-service whitelist stays {name, image} after #13881 (column lands readonly, system-context writes only) #14787

          Description

          @claude

          Filed by the domain:spec seat (session_017RbbUMnxkUnWhE4j94v8FE) from the contract review of PR #14775 (#13881, isolated reviewer verdict 5518923117, "Decisions to file for the maintainer" item 1). Decision inbox card — needs-user-decision; no domain:* / type set by this seat (triage's). Depends on PR #14775 landing (the column itself); nothing here blocks that landing.

          背景

          #13881 的裁决(2026-09-01,A 路线)给 sys_user 加了一等列 locale(PR #14775)。落地形状:该列 readonly,只有系统上下文(system context)能写 —— 不在 ADR-0092 D2 的用户自助编辑白名单 SYS_USER_PROFILE_EDIT_FIELDS = {name, image} 里,也不在 plugin-auth 的 MANAGED_EXTENSION_EDITABLE_FIELDS.sys_user 里;今天没有任何管理员界面写它。首个消费者 hotcrm#1185 可以在系统上下文里盖章,所以 PR 可以不等本决策先落地。剩下的问题是身份表写路径的安全边界,裁决没有覆盖,座位不能自裁。

          带 re-check 命令的前提

          • 白名单今天是 {name, image}:git grep -n "SYS_USER_PROFILE_EDIT_FIELDS" origin/main -- packages/plugins/plugin-auth/src/sys-user-writable-fields.ts
          • 可编辑扩展字段表没有 sys_user 条目:git grep -n "MANAGED_EXTENSION_EDITABLE_FIELDS" origin/main -- packages/plugins/plugin-auth/src/managed-extension-fields.ts
          • 列已落地且 readonly(PR feat(spec,platform-objects,service-messaging,plugin-auth): sys_user.locale + per-recipient notification locale (#13881) #14775 合并后成立):git grep -n -A3 "locale: Field.text" origin/main -- packages/platform-objects/src/identity/sys-user.object.ts
          • better-auth 的 /update-user 走不到这列(它不是 better-auth 字段):git grep -n "additionalFields" origin/main -- packages/plugins/plugin-auth/src/auth-schema-config.ts(只有 invitation 模型有)

          一句话问题

          一个中文用户登录进来,想把自己的通知语言从英文改成中文 —— 今天没有任何地方让他自己改,只能等管理员或系统替他填。

          选项 × 真实代价

          选项做什么真实代价
          A 保持现状readonly,只有系统上下文写(hotcrm 之类的应用在系统上下文里盖章)用户收到的通知语言由别人决定;平台自己的 Studio/Console 没有入口让用户改语言;每个应用都要自己造一条「系统替用户填语言」的通道
          B 放开自助白名单加 locale({name, image, locale}),MANAGED_EXTENSION_EDITABLE_FIELDS.sys_userlocale,去掉 readonly,身份写守卫的 pin 同步翻转身份表多开一个用户可写字段(安全边界扩一格);用户填错格式由列的 BCP-47 形状校验响亮拒绝;objectui 侧还要一个「我的语言」表单项才真正可达(另开 ui 卡)

          业务上:A = 像早期企业软件,语言是 IT 管理员配置的;B = 像 Salesforce / Slack 的个人设置页里的 Language 下拉,用户自己选。

          四轴(从业务立场)

          • 实际业务需求:hotcrm 发布 4 种语言、16 个 notify 节点,zh-CN / ja-JP / es-ES 用户此前全收英文 —— 拉动已实测(hotcrm#1185)。但「谁来填这一列」在应用侧还是空的:A 下每个应用都得自己写一段系统上下文盖章逻辑;B 下用户第一次登录就能自己选。
          • 项目长远合理性(权重 ≥50%):用户自己的语言是主流平台的一等个人属性,长远终态就是用户自助;A 是过渡态,过渡态在创业阶段不留。B 不扩大契约(列已在),只是把已有的白名单机制多列一项,特例没有增生。
          • 防 AI 犯错:B 下 AI 写应用元数据时不需要为「怎么替用户填语言」发明系统上下文写路径(那正是 AI 最容易写错权限的地方);用户填错值由 LOCALE_TAG_SHAPE 校验响亮拒绝、回退到部署默认,不会静默死信。A 下 AI 会在应用侧造绕行。
          • 创业阶段不扩散:B 的改动是三行(两个白名单各一项 + 去 readonly)+ 一个 pin,不新增能力面;A 反而会让每个应用各自扩散一套盖章逻辑。

          推荐 + 回退 + 置信缺口

          • 推荐 B(长远终态领起,实测拉动在,防错与不扩散同向)。安全边界扩一格是本卡进决策箱的唯一原因:身份表的用户可写集从 2 个字段变 3 个。
          • 回退 A:列保持只读,由应用在系统上下文里盖章;hotcrm 照常可用。
          • 本分析看不见什么:objectui 是否已有「个人设置」表单可以挂 locale(未测);cloud 仓是否有自己的用户语言设置面(未测,本容器不可达 objectstack-ai/cloud)。

          裁后我会怎么执行(你不用管)

          Refs

          #13881(裁决 5494464459)· PR #14775(复审 5518923117)· hotcrm#1185 · #14641(邀请邮件梯级)· #14762(auth 短信/邮件读列)· ADR-0092 D2 / D4 · ADR-0105 D7

          os-decision-facets

          • ① 长远合理性:用户自选语言是终态;B 只在既有白名单机制上加一项,不扩大契约、不增特例 —— 荐 B。
          • ② 实际业务拉动:已实测(hotcrm 4 语言 × 16 节点 × 0 可本地化);A 下每个应用要各造一条系统盖章路径才能填列。
          • ③ 防 AI 犯错:B 让 AI 写应用时不必发明权限绕行;错值由 BCP-47 形状校验响亮拒绝并回退部署默认。
          • ④ 创业阶段不扩散:B = 三行 + 一个 pin,无新能力面;A 会让盖章逻辑在应用间扩散。
          • 推荐:B(回退 A)。
          • 置信缺口:objectui 个人设置表单是否存在、cloud 仓是否另有用户语言面 —— 均未测。

          Generated by Claude Code

          Activity

          Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

          Metadata

          Metadata

          Assignees

          Type

          No type

          Projects

          No projects

            Milestone

            No milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
            Skip to content

            [Decision] May a user set their own sys_user.locale? — the ADR-0092 D2 self-service whitelist stays {name, image} after #13881 (column lands readonly, system-context writes only) #14787

            Description

            @claude

            Filed by the domain:spec seat (session_017RbbUMnxkUnWhE4j94v8FE) from the contract review of PR #14775 (#13881, isolated reviewer verdict 5518923117, "Decisions to file for the maintainer" item 1). Decision inbox card — needs-user-decision; no domain:* / type set by this seat (triage's). Depends on PR #14775 landing (the column itself); nothing here blocks that landing.

            背景

            #13881 的裁决(2026-09-01,A 路线)给 sys_user 加了一等列 locale(PR #14775)。落地形状:该列 readonly,只有系统上下文(system context)能写 —— 不在 ADR-0092 D2 的用户自助编辑白名单 SYS_USER_PROFILE_EDIT_FIELDS = {name, image} 里,也不在 plugin-auth 的 MANAGED_EXTENSION_EDITABLE_FIELDS.sys_user 里;今天没有任何管理员界面写它。首个消费者 hotcrm#1185 可以在系统上下文里盖章,所以 PR 可以不等本决策先落地。剩下的问题是身份表写路径的安全边界,裁决没有覆盖,座位不能自裁。

            带 re-check 命令的前提

            • 白名单今天是 {name, image}:git grep -n "SYS_USER_PROFILE_EDIT_FIELDS" origin/main -- packages/plugins/plugin-auth/src/sys-user-writable-fields.ts
            • 可编辑扩展字段表没有 sys_user 条目:git grep -n "MANAGED_EXTENSION_EDITABLE_FIELDS" origin/main -- packages/plugins/plugin-auth/src/managed-extension-fields.ts
            • 列已落地且 readonly(PR feat(spec,platform-objects,service-messaging,plugin-auth): sys_user.locale + per-recipient notification locale (#13881) #14775 合并后成立):git grep -n -A3 "locale: Field.text" origin/main -- packages/platform-objects/src/identity/sys-user.object.ts
            • better-auth 的 /update-user 走不到这列(它不是 better-auth 字段):git grep -n "additionalFields" origin/main -- packages/plugins/plugin-auth/src/auth-schema-config.ts(只有 invitation 模型有)

            一句话问题

            一个中文用户登录进来,想把自己的通知语言从英文改成中文 —— 今天没有任何地方让他自己改,只能等管理员或系统替他填。

            选项 × 真实代价

            选项做什么真实代价
            A 保持现状readonly,只有系统上下文写(hotcrm 之类的应用在系统上下文里盖章)用户收到的通知语言由别人决定;平台自己的 Studio/Console 没有入口让用户改语言;每个应用都要自己造一条「系统替用户填语言」的通道
            B 放开自助白名单加 locale({name, image, locale}),MANAGED_EXTENSION_EDITABLE_FIELDS.sys_userlocale,去掉 readonly,身份写守卫的 pin 同步翻转身份表多开一个用户可写字段(安全边界扩一格);用户填错格式由列的 BCP-47 形状校验响亮拒绝;objectui 侧还要一个「我的语言」表单项才真正可达(另开 ui 卡)

            业务上:A = 像早期企业软件,语言是 IT 管理员配置的;B = 像 Salesforce / Slack 的个人设置页里的 Language 下拉,用户自己选。

            四轴(从业务立场)

            • 实际业务需求:hotcrm 发布 4 种语言、16 个 notify 节点,zh-CN / ja-JP / es-ES 用户此前全收英文 —— 拉动已实测(hotcrm#1185)。但「谁来填这一列」在应用侧还是空的:A 下每个应用都得自己写一段系统上下文盖章逻辑;B 下用户第一次登录就能自己选。
            • 项目长远合理性(权重 ≥50%):用户自己的语言是主流平台的一等个人属性,长远终态就是用户自助;A 是过渡态,过渡态在创业阶段不留。B 不扩大契约(列已在),只是把已有的白名单机制多列一项,特例没有增生。
            • 防 AI 犯错:B 下 AI 写应用元数据时不需要为「怎么替用户填语言」发明系统上下文写路径(那正是 AI 最容易写错权限的地方);用户填错值由 LOCALE_TAG_SHAPE 校验响亮拒绝、回退到部署默认,不会静默死信。A 下 AI 会在应用侧造绕行。
            • 创业阶段不扩散:B 的改动是三行(两个白名单各一项 + 去 readonly)+ 一个 pin,不新增能力面;A 反而会让每个应用各自扩散一套盖章逻辑。

            推荐 + 回退 + 置信缺口

            • 推荐 B(长远终态领起,实测拉动在,防错与不扩散同向)。安全边界扩一格是本卡进决策箱的唯一原因:身份表的用户可写集从 2 个字段变 3 个。
            • 回退 A:列保持只读,由应用在系统上下文里盖章;hotcrm 照常可用。
            • 本分析看不见什么:objectui 是否已有「个人设置」表单可以挂 locale(未测);cloud 仓是否有自己的用户语言设置面(未测,本容器不可达 objectstack-ai/cloud)。

            裁后我会怎么执行(你不用管)

            Refs

            #13881(裁决 5494464459)· PR #14775(复审 5518923117)· hotcrm#1185 · #14641(邀请邮件梯级)· #14762(auth 短信/邮件读列)· ADR-0092 D2 / D4 · ADR-0105 D7

            os-decision-facets

            • ① 长远合理性:用户自选语言是终态;B 只在既有白名单机制上加一项,不扩大契约、不增特例 —— 荐 B。
            • ② 实际业务拉动:已实测(hotcrm 4 语言 × 16 节点 × 0 可本地化);A 下每个应用要各造一条系统盖章路径才能填列。
            • ③ 防 AI 犯错:B 让 AI 写应用时不必发明权限绕行;错值由 BCP-47 形状校验响亮拒绝并回退部署默认。
            • ④ 创业阶段不扩散:B = 三行 + 一个 pin,无新能力面;A 会让盖章逻辑在应用间扩散。
            • 推荐:B(回退 A)。
            • 置信缺口:objectui 个人设置表单是否存在、cloud 仓是否另有用户语言面 —— 均未测。

            Generated by Claude Code

            Activity

            Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

            Metadata

            Metadata

            Assignees

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              [Decision] May a user set their own sys_user.locale? — the ADR-0092 D2 self-service whitelist stays {name, image} after #13881 (column lands readonly, system-context writes only) #14787

              Description

              @claude

              Filed by the domain:spec seat (session_017RbbUMnxkUnWhE4j94v8FE) from the contract review of PR #14775 (#13881, isolated reviewer verdict 5518923117, "Decisions to file for the maintainer" item 1). Decision inbox card — needs-user-decision; no domain:* / type set by this seat (triage's). Depends on PR #14775 landing (the column itself); nothing here blocks that landing.

              背景

              #13881 的裁决(2026-09-01,A 路线)给 sys_user 加了一等列 locale(PR #14775)。落地形状:该列 readonly,只有系统上下文(system context)能写 —— 不在 ADR-0092 D2 的用户自助编辑白名单 SYS_USER_PROFILE_EDIT_FIELDS = {name, image} 里,也不在 plugin-auth 的 MANAGED_EXTENSION_EDITABLE_FIELDS.sys_user 里;今天没有任何管理员界面写它。首个消费者 hotcrm#1185 可以在系统上下文里盖章,所以 PR 可以不等本决策先落地。剩下的问题是身份表写路径的安全边界,裁决没有覆盖,座位不能自裁。

              带 re-check 命令的前提

              • 白名单今天是 {name, image}:git grep -n "SYS_USER_PROFILE_EDIT_FIELDS" origin/main -- packages/plugins/plugin-auth/src/sys-user-writable-fields.ts
              • 可编辑扩展字段表没有 sys_user 条目:git grep -n "MANAGED_EXTENSION_EDITABLE_FIELDS" origin/main -- packages/plugins/plugin-auth/src/managed-extension-fields.ts
              • 列已落地且 readonly(PR feat(spec,platform-objects,service-messaging,plugin-auth): sys_user.locale + per-recipient notification locale (#13881) #14775 合并后成立):git grep -n -A3 "locale: Field.text" origin/main -- packages/platform-objects/src/identity/sys-user.object.ts
              • better-auth 的 /update-user 走不到这列(它不是 better-auth 字段):git grep -n "additionalFields" origin/main -- packages/plugins/plugin-auth/src/auth-schema-config.ts(只有 invitation 模型有)

              一句话问题

              一个中文用户登录进来,想把自己的通知语言从英文改成中文 —— 今天没有任何地方让他自己改,只能等管理员或系统替他填。

              选项 × 真实代价

              选项做什么真实代价
              A 保持现状readonly,只有系统上下文写(hotcrm 之类的应用在系统上下文里盖章)用户收到的通知语言由别人决定;平台自己的 Studio/Console 没有入口让用户改语言;每个应用都要自己造一条「系统替用户填语言」的通道
              B 放开自助白名单加 locale({name, image, locale}),MANAGED_EXTENSION_EDITABLE_FIELDS.sys_userlocale,去掉 readonly,身份写守卫的 pin 同步翻转身份表多开一个用户可写字段(安全边界扩一格);用户填错格式由列的 BCP-47 形状校验响亮拒绝;objectui 侧还要一个「我的语言」表单项才真正可达(另开 ui 卡)

              业务上:A = 像早期企业软件,语言是 IT 管理员配置的;B = 像 Salesforce / Slack 的个人设置页里的 Language 下拉,用户自己选。

              四轴(从业务立场)

              • 实际业务需求:hotcrm 发布 4 种语言、16 个 notify 节点,zh-CN / ja-JP / es-ES 用户此前全收英文 —— 拉动已实测(hotcrm#1185)。但「谁来填这一列」在应用侧还是空的:A 下每个应用都得自己写一段系统上下文盖章逻辑;B 下用户第一次登录就能自己选。
              • 项目长远合理性(权重 ≥50%):用户自己的语言是主流平台的一等个人属性,长远终态就是用户自助;A 是过渡态,过渡态在创业阶段不留。B 不扩大契约(列已在),只是把已有的白名单机制多列一项,特例没有增生。
              • 防 AI 犯错:B 下 AI 写应用元数据时不需要为「怎么替用户填语言」发明系统上下文写路径(那正是 AI 最容易写错权限的地方);用户填错值由 LOCALE_TAG_SHAPE 校验响亮拒绝、回退到部署默认,不会静默死信。A 下 AI 会在应用侧造绕行。
              • 创业阶段不扩散:B 的改动是三行(两个白名单各一项 + 去 readonly)+ 一个 pin,不新增能力面;A 反而会让每个应用各自扩散一套盖章逻辑。

              推荐 + 回退 + 置信缺口

              • 推荐 B(长远终态领起,实测拉动在,防错与不扩散同向)。安全边界扩一格是本卡进决策箱的唯一原因:身份表的用户可写集从 2 个字段变 3 个。
              • 回退 A:列保持只读,由应用在系统上下文里盖章;hotcrm 照常可用。
              • 本分析看不见什么:objectui 是否已有「个人设置」表单可以挂 locale(未测);cloud 仓是否有自己的用户语言设置面(未测,本容器不可达 objectstack-ai/cloud)。

              裁后我会怎么执行(你不用管)

              Refs

              #13881(裁决 5494464459)· PR #14775(复审 5518923117)· hotcrm#1185 · #14641(邀请邮件梯级)· #14762(auth 短信/邮件读列)· ADR-0092 D2 / D4 · ADR-0105 D7

              os-decision-facets

              • ① 长远合理性:用户自选语言是终态;B 只在既有白名单机制上加一项,不扩大契约、不增特例 —— 荐 B。
              • ② 实际业务拉动:已实测(hotcrm 4 语言 × 16 节点 × 0 可本地化);A 下每个应用要各造一条系统盖章路径才能填列。
              • ③ 防 AI 犯错:B 让 AI 写应用时不必发明权限绕行;错值由 BCP-47 形状校验响亮拒绝并回退部署默认。
              • ④ 创业阶段不扩散:B = 三行 + 一个 pin,无新能力面;A 会让盖章逻辑在应用间扩散。
              • 推荐:B(回退 A)。
              • 置信缺口:objectui 个人设置表单是否存在、cloud 仓是否另有用户语言面 —— 均未测。

              Generated by Claude Code

              Activity

              Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

              Metadata

              Metadata

              Assignees

              Type

              No type

              Projects

              No projects

                Milestone

                No milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                Skip to content

                [Decision] May a user set their own sys_user.locale? — the ADR-0092 D2 self-service whitelist stays {name, image} after #13881 (column lands readonly, system-context writes only) #14787

                Description

                @claude

                Filed by the domain:spec seat (session_017RbbUMnxkUnWhE4j94v8FE) from the contract review of PR #14775 (#13881, isolated reviewer verdict 5518923117, "Decisions to file for the maintainer" item 1). Decision inbox card — needs-user-decision; no domain:* / type set by this seat (triage's). Depends on PR #14775 landing (the column itself); nothing here blocks that landing.

                背景

                #13881 的裁决(2026-09-01,A 路线)给 sys_user 加了一等列 locale(PR #14775)。落地形状:该列 readonly,只有系统上下文(system context)能写 —— 不在 ADR-0092 D2 的用户自助编辑白名单 SYS_USER_PROFILE_EDIT_FIELDS = {name, image} 里,也不在 plugin-auth 的 MANAGED_EXTENSION_EDITABLE_FIELDS.sys_user 里;今天没有任何管理员界面写它。首个消费者 hotcrm#1185 可以在系统上下文里盖章,所以 PR 可以不等本决策先落地。剩下的问题是身份表写路径的安全边界,裁决没有覆盖,座位不能自裁。

                带 re-check 命令的前提

                • 白名单今天是 {name, image}:git grep -n "SYS_USER_PROFILE_EDIT_FIELDS" origin/main -- packages/plugins/plugin-auth/src/sys-user-writable-fields.ts
                • 可编辑扩展字段表没有 sys_user 条目:git grep -n "MANAGED_EXTENSION_EDITABLE_FIELDS" origin/main -- packages/plugins/plugin-auth/src/managed-extension-fields.ts
                • 列已落地且 readonly(PR feat(spec,platform-objects,service-messaging,plugin-auth): sys_user.locale + per-recipient notification locale (#13881) #14775 合并后成立):git grep -n -A3 "locale: Field.text" origin/main -- packages/platform-objects/src/identity/sys-user.object.ts
                • better-auth 的 /update-user 走不到这列(它不是 better-auth 字段):git grep -n "additionalFields" origin/main -- packages/plugins/plugin-auth/src/auth-schema-config.ts(只有 invitation 模型有)

                一句话问题

                一个中文用户登录进来,想把自己的通知语言从英文改成中文 —— 今天没有任何地方让他自己改,只能等管理员或系统替他填。

                选项 × 真实代价

                选项做什么真实代价
                A 保持现状readonly,只有系统上下文写(hotcrm 之类的应用在系统上下文里盖章)用户收到的通知语言由别人决定;平台自己的 Studio/Console 没有入口让用户改语言;每个应用都要自己造一条「系统替用户填语言」的通道
                B 放开自助白名单加 locale({name, image, locale}),MANAGED_EXTENSION_EDITABLE_FIELDS.sys_userlocale,去掉 readonly,身份写守卫的 pin 同步翻转身份表多开一个用户可写字段(安全边界扩一格);用户填错格式由列的 BCP-47 形状校验响亮拒绝;objectui 侧还要一个「我的语言」表单项才真正可达(另开 ui 卡)

                业务上:A = 像早期企业软件,语言是 IT 管理员配置的;B = 像 Salesforce / Slack 的个人设置页里的 Language 下拉,用户自己选。

                四轴(从业务立场)

                • 实际业务需求:hotcrm 发布 4 种语言、16 个 notify 节点,zh-CN / ja-JP / es-ES 用户此前全收英文 —— 拉动已实测(hotcrm#1185)。但「谁来填这一列」在应用侧还是空的:A 下每个应用都得自己写一段系统上下文盖章逻辑;B 下用户第一次登录就能自己选。
                • 项目长远合理性(权重 ≥50%):用户自己的语言是主流平台的一等个人属性,长远终态就是用户自助;A 是过渡态,过渡态在创业阶段不留。B 不扩大契约(列已在),只是把已有的白名单机制多列一项,特例没有增生。
                • 防 AI 犯错:B 下 AI 写应用元数据时不需要为「怎么替用户填语言」发明系统上下文写路径(那正是 AI 最容易写错权限的地方);用户填错值由 LOCALE_TAG_SHAPE 校验响亮拒绝、回退到部署默认,不会静默死信。A 下 AI 会在应用侧造绕行。
                • 创业阶段不扩散:B 的改动是三行(两个白名单各一项 + 去 readonly)+ 一个 pin,不新增能力面;A 反而会让每个应用各自扩散一套盖章逻辑。

                推荐 + 回退 + 置信缺口

                • 推荐 B(长远终态领起,实测拉动在,防错与不扩散同向)。安全边界扩一格是本卡进决策箱的唯一原因:身份表的用户可写集从 2 个字段变 3 个。
                • 回退 A:列保持只读,由应用在系统上下文里盖章;hotcrm 照常可用。
                • 本分析看不见什么:objectui 是否已有「个人设置」表单可以挂 locale(未测);cloud 仓是否有自己的用户语言设置面(未测,本容器不可达 objectstack-ai/cloud)。

                裁后我会怎么执行(你不用管)

                Refs

                #13881(裁决 5494464459)· PR #14775(复审 5518923117)· hotcrm#1185 · #14641(邀请邮件梯级)· #14762(auth 短信/邮件读列)· ADR-0092 D2 / D4 · ADR-0105 D7

                os-decision-facets

                • ① 长远合理性:用户自选语言是终态;B 只在既有白名单机制上加一项,不扩大契约、不增特例 —— 荐 B。
                • ② 实际业务拉动:已实测(hotcrm 4 语言 × 16 节点 × 0 可本地化);A 下每个应用要各造一条系统盖章路径才能填列。
                • ③ 防 AI 犯错:B 让 AI 写应用时不必发明权限绕行;错值由 BCP-47 形状校验响亮拒绝并回退部署默认。
                • ④ 创业阶段不扩散:B = 三行 + 一个 pin,无新能力面;A 会让盖章逻辑在应用间扩散。
                • 推荐:B(回退 A)。
                • 置信缺口:objectui 个人设置表单是否存在、cloud 仓是否另有用户语言面 —— 均未测。

                Generated by Claude Code

                Activity

                Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                Metadata

                Metadata

                Assignees

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions