Skip to content

QA run · access-security (9/20 items consulted) · e4e5c6e3 · 2026-08-17 · 1 PASS / 7 PARTIAL / 0 FAIL / 2 BLOCKED / 10 NOT-RUN #9330

Description

@os-zhuang

R3 runner, area 1 of 4. Part of the #9296 wave. No clause failed. Ten items were not reached and are recorded not-run rather than inferred — see Coverage honesty.

⚠️Title deviates from the wave's (FULL area) convention on purpose: this run consulted 9 of 20 items, so claiming a full area would be false. Scope is stated per item below.

Environment fingerprint

framework sha (subject)e4e5c6e3c608b1b807c83a0d5b734f213eb1a1dd — unmoved all run
.objectui-sha (pin)665661ab093263f39f2e660a295ea615dbcee35a
console dist stamp665661ab0932pnpm check:console-sha"Console dist matches the objectui pin"
ports3300 (smoke), 3301, 3302, 3303 — band 3300–3399
dbsfile:/tmp/qa-r3b/<area>-<item>.db, rm -f before each boot
seed--seed-adminadmin@objectos.ai; members via real POST /auth/sign-up/email
boot smoke/api/v1/health 200 in 2.2s; /_console/ 200, text/html, app shell present
date2026-08-17

Origin note: better-auth refuses 127.0.0.1 with 403 INVALID_ORIGIN. All driving used http://localhost:<port>.

⚠️ Spec provenance — read this before trusting the revision column

The brief said to take the checklist forward from origin/main once PR #9309 merged. #9309 has not merged — it is open, CI-green, and explicitly held ("no auto-merge, not enqueued", pending the maintainer). Waiting would have stalled all four areas indefinitely.

So the spec was taken from PR #9309 head fbd1c0491215406c379405cef262befad1de458d, not from merged main. Validated with the pinned validator (#9309 is docs-only, so scripts/ is the subject's own):

check-platform-checklist: OK — 15 areas, 190 items (190 active); coverage: 30 kinds mapped, 0 waived.
✓ checklist-select self-test: 17 cases pass.

git status showed 8 modified paths, all under docs/qa/platform-checklist/, 0 elsewhere. Never committed. If #9309 changes before merge, re-check the revisions cited here.

Selector: node scripts/checklist-select.mjs area:access-security --json → 19 runnable + 1 hidden-blocked = 20, matching the post-sweep ledger.

Setup finding — the wave's documented recipe does not reach a bootable server on a cold tree

Following the brief exactly (pnpm install → client prebuild → pnpm objectui:build → boot) the smoke failed:

› Error: command dev not found

packages/cli had no dist at all — the @objectstack/client... filter does not reach it, and objectui:build builds the console, not the framework CLI. Fixed with pnpm exec turbo run build --filter=@objectstack/cli... --filter=@objectstack/example-showcase... (60 tasks, 1m19s). Not a product defect — the recipe assumed a tree R0 had already fully built — but the setup steps as written are incomplete for a fresh container. Worth folding into the R0 card.

The #9307 workaround itself behaved exactly as documented: client prebuild 32 tasks / 6m08s, then objectui:build succeeded first try.

Automated pins (RUNNER rule 6)

One vitest invocation over the area's 12 pinned files — 12 files / 91 tests / 91 passed / 0 failed, 79.56s. Re-run with --reporter=json for per-test citation (/tmp/qa-r3b/access-security-pins.json). Per-file: anonymous-deny-surfaces 25 · permission-zoo 13 · owner-anchor-and-bulk-writes 9 · scope-depth-write 9 · scope-depth 7 · private-owd 5 · readonly-when-parent 5 · controlled-by-parent 4 · invoice-cbp 4 · scope-depth-fallback 4 · public-read-owd 3 · static-readonly 3.

objectstack verify --rls --json (the packages/verify pin), stock showcase, multiTenant: false:

rls : {objects:23, consistent:20, holes:0, memberVisible:0, probeBlocked:0, skipped:3, proven:20, unproven:3}
crud : {objects:23, verified:15, fidelityGaps:0, createFailed:0, needsFixture:1, readFailed:0, skipped:7, mismatchTotal:0}
hardFailures: 0
positionCoverage: declared 9 / ran 9 / notRun []

Per-clause verdicts

access-security.rls-both-sides — rev 3, P0, api → partial

clauseverdictoracle evidence (text)
A0 list isolation both wayspassA list 200 ["A note 1","A note 2"]; B list 200 ["B note 1"]; each excludes the other
A1 foreign by-id READ deniedpassGET /data/showcase_private_note/<B id> as A → 404 RECORD_NOT_FOUND (not-found-shaped denial, which the clause admits alongside 403)
A2 foreign by-id WRITE denied + row unchangedpassPATCH as A → 403 PERMISSION_DENIED; admin re-read body"b-original" before and after
A3 admin reads full setpassadmin list 200, n=3, contains both A's and B's ids
A4 runRlsProofs holes = 0passsummary.holes: 0 over 23 showcase objects, 20 proven consistent
A5 skips explainable + D11-judged spot-checkblocked(dependency)explainability half proven — the 3 skips each name a benign reason (below). The spot-check half is judged against a write-up delivered privately under the #7463 ruling, which this runner does not hold. Per the clause's own instruction it is not ticked green.
N0 no silent foreign writepassforged PATCH 403, rowMutated=false

The 3 skipped rows, each with a named benign reason: showcase_project_membership"no plain-text probe field"; showcase_ext_customer / showcase_ext_order"external read-only object (federated datasource "showcase_external"; no inserts)".

access-security.write-path-guards — rev 2, P0, api → partial

clauseverdictoracle evidence (text)
A0 readonly stripped on INSERT, create still succeedspassPOST 201 with X-ObjectStack-Dropped-Fields: lead_score;reason=readonly; readback lead_score absent (payload sent 99) — admit-and-strip, #3431 advertised
A1 readonly stripped on UPDATEpassPATCH 200, same dropped header; lead_score unchanged; sibling editable namedid land
A2 owner forge on INSERT denied, no rowpass403 "'owner_id' … is system-managed — creating a record owned by another user requires the transfer grant"; admin-side planted count 0
A3 transfer + disown denied, self-owner insert OKpassboth PATCHes 403; owner re-reads as A; POST owner_id=<self> → 201, readback owner = A
A4 empty-owner bulk insert stamps callerpassPOST /data/showcase_private_note/batch {operation:'create',records:[…]} → 200, succeeded:2; both rows read back with the single owner = A's id
A5 readonlyWhen locks on paid invoice + linespass (pin)showcase-readonly-when-parent 5/5 incl. "a paid invoice's frozen line cannot be rewritten over the API", "leaves an unlocked field on the same frozen line writable"
A6 bulk update validates per row (#3106)not-runno mixed-batch probe driven
N0 same ops as admin succeedpassadmin POST owner_id=<B> → 201, readback owner = B — guard gates on privilege, not on the key
N1 isSystem may still set readonlynot-runneeds an objectql write with context:{isSystem:true}

showcase_contact confirmed from live metadata: requires name+email; lead_score is readonly:true with no defaultValue — matching the fixture.

access-security.anonymous-deny-surfaces — rev 1, P0, api → pass

clauseverdictoracle evidence (text)
A0 every surface 401 UNAUTHENTICATEDpass (pin)25/25; /meta, /data, /actions, /automation (trigger, list, delete), /packages (list, export, discard-drafts, publish-drafts)
A1 deny precedes resource resolutionpass (pin)anonymous action probe 401; and the member-side contrast is pinned separately (#7867: member + nonexistent id → 404 RECORD_NOT_FOUND, same as the data surface)
A2 exactly two envelope families, no third dialectpass (pin)6 per-surface assertions each naming its single declared family — rest-flat for /meta/data, dispatcher-wrapper for /actions/automation; plus "ANONYMOUS_DENY_BODY is the REST seam body, and NOT the dispatcher one (#5632)"
A3 authenticated member unaffectedpass (pin)member allowed on /meta, data surface, action surface
A4 declared-public routes stay publicpass (live)anonymous GET /forms/<slug>, GET /forms/<slug>/lookup/<f>, POST /forms/<slug>/submit404 FORM_NOT_FOUND (the handler's own error — auth did not intercept); same-server contrast anonymous GET /data/showcase_private_note401 UNAUTHENTICATED
A5 every variant probedpass (pin)all listed families driven; none left unprobed
N0 destructive automation denied, flow survivespass (pin)"anonymous DELETE /automation/:name is denied (401) — the destructive one" + authed caller reaches the domain (501, not 401)

access-security.crud-permission-matrix — rev 1, P0, api → partial

Engine-level halves proven by the pins: runCrudVerification 23 objects / 15 verified / 0 fidelityGaps / 0 mismatchTotal; runRlsProofs0 holes; positionCoverage all 9 declared positions ran (notRun: []); permission-zoo pins the VAMA read bypass, the read-only auditor's write refusal, and delegated-admin subtree limits (13/13).

The 31-row × 4-verb per-cell matrix from access-matrix.json was not driven cell-by-cell — A6 explicitly makes an undriven cell set at best partial, so that is the verdict. A0–A5 are supported but not per-cell proven; not ticked.

access-security.owd-sharing-matrix — rev 2, P0, api → partial

modelverdictevidence
private (A0)pass (pin)private-owd 5/5 — owner-only list, foreign by-id read refused, foreign write refused, symmetric for the other member
public_read (A1, A2)pass (pin)public-read-owd 3/3 — every member reads a foreign announcement, only the owner edits; explicit contrast with private
controlled_by_parent (A4, A5)pass (pin)controlled-by-parent 4/4 + invoice-cbp 4/4 — derived read and by-id write refusal, plus "NOT over-blocked" for the owned master
public_read_write (A3)not-runneeds a live C1-vs-A PATCH pair on showcase_project
A6 all four models recordedpartialthree of four driven

A4 carries the dedupe note pointing at rls-both-sides clause 5; that verdict is recorded there only, once (#7687).

access-security.scope-depth-asymmetry — rev 1, P1, api → partial

Pins (20 tests across 3 files) prove the depth semantics directly: read widening to BU co-members but not child/sibling BUs; subtree BFS descent; manager-chain BFS; the read/write asymmetry ("the co-member's note is READABLE (readScope 'unit')… yet NOT editable"); writeScope 'unit' widening a co-member edit; a child-BU member's note not editable; and three separate fail-closed degradations ("degrades to owner-only — no widening, never fail-open"). A5's explicit diff of recorded outcomes against access-matrix.json's readScope/writeScope columns was not performed → partial.

access-security.record-access-explain — rev 2, P1, mixed → partial

Pin covers the explain attribution case: "explain() reports the vama_bypass layer with the auditor set as contributor". Console-side clauses not driven.

access-security.fls-mask-and-strip — rev 1, P1, mixed → partial

Pin covers the write half only, as the item's own automated.ref states: "FLS: a contributor can edit a project but NOT its budget fields". Read-masking half not driven.

access-security.no-active-org-session-semantics — rev 1, P0, api → blocked(fixture)

New in the R4 sweep; never run before. The state under test is not reachable on the stock fixture, and the deployment shape that would make it reachable cannot be booted from this repo. Both halves measured, not assumed:

  1. Stock posture is single, so the wall is inert.resolveTenancyPosture() returns single when OS_TENANCY_POSTURE/OS_MULTI_ORG_ENABLED are unset; postureEnforcesWall('single') === false; computeTenantLayer0Filter returns null before reaching any deny sentinel. objectstack verify independently reported multiTenant: false. The item itself says every clause is vacuous on an unwalled deployment. Rows do carry a real organization_id (e.g. org_msxay12mhknse86x) from the ADR-0081 default-org bootstrap, so a session with no active organization is not reachable this way either.

  2. Provisioning the wall is refused at boot.OS_TENANCY_POSTURE=isolated on the showcase → server never listens (health=000), FATAL:

tenancy posture 'isolated' was requested but @objectstack/organizations could not be loaded, so the organization wall is INACTIVE. Refusing to bootcause: Cannot find package '@objectstack/organizations': the host app does not declare it.

That refusal is the correct ADR-0093 D5 / ADR-0105 D12 behaviour ("you never silently get an unenforced organization boundary") and is recorded here as a positive observation, not a defect. Its consequence for this item is that the fixture is unprovisionable in the open repo: the offered escape OS_ALLOW_DEGRADED_TENANCY=1 boots explicitly degraded single-org, i.e. still no wall, so it does not supply the fixture either.

Checklist feedback (not a code defect): the item's knownGaps anticipate that producing the no-active-org state is awkward (racy signup vs. removed-member), but not that the walled deployment itself is unavailable on the stock showcase without an enterprise package. As written, the item is unrunnable here regardless of which producer a run picks. It needs either an @objectstack/organizations-bearing fixture or a blocked marker. Recommend a checklist edit (revise + bump revision + append history) rather than leaving future runners to re-derive this.

access-security.share-link-capability-tokens — rev 1, P2, api → blocked(fixture)

Carried blocked in the ledger; hidden by the resolver by design. Recorded, not run.

Not-run (10)

No oracle consulted — not inferred from siblings: readonly-package-locks-studio (P2) · sharing-rules-widen (P1) · record-share-grant-revoke (P1) · audit-log-browser (P1) · owd-save-gate (P1) · public-form-intake (P1) · suggested-binding-loop (P2) · sharing-rule-authoring-ui (P2) · permission-matrix-edit-loop (P1) · capability-declaration-lifecycle (P1).

Two notes for whoever picks these up. suggested-binding-loop's clause 0 is a known expected-fail on stock (#7677 — stock auto-binds the only isDefault suggestion at boot and surfaces no row at all); its knownGaps carry the unbind sequence that provisions a genuine pending row. And per the item's own N2, an empty list must never be ticked green.

Coverage honesty

9 of 20 items consulted; no clause failed; 1 item fully passed, 7 partial, 2 blocked, 10 not-run. The area's two-sided gate discipline held everywhere it was exercised: every refusal above was proven server-side on a forged direct request, with an admin re-read confirming non-mutation, and every guard was paired with the entitled-persona contrast that proves the route is not simply broken (wrong-persona). No verdict rests on UI absence, and no DOM oracle was used.

Fixture gaps found: the tenancy-wall gap above (P0 item unrunnable) and the pre-existing share-link-capability-tokens block. Neither is a product defect. #9308 remains the home for the seed-shaped second-login gaps — though note that stack.signUp() / POST /auth/sign-up/emaildoes yield real loginable second personas, which is what let every gate here be driven from both sides without one.

Links: #9296 (wave) · #9309 (spec provenance) · #9307 (console-build flake, worked around as documented) · #9308 (fixture card).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions