Repository navigation
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
Activity
- addedpriority:p1High: required for production / M2High: required for production / M2area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsand removed
on Oct 8, 2026 objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 (round 1 of 3: measurement only, per the card's Process) · 2026-10-08T04:26Z
Session:session_01LAi5BVvQNiYzepSAcsoFLK
Account:os-litant(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-22146-guest-model-measure
Worktree:objectstack-issue-22146
Domain:domain:spec
Seat:domain:spec#1(seat post #6017)
File surface: ⛔ none written. Measurement round only (the card's Process, round 1: "measurement only, no production change", "⛔ Round 1 dispatches no build"). The branch stays at its base and no PR is opened, in the shape of #8346's round (6041073455). Measured onorigin/main959c209d56:- an AST census of every reader of
GUEST_POSITION,'guest'andprincipalKind === 'guest', with their positions; - the anonymous behaviour of every HTTP door, by door class, on a booted showcase (the dev's own temporary server, stopped by the dev);
- a read of the hotcrm tree (public, a read-only shallow clone in scratch) for any anonymous surface;
- the five-platform comparison against the eight questions.
Every probe is a scratch file, deleted after the run. Stop on breach and explain in the report.
Container & model:L(breadth: census, booted measurement, a second tree, a comparison),mode:subagent,model: opus(--tier: no path-derived mandate; the default tier for a design round with judgment).
Clause-②: no
Responsibility: n/a — not a defect card
Thread-read: none
Serial constraints cleared: none. The round writes no file. The dispatcher interim (the guest entry, ruled C) is its own card and is not touched here.What the round delivers: a measured design note in the
os-dev-report, in classes, positions and functions only. Door-level readings that would work as an exploit recipe stay off GitHub, as #21158's did: they go only to this seat's session. Then this seat writes round 2's decision request on this card (维护者速读, the four-facet block,Prior rulings read:) for the director seat to present. ⛔ No production change, and ⛔ no change topackages/specbefore the ADR is accepted.- an AST census of every reader of
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 22146, "status": "done", "branch": "claude/issue-22146-guest-model-measure", "pr": null, "session": "session_01LAi5BVvQNiYzepSAcsoFLK", "premise_still_valid": true, "summary": "Round 1 measurement (no production change; branch stays at base 959c209d56, no PR). The card's table re-measures true on today's main except one row that moved on since the card was written: the dispatcher's anonymous object_operation path at endpoint-executor.ts:95 no longer threads undefined for anonymous. #22147 / PR #22177 established on 2026-10-08 that the runtime face resolves the guest envelope upstream at resolveRequestScope, so the executor receives principalKind 'guest'. The AST census confirms GUEST_POSITION the exported constant is read only to compose the anchor vocabulary and its display metadata; every enforcement branch keys on the string literal 'guest', not on the constant. The guest anchor's grants channel is still declared-not-enforced (ADR-0090 D9; #21158). The five-platform comparison is recorded from public docs. The booted anonymous HTTP-door sweep (card item 3) was NOT RUN; recorded as NOT MEASURED, and no door-level readings are claimed.", "tests": "No tracked file changed (git status --porcelain empty; remote branch head 959c209d56 == base). Instrument: a TypeScript-compiler AST walk over 7810 packages/** source files (scratch, deleted after the run), with EVERYONE_POSITION as the control symbol — the walk counts 3 non-test non-import reads of it (builtin-positions.ts:85, suggested-audience-bindings.ts:396, position.zod.ts:151), proving the walk detects reads. Closure build for local gate 1 (showcase dependency closure, OS_SKIP_DTS, under os-verify-lock): 60/60 tasks exit 0. hotcrm read-only shallow clone c529de2b into scratch, read statically (no network probing). Platform comparison from public documentation URLs.", "mcp_calls": "0", "api_writes": "2 — (1) the empty-branch git push establishing the claim marker (not REST); (2) this os-dev-report comment via scripts/pm/post-stamped.mjs (POST /repos/objectstack-ai/objectstack/issues/22146/comments). No PR, no labels, no MCP write tool.", "census": { "instrument": "TS AST walk, packages/** (7810 files, 0 parse failures); control EVERYONE_POSITION = 3 src reads detected.", "GUEST_POSITION_constant": "1 declaration (packages/spec/src/identity/position.zod.ts:150), 2 re-export specifiers (spec/src/index.ts:177, plugin-security/src/builtin-positions.ts:68), 2 reads — both compose DECLARED vocabulary, not an enforcement branch: AUDIENCE_ANCHOR_POSITIONS array (position.zod.ts:151) and the Guest display metadata key (builtin-positions.ts:90). FINDING: no enforcement decision reads the exported constant; every runtime branch below keys on the string literal 'guest'.", "AUDIENCE_ANCHOR_POSITIONS": "1 declaration (position.zod.ts:151), 4 reads: plugin-security builtin-positions.ts:103 (registers the six built-in positions to the engine registry — DECLARED metadata, ADR-0131 D2), suggested-audience-bindings.ts:450 (findAnchorPositions, install-time suggestion resolution), packages/verify/src/rls.ts:134 (test-persona exclusion).", "principalKind_guest_enum": "The 'guest' member of the principalKind enum is declared in packages/spec/src/kernel/execution-context.zod.ts:126 and :193 and packages/spec/src/security/explain.zod.ts:358 (3 declaration sites).", "guest_literal_readers_enforced": [ "packages/core/src/security/assemble-execution-context.ts:302 — entryFields sets positions ['guest'] on the anonymous branch (ENFORCED: the envelope the runtime/MCP face emits via assembleExecutionContextOrGuest).", "packages/core/src/security/assemble-execution-context.ts:317 — entryFields sets principalKind 'guest' on the anonymous branch (ENFORCED).", "packages/plugins/plugin-security/src/explain-engine.ts:141 — derivePosture: principalKind==='guest' maps to EXTERNAL (ENFORCED as explain output; the engine explains, it does not admit).", "packages/plugins/plugin-security/src/security-plugin.ts:7020 and :7044 — assertAudienceAnchorBindingGate: a sys_position_permission_set write naming the guest (or everyone) anchor is gated by the strictest-tier predicate (ENFORCED at binding time).", "packages/spec/src/security/high-privilege.ts:201/:210/:212 — describeAnchorForbiddenBits: anchor==='guest' drops the app-token excusal and takes the strictest tier, no star, no edit bit (ENFORCED predicate the gate above calls).", "packages/plugins/plugin-security/src/delegated-admin-gate.ts:149 — ANCHOR_POSITIONS Set(everyone, guest): anchors stay tenant-level, never delegatable (ENFORCED, ADR-0090 D12)." ], "guest_literal_readers_declared_or_vocabulary": [ "packages/plugins/plugin-security/src/objects/sys-audience-binding-suggestion.object.ts:128 — a suggestion row's anchor select offers 'guest' (DECLARED; the binding it would suggest resolves nothing today — #21158).", "packages/plugins/plugin-security/src/suggested-audience-bindings.ts:975 — everyone|guest cast in confirmAudienceBindingSuggestion (the confirm path for the above).", "packages/plugins/plugin-sharing/src/objects/sys-record-share.object.ts:175 — recipient_type select includes 'guest' (DECLARED-ONLY; ISharingService.grant refuses it, ADR-0078 — persisted-for-forward-compat vocabulary).", "packages/spec/src/contracts/sharing-service.ts:100 — RecordShareRecipientType union member 'guest' (DECLARED type, refused at grant).", "packages/spec/src/system/book.zod.ts:126 — comment: audience public is the built-in guest position (DOC).", "packages/spec/src/migrations/entries/semantic/17.* and registry.ts — the retired sharing-rule 'guest' recipient conversion prose (MIGRATION, already retired)." ] }, "card_table_remeasured_on_main": { "commit": "959c209d56", "ADR-0056 D2 / anonymous-deny.ts": "TRUE. shouldDenyAnonymous (anonymous-deny.ts:152) is the one no-user, no-system -> 401 decision; 11 non-test call sites across rest + runtime domains + endpoint-policy + service-datasource. requireAuth opt-out retired (spec tombstone + ADR-0087 conversion). Today's floor, enforced.", "ADR-0090 D9/D10": "TRUE as the card states. D9 guest anchor: declared (position.zod.ts GUEST_POSITION/AUDIENCE_ANCHOR_POSITIONS; plugin-security registers the six built-ins to the registry and the metadata door, ADR-0131 D2). A permission set bound to the guest anchor is gated (strictest tier) but resolves NOTHING at an anonymous request: resolve-authz-context.ts:415 returns for a user-less request before any anchor/binding expansion, and resolvePermissionSetsForContextUnmemoized resolves position NAMES as set names only. So D9's grants channel is DECLARED-NOT-ENFORCED (confirms #21158). D10 principalKind taxonomy incl. 'guest' is the live enum.", "2026-08-08 Option A / assemble-execution-context.ts": "TRUE. commit f586f1a89b confirmed in history. Two named entries: assembleExecutionContext (:386, fail-closed, undefined for no userId -> 401) and assembleExecutionContextOrGuest (:395, guest envelope). Positions near :30 (module doc), :386, :395. In no ADR, as the card says.", "ADR-0096 D5/E1": "TRUE. ADR-0096 status Proposed; D5 strict mode not built; E1 principal-less hand-off is isPrincipalLessContext (security-plugin.ts:372, read around the middleware :2451) — being closed by #21908 (Blocked-by #22147, #22046).", "ADR-0106 D7": "TRUE, enforced. getMetadataReadableFields (security-plugin.ts:5903) resolves the configured fallback set for a zero-set caller on the metadata plane; liveness planned+authorWarn applies only to externalSharingModel (D11), not D7.", "ADR-0121 D6": "TRUE, enforced. endpoint-publish-gate.ts:509 refuses authRequired:false without an armed rateLimit. Webhook signature vocabulary (HMAC/timestamp/replay) is NOT in ApiEndpointSchema — named future-vocabulary, not promised. The trigger-api inbound-hook door is a SEPARATE channel from authRequired:false endpoints: api-trigger.ts verifies HMAC with timingSafeEqual, 404 unknown-hook, 503 unreadable-secret, 401 bad-signature; no timestamp/replay window.", "anonymous form intake / anonymous-form-intake.ts": "TRUE, enforced, a posture of its own. anonymous-form-intake.ts re-exports the spec candidates rule; the public-form submit route (rest-server.ts near :11175) builds context with publicFormGrant {object}, permissions ['guest_portal'], anonymous true — the guest literal here is 'guest_portal' (a deployment profile name), NOT the guest anchor. On a walled posture an anonymous insert into an org-walled object is refused (anonymousFormIntakeUnavailability; engine resolveSystemInsertOrganization, SystemWriteOrganizationRequiredError).", "dispatcher anonymous object_operation / endpoint-executor.ts:95": "CHANGED SINCE THE CARD. The card calls this a gap (executor threads undefined). On today's main the executor's executionContext param is still optional, but #22147 (PR #22177, 2026-10-08, ACCEPTed pending CI/contract-review) measured that the anonymous request already arrives as the guest envelope: dispatcher-plugin.ts fallback -> HttpDispatcher.resolveRequestScope -> resolveExecutionContext -> assembleExecutionContextOrGuest, so the value is not undefined on the normal path; with no guest grants the object_operation is refused at the CRUD gate. #22177 is comments-only + pins (no production change), so the card's row 8 is a doc-comment artifact, not a live gap. Question 2's closed-list-of-doors answer must account for this already holding." }, "anonymous_http_door_classes": "NOT MEASURED (card item 3). The booted-showcase anonymous door sweep was not run and no door-level status/body readings are claimed (public or private). What IS established statically on main, by door class and governing function, with no request made: control-plane allowlist (auth-gate.ts ALLOW_ROUTES: health, ready, discovery, me/apps, me/localization) exempt; meta read + data CRUD + actions + flows + analytics domains gated by shouldDenyAnonymous (11 call sites) -> 401 for a non-system caller; authRequired:false declared endpoints -> guest envelope then CRUD/flow gates (denied with no grants); public form doors -> publicFormGrant (insert-only, that object only); share-link resolve -> token-gated SYSTEM read; book.audience public meta read -> the §6.7 audience gate (reachability only). Round 2 or a measurement sub-round should boot and record these by class before the ADR fixes the closed list of doors.", "five_platform_comparison": { "note": "Public documentation, cited by URL. Each row read against the eight questions.", "salesforce_experience_cloud": "A Site has its own guest user + guest user profile (per-site). Secure Guest User policy: external org-wide defaults forced to Private and cannot be loosened; guest access capped at READ; no public-group/queue membership; manual sharing cannot reach guests; records exposed only by explicit guest sharing rules. Ownership: 'Reassign/Assign new records created by guest users to the default owner' assigns guest-created records to a named internal default owner, so a guest owns nothing. Docs: resources.docs.salesforce.com communities_secure_guest_users.pdf; salesforce.com/blog/protecting-your-data-essential-actions-to-secure-experience-cloud-guest-user-access/", "servicenow": "Public portal pages (sys_public record + public role on the table) and scripted REST APIs with 'Requires authentication' unchecked run as the guest user under ACLs; path-based REST Endpoint ACLs (sys_security_acl, type REST Endpoint) restrict guest-reachable endpoints. Docs: servicenow.com/docs (Scripted REST APIs; Add a path-based ACL for a scripted REST API; Configure tables to work with guests).", "microsoft_power_pages": "A Site; the Anonymous Users web role (exactly one per site may carry the anonymous flag); Table Permissions take effect only when linked to a web role; a page cannot be restricted TO the anonymous role (use Anyone can see this page); a governance control (admin center) can disable anonymous access to Dataverse data, scoped all-sites down to one site, after which makers cannot grant anonymous access on new table permissions. Docs: learn.microsoft.com/power-pages/security/assign-table-permissions; learn.microsoft.com/power-pages/security/disable-anonymous-access", "odoo": "http.route auth public runs anonymous requests as the shared Public user under record rules; auth none stays active with no DB, for framework/webhook internals and skips the session/auth system; auth user requires login. An ir.model.access line or ir.rule with no group applies to all users incl. public. Multi-website: a per-website public user (website.user_id) + website_id record rules scope a site's anonymous reach (community pattern, not first-party docs). Docs: odoo.com/documentation/18.0/developer/reference/backend/http.html", "supabase": "Unauthenticated requests map to the Postgres anon role; RLS-on with no policy denies anon entirely; a policy's TO clause lists anon to expose a table to logged-out callers; the anon API key ships in the client so policies are the whole control; service_role bypasses RLS and is server-only. Anonymous sign-in is a different concept (assumes authenticated role, is_anonymous JWT claim). Docs: supabase.com/docs/guides/auth/row-level-security; supabase.io/docs/guides/api/api-keys", "hasura": "With an admin secret, unauthenticated requests are rejected by default; HASURA_GRAPHQL_UNAUTHORIZED_ROLE (or --unauthorized-role) names a role those requests run as, then you grant that role per-table permissions (blocked by default). Docs warn against session variables in the unauthorized role. v3/DDN uses a claims-map default public role instead. Docs: hasura.io/docs/latest/auth/authentication/unauthenticated-access/; hasura.io/docs/3.0/recipes/authorization/public-access-role", "convergence": "Every one ships a FIRST-CLASS anonymous principal (guest user / guest role / anon role / Public user / unauthorized role) that runs the same evaluation pipeline as an authenticated principal, denies by default, and grants only by explicit, admin-confirmed configuration — a site/role binding (Salesforce, Power Pages, ServiceNow), a policy TO-clause (Supabase), or a named role's permissions (Hasura/Odoo). The ObjectStack guest anchor (D9) is the same shape; its gap is the grants channel (question 3) and the organization binding (questions 4/5), not the principal, which already exists (Option A guest envelope)." }, "eight_questions_facts": { "note": "Facts bearing on each question (enforced vs declared), with a labeled LEAN (not a decision). Each lean's four-axis analysis is in the dev's final message to the PM, not decided here.", "q1_identity_ownership": "ENFORCED: the guest principal exists (principalKind guest, positions ['guest'], isSystem false) via assembleExecutionContextOrGuest. OWNERSHIP is UNDECLARED: a guest holds no userId, the public-form path stamps no owner (case/lead hooks null a forged owner_id; ADR-0131 forbids a null organization_id). Every platform reassigns guest-created records to a system/default owner. LEAN: guest never owns; a guest write is owned by the deployment's system/default-owner identity.", "q2_closed_list_of_doors": "ENFORCED today: public-form submit (publicFormGrant), share-link resolve (token->SYSTEM), book.audience public meta read, authRequired:false declared endpoints (guest envelope; object_operation + flow). The dispatcher anonymous object_operation path ALREADY reaches the guest (row 8), so the list is nearly whole. LEAN: enumerate exactly these as the closed set; every other surface answers 401 via shouldDenyAnonymous; gate the domain not each face (the analytics lesson in authz-conformance.matrix.ts:286).", "q3_grants_channel": "DECLARED-NOT-ENFORCED (the #21158 gap). resolve-authz-context.ts:415 returns for a user-less request before anchor/binding expansion; the guest anchor's sys_position_permission_set bindings resolve nothing. The binding GATE exists (assertAudienceAnchorBindingGate) but nothing consumes the binding at request time. ADR-0106 D7 fallbackPermissionSet is the one channel that reaches a zero-set caller, but only on the metadata plane. LEAN: enforce D9 — resolve the guest anchor's bindings for the guest principal (deny-all empty state); contract-first, not a consumer fallback.", "q4_organization": "UNDECLARED for the general guest; the form doors answer for themselves (walled posture refuses an org-less insert; single posture derives the one org). resolveSystemInsertOrganization throws SystemWriteOrganizationRequiredError on the multi-org branch. ADR-0131: no null organization_id anywhere. LEAN: resolve only where one organization is unambiguous and refuse elsewhere (the director's (a) carried from #21158/#21079 Q2), OR bind it declaratively via question 5.", "q5_public_site_binding": "NO metadata object binds host/path-prefix -> organization -> guest permission set -> allowed doors today. Each platform has exactly this (Salesforce Site, Power Pages Site, ServiceNow portal). This is where q4 is answered declaratively. LEAN: a 'site' metadata type is the long-term-correct shape, but startup-focus says do not ship it until a pulled scenario needs more than the single-org form doors already serve.", "q6_disclosure_explain": "ENFORCED and stands: ADR-0106 D7 metadata-plane fallback; explain derivePosture guest -> EXTERNAL. LEAN: leave unchanged.", "q7_abuse_limits": "ENFORCED: ADR-0121 D6 (authRequired:false requires an armed rateLimit). Webhook signature vocabulary (HMAC/timestamp/replay) is NOT in ApiEndpointSchema; the trigger-api channel already does HMAC (no timestamp/replay window). LEAN: D6 stands; adding the signature vocabulary needs its own executor and card (ADR-0078 forbids keys with no consumer) — do not fold it in speculatively.", "q8_fate_of_declared_keys": "GUEST_POSITION / AUDIENCE_ANCHOR_POSITIONS: LIVE (compose the registered anchor vocabulary + the binding gate), KEEP — but the constant feeds declaration only; enforcement keys on the literal. sys_audience_binding_suggestion 'guest' option: DECLARED, inert until q3 lands. sys_record_share recipient_type 'guest' + RecordShareRecipientType 'guest': DECLARED-ONLY, refused at grant (ADR-0078) — an ADR-0049 enforce-or-remove candidate independent of this ADR. The principalKind enum 'guest' members and explain enum: LIVE. The fallbackPermissionSet guest reading: LIVE (D7). LEAN: keep the anchor+taxonomy; resolve the suggestion/binding with q3; retire or enforce the sys_record_share 'guest' recipient on its own ADR-0049 card." }, "out_of_scope_findings": [ "class: c (an authoring trap: a declared key the runtime does not honor) · reach: named producer — sys_record_share.recipient_type offers 'guest' (sys-record-share.object.ts:175) and RecordShareRecipientType declares it (contracts/sharing-service.ts:100), but ISharingService.grant refuses it (ADR-0078); an author (or AI) picking 'guest' in the record-share picker writes a value the service rejects. Distinct from the guest ANCHOR this card designs. carrier: #22146 (q8 names it) and an independent ADR-0049 enforce-or-remove card · dedupe words: sys_record_share guest recipient, RecordShareRecipientType guest, ADR-0078 refused recipient, record share picker guest", "class: c · reach: named producer — sys_audience_binding_suggestion offers a 'guest' anchor option (sys-audience-binding-suggestion.object.ts:128) and the binding gate accepts a guest binding (strictest tier), but the binding resolves nothing at an anonymous request (#21158). An admin confirming a guest-anchor suggestion is told it is bound while it grants nothing. carrier: #22146 q3 (the grants channel closes it) · dedupe words: guest anchor suggestion inert, sys_audience_binding_suggestion guest, binding resolves nothing, ADR-0090 D9 not enforced", "carrier: #22146 q8 · noted, not filed — GUEST_POSITION the exported constant is read only to compose the anchor array and display metadata; no enforcement decision reads the constant, every runtime branch keys on the string literal 'guest'. Not a defect, but q8 should note that deleting/renaming the constant would NOT move any enforcement branch, so its liveness is declaration-composing, not enforcement-read." ], "gates": "none — measurement round, no tracked file changed. Proof: git status --porcelain empty; remote branch head 959c209d56 == base 959c209d56 == origin/main. Local closure build for gate 1 ran green (60/60) but produced no tracked change. No skills/** or governed surface touched." }objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsPointer from the
domain:cliseat (#6024) ·session_01RWZbGvPFcRKvUqASZtunCU· 2026-10-08T05:06Z. ⛔ Not a request for work and not a claim. It is input for this card's measurement round.#22147 (ruled C) landed as PR #22177 (
6ed0c0f3e5), and its premise was falsified. Three facts bear on this card:- The table row for "the dispatcher's anonymous
object_operationpath" is not a gap onmain.- The endpoint fallback's scope resolution (
HttpDispatcher.resolveRequestScope→resolveExecutionContext) already takes the explicit guest entry,assembleExecutionContextOrGuest. - So an anonymous request admitted by an
authRequired: falseendpoint executes as the guest (principalKind: 'guest',positions: ['guest'],isSystem: false). - It is refused at the CRUD check (
403), with nothing disclosed. "The executor threadsundefined" came from a stale doc comment, now corrected. - Pinned at two tiers by PR test(runtime): pin the guest principal an anonymous request executes as at authRequired:false endpoints #22177; independent contract review
6052244724.
- The endpoint fallback's scope resolution (
AutomationContext(the spec contract) has noprincipalKindorisSystemmember.- A guest-triggered flow sees the guest only as the
guestposition. service-automationrefuses a user-lessrunAs: 'user'run instead of running it as the guest.- When this card's grants channel lands, a guest-triggered flow will need both.
- A guest-triggered flow sees the guest only as the
- An anonymous request at an
authRequired: falseflow endpoint whose target declaresrunAs: 'system'runs that flow's data steps as the system principal, by the flow author's declaration (ADR-0073 D2: explicit opt-in).- Whether ruling C's "never the system principal" reaches that is put to the maintainer on security(rest, runtime): an anonymous request at an app-declared
authRequired: falseendpoint executes principal-less — execute it as the guest principal (ruled C), never principal-less, never system #22147 (needs-user-decision). - The seat's recommendation there is A: keep it, and fold "which doors may trigger an elevated flow, and how loudly publish says so" into this card's doors question.
- Whether ruling C's "never the system principal" reaches that is put to the maintainer on security(rest, runtime): an anonymous request at an app-declared
Read by class only; the measured readings stay on #22147's thread.
- The table row for "the dispatcher's anonymous
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsSeat verdict: ACCEPT the measurement round (
6052668454), with one item NOT MEASURED. The eight questions go to the maintainer ·domain:specseat 1 (#6017) ·os-litant·session_01LAi5BVvQNiYzepSAcsoFLK· 2026-10-08T05:07ZChecked on GitHub and on
origin/main6ed0c0f3e5, not from the report:- Round terms met: branch
claude/issue-22146-guest-model-measuresits at its base959c209d56, with no commit and no PR. The report comment is intact. - An anonymous request resolves no position and no bound permission set:
resolve-authz-context.tsreturnsif (!userId) return ctx;before the position/permission aggregation. So ADR-0090 D9'sguestanchor binding is declared and grants nothing (security(core, plugin-security): an unauthenticated principal never resolves the permission sets bound to theguestanchor; ADR-0090 D9 is declared and seeded but not enforced #21158). - The 2026-08-08 Option A pair exists:
assembleExecutionContext(assemble-execution-context.ts:377, fail-closed) andassembleExecutionContextOrGuest(:395). No ADR records it. - ADR-0121 D6 is enforced:
packages/spec/src/api/endpoint-publish-gate.ts:509refusesauthRequired: falsewithout an armed rate limit. GUEST_POSITIONis read only to compose the anchor list (position.zod.ts:151) and its display metadata (builtin-positions.ts:90). Every enforcement branch keys on the literal'guest'.sys_record_share.recipient_typeoffers'guest'(sys-record-share.object.ts:175, contractsharing-service.ts:100), and the grant refuses it (ADR-0078).- The card's row 8 has changed since filing: security(rest, runtime): an anonymous request at an app-declared
authRequired: falseendpoint executes principal-less — execute it as the guest principal (ruled C), never principal-less, never system #22147's PR test(runtime): pin the guest principal an anonymous request executes as at authRequired:false endpoints #22177 (merged) pins that an anonymous request at anauthRequired: falseendpoint already reaches the guest envelope upstream and is denied with no grants.
Not measured (confidence gap): item 3, the booted anonymous sweep of every HTTP door class. The environment's safety check stopped the dev while it was authoring the unauthenticated door driver. The dev did not work around it, and the seat does not either: whether and where to run that probe is the maintainer's call (question G below). What the round has instead is the static door-class reading. The allowlisted control routes are exempt. Meta read, data CRUD, actions, flows and analytics deny a non-system anonymous caller through
shouldDenyAnonymous.authRequired: falseendpoints reach the guest envelope, then the gate. The public form doors admit only an insert into that one object. A share link is token-gated.book.audience: publicpasses the audience gate for metadata reachability only. No exploit-recipe reading exists, so nothing is withheld.Out of scope, routed to this card's questions (no new cards): the
sys_record_share'guest'recipient → Q8; the inertguestaudience-binding suggestion → Q3.Release:
session_01LAi5BVvQNiYzepSAcsoFLK· why: round 1 is delivered, and the eight answers are the maintainer's (the card's Process, round 2) · to:needs-user-decision, unassigned. Round 3 (the ADR draft) is claimed afresh after the ruling.
os-decision-facets
待裁(
needs-user-decision):平台的 guest(未登录访问者)模型,按什么方向写进一份 ADR?维护者速读
- 改了什么: 没改代码。本轮只测量,把这张卡要你定的八个问题(加一个测量缺口)摆在下面。
- 为什么要你定: 你说 guest 是元数据平台的常见需求、要整体重新设计并开 ADR(「同意 p1」)。八个问题里的身份、开放哪些入口、怎么授权、落哪个组织,都是安全与产品语义的取舍,属于你的地板。
- 测到的关键事实:
- guest 主体今天已经存在,匿名请求被识别为 guest、没有任何授权。
- 后台能把权限集绑到 guest 锚点,可匿名请求时这个绑定根本不解析。界面让管理员以为给访客授了权,实际一点都没给。
- 五家主流平台(Salesforce、ServiceNow、Power Pages、Odoo、Supabase)全都是同一个形状:一个一等的匿名主体,走同一套权限管线,默认全拒,只靠管理员显式配置授权。
- 席位意见: 推荐整包 A:guest 永不拥有记录;开放入口按现有五类闭合列表定死,其余一律 401;让 guest 锚点的授权绑定真正生效,空集即全拒;组织只在能唯一确定时解析,多组织部署先拒绝;站点绑定在 ADR 里定形状、等真实需求再建;防滥用不变;旧的 guest 键逐一定去留。测量缺口选 G2,先按静态读数裁,启动实测列为 ADR 验收前提。
- 你要做的: 回一行字母,例如「22146 A G2」表示整包按推荐;想改某一问就写出那一问的字母,例如「22146 A Q4B G1」。
一句话问题
没登录的人(门户首页访客、填公开表单的客户、查订单的路人、合作方的 webhook)能在应用里看到什么、做什么、以谁的名义做、写进哪个组织?今天这件事分散在七处,两处只声明没兑现,一处只写在代码注释里,"落哪个组织"没人答过。
背景
- 本卡由总监席按你的原话立卡(「guest 是不是应该完整的重新设计并开adr」「最为一个元数据开发平台,guest 是常见的需求吧?」「同意 p1」)。流程三轮:测量、你裁、ADR 草稿。本条是第二轮。
- 测量轮报告
6052668454,只读、未改码。五平台对照附公开文档链接。 - 匿名调用
authRequired: false端点的过渡做法已在 security(rest, runtime): an anonymous request at an app-declaredauthRequired: falseendpoint executes principal-less — execute it as the guest principal (ruled C), never principal-less, never system #22147 裁为 C(以 guest 主体执行),对应的 PR test(runtime): pin the guest principal an anonymous request executes as at authRequired:false endpoints #22177 已合入。
Governing text
- ADR-0090 D9 / D10:声明了
guest内置职位(由未认证主体隐式、排他持有)与everyone/guest受众锚点,以及 human / agent / guest 主体分类。D9 的锚点绑定目前没有兑现。 - ADR-0056 D2(
packages/core/src/security/anonymous-deny.ts):平台默认拒绝匿名调用。 - ADR-0106 D7:面向 guest 的元数据披露与兜底权限集。
- ADR-0121 D6:
authRequired: false必须挂已武装的限流(endpoint-publish-gate.ts:509强制)。 - ADR-0131:组织归属是全称的,没有 NULL
organization_id。 - ADR-0049:声明了就必须兑现,否则退役。
- 2026-08-08 你的 Option A 裁定(
f586f1a89,只写在assemble-execution-context.ts注释里):两个入口,失败即拒的assembleExecutionContext与真正服务匿名者才用的assembleExecutionContextOrGuest。
前提(各带复核命令与阳性对照)
- 匿名请求不解析任何职位或绑定。 复核:
git grep -n "if (!userId) return ctx;" origin/main -- packages/core/src/security/resolve-authz-context.ts。对照:同文件git grep -c "resolveExecutionContext"应大于 0。 - Option A 两个入口都在。 复核:
git grep -n "export function assembleExecutionContextOrGuest" origin/main -- packages/core/src/security/assemble-execution-context.ts。对照:同文件git grep -c "export function assembleExecutionContext("应为 1。 - D6 在发布时强制。 复核:
git grep -n "authRequired === false && !armed" origin/main -- packages/spec/src/api/endpoint-publish-gate.ts。对照:同文件git grep -c "ADR-0121"应大于 0。 - 共享接收方
guest只声明不兑现。 复核:git grep -n "'unit_and_subordinates', 'guest'" origin/main -- packages/plugins/plugin-sharing/src/objects/sys-record-share.object.ts。对照:同文件git grep -c "recipient_type"应大于 0。 GUEST_POSITION常量只用于拼词表。 复核:git grep -n "GUEST_POSITION" origin/main -- 'packages/**/*.ts' ':!**/*.test.ts',应只命中position.zod.ts、index.ts、builtin-positions.ts。对照:同一命令换成EVERYONE_POSITION应多命中suggested-audience-bindings.ts。
八问 × 选项 × 真实代价(每问荐 A)
问 A(推荐) 另一选项 客户感受到的后果 Q1 归属 guest 永不拥有记录;它写入的记录归一个声明好的系统默认 owner B:guest 当 owner A:公开表单进来的线索有明确负责人(Salesforce 也是改派给默认 owner)。B:出现没人能管的"访客的记录",违反 ADR-0131 的归属全称 Q2 开放入口 闭合列表 = 现有五类:公开表单提交、分享链接、公开 book 的元数据读、 authRequired: false的 object_operation 与 flow 端点;其余一律 401,按域整体拦而不是逐个面拦B:任何声明 authRequired: false的面都向 guest 开放A:新加一个面默认就是 401,不会悄悄多出一扇门。B:每多一个面就可能多一扇没人审的门 Q3 授权通道 让 D9 生效:匿名请求时解析 guest 锚点上的权限集绑定,没绑定就全拒 B:维持只声明不兑现;C:退役 guest 锚点绑定 A:管理员给访客授的权真的起作用,门户、公开目录才做得出来。B:管理员继续被误导。C:guest 需求整体做不了 Q4 组织 只有能唯一确定组织时才解析(单组织部署就是那一个组织),多组织部署拒绝,等 Q5 的站点绑定声明 B:一律落默认组织;C:现在就做按站点映射 A:多组织部署不会把访客数据写进别的租户。B:多组织部署可能串租户。C:提前造能力 Q5 公开站点绑定 ADR 里定形状(站点 → 组织 → guest 权限集 → 允许的入口),标为先实现后声明,等真实需求再建 B:现在就建 site元数据类型A:不为还没人要的场景造类型,形状先定好免得以后各写各的。今天 hotcrm 和 showcase 都是单组织表单入口,零站点需求 Q6 披露与解释 不变(ADR-0106 D7;explain 把 guest 判为 EXTERNAL) — 无变化 Q7 防滥用 ADR-0121 D6 不变;webhook 签名词汇(HMAC、时间戳、防重放窗)连同执行器另立卡 B:本 ADR 一并纳入 A:不声明没有执行器的键。今天 webhook 走的是独立的 trigger-api 通道,已有 HMAC Q8 已声明的 guest 键 锚点与主体分类保留(已生效);绑定建议随 Q3 生效; sys_record_share的guest接收方(声明了但授予时被拒)按 ADR-0049 退役或兑现,另走一张卡B:本 ADR 一并改 A:每个键都有明确去留,不留"能选但会被拒"的选项误导 AI G 测量缺口 G2:先按静态读数裁;启动后逐类实测匿名入口列为 ADR 第三轮的验收前提 G1:先在你允许的环境里补测,再裁 G2:不卡你的裁决;静态读数显示今天每类入口都是拒绝或只有窄口,失败方向是安全的。G1:多一轮等待 业务含义直译
- Q1 A 等于"访客交上来的单子,一定有个内部负责人"。
- Q2 A 等于"大门清单写死,想开新门得改清单"。
- Q3 A 等于"门禁卡发给访客才算数,没发就进不来"。
- Q4 A 等于"只有一家店时访客自然进这家;连锁店先不让进,等挂了店招(Q5)再进"。
四棱(四轴从业务立场)
- ① 项目长远合理性: A 整包缩小特例。七处分散声明收进一份 ADR;"声明了不兑现"的 D9 变成兑现;代码注释里的 Option A 进入 ADR。两年后的平台样子与五家主流平台同形:一个匿名主体、同一管线、默认全拒、显式授权、站点绑定。
- ② 实际业务拉动: 门户首页、公开表单、公开目录、凭号查询、合作方 webhook。hotcrm 的线索与工单表单今天就在用公开表单入口。多组织站点绑定今天零拉动,所以 Q5 只定形状不建。
- ③ 防 AI 犯错: 闭合入口清单加按域拦截,新面默认 401;空授权集响亮拒绝;多组织歧义时拒绝而不是挑一个。AI 写不出"悄悄对访客开放"的应用。B 类选项(开放清单、默认组织)都是静默失败方向。
- ④ 创业阶段不扩散: 不新建元数据类型(Q5 延后);不新增门禁;沿用现有职位与权限集机制;已声明零兑现的
sys_record_shareguest接收方走退役或兑现。
Prior rulings read: guest, anonymous principal, audience anchor, guest permission set → 4 hits (#22146, #21158 closed and folded in, #22147 ruled C, #17971 unrelated); ADR-0090 D9/D10, ADR-0056 D2, ADR-0106 D7, ADR-0121 D6, ADR-0131; the 2026-08-08 Option A ruling (code comment
f586f1a89); thread: 2 comments (this seat's claim and the round-1 report).推荐:22146 A G2。 自检:只看①选 A。②③④ 是否翻转:否(②只把 Q5 推迟建设,不改字母)。
置信缺口: 启动后的逐类匿名入口实测没做(运行环境的安全检查拦下了未认证探测脚本的编写);cloud 仓的匿名面没读。裁后执行
- 本席认领第三轮:按裁定写 ADR 草稿(
docs/adr/**,Tier H),走授权批准或你亲手合并。 - ADR 接受后,按裁定拆执行卡进 v18:Q3 授权通道兑现;Q2 入口清单与按域拦截;Q1 默认 owner;Q8 的
sys_record_shareguest退役或兑现。每张带 pin 与 ADR-0087 处置。 - 选 G2 时,ADR 的验收条件写入"启动后逐类匿名入口实测",在允许该探测的环境里执行。
- ⛔ ADR 接受前不改
packages/spec。
- Round terms met: branch
27 remaining items
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsRuling: batch #300 item 1 · letter A (G2 result: close the three doors, then accept) · maintainer 「同意」 2026-10-09T05:32Z
Director seat, summon #35,
session_01VYToj6PQehTEKNrjGM9akg(GitHubos-zhuang; written asobjectstack-fleet[bot]via the relay). Presented in batch #300 from thedomain:specseat 1's decision request (6074404262) on the G2 measurement (6074331185, ACCEPTED by the seat): on a booted reference deployment, D2's five classes answer as the ADR-0138 table says, and the everything-else class does not: three surface families answer an anonymous caller with 2xx, so acceptance criterion 2 is not met. A close the three doors, then accept; B accept now and close them under E2; C as A without public cards. The seat recommended A with C as the fallback; this seat recommended A with one added condition; the maintainer answered 「同意」. Thread-read: 6074404262. Freshness: body unchanged since the two earlierRuled:lines; no comment since the presentation; labelspriority:p1,security,needs-user-decision,domain:spec,target:v18,area:access. Premises re-read onorigin/main27a8b33dec:packages/runtime/src/domains/i18n.tscallsshouldDenyAnonymousin 0 files while the other eight dispatcher domains (actions,ai,analytics,auth,automation,meta,packages,security) do;registerOpenApiEndpointsis defined atpackages/rest/src/rest-server.ts:5095;packages/services/service-storage/src/storage-routes.ts:226calls download gating "a tracked follow-up". Added by this seat on objectuimain8f8f760fa4:apps/console/src/main.tsx:158handsloadLanguageandloadLocalesto theI18nProviderat application boot, before sign-in;loadLanguage.ts:19andloadLocales.ts:23fetch/api/v1/i18n/translations/:langand/api/v1/i18n/localeswith no Authorization header and degrade to the built-in packs on any failure. That closes the request's first confidence gap: the Console does read the/i18ndomain anonymously today.The ruling
A — close the three doors, then accept. The spec seat files three defect cards, public and at function level, for triage to route:
- The
/i18ndispatcher domain gains the domain-level anonymous refusal every other dispatcher domain has (ADR-0056 D2; 401), with an objectui companion: the Console serves its sign-in page from the built-in packs and loads the application's translations after sign-in. The dev measures first whether the Console re-fetches translations once signed in today, and sizes the companion from that reading; the two land together so no signed-in user sees raw keys. - The API-description endpoints refuse an anonymous caller (401).
- Storage download of a file with no scope and no field owner requires a signed-in caller; a file declared
acl: 'public_read'stays anonymous (ADR-0104).
ADR-0138 D2's closed list gains a sixth class, the
public_readfile download, as a record revision (Tier H; the maintainer approves that PR). When the three cards have landed, the everything-else class is re-measured on a booted reference deployment; when it answers 401 throughout, the acceptance PR opens, with acceptance criterion 4's two back-references. ADR-0138 stays Proposed until then. ⛔ Not taken: B (an Accepted record with a known, measured gap empties the word, and sets the precedent for the next record), C (the code is open source and the three families read from it, so function-level cards add little exposure and keep the fixes in the ordinary queue). Disclosure: the route-level readings stay in the seat's container; this record and the three cards stay at function level, as the card body allows.Prior rulings read: 6054113537 (A on the eight questions and G2 as the gap) and 6056614963 (D1 A′ D2b R), both on this card, neither ruling on these families; ADR-0138 D2 and acceptance criterion 2; ADR-0056 D2 (default-deny for anonymous) and D9; ADR-0104 (the anonymous capability URL demoted to the opt-in
acl: 'public_read'); ADR-0090 D9; ADR-0049.check-prior-rulingsover 8 terms → 23 ADR hits (ADR-0056 D1 / D2 / D9, ADR-0023 §1–§5 on the OpenAPI connector, ADR-0029 D8, ADR-0036 §2 among them), none ruling the anonymous answer of these three families; thread: 2 rulings of 16 comments. 自检: 只看①选 A;②③④ 是否翻转:否(C 只改公开方式,不改方向)。置信缺口:Console 登录后是否重拉应用翻译未测(决定 objectui 配套的大小);API 文档是否有匿名的真实使用者(公开 API 门户)未测;旧「未归属上传」链接受影响的范围未测。State
needs-user-decision→pm:queue(domain:spec,priority:p1,security,target:v18,area:access) in this act; a thirdRuled:line added above the two earlier ones. The spec seat files the three defect cards and drafts the D2 revision; this card stays open for the re-measurement and the acceptance PR. The empty branchclaude/issue-22146-g2-sweepmay be deleted.
Generated by Claude Code
- The
- added a commit that references this issue
on Oct 9, 2026 objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 (#22146 round 4: the ADR-0138 D2 record revision under the ruling
6074960686, letter A) · 2026-10-09T06:33Z
Session:session_01VZqqwTj2wsihZEbfT6yyYN
Account:os-tesla(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-22146-d2-public-read-class
Worktree:objectstack-issue-22146-d2
Domain:domain:spec
Seat:domain:spec#1
File surface:docs/adr/0138-guest-model-anonymous-principal-doors-grants-and-organization.mdonly (stop on breach and explain in the report). The record stays Proposed.- D2's closed list gains a sixth class, the
acl: 'public_read'file download (ADR-0104), with its admission, what it runs as and its enforcer, and every place the record counts "five" classes or names classes by number (onorigin/main27a8b33dec: the decision table:63, the ruling summary:74, D2:215and the adoption rule:244–:245, the enforcement map:487, acceptance criterion 2:578, the ADR-0090 back-pointer text:592, E2:607— re-read on the tree; this list is a lead, not a spec). - The G2 result and the ruling are recorded at class level: classes 1–5 match, the everything-else class did not (three families, named at function level as the ruling and runtime: the
/i18ndispatcher domain answers an anonymous caller —handleI18nRequestmakes noshouldDenyAnonymouscall, unlike every other dispatcher domain (ADR-0056 D2); with an objectui companion for the Console sign-in page #22432 / rest: the API-description endpoints (RestServer.registerOpenApiEndpoints) answer an anonymous caller with the whole object model; ADR-0056 D2 and ADR-0138 D2 require 401 #22430 / service-storage: a file with no scope and no field owner is downloadable by an anonymous caller who has its id; the download should require a signed-in caller, whileacl: 'public_read'stays anonymous (ADR-0104) #22431 name them), and acceptance waits on those three cards and a re-measurement. - ⛔ No code, no other record, no back-pointer lines (criterion 4 lands with the accepting change, not here).
Container & model:S(one record),mode:subagent,model: default tier.docs/adr/**is a Tier H governed surface: the PR stays draft with the four-part final set (速读 draft by the dev, the seat's final, reviews requested fromos-zhuangandhotlong), and an at-tier contract review (CONTRACT_REVIEW_TIER, governed rule text) is owed before the maintainer's approval is asked for.
Clause-②: no (a record revision; no accept set or public surface moves)
Responsibility:n/a — not a defect card
Thread-read: 6074960686
Serial constraints cleared: no open PR touchesdocs/adr/0138-*.md(read 2026-10-09T06:33Z). The three defect cards it names (runtime: the/i18ndispatcher domain answers an anonymous caller —handleI18nRequestmakes noshouldDenyAnonymouscall, unlike every other dispatcher domain (ADR-0056 D2); with an objectui companion for the Console sign-in page #22432, rest: the API-description endpoints (RestServer.registerOpenApiEndpoints) answer an anonymous caller with the whole object model; ADR-0056 D2 and ADR-0138 D2 require 401 #22430, service-storage: a file with no scope and no field owner is downloadable by an anonymous caller who has its id; the download should require a signed-in caller, whileacl: 'public_read'stays anonymous (ADR-0104) #22431) are filed and unassigned; this revision does not wait on them.
Ruling:
6074960686(batch #300 item 1, letter A, maintainer 「同意」): "ADR-0138 D2's closed list gains a sixth class, thepublic_readfile download, as a record revision (Tier H; the maintainer approves that PR). When the three cards have landed, the everything-else class is re-measured on a booted reference deployment; when it answers 401 throughout, the acceptance PR opens, with acceptance criterion 4's two back-references. ADR-0138 stays Proposed until then."The three defect cards filed under the ruling: #22432 (the
/i18ndispatcher domain, with its objectui companion), #22430 (the API-description endpoints), #22431 (storage download of a file with no scope and no field owner). After this round's PR, the card carriesBlocked-by:on all three for the re-measurement.This act moves the card
pm:queue→pm:dispatchedand assignsos-tesla.- D2's closed list gains a sixth class, the
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 22146, "status": "done", "branch": "claude/issue-22146-d2-public-read-class", "pr": "https://github.com/objectstack-ai/objectstack/pull/22433", "session": "session_01VZqqwTj2wsihZEbfT6yyYN", "premise_still_valid": true, "summary": "Round 4 delivered as draft PR #22433 (Tier H, stays draft; one file, +121 / -37; Status stays Proposed): the ADR-0138 record revision under the G2 ruling 6074960686. D2's closed list gains class 6, the acl: 'public_read' file download (ADR-0104 D3 wave 2), in the existing columns: admitted by the file's own acl: 'public_read' (packages/services/service-storage/src/objects/system-file.object.ts#acl), running as a read of that one file's bytes with no principal evaluated, enforced in packages/services/service-storage/src/storage-routes.ts#registerStorageRoutes at its download gate authorizeDownload (a closure both download routes call; read on origin/main e02833c240). The adoption rule (classes 4 and 5 take the guest envelope; 1 to 3 and 6 their own declaration), every count of the door classes and the quoted ADR-0056 back-pointer count follow; the G2 result is recorded at class level under acceptance criterion 2 (classes 1-5 match; the public_read download was served as ADR-0104 rules and is now class 6; the everything-else class did not match: handleI18nRequest's /i18n domain, RestServer.registerOpenApiEndpoints, storage download of a file with no scope and no field owner) with the ruling's acceptance sentence quoted verbatim, and acceptance waits on #22432, #22430, #22431 and a re-measurement. Premises re-read on e02833c240 (5 commits past 27a8b33dec, none touching the record or the three families' files): i18n.ts still makes no shouldDenyAnonymous call. No code, no other record, no back-pointer lines; the card's assignee was not written.", "tests": "All at HEAD 024872cb90. Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (stderr: tree of objectstack-ai/objectstack at 024872cb90, --repo holds; 1 path, +121/-37 vs merge base e02833c24): 19 commands, all exit 0, each exit captured before any pipe (list under gates). check-adr-symbol-anchors: '2232 anchors across 141 records resolve'; positive control: the shared extractor reads 57 anchors in this record at base and 63 at head, the +6 (system-file.object.ts#acl, storage-routes.ts#registerStorageRoutes x2, the #authorizeDownload continuation, i18n.ts#handleI18nRequest, rest-server.ts#RestServer.registerOpenApiEndpoints) each resolving as a declaration via symbolSegmentResolution; negative control: authorizeDownloadNope reads absent. check-adr-links: '715 relative link destination(s) under docs/adr/ resolve'. check:doc-formula-expressions: first run exit 3 = PREREQUISITE NOT MET (@objectstack/formula unbuilt in the fresh worktree), not a measurement; built formula+lint under os-verify-lock (slot issue-22146-d2; 'VERDICT command-exit 0', held 104s, waited 71s; turbo 4/4), rerun exit 0. check:nul-bytes OK; separate control-byte grep on the record and the PR body: exit 1 (none). --ran reconciliation with recorded exit codes: '19 derived, 19 run, 0 NOT-MEASURED, 0 UNRUN'. check-issue-citations (dispatch-named, not in the derived set): exit 0 but '0 file(s) read' because docs/adr/** is a deferred surface, so NOT MEASURED for this diff; instead the three cited cards were read by REST GET: #22430, #22431, #22432 are open issues, not pull requests. No ablation (no code). CI: in_progress, not awaited.", "gates": [ "node scripts/check-adr-links.mjs :: 0", "node scripts/check-adr-links.mjs --self-test :: 0", "node scripts/check-adr-symbol-anchors.mjs :: 0", "node scripts/check-adr-symbol-anchors.mjs --self-test :: 0", "node scripts/check-ci-filter-parity.mjs :: 0", "node scripts/check-closing-keyword-parity.mjs :: 0", "node scripts/check-closing-keyword-parity.mjs --self-test :: 0", "node scripts/check-comment-mask-corpus.mjs :: 0", "pnpm check:adr-anchors :: 0", "pnpm check:cross-package-test-inputs :: 0", "pnpm check:doc-authoring :: 0", "pnpm check:driver-memory-census :: 0", "pnpm check:gitlink-declared :: 0", "pnpm check:nul-bytes :: 0", "pnpm check:pm-governed-merges :: 0", "pnpm check:pm-prior-rulings :: 0", "pnpm check:refd-timer-probe :: 0", "pnpm check:watch-hint-literal :: 0", "pnpm --filter @objectstack/lint run check:doc-formula-expressions :: 0", "(dispatch-named, outside the derived 19) node scripts/check-issue-citations.mjs :: 0, vacuous: 0 files read (docs/adr/** deferred)" ], "mcp_calls": "0", "api_writes": "3 REST writes, all through scripts/pm via the fleet-write relay (each a POST /repos/objectstack-ai/objectstack/dispatches, HTTP 204, run success, executed as objectstack-fleet[bot]): (1) pr_create via with-fleet.sh --via dispatch → POST /repos/objectstack-ai/objectstack/pulls (#22433, draft forced; body read back byte-identical, 12299 bytes, footer intact); (2) label-write.mjs --issue 22433 --add skip-changeset --assign os-tesla → POST /repos/objectstack-ai/objectstack/issues/22433/labels + POST /repos/objectstack-ai/objectstack/issues/22433/assignees (read-back matches: documentation, size/m, skip-changeset, assignee os-tesla; documentation and size/m added by the labelers, preserved; final re-read identical); (3) this os-dev-report via post-stamped.mjs → POST /repos/objectstack-ai/objectstack/issues/22146/comments. Plus git push x2 (not REST): the empty-branch claim marker at e02833c240 and the revision 024872cb90. No reviewer requested, no MCP call, no card-assignee write. Reads: the card and all 18 comments (one page), the three defect cards and the PR, by REST GET.", "files_changed": [ "docs/adr/0138-guest-model-anonymous-principal-doors-grants-and-organization.md" ], "count_sites_changed": [ "TL;DR decision table D2 row :63 → :77: 'five door classes' → 'six door classes'; Ruled cell + 'class 6 added by the G2 ruling'", "D2 intro :215 → :238: 'exactly these five door classes' → 'six'; the Ruled line names 6074960686", "D2 table: row 6 added (:248)", "D2 adoption rule :244-:245 → :268-:270: 'classes 1 to 3 take their own declared grant' → 'classes 1 to 3 and 6 take their own declaration'", "D2 Enforced by :249 → :274 (NOT in the dispatch's lead list): 'Card E2 records the five classes' → 'six'", "Enforcement map :487 → :519: 'the five door classes' → 'the six door classes'", "Acceptance criterion 2 :578 → :624: 'each of D2's five classes' → 'six', plus the measured result and what remains", "Criterion 4 quoted ADR-0056 back-pointer text :592 → :665: 'closed list of five door classes' → 'six' (the count inside the quote only; the back-pointer itself not landed)", "E2 :607 → :682: 'the five classes recorded in the conformance matrix' → 'six'" ], "count_sites_left": [ ":74 → :88 the first ruling's summary, verbatim quote of 6054113537 ('the closed list of five door classes'): left, because editing it would rewrite a ruling; a new paragraph under it (:101-:106) records the G2 ruling and says why the quote still says five", "D1 point 1 '(D2, classes 4 and 5)' :207, D3 point 1 same :330, D8 'D2 class 1' :449, D2b 'D2's class 5 row' :504, E1 'denied on class 4' :681: left, class 6 is appended so classes 1-5 keep their numbers", "D2 'except at the two endpoint classes above' :254: left, still two (class 6 takes no guest envelope)", "Context table 're-measured at 7b926f7600' :122: left, a dated reading", "Alternatives G1 'every door class reads as a denial or a narrow door': left, the recorded reasoning for a letter not taken", "Criterion 1: left as the record has it (unmarked); see open_questions" ], "deviations": [ "Status line: the record's '⛔ Nothing in packages/** ... changes before acceptance' would contradict the ruling, which lands three defect cards before acceptance; reworded to 'no change this record decides lands in packages/** ... before acceptance', plus one sentence that the defect cards land first because they enforce ADR-0056 D2 and ADR-0104, both Accepted. A matching sentence was added under the Execution plan ('not execution cards of this record').", "Consistency edits beyond the dispatch's lead list, each forced by class 6 or the G2 result and named in the PR body: Decided by (+ the G2 ruling), Builds on (+ ADR-0104 D3 wave 2), Leaves unchanged (+ ADR-0104), Consumers (+ @objectstack/service-storage), Card line, the TL;DR note under the verbatim summary, the Context G2 bullet's pointer, a D2 'Measured, and not yet held' paragraph, the enforcement map's default-deny row status (names the three G2 exceptions), two Consequences bullets, Alternatives (+ G2-result B and C in the ruling's words), criterion 4's 'No line is added to' (+ ADR-0104), References (+ 6074331185, 6074404262, 6074960686 and the three cards).", "The dispatch's lead :74 (the ruling summary) was deliberately not edited: it is a verbatim quote of 6054113537; annotated instead (count_sites_left).", "check-issue-citations was named by the dispatch but is outside the derived 19; run anyway, it read 0 files (docs/adr/** is one of its deferred surfaces), so recorded as vacuous, with the three citations checked by REST instead.", "Worktree base is origin/main e02833c240, 5 commits past the dispatch's 27a8b33dec; none touch the record (byte-identical at both) or the three families' files, so the dispatch's line leads hold.", "Attribution: the commit carries the AGENTS.md model-free trailer pair (Claude-Session + Co-authored-by: Claude) and the PR body ends with the AGENTS.md session-URL footer, not the harness reminder's model-named trailer or its footer (os-dev.md: the harness reminder yields).", "Cleanup: worktree node_modules removed and git worktree remove (no --force) run after the PR and labels, before this report; the remote branch head 024872cb90 equals the local head at removal." ], "open_questions": [ { "question": "Should the post-card re-measurement also read class 6? The ruling names only the everything-else class for the re-measurement, but criterion 2 now names six classes, class 6's G2 reading is the observation that public_read files were served anonymously, and #22431 edits the very gate (authorizeDownload) that class 6 passes through.", "options": [ "A: the re-measurement reads class 6 beside the everything-else class (one more class-level line; no record change, criterion 2 already requires six)", "B: re-measure the everything-else class only, as the ruling words it, and rely on #22431's own pin that a public_read file stays anonymous" ], "recommendation": "A, because the change most likely to move class 6 is #22431 itself and a class-level line costs nothing; it is the seat's call, so the record does not add it." }, { "question": "Criterion 1 (the maintainer's approval): the record still says only 'the third is met', while the landing comment 6058198880 read criterion 1 as met by the approval on PR #22239. This revision is itself a Tier H approval.", "options": [ "A: leave the record's wording; the accepting change states criterion 1 met", "B: the seat marks criterion 1 met in this PR before asking for approval" ], "recommendation": "A: the revision did not touch criterion 1's wording, and the accepting PR is where all four are stated met together." } ], "out_of_scope_findings": [ "carrier: #22431 · noted, not filed — the acl: 'public_read' opt-in has no in-repo producer: both upload routes in registerStorageRoutes create sys_file rows as private, copy-on-claim copies the source row's acl, and FieldSchema declares no file acl, so ADR-0104's 'the field/file explicitly declares' exists only on the file-row side (the sys_file.acl select). Not a defect under ADR-0104 (an opt-in), but it bears on #22431: once an unclaimed upload such as an organization logo requires sign-in, nothing in the repository marks it public_read. Recorded in PR #22433's Acceptance notes; #22431's acceptance already asks its dev to measure the in-repo producers of unclaimed uploads." ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actions✅ ACCEPT — PR #22433 at
024872cb90(round 4: ADR-0138 D2 class 6). Tier H: waits on the maintainer's approvaldomain:specseat 1 (#6017) ·os-tesla· sessionsession_01VZqqwTj2wsihZEbfT6yyYN· 2026-10-09T07:07Z · holder of claim6075714215. Report:os-dev-report6076046095.Checked on GitHub and in the diff, not from the report:
- Shape: draft, base
main; line 1Part of #22146(the card stays open), line 2Clause-②: no; one file,docs/adr/0138-guest-model-anonymous-principal-doors-grants-and-organization.md, +121 / −37;skip-changeset; assigneeos-tesla; the## 维护者速读(草稿)section is present. - The revision, read in the diff: D2 gains class 6, the
acl: 'public_read'file download, with its admission (system-file.object.ts#acl), what it runs as (that one file's bytes, no principal evaluated) and its enforcer (registerStorageRoutes'authorizeDownload); the adoption rule reads "classes 1 to 3 and 6 take their own declaration"; every door-class count reads six, and the one remaining "five" is the verbatim summary of ruling6054113537, annotated beneath it; criterion 2 records the G2 result at class level, the three cards, and the ruling's acceptance sentence verbatim; Status stays Proposed. Readings stay at function level. - CI on
024872cb90: 35 runs, 24 success and 11 skipped;check-expected-skips --pr 22433: "OK — 11 skipped check-run(s), every one in the roster". - Governed surface:
check-governed-merges --pr 22433:docs/adr/**, landing tier H — an authorized APPROVED review, then the owning seat lands it; 158 changed lines. - Contract review: at-tier, PASS on
024872cb90(6076189164).
The dev's open questions, decided by the seat:
- The post-card re-measurement reads class 6 as well as the everything-else class (A). Criterion 2 as revised requires all six classes to match, and service-storage: a file with no scope and no field owner is downloadable by an anonymous caller who has its id; the download should require a signed-in caller, while
acl: 'public_read'stays anonymous (ADR-0104) #22431 edits the gate class 6 passes through. No record change is needed. - Criterion 1's wording stays as it is (A); the acceptance PR states all four criteria met together.
The
public_readproducer gap (no in-repo producer marks a filepublic_read) is carried to #22431 (6076074763).Next: the final 速读 on the PR,
needs-user-decisionon the PR, and review requests toos-zhuangandhotlong. After the approval, the seat lands the PR through the queue; the card then waits,pm:blocked, on #22432, #22430 and #22431 for the re-measurement.- Shape: draft, base
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsLanded: round 4, PR #22433 →
05c7c3fa3b(ADR-0138 D2 gains class 6; the G2 result recorded). The card waits on the three defect cards:pm:dispatched→pm:blockeddomain:specseat 1 (#6017) ·os-tesla· sessionsession_01VZqqwTj2wsihZEbfT6yyYN· 2026-10-09T07:40Z · holder of claim6075714215, released by this act.- Landed: merged through the merge queue as
05c7c3fa3b(one parent,46692c118b), an ancestor oforigin/main.docs/adr/0138-guest-model-anonymous-principal-doors-grants-and-organization.mdis blob-equal to the approved head024872cb90. - The review record: Tier H —
os-zhuang's APPROVED review on024872cb90(2026-10-09T07:24Z); the seat's ACCEPT6076202284; at-tier contract review PASS6076189164; the final 维护者速读6076213418. - What now holds on
main: ADR-0138, still Proposed. D2's closed list has six classes (class 6, theacl: 'public_read'file download, ADR-0104). Criterion 2 records the G2 result at class level and what remains.
What acceptance still needs (ruling
6074960686):- The three defect cards land: runtime: the
/i18ndispatcher domain answers an anonymous caller —handleI18nRequestmakes noshouldDenyAnonymouscall, unlike every other dispatcher domain (ADR-0056 D2); with an objectui companion for the Console sign-in page #22432 (the/i18ndispatcher domain, with its objectui companion), rest: the API-description endpoints (RestServer.registerOpenApiEndpoints) answer an anonymous caller with the whole object model; ADR-0056 D2 and ADR-0138 D2 require 401 #22430 (the API-description endpoints), service-storage: a file with no scope and no field owner is downloadable by an anonymous caller who has its id; the download should require a signed-in caller, whileacl: 'public_read'stays anonymous (ADR-0104) #22431 (storage download of a file with no scope and no field owner). They are filed for triage to route. - The re-measurement on a booted reference deployment: the everything-else class, and class 6 as well (the seat's decision in
6076202284, since service-storage: a file with no scope and no field owner is downloadable by an anonymous caller who has its id; the download should require a signed-in caller, whileacl: 'public_read'stays anonymous (ADR-0104) #22431 edits the gate class 6 passes through). - When every class answers as D2 says, the acceptance PR (Status → Accepted, criterion 4's back-pointers), Tier H.
Blocked-by:lines for the three cards are in the body. When the last one closes, the unblock sweep returns this card, and the spec seat re-claims it for the re-measurement.Release:
session_01VZqqwTj2wsihZEbfT6yyYN· why: round 4 is delivered and landed; what remains waits on three cards in other lanes · to:pm:blocked, unassigned. This act moves the cardpm:dispatched→pm:blockedand removes the assigneeos-tesla.- Landed: merged through the merge queue as
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsPointer from the
domain:cliseat: ruling6074960686item 2 has landed (#22430 → PR #22446 →166a94f75d). · seatdomain:cli#1(#6024) ·session_01BmsuLyUeuG5CNpZFMH1jzS· 2026-10-09T11:51Z. ⛔ Not a claim, ⛔ not a request for work. Function level.- What landed: the API-description endpoints that
RestServer.registerOpenApiEndpointsmounts (the document and its viewer) refuse an anonymous caller with the shared401 UNAUTHENTICATED. The authorization matrix classifies the family (anonymous-deny-api-description), and the population baseline reads 31. The changeset declaresClause-②: no (narrowing),minor, as PR fix(service-storage)!: downloading a file with no attachments scope and no field owner requires a signed-in caller #22439 (item 3) did. - One open observation for the everything-else re-measurement, recorded in PR fix(rest): the API-description endpoints refuse an anonymous caller (#22430) #22446's Acceptance notes and NOT measured:
- On the environment-scoped twin of these two routes, a signed-in caller is judged by the auth service of the environment the URL names.
- The two handlers add no environment-ownership comparison of the kind the UI-view route makes.
- Item 2 covers anonymous callers only, and the reference boot mounts no scoped base, so neither the fix nor its proof reached this.
- The seat leaves it here for whoever re-measures D2's everything-else class, instead of filing a card with no measured reach.
- Item 1 (runtime: the
/i18ndispatcher domain answers an anonymous caller —handleI18nRequestmakes noshouldDenyAnonymouscall, unlike every other dispatcher domain (ADR-0056 D2); with an objectui companion for the Console sign-in page #22432,/i18nwith the objectui companion, now landed on objectuimain) is next in this seat's lane.
Generated by Claude Code
- What landed: the API-description endpoints that
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 anonymousobject_operationposture). The maintainer asked 「guest 是不是应该完整的重新设计并开adr」, then stated the demand 「最为一个元数据开发平台,guest 是常见的需求吧?」, and answered the seat's proposal (adomain:specdesign 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:specseat, 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:
origin/mainpackages/core/src/security/anonymous-deny.ts(#2567)!userId && !isSystem → 401decisionguestbuiltin position, held implicitly and exclusively by unauthenticated principals; theeveryone/guestaudience anchors packages suggest and never own; the human / agent / guest principal taxonomyguestanchor resolves nothing (#21158, closednot_plannedon 2026-10-04 for want of demand; its question is folded into this card)f586f1a89)assembleExecutionContext(fail-closed, 401) andassembleExecutionContextOrGuest(a first-class guest envelope), the latter adopted only by surfaces that genuinely serve anonymous principalspackages/core/src/security/assemble-execution-context.tsnear:30and:386, in no ADRauthRequired: falseendpoint must carry an armedrateLimit; webhook signature keys named as a future vocabulary, not promisedpackages/metadata-core/src/anonymous-form-intake.ts(#21967, #21980, #21331)object_operationpathundefined(packages/runtime/src/endpoint-executor.ts:95)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
guestuser 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 (theanonrole 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)
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.authRequired: falseendpoints of typeflow,object_operationreads, anything else; every other surface answers 401.guestanchor's bindings become enforced (the security(core, plugin-security): an unauthenticated principal never resolves the permission sets bound to theguestanchor; 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).EXTERNALposture stand unchanged.GUEST_POSITION,AUDIENCE_ANCHOR_POSITIONS(packages/spec/src/identity/position.zod.ts:150), theguestvalue ofsys_audience_binding_suggestion, the two named context entries, the fallback permission set's guest reading.Process
GUEST_POSITION,'guest'andprincipalKind === '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 theguestanchor; ADR-0090 D9 is declared and seeded but not enforced #21158's did.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.packages/specbefore 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