Repository navigation
rest: filterAppForUser treats app hidden flag as builder-only access gate — built-in account app returns 404 for all normal users #4829
Description
Activity
Claim: PM loop round 1 (explicit dispatch)
Branch:claude/issue-4829-app-hidden-nav-only
Worktree:framework-issue-4829Triage note: gate confirmed still present at
packages/rest/src/rest-server.ts:1841, with a comment citing ADR-0045 (hidden: true= unpublished, externally invisible). That reading conflicts withplatform-objectsACCOUNT_APP's authoredhidden: true("surface via avatar dropdown, not the App Switcher"). The fix must resolve the two semantics contract-first, not special-case blindly — agent will analyze against ADR-0045 and stop with needs_decision if the ADR genuinely mandates the current behavior.Takeover: the 09:27 claim's session ended with no remote branch and no PR — presumed dead. This PM session (worktree objectstack-pm-dispatch-issues-8b2071) takes over the claim per maintainer's explicit dispatch instruction, reusing branch
claude/issue-4829-app-hidden-nav-only. Dispatching now.Escalation:
App.hidden的语义冲突需要维护者裁决开发 agent 已完成勘察,未写代码——两份已记录合同在同一个键上正面冲突,任一解法都要新增公共 spec 字段并修订 ADR,超出单个 PR 可单方面裁决的范围。
背景(证据链,均已核实)
App.hidden与ACCOUNT_APP同一 commit 诞生(74470ad, 2026-05-28)。spec Zod 文档(packages/spec/src/ui/app.zod.ts:1072-1086)明确定义为纯导航语义:"Hidden apps stay fully routable and permission-checked",shell 经头像菜单呈现。- ADR-0045(Accepted 2026-06-12, fa65285)把
hidden:true重定义为 unpublished/外部不可见,依据是 "ADR-0019 launcher contract (hidden, active)"——但 ADR-0019 全文不含 hidden 一词,即 ADR-0045 引用了不存在的合同,且全文未提 account/头像菜单,从未有意识地废除导航语义。 - 两套语义各有完整活体实现:导航侧 = ACCOUNT_APP + console 头像菜单;访问侧 = rest gate(
packages/rest/src/rest-server.ts:1841)+ publish-drafts 翻转(packages/runtime/src/domains/packages.ts:197-227)+ AI 物化路径。
后果(本 issue 现象):普通用户的 account 应用被 REST 层整个抹掉,改密码/头像/会话管理全部 404;持 setup.access 的管理员无感,长期无人发现。
待决问题
App.hidden的唯一语义应是哪个?unpublished(访问门)信号应落在哪里?方案 A —— hidden 恢复纯导航语义(spec 的出生合同),unpublished 另立机器管理键
建议:app 上新增 machine-managed 布尔键,从 authorable input schema 排除,仅由 AI 物化路径写入、publish-drafts 清除;rest gate 改判该键。改动面:spec 新键 + ADR-0045 修订 + cloud 物化写入点 + runtime 翻转点 + rest gate + objectui Unpublished 横幅/Publish 按钮;存量 sys_metadata 行经 ADR-0087 转换(stored 行的 hidden:true 在现政权下语义唯一 = unpublished,可机械改写;代码态 ACCOUNT_APP 不入 sys_metadata,无歧义)。- 轴①(长期架构):呈现与生命周期是正交概念,合并进一个布尔正是本 bug 根因。A 保留 ADR-0045 全部机制(materialize-invisible、publish=翻转、外部 404),只换承载字段,并把悬空引用修正为真实合同——是修正记录,不是推翻决定。
- 轴②(AI 难写错):
hidden:true是 AI/人类为个人设置类应用会自然写出的键(spec 文档至今就这么教)。A 政权下 unpublished 为机器专属键,input schema 不收,结构性杜绝误写。
方案 B —— 维持 ADR-0045 的 hidden=unpublished,另铸导航呈现键(如 showInLauncher:false)
改 ACCOUNT_APP 授权、重写 spec 文档字符串、objectui 切换器 + 头像菜单改读新键。改动面同样跨三仓。- 轴①:把误导性命名固化为公共合同,此后每个读到 hidden 的人都要背注解,长期成本显著。
- 轴②:AI 自然写出的 hidden:true = 全员 404,且授权时零反馈,本类 bug 必然复发。
方案 C —— rest 层特判 name==='account':已否决,无合同可指,纯打补丁。
PM 推荐
A,两轴同向,无需权衡。附属子问题(若采纳 A):unpublished 键放 app 级(忠实 ADR-0045 逐 app 翻转,改动最小)还是 package 级(概念上发布单元是包,但要改 gate 取数路径)——推荐 app 级。无论 A/B,均需修订 ADR-0045 并在 gate/翻转点留 ADR 锚(
scripts/adr-anchors.json)。关联
- 分支已认领未产码:
claude/issue-4829-app-hidden-nav-only - 下游临时对策:steedos-labs/os-project-titanwind-ehr PLAT-DEF-040(PR feat: adapt better-auth adapter and objects for sys_ prefix naming convention #745,修复后可整体删除)
Maintainer decision: 暂缓处理(2026-08-03)。issue 保持 needs-user-decision 挂起,不进入派发队列;上面的两轴分析为裁决时的输入。下游 titanwind 的 PLAT-DEF-040 overlay 临时对策继续生效。
xuyushun441-sys commented
on Aug 4, 2026 CollaboratorMore actions维护者裁决(2026-08-04,经 PM 会话
session_018iARDqtrhQgz6fVHDeDkbQ转达并留档)—— 方向 A1,v17 后实施本条取代 2026-08-03 09:41 的「暂缓处理」记录。裁决过程说明:PM 曾按议题正文的三选项框架建议"hidden 直接退出访问判定",派发后 dev 正确地停手拒做并核出两个否定该方案的事实 —— ① 该访问门是 ADR-0045 §3(Accepted)的正文机制,带 4 个 pin 测试与
publish-drafts的翻转实现,直接删除等于用 patch 静默推翻已接受的 ADR(PD #13 禁止);② 真正的病根是 spec 与 ADR 对同一个键各说各话:app.zod.ts:1073-1086给hidden写的出生合同是纯导航("Hidden apps stay fully routable and permission-checked",且点名 Account 类应用),与 ADR-0045 §3 正面冲突。维护者据更正后的分析重新拍板:裁决内容
- 方向:A1(新机器管理键承载发布门) —— app 上新增机器专属键(如
unpublished,命名实施时定),从可作者化 input schema 排除:仅 AI 物化路径写入、publish-drafts清除;REST 访问门改判该键;hidden回归 spec 出生合同的纯导航语义。同时:修订 ADR-0045(修订状态行,非新 ADR)、在rest-server.ts门本体与runtime/domains/packages.ts翻转点补adr-anchors锚(dev 已核实两处均无锚,正是 PD [WIP] Add Chinese version of the documentation #13 的复发形态)、存量sys_metadata行走 ADR-0087 转换、顺带修 ADR-0045 对 ADR-0019 的悬空引用。键放 app 级(忠实 ADR-0045 逐 app 翻转)。 - 时机:v17 发布之后实施 —— 跨 spec/runtime/rest(+cloud/objectui)的协议变更不进发版窗口。期间下游 titanwind PLAT-DEF-040 的 overlay 临时对策继续有效,修复落地后整体删除。
- 车道:含 spec 可作者化面变更,实施需 spec 车道牵头或协同;本条裁决供其排批,实施单届时按 ADR-0087/retirement 流程拆分。
归档要点(取自停手 dev 的核查,供实施者直接使用)
- 门本体:
packages/rest/src/rest-server.ts:1837-1843;pin 测试:packages/rest/src/rest.test.ts:2890-2922;publish 翻转:packages/runtime/src/domains/packages.ts:195-234;spec 合同:packages/spec/src/ui/app.zod.ts:1073-1086;ADR:docs/adr/0045-...md:96-102, 171-173。 - 两轴论证:呈现与生命周期正交,压进一个布尔是病根;机器专属键让 AI 写出的
hidden:true结构性地只影响导航(防误写),而删门方案的失败方向是误露且静默,比现状更糟。
needs-user-decision摘除,改挂protocol:breaking待 spec 车道排批。
Generated by Claude Code
- 方向:A1(新机器管理键承载发布门) —— app 上新增机器专属键(如
- added and removed
on Aug 4, 2026 4 remaining items
os-project-manager commented
on Aug 8, 2026 CollaboratorMore actions维护者裁定执行存档(2026-08-08 06:2xZ,
domain:cli席代执行)维护者已批准 cli 席决策箱分析中对本卡的处置:按既有裁定(本卡归 spec 车道 lead),标签
domain:cli→domain:spec,使 spec 席的 sweep 能够看见并派发本卡。本次改标是维护者裁定的代执行,非本席自行分诊(cli 席会话session_017uFVNMmTxLpmfQYiuKM1Yx)。bug/pm:queue/protocol:breaking/target:v17保持不变。
Generated by Claude Code
Claim: PM loop (
domain:specseat, #6017; maintainer directive 2026-08-09: continue v17 spec tasks — this is the top open v17 board item)
Session:session_011sGk4SKHqGRgmmqUok1P8M
Branch:claude/issue-4829-unpublished-key-a1
Worktree: cloud session (own container — per the 2026-08-07 window re-ruling's own sizing: «L 级,mode:cloud单容器,spec 车道牵头跨 rest/runtime 协同»)
Domain:domain:spec(maintainer-approved relabel 2026-08-08; spec-lane lead per the A1 ruling)
File surface (cross-lane touches authorized by the ruling itself):packages/spec/src/ui/app.zod.ts(machine-managed key +hiddendoc restored to birth contract, key EXCLUDED from authorable input schema); ADR-0087 conversion + registries;packages/rest/src/rest-server.tsgate (:1837-1843) re-judged on the new key +:2890-2922);packages/rest/src/rest.test.tspins (packages/runtime/src/domains/packages.tsflip (~:195-234);docs/adr/0045-*.mdrevision (+ dangling ADR-0019 reference fix);scripts/adr-anchors.json(gate + flip anchors); generated trees. ⛔ cloud/objectui halves are follow-up cards filed at ACCEPT (materialization write point; Unpublished banner). (Stop on breach; explain in the report.)
Serial constraints cleared:domain:cliin-flight = 0 (verified);domain:specin-flight = #6866 only (landing lap; shares the ADR-0087/migrations tables ⇒ this card branches off main AFTER #6866 merges or re-merges over it — landing serialized by this seat).scripts/adr-anchors.jsonis tonight's known hot file (PR #6608's conflict) — union-keep discipline applies.
Container assessment: L,mode:cloud(per the ruling).
Generated by Claude Code
- added a commit that references this issue
on Aug 9, 2026 OS-DEV-REPORT
{ "issue": 4829, "status": "done", "branch": "claude/issue-4829-unpublished-key-a1", "pr": "https://github.com/objectstack-ai/objectstack/pull/6942", "premise_still_valid": true, "summary": "Implemented direction A1 as ruled. The ADR-0045 §3 publish gate moves off `app.hidden` onto a new machine-managed key `app._unpublished`: declared on AppSchema, written by the AI materialization path (cloud), cleared by `publish-drafts`, and judged by `filterAppForUser`. `hidden` keeps its birth contract (navigation presentation only) with the incident written into its docblock, so the built-in Account app is served to every user again. ADR-0045 is amended (status line + §2/§3 + a closing amendment section) and its dangling ADR-0019 reference corrected — re-verified: ADR-0019 contains zero occurrences of the word `hidden`. Both implementation sites are now anchored in scripts/adr-anchors.json (neither carried an anchor before, which is why §3 could be changed without anyone knowing a decision was being changed). Stored rows cross over via ADR-0087 D2 conversion `app-hidden-to-unpublished`. NAMING: `_unpublished`, not bare `unpublished` — the repo has no per-key authorability switch (check:authorable-surface derives the surface from the Zod graph, so declared == authorable there), and the one marker it does have for machine-written keys is the `_` prefix (ADR-0010's `_lock`/`_provenance` envelope, the prefix lintAuthoredRecordKeys skips). A bare `unpublished` would have been advertised to authors by spec-changes.json, the upgrade guide and the generated reference — teaching exactly the mis-authoring axis ② wants prevented. It is nonetheless DECLARED rather than omitted, because the write path validates against that same schema (saveMetaItem → 422; Registry.validate('app') → AppSchema.parse at objectql/src/registry.ts:1627), so an undeclared key would make the platform's own flip unwritable; the strict door answers `unpublished`/`published`/`draft` with 'publish state is not authorable' instead of a rename suggestion. RETIRED-FROM-LOAD-PATH IS LOAD-BEARING: `hidden` is not retired, so a conversion running on the load path would rewrite defineApp({hidden:true}) and ACCOUNT_APP itself into unpublished apps and reproduce this very bug through the conversion layer; retiredFromLoadPath confines the rewrite to stored rows and that exclusion is pinned in the negative.", "tests": "ALL LOCAL GREEN. Suites: spec `vitest run` 349 files / 9058 tests passed (merged tree); rest `vitest run` 73 files / 1142 passed; runtime `vitest run` 115 files / 1744 passed; objectql sys-metadata-repository 36 passed. GATES — full lint.yml list enumerated and run one by one, ALL PASS: lint, check:slot-lookup, check:query-options-erasure, check:verify-stand-in, check:nul-bytes, check:doc-authoring, check:docs-audit-scope, check:role-word, check:quick-reference-counts, check:adr-anchors (43 anchors, both mine present), check:org-identifier, check:authz-resolver, check:service-providers, check:route-envelope, check:error-code-casing, check:wildcard-fallthrough, check:meta-type-normalized, check:init-service-contract, check:durability-log-level, check:startup-registry-verdict, check:objectui-changeset, check:release-notes, check:release-body, check:workflow-status-functions, check:shard-attestation, check:published-files, check:engine-double-contract, check:kernel-hook-pairs, check:resume-authority-declared, check:driver-memory-census, check:merge-driver, check:spec-parsed-alias, check:tenant-chokepoint, check:type-check-coverage, check:driver-conformance, check:stall-guard, check:skill-frame-sync, check:skill-compatibility, check:agent-model-declared, check:i18n, check:i18n-coverage, check:app-nav-i18n, spec tsc --noEmit, turbo typecheck (120/120 tasks), check:type-check-debt (re-measured after the closure build; no entry above its ceiling; the reported surpluses are pre-existing and in packages I did not touch), plus the spec-package artifact gates: check:generated --reconcile-only, check:skill-docs, check:spec-changes, check:upgrade-guide, check:authorable-surface, check:docs, check:skill-refs, check:react-blocks, check:api-surface, check:exported-any, check:dual-source-exports, check:skill-examples, check:doc-formula-expressions. PLUS the pr-automation.yml blind spot: node scripts/check-adr-0087-registration.mjs --base origin/main → '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … registered app-hidden-to-unpublished (new here)'. REVERSE VERIFICATION (direction predicted BEFORE each run, work committed and pushed first): (1) gate reverted to `hidden` → predicted the account pins and the unpublished-drop pin fail; measured 10 failures across rest.test.ts + meta-app-publish-gate.test.ts, including 'LIST: a user with NO permissions receives the hidden `account` app' — the user-facing bug reproduced on the wire. (2) flip reverted to `hidden` → predicted 0 saveMetaItem calls and the hidden-untouched pin failing; measured 4 failures, exactly those plus the two log/mid-loop pins that read the key. (3) retiredFromLoadPath dropped from the conversion → predicted the authored Account app gets converted on the load path; measured 2 failures including '⛔ does NOT fire on the LOAD path'. Tree restored clean after each. REJECTION CASES assert code+status per ADR-0112: the single-item 404 pin asserts statusCode 404 AND error.code RESOURCE_NOT_FOUND. NEW TEST FILE: packages/rest/src/meta-app-publish-gate.test.ts — check-type-check-coverage --re-measure run after the closure build, no ledger entry raised. SERIALIZATION: #6866 merged mid-flight; merged origin/main (11 commits), resolved the one conflict (objectql test — union-kept main's `intent: 'runtime-only'` plus my key rename), committed the merge FIRST, then re-ran every generator on the merged tree; asserted both siblings' entries coexist (conversions major-17 list has appHiddenToUnpublished, migrations chain has notification-list-cursor-retired, adr-anchors 43 with both of mine, upgrade guide carries both). check:authorable-surface GREEN on the merged tree. CI ON PR #6942 (head 792475a) CONVERGED — no fleet stall: ESLint success, TypeScript Type Check success, Check Changeset success, Spec property liveness success, Test Core 1/3+2/3+3/3 success, Dogfood Regression Gate 1/3+2/3+3/3 success, Dogfood Verify CLI success, Build Core success, Build Docs success, Temporal Conformance (live PG + MySQL) success, Console Pin Freshness success, Check Documentation Links success, Check PR Size success. ONE RED, BY DESIGN: 'ADR maintainer approval' — the #6785 gate on docs/adr/** PRs. Its own log: 'Drafting this PR was fine and stays fine -- only the MERGE is reserved. Green path: the maintainer (@hotlong) reviews and approves'. Left as a DRAFT, not merged, not queued, no auto-merge armed (AGENTS.md PD #14).", "open_questions": [ { "question": "Rename the `publish-drafts` response fields `unhiddenApps` / `unhideError` to match the new mechanism?", "options": [ "A - leave them (what this PR did): they are a wire contract read by the objectui Publish button, and this repo cannot verify or update that consumer, so a rename here is a silent cross-repo break of exactly the kind #4829 is about. A code comment records that the name predates the split.", "B - rename now and document it in the changeset: the names are misleading the moment `hidden` stops being the mechanism, and this PR is already protocol:breaking with an upgrade-guide entry." ], "recommendation": "A, and fold the rename into the objectui follow-up card so both halves land together. The field is informational (a name list for the re-sync announce), so the cost of the stale name is one comment; the cost of getting B wrong is an objectui Publish button that silently stops reporting what it published — a silent failure, which is the failure class this whole card exists to remove." }, { "question": "Should a STORED app row (sys_metadata, not a code artifact) be able to declare nav-hidden going forward?", "options": [ "A - not yet (what this PR did): the conversion permanently rewrites stored `hidden: true` to `_unpublished: true`, so nav-hidden is declarable on the app ARTIFACT (which the conversion never touches) and not on a stored row. Measured as costless today: the Studio app form has no `hidden` control, and under the old regime a stored `hidden: true` could not mean nav-hidden anyway because it 404'd the app for everyone.", "B - make the stored population distinguishable now, e.g. by having the write path stamp `_unpublished: false` on every app save so the conversion can skip already-canonical rows." ], "recommendation": "A. B buys a capability nothing asks for today and pays for it with a stamp on every app write plus a rule about two populations that cloud and objectui would both have to honour. If a stored nav-hidden app is ever wanted it deserves its own decision (a Studio control and a coexistence rule), which the conversion's docblock says out loud rather than leaving the silence to be discovered." } ], "out_of_scope_findings": [ "cloud (follow-up card, PM to file): the AI additive-materialization write point must stamp `_unpublished: true` where it stamps `hidden: true` today, and STOP writing `hidden` — writing it now only affects App Switcher placement and gates nothing. No other change to apply_blueprint's envelope.", "objectui (follow-up card, PM to file): Unpublished banner reads `app._unpublished === true` (was `app.hidden`); Publish button keeps calling POST /packages/:id/publish-drafts unchanged; a direct per-app publish becomes PUT /meta/app/{name} with body {\"_unpublished\": false} (was {\"hidden\": false}); the App Switcher KEEPS filtering on `hidden` and must NOT filter on `_unpublished` (the server already withholds those apps); `unhiddenApps` / `unhideError` unchanged in this PR — rename in lockstep or not at all.", "os-project-titanwind-ehr: PLAT-DEF-040 (PR steedos-labs/os-project-titanwind-ehr#745) — the startup {hidden:false} overlay is deletable once this ships, and its side effect (Account appearing in the App Switcher) goes with it. Verified NO in-repo action is needed for it.", "PM mechanism assumption CONFIRMED, not assumed: a whole-repo sweep found NO in-repo materialization writer of app-level `hidden: true`. The only in-repo writer of app visibility is the publish-drafts flip (which writes false), and the only in-repo `hidden: true` AUTHOR is platform-objects' ACCOUNT_APP, which is a navigation declaration and correctly stays as it is.", "check:type-check-debt reports pre-existing surpluses unrelated to this change (@objectstack/plugin-security TEST_DEBT 21 vs 10 measured; @objectstack/http-conformance 4 vs 3; 307 raw errors across 13 entries below their ceilings). Not touched here — lowering them is bookkeeping the gate itself calls optional, and #6376 already tracks the class." ] }
Generated by Claude Code
ACCEPT → PR #6942. Reviewed under the report-missing fallback (no
OS-DEV-REPORTcomment landed; cloud session is IDLE/review-ready, the PR body carries the full delivery record, CI converged): 24/25 green, and the single red — ADR maintainer approval — is that gate working as designed, not a failure.⛔ This seat does NOT arm auto-merge and does not merge: the diff touches
docs/adr/**, so AGENTS.md Prime Directive #14 reserves the merge for the maintainer, by hand. The dev correctly recognised this, left the PR out of the queue, and requested @hotlong's review. Maintainer action required to land it.Review highlights: the naming call (
_unpublished, not bareunpublished) was measured rather than chosen — there is no per-key authorability switch in this repo,check:authorable-surfacederives authorability from the Zod graph, and the_prefix is the repo's one existing "machine writes this" marker (ADR-0010 envelope,lintAuthoredRecordKeysskip); a bare spelling would have advertised itself in the upgrade guide as newly authorable, teaching the exact mis-authoring axis ② forbids.retiredFromLoadPath: trueon the conversion is load-bearing and pinned in the negative — without it the repair machinery would rewritedefineApp({hidden:true})andACCOUNT_APPinto unpublished apps, reproducing #4829 through its own fix. Three reverse verifications predicted-then-measured, incl. the user-facing repro ("a user with NO permissions receives the hiddenaccountapp", 10 reds). The PM's mechanism assumption (in-repo materialization writer) was tested and confirmed absent. TheunhiddenApps/unhideErrorwire fields deliberately NOT renamed — correct call: renaming a contract this repo cannot verify would be the same class of silent break the PR exists to fix.Follow-up cards filed from the PR's mandatory-answer contracts: cloud half and objectui half (cross-referenced below).
Generated by Claude Code
Queue steward — landing note for this issue's PR #6942 (round 71).
PR #6942 entered the objectstack merge queue at 06:17:28Z and its
ADR Merge Approvalgate returnedfailureat 06:17:46Z, because the diff touchesdocs/adr/0045-additive-materialization-and-visibility-gate.mdand the PR carries no APPROVED review from the maintainer's own account (the ruling being enforced is #6741). Full signature and the gate's verbatim output are on the PR: comment.This is a deterministic governance gate, not a flake — ⛔ the steward did not requeue, since every rebuild reproduces it identically. The only green path is @hotlong's own APPROVED review, which re-runs the check automatically via the
pull_request_reviewtrigger.Nothing here is code work and nothing is being asked of this lane beyond awareness: the queue entry will not land until the approval exists.
Generated by Claude Code
- added a commit that references this issue
on Aug 9, 2026
现象(17.0.0-rc.1,真机 A/B 实测)
@objectstack/rest的filterAppForUser():把应用的
hidden(语义=不进应用切换器的导航呈现开关)当成了「builder-only / 未发布」的访问开关。而平台内置 account(账户)应用按设计就是hidden: true(@objectstack/platform-objects的ACCOUNT_APP,注释:「Surface via the avatar dropdown, not the App Switcher」;console 头像菜单也特判e.name !== 'account'单独给它「个人资料」入口)。结果:
GET /api/v1/meta/app中 account 应用被整个抹掉,点头像 →「个人资料」→ 整屏「App not available — it may still be publishing」;改密码/头像/已关联账户/活动会话/收件箱全部不可达;期望
hidden只影响导航呈现(应用切换器/我的应用),不参与访问判定;或给内置 account 应用豁免。复现
GET /api/v1/meta/app无 account。下游临时对策(修复后可整体删除)
os-project-titanwind-ehr 登记 PLAT-DEF-040,PR steedos-labs/os-project-titanwind-ehr#745:启动插件以系统身份幂等下发
{hidden:false}的 env 级 overlay(即 UI「Publish」按钮做的事)。副作用是「账户」出现在应用切换器——正因如此更说明 hidden 的两个语义(导航 vs 访问)必须拆开。