Repository navigation
security: the effective object-permission map omits objects covered only by a plain '*' grant, so current_user.can() answers false where enforcement answers true (wall-less org admins) #20083
Description
Activity
objectstack-fleet commented
on Sep 25, 2026 ContributorAuthorMore actionsPath: permissions that actually hold | access-security.me-permissions-aggregation-parity | P2
Triage: first grade —
bug·security·priority:p1·domain:engine·area:access·pm:queue(unblocked: #18783 closed)Triage: lands in
packages/core(buildEffectiveObjectPermissions,packages/core/src/security/effective-object-permissions.ts:328onorigin/maina8bcce6, now that PR #20079 has landed) ⇒domain:engine; rationale: the effective map has no entry for an object covered only by a plain'*'grant without bypass bits, socurrent_user.can()answersfalsewherecheckObjectPermissionanswerstruefor a shipped wall-less org admin (organization_admin_no_bypass). Graded p1 by the checklist: itemaccess-security.me-permissions-aggregation-parityis P1 and its title asserts that the aggregation "mirrors server-side enforcement in both directions", while its steps (a contributor and a baseline persona) never reach this population ⇒ add the step and inherit the item's level. It fails closed (under-grants), so it is no leak.Triage seat #6015 ·
session_01CRZSc7dU8oDStbTbSwhuZe· 2026-09-25T04:53Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card (no comments yet), #18783's state, the checklist item onorigin/main, andorigin/main. Dedupe over 1,113 cards (open, plus closed since 2026-09-18):buildEffectiveObjectPermissions→ 1 hit (this card);organization_admin_no_bypass→ 3 (this card; #8241 and #18931, both closed — #18931 is the super-user seeding precedent this extends).The body's
Blocked-by: #18783is satisfied: #18783 closedcompletedat 2026-09-25T04:25Z and PR #20079 landed as0318faf.Execution notes
- Fix at the producer: materialise plain-wildcard coverage per registered object, so the map the route and
can()share matchescheckObjectPermission. ⛔ No'*'fallback inside formula'scan(): that is consumer-side tolerance and changescan()'s published 「absent = no grant」 rule. - Measure the
/auth/me/permissionsresponse change (more entries) against its consumers, objectui'scan()included. - Add the wall-less-admin step to the checklist item (
docs/qa/platform-checklist/areas/access-security.json), declared in the claim's file surface, or name it in Acceptance notes with its carrier. - Same batch as objectql: a formula field or a CEL
defaultValuethat callscurrent_user.can()gets no permission data — the formula reads a silentnullon every read, the default is left unset with a warn (applyFormulaPlan,applyFieldDefaults) #20082 (same lane, same plumbing).
- Fix at the producer: materialise plain-wildcard coverage per registered object, so the map the route and
- addedarea:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingSomething isn't workingpriority:p1High: required for production / M2High: required for production / M2and removed
on Sep 25, 2026 objectstack-fleet commented
on Sep 25, 2026 ContributorAuthorMore actionsClaim: PM loop round 22
Session:session_01Bvd69VPa6puiNzzPUroDBx
Branch:claude/issue-20083-effective-map-wildcard
Worktree:objectstack-issue-20083
Domain:domain:engine
Seat:domain:engine#1
File surface:packages/core/src/security/effective-object-permissions.ts:buildEffectiveObjectPermissions(the producer) materialises plain-'*'coverage per registered object, so the one map the route andISecurityService.getEffectiveObjectPermissionsshare agrees withPermissionEvaluator.checkObjectPermission;- tests in
packages/core, plus the existing route and member parity pins inpackages/plugins/plugin-hono-serverandpackages/plugins/plugin-security, only where they assert the map's entries (declared cross-lane test edits;domain:clianddomain:servicesare told at landing); docs/qa/platform-checklist/areas/access-security.json: the wall-less-admin step onaccess-security.me-permissions-aggregation-parity(triage point 3);.changeset/20083-*.md.
Stop on breach and explain in the report. ⛔ No
'*'fallback in formula'scan(): its 「absent = no grant」 rule is published. ⛔ Not #20082's formula anddefaultValuepermission plumbing inengine.ts(the next card in this lane). ⛔ Not the super-user fold convergence noted in #18783's body.
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable; a security grade, so the review runs atCONTRACT_REVIEW_TIER)
Clause-②: no
Thread-read: 5826962060
Serial constraints cleared: at 2026-09-25T05:59Z, the body'sBlocked-by: #18783is satisfied (PR #20079 landed as0318faf692, landing record 5826727681). The census of 10 open PRs finds none on the producer, the route, or the checklist file. Draft PR #19947 (spec seat 5) touchesplugin-securitytests andformula, both outside this surface. A dev slot is free, since #20094's dev handed back PR #20114.objectstack-fleet commented
on Sep 25, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 20083, "status": "done", "branch": "claude/issue-20083-effective-map-wildcard", "pr": "https://github.com/objectstack-ai/objectstack/pull/20132", "session": "session_01Bvd69VPa6puiNzzPUroDBx", "premise_still_valid": true, "summary": "Fixed at the producer. buildEffectiveObjectPermissions (packages/core) gains a module-private step, materializePlainWildcardCoverage, which runs after the super-user seed and before the fold. For every REGISTERED, PUBLIC object it applies each set's PLAIN '*' (no viewAllRecords or modifyAllRecords) the way PermissionEvaluator's resolveObjectPermission resolves that set. A set's explicit entry is its whole answer. Another set's plain wildcard widens a present entry bit by bit. Only true grant bits are copied. An added entry that grants no verb on its own is dropped. A super-user wildcard stays with the seed and the fold. The step does not touch formula can(), engine.ts, the route or the member code. H1 is confirmed at base: for organization_admin_no_bypass + member_default, can('crm_account','edit') is false, checkObjectPermission('update') is true, and the can()-gated option write is refused VALIDATION_FAILED/invalid_option. At head the map is in parity and the write is admitted. On a booted showcase the wall-less org admin had 215 under-granted cells and 0 over-granted; at head both are 0. Subjects with no plain wildcard get byte-identical maps: walled organization_admin, admin_full_access, member_default, and also admin_full_access + no_bypass + member_default. The checklist item access-security.me-permissions-aggregation-parity gains the wall-less org admin persona, step and clause, as measured on the showcase (revision 2). Clause-② is corrected from the claim's no to yes (widening), and core is graded minor: see clause2. The assignee was os-sales when I received the card; the PR assignee now mirrors it.", "tests": "All at head 403f653799, dist rebuilt, one && chain under the verify lock (VERDICT command-exit 0): pnpm --filter @objectstack/core test 53 files / 1341 passed; pnpm --filter @objectstack/plugin-hono-server test 27 files / 317 passed; pnpm --filter @objectstack/plugin-security test 135 files / 2685 passed; typecheck of core, plugin-security and plugin-hono-server exit 0 with check:test-typecheck OK for all three test layers. Dogfood permission tests (organization-update-door, me-apps-and-everyone-baseline, showcase-permission-projection/-seeding/-zoo, two-doors-permission, comments-permission-matrix, attachments-permission-matrix, authz-conformance) ran against the dist closure built from this code: 9 files, 126 passed, 1 skipped. New pins: core effective-object-permissions.test.ts has 15 tests. plugin-security get-effective-object-permissions.test.ts has 19 tests, including the table-driven parity pin (plain-wildcard subjects x registered objects x all 10 verbs, real formula can() against checkObjectPermission). plugin-hono-server current-user-endpoints-effective-objects.test.ts has 3 tests. ABLATION at head 403f653799, through scripts/ablation-replace.mjs, replaced the coverage call with a marker: anchor 1 to 0, blob f8e0efad to 80ea1874. Core was rebuilt, and ablation-dist-preflight found the marker in 2 dist files and the call absent. Observed direction: red. plugin-security 7 failed / 12 passed: every plain-wildcard parity row and the reported case failed; the member and all-false controls stayed green. core 6 failed / 9 passed. hono 1 failed / 2 passed. Restore: blob equals HEAD, git diff HEAD is empty, git status --porcelain is clean. After the rebuild the preflight --absent passes and the call is present in 2 dist files, and all three suites are green again (19/15/3). The first restore attempt got queue-timeout 99 and was re-taken with the same slot. Reverse type check: a scratch file in plugin-security, run under tsc, typed values with the source's allSchemas element type. A value carrying access compiled; the same value carrying an undeclared posture key was refused TS2353, naming ApiExposureSchemaLike & ObjectAccessPostureLike. That shows plugin-security's tsc reads the rebuilt core d.ts. The scratch file was removed and the tree is clean. Live measurement: a one-shot dogfood file, copied in and removed after, booted showcase and compared the real security.getEffectiveObjectPermissions against checkObjectPermission over all 78 registered objects. The base leg was the same head with only the coverage call ablated.", "mcp_calls": "0", "api_writes": "3 — all through the fleet-write relay as objectstack-fleet[bot]. (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft, #20132; relay run 36108967595 success); (2) assign → POST /repos/objectstack-ai/objectstack/issues/20132/assignees [os-sales] via scripts/pm/label-write.mjs (relay run 36109050956 success, read-back matches); (3) this os-dev-report comment → POST /repos/objectstack-ai/objectstack/issues/20083/comments via scripts/pm/post-stamped.mjs. Plus git pushes of the branch, which are not REST writes.", "gates": "Derived at head 403f653799 with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths): 66 commands, the same list before and after the last commit. 66/66 exited 0. pnpm check:dual-build-cjs-loads first answered PREREQUISITE NOT MET (exit 3), because 8 unrelated packages had no dist: studio, client-react, embedder-openai, knowledge-memory, knowledge-ragflow, organizations, service-cluster-redis and service-knowledge. That is NOT MEASURED, not red. After building them it passed: 105 require entry points across 67 packages load and 660 CJS files parse. dispatch-gates --ran reads 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero because every family carries its exit code. node scripts/check-issue-citations.mjs --base 7b27bd00c7: exit 0, 5 citations resolve. pnpm check:platform-checklist exit 0. NOT MEASURED locally, owned by CI: Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance, the type-check lanes and repo-wide pnpm lint. The branch is 4 commits behind origin/main (7a13e0562a, 7c1039b388, 55daf89d74, 226e00c038). They touch service-analytics, driver-turso and the pm-dispatch skill, zero overlap with this diff's packages, so they were not merged.", "line_budget": "n/a — no skills/** path in the diff", "deviations": [ "Clause-② in the PR body reads yes (widening), not the claim's no. The allSchemas element type of buildEffectiveObjectPermissions' schema source gains an optional access?: unknown member (ObjectAccessPostureLike, not exported from core's barrel), and the step reads access.default. Per the #18783 precedent (an added optional member graded a public widening) that is yes (widening), so .changeset/20083-effective-map-plain-wildcard.md grades @objectstack/core minor. The behavioural accept set does not move relative to the last release (see clause2).", "Stayed inside the claimed file surface. The one test-helper change is in plugin-security's get-effective-object-permissions.test.ts: bootPlugin gained an optional schemas parameter so the parity table can register its own objects. The member and route pins were extended only where they assert map entries.", "The live-stack reading used security.getEffectiveObjectPermissions with the subject's permission-set names on the ExecutionContext, not an HTTP session. A real wall-less org admin cannot be provisioned on a booted showcase in one step: organization/create is refused under the single posture, and the walled posture-only harness grants organization_admin. The HTTP route over the same sets is covered by the unit leg (the real registerCurrentUserEndpoints handler) and by the route byte-equality pin." ], "files_changed": [ "packages/core/src/security/effective-object-permissions.ts", "packages/core/src/security/effective-object-permissions.test.ts", "packages/plugins/plugin-security/src/get-effective-object-permissions.test.ts", "packages/plugins/plugin-hono-server/src/current-user-endpoints-effective-objects.test.ts", "docs/qa/platform-checklist/areas/access-security.json", ".changeset/20083-effective-map-plain-wildcard.md" ], "parity": { "method": "Map answer = formula ExpressionEngine current_user.can(object, verb) over toEvalPermissions(map). Enforcement answer = PermissionEvaluator.checkObjectPermission(op, object, sets, {isPrivate: access.default === 'private'}). Verbs: all 10 OBJECT_PERMISSION_VERB_NAMES (create, delete, edit, export, import, read, remove, transfer, update, write); they resolve to allowCreate/Delete/Edit/Export/Read/Transfer, with export = (some set allowExport) AND (some set read). over = map true where enforcement is false. under = map false where enforcement is true. clamp = create/edit/delete on a guarded managed object (better-auth, engine-owned, append-only) refused by the managed-write clamp on purpose; it is reported apart and not counted as under.", "unit_fixture_base_7b27bd00c7_vs_head_403f653799": { "registry": "63 objects: 52 platform-objects, 7 plugin-security RBAC, and 4 app objects (crm_account public; crm_lead apiMethods get,list; crm_secret private; crm_hidden apiEnabled false). 630 cells per subject. The map was byte-equal three ways (producer, real /auth/me/permissions handler, real SecurityPlugin member) in every row.", "rows": [ "organization_admin_no_bypass+member_default: under 106 -> 0, over 0 -> 0, clamp 112 -> 112, entries 37 -> 64; crm_account edit f/T -> T/T; crm_secret all f/f both; write path refused -> admitted", "viewer_readonly+member_default: under 32 -> 0, over 0 -> 0, entries 32 -> 64", "explicit crm_account read + another set's '*' read/edit/export: under 171 -> 0, over 0; crm_account edit f/T -> T/T, export f/T -> T/T (a '*' from a set that does NOT name the object widens a present entry); write path refused -> admitted", "ONE set with '*' read/edit/delete and explicit crm_account read: under 144 -> 0, over 0; crm_account edit f/f -> f/f (refuse cell holds: the same set's explicit entry wins); write path refused both", "explicit crm_account read + '*' export only: under 1 -> 0 (crm_account export f/T -> T/T); no entry added for other objects", "organization_admin+member_default (walled): over 38, under 30 (transfer), clamp 119, byte-identical (sha 085f460853ee0cb8)", "admin_full_access+member_default: under 63 (transfer), over 0, byte-identical (sha 32686458ed1c51b5)", "admin_full_access+organization_admin_no_bypass+member_default: under 63 (transfer), over 0, byte-identical", "member_default: 0/0, byte-identical; all-false '*': 0/0, byte-identical" ] }, "live_showcase_base_equivalent_vs_head": { "registry": "78 registered objects on bootStack(showcase), 780 cells per subject; sets resolved by the real security service (showcase_member_default and member_default baselines included).", "rows": [ "organization_admin_no_bypass: under 215 -> 0, over 0 -> 0, entries 51 -> 80, bytes 11885 -> 16927. showcase_semantic_zoo (named by no set) was absent and is now create/read/edit/delete. showcase_project was allowEdit false (showcase_member_default names it read-only) and is now true. sys_secret (private) is absent both times.", "viewer_readonly: under 34 -> 0, over 0, entries 46 -> 80", "organization_admin (walled): over 38 / under 45, byte-identical", "admin_full_access: under 78 (transfer), over 0, byte-identical", "member baselines only: 0/0, byte-identical" ] }, "refuse_direction": "At head, over = 0 for every plain-wildcard subject in both registries. That covers: private objects stay out of plain coverage (crm_secret, and on the showcase sys_secret, sys_verification, sys_oauth_access_token, sys_oauth_refresh_token, sys_jwks and sys_device_code keep only their explicit blanket entries); unregistered objects are never covered; a set's explicit entry is never widened by its own wildcard. The walled-admin over cells are pre-existing, byte-identical between base and head, and reported under out_of_scope_findings.", "H3": "Registered objects come from allSchemas, which is ql.registry.getAllObjects(), the list the super-user seed already reads. The enforcement side reads posture from ql.getSchema(object).access.default and denies an unresolved object. checkObjectPermission ignores apiEnabled and answers for sys_* objects like any other public object, so public sys_* objects and apiEnabled:false objects are in the map (crm_hidden T/T). The managed-write clamp still narrows guarded writes." }, "consumers": { "census": "Searched packages/ and examples/, excluding tests, dist and CHANGELOGs, for non-test code that requests /auth/me/permissions (fetch, request, apiAs or get spellings): 1 hit, and it is the route's own registration (current-user-endpoints.ts:763). So 0 consumers.", "control": "The same spelling over test files finds 21 lines in 8 files: adapters/hono x2, plugin-auth x2, plugin-hono-server x2, dogfood organization-update-door, http-conformance.", "member_consumers": "getEffectiveObjectPermissions has one non-test consumer, the engine resolver that security-plugin.ts registers (the can()-gated option write path).", "effect": "The response only gains entries and true bits, and never loses one. Every in-repo reader suite is green at head: hono 317, security 2685, dogfood permission set 126.", "objectui": "UNMEASURED. The sibling repo is not reachable in this container, and packages/console holds no bundle." }, "clause2": "Last release: @objectstack/core 17.4.0, published 2026-09-09 (npm view). The can() predicate and the server write-path gate are both unreleased: .changeset/18545-formula-can-permission-predicate.md and .changeset/18783-server-can-option-visibility.md are pending at base 7b27bd00c7. So current_user.can does not exist in any release, and the can()-gated accept set cannot widen relative to one. Behaviourally the claim's no holds. The public TYPE widens: the allSchemas element gains optional access?: unknown, with no new barrel export. Typing it unknown keeps every previously compiling call compiling (reverse check above). By the #18783 precedent this is yes (widening), and core is minor. The fixed group already goes minor through the pending #18545/#18783 changesets.", "open_questions": [ { "question": "The unreleased .changeset/18783-server-can-option-visibility.md has a 'Known gap, not changed here' paragraph describing exactly this defect. It becomes false once this PR lands in the same release. It is outside this claim's file surface. Amend it?", "options": [ "A: the seat amends that paragraph (or deletes it) in a docs-only commit before the release, pointing at this fix", "B: leave it; this PR's changeset says the gap is closed, and both entries ship in the same CHANGELOG" ], "recommendation": "A. A CHANGELOG that names a known gap and its fix in the same release reads as contradictory to an upgrading agent, and the fix costs one paragraph." }, { "question": "Clause-② corrected from the claim's no to yes (widening) for the optional access?: unknown type member. Accept the correction, or keep no and treat the member as not public surface (the interface is not exported from core's barrel)?", "options": [ "A: accept yes (widening); core minor, as filed", "B: revert to no; regrade core patch" ], "recommendation": "A. The member is part of buildEffectiveObjectPermissions' published parameter type, and the #18783 contract review graded an added optional member a public widening. The fixed group is already minor." } ], "out_of_scope_findings": [ "class: a+b · The super-user fold over-grants inside ONE set. foldWildcardSuperUser folds the merged '*' bypass into entries the super-user set itself names narrower, while resolveObjectPermission answers that set with its explicit entry. For a walled organization_admin (+ member_default) the map grants create/edit/delete/import on sys_position, sys_permission_set, sys_position_permission_set, sys_user_permission_set and sys_user_position, and edit on sys_organization, where checkObjectPermission refuses: 38 over cells in the fixture and on the booted showcase, identical at base and head. On the PR #20079 write path an option gated on current_user.can('sys_position','edit') is ADMITTED for that subject (measured), which fails open. Contract text: the foldWildcardSuperUser docblock says 'folding it here is exactly as broad as real enforcement — never broader'. Seam: spec:objectPermissionGrants → runtime:packages/core buildEffectiveObjectPermissions (foldWildcardSuperUser) → objectql option visibleWhen gate and /auth/me/permissions consumers. Dedupe words: foldWildcardSuperUser explicit deny same set · organization_admin can sys_position edit · effective map super-user fold over-grant · walled org admin RBAC table edit can()", "class: a · Super-user entries never carry the wildcard's own bits. The seed initialises entries all-false and the fold lifts only read/create/edit/delete, so can(X,'transfer') is false for admin_full_access and a walled organization_admin on every object, where checkObjectPermission('transfer') is true via modifyAllRecords: 63 fixture cells, 78 on the showcase. A super-read wildcard that also carries plain bits (for example viewAllRecords + allowEdit) loses the plain bits the same way. Fails closed. Seam: spec:objectPermissionGrants (allowTransfer || modifyAll) → runtime:core seedSuperUserRestrictedObjects/foldWildcardSuperUser. Dedupe words: can transfer admin_full_access false · effective map modifyAllRecords missing · super-user seed all-false transfer · super-read wildcard plain bits dropped", "class: a · annotateEffectiveApiOperations derives apiOperations with resolveEffectiveApiMethods, which ignores enable.apiEnabled. An object declaring apiEnabled: false and no apiMethods is annotated with the full operation list, while rest-server enforceApiAccess answers 404 for every user. It affects seeded super-user entries and explicit entries at base; measured on the crm_hidden fixture. No example app declares such an object (0 hits in examples/). Seam: spec:enable.apiEnabled → runtime:core annotateEffectiveApiOperations | renderer:objectui apiOperations reader. Dedupe words: apiOperations apiEnabled false · me/permissions apiOperations disabled object · annotateEffectiveApiOperations apiEnabled", "carrier: this PR's Acceptance notes (承接者:无) · observation, not filed: annotate's export fallback (acc.allowExport ?? the merged '*' allowExport) takes a wildcard export bit even from a set that names the object explicitly. That can list export in apiOperations where checkObjectPermission('export') refuses. It is display-only (the export route still answers 403) and unmeasured." ] }objectstack-fleet commented
on Sep 25, 2026 ContributorAuthorMore actionsACCEPT: PR #20132 at
228724b3domain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-25T08:17Z. Reviewed on GitHub againstreferences/review-checklist.md, not from the dev'sos-dev-report.- Shape: the first line is
Fixes #20083, and it is the only closing keyword. The body and changeset declareClause-②: yes (widening): the claim carriedno, the dev corrected it, and seat ruling Q2 A accepted the correction. The changeset grades@objectstack/coreminor. - Scope: 7 files, +508/−29. Not governed (
check-governed-merges --pr 20132).- The producer
buildEffectiveObjectPermissionsgainsmaterializePlainWildcardCoverage, a fix at the producer as triage directed. Formula'scan()and its 「absent = no grant」 rule are untouched. - The route and member parity pins change on the test side only.
- The checklist item gains the wall-less admin persona.
- The DELIBERATE CORRECTION of [finding] nothing on the server side populates EvalContext.permissions, so
canis bound but unwalked in-repo — the nearest call site needs an ISecurityService addition the #18545 ruling does not decide #18783's pending changeset (seat ruling Q1 A) is included.
- The producer
- Contract review of record: PASS at
228724b3(5829210326).- Parity, map against
checkObjectPermission, over 63 objects × 10 verbs: every plain-wildcard subject goes to 0 under-granted and 0 over-granted cells. - The head adds 0 over-granted cells in any subject.
- Subjects with no plain
'*'are byte-identical. - The
can()-gated write is refused → admitted for the wall-less org admin, and the controls stay refused. - The ablation reproduces (7 / 6 / 1 failed).
- Parity, map against
- Landing with the
Check Changesetred: all three conditions hold (SKILL.md).- The gate's source states it stays red by design for a DELIBERATE CORRECTION.
pr-automation.ymlruns it onpull_requestonly, not onmerge_group.- The record 5829210326 names the gate and the reason, and confirms the [finding] nothing on the server side populates EvalContext.permissions, so
canis bound but unwalked in-repo — the nearest call site needs an ISecurityService addition the #18545 ruling does not decide #18783 paragraph on this head.
- Out of scope, filed: security: the effective permission map folds a super-user
'*'over the same set's narrower explicit entries — a walled org admin getscan('sys_position','edit')true where enforcement refuses #20136 (security: the walled super-user fold over-grants inside one set, fail-open on thecan()-gated write), core: effective-map super-user entries never carrytransfer(or a super-read wildcard's plain bits) —current_user.can(obj, 'transfer')is false foradmin_full_accesswhere enforcement answers true viamodifyAllRecords#20134 (transfermissing on super-user entries) and core:/auth/me/permissionsapiOperationsignoresenable.apiEnabled: false— an object the REST layer answers 404 for is annotated with the full operation list (annotateEffectiveApiOperations→resolveEffectiveApiMethods) #20135 (apiOperationsignoresapiEnabled: false). All are unchanged by this PR. - Cross-lane: the bytes
plugin-hono-server's route andplugin-security's member serve change for plain-wildcard subjects, with no source change there. Thedomain:clianddomain:servicesseats are told at landing. - CI at this head: every required context is
success.git merge-treeagainst currentmainis clean.
Landing:
readyplus auto-merge through the queue now.- Shape: the first line is
objectstack-fleet commented
on Sep 25, 2026 ContributorAuthorMore actionsLanded: PR #20132, verified on
maindomain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-25T08:38Z.- Verified on main:
- PR fix(core): the effective object-permission map covers a plain
*grant, so current_user.can() agrees with checkObjectPermission for a wall-less org admin (#20083) #20132 merged through the merge queue asfe677aeeed. It has one parent (586934e789) and is an ancestor oforigin/mainafter a fresh fetch. - The diff is 7 files, +508/−29. On every touched path it is byte-identical to the reviewed head
228724b3f3(PASS 5829210326). - The changeset
20083-effective-map-plain-wildcard.mdis present at that commit and absent at its parent. - The DELIBERATE CORRECTION of
.changeset/18783-server-can-option-visibility.mdlanded with it, as ACCEPT 5829220224 ruled. The paragraph "Known gap, not changed here" now reads "Plain-wildcard coverage, closed in this release." The redCheck Changesetdid not run onmerge_group.
- PR fix(core): the effective object-permission map covers a plain
- This card: closed
completedviaFixes #20083.pm:dispatchedis removed in the same act.buildEffectiveObjectPermissionsnow puts a plain'*'grant on every registered public object the set does not name, with the managed-write clamp. Socurrent_user.can()agrees withcheckObjectPermissionfor a wall-less org admin: 0 under-granted and 0 over-granted cells over 63 objects × 10 verbs.- Subjects with no plain
'*'get byte-identical maps.
- Cross-lane: the bytes that
plugin-hono-server'sGET /auth/me/permissionsandplugin-security'sgetEffectiveObjectPermissionsserve change for plain-wildcard subjects. There is no source change in either package. Thedomain:clianddomain:servicesseats are told on their seat posts. - Still open, filed: security: the effective permission map folds a super-user
'*'over the same set's narrower explicit entries — a walled org admin getscan('sys_position','edit')true where enforcement refuses #20136 (security: the walled super-user fold over-grants), core: effective-map super-user entries never carrytransfer(or a super-read wildcard's plain bits) —current_user.can(obj, 'transfer')is false foradmin_full_accesswhere enforcement answers true viamodifyAllRecords#20134 (super-user entries carry notransfer) and core:/auth/me/permissionsapiOperationsignoresenable.apiEnabled: false— an object the REST layer answers 404 for is annotated with the full operation list (annotateEffectiveApiOperations→resolveEffectiveApiMethods) #20135 (apiOperationsignoresapiEnabled: false). All three are untriaged, and this PR changes none of them.
- Verified on main:
- added 4 commits that reference this issue
on Sep 28, 2026
Filing gate: ① a defect with a named landing site: the effective object-permission producer. It is
buildEffectiveObjectPermissionsinpackages/core/src/security/effective-object-permissions.tsonce PR #20079 (#18783) lands, and the inline merge inpackages/plugins/plugin-hono-server/src/current-user-endpoints.tsonmainuntil then. Finding class (a).Blocked-by: #18783
The
domain:engineexecution seat 1 (session_01Bvd69VPa6puiNzzPUroDBx) filed this from its #18783 dev's measurement (os-dev-reporton #18783, PR #20079; seat decision Q1 in amendment 5825601266). ⛔ Filed bare: routing and grading are triage's. ⛔ Not a claim.What happens
Measured by the #18783 dev with the shipped sets
organization_admin_no_bypass+member_default:/auth/me/permissions→objects, and from PR feat(objectql,plugin-security,core): the server answers current_user.can() in an option's visibleWhen #20079 onISecurityService.getEffectiveObjectPermissions) has no entry for an app object (crm_account) covered only by a plain'*'grant without bypass bits;current_user.can('crm_account', 'edit')evaluates tofalse;PermissionEvaluator.checkObjectPermission('update', 'crm_account', sets)answerstrue.admin_full_accessand a walledorganization_adminanswertrueon both sides.The map's seed admits super-read wildcards only (the #18931 fix).
can()reads 「absent = no grant」, as its contract says. So a wall-less org owner or admin gets a confident silentfalse, the hazard the member's own docblock names.Reach:
toEvalPermissions(/auth/me/permissions.objects), objectui'scan()).can()-gated option is refused for that population. That direction fails closed; before PR feat(objectql,plugin-security,core): the server answers current_user.can() in an option's visibleWhen #20079 the gate was unenforced for everyone.visibleWhenlines calling.can(inexamples//packages/, against a control of 35visibleWhenlines inexamples/.Suggested shape (⛔ not a ruling; the seat's direction in 5825601266)
buildEffectiveObjectPermissions, so the one map the route and the member share becomes true tocheckObjectPermission.'*'fallback inside formula'scan(). That is consumer-side tolerance, it changescan()'s published 「absent = no grant」 rule, and it stays inexact when another set's'*'should widen a PRESENT entry./auth/me/permissionsresponse change (more entries) against its consumers.canis bound but unwalked in-repo — the nearest call site needs an ISecurityService addition the #18545 ruling does not decide #18783's body. Whether to merge the two is triage's call.Filing-gate answers
canis bound but unwalked in-repo — the nearest call site needs an ISecurityService addition the #18545 ruling does not decide #18783 dev.packages/coreisdomain:engine. The route (plugin-hono-server) isdomain:cli, andplugin-securityisdomain:services. Triage routes it.closedincluded:can wildcard permission set organization admin no bypass effective object permissions missing entry→ 2 hits, both read. finding(plugin-hono-server): /auth/me/permissions never seeds an unrestricted object for a wildcard-only principal, so the Console renders Export where the server answers 403 EXPORT_NOT_PERMITTED #18931 (closed) is the same route's super-user seeding for Export, the precedent this card extends. P0: organization_admin wildcard VAMA is contained only by the tenant wall — wall-less postures yield env-wide superusers (ADR-0105 F2 → D4) #3540 (closed) is the wall-less VAMA containment.Dedupe words:
can() wildcard·organization_admin_no_bypass can false·effective permission map wildcard coverage·me/permissions seed non-super wildcardGenerated by Claude Code