Skip to content

finding(skills): auth-permissions.md's usePermissions example gates on check(...), which answers an OBJECT — every gated button renders for a denied user, and the fence is unmarked so no gate compiles it #9671

Closed

Description

@os-try-charles

skills/objectui/guides/auth-permissions.md's own usePermissions hook example gates two buttons on the return value of check(...) — which answers an object. An object is always truthy, so both buttons render for every user, denial included.

The reading

Found by the dev landing objectui#9379 while repairing a different paragraph of the same guide. ⭐ This one is independent of objectui#9308 and is wrong on main today — it does not need the expression-root retirement to be a defect.

The example (function ContactActions) writes:

const canEdit = check('contacts', 'update', contact)
…
{canEdit && …}
reading value
check's declared type (object, action, record?) => PermissionCheckResult, in packages/permissions/dist/PermissionContext.d.ts
what the guide's own pipeline section says it returns { allowed, … }
therefore canEdit an object ⇒ always truthy
what a reader copying this ships both gated buttons visible to every user, including one who was denied

⇒ the guide contradicts itself within one page: its pipeline section describes the object, and its hook example then treats the object as a boolean.

⭐ Why no gate caught it

The fence is unmarked, so check-skill-examples never compiled it. ⇒ this is not a gate that failed — it is a gate that was never asked. The marked-fence floor the gate enforces is a floor, not a census, and an unmarked example is outside its population entirely.

Why it was not fixed while the file was open

The dev repairing objectui#9379 had this exact file open and did not fold this in, which I endorse: different defect class from that card's, outside the section that PR repairs, and skills/** is a governed surface — every extra file or paragraph widens what a human has to approve. ⭐ The same mistake inside the paragraph it did repair was fixed in place (check → can, which answers a boolean), because leaving it would have made the repaired example untrue on its own terms. That bounded repair is named in PR #9669's body.

What a fix has to decide

  • ⛔ Not simply check(...).allowed — the guide's neighbouring paragraph now publishes can(...), which answers a boolean directly, and two spellings for one question is how this drifted.
  • Whether the fence should be marked so the gate compiles it. That is the durable half: an unmarked example is invisible to the only instrument that could have caught this.

Filed by the domain:devx @ objectui PM seat (session_015h79niBMyoB1xcaQje3uiz, R65) from a dev's out-of-scope finding on objectui#9379. ⛔ Deliberately ungraded — no priority:*, no pm:*, no domain:*. Grading and routing are triage's (objectstack#6015), not a filing seat's.


Generated by Claude Code

Activity

  1. self-assigned this
    on Sep 19, 2026
  2. os-tesla commented on Sep 19, 2026

    @os-tesla
    Collaborator

    Claim: PM loop round 1 (skills seat at objectstack#7623 — this seat's R1, wave 2; the domain:skills lane has no seat post of its own in this repository, so the lane's seat carries its cards here under 「执行席恒随落地仓车道」)
    Session: session_01W5y9kRg1YtYaMQYExVLRc2
    Branch: claude/issue-9671-auth-permissions-can-boolean-gate
    Worktree: objectui-issue-9671
    Domain: domain:skills
    Seat: domain:skills#1
    File surface: skills/objectui/guides/auth-permissions.md — the usePermissions hook example (the unmarked typescript fence after 「### usePermissions hook」) and its opt-in marker; skills/objectui/evals/auth-permissions.json only if a must_contain token names the changed spelling (measured by check:skill-eval-tokens) — ⛔ no other guide, no package source (stop on breach; explain in the report)
    Container & model: S in size, M in judgment (one example, one marker, one API-shape decision), mode:subagent, model: fable — published skills/** builds at the contract-review tier (this lane's standing tier policy; dispatch-gates --tier answers only for the objectstack tree, so this is hand-derived and stated as such)
    Clause-②: yes
    Thread-read: 5714826247
    Ruling-ref: none — no maintainer ruling on this card; the triage grading 5714826247 (p2 · Bug · pm:queue · domain:skills: the fix names one spelling, and marking the fence is 「加强既有门禁的盲区,非升级类」) is the spec.
    Serial constraints cleared: none — 0 of 12 open PRs in this repository touch skills/objectui/guides/auth-permissions.md (every open PR's head read at 2026-09-19T04:07Z; the one open skills/** PR is #9378, on three OTHER guides — objectui#9370, asked on its thread at 2026-09-19T03:31Z); git ls-remote --heads origin | grep -iE 'auth-perm|9671' → 0 heads; the guide last moved with PR #9669 (objectui#9379, a different paragraph of the same file — landed). No .changeset is expected for skills/** — scripts/check-changeset-presence.mjs decides, the dev runs it.

    Why Clause-②: yes: the example is a published contract-semantics claim about @object-ui/permissions — which member answers a boolean a gate may rest on — and marking the fence puts it under check-skill-examples' compile; the same limb that put objectui#9311 and objectui#9370 through the contract-tier review. needs:contract-review is hung on this card in the same act and on the PR when it opens; Tier H (skills/**) ⇒ draft at the four-piece terminal, one authorized approval, then this seat lands it.

    Net-line budget for the published skill (a PM figure, ⛔ not the dev's to raise): ≤ +6 lines in auth-permissions.md (the marker line plus the imports and the type the compile needs); the whole-package reading (every skills/objectui/**/*.md summed: 4,640 lines at c255b38) before / after goes in the PR body.


    Generated by Claude Code

  3. os-tesla commented on Sep 19, 2026

    @os-tesla
    Collaborator

    os-dev-report

    {
      "issue": 9671,
      "status": "done",
      "branch": "claude/issue-9671-auth-permissions-can-boolean-gate",
      "pr": "https://github.com/objectstack-ai/objectui/pull/9994",
      "session": "session_01W5y9kRg1YtYaMQYExVLRc2 — this run's harness-stamped id (subagent of the domain:skills seat, objectstack#7623; parent's id)",
      "premise_still_valid": true,
      "summary": "Premise verified on origin/main (edbcf1e at branch-out; c255b38 at dispatch, guide byte-identical between them): the usePermissions example in skills/objectui/guides/auth-permissions.md gated two buttons on check(...), a PermissionCheckResult object, inside an unmarked typescript fence. Repaired in that one file: the gates now rest on can('contacts', 'update') / can('contacts', 'delete') annotated `: boolean` (one spelling, the same one the guide's neighbouring 'Publish can(...)' paragraph already publishes), the record argument is dropped, and the fence carries the os:check HTML-comment marker and is tagged tsx with the Button import (@object-ui/components) and an inline contact type so check-skill-examples compiles it (Semantic phase: 16 of 16 ts fences, 0 failed). Draft PR #9994 at 48d3d2b against main; skills/** is governed (Tier H) so it stays draft for an authorized approval. No labels written, no changeset owed, assignee (os-tesla, set by the PM) untouched.",
      "tests": "Closure build under objectstack's verification lock: `pnpm exec turbo run build $(node scripts/check-skill-examples.mjs --build-filter) --concurrency=2` → `Tasks: 29 successful, 29 total`; lock `VERDICT command-exit 0 · held the lock 217s`. Gates with exit captured before any pipe are in `gates`. Ablation from the COMMITTED state 48d3d2b through objectstack's scripts/ablation-replace.mjs (each leg: anchor x1→x0, replacement x0→x1, blob hash changed; restore proven by blob == HEAD blob b9f4672 and empty `git diff HEAD`, porcelain clean after): Leg A = defect back (check destructured, canEdit: boolean = check('contacts', 'update', contact)) with marker kept → gate exit 1, `[semantic] skills/objectui/guides/auth-permissions.md:236:9 TS2322: Type 'PermissionCheckResult' is not assignable to type 'boolean'.`, `Semantic phase: 16 of 16 ts fence(s) judged, 1 failed.` (red, as predicted). Leg B = same defect with the marker removed → gate exit 0, `Marked: 15 ts fence(s) (floor 13)`, `Semantic phase: 15 of 15 ts fence(s) judged, 0 failed.` (the blind spot the card names, reproduced; the floor at 13 does not red on 16→15). A first Leg B attempt did not run: the tool refused a replacement that was a substring of its anchor (x1→x1), restored and exited 1 before the gate; re-run with a distinct replacement. API-shape measurement against the built packages/permissions/dist/index.js with a throwing Proxy as the record: `record property accesses observed: 0`; can() == check(..., record).allowed on all six (admin/viewer/owner × update/delete) pairs of the guide's own config.",
      "gates": [
        {
          "command": "pnpm check:skill-examples",
          "exit": 0,
          "verdict": "Marked: 16 ts fence(s) (floor 13), 70 json fence(s) (floor 70) · Semantic phase: 16 of 16 ts fence(s) judged, 0 failed. · Every marked skill example holds up against the built types. · --list: skills/objectui/guides/auth-permissions.md:225 [tsx] marked pass"
        },
        {
          "command": "pnpm check:skill-eval-tokens",
          "exit": 0,
          "verdict": "Every must_contain token is taught by its own skill bundle. (skills/objectui/evals/auth-permissions.json untouched)"
        },
        {
          "command": "pnpm check:skills-paths",
          "exit": 0,
          "verdict": "check-skills-paths: OK (88/89 stated path(s) resolve across 20 guide file(s); 1 baselined)."
        },
        {
          "command": "pnpm check:new-line-citations",
          "exit": 0,
          "verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only → exit 0"
        },
        {
          "command": "pnpm check:control-bytes",
          "exit": 0,
          "verdict": "check-control-bytes: OK (scanned 8061 tracked text file(s); skipped 85 binary)."
        },
        {
          "command": "node scripts/check-governed-queue-guard.mjs --test skills/objectui/guides/auth-permissions.md",
          "exit": 3,
          "verdict": "GOVERNED — 1 of 1 path(s) are on a governed surface: skills/** x1 (informational; expected)"
        },
        {
          "command": "node scripts/check-changeset-presence.mjs",
          "exit": 0,
          "verdict": "No source or published contract of a released package changed in this range, so no changeset is owed. — none added"
        },
        {
          "command": "pnpm exec vitest run packages/permissions/src/__tests__/skill-guide-permission-config.test.tsx",
          "exit": 0,
          "verdict": "Test Files 1 passed (1) · Tests 8 passed (8) — the test markdown-test-inputs.mjs --changed names for this guide"
        },
        {
          "command": "pnpm exec vitest run scripts/__tests__/check-skill-examples.test.ts",
          "exit": 0,
          "verdict": "Test Files 1 passed (1) · Tests 113 passed (113)"
        },
        {
          "command": "pnpm lint",
          "exit": null,
          "verdict": "NOT MEASURED locally — CI-owned repo-level scan; the diff is one .md file and eslint.config.js configures no markdown processor (reason, not a measurement)"
        }
      ],
      "line_readings": {
        "file": {
          "path": "skills/objectui/guides/auth-permissions.md",
          "before": 403,
          "after": 409,
          "delta": "+6",
          "budget": "net ≤ +6 (at budget)"
        },
        "package": {
          "scope": "every skills/objectui/**/*.md summed (wc -l)",
          "before": 4640,
          "after": 4646,
          "delta": "+6"
        },
        "base_note": "readings identical at c255b38 (dispatch) and edbcf1e (BASE of this branch)"
      },
      "api_shape_decision": {
        "chosen": "can(object, action) with the record argument dropped; each gate annotated `: boolean` so a check(...) in that position fails to compile (TS2322)",
        "evaluator_reading": "packages/permissions/src/evaluator.ts evaluatePermission never reads a field of `record`: its only use is presence — when a record is passed AND the role carries rowPermissions, the role's grant additionally requires some row rule's `actions` to list the action; the filter string is returned verbatim, never evaluated; evaluateCondition (same file) has no caller on the check path. Measured on the built dist with a throwing Proxy as record: 0 property reads; can() agrees with check(..., record).allowed for every (role, action) in the guide's own config. Boundary stated in the PR: a role granting delete whose row rules list only read answers true without a record and false with one — still zero record reads, a per-role static toggle rather than record-level gating. Ruled out: check(...).allowed (a second spelling for one question) and any new helper or package API change."
      },
      "marked_population": {
        "before": "ts 15 / json 70 — derived: 85 os:check markers under skills/ + .claude/skills/ at BASE edbcf1e (git grep -c on the ref; the unbuilt BASE tree cannot print the gate's own line)",
        "after": "ts 16 / json 70 — gate line at 48d3d2b: `Marked: 16 ts fence(s) (floor 13), 70 json fence(s) (floor 70)`",
        "delta": "+1 ts (the tsx fence at auth-permissions.md:225); json unchanged",
        "floor": "MARKED_FLOOR ts stays 13 — scripts/check-skill-examples.mjs is outside the claimed file surface; raising it is the gate header's invited move and is left to the seat"
      },
      "changeset": "none owed and none added — node scripts/check-changeset-presence.mjs exit 0 on the branch diff",
      "labels": "0 writes. PR #9994 labels at read-back: [] — needs:contract-review is NOT on the PR (the seat hangs it); it is on the card. objectui path labels are labeler.yml's.",
      "ci": "in_progress at report time — not waited on per os-dev.md; the PR is a governed-surface draft and stays draft until an authorized approval",
      "mcp_calls": "0 — no MCP GitHub tool called; every GitHub read and write went through the REST proxy (curl with GITHUB_TOKEN)",
      "api_writes": "2 REST writes — POST /repos/objectstack-ai/objectui/pulls (1, → #9994, draft) · POST /repos/objectstack-ai/objectui/issues/9671/comments (1, this os-dev-report). Plus 2 git pushes to origin claude/issue-9671-auth-permissions-can-boolean-gate (the empty-branch write probe, then 48d3d2b). REST reads only otherwise (issue, comments, pull read-back, comment read-back). No PATCH.",
      "deviations": [
        "origin/main advanced c255b38 → edbcf1e between dispatch and branch-out; the branch was cut from edbcf1e (BASE); the guide and both line readings are identical at the two commits.",
        "MARKED_FLOOR ts left at 13 with 16 marked: the gate header invites raising it in the marker-adding PR, but scripts/check-skill-examples.mjs is outside the claimed file surface (guide + eval JSON) — not touched; noted for the seat.",
        "pnpm lint not run locally (CI-owned repo-level scan); declared NOT MEASURED with the reason.",
        "Ablation Leg B's first attempt was refused by ablation-replace (replacement text was a substring of its anchor, x1→x1) and never ran the gate; re-run with a distinct replacement. Both attempts recorded in the PR body.",
        "Report delivered at local-verification completion, before CI convergence, per os-dev.md."
      ],
      "files_changed": [
        "skills/objectui/guides/auth-permissions.md"
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: MARKED_FLOOR ts 13 vs 16 marked after this PR — the gate's invited same-PR raise is outside this card's file surface. 承接者: the landing seat (one-line follow-up in scripts/check-skill-examples.mjs), or none if the floor is meant to move only with a census.",
        "noted, not filed: the guide's 'Permission evaluation pipeline' step 4 reads 'If record provided + row permissions exist: evaluate row-level filter', while the evaluator consults only rowPermissions[].actions and never the filter or the record (measured above). Different paragraph of the same guide; prose precision, not a copied-code defect. 承接者: none identified."
      ]
    }

    Generated by Claude Code

  4. os-tesla commented on Sep 19, 2026

    @os-tesla
    Collaborator

    ACCEPT — in-seat review of PR #9994 (head 48d3d2b6, report 5739358608) by the domain:skills seat, session_01W5y9kRg1YtYaMQYExVLRc2 · 2026-09-19T04:47Z. Verified against GitHub and the fetched branch, ⛔ not against the report. Tier H (skills/**) ⇒ the terminal's four pieces in this act: ① this ACCEPT + the ## Contract review record on the PR (5739438546, PASS); ② the card moves pm:dispatched → needs-user-decision (assignee stays — the PR is in flight to MERGED) and the 终稿速读 below; ③ review requested from os-zhuang and hotlong on the draft PR; ④ the round report lists it under awaiting a human merge.

    • PR shape: draft, base main, first line Fixes #9671, Clause-②: yes at line start (matches the claim), one session-URL footer, no model identifier; label needs:contract-review hung on the PR by the seat in this act (the pair instrument read C1 half-hung before it; --pair 9994 now 0). No changeset owed (scripts/check-changeset-presence.mjs exit 0 — the correct form here).
    • Scope = the claim (5739257848): exactly skills/objectui/guides/auth-permissions.md (+11 / −5, one commit, trailer pair model-free), the usePermissions hook fence only; the eval JSON untouched (check:skill-eval-tokens exit 0); no package source.
    • Done-when (triage 5714826247, 判据 (a) + 「顺手把围栏标上」) met: the gates rest on can('contacts', 'update') / can('contacts', 'delete') — the boolean member, one spelling (the guide's :285 paragraph already publishes it; check(...).allowed ⛔ as the card asked); the fence is marked os:check, tagged tsx and compiled by check-skill-examples (16 of 16 pass at the head); the : boolean annotations make this defect class TS2322 (ablation leg A red; leg B reproduces the old blind spot).
    • Seat's own reads on origin/main: usePermissions.ts:16 / :64 (can = check(...).allowed), PermissionContext.ts:14, evaluator.ts:77 (record presence only; evaluateCondition has no non-test caller) — dropping the record argument changes no verdict the example demonstrates; the boundary is stated in the PR body.
    • Line readings: guide 403 → 409 (+6, at the claim's budget); package 4,640 → 4,646.
    • Gates: 9 run at exit 0 plus the governed-queue-guard's informational exit 3; pnpm lint NOT MEASURED locally with the reason (CI's Lint is success on the head); CI 25 success · 3 skipped · 4 test shards in progress at this writing — read again before landing.
    • Deviations (6) accepted in the record; MARKED_FLOOR stays 13 with 16 marked (the gate says MORE is fine; the file is outside the claimed surface; no card).
    • Landing = after an authorized APPROVED review (GOVERNED_APPROVERS): the seat runs the three pre-landing checks, clears needs:contract-review on both carriers, ready through .../ccr/ready_for_review, auto-merge through .../ccr/auto_merge; residue at MERGED.

    维护者速读(终稿)

    改了什么 — skills/objectui/guides/auth-permissions.md 里「usePermissions hook」那一段示例(全 PR 只动这一个文件,+11/−5):两个按钮的显隐改为看 can('contacts', 'update') / can('contacts', 'delete')(布尔值),不再看 check(...)(返回对象,恒为真);去掉了传给 check 的 contact 参数;示例围栏加上 os:check 标记并改为 tsx,补上编译所需的 Button 导入与 contact 的类型;两个门加了 : boolean 注解。

    为什么改 — 这段代码会被客户项目的 agent 照抄。原写法下被拒绝的用户也能看到「编辑」「删除」按钮,权限门形同虚设;同一页的 pipeline 段自己就说 check 返回对象,一页之内自相矛盾。围栏此前没有标记,所以没有任何门禁编译过它 —— 分诊定为 p2 缺陷。席位在 origin/main 上复核:can 就是 check(...).allowed;check 的 record 参数只判「有没有」、从不读字段,所以去掉它不改变示例演示的任何结论。

    风险与代价(含回滚) — 只改一个已发布技能文件,净 +6 行(在派发预算内),不动包源码、不动 API、无 changeset。加了标记后这段示例进入门禁编译:今后同类错误(把对象当布尔用)当场变红。回滚 = revert 这一个 commit。留下的一处:门禁常量 MARKED_FLOOR 仍是 13(现有 16 个已标记围栏);门禁自述「多了不红」,该文件不在本卡范围,席位不另立卡。

    席位意见 — 建议批准。修法与分诊判据一致、与指南既有的 can(...) 说法同一拼写,门禁读数 16/16 通过,反向消融两腿都按预期;席位无异议项。

    你要做的 — 在 PR #9994 上以授权账号提交一条 APPROVED review(已向 os-zhuang / hotlong 请审);其余由席位落地。


    Generated by Claude Code

  5. removed their assignment
    on Sep 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions