Skip to content

QA run · tier2b:browser-1 (21/21) · e4e5c6e3 · 2026-08-18 · 7 PASS / 12 PARTIAL / 2 FAIL #9453

Description

@os-zhuang

Tier-2B runner for the platform-checklist QA wave — 21 unpinned P0/P1 items on the browser / mixed surfaces, the half no automation touches. Every one of the 21 carries a verdict. Links #9296.

Environment fingerprint

framework sha (subject, pinned)e4e5c6e3c608b1b807c83a0d5b734f213eb1a1dd — never moved; git log e4e5c6e3c..HEAD = 0 commits
.objectui-sha665661ab093263f39f2e660a295ea615dbcee35a
console dist stamp665661ab093263f39f2e660a295ea615dbcee35aequals the pin; objectui:build/objectui:refresh never run
tree/home/user/objectstack-qa-wave (detached at the pin)
spec (checklist)re-synced from origin/main at start (83fe945d6..1b93550af); validator: 15 areas, 190 items (190 active), 30 kinds mapped, 0 waived
instancesA: localhost:3161 / file:/tmp/qa-wave/t2b-a.db · B: localhost:3162 / file:/tmp/qa-wave/t2b-b.db (both rm -f'd before boot)
personasadmin admin@objectos.ai · member qa.member.t2b@example.com provisioned in-run via POST /auth/sign-up/email (id i8CaANTg7X…)
browserplaywright-core 1.62.1 with executablePath=/opt/pw-browsers/chromium-1194/chrome-linux/chrome
livenesscurl /api/v1/health only — ss never used
concurrencymax 2 instances; no test suite ever run beside a build
final stateboth servers stopped; probe metadata/packages removed; git status shows only docs/qa/ paths; 0 commits

Scope + per-item revision + derived verdict

#itemprisurfacerevverdict
1platform-core.boot-healthP0mixed2pass
2platform-core.console-loginP0browser2pass
3platform-core.builtin-apps-nav-renderP0browser1partial
4platform-core.settings-hub-roundtripP1mixed2partial
5platform-core.package-lifecycle-enable-disableP1mixed1partial
6platform-core.notification-centerP1mixed1partial
7records-forms.crud-roundtripP0browser3FAIL
8records-forms.form-view-galleryP1browser1partial
9records-forms.record-discussion-mentionsP1browser1partial
10studio-authoring.first-run-loopP0mixed1pass
11studio-authoring.view-authoring-liveP1mixed1FAIL
12studio-authoring.authoring-validation-not-persistedP1mixed2partial
13approvals.account-app-entryP0browser1pass
14approvals.per-group-signoffP1browser4partial
15approvals.inbox-metadata-actionsP1browser3pass
16approvals.decision-action-matrixP1mixed2pass
17access-security.audit-log-browserP1mixed1pass
18access-security.owd-save-gateP1mixed1partial
19access-security.public-form-intakeP1mixed1partial
20access-security.permission-matrix-edit-loopP1mixed1partial
21access-security.capability-declaration-lifecycleP1mixed2partial

7 PASS / 12 PARTIAL / 2 FAIL / 0 not-run items. No access-control finding — the #9296 carve-out was not triggered.


Per-clause verdicts

Oracle evidence is text only. Screenshot oracles are recorded as a one-line description of what the screenshot showed; no images are attached or linked anywhere.

1 · platform-core.boot-health — pass

clauseverdictoracle evidence
c0 health 200 in boot windowpasscurl /api/v1/health → 200, {"status":"ok","uptime":10.52}; first 200 at ~10s from launch
c1 ready 200passcurl /api/v1/ready → 200
c2 no ⚠ flows / no ERROR linespassbanner Flows: 29 flow(s) 22 bound to triggers · 7 draft — no ⚠; zero ERROR-level lines in the 46-line boot log; Seeds: com.example.showcase 130 rows, no SeedLoader rejection
c3 console shell + app metadatapass/_console/ → 200 text/html 6467 B; merged app metadata resolves via ?id=showcase_app (see checklist finding CF-1)
c4 served nav == authored navpass58 served nav ids vs 58 authored in src/ui/apps/index.ts; zero diff in either direction; grp_data serves all 12 objects

Recorded observation: the boot log carries a ⚠ Boot diagnostics — 3 warnings banner — #8686 autonumber tenancy split on showcase_field_zoo.f_autonumber (already filed), plus two SHARING_NOT_ENABLED rule-backfill warns. All WARN, no ERROR; the clause as written is satisfied.

2 · platform-core.console-login — pass

clauseverdictoracle evidence
c0 login lands in shellpassscreenshot showed the ObjectOS home shell — "Working late, Dev Admin", "Your apps 2", nav + header; URL /_console/home
c1 session survives reloadpasspost-reload trace: /api/v1/runtime/config 200, /auth/get-session 200, /i18n/locales 200, no redirect to login
c2 expired session → 401 from the SERVERpassafter POST /api/v1/auth/sign-out (200): GET /api/v1/data/showcase_account?$top=1401 UNAUTHENTICATED; GET /api/v1/meta/app?id=showcase_app401 UNAUTHENTICATED
c3 console returns user to loginpassnavigation after true expiry redirected to /_console/login?redirect=%2Fhome; screenshot showed the "Sign in to your account" card — not a dead shell
c4 re-auth restorespassre-login → /_console/home, POST /auth/sign-in/email 200, zero 401s in the post-login trace, fresh GET /data/showcase_account 200, new bearer differs from the old
negative (step 8)passwrong password → 401{"code":"INVALID_EMAIL_OR_PASSWORD"}

3 · platform-core.builtin-apps-nav-render — partial

clauseverdictoracle evidence
c0 every destination renders (3 apps)blocked(fixture)Studio app absent (see FG-1). Setup 43/43 and Account 7/7 destinations walked screenshot-first then DOM-checked: 50/50 ok, zero pageerror, zero blank <main>, zero "failed to load"/placeholder text
c1 served nav == authored shell (3 apps)blocked(fixture)Setup serves all 9 authored group anchors (group_overview/apps/people_org/access_control/approvals/configuration/diagnostics/integrations/advanced) filled to 43 leaves; Account serves Profile + Inbox/Security/Developer (10 ids). Studio unavailable
c2 member refused Setup+Studio, Account opensblocked(fixture)Setup: absent from the member's /meta/app; /apps/setup root falls back to the member's own app; Account opens and renders their Profile. Studio leg unavailable
c3 app gate is server-sidepassforged member GET /api/v1/meta/app?id=setup{"type":"app","items":[]} — the server prunes, it is not a launcher courtesy
c4 gated ENTRIES absent from member navpassadmin-vs-member diff: nav_api_keys + the 10 nav_settings_* (manage_platform_settings) appear only in the admin set. Named limitation: on this fixture the proof degenerates to app-level pruning — the member holds no setup.access, so no member payload contains a manage_platform_settings entry to prune entry-by-entry
c5 service/object gates resolve to ABSENCEpassnav_organizations (requiresService: org-scoping) absent in single-org mode; nav_business_units (requiresObject: sys_business_unit) present, object registered
c6 off-capability destinations prunedpassno nav_sso_providers on stock boot (knownGap holds); no nav_jwks for any persona incl. admin (#7544); sys_verification/sys_device_code absent

Server-truth control run alongside (member vs admin, direct API): sys_permission_set 403/200 · sys_audit_log 403/200 · sys_capability 403/200 · /api/settings/localization 403/200 · sys_user RLS-scoped 2 rows vs 4. The app-level gate is intact and no privileged dataset leaked.

4 · platform-core.settings-hub-roundtrip — partial

clauseverdictoracle evidence
c0 value round-trips, source flipspassPUT /api/settings/localization {timezone:'Asia/Tokyo'} 200; re-read {"value":"Asia/Tokyo","source":"tenant",...,"cascadeChain":[{"scope":"tenant",...,"effective":true},{"scope":"default","value":"UTC"}]} — default→tenant
c1 EFFECT reaches a consumerblocked(fixture)no observable consumer surfaced: /api/v1/auth/me/localization reports {timezone:null,locale:null,currency:null} (user scope, not the tenant cascade); the analytics date-bucket route 404s at the path tried; the sanctioned fallback branding.workspace_name persists (source:'tenant') but does not render in the console shell (shell still reads "ObjectOS"). Per the item's own knownGap, effect is not ticked off a persisted form
c2 audit row per writepasssys_setting_audit row: {namespace:'localization', key:'timezone', action:'set', source:'api', actor_id:'VcatFzcx…', new_hash:true, scope:'tenant'}
c3 console source badge flipspassscreenshot of /apps/setup/system/settings/localization showed the timezone row reading **"Overrides Default
c4 env-pinned key is server-authoritativepasswith OS_LOCALIZATION_TIMEZONE=Europe/Paris: {"value":"Europe/Paris","source":"env","locked":true,"lockedReason":"Set via env: OS_LOCALIZATION_TIMEZONE"}; PUT {timezone:'UTC'}409 SETTINGS_LOCKEDreason:'locked-by-env'
c5 EnvLockBadge + non-editablepassscreenshot showed "Locked by env" beside the timezone field (and beside mail Password), with the control disabled
c6 test action does not fake successpassPOST /api/settings/mail/test400 SETTINGS_ACTION_FAILED, {ok:false, message:"Provide a \"to\" address (or set from_email)."}
c7 secret at rest is a handlepasssys_secret row sec_998094ee611623a81f6ebef4d2586246 (ciphertext present); sys_setting row {namespace:'mail', key:'smtp_password', value_enc:'sec_998094…', value:null}
c8 POSITIVE masked read, both flavourspassmail.smtp_password served as exactly •••••••• (8×U+2022); every cascadeChain entry mask-or-null; UNSET keys (smtp_host,smtp_user,api_key,from_email) still null (presence-preserving); no plaintext anywhere in the body. Second flavour ai.openai_api_key: same — mask exact, chain masked, no leak
c9 redaction is METADATA-preservingpasswith OS_MAIL_SMTP_PASSWORD set: {"value":"••••••••","source":"env","locked":true,"lockedReason":"Set via env: OS_MAIL_SMTP_PASSWORD","cascadeChain":[{"scope":"env","value":"••••••••","locked":true,…}]}; PUT409 SETTINGS_LOCKED
c10 echoed mask is a NO-OPpassboth shapes ({smtp_password: mask} and {values:{smtp_password:{value:mask}}}) → 200 with sys_secret inventory unchanged (1 row, same id) and sys_setting.value_enc unchanged; a genuinely new value minted a different handle (sec_998094…sec_72cb82…) and its PUT response is redacted (contains the mask, not the plaintext); non-encrypted control company.legal_name set to the mask stores it verbatim. Named gap: the in-process consumer read (getNamespace, deliberately unmasked) is not reachable over REST, so "still resolves the ORIGINAL plaintext" rests on the unchanged-handle proxy rather than a direct read
c11 anonymous deniedpassanon GET /api/settings/localization403 SETTINGS_FORBIDDEN; anon GET /api/settings → 200 with zero namespaces enumerated
c12 every namespace resolvespass11 namespaces each 200 with {manifest, values}: branding 7, company 16, localization 12, auth 34, mail 16, sms 15, storage 16, ai 52, knowledge 16, feature_flags 7, lifecycle 5 specifiers. No 500, no empty manifest
guardrails (step 14)pass{timezone:'Mars/Phobos'}400 SETTINGS_VALIDATION naming the IANA requirement; {bogus_key:1}400 UNKNOWN_KEY

5 · platform-core.package-lifecycle-enable-disable — partial

clauseverdictoracle evidence
c0 disable STOPS servingpassprobe com.qa.t2bx + published object t2bx_qa_widget: GET /api/v1/data/t2bx_qa_widget 200 → after PATCH /disable (200) → 404
c1 enable RESTORESpassafter PATCH /enable (200) the same data read is 200 again
c2 uninstall DE-REGISTERSpassDELETE /api/v1/packages/com.qa.t2bx → 200 {registryRemoved:true, deleted:[{type:'object',name:'t2bx_qa_widget',state:'active'}]}; GET /meta/object/t2bx_qa_widget → 404; the package is gone from GET /packages
c3 commits GROW per publishblocked(fixture)GET /packages/:id/commits answers 200 {"commits":[]} before and after two successful publish-drafts (delta 0). The route the clause names, POST /packages/:id/publish, answers 200 but reports "No metadata items found for package" for a draft saved with ?package=<id> — so the documented publish path could not be driven. Not scored as a defect: two candidate causes (commit minting bound to a publish path I could not exercise; org-scoped listCommits filtering) were not separated
c4 revert restores prior shapeblocked(dependency)no commits exist to revert (c3)
c5 console shows the disabled package gonenot-runneeds a probe package carrying a nav entry; not driven
c6 DENY side on read-only code packagespassPATCH /disable and DELETE on bothcom.example.showcase and com.objectstack.setup422 WRITABLE_PACKAGE_REQUIRED naming the read-only cause
c7 rows survive the togglepass1 row before disable, 1 row after re-enable

Recorded observation: after disable, the data door 404s but GET /api/v1/meta/object/<probe_object> still answers 200 — a disabled package's schema stays readable through the metadata door. The clause's own verify (the data/route reads) is met, so this is filed as an observation, not a fail.

6 · platform-core.notification-center — partial

clauseverdictoracle evidence
c0 badge = distinct unread TOPICS + approvalspassbell badge read 5; the popover states it in words: "5 total · 2 notifications + 3 pending approvals". The recurring digest contributes one topic while carrying 19 rows — the row reads "Scheduled project digest ×19 … 19 unread". sys_approval_request my_pending independently = 3. Exactly the #2765 property
c1 three tabspasspopover tabs: Notifications 2 / Approvals 3 / Activity feed
c2 (topic,title) repeats COALESCE with ×N pillpassafter the screenshot confirmed the popover rendered, the group row carries the pill ×19 with a single collapsed entry. Named gap: the chevron-expand revealing the 19 members was not driven
c3 per-group mark-read is ONE requestnot-runonly global mark-all was driven
c4 mark-all-read drops the notifications portionpasscaptured POST /api/v1/notifications/read/all (200); badge 5 → 3 — notifications portion to 0, approvals portion (3) unaffected
c5 Approvals tab count == my_pendingpasstab reads Approvals 3; direct sys_approval_request my_pending query = 3
c6 read-state PERSISTS across reloadpassafter mark-all + full reload the badge stays 3 (does not revert to 5) — the sys_notification_receipt upsert held

7 · records-forms.crud-roundtripFAIL (c2)

clauseverdictoracle evidence
c0 create + verbatim re-readpassconsole "Create Account" modal → POST /api/v1/data/showcase_account201 with body {"name":"os-qa-qa872072","industry":"technology","status":"active"}; filtered re-read returns those three verbatim and the untouched optional website is null, not defaulted
c1 update persists ONLY the edited fieldpassPATCH {annual_revenue:4242} 200; the record History entry reads exactly "Annual Revenue: — → 4242" and no other field row; full-row reads differ only in annual_revenue + updated_at
c2 delete removes the rowFAILsee reproduction rule R-1
c3 list reflects each mutation after reloadblocked(dependency)create half verified — post-reload list screenshot showed the new row (Annual Revenue 4,242) and "17 records". The delete half is unprovable because the delete never succeeds (c2)
c4 History shows create + update with display valuespassafter the screenshot confirmed the tab rendered: "Dev Admin UPDATE 17 seconds ago — Annual Revenue: — → 4242" and "Dev Admin CREATE 39 seconds ago — ▸ 5 fields populated" — real actor, display values, no phantom value→null rows
c5 unicode round-tripspassos-qa-qa872072-华宁 created 201, re-read byte-identical; $search=华宁 returns it alongside the seeded 华宁科技
c6 clone re-derives engine columns, owned by clonerpassPOST /data/showcase_account/<id>/clone201{id:'Dj3B405h57_X1ldZ' ≠ sourceId:'sXI6lveJX_4Wv-km', sourceId, record}. Field diff: re-derived = id,created_at,updated_at (+ overridden name); copied = all 19 business columns. Named gap: cloner≠source-owner is untested — admin owns both on this fixture
c7 clone is RLS-gatedblocked(fixture)showcase_account is public_read_write, so no bound persona cannot read the source row — exactly the condition the item says to record blocked(fixture) for

8 · records-forms.form-view-gallery — partial

clauseverdictoracle evidence
c0 per-variant layout mechanicsnot-runall five variants exist in metadata (GET /meta/view?object=showcase_task lists showcase_task.edit / .tabbed / .wizard / .split / .quick), but no console door was found that selects a form-view variant — ?formView=<name> is ignored (all five URLs rendered byte-identical detail content, len 378). The modal variant stays blocked(fixture) per the knownGap
c1 wizard issues ONE save at the endnot-runwizard variant not reachable (c0)
c2 view-level visibleWhen works livepassin the Create Task form, screenshot first, then DOM label read: before priority=urgent labels = [Title*, Project*, Assignee, Status*, Priority, Due Date]; after setting priority=urgent via native setter + input/change = […, Due Date, **Notes**]. Notes mounts on the CEL condition
c3 every variant's save persistsnot-rundepends on c0
c4 tab/step navigation preserves draftsnot-rundepends on c0

9 · records-forms.record-discussion-mentions — partial

clauseverdictoracle evidence
c0 comment PERSISTS to sys_commentpasscaptured POST /api/v1/data/sys_comment 201 with {thread_id:'showcase_task:4JOOoiUJcrR25Knt', author_id:'EMA74oMclN…', body:'QA t2b probe @QA Member T2B please review', mentions:'["i8CaANTg7XEXXBHKvxO2ZD3TqHjn6bG3"]'}; the API read returns the row
c1 optimistic row reconciles by idpassacross the post→refetch transition the feed shows the body exactly once (Discussion (1), 1 occurrence) — no duplicate, no phantom
c2 sys_activity interleaves into the same feednot-runan attributable edit + re-read was not driven
c3 mentioned user's bell + deep linknot-runthe member session's bell was not opened
c4 recipient scoping both sidesnot-runnot driven

Positive observation for the wave: the mention autocomplete works — typing @QA opened the suggestion list with "QA Member T2B", and selecting it resolved the member's real id into the mentions array. The composer's action bar is [Bold, Italic, List, Inline code, Mention someone, Preview, Submit (Ctrl+Enter)].

10 · studio-authoring.first-run-looppass (contradicts the wave's standing "blocked")

clauseverdictoracle evidence
c0 package create round-trips, switcher lists itpassPOST /api/v1/packages {id:'com.example.repairs', label:'维修中心'}201, namespace repairs. The App Builder's "MY PACKAGES (WRITABLE)" panel lists com.example.repairs in the same session, no reload
c1 published object is server-real with its 3 optionspassGET /api/v1/meta/object/repairs_repair_ticketstatus field type:'select' with exactly [{New,new},{In Repair,in_repair},{Done,done}]
c2 record persists to the data planepassPOST /api/v1/data/repairs_repair_ticket 201; read back [{"name":"Broken drill","status":"in_repair"}]
c3 app live in the launcher, label chip renderspassHome launcher screenshot showed "Your apps 3 · Showcase · Setup · Repair Center"; the end-user list at /apps/repairs_repair_center/repairs_repair_ticket rendered **"Broken drill
c4 ZERO server restartspassgrep -c "Plugins:" boot-b.log = 1 — a single boot spans the whole loop
c5 read-only gate is SERVER-sidepassauthoring write into com.example.showcase422 writable_package_required: "that package is read-only (provided by code or an installed app), so it is not a writable base"

This overturns the standing wave fact.#9296 and the R1 record say this item is blocked(fixture) because Studio is not installed. The app registration is indeed absent (GET /meta/app = showcase_app, setup, account) — but the Studio App Builder UI is reachable at /_console/studio and at /_console/apps/setup/component/studio/builder, and Home carries the "Build an app" tile the item's step 2 starts from. The whole loop closes. See CF-4.

11 · studio-authoring.view-authoring-liveFAIL (c3)

clauseverdictoracle evidence
c0 draft receipt with state + versionpassPUT /meta/view/qa_repair_asset_views?mode=draft{"success":true,"version":"sha256:06fc841b…","seq":2,"state":"draft"}
c1 pre-publish the draft is staged onlypasswhile drafted: GET /meta/view?object=qa_repair_asset[] and direct GET /meta/view/qa_repair_asset_views404 — no draft leakage
c2 post-publish the list renders authored columnsblocked(dependency)precondition provably fails (c3); no screenshot taken, so no screenshot verdict is reported
c3 published container round-trips through the consumer doorFAILsee reproduction rule R-2
c4 edit + re-publish updates the live listblocked(dependency)same precondition failure

12 · studio-authoring.authoring-validation-not-persisted — partial

clauseverdictoracle evidence
c0 invalid object rejected with a LOCATED errorpass422 INVALID_METADATA, message [invalid_metadata] object/qa_invalid_probe2 failed spec validation: fields.amount.type: …, issue {path:"fields.amount.type", code:"invalid_value"}
c1 wrong-layer view key rejected with guidancepasscontainer-level type/columns422, text carries the ViewSchema prescription: "Wrap it: defineView({ list: { type, data, columns, … } }), or name it — defineView({ listViews: { my_view: { … } } })"
c2 rejected drafts NOT persistedpassGET after each rejected PUT → 404 for both names
c3 browser door surfaces the same located errorblocked(fixture)the object designer refuses to edit either candidate: a runtime-authored object reads back as source "Artifact" with "This object is provided by an installed package, so it is read-only at runtime", and no editable Settings panel is offered. Driving an invalid save through the designer needs a draft-state object in a writable package that the designer will open for editing
c4 corrected shapes save 200passcorrected object → 200 state:'draft'; corrected view (wrapped under list:) → 200 state:'draft'

Known spurious _diagnostics banner (knownGap) was not observed this run.

13 · approvals.account-app-entry — pass

clauseverdictoracle evidence
c0 registration survives BUNDLINGpassdist/.objectui-sha == .objectui-sha == 665661ab0932…; grep -o 'approvals:inbox' packages/console/dist/assets/index-B1QKp1HQ.js = 1 hit, in context V({ref:'approvals:inbox', label:'Approvals Inbox', source:'@object-ui/console', component:…})
c1 entry renders the FULL inboxpassas the member, screenshot of the nav entry's target showed tabs My Pending 1 / Submitted by me / All, a request row (Invoice Dual Sign-off · INV-1010 · Pending) and the keyboard hint bar. Not a component-not-found state. URL /_console/apps/account/component/approvals/inbox
c2 a NON-ADMIN reaches itpassthat session's GET /api/v1/meta/app = ["showcase_app","account"]no setup — and the same session renders the inbox
c3 decision actions render and one moves the requestpassmember drawer rendered the server-declared set [Approve, Reject, Reassign, Send back, Request info, Close]; the approve executed and the timeline gained approve by i8CaANTg7X… (member); the request left the member's pending slate
c4 ?request= deep-links into the drawerpasscold-load /apps/account/component/approvals/inbox?request=areq_2439498e… opened the drawer on that exact request — screenshot showed "INV-1010 … Waiting on QA Member T2B ×2"
c5 record link keeps the CURRENT app segmentpasshref = /_console/apps/**account**/showcase_invoice/record/GsufOSAcr7Nar-c- — the mounting app, never setup
c6 zh-CN wording consistentpasswith locale zh-CN, screenshot showed nav entry 待我审批, tab 待我审批, status badge 待审批 (page chrome 审批中心 / 我发起的 / 全部)

14 · approvals.per-group-signoff — partial

clauseverdictoracle evidence
c0 both groups 0/1, two distinct pending userspassEXP-2001 areq_cba953dc…: status:'pending', pending_approvers:['EMA74oMclN…','usr_showcase_auditor_demo'], pending_approver_groups:{admin:['manager'], auditor:['finance']}, decision_progress:{behavior:'per_group', got:0, need:2, groups:[{finance,0/1,false},{manager,0/1,false}]}
c1 manager satisfied and dropped, still pending on financepassafter the admin's approve: pending_approvers:['usr_showcase_auditor_demo'], decision_progress:{got:1, need:2, groups:[{finance,0/1,false},{manager,1/1,**satisfied:true**}]}, status still pending
c2 drawer group chips are server-computedpassdrawer screenshot read **"Sign-off progress — 1 of 2 groups
c3 finance approval finalizes; run resumesblocked(fixture)the finance holder Ada Auditor is a sys_user row with no better-auth credential account (the item's own knownGap). Path used and its outcome recorded: the documented reassign action moved the slot to the provisioned member (pending_approvers:['i8CaANTg7X…']) and that member's approve returned 200 — but the finance tally stayed 0/1 and the slate re-resolved to Ada, so the group was never satisfied. Request remained pending; run remained paused at dual_signoff. Admin override is not a path: can_override:true does not let the admin act for another group
c4 timeline: one approve per group, distinct actors, commentspass4 rows: submit(usr_showcase_phone_demo), approve(admin, "QA t2b finance-group via admin override"), reassign(admin, …), approve(member, "QA t2b finance-group approval by reassigned persona") — distinct actors, comments round-tripped
c5 server derives the actor from the sessionpassstronger than the clause expects: posting {actorId:'usr_showcase_auditor_demo'} is refused 403 FORBIDDEN"cannot act as 'usr_showcase_auditor_demo' — an approval action is recorded against the authenticated caller". The build rejects the forged hint outright rather than ignoring it

Recorded observation for the wave: a reassign to a user outside the node's group yields a 200 approve that satisfies no group and silently re-resolves the slate back to the group's real holder. Coherent with per-group semantics (a group needs one approval from that group), but the 200 gives the actor no signal that their decision counted for nothing. No clause covers it; recorded, not scored.

15 · approvals.inbox-metadata-actions — pass

clauseverdictoracle evidence
c0 approver-side actions render (other-submitter)passEXP-DEMO (submitted by Mei Phone) drawer: [Approve, Reject, Reassign, Send back, Request info, Close]
c1 submitter-side actions ONLY on the own requestpassown pending request (areq_06008af9…, submitter = admin): […, **Send reminder**, **Recall**, Close]; the other-submitter drawer shows neither. Side-by-side confirmed
c2 rendering mirrors the server viewer flagspassthree data points: {can_act:true, is_submitter:true} → approver and submitter groups render; {can_act:true, is_submitter:false} → approver group only; a finalized request renders [Close] only — the declared record.status gate
c3 each action executes its REST routepassremind click path → POST /api/v1/approvals/requests/:id/remind200, status stays pending
c4 every executed action appends one timeline rowpassbefore/after GET /:id/actions: delta +1, ['submit','remind']

16 · approvals.decision-action-matrix — pass

clauseverdictoracle evidence
c0 all nine actions execute + exactly one timeline row eachpassapprove ✓ · reject ✓ · revise ✓ · resubmit ✓ · recall ✓ · reassign ✓ · remind ✓ · request-info ✓ · comment ✓. The three non-finalizing ones measured +1 row each (cumulative delta 1, 2, 3)
c1 decisions finalize per APPROVAL_STATUSESpassreject → status:'rejected', run completed, steps start:success → manager_review:success; approve advances the node (manager approve opened exec_review)
c2 revise → returned, run parked at wait_revisionpassstatus:'returned'; run paused, steps start → manager_review → **wait_revision:success**
c3 resubmit MINTS a new round requestpassafter the submitter's resubmit: a newareq_0596255b…status:'pending' on the same flow_run_id, freshly resolved approvers
c4 the resubmitted row is TERMINAL at returnedpassGET /:id on the original still reads status:'returned' after the new round opened
c5 maxRevisions guard holdspassclean chain: round 1 revise→returned, resubmit→new; round 2 revise→returned, resubmit→new; round 3 revise → status:'rejected' (auto-rejected). Final chain = 2 returned siblings + 1 rejected — exactly maxRevisions: 2
c6 recall is submitter-onlypassnon-submitter (member) → 403 FORBIDDEN"only the submitter may recall this request"; submitter → 200, status:'recalled'
c7 non-finalizing actions mutate only what they ownpassreassign swapped the slate to the target (pending_approvers:['i8CaANTg7X…'], source removed); remind/request-info/comment each left status:'pending' and added one row. Bonus: a second immediate remind → 429 (throttle)
c8 lockRecord enforced on BOTH sidespassunder exec_review (lock_record:true): PATCH /data/showcase_project/<id>409 RECORD_LOCKED"is locked while an approval is in progress"; under manager_review (lock_record:false): the same PATCH → 200
exec no-revise gate (step 12)passPOST /:id/revise at exec_review400 VALIDATION_FAILED"approval node 'exec_review' has no 'revise' out-edge — the flow does not support send-back for revision"

Named gap: resubmit-by-a-non-submitter was not attempted, so that half of c3's "refused for anyone but the submitter" is unproven.

17 · access-security.audit-log-browser — pass

clauseverdictoracle evidence
c0 three ops produce correctly attributed rowspassdelete on showcase_task with record_id:'0M3CY0CC4DYXilAE' + actor; config_change on sys_setting + actor; login rows attributed to a user id with object_name:'sys_session'
c1 the browser renders those same rowspassscreenshot confirmed the table painted, then DOM read: `["1
c2 filter narrows server-sidepassthe "Writes" view re-issued GET /api/v1/data/sys_audit_log?…&filter=[["action","in",["create","update","delete"]]] — the narrowing rides the request, and the table then showed only write actions. Named deviation: the console exposes prebuilt filtered views rather than the item's "Action = delete" dropdown; server-side narrowing is proven, the exact one-value selection is not
c3 drawer shows Before/After JSON panelspassdrawer screenshot showed "…ACTION update · ACTOR EMA74oMclN… · AUDIT LOG ID koWHGrKm7UAa1sb3 · More details · OLD VALUE [Object] · NEW VALUE [Object]" — two separate before/after panels, not a merged semantic diff
c4 API twin reconciles with the pagepassthe four page rows correspond 1:1 to GET /api/v1/data/sys_audit_log bodies; no page row lacks a backing API row
c5 trail is append-onlypassPOST /api/v1/data/sys_audit_log405; PATCH /api/v1/data/sys_audit_log/<id>405; no forged row appears in a follow-up list

18 · access-security.owd-save-gate — partial

clauseverdictoracle evidence
c0 Studio flags external-wider inlineblocked(fixture)the object designer opens runtime-authored objects read-only ("provided by an installed package, so it is read-only at runtime", source "Artifact") and offers no editable Settings panel, so isExternalWider could not be driven in the browser
c1 server refuses external > internalpasswith a corrected oracle — see CF-2.sharingModel:'private' + externalSharingModel:'public_read' is refused 422 INVALID_METADATA / rule security-external-wider-than-internal: "externalSharingModel 'public_read' is WIDER than the internal sharingModel 'private' … (ADR-0090 D11)", at path: objects[96].externalSharingModel. Reproduced on both the draft→active publish and a direct active PUT. The 403 owd_external_wider the clause names no longer exists — retired by the #8310 ruling, and draft-state saves defer by design
c2 server admits external ≤ internalpassqa_owd_probe3 with public_read / private200 state:'active', read back sharingModel:'public_read', externalSharingModel:'private'
c3 packaged OWD cannot be widenedpassfull-body PUT on showcase_announcement widening both → 403 owd_widening_forbidden: "an environment overlay may only TIGHTEN a packaged object's OWD … (sharingModel 'public_read_write' > declared 'public_read'; externalSharingModel 'public_read' > declared 'private')". Answering layer recorded: R1 (the plugin-security authoring gate), on a STOCK deploy — not WRITABLE_PACKAGE_REQUIRED, contradicting the item's knownGap (CF-3)
c4 the denied widening is inertpassGET /meta/object/showcase_announcement before vs after the refused PUT: byte-identical (JSON.stringify equality), sharingModel:'public_read', externalSharingModel:'private' unchanged
c5 the shipped app passes its own gatepassshowcase_account external public_read ≤ internal public_read_write ✓; showcase_announcement external private ≤ internal public_read

19 · access-security.public-form-intake — partial

clauseverdictoracle evidence
c0 resolved schema is the whitelist onlypassanon GET /api/v1/forms/contact-us → 200, objectSchema.fields keys = exactly ["name","email","company","message"]; owner_id/organization_id/status/source/created_by all absent
c1 forged anchors stripped on submitpasssubmitted owner_id:'usr_victim', organization_id:'org_victim', created_by:'usr_victim'; landed row reads owner_id:null, organization_id:'org_msxwsg97x4ia76nh' (the server's real org), created_by:null. Observation:owner_id lands null rather than a server-stamped owner — the forge is defeated either way
c2 non-whitelisted business fields droppedpasssubmitted status:'closed', source:'forged'; landed status:'new' (seeded default), source:'web' (server-stamped)
c3 inquiry lands under publicFormGrantpassanon POST …/submit201; staff read returns the row with name/email/company/message persisted
c4 non-whitelisted lookup refusedpassanon GET /api/v1/forms/contact-us/lookup/owner_id403 LOOKUP_NOT_PUBLIC"Field "owner_id" is not enabled for public lookup on this form"
c5 submitBehavior thank-you rendersblocked(environment)the anonymous browser page could not be located on this build: GET /forms/contact-us (the publicLink in inquiry.view.ts) returns {"error":"Not found"} and renders no form, while the API door /api/v1/forms/contact-us serves correctly. The public-page route needs identifying before this clause is runnable
c6 unpublish kills the link both waysnot-runnot driven

Also passed (step 8, no clause): a __proto__-nested owner_id/status smuggle landed nothing inherited — the row came back with owner_id:null, status:'new'.

20 · access-security.permission-matrix-edit-loop — partial

clauseverdictoracle evidence
c0 baseline withheld verb deniedpassmember DELETE /api/v1/data/showcase_task/<id>403 PERMISSION_DENIED"You do not have permission to perform this action" — the object-verb denial
c1 matrix edit saves and publishespass (substantive half)PUT /api/v1/meta/permission/qa_t2b_set200 state:'active', and the published set reads back objects.showcase_task.allowDelete: true. Route deviation:POST /api/v1/meta/permission/<name>/publish answers 404 — the publish sub-route the clause names is not mounted for permission (it is for view and object); the PUT lands active directly (CF-5)
c2 access flips ON after publishblocked(fixture)across four matrix states M's DELETE never reached 2xx. The gate did move: with allowDelete:false the refusal is "You do not have permission to perform this action" (verb), with allowDelete:true it becomes "You do not have access to this record" (record level) — including on a task created with owner_id = M. Could not establish an effective end-to-end grant of a runtime-authored set on this fixture; a control granting a shipped delete-bearing set was not run
c3 revoke flips it backblocked(dependency)c2 never reached the ON state
c4 assign/unassign moves accessblocked(fixture)the grant row was accepted (POST sys_user_permission_set {user_id, permission_set_id:'ps_msxzisf0v1jy79ti'}201) and removed (200), but no observable access change bracketed it
c5 editor reflects the published statenot-runthe matrix editor DOM was not read

Useful mechanics recorded: the permission-set matrix key is objects, not objectPermissions — the server's guidance error says so outright ("Unrecognized key(s) … Did you mean objectPermissionsobjects? Until #4001 these were dropped silently"). The grant field is permission_set_id and it wants the sys_permission_set row id (ps_…), not the metadata name; a runtime-authored set is projected into that table (ps_msxzisf0v1jy79ti | qa_t2b_set).

21 · access-security.capability-declaration-lifecycle — partial

clauseverdictoracle evidence
c0 declared capability materializes at bootpasssys_capability (10 rows) carries {name:'showcase.export_data', label:'Export Showcase Data', description:'Bulk-export showcase records (accounts, invoices) to CSV/XLSX.', scope:'org'} matching examples/app-showcase/src/security/capabilities.ts; showcase.restricted_ops likewise
c1 the three-way resolves on the platform action pathblocked(fixture)no reachable invoke surface exposes the specimens. REST: POST /api/v1/actions, /api/v1/actions/<name>, /api/v1/data/zoo/actions/<name> all 404 — correct per #7680. MCP: the bridge is up (initialize 200, 11 tools incl. run_action), but run_action refuses all four at an earlier gate — "Action 'showcase_zoo_perm_held' on 'showcase_field_zoo' is not exposed to AI — the app author must opt it in with ai: { exposed: true, … }"identically for admin and member, so the AI-exposure gate shadows the capability gate. In-process ql.executeAction is not reachable from an HTTP client. See FG-2
c2 platform-name shadowing refused LOUDLYpass (oracle downgraded to test)the guard's boot seeder cannot be driven without authoring a scratch package into the app source, which this wave forbids. Cited instead: packages/plugins/plugin-security/src/bootstrap-declared-capabilities.test.ts at the pin → 23/23 passed, including "refuses to hijack a curated platform capability" and "CURATED platform name: suppresses, and the curated pass seeds the row regardless"
c3 write door validates before the upsertblocked(fixture)same constraint — needs a scratch package carrying a malformed capability plus a boot; no runtime metadata door reaches upsertPackageCapability

Recorded: the four REQUIRE-side specimens exist and are correctly declared — GET /api/v1/meta/action (70 actions) lists showcase_zoo_perm_held (requiredPermissions:['showcase.export_data']), _missing (['showcase.restricted_ops']), _and (both), _empty ([]), all type:'script' on showcase_field_zoo. OpsPermissionSet grants showcase.export_data via systemPermissions as documented.


Reproduction rules

R-1 — records-forms.crud-roundtrip c2 · DELETE is refused for any object targeted by a multiple: true reference

Not a new defect. Already twice-derived and reported this wave — #9296 comment (R1) and #9351 (R3). Confirmed a third time here, and for the first time through the console door.

pnpm -C examples/app-showcase exec objectstack dev --ui --seed-admin -p 3161 -d file:/tmp/qa-wave/t2b-a.db
POST /api/v1/auth/sign-in/email {"email":"admin@objectos.ai","password":"admin123"}
# create through the console: Accounts -> New -> name/industry/status -> Create (POST /api/v1/data/showcase_account -> 201)
DELETE /api/v1/data/showcase_account/<id>
  • expected: 200, row gone.
  • actual: 400"The bare equality spelling { "f_lookups": value } WAS NOT APPLIED: "f_lookups" is a multi-value (or otherwise JSON-valued) field, stored by this driver as a JSON TEXT column … Use "$contains" for membership" — and GET /api/v1/data/showcase_account/<id> still answers 200.

Reproduced ×2 on distinct fresh ids in this run (sXI6lveJX_4Wv-km, eIvHMCjKKc0AvJ1R). Control ruling out "DELETE is broken":DELETE /api/v1/data/showcase_category/<id>200 on the same boot, same session.

R-2 — studio-authoring.view-authoring-live c3 · a runtime-authored view is accepted with a 200 state:'active' receipt but is served by no read door

New defect. P1. Not an auth/authz finding — a metadata read-path gap, so the recipe is published in full.

pnpm -C examples/app-showcase exec objectstack dev --ui --seed-admin -p 3162 -d file:/tmp/qa-wave/t2b-b.db
POST /api/v1/auth/sign-in/email {"email":"admin@objectos.ai","password":"admin123"} # use the bearer token it returns
# 1. baseline — the read door works for shipped views
GET /api/v1/meta/view?object=showcase_task -> 200, 20 items
# 2. author a view container over the SAME object
PUT /api/v1/meta/view/qa_t2b_task_views
{"name":"qa_t2b_task_views","object":"showcase_task",
"list":{"type":"grid","columns":["name","status"]}}
-> 200
{"success":true,"version":"sha256:8f18ae7d…","seq":5,"state":"active",
"message":"Saved view 'qa_t2b_task_views' (org=org_msxx3cuqkidc7f5c, state=active) [seq=5]"}
# 3. read it back
GET /api/v1/meta/view?object=showcase_task -> 200, still 20 items; 'qa_t2b_task_views' ABSENT
GET /api/v1/meta/view/qa_t2b_task_views -> 404 RESOURCE_NOT_FOUND
GET /api/v1/meta/view -> 200, 35 items; 'qa_t2b_task_views' ABSENT
  • expected: the published/active container is served by GET /meta/view?object=… (the getViewsByObject consumer door) and by its direct name.
  • actual: accepted, versioned, sequenced — and invisible to every read door. An authored view can never render.

The ?mode=draftPOST …/publish path behaves identically: draft receipt {state:'draft', seq:2}, publish 200{"message":"Published draft — type=view, name=qa_repair_asset_views [seq=3]"}, then the same 404 / [] / absent-from-listing triple.

Scope measured, not assumed — 4 saves, 3 names, 2 objects, 2 independent boots:

host / dbnamedoorobjectsaveread-back
3162 / t2b-b.dbqa_repair_asset_viewsdraft → publishruntime-authored qa_repair_asset200 / 200404, ?object=[]
3162 / t2b-b.dbqa_asset_views2direct active PUTruntime-authored qa_repair_asset200404, ?object=[]
3162 / t2b-b.dbqa_t2b_task_viewsdirect active PUTshippedshowcase_task200404, listing 20→20
3161 / t2b-a.dbqa_t2b_a_viewsdirect active PUTshippedshowcase_task200404, listing 20→20

Traps and confounds ruled out

  • stale-console-bundle — n/a: every oracle is a direct HTTP call to the running objectstack dev; no console, no DOM.
  • hydration-race — n/a: no DOM oracle anywhere in this finding.
  • dispatcher-vs-hono-route — the oracle is the live server, not a simulated dispatch.
  • automation self-check — plain fetch with a Bearer header; no coordinate clicks, no React controlled inputs. The server's own 200 receipt is the acceptance evidence.
  • read door broken? No — the same request in the same session returns the 20 shipped showcase_task views.
  • org-scoping mismatch? No — sys_organization holds exactly 1 row, org_msxx3cuqkidc7f5c, and GET /api/v1/auth/organization/get-full-organization resolves that same id as the session's active org: the write org and the read org are identical.
  • writability? RUNNER's standing environment fact says view is in the overlay-allowed set, so a 2xx write is expected and observed. The failure is on the read side, after an accepted write.

Because c3 fails, c2 and c4 (the post-publish rendering clauses) are recorded blocked(dependency)no screenshot was taken and no screenshot verdict is claimed.


Checklist-accuracy findings (for the wave anchor, not extraction)

  • CF-1 — ?id= on /api/v1/meta/app keys on the app NAME.?id=com.example.showcase returns {"items":[]} even authenticated; ?id=showcase_app returns the merged app. Re-confirmed; boot-health step 6 / c3 still carry the package-id spelling. (Already known from R1.)
  • CF-2 — owd-save-gate c1 names a retired refusal. The item (rev 1) expects 403 owd_external_wider on a ?mode=draft PUT. That gate was retired by the [3 of #7891] Flip the registration: runtimeTypes gains object + permission/book — whole rule families cross the publish door #8310 maintainer ruling (2026-08-13)object-posture-gate.ts says so in its header, and draft saves defer to the draft→active promotion by design. The live refusal is 422 INVALID_METADATA, rule security-external-wider-than-internal, raised at the active-state door. Item needs the code/status/door corrected and revision bumped.
  • CF-3 — owd-save-gate's knownGap mispredicts which layer answers. It says R1 owd_widening_forbidden is reachable only via OS_METADATA_WRITABLE=object, and that a stock deploy answers WRITABLE_PACKAGE_REQUIRED first. On this stock deploy R1 answered directly with 403 owd_widening_forbidden.
  • CF-4 — first-run-loop is NOT blocked on stock showcase.tracking: full platform-checklist regression against main @ e4e5c6e3 — five-round orchestration #9296 finding 4 and R1 record it blocked(fixture) because Studio is not installed. The app is indeed unregistered (meta/app = showcase_app, setup, account), but the App Builder UI is reachable at /_console/studio and /_console/apps/setup/component/studio/builder, Home carries the "Build an app" tile, and the whole loop closes (item 10 above: 6/6 clauses pass). The standing fact should be narrowed to "the Studio app registration is absent" and the item taken off the blocked list.
  • CF-5 — permission-matrix-edit-loop c1 names a route that 404s for permission.POST /api/v1/meta/permission/<name>/publish404; the same sub-route works for view (200) and is mounted for object. The PUT lands state:'active' directly, so the publish step is unnecessary here — but the clause requires a 2xx from it.
  • CF-6 — capability-declaration-lifecycle names the wrong object. The fixture block says the four specimens are "type:'script' on the zoo object"; they are on showcase_field_zoo. An invoke keyed on zoo returns "Action … not found on object 'zoo'", which reads like a missing specimen.
  • CF-7 — settings-hub-roundtrip says 10 open-framework namespaces; the build serves 11.GET /api/settings enumerates branding, company, localization, auth, mail, sms, storage, ai, knowledge, feature_flagsplus lifecycle (5 specifiers).
  • CF-8 — console-login step 5's expiry gesture is wrong for this console. "clear the auth cookies for the origin" does not expire the session: the console authenticates with a bearer token in localStorage under auth-session-token, sent as an Authorization header (requests carry authHdr=true, cookie=NO; the only cookie, better-auth.session_token, is not used by the console's own client). Clearing cookies leaves a fully valid session, and the shell then legitimately renders authed data — which reads exactly like the "dead shell rendering stale data" failure c3 warns about. This run nearly filed that false P0; the automation self-check caught it. The step should say: sign out server-side (POST /api/v1/auth/sign-out) or clear localStorage as well.
  • CF-9 — audit-log-browser c2's "Action filter" control does not exist as described. The console offers prebuilt filtered views (Recent / Writes / Auth / Config) plus a generic "Filter Records / Where / Add filter" builder, not an Action dropdown. Server-side narrowing is provable through the Writes view; the clause should name the actual control.

Fixture gaps

  • FG-1 — the three platform apps are not all present on a stock boot.builtin-apps-nav-render's fixture block asserts Setup/Studio/Account "register as one-app packages … present on any stock boot, no showcase-specific fixture". Studio is not served (GET /meta/app?id=studio{"items":[]} for admin and member alike), so three of the item's seven clauses cannot be completed. Either the fixture must install @objectstack/studio or the item must scope itself to the apps a stock boot actually registers.
  • FG-2 — the capability REQUIRE-side specimens are unreachable on every surface this build boots. REST mounts no action-invoke route by design (capability-declaration-lifecycle: no shipped resource enforces a package capability server-side (no REST action-invoke route on this build) #7680, confirmed 404). The MCP run_action bridge refuses them at the ai.exposed opt-in gate — before the capability check and identically for every persona — so the ADR-0066 D4 actionPermissionError is never reached. Making c1 runnable needs either ai: { exposed: true } on the four predicate-matrix.action.ts specimens, or a build that mounts the dispatcher /actions domain.
  • FG-3 — no credentialed persona holds the finance group.per-group-signoff c3 stays blocked: Ada Auditor (usr_showcase_auditor_demo) is a routable sys_user with no better-auth credential account, and neither can_override nor reassign substitutes — reassigning the slot to an out-of-group user produces a 200 approve that satisfies nothing. The item's own knownGap names the fix (provision the account with issuer local:credential); it remains unprovisioned on stock.
  • FG-4 — no non-reading persona for showcase_account.crud-roundtrip c7 (clone RLS refusal) cannot be driven: the object is public_read_write, so no bound persona is unable to read the source row.
  • FG-5 — the object designer will not open a runtime-authored object for editing. Both owd-save-gate c0 and authoring-validation-not-persisted c3 need the Studio designer's Settings/save path, but a published runtime object reads back as source "Artifact" with "This object is provided by an installed package, so it is read-only at runtime". A draft-state object in a writable package that the designer opens editable is the missing fixture.
  • FG-6 — the public form's browser page route is unknown.public-form-intake c5 needs the anonymous rendered form; the publicLink value /forms/contact-us answers {"error":"Not found"} while /api/v1/forms/contact-us serves correctly.
  • FG-7 — package commit history could not be exercised.GET /packages/:id/commits answers {"commits":[]} after two successful publish-drafts, and POST /packages/:id/publish reports "No metadata items found for package" for drafts saved with ?package=<id>. package-lifecycle c3/c4 need whichever publish path actually mints commits.

Other observations (no clause covers these)

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