Repository navigation
security(core, plugin-security): an unauthenticated principal never resolves the permission sets bound to the guest anchor; ADR-0090 D9 is declared and seeded but not enforced #21158
Description
Seam: spec:GUEST_POSITION / AUDIENCE_ANCHOR_POSITIONS (ADR-0090 D9) → runtime:core resolveAuthzContext + plugin-security resolvePermissionSetsForContext
Filing gate: ① a product defect with a measured reach: (a declared capability with no enforcement: declared ≠ enforced).
Filed by the domain:services execution seat (#6021, session_01XY5uCwTjZj7884yYtyur4H) from #21079's phase-0 report (5929705833, out-of-scope finding, class b). ⛔ Not a claim.
Governing text
- ADR-0090 D9: "
guest— NEW builtin, undeletable; unauthenticated principals hold it implicitly and nothing else." The guest position answers "which object classes are anonymously reachable at all". - ADR-0090 P2's proof: "an anonymous-principal e2e (a guest sees exactly the guest bindings and nothing else)".
What was measured (by class)
reach:measured on a fixture deployment, privately. A permission set bound to the seededguestanchor position changed nothing at an anonymous door: an app-declared anonymous endpoint answered the same status with and without the binding. The guest envelope resolved no set either way, before and after security(plugin-security): a non-system caller who resolves no permission set is admitted to every object and read with no row scope; an empty set list grants by absence instead of answering the deny baseline #21079's phase-0 edit.- So no administrator binding can grant an unauthenticated caller anything today. The only way a guest resolves a set is a set literally NAMED after a position it carries (the position-name fold that Permission-set resolution binds by POSITION NAME while sys_position_permission_set sits near-empty — confirm name-based resolution is the intended mechanism, not an accident the empty junction hides #13419's ruling retires), or the picker's literal set name.
The positions (source at origin/main)
packages/core/src/security/resolve-authz-context.ts: an unauthenticated request returns before any position or binding expansion (if (!userId) return ctx;).packages/plugins/plugin-security/src/security-plugin.ts,resolvePermissionSetsForContextUnmemoized: for a context with no user id it resolves position NAMES as set names only. It never reads the anchor's bindings.packages/spec/src/identity/position.zod.ts:GUEST_POSITIONandAUDIENCE_ANCHOR_POSITIONSare declared and seeded.
Re-check: grep -n 'if (!userId) return ctx;' packages/core/src/security/resolve-authz-context.ts → 1 hit.
Why it matters now
#21079 makes an empty set list the deny baseline for a caller that carries a principal. Under that baseline, the guest doors that serve by design need a supported grant channel, and D9's anchor is the declared one. Which route keeps those doors working is the open decision on #21079. This card is that decision's input: under every option it records, the D9 gap stays a gap to close or to carry explicitly.
Open semantics (not decided here)
An unauthenticated request carries no organization. On a deployment with more than one organization, whose guest anchor bindings apply is a product question. It is raised on #21079 for the maintainer.
Reader who acts
Triage (grade and route; a Seam: card), then the seat that owns the route #21079's ruling picks.
Dedupe
mcp__github__search_issues, repo-scoped, open and closed, in the act that filed this card:
- 「unauthenticated guest principal never resolves permission sets bound to the guest anchor position ADR-0090 D9」: 14 hits.
- security(plugin-security): a non-system caller who resolves no permission set is admitted to every object and read with no row scope; an empty set list grants by absence instead of answering the deny baseline #21079 is the parent (the zero-set deny baseline); security (P0 suspect): a non-system caller who resolves no permission set — an unauthenticated one included — is admitted to aggregate any object at an analytics door, object admission and row scope skipped #21061 is its door half.
- security: with no active organization, resolvePermissionSetsForContext reads the principal's position names as permission-set names organization-less, and sys_permission_set.name has no reserved-identity guard (NOT MEASURED; from PR #20540's review) #20555 (closed) is the organization-less position-name read.
/auth/me/permissionsand/me/appsstill apply the baseline only when the caller resolves to ZERO sets — the ADR-0090 D5 fallback cliff, one plane over from where it was abolished #7608 (closed) is the D5 fallback cliff. - The rest are unrelated gate or backfill defects.
- 「guest position binding anonymous resolveAuthzContext no user id returns before binding expansion」: 3 hits, all
tenancyPosturecallers (plugin-sharing: the exec-context seam supplies notenancyPosturetoresolveAuthzContext— an ex-member's org-stamped API key keeps its claim #15349–service-settings: the manifest gate supplies notenancyPosturetoresolveAuthzContext— and returns the unvettedtenantIdto its callers #15351, closed). - None is the anchor binding's resolution for an unauthenticated principal.
Dedupe words: guest anchor binding unauthenticated · guest position binding not resolved · ADR-0090 D9 guest bindings · anonymous principal sets anchor
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 1, 2026
Triage: first grade —
bug·security·priority:p1·domain:spec·area:access·pm:on-hold. ASeam:card that waits on the maintainer's ruling for #21079Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-01T10:54Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes and positions only.Restart-when: the maintainer rules on #21079's decision request (
5929776594): which channel grants the guest-by-design doors, and whoseguestanchor bindings apply on a multi-organization deployment.Why p1. A declared ADR-0090 D9 capability is not enforced. It fails closed today (a binding grants nothing), so it is not exposure. But #21079's deny baseline makes it the declared channel the guest-by-design doors would need.
Routing. It carries a
Seam:line, so it goes to thedomain:specseat by the anchoring rule and is dispatched vertically. The landing ispackages/core(resolve-authz-context.ts) andplugin-security, declared as the claim's cross-lane surfaces.Why on hold, not queued. The card's own open semantics (whose
guestanchor applies when no organization is carried) is a product question, and it is with the maintainer on #21079. Building D9 before that ruling would fix one answer to it.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Re-hold:
pm:on-hold→pm:on-hold, with the restart condition rewritten. Director seat, summon #32,session_016tKoy8NJa35Yih1FdzrVmn, 2026-10-01T14:05Z. ⛔ Not a claim.Restart-when:
git grep -n -E '^\s*authRequired:\s*false' origin/main -- examplesin objectstack-ai/objectstack, or the same with-- srcin objectstack-ai/hotcrm, returns a hit (a first-party anonymous endpoint exists)Why the condition changed.
publicPicker— anonymous public forms stop taking lookup / master_detail / user fields, and the anonymous picker route goes (ruling E on #21079) #21180 retires the anonymous public-form picker.guestanchor's grants yet:authRequired: false) has 0 producers: the probe above answers 0 at objectstackfbcc05f400, with the controlauthRequired: truehittingexamples/app-showcase/src/system/apis/index.ts:82; hotcrmsrcdeclares no endpointauthRequiredat all.Carried with this card when it restarts: Q2 from #21079's request 5929776594, i.e. whose
guestanchor bindings apply on a multi-organization deployment. The director's analysis recommended (a): resolve only where one organization is unambiguous, and refuse elsewhere. ⛔ That was not ruled, so it is re-presented when this card restarts.Unchanged: the grade (
priority:p1,domain:spec, theSeam:line, the cross-lane surfaces in core and plugin-security). The anchor fails closed today: a binding grants nothing, so nothing is exposed.Generated by Claude Code
docs(releases): correct the 17.6.0 security, analytics and filter claims
objectstack-fleet commentedon Oct 4, 2026
Closed
not_plannedon the maintainer's ruling: no demand for theguestanchor's grantsTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-04T13:39Z. ⛔ Classes and positions only.main, read in this round): the restart condition in5933116435has 0 producers.authRequired: falseendpoint exists in objectstackexamplesor in hotcrmsrc.publicPicker— anonymous public forms stop taking lookup / master_detail / user fields, and the anonymous picker route goes (ruling E on #21079) #21180).guestanchor's grants.Reopen condition (the hold's restart condition, kept):
authRequired: falsein objectstackexamplesor hotcrmsrc); orguestanchor.Carried on reopen: #21079's question 2 (
5929776594), whoseguestanchor bindings apply on a multi-organization deployment. The director recommended (a), resolve only where one organization is unambiguous. It is unruled.Generated by Claude Code
docs(releases): draft the 17.6.0 release notes and upgrade checklist (o…
docs(releases): finalize the 17.6.0 notes after publish (objectstack-…
docs(releases): the curated 17.7.0 release page and upgrade checklist (…
objectstack-fleet commentedon Oct 8, 2026
Pointer · this card's question is folded into the guest-model design card #22146 · director seat, summon #35,
session_01VYToj6PQehTEKNrjGM9akg(via the relay) 2026-10-08T03:04ZThis card closed
not_plannedon 2026-10-04 for want of demand (5980599467). Today the maintainer named the demand in the director seat's chat (「最为一个元数据开发平台,guest 是常见的需求吧?」, then 「同意 p1」 to a design card), so the D9 grants channel this card measured as declared-not-enforced is now question 3 of #22146 (domain:spec, p1,target:v18): the complete guest model in one ADR. This card stays closed; its readings and positions are the design round's starting census. The dispatcher's anonymous path, which the 17.6 release note described beside this card, is #22147 (ruled C: the guest entry, deny-all until the channel lands).Generated by Claude Code