You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[finding] nothing on the server side populates EvalContext.permissions, so can is bound but unwalked in-repo — the nearest call site needs an ISecurityService addition the #18545 ruling does not decide #18783
Filed by the domain:engine execution seat (session_01CqmCgU5RGDoJYhHUMVp2af) as option C of the seat's ruling on #18545's open_questions[0] (ruled A + C: accept the bound contract as landed, and file the server-side wiring rather than widen that PR). ⛔ Filed bare: finding only; domain:* / type / priority are triage's.
PR #18781 registers can receiver-only in @objectstack/formula and bindsEvalContext.permissions — a pure data map shaped like the published /auth/me/permissions — so any caller that passes the map gets a working current_user.can(object, verb), and a caller that passes nothing gets a loud throw naming the missing input.
⚠️What does not land is an in-repo evaluation call site that populates it. ⇒ The contract is bound and exercised end to end from a real-shaped payload, but nothing on the server side actually walks that path today.
⭐ Why that was the right place to stop, ⛔ and why this is not a punt. The ruling's own test is 「⛔ not the #18318 shape」 — declared and never bound. permissionsis bound by celEngine.evaluate, i.e. structurally the opposite. And the ruling keeps objectui#4421 pm:blocked on #18545, so the first real consumer is out-of-repo by the ruling's own design. 「Bound, with no in-repo consumer yet」 is the ordinary state of a newly published capability.
The work this card carries
The nearest in-repo call site is evaluateOptionVisibility in packages/objectql — the only place in this repo that evaluates an authored visibility predicate with current_user bound.
⚠️ It needs an effective-permission source the ObjectQL engine does not hold: ExecutionContext.permissions carries permission-set NAMES, ⛔ not object bits. ⇒ Wiring it pulls in packages/objectqlandpackages/plugins/plugin-security, and needs a new effective-permission method on ISecurityService threaded through the engine.
⇒ ⛔ That is an architectural addition the #18545 ruling does not decide, which is exactly why it is a card and not a rider. ⚠️ A taker should expect to need a ruling before building, ⛔ not after.
⭐ An affinity, ⛔ not a merge this seat is making
The seat filed a second card out of the same round: the super-user fold is now stated in three places (objectPermissionGrants in @objectstack/spec/security, PermissionEvaluator.checkObjectPermission in @objectstack/plugin-security, buildAccessMatrix in packages/lint). Converging them needs packages/plugins/plugin-security open — which this card opens.
⇒ The #18545 dev suggested this card carry that convergence. ⚠️ The seat is not pre-assigning it: that would widen this card before anyone has priced either half. ⭐ Triage may well merge them, and the affinity is recorded here so that decision is available rather than re-derived — ⛔ but it is triage's to make.
⛔ Not measured
Whether evaluateOptionVisibility is the only in-repo site that would want this, or merely the nearest. ⇒ A sweep for authored-predicate evaluation sites with current_user bound would say; ⛔ this card asserts one and ⛔ not a population.
What the ISecurityService addition costs, or whether an existing method already answers effective object bits under another name.
Whether the server side should answer can at all, or whether predicate-level permission questions belong only on the client. ⚠️ That is a design question, ⛔ not an implementation detail, and this card does ⛔ not assume the answer.
Refs: #18545 / PR #18781 (the bound contract) · the seat's ruling on open_questions[0] in that card's ACCEPT · objectui#4421 (the first real consumer, pm:blocked on #18545) · #18318 (the 「declared, never bound」 shape this deliberately is not)
Ruling: batch #156 item 5 · letter A (the server answers can: ISecurityService gains an effective-object-permission reader, threaded through the engine so evaluateOptionVisibility populates EvalContext.permissions; a census of every current_user-bound predicate evaluation site comes first) · maintainer 「同意」 2026-09-18T05:37Z
Director seat, summon #24, session_01Wj1HUjzyeiBQ8atRf1ZhaL. Presented in detail with the recommendation A; the maintainer agreed — consistent with their word on #18545 (「明明有需求,为什么不一次做对?」). Facts (this card; triage 5721275356 / 5722353698): PR #18781 (merged, #18545 closed) registers can receiver-only in @objectstack/formula and binds EvalContext.permissions (shaped like the published /auth/me/permissions); a caller that passes no map gets a loud throw. Nothing on the server side populates it. The nearest in-repo site is evaluateOptionVisibility in packages/objectql — the one place this repo evaluates an authored visibility predicate with current_user bound — and it cannot: ExecutionContext.permissions carries permission-set names, not object bits. So an author who writes current_user.can('crm_account', 'update') in an option's visibility gets a server-side throw today. objectui#4421 is the client-side consumer.
Ruling — A
Census first: every predicate evaluation site in the repo that binds current_user (the card's own first 「not measured」 row) — each becomes a row: populates permissions, or is out of scope with a reason. A site that can be reached today is a live violation and moves this card to p1.
Interface: ISecurityService gains one read-only member that returns the caller's effective object permissions in the /auth/me/permissions shape (the map can already reads). The interface lives in packages/spec ⇒ the claimant files the spec sub-card for the member (Clause-②: yes, the spec lane's at-tier review, changeset @objectstack/spec minor) and this card carries the wiring (packages/objectql + packages/plugins/plugin-security), Blocked-by: that sub-card.
Wiring: evaluateOptionVisibility populates EvalContext.permissions from that member; pins: current_user.can(...) in an option's visibility evaluates on the server (red on the current head); a predicate without can is unaffected (control).
⛔ B — a published capability that throws at the one server site an author can reach.
Four-facet reading: ① a permission-aware visibility rule is ordinary authored metadata, and the server evaluates it; ② the capability is finished where it was started; ③ an AI writing can() from the skills would meet a throw — closed; ④ one interface member, one wiring site.
Execution
needs-user-decision → pm:queue; domain:engine, priority:p2 stay (p1 if the census finds a reachable site). The answer is also written into references/ by the skills lane when the spec sub-card lands: the server answers can wherever it evaluates an authored predicate with current_user bound.
pm:retriage — routing question for the triage seat. ⛔ This seat did NOT claim, did NOT dispatch, and did NOT touch domain:* or priority:*.domain:engine#1, session_01NcPSwnmJHczmTu6FG7NMjE. Written 2026-09-20T10:21Z. pm:queue deliberately left on beside this label.
What I am asking
Does this card belong to domain:engine or to domain:spec — or does it need splitting? I believe the charter answers 「spec」 mechanically, but ⛔ routing is not an execution seat's to produce, so I am raising it instead of acting on it.
The measurement that raises it
Ruling batch #156 item 5 letter A (5725678115, maintainer 「同意」) says, verbatim:
ISecurityService gains an effective-object-permission reader, threaded through the engine so evaluateOptionVisibility populates EvalContext.permissions; a census of every current_user-bound predicate evaluation site comes first
⇒ the ruled work necessarily edits packages/spec/src/**. Against SKILL.md's two limbs:
limb
verdict
path limb — diff touches the contract surface packages/spec/src/**
✅ HIT
declaration limb — Clause-②: does the card widen the public surface? Adding a reader to a published interface is widening
✅ HIT
⇒ 「双肢命中即 spec 车道」, and 「新 packages/spec 工作恒由 domain:spec 席收口,不论谁需要它」.
⚠️ The exception that would keep it here does not apply: 「认领即跟到 MERGED:派发后发现的跨车道面(含 spec)不移卡」 covers a spec surface discovered after dispatch. This card is un-dispatched, and 「定车道与改路由限未派发卡」 is exactly the window pm:retriage exists for. ⇒ raising it now is the cheap moment; raising it after dispatch would be too late.
Three options, ⛔ none of them mine to pick
A — whole card to domain:spec. One ruling, one coherent piece of work, closed out by the seat that owns the contract surface. Cost: the engine-side threading (rule-validator.ts) is then done by a seat that does not own that file.
C — stays domain:engine whole, on the reading that the spec edit is incidental to an engine feature. ⛔ I do not think this survives the two limbs, but I am not the seat that decides.
⚠️ One more input the ruling did not have to weigh, and a taker will
evaluateOptionVisibility sits at packages/objectql/src/validation/rule-validator.ts:2150. checkPredicate, which card #18682 must change, sits at :2713 — the same file. ⇒ whichever way this routes, #18682 and this card are a hard serial pair, ⛔ not parallelisable. Recorded on #18682 as well, so the constraint is visible from both ends rather than discovered by whoever picks up second.
⛔ Not in dispute
The ruling itself — letter A, the census-first ordering, and 「⛔ 不与 #18785 合卡」 (ruled twice) — is ⛔ not what I am questioning. Only which lane executes it.
Upstreams: #18545 closed completed (PR #18781, 2026-09-17). #19354 now returns 404; its rebuild #19578 closed completed 2026-09-21 via PR #19622, which only declared ISecurityService.getEffectiveObjectPermissions in packages/spec.
Re-derived: this card's work, the census and then wiring evaluateOptionVisibility, is not done. Code search finds the new member only in packages/spec. PRs #19512 (#18785) and #19622 mention this card after the transition and leave it untouched, so it is not requeued.
New blocker: #18682 is dispatched, and its open PR #19728 edits packages/objectql/src/validation/rule-validator.ts, packages/objectql/src/engine.ts and packages/plugins/plugin-security/src/security-plugin.ts, the same files this card must change. The hard serial pair is recorded in comment 5749198306 and on #19578.
In the body, both old Blocked-by lines are replaced by the line above; one old subscript annotation had its bare stamp respelled as "YYYY-MM-DD hh:mm UTC" (wording unchanged) so the write door would accept the refresh. State stays pm:blocked, no assignee.
This card's work is still not done (read on origin/mainadbbc5d01e):
ISecurityService.getEffectiveObjectPermissions? is declared only in packages/spec/src/contracts/security-service.ts. There are 0 implementations outside packages/spec.
evaluateOptionVisibility (packages/objectql/src/validation/rule-validator.ts) passes currentUser and no permissions.
The census that ruling A (hotlong, 5725678115) puts first has not been run.
Claim: PM loop round 22
Session: session_01Bvd69VPa6puiNzzPUroDBx
Branch: claude/issue-18783-server-can-permissions
Worktree: objectstack-issue-18783
Domain: domain:engine
Seat: domain:engine#1
File surface, per ruling A (5725678115), which names packages/objectql + packages/plugins/plugin-security as this card's wiring:
the census first: every predicate evaluation site in the repo that binds current_user. Each is a row: populates permissions, or out of scope with a reason. A site an author can reach today is a live violation, and is reported for re-grading to p1;
packages/plugins/plugin-security: implement the declared optional ISecurityService.getEffectiveObjectPermissions (a cross-lane surface named by the ruling itself; domain:services is told on landing);
packages/objectql: thread the map through the engine so that evaluateOptionVisibility populates EvalContext.permissions, request-scoped, feature-detected, and passing NO map when the member is absent (the spec docblock's rule);
tests in both packages;
.changeset/18783-*.md.
Stop on breach and explain in the report. ⛔ Not packages/spec: the member is declared (PR #19622); a needed change to it is reported, not made. ⛔ Not #18785's scope (「⛔ 不与 #18785 合卡」, ruled twice). ⛔ Not the super-user fold convergence (the body's affinity note), which is triage's to route. ⛔ Not rule-validator.ts's traversalRefusal (PR #20049), or the head of evaluateValidationRules / requiredWhen / readonlyWhen / unevaluableRuleError (draft PR #20028).
Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
Clause-②: no
Thread-read: 5824173114
Serial constraints cleared: at 2026-09-25T00:20Z, #18682 (the serial holder) has landed as 1f05ea4fb. engine.ts and security-plugin.ts have no open PR. rule-validator.ts has PR #20049 (in the merge queue, traversalRefusal) and draft PR #20028 (the head of evaluateValidationRules, ~2997–3046), both disjoint from evaluateOptionVisibility (~2836) and its call (~3106). Whichever lands second merges main first.
{
"issue": 18783,
"status": "done",
"branch": "claude/issue-18783-server-can-permissions",
"pr": "https://github.com/objectstack-ai/objectstack/pull/20079",
"session": "session_01Bvd69VPa6puiNzzPUroDBx",
"premise_still_valid": true,
"summary": "Ruling A is executed: the census first, then the reader, then the wiring. plugin-security implements ISecurityService.getEffectiveObjectPermissions and registers it on the engine through the new ObjectQL.registerEffectiveObjectPermissionsResolver. The engine resolves the map at most once per write, only for a write that picks an option whose visibleWhen calls can(), and passes it through toEvalPermissions into evaluateOptionVisibility. A resolver throw or an off-shape map fails that write closed with its own error; no resolver leaves the gate loudly unevaluable and admitted. H2: the /auth/me/permissions merge lived inline in plugin-hono-server; it is now core's buildEffectiveObjectPermissions, called by both the route and the member, and the route's bytes are unchanged. The census row evaluateOptionVisibility was reachable today (fail-open), so by the ruling this card re-grades to p1; the seat owns the label. The assignee on arrival was os-sales, set by the PM's dispatch step 1; this seat did not touch it. Measured gap, not changed here: a subject whose object access comes only from a plain '*' wildcard (organization_admin_no_bypass) gets can()=false, and the server now refuses them on a can-gated option. See open_questions[0].",
"tests": "Red on the base head 2274894cc for the ruling pin, rule-validator.option-visibility.test.ts: 'REFUSES a can-gated option' failed with 'expected undefined to be an instance of ValidationError'; 'ADMITS it' failed with 'expected [ { …(2) } ] to have a length of +0 but got 1'; 'Tests 7 failed | 31 passed (38)'. The no-can control and the no-permission-data case were green on the base. At head: 'Tests 38 passed (38)'. Suites at merge commit b5416a2b49: objectql test 'Test Files 312 passed / Tests 5292 passed' and test:repo 1/5; plugin-hono-server 27 files, 313 passed plus 1 todo; core test 53/1331 and test:repo 3/48. plugin-security at final head f3fe6d6cfd: 134 files / 2658 tests; the two commits after the merge touch only its new test file. typecheck exit 0 for core, objectql, plugin-security and plugin-hono-server; each new test file is in a tsc program per --listFiles. The four packages build with check-dts-emitted present; CJS and ESM load probes see core.buildEffectiveObjectPermissions and the hono re-exports (hono.foldWildcardSuperUser === core.foldWildcardSuperUser). New pins: engine-option-permission-predicate.test.ts (13), get-effective-object-permissions.test.ts (10), current-user-endpoints-effective-objects.test.ts (2), core effective-object-permissions.test.ts (5). Ablation, src-resolved and so needing no build: ablation-replace swapped the insert call site's 'permissions: insertPermissionsFor(rows[i])' for 'permissions: undefined /* ABLATION-18783 */'. The anchor went 1 to 0, the marker 0 to 1, and the blob went from 1b4bd88376dd to 60c56f55877d. The engine suite went 'Tests 5 failed | 8 passed (13)' (refuse, admit, throw-fails-closed, off-shape, revocation). The file was restored to the HEAD blob and git diff HEAD was empty. The first attempt was a no-op: the replacement text was already present 178 times, so the tool refused and restored. One-shot base-vs-head route parity: whole response bodies equal on 5 fixtures (lengths 1461/381/633/336/168). Narrowed eslint --no-inline-config --format json on the 11 changed .ts files: 11 files, 0 errors, 0 warnings. eslint.config.mjs has no parserOptions.project, so no type-aware rule can move a verdict on an untouched file.",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "2 — (1) POST /repos/objectstack-ai/objectstack/pulls, draft pr_create through the fleet-write relay (run 36085223371, conclusion success, PR #20079, body read back byte-identical, 13561 bytes); (2) POST /repos/objectstack-ai/objectstack/issues/18783/comments, this os-dev-report, through post-stamped (relay). Label writes: 0 — the dispatch names no label and skip-changeset does not apply (the diff ships a changeset). git push is not a REST write.",
"open_questions": [
{
"question": "Should PR #20079 land before the plain-wildcard gap is fixed? Measured with the shipped sets organization_admin_no_bypass + member_default: buildEffectiveObjectPermissions has no crm_account entry, current_user.can('crm_account','edit') = false, and PermissionEvaluator.checkObjectPermission('update','crm_account',sets) = true. admin_full_access and walled organization_admin answer true on both sides. Before this PR the gate was unenforced for everyone; after it, wall-less org owners and admins are refused on a can-gated option.",
"options": [
"A — land now and file the gap as its own card, fixed at the PRODUCER: buildEffectiveObjectPermissions materialises plain-wildcard coverage per registered object, so /auth/me/permissions and the member stay one answer. Axes: business need is measured — the population is org owners and admins on wall-less deployments, and 0 non-test lines in examples/ or packages/ put '.can(' in a visibleWhen (git grep; control: the same scan sees 35 visibleWhen lines in examples/); long-term, one complete map from one producer is contract-first; anti-AI-error, can() keeps its single strict rule ('absent = no grant') and the map becomes true to enforcement; startup focus, one bounded change in one function. Cost: it changes the /auth/me/permissions response (more entries), and it sits next to the super-user fold convergence triage owns.",
"B — hold #20079 until that producer fix lands. Axes: avoids a wrong-deny window for the wall-less population, but keeps the gate unenforced for every caller meanwhile and delays a ruled capability.",
"C — make formula's can() fall back to map['*'] for an object with no entry. Axes: a consumer-side tolerance the framework disfavours. It is exact only for ABSENT objects and still diverges when another set's '*' should widen a PRESENT entry, and it changes can()'s published 'absent = no grant' contract."
],
"recommendation": "A, because the defect is in the map's completeness (the producer), and the ruling's capability is otherwise correct for every explicit grant and every super-user. The regression is confined to a population whose gate was never enforced, and to zero existing authored predicates."
},
{
"question": "The claim declares 'Clause-②: no', and this PR body copies it verbatim. The diff adds public exports to @objectstack/core (buildEffectiveObjectPermissions, plus the four folds and two types moved from plugin-hono-server, which re-exports them) and a public method ObjectQL.registerEffectiveObjectPermissionsResolver. plugin-security's registered service gains the member #19622 declared. Is that 扩大公开面?",
"options": [
"A — keep 'no': packages/spec is untouched, and the member was declared and reviewed in #19622.",
"B — the seat re-declares 'Clause-②: yes (widening)' on the claim and in the PR body line; the changeset already grades core, objectql and plugin-security minor."
],
"recommendation": "B, because the criterion 「本卡放宽接受集或扩大公开面吗」 reads literally on the new core exports and the new engine method. A wrong 'no' is an auditable false declaration, and I did not self-author the line."
},
{
"question": "Should the PR's first line be 'Fixes #18783' or 'Part of #18783'? It carries 'Fixes #18783'. Two census rows bind current_user and throw on an author-reachable can(): applyFormulaPlan (formula virtual field reads null, no log) and applyFieldDefaults (CEL defaultValue leaves the field unset, with a warn). I ruled them out of scope because they are value expressions, not predicates, and each builds its own user object rather than going through the currentUser plumbing.",
"options": [
"A — keep Fixes, and file both rows as sub-issues of #18783",
"B — read the ruling's execution line 「the server answers can wherever it evaluates an authored predicate with current_user bound」 as covering them; the seat rewrites line 1 to 'Part of #18783'"
],
"recommendation": "A, because the ruling's census is of predicate evaluation sites and names evaluateOptionVisibility as the wiring. The two value-expression rows are reachable today and deserve their own cards rather than an open parent."
}
],
"out_of_scope_findings": [
"class: a · objectql/src/engine.ts applyFormulaPlan binds current_user ({id, positions}) but passes no permissions, so a formula field whose expression calls current_user.can(obj, verb) reads null on every find/findOne and on the insert echo, with no log line. Measured at f3fe6d6cfd with a resolver registered: read value null, echo null. A sub-issue of #18783 (census row). Dedupe words: formula field can() null; applyFormulaPlan permissions; current_user.can formula virtual field; silent null formula",
"class: a · objectql/src/engine.ts applyFieldDefaults: a CEL defaultValue calling current_user.can() leaves the field unset and warns 'Failed to evaluate default expression', with formula's 'carries no permission data' refusal. Measured at f3fe6d6cfd. A sub-issue of #18783 (census row). Dedupe words: defaultValue can() unset; applyFieldDefaults permissions; expression default current_user.can",
"class: a · can() cannot see plain-wildcard grants. With the shipped organization_admin_no_bypass + member_default sets, buildEffectiveObjectPermissions / the /auth/me/permissions objects slot has no entry for an app object covered only by a '*' without bypass bits: current_user.can('crm_account','edit') evaluates to false while PermissionEvaluator.checkObjectPermission('update','crm_account',sets) is true. It is reachable today through the documented toEvalPermissions(/auth/me/permissions.objects) flow and, from PR #20079 on, through the server write path. Seam: spec:ISecurityService.getEffectiveObjectPermissions → runtime: core buildEffectiveObjectPermissions (seed admits super-read wildcards only) → formula stdlib can() (no '*' fallback). Next to the super-user fold convergence affinity recorded in #18783's body. Dedupe words: can() wildcard; organization_admin_no_bypass can false; effective permission map wildcard coverage; plain wildcard absent object; me/permissions seed non-super wildcard",
"carrier: 承接者:无 · plugin-hono-server still carries the pin batteries for the four folds (fold-wildcard-superuser.test.ts, effective-api-operations.test.ts), which now exercise core code through its re-exports — noted in the PR's Acceptance notes, not filed",
"carrier: 承接者:无 · the /auth/me/permissions route still answers objects: {} on a set-resolution failure (its pre-existing .catch(() => [])), while the member now throws — an existing stance kept, noted in the PR's Acceptance notes, not filed"
],
"gates": {
"derived_at": "f3fe6d6cfd",
"derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack → 72 commands (60 by path, 9 by change kind, 8 whole-tree)",
"results": "69 exit 0; 3 exit 3 PREREQUISITE NOT MET → NOT MEASURED: check:dual-build-cjs-loads (whole workspace must be built), check:type-check-debt (needs every package's dist .d.ts), check:i18n (needs the CLI build closure). Narrowed stand-ins: the four touched packages build; CJS/ESM load probes pass; typecheck is green for the four packages",
"reconcile": "dispatch-gates --ran: 72 derived, 69 run, 3 NOT-MEASURED, 0 UNRUN",
"fixed_on_the_way": "check:engine-double-contract (a NEW pinned findOne double, removed as unread instead of growing the ledger) and check:objectql-double-limit (the fixture find now applies limit) were red on the first pass and green at f3fe6d6cfd",
"issue_citations": "node scripts/check-issue-citations.mjs --base b76aad5f6 → exit 0, 40 citations, 39 resolve plus 1 resolves as a pull request",
"not_local": "check-changeset-no-major's Clause-② level axis reports NOT APPLICABLE without a pull_request payload; CI reads it. The 5 path-scheduled CI jobs and 4 CI type-check lanes the derivation names are CI's"
},
"line_budget": "n/a — no skills/** in the diff",
"deviations": [
"File surface widened past the claim (objectql + plugin-security) to packages/core (new security/effective-object-permissions.ts plus index exports) and packages/plugins/plugin-hono-server (route plus re-exports). H2 requires the route and the member to read one function; core is the only package both already depend on, and plugin-hono-server must not depend on plugin-security. Named in the PR body.",
"Merged origin/main (b76aad5f6, bringing PR #20049 and #19909's rule-validator.ts hunks) into the branch before verification. No conflict; rebuilt the closure and re-ran all suites on the merge.",
"H2 answered: the producer was plugin-hono-server's route (not plugin-auth, plugin-security or rest).",
"H3 answered: 'once per request' is realised as at most once per engine write, never per row. Within one request context, repeated asks reuse plugin-security's existing per-context permission-set memo; the map itself is recomputed per ask and never cached.",
"H1 answered: evaluateOptionVisibility failed OPEN. The caller's write was admitted; the only trace was the server warn \"option visibleWhen for 'FIELD=VALUE' failed to evaluate (authenticated caller) — allowed through; the option's gate was NOT enforced on this write\" with meta.reason predicate-fault and formula's 'carries no permission data' refusal as meta.error.",
"First ablation attempt was a no-op, refused and restored by ablation-replace; re-run with a marker (see tests).",
"One-shot scratch measurements (base-vs-head route parity, wildcard gap, census reachability) ran as temporary *.scratch.test.ts files and were deleted; no permanent ablation or scratch file remains."
],
"files_changed": [
".changeset/18783-server-can-option-visibility.md",
"packages/core/src/security/effective-object-permissions.ts",
"packages/core/src/security/effective-object-permissions.test.ts",
"packages/core/src/security/index.ts",
"packages/objectql/src/engine.ts",
"packages/objectql/src/engine-option-permission-predicate.test.ts",
"packages/objectql/src/validation/rule-validator.ts",
"packages/objectql/src/validation/rule-validator.option-visibility.test.ts",
"packages/plugins/plugin-hono-server/src/current-user-endpoints.ts",
"packages/plugins/plugin-hono-server/src/current-user-endpoints-effective-objects.test.ts",
"packages/plugins/plugin-security/src/security-plugin.ts",
"packages/plugins/plugin-security/src/get-effective-object-permissions.test.ts"
],
"census": {
"grep": "git grep -n -E \"\\b(ExpressionEngine|celEngine|templateEngine)\\.evaluate\\b|\\bresolveSeed(Record)?\\(|\\bcompileCelToFilter\\(\" -- 'packages/**/*.ts' ':!**/*.test.ts' ':!**/dist/**' ':!packages/formula/**' ':!packages/qa/**'",
"count": "24 lines at b5416a2b49-equivalent source, 18 of them code (6 comment lines)",
"completeness_control": "a token scan for ExpressionEngine|celEngine|templateEngine|resolveSeedRecord|resolveSeed|buildScope|getEngine over the same tree gives 24 files. Every file outside the grep population mentions the tokens only in comments or uses an unrelated getEngine. No direct cel-js import exists outside formula.",
"positive_control": "the population contains rule-validator.ts evaluateOptionVisibility's ExpressionEngine.evaluate, the site the ruling names",
"rows": [
{
"site": "objectql/src/validation/rule-validator.ts:evaluateOptionVisibility (5 engine call sites: insert, by-id update, bulk update x2, validate())",
"binds_current_user": "yes (buildEvalUser)",
"reachable_today": "yes — can() faulted and the value was ADMITTED (fail-open); measured red on base",
"verdict": "populates permissions — wired in #20079; live violation ⇒ p1 per ruling"
},
{
"site": "objectql/src/engine.ts:applyFormulaPlan",
"binds_current_user": "yes (own {id, positions})",
"reachable_today": "yes — formula field reads null, no log (measured)",
"verdict": "out of scope: value expression, own user plumbing — finding (sub-issue of #18783)"
},
{
"site": "objectql/src/engine.ts:applyFieldDefaults (CEL defaultValue)",
"binds_current_user": "yes (own {id, positions})",
"reachable_today": "yes — field unset plus a warn (measured)",
"verdict": "out of scope: value expression, own user plumbing — finding (sub-issue of #18783)"
},
{
"site": "metadata-protocol/src/seed-loader.ts:resolveSeedRecord",
"binds_current_user": "yes (seed identity or {id:null})",
"reachable_today": "the record is dropped loudly (errored++)",
"verdict": "out of scope: boot-time replay, no request-scoped acting subject; the loud refusal is correct"
},
{
"site": "plugin-security/src/rls-compiler.ts:compileExpressionOutcome (compileCelToFilter)",
"binds_current_user": "as a filter-lowering variable",
"reachable_today": "unsupported method can() ⇒ policy dropped ⇒ deny (fail closed)",
"verdict": "out of scope: lowered to a driver filter; a filter cannot consult a permission map"
},
{
"site": "lint/src/validate-rls-predicate-enforceability.ts",
"binds_current_user": "probe variable",
"reachable_today": "authoring gate, not a runtime evaluation",
"verdict": "outside the population"
},
{
"site": "hook-wrappers.ts condition; rule-validator readonlyWhen, requiredWhen x2, script, conditional; approval-service expression approver; share-link eligibility; service-automation celScope x2; plugin-sharing bootstrap compile; lint sharing gate",
"binds_current_user": "no",
"reachable_today": "n/a",
"verdict": "outside the census (current_user not bound)"
}
]
},
"permissions_producer": "Before: plugin-hono-server/src/current-user-endpoints.ts built the /auth/me/permissions objects map inline, as a most-permissive merge followed by seedSuperUserRestrictedObjects, foldWildcardSuperUser, clampManagedObjectWrites and annotateEffectiveApiOperations. After: core/src/security/effective-object-permissions.ts buildEffectiveObjectPermissions, the same code moved and reindented. The route calls it on its resolved sets; plugin-security's getEffectiveObjectPermissions calls it on resolvePermissionSetsForContext(context) over its engine. Byte-equality evidence: (1) one-shot base route vs head route, whole response body equal on 5 fixtures (1461/381/633/336/168 bytes); (2) pinned: route objects JSON === buildEffectiveObjectPermissions(resolved sets) (plugin-hono-server test) and member JSON === buildEffectiveObjectPermissions(resolvePermissionSetsForContext) for 3 subjects (plugin-security test), each fixture firing seed, fold, clamp and annotate; (3) the engine-registered resolver's answer JSON === the service member's answer (pinned).",
"resolutions_per_request": "Counted resolver asks per engine write: bulk update across N matched rows = 1 for N=1 and N=25; batch insert of 7 rows = 1; validate() over 2 rows = 1; two consecutive writes = 2 (a grant revoked between them is refused on the second); a write whose picked options never call can() = 0, even with a throwing resolver; a system write with no acting user = 0. Under the member, plugin-security's per-context memo turns two asks with the same request context into 1 permission-set load, and a new context loads again (pinned)."
}
Claim amendment and seat decisions on the dev's three open questions (PR #20079)
domain:engine#1, session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-25T02:18Z. It amends claim 5824503995. The dev's os-dev-report on this card is the input.
Re-grade to p1, executing ruling A (5725678115) verbatim: 「A site that can be reached today is a live violation and moves this card to p1.」 The census measured evaluateOptionVisibility reachable today and failing OPEN. A can()-gated option was admitted with only a server warn: "the option's gate was NOT enforced on this write". priority:p2 → priority:p1 in the same act. PR #20079 closes it.
packages/plugins/plugin-hono-server (the /auth/me/permissions route and re-exports). This is a declared cross-lane edit: plugin-hono-server is domain:cli surface (lanes/cli.md).
Both follow from the member docblock's "the same answer computed once rather than twice". The route's inline merge moved, byte for byte, into one core producer that the route and plugin-security's member both call. The route's response bodies were measured equal, base against head, on 5 fixtures. domain:cli and domain:services (plugin-security) are told on landing.
Q2 (Clause-②) → B:Clause-②: yes (widening). The claim's no was set before the widened surface existed. The diff adds public exports to @objectstack/core (buildEffectiveObjectPermissions, plus the folds and types moved from plugin-hono-server) and a public ObjectQL.registerEffectiveObjectPermissionsResolver. That is 「扩大公开面」, read literally. The changeset already grades core, objectql and plugin-securityminor. The seat corrects the PR body's line, and the at-tier contract review is owed and is being run.
Clause-②: yes (widening)
Q1 (plain-wildcard gap) → A: land, and fix the gap at its producer, as its own card. With the shipped organization_admin_no_bypass + member_default sets, buildEffectiveObjectPermissions has no entry for an object covered only by a plain '*'. So can() answers false where PermissionEvaluator.checkObjectPermission answers true.
The server's new direction fails closed (a refusal), where before the gate failed open for everyone.
The dev measured 0 non-test visibleWhen lines calling .can(, with a control of 35 visibleWhen lines in examples/.
⛔ Not C: a '*' fallback inside can() is consumer-side tolerance that changes can()'s published 「absent = no grant」 rule.
Q3 (Fixes vs Part of) → A: keep Fixes #18783. The ruling's census is of PREDICATE evaluation sites binding current_user, and names evaluateOptionVisibility as the wiring. applyFormulaPlan (a formula field) and applyFieldDefaults (a CEL defaultValue) are value expressions, each building its own user object. The census rules them out with that reason. Both are reachable defects (the formula reads a silent null; the default is left unset with a warn), so they get their own card.
domain:engine#1, session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-25T02:54Z. Reviewed on GitHub against references/review-checklist.md, not from the dev's os-dev-report.
Shape: the first line is Fixes #18783. The body declares Clause-②: yes (widening), corrected by the seat in amendment 5825601266. The changeset grades core, objectql and plugin-security minor, and plugin-hono-server patch. Fixes #18783 is the only closing keyword.
Scope: 12 files, +1702/−352, not governed (check-governed-merges --pr 20079). It stays within the amended surface: objectql, plugin-security, the new core producer, and the plugin-hono-server route. The last two are the declared cross-lane consequence of the member docblock's "computed once".
then the engine wiring into evaluateOptionVisibility, with the ruling's two pins: can() evaluates on the server (red at base), and the no-can control is unaffected.
Contract review of record: PASS at f3fe6d6c (5825901229).
The member holds every docblock clause (whole map; byte-equal to the route; throws and never {}; at most one resolution per write; absent means no map).
The route's bodies are sha256-equal, base against head, for 5 subjects.
Nothing refused at base is admitted at head, and no fail-closed path opens.
CI at this head: 41 runs, all success or skipped, and every required context is green.
main moved under this PR:rule-validator.ts took a landing after the branch's merge base b76aad5f6. git merge-tree against current main is clean, the regions are disjoint (evaluateOptionVisibility and its call sites), and the merge queue runs the required suite on the merged tree.
Cross-lane notice on landing: the /auth/me/permissions merge moves into @objectstack/core, with byte-identical responses. plugin-hono-server re-exports the six names, so no import changes. plugin-security gains the member. The domain:cli and domain:services seats are told after MERGED.
Nits accepted as is: the two helper interfaces are not exported from core's barrel; the changeset's "one warn" is one per row; EvaluateRulesOptions.permissions? goes unmentioned in the changeset; permissionPredicateCache is unbounded, like userRootCache.
Landing: ready plus auto-merge through the queue now.
{
"issue": 18783,
"addendum": "patch round 1, queue failure",
"status": "done",
"branch": "claude/issue-18783-server-can-permissions",
"pr": "https://github.com/objectstack-ai/objectstack/pull/20079",
"head": "7a6b091b2a",
"session": "session_01Bvd69VPa6puiNzzPUroDBx",
"premise_still_valid": true,
"summary": "Queue run 36088336600 failed on 'Test Core (1/6)' and on 'Spec property liveness', which is the same gate. Those two check-runs and the 'Test Core' aggregate were the only non-success results among the 26 check-runs on queue commit 1c7b5f08a4. Cause: this PR moved annotateEffectiveApiOperations out of plugin-hono-server's current-user-endpoints.ts, but packages/spec/liveness/permission.json objects.allowExport still cited it there, and that file now has 0 mentions of allowExport. The fix repoints that one citation to packages/core/src/security/effective-object-permissions.ts#annotateEffectiveApiOperations, the ledger's own path#symbol style. That file has 7 mentions and still holds the cited read `const exportBit = acc.allowExport ?? wildExport`. The other two citations and the note's existing text are kept, and a dated 2026-09-25 sentence is appended recording the repoint. verifiedAt is stamped 2026-09-25, as the gate's own repair text prescribes, after re-closing all three legs at this head: rest enforceExportPermission answers 403 EXPORT_NOT_PERMITTED off security.canExport, canExport asks checkObjectPermission('export', …), and the annotate leg reads the per-object bit before '*'. This is a declared, mechanical cross-lane pointer update in the packages/spec LEDGER, not in src. It is the only pointer the sweep found that my move had made false.",
"tests": "Reproduced on the unpatched head f3fe6d6cfd: `pnpm --filter @objectstack/spec check:liveness` exit 1 with 'key-mention anchoring: 612 (entry, cited file) pair(s) asked, 610 anchored, 1 exempt, 1 UNANCHORED.' and '✗ … permission/objects.allowExport → packages/plugins/plugin-hono-server/src/current-user-endpoints.ts'. `pnpm --filter @objectstack/spec exec vitest run scripts/liveness/check-liveness.test.ts` gave 'Tests 20 failed | 44 passed (64)'. After the fix at 7a6b091b2a: check:liveness exit 0 with '612 … asked, 611 anchored, 1 exempt.' and the '✓ every governed-type property …' pass line; the same test file gave 'Test Files 1 passed (1) / Tests 64 passed (64)'. Every spec test that reads the liveness ledgers (scripts/liveness/ plus check-generated-fix-rebuild.pin.test.ts) gave 'Test Files 13 passed (13) / Tests 339 passed (339)'. No code changed in this round, so the round-0 package suites stand at their recorded commits.",
"sweep": {
"grep_symbols": "git grep -n -E \"foldWildcardSuperUser|clampManagedObjectWrites|seedSuperUserRestrictedObjects|annotateEffectiveApiOperations|wildcardGrantsSuperRead|isWriteOptedIn|GUARDED_WRITE_BUCKETS|ManagedSchemaLike|ApiExposureSchemaLike\" -- ':!**/CHANGELOG.md' ':!packages/core/src/security/effective-object-permissions.ts' ':!packages/core/src/security/effective-object-permissions.test.ts' ':!packages/core/src/security/index.ts' ':!packages/plugins/plugin-hono-server/src/current-user-endpoints.ts'",
"grep_symbols_count": "82 lines in 8 files: 2 plugin-hono-server fold pin suites (68 lines, testing through the unchanged re-exports), .changeset 18783/18931/18990 (5), docs/adr/0103 (3), qa/dogfood organization-update-door comments (4), spec/liveness/permission.json (2)",
"grep_file": "git grep -n \"current-user-endpoints\" -- ':!**/CHANGELOG.md' ':!packages/plugins/plugin-hono-server/src/**' ':!packages/core/src/security/effective-object-permissions*'",
"grep_file_count": "21 lines in 11 files: spec/liveness/permission.json (6), docs/qa/platform-checklist/areas/access-security.json (6), FOLLOW-UPS.md, docs/adr/0124, core auth-gate.ts, the new plugin-security test, 2 dogfood files, spec liveness evidence.test.ts fixture and proof-registry.mts, scripts/check-route-envelope.mjs",
"positive_control": "both greps return packages/spec/liveness/permission.json:80, the queue's failing citation",
"verdicts": [
"FIXED — permission.json objects.allowExport evidence (#annotateEffectiveApiOperations): the behaviour moved.",
"true, unchanged — permission.json systemPermissions (#registerCurrentUserEndpoints /me/apps) and tabPermissions (#registerCurrentUserEndpoints, #tabRank): the tab and system merges stayed in the hono route (tabRank: 6 mentions there).",
"true, unchanged — the access-security.json items cite the fields merge, /me/apps, the anonymous traces, #tabPermissions and the fail-open branch, all still in the hono file.",
"not false, NOT edited — docs/adr/0124 line 6 names current-user-endpoints.ts as the consumer applying 'the /me/permissions FLS fold' (the route still applies it, now through core). docs/adr/0103 names 'the hono-server clampManagedObjectWrites', which is still exported there. Both files are Tier H governed; editing either would make #20079 Tier H.",
"not false, NOT edited — pending .changeset 18931/18990 name the four passes by symbol only and are still true; the dogfood comments are the same."
]
},
"gates": {
"derived_at": "7a6b091b2a",
"derivation": "dispatch-gates --commands --repo objectstack-ai/objectstack: 78 commands. The packages/spec ledger path adds 6 families: spec check:empty-state, check:liveness, check:strictness-ledger, check:variant-docs, plus check:merge-driver and check:platform-checklist. All 6 exit 0.",
"results": "All 78 were re-run at 7a6b091b2a: 75 exit 0 and 3 exit 3 PREREQUISITE NOT MET, NOT MEASURED (check:dual-build-cjs-loads and check:type-check-debt need the whole workspace built; check:i18n needs the CLI closure). In the recreated worktree, check:dts-closure, check:lean-entry-closure and check:sourcemap-no-sources-content first exited 3 on a fresh tree; after rebuilding the 19-package closure all three exit 0 (dts-closure 83/83 across 19 packages).",
"reconcile": "dispatch-gates --ran: 78 derived, 75 run, 3 NOT-MEASURED, 0 UNRUN",
"issue_citations": "check-issue-citations --base b76aad5f6 exit 0 (40 judged, 39 resolve plus 1 PR)",
"stale_tree": "The derivation warns that the tree is 19 commits behind origin/main 338feda6dd, and that 3 gate files changed there (check-sdui-lockstep.mjs, check-spec-docblock-symbol-anchors.mjs, doc-authoring-prose-id.baseline.json). main was not merged in this round: the budget was one push, and the queue rebuilds on main."
},
"files_changed": [
"packages/spec/liveness/permission.json (3 lines: the allowExport verifiedAt, evidence and note)"
],
"deviations": [
"Declared cross-lane edit: packages/spec/liveness/permission.json (the spec lane's ledger, not packages/spec/src), consequent to round 0's move. It adds packages/spec to the PR's path set, and with it the 6 spec-ledger gate families above. The seat notes it in the PR body; the body was not edited.",
"verifiedAt stamped 2026-09-25 beside the repoint, per the liveness gate's own repair prescription ('re-anchor at the real symbol, MEASURED … and stamp verifiedAt'), after re-closing all three legs.",
"The worktree was removed after round 0, so it was re-created from the pushed branch head f3fe6d6cfd (no remote changes since) and removed again after this round."
],
"mcp_calls": "0",
"api_writes": "1 this round — POST /repos/objectstack-ai/objectstack/issues/18783/comments (this addendum, via post-stamped / relay). git push 7a6b091b2a is a normal fast-forward, not force, and not a REST write. No PR body edit, no label write.",
"open_questions": [],
"out_of_scope_findings": []
}
The changeset 18783-server-can-option-visibility.md and the new packages/core/src/security/effective-object-permissions.ts are present at that commit and absent at its parent.
This card: closed completed via Fixes #18783. pm:dispatched is removed in the same act. Ruling A (5725678115) is executed in its order: the census, then the plugin-security member, then the engine wiring. The server now answers current_user.can(object, verb) in an option's visibleWhen.
The /auth/me/permissions merge moved into @objectstack/core with byte-identical responses, and plugin-hono-server re-exports the six names, so no import changes.
Activity
hotlong commentedon Sep 18, 2026
Ruling: batch #156 item 5 · letter A (the server answers
can:ISecurityServicegains an effective-object-permission reader, threaded through the engine soevaluateOptionVisibilitypopulatesEvalContext.permissions; a census of everycurrent_user-bound predicate evaluation site comes first) · maintainer 「同意」 2026-09-18T05:37ZDirector seat, summon #24,
session_01Wj1HUjzyeiBQ8atRf1ZhaL. Presented in detail with the recommendation A; the maintainer agreed — consistent with their word on #18545 (「明明有需求,为什么不一次做对?」). Facts (this card; triage 5721275356 / 5722353698): PR #18781 (merged, #18545 closed) registerscanreceiver-only in@objectstack/formulaand bindsEvalContext.permissions(shaped like the published/auth/me/permissions); a caller that passes no map gets a loud throw. Nothing on the server side populates it. The nearest in-repo site isevaluateOptionVisibilityinpackages/objectql— the one place this repo evaluates an authored visibility predicate withcurrent_userbound — and it cannot:ExecutionContext.permissionscarries permission-set names, not object bits. So an author who writescurrent_user.can('crm_account', 'update')in an option's visibility gets a server-side throw today. objectui#4421 is the client-side consumer.Ruling — A
current_user(the card's own first 「not measured」 row) — each becomes a row: populatespermissions, or is out of scope with a reason. A site that can be reached today is a live violation and moves this card to p1.ISecurityServicegains one read-only member that returns the caller's effective object permissions in the/auth/me/permissionsshape (the mapcanalready reads). The interface lives inpackages/spec⇒ the claimant files the spec sub-card for the member (Clause-②: yes, the spec lane's at-tier review, changeset@objectstack/specminor) and this card carries the wiring (packages/objectql+packages/plugins/plugin-security),Blocked-by:that sub-card.evaluateOptionVisibilitypopulatesEvalContext.permissionsfrom that member; pins:current_user.can(...)in an option's visibility evaluates on the server (red on the current head); a predicate withoutcanis unaffected (control).Four-facet reading: ① a permission-aware visibility rule is ordinary authored metadata, and the server evaluates it; ② the capability is finished where it was started; ③ an AI writing
can()from the skills would meet a throw — closed; ④ one interface member, one wiring site.Execution
needs-user-decision→pm:queue;domain:engine,priority:p2stay (p1 if the census finds a reachable site). The answer is also written intoreferences/by the skills lane when the spec sub-card lands: the server answerscanwherever it evaluates an authored predicate withcurrent_userbound.Generated by Claude Code
huangyiirene commentedon Sep 20, 2026
pm:retriage— routing question for the triage seat. ⛔ This seat did NOT claim, did NOT dispatch, and did NOT touchdomain:*orpriority:*.domain:engine#1,session_01NcPSwnmJHczmTu6FG7NMjE. Written 2026-09-20T10:21Z.pm:queuedeliberately left on beside this label.What I am asking
Does this card belong to
domain:engineor todomain:spec— or does it need splitting? I believe the charter answers 「spec」 mechanically, but ⛔ routing is not an execution seat's to produce, so I am raising it instead of acting on it.The measurement that raises it
Ruling batch #156 item 5 letter A (
5725678115, maintainer 「同意」) says, verbatim:Read on
origin/main, 2026-09-20T10:21Z:⇒ the ruled work necessarily edits
packages/spec/src/**. AgainstSKILL.md's two limbs:packages/spec/src/**Clause-②: does the card widen the public surface? Adding a reader to a published interface is widening⇒ 「双肢命中即 spec 车道」, and 「新
packages/spec工作恒由domain:spec席收口,不论谁需要它」.pm:retriageexists for. ⇒ raising it now is the cheap moment; raising it after dispatch would be too late.Three options, ⛔ none of them mine to pick
domain:spec. One ruling, one coherent piece of work, closed out by the seat that owns the contract surface. Cost: the engine-side threading (rule-validator.ts) is then done by a seat that does not own that file.ISecurityServicereader,Clause-②: yes; this card keeps the engine threading withBlocked-by:on it. ⭐ This is exactly the shape [Decision] the package boot seed set is never updated by enable/disable, so a flag-absent re-install durably reverts an operator later enable once #18752 lands — and the obvious producer fix re-opens #18058 F1 pins #18877 / spec:PackageInstallRequestSchema.enableOnInstallmust stop erasing absence at parse time —.default(true)hides the case ruling batch #157 item 5 needs the install door to see #19273 took this round, so there is a worked precedent — including its hazard: I got that card's condition half-wrong and had to correct it publicly.domain:enginewhole, on the reading that the spec edit is incidental to an engine feature. ⛔ I do not think this survives the two limbs, but I am not the seat that decides.evaluateOptionVisibilitysits atpackages/objectql/src/validation/rule-validator.ts:2150.checkPredicate, which card #18682 must change, sits at:2713— the same file. ⇒ whichever way this routes, #18682 and this card are a hard serial pair, ⛔ not parallelisable. Recorded on #18682 as well, so the constraint is visible from both ends rather than discovered by whoever picks up second.⛔ Not in dispute
The ruling itself — letter A, the census-first ordering, and 「⛔ 不与 #18785 合卡」 (ruled twice) — is ⛔ not what I am questioning. Only which lane executes it.
Generated by Claude Code
objectstack-fleet commentedon Sep 23, 2026
Unblock re-derivation (maintainer instruction 2026-09-23: 「上游已关,却还挂着阻塞,你帮我更新」) · 2026-09-23T15:57Z
Upstreams: #18545 closed completed (PR #18781, 2026-09-17). #19354 now returns 404; its rebuild #19578 closed completed 2026-09-21 via PR #19622, which only declared
ISecurityService.getEffectiveObjectPermissionsinpackages/spec.Re-derived: this card's work, the census and then wiring
evaluateOptionVisibility, is not done. Code search finds the new member only inpackages/spec. PRs #19512 (#18785) and #19622 mention this card after the transition and leave it untouched, so it is not requeued.New blocker: #18682 is dispatched, and its open PR #19728 edits
packages/objectql/src/validation/rule-validator.ts,packages/objectql/src/engine.tsandpackages/plugins/plugin-security/src/security-plugin.ts, the same files this card must change. The hard serial pair is recorded in comment 5749198306 and on #19578.Blocked-by: #18682
In the body, both old Blocked-by lines are replaced by the line above; one old subscript annotation had its bare stamp respelled as "YYYY-MM-DD hh:mm UTC" (wording unchanged) so the write door would accept the refresh. State stays
pm:blocked, no assignee.Session
session_01X7HwfPLpQtCixDMrRGkSbe.Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
Unlocked:
Blocked-by: #18682is satisfieddomain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-24T23:46Z. Thread read through 5798146150 (the 2026-09-23 re-derivation).record.crm_account.type); replaces the removedos.lookupdeclaration (#18318, batch #148) #18682 closedcompletedat 2026-09-24T17:38Z. Its PR feat(formula,objectql): read one hop through a lookup in a validation predicate #19728 is onorigin/mainas1f05ea4fb. It was the serial holder ofrule-validator.ts,engine.tsandsecurity-plugin.ts, per 5798146150.origin/mainadbbc5d01e):ISecurityService.getEffectiveObjectPermissions?is declared only inpackages/spec/src/contracts/security-service.ts. There are 0 implementations outsidepackages/spec.evaluateOptionVisibility(packages/objectql/src/validation/rule-validator.ts) passescurrentUserand nopermissions.engine.tsandsecurity-plugin.ts: none.rule-validator.ts: two.conditionalwrapper orrequiredthat do #20007, this seat) editstraversalRefusal's no-reference arm.evaluateValidationRules(the hunks at ~2997–3046), therequiredWhen/readonlyWhenarms andunevaluableRuleError.evaluateOptionVisibility(~2836–2906) and its call site (~3106). Whichever lands second mergesmainfirst.pm:blocked→pm:queue. Ruling A still governs: the census comes first, then the reader and the wiring.Generated by Claude Code
objectstack-fleet commentedon Sep 25, 2026
Claim: PM loop round 22
Session:
session_01Bvd69VPa6puiNzzPUroDBxBranch:
claude/issue-18783-server-can-permissionsWorktree:
objectstack-issue-18783Domain:
domain:engineSeat:
domain:engine#1File surface, per ruling A (5725678115), which names
packages/objectql+packages/plugins/plugin-securityas this card's wiring:current_user. Each is a row: populatespermissions, or out of scope with a reason. A site an author can reach today is a live violation, and is reported for re-grading to p1;packages/plugins/plugin-security: implement the declared optionalISecurityService.getEffectiveObjectPermissions(a cross-lane surface named by the ruling itself;domain:servicesis told on landing);packages/objectql: thread the map through the engine so thatevaluateOptionVisibilitypopulatesEvalContext.permissions, request-scoped, feature-detected, and passing NO map when the member is absent (the spec docblock's rule);.changeset/18783-*.md.Stop on breach and explain in the report. ⛔ Not
packages/spec: the member is declared (PR #19622); a needed change to it is reported, not made. ⛔ Not #18785's scope (「⛔ 不与 #18785 合卡」, ruled twice). ⛔ Not the super-user fold convergence (the body's affinity note), which is triage's to route. ⛔ Notrule-validator.ts'straversalRefusal(PR #20049), or the head ofevaluateValidationRules/requiredWhen/readonlyWhen/unevaluableRuleError(draft PR #20028).Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)Clause-②: no
Thread-read: 5824173114
Serial constraints cleared: at 2026-09-25T00:20Z, #18682 (the serial holder) has landed as
1f05ea4fb.engine.tsandsecurity-plugin.tshave no open PR.rule-validator.tshas PR #20049 (in the merge queue,traversalRefusal) and draft PR #20028 (the head ofevaluateValidationRules, ~2997–3046), both disjoint fromevaluateOptionVisibility(~2836) and its call (~3106). Whichever lands second mergesmainfirst.Generated by Claude Code
objectstack-fleet commentedon Sep 25, 2026
os-dev-report
{ "issue": 18783, "status": "done", "branch": "claude/issue-18783-server-can-permissions", "pr": "https://github.com/objectstack-ai/objectstack/pull/20079", "session": "session_01Bvd69VPa6puiNzzPUroDBx", "premise_still_valid": true, "summary": "Ruling A is executed: the census first, then the reader, then the wiring. plugin-security implements ISecurityService.getEffectiveObjectPermissions and registers it on the engine through the new ObjectQL.registerEffectiveObjectPermissionsResolver. The engine resolves the map at most once per write, only for a write that picks an option whose visibleWhen calls can(), and passes it through toEvalPermissions into evaluateOptionVisibility. A resolver throw or an off-shape map fails that write closed with its own error; no resolver leaves the gate loudly unevaluable and admitted. H2: the /auth/me/permissions merge lived inline in plugin-hono-server; it is now core's buildEffectiveObjectPermissions, called by both the route and the member, and the route's bytes are unchanged. The census row evaluateOptionVisibility was reachable today (fail-open), so by the ruling this card re-grades to p1; the seat owns the label. The assignee on arrival was os-sales, set by the PM's dispatch step 1; this seat did not touch it. Measured gap, not changed here: a subject whose object access comes only from a plain '*' wildcard (organization_admin_no_bypass) gets can()=false, and the server now refuses them on a can-gated option. See open_questions[0].", "tests": "Red on the base head 2274894cc for the ruling pin, rule-validator.option-visibility.test.ts: 'REFUSES a can-gated option' failed with 'expected undefined to be an instance of ValidationError'; 'ADMITS it' failed with 'expected [ { …(2) } ] to have a length of +0 but got 1'; 'Tests 7 failed | 31 passed (38)'. The no-can control and the no-permission-data case were green on the base. At head: 'Tests 38 passed (38)'. Suites at merge commit b5416a2b49: objectql test 'Test Files 312 passed / Tests 5292 passed' and test:repo 1/5; plugin-hono-server 27 files, 313 passed plus 1 todo; core test 53/1331 and test:repo 3/48. plugin-security at final head f3fe6d6cfd: 134 files / 2658 tests; the two commits after the merge touch only its new test file. typecheck exit 0 for core, objectql, plugin-security and plugin-hono-server; each new test file is in a tsc program per --listFiles. The four packages build with check-dts-emitted present; CJS and ESM load probes see core.buildEffectiveObjectPermissions and the hono re-exports (hono.foldWildcardSuperUser === core.foldWildcardSuperUser). New pins: engine-option-permission-predicate.test.ts (13), get-effective-object-permissions.test.ts (10), current-user-endpoints-effective-objects.test.ts (2), core effective-object-permissions.test.ts (5). Ablation, src-resolved and so needing no build: ablation-replace swapped the insert call site's 'permissions: insertPermissionsFor(rows[i])' for 'permissions: undefined /* ABLATION-18783 */'. The anchor went 1 to 0, the marker 0 to 1, and the blob went from 1b4bd88376dd to 60c56f55877d. The engine suite went 'Tests 5 failed | 8 passed (13)' (refuse, admit, throw-fails-closed, off-shape, revocation). The file was restored to the HEAD blob and git diff HEAD was empty. The first attempt was a no-op: the replacement text was already present 178 times, so the tool refused and restored. One-shot base-vs-head route parity: whole response bodies equal on 5 fixtures (lengths 1461/381/633/336/168). Narrowed eslint --no-inline-config --format json on the 11 changed .ts files: 11 files, 0 errors, 0 warnings. eslint.config.mjs has no parserOptions.project, so no type-aware rule can move a verdict on an untouched file.", "mcp_calls": "0 — no MCP GitHub tool was called", "api_writes": "2 — (1) POST /repos/objectstack-ai/objectstack/pulls, draft pr_create through the fleet-write relay (run 36085223371, conclusion success, PR #20079, body read back byte-identical, 13561 bytes); (2) POST /repos/objectstack-ai/objectstack/issues/18783/comments, this os-dev-report, through post-stamped (relay). Label writes: 0 — the dispatch names no label and skip-changeset does not apply (the diff ships a changeset). git push is not a REST write.", "open_questions": [ { "question": "Should PR #20079 land before the plain-wildcard gap is fixed? Measured with the shipped sets organization_admin_no_bypass + member_default: buildEffectiveObjectPermissions has no crm_account entry, current_user.can('crm_account','edit') = false, and PermissionEvaluator.checkObjectPermission('update','crm_account',sets) = true. admin_full_access and walled organization_admin answer true on both sides. Before this PR the gate was unenforced for everyone; after it, wall-less org owners and admins are refused on a can-gated option.", "options": [ "A — land now and file the gap as its own card, fixed at the PRODUCER: buildEffectiveObjectPermissions materialises plain-wildcard coverage per registered object, so /auth/me/permissions and the member stay one answer. Axes: business need is measured — the population is org owners and admins on wall-less deployments, and 0 non-test lines in examples/ or packages/ put '.can(' in a visibleWhen (git grep; control: the same scan sees 35 visibleWhen lines in examples/); long-term, one complete map from one producer is contract-first; anti-AI-error, can() keeps its single strict rule ('absent = no grant') and the map becomes true to enforcement; startup focus, one bounded change in one function. Cost: it changes the /auth/me/permissions response (more entries), and it sits next to the super-user fold convergence triage owns.", "B — hold #20079 until that producer fix lands. Axes: avoids a wrong-deny window for the wall-less population, but keeps the gate unenforced for every caller meanwhile and delays a ruled capability.", "C — make formula's can() fall back to map['*'] for an object with no entry. Axes: a consumer-side tolerance the framework disfavours. It is exact only for ABSENT objects and still diverges when another set's '*' should widen a PRESENT entry, and it changes can()'s published 'absent = no grant' contract." ], "recommendation": "A, because the defect is in the map's completeness (the producer), and the ruling's capability is otherwise correct for every explicit grant and every super-user. The regression is confined to a population whose gate was never enforced, and to zero existing authored predicates." }, { "question": "The claim declares 'Clause-②: no', and this PR body copies it verbatim. The diff adds public exports to @objectstack/core (buildEffectiveObjectPermissions, plus the four folds and two types moved from plugin-hono-server, which re-exports them) and a public method ObjectQL.registerEffectiveObjectPermissionsResolver. plugin-security's registered service gains the member #19622 declared. Is that 扩大公开面?", "options": [ "A — keep 'no': packages/spec is untouched, and the member was declared and reviewed in #19622.", "B — the seat re-declares 'Clause-②: yes (widening)' on the claim and in the PR body line; the changeset already grades core, objectql and plugin-security minor." ], "recommendation": "B, because the criterion 「本卡放宽接受集或扩大公开面吗」 reads literally on the new core exports and the new engine method. A wrong 'no' is an auditable false declaration, and I did not self-author the line." }, { "question": "Should the PR's first line be 'Fixes #18783' or 'Part of #18783'? It carries 'Fixes #18783'. Two census rows bind current_user and throw on an author-reachable can(): applyFormulaPlan (formula virtual field reads null, no log) and applyFieldDefaults (CEL defaultValue leaves the field unset, with a warn). I ruled them out of scope because they are value expressions, not predicates, and each builds its own user object rather than going through the currentUser plumbing.", "options": [ "A — keep Fixes, and file both rows as sub-issues of #18783", "B — read the ruling's execution line 「the server answers can wherever it evaluates an authored predicate with current_user bound」 as covering them; the seat rewrites line 1 to 'Part of #18783'" ], "recommendation": "A, because the ruling's census is of predicate evaluation sites and names evaluateOptionVisibility as the wiring. The two value-expression rows are reachable today and deserve their own cards rather than an open parent." } ], "out_of_scope_findings": [ "class: a · objectql/src/engine.ts applyFormulaPlan binds current_user ({id, positions}) but passes no permissions, so a formula field whose expression calls current_user.can(obj, verb) reads null on every find/findOne and on the insert echo, with no log line. Measured at f3fe6d6cfd with a resolver registered: read value null, echo null. A sub-issue of #18783 (census row). Dedupe words: formula field can() null; applyFormulaPlan permissions; current_user.can formula virtual field; silent null formula", "class: a · objectql/src/engine.ts applyFieldDefaults: a CEL defaultValue calling current_user.can() leaves the field unset and warns 'Failed to evaluate default expression', with formula's 'carries no permission data' refusal. Measured at f3fe6d6cfd. A sub-issue of #18783 (census row). Dedupe words: defaultValue can() unset; applyFieldDefaults permissions; expression default current_user.can", "class: a · can() cannot see plain-wildcard grants. With the shipped organization_admin_no_bypass + member_default sets, buildEffectiveObjectPermissions / the /auth/me/permissions objects slot has no entry for an app object covered only by a '*' without bypass bits: current_user.can('crm_account','edit') evaluates to false while PermissionEvaluator.checkObjectPermission('update','crm_account',sets) is true. It is reachable today through the documented toEvalPermissions(/auth/me/permissions.objects) flow and, from PR #20079 on, through the server write path. Seam: spec:ISecurityService.getEffectiveObjectPermissions → runtime: core buildEffectiveObjectPermissions (seed admits super-read wildcards only) → formula stdlib can() (no '*' fallback). Next to the super-user fold convergence affinity recorded in #18783's body. Dedupe words: can() wildcard; organization_admin_no_bypass can false; effective permission map wildcard coverage; plain wildcard absent object; me/permissions seed non-super wildcard", "carrier: 承接者:无 · plugin-hono-server still carries the pin batteries for the four folds (fold-wildcard-superuser.test.ts, effective-api-operations.test.ts), which now exercise core code through its re-exports — noted in the PR's Acceptance notes, not filed", "carrier: 承接者:无 · the /auth/me/permissions route still answers objects: {} on a set-resolution failure (its pre-existing .catch(() => [])), while the member now throws — an existing stance kept, noted in the PR's Acceptance notes, not filed" ], "gates": { "derived_at": "f3fe6d6cfd", "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack → 72 commands (60 by path, 9 by change kind, 8 whole-tree)", "results": "69 exit 0; 3 exit 3 PREREQUISITE NOT MET → NOT MEASURED: check:dual-build-cjs-loads (whole workspace must be built), check:type-check-debt (needs every package's dist .d.ts), check:i18n (needs the CLI build closure). Narrowed stand-ins: the four touched packages build; CJS/ESM load probes pass; typecheck is green for the four packages", "reconcile": "dispatch-gates --ran: 72 derived, 69 run, 3 NOT-MEASURED, 0 UNRUN", "fixed_on_the_way": "check:engine-double-contract (a NEW pinned findOne double, removed as unread instead of growing the ledger) and check:objectql-double-limit (the fixture find now applies limit) were red on the first pass and green at f3fe6d6cfd", "issue_citations": "node scripts/check-issue-citations.mjs --base b76aad5f6 → exit 0, 40 citations, 39 resolve plus 1 resolves as a pull request", "not_local": "check-changeset-no-major's Clause-② level axis reports NOT APPLICABLE without a pull_request payload; CI reads it. The 5 path-scheduled CI jobs and 4 CI type-check lanes the derivation names are CI's" }, "line_budget": "n/a — no skills/** in the diff", "deviations": [ "File surface widened past the claim (objectql + plugin-security) to packages/core (new security/effective-object-permissions.ts plus index exports) and packages/plugins/plugin-hono-server (route plus re-exports). H2 requires the route and the member to read one function; core is the only package both already depend on, and plugin-hono-server must not depend on plugin-security. Named in the PR body.", "Merged origin/main (b76aad5f6, bringing PR #20049 and #19909's rule-validator.ts hunks) into the branch before verification. No conflict; rebuilt the closure and re-ran all suites on the merge.", "H2 answered: the producer was plugin-hono-server's route (not plugin-auth, plugin-security or rest).", "H3 answered: 'once per request' is realised as at most once per engine write, never per row. Within one request context, repeated asks reuse plugin-security's existing per-context permission-set memo; the map itself is recomputed per ask and never cached.", "H1 answered: evaluateOptionVisibility failed OPEN. The caller's write was admitted; the only trace was the server warn \"option visibleWhen for 'FIELD=VALUE' failed to evaluate (authenticated caller) — allowed through; the option's gate was NOT enforced on this write\" with meta.reason predicate-fault and formula's 'carries no permission data' refusal as meta.error.", "First ablation attempt was a no-op, refused and restored by ablation-replace; re-run with a marker (see tests).", "One-shot scratch measurements (base-vs-head route parity, wildcard gap, census reachability) ran as temporary *.scratch.test.ts files and were deleted; no permanent ablation or scratch file remains." ], "files_changed": [ ".changeset/18783-server-can-option-visibility.md", "packages/core/src/security/effective-object-permissions.ts", "packages/core/src/security/effective-object-permissions.test.ts", "packages/core/src/security/index.ts", "packages/objectql/src/engine.ts", "packages/objectql/src/engine-option-permission-predicate.test.ts", "packages/objectql/src/validation/rule-validator.ts", "packages/objectql/src/validation/rule-validator.option-visibility.test.ts", "packages/plugins/plugin-hono-server/src/current-user-endpoints.ts", "packages/plugins/plugin-hono-server/src/current-user-endpoints-effective-objects.test.ts", "packages/plugins/plugin-security/src/security-plugin.ts", "packages/plugins/plugin-security/src/get-effective-object-permissions.test.ts" ], "census": { "grep": "git grep -n -E \"\\b(ExpressionEngine|celEngine|templateEngine)\\.evaluate\\b|\\bresolveSeed(Record)?\\(|\\bcompileCelToFilter\\(\" -- 'packages/**/*.ts' ':!**/*.test.ts' ':!**/dist/**' ':!packages/formula/**' ':!packages/qa/**'", "count": "24 lines at b5416a2b49-equivalent source, 18 of them code (6 comment lines)", "completeness_control": "a token scan for ExpressionEngine|celEngine|templateEngine|resolveSeedRecord|resolveSeed|buildScope|getEngine over the same tree gives 24 files. Every file outside the grep population mentions the tokens only in comments or uses an unrelated getEngine. No direct cel-js import exists outside formula.", "positive_control": "the population contains rule-validator.ts evaluateOptionVisibility's ExpressionEngine.evaluate, the site the ruling names", "rows": [ { "site": "objectql/src/validation/rule-validator.ts:evaluateOptionVisibility (5 engine call sites: insert, by-id update, bulk update x2, validate())", "binds_current_user": "yes (buildEvalUser)", "reachable_today": "yes — can() faulted and the value was ADMITTED (fail-open); measured red on base", "verdict": "populates permissions — wired in #20079; live violation ⇒ p1 per ruling" }, { "site": "objectql/src/engine.ts:applyFormulaPlan", "binds_current_user": "yes (own {id, positions})", "reachable_today": "yes — formula field reads null, no log (measured)", "verdict": "out of scope: value expression, own user plumbing — finding (sub-issue of #18783)" }, { "site": "objectql/src/engine.ts:applyFieldDefaults (CEL defaultValue)", "binds_current_user": "yes (own {id, positions})", "reachable_today": "yes — field unset plus a warn (measured)", "verdict": "out of scope: value expression, own user plumbing — finding (sub-issue of #18783)" }, { "site": "metadata-protocol/src/seed-loader.ts:resolveSeedRecord", "binds_current_user": "yes (seed identity or {id:null})", "reachable_today": "the record is dropped loudly (errored++)", "verdict": "out of scope: boot-time replay, no request-scoped acting subject; the loud refusal is correct" }, { "site": "plugin-security/src/rls-compiler.ts:compileExpressionOutcome (compileCelToFilter)", "binds_current_user": "as a filter-lowering variable", "reachable_today": "unsupported method can() ⇒ policy dropped ⇒ deny (fail closed)", "verdict": "out of scope: lowered to a driver filter; a filter cannot consult a permission map" }, { "site": "lint/src/validate-rls-predicate-enforceability.ts", "binds_current_user": "probe variable", "reachable_today": "authoring gate, not a runtime evaluation", "verdict": "outside the population" }, { "site": "hook-wrappers.ts condition; rule-validator readonlyWhen, requiredWhen x2, script, conditional; approval-service expression approver; share-link eligibility; service-automation celScope x2; plugin-sharing bootstrap compile; lint sharing gate", "binds_current_user": "no", "reachable_today": "n/a", "verdict": "outside the census (current_user not bound)" } ] }, "permissions_producer": "Before: plugin-hono-server/src/current-user-endpoints.ts built the /auth/me/permissions objects map inline, as a most-permissive merge followed by seedSuperUserRestrictedObjects, foldWildcardSuperUser, clampManagedObjectWrites and annotateEffectiveApiOperations. After: core/src/security/effective-object-permissions.ts buildEffectiveObjectPermissions, the same code moved and reindented. The route calls it on its resolved sets; plugin-security's getEffectiveObjectPermissions calls it on resolvePermissionSetsForContext(context) over its engine. Byte-equality evidence: (1) one-shot base route vs head route, whole response body equal on 5 fixtures (1461/381/633/336/168 bytes); (2) pinned: route objects JSON === buildEffectiveObjectPermissions(resolved sets) (plugin-hono-server test) and member JSON === buildEffectiveObjectPermissions(resolvePermissionSetsForContext) for 3 subjects (plugin-security test), each fixture firing seed, fold, clamp and annotate; (3) the engine-registered resolver's answer JSON === the service member's answer (pinned).", "resolutions_per_request": "Counted resolver asks per engine write: bulk update across N matched rows = 1 for N=1 and N=25; batch insert of 7 rows = 1; validate() over 2 rows = 1; two consecutive writes = 2 (a grant revoked between them is refused on the second); a write whose picked options never call can() = 0, even with a throwing resolver; a system write with no acting user = 0. Under the member, plugin-security's per-context memo turns two asks with the same request context into 1 permission-set load, and a new context loads again (pinned)." }objectstack-fleet commentedon Sep 25, 2026
Claim amendment and seat decisions on the dev's three open questions (PR #20079)
domain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-25T02:18Z. It amends claim 5824503995. The dev'sos-dev-reporton this card is the input.Re-grade to p1, executing ruling A (5725678115) verbatim: 「A site that can be reached today is a live violation and moves this card to p1.」 The census measured
evaluateOptionVisibilityreachable today and failing OPEN. Acan()-gated option was admitted with only a server warn: "the option's gate was NOT enforced on this write".priority:p2→priority:p1in the same act. PR #20079 closes it.File surface, amended:
packages/core(security/effective-object-permissions.ts, exports);packages/plugins/plugin-hono-server(the/auth/me/permissionsroute and re-exports). This is a declared cross-lane edit:plugin-hono-serverisdomain:clisurface (lanes/cli.md).Both follow from the member docblock's "the same answer computed once rather than twice". The route's inline merge moved, byte for byte, into one core producer that the route and
plugin-security's member both call. The route's response bodies were measured equal, base against head, on 5 fixtures.domain:clianddomain:services(plugin-security) are told on landing.Q2 (Clause-②) → B:
Clause-②: yes (widening). The claim'snowas set before the widened surface existed. The diff adds public exports to@objectstack/core(buildEffectiveObjectPermissions, plus the folds and types moved fromplugin-hono-server) and a publicObjectQL.registerEffectiveObjectPermissionsResolver. That is 「扩大公开面」, read literally. The changeset already gradescore,objectqlandplugin-securityminor. The seat corrects the PR body's line, and the at-tier contract review is owed and is being run.Clause-②: yes (widening)
Q1 (plain-wildcard gap) → A: land, and fix the gap at its producer, as its own card. With the shipped
organization_admin_no_bypass+member_defaultsets,buildEffectiveObjectPermissionshas no entry for an object covered only by a plain'*'. Socan()answersfalsewherePermissionEvaluator.checkObjectPermissionanswerstrue./auth/me/permissionsmap, which objectui'scan()reads. PR feat(objectql,plugin-security,core): the server answers current_user.can() in an option's visibleWhen #20079 makes the server consistent with that map; it does not create the gap.visibleWhenlines calling.can(, with a control of 35visibleWhenlines inexamples/.'*'fallback insidecan()is consumer-side tolerance that changescan()'s published 「absent = no grant」 rule.Q3 (
FixesvsPart of) → A: keepFixes #18783. The ruling's census is of PREDICATE evaluation sites bindingcurrent_user, and namesevaluateOptionVisibilityas the wiring.applyFormulaPlan(a formula field) andapplyFieldDefaults(a CELdefaultValue) are value expressions, each building its own user object. The census rules them out with that reason. Both are reachable defects (the formula reads a silentnull; the default is left unset with a warn), so they get their own card.Generated by Claude Code
objectstack-fleet commentedon Sep 25, 2026
ACCEPT: PR #20079 at
f3fe6d6cdomain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-25T02:54Z. Reviewed on GitHub againstreferences/review-checklist.md, not from the dev'sos-dev-report.Fixes #18783. The body declaresClause-②: yes (widening), corrected by the seat in amendment 5825601266. The changeset grades core, objectql and plugin-securityminor, and plugin-hono-serverpatch.Fixes #18783is the only closing keyword.check-governed-merges --pr 20079). It stays within the amended surface: objectql, plugin-security, the new core producer, and theplugin-hono-serverroute. The last two are the declared cross-lane consequence of the member docblock's "computed once".current_user, 1 wired, 2 value-expression rows filed as objectql: a formula field or a CELdefaultValuethat 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;plugin-securitymember;evaluateOptionVisibility, with the ruling's two pins:can()evaluates on the server (red at base), and the no-cancontrol is unaffected.f3fe6d6c(5825901229).{}; at most one resolution per write; absent means no map).'*'grant, socurrent_user.can()answers false where enforcement answers true (wall-less org admins) #20083), Q2 B (yes (widening)) and Q3 A (objectql: a formula field or a CELdefaultValuethat 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).successorskipped, and every required context is green.mainmoved under this PR:rule-validator.tstook a landing after the branch's merge baseb76aad5f6.git merge-treeagainst currentmainis clean, the regions are disjoint (evaluateOptionVisibilityand its call sites), and the merge queue runs the required suite on the merged tree./auth/me/permissionsmerge moves into@objectstack/core, with byte-identical responses.plugin-hono-serverre-exports the six names, so no import changes.plugin-securitygains the member. Thedomain:clianddomain:servicesseats are told after MERGED.warn" is one per row;EvaluateRulesOptions.permissions?goes unmentioned in the changeset;permissionPredicateCacheis unbounded, likeuserRootCache.Landing:
readyplus auto-merge through the queue now.objectstack-fleet commentedon Sep 25, 2026
os-dev-report
{ "issue": 18783, "addendum": "patch round 1, queue failure", "status": "done", "branch": "claude/issue-18783-server-can-permissions", "pr": "https://github.com/objectstack-ai/objectstack/pull/20079", "head": "7a6b091b2a", "session": "session_01Bvd69VPa6puiNzzPUroDBx", "premise_still_valid": true, "summary": "Queue run 36088336600 failed on 'Test Core (1/6)' and on 'Spec property liveness', which is the same gate. Those two check-runs and the 'Test Core' aggregate were the only non-success results among the 26 check-runs on queue commit 1c7b5f08a4. Cause: this PR moved annotateEffectiveApiOperations out of plugin-hono-server's current-user-endpoints.ts, but packages/spec/liveness/permission.json objects.allowExport still cited it there, and that file now has 0 mentions of allowExport. The fix repoints that one citation to packages/core/src/security/effective-object-permissions.ts#annotateEffectiveApiOperations, the ledger's own path#symbol style. That file has 7 mentions and still holds the cited read `const exportBit = acc.allowExport ?? wildExport`. The other two citations and the note's existing text are kept, and a dated 2026-09-25 sentence is appended recording the repoint. verifiedAt is stamped 2026-09-25, as the gate's own repair text prescribes, after re-closing all three legs at this head: rest enforceExportPermission answers 403 EXPORT_NOT_PERMITTED off security.canExport, canExport asks checkObjectPermission('export', …), and the annotate leg reads the per-object bit before '*'. This is a declared, mechanical cross-lane pointer update in the packages/spec LEDGER, not in src. It is the only pointer the sweep found that my move had made false.", "tests": "Reproduced on the unpatched head f3fe6d6cfd: `pnpm --filter @objectstack/spec check:liveness` exit 1 with 'key-mention anchoring: 612 (entry, cited file) pair(s) asked, 610 anchored, 1 exempt, 1 UNANCHORED.' and '✗ … permission/objects.allowExport → packages/plugins/plugin-hono-server/src/current-user-endpoints.ts'. `pnpm --filter @objectstack/spec exec vitest run scripts/liveness/check-liveness.test.ts` gave 'Tests 20 failed | 44 passed (64)'. After the fix at 7a6b091b2a: check:liveness exit 0 with '612 … asked, 611 anchored, 1 exempt.' and the '✓ every governed-type property …' pass line; the same test file gave 'Test Files 1 passed (1) / Tests 64 passed (64)'. Every spec test that reads the liveness ledgers (scripts/liveness/ plus check-generated-fix-rebuild.pin.test.ts) gave 'Test Files 13 passed (13) / Tests 339 passed (339)'. No code changed in this round, so the round-0 package suites stand at their recorded commits.", "sweep": { "grep_symbols": "git grep -n -E \"foldWildcardSuperUser|clampManagedObjectWrites|seedSuperUserRestrictedObjects|annotateEffectiveApiOperations|wildcardGrantsSuperRead|isWriteOptedIn|GUARDED_WRITE_BUCKETS|ManagedSchemaLike|ApiExposureSchemaLike\" -- ':!**/CHANGELOG.md' ':!packages/core/src/security/effective-object-permissions.ts' ':!packages/core/src/security/effective-object-permissions.test.ts' ':!packages/core/src/security/index.ts' ':!packages/plugins/plugin-hono-server/src/current-user-endpoints.ts'", "grep_symbols_count": "82 lines in 8 files: 2 plugin-hono-server fold pin suites (68 lines, testing through the unchanged re-exports), .changeset 18783/18931/18990 (5), docs/adr/0103 (3), qa/dogfood organization-update-door comments (4), spec/liveness/permission.json (2)", "grep_file": "git grep -n \"current-user-endpoints\" -- ':!**/CHANGELOG.md' ':!packages/plugins/plugin-hono-server/src/**' ':!packages/core/src/security/effective-object-permissions*'", "grep_file_count": "21 lines in 11 files: spec/liveness/permission.json (6), docs/qa/platform-checklist/areas/access-security.json (6), FOLLOW-UPS.md, docs/adr/0124, core auth-gate.ts, the new plugin-security test, 2 dogfood files, spec liveness evidence.test.ts fixture and proof-registry.mts, scripts/check-route-envelope.mjs", "positive_control": "both greps return packages/spec/liveness/permission.json:80, the queue's failing citation", "verdicts": [ "FIXED — permission.json objects.allowExport evidence (#annotateEffectiveApiOperations): the behaviour moved.", "true, unchanged — permission.json systemPermissions (#registerCurrentUserEndpoints /me/apps) and tabPermissions (#registerCurrentUserEndpoints, #tabRank): the tab and system merges stayed in the hono route (tabRank: 6 mentions there).", "true, unchanged — the access-security.json items cite the fields merge, /me/apps, the anonymous traces, #tabPermissions and the fail-open branch, all still in the hono file.", "not false, NOT edited — docs/adr/0124 line 6 names current-user-endpoints.ts as the consumer applying 'the /me/permissions FLS fold' (the route still applies it, now through core). docs/adr/0103 names 'the hono-server clampManagedObjectWrites', which is still exported there. Both files are Tier H governed; editing either would make #20079 Tier H.", "not false, NOT edited — pending .changeset 18931/18990 name the four passes by symbol only and are still true; the dogfood comments are the same." ] }, "gates": { "derived_at": "7a6b091b2a", "derivation": "dispatch-gates --commands --repo objectstack-ai/objectstack: 78 commands. The packages/spec ledger path adds 6 families: spec check:empty-state, check:liveness, check:strictness-ledger, check:variant-docs, plus check:merge-driver and check:platform-checklist. All 6 exit 0.", "results": "All 78 were re-run at 7a6b091b2a: 75 exit 0 and 3 exit 3 PREREQUISITE NOT MET, NOT MEASURED (check:dual-build-cjs-loads and check:type-check-debt need the whole workspace built; check:i18n needs the CLI closure). In the recreated worktree, check:dts-closure, check:lean-entry-closure and check:sourcemap-no-sources-content first exited 3 on a fresh tree; after rebuilding the 19-package closure all three exit 0 (dts-closure 83/83 across 19 packages).", "reconcile": "dispatch-gates --ran: 78 derived, 75 run, 3 NOT-MEASURED, 0 UNRUN", "issue_citations": "check-issue-citations --base b76aad5f6 exit 0 (40 judged, 39 resolve plus 1 PR)", "stale_tree": "The derivation warns that the tree is 19 commits behind origin/main 338feda6dd, and that 3 gate files changed there (check-sdui-lockstep.mjs, check-spec-docblock-symbol-anchors.mjs, doc-authoring-prose-id.baseline.json). main was not merged in this round: the budget was one push, and the queue rebuilds on main." }, "files_changed": [ "packages/spec/liveness/permission.json (3 lines: the allowExport verifiedAt, evidence and note)" ], "deviations": [ "Declared cross-lane edit: packages/spec/liveness/permission.json (the spec lane's ledger, not packages/spec/src), consequent to round 0's move. It adds packages/spec to the PR's path set, and with it the 6 spec-ledger gate families above. The seat notes it in the PR body; the body was not edited.", "verifiedAt stamped 2026-09-25 beside the repoint, per the liveness gate's own repair prescription ('re-anchor at the real symbol, MEASURED … and stamp verifiedAt'), after re-closing all three legs.", "The worktree was removed after round 0, so it was re-created from the pushed branch head f3fe6d6cfd (no remote changes since) and removed again after this round." ], "mcp_calls": "0", "api_writes": "1 this round — POST /repos/objectstack-ai/objectstack/issues/18783/comments (this addendum, via post-stamped / relay). git push 7a6b091b2a is a normal fast-forward, not force, and not a REST write. No PR body edit, no label write.", "open_questions": [], "out_of_scope_findings": [] }objectstack-fleet commentedon Sep 25, 2026
Landed: PR #20079, verified on
maindomain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-25T04:27Z.0318faf692, which has one parent and is an ancestor oforigin/mainafter a fresh fetch. The diff is 13 files, +1705/−355: the reviewed PR at7a6b091b(delta PASS 5826495609 over PASS 5825901229).18783-server-can-option-visibility.mdand the newpackages/core/src/security/effective-object-permissions.tsare present at that commit and absent at its parent.completedviaFixes #18783.pm:dispatchedis removed in the same act. Ruling A (5725678115) is executed in its order: the census, then theplugin-securitymember, then the engine wiring. The server now answerscurrent_user.can(object, verb)in an option'svisibleWhen.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 (formula anddefaultValuecan()get no permission data) and security: the effective object-permission map omits objects covered only by a plain'*'grant, socurrent_user.can()answers false where enforcement answers true (wall-less org admins) #20083 (the effective map omits plain-'*'coverage) carryBlocked-by: #18783. Both were filed bare and are still unrouted, so routing and grading stay with triage. Their blocker is now closed.domain:cliseat post [PM seat] domain:cli — 🟢 zhuangjianguo · session_01SYEJqDL3NXxrJRBVqaMu9T #6024 and thedomain:servicesseat post [PM seat] domain:services — 🟢 marchtian · session_01Nk17Z26X5DB59iQztDBADH · p1–p3, serial (maintainer) #6021./auth/me/permissionsmerge moved into@objectstack/corewith byte-identical responses, andplugin-hono-serverre-exports the six names, so no import changes.plugin-securitygainsgetEffectiveObjectPermissions.refactor(security): one permission fold — the enforcement door and th…
feat(spec): ISecurityService gains the effective-object-permission re…
feat(objectql,plugin-security,core): the server answers current_user.…
fix(core): the effective object-permission map covers a plain `*` gra…
fix(core): effective-map entries reached through a super-user `*` car…