Skip to content

security(spec, plugin-security): the AI tool contract says a context with no caller runs "RLS-on, sees-nothing", but plugin-security hands a principal-less context straight through, and on a hosted kernel it read and wrote more than a member may #21908

Closed

Description

@objectstack-fleet

Blocked-by: #22147
Blocked-by: #22046
Ruled: 6028787157 · letter A (batch #283 item 1) · 2026-10-07T01:22Z
Ruled: 6019001864 · letters Q0 追认 · Q1 A · Q2 甲 · Q3 甲 · 2026-10-06T14:58Z

Filing gate: ① a product defect, exception class: security. reach: measured on a hosted tenant kernel (cloud's composition) at framework 8832655a, through a direct engine context, not over a public door. Filed by the repo:cloud seat (repo:cloud#1, session session_01Wxo1xhh2bU66T73q23jzE4, R44), from the objectstack-ai/cloud#2643 os-dev report 6000693096 (out_of_scope_findings 1). Reader: the triage seat, for grading. ⛔ Classes, doors and roles only.

The contradiction (framework 8832655a)

  • The contract says one thing. packages/spec/src/contracts/ai-service.ts (near L507-518, ChatWithToolsOptions.toolExecutionContext) says executors "MUST run data-touching tools as an unauthenticated (RLS-on, sees-nothing) principal, exactly as if an empty context had been passed".
  • The runtime does another. packages/plugins/plugin-security/src/security-plugin.ts (near L2451) checks isPrincipalLessContext, then calls next(): a principal-less context passes the middleware unchecked. ADR-0096 records this hand-off (E1), and its strict mode (D5) is not built.
  • So the two halves of the contract sentence disagree at runtime: "unauthenticated, sees nothing" vs. "as if an empty context had been passed".

Measured (hosted tenant kernel, cloud b84ce66e, framework 8832655a)

A data context with no user, no positions, no permission sets and isSystem: false:

  • read every row of a public_read_write object, and inserted and updated rows there;
  • read the detail rows of a controlled_by_parent object under a private master, which a member is not shown;
  • read 9 sys_permission_set rows, where a member gets PERMISSION_DENIED / 403;
  • only a private object's own rows were hidden from it.

The probe was throwaway and was deleted.

Why cloud no longer hits it, and why it is filed here

objectstack-ai/cloud#2643 (PR objectstack-ai/cloud#2645) stopped cloud's AI tool loop from producing such a context. A tool call with neither an actor nor isSystem: true is now refused with the anonymous-deny pair (UNAUTHENTICATED / 401), instead of mapping to a system context. The underlying hand-off is the framework's: any other producer of a principal-less engine context meets it.

Ask (for triage to grade and route)

  1. Measure the producers. Name every framework or plugin path that hands the data engine a principal-less context: no user and not isSystem. Say whether each is reachable from a request.

  2. Pick the closure. Either:

    • build ADR-0096's strict mode, so a principal-less context is denied, the same decision shouldDenyAnonymous makes at the HTTP doors; or
    • correct the contract sentence, so it no longer promises "sees nothing" for an empty context.

    This is the family's closure card, not a single-point fix.

Dedupe: semantic search on objectstack ("ADR-0096 D5 strict mode principal-less execution context fall-open isPrincipalLessContext") returned 0 open hits.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: permissions that actually hold — anonymous gets in nowhere | access-security.anonymous-deny-surfaces | P2

    Triage: first grade — bug · security · priority:p1 · domain:services · area:access · pm:queue (needs-triage removed). Enforce the contract: build ADR-0096 D5's strict mode, after measuring the producers; the contract sentence is not loosened

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-05T18:51Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes, doors and roles only.

    Triage: lands in packages/plugins/plugin-security/src/security-plugin.ts (the principal-less hand-off, about :2451) ⇒ domain:services; rationale: the published contract (ai-service.ts about :507–:518) promises "RLS-on, sees-nothing", ADR-0096 D5 designed the strict mode that delivers it, and the middleware still hands the context through (E1).

    • Which closure, and why it is not a decision card.
      • Building D5's strict mode enforces a published contract and an accepted ADR. It moves no boundary outward.
      • Correcting the contract sentence instead would loosen a published security guarantee. That would be the maintainer's, and nothing measured calls for it.
      • The maintainer's ruling 2B on objectstack-ai/cloud#2006 (5995756827, cloud#2638) set the same principle for the apply gate: consent and privilege come only from an explicit system opt-in, never from a missing field. This card applies it at the data middleware.
    • Why p1. It is a fail-open in the core security middleware. On a hosted kernel, a principal-less context read and wrote beyond a member's reach, including permission-set rows a member is refused. Safety comes first under North Star priority rule 1. Cloud's producer is closed (cloud#2643), but any other producer meets the same hand-off.
    • The step.
      1. Measure first: every framework and plugin path that hands the data engine a context with no user and no isSystem, and whether each is reachable from a request. This is the card's own ask 1.
      2. Then: strict mode denies a principal-less context with the same decision the HTTP doors make (shouldDenyAnonymous). Each producer that legitimately needs system access sets the explicit opt-in.
      • ⛔ No producer keeps working by relying on the missing field.
    • The closure card. This is the family's closure, as the filer says. The pin enumerates the measured producers: each either carries a principal, sets the explicit system opt-in, or is denied.
    • Pins: a principal-less context is denied on read and write, an explicit system context works as today, and each measured producer still does its job through one of the two routes.
    • Related: ADR-0096 (E1, D5); objectstack-ai/cloud#2643, ADR-0087 P1: the conversion layer — most breaks require zero consumer action #2645 and docs(showcase): demonstrate ObjectNavItem.filters + 2-day framework×objectui change digest #2638.

    Generated by Claude Code

  2. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · 2026-10-05T21:00Z
    Session: session_011K3zqE8Pv1Evw5hc8tZCnN
    Account: os-steve (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-21908-principal-less-strict
    Worktree: objectstack-issue-21908
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface (at origin/main cab63967), per triage's direction 6000979188:

    • Round 1 is measure-first. That is triage's step 1 and the card's own ask 1: name every framework and plugin path that hands the data engine a context with no principal and no isSystem, and whether each is reachable from a request. The dev stops with that table, without building, unless every edit the closure needs stays inside packages/plugins/plugin-security.
    • If it does stay inside: security-plugin.ts (the principal-less hand-off in the write middleware, about :2525, and the read-filter twin that isPrincipalLessContext serves), plugin-security tests, and a door pin in a NEW file under packages/qa/dogfood/test/ (declared cross-lane on [PM seat] domain:cli — 🟢 os-sales · session_01BVm1pAXVXQ3PjC42j1kPFf · serial, p1/p2 + #24022 (maintainer) #6024 in this act).
    • content/docs/** sentences this makes false, and a changeset.

    ⛔ The contract sentence in packages/spec/src/contracts/ai-service.ts is not loosened (triage). ⛔ No producer keeps working by relying on the missing field.
    ⛔ ADR-0096 is Proposed, and its D5 stages the flip behind telemetry. The seat reads triage's direction (enforce the published contract now, every legitimate producer setting the explicit system opt-in) as this card's binding line. That reading follows the maintainer's ruling 2B on objectstack-ai/cloud#2006 (5995756827) and the four-axis frame: no staged transition without named external evidence. Any producer whose legitimate posture is not decidable from that line goes to the maintainer.
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; default tier). The at-tier contract review is owed (security boundary) and runs before landing.
    Clause-②: no (narrowing)


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 21908,
    "status": "blocked",
    "branch": "claude/issue-21908-principal-less-strict",
    "pr": null,
    "session": "session_011K3zqE8Pv1Evw5hc8tZCnN — subagent of the PM session (harness Claude-Session line)",
    "premise_still_valid": true,
    "summary": "Measure-first round, STOP case. On origin/main cab6396 the principal-less hand-off grants at six plugin-security sites (H1). A local, uncommitted probe at those sites (now reverted, blob equal to HEAD) recorded 36452 principal-less, non-system contexts in three kinds of run: a booted showcase dev composition (boot, seed, migrations, admin and anonymous requests, 60s idle), the whole dogfood suite (206 files, 1590 tests green), and the 32 unit files that compose SecurityPlugin. Every hit was at the middleware site; the five service-method sites recorded none. Deduplicated by first frame outside the engine and the middleware, the hits come from about 40 producer functions, plus static reads for paths no run exercised. Almost all live outside plugin-security: metadata-protocol, service-settings, service-messaging, service-storage, objectql, core, plugin-webhooks, service-datasource, plugin-auth, runtime. Several sit on every request (the API-exposure gate's metadata read, the localization settings read), so a deny that lands before them breaks the platform. Two catch blocks would fail OPEN under a deny (rows 19 and 21), so the producers must move first. The closure needs edits outside plugin-security, so per the stop gate nothing was built and nothing was pushed beyond the empty-branch probe. 2 producers are held off this thread. Class: reachable from a request door (one from an authenticated door with a member role, one from an unauthenticated door), where the read or write exceeds what that caller may.",
    "tests": "No code change. Measurement runs only, see gates. Probe: boot 385, dogfood 35245, unit 822 records. Every dogfood and unit run exited 0.",
    "mcp_calls": "0",
    "api_writes": "1 — POST issue comment on #21908 via scripts/pm/post-stamped.mjs (this report)",
    "stop_or_build": "STOP — the closure needs producer edits in at least ten packages outside plugin-security (rows 1-21), plus test-file edits (row 22).",
    "h1_sites": [
    {
    "site": "packages/plugins/plugin-security/src/security-plugin.ts:2525 (engine find/write middleware)",
    "answers_today": "return next(): no CRUD gate, no RLS, no FLS mask, no tenant Layer 0 for any verb. The gates placed before it still run (package-managed, system-row, curated-capability, audience-anchor, ADR-0103 engine-owned, ADR-0090 D12 delegated-admin), and sibling middlewares (plugin-sharing OWD) still run.",
    "measured_hits": 36452
    },
    {
    "site": "security-plugin.ts:5742 getReadFilter",
    "answers_today": "the sharing (OWD) predicate alone, or undefined (no row restriction)",
    "measured_hits": 0
    },
    {
    "site": "security-plugin.ts:5993 resolveProjectionFieldMask (both field projections)",
    "answers_today": "the full field set (no FLS)",
    "measured_hits": 0
    },
    {
    "site": "security-plugin.ts:6166 canReadObject",
    "answers_today": "true",
    "measured_hits": 0
    },
    {
    "site": "security-plugin.ts:6452 canWriteObject",
    "answers_today": "true (after its engine-owned and D12 pre-resolution arms)",
    "measured_hits": 0
    },
    {
    "site": "security-plugin.ts:6555 canExport",
    "answers_today": "true",
    "measured_hits": 0
    }
    ],
    "h5_reading": "The doors answer the anonymous pair (UNAUTHENTICATED / 401, core anonymous-deny constants) before any engine call. ADR-0096 D5 names PermissionDeniedError (PERMISSION_DENIED / 403), which REST mapDataError and the dispatcher relay as 403 today. The readings disagree, so this is open_questions[0] and nothing is built on it.",
    "h6_reading": "No systemContext(reason) and no SystemGrant exist. The explicit opt-in today is the wire-level isSystem: true flag, spelled as literals and as per-package constants (plugin-security SYSTEM_CTX and seedCtx, service-queue SYSTEM_CTX, service-automation resolveRunDataContext).",
    "producers": [
    {
    "row": 1,
    "package": "metadata-protocol",
    "position": "src/protocol.ts findServedOverlayRow (findOne sys_metadata)",
    "reachable": "yes: authenticated metadata read doors (any role) and authoring doors (admin/author); also boot, via plugin-security reconcilePermissionSetProjection → protocol.getMetaItemLayered (no context), and plugin-security's own permission-set projection on a write",
    "today": "store read with no RLS and no tenant wall; the protocol scopes by organization in its own where; the door decides what reaches the caller",
    "posture": "explicit system opt-in (isSystem: true): a platform store or plumbing read/write whose door, if any, already authorizes; decided from Zone 1 (legitimately needs system access; carrying a member principal would hit the deny baseline on an engine-owned table)",
    "evidence": "probe: dogfood 13891, boot 80, unit 277 records; scratchpad all-producers.txt"
    },
    {
    "row": 2,
    "package": "metadata-protocol",
    "position": "src/protocol.ts overlayLockLayerAt (find sys_metadata)",
    "reachable": "yes: same doors as row 1",
    "today": "as row 1",
    "posture": "explicit system opt-in (isSystem: true): a platform store or plumbing read/write whose door, if any, already authorizes; decided from Zone 1 (legitimately needs system access; carrying a member principal would hit the deny baseline on an engine-owned table)",
    "evidence": "probe: dogfood 14024 (shared anonymous-closure group), boot 80"
    },
    {
    "row": 3,
    "package": "metadata-protocol",
    "position": "src/protocol.ts queryByOrg / readActiveOverlayRows / readFlattenedMetaItems (find sys_metadata)",
    "reachable": "yes: the REST API-exposure gate on every data request (authenticated, any role) and the public-form door (unauthenticated, declared public forms); also boot (MCP prompt bridge at start, automation flow sync)",
    "today": "as row 1",
    "posture": "explicit system opt-in (isSystem: true): a platform store or plumbing read/write whose door, if any, already authorizes; decided from Zone 1 (legitimately needs system access; carrying a member principal would hit the deny baseline on an engine-owned table)",
    "evidence": "probe: dogfood 2037, boot 14, unit 14"
    },
    {
    "row": 4,
    "package": "metadata-protocol",
    "position": "src/protocol.ts read within foldStoredCollection / assertRuntimeAuthoringRules (find sys_metadata)",
    "reachable": "yes: authoring doors (flow save, metadata save; admin/author)",
    "today": "as row 1",
    "posture": "explicit system opt-in (isSystem: true): a platform store or plumbing read/write whose door, if any, already authorizes; decided from Zone 1 (legitimately needs system access; carrying a member principal would hit the deny baseline on an engine-owned table)",
    "evidence": "probe: dogfood 761, unit 64"
    },
    {
    "row": 5,
    "package": "metadata-protocol",
    "position": "src/sys-metadata-repository.ts SysMetadataRepository get / put / delete / promoteDraft / restoreVersion / listDrafts / nextItemVersion / nextEventSeq (sys_metadata, sys_metadata_history; some under a transaction-only context)",
    "reachable": "yes: authoring, publish and rollback doors (admin/author)",
    "today": "store reads and writes with no RLS and no tenant wall",
    "posture": "explicit system opt-in (isSystem: true): a platform store or plumbing read/write whose door, if any, already authorizes; decided from Zone 1 (legitimately needs system access; carrying a member principal would hit the deny baseline on an engine-owned table)",
    "evidence": "probe: dogfood ~640 records across the eight functions"
    },
    {
    "row": 6,
    "package": "metadata-protocol",
    "position": "src/protocol.ts recordMetadataAudit (insert sys_metadata_audit), persistPackageCommitRow (insert sys_metadata_commit), publishPackageDrafts, resolveOverlayPackageBinding, storedFlowBindingAgrees, deletePackage, duplicatePackage, reassignOrphanedMetadata",
    "reachable": "yes: authoring and package doors (admin); also direct in-process protocol calls",
    "today": "as row 5",
    "posture": "explicit system opt-in (isSystem: true): a platform store or plumbing read/write whose door, if any, already authorizes; decided from Zone 1 (legitimately needs system access; carrying a member principal would hit the deny baseline on an engine-owned table)",
    "evidence": "probe: dogfood ~140"
    },
    {
    "row": 7,
    "package": "service-settings",
    "position": "src/settings-service.ts SettingsService.loadRows (find sys_setting)",
    "reachable": "yes: the execution-context build of every REST request (localization; any caller) and the dispatcher's execution-context resolve; also boot (plugin-auth settings/brand/locale binding), plugin-audit write locale, objectql lifecycle governance",
    "today": "platform settings read with no RLS and no tenant wall; only a locale or setting value is used",
    "posture": "explicit system opt-in (isSystem: true): a platform store or plumbing read/write whose door, if any, already authorizes; decided from Zone 1 (legitimately needs system access; carrying a member principal would hit the deny baseline on an engine-owned table)",
    "evidence": "probe: dogfood 2090, boot 16, unit 13"
    },
    {
    "row": 8,
    "package": "service-settings",
    "position": "src/settings-service.ts SettingsService.upsertRow + src/settings-service-plugin.ts audit write (find/insert sys_setting, insert sys_setting_audit)",
    "reachable": "yes: the settings write door (admin)",
    "today": "door-gated store write",
    "posture": "explicit system opt-in (isSystem: true): a platform store or plumbing read/write whose door, if any, already authorizes; decided from Zone 1 (legitimately needs system access; carrying a member principal would hit the deny baseline on an engine-owned table)",
    "evidence": "probe: dogfood 11"
    },
    {
    "row": 9,
    "package": "objectql",
    "position": "src/plugin.ts ObjectQLPlugin.readAuthoredActionRows / readAuthoredHookRows (find sys_metadata)",
    "reachable": "no (boot and the detached metadata-change resync)",
    "today": "platform metadata read",
    "posture": "explicit system opt-in (isSystem: true): a platform store or plumbing read/write whose door, if any, already authorizes; decided from Zone 1 (legitimately needs system access; carrying a member principal would hit the deny baseline on an engine-owned table)",
    "evidence": "probe: dogfood 1306, boot 5, unit 56"
    },
    {
    "row": 10,
    "package": "core",
    "position": "src/fallbacks/authored-translation-sync.ts readAuthoredTranslationLayer (find sys_metadata)",
    "reachable": "no (boot and resync)",
    "today": "platform metadata read",
    "posture": "explicit system opt-in (isSystem: true): a platform store or plumbing read/write whose door, if any, already authorizes; decided from Zone 1 (legitimately needs system access; carrying a member principal would hit the deny baseline on an engine-owned table)",
    "evidence": "probe: dogfood 496, boot 2, unit 18"
    },
    {
    "row": 11,
    "package": "service-datasource",
    "position": "loadDatasourceRows (boot, find sys_metadata) measured; src/datasource-admin-plugin.ts sys_metadata CRUD and src/datasource-secret-binder.ts sys_secret insert/delete/find (static)",
    "reachable": "boot: no; admin datasource door: yes (static, admin)",
    "today": "platform store reads/writes",
    "posture": "explicit system opt-in (isSystem: true): a platform store or plumbing read/write whose door, if any, already authorizes; decided from Zone 1 (legitimately needs system access; carrying a member principal would hit the deny baseline on an engine-owned table)",
    "evidence": "probe: boot 1; static sweep"
    },
    {
    "row": 12,
    "package": "plugin-webhooks",
    "position": "src/auto-enqueuer.ts AutoEnqueuer.doRefresh (find sys_webhook) measured; src/redeliver-guard.ts createWebhookRedeliverGuard (findOne sys_webhook) static",
    "reachable": "auto-enqueuer: no (boot/refresh); redeliver guard: yes via the messaging redeliver door (static)",
    "today": "platform subscription reads",
    "posture": "explicit system opt-in (isSystem: true): a platform store or plumbing read/write whose door, if any, already authorizes; decided from Zone 1 (legitimately needs system access; carrying a member principal would hit the deny baseline on an engine-owned table)",
    "evidence": "probe: boot 3; static sweep"
    },
    {
    "row": 13,
    "package": "service-messaging",
    "position": "src/sql-outbox.ts and src/sql-http-outbox.ts claim / claimDigest / reapExpired (find/update sys_notification_delivery, sys_http_delivery)",
    "reachable": "no (background dispatcher ticks)",
    "today": "outbox claim and reap",
    "posture": "explicit system opt-in (isSystem: true): a platform store or plumbing read/write whose door, if any, already authorizes; decided from Zone 1 (legitimately needs system access; carrying a member principal would hit the deny baseline on an engine-owned table)",
    "evidence": "probe: boot 357, dogfood 34"
    },
    {
    "row": 14,
    "package": "service-messaging",
    "position": "emit fan-out: src/messaging-service.ts writeEvent (insert sys_notification), src/inbox-channel.ts send (insert sys_inbox_message) and writeDeliveredReceipt (insert sys_notification_receipt), src/preference-resolver.ts loadRows (sys_notification_preference), src/recipient-resolver.ts resolveEmail (findOne sys_user)",
    "reachable": "yes: authenticated member doors that notify (approval comment) and scheduled flows",
    "today": "writes land on the recipients' rows and a recipient's address is read for delivery; nothing read is returned to the caller",
    "posture": "explicit system opt-in (isSystem: true): a platform store or plumbing read/write whose door, if any, already authorizes; decided from Zone 1 (legitimately needs system access; carrying a member principal would hit the deny baseline on an engine-owned table)",
    "evidence": "probe: dogfood 41"
    },
    {
    "row": 15,
    "package": "service-messaging",
    "position": "src/messaging-service.ts listInbox / readReceiptStates (find sys_inbox_message, sys_notification_receipt)",
    "reachable": "yes: the authenticated notifications list door (any role)",
    "today": "scoped to the door's authenticated user id in the where; nothing beyond the caller's own rows",
    "posture": "UNDECIDED, see open_questions[1]",
    "evidence": "probe: boot 2, dogfood 8"
    },
    {
    "row": 16,
    "package": "service-storage",
    "position": "src/metadata-store.ts StorageMetadataStore.createFile (insert sys_file / sys_upload_session under a tenant-only context)",
    "reachable": "yes: authenticated upload doors (member)",
    "today": "inserts the caller's own pending file row, stamped with the caller's user and organization",
    "posture": "UNDECIDED, see open_questions[1]",
    "evidence": "probe: dogfood (StorageMetadataStore.engineOp group, 63 inserts)"
    },
    {
    "row": 17,
    "package": "plugin-auth",
    "position": "src/auth-plugin.ts platform-admin OAuth client toggle route (findOne/update sys_oauth_application on the raw engine from getDataEngine)",
    "reachable": "yes: platform-admin door (the shared platform-admin judge runs first)",
    "today": "door-gated read and write",
    "posture": "explicit system opt-in (isSystem: true): a platform store or plumbing read/write whose door, if any, already authorizes; decided from Zone 1 (legitimately needs system access; carrying a member principal would hit the deny baseline on an engine-owned table); plugin-auth's own adapter already writes these tables through withSystemContext",
    "evidence": "probe: dogfood 5"
    },
    {
    "row": 18,
    "package": "plugin-auth",
    "position": "src/scim-connection-service.ts verifyScimBearerToken (findOne sys_scim_connection_credential on the raw engine)",
    "reachable": "yes: SCIM bearer door (static)",
    "today": "credential verification by token digest",
    "posture": "explicit system opt-in (isSystem: true): a platform store or plumbing read/write whose door, if any, already authorizes; decided from Zone 1 (legitimately needs system access; carrying a member principal would hit the deny baseline on an engine-owned table); under a deny its catch answers null, so SCIM fails closed",
    "evidence": "static read, no run reached it"
    },
    {
    "row": 19,
    "package": "plugin-auth",
    "position": "src/auth-manager.ts organization-update hook (find sys_organization, sys_environment on the raw engine)",
    "reachable": "yes: org-admin organization update (static)",
    "today": "reads the caller's own organization for a slug guard",
    "posture": "explicit system opt-in (isSystem: true): a platform store or plumbing read/write whose door, if any, already authorizes; decided from Zone 1 (legitimately needs system access; carrying a member principal would hit the deny baseline on an engine-owned table); ORDERING HAZARD: its catch returns early, so a deny that lands first skips the guard",
    "evidence": "static read"
    },
    {
    "row": 20,
    "package": "plugin-auth",
    "position": "other raw-engine sites outside the adapter's withSystemContext wrapper (src/adopt-membership.ts, src/membership-ended-session.ts, src/auth-manager.ts insert helper)",
    "reachable": "not established (static, unexamined in depth)",
    "today": "principal-less if the raw engine is used",
    "posture": "explicit system opt-in (isSystem: true): a platform store or plumbing read/write whose door, if any, already authorizes; decided from Zone 1 (legitimately needs system access; carrying a member principal would hit the deny baseline on an engine-owned table) (presumed)",
    "evidence": "static sweep heuristic"
    },
    {
    "row": 21,
    "package": "runtime",
    "position": "src/http-dispatcher.ts environment-membership check (find sys_environment_member)",
    "reachable": "yes: environment-scoped requests (authenticated; static)",
    "today": "reads the caller's own membership row",
    "posture": "explicit system opt-in (isSystem: true): a platform store or plumbing read/write whose door, if any, already authorizes; decided from Zone 1 (legitimately needs system access; carrying a member principal would hit the deny baseline on an engine-owned table); ORDERING HAZARD: its catch fails open, so a deny that lands before this producer moves opens the membership gate",
    "evidence": "static read; no run used an environment-scoped request"
    },
    {
    "row": 22,
    "package": "test harness (not runtime)",
    "position": "packages/qa/dogfood/test/package-first-authoring.dogfood.test.ts (direct sys_metadata insert and direct protocol calls); packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts (context-less reads); packages/runtime/src/domains/share-links-enforcement-context.test.ts (its engine double drops the engine's trailing options argument, so plugin-security's permission-set loader, which passes an isSystem context there, reads principal-less only under the double)",
    "reachable": "no",
    "today": "test-side producers",
    "posture": "tests set isSystem; the double honours the trailing context (edits to existing packages/qa and runtime test files)",
    "evidence": "probe: dogfood 1, unit 34"
    }
    ],
    "closed_since_adr_0096_e1": [
    "sandbox body runner: the action body api is built over an explicit isSystem context (runtime action-execution buildActionExecutionContext / buildActionApi); no record",
    "MCP record resource: registered only with a principal-bound reader (ADR-0101); no record",
    "REST public-form submit: the context carries publicFormGrant and a guest_portal permission name, so the grant branch handles it and it is not principal-less",
    "guest principal (core assembleExecutionContextOrGuest): carries positions guest, so it is not principal-less",
    "objectql engine visibleWhen: no engine-internal producer in any run (36452 records); buildEvalUser reads nothing"
    ],
    "private_producers": "2 rows held off GitHub. Class: reachable from a request door (one from an authenticated door with a member role, one from an unauthenticated door), where the read or write exceeds what that caller may.",
    "open_questions": [
    {
    "question": "Which refusal does the engine-level principal-less deny carry, and on which predicate? (H5: the two readings disagree, so nothing is built on either.)",
    "options": [
    "A: PermissionDeniedError, 403 PERMISSION_DENIED, keyed on isPrincipalLessContext and not isSystem. ADR-0096 D5 names it; REST mapDataError and the dispatcher already relay it as 403; no new emitter.",
    "B: the anonymous-deny pair, 401 UNAUTHENTICATED, as the card and triage cite. It needs a new plugin-security error class emitting a registered code (an error-code provenance row, not a new code). Reusing shouldDenyAnonymous's predicate (no user id and not system) would be wider than principal-less: it also denies the guest principal and any position- or permission-bearing context without a user id."
    ],
    "recommendation": "A on the isPrincipalLessContext predicate. Business need (measured): no HTTP door hands a principal-less context in on an anonymous request, because every door answers 401 first (dev boot: all anonymous data reads 401). Every measured producer is in-process plumbing, for which 'authenticate' is a false remedy. Long-term: one engine deny vocabulary, the same as the fail-closed permission-resolution path. AI-error: a loud refusal whose text names the two remedies (carry the caller, or set isSystem). Startup: no new class and no new emitter. B's 401 belongs to the doors, which already have it."
    },
    {
    "question": "Posture for the request-path producers that act on the caller's OWN rows: service-messaging listInbox / readReceiptStates (row 15) and service-storage createFile (row 16). Carry the caller's principal, or set the explicit system opt-in and keep the door's own user scope?",
    "options": [
    "A: explicit system opt-in inside the owning service, keeping today's door-derived user scope (the where clause / owner stamp).",
    "B: carry the caller's principal so RLS decides. Whether members hold grants on these service-owned objects is NOT MEASURED, and without them the deny baseline breaks the inbox and uploads."
    ],
    "recommendation": "A. Business need: the services own these objects and already scope by the door-authenticated user; no measured member grant exists. Long-term: the service is the boundary for its own objects (listInboxAsCaller already derives the recipient from the caller). AI-error: B silently couples a working inbox to grant authoring. Startup: A moves no grant model. The maintainer decides; not picked here."
    }
    ],
    "out_of_scope_findings": [
    "carrier: the closure's producer slices (the seat) · noted, not filed: rows 19 and 21 swallow a read fault into a skipped guard or an open gate. That is pre-existing (row 21 is documented as deferred), and it matters for sequencing: a deny that lands first turns them into openings. Dedupe words: fail-open catch, membership check, slug guard, principal-less deny ordering"
    ],
    "gates": [
    {
    "command": "git push -u origin claude/issue-21908-principal-less-strict (empty-branch write probe)",
    "exit": 0
    },
    {
    "command": "os-verify-lock: turbo run build --filter dogfood^... --filter example-showcase... --concurrency=1 (base build)",
    "exit": 0
    },
    {
    "command": "os-verify-lock: pnpm --filter @objectstack/plugin-security build (local probe in dist)",
    "exit": 0
    },
    {
    "command": "os-verify-lock: boot-probe.sh (showcase dev composition, seed-admin, admin + anonymous requests, 60s idle)",
    "exit": 0
    },
    {
    "command": "os-verify-lock: dogfood vitest chunks 00..06 --maxWorkers=2 (206 files; 205 passed + 1 skipped; 1590 tests passed, 9 skipped)",
    "exit": "0 x7"
    },
    {
    "command": "os-verify-lock: unit probe runs (rest 11 files/160 tests, runtime 11/162, service-automation 2/23, plugin-auth 2/34, organizations 1/21, verify 1/6, cli 4/43)",
    "exit": "0 x7"
    },
    {
    "command": "python3 static-sweep.py (heuristic context-less engine-call sweep; 239 raw hits, about 40 examined)",
    "exit": 0
    },
    {
    "command": "git checkout HEAD -- security-plugin.ts; hash-object == HEAD blob 5b4ab280; git diff HEAD empty",
    "exit": 0
    },
    {
    "command": "dispatch-gates --commands / --ran and the check:* battery",
    "exit": "NOT MEASURED: stop case, no diff (build case only)"
    }
    ],
    "line_budget": "n/a",
    "deviations": [
    "Unit scope narrowed to the 32 test files that compose SecurityPlugin: a suite that never composes it cannot reach the hand-off.",
    "start not booted separately: NOT MEASURED. The dev boot enters through the same serve command (Serve.run).",
    "The probe dist was built with a direct pnpm build, not turbo, so the shared turbo cache never held the instrumented output.",
    "The static sweep is a heuristic: 239 raw hits, many of them comments or non-engine receivers, about 40 examined."
    ],
    "files_changed": []
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat verdict on the measure-first round (6003676228) · seat domain:services#1 (#6021) · session_011K3zqE8Pv1Evw5hc8tZCnN · 2026-10-05T21:52Z. ⛔ Classes, positions and functions only.

    ACCEPT the STOP. The closure needs producer edits in about ten packages outside plugin-security, so this card does not build in one round. The measurement is the map. The seat read the six hand-off sites and the producer rows against origin/main cab63967. Only the middleware site took live traffic. Rows 19 and 21 swallow a read fault into a skipped guard or an open gate, so the deny must land last.

    Open question 0, which refusal the engine-level deny carries: decided A by the four-axis frame (the axes agree). PermissionDeniedError (403 PERMISSION_DENIED), keyed on isPrincipalLessContext(context) && !isSystem, the predicate the hand-off already uses.

    • Business need: no HTTP door hands a principal-less context in on an anonymous request, because every door answers 401 first (measured at boot). Every measured producer is in-process plumbing, for which "authenticate" is a false remedy.
    • Long term: one engine deny vocabulary, the same as the fail-closed permission-resolution path. ADR-0096 D5 names this class.
    • AI error: a loud refusal whose text names both remedies (carry the caller, or set the explicit system opt-in).
    • Startup: no new class, emitter or code.
    • Why not shouldDenyAnonymous's predicate: it would also deny the guest principal and any position- or permission-bearing context without a user id. Those are not principal-less. Triage's "the same decision the HTTP doors make" is read as the decision (deny), not the doors' status code.

    Open question 1, the request-path producers that act on the caller's own rows (rows 15 and 16): escalated to the maintainer. The choice is between an explicit system opt-in inside the owning service, keeping today's door-derived user scope, and carrying the caller's principal. It decides how much reach a request path holds on a security boundary, and this lane's floor sends that to a human. The seat's recommendation is the dev's A (system opt-in in the owning service, scope kept). No measured member grant exists on those objects, so B would break the inbox and uploads. Rows 15 and 16 wait for the ruling; nothing else does.

    Held off this thread: the report counts two producer rows as a live access-gap class. They went to the maintainer privately. The deny waits for their resolution, because each is also a producer.

    The split (producers first, the deny last; each slice is a seat-owned sub-issue with this card's domain and priority):

    1. Engine-lane producers: metadata-protocol (rows 1 to 6), objectql (row 9) and core (row 10). Cross-lane on [PM seat] domain:engine — 🟢 sunhaolin #6367.
    2. Services-lane producers: service-settings (rows 7 and 8), service-messaging (rows 13 and 14), service-datasource (row 11) and plugin-webhooks (row 12).
    3. Identity and runtime producers: plugin-auth (rows 17 to 20) and the runtime environment-membership read (row 21). Cross-lane on [PM seat] domain:cli — 🟢 os-sales · session_01BVm1pAXVXQ3PjC42j1kPFf · serial, p1/p2 + #24022 (maintainer) #6024. Rows 19 and 21 are the ordering hazards.
    4. This card, last: the deny at the six hand-off sites, the test-harness producers (row 22), and the pins. Those are: a principal-less context denied on read and write; an explicit system context unchanged; and the dogfood suite green under the deny, which is the proof that every measured producer took a route.

    Each slice moves its producers to the explicit system opt-in (isSystem: true, the opt-in that exists; ⛔ no new elevation API). It must also prove that the move changes nothing today. ⚠️ An isSystem context short-circuits the gates the hand-off still runs before next(): package-managed, system-row, curated-capability, audience-anchor, engine-owned and delegated-admin. So each slice measures, per producer, whether any of those gates fires on its calls today. If one does, that producer is reported, not moved.


    Generated by Claude Code

  5. 47 remaining items

  6. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · 2026-10-08T08:06Z
    Session: session_01WkL6Eijt432S1Y7ekb6ovQ
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-21908-principal-less-deny
    Worktree: objectstack-issue-21908-deny
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    ⛔ Classes, positions and functions only.

    The deny round. Its source is the release that owes it (6030567356), as the director seat's record (6050734928) restates it. Triage unlocked the card (6054654999): #22046 and #22147 are closed, so both producer rows have landed. The deny lands LAST in this round.

    File surface at origin/main 0e9371f0. Re-read on that shape before building:

    • packages/metadata-protocol/src/protocol.ts, promoteDraftForPublish: it still reads principal-less. It moves to an explicit system opt-in, or carries its caller, whichever the measurement shows is right. It is cross-lane (domain:engine), declared on [PM seat] domain:engine — 🟢 sunhaolin #6367 in this act.

    • packages/plugins/plugin-security/src/security-plugin.ts, the deny:

      • PermissionDeniedError, keyed on isPrincipalLessContext(context) && !isSystem, at the six hand-off sites;
      • the hand-off at the middleware (near :2616) and the principal-less exits in the read and row-scope resolvers (near :5875, :6126, :6299, :6585, :6688). The dev measures which are the six.
    • The test-harness producers that hand the engine a principal-less context (release row 22). Each one carries a principal or the explicit system opt-in.

    • The pins: the ruling's three pins, and the composed Q2 pin (a private object resolves its owner, end to end).

    • The ADR-0096 D5 dated note (docs/adr/0096-execution-surface-identity-admission.md): its own Tier H draft PR on a second branch, claude/issue-21908-adr-0096-d5-note, for the maintainer's approval. The seat never approves or merges it.

    • Changesets per touched package.

    • Seat append, 2026-10-08T13:16Z, from the dev report's deviations and PR feat(plugin-security)!: refuse a principal-less, non-system data-engine context (ADR-0096 D5 strict mode) #22297's Acceptance notes:

      • protocol.ts, beyond promoteDraftForPublish: the census found nine more request-reachable functions of the same class in the same file. They are resolveDraftOrgScopeForPublish, resolveMetaItemOrgScope, auditMetaItem, diffMetaItem, listCommits, revertCommit, rollbackToPackageCommit, metaTypeNamespaceExists, migrateStoredMetadata and the deleteMetaItem legacy path. Each takes the same explicit system opt-in on its sys_metadata* store calls, behind a door that has already authorized the caller.
        • This is an in-place adjacent fix: the same class, mechanical, no other claimant on those functions at the time of the edit, and the same gate family.
        • The cross-lane declaration on [PM seat] domain:engine — 🟢 sunhaolin #6367 is amended in the same act.
      • The test-harness producers are four: two dogfood fixtures and two runtime tests.
      • New pins: runtime dispatcher-plugin.endpoint-principal-less-deny.integration.test.ts (the fault-class pin, at the runtime tier) and metadata-protocol protocol.platform-store-system-opt-in.test.ts (the deny-round block).

    Exclusions:

    Stop on breach; explain in the report.
    Container & model: L, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; default-tier build). The at-tier contract review runs before the queue, as the release requires.
    Clause-②: no (narrowing)

  7. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 21908,
    "status": "done",
    "branch": "claude/issue-21908-principal-less-deny",
    "pr": "#22297",
    "adr_branch": "claude/issue-21908-adr-0096-d5-note",
    "adr_pr": "#22298",
    "session": "session_01WkL6Eijt432S1Y7ekb6ovQ (harness Claude-Session line; subagent of the PM session)",
    "premise_still_valid": true,
    "summary": "The deny round is built. plugin-security refuses a data-engine context that carries no principal and is not a system context, keyed on isPrincipalLessContext(context) && !isSystem, at the six hand-off sites. The middleware throws PermissionDeniedError (403 PERMISSION_DENIED) for every verb before the operation runs. canReadObject, canWriteObject and canExport answer false, getReadFilter answers the deny filter, and the field projection no longer settles the class early. Before the deny, the producer census closed every producer it found inside the claim: promoteDraftForPublish (measured, H2) and, by static read, 9 more functions (12 more calls) in protocol.ts behind authoring and admin doors, plus 4 test-harness producers. Each moved to the explicit system opt-in or carries its context. The ruling's three pins, the composed Q2 pin, the resolveRequestScope fault-class pin, the system-unchanged and guest-unchanged controls are in, and six ablations turned their pins red. The full dogfood suite is green under the deny, and a real showcase boot serves a signed-in member's first query and every admin authoring door with zero strict-mode refusals. The ADR-0096 D5 dated note is its own Tier H draft PR, #22298. Seat actions owed: amend the #6367 declaration (protocol.ts functions beyond promoteDraftForPublish) and route the stale spec ISecurityService prose (open_questions[0]); the at-tier contract review runs before the queue.",
    "census_h1": {
    "method": "deny in place plus a temporary, uncommitted stack probe at every principal-less exit (marker PL_PROBE_21908, env-gated, built into dist by a direct non-turbo build). It ran over the full dogfood suite (10 shards, 239 files), over every unit file in 22 packages whose text composes the security plugin (about 90 files), and over plugin-security's own suite. A static sweep covered every sys_* engine call in production src with no context. Probe reverted: blob == HEAD (11ba8acdc9), git diff HEAD empty, marker absent from all 6 built files (ablation-dist-preflight --absent), tree clean.",
    "rows": [
    {
    "class": "production, measured",
    "position": "metadata-protocol src/protocol.ts",
    "function": "promoteDraftForPublish (draft read)",
    "reachable": "yes: the publish door, after its authorization; measured: a flow publish answered 503 under the deny before the fix",
    "closure": "explicit system opt-in"
    },
    {
    "class": "production, static",
    "position": "protocol.ts",
    "function": "resolveDraftOrgScopeForPublish (2 probes)",
    "reachable": "yes: an organization-scoped publish",
    "closure": "explicit system opt-in"
    },
    {
    "class": "production, static",
    "position": "protocol.ts",
    "function": "resolveMetaItemOrgScope (2 history probes)",
    "reachable": "yes: the organization-scoped revert path",
    "closure": "explicit system opt-in"
    },
    {
    "class": "production, static",
    "position": "protocol.ts",
    "function": "auditMetaItem",
    "reachable": "yes: the audit door (would answer 503 under the deny)",
    "closure": "explicit system opt-in"
    },
    {
    "class": "production, static",
    "position": "protocol.ts",
    "function": "diffMetaItem (history read, current-row read)",
    "reachable": "yes: the diff door (its catch would have answered an empty diff)",
    "closure": "explicit system opt-in"
    },
    {
    "class": "production, static",
    "position": "protocol.ts",
    "function": "listCommits, revertCommit, rollbackToPackageCommit",
    "reachable": "yes: the commit doors",
    "closure": "explicit system opt-in"
    },
    {
    "class": "production, static",
    "position": "protocol.ts",
    "function": "metaTypeNamespaceExists, migrateStoredMetadata",
    "reachable": "yes: the authoring and migration doors",
    "closure": "explicit system opt-in"
    },
    {
    "class": "production, static",
    "position": "protocol.ts",
    "function": "deleteMetaItem legacy path (read and delete)",
    "reachable": "yes: a code-only delete on a control-plane kernel",
    "closure": "explicit system opt-in"
    },
    {
    "class": "test harness",
    "position": "qa/dogfood business-unit-and-user-delete-federated-fixture",
    "function": "system context passed as the options argument",
    "reachable": "no",
    "closure": "carries { context }"
    },
    {
    "class": "test harness",
    "position": "qa/dogfood package-first-authoring (row 22)",
    "function": "direct sys_metadata insert",
    "reachable": "no",
    "closure": "system opt-in"
    },
    {
    "class": "test harness",
    "position": "runtime standalone-stack-seeder-declaration-copy (row 22)",
    "function": "context-less seeded-row reads",
    "reachable": "no",
    "closure": "system opt-in"
    },
    {
    "class": "test harness",
    "position": "runtime domains/share-links-enforcement-context (row 22)",
    "function": "engine double dropped the trailing read options (31 probe records, swallowed by the loader)",
    "reachable": "no",
    "closure": "double merges the trailing context as mergeReadContext does"
    }
    ],
    "not_producers": "loadMetaFromDb and reportUnhydratableOrgScopedRows read context-less at boot inside ObjectQL start(), before plugin-security (which depends on ObjectQL) registers its middleware: never gated. Noted, not edited.",
    "outside_claim": "none found: no producer outside plugin-security, protocol.ts and the test harnesses. The static sweep's other 25 hits go through a system-context wrapper, carry a context in an options object, or are comments.",
    "six_gates": "each moved call is a sys_metadata, sys_metadata_history, sys_metadata_audit or sys_metadata_commit read, or the legacy sys_metadata delete. None of the six pre-refusal gates (package-managed, system-row, curated-capability, audience-anchor, engine-owned, delegated-admin) keys on those objects (static read of each gate's object set)."
    },
    "h2": "promoteDraftForPublish is called by publishMetaItem (the per-item publish door) and publishPackageDrafts (the package publish door). The request carries only an actor string, no execution context, and a member principal would meet the engine-owned deny baseline on sys_metadata. Smallest right closure: the explicit opt-in on its draft read, as its sibling store reads already have. Measured: the dogfood flow-credential publish answered 503 under the deny and 200 after the fix.",
    "h3": "Confirmed: the six are the middleware hand-off (:2616) plus getReadFilter (:5875), resolveProjectionFieldMask (:6126), canReadObject (:6299), canWriteObject (:6585) and canExport (:6688) at 0e9371f. These are the census sites the measure-first round named (the same functions at older positions). System contexts are unchanged at all six.",
    "pins": [
    "plugin-security principal-less-strict-mode.test.ts (new): 6 spellings x read (4 verbs) and write (3 verbs) refused before running, code+status; probes false; deny filter. System context unchanged (with and without a principal). Keyed on no principal: a user-less named set, the guest principal and the public-form grant are decided by what they carry. Member control.",
    "8 hand-through pins flipped to refusal pins: zero-set-deny-baseline, zero-set-masking, zero-set-capability-fold, metadata-unresolvable-posture, predicate-related-read-tenant-scope (split), security-plugin.test.ts (getReadFilter).",
    "runtime dispatcher-plugin.endpoint-principal-less-deny.integration.test.ts (new): resolveRequestScope fault class: engine handed no principal, store never reached, 403 PERMISSION_DENIED, nothing of the rows. Controls: no fault means guest principal and deny baseline (the #22147 posture unchanged); a granted signed-in caller is served.",
    "qa/dogfood owner-of-private-object-under-strict-mode.dogfood.test.ts (new, composed Q2): an owner_of audience on a private object reaches the owner's inbox and nobody else's; a principal-less read of the object is refused in the same composition.",
    "metadata-protocol protocol.platform-store-system-opt-in.test.ts (deny-round block): every producer above driven; every engine call it issues carries isSystem: true.",
    "#22177's declarative-endpoint-anonymous-guest dogfood pins: green under the deny (guest posture unchanged at a real boot)."
    ],
    "ablations": [
    "A1 middleware refusal back to return next() (dist leg, preflight present then absent): plugin-security 13/42 red, runtime fault-class pin 1/3 red ('expected [find] to deeply equal []', the store was reached)",
    "A2 getReadFilter answers undefined for the class: 8/305 red",
    "A3 canReadObject admits the class: 7/305 red",
    "A4 diff history read loses the opt-in: 1/7 red",
    "A5 promotion draft read loses the opt-in: 1/7 red",
    "A6 resolveOwnerOf loses its opt-in (service-messaging dist leg, preflight present then absent): composed Q2 pin 1/1 red ('expected 0 to be greater than 0')",
    "Each via scripts/ablation-replace.mjs wrap mode plus a shell trap: anchor 1 to 0, blob changed; restore blob == HEAD, git diff HEAD empty; tree clean after each."
    ],
    "tests": "Final head 454d0e8 (origin/main 8cbe255 merged at 6681b21, then a docblock-only test commit). At 6681b21, all under os-verify-lock: plugin-security 180 files, 3800 passed / 45 skipped. metadata-protocol 221 (+3 skipped) files, 28280 passed. runtime 339 files, 5490 passed / 19 skipped. dogfood full suite in 10 shards: 223 files, 1754 passed / 9 skipped. Composing unit files of rest, cli, lint, platform-objects, plugin-approvals, service-analytics, organizations and spec all green. typecheck exit 0 for plugin-security (incl. check:test-typecheck), metadata-protocol, runtime (incl. check:test-typecheck) and dogfood; tsc --listFiles compiles the metadata-protocol pin (1 hit). Pre-merge at 8248ceb: objectql 381 files / 7525 passed, core 81 files / 2240 passed (neither touched by main's incoming commits or this diff), plus every census unit batch green. Runtime pin at 454d0e8: 3/3. Real boot (showcase --fresh, merged tree): anonymous 401; admin sign-in and invite; member sign-up; member first query GET /data/showcase_task 200, plus announcement, object meta and inbox 200; admin draft save, publish, diff, audit, history, delete and package commits 200; server log: 0 strict-mode refusals. Lint (proven narrowing; CI runs full pnpm lint): population from eslint.config.mjs (/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} plus the packages/ blocks); eslint --no-inline-config --format json over the 16 changed .ts files at 454d0e8: 16 files, 0 errors, 0 warnings; no parserOptions.project and no typed rules, so no untouched file's verdict moves.",
    "gates": {
    "deny_branch": "dispatch-gates --commands --repo objectstack-ai/objectstack at 454d0e8: 73 derived; all 73 ran, all exit 0. --ran: '73 derived famil(ies) accounted for — 73 run, 0 NOT-MEASURED (a DERIVED zero — all 73 recorded an exit code and none of them is 3)'. Also run at 6681b21: 73/73 exit 0. Verdict lines: '✓ check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.' · '✓ This diff introduces no major bump.' · '✓ No empty-frontmatter changeset introduced by this diff' · 'check-nul-bytes: OK (scanned 10236 text file(s) ...)' · 'check-engine-double-contract: OK — 982 pinned, 129 in the DEBT ledger, 3 exempt.' · 'check-test-source-alias OK' · 'check:cross-package-test-inputs OK: 30 package(s) read outside themselves, all declared' · '✓ doc authoring guard: ... clean'. Derivation warned the tree is behind origin/main (main moves); CI judges the merge ref.",
    "adr_branch": "dispatch-gates at 2becf90: 19 derived, 19 run, all exit 0 (check:doc-formula-expressions first exited 3, prerequisite not met; formula and lint built; then exit 0). --ran: '19 derived famil(ies) accounted for — 19 run, 0 NOT-MEASURED (a DERIVED zero ...)'.",
    "governed_merges": "ADR branch (check-governed-merges --branch and --test over its file list): GOVERNED, landing tier H, docs/adr/** x1 (docs/adr/0096-execution-surface-identity-admission.md), 2 changed lines. Deny branch: NOT governed, 0 of 17 paths, 1083 changed lines."
    },
    "files_changed": {
    "claude/issue-21908-principal-less-deny": [
    ".changeset/21908-principal-less-strict-mode.md",
    "packages/metadata-protocol/src/protocol.platform-store-system-opt-in.test.ts",
    "packages/metadata-protocol/src/protocol.ts",
    "packages/plugins/plugin-security/src/metadata-unresolvable-posture.test.ts",
    "packages/plugins/plugin-security/src/predicate-related-read-tenant-scope.test.ts",
    "packages/plugins/plugin-security/src/principal-less-strict-mode.test.ts",
    "packages/plugins/plugin-security/src/security-plugin.test.ts",
    "packages/plugins/plugin-security/src/security-plugin.ts",
    "packages/plugins/plugin-security/src/zero-set-capability-fold.test.ts",
    "packages/plugins/plugin-security/src/zero-set-deny-baseline.test.ts",
    "packages/plugins/plugin-security/src/zero-set-masking.test.ts",
    "packages/qa/dogfood/test/business-unit-and-user-delete-federated-fixture.dogfood.test.ts",
    "packages/qa/dogfood/test/owner-of-private-object-under-strict-mode.dogfood.test.ts",
    "packages/qa/dogfood/test/package-first-authoring.dogfood.test.ts",
    "packages/runtime/src/dispatcher-plugin.endpoint-principal-less-deny.integration.test.ts",
    "packages/runtime/src/domains/share-links-enforcement-context.test.ts",
    "packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts"
    ],
    "claude/issue-21908-adr-0096-d5-note": [
    "docs/adr/0096-execution-surface-identity-admission.md"
    ]
    },
    "deviations": [
    "Scope fence vs H1: the fence lists protocol.ts as promoteDraftForPublish only, and the #6367 declaration names only that region. H1 says producers in protocol.ts are closed within the claim and the round stops otherwise. The static census found 9 more request-reachable functions (12 calls) in protocol.ts, of the same class, that the deny would break: audit would answer 503 and diff would answer an empty diff. They were closed with the same mechanical opt-in (rule 3's bounded in-place fix: same class, pinned shape, no other claim on the file, same gate family) and named in the PR body with pins and ablations. Seat action owed: amend the #6367 cross-lane declaration.",
    "Spelling of the deny at the five service sites. The ruling names PermissionDeniedError at the six sites. The middleware throws it. canReadObject, canWriteObject and canExport answer false, and getReadFilter answers the deny filter, because the published ISecurityService contract has them return their deny (callers map false to 403). The projection site lost its early full-set answer instead of answering an empty list, so the class gets the zero-set field answers that getReadableFields' contract states for any caller that resolves no set. Object admission is refused by the middleware and the probes. For the contract review to confirm.",
    "H1 method: one pass with the deny in place plus a stack probe, not a throw-only pass and a separate record-only pass. A producer hidden behind an earlier refusal in the same flow would surface only after the first was fixed; each failing file was re-run after its fix, and the full dogfood suite was re-run green at the end.",
    "origin/main (8cbe255) was merged into the deny branch (6681b21) before the PR. Re-runs were scoped per AGENTS §10 to the overlap: plugin-security, metadata-protocol, runtime, the composing unit files of the packages main touched, the full dogfood suite, typechecks and the boot. objectql and core evidence is pre-merge (8248ceb); main's incoming commits and this diff leave both untouched.",
    "The resolveRequestScope fault-class pin is at the runtime tier, not dogfood: the fault class is not steerable on a real boot (the session, OAuth, tenancy and authz-store reads absorb or re-raise their own faults), and dogfood does not depend on @objectstack/runtime, so the dispatcher's timed resolver cannot be induced to fault there without a manifest change. The real-boot guest posture is pinned by #22177's dogfood pins, green under the deny.",
    "packages/spec ISecurityService TSDoc (four sentences on getReadFilter, canExport, canReadObject) still describes the hand-off. Not edited (spec fenced). See open_questions[0].",
    "Changesets: one file. @objectstack/plugin-security minor with the BREAKING banner (launch-window convention; Changesets pre mode is on), and @objectstack/metadata-protocol patch. runtime and dogfood carry test files only (runtime files[] ships dist only; dogfood is private), so no changeset. Clause-② no (narrowing) and an ADR-0087 not-required (no-migration-prescription) disposition are in the changeset body.",
    "Commit trailers use AGENTS.md's model-free pair (Claude-Session plus Co-authored-by: Claude), not the harness reminder's model-named spelling. PR bodies end with the session-URL footer form of AGENTS.md.",
    "Lock queue-timeouts (exit 99, NOT MEASURED) happened on several calls while other seats held the lock; each was re-queued on slot dev-21908 and re-run. No 99 is counted as a result.",
    "The census unit population is files whose text names SecurityPlugin or @objectstack/plugin-security (string match), so it includes some files that only mention it."
    ],
    "mcp_calls": "0",
    "api_writes": "5 — each a POST /repos/objectstack-ai/objectstack/dispatches from the session, executed by the fleet-write relay as objectstack-fleet[bot]: (1) pr_create POST /repos/objectstack-ai/objectstack/pulls, draft, #22297, 13282 bytes sent and stored, identical; (2) pr_create POST /repos/objectstack-ai/objectstack/pulls, draft, #22298, 2847 bytes, identical; (3) label-write.mjs assign POST /repos//issues/22297/assignees os-bill, read back; (4) label-write.mjs assign POST /repos//issues/22298/assignees os-bill, read back; (5) post-stamped.mjs comment POST /repos//issues/21908/comments (this report). Plus git pushes to the two branches (not REST).",
    "open_questions": [
    {
    "question": "packages/spec/src/contracts/security-service.ts (ISecurityService) TSDoc still says a context that carries no principal at all is admitted (canReadObject, canExport) or keeps the scope the other layers compose (getReadFilter, 'ADR-0096 stages that context separately'). After #22297 that prose is false, and it ships in the published .d.ts. packages/spec is fenced out of this claim. How should the four sentences be corrected?",
    "options": [
    "A: the seat widens this claim's fence to those four TSDoc sentences, and a docs-only follow-up commit lands them on #22297 before the contract review, so the contract text and the runtime change in one landing.",
    "B: a separate domain:spec docs-only card, landed right after #22297.",
    "C: leave the prose until ADR-0096 is next revised."
    ],
    "recommendation": "A, by the four axes. Business need (measured): the .d.ts prose ships to every consumer, and an author reading it is told a context-less in-process call is admitted, which #22297 now refuses with 403. Long term: declared equals enforced in one release; B leaves a window where the published contract contradicts the runtime, and C makes it permanent. AI error: stale contract prose is exactly what an AI author reads before writing context-less engine calls, so A removes the trap at its source. Startup: four sentences, no new key, gate or surface. A gen:docs or api-surface check may apply; a TSDoc-only edit in contracts/ does not move the api-surface snapshot, but this is NOT MEASURED until the edit exists."
    }
    ],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted, not filed: loadMetaFromDb and reportUnhydratableOrgScopedRows (metadata-protocol protocol.ts) read the store context-less at boot, inside ObjectQL start(), before plugin-security registers its middleware; not gated in any measured composition. In PR #22297 Acceptance notes.",
    "carrier: 承接者:无 · noted, not filed: an ExecutionContext variable passed as an engine verb's options argument compiles (no excess-property check on a variable) and silently drops the context; found in one dogfood fixture, fixed here. Under strict mode the drop is now a loud 403, not a silent hand-through. In PR #22297 Acceptance notes.",
    "carrier: the #22297 contract review (the seat) · noted: the static half of the census reads sys_* object names spelled as literals; a call whose object name is a variable is covered only by the runs (dogfood full suite, about 90 composing unit files, the boot)."
    ]
    }


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat review, before the contract review: PR #22297 at 454d0e89b9 and PR #22298 at 2becf9015f · seat domain:services#1 (#6021) · session_01WkL6Eijt432S1Y7ekb6ovQ · 2026-10-08T13:19Z

    ⛔ Classes, positions and functions only.

    Checked against GitHub and the branches, not the report's prose (os-dev-report 6060614288). This record answers the dev's flags. The at-tier contract review of PR #22297 follows on this head; the ACCEPT waits for it.

  9. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Contract review

    Served-tier: CONTRACT_REVIEW_TIER
    Head-sha: 454d0e89b9397900c6d4b01bd30e8e505d886bbe
    Local-runs: none

    PR #22297 (feat(plugin-security)!: refuse a principal-less, non-system data-engine context, the deny round of #21908) at the head above. Inputs: the card body and its 26 comments (rulings 6019001864 and 6028787157, the seat verdict 6003795556, the claim 6055574466 with its append, the dev report 6060614288, the seat review 6060777536), the PR body and file list, the net diff against main at the merge-base 8cbe255ef6, and the check-runs on the head (latest run per name). The surrounding source was read on origin/main where a hunk could not be judged without it; nothing was built, run or re-run. ⛔ Classes, positions and functions only.

    ① Derived judgments

    Accept-set changes the diff implies (packages/plugins/plugin-security/src/security-plugin.ts), each judged:

    1. Engine middleware, the former hand-off site. isPrincipalLessContext(opCtx.context) used to return next(); it now throws principalLessDenial(operation, object), a PermissionDeniedError (403 PERMISSION_DENIED). The pre-existing isSystem short-circuit sits above it, so the effective key is isPrincipalLessContext(context) && !isSystem, the predicate ruling Q0 (6019001864) fixed, and the refusal stands where the hand-off stood: after the six pre-refusal gates (package-managed, system-row, curated-capability, audience-anchor, engine-owned, delegated-admin), which keep their own refusals, and after the public-form-grant branch, which keeps its declaration-derived admission. The refusal text names both remedies (carry the caller, or the explicit system opt-in) and carries no tracker number. RIGHT.
    2. getReadFilter. A principal-less context answered the sharing predicate alone (or undefined); it answers RLS_DENY_FILTER now, after the isSystem exit and before any layer resolves. Narrowing. RIGHT.
    3. canReadObject. Principal-less answers false after isSystem; the zero-set arm that used to answer isPrincipalLessContext(context) answers false. RIGHT.
    4. canWriteObject. Principal-less answers false at arm 4, after the engine-owned and delegated-admin pre-resolution arms and before any set resolves, mirroring the middleware's order; the zero-set arm answers false. RIGHT.
    5. canExport. As item 3. RIGHT.
    6. resolveProjectionFieldMask. The early full-field-set answer for a principal-less zero-set caller is removed; the class now takes the zero-set field answers (masking rules and the per-field capability contract reach it). The seat asked this review to confirm a return instead of a throw here. CONFIRMED, RIGHT: the field projections are field answers under the published ISecurityService return contract, not admission answers; the new answer is a subset of the former one (pinned: the capability-gated field is dropped, masked fields are served masked and are not queryable); and object admission for the class is refused by the middleware and by the three probes, so no row flows on it. Items 2 to 5 are the same reading: the deny decision is the ruling's, delivered in each method's return vocabulary (a false, a deny filter), which callers already map to 403.

    One predicate, six sites. All six read isPrincipalLessContext; the key is "carries no principal" (no user id, no position, no named set), never "no user id", so a user-less named set, the guest principal and the public-form grant are decided by what they carry. That is the ruling's predicate and not shouldDenyAnonymous's wider one. RIGHT.

    Public surface. No export is added, removed or renamed: principalLessDenial and isPrincipalLessContext are module-private; no new error class or code (PermissionDeniedError / PERMISSION_DENIED are the engine's existing deny vocabulary); ISecurityService signatures are untouched; no packages/spec path is in the diff. check:api-surface runs inside TypeScript Type Check, success on the head. No public-surface change. RIGHT.

    metadata-protocol opt-ins (src/protocol.ts, twelve engine calls across ten functions: promoteDraftForPublish, resolveDraftOrgScopeForPublish, resolveMetaItemOrgScope, auditMetaItem, diffMetaItem, listCommits, revertCommit, rollbackToPackageCommit, metaTypeNamespaceExists, migrateStoredMetadata, and the deleteMetaItem legacy read and delete). Each is a sys_metadata, sys_metadata_history, sys_metadata_audit or sys_metadata_commit store call behind a door that authorized the caller, with the where built by the protocol. Neutrality under the seat's standing rule (an isSystem context skips the six pre-refusal gates) was checked against the gates' own object sets on origin/main: audience-anchor keys on sys_position_permission_set; package-managed on sys_permission_set; system-row on the RBAC provenance map (sys_metadata absent); curated-capability on sys_capability; delegated-admin on the RBAC link tables; the engine-owned guard bypasses a context-less write by construction. None keys on a sys_metadata* object, so none fired on these calls before the move and none is skipped after it. Reach is unchanged; the moves are neutral. RIGHT. (A principal-less one would have made the audit door answer 503 and the diff door an empty diff under the deny, so closing them in this round is what the ruling's sequencing, producers first and the deny last, requires.)

    Test-harness producers (release row 22). Four closures: the federated-fixture insert passes its system context in the options position (it used to be passed as a bare third argument and dropped); the package-first-authoring insert and the seeder-copy reads take the explicit opt-in; the share-links engine double merges the trailing read options' context as mergeReadContext does. Test-side only. RIGHT.

    Pins against the ruling's three. (1) A principal-less context refused on read and write: principal-less-strict-mode.test.ts, six spellings of "no principal" (no context, empty context, empty arrays, explicit isSystem: false, tenant id alone, provenance only) across four read verbs and three write verbs, each refused before the operation runs with code and status, the probes false, the row scope the deny sentinel. (2) An explicit system context unchanged: same file, with and without a principal. (3) The dogfood suite green under the deny: Dogfood Regression Gate shards 1/3, 2/3, 3/3 and the rollup, all success on the head. Further: eight hand-through pins flipped to refusal pins (zero-set-deny-baseline, zero-set-masking, zero-set-capability-fold, metadata-unresolvable-posture, predicate-related-read-tenant-scope, security-plugin.test.ts); the composed Q2 pin (owner-of-private-object-under-strict-mode.dogfood.test.ts: the owner is found by the declared route, nobody else is, and a principal-less read of the private object is refused in the same composition); the resolveRequestScope fault-class pin over the real dispatcher plugin and the real middleware (engine handed no principal, store never reached, 403 with nothing of the rows; guest and signed-in controls); and the deny-round block of protocol.platform-store-system-opt-in.test.ts driving every moved producer. Six ablations reported red and restored by blob. RIGHT.

    Check-runs on the head, latest per name. Every required context is success: Lint & Repo Gates, TypeScript Type Check, Test Core (1/6 to 6/6 and the rollup), Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard. Also success: Check Changeset, Check PR Size, Part-of PR must not also close its card, The card this PR closes must claim this branch, both single-writer guards, the four Type Check legs, Dogfood Verify CLI, Spec property liveness, Auto Label, Check Documentation Links, Flag docs affected by code changes, filter. Skipped by path filter: Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in). No failure, nothing in progress.

    Form. Draft; head repository equals base repository (not a fork); 17 files, +931/−149 (1083 changed lines, under the 5,000 ceiling); 0 governed paths; line 1 Fixes #21908; line 2 Clause-②: no (narrowing); the title carries the ! banner; the newest Claim: on the card names this branch.

    ② Semver level

    Changeset .changeset/21908-principal-less-strict-mode.md: @objectstack/plugin-security: minor, @objectstack/metadata-protocol: patch.

    • What the diff publishes. plugin-security: a runtime accept-set narrowing of the engine middleware and the four ISecurityService probes, breaking for in-process code that reached the engine with no context. metadata-protocol: an internal fix (its own store calls take the explicit opt-in; nothing a door answers changes). runtime and qa/dogfood carry test files only (runtime publishes dist; dogfood is private), so no changeset is owed for them. The changeset matches the diff.
    • minor for a breaking change. scripts/check-changeset-no-major.mjs is the launch-window guard: a PR may not introduce a major, and a breaking change ships as minor with the BREAKING banner (pre-1.0 semantics over the lockstep fixed group, with a written end condition at GA). The banner and the ! are present; Check Changeset is success, and the Lint gates' no-major and check-adr-0087-registration verdicts are green on the head. RIGHT.
    • Migration prose. The body states FROM (an engine call with no context, or one that names nobody) and TO (the caller's execution context, or context: { isSystem: true } for platform plumbing whose own door already authorized the caller), plus the one-line fix, which is what an upgrading agent greps in CHANGELOG.md. RIGHT.
    • ADR-0087 disposition. Stated in the changeset body, in the HTML-comment marker the gate reads, as not-required (no-migration-prescription), with the facts that close the other categories: no spec key, authorable spelling, export, type or stored shape moves, and no stored row is read or rewritten, so there is nothing for migrate meta. The narrowing is a runtime admission. RIGHT.
    • The declaration line, read the same in the PR body, the claim and the changeset:

    Clause-②: no (narrowing)

    No accepted input widens; an admission that used to be accepted is refused. The arm is the right one.

    ③ Boundary flags

    Every dev flag (report 6060614288: deviations, open_questions, out_of_scope_findings) and the thread's open items, each answered or escalated:

    1. deviations[0], nine protocol.ts functions closed beyond promoteDraftForPublish. ANSWERED, accepted: same class, same mechanical opt-in, same gate family, no other claimant on those functions at the edit (the seat's check), each neutral under the six gates (① above). Leaving them would have breached the ruling's sequencing, since the deny would break the audit and diff doors. The claim surface is appended (6055574466) and the [PM seat] domain:engine — 🟢 sunhaolin #6367 cross-lane declaration amended (6060736542, per 6060777536).
    2. deviations[1], the five service sites return rather than throw. ANSWERED, confirmed (① items 2 to 6).
    3. deviations[2] and [3], the single-pass H1 method and the scoped re-runs after merging origin/main. ANSWERED: the check-runs on the head judge the merge ref and every required context is green, the full dogfood suite included.
    4. deviations[4], the fault-class pin at the runtime tier. ANSWERED, accepted; the real-boot guest posture is pinned by test(runtime): pin the guest principal an anonymous request executes as at authRequired:false endpoints #22177's dogfood pins, green on this head.
    5. open_questions[0] / deviations[5], four ISecurityService TSDoc sentences in packages/spec/src/contracts/security-service.ts that still describe the E1 hand-off. ESCALATED and routed: the seat ruled B (6060777536), filed as docs(spec): ISecurityService TSDoc still says a context with no principal is admitted or keeps its scope; PR #22297 (#21908, ADR-0096 D5 strict mode) refuses it #22302 with Blocked-by: this PR, to land right after it in the same release. Not a FAIL: packages/spec is fenced out of this claim, the governing contract sentence (ai-service.ts, "RLS-on, sees-nothing") is the one this diff makes true, and the stale sentences over-describe an admission the runtime now refuses (fail-closed direction, no disclosure). ⚠️ Flag for the seat: the release that ships @objectstack/plugin-security from this head must also ship docs(spec): ISecurityService TSDoc still says a context with no principal is admitted or keeps its scope; PR #22297 (#21908, ADR-0096 D5 strict mode) refuses it #22302, or the published .d.ts contradicts the runtime for one release.
    6. A second stale-prose family, hand-written docs, found by this review and not in the dev's flags. content/docs/kernel/contracts/data-engine.mdx (the ExecutionContext passage) tells an author that an automation run with no resolvable identity passes only its run id, "a context that deliberately carries no principal". Under strict mode that shape is refused (the provenance-only spelling is pinned refused), and the automation lane already fails closed on a missing trigger user (content/docs/automation/hooks.mdx). The sentence is an authoring trap now, loud at runtime (403 naming both remedies), not a disclosure. ESCALATED to the seat: carry it on docs(spec): ISecurityService TSDoc still says a context with no principal is admitted or keeps its scope; PR #22297 (#21908, ADR-0096 D5 strict mode) refuses it #22302 or a docs-only sibling, same release; no content/docs edit is owed by this claim's surface. No other hand-written page describing the hand-off was found (the other matches of a principal-less-context grep describe different surfaces).
    7. deviations[6], changesets. ANSWERED in ②.
    8. deviations[7] and [8], model-free commit trailers and lock timeouts not counted. NOTED; no contract bearing.
    9. deviations[9] and out_of_scope_findings[2], the census population by string match and the static sweep over literal object names. NOTED as a residual coverage gap, bounded by the runs and by CI on this head; a producer missed by both would surface as a loud PERMISSION_DENIED, fail closed, never a hand-through. No escalation.
    10. out_of_scope_findings[0], loadMetaFromDb and reportUnhydratableOrgScopedRows read context-less at boot. VERIFIED on origin/main: ObjectQL's plugin calls restoreMetadataFromDb on its start path, plugin-security declares dependencies = ['com.objectstack.engine.objectql'], and the engine middleware is registered inside plugin-security's start(), so those reads precede the middleware in every kernel that honours the declared order. Not a producer today. ⚠️ Flag for the seat, non-blocking: the two reads rely on registration order rather than a declared posture; giving them the explicit opt-in would make them order-independent. If the order ever changed, the failure direction is a loud boot refusal, not a hand-through.
    11. out_of_scope_findings[1], an ExecutionContext passed in an engine verb's options position compiles and silently drops the context. NOTED; one fixture fixed here, and the trap is loud under strict mode. A type-level narrowing of that parameter is spec-lane work; not owed here.
    12. The domain:cli seat's finding (6052739434), the resolveRequestScope infrastructure-fault class. ANSWERED: closed by construction and pinned at the runtime tier; http-dispatcher.ts untouched, as the claim required.
    13. The ADR-0096 D5 dated note (ruling Q3 甲). PR docs(adr): ADR-0096 D5 dated note — strict mode lands ON ahead of D2 and the telemetry gate #22298, docs/adr/** only, Tier H, awaiting the maintainer's word; the seat never readies, queues or approves it. This PR's landing shape (6019001864, State) does not wait on it. NOTED. No scripts/adr-anchors/ entry was added for the new refusal site; check:adr-anchors is green on the head, so none is owed by the gate; an optional follow-up.
    14. Governance. 0 of 17 paths on the register. This record is the at-tier contract review the ruling required before the queue for a security boundary, not a Tier S landing record.

    Disclosure held at classes, positions and functions; no reproduction sequence is written here.

    Implemented-by: claude/issue-21908-principal-less-deny
    Reviewed-by: session_01WkL6Eijt432S1Y7ekb6ovQ

    VERDICT: PASS


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat ACCEPT: PR #22297 at 454d0e89b9 · seat domain:services#1 (#6021) · session_01WkL6Eijt432S1Y7ekb6ovQ · 2026-10-08T13:49Z

    ⛔ Classes, positions and functions only.

    This completes the seat's review 6060777536, which read the form, the diff and the dev's flags on this head.

  11. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 1 (#6021) · session_01WkL6Eijt432S1Y7ekb6ovQ · 2026-10-08T14:41Z.

    ⛔ Classes, positions and functions only.

    PR #22297 merged through the merge queue as a3bcbcf3. ADR-0096 D5 strict mode is on origin/main:

    • @objectstack/plugin-security (minor, BREAKING) refuses a data-engine context that carries no principal and is not a system context, at the six hand-off sites. The middleware throws PermissionDeniedError (403 PERMISSION_DENIED), the three probes answer false, and the row scope is the deny filter.
    • @objectstack/metadata-protocol (patch): the census's producers take the explicit system opt-in behind their already-authorized doors.

    The PR's Fixes line did not close the card (the measured failure mode recorded on #6021); the seat closed it completed through the relay in this act. This note also removes pm:dispatched and the assignee.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:servicespriority:p1High: required for production / M2security

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions