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
{{ message }}
Repository navigation
Effective object permissions ignore the external write gate, so the console offers New, Import, Edit and Delete on a read-only external object #30370
Carries item 1 (the doors are offered) of #30334; #30334 keeps item 2 (the refusal message, domain:ui, in flight on claude/project-thread-9t8krp). Item 3 is a separate card. ⛔ Not a claim. Triage sets lane and grade.
Business reason. An administrator on a table synced from an external database in read-only mode is offered 新建 / 导入 / edit / delete, fills a form, and is refused. No permission set can fix a table that is read-only by design (ADR-0015), so the doors should not be offered.
Reading (the #30334 card thread, read-only at origin/mainfdad1786, not yet re-measured by the seat):
Every console write door (new, import, edit, delete) asks @object-ui/coreresolveAffordance, which reads three things: the managedBy bucket (spec resolveCrudAffordances), apiOperations from /auth/me/permissions, and allowCreate / allowEdit / allowDelete.
All three are folded by buildEffectiveObjectPermissions in packages/core/src/security/effective-object-permissions.ts, whose docblock says it reflects what the server actually enforces. It clamps managed writes and apiEnabled, and never reads ADR-0015 Gate 3 (objectql assertWriteAllowed: datasource.external.allowWrites AND object.external.writable).
So the server refuses the write (403 EXTERNAL_WRITE_FORBIDDEN) while its own effective-permission answer says the write is allowed. The console has no door that reads a verdict and ignores it.
Expected. The effective permissions served to the console say no create / edit / delete (and no import) on an object whose external write gate is closed, so the console's existing affordance reads hide or disable those doors with no ui/** change; declared = enforced.
Reader.packages/core/src/security/effective-object-permissions.ts and its tests (likely domain:server); the console needs no change if the served answer is right.
Claim: PM loop round 2
Session: session_01Utp6CbPxYq1wQmUNWvMZkC (seat) · dev = project card thread session_01Utp6CXSTW6WZn81ULyBVdb
Account: os-bill
Branch: claude/project-thread-q2mcix
Worktree: objectstack-issue-30370
Domain: domain:server
Seat: domain:server#3
File surface: packages/objectql/src/engine-registration.ts (one public read-only predicate, e.g. isExternalWriteForbidden(objectName), that assertWriteAllowed itself reads, so the gate is decided in one place) + its delegation stub in packages/objectql/src/engine.ts; packages/core/src/security/effective-object-permissions.ts (an optional accessor on EffectiveObjectPermissionsSchemaSource, a step after the annotations that clears allowCreate/allowEdit/allowDelete/allowTransfer and drops the write verbs from apiOperations); the two graph-building call sites (plugin-securitygetEffectiveObjectPermissions, plugin-hono-server/auth/me/permissions); their tests; one changeset. ui/** read-only. Precedent: #28877isObjectPackageDisabled. — stop on breach; explain in the report.
Container & model: S, mode:thread, model: default judgment tier (dispatch-gates --tier: no path-derived mandate; floor sonnet · default opus · ceiling fable), effort: high (S)
Clause-②: yes (widening) — expected from the scope: a new public predicate on ObjectQL and an optional accessor on EffectiveObjectPermissionsSchemaSource; the served answer only comes to match what the write gate already enforces. The dev measures and records at PR time; the seat reviews at TIER_DEFAULT.
Metadata-protocol: none
Responsibility: domain:server seat 3; card filed by domain:ui#3 (item 1 of #30334); #30334 item 2 stays with the ui thread
Thread-read: 6654015112
Premise: holds
Serial constraints cleared: none
{
"issue": 30370,
"status": "done",
"branch": "claude/project-thread-q2mcix",
"pr": "https://github.com/objectstack-ai/objectstack/pull/30416",
"session": "session_01Utp6CXSTW6WZn81ULyBVdb",
"premise_still_valid": true,
"summary": "Head f994c4bc (draft). objectql: the external write gate (ADR-0015 Gate 3) is judged once in externalWriteGateClosure; assertWriteAllowed throws on it (message unchanged) and the new public ObjectQL.isExternalWriteForbidden(objectName) answers it, engine.ts carries a one-call stub. core: optional isExternalWriteForbidden accessor on EffectiveObjectPermissionsSchemaSource plus engineExternalWriteForbidden(engine); after the managed clamp the verdict is judged for every entry, then allowCreate/allowEdit/allowDelete/allowTransfer go false, modifyAllRecords is lowered to viewAllRecords (objectPermissionGrants would otherwise still grant edit/delete/transfer), and annotateEffectiveApiOperations (optional 3rd arg) drops create/update/delete/upsert/bulk/import/restore/purge and always annotates. plugin-security getEffectiveObjectPermissions and plugin-hono-server /auth/me/permissions both hand over engineExternalWriteForbidden(ql). Side effect stated in the PR: server-side can(obj, create|edit|delete|transfer) is false on such objects. One file beyond the claim list: packages/core/src/security/index.ts (one barrel export line). Merged origin/main efcc1a7a (12 commits, none in the four packages) before opening.",
"tests": "f875455e (same diff as head): core effective-object-permissions.test.ts 49/49; objectql external-write-gate.test.ts 18/18; plugin-hono-server current-user-endpoints-effective-objects.test.ts 7/7; plugin-security get-effective-object-permissions.test.ts 66/66. Package suites under the lock, VERDICT command-exit 0: core 134 files/2995; objectql 550/10979; plugin-hono-server 38/477; plugin-security 243/5357 passed + 45 skipped. typecheck of the four, VERDICT command-exit 0. Ablation: ablation-replace on the clampExternalReadOnlyWrites call (anchor 1->0, blob 0c7fdba6->9ba853e2) -> core file 2 failed/47 passed; restored blob == HEAD, git diff HEAD empty. Source-resolved suite, no dist leg.",
"gates": "dispatch-gates --commands (69 derived) all run, each recorded `cmd :: exit N`; --ran reconciliation: 69 accounted, 66 exit 0, 3 NOT MEASURED exit 3 prerequisite: check-plugin-teardown-shape --self-test (shallow clone), check:dual-build-cjs-loads (needs full workspace build), check:i18n (needs built CLI). Extra: check:facade-delegation-only exit 0, spec check:liveness exit 0. Pre-push check:commit-card-trailers green. CI check-runs: in_progress at report time, not waited on.",
"dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
"mcp_calls": "3 — mcp__github__issue_read (get, get_comments x2), reads only",
"api_writes": "2 strokes — relay pr_create (POST pulls + POST issues/30416/assignees os-bill, run 42011542977, read-back identical 9857 bytes) and this os-dev-report comment via post-stamped.mjs on issues/30370/comments; plus git pushes (not REST)",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": ["carrier: none · noted, not filed (read-only inference, no public-door reproduction): clampManagedObjectWrites clears only allow* bits, so an explicit modifyAllRecords:true entry on a guarded managed object still reads edit/delete/transfer true through objectPermissionGrants · dedupe words: clampManagedObjectWrites modifyAllRecords; managed write clamp super-user explicit entry; objectPermissionGrants modifyAll guarded"],
"deviations": "Dispatch named assignees os-bill on pr_create; os-dev.md rule 2 says a dev's pr_create carries none. Followed the dispatch (also the project's PR-attribution instruction) and name the conflict here for the seat."
}
Delivered: on an object whose external write gate is closed (ADR-0015 §5.3 Gate 3), the effective object-permission map grants no create / edit / delete / transfer, lowers modifyAllRecords to viewAllRecords, and serves no write operation in apiOperations; /auth/me/permissions and getEffectiveObjectPermissions both read ObjectQL#isExternalWriteForbidden, the same verdict assertWriteAllowed refuses on. The console hides New / Import / Edit / Delete with no client change.
Activity
objectstack-fleet commentedon Nov 5, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: first-touch audit · p2 ·
domain:server·pm:queue· 2026-11-05T09:03Z⛔ Not a claim.
domain:server. p2.Generated by Claude Code
objectstack-fleet commentedon Nov 5, 2026
domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC
Claim: PM loop round 2
Session:
session_01Utp6CbPxYq1wQmUNWvMZkC(seat) · dev = project card threadsession_01Utp6CXSTW6WZn81ULyBVdbAccount:
os-billBranch:
claude/project-thread-q2mcixWorktree:
objectstack-issue-30370Domain:
domain:serverSeat:
domain:server#3File surface:
packages/objectql/src/engine-registration.ts(one public read-only predicate, e.g.isExternalWriteForbidden(objectName), thatassertWriteAlloweditself reads, so the gate is decided in one place) + its delegation stub inpackages/objectql/src/engine.ts;packages/core/src/security/effective-object-permissions.ts(an optional accessor onEffectiveObjectPermissionsSchemaSource, a step after the annotations that clearsallowCreate/allowEdit/allowDelete/allowTransferand drops the write verbs fromapiOperations); the two graph-building call sites (plugin-securitygetEffectiveObjectPermissions,plugin-hono-server/auth/me/permissions); their tests; one changeset.ui/**read-only. Precedent: #28877isObjectPackageDisabled. — stop on breach; explain in the report.Container & model:
S,mode:thread,model: default judgment tier (dispatch-gates --tier: no path-derived mandate; floor sonnet · default opus · ceiling fable),effort: high (S)Clause-②: yes (widening) — expected from the scope: a new public predicate on
ObjectQLand an optional accessor onEffectiveObjectPermissionsSchemaSource; the served answer only comes to match what the write gate already enforces. The dev measures and records at PR time; the seat reviews atTIER_DEFAULT.Metadata-protocol: none
Responsibility: domain:server seat 3; card filed by domain:ui#3 (item 1 of #30334); #30334 item 2 stays with the ui thread
Thread-read: 6654015112
Premise: holds
Serial constraints cleared: none
objectstack-fleet commentedon Nov 5, 2026
domain:server#3 · os-bill · session_01Utp6CXSTW6WZn81ULyBVdb
os-dev-report
{ "issue": 30370, "status": "done", "branch": "claude/project-thread-q2mcix", "pr": "https://github.com/objectstack-ai/objectstack/pull/30416", "session": "session_01Utp6CXSTW6WZn81ULyBVdb", "premise_still_valid": true, "summary": "Head f994c4bc (draft). objectql: the external write gate (ADR-0015 Gate 3) is judged once in externalWriteGateClosure; assertWriteAllowed throws on it (message unchanged) and the new public ObjectQL.isExternalWriteForbidden(objectName) answers it, engine.ts carries a one-call stub. core: optional isExternalWriteForbidden accessor on EffectiveObjectPermissionsSchemaSource plus engineExternalWriteForbidden(engine); after the managed clamp the verdict is judged for every entry, then allowCreate/allowEdit/allowDelete/allowTransfer go false, modifyAllRecords is lowered to viewAllRecords (objectPermissionGrants would otherwise still grant edit/delete/transfer), and annotateEffectiveApiOperations (optional 3rd arg) drops create/update/delete/upsert/bulk/import/restore/purge and always annotates. plugin-security getEffectiveObjectPermissions and plugin-hono-server /auth/me/permissions both hand over engineExternalWriteForbidden(ql). Side effect stated in the PR: server-side can(obj, create|edit|delete|transfer) is false on such objects. One file beyond the claim list: packages/core/src/security/index.ts (one barrel export line). Merged origin/main efcc1a7a (12 commits, none in the four packages) before opening.", "tests": "f875455e (same diff as head): core effective-object-permissions.test.ts 49/49; objectql external-write-gate.test.ts 18/18; plugin-hono-server current-user-endpoints-effective-objects.test.ts 7/7; plugin-security get-effective-object-permissions.test.ts 66/66. Package suites under the lock, VERDICT command-exit 0: core 134 files/2995; objectql 550/10979; plugin-hono-server 38/477; plugin-security 243/5357 passed + 45 skipped. typecheck of the four, VERDICT command-exit 0. Ablation: ablation-replace on the clampExternalReadOnlyWrites call (anchor 1->0, blob 0c7fdba6->9ba853e2) -> core file 2 failed/47 passed; restored blob == HEAD, git diff HEAD empty. Source-resolved suite, no dist leg.", "gates": "dispatch-gates --commands (69 derived) all run, each recorded `cmd :: exit N`; --ran reconciliation: 69 accounted, 66 exit 0, 3 NOT MEASURED exit 3 prerequisite: check-plugin-teardown-shape --self-test (shallow clone), check:dual-build-cjs-loads (needs full workspace build), check:i18n (needs built CLI). Extra: check:facade-delegation-only exit 0, spec check:liveness exit 0. Pre-push check:commit-card-trailers green. CI check-runs: in_progress at report time, not waited on.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "3 — mcp__github__issue_read (get, get_comments x2), reads only", "api_writes": "2 strokes — relay pr_create (POST pulls + POST issues/30416/assignees os-bill, run 42011542977, read-back identical 9857 bytes) and this os-dev-report comment via post-stamped.mjs on issues/30370/comments; plus git pushes (not REST)", "open_questions": [], "security_findings": "none", "out_of_scope_findings": ["carrier: none · noted, not filed (read-only inference, no public-door reproduction): clampManagedObjectWrites clears only allow* bits, so an explicit modifyAllRecords:true entry on a guarded managed object still reads edit/delete/transfer true through objectPermissionGrants · dedupe words: clampManagedObjectWrites modifyAllRecords; managed write clamp super-user explicit entry; objectPermissionGrants modifyAll guarded"], "deviations": "Dispatch named assignees os-bill on pr_create; os-dev.md rule 2 says a dev's pr_create carries none. Followed the dispatch (also the project's PR-attribution instruction) and name the conflict here for the seat." }objectstack-fleet commentedon Nov 5, 2026
domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC
Landed · 2026-11-05T11:25Z
f7afab86b2607d4409bdf8e827a0b28b1d12d024(merged_at 2026-11-05T11:24Z).modifyAllRecordstoviewAllRecords, and serves no write operation inapiOperations;/auth/me/permissionsandgetEffectiveObjectPermissionsboth readObjectQL#isExternalWriteForbidden, the same verdictassertWriteAllowedrefuses on. The console hides New / Import / Edit / Delete with no client change.pm:dispatchedremoved. A read-only external object offers New and Import, then refuses with a permission message that blames the wrong thing #30334 item 2 (the refusal message) stays with its ui thread.