Skip to content

design(v18): the complete guest model in one ADR — identity, doors, grants channel, organization, public-site binding, disclosure, rate limits, and each declared guest key's fate (ADR-0090 D9 enforce-or-remove) #22146

Description

@objectstack-fleet

Ruled: 6074960686 · letter A (G2: close the three doors, then accept) · 2026-10-09T05:32Z
Ruled: 6056614963 · letter D1 A′ D2b R · 2026-10-08T09:14Z
Ruled: 6054113537 · letter A G2 · 2026-10-08T06:44Z
Blocked-by: #22432
Blocked-by: #22430
Blocked-by: #22431

Filing gate: ② a design card on the maintainer's word. Filed by the director seat (seat post #12708, summon #35, session_01VYToj6PQehTEKNrjGM9akg) after the decision discussion on #21908's second privately held producer row (the anonymous object_operation posture). The maintainer asked 「guest 是不是应该完整的重新设计并开adr」, then stated the demand 「最为一个元数据开发平台,guest 是常见的需求吧?」, and answered the seat's proposal (a domain:spec design card at the priority named, with the interim C on the dispatcher) with 「同意 p1」. That sentence is the named demand this card rests on. ⛔ Not a claim. ⛔ Classes, positions and functions only; the anonymous doors' measured readings stay private where #21158 kept them private.

Reader: the domain:spec seat, in the design-round pattern of #8346 and #8345 (a measurement round first, then a decision request, then the ADR draft). priority:p1 · security · target:v18.

Why one ADR, and why now

The anonymous principal is declared in at least seven places today, two of them declared but not enforced, one of them a maintainer ruling that lives only in a code comment, and one question nobody has answered:

Where What it says State on origin/main
ADR-0056 D2; packages/core/src/security/anonymous-deny.ts (#2567) the platform denies anonymous callers by default, one shared !userId && !isSystem → 401 decision enforced; today's floor
ADR-0090 D9 / D10 the guest builtin position, held implicitly and exclusively by unauthenticated principals; the everyone / guest audience anchors packages suggest and never own; the human / agent / guest principal taxonomy declared, not enforced: a permission set bound to the guest anchor resolves nothing (#21158, closed not_planned on 2026-10-04 for want of demand; its question is folded into this card)
the maintainer's ruling of 2026-08-08, Option A (commit f586f1a89) two named entries: assembleExecutionContext (fail-closed, 401) and assembleExecutionContextOrGuest (a first-class guest envelope), the latter adopted only by surfaces that genuinely serve anonymous principals enforced; written in packages/core/src/security/assemble-execution-context.ts near :30 and :386, in no ADR
ADR-0096 D5 / E1 the principal-less hand-off and strict mode being closed by #21908
ADR-0106 D7 guest-facing metadata disclosure; the fallback permission set enforced
ADR-0121 D6 an authRequired: false endpoint must carry an armed rateLimit; webhook signature keys named as a future vocabulary, not promised enforced
packages/metadata-core/src/anonymous-form-intake.ts (#21967, #21980, #21331) the public form doors' candidates, withdrawal and organization (the deployment's default organization) enforced; a posture of its own
the dispatcher's anonymous object_operation path the docs promise "under the anonymous principal"; the executor threads undefined (packages/runtime/src/endpoint-executor.ts:95) gap; the interim is its own card, ruled C (the guest entry), filed beside this one
"which organization does a guest act in" #21158's open semantics; only the form doors answer for themselves unruled

Every mainstream metadata-driven application platform ships a complete anonymous model as a first-class capability: Salesforce Experience Cloud (a site, a guest user with a guest profile, guest-only sharing rules, secure-guest defaults: private org-wide defaults, no record ownership), ServiceNow (public portal pages and scripted REST executing as the guest user under the same ACLs), Microsoft Power Pages (a site, the Anonymous Users web role, table permissions), Odoo (auth='public' executing as the public user under record rules; auth='none' reserved for infrastructure), Supabase (the anon role under row-level security), Hasura (the unauthorized role, or refusal). The scenarios are the ones this platform's customers have: the pre-login half of a customer portal, public forms, public catalogs (products, positions, locations), token-addressed lookups (order tracking, unsubscribe, confirmation), partner webhooks.

The eight questions the ADR decides (questions, not answers)

  1. Identity and ownership. The guest is a principal (principalKind: 'guest', positions: ['guest']), never the system principal; whether a guest may own a record, and if not, whose record a guest's write becomes.
  2. The closed list of doors that may adopt the guest entry: the anonymous form doors, authRequired: false endpoints of type flow, object_operation reads, anything else; every other surface answers 401.
  3. The grants channel. Whether the guest anchor's bindings become enforced (the 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 gap closed), how a deployment grants a guest permission set, and what the empty state answers (deny-all).
  4. Organization. Which organization an anonymous request acts in: the deployment default, a per-site mapping, the walled deployment's answer; alignment with ADR-0131.
  5. Public-site binding. Whether a metadata object binds host or path prefix → organization → guest permission set → allowed doors, the way the platforms above bind a site; this is also where question 4 would be answered declaratively.
  6. Disclosure and explain. Whether ADR-0106 D7 and the explain engine's EXTERNAL posture stand unchanged.
  7. Abuse limits. ADR-0121 D6 stands; whether the webhook signature vocabulary (HMAC, timestamp, replay window) enters with this ADR, with its executor.
  8. The fate of every declared guest key under ADR-0049 (enforce or remove) with ADR-0087 conversions: GUEST_POSITION, AUDIENCE_ANCHOR_POSITIONS (packages/spec/src/identity/position.zod.ts:150), the guest value of sys_audience_binding_suggestion, the two named context entries, the fallback permission set's guest reading.

Process

  • Round 1, measurement only, no production change: an AST census of every reader of GUEST_POSITION, 'guest' and principalKind === 'guest' with positions; the anonymous behaviour of every HTTP door measured on a booted showcase (status and what is served), by door class; the hotcrm tree read for any anonymous surface; a comparison table of the five platforms above against the eight questions. Readings that would be exploit recipes stay private, as 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's did.
  • Round 2: a decision request in the six-item form, the eight questions with options and the four axes, presented by the director seat; the maintainer rules.
  • Round 3: the ADR draft (docs/adr/**, Tier H, the maintainer's approval), then execution cards in the v18 line, each with pins and ADR-0087 dispositions where a key moves.
  • ⛔ No change to packages/spec before the ADR is accepted. ⛔ Round 1 dispatches no build.

Not decided here

The answers. The interim for the dispatcher (the guest entry, ruled C) lands on its own card before #21908's deny and is consistent with any answer this ADR gives: with no grants channel it is deny-all; when the channel lands, the same path serves.

Related: #21158 (folded in) · #21908 · #21967 · #21980 · #21331 · ADR-0056 · ADR-0090 · ADR-0096 · ADR-0106 · ADR-0121 · ADR-0131.

Prior rulings read: guest, anonymous, audience anchor, authRequired, public form → ADR-0090 D9/D10, ADR-0106 D7, ADR-0121 D6, the 2026-08-08 Option A ruling (code comment), #21158's closure 5980599467 (「21158 既然没有需求那就关闭」, superseded by the demand named above), #21967 / #21980 (anonymous intake withdrawal); none designs the model whole.


Generated by Claude Code

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions