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
access: an external object on the default sharing model refuses every member edit (403) because the ownership checks read owner_id / created_by, which no remote row carries — os validate says nothing (epic #27261, found by the P0 spike) #27332
epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
Filing gate: ① product defect, class c, reach measured through the public HTTP door.
Reader: the epic PM (#27261) routes it after triage sets the lane; the fix PR is the os validate warning below. #27278 (the guide) carries the author-facing rule meanwhile.
Dedupe (open AND closed): "external federated object sharing model owner_id created_by 403 member edit" → 20 hits, none this defect (#26596 is the public_read_write write-floor change, #23268 the retired externalSharingModel); "external object datasource sharingModel owner column validate warning ADR-0015" → 19 hits, none this defect.
reach: public HTTP entry, measured: PATCH /api/v1/data/spike_owned/o_1 by a member whose permission set grants allowEdit answers 403 PERMISSION_DENIED — pinned as step (e3) in packages/runtime/src/plugin-driver-external-object.spike.test.ts (PR #27330, head 680ce37b).
type: Bug
Part of #27261 (Tier A, ruling 6471382887). Found by the P0 spike #27276; its report is comment 6476152968 on #27276.
The spike's second external object spike_owned (no sharingModel, so the platform default) on the plugin driver driver.spike_tree. The member holds { allowRead: true, allowEdit: true } on it. Before refusing, the remote is asked exactly:
find { where: { id: 'o_1' }, fields: ['id', 'owner_id'], limit: 1 } (the sharing check), and
Neither column exists on a remote row, so every row reads as someone else's; driver.update is never called. On sharingModel: 'public_read_write' the same member's write reaches beforeUpdate and then driver.update (steps e1/e2 of the same file).
Task
os validate warning (not a refusal) for an object with an external binding whose effective sharing model is owner-keyed and whose external.columnMap maps no owner column; the message names both ways out.
Pin: the spike's spike_owned shape gets the warning; the spike_node shape (public_read_write) does not.
Check whether the same two reads also refuse delete, and name it in the PR.
Clause-②: no (a warning narrows nothing) · Metadata-protocol: none.
⚠️ This waited since filing: triage's sweep skipped every card that carries pm:epic, children included. That is fixed from this round. pm:epic stays, since it is the epic PM's subtree reservation.
Read fresh. An external object on the default sharing model refuses every member edit with 403. The ownership checks read owner_id and created_by, which no remote row carries, and os validate says nothing. This is measured through the public door (the spike's step e3). Members are blocked from a capability their permission set grants, so p2.
Lane domain:spec: the fix is the os validate warning in packages/lint. It names the two ways out (public_read_write with a hook deciding who may edit, or an owner column in columnMap). ⛔ A warning, not a defineStack refusal, so the published accept set does not narrow, as the card says.
epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
Claim: PM loop round 3 2026-10-29T04:23Z
Session: session_017ihL2c5z3q4h8inrn5kwqV
Account: marchtian
Branch: claude/project-thread-6ml89f (this container can push only its designated branch; restarted from origin/main once PR #27349, the branch's current head, leaves the merge queue — measured platform exception to claude/issue-<n>-<slug>)
Worktree: objectstack-issue-27332
Domain: domain:spec (triage 6487601214: lint lane)
Seat: epic:#27261#1 (epic PM; no domain:* seat). Maintainer order, 田騏 2026-10-29T04:21Z: 「影响你的任务进度的,你都统一接管」 — the epic PM takes over every derived task that affects the epic's progress.
File surface: packages/lint/src/** (the new warning rule, its explanation entry, tests), packages/runtime/src/plugin-driver-external-object.spike.test.ts only for the pin, .changeset/** (stop on breach; explain in the report)
Container & model: S, mode:subagent, model: default tier, effort: high; full Fable review before landing (project rule for the spec lane)
Clause-②: no (a warning narrows nothing)
Metadata-protocol: none
Responsibility: the access/ownership checks predate external objects; the defect is the missing author-time signal, measured by the P0 spike (e3)
Thread-read: 6487601214
Premise: holds — on origin/main6e0e7d7packages/lint/src has no rule pairing an external binding with an owner-keyed sharing model; the neighbouring hints in system-fields.ts:230-242 already name columnMap and ownership: 'none' as ways out for a missing system column, so the dev checks whether ownership: 'none' is a third way out here.
Serial constraints cleared: #27277 (card thread) touches packages/objectql/src/**, packages/spec/src/** and the guide; no overlap.
{
"issue": 27332,
"status": "done",
"branch": "claude/project-thread-6ml89f",
"pr": "https://github.com/objectstack-ai/objectstack/pull/27422",
"session": "session_017ihL2c5z3q4h8inrn5kwqV (parent PM session; this run is its subagent)",
"premise_still_valid": true,
"summary": "New advisory lint rule security-external-owner-unmapped (os validate / build / lint, CLI only): warns on a writable external object on an owner-keyed sharing model (private, public_read, or unset on a custom object) that declares no owner_id (or, with ownership none/org/business_unit, no created_by), because record sharing reads owner_id and the ownership floor reads created_by == caller and no remote row carries either, so every member edit AND delete is 403. Per the PM note, external.columnMap is applied only by built-in SQL drivers, so the rule is silenced only by a DECLARED field; a columnMap-only entry still warns with its own wording; the hint names both ways out driver-neutrally (public_read_write + beforeUpdate hook, or declare the field and have the driver return the remote owner under that name: SQL via columnMap, plugin driver itself), matching PR #27403's guide wording. Measured: delete is refused by the same two reads; public_read_write opens edits but NOT deletes (floor keeps deletes by design); ownership: 'none' is not a way out (moves the refusal to the floor, measured 403), so the rule fires on it naming created_by. Premise partly corrected: spike_owned already got security-owd-unset (error) whose hint recommends 'private', the 403 shape; nothing named the refusal. Pin: spike_owned warns, spike_node silent (lint test with the spike's shapes verbatim). Card assignee untouched.",
"tests": "At ee143ecb: pnpm --filter @objectstack/lint test to Test Files 177 passed (177), Tests 7583 passed (7583); pnpm --filter @objectstack/lint typecheck to tsc clean, check:test-typecheck OK; new validate-external-owner-sharing.test.ts 14/14. Ablation (src-imported test, no dist leg): owner === 'author' silence arm removed via scripts/ablation-replace.mjs (anchor 1 to 0, blob 1cbc9bde to 24cd3fc6) to 1 failed / 13 passed; restored blob == HEAD, git diff HEAD empty. Runtime measurement: one-off scratch boot of the spike composition (deleted before commit) over 17 object shapes, PATCH/DELETE/GET per shape (table in PR body); SQL half measured on SqlDriver (better-sqlite3) with columnMap { assignee: owner_id }: the sharing reads return owner_id.",
"gates": "dispatch-gates --commands at ee143ecb: 88 derived, all run sequentially at ee143ecb, exits captured before pipes; --ran with COMMAND :: exit N record: 88 accounted, 87 run exit 0, 1 NOT-MEASURED (node scripts/check-plugin-teardown-shape.mjs --self-test exit 3: positive control pinned to commit 621a4876 unreachable in this shallow checkout; checker health only). First run at 4199782a: check:docs-transcript-drift red (rule count 55 to 56 in four CLI transcripts) to fixed in b0e37b9d, green at head. CI check-runs on PR #27422: in_progress / not yet read (no polling per contract).",
"dogfood_battery": "n/a - no ! in the PR title and no (narrowing) arm",
"mcp_calls": "2 - mcp__github__issue_read (get, get_comments) on #27332; no MCP write tool called",
"api_writes": "2 relay writes (+ git push x3, not REST): fleet-write pr_create to PR #27422 (draft, assignee marchtian, body read-back identical 13764 bytes); post-stamped comment on #27332 (this os-dev-report)",
"line_budget": "diff vs merge base 7e200b4a: 11 files; lint rule ~170 lines, test ~150, explanation ~45, registry/barrel ~30, changeset ~20, 4 docs lines; size S-M",
"deviations": [
"File surface breach (declared): four content/docs transcripts (deployment/cli.mdx, deployment/validating-metadata.mdx, getting-started/build-with-claude-code.mdx, ui/react-pages.mdx) changed 'author-time rules (55)' to '(56)' because check:docs-transcript-drift is a gate the new rule trips (dispatch: follow whatever gates the rule trips). Not the virtual-objects guide.",
"Branch/trailers/PR body/comment stamp per PM container deviations (claude/project-thread-6ml89f; model-free trailers; PR body ends with the harness footer, not the session-URL footer form; comment's first line is the stamp, os-dev-report on line 2).",
"Rule design revised mid-run on the PM note (columnMap not applied by the engine): first revision silenced on a columnMap entry; second (ee143ecb) silences only on a declared field.",
"Pin lives in packages/lint (the spike's two object shapes copied verbatim); the spike test file was not edited.",
"No test:repo run locally (CI)."
],
"files_changed": [
"packages/lint/src/validate-external-owner-sharing.ts (new)",
"packages/lint/src/validate-external-owner-sharing.test.ts (new)",
"packages/lint/src/authoring-rules.ts",
"packages/lint/src/index.ts",
"packages/lint/src/rule-explanations.ts",
"packages/lint/src/validate-sharing-rule-enforceability.ts (export effectiveSharingModelOf)",
".changeset/27332-external-owner-unmapped-warning.md",
"content/docs/deployment/cli.mdx, content/docs/deployment/validating-metadata.mdx, content/docs/getting-started/build-with-claude-code.mdx, content/docs/ui/react-pages.mdx (rule count only)"
],
"open_questions": [
{
"question": "Guide wording on PR #27403 (#27277's surface): 'Either set sharingModel explicitly' alone is not a way out (explicit 'private' is still 403), and the callout does not say member deletes stay refused on public_read_write. Change the guide?",
"options": [
"A: #27277's dev adds two sentences (way out is 'public_read_write'; deletes stay refused there unless the driver returns created_by or a policy/share grants delete)",
"B: leave the guide; the os explain text carries it"
],
"recommendation": "A, because the guide and the warning must agree and the guide is what authors read first"
},
{
"question": "security-owd-unset's hint recommends sharingModel 'private' unconditionally; on a writable external object that is the 403 shape (spike_owned gets both findings). Reword it for external objects?",
"options": [
"A: separate small card to make the hint name public_read_write for a writable external object",
"B: leave it; the new warning sits beside it"
],
"recommendation": "A, low cost, removes contradictory advice on the same object"
}
],
"out_of_scope_findings": [
"class: b · reach: exception: security (possible data exposure); also measured through GET /data/OBJECT in a booted spike composition · evidence: on a sharingModel 'private' external object whose driver returns owner_id (columnMap or plugin driver) but owner_id is NOT declared in fields, a member GET returned 2 of 2 rows including the admin-owned one; with owner_id declared, 1 (own only). Writes on the same object are owner-scoped (PATCH other 403). Seam: spec:ObjectSchema.sharingModel 'private' (owner-only) + external.columnMap to runtime: plugin-sharing SharingService.buildReadFilter via hasPhantomOwnerAnchor (federated-phantom-anchors.ts; provenance reads fields only, so a served owner counts as unstored and reads go unscoped) · dedupe words: federated phantom owner_id read filter unscoped private external columnMap, hasPhantomOwnerAnchor read scope",
"class: a · reach: named producer: the engine's update stamp (spike step e2 pins driver.update data keys end_date, guard_note, id, start_date, updated_at, updated_by) · evidence: SqlDriver (better-sqlite3, schemaMode external) over a remote table without audit columns: update('m','r1',{ name, updated_by }) to 'no such column: updated_by'; not yet measured through PATCH on a SQL-backed external object, so it may be handled upstream · dedupe words: external object update updated_by updated_at no such column SQL driver federated audit stamp",
"carrier: #27277 (PR #27403) · guide callout: 'set sharingModel explicitly' is not a way out by itself; deletes stay refused on public_read_write - noted, not filed",
"carrier: none · security-owd-unset hint recommends 'private' for a writable external object (a warning steering to runtime-honoured metadata that makes things worse: boundary, not class c) - in PR acceptance notes"
]
}
merged 2026-10-29T07:16Z as 922973840dd990fda6fc727b4f7e77113d3d6a8d (merge queue), head 120864f4
Card
closed completed by Fixes #27332
State: pm:dispatched removed in the same act. Landed by the epic PM after seat ACCEPT on the PR, following an independent full Fable review. The maintainer's order (田騏, 2026-10-29T04:19Z): 「你应该使用 pm-dispatch 技能处理 issue 和 pr 落地」. The read-scope exposure found on the way is #27425. Still carried, not filed: security-owd-unset's hint recommends private for a writable external object, which is the 403 shape; the seat carries it on the epic.
Activity
objectstack-fleet commentedon Oct 29, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: 真挡得住的权限 | none | P②
Triage: p2 ·
domain:spec(lint) ·area:access· 2026-10-29T04:04Z⛔ Not a claim.
pm:epic, children included. That is fixed from this round.pm:epicstays, since it is the epic PM's subtree reservation.owner_idandcreated_by, which no remote row carries, andos validatesays nothing. This is measured through the public door (the spike's step e3). Members are blocked from a capability their permission set grants, so p2.domain:spec: the fix is theos validatewarning inpackages/lint. It names the two ways out (public_read_writewith a hook deciding who may edit, or an owner column incolumnMap). ⛔ A warning, not adefineStackrefusal, so the published accept set does not narrow, as the card says.bug,pm:queue.Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
Claim: PM loop round 3 2026-10-29T04:23Z
Session:
session_017ihL2c5z3q4h8inrn5kwqVAccount:
marchtianBranch:
claude/project-thread-6ml89f(this container can push only its designated branch; restarted fromorigin/mainonce PR #27349, the branch's current head, leaves the merge queue — measured platform exception toclaude/issue-<n>-<slug>)Worktree:
objectstack-issue-27332Domain:
domain:spec(triage 6487601214: lint lane)Seat:
epic:#27261#1(epic PM; nodomain:*seat). Maintainer order, 田騏 2026-10-29T04:21Z: 「影响你的任务进度的,你都统一接管」 — the epic PM takes over every derived task that affects the epic's progress.File surface:
packages/lint/src/**(the new warning rule, its explanation entry, tests),packages/runtime/src/plugin-driver-external-object.spike.test.tsonly for the pin,.changeset/**(stop on breach; explain in the report)Container & model:
S,mode:subagent,model: default tier,effort: high; full Fable review before landing (project rule for the spec lane)Clause-②: no (a warning narrows nothing)
Metadata-protocol: none
Responsibility: the access/ownership checks predate external objects; the defect is the missing author-time signal, measured by the P0 spike (e3)
Thread-read: 6487601214
Premise: holds — on
origin/main6e0e7d7packages/lint/srchas no rule pairing anexternalbinding with an owner-keyed sharing model; the neighbouring hints insystem-fields.ts:230-242already namecolumnMapandownership: 'none'as ways out for a missing system column, so the dev checks whetherownership: 'none'is a third way out here.Serial constraints cleared: #27277 (card thread) touches
packages/objectql/src/**,packages/spec/src/**and the guide; no overlap.objectstack-fleet commentedon Oct 29, 2026
epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
os-dev-report
{ "issue": 27332, "status": "done", "branch": "claude/project-thread-6ml89f", "pr": "https://github.com/objectstack-ai/objectstack/pull/27422", "session": "session_017ihL2c5z3q4h8inrn5kwqV (parent PM session; this run is its subagent)", "premise_still_valid": true, "summary": "New advisory lint rule security-external-owner-unmapped (os validate / build / lint, CLI only): warns on a writable external object on an owner-keyed sharing model (private, public_read, or unset on a custom object) that declares no owner_id (or, with ownership none/org/business_unit, no created_by), because record sharing reads owner_id and the ownership floor reads created_by == caller and no remote row carries either, so every member edit AND delete is 403. Per the PM note, external.columnMap is applied only by built-in SQL drivers, so the rule is silenced only by a DECLARED field; a columnMap-only entry still warns with its own wording; the hint names both ways out driver-neutrally (public_read_write + beforeUpdate hook, or declare the field and have the driver return the remote owner under that name: SQL via columnMap, plugin driver itself), matching PR #27403's guide wording. Measured: delete is refused by the same two reads; public_read_write opens edits but NOT deletes (floor keeps deletes by design); ownership: 'none' is not a way out (moves the refusal to the floor, measured 403), so the rule fires on it naming created_by. Premise partly corrected: spike_owned already got security-owd-unset (error) whose hint recommends 'private', the 403 shape; nothing named the refusal. Pin: spike_owned warns, spike_node silent (lint test with the spike's shapes verbatim). Card assignee untouched.", "tests": "At ee143ecb: pnpm --filter @objectstack/lint test to Test Files 177 passed (177), Tests 7583 passed (7583); pnpm --filter @objectstack/lint typecheck to tsc clean, check:test-typecheck OK; new validate-external-owner-sharing.test.ts 14/14. Ablation (src-imported test, no dist leg): owner === 'author' silence arm removed via scripts/ablation-replace.mjs (anchor 1 to 0, blob 1cbc9bde to 24cd3fc6) to 1 failed / 13 passed; restored blob == HEAD, git diff HEAD empty. Runtime measurement: one-off scratch boot of the spike composition (deleted before commit) over 17 object shapes, PATCH/DELETE/GET per shape (table in PR body); SQL half measured on SqlDriver (better-sqlite3) with columnMap { assignee: owner_id }: the sharing reads return owner_id.", "gates": "dispatch-gates --commands at ee143ecb: 88 derived, all run sequentially at ee143ecb, exits captured before pipes; --ran with COMMAND :: exit N record: 88 accounted, 87 run exit 0, 1 NOT-MEASURED (node scripts/check-plugin-teardown-shape.mjs --self-test exit 3: positive control pinned to commit 621a4876 unreachable in this shallow checkout; checker health only). First run at 4199782a: check:docs-transcript-drift red (rule count 55 to 56 in four CLI transcripts) to fixed in b0e37b9d, green at head. CI check-runs on PR #27422: in_progress / not yet read (no polling per contract).", "dogfood_battery": "n/a - no ! in the PR title and no (narrowing) arm", "mcp_calls": "2 - mcp__github__issue_read (get, get_comments) on #27332; no MCP write tool called", "api_writes": "2 relay writes (+ git push x3, not REST): fleet-write pr_create to PR #27422 (draft, assignee marchtian, body read-back identical 13764 bytes); post-stamped comment on #27332 (this os-dev-report)", "line_budget": "diff vs merge base 7e200b4a: 11 files; lint rule ~170 lines, test ~150, explanation ~45, registry/barrel ~30, changeset ~20, 4 docs lines; size S-M", "deviations": [ "File surface breach (declared): four content/docs transcripts (deployment/cli.mdx, deployment/validating-metadata.mdx, getting-started/build-with-claude-code.mdx, ui/react-pages.mdx) changed 'author-time rules (55)' to '(56)' because check:docs-transcript-drift is a gate the new rule trips (dispatch: follow whatever gates the rule trips). Not the virtual-objects guide.", "Branch/trailers/PR body/comment stamp per PM container deviations (claude/project-thread-6ml89f; model-free trailers; PR body ends with the harness footer, not the session-URL footer form; comment's first line is the stamp, os-dev-report on line 2).", "Rule design revised mid-run on the PM note (columnMap not applied by the engine): first revision silenced on a columnMap entry; second (ee143ecb) silences only on a declared field.", "Pin lives in packages/lint (the spike's two object shapes copied verbatim); the spike test file was not edited.", "No test:repo run locally (CI)." ], "files_changed": [ "packages/lint/src/validate-external-owner-sharing.ts (new)", "packages/lint/src/validate-external-owner-sharing.test.ts (new)", "packages/lint/src/authoring-rules.ts", "packages/lint/src/index.ts", "packages/lint/src/rule-explanations.ts", "packages/lint/src/validate-sharing-rule-enforceability.ts (export effectiveSharingModelOf)", ".changeset/27332-external-owner-unmapped-warning.md", "content/docs/deployment/cli.mdx, content/docs/deployment/validating-metadata.mdx, content/docs/getting-started/build-with-claude-code.mdx, content/docs/ui/react-pages.mdx (rule count only)" ], "open_questions": [ { "question": "Guide wording on PR #27403 (#27277's surface): 'Either set sharingModel explicitly' alone is not a way out (explicit 'private' is still 403), and the callout does not say member deletes stay refused on public_read_write. Change the guide?", "options": [ "A: #27277's dev adds two sentences (way out is 'public_read_write'; deletes stay refused there unless the driver returns created_by or a policy/share grants delete)", "B: leave the guide; the os explain text carries it" ], "recommendation": "A, because the guide and the warning must agree and the guide is what authors read first" }, { "question": "security-owd-unset's hint recommends sharingModel 'private' unconditionally; on a writable external object that is the 403 shape (spike_owned gets both findings). Reword it for external objects?", "options": [ "A: separate small card to make the hint name public_read_write for a writable external object", "B: leave it; the new warning sits beside it" ], "recommendation": "A, low cost, removes contradictory advice on the same object" } ], "out_of_scope_findings": [ "class: b · reach: exception: security (possible data exposure); also measured through GET /data/OBJECT in a booted spike composition · evidence: on a sharingModel 'private' external object whose driver returns owner_id (columnMap or plugin driver) but owner_id is NOT declared in fields, a member GET returned 2 of 2 rows including the admin-owned one; with owner_id declared, 1 (own only). Writes on the same object are owner-scoped (PATCH other 403). Seam: spec:ObjectSchema.sharingModel 'private' (owner-only) + external.columnMap to runtime: plugin-sharing SharingService.buildReadFilter via hasPhantomOwnerAnchor (federated-phantom-anchors.ts; provenance reads fields only, so a served owner counts as unstored and reads go unscoped) · dedupe words: federated phantom owner_id read filter unscoped private external columnMap, hasPhantomOwnerAnchor read scope", "class: a · reach: named producer: the engine's update stamp (spike step e2 pins driver.update data keys end_date, guard_note, id, start_date, updated_at, updated_by) · evidence: SqlDriver (better-sqlite3, schemaMode external) over a remote table without audit columns: update('m','r1',{ name, updated_by }) to 'no such column: updated_by'; not yet measured through PATCH on a SQL-backed external object, so it may be handled upstream · dedupe words: external object update updated_by updated_at no such column SQL driver federated audit stamp", "carrier: #27277 (PR #27403) · guide callout: 'set sharingModel explicitly' is not a way out by itself; deletes stay refused on public_read_write - noted, not filed", "carrier: none · security-owd-unset hint recommends 'private' for a writable external object (a warning steering to runtime-honoured metadata that makes things worse: boundary, not class c) - in PR acceptance notes" ] }docs(data-modeling): the sharing callout says what actually lets a me…
objectstack-fleet commentedon Oct 29, 2026
epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
Landed · 2026-10-29T07:18Z
922973840dd990fda6fc727b4f7e77113d3d6a8d(merge queue), head120864f4completedbyFixes #27332State:
pm:dispatchedremoved in the same act. Landed by the epic PM after seat ACCEPT on the PR, following an independent full Fable review. The maintainer's order (田騏, 2026-10-29T04:19Z): 「你应该使用 pm-dispatch 技能处理 issue 和 pr 落地」. The read-scope exposure found on the way is #27425. Still carried, not filed:security-owd-unset's hint recommendsprivatefor a writable external object, which is the 403 shape; the seat carries it on the epic.