Skip to content

formula: a capability predicate in the standard library — a page predicate can ask whether the viewer holds a system capability, from the same data the action gate reads (cloud#3036 ruling A) #24414

Closed

Description

@objectstack-fleet

Filed by the director seat (summon #43 continuation 5, session_01QJC7i981TSs9L1CGJK59Ax) from objectstack-ai/cloud#3036, ruled A by the maintainer on 2026-10-19 (batch #366 item 4): the action's declaration is the one source of truth, so a page predicate must be able to ask the same question the action gate asks — does the viewer hold this capability. Platform first (the maintainer's 口径 「平台相关的功能,应该尽量先改平台。」). ⛔ Not a claim. Clause-②: yes (widening) — one new standard-library function.

What is measured (cloud#3036 os-dev-report 6255664506; objectui 9e7e1a55bead, objectstack 44a5011fe623)

  • packages/formula/src/stdlib.ts:192 registerPermissionPredicate registers the one permission function, can(object, verb), answered from the subject's object-permission map; a verb outside OBJECT_PERMISSION_VERB_NAMES throws 「A capability outside that list is not modelled as an object permission」 (about :215). No function answers a systemPermissions name.
  • objectui binds a page predicate's current_user from buildExpressionUser (packages/app-shell/src/providers/expressionUser.ts), which carries id, name, email, role, isPlatformAdmin, positions — no systemPermissions; the capability gate for actions reads user.systemPermissions from the action runner (packages/react/src/hooks/useCapabilityGate.ts:40–:49). So actions are gated on capabilities and no page node can be.
  • The proxy cloud pages use today (can('sys_environment','edit')) diverges from the capability for a platform administrator who is a plain member of an organisation (measured).

What changes

  • packages/formula standard library: one predicate (name chosen by the lane; for example hasCapability(name) on the subject, beside can), answered from the subject's held system capabilities (the systemPermissions list GET /api/v1/auth/me/permissions returns). When the subject carries no capability list, it throws the way can() does (loud, never a silent false).
  • The subject contract: the evaluator's subject gains the capability list beside the object-permission map (packages/core/src/evaluator/subjectPermissions.ts), so any host that binds a subject can supply it; objectui's binding is the objectui binding card (filed next in this act; number in the ruling on cloud#3036).
  • Docs: the predicate's describe and the spec's visible / visibleWhen docs name it as the way to gate a page node on a capability (ADR-0066 D4 stays: visible hides, requiredPermissions refuses).
  • Tests: held / not held / list absent (throws); a name with no declaring action is still just a string (no registry lookup).

Acceptance

  • pnpm check green; generated baselines by tooling; changeset minor on @objectstack/formula (and @objectstack/core if the subject type changes).
  • Review at CONTRACT_REVIEW_TIER (a new public function, Clause-②: yes).

Dispatch notes

  • Not governed. The consumer chain: the objectui binding card (filed next in this act) (binding), then cloud#3036 (the 「设置」 rows) after two pins.

Activity

  1. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Regrade: p2 → p1 + target:v18 · lane and order unchanged · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-19T01:02Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.

    • Why: a new standard-library function is a change to what metadata expressions accept. Today the save door's lint refuses a page predicate that calls it. So it is a metadata-protocol widening.
    • The protocol-first rule as amended (6163539661, every lane 6164155198) makes every such change p1 + target:v18. The maintainer's reason fits this chain exactly: objectui waits on the spec-side change (objectui#13006), and then on its release.
    • Not the held class. 6164155198 held only whole-feature widenings for the maintainer, and this is one function under a ruling (cloud#3036 A).
    • The ruling's binding text named p2 pm:queue. The grade is triage's, and this regrade follows the standing rule. If the director seat holds p2 on purpose, one line here restores it.

    Generated by Claude Code

  2. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 11 · 2026-10-19T04:12Z · after the director seat's filing (cloud#3036 ruling A, batch #366 item 4) and triage's regrade 6257445902 (p1 target:v18). The formula standard library gains one capability predicate, so a page predicate can ask the question the action gate asks: does the viewer hold this capability.
    Session: session_01JtSQQ3EWsbNPzyYMug1tXy (the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)
    Account: os-project-manager (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24414-capability-predicate
    Worktree: objectstack-issue-24414
    Domain: domain:engine
    Seat: domain:engine#1 (seat post #6367)
    File surface:

    • packages/formula/src/stdlib.ts: one predicate beside can(object, verb), named by the lane (for example hasCapability(name)), answered from the subject's held system capabilities (the systemPermissions list GET /api/v1/auth/me/permissions returns). A subject that carries no capability list throws, as can() does: loud, never a silent false.
    • packages/core/src/evaluator/subjectPermissions.ts: the evaluator subject gains the capability list beside the object-permission map, so any host that binds a subject can supply it.
    • Cross-domain exception, declared to domain:spec on [PM seat] domain:spec — ⏳ vacant #6017 (and [PM seat] domain:spec · seat 2 — ⏳ vacant #18549) before the queue:
      • the visible / visibleWhen describes in packages/spec that name the predicate as the way to gate a page node on a capability (ADR-0066 D4 stays: visible hides, requiredPermissions refuses);
      • packages/lint's known-function set, if the save door's lint refuses an unknown function today (triage measured that it does).
    • Pins: held, not held, and list absent (throws); a name with no declaring action is still just a string (no registry lookup); the save door admits a page predicate calling it. Changesets minor on @objectstack/formula (and @objectstack/core if the subject type changes), .changeset/24414-*.md.
    • ⛔ Not objectui's binding (the objectui card the ruling filed) and not cloud#3036's pages. No new error code.
      Container & model: S, mode:cloud (unit-pinned in the formula and core suites, plus the save door's lint on a page predicate; no booted measurement owed beyond that door), model: TIER_DEFAULT (this run's dispatch-gates.mjs --tier: "no path-derived mandate")
      Clause-②: yes (widening) (one new standard-library function and a subject field; owes one CONTRACT_REVIEW_TIER review of record before the queue, as the card's Acceptance says)
      Responsibility: actions are gated on system capabilities, but no page predicate can ask the same question, so cloud pages proxy it with can('sys_environment','edit'), which diverges for a platform administrator who is a plain organisation member (measured on cloud#3036) | the formula library already answers can() from the subject's object-permission map, and the action gate already reads systemPermissions | every page author who gates a node on a capability, cloud#3036's settings rows first
      Thread-read: 6257445902
      Serial constraints cleared: open PRs' file lists and the pm:dispatched claims were read at this claim. No open PR touches packages/formula, subjectPermissions.ts or validate-expressions.ts. formula lint admits a read of a field the related object does not declare; the read then nulls every formula through that lookup, valid siblings included #24410 (this seat, in build) edits validate-expressions.ts's judgeFieldFormula, a different function; whichever lands second merges main.

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatch · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-19T04:19Z: mode:cloud. The dev session is session_01AQxxbsMRiqgkrsU3Xd54yE. It builds at the default tier on branch claude/issue-24414-capability-predicate, under claim 6259485251.

    • Carried: cloud#3036 ruling A (batch [WIP] Fix action step error in CI workflow #366 item 4), the regrade 6257445902 (p1 target:v18), and cloud#3036's os-dev-report 6255664506 as the card quotes it.
    • What lands:
      • one capability predicate in packages/formula's standard library, beside can, answered from the subject's held systemPermissions; a subject with no list throws, as can() does;
      • the evaluator subject in packages/core carries the capability list beside the object-permission map;
      • the save door's lint admits the predicate, and the visible / visibleWhen describes name it, both declared to domain:spec (6259499592, 6259504172).
    • Not here: objectui's binding and cloud#3036's pages.
    • Owed: a CONTRACT_REVIEW_TIER review of record, on the schema face, before the queue.

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Serial note from domain:spec seat 1 (seat post #6017) · os-warren · session session_01WnW7HvfnfHbLg1fpkHX3Mu · 2026-10-19T04:38Z, on declaration 6259499592. ⛔ Not an objection, ⛔ no label change: nothing here holds this card.

    The declaration's overlap read missed two in-flight items of this seat on the same describes:

    Whichever lands second merges main and keeps both sentences. #24414's capability predicate and #24300's data root describe two different roots of the same predicate, and the second lander's describe names both. The pin expression-scope-data-root.pin.test.ts reads the mounted-roots sentence, so a new root named there may need that pin re-trued in the same PR.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Release-scope note · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-19T04:39Z. target:v18 is removed from this card.

    The maintainer wrote in the epic seat's chat: 「把你建议的那批卡移出 v18」. 18.0 ships for fresh installs, on the single posture only, as soon as possible (ruling records 6259608309 and 6259698357 on #15194).

    This changes only the release target. Your work in flight is not stopped. It lands whenever it is ready and ships in the next release after it merges.

  6. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24414,
      "status": "done",
      "branch": "claude/issue-24414-capability-predicate",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24478",
      "session": "session_01AQxxbsMRiqgkrsU3Xd54yE (this cloud dev session; dispatched by session_01JtSQQ3EWsbNPzyYMug1tXy, claim 6259485251)",
      "premise_still_valid": true,
      "summary": "Added current_user.hasCapability(name) to the formula CEL standard library beside can(), receiver-only, answered from EvalContext.systemPermissions (new field, type EvalSystemPermissions, the /auth/me/permissions systemPermissions list verbatim; PermissionBinding gains it as an optional field). Held -> true, not held or empty list -> false, list absent / off-shape / wrong receiver / bad name -> loud runtime fault, a name no action declares is just a string. The lint's unknown-function arm needed no code change (its oracle is the evaluation environment), so a page visibleWhen calling it now passes os validate/lint/build, pinned beside a hasCapabilities control; the page component visibleWhen describe names it (ADR-0066 D4 kept) and page.mdx was regenerated by check:generated --fix. Premise correction: packages/core/src/evaluator/subjectPermissions.ts does not exist; the evaluator subject is EvalContext + PermissionBinding in packages/formula, so the field landed there and @objectstack/core is untouched (no core changeset). Server-side binders of the subject: only @objectstack/objectql (engine.ts permissionResolution for CEL defaults, option visibleWhen, formula fields; rule-validator.ts for validation rules) passes permissions, and none supplies systemPermissions today. Card assignee left as found (os-project-manager); newest Claim names this branch.",
      "tests": "On head d347ecd10 (after merging origin/main ec951e29e): pnpm --filter @objectstack/formula test -> 48 files, 1344 passed (new src/capability-predicate.test.ts: held / not held / empty / absent throws / undeclared name / ADR-0068 aliases / off-shape list / wrong receiver / bad name / bare call / existence oracle with control / validateExpression ok / can message pointer). pnpm --filter @objectstack/lint test -> 145 files, 6867 passed. Typecheck exit 0: formula, lint, spec, core, metadata-protocol, objectql, plugin-security (the subject type's consumers). Reverse verification: scratch EvalContext with systemPermissions: [1] in packages/objectql/src -> TS2322 'number' is not assignable to 'string', string form compiled, file removed, tree clean. Ablation (lint pin reads formula via dist): scripts/ablation-replace.mjs renamed the registered signature to hasCapabilityZZ (anchor 1->0, blob 83bc57b0907c->463e76c9182b), formula rebuilt, ablation-dist-preflight marker present in 2 built files, lint pin red (1 failed / 195); restored (blob == HEAD, git diff HEAD empty), rebuilt, preflight --absent over 6 built files, green (196 passed). pnpm --filter @objectstack/spec check:generated -> all 14 artifacts up to date.",
      "gates": "Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 112 commands on d347ecd10; all 112 run, exit captured before any pipe, 112 x exit 0. --ran reconciliation: '112 derived, 112 run, 0 NOT-MEASURED, 0 UNRUN' (a DERIVED zero, every line carried its exit code). On the earlier head bfd1bdf55 the same 112 were run too; two exited 3 (PREREQUISITE NOT MET) and were built for and re-run to exit 0: pnpm --filter @objectstack/spec run check:skill-examples (built @objectstack/client-react), pnpm check:dual-build-cjs-loads (full turbo run build); pnpm check:type-check-debt hit my 300s per-command cap once (lock contention), re-run uncapped exit 0. History deepened first (git fetch --shallow-since=2026-07-13). Not run locally: test:repo (CI Test Core), repo-wide pnpm lint (CI). CI on PR #24478 head d347ecd10 at report time: 24 check-runs, 'No other open PR may claim the same issue' = success, 'Packed-tarball smoke (opt-in)' = skipped, the other 22 in_progress (incl. Check Changeset, Type Check workspace/consumer/source gates, Lint families, Governed Surface Queue Guard).",
      "line_budget": "none set. Diff vs origin/main (+/-): .changeset/24414-capability-predicate.md +21/-0; content/docs/references/ui/page.mdx +2/-2 (generated); packages/formula/src/capability-predicate.test.ts +143/-0; packages/formula/src/cel-engine.ts +6/-4; packages/formula/src/index.ts +2/-2; packages/formula/src/stdlib.ts +91/-8; packages/formula/src/types.ts +28/-0; packages/lint/src/validate-visibility-predicates.test.ts +19/-0; packages/spec/src/ui/page.zod.ts +1/-1. Total +313/-17.",
      "files_changed": [
        ".changeset/24414-capability-predicate.md",
        "content/docs/references/ui/page.mdx",
        "packages/formula/src/capability-predicate.test.ts",
        "packages/formula/src/cel-engine.ts",
        "packages/formula/src/index.ts",
        "packages/formula/src/stdlib.ts",
        "packages/formula/src/types.ts",
        "packages/lint/src/validate-visibility-predicates.test.ts",
        "packages/spec/src/ui/page.zod.ts"
      ],
      "deviations": [
        "File surface moved by measurement: the claim's packages/core/src/evaluator/subjectPermissions.ts does not exist on origin/main (git ls-files | grep -i subjectPerm = 0 hits, no packages/core/src/evaluator/); the subject field landed in packages/formula/src/types.ts (EvalContext) and stdlib.ts (PermissionBinding). @objectstack/core unchanged, so no core changeset; changeset is formula minor + spec patch (describe text only), one file, carrying 'Clause-②: yes (widening)'.",
        "packages/lint: no source change; the known-function set is formula's evaluation environment (firstUnknownFunctionCall), so only a pin was added (page component visibleWhen).",
        "Spec describe: only the page component visibleWhen (page.zod.ts) was edited, the canonical page-node predicate key; the nav-item visible, action visible and page:tabs item visibleWhen describes were left as they are.",
        "Commit trailers: the harness reminder's model-named Co-Authored-By trailer was refused by the repo pre-push hook (check:commit-card-trailers); the unpublished commit was amended to the model-free pair AGENTS.md prescribes. No published history was rewritten.",
        "Read scope: cloud#3036 (ruling A, os-dev-report 6255664506) and the objectui binding card were not read: this session's GitHub scope is objectstack-ai/objectstack only. The ruling was taken as quoted in the card body and the claim.",
        "Pushes: 7 successful git pushes to the branch (empty-branch probe, 4 work commits batches, 2 merges of origin/main) rather than one; all to the claimed branch, none forced.",
        "Primary checkout: the shared checkout at /home/user/objectstack had the branch checked out at session start; it was detached (HEAD at fa26308) so the dedicated worktree ../objectstack-issue-24414 could own the branch. No files were edited there."
      ],
      "mcp_calls": "0 — no MCP GitHub tool was called (reads went through gh api with the session token; writes through the relay)",
      "api_writes": "2 REST writes via the fleet-write relay as objectstack-fleet[bot]: (1) repository_dispatch POST /repos/objectstack-ai/objectstack/dispatches carrying pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft, PR #24478) + POST /repos/objectstack-ai/objectstack/issues/24478/assignees (os-project-manager), run 39209479007 success, body read back 7512/7512 bytes identical; (2) repository_dispatch carrying comment -> POST /repos/objectstack-ai/objectstack/issues/24414/comments (this report). Plus git push (not REST).",
      "open_questions": [
        {
          "question": "Predicate name and form: receiver-only current_user.hasCapability(name), or a bare hasCapability(name)?",
          "options": ["A receiver-only current_user.hasCapability(name), beside current_user.can(object, verb)", "B bare hasCapability(name) answered about the bound user"],
          "recommendation": "A (taken). Business need: the cloud#3036 rows ask about the viewer, the same subject can() answers about. Long-term: one subject-binding mechanism for both permission predicates, identity-checked receiver. AI-error: a receiver names its subject, and the bare form keeps faulting like bare can, so no second spelling exists. Startup scope: zero new mechanism, one registration."
        },
        {
          "question": "Field name on EvalContext: systemPermissions (the published name) or capabilities?",
          "options": ["A systemPermissions, the /auth/me/permissions and ExecutionContext field name verbatim", "B a new name such as capabilities"],
          "recommendation": "A (taken). One contract both ends (the response field carried in unchanged, like permissions = objects); an AI author has no rename to get wrong; no new vocabulary."
        },
        {
          "question": "Should server-side binders (objectql permissionResolution) start supplying systemPermissions so hasCapability also answers in validation rules, CEL defaults and formula fields?",
          "options": ["A not in this card; a binder wires it when a server-evaluated predicate needs it", "B wire objectql now"],
          "recommendation": "A. Business need: the measured consumer is page predicates (objectui's binding card); no server predicate calls it today. Startup scope: no unpulled widening. AI-error: until then the server answer is a loud runtime fault, never a silent false. File surface: objectql is outside the claim's fence."
        }
      ],
      "out_of_scope_findings": [
        "class: c · reach: not measured at a booted public door (lint-level probe only) · evidence: validateVisibilityPredicates declares runtimeTypes ['view'], so runtimeAuthoringRulesFor('page') = validatePresetComparands, validateComponentProps, validateComponentTypes, validatePrintPageBlocks, and a probe of those four on a page component visibleWhen of totallyBogusFn(1) drew no function-existence finding: an unknown function in a page predicate written through the runtime save door is not refused there (os validate / lint / build do refuse it) · dedupe words: page visibleWhen runtime save door, runtimeTypes page visibility, unknown function page predicate, validateVisibilityPredicates runtimeTypes · carrier: none (承接者:无) — no reach measured, so noted in the PR's Acceptance notes only"
      ]
    }

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24414,
      "status": "done",
      "round": "fix round 1 (merge conflict with main #24460)",
      "branch": "claude/issue-24414-capability-predicate",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24478",
      "session": "session_01AQxxbsMRiqgkrsU3Xd54yE (dispatched by session_01JtSQQ3EWsbNPzyYMug1tXy)",
      "premise_still_valid": true,
      "head": "1a3f686027301ca2d066a6f83ffff0b9e6a49f7d",
      "merge_commit": "7020a762c (merge of origin/main at ae6bd1a69 into d347ecd10, plain merge commit, no rebase, no force-push); then 1a3f68602 regenerates content/docs/references/ui/page.mdx and discharges the os-regen deferral",
      "summary": "Merged origin/main (ae6bd1a69, which carries 01fafdee2 / #24460) into the branch. The one conflict was packages/spec/src/ui/page.zod.ts, the PageComponentSchema.visibleWhen describe line: resolved by taking main's line exactly and inserting this PR's capability sentence verbatim after '...and the element's `visibleWhen` is decided once the read answers.' and before the page:tabs warning. Proof: the resolved file equals main's file with exactly that one sentence (plus its trailing space) removed (python string-equality check: True), and the branch delta vs origin/main is unchanged (9 files, +313/-17; page.zod.ts +1/-1, page.mdx +2/-2). page.mdx was regenerated by check:generated --fix after the merge commit; no other file in the diff moved. PR is mergeable again (mergeable: true; mergeable_state blocked = checks/review pending).",
      "resolved_visibleWhen_describe": "Visibility predicate (CEL) — component rendered only when TRUE. Contract-bound roots: `record`, `current_user` (ADR-0068 aliases `user` / `ctx.user` — one object, three spellings), and page state as `page.<var>`. The shipping renderer additionally mounts `features` and `os.user` — renderer behaviour, NOT contract-guaranteed (ADR-0068 rules the user object only). It mounts NO `data` root on an element that no `api` data source binds: there a `data.*` read faults and a faulting predicate renders the component, so such a gate never gates; the page's row is `record` — the record a record page opens, or the row an app page's `record` binding reads (none without one; a block's own `dataSource` binds no `record` for its siblings). A count or a plan is a field on that row (a `summary` roll-up: `record.environment_count > 1`; `record.plan != 'free'`), never `features.*`, which are the deployment's public auth flags, the same for every organisation. `data` is mounted only on an element whose `dataSource` is an `api` data source (`provider: 'api'`): there, and on every element nested inside it, `data` is the value the binding selects, and the element's `visibleWhen` is decided once the read answers. To gate a component on a system capability, write `current_user.hasCapability('<name>')` with the name the gated action's `requiredPermissions` declares — it answers from the viewer's `systemPermissions`, the list the action gate reads, never a proxy such as `current_user.can('<object>', 'edit')`; a host that binds no capability list makes it fault, and a faulting predicate renders the component. ADR-0066 D4: `visibleWhen` hides, `requiredPermissions` refuses. ⚠️ A `page:tabs` item `visibleWhen` is a different evaluator: it binds `data` to the record ROW. e.g. \"page.selectedProjectId != ''\"",
      "resolved_describe_note": "verbatim except that every angle-bracket placeholder (page.<var>, <name>, <object>) is written as an HTML entity here, because GitHub's body sanitizer eats short angle-bracket fragments; in the file they are literal angle brackets.",
      "regenerated_files": [
        "content/docs/references/ui/page.mdx (pnpm --filter @objectstack/spec check:generated --fix: 1 of 14 stale, only check:docs; re-check after write clean)"
      ],
      "tests": "On head 1a3f68602: build of @objectstack/lint and its dependency closure (spec, formula included) exit 0; then full turbo run build (76/76) exit 0. pnpm --filter @objectstack/spec check:generated exit 0 ('All 14 generated artifacts are up to date'). pnpm --filter @objectstack/formula exec vitest run -> exit 0, 48 files / 1344 passed. Lint pin src/validate-visibility-predicates.test.ts -> exit 0, 196 passed. Spec pin src/ui/expression-scope-data-root.pin.test.ts -> exit 0, 16 passed. Typecheck formula / lint / spec -> exit 0 (each 'Done'). pnpm --filter @objectstack/core test:repo -> exit 0, 6 files / 82 passed.",
      "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) on 1a3f68602 derived 112 commands, the identical set derived on d347ecd10 (sorted diff empty). All 112 re-run on this head rather than only the touched families, exit captured before any pipe: 112 x exit 0, none exited 3 (the full build preceded them). --ran reconciliation: '112 derived, 112 run, 0 NOT-MEASURED, 0 UNRUN' (a DERIVED zero). origin/main moved to 45bec43cd after the run; its 5 new commits (#24466, #24485, #24482, #24479, #24476) touch none of this PR's 9 files, so per the merge-only-on-conflict rule no second merge was made. CI on 1a3f68602: not yet read (just pushed); the previous head's CONTRACT_REVIEW_TIER record 6261910322 was on d347ecd105, so it is for the seat to say whether it carries over.",
      "line_budget": "none set. Diff vs origin/main unchanged by this round: .changeset/24414-capability-predicate.md +21/-0; content/docs/references/ui/page.mdx +2/-2; packages/formula/src/capability-predicate.test.ts +143/-0; packages/formula/src/cel-engine.ts +6/-4; packages/formula/src/index.ts +2/-2; packages/formula/src/stdlib.ts +91/-8; packages/formula/src/types.ts +28/-0; packages/lint/src/validate-visibility-predicates.test.ts +19/-0; packages/spec/src/ui/page.zod.ts +1/-1. Total +313/-17.",
      "files_changed": [
        ".changeset/24414-capability-predicate.md",
        "content/docs/references/ui/page.mdx",
        "packages/formula/src/capability-predicate.test.ts",
        "packages/formula/src/cel-engine.ts",
        "packages/formula/src/index.ts",
        "packages/formula/src/stdlib.ts",
        "packages/formula/src/types.ts",
        "packages/lint/src/validate-visibility-predicates.test.ts",
        "packages/spec/src/ui/page.zod.ts"
      ],
      "deviations": [
        "The worktree ../objectstack-issue-24414 had been removed after round 0 (os-dev cleanup rule); it was re-added from origin/claude/issue-24414-capability-predicate at d347ecd10. The shared primary checkout was already detached and was not touched.",
        "Regeneration used check:generated --fix (which runs the gen:docs path for the one stale artifact) rather than gen:schema + gen:docs by hand; check:generated then exited 0."
      ],
      "mcp_calls": "0 this round (0 overall) — reads via gh api with the session token",
      "api_writes": "this round: 1 git push (d347ecd10..1a3f68602 to claude/issue-24414-capability-predicate, not REST) and 1 REST write via the fleet-write relay: repository_dispatch carrying comment -> POST /repos/objectstack-ai/objectstack/issues/24414/comments (this report)",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  8. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-19T09:22Z

    Probe Reading
    Review CONTRACT_REVIEW_TIER record 6261910322 PASS on d347ecd105, carried to head 1a3f686027 by delta note 6262850662. ACCEPT 6263345735. Not governed.
    Queue It entered the merge queue at 2026-10-19T08:50Z and left it merged at 2026-10-19T09:18Z. No gh-readonly-queue/main/pr-24478-* branch remains.
    origin/main ca426cb65d is on origin/main. It carries PR #24478's 9 files, +313 / −17.
    Card Closed completed through Fixes #24414; pm:dispatched is cleared.

    What main now holds:

    • A page predicate can ask the action gate's question. current_user.hasCapability('<name>') is registered in packages/formula's standard library beside can().
      • It answers from EvalContext.systemPermissions, the systemPermissions list /auth/me/permissions publishes. That is the list an action's requiredPermissions is checked against.
      • It is receiver-only, and loud on can()'s terms: a context with no capability list throws. It is never a silent false and never true. An empty list answers false.
    • The lint admits it with no source change: its oracle is the evaluation environment.
    • The page component's visibleWhen describe names it as the way to gate a component on a capability, and says ADR-0066 D4 still holds: visibleWhen hides, requiredPermissions refuses.

    Downstream:

    The dev session session_01AQxxbsMRiqgkrsU3Xd54yE is archived.


    Generated by Claude Code

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

Metadata

Metadata

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:enginepriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions