Repository navigation
Description
Filing gate: ① product defect, reproduced twice, landing site named (maintainer direct dispatch, authorizing words quoted in the audit comment).
Symptom (customer-visible)
An admin changes an existing object's org-wide default to Public read/write (Studio › Access › org-wide defaults, or a metadata save) and publishes. A member whose permission set grants Read + Edit on that object can now read another user's record, but editing it is refused with the record-level sentence "You do not have access to this record…".
The same object declared public_read_write from its first publish lets the same member edit (2xx). The only difference between the two objects is whether the OWD was changed after the first publish.
This contradicts the ruling #8023 landed in PR #8072: a public_read_write OWD opens row-level updates, not just the creator's.
- Fail-closed: this is over-refusal, and nothing is exposed.
- The step-by-step reproduction is withheld from this card, per the maintainer's instruction for authorization defects. It goes to the implementer privately.
Measured
- Repo
3fcafcd3, rerun onorigin/main898703f9. No commit toplugin-securityorplugin-sharinglies in between. - 2026-10-27: two independent reproductions on a fresh probe package, plus one control object declared
public_read_writefrom birth (2xx). One reproduction used an object the member had never touched before the change. - The Studio QA Access card reproduced it independently on its own server.
Landing site (suspected — the implementer verifies)
packages/plugins/plugin-security/src/security-plugin.ts: the per-object posture (owdOpensRowWrites, written byowdDeclaresOpenRowWrites) appears not to be re-derived when a published object'ssharingModelchanges.- Reads (the sharing service) do pick the change up, so it is the security plugin's posture/collection path, not the OWD itself.
- The existing pin
packages/qa/dogfood/test/owd-public-read-write-write-floor.dogfood.test.tsonly boots objects with their final OWD, so it cannot see a change at runtime.
Acceptance
- After an OWD change to
public_read_writeis published (no restart), a member with Edit on the object can update a row they did not create. - The reverse change (
public_read_write→private/public_read) brings the creator-only update floor back. - The delete floor is unchanged. fix(plugin-security): require by-id write targets to be within the caller's readable set under select-only RLS #7792's by-id write-visibility property still holds.
- A pin that changes the OWD at runtime and asserts both directions over HTTP.
Filing facts
- Business reason: admins tighten or open an object's OWD as their data model matures, and Studio's OWD panel exists for that. Today opening it silently fails for members until the next restart (suspected), so "everyone edits" does not work for the teams that turn it on.
- Reader who acts: an
os-devimplementer, dispatched directly by this PM session on the maintainer's instruction (domain:server). - Dedupe: semantic search
public_read_write sharing model changed after publish member update still refused owner_only_writes flooron objectstack-ai/objectstack → 30 hits. No open duplicate; the nearest is closed Apublic_read_writeobject is only writable by the row's creator —showcase_contributorgets 403 editing ashowcase_projectit can read, where the access matrix declaresedit:true#8023 (the origin ruling), and closed sharing: a share or sharing rule to an external member is refused as inert on a public_read_write or model-less system object, though the external OWD floor would consult it #25549 is a different path (external members). - Source: Studio QA run, 2026-10-27, coordinator session
session_01J8VVAkZns5rNnK2rtszzbQ.
Activity
objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 1 (maintainer direct dispatch)
Session:
session_01J8VVAkZns5rNnK2rtszzbQAccount:
huangyiireneBranch:
claude/issue-26596-owd-change-write-floorWorktree:
objectstack-issue-26596Domain:
domain:serverSeat:
domain:server#1(direct-dispatch session, not a lane seat)File surface:
packages/plugins/plugin-security/src/**,packages/qa/dogfood/test/**(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate),effort: xhigh(security)Clause-②: no
Metadata-protocol: none
Responsibility:
plugin-security's per-object posture keeps the creator-only update floor after a published OWD change|none — #8072's exemption covers only an OWD declared from the first publish|any member of an org whose admin opens an existing object to public_read_write; fail-closed (over-refusal)Thread-read: none
Premise: holds (reproduced twice 2026-10-27 on a server built from 3fcafcd; no plugin-security/plugin-sharing commit between 3fcafcd and origin/main 898703f)
Serial constraints cleared: open PR #26337 (ADR-0142 phase 2, ownership queues) may touch plugin-security — rebase over it if it lands first; no other in-flight claim on this surface
Direct-dispatch authority — maintainer, this session, 2026-10-27, verbatim: 「3 个 AUTH 类问题(复现细节都不公开);你可以直接派发处理」
Reproduction steps stay off GitHub per that instruction; they go to the implementer in the private dispatch prompt.
Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Fold: this card is now the chain head of a folded family dispatch with #26698 (same root cause; that card's details are withheld pending maintainer).
claude/issue-26596-owd-change-write-floor.Fixes #26596andFixes #26698, one commit per card.packages/metadata/src/**, the subscription seam only.session_01J8VVAkZns5rNnK2rtszzbQGenerated by Claude Code
fix(plugin-security): metadata-derived caches invalidate on a runtime…
objectstack-fleet commentedon Oct 27, 2026
os-dev-report
{
"issue": 26596,
"folded_with": [
26698
],
"status": "done",
"branch": "claude/issue-26596-owd-change-write-floor",
"pr": "#26878",
"session": "session_01J8VVAkZns5rNnK2rtszzbQ",
"premise_still_valid": true,
"summary": "plugin-security's metadata-derived caches (objectSecurityMetaCache, fieldNamesCache and its datetime/number siblings, tenancyDisabledCache, cbpRelCache, plus the epoch.bump('metadata') memo retirement) now clear on the protocol's post-persistence onMetadataMutation (non-draft; every authoring door; cluster peers replay into the same listeners after convergence) and on the metadata:reloaded kernel hook, via the new metadata-change-invalidation.ts; the dead md.watch('') probe is gone and the clearing body moved verbatim into invalidateMetadataDerivedCaches(). Landing site is plugin-security only: measurement showed the metadata service (NodeMetadataManager) is not the producer for runtime authoring, so packages/metadata/src was not touched. HTTP dogfood pin covers five runtime declaration changes; red 5/6 on the pre-fix build, 6/6 after. One commit per card: 9864bb9 (#26596) and 8dcb3e2 (#26698). PR body and this comment carry no symptom detail (maintainer disclosure rule); the private detail is in the final report to the PM only.",
"hypotheses": {
"H1": "CONFIRMED: owdOpensRowWrites is read from objectSecurityMetaCache, populated at first use and never cleared; the pin case for an OWD change goes green once the subscription fires, and red again when only the protocol listener is ablated.",
"H2": "CONFIRMED and WIDER: notifyWatchersLocal keys by exact type, so subscribe('') never fires; but measured, even exact-type subscribe('object') / watchService('object') fire 0 times for two runtime PUT /meta/object saves, while protocol onMetadataMutation fired 2. Runtime authoring goes protocol saveMetaItem then applyRegistryWriteThrough, and ObjectQL mirrors the object into the metadata service with registerInMemory / notify:false. The PM-suggested route (subscribe through MetadataManager's API) would not have fixed either card.",
"H3": "onMetadataMutation receives cluster replays: applyRemoteMetadataMutation calls notifyMutationListenersLocal after the peer's own registry convergence; already pinned by packages/metadata-protocol/src/protocol.cluster-mutation-fanout.test.ts ('the peer's onMetadataMutation listeners receive the remote event, once, after convergence'). Not re-measured; stated in the PR body.",
"H4": "plugin-security is the ONLY probe of a metadata-service watch in packages/** (git grep for '.watch(' / md?.watch / watch === 'function'). Other hits are live and unrelated: MetadataRepository.watch in metadata-core / metadata-fs / metadata-protocol / MetadataManager's repo loop, chokidar in cli dev and metadata-fs, the Redis client in service-cluster-redis. Nothing else fixed."
},
"tests": "Pin packages/qa/dogfood/test/security-verdict-follows-runtime-metadata-change.dogfood.test.ts: pre-fix (base 4373103, closure built from it) 'Tests 5 failed | 1 passed (6)' VERDICT command-exit 1; post-fix 'Tests 6 passed (6)'; post-merge with origin/main 3c2187f (closure rebuilt, 67/67 tasks) pin + owd-public-read-write-write-floor 'Test Files 2 passed (2) Tests 16 passed (16)'. Ablation (protocol listener disabled, metadata:reloaded hook kept): ablation-replace anchor x1 to x0, blob 677b3111 to df2e7be4; plugin-security build exit 0; ablation-dist-preflight marker present in dist/index.js + dist/index.mjs; pin 'Tests 5 failed | 1 passed (6)'; restore blob == HEAD 677b3111, git diff HEAD empty; rebuild exit 0; preflight --absent 'marker absent from all 6 built files'; pin 'Tests 6 passed (6)'. Final head 8dcb3e2 (tree f87a2d44, identical to the tested merge head 8b4f335): plugin-security typecheck green (check:test-typecheck OK, 0 errors), dogfood typecheck green, 'pnpm --filter @objectstack/plugin-security test' -> 'Test Files 232 passed (232) Tests 5040 passed | 45 skipped (5085)'. Earlier full runs: one red on everyone-rederivation-on-permission-mutation.test.ts (it counted every protocol listener; now reads the bootstrap's own) and one load run with 2 x 5000 ms timeouts (delegated-organization-membership, rls-stored-list-ordering-fails-closed) that passed in isolation and in the final full run. Seam probe (throwaway file, deleted): 'NodeMetadataManager has watch: undefined', seen {subscribeObject:0, subscribeStar:0, watchServiceObject:0, watchServiceStar:0, protocolMutation:2}.",
"gates": "Local, on head 8dcb3e2: dispatch-gates --commands --repo objectstack-ai/objectstack (no paths; change set from merge base 3c2187f, 9 paths, +720/-62) derived 66 commands; all 66 run, each exit captured before any pipe; 63 exit 0 first time; 3 exit 3 (PREREQUISITE NOT MET) re-run to a reading: check-plugin-teardown-shape --self-test (fetched pinned commit 621a487 at depth 1) exit 0, 48 cases; check:dual-build-cjs-loads (built 7 packages lacking dist, turbo exit 0) exit 0; check:type-check-debt under the verify lock exit 0 '0 raw tsc error(s)'. 'dispatch-gates --ran': '66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN' exit 0. eslint --no-inline-config on the 8 changed TS files exit 0. CI on PR #26878 at report time: 21 check-runs, 2 skipped (18.x upgrade harness, Packed-tarball smoke: opt-in), 19 in_progress including Type Check (workspace / source / consumer / debt ledger), Lint (ESLint / repo & tooling / CI, release & engine / docs & code / PM dispatch-gates / scoped x3), Check Changeset, PR-body guards, Governed Surface Queue Guard, Spec property liveness, Check PR Size; Test Core / Build Core not yet listed. Not waited on (CI convergence is the PM's).",
"dogfood_battery": "n/a — no
!in the PR title and no (narrowing) arm (Clause-②: no)","mcp_calls": "0",
"api_writes": "2 relay strokes (POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create -> relay POST /repos/objectstack-ai/objectstack/pulls (#26878, draft) + POST /repos//issues/26878/assignees [huangyiirene], read-back 10414/10414 bytes identical; (2) comment -> relay POST /repos//issues/26596/comments (this report). git pushes (not REST): empty-branch probe, WIP pushes, one --force-with-lease=claude/issue-26596-owd-change-write-floor:8b4f335d re-lay.",
"files_changed": [
".changeset/26596-security-cache-invalidation.md",
"packages/plugins/plugin-security/src/metadata-change-invalidation.ts",
"packages/plugins/plugin-security/src/metadata-change-invalidation.test.ts",
"packages/plugins/plugin-security/src/security-plugin.ts",
"packages/plugins/plugin-security/src/security-plugin-cbp-relation.ts",
"packages/plugins/plugin-security/src/security-plugin.test.ts",
"packages/plugins/plugin-security/src/controlled-by-parent-reference-alias.test.ts",
"packages/plugins/plugin-security/src/everyone-rederivation-on-permission-mutation.test.ts",
"packages/qa/dogfood/test/security-verdict-follows-runtime-metadata-change.dogfood.test.ts"
],
"line_budget": "9 files, +720 / -62 = 782 changed lines vs merge base 3c2187f (human-merge threshold 3000: under); no skills/** or governed surface touched.",
"deviations": [
"dispatch-gates was run WITHOUT path arguments (the role file's rule: derive the change set from the merge base, never a hand-built list); the dispatch said 'YOUR_CHANGED_PATHS'. Conflict noted, role file followed.",
"File surface: dispatch named packages/metadata/src/** for the subscription seam; nothing there was changed (measurement put the seam in the protocol). Touched only plugin-security/src, packages/qa/dogfood/test and .changeset.",
"History re-laid once: 5 WIP commits + a merge of origin/main were replaced by two card commits on 3c2187f (9864bb9 #26596, 8dcb3e2 #26698), tree-identical to the tested head (f87a2d44), pushed with --force-with-lease=claude/issue-26596-owd-change-write-floor:8b4f335d before the PR existed (all five AGENTS.md conditions held).",
"PR footer uses the AGENTS.md session-URL form, not the harness reminder form (CLAUDE.md -> AGENTS.md outranks the reminder).",
"The GitHub copy of this report omits private_detail (maintainer disclosure rule); the final message to the PM carries it.",
"A pid file was written to /post-merge.pid (filesystem root) by a shell-variable scoping slip; removal was refused by the sandbox safety check. It holds only the text 18958. Please delete it by hand.",
"Fetched commit 621a487 at depth 1 into the shared object store so check-plugin-teardown-shape --self-test could read its pinned fixture.",
"test:repo (vitest repo project) not run locally: the diff touches none of its tests; CI Test Core owns it. pnpm lint not run (CI-owned); eslint on the changed files run instead.",
"New dogfood file: 24.9 s file wall (tests 6.65 s) on a shared box, over the 10 s file line like every stack-booting dogfood file; no test-budget class fits, none declared."
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed — IMPORTANT for the spec lane: IMetadataService.watch? (packages/spec/src/contracts/metadata-service.ts) is declared, implemented by no shipped metadata service, and after this PR consumed by nobody; it is exactly the optional member whose silent absence produced this defect. Declared surface with zero pull: PR acceptance note only (not a class a/b/c filing: optional, no contract violated, not authorable metadata). Dedupe words: IMetadataService watch optional member unimplemented, metadata service watch retire, MetadataManager watchService.",
"carrier: none · noted, not filed — metadata-service watcher events (dev FileSystemRepository watch and its metadata.changed cluster replay) are not subscribed: no runtime object registration announces there (all notify:false), and a clear on those events would land before ObjectQL's async registry refresh for the same event. Not reproduced. Dedupe words: security cache FileSystemRepository watch, metadata.changed replay security invalidation.",
"carrier: none · noted, not filed — getObjectFieldNames awaits loadObjectFieldNames before fieldNamesCache.set, a microtask window in which a mutation's clear could be overtaken by a stale set. Theoretical, not exercised, pre-existing."
]
}
Generated by Claude Code