Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/dogfood-verification/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -110,7 +110,7 @@ dev 工作树、dev-server 端口、preview 浏览器全是**共享的**:并行
- [ ] 确认页面渲染完成*之后*(先截图,再查 selector),DOM dump 没问题。
- [ ] **门的两侧都要测**:`requiresService`/`requiresObject`/权限门要在依赖存在*与*
缺席两种状态下各验一次。
- [ ] 服务端是权威可见性门(ADR-0057 D10)—— 客户端过滤只是「礼貌」。元数据开关不改
- [ ] 服务端是权威可见性门(ADR-0124 D1)—— 客户端过滤只是「礼貌」。元数据开关不改
UI 时,先查强制在服务端(框架,可在本仓修)还是客户端(objectui console,另一
个仓)。
- [ ] 用 `preview_screenshot`(API 改动用 `preview_network`)向用户证明;loading
Expand Down
2 changes: 1 addition & 1 deletion docs/audits/2026-07-studio-package-create-ux-dogfood.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@ object with fields, enter a record, create an app with navigation, publish, and
use the result as an end user — driving a real Chromium against `/_console`
(vendored console build `7782698`, matching the `.objectui-sha` pin, so findings
are not stale-bundle artifacts). Both sides of the read-only gate were exercised
per ADR-0057 D10.
per ADR-0124 D5.

Run: `objectstack dev --ui --seed-admin` on the showcase example, fresh SQLite DB,
`admin@objectos.ai` seeded admin, headless Chromium via CDP with screenshots at
Expand Down
6 changes: 3 additions & 3 deletions docs/qa/platform-checklist/RUNNER.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -75,9 +75,9 @@ test-run output the clause's `evidence` field names.
4. **Both sides of every gate.** For any permission/visibility/feature gate, verify
presence for the entitled persona AND absence (or server-side rejection) for the
unentitled one. UI absence alone is a client courtesy; the server is the authority.
(That rule is cited across the framework as `ADR-0057 D10`; treat it as an
attribution — D10 itself decides Setup-nav capability surfacing, and recording the
general rule is tracked in #9628. The rule is not in doubt, only its anchor.)
(That rule is `ADR-0124 D1`, and this rule is its verification half — `ADR-0124 D5`
states it directly: a test that asserts only that the interface hides something has
tested the courtesy layer and left the enforcement point unobserved.)
Where feasible, prove denial with a direct forged request.
5. **Severe findings are hypotheses.** "The whole surface is unreachable" gets
disproven-or-confirmed via screenshot + the server's own metadata before it is
Expand Down
12 changes: 6 additions & 6 deletions docs/qa/platform-checklist/areas/access-security.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,7 +51,7 @@
"evidence": "PATCH response + admin re-read"
},
{
"clause": "admin (platform posture) reads the full set — the entitled side of the same gate (both sides, RUNNER rule 4 / ADR-0057 D10)",
"clause": "admin (platform posture) reads the full set — the entitled side of the same gate (both sides, RUNNER rule 4 / ADR-0124 D5)",
"oracle": "api",
"verify": "admin GET list contains every id created in this run",
"evidence": "admin listing"
Expand DownExpand Up@@ -83,7 +83,7 @@
},
"source": [
"packages/verify/src/rls.ts",
"ADR-0057 D10",
"ADR-0124 D1",
"packages/qa/dogfood/test/showcase-private-owd.dogfood.test.ts",
"authz-conformance.matrix.ts rows rls-read / rls-by-id-write",
"#7637 (run record) — the by-id spot-check on a skipped object is where the private D11 defect surfaced",
Expand DownExpand Up@@ -335,7 +335,7 @@
"evidence": "screenshot + disabled-state DOM read"
},
{
"clause": "the SERVER refuses the same write: direct PUT /api/v1/meta/object/<name> on the read-only package answers 4xx with a ledgered metadata-protocol code (WRITABLE_PACKAGE_REQUIRED, or ITEM_LOCKED for _lock'd items) — UI absence never suffices (ADR-0057 D10)",
"clause": "the SERVER refuses the same write: direct PUT /api/v1/meta/object/<name> on the read-only package answers 4xx with a ledgered metadata-protocol code (WRITABLE_PACKAGE_REQUIRED, or ITEM_LOCKED for _lock'd items) — UI absence never suffices (ADR-0124 D1)",
"oracle": "api",
"verify": "PUT response status >=400 and error.code ∈ {WRITABLE_PACKAGE_REQUIRED, ITEM_LOCKED} (packages/spec/src/api/error-code-ledger.zod.ts, @objectstack/metadata-protocol entry)",
"evidence": "PUT trace"
Expand DownExpand Up@@ -421,7 +421,7 @@
"evidence": "the verdict matrix + spot re-reads"
},
{
"clause": "every withheld cell is DENIED SERVER-SIDE with the ledgered code: verbs marked false answer 403 with error.code PERMISSION_DENIED (rest-server maps explicit security denials to 403 PERMISSION_DENIED) — UI absence never counts (ADR-0057 D10)",
"clause": "every withheld cell is DENIED SERVER-SIDE with the ledgered code: verbs marked false answer 403 with error.code PERMISSION_DENIED (rest-server maps explicit security denials to 403 PERMISSION_DENIED) — UI absence never counts (ADR-0124 D1)",
"oracle": "api",
"verify": "per-cell status 403 and body code PERMISSION_DENIED; capture any cell answering a different code for triage",
"evidence": "the verdict matrix"
Expand DownExpand Up@@ -1293,7 +1293,7 @@
}
],
"negative": [
"a Studio panel that greys the save while a direct meta PUT widens is a FAIL — the server gate is the authority (ADR-0057 D10 both-sides)",
"a Studio panel that greys the save while a direct meta PUT widens is a FAIL — the server gate is the authority (ADR-0124 D1 both-sides)",
"a stock-deploy packaged-object widening PUT that answers 2xx (or leaves the object widened) is a FAIL",
"do NOT tick external-principal read/write enforcement: that evaluation is liveness `planned` (#2696) — this item is the save/authoring gate only"
],
Expand All@@ -1310,7 +1310,7 @@
"packages/lint/src/validate-security-posture.ts (OWD_WIDTH + SECURITY_EXTERNAL_WIDER lint parity)",
"objectui packages/app-shell/src/views/studio-design/owd-sharing.ts (isExternalWider) + PackageOwdOverviewPanel.tsx + ObjectSettingsPanel.tsx",
"examples/app-showcase/src/data/objects/announcement.object.ts + account.object.ts (externalSharingModel declarations)",
"ADR-0090 D11, ADR-0086 D1, ADR-0057 D10, #3050, objectui#2508"
"ADR-0090 D11, ADR-0086 D1, ADR-0124 D1, #3050, objectui#2508"
],
"history": [
{
Expand Down
4 changes: 2 additions & 2 deletions docs/qa/platform-checklist/areas/approvals.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -280,7 +280,7 @@
"evidence": "request read"
},
{
"clause": "the gate is server-side: a forged direct POST of the decision route as the submitter is rejected (ADR-0057 D10 — UI absence alone is a client courtesy)",
"clause": "the gate is server-side: a forged direct POST of the decision route as the submitter is rejected (ADR-0124 D1 — UI absence alone is a client courtesy)",
"oracle": "api",
"verify": "the forged approve answers FORBIDDEN (403-mapped); test BOTH sides — the entitled approver's decision on the same request succeeds",
"evidence": "the rejected call + the entitled approver's accepted call"
Expand All@@ -298,7 +298,7 @@
"traps": ["hydration-race", "wrong-persona"],
"source": [
"#3358 §1",
"ADR-0057 D10 (server is the authoritative visibility gate)",
"ADR-0124 D1 (server is the authoritative visibility gate)",
"examples/app-showcase/src/security/seed-approval-demo.ts (Mei Phone: 'a clean submitter — a requester who is never also one of her own approvers')"
],
"history": [
Expand Down
2 changes: 1 addition & 1 deletion docs/qa/platform-checklist/areas/attachments-storage.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -233,7 +233,7 @@
}
],
"negative": [
"a gated file downloadable anonymously (silent success) is a FAIL — UI absence of a download button is a client courtesy; the route is the authority (ADR-0057 D10)",
"a gated file downloadable anonymously (silent success) is a FAIL — UI absence of a download button is a client courtesy; the route is the authority (ADR-0124 D1)",
"a deny that surfaces in the panel as 'Download failed (403)' instead of the mapped copy means the error-envelope dialect broke (#3689 note in the panel) — file it against objectui, not storage"
],
"traps": ["wrong-persona", "dispatcher-vs-hono-route", "stale-console-bundle"],
Expand Down
6 changes: 3 additions & 3 deletions docs/qa/platform-checklist/areas/identity-auth.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -211,7 +211,7 @@
"evidence": "the screenshot set"
},
{
"clause": "a disabled method is refused SERVER-SIDE, not merely hidden — UI absence is a client courtesy; the server is the authority (ADR-0057 D10)",
"clause": "a disabled method is refused SERVER-SIDE, not merely hidden — UI absence is a client courtesy; the server is the authority (ADR-0124 D1)",
"oracle": "api",
"verify": "firing each disabled method's endpoint returns a non-2xx (the plugin's routes are absent or refuse)",
"evidence": "the forged-request responses"
Expand DownExpand Up@@ -347,7 +347,7 @@
],
"negative": [
"an admin-role invitation that returns success, or that leaves ANY row behind, is a FAIL of privilege-escalation severity — file immediately, P0-verify per RUNNER rule 7",
"UI-only enforcement (affordance hidden but the forged request succeeds) is a FAIL — the server is the authority (ADR-0057 D10)",
"UI-only enforcement (affordance hidden but the forged request succeeds) is a FAIL — the server is the authority (ADR-0124 D1)",
"my-delegable-scope OVER-reporting is a FAIL — a position, permission set or BU subtree the caller cannot actually delegate, or (were the shape ever to grow one) a role the caller cannot mint: a client that trusts the scope would offer what the endpoint then refuses. UNDER-reporting is NOT a FAIL and must not be filed as one: the endpoint answering an empty scope while the delegate can still mint a member invitation is the safe direction and the expected state on stock fixtures (run #7663 — DelegableScope has no invitation-role field at all)"
],
"automated": { "kind": "e2e", "ref": "packages/qa/dogfood/test/delegated-admin-invite.dogfood.test.ts" },
Expand DownExpand Up@@ -807,7 +807,7 @@
}
],
"negative": [
"an org management surface where the affordance is hidden but the forged endpoint succeeds for a non-admin is a FAIL — the server is the authority (ADR-0057 D10)",
"an org management surface where the affordance is hidden but the forged endpoint succeeds for a non-admin is a FAIL — the server is the authority (ADR-0124 D1)",
"remove-member that drops the roster row but leaves the ex-member's org-scoped access intact is a FAIL — removal must change authorization",
"a role written outside the {owner, admin, delegated_admin, member} vocabulary is a FAIL — including a stored 'guest': the closed list is the write-side guardrail that makes an ungoverned capability grant unrepresentable (ADR-0108), so a 2xx that persists 'guest' is a regression of the closure, not a vocabulary difference. A role change that does not flip any gate is equally a FAIL",
"the Organization nav landing on the raw sys_organization list because {current_org_id} did not resolve (when an active org exists) is a FAIL of the ADR-0081 wiring"
Expand Down
2 changes: 1 addition & 1 deletion docs/qa/platform-checklist/areas/platform-core.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -518,7 +518,7 @@
"evidence": "three screenshots"
},
{
"clause": "the app-level gate is server-side, not a client courtesy: a forged member GET /api/v1/meta/app?id=setup is denied/empty at the server, not merely hidden in the launcher (ADR-0057 D10 both-sides)",
"clause": "the app-level gate is server-side, not a client courtesy: a forged member GET /api/v1/meta/app?id=setup is denied/empty at the server, not merely hidden in the launcher (ADR-0124 D1 both-sides)",
"oracle": "api",
"verify": "the forged request's status/body proves server-side denial",
"evidence": "the forged response"
Expand Down
4 changes: 2 additions & 2 deletions docs/qa/platform-checklist/areas/records-forms.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -1120,7 +1120,7 @@
{
"clause": "related lists are READ-gated on BOTH ends: a persona WITHOUT read on the child object (showcase_contributor lacks showcase_contact read) sees NO Contacts section on the account detail (UI courtesy — deriveRelatedLists drops children the user cannot read, objectui#2359) AND a direct child query is refused server-side (403); a persona WITH child read (showcase_manager) sees the section AND the query 200s",
"oracle": "api",
"verify": "as showcase_contributor: screenshot confirms the Contacts tab is absent, and the forged GET /api/v1/data/showcase_contact?$filter=[[\"account\",\"=\",\"<northwind id>\"]] returns 403; as showcase_manager: the tab renders and the identical query returns 200 with rows — the server is the authority (ADR-0057 D10, RUNNER rule 4), the UI drop is courtesy",
"verify": "as showcase_contributor: screenshot confirms the Contacts tab is absent, and the forged GET /api/v1/data/showcase_contact?$filter=[[\"account\",\"=\",\"<northwind id>\"]] returns 403; as showcase_manager: the tab renders and the identical query returns 200 with rows — the server is the authority (ADR-0124 D1, RUNNER rule 4), the UI drop is courtesy",
"evidence": "both personas' detail screenshots + the 403 and the 200 child queries"
}
],
Expand DownExpand Up@@ -2489,7 +2489,7 @@
{
"clause": "the SERVER is the boundary: a forged PUT /api/v1/meta/view/<name> by the non-admin is refused (4xx) — UI absence alone is courtesy",
"oracle": "api",
"verify": "the direct non-admin PUT returns a 403-class refusal and no overlay view is created (RUNNER rule 4, ADR-0057 D10)",
"verify": "the direct non-admin PUT returns a 403-class refusal and no overlay view is created (RUNNER rule 4, ADR-0124 D5)",
"evidence": "the forged-request refusal"
}
],
Expand Down
4 changes: 2 additions & 2 deletions docs/qa/platform-checklist/areas/studio-authoring.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -73,14 +73,14 @@
}
],
"negative": [
"silent acceptance of an authoring write into a read-only package is a FAIL (ADR-0057 D10 — the server is the authoritative gate; the client lock is courtesy)",
"silent acceptance of an authoring write into a read-only package is a FAIL (ADR-0124 D1 — the server is the authoritative gate; the client lock is courtesy)",
"a 'published' app absent from the Home launcher, or an end-user list rendering raw picklist values instead of labels, is a FAIL"
],
"traps": ["stale-console-bundle", "automation-input", "hydration-race"],
"source": [
"docs/audits/2026-07-studio-package-create-ux-dogfood.md ('The loop closes' — the canonical walk; findings 1/3/4/6 carried as knownGaps)",
"ADR-0016 §9 (the MVP loop this proves)",
"ADR-0057 D10 (server-side gate authority)",
"ADR-0124 D1 (server-side gate authority)",
"access-security.readonly-package-locks-studio (client-side lock — cross-referenced, not duplicated)"
],
"history": [
Expand Down
8 changes: 4 additions & 4 deletions packages/lint/src/validate-expressions.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1146,8 +1146,8 @@ describe('validateStackExpressions (ADR-0032 build-time)', () => {
* `stripReadonlyWhenFields` deletes the value from the
* payload; client `fallback: false` ⇒ editable. The two ends
* fault in OPPOSITE directions and the "server enforces,
* client is courtesy" rule (cited as ADR-0057 D10; an
* attribution, #9628) gives it to the server. ⇒ the old sentence was BACKWARDS here.
* client is courtesy" rule (ADR-0124 D1) gives it to the
* server. ⇒ the old sentence was BACKWARDS here.
* requiredWhen server logs the unbound root and `continue`s (#4977 did not
* copy the carve-out); client `fallback: false`. Both ends
* fail open and neither is about visibility. ⇒ the old
Expand DownExpand Up@@ -1186,8 +1186,8 @@ describe('validateStackExpressions (ADR-0032 build-time)', () => {
});

it('`readonlyWhen` — names the client/server disagreement, not just the server verdict', () => {
// Server enforces, client is courtesy (cited as ADR-0057 D10, an
// attribution — #9628): the form renders the field editable
// Server enforces, client is courtesy (ADR-0124 D1): the form renders
// the field editable
// (`fallback: false`)
// while the server locks it. An author who only reads "LOCKED" cannot
// reconcile that with the editable input in front of them.
Expand Down
3 changes: 1 addition & 2 deletions packages/lint/src/validate-expressions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -563,8 +563,7 @@ function rulePredicates(rule: AnyRec, path: string): Array<{ label: string; raw:
* payload and lets the rest of the write through. Client:
* `resolveFieldRuleState` passes `fallback: false`, so the form renders the
* field editable. The standing "server enforces, client is courtesy" rule
* — cited in this repo as ADR-0057 D10, an attribution rather than a
* resolvable anchor (#9628) — resolves the disagreement: the author edits
* — ADR-0124 D1 — resolves the disagreement: the author edits
* the field, the save reports
* success, and the value silently never lands. The old sentence told this
* author the field would be VISIBLE TO EVERYONE — the opposite failure, and
Expand Down
3 changes: 1 addition & 2 deletions packages/objectql/src/engine-readonly-when-parent.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,8 +7,7 @@
// Paid, its lines are frozen". It was enforced only in the client grid: the
// server-side strip bound `record` and `previous` and nothing else, so every
// `parent.*` predicate faulted, took the fail-OPEN branch, and the write landed
// with a 200 while the UI still drew the cell locked. The rule this repo cites
// as ADR-0057 D10 (an attribution, not a resolvable anchor — #9628) puts
// with a 200 while the UI still drew the cell locked. ADR-0124 D1 puts
// enforcement on the SERVER and makes the client courtesy; this suite pins that
// direction end-to-end through the real engine + a real driver, not through the
// strip function in isolation (PD #10: a `case` label is not enforcement —
Expand Down
5 changes: 2 additions & 3 deletions packages/objectql/src/engine.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5391,9 +5391,8 @@ export class ObjectQL implements IObjectQLEngine {
* `readonlyWhen` reads, or `null` when this write cannot resolve one.
*
* `readonlyWhen: parent.status == 'paid'` is a documented **server**
* guarantee (the rule this repo cites as ADR-0057 D10 puts enforcement here;
* the client grid is courtesy — an attribution, not a resolvable anchor,
* #9628), but the strip is a pure function over the payload and the prior
* guarantee (ADR-0124 D1 puts enforcement here; the client grid is
* courtesy), but the strip is a pure function over the payload and the prior
* row — it has no driver and cannot fetch a header. So the engine resolves it
* and passes it in.
*
Expand Down
Loading
Loading