Repository navigation
feat(formula): current_user.hasCapability(name) — a page predicate can ask whether the viewer holds a system capability - #24478
Conversation
…n ask whether the viewer holds a system capability EvalContext gains systemPermissions (the /auth/me/permissions list the action gate reads); hasCapability throws when it is absent, as can() does. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AQxxbsMRiqgkrsU3Xd54yE
…r.hasCapability Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AQxxbsMRiqgkrsU3Xd54yE
…lity as the capability gate Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AQxxbsMRiqgkrsU3Xd54yE
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AQxxbsMRiqgkrsU3Xd54yE
…bility Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AQxxbsMRiqgkrsU3Xd54yE
…pability-predicate
…pability-predicate
📓 Docs Drift CheckThis PR changes 2 package(s): 17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 144 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 906cfd34faeab16fdd879006996b1382800b69cb && git checkout 906cfd34faeab16fdd879006996b1382800b69cb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 45bec43cd981684906a0b4c1a8e15d08ca9ec09f 1a3f686027301ca2d066a6f83ffff0b9e6a49f7d && git checkout -B drift-repro 45bec43cd981684906a0b4c1a8e15d08ca9ec09f && git merge --no-ff 1a3f686027301ca2d066a6f83ffff0b9e6a49f7d
node scripts/docs-audit/affected-docs.mjs --json 45bec43cd981684906a0b4c1a8e15d08ca9ec09f
|
Contract reviewServed-tier: Base read against: ① Derived judgmentsThe predicate
The subject contract — judged right.
The lint — judged right, oracle verified. The describe — judged truthful and complete for the key it edits. ② Semver level
③ Boundary flagsDev deviations, each answered:
Residual named in ① for the lane, not for this PR: the latent baseline-set divergence between Implemented-by: VERDICT: PASS Generated by Claude Code |
…pability-predicate # Conflicts: # packages/spec/src/ui/page.zod.ts
…eWhen describe Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AQxxbsMRiqgkrsU3Xd54yE
席位核对 · delta ·
|
| Item | Reading |
|---|---|
| Net diff, seat's own read | Against its base, the PR's net file list on 1a3f686027 equals the one on d347ecd105. Per file, the net patch is byte-identical except two paths. |
packages/spec/src/ui/page.zod.ts |
The head's visibleWhen describe is exactly main's new describe with this PR's one capability sentence inserted, verbatim and unchanged. It sits where it sat before: after "…and the element's visibleWhen is decided once the read answers." and before the page:tabs warning. Checked by string equality. main's data-root sentences are kept as #24460 wrote them. |
content/docs/references/ui/page.mdx |
Regenerated by the tooling. Its two describe occurrences each gain that sentence and nothing else. |
| Record | The PR's own contribution to the one schema-face path is the same sentence the CONTRACT_REVIEW_TIER record 6261910322 (PASS) judged true. It is true beside main's new data sentences too: it names a capability gate, not a data read. So the record carries to this head. |
| Landing | ACCEPT and the ordinary queue once CI on 1a3f686027 is read green. |
Generated by Claude Code
席位核对 ·
|
| Item | Reading |
|---|---|
| Shape | Draft PR → main. The body opens Fixes #24414. 9 files. Not governed. |
| Scope | current_user.hasCapability(name) is registered in packages/formula's standard library beside can(). It is receiver-only and answered from EvalContext.systemPermissions (a new optional field, EvalSystemPermissions); a subject with no list faults loudly. The lint admits it with no source change (its oracle is the evaluation environment). The page component's visibleWhen describe names it. @objectstack/core is untouched, since the card's subjectPermissions.ts does not exist. |
| Seat's own read | The server action gate reads new Set(ec.systemPermissions ?? []), and the predicate reads the same list by exact membership. The lint oracle is buildEnv, the environment compile and evaluate use. |
| Review | CONTRACT_REVIEW_TIER record of record 6261910322, PASS on d347ecd105. It carries to this head (delta 6262850662: the describe is main's new text plus the PR's sentence, verbatim). |
| Declaration | Clause-②: yes (widening) on the claim, the PR body and the changeset (@objectstack/formula minor, @objectstack/spec patch). |
| Cross-lane | domain:spec 6259499592, 6259504172 and addendum 6261919080, 6261923828; domain:devx 6261928398, 6261933255 (the generated page.mdx). |
| Gates | Dev, on 1a3f686027: 112 derived, 112 run, 112 exit 0. |
| CI | At this read on 1a3f686027: every check completed, none failed. |
Disposition:
open_questions: all three accepted (record ③): receiver-only, the published field name, and server binders left unwired (a loud fault there).deviations: all accepted (record ③.1–③.7).out_of_scope_findings: the runtime save door's missing page visibility-predicate rules is filed as lint: the visibility-predicate rules run at the runtime save door for a view but not for a page, so a page visibleWhen calling an unknown function is saved through Studio, REST or MCP and refused only by os validate (ruling B on #23388's principle) #24491 (p1,domain:spec). The navvisibledescribe naming the predicate is a docs-only follow-up, below the gate.
Landing: ordinary queue (pr_ready + automerge_enable through the relay). Fixes #24414 then closes the card. The objectui binding card and cloud#3036 consume it next.
Generated by Claude Code
Fixes #24414
Clause-②: yes (widening)
Dev session
session_01AQxxbsMRiqgkrsU3Xd54yE, dispatched by thedomain:engineseat 1 PM (session_01JtSQQ3EWsbNPzyYMug1tXy, claim 6259485251). Owes oneCONTRACT_REVIEW_TIERreview of record before the queue.What changed
packages/formulastandard library:current_user.hasCapability(name), registered besidecan, receiver-only (a barehasCapability(name)names no subject and does not resolve, exactly as a barecandoes). It answers from the acting subject's held system capabilities: thesystemPermissionslistGET /api/v1/auth/me/permissionsreturns, which is the list the action gate checksrequiredPermissionsagainst (ADR-0066 D4).true; not held:false; empty list:false(a real answer).ok: false,kind: 'runtime'), the waycan()does. Also throws on a non-list, a non-string or empty name, and a receiver that is not the acting subject.can(object, verb)with an out-of-vocabulary verb still throws; its message now also namescurrent_user.hasCapability('NAME')for a capability question. No new error code.EvalContextgainssystemPermissions?: EvalSystemPermissions(a list of names, the published field verbatim), andPermissionBindinggains the same field (optional, so existing constructors still compile).celEngine.evaluatepins it into the binding besidepermissions. Likepermissions, it is not mounted as a CEL variable.validateVisibilityPredicates' unknown-function arm asks formula'sfirstUnknownFunctionCall, whose oracle is the evaluation environment itself (ruling on lint:validate-visibility-predicatespasses unknown CEL functions clean (thevalidateExpressionpremise is falsified) — ruled: extend to function-existence ERROR, scoped supersession of the parse-only ruling #13594, never a name list). Registering the function admits it. Pinned on a page componentvisibleWhen, beside a control (hasCapabilities, still refused asvisibility-predicate-unknown-function).visibleWhendescribe (packages/spec/src/ui/page.zod.ts) namescurrent_user.hasCapability('NAME')as the way to gate a component on a capability, says it reads the list the action gate reads (never acan(...)proxy), says a host that binds no list makes it fault (and a faulting predicate renders the component), and keeps ADR-0066 D4:visibleWhenhides,requiredPermissionsrefuses.content/docs/references/ui/page.mdxregenerated bycheck:generated --fix(only artifact it proved stale)..changeset/24414-capability-predicate.md:@objectstack/formulaminor,@objectstack/specpatch (describe text only), carrying theClause-②line.Where the subject contract really lives (measured, so the file surface moved)
The claim's
packages/core/src/evaluator/subjectPermissions.tsdoes not exist onorigin/main(git ls-files | grep -i subjectPerm= 0 hits;packages/core/srchas noevaluator/directory). The evaluator subject isEvalContext(packages/formula/src/types.ts) plusPermissionBinding(packages/formula/src/stdlib.ts), so the field landed there.@objectstack/coreis untouched, so it gets no changeset.Server-side binders of the subject, and which supply the list today
Every server-side binder that passes object permissions into
EvalContextis in@objectstack/objectql:engine.tspermissionResolutionfeeds CELdefaultValues, optionvisibleWhengates and formula fields, andvalidation/rule-validator.tsfeeds validation rules (permissions:onevaluateValidationRules). None suppliessystemPermissionstoday, sohasCapabilityin a server-evaluated predicate throws loudly until a binder supplies it. The page surface's binder is objectui's (the objectui card the ruling filed), not this PR.Verification
All on head
d347ecd10(after mergingorigin/mainatec951e29e) unless noted.pnpm --filter @objectstack/formula test: 48 files, 1344 tests passed. New filesrc/capability-predicate.test.tspins held / not held / empty list / list absent (throws) / undeclared name is just a string / every ADR-0068 alias / off-shape list / wrong receiver / non-string name / bare call / existence oracle (with an invented-sibling control) /validateExpression('predicate', …)ok / thecanmessage pointer.pnpm --filter @objectstack/lint test: 145 files, 6867 tests passed (the new page-predicate pin included).@objectstack/formula,@objectstack/lint,@objectstack/spec, and the subject type's consumers@objectstack/core,@objectstack/metadata-protocol,@objectstack/objectql,@objectstack/plugin-security..d.ts): a scratch file inpackages/objectql/srcwithsystemPermissions: [1]gaveTS2322: Type 'number' is not assignable to type 'string', the string form compiled; file removed, tree clean.dist/): the registered signature renamed tohasCapabilityZZviascripts/ablation-replace.mjs(anchor 1 to 0, blob83bc57b0907cto463e76c9182b), formula rebuilt,ablation-dist-preflightmarker present in 2 built files, lint pin red (1 failed / 195). Restored (blob equal to HEAD,git diff HEADempty), rebuilt, preflight--absentover 6 built files, pin green (196 passed).pnpm --filter @objectstack/spec check:generated: all 14 artifacts up to date.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) derived 112 commands ond347ecd10; every line run, exit captured before any pipe; 112 exit 0.--ranreconciliation: "112 derived, 112 run, 0 NOT-MEASURED, 0 UNRUN" (a derived zero, every line carried its exit code). Two families first exited 3 (prerequisite) on the earlier head and were built for and re-run, not left unmeasured:check:skill-examples(built@objectstack/client-react) andcheck:dual-build-cjs-loads(fullturbo run build). History deepened first (--shallow-since=2026-07-13).test:repo(CI'sTest Core); the repo-widepnpm lint(CI).Acceptance notes
validateVisibilityPredicatesdeclaresruntimeTypes: ['view'], soruntimeAuthoringRulesFor('page')isvalidatePresetComparands, validateComponentProps, validateComponentTypes, validatePrintPageBlocks; a lint-level probe of those four on a pagevisibleWhenoftotallyBogusFn(1)drew no function-existence finding. So the door this PR admits the predicate at is the CLI one (os validate/os lint/os build,commands: ALL), which is what the pin measures. Not measured through a booted HTTP save.skills/objectstack-formula/SKILL.mdis not edited (it documents bare-callable catalog functions;canis not in it either, andskills/**is a Tier H surface).hasCapabilityis receiver-only, so the catalog drift pin (bare-callables) does not move.systemPermissionsinto page predicates is the objectui card the ruling filed; until it lands, a pagevisibleWhencallinghasCapabilityfaults and the component renders (fail-open, stated in the describe).🤖 Generated with Claude Code
https://claude.ai/code/session_01AQxxbsMRiqgkrsU3Xd54yE
Generated by Claude Code