You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Full access-security area run of the checklist-test skill — all 19 items (18 runnable + 1 fixture-blocked) driven against a live showcase across 4 opus subagents, each on its own port + file DB. Text-only per RUNNER.md.
Result: 8 PASS · 2 PARTIAL · 8 FAIL · 1 BLOCKED.
⚠️One security-sensitive finding is NOT described here.rls-both-sides clause 5 and owd-sharing-matrix clause 4 are the same defect, and its write-up was delivered privately as FOLLOW-UPS.md D11 per the maintainer ruling recorded on #7463 (same handling as D1). Everything below is the non-sensitive remainder.
Environment — framework 92f26f75 (branch claude/platform-test-checklist-ocwugl) · console bundle 09987b680 (packages/console/dist/.objectui-sha) · showcase app · isolated port + file DB per batch · personas provisioned as real runtime sign-ups, never fixtures.
✅ PASS — 8 items
The enforcement core is genuinely strong. Highlights, all proven with effect-follow-up reads rather than status codes alone:
crud-permission-matrix drove 124 cells (31 access-matrix rows × 4 verbs), each as a dedicated persona holding exactly that set: zero withheld cells answered 2xx, zero denied edits mutated, zero denied deletes removed. (The item still fails on one allowed cell — see below.)
scope-depth-asymmetry (6/6) — read-org/write-own asymmetry proven on the same record id across four personas: manager reads a foreign private-OWD row 200 but PATCH → 403 with the row verifiably unchanged, while an org-writeScope persona PATCHes that same row 200 and it persists — so the denial is the guard, not a dead route.
fls-mask-and-strip (5/5) — a mixed forged write is refused atomically (the allowed field doesn't land either); the filter oracle is refused (filtering by a hidden field → 403, while the identical filter as admin returns rows). ⚠️Authoring caveat: masking budget+spent leaves the formula field budget_remaining unmasked — a formula over masked inputs is not auto-masked.
record-access-explain (5/5) — explain agrees with reality in both directions across three personas on one record. ⚠️Reader trap: the panel's top banner is the object-level verdict and reads ALLOWED even for a persona whose record verdict is not visible.
public-form-intake (7/7 + 3 negatives) — forged owner_id/organization_id/created_by all land null; a __proto__ body smuggling owner_id/status inherited nothing; no whitelisted field opens an anonymous lookup, so the form is not a directory oracle; 11 anonymous probes confirm the intake door opens nothing else.
🔴 FAIL — 8 items
1. crud-permission-matrix — an allowed create returns 500 while the row is written
123 of 124 cells are correct. The one break is a permitted operation, not a denied one.
Reproduction rule — boot showcase, sign up a plain member (no explicit grants), admin creates a project, then as the member: POST /api/v1/data/showcase_task {title, project:<id>, status:'todo'} Expected 201 (the access matrix says create:true; /security/explain confirms allowed=true). Actual500 INTERNAL_ERROR — yet an admin read shows the task did persist, with owner_id = the member. Reproduced 4×.
Server log: WARN Roll-up summary recompute failed {childObject:showcase_task, parentObject:showcase_project, field:task_count, error: Access denied: operation update on showcase_project …} ×2, then ERROR SummaryRecomputeError '…the triggering records WERE written (summary values may be stale)'.
Root cause (located).objectql/src/engine.ts:6139 — recomputeSummaries issues the parent roll-up write under the caller's execution context, so it passes through security as the caller. showcase_project declares two Field.summary roll-ups over showcase_task, and member_default grants read but not edit on showcase_project, so the internal recompute is refused; withTransientRetry then retries a non-transientPERMISSION_DENIED, and engine.ts:8454 throws SummaryRecomputeError, which REST maps to 500.
Contrast isolating it: the identical POST succeeds 201 for showcase_contributor (holds allowEdit on showcase_project) and for admin.
Suggested fix: run the roll-up recompute in a system context — it is an engine-internal derived write, not a caller write. At minimum, stop retrying a permission denial and stop surfacing a committed write as a 500. A tracker search suggests this is unreported.
2. owd-save-gate — the ADR-0090 D11 authoring gate never runs on any host-config deployment
Both layers fail: nothing on this deploy refuses a D11 violation. Studio turns the external-OWD description amber and literally says "publishing will be rejected (ADR-0090 D11)" — then leaves Save draft and Publish enabled, and both succeed.
Reproduction rule — PUT /api/v1/meta/object/qa_probe {…, sharingModel:'private', externalSharingModel:'public_read'}. Expected 403 owd_external_wider. Actual 200, and GET returns the violating pair persisted — on the draft path, the active path, and with ?package=. Reproduced 3× on one boot, 3× on a second boot with a fresh DB, plus once through the browser.
Root cause (located).metadata-protocol/src/protocol.ts:9667 wraps the #3050 authoring-gate call in if (this.environmentId !== undefined). The showcase config is a host config, so isHostConfig → shouldBootWithLibrary(config) === false, and serve.ts takes the lightweight assembler which constructs new ObjectQLPlugin() with no options → environmentId stays undefined → the guard is false and runAuthoringGate is never called. So R2 (owd_external_wider) and R1 (owd_widening_forbidden) execute on no self-hosted/host-config deployment.
This is the exact proxy-signal hazard #6710 already diagnosed and retired for the sibling #4463 gate.protocol.ts's own comment names "the CLI's lightweight host-config assembler … ALSO leaves environmentId undefined, and it serves an end-user PUT /api/v1/meta/"* — and then keeps the #3050 gate on the retired signal anyway. Suggested fix: key it on the declared authoringChannel, as #6710 did.
The gate logic is not the bug — object-posture-gate.test.ts is 18/18 green. A grep for owd_external_wider finds only the gate source and its unit test: there is no integration test exercising R2 through the real save path.
Security impact is bounded today: external-principal enforcement is #2696-planned, so a wider external baseline does not itself disclose anything yet. That is why this one is filed publicly rather than held back — it is an authoring-validation gap, not a live disclosure.
3. audit-log-browser — four of the ten audit actions have no writer anywhere
Reproduction rule — on a fresh boot: (1) sign up + sign in a member, then GET /api/v1/data/sys_audit_log?$filter={"action":"login"} as admin → total 0; the only trace is an unattributedupdate sys_user row (user_id null) diffing last_login_at. (2) PUT /api/settings/branding {"workspace_name":"X"} → 200, then filter {"action":"config_change"} → total 0; the event went to sys_setting_audit with action set instead. Both reproduced twice.
Action census over all 57 rows: {'create':48,'update':7,'delete':2} — only CRUD actions ever materialize. The delete half is correct (full old_value, actor, tenant).
Root cause (located).plugin-audit/src/audit-writers.ts subscribes only to the ObjectQL wildcard before*/after* CRUD lifecycle events, so it can emit create/update/delete/restore and nothing else. A repo-wide search finds no writer for login, logout, permission_change, config_change, export or import — though all are declared in the action enum of sys-audit-log.object.ts. Settings writes are audited by settings-service-plugin.ts:315 into sys_setting_audit, never sys_audit_log, while settings-service.types.ts:16 documents the service as "Emit sys_audit_log rows for every successful write."
Net effect: four enum values, two shipped list views (auth_events, config_changes) and two dashboard widgets (system_overview.dashboard.ts L89 + L109) are permanently empty. The console makes it worse by listing login/config_change in its own ACTION_OPTIONS — the client offers filters for rows the server never writes.
The tamper-proofing half is solid: POST/PATCH/DELETE to sys_audit_log all 405 OBJECT_API_METHOD_NOT_ALLOWED, list total unchanged.
4. permission-matrix-edit-loop — the field-level half of the permission matrix is dead for every object
The object-verb loop is fully working (checkbox → PUT → 200 → the persona's live DELETE flips 403→200, and back again on reverse; the editor re-reads the saved state after reload).
Reproduction rule — open /_console/apps/<app>/metadata/permission/<set> and expand any object row. Expected the field sub-table listing that object's fields with a readable/editable checkbox pair each. Actual"No fields registered for this object." and zero field checkboxes — for every object. Reproduced twice on fresh loads for two objects. Not a data problem: the network shows GET /api/v1/meta/object/showcase_project → 200 carrying 21 fields.
Root cause (located). objectui PermissionMatrixEditor.tsxensureFields() does const obj = await client.get('object', objectName); const raw = obj?.fields; — but MetadataClient.get() (metadata-client.ts:495-512) returns the raw server envelope with no unwrapping, and the framework answers {type, name, item:{…fields}}. So obj.fields is always undefined and every object reports zero fields. The client's own docblock at metadata-client.ts:522 asserts the opposite ("the legacy get() returns the unwrapped body") — that is where the two sides disagree.
Not a stale bundle — the same code is on objectui origin/main (6d01319dd). Collateral: the RLS CEL editor's field lint/autocomplete (loadObjectFields) is empty for the same reason.
5. record-share-grant-revoke — package-seeded sharing rules are unaddressable by name
Clauses 0–5 all pass, including that read access confers no re-share authority (403, ADR-0111 D1) and that a mis-scoped revoke 404s with the share surviving.
Reproduction rule — stock boot, admin bearer: (1) GET /api/v1/sharing/rules → 200 {"data":[]} though sys_sharing_rule lists 4 active seeded rules. (2) GET /api/v1/sharing/rules/share_red_projects_with_execs → 404 RULE_NOT_FOUND. (3) POST …/<name>/evaluate → 404. (4) Control: POST /api/v1/sharing/rules/srule_<row id>/evaluate → 200 {matchedRecords:1, expandedUsers:2, grantsCreated:1}.
Root cause.plugin-sharing/src/sharing-rule-service.ts applies a strict-equality org filter that package-seeded rows cannot satisfy: listRules does if (orgId) where.organization_id = orgId, and getRule's name fallback is orgId ? {name, organization_id: orgId} : {name} — while package-seeded rows carry organization_id = null and an authenticated admin's context carries organizationId='org_…'. Only the unfiltered by-id branch survives.
Confirming experiment: a rule created via the API (where defineRule stamps organization_id from the same context) is findable by name, listed, and evaluable by name.
Blast radius: every package-declared sharing rule is invisible and unmanageable through the admin API. Enforcement is unaffected, because the boot reconcile uses SYSTEM_CTX which carries no org — which is exactly why this stayed invisible. A sibling batch independently bounded the bug to package-seeded rows and confirmed the console's Sharing Rules page is not affected (it reads the data API, not /api/v1/sharing/rules, so it lists all four honestly).
6. suggested-binding-loop — two defects
(a) The isDefault suggestion is never surfaced on stock.GET /api/v1/security/suggested-bindings → 200 {suggestions:[], synced:{created:0}} and the table is empty, though the permission set and its everyone binding both exist. Discriminating step: delete that binding row and re-list → synced {created:1} and a PENDING row appears — so the declaration is collected. Root cause: suggested-audience-bindings.tssyncAudienceBindingSuggestions does if (bound) continue when no row exists yet, and the "confirmed (observed)" transition only fires for a row already in status pending — but the security plugin auto-binds the app's isDefault set to everyoneat boot, before any list call, so stock always takes the continue. This contradicts the module's own docblock.
(b) An unknown ?status filter returns 200 EMPTY instead of 400 — exactly the "reads as there are no suggestions" failure the clause forbids. GET …/suggested-bindings?status=garbage → 200 empty (same for ?status=PENDING). Root cause: the live route is rest-server.tsregisterSecurityEndpoints, which forwards req.query.status through with no validation; the isSuggestionStatus 400 guard exists only on the parallel runtime dispatcher domain, whose own comment describes precisely this observed behaviour. The fix landed on the dispatcher path only.
Clauses 2–6 pass, including the one the item carried as a knownGap — the runner closed it by provisioning rather than recording blocked, deleting the stock auto-binding to raise a genuine PENDING row and installing a scratch package to raise a second.
🟡 PARTIAL — 2 items
capability-declaration-lifecycle
Declaration→sys_capability seeding is exact, including both documented fallbacks (humanize(name) label, generated description). Platform-capability hijack is refused: a package declaring manage_users gets a located WARN, skippedPlatform:1, and the platform row is unchanged — though note the refusal is a WARN and boot proceeds; the protection is that the row isn't overwritten, not that the declaration is rejected at authoring. Malformed declarations abort boot at Zod parse with per-path messages, before any sys_capability write — so #5961 (an unvalidated row reaching the authz namespace) does not hold.
Clause 1 is partial, and the item's knownGap is confirmed wider than stated. The three-way grant/deny/regrant resolves by name — but only demonstrable with a platform capability (manage_metadata), because no shipped resource enforces a package capability server-side. The three showcase actions carrying requiredPermissions have no REST execution route on this build (/api/v1/actions/<n>, /api/v1/action/<n>, /api/v1/objects/<o>/actions/<n>, /api/v1/data/<o>/actions/<n> all 404; rest-route-ledger.ts registers no action-invoke route), so their requiredPermissions gate is client-side only. The grant side was provisioned for real; there is simply no server surface on which to exercise the contrast.
readonly-package-locks-studio
Studio's read-only lock is real, not cosmetic: 207/207 checkboxes disabled, 92/92 bulk buttons disabled, no Save rendered, Publish disabled; a forced click left data-state unchanged. The direct write is refused and the artifact is byte-identical afterwards (sha256 match before/after the denied sequence).
Clause 1 is partial: the refusal is a ledgered code, but not one of the two the clause names and it does not key on package writability — PUT /api/v1/meta/object/showcase_task → 403 NOT_OVERRIDABLE ("'object' is not allowOrgOverride in the registry"), the same with ?package= pointing at either a read-only or a writable package. ITEM_LOCKED and WRITABLE_PACKAGE_REQUIRED are both in the error-code ledger and neither was ever emitted on this path. SysMetadataRepository.assertAllowed discriminates on the metadata type's overlay policy, not on a package-writability check.
⚠️Caveat worth a decision: with the documented operator hatch OS_METADATA_WRITABLE=permission, PUT /api/v1/meta/permission/showcase_contributor?package=com.example.showcase — a set belonging to the read-only package — succeeds (200, env-wide overlay), while Studio still renders that matrix fully disabled with a "Read-only" badge. The hatch is documented as unlocking the write, so this may be intended; but the Studio badge then asserts a lock the server is not applying.(The overlay was reverted afterwards.)
⛔ BLOCKED — 1 item
share-link-capability-tokens — fixture-blocked on stock showcase, unchanged from its ledgered blocked:{by,ref}.
Cross-cutting findings
verify --rls reports 0 HOLES but structurally cannot reach the by-id-write class. Its member probe holds no object grants, so every such probe is masked by the object-level gate (403) before record scope is ever tested. A second probe persona holding object read+edit but outside the record scope would catch it. Compounding this, a showcase_account auto-record 400 cascades into 5 downstream skips, so 8 of 23 objects are skipped on a stock run — and a skip is exactly where the privately-reported D11 defect hid. This is the single highest-value fix in this report: the tool's green is currently not evidence.
Console permission-matrix editor is read-only by default. On a stock boot it renders "Read-only (OS_METADATA_WRITABLE not enabled)" with every checkbox disabled, at both the metadata-admin route and inside Studio's Access pillar for a writable package — because the editor computes writable = !!resolved.allowOrgOverride && !readOnly and the server returns allowOrgOverride:false for type permission. Yet the server accepts the package-door write in that same default env (PUT …?package=<writable pkg> → 200 via allowRuntimeCreate). The type gate locks a surface the server would allow.
Checklist maintenance falling out of this run
owd-sharing-matrix clause 4 and rls-both-sides clause 5 are the same defect — dedupe so it isn't counted twice.
suggested-binding-loop's knownGaps text is wrong on two points: stock produces no row at all (not a "confirmed (observed)" row), so the dependent clauses have nothing to run against until the unbind sequence is performed.
record-share-grant-revoke clause 6 says the materialized share carries source_id = the rule name; the implementation writes the rule row id (the stable FK purgeRuleGrants relies on). Semantic intent holds, text is wrong.
sharing-rule-authoring-ui names showcase_project as "a private-OWD object", but it reports sharingModel:'public_read_write', so a rule on it widens nothing observable. The private-OWD showcase objects are showcase_contact, showcase_inquiry, showcase_private_note — the runner substituted showcase_contact.
Runner-environment fact worth adding to the briefing:viewis in the overlay-allowed set (view, dashboard, report, translation, email_template), so PUT /api/v1/meta/view/<name> works on the stock read-only showcase package with no escape hatch.
One false alarm was raised and retracted by the runner before reporting: an apparent allowDelete:false bypass turned out to be documented modifyAllRecords ("Modify All Data") semantics per ADR-0066 D2, isolated with a 4-way A/B and not filed.
Full
access-securityarea run of thechecklist-testskill — all 19 items (18 runnable + 1 fixture-blocked) driven against a live showcase across 4 opus subagents, each on its own port + file DB. Text-only per RUNNER.md.Result: 8 PASS · 2 PARTIAL · 8 FAIL · 1 BLOCKED.
Environment — framework
92f26f75(branchclaude/platform-test-checklist-ocwugl) · console bundle09987b680(packages/console/dist/.objectui-sha) · showcase app · isolated port + file DB per batch · personas provisioned as real runtime sign-ups, never fixtures.✅ PASS — 8 items
The enforcement core is genuinely strong. Highlights, all proven with effect-follow-up reads rather than status codes alone:
crud-permission-matrixdrove 124 cells (31 access-matrix rows × 4 verbs), each as a dedicated persona holding exactly that set: zero withheld cells answered 2xx, zero denied edits mutated, zero denied deletes removed. (The item still fails on one allowed cell — see below.)write-path-guards(7/7) — readonly fields are admit-and-strip withdroppedFields+ thex-objectstack-dropped-fieldsheader on both create and update;owner_idforging refused on create, update and disown, with an admin-context marker query proving 0 rows were planted; the guard gates on identity, not on the key's presence (own-id create is allowed);readonly_whenlocks fire per-field including the bulk door (安全:readonlyWhen 服务端剥离仅覆盖单条 update 路径,多行 updateMany 不强制(条件只读可被批量绕过) #3042); mixed-batch validation is per-row (Validation rules, requiredWhen and option visibleWhen are silently skipped on multi-row updates (options.multi) #3106 does not reproduce).anonymous-deny-surfaces(6/6) — all six surfaces 401 with the platform message, and the gate fires before object/record resolution (a nonexistent id still 401s, never 404); every body matched exactly one envelope family with no hybrid dialect (ANONYMOUS_DENY_BODY自称是「每个 seam 都返回的唯一 401 body 形状」,但 dispatcher 侧五个 seam 返回的是另一种 wrapper #5632); declared-public forms still serve anonymously, so the deny doesn't blanket the server. AnonymousDELETE /automation/<flow>→ 401 with the flow still registered ([17.0-rc2验收] 安全:REST /actions 与 /automation 派发路由缺少匿名拒绝门 —— 未认证调用者可触发 system 提权的 RLS/FLS 绕过写入 #5519 does not reproduce).scope-depth-asymmetry(6/6) — read-org/write-own asymmetry proven on the same record id across four personas: manager reads a foreign private-OWD row 200 but PATCH → 403 with the row verifiably unchanged, while an org-writeScope persona PATCHes that same row 200 and it persists — so the denial is the guard, not a dead route.sharing-rules-widen(5/5) —/security/explainshows the decision layer-by-layer (owd_baseline narrows, sharing widens naming the rule, and for an outsider "1 share(s) attached; none grants the caller access"). Audit sibling declared-metadata↔record two-store types (sys_position, sys_sharing_rule, sys_capability) per ADR-0094 addendum #2909 regression checked: three shares survived a real restart byte-identical.fls-mask-and-strip(5/5) — a mixed forged write is refused atomically (the allowed field doesn't land either); the filter oracle is refused (filtering by a hidden field → 403, while the identical filter as admin returns rows).budget+spentleaves the formula fieldbudget_remainingunmasked — a formula over masked inputs is not auto-masked.record-access-explain(5/5) — explain agrees with reality in both directions across three personas on one record.public-form-intake(7/7 + 3 negatives) — forgedowner_id/organization_id/created_byall land null; a__proto__body smugglingowner_id/statusinherited nothing; no whitelisted field opens an anonymous lookup, so the form is not a directory oracle; 11 anonymous probes confirm the intake door opens nothing else.🔴 FAIL — 8 items
1.
crud-permission-matrix— an allowed create returns 500 while the row is written123 of 124 cells are correct. The one break is a permitted operation, not a denied one.
Reproduction rule — boot showcase, sign up a plain member (no explicit grants), admin creates a project, then as the member:
POST /api/v1/data/showcase_task {title, project:<id>, status:'todo'}Expected 201 (the access matrix says
create:true;/security/explainconfirmsallowed=true). Actual500 INTERNAL_ERROR — yet an admin read shows the task did persist, withowner_id= the member. Reproduced 4×.Server log:
WARN Roll-up summary recompute failed {childObject:showcase_task, parentObject:showcase_project, field:task_count, error: Access denied: operation update on showcase_project …}×2, thenERROR SummaryRecomputeError '…the triggering records WERE written (summary values may be stale)'.Root cause (located).
objectql/src/engine.ts:6139—recomputeSummariesissues the parent roll-up write under the caller's execution context, so it passes through security as the caller.showcase_projectdeclares twoField.summaryroll-ups overshowcase_task, andmember_defaultgrants read but not edit onshowcase_project, so the internal recompute is refused;withTransientRetrythen retries a non-transientPERMISSION_DENIED, andengine.ts:8454throwsSummaryRecomputeError, which REST maps to 500.Contrast isolating it: the identical POST succeeds 201 for
showcase_contributor(holdsallowEditonshowcase_project) and for admin.Suggested fix: run the roll-up recompute in a system context — it is an engine-internal derived write, not a caller write. At minimum, stop retrying a permission denial and stop surfacing a committed write as a 500. A tracker search suggests this is unreported.
2.
owd-save-gate— the ADR-0090 D11 authoring gate never runs on any host-config deploymentBoth layers fail: nothing on this deploy refuses a D11 violation. Studio turns the external-OWD description amber and literally says "publishing will be rejected (ADR-0090 D11)" — then leaves Save draft and Publish enabled, and both succeed.
Reproduction rule —
PUT /api/v1/meta/object/qa_probe {…, sharingModel:'private', externalSharingModel:'public_read'}. Expected 403owd_external_wider. Actual 200, andGETreturns the violating pair persisted — on the draft path, the active path, and with?package=. Reproduced 3× on one boot, 3× on a second boot with a fresh DB, plus once through the browser.Root cause (located).
metadata-protocol/src/protocol.ts:9667wraps the #3050 authoring-gate call inif (this.environmentId !== undefined). The showcase config is a host config, soisHostConfig→shouldBootWithLibrary(config) === false, andserve.tstakes the lightweight assembler which constructsnew ObjectQLPlugin()with no options →environmentIdstays undefined → the guard is false andrunAuthoringGateis never called. So R2 (owd_external_wider) and R1 (owd_widening_forbidden) execute on no self-hosted/host-config deployment.This is the exact proxy-signal hazard #6710 already diagnosed and retired for the sibling #4463 gate.
protocol.ts's own comment names "the CLI's lightweight host-config assembler … ALSO leaves environmentId undefined, and it serves an end-user PUT /api/v1/meta/"* — and then keeps the #3050 gate on the retired signal anyway. Suggested fix: key it on the declaredauthoringChannel, as #6710 did.The gate logic is not the bug —
object-posture-gate.test.tsis 18/18 green. A grep forowd_external_widerfinds only the gate source and its unit test: there is no integration test exercising R2 through the real save path.Security impact is bounded today: external-principal enforcement is #2696-planned, so a wider external baseline does not itself disclose anything yet. That is why this one is filed publicly rather than held back — it is an authoring-validation gap, not a live disclosure.
3.
audit-log-browser— four of the ten audit actions have no writer anywhereReproduction rule — on a fresh boot: (1) sign up + sign in a member, then
GET /api/v1/data/sys_audit_log?$filter={"action":"login"}as admin → total 0; the only trace is an unattributedupdate sys_userrow (user_idnull) diffinglast_login_at. (2)PUT /api/settings/branding {"workspace_name":"X"}→ 200, then filter{"action":"config_change"}→ total 0; the event went tosys_setting_auditwith actionsetinstead. Both reproduced twice.Action census over all 57 rows:
{'create':48,'update':7,'delete':2}— only CRUD actions ever materialize. The delete half is correct (fullold_value, actor, tenant).Root cause (located).
plugin-audit/src/audit-writers.tssubscribes only to the ObjectQL wildcardbefore*/after*CRUD lifecycle events, so it can emitcreate/update/delete/restoreand nothing else. A repo-wide search finds no writer forlogin,logout,permission_change,config_change,exportorimport— though all are declared in the action enum ofsys-audit-log.object.ts. Settings writes are audited bysettings-service-plugin.ts:315intosys_setting_audit, neversys_audit_log, whilesettings-service.types.ts:16documents the service as "Emit sys_audit_log rows for every successful write."Net effect: four enum values, two shipped list views (
auth_events,config_changes) and two dashboard widgets (system_overview.dashboard.tsL89 + L109) are permanently empty. The console makes it worse by listinglogin/config_changein its ownACTION_OPTIONS— the client offers filters for rows the server never writes.The tamper-proofing half is solid: POST/PATCH/DELETE to
sys_audit_logall 405OBJECT_API_METHOD_NOT_ALLOWED, list total unchanged.4.
permission-matrix-edit-loop— the field-level half of the permission matrix is dead for every objectThe object-verb loop is fully working (checkbox →
PUT→ 200 → the persona's liveDELETEflips 403→200, and back again on reverse; the editor re-reads the saved state after reload).Reproduction rule — open
/_console/apps/<app>/metadata/permission/<set>and expand any object row. Expected the field sub-table listing that object's fields with a readable/editable checkbox pair each. Actual"No fields registered for this object." and zero field checkboxes — for every object. Reproduced twice on fresh loads for two objects. Not a data problem: the network showsGET /api/v1/meta/object/showcase_project→ 200 carrying 21 fields.Root cause (located). objectui
PermissionMatrixEditor.tsxensureFields()doesconst obj = await client.get('object', objectName); const raw = obj?.fields;— butMetadataClient.get()(metadata-client.ts:495-512) returns the raw server envelope with no unwrapping, and the framework answers{type, name, item:{…fields}}. Soobj.fieldsis alwaysundefinedand every object reports zero fields. The client's own docblock atmetadata-client.ts:522asserts the opposite ("the legacyget()returns the unwrapped body") — that is where the two sides disagree.Not a stale bundle — the same code is on objectui
origin/main(6d01319dd). Collateral: the RLS CEL editor's field lint/autocomplete (loadObjectFields) is empty for the same reason.5.
record-share-grant-revoke— package-seeded sharing rules are unaddressable by nameClauses 0–5 all pass, including that read access confers no re-share authority (403, ADR-0111 D1) and that a mis-scoped revoke 404s with the share surviving.
Reproduction rule — stock boot, admin bearer: (1)
GET /api/v1/sharing/rules→ 200{"data":[]}thoughsys_sharing_rulelists 4 active seeded rules. (2)GET /api/v1/sharing/rules/share_red_projects_with_execs→ 404RULE_NOT_FOUND. (3)POST …/<name>/evaluate→ 404. (4) Control:POST /api/v1/sharing/rules/srule_<row id>/evaluate→ 200{matchedRecords:1, expandedUsers:2, grantsCreated:1}.Root cause.
plugin-sharing/src/sharing-rule-service.tsapplies a strict-equality org filter that package-seeded rows cannot satisfy:listRulesdoesif (orgId) where.organization_id = orgId, andgetRule's name fallback isorgId ? {name, organization_id: orgId} : {name}— while package-seeded rows carryorganization_id = nulland an authenticated admin's context carriesorganizationId='org_…'. Only the unfiltered by-id branch survives.Confirming experiment: a rule created via the API (where
defineRulestampsorganization_idfrom the same context) is findable by name, listed, and evaluable by name.Blast radius: every package-declared sharing rule is invisible and unmanageable through the admin API. Enforcement is unaffected, because the boot reconcile uses
SYSTEM_CTXwhich carries no org — which is exactly why this stayed invisible. A sibling batch independently bounded the bug to package-seeded rows and confirmed the console's Sharing Rules page is not affected (it reads the data API, not/api/v1/sharing/rules, so it lists all four honestly).6.
suggested-binding-loop— two defects(a) The
isDefaultsuggestion is never surfaced on stock.GET /api/v1/security/suggested-bindings→ 200{suggestions:[], synced:{created:0}}and the table is empty, though the permission set and itseveryonebinding both exist. Discriminating step: delete that binding row and re-list →synced {created:1}and a PENDING row appears — so the declaration is collected. Root cause:suggested-audience-bindings.tssyncAudienceBindingSuggestionsdoesif (bound) continuewhen no row exists yet, and the "confirmed (observed)" transition only fires for a row already in statuspending— but the security plugin auto-binds the app'sisDefaultset toeveryoneat boot, before any list call, so stock always takes thecontinue. This contradicts the module's own docblock.(b) An unknown
?statusfilter returns 200 EMPTY instead of 400 — exactly the "reads as there are no suggestions" failure the clause forbids.GET …/suggested-bindings?status=garbage→ 200 empty (same for?status=PENDING). Root cause: the live route isrest-server.tsregisterSecurityEndpoints, which forwardsreq.query.statusthrough with no validation; theisSuggestionStatus400 guard exists only on the parallel runtime dispatcher domain, whose own comment describes precisely this observed behaviour. The fix landed on the dispatcher path only.Clauses 2–6 pass, including the one the item carried as a
knownGap— the runner closed it by provisioning rather than recording blocked, deleting the stock auto-binding to raise a genuine PENDING row and installing a scratch package to raise a second.🟡 PARTIAL — 2 items
capability-declaration-lifecycleDeclaration→
sys_capabilityseeding is exact, including both documented fallbacks (humanize(name)label, generated description). Platform-capability hijack is refused: a package declaringmanage_usersgets a located WARN,skippedPlatform:1, and the platform row is unchanged — though note the refusal is a WARN and boot proceeds; the protection is that the row isn't overwritten, not that the declaration is rejected at authoring. Malformed declarations abort boot at Zod parse with per-path messages, before anysys_capabilitywrite — so #5961 (an unvalidated row reaching the authz namespace) does not hold.Clause 1 is
partial, and the item'sknownGapis confirmed wider than stated. The three-way grant/deny/regrant resolves by name — but only demonstrable with a platform capability (manage_metadata), because no shipped resource enforces a package capability server-side. The three showcase actions carryingrequiredPermissionshave no REST execution route on this build (/api/v1/actions/<n>,/api/v1/action/<n>,/api/v1/objects/<o>/actions/<n>,/api/v1/data/<o>/actions/<n>all 404;rest-route-ledger.tsregisters no action-invoke route), so theirrequiredPermissionsgate is client-side only. The grant side was provisioned for real; there is simply no server surface on which to exercise the contrast.readonly-package-locks-studioStudio's read-only lock is real, not cosmetic: 207/207 checkboxes disabled, 92/92 bulk buttons disabled, no Save rendered, Publish disabled; a forced click left
data-stateunchanged. The direct write is refused and the artifact is byte-identical afterwards (sha256 match before/after the denied sequence).Clause 1 is
partial: the refusal is a ledgered code, but not one of the two the clause names and it does not key on package writability —PUT /api/v1/meta/object/showcase_task→ 403NOT_OVERRIDABLE("'object' is not allowOrgOverride in the registry"), the same with?package=pointing at either a read-only or a writable package.ITEM_LOCKEDandWRITABLE_PACKAGE_REQUIREDare both in the error-code ledger and neither was ever emitted on this path.SysMetadataRepository.assertAlloweddiscriminates on the metadata type's overlay policy, not on a package-writability check.OS_METADATA_WRITABLE=permission,PUT /api/v1/meta/permission/showcase_contributor?package=com.example.showcase— a set belonging to the read-only package — succeeds (200, env-wide overlay), while Studio still renders that matrix fully disabled with a "Read-only" badge. The hatch is documented as unlocking the write, so this may be intended; but the Studio badge then asserts a lock the server is not applying.(The overlay was reverted afterwards.)⛔ BLOCKED — 1 item
share-link-capability-tokens— fixture-blocked on stock showcase, unchanged from its ledgeredblocked:{by,ref}.Cross-cutting findings
verify --rlsreports 0 HOLES but structurally cannot reach the by-id-write class. Its member probe holds no object grants, so every such probe is masked by the object-level gate (403) before record scope is ever tested. A second probe persona holding object read+edit but outside the record scope would catch it. Compounding this, ashowcase_accountauto-record 400 cascades into 5 downstream skips, so 8 of 23 objects are skipped on a stock run — and a skip is exactly where the privately-reported D11 defect hid. This is the single highest-value fix in this report: the tool's green is currently not evidence.authz-conformance.matrix.tsmarks bothrls-by-id-write(fix(security)[P0]: enforce RLS on by-id writes — close member-edits-others'-records hole (#1985) #1994) andcontrolled-by-parentasstate:'enforced'; neither holds as shipped. The showcase's ownpermission-sets.tscomment makes the same false claim./meta/types,/meta/:type/:name/published,/meta/objects/:name/state/:field). Here a fix (the?status400 guard) landed on the dispatcher while the Hono/REST route the server actually serves never got it. This strengthens the case for the route-ledger↔live-mount parity gate (Three ledgered /meta routes are never mounted and die in the/meta/:typecatch-all — the route audit can't see this class because it treats the ledger as ground truth for what's mounted #7526) — and argues it should cover behaviour divergence, not just mount presence.writable = !!resolved.allowOrgOverride && !readOnlyand the server returnsallowOrgOverride:falsefor typepermission. Yet the server accepts the package-door write in that same default env (PUT …?package=<writable pkg>→ 200 viaallowRuntimeCreate). The type gate locks a surface the server would allow.Checklist maintenance falling out of this run
owd-sharing-matrixclause 4 andrls-both-sidesclause 5 are the same defect — dedupe so it isn't counted twice.suggested-binding-loop'sknownGapstext is wrong on two points: stock produces no row at all (not a "confirmed (observed)" row), so the dependent clauses have nothing to run against until the unbind sequence is performed.record-share-grant-revokeclause 6 says the materialized share carriessource_id= the rule name; the implementation writes the rule row id (the stable FKpurgeRuleGrantsrelies on). Semantic intent holds, text is wrong.sharing-rule-authoring-uinamesshowcase_projectas "a private-OWD object", but it reportssharingModel:'public_read_write', so a rule on it widens nothing observable. The private-OWD showcase objects areshowcase_contact,showcase_inquiry,showcase_private_note— the runner substitutedshowcase_contact.viewis in the overlay-allowed set (view, dashboard, report, translation, email_template), soPUT /api/v1/meta/view/<name>works on the stock read-only showcase package with no escape hatch.One false alarm was raised and retracted by the runner before reporting: an apparent
allowDelete:falsebypass turned out to be documentedmodifyAllRecords("Modify All Data") semantics per ADR-0066 D2, isolated with a 4-way A/B and not filed.