Skip to content

docs(skills): gate the usePermissions example on can(), a boolean, and mark its fence (objectui#9671) - #9994

Draft
os-tesla wants to merge 1 commit into
mainfrom
claude/issue-9671-auth-permissions-can-boolean-gate
Draft

os-tesla wants to merge 1 commit into
mainfrom
claude/issue-9671-auth-permissions-can-boolean-gate

Conversation

@os-tesla

Copy link
Copy Markdown
Collaborator

Fixes #9671

Clause-②: yes

Draft against main. skills/** is a governed surface (Tier H): this PR stays draft until an authorized approval, and the seat lands it. Dev run under the domain:skills seat (objectstack#7623), session https://claude.ai/code/session_01W5y9kRg1YtYaMQYExVLRc2.

What changed

One file: skills/objectui/guides/auth-permissions.md, the usePermissions hook example under the heading "usePermissions hook".

  • The two button gates rest on can('contacts', 'update') / can('contacts', 'delete') — a boolean, and the one spelling the guide's neighbouring paragraph ("Publish can(...)") already publishes. check(...) is gone from the example: it answers a PermissionCheckResult object, an object is truthy, and so gated on it both buttons rendered for a denied user.
  • Each gate is annotated : boolean. That annotation is the tripwire: a check(...) put back in that position no longer compiles (TS2322), so the next instance of this defect class goes red in the gate instead of shipping (ablation leg A below).
  • The fence carries the os:check HTML-comment marker on the line directly above it and is tagged tsx, so scripts/check-skill-examples.mjs compiles it against the built dist from now on. To compile it: Button is imported from @object-ui/components, and contact is typed inline as an object with an optional numeric salary. checkField(...) is untouched (it already answers a boolean).
  • The contact argument is dropped from the gates — see the API-shape decision.

API-shape decision: can(object, action), record dropped

can takes no record; the old example passed contact to check. Measured against the BUILT packages/permissions/dist/index.js, with a throwing Proxy handed in as record (any get / has / ownKeys / descriptor read on it throws and is counted), over the guide's own PermissionProvider config:

role action can() (no record) check(..., record).allowed agree
admin update true true yes
admin delete true true yes
viewer update false false yes
viewer delete false false yes
owner update true true yes
owner delete true true yes
  • record property accesses observed: 0evaluatePermission (packages/permissions/src/evaluator.ts) never reads a field of the record. Its only use of record 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. evaluateCondition in the same file has no caller on the check path (its callers are tests and other packages' own evaluators).
  • On the guide's own config the two spellings agree on every (role, action) above, so dropping the argument changes no verdict the example demonstrates.
  • Boundary, stated so the parameter is not read as inert: a role that grants delete whose row rules list only read answers true without a record and false with one — still with zero reads of the record. That is a per-role static toggle, not record-level gating; the guide's own section "Row filters are returned verbatim — nothing interpolates them" already says the package evaluates no row filter client-side. An example passing contact therefore taught record-level gating the package does not perform; the boolean spelling that keeps the example true is can(...) with no record.
  • Ruled out: check(...).allowed (a second spelling for the same question — the drift the card names), and any new helper or package API change (the repair is in the guide).

Line readings (the two the skills rule requires)

reading before (c255b38, unchanged at edbcf1e) after (48d3d2b) delta
skills/objectui/guides/auth-permissions.md 403 409 +6 (budget: net ≤ +6)
whole package, every skills/objectui/**/*.md summed 4,640 4,646 +6

(wc -l; origin/main moved from c255b38 to edbcf1e between dispatch and branch-out with both readings unchanged.)

Marked population

At 48d3d2b the gate prints Marked: 16 ts fence(s) (floor 13), 70 json fence(s) (floor 70); --list shows the new row skills/objectui/guides/auth-permissions.md:225 [tsx] marked pass. Before: the BASE tree carries 85 os:check markers under skills/ plus .claude/skills/ and this branch 86 (git grep -c on the BASE ref vs the worktree — the unbuilt BASE tree cannot print the gate's own line), so ts 15 → 16, json 70 → 70.

MARKED_FLOOR in scripts/check-skill-examples.mjs is left at ts: 13. Its header invites raising it in the PR that adds a marker; that file is outside this card's claimed file surface (the guide, and the eval JSON only if a gate demanded it), so it is not touched here — see Acceptance notes.

Gates (exit captured before any pipe; verdict lines quoted)

Closure build first, under objectstack's verification lock: pnpm exec turbo run build $(node scripts/check-skill-examples.mjs --build-filter) --concurrency=2Tasks: 29 successful, 29 total · lock VERDICT command-exit 0 · held the lock 217s.

command exit verdict line
pnpm check:skill-examples 0 Semantic phase: 16 of 16 ts fence(s) judged, 0 failed. · Every marked skill example holds up against the built types.
pnpm check:skill-eval-tokens 0 Every must_contain token is taught by its own skill bundle. (eval JSON untouched)
pnpm check:skills-paths 0 check-skills-paths: OK (88/89 stated path(s) resolve across 20 guide file(s); 1 baselined).
pnpm check:new-line-citations 0 VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only → exit 0
pnpm check:control-bytes 0 check-control-bytes: OK (scanned 8061 tracked text file(s); skipped 85 binary).
node scripts/check-governed-queue-guard.mjs --test skills/objectui/guides/auth-permissions.md 3 GOVERNED — 1 of 1 path(s) are on a governed surface: skills/** x1 (informational; the expected reading)
node scripts/check-changeset-presence.mjs 0 No source or published contract of a released package changed in this range, so no changeset is owed. — no changeset added
pnpm exec vitest run packages/permissions/src/__tests__/skill-guide-permission-config.test.tsx (the test markdown-test-inputs.mjs --changed names for this guide) 0 Test Files 1 passed (1) · Tests 8 passed (8)
pnpm exec vitest run scripts/__tests__/check-skill-examples.test.ts 0 Test Files 1 passed (1) · Tests 113 passed (113)

NOT MEASURED locally: pnpm lint (repo-level turbo run lint, CI's run). Reason it was not run, not a measurement: the diff is one .md file and eslint.config.js configures no markdown processor.

Ablation

From the committed state 48d3d2b, each leg through objectstack's scripts/ablation-replace.mjs: mutation proven by anchor counts and blob hash, restore proven by blob == HEAD blob (b9f4672) and an empty git diff HEAD, tree clean after each leg.

  • Leg A — defect back, marker kept: check re-added to the destructure and canEdit: boolean = check('contacts', 'update', contact). 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. Direction: red, as predicted.
  • Leg B — same defect, marker removed: gate exit 0, Marked: 15 ts fence(s) (floor 13), Semantic phase: 15 of 15 ts fence(s) judged, 0 failed, Every marked skill example holds up. That is the blind spot the card names, reproduced: unmarked, the defect is invisible to the only instrument that can see it. It also shows the floor at 13 does not red on this fence being unmarked (16 → 15), which is why the floor note above exists.
  • A first attempt at leg B did not run: the tool refused a marker removal whose replacement text was a substring of its anchor (x1 → x1), restored, and exited 1 before the gate; the leg was re-run with a distinct replacement. Recorded so the count of runs is honest.

Acceptance notes

  • noted, not filed: MARKED_FLOOR ts stays 13 with 16 marked; the gate header's invited raise is outside this card's file surface. 承接者: the landing seat (a one-line follow-up in scripts/check-skill-examples.mjs), or none if the seat prefers the floor 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"; the evaluator consults the row rules' actions list and never the filter or the record (measured above). Different paragraph, outside this card's section; prose precision, not a copied-code defect. 承接者: none identified.
  • Board: 0 of 12 open PRs touched this guide at dispatch; PR docs(skills): move the three published guides off the retired dataSource expression root #9378 (objectui#9370) holds three other guides of the package and is not touched.
  • No label writes from this run; needs:contract-review is the seat's to hang on this PR.

维护者速读(草稿)

改了什么skills/objectui/guides/auth-permissions.mdusePermissions 那段示例:两个按钮的显隐改为看 can('contacts', 'update') / can('contacts', 'delete')(布尔值),不再看 check(...)(一个对象,恒为真);示例围栏加了 os:check 标记并改为 tsx,补上编译所需的 Button 导入与 contact 类型;去掉了传给 checkcontact 参数。

为什么改 — 这段代码会被客户项目照抄。原写法下被拒绝的用户也能看到「编辑」「删除」按钮,权限门形同虚设;而该围栏此前未被任何门禁编译,所以没人发现。实测(对着已构建的 @object-ui/permissions)check 从不读取记录的任何字段,can 在指南自己的配置上与 check(..., record).allowed 逐项一致,所以去掉记录参数不改变示例演示的任何结论。

风险与代价(含回滚) — 只改一个已发布技能文件(净 +6 行,在 PM 预算内),不动任何包源码、不动 API,不需要 changeset。回滚 = revert 这一个 commit。留下的一处:门禁的 MARKED_FLOOR 仍是 13(现有 16 个标记围栏),要不要同步抬到 16 交席位决定。

席位意见

你要做的 — 受管面 PR,需要一条获授权账号的 APPROVED review;批准后由席位入队落地。无需其他动作。


Generated by Claude Code

…d mark its fence

The `usePermissions` hook example in `skills/objectui/guides/auth-permissions.md`
gated two buttons on `check('contacts', 'update', contact)`. `check` answers a
`PermissionCheckResult` object, an object is truthy, and so both buttons rendered
for every user, denied ones included. The fence was unmarked, so
`check-skill-examples` never compiled it.

The example now gates on `can('contacts', 'update')` / `can('contacts', 'delete')`
— the one boolean spelling the guide's neighbouring paragraph already publishes —
annotated `: boolean` so a `check(...)` put back in that position fails to compile.
The record argument is dropped: measured against the built `@object-ui/permissions`
dist with a throwing Proxy as the record, `evaluatePermission` performs zero
property reads on it, and `can()` agrees with `check(..., record).allowed` on every
(role, action) of the guide's own config.

The fence carries the `os:check` marker and is tagged `tsx`, imports `Button`
from `@object-ui/components`, and types `contact` inline, so the gate compiles
it against the built dist from now on (Semantic phase: 16 of 16 ts fences).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5y9kRg1YtYaMQYExVLRc2

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 48d3d2b6096664bc50cdbe56ff42a25e4af0f216

In-seat review by the domain:skills seat at the contract-review tier (served tier read at seating, objectstack#7623 marker 5738863635) — the lane's record; Clause-②: yes declared on the card claim (5739257848) and in the PR body, needs:contract-review on both carriers (--pair 9994 reads 0 on the carriers). Verified against GitHub (GET /pulls/9994/files: one file, skills/objectui/guides/auth-permissions.md, +11 / −5) and the fetched branch (origin/claude/issue-9671-auth-permissions-can-boolean-gate at 48d3d2b, base edbcf1e = origin/main), ⛔ not against the report: one commit, trailer pair model-free; the hunk is the usePermissions hook fence only (branch lines 224–246). Tier H (skills/**): this record is piece ① of the terminal; the PR stays draft for an authorized approval.

① Derived judgments

  • The public-surface claim the guide now makes: can(object, action) answers a boolean and is the gate for a rendered control; check(...) answers PermissionCheckResult. Read on origin/main: packages/permissions/src/usePermissions.ts:16 can: (object: string, action: PermissionAction) => boolean and :64 can: (object, action) => ctx.check(object, action).allowed; PermissionContext.ts:14 check: (object, action, record?) => PermissionCheckResult. The guide's own paragraph at :285 already publishes can(...) as the boolean spelling — one spelling, the card's ⛔ on check(...).allowed honoured. Correct.
  • The dropped record argument: evaluator.ts uses record for presence only (:77, if (record && roleConfig.rowPermissions?.length), then reads rp.actions, never a field of the record); evaluateCondition (:194) has no non-test caller in packages/permissions/src (git grep on origin/main). The dev's throwing-Proxy measurement (0 property reads; six (role, action) pairs agree) is consistent with the source. The boundary the PR body states — a role whose row rules omit the action answers differently with and without a record, still with zero reads — is real and stated, not hidden. Correct: no contract widens or narrows; the example teaches the member that exists.
  • The fence: <!-- os:check --> directly above it, tagged tsx, Button imported from @object-ui/components, contact typed inline, both gates annotated : boolean so a check(...) in that position is TS2322 (ablation leg A red; leg B reproduces the unmarked blind spot the card names). Gate line at the head: Semantic phase: 16 of 16 ts fence(s) judged, 0 failed. Nothing else in the guide moves; the pipeline section's step-4 prose (row filter 「evaluated」) is a pre-existing imprecision outside this card's section — noted by the dev, not a copied-code defect, no card (三类外; 承接者:无).
  • Value density, read from the loading agent's seat: this is the example a client agent copies for a gated control; it now compiles under the gate and gates on the boolean. Net +6 lines in the guide, package 4,640 → 4,646, at the claim's ≤ +6 budget.

② Semver level

None owed — skills/** publishes no package; scripts/check-changeset-presence.mjs exit 0 on the branch diff (no changeset is the correct form in this repository).

③ Boundary flags

  • Deviations (6) read: BASE moved c255b38edbcf1e with the guide byte-identical (accepted); MARKED_FLOOR ts left at 13 with 16 marked — the gate's header says MORE is fine and nothing reds, and scripts/check-skill-examples.mjs is outside the claimed surface (accepted; no follow-up card — not one of the three classes); pnpm lint NOT MEASURED locally with the reason (CI's Lint check reads success on the head); ablation leg B's refused first attempt (recorded); the report before CI convergence (per os-dev.md); the report comment's trailing-newline normalisation (the platform's).
  • open_questions: none. out_of_scope_findings (2): the floor and the pipeline prose, both above; neither filed.
  • CI on the head at this record's writing: 25 success, 3 skipped (Test (coverage), its shard matrix, dependabot — conditional jobs on this PR; this seat holds no expected-skip roster for this repository and re-reads them at landing), 4 Test (shard N/4) in progress. Landing waits on the authorized approval in any case and re-reads every check first.

Implemented-by: claude/issue-9671-auth-permissions-can-boolean-gate
Reviewed-by: session_01W5y9kRg1YtYaMQYExVLRc2

VERDICT: PASS


Generated by Claude Code

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

Projects

None yet

2 participants