Skip to content

QA run · priority:P0 (23/23) · e5f194ba · 2026-10-22 · 19 PASS / 3 PARTIAL / 1 FAIL / 0 BLOCKED / 0 NOT-RUN #25227

Closed

Description

@os-sales

Pre-release P0 smoke for 18.1.0, run on latest main at the maintainer's request before merging the Version Packages PR (#24876). One selector (priority:P0), one record. The per-clause verdict tables are in the five comments below this body, one per runner group (an issue body has a size cap); this body carries the fingerprint, scope, the derived item verdicts, the one FAIL's reproduction rule, the findings and the probe line.

Environment fingerprint

  • framework e5f194ba13d8d9a2818c1ece98a9c206fa0c7623 (origin/main at 2026-10-22 12:23Z); .objectui-sha cd14d8eeffe11ae03b6acf82e663c1d293dfdf5f
  • tree: pnpm install --frozen-lockfile, then pnpm build --concurrency=2 (76/76 tasks green, 7m15s). node scripts/check-dev-prereqs.mjs green.
  • console: pnpm objectui:build after the framework build, never in parallel, with OBJECTUI_DEPS_BUILD_CMD='pnpm exec turbo run build --concurrency=2 --filter=@object-ui/console^...'. The first attempt died with a V8 heap OOM (exit 134 in @object-ui/app-shell#build, "Ineffective mark-compacts near heap limit") at node's default heap; the re-run with NODE_OPTIONS=--max-old-space-size=6144 succeeded. dist/.objectui-sha = pin; pnpm check:console-sha passes.
  • container: 4 vCPU, 15 GiB; Chromium 141 headless at /opt/pw-browsers/chromium (playwright-core 1.63.0).
  • boots: stock examples/app-showcase, OS_PORT=<port> objectstack dev --ui --seed-admin -p <port> -d file:(scratch)/<run>.db per runner, each runner on its own ports (476xx) and file DB, started with setsid and stopped by process group. Walled boot (OS_TENANCY_POSTURE=isolated) from a scratch copy declaring @objectstack/organizations; os start boots for the MCP stdio item; v17 and 18.0.0-next.5 fixtures for the ceremony item, built from npm packages. Timestamps per runner are in the comments.
  • execution: five runner agents in parallel (security · api/automation · cli/mcp · boot/console/nav · approvals/crud/studio), plus a sixth, independent agent for the rule-7 re-derivation of the one FAIL. git status stayed clean; examples/app-showcase/.objectstack/installed-packages/ never existed.

Scope

Selector priority:P0 → 23 runnable items, judged 23 of 23. Hidden (blocked, not run): access-security.no-active-org-session-semantics. No planned items matched.

item rev surface verdict driven by
access-security.rls-both-sides 7 api PARTIAL pins + objectstack verify --rls + live; acceptance[5] D11 spot-check blocked(dependency) per its knownGaps
access-security.write-path-guards 6 api PASS pins + live (pins don't reach A4, bulk half of A5, A6)
access-security.crud-permission-matrix 7 api PARTIAL pins + live delegate × sys_* rows; acceptance[5] blocked(fixture) per its knownGaps
access-security.owd-sharing-matrix 4 api PASS pins + live, all four models
access-security.anonymous-deny-surfaces 3 api PASS pin + live, all seven families
access-security.walled-environment-metadata-operator-only 5 api PASS pins + a provisioned walled isolated boot (grant row placed outside HTTP, as the item's fixture prescribes)
ai.mcp-stdio-fail-closed 6 cli PASS pin + live os start boots with a stdio client
api-backend.query-contract-matrix 5 api PASS live (engine pins reach only the engine layer)
api-backend.packaged-action-disabled-dispatch 6 api PASS pins + live, incl. restart and live MCP run_action
approvals.account-app-entry 4 browser FAIL manual browser; re-derived independently (rule 7)
automation.packaged-flow-disable-durable 5 api PASS live, cold restart over the same file DB
cli.dev-boot-contract 7 cli PASS live
cli.organization-ownership-ceremony 4 cli PASS pins + v17 and 18.0.0-next.5 fixtures
identity-auth.default-organization-boot-invariant 2 api PASS pins + live
integration-system.datasource-credential-refusal-matrix 5 mixed PASS live, scratch PostgreSQL 16 for the connect leg
platform-core.boot-health 9 mixed PASS live
platform-core.seed-integrity 4 api PASS live, incl. a restart
platform-core.console-login 4 browser PASS manual browser
platform-core.nav-surfaces-render 6 browser PASS pin showcase-smoke.spec.ts 49/49 on own boot
platform-core.builtin-apps-nav-render 7 browser PARTIAL manual browser; Browse Marketplace catalog blocked(environment): egress to cloud.objectos.ai denied, server answers 502 MARKETPLACE_PROXY_FAILED with a named error state
platform-core.v18-boot-refusals 6 cli PASS pins + live
records-forms.crud-roundtrip 13 browser PASS manual browser + API
studio-authoring.first-run-loop 6 mixed PASS manual browser + API

Derived: 19 PASS / 3 PARTIAL / 1 FAIL / 0 BLOCKED / 0 NOT-RUN. All three PARTIALs are the gaps their own items already record (two fixture debts, one sandbox egress block); no clause in them failed.

FAIL — approvals.account-app-entry acceptance[3] (release-blocking regression)

A non-admin pending approver who opens a request from the account app's Approvals inbox gets a blank request page: no fields, no decision actions, no can_act=false explanation. The admin opening the same URL gets the full page. The inbox's in-row Approve/Reject still works (server effects verified), so a non-admin can decide from the list, but Reassign, Send back, Request info, Reply and the request context are unreachable for them.

Reproduction rule (fresh showcase boot, own port and DB):

  1. As admin: POST /api/v1/auth/organization/invite-member {"email":"E","role":"member"}; then POST /api/v1/auth/sign-up/email {"email":"E","password":"P","name":"QA Approver"}; resolve the user id U with GET /api/v1/data/sys_user?email=E; POST /api/v1/data/sys_user_position {"user_id":U,"position":"contributor","organization_id":<default org>} and the same with "position":"finance".
  2. Launch a NEW sign-off after the positions exist: as E, POST /api/v1/data/showcase_invoice {"name":"INV-X","owner":"E","account":<seeded account id>}, then PATCH /api/v1/data/showcase_invoice/<id> {"status":"sent","issued_on":"2026-10-22"} (or have the admin submit one; both shapes reproduce). GET /api/v1/approvals/requests/R → 200 with viewer.can_act: true and U in pending_approvers.
  3. Sign E in through the console form; Avatar › Profile → sidebar Inbox › Approvals (/_console/apps/com.objectstack.account/component/approvals/inbox); click the row, or press Enter on it, or cold-load …/inbox?request=R.
  4. Expected: the request's page with its fields and the declared decision action set. Actual: /_console/apps/com.objectstack.account/sys_approval_request/record/R shows only the heading "Approval Request areq_…", a Flow-initiated chip and empty Details/Timeline tabs; the header carries only "…" and refresh.
  5. Control: the admin on the same URL sees "Invoice Dual Sign-off (parallel approval)" with its fields and Approve / Reject / Reassign / Send back / Request info / Recall / Reply.

Rule-2 / rule-7: reproduced 5× by the first runner (fresh contexts, form sign-in, clicks and direct URL loads, 7–8 s settle) and 3× by an independent second runner on its own boot with its own persona and two new requests (one the persona submitted, one the admin submitted). Identity proven per context with get-session; console stamp = pin; no coordinate clicks.

Mechanism (second runner, network trace + source at the pin): every request the page makes answers 2xx — GET /api/v1/approvals/requests/R 200 (viewer.can_act: true), GET /api/v1/approvals/requests/R/actions 200, GET /api/v1/meta/object/sys_approval_request and …/sys_approval_action 200, two POST /api/v1/security/explain 200. The console never calls the data door for the request. In objectui at cd14d8eeffe1, RecordDetailView.tsx folds the row the approvals door returned through withoutDeniedFields against /auth/me/permissions (readablePageRecord = readableRow(...)), and MePermissionsProvider.checkField answers false for any object the permissions answer omits; the persona's answer has no sys_approval_request entry (by design: the object is engine-owned, ADR-0103, and request-read-gate.ts / approvals-plugin.ts keep data-door read closed to non-administrators). So every field but id is dropped, including viewer and status, and every action's visible predicate on record.viewer.can_act fails. Causal check: rewriting only the persona's /auth/me/permissions response in the browser to include read on sys_approval_request / sys_approval_action made the same page render fully with the right action set. AppContent.approvalRecordReads.test.tsx stubs MePermissionsProvider, which is why objectui's tests miss it.

Regression: yes, against 18.0.0. 18.0.0 shipped objectui 616df6598b42 (the published @objectstack/console@18.0.0 dist stamp), which still opened requests in the side sheet; the 18.0 sweep's run #24542 passed this clause with the same persona shape. The side sheet was replaced by the served record page in objectui a91f6bbb1 (objectui#12484, 2026-10-21), which entered this repo's pin with #25160 (cf153eb2e535 → 56cf4d8e89b7; cf153eb2e535 does not contain it, 56cf4d8e89b7 does). Disposition: product defect, fix belongs in objectui; granting sys_approval_request read to members would hide the symptom by opening a door the platform keeps closed on purpose.

Findings outside the clauses

  • Data API paging (pre-existing, not a regression): GET /api/v1/data/showcase_account?$skip=-5 (and ?$skip=-1&$top=3, and POST …/query {"offset":-5}) answer 500 INTERNAL_ERROR (server log: the SQL builder's "A non-negative integer must be provided to offset"); ?$top=abc, ?$top=-1 and POST …/query {"limit":-1} answer 200 as if no limit was sent, while the POST door refuses {"limit":"ten"} 400 VALIDATION_FAILED. Cause: packages/metadata-protocol/src/protocol.ts coerces with Number(options.limit) / Number(options.offset) and never range-checks. Re-measured by the run lead on a separate boot. The same two lines are in the published @objectstack/metadata-protocol@18.0.0, so 18.0.0 behaves the same. Extracted as its own card (below).
  • Authorization-adjacent, low severity: one observation beside ai.mcp-stdio-fail-closed acceptance[5]'s doors (field-level security, no value leaked) — detail withheld pending maintainer. It moves no verdict.
  • Transient: one showcase-smoke.spec.ts surface (Hours by Status (Chart)) died once with Playwright Target crashed before any assertion; it did not reproduce (6/6 twice in isolation, 49/49 on a full re-run). Treated as environment.

Checklist-accuracy findings and fixture gaps (for the wave anchor; not extracted)

  • approvals.account-app-entry: steps[5–6] and acceptance[1], [3], [4] still say "drawer"; since objectui#12484 the surface is the served sys_approval_request record page, and the inbox also has in-row and bulk Approve/Reject, so acceptance[3] should name which surface it scores.
  • platform-core.boot-health acceptance[2] quotes the deployment-policy flow line with ⚠; the banner now prints it with ℹ.
  • platform-core.builtin-apps-nav-render steps[5] describes Account › Approvals as sys_approval_request/my_pending; it is served as the approvals:inbox component.
  • platform-core.nav-surfaces-render steps[0] says test:smoke, which runs every spec in e2e/, not only the pin; examples/app-showcase/test-results/ is not git-ignored, so a run without --output leaves untracked files.
  • records-forms.crud-roundtrip acceptance[4]: no step sets a date (the localized-date half was checked on a seeded date field); acceptance[6]: f_formula cannot show re-derivation with unchanged inputs, only f_autonumber does.
  • studio-authoring.first-run-loop steps[3]: the Changes panel goes straight to "Publish all".
  • automation.packaged-flow-disable-durable: the urgent showcase_task create needs a project (400 without it); the flow runs list caps limit at 100 (limit=200 answers 400, which can read as "no runs").
  • integration-system.datasource-credential-refusal-matrix steps[0]/knownGaps: with driver:'mongo' plus config.password, the credential refusal is reported, not the name refusal; the name refusal appears only with no credential key.
  • platform-core.seed-integrity: f_password reads back masked and should be listed as expected-masked.
  • api-backend.query-contract-matrix: no stock persona fits the contributor fixture; the runner minted one (a fixture of its own making).
  • ai.mcp-stdio-fail-closed: os start logs at warn by default, so its INFO-line oracles need --log-level info.
  • cli.organization-ownership-ceremony: on a v17 DB the apply blocks at rewrite until a v18 boot has been refused once (not printed by the plan); the ceremony needs the deployment's auth composition (OS_AUTH_SECRET or --dev) or it blocks at authored-conversion; on a raw v17 copy refused_comment_reactions_unconverted comes first, and comment-reactions --apply needs --yes non-interactively. Fixture gaps: the v17 DB came from cli@17.7.0 on a minimal app, and the next.5 fixture had no pending mail jobs (jobsRewritten 0).
  • platform-core.v18-boot-refusals: the SecurityPlugin rig is refused earlier by the "mounts no auth" gate; the createIdentityObjectsPlugin() refusal is reached with os serve --dev --no-server --preset minimal.
  • access-security.write-path-guards and owd-sharing-matrix: their pins do not reach several clauses (write-path A4, bulk half of A5, A6; owd A2, A3, A5 on stock grants) — candidates for pin promotion.
  • access-security.crud-permission-matrix knownGaps calls a baseline member's /data/sys_user read "self-scoped"; measured, it lists every user of the single organization (org-scoped). Wording to check.
  • objectstack verify --rls lists showcase_announcement (public_read) as "probe cannot read"; that is the probe persona's owner-scoped policy, not an OWD contradiction, but reads like one.
  • RUNNER environment facts worth adding: pnpm objectui:build needs a larger V8 heap here (NODE_OPTIONS=--max-old-space-size=6144); on the sqlite-wasm driver a hand-placed grant row must be written while the server is stopped, or the server's next save overwrites it; a selective, non-reproducing Playwright Target crashed is renderer-level, not a chart defect.
  • showcase fixture: the boot warns that sharing rule share_new_inquiries_with_field_ops (business unit bu_field_ops) expands to no recipients on a fresh boot.

Off-clause probe line (rule 8)

About 60 runner-minutes in total, after the clauses had verdicts, as admin, plain members, contributor/liaison/approver personas, org owner and operator on the walled boot, and anonymous. Doors: data CRUD and query (empty / missing / unknown / negative / non-numeric values on ids and paging params), auth sign-in (empty email, missing password, unknown email, 32 bad-credential combinations, bearer after sign-out), MCP stdio admission (empty, whitespace and osk_ keys, every enable-switch spelling), approvals act/read as non-participant and with a forged actor, walled-boot doors beyond the clauses (draft, rollback, dashboard, translation writes, package PATCH, automation clone/delete), studio clone with unknown/empty ids, writes into the read-only showcase package. Found: the paging defect above, the withheld low-severity observation above, and a cosmetic one (record History shows a currency change raw, "— → 1234567", while the header shows "$1,234,567.00"). No existence leak on foreign vs nonexistent ids, no account enumeration on sign-in.

Handover

None — every selected item has a verdict.

Activity

  1. os-sales commented on Oct 22, 2026

    @os-sales
    CollaboratorAuthor

    Per-clause verdicts 1/5 — access-security (6 items)

    4 PASS / 2 PARTIAL / 0 FAIL / 0 BLOCKED / 0 NOT-RUN. The two PARTIALs come only from legs the items themselves record as unreachable (fixtures.knownGaps).

    Environment

    • Boot 1 (stock, single): OS_PORT=47601 setsid pnpm -C examples/app-showcase exec objectstack dev --ui --seed-admin -p 47601 -d file:(scratch)/main.db. Started 2026-10-22T12:46:30Z, banner Tenancy: single, stopped through its process group at 12:57Z.
    • Boot 2/3 (walled, isolated): a scratch full copy of examples/app-showcase; every dependency a link: to its workspace or store path, plus @objectstack/organizations → link:packages/plugins/organizations, installed with pnpm install --ignore-workspace --offline inside the scratch dir only. Booted with OS_TENANCY_POSTURE=isolated OS_PLATFORM_OWNER_EMAIL=admin@objectos.ai OS_AUTH_MEMBERSHIP_POLICY=invite-only … objectstack dev --ui --seed-admin -p 47602. Started 12:58:06Z. Banner Tenancy: isolated, Organizations mounted, nothing DEGRADED; /auth/config reported tenancyPosture:"isolated", multiOrgEnabled:true, degradedTenancy:false. Restarted once around a fixture write (below); stopped at 13:00:37Z.
    • Personas minted by invite-then-sign-up (stock) or admin create-user + change-password (walled); each identity checked with GET /auth/get-session before use (auth-state-leak trap).
    • Pin layer. The dogfood pins boot @objectstack/verify's bootStack, the full serve composition answered through Hono app.request in-process (packages/verify/src/harness.ts): the HTTP layer without a socket. The objectql pins are the engine layer. The rest and runtime walled pins are plugin/domain unit layer, which is why the walled item's live legs were also driven.

    Pin results (all --maxWorkers=1):

    • dogfood, 13 files (all 6 items): Test Files 13 passed (13) · Tests 226 passed (226).
    • re-run of the 3 wall-* dogfood files, verbose: Test Files 3 passed (3) · Tests 63 passed (63) (wall-environment-metadata-refusal 23, wall-package-doors-refusal 23, wall-catalog-refusal 17).
    • objectql engine-insert-static-readonly-strip.test.ts + engine-readonly-strip-caller-values.test.ts: Test Files 2 passed (2) · Tests 55 passed (55); the named cases "isSystem seeds the readonly column — the exemption the ruling names" and "an isSystem caller is untouched by any of this" both ✓.
    • rest meta-migrate-stored-walled-refusal.test.ts: Tests 6 passed (6).
    • runtime packages-walled-write-refusal + automation-walled-definition-write + meta-walled-catalog-refusal: Test Files 3 passed (3) · Tests 61 passed (61).
    • objectstack verify --rls (12:53:20–12:53:54Z, exit 0): CRUD tail ── 15 verified, 0 gaps, 0 FAILED, 1 needs-fixture, 8 skipped (0 mismatches); per-persona line (probe persona, unit = objects) ── 21 PROVEN (21 consistent, 0 HOLES) · 3 NOT PROVEN (0 member-visible, 0 probe-blocked, 3 skipped); 10 of 10 declared position(s) probed; total line (unit = object × persona) ══ all personas: 38 PROVEN (38 consistent, 0 HOLES) · 226 NOT PROVEN (131 member-visible, 62 probe-blocked, 33 skipped).

    access-security.rls-both-sides — rev 7 — PARTIAL

    Pins (showcase-private-owd dogfood, HTTP; verify --rls) plus a manual leg on Boot 1 with fresh members A and B (baseline only) and the admin.

    clause verdict evidence
    acceptance[0] A's list = A's notes only, symmetric for B pass A's list ["A note 1 …","A note 2 …"], B's ["B note …"], from the server row sets. Pin showcase-private-owd (5 tests, HTTP) ✓
    acceptance[1] foreign by-id READ denied pass A GET /data/showcase_private_note/<B id> → 404 RECORD_NOT_FOUND (not-found-shaped denial). Pin ✓
    acceptance[2] foreign by-id WRITE denied, row unchanged pass A PATCH …/<B id> {"body":"forged"} → 404 RECORD_NOT_FOUND; admin re-read byte-identical. Extra A DELETE → 403 PERMISSION_DENIED, row still present
    acceptance[3] admin reads the full set pass admin list contains all 3 ids created in the run
    acceptance[4] runRlsProofs, 0 holes pass verify --rls per-persona line 21 PROVEN (21 consistent, 0 HOLES); total line all personas: 38 PROVEN (38 consistent, 0 HOLES) over 10 of 10 positions; no rls-hole row. Layer: verify harness, HTTP in-process
    acceptance[5]a every skipped object explained pass 3 skipped: showcase_project_membership "no plain-text probe field"; showcase_ext_customer and showcase_ext_order "external read-only object (federated datasource "showcase_external"; no inserts)" — all benign
    acceptance[5]b D11 by-hand spot-check blocked(dependency, #22017 row 11 / #7463) per knownGaps: the private write-up is maintainer-held; not driven, not restated
    negative[0] forged PATCH does not silently succeed held 404 and an unchanged row

    access-security.write-path-guards — rev 6 — PASS

    Pins (owner-anchor-and-bulk-writes, showcase-static-readonly, showcase-readonly-when-parent dogfood, HTTP; the two objectql engine pins) plus manual legs on Boot 1 for the clauses the pins do not reach.

    clause verdict evidence
    acceptance[0] readonly stripped on INSERT pass admin POST /data/showcase_contact {…,"lead_score":10} → 201 with X-ObjectStack-Dropped-Fields: lead_score;reason=readonly; re-read lead_score=null. Pin ✓
    acceptance[1] readonly stripped on UPDATE pass PATCH {"lead_score":99} → 200; re-read lead_score=null
    acceptance[2] owner forge on INSERT denied, no row pass Alice POST showcase_private_note {"title":"planted …","owner_id":<Bob>} → 403 PERMISSION_DENIED; admin filtered count 0. Pin ✓
    acceptance[3] transfer and disown denied; self-owner insert OK pass PATCH owner_id=<Bob> → 403, PATCH owner_id=null → 403 (both PERMISSION_DENIED), owner re-reads Alice; POST owner_id=<Alice> → 201, persists. Pin ✓
    acceptance[4] empty-owner createMany stamps the caller pass Alice POST …/createMany bare array of 3 → 201; all 3 read back owner_id = Alice. Live only
    acceptance[5] readonlyWhen locks, judged per row pass paid invoice PATCH tax_rate=99 → 200, dropped tax_rate;reason=readonly_when, re-read 8; paid line PATCH quantity/unit_price/product → all three dropped readonly_when; mixed updateMany [paid line, draft line]: results[0].droppedFields=[{fields:["quantity"],reason:"readonly_when"}], results[1] none; re-reads paid qty 2 (unchanged), draft qty 9 (applied). Pin showcase-readonly-when-parent ✓ for the single-row half
    acceptance[6] bulk validation per row pass updateMany [L3 q5, L4 q-1, L5 q6] with continueOnError:true → [ok, VALIDATION_FAILED, ok]; re-reads L3=5, L4=4 (unchanged), L5=6. Default run → [ok, VALIDATION_FAILED, NOT_ATTEMPTED], L5 unchanged
    negative[0] admin forge succeeds pass admin POST owner_id=<Bob> → 201, owner Bob. Pin case ✓
    negative[1] isSystem can set readonly pass engine layer: the two objectql pins ✓ (55 tests)

    access-security.crud-permission-matrix — rev 7 — PARTIAL

    access-matrix.json recount: 45 entries = 41 showcase_* rows + 4 showcase_field_ops_delegate × sys_* rows. Pin showcase-crud-persona-matrix (HTTP verify harness): 41 rows × 4 verbs = 164 cells, 87 allow / 77 deny against the baseline union, admin payload control first; passed. The 4 delegate rows driven by hand on Boot 1: admin granted the set via POST /data/sys_user_permission_set (201); delegate D placed in bu_field_ops, target T in bu_west_coast.

    object C R E D
    sys_business_unit (R only) 403 PERMISSION_DENIED; marker count 0 list 200 (5), by-id 200 403 403; row still 200
    sys_business_unit_member (R only) 403; 0 rows list 200 403 403; row unchanged
    sys_user (R only; C/D not exposed) 405 OBJECT_API_METHOD_NOT_ALLOWED (admin control 405 too) list 200, by-id(T) 200 403; T unchanged 405 (admin 405 too)
    sys_user_position (CRUD, subtree-gated) in-subtree 201, re-read 200 list 200, by-id 200 own row 200, changed own row 200, then 404

    Out-of-subtree create (bu_hq_finance) → 403 PERMISSION_DENIED "…is outside the delegated subtree". Contrast baseline member: sys_user_position create → 403.

    clause verdict evidence
    acceptance[0] allowed cells succeed and persist pass pin: 87 allow cells (HTTP); live: 7 delegate allow cells 2xx with re-reads
    acceptance[1] withheld cells 403 PERMISSION_DENIED (405 on sys_user C/D) pass pin: 77 deny cells 403 PERMISSION_DENIED; live: 7 delegate deny cells 403, sys_user C/D 405 with admin control 405
    acceptance[2] denied CREATE leaves no row pass pin (showcase_*); live marker counts 0
    acceptance[3] viewAllRecords on exactly the named objects pass pin; live: auditor GET showcase_invoice_line/<C1 line> 200 and in list, baseline A's line list → 403
    acceptance[4] modifyAllRecords grants foreign write only where held pass pin; live: ops PATCH of admin's announcement 200 persisted, member A's 403 unchanged
    acceptance[5] guest_portal create-without-read blocked(fixture, item knownGaps[2]) unobservable on a stock boot; not driven
    acceptance[6] every set × every row has a verdict pass 164 pin cells + 16 live = 180 = 45 × 4
    negative[0] admin control on denied cells pass pin's admin control; live sys_user C/D 405 for admin too; delegate 201 where baseline 403
    negative[1] no withheld cell answers 2xx held none

    access-security.owd-sharing-matrix — rev 4 — PASS

    Pins (showcase-private-owd, showcase-public-read-owd, controlled-by-parent, showcase-invoice-cbp; HTTP) — the last two boot their own SecurityPlugin configuration, so all four models were also driven live on Boot 1 with A (baseline), C1/C2 (showcase_contributor granted 201), ops and auditor.

    clause verdict evidence
    acceptance[0] private pass B's list excludes A's notes; A's own GET/PATCH 200; foreign by-id GET/PATCH 404 RECORD_NOT_FOUND, admin re-read unchanged. Pin ✓
    acceptance[1] public_read: everyone reads, only owner writes pass A's list contains the admin's announcement, by-id 200; A PATCH → 403 PERMISSION_DENIED, unchanged. Pin (3 tests) ✓
    acceptance[2] ops modifyAllRecords crosses owner-writes pass ops PATCH → 200, re-read title changed
    acceptance[3] public_read_write: record open, verb gated pass C1 GET 200, PATCH name 200 re-read changed; A PATCH → 403 PERMISSION_DENIED, unchanged
    acceptance[4] controlled_by_parent derivation pass C1 invoice 201 + line 201; C2 list excludes, by-id 404, PATCH 404 with admin re-read qty=1; C1 list/GET/PATCH 200 (qty=3). Pins controlled-by-parent (4) and showcase-invoice-cbp (4) ✓. D11 dedupe: carried only on rls-both-sides acceptance[5]
    acceptance[5] auditor VAMA over derived scope pass auditor list contains C1's line; by-id 200
    acceptance[6] all four models driven pass as above
    negative[0] public_read foreign PATCH as a forged request held 403, unchanged
    negative[1] C2's probes also as C1 held C1 by-id GET/PATCH 200 — the denial is derivation

    access-security.anonymous-deny-surfaces — rev 3 — PASS

    Pin showcase-anonymous-deny-surfaces (36 cases, HTTP) plus a live run of every family on Boot 1 with no Authorization header and no cookie.

    clause verdict evidence
    acceptance[0] every surface 401 UNAUTHENTICATED pass GET /data/showcase_private_note, GET /meta, POST /actions/showcase_task/showcase_mark_done/anon-probe-id, GET /automation/_status, POST /batch {"operations":[]}, GET /security/explain, analytics query/sql/meta → all 401 UNAUTHENTICATED. Pin ✓
    acceptance[1] deny before resolution pass actions probe with anon-probe-id → 401, not 404
    acceptance[2] one envelope family per body pass rest-flat: /data, /meta, /batch, /security/explain; dispatcher-wrapper: /actions, /automation, three analytics faces; no third dialect
    acceptance[3] analytics refuses before reading pass anonymous query/sql/meta → 401, malformed body on query and sql → 401 (not 400); member: 200/200/200, malformed → 400 VALIDATION_FAILED
    acceptance[4] member unaffected pass member GET /data/showcase_private_note 200, GET /meta 200
    acceptance[5] declared-public stays public pass anonymous GET /api/v1/forms/contact-us → 200
    acceptance[6] every variant probed pass all 7 families, live and in the pin
    negative[0] destructive automation DELETE denied held anonymous DELETE /automation/showcase_reassign_wizard → 401; authed GET afterwards → 200

    access-security.walled-environment-metadata-operator-only — rev 5 — PASS

    Walled boot provisioned per RUNNER "A walled boot is a fixture of its own". Operator: admin@objectos.ai (named by OS_PLATFORM_OWNER_EMAIL). Org owner O and contrast owner X minted by operator create-user → change-password → organization/create → set-active.

    Fixture write made outside HTTP: the operator saved package com.qa.catalog (201) and permission set qa_meta_author with systemPermissions:["manage_metadata"] (200). Both HTTP grant doors refused as the item predicts (operator insert 403 "…organization_id is outside the caller's organization scope"; O's self-insert 403 D12 condition (c)). The grant row {user_id:O, permission_set:qa_meta_author, organization_id:<O's org>} was inserted into the run's own scratch DB while the server was stopped, then restarted. Guard-the-guard: X (no grant) PUT /meta/object/… → 403 "…requires the manage_metadata capability"; O's identical PUT → 403 "…requires the platform operator: this deployment runs the 'isolated' tenancy posture" — O's grant is live and the refusal comes from the wall.

    clause verdict evidence
    acceptance[0] pins pass at the subject sha pass rest 1 file 6 tests; runtime 3 files 61 tests; dogfood wall-* 3 files 63 tests; every file ≥1 test, 0 failed, at e5f194ba
    acceptance[1] org-owner writes → 403 and store nothing pass O: PUT /meta/object/qa_walled_probe 403 FORBIDDEN; PUT /meta/permission/showcase_member_access 403 FORBIDDEN; POST /meta/view/showcase_task.in_progress/publish 403 FORBIDDEN; view DELETE 403 FORBIDDEN; POST /meta/_migrate-stored 403 FORBIDDEN; DELETE /packages/com.example.showcase 403 PERMISSION_DENIED; POST /packages 403 PERMISSION_DENIED; POST /security/permission-sets/showcase_member_default/clone 403 PERMISSION_DENIED; POST /automation 403 PERMISSION_DENIED. Operator read-backs: object 404, permission set byte-identical, view 200, package 200, would-be package/clone/flow 404
    acceptance[2] operator admitted (walled) + single admin admitted pass walled operator PUT 200, read-back 200, cleanup DELETE 200 → 404; Boot 1 admin identical PUT 200, read-back 200, cleanup → 404
    negative[0] no org-owner environment-metadata write succeeds held none admitted on any door driven, incl. off-clause doors

    group was not booted; it is covered only by the pins.

    Off-clause probes (~10 min)

    Data doors as a baseline member against foreign, nonexistent and blank ids: GET/PATCH 404 and DELETE 403 uniformly (no existence oracle); bulk update/delete over a foreign id → per-row RECORD_NOT_FOUND / PERMISSION_DENIED, row unchanged. Anonymous: 32 combinations (no auth, garbage/empty bearer, bogus session cookie × data/meta/actions/flows/analytics/security doors) all 401 UNAUTHENTICATED; a bearer after sign-out → 401. Walled org owner: draft/rollback/dashboard/translation writes, object DELETE → 403 FORBIDDEN; package PATCH, automation clone/delete → 403 PERMISSION_DENIED; read-backs unchanged. Findings: none.


    Generated by Claude Code

  2. os-sales commented on Oct 22, 2026

    @os-sales
    CollaboratorAuthor

    Per-clause verdicts 2/5 — api-backend · automation · identity · seed · datasource (6 items)

    6 PASS / 0 PARTIAL / 0 FAIL / 0 BLOCKED / 0 NOT-RUN.

    Environment

    • Boot 1: OS_PORT=47611 setsid pnpm -C …/examples/app-showcase exec objectstack dev --ui --seed-admin --log-level info -p 47611 -d file:(scratch)/main.db, fresh DB, started 2026-10-22T12:50Z, ready 12:50:34Z, stopped by process group 13:02:15Z.
    • Boot 2 (cold restart): same command over the SAME db file, 13:02:41Z–13:06Z.
    • Scratch PostgreSQL 16 on port 47619 (initdb -A scram-sha-256, role/db qa_ds with a password), for the datasource connect legs.
    • Personas: seeded admin; a plain member (admin create-user → change-password); ada@example.com (create-user → change-password → showcase_contributor granted via POST /data/sys_user_permission_set; a fixture of this run's making, since contributor invoice RLS keys on that email); anonymous.

    api-backend.query-contract-matrix — rev 5 — PASS

    Pins: objectql engine.test.ts + engine-filter-array-lowering.test.ts → Test Files 2 passed · Tests 196 passed (engine layer, stub driver — covers C4 only at that layer); rest request-schema-gate.conformance.test.ts → Tests 16 passed (door over a MOCKED protocol). Every clause driven live. Baseline: GET /data/showcase_account?$top=500 → 14 rows.

    clause verdict evidence
    acc[0] operator answer sets, both spellings pass 19 variants × 3 doors (POST $-object, POST AST, GET $filter AST), id-set equality with the local answer: eq(9), ne(5), gt>1e7(9), gte≥12e6(9, boundary incl.), lt(5), lte≤12e6(6), gt on date(6), in(12), not_in(2), between5e6,5e7, contains 'orth'(1), not_contains(13), starts_with 'Con'(1), ends_with 'Industries'(2), like '%Industries'(2), like 'Contoso' no wildcard (1, not wrapped), is_null(12), is_not_null(2). 0 failures
    acc[1] pagination windows pass GET $orderby=name&$top=3&$skip=2 = rows 3–5 of the sorted baseline; $top=4&$skip=9 = rows 10–13; POST limit/offset and top/offset identical
    acc[2] $select/fields + dotted refusal pass key set exactly name,status on all 14 rows (no id), GET and POST; POST fields:['account.name'] on showcase_invoice → 400 INVALID_FIELD naming expand; GET twin also 400 INVALID_FIELD
    acc[3] full sort both directions + INVALID_SORT pass full 14-value sequences asc/desc on annual_revenue equal the local sort (GET and POST); $orderby=no_such_field and POST orderBy:[{field:'nope'}] → 400 INVALID_SORT
    acc[4] expand enforces target RLS/FLS pass admin expand of INV-1001 → contact:{name:"Prospect 13"}; contributor's identical query → own row with contact left as the raw id, no contact fields; contributor GET /data/showcase_contact → 403 PERMISSION_DENIED
    acc[5] scalar comparand on a collection operator pass POST {status:{$nin:'done'}} → 400 VALIDATION_FAILED, fields[0].field = query.where.status.$nin, message names $nin, status, array; AST twin → 400 VALIDATION_FAILED at query.where; GET $filter twin → 400 INVALID_FILTER; no rows on any door
    acc[6] unknown inputs get exact codes pass ?$pageSize=5 → 400 UNSUPPORTED_QUERY_PARAM listing the supported params; ?not_a_field=x, POST where:{not_a_field}, GET $filter on an unknown field → 400 INVALID_FIELD
    acc[7] empty-list boundary pass $in:[] → 200, 0 rows; $nin:[] → 200, 10 rows (task total 10); GET twins the same
    acc[8] one verdict per variant pass all op:* and param:* variants recorded; $count=true → total:14; $search answers equal local; $expand=account → expanded {name}
    neg[0] no silent widening pass POST {limit:"ten"} → 400 VALIDATION_FAILED; {not_a_query_key:{…}} → 400 INVALID_FIELD; no rows; declared filter spelling served (control)

    api-backend.packaged-action-disabled-dispatch — rev 6 — PASS

    Pins: dogfood packaged-activation-ledger-reach → Tests 11 passed (full kernel boot, in-process HTTP); runtime action-activation-dispatch.test.ts → Tests 11 passed (unit, stub engine). REST, MCP and restart legs live.

    clause verdict evidence
    acc[0] REST 409 + nothing dispatched pass flip → 200; ledger row {metadata_type:'action', name:'showcase_mark_done', package_id:'com.example.showcase', active:false}, KEY SET [active, created_at, created_by, id, metadata_type, name, package_id, updated_at, updated_by] (no organization_id); invoke with recordId:'no-such-record-0000' → 409 ACTION_DISABLED "Action 'showcase_mark_done' is disabled — it is switched off for this installation in the packaged-metadata activation ledger (sys_metadata_activation, ADR-0126 §8)…"; task snapshot unchanged
    acc[1] consult precedes the param contract pass showcase_action_param_gallery {params:{bogus_key:1}}: disabled → 409 ACTION_DISABLED; re-enabled identical → 400 VALIDATION_ERROR "Invalid action params: Action param "p_text" is required; Unknown action param "bogus_key"…"
    acc[2] consult precedes the record load (by absence) pass disabled traces 409 with a nonexistent recordId; enabled control not expected to 404 (best-effort load, per knownGaps); no row changed
    acc[3] a 403 is never an oracle pass showcase_zoo_perm_missing 403 enabled and 403 disabled, bodies byte-identical; still 403 after restart
    acc[4] live MCP door pass POST /api/v1/mcp initialize → tools/list (12 tools incl. run_action) → tools/call run_action {actionName:'showcase_portfolio_snapshot'} ok; after disable isError:true with {"error":{"code":"ACTION_DISABLED","message":<same sentence as REST>,"status":409}}; re-enabled → ok
    acc[5] durable across restart, in-place re-enable pass boot-2 info log [ObjectQLPlugin] Activation ledger: 3 packaged action(s) are switched off …; post-restart 409; re-enable 200 then invoke 200 (progress 0 → 100); same row id qTiyulfRmAcji8qi, active:true
    acc[6] flow-type action refused by its own switch pass showcase_bulk_reassign → 409 ACTION_DISABLED naming it, before and after restart; target flow runs 0
    negatives pass anonymous invoke 401; 0 ledger rows at boot-1 start (absence = active); enabled param_gallery answered 400, not 409

    automation.packaged-flow-disable-durable — rev 5 — PASS

    No automated.ref (supporting only: service-automation flow-activation-ledger.test.ts 47 passed, engine layer). Boots 1 and 2 on the same file DB. The first baseline create answered 400 because project is required, so the sequence was toggle off → on → baseline urgent create (run completed) → off, all on the same row.

    clause verdict evidence
    acc[0] durable row, no org column pass one row {id:'_93udVOz4uQ8b5bH', metadata_type:'flow', name:'showcase_urgent_task_alert', package_id:'com.example.showcase', active:false}; key set has no organization_id
    acc[1] refusal names the ledger pass POST /automation/showcase_urgent_task_alert/trigger → 409 FLOW_DISABLED "Flow 'showcase_urgent_task_alert' is disabled — it is switched off for this installation in the packaged-metadata activation ledger (sys_metadata_activation, ADR-0126 §7.2)…"
    acc[2] survives a cold restart, unbound pass _status enabled:false, bound:false; boot-2 log [Automation] Activation ledger: 1 packaged flow(s) are switched off for this installation and were left unbound — 'showcase_urgent_task_alert'.; banner 18 bound vs 19 on boot 1
    acc[3] kill switch holds on the new process pass runs 1 before; urgent create 201; runs 1 after
    acc[4] re-enable updates in place, firing restored pass toggle on → 200, same row id, active:true; _status enabled/bound; next urgent create → runs 2, newest run completed on the new task
    negatives pass row written (not process-only); restart did not re-arm; re-enable did not delete

    identity-auth.default-organization-boot-invariant — rev 2 — PASS

    clause verdict evidence
    acc[0] pins pass pass plugin-auth ensure-default-organization.test.ts Tests 57 passed; runtime identity-objects-default-organization.pin.test.ts Tests 4 passed
    acc[1] Default Org exists before the first sign-up pass log 12:50:22.758Z [default-org] created the Default Organization (org_mvjjgxp3h7i41p75) → 12:50:31.260Z Dev admin seeded; org created_at 12:50:22.744Z vs admin created_at 12:50:31.039Z
    acc[2] first user owner, second member, active org pass sys_member admin owner, member member; both sessions activeOrganizationId: org_mvjjgxp3h7i41p75
    acc[3] seeded business units carry the Default Org pass 5/5 sys_business_unit rows stamped; all 19 seeded objects 0 NULL organization_id

    Note: admin create-user answered membershipCreated:false yet a member row existed afterwards (reconciled at sign-in, presumably); no clause affected.

    platform-core.seed-integrity — rev 4 — PASS

    clause verdict evidence
    acc[0] counts equal the authored seed pass ShowcaseSeedData imported via tsx: 19 datasets, 132 rows; live totals 0 mismatches (account 14, contact 32, inquiry 3, product 4, project 5, task 10, category 4, showcase_business_unit 9, sys_business_unit 5, team 2, project_membership 3, field_zoo 2, invoice 12, invoice_line 5, expense_report 4, expense_line 13, preference 1, announcement 2, client_brief 2)
    acc[1] values land verbatim pass Specimen — Full field-by-field: multiselect as a set, lookups resolve to the right ids, json structurally equal; normalizations only 14:30 → 14:30:00 and datetime .000Z; f_password reads back masked
    acc[2] boot log reports success pass Seeds: com.example.showcase 132 rows; [Seeder] Seed loading complete {"inserted":132,"updated":0,"skipped":0,"errored":0}
    acc[3] replay is idempotent pass pre/post-restart sweep diff empty; boot 2 {"inserted":0,"updated":0,"skipped":132,"errored":0}
    acc[4] deliberately unseeded fields pass f_secret, f_user, f_users, showcase_task.cover null as authored
    negatives pass no zero count, no growth on restart

    integration-system.datasource-credential-refusal-matrix — rev 5 — PASS

    No pins. Live boots, the scratch PG, and os build on 14 scratch apps outside the tree. Legacy rows planted by direct INSERT into the run's own sys_metadata while the server was stopped (boot 2 logged [metadata_spec_invalid] … Registered anyway for each).

    clause verdict evidence
    acc[0] every inline-credential door refuses at publish pass build door: 13 cases exit 2 with no artifact, each naming its path (config.password pg/mysql, authToken turso, url userinfo, syncUrl userinfo, ?password=, ?authToken=, mongodb options.auth.password, nested pwd, ${PG_HOST} placeholder, root alias passwd, turso encryptionKey on file:, connector inline authentication); 12 point at credentialsRef/credentialRef, encryptionKey gets the removal prescription; bound control builds. Runtime POST /api/v1/datasources: 21 variants → 400 DATASOURCE_ADMIN_ERROR naming the path, no value echoed. PUT /meta/connector/:name with inline authentication → 422 INVALID_METADATA pointing at auth: {type, credentialRef}; {type:'none'} → 200
    acc[1] credentialsRef path publishes AND connects pass POST /api/v1/datasources {name:'qa_pg', driver:'postgres', …, secret:…} → 201; POST /datasources/qa_pg/test → {ok:true, latencyMs:8}; wrong secret → ok:false "password authentication failed"
    acc[2] read doors redact and agree pass planted values absent from the admin door, the meta door and GET /data/sys_metadata?type=datasource; doors agree on the redacted config; meta door serves only external.credentialsRef: "sys_secret:sec_…"; raw DB still holds the planted cleartext (redaction is on read)
    acc[3] presence-preserving pass hasSecret:true vs false distinguishable with the same visible config; legacy rows list withheld keys in redactedConfigKeys
    acc[4] legacy alias spellings pass passwd, pwd, token, jwt, auth_token, authtoken, encryptionKey, URL userinfo — none readable on any door, aliases listed in redactedConfigKeys
    acc[5] unknown driver pass fixed spellings redacted and URL userinfo stripped on both doors; apiSecret reads back as written (the ruled #21921 boundary)
    acc[6] echoed-config write guard pass PATCH with the config as read → 200, hasSecret still true, same credentialsRef, /test ok; PUT → 405; withheld keys carried forward on a legacy row
    acc[7] not a privilege artifact pass member 403 PERMISSION_DENIED and anonymous 401 UNAUTHENTICATED on all five read doors
    negatives pass no cleartext on any read; build and runtime doors agree

    Off-clause probes (~8 min, boot 2, admin / member / anonymous)

    Data GET and /query with empty, negative and non-numeric paging values, unknown operators, malformed $between / $in:null; activation and toggle doors with unknown ids, wrong object segments, string booleans, member flips (403), member ledger read (403), generic POST to sys_metadata_activation (405); anonymous MCP tools/call (401); $search/filter side channels over stored metadata (none). Findings: the paging defect in the body (negative $skip/offset → 500; non-numeric or negative $top/limit ignored), extracted separately.


    Generated by Claude Code

  3. os-sales commented on Oct 22, 2026

    @os-sales
    CollaboratorAuthor

    Per-clause verdicts 3/5 — CLI and MCP stdio (4 items)

    4 PASS / 0 PARTIAL / 0 FAIL / 0 BLOCKED / 0 NOT-RUN.

    All boots from a scratch copy of examples/app-showcase (tar copy, node_modules re-linked to the workspace packages) or scratch projects outside the tree; ports 47621–47627; sequential within this runner, ~85 min (about a quarter of it fixture building).

    Tier 1 pins (--maxWorkers=1; no pin here is *.e2e.*, so none needed OS_TEST_TIERS)

    package files summary layer reached
    @objectstack/mcp src/__tests__/plugin.test.ts Test Files 1 passed (1) · Tests 17 passed (17) plugin unit (MCPServerPlugin over a stubbed engine)
    @objectstack/runtime organization-ownership-boot-guard.test.ts + .kernel.test.ts Test Files 2 passed (2) · Tests 32 passed (32) engine (real kernel + ObjectQL + SqlDriver/node:sqlite; no HTTP)
    @objectstack/plugin-security src/authz-identity-objects-boot-refusal.test.ts Test Files 1 passed (1) · Tests 3 passed (3) engine
    @objectstack/cli the 8 ceremony pin files of the item's automated.ref Test Files 8 passed (8) · Tests 179 passed (179) CLI integration over real SQLite files (no HTTP)

    ai.mcp-stdio-fail-closed — rev 6 — PASS

    Pin (plugin unit) + live os start -p 47626 -d file:(scratch)/mcp.db (--log-level info where an info line is the oracle), 13:05–13:22 UTC. Fixture provisioned on os dev --seed-admin over the same file: a contributor-bound member owning one invoice it created, a client_liaison-bound member, admin; each minted its own osk_ key with POST /api/v1/keys. Client: a node script speaking newline-delimited JSON-RPC over the child's stdin/stdout. tools/list carried query_records, get_record, aggregate_records. stdout carried only JSON-RPC lines in every session, 0 bytes on every refused boot.

    clause verdict evidence
    acceptance[0] keyless refusal pass OS_MCP_STDIO_ENABLED=true, key unset: exit 1, no listener, stderr [MCP] The stdio transport is enabled (OS_MCP_STDIO_ENABLED / autoStart) but OS_MCP_STDIO_API_KEY is not set. … Refusing to start an unscoped stdio server (ADR-0101).
    acceptance[1] unknown/revoked key refused up front pass osk_unknown: exit 1, did not resolve to a valid identity (unknown / revoked / expired / owner-less). Refusing to start stdio (ADR-0101).; revoked key: exit 1, [MCP] OS_MCP_STDIO_API_KEY is refused (API_KEY_REVOKED): This API key (osk_fxDvtA-J…) was revoked. … Refusing to start stdio (ADR-0101).; neither served
    acceptance[2] principal-bound line pass INFO [MCP] stdio transport principal-bound to OS_MCP_STDIO_API_KEY identity 2Yvp5P4E… (RLS/FLS/tenant applied) = the contributor's sys_user id
    acceptance[3] MCP reads == REST, both sides pass showcase_invoice: member MCP 1 row == member REST 1 row; member MCP get_record of a foreign invoice → isError "not found" == REST 404 RECORD_NOT_FOUND; admin REST and admin-keyed MCP 13 rows incl. it; field sets equal (18 == 18). FLS (liaison): showcase_project 5 rows on both, budget/spent/budget_remaining present on 0 rows on both (19 == 19 fields); admin 22 fields with all three present
    acceptance[4] member aggregate == member REST, < admin pass count by status: member MCP [{draft:1}] == member REST; admin REST and MCP draft 3 / paid 5 / sent 5
    acceptance[5] FLS aggregate fails closed, control succeeds pass liaison MCP sum(budget), count_distinct(spent), groupBy budget → isError {"error":{"code":"PERMISSION_DENIED","message":"[Security] Field read denied: not permitted to aggregate [budget] on 'showcase_project'","status":403}}, no number; REST the same 403; control count-by-status identical on MCP and REST
    acceptance[6] revocation bites the next call, coded pass same live session: pre-revoke query_records 1 row; admin PATCH /api/v1/data/sys_api_key/<id> {"revoked":true} → 200; next query_records isError: true, content {error:{code:"API_KEY_REVOKED", status:403, message}}, no records
    acceptance[7] stdio disabled ⇒ no key demanded pass health 200; INFO [MCP] Transport not auto-started (HTTP is served per-request at /api/v1/mcp regardless). …

    cli.dev-boot-contract — rev 7 — PASS

    No automated.ref; all live, 12:46–12:55 UTC, under a wrapper recording $?.

    clause verdict evidence
    acceptance[0] health + ready pass dev --ui --seed-admin -p 47621 -d file:…/dev.db: health 200, /api/v1/ready 200 {"status":"ready","state":"running"}; banner prints Environment ID, Artifact, Database
    acceptance[1] fixed seeded admin, idempotent pass admin123 → 200; restart on the same DB with --admin-password changed99: admin123 → 200, changed99 → 401
    acceptance[2] one DB resolution matrix pass -d file: → that file; OS_DATABASE_URL=file: → that file (beat a legacy file present); -d :memory: → :memory:, sign-in 200; config-declared default datasource → its own file, not objectstack.db; nothing chosen → <cwd>/.objectstack/data/objectstack.db, persistent across boots; legacy dev.db only → served with exactly 1 ⚠ Reading legacy database file … migrate with: mv … notice
    acceptance[3] --fresh tempdir, carve-out honest pass 🧪 Fresh OS_HOME: /tmp/objectstack-dev-…; dir gone after SIGINT, exit 130; project's showcase_external.db survives (documented #5594 carve-out)
    acceptance[4] port shift reported pass ⚠ Port 47621 is in use — serving on 47622 instead., ↪ server bound to port 47622 (requested 47621); health on 47622 200
    acceptance[5] stale artifact warned, still boots pass ⚠ dist/objectstack.json is OLDER than your sources — this boot serves the STALE build. + newest source + fix line; health 200
    acceptance[6] no config ⇒ exit 1 with remedy pass ✗ Config file not found … + remedy line; exit 1
    acceptance[7] console login page pass /_console/ → /_console/login; screenshot shows the "Welcome back" sign-in card (Email, Password, Sign In, "Sign in as dev admin")

    Exit codes: plain os dev SIGINT → 0 (8 boots); --fresh SIGINT → 130; no-config → 1. Negatives held: --database-driver memory → INVOCATION ERROR, exit 2; OS_DATABASE_DRIVER=memory|mingo|in-memory → exit 1 naming --fresh and :memory:; -d memory:// / mingo:// → ✗ An OS_DATABASE_URL with the "memory://" scheme selects the in-memory (mingo) engine …, exit 1.

    platform-core.v18-boot-refusals — rev 6 — PASS

    v17-shaped DB from published @objectstack/cli@17.7.0 booting a minimal one-object app on a fresh file DB, then a few rows through its HTTP doors (68 tables, 4 adr-0093/0104 markers, no adr-0131); copies only. v18 boot: OS_DATABASE_URL=file:COPY OS_AUTH_SECRET=… OS_SECRET_KEY=… os serve --no-ui -p 47624, 12:58 UTC.

    clause verdict evidence
    acceptance[0] pins pass runtime 2 files / 32 tests; plugin-security 1 file / 3 tests (engine layer)
    acceptance[1] refusal before listening pass exit 1, no ready/listening line; ✗ This database carries no organization-ownership marker: 17.x or an 18.0 pre-release (18.0.0-next.*) created it, … still carry a physical organization_id column … Run 'os migrate organization-ownership --out PLAN' … then … --apply --plan-file PLAN --backup-taken, and start again.; 0 hits for 17.x release / fresh installs only; same text on a second copy
    acceptance[2] refused boot wrote no row pass node:sqlite row-level snapshots before/after (WAL incl.): 68 pre-existing tables, 0 row/value differences, 0 columns dropped; 47 additive tables created with 0 rows; 0 adr-0131 marker rows
    acceptance[3] auth-less security boot refused naming createIdentityObjectsPlugin() pass scratch project mounting new SecurityPlugin(): os serve --dev --no-server --no-ui --preset minimal → exit 1, ✗ SecurityPlugin cannot boot: its authorization store reads sys_user, sys_member, and this kernel does not register sys_user, sys_member. … mount createIdentityObjectsPlugin() from @objectstack/plugin-auth.

    cli.organization-ownership-ceremony — rev 4 — PASS

    Pins + live on copies of the v17 DB above and of an 18.0.0-next.5 DB, 12:59–13:31 UTC, under the deployment's composition (OS_AUTH_SECRET + OS_SECRET_KEY, or --dev for the next.5 leg).

    clause verdict evidence
    acceptance[0] pins pass cli 8 files / 179 tests; runtime boot-guard 2 files / 32 tests
    acceptance[1] bare plan changes nothing; --apply without a plan refused pass plan exit 0, kind: organization-ownership-plan, ceremony: ADR-0131 D10, 135 table entries; DB sha256 (+ -wal) identical and row diff 0 on 3 copies; --apply --backup-taken without --plan-file → exit 1 ✗ Refused: --apply executes only a plan you reviewed. …, checksum unchanged
    acceptance[2] reviewed apply completes, marker at generation 1 pass after one refused v18 boot: apply exit 0, complete: true, blocking: 0, all 13 steps done; adr-0131 marker row blocking 0, shapeGeneration 1 == ORGANIZATION_OWNERSHIP_SHAPE_GENERATION
    acceptance[3] served boot refuses unmigrated, boots migrated pass unmigrated copy: exit 1, no listener, refusal names os migrate organization-ownership; migrated copy: ✓ Server is ready, /ready 200, v17 admin password signs in 200
    acceptance[4] drop gate on an unmigrated v17 DB pass (reactions converted first) os migrate apply --allow-destructive --yes --json → exit 1, refused_organization_ownership_unmigrated, the 8 tables named, applied 0, main-file checksum unchanged, row diff 0
    acceptance[5] 18.0 pre-release DB upgrade keeps its data pass fixture: npm @objectstack/*@18.0.0-next.5 + showcase source at tag commit 820fd5a, os dev --seed-admin, then members, password resets, verification resends, invitations, global mail settings, an activated declared webhook mirror, tasks — all through HTTP. Run subject over a copy: exit 1 naming sys_flow_credential, sys_secret; refused boot row diff 0. Plan exit 0; apply exit 0, complete, blocking 0; second boot ready. 4/4 identity mails moved to sys_identity_email under original ids, 3/3 invitation mails stay in sys_email; 6/6 global sys_setting_audit rows moved to sys_platform_setting_audit, 1 tenant row untouched; completed mail jobs unchanged; per-table count changes exactly equal the apply report. After upgrade: three accounts sign in with their 18.0 passwords (200; wrong → 401); a token from a moved reset mail resets the password (200); one task PATCH wrote exactly one declared:showcase_task_changed delivery row

    Negative held: no refusing arm or plan run altered a DB (checksums and row diffs above).

    Fixture gaps / deviations

    • The v17 DB came from cli@17.7.0 booting a minimal app, not a 17.x showcase (17.x showcase source does not load on the run subject's spec).
    • The next.5 fixture held no pending mail job, so the "pending job payload rewritten" sub-leg of ceremony acceptance[5] held only vacuously (jobsRewritten 0); that sub-leg rests on the unit pins.
    • The contributor recipe's own-invoice step fails on a churned account (400 "This account has churned").

    Off-clause probes (~12 min)

    os start stdio admission with empty, whitespace and prefix-only keys and every enable-switch spelling (all failed closed, 0 bytes stdout); aggregate doors over field-level-security-hidden fields as the liaison; the ceremony's partial-apply/resume path. Findings: one low-severity authorization-adjacent observation, detail withheld pending maintainer (no value leak); a UX note that the ceremony plan's printed summary does not surface its "boot this release once" precondition (in the body's accuracy findings).


    Generated by Claude Code

  4. os-sales commented on Oct 22, 2026

    @os-sales
    CollaboratorAuthor

    Per-clause verdicts 4/5 — boot, console login, nav (4 items)

    3 PASS / 1 PARTIAL / 0 FAIL / 0 BLOCKED / 0 NOT-RUN.

    One boot served all four items: OS_PORT=47631 setsid pnpm -C examples/app-showcase exec objectstack dev --ui --seed-admin -p 47631 -d file:(scratch)/boot.db, fresh DB, process start 2026-10-22T12:46:30Z, health 200 at 12:47:04Z; banner Tenancy: single, 53 plugins, Seeds: com.example.showcase 132 rows; stopped by process group. Headless Chromium via /opt/pw-browsers/chromium, own context and port. Personas: seeded admin; seeded member Mei (sys_member.role member), identity checked with get-session before each member cell. Sequential within this runner.

    platform-core.boot-health — rev 9 — PASS

    Boot-window log copied at the first health 200, before any other request.

    clause verdict evidence
    acceptance[0] health 200 pass GET /api/v1/health 200 {"status":"ok","version":"18.0.0"}, 34.1 s from launch (polled every 0.5 s)
    acceptance[1] ready 200 pass GET /api/v1/ready 200 {"status":"ready","state":"running"}
    acceptance[2] no misauthored Flows warning, no ERROR pass Flows: 32 flow(s) 19 bound … · 8 draft; only two ℹ "NOT bound — disabled by deployment policy" lines (showcase_task_due_reminder time_relative, showcase_scheduled_digest schedule); grep of the window for ERROR/⚠/reject/dropped/FATAL/error → 0 lines; 2 informational WARNs in diagnostics (print engine; sharing-rule note)
    acceptance[3] merged app metadata resolves pass GET /api/v1/meta/app?id=showcase_app 200 with 1 item; bare /meta/app lists showcase_app, setup, account
    acceptance[4] served nav equals authored nav pass 49 served leaves vs 49 authored nav_* ids in src/ui/apps/index.ts; diff empty both ways
    acceptance[5] console shell served pass GET /_console/ 200 text/html; charset=utf-8 (18301 B)
    negative[0] SeedLoader rejection held 132 rows, no ⚠, no error count, no dropped
    negative[1] ERROR in boot window held 0 ERROR lines from process start to health green

    platform-core.console-login — rev 4 — PASS

    Real form sign-in (role-targeted fill and click); both auth halves present (auth-session-token in localStorage and the better-auth.session_token cookie). pageerrors: 0.

    clause verdict evidence
    acceptance[0] login lands in the shell pass sign-in POST 200 → /_console/home; screenshot: header, "Your apps" launcher (Showcase tile), My work, Recently Accessed; first authed calls all 200
    acceptance[1] session survives reload pass reload on /apps/com.example.showcase/showcase_project stays put; get-session 200, meta/app 200
    acceptance[2] server 401 after expiry pass server-side POST /api/v1/auth/sign-out 200; old bearer → GET /api/v1/meta/app?id=showcase_app 401 UNAUTHENTICATED
    acceptance[3] console returns the user to login pass next navigation → get-session 401 → /_console/login?redirect=%2Fapps%2Fcom.example.showcase%2Fshowcase_task; screenshot shows the login form, not a dead shell
    acceptance[4] re-auth restores the session pass re-sign-in 200 lands on the same route; Tasks grid with 10 records; fresh reads 200
    negative[0] wrong password held 401 INVALID_EMAIL_OR_PASSWORD, no set-cookie, no token, red alert "Invalid email or password. Please try again.", stays on /login
    negative[1] expired session serving a cached shell held not observed (acceptance[2]–[3])

    platform-core.nav-surfaces-render — rev 6 — PASS

    Pin: SMOKE_API_URL=http://localhost:47631 OS_TEST_CHROMIUM_EXECUTABLE_PATH=/opt/pw-browsers/chromium pnpm -C examples/app-showcase exec playwright test --config=playwright.config.ts e2e/showcase-smoke.spec.ts (webServer reused the boot). Layer: HTTP + real browser.

    clause verdict evidence
    acceptance[0] no pageerror / empty main / splash; charts laid out pass full re-run 49 passed (2.1m). The first full run was 48 passed / 1 failed: Hours by Status (Chart) died with locator.waitFor: Target crashed (renderer crash before any assertion); the 6 chart surfaces in isolation went 6/6 twice and the full re-run 49/49 — non-reproducing, environment-class, nothing filed
    acceptance[1] smoke coverage is current pass 49 served nav leaves mapped to console paths vs 49 SURFACES; diff empty both ways
    acceptance[2] bad route gives a named state inside a live shell pass /_console/apps/com.example.showcase/object_that_does_not_exist: header and full sidebar alive, main "Object Not Found — The object "object_that_does_not_exist" does not exist in the current configuration…"; same on a fresh load; /page/page_that_does_not_exist → "Page Not Found" in the shell
    acceptance[3] render failures surface the ErrorBoundary pass (none encountered) no render failure in 49+49+12 smoke loads or 3 bad-route probes

    platform-core.builtin-apps-nav-render — rev 7 — PARTIAL

    Manual browser + API; no pin. Admin's apps ["showcase_app","setup","account"]; ?id=studio → 200 items: [] (expected: studio is not loaded on a stock boot).

    clause verdict evidence
    acceptance[0] every Setup and Account destination renders (admin) partial collapsed groups expanded; every sidebar href visited, screenshot first then DOM. Setup 42/42 (+ "All reports") and Account 8/8 (+ "All reports"): 0 pageerrors, no blank main, no "no actions configured", no ErrorBoundary; all 52 screenshots show real surfaces. Exception: Browse Marketplace shows the named "Failed to load marketplace · … (HTTP 502) · fetch failed" with Retry, because GET /api/v1/marketplace/packages → 502 MARKETPLACE_PROXY_FAILED (egress to cloud.objectos.ai denied) — the catalog content is blocked(environment, knownGap / #22017 row 10); the surface renders honestly. All 9 settings namespaces and the hub render; Users, Organization, Business Units, Teams, Sessions, OAuth Applications render; a user record's Preferences and Linked Accounts tabs render
    acceptance[1] served nav matches authored shell + contributions pass Setup 9 groups / 42 leaves = setup.app.ts + setup-nav.contributions.ts after gates; Account Profile + Inbox (2), Security (2), Developer (3) = 8
    acceptance[2] member: Setup refuses with a named state, Account opens pass as Mei, /_console/apps/com.objectstack.setup → console's GET /api/v1/meta/app/com.objectstack.setup 403 → "You don't have access to this app — This app exists, but your account is not authorized to open it…"; same on a deep link; Account opens on Profile
    acceptance[3] app gate is server-side pass member GET /api/v1/meta/app?id=setup → 200 items: []; GET /api/v1/meta/app/com.objectstack.setup → 403; member's bare list ["showcase_app","account"]
    acceptance[4] entry gate enforced one level up pass member's ?id=setup empty, so gated Setup entries are unreachable; Account nav 8 vs 8, empty diff (presence-only control, as the clause says)
    acceptance[5] service/object gates resolve to absence pass nav_organizations and nav_account_memberships absent (single tenancy); nav_business_units and nav_notifications present and render; nav_jwks absent
    acceptance[6] off-capability destinations pruned pass SSO Providers absent (OS_SSO_ENABLED unset); sys_verification / sys_device_code entries absent
    negative[0] blank or placeholder destination held 0 of 52
    negative[1] member reaches Setup or Studio content held 403 and an empty payload
    negative[2] gated entry rendered for the member, then errors held the member has no Setup nav

    Off-clause probes (~12 min, admin and member)

    Sign-in with empty email+password (400 INVALID_EMAIL), unknown email (401, same answer as a wrong password — no account enumeration), missing password (400 VALIDATION_ERROR), none setting a cookie or token. In-SPA expiry after a server sign-out shows the named "Sign in required" state with Retry and no stale data. An unknown app shows "This app can't be opened" with Retry / Back to home. One client-courtesy note: on one alternate Setup route spelling the console renders settings chrome to a non-entitled member instead of the app-level refusal; the server refuses the data read 403 and nothing is exposed. Findings filed: none.


    Generated by Claude Code

  5. os-sales commented on Oct 22, 2026

    @os-sales
    CollaboratorAuthor

    Per-clause verdicts 5/5 — approvals entry, CRUD round-trip, Studio first-run loop (3 items)

    2 PASS / 0 PARTIAL / 1 FAIL / 0 BLOCKED / 0 NOT-RUN.

    Boot 1: OS_PORT=47636 setsid pnpm -C …/examples/app-showcase exec objectstack dev --ui --seed-admin -p 47636 -d file:(scratch)/main.db, 2026-10-22T12:46:30Z → 13:12Z, covered every clause. Boot 2 (13:12:50Z, same DB) only for crud-roundtrip's readonly clone leg (a row planted while stopped) and the probes. Headless Chromium 141 via playwright-core 1.63.0, form sign-in every time (bearer token and cookie both present, checked). The approvals FAIL was then re-derived by an independent runner on its own boot (port 47641, 13:17–13:23Z) with its own persona — see the body.

    approvals.account-app-entry — rev 4 — FAIL

    Persona minted at runtime: admin invite-member → sign-up/email → admin POST /data/sys_user_position contributor (201) and finance (201); get-session positions ["org_member","contributor","finance","everyone"], role member, no Setup. Fixture: the persona created invoice INV-UI2-APPR (owner = its own email) and PATCHed it status=sent, issued_on → launched areq_9d00f955-… on flow:showcase_invoice_signoff, persona in pending_approvers, decision_progress {got:0, need:2}.

    clause verdict evidence
    acceptance[0] registration survives bundling pass dist/.objectui-sha = .objectui-sha (cd14d8ee…); grep -o approvals:inbox dist/assets/index-*.js → 1 hit in the entry chunk, context {ref:\approvals:inbox`,label:`Approvals Inbox`,source:`@object-ui/console`,component:…Suspense…}`
    acceptance[1] entry renders the full inbox pass Avatar › Profile → Inbox › Approvals → /_console/apps/com.objectstack.account/component/approvals/inbox; screenshot: "Approvals Inbox", tabs My Pending (1) / Submitted by me / All, one row (Invoice Dual Sign-off · INV-UI2-APPR · Pending), keyboard hint bar; no component-not-found state
    acceptance[2] a non-admin reaches it pass persona GET /api/v1/meta/app → ["showcase_app","account"] (no setup); ?id=setup → items: []; account nav keeps nav_account_approvals (component, approvals:inbox)
    acceptance[3] decision actions render and one moves the request fail Server half holds: in-row Approve → POST /approvals/requests/<id>/approve 200; progress 0/2 → 1/2, persona dropped from pending_approvers, timeline row appended, viewer.can_act true → false. Rendered half fails: opening the request (row click, Enter, or ?request=) lands on /_console/apps/com.objectstack.account/sys_approval_request/record/<id>, which renders only the heading "Approval Request areq_…" and empty Details/Timeline tabs — no fields, no action set, no can_act=false explanation, before and after the decision. Admin on the same URL: full page with Approve / Reject / Reassign / Send back / Request info / Recall / Reply. Reproduction rule and mechanism in the issue body
    acceptance[4] ?request= deep-links on this component route pass (routing) cold load of …/component/approvals/inbox?request=<id> → redirects to …/sys_approval_request/record/<id>, the exact request; its blank body is the acceptance[3] defect, not double-counted
    acceptance[5] record link keeps the current app segment pass row link href="/_console/apps/com.objectstack.account/showcase_invoice/record/<id>"; persona's direct GET /api/v1/data/showcase_invoice/<id> → 200
    acceptance[6] zh-CN wording pass Avatar › Preferences › Language → 中文(中国): nav 待我审批, tabs 待我审批 (1) / 我发起的 / 全部, status badge 待审批
    negative[0] no missing-component empty state held inbox renders fully
    negative[1] Approvals entry survives for the non-admin; setup absent held as acceptance[2]

    Rule-2 checks: reproduced 5× on fresh contexts (title click, status-cell click, j+Enter, ?request= cold load, direct URL with a 7–8 s settle); URL-level navigation reproduces with no click (no coordinate artifact); identity proven per context; console stamp = pin. Independent re-derivation: 3 more reproductions with a different persona and two new requests, an admin control, and a causal check (see body).

    records-forms.crud-roundtrip — rev 13 — PASS

    Admin, plus the search:qa-contributor-bound-member recipe for clauses 6–7 (invite → sign-up → sys_user_position contributor); premise: the member's invoice list total 0 and a foreign invoice by id → 404 RECORD_NOT_FOUND; own row visible.

    clause verdict evidence
    acc[0] create verbatim re-read pass form create → POST /api/v1/data/showcase_account {"industry":"finance","status":"active","sales_region":"amer","name":"os-qa-ui2r1"} 201; filtered GET total 1, values identical, website:null (not defaulted), owner_id = admin
    acc[1] update persists only the edited field pass Edit Annual Revenue → PATCH {"annual_revenue":1234567} 200; full-row diff: only annual_revenue and updated_at changed
    acc[2] delete authoritative pass Delete → confirm → DELETE 200 {success:true}; filtered GET total 0; by id 404 RECORD_NOT_FOUND
    acc[3] list reflects each mutation after a full reload pass post-create reload shows the row with option chips; post-delete reload: row gone
    acc[4] History shows display values pass "Dev Admin · UPDATE · Annual Revenue: — → 1234567"; "Dev Admin · CREATE · 5 fields populated → Owner: Dev Admin, Account Name: os-qa-ui2r1, Industry: Finance, Lifecycle: Active, Sales Region: AMER"; localized date on a seeded field "Customer Since: 3/15/2024" (stored 2024-03-15); no phantom rows
    acc[5] unicode round-trip + quick-search pass os-qa-ui2r1-华宁 201, byte-identical on read (hex compared); $search=华宁 → 2 rows; list quick-search UI shows both
    acc[6] clone semantics pass admin POST /data/showcase_account/<id>/clone {overrides:{name:…}} 201, only id/timestamps/name differ; contributor clone of an admin-owned zoo row → owner_id/created_by/f_owner = contributor, f_autonumber 0002 → 0003 re-derived; clone of a row whose lead_score was planted 42 → 201 with droppedFields:[{"object":"showcase_contact","fields":["lead_score"],"reason":"readonly"}], clone reads lead_score:null
    acc[7] clone is RLS-gated pass contributor clone of an invisible invoice → 404 RECORD_NOT_FOUND; admin invoice count 13 before and after; own-row clone 201
    neg[0] empty Account Name blocks held field error "Account Name is required" + toast; no POST sent; 0 empty-name rows
    neg[1] clone carry-over / RLS success held as acc[6]–[7]
    neg[2] direct POST missing name held 400 VALIDATION_FAILED fields:[{field:"name",code:"required"}]
    neg[3] bad tax_id held 400 VALIDATION_FAILED {field:"tax_id",code:"invalid_format",message:"Tax ID must look like 12-3456789."}; dialog stays open; 0 rows

    studio-authoring.first-run-loop — rev 6 — PASS

    Admin, boot 1, loop 13:05:14 → 13:11:51 UTC (the acc[0] pin is objectui-only, so driven by hand).

    clause verdict evidence
    acc[0] package create round-trips, switcher lists it without a reload pass Home → Build an app → New package (writable base): 维修中心 / com.example.repairs → POST /api/v1/packages 201 status:"installed"; switcher lists "维修中心 · com.example.repairs · Writable"; a second create kept a pre-set window marker with 0 load events
    acc[1] published object is server-real pass PUT /meta/object/repairs_repair_ticket?mode=draft… 200; Publish all → "Published 1 item in this package (one atomic release)"; GET /api/v1/meta/object/repairs_repair_ticket 200 with exactly the 3 authored status options (404 before publish)
    acc[2] Studio Records grid record persists pass Add record → POST /data/repairs_repair_ticket 201; GET total 1, status:"in_progress"
    acc[3] app live in launcher; list shows the label chip pass Create app "Repair Center" → publish 200; launcher shows Showcase + Repair Center; list shows the row with an "In Progress" chip (raw value absent); /meta/app adds repair_center with nav_repairs_repair_ticket
    acc[4] zero restarts / zero code pass one "Server is ready" in the log, same serve --dev child pid before and after the loop, no file edits
    acc[5] read-only gate enforced server-side pass forged new object into com.example.showcase → 422 WRITABLE_PACKAGE_REQUIRED; add-field on showcase_account → 403 ITEM_LOCKED; without package/draft → 403 NOT_OVERRIDABLE; nothing landed
    neg[0] silent acceptance into a read-only package held forged app also 422
    neg[1] app absent from launcher / raw values held as acc[3]

    Off-clause probes (~6 min; approver member, contributor, admin)

    Approvals: a non-participant's GET of a foreign request → 404 REQUEST_NOT_FOUND; approve of a foreign request → 403; approve while forging the admin as actor_id → 403, tally unchanged. Clone of unknown/empty id → 404. Contributor PATCH of an invisible invoice → 404. Forged app into the read-only package → 422. Finding: record History renders a currency change raw ("— → 1234567") while the header shows "$1,234,567.00" (cosmetic, objectui display).


    Generated by Claude Code

  6. os-sales commented on Oct 22, 2026

    @os-sales
    CollaboratorAuthor

    Close-out (extraction):

    Extracted: #25228 · approvals.account-app-entry · acceptance[3]
    Extracted: #25229 · api-backend.query-contract-matrix · off-clause probe
    

    This record stays open until #25228 is resolved and the approvals item is re-run on the moved pin.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    Contributor

    Re-test · approvals.account-app-entry acceptance[3] on the moved pin · PASS

    Session session_016ZEJXR398Mp5xcwuMncpK9, the maintainer-requested re-run of this record's one FAIL (decision on #25228). It ran on PR #25262's head: framework 8010b641 plus the pin move to objectui 0d12e95361b2 (objectstack-ai/objectui#13326, the fix for objectstack-ai/objectui#13324). The record stays open until #25262 merges.

    Environment: pnpm build --concurrency=2 all green; pnpm objectui:build at the pin, packages/console/dist/.objectui-sha = 0d12e95361b20b1eb9012f97d2d36e5fa7f14374; the built entry chunk carries the approvals:inbox registration. Stock examples/app-showcase on its own port and file DB, --seed-admin; headless Chromium 141.

    Fixture, this record's reproduction rule: a new persona, invited and signed up, holds positions contributor + finance. It owns a new invoice and PATCHed it to sent + issued_on, which launched a new showcase_invoice_signoff request. Before, as the persona: GET /api/v1/approvals/requests/:id → 200, viewer.can_act: true, persona in pending_approvers, decision_progress 0 of 3.

    step oracle reading
    console form sign-in get-session reads the persona; GET /api/v1/meta/app lists showcase_app, account, no setup
    Account › Inbox › Approvals /_console/apps/com.objectstack.account/component/approvals/inbox: tabs My Pending (1) / Submitted by me / All, the request row, the keyboard hint bar
    click the row /_console/apps/com.objectstack.account/sys_approval_request/record/:id renders the heading "Invoice Dual Sign-off (parallel approval)", the invoice fields, "Approvals — 0 of 3", waiting-on chips, and Approve / Reject / Reassign / Send back / Request info / Send reminder / Recall / Reply
    Approve, with a comment POST /api/v1/approvals/requests/:id/approve → 200; the page reads "Approvals — 1 of 3", the persona is gone from waiting-on, the approver actions are gone (the submitter's Send reminder / Recall / Reply remain), and Timeline lists Submit + Approve with the comment
    after, as the persona GET …/requests/:id → viewer.can_act: false, persona dropped from pending_approvers, decision_progress 1 of 3; GET …/requests/:id/actions → submit + approve

    On the old pin cd14d8eeffe1, the same steps give the blank page this record's FAIL describes. The pre-merge rehearsal on objectui PR head 8a3ef97 is objectstack-ai/objectui#13326 comment 6344288566.

    Checklist-accuracy note (not a fail), for the wave anchor: acceptance[3]'s "replaces the action bar with the can_act=false explanation" names an element of the side drawer that objectui#12484 retired. The served request page shows no separate "you can no longer act" sentence for any viewer; its declaration carries none. The rest of the clause (server-declared action set, tally moves, user dropped from pending_approvers, timeline row appended, decision actions leave the bar) holds as written.

  8. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    Contributor

    Close-out (checklist-test §5). Every fail in this record has a disposition, both extracted cards are closed, and the condition comment 6342183324 set for closing (#25228 resolved, the approvals item re-run on the moved pin) is met. The Extracted: lines are in comment 6342183324; this comment adds no new extraction.

    Closed on the conditions in comment 6342183324, and on the maintainer's instruction of 2026-10-19 quoted on #24908 (「qa-run的卡片处理完的是不是可以关闭」). Session session_016ZEJXR398Mp5xcwuMncpK9.


    Generated by Claude Code

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions