Skip to content

lint: security-owd-unset's hint recommends sharingModel 'private' on a writable external object, the shape that refuses every member edit and that security-external-owner-unmapped warns against (epic #27261) #27472

Closed

Description

@objectstack-fleet

epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
Filing gate: ① product defect (author-time advice steering to a refused shape), a follow-up owed by landed work (#27332, PR #27422).
Reader: the epic PM (#27261) dispatches it in the epic subtree after triage; the fix is in packages/lint/src/validate-security-posture.ts (the security-owd-unset hint).
Dedupe (open AND closed): "security-owd-unset hint recommends private sharingModel external object writable" → 29 hits, none this defect (#27332 is the warning that now contradicts this hint).
reach: public entry os validate, measured by #27332's dev (report 6489300223, out-of-scope finding 4). The spike's spike_owned shape (a writable external object with no sharingModel) gets security-owd-unset (error). Its hint reads "declare sharingModel: 'private' (owner + shares; recommended), …" (validate-security-posture.ts:819). Following it yields the shape that security-external-owner-unmapped (PR #27422, 92297384) warns refuses every member edit with 403.
type: Bug

维护者速读

  • 现象: 可写外部对象没写 sharingModel 时,os validate 的报错提示「推荐 private」;照做后,新警告又说成员每次改都会 403。两条提示互相打架。
  • 原因: security-owd-unset 的提示对所有自定义对象一刀切推荐 private,没区分外部对象。
  • 影响: 作者(尤其是 AI)照推荐写出必然 403 的元数据。
  • 最小修法: 对带 external 绑定且 writable: true 的对象,提示改为推荐 public_read_write 加 beforeUpdate 判权,或声明远端会返回的 owner 字段;其它对象不变。
  • 需要您做的: 无。

Task

  • security-owd-unset's hint on an object with external.writable: true names the ways out security-external-owner-unmapped names, and does not recommend private. Severity and message are unchanged; managed objects keep today's hint.
  • Pin: spike_owned gets the external hint, and a managed custom object keeps the old one.
  • os explain security-owd-unset mentions the external case.

Clause-②: no (hint text only) · Metadata-protocol: none.

Activity

  1. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 真挡得住的权限 | none | P③

    Triage: p2 · domain:spec · area:access · 2026-10-29T07:57Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
    Claim: PM loop round 5 2026-10-29T15:13Z
    Session: session_017ihL2c5z3q4h8inrn5kwqV
    Account: marchtian
    Branch: claude/project-thread-6ml89f (this container can push only its designated branch; restarted from origin/main — measured platform exception to claude/issue-<n>-<slug>)
    Worktree: objectstack-issue-27472
    Domain: domain:spec
    Seat: epic:#27261#1 (epic PM; no domain:* seat). Maintainer order, 田騏 2026-10-29T04:21Z: 「影响你的任务进度的,你都统一接管」
    File surface: packages/lint/src/** (the security-owd-unset hint for a writable external object, its explanation, tests), .changeset/** (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: default tier (dispatch-gates --tier: no path-derived mandate), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the hint predates external objects; #27422 made the contradiction visible
    Thread-read: 6491214773
    Premise: holds — on origin/main the security-owd-unset hint recommends sharingModel: 'private' for every custom object (validate-security-posture.ts, the SECURITY_OWD_UNSET arm).
    Serial constraints cleared: none (#27471 touches packages/objectql and the guide only)

  3. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
    os-dev-report

    {
      "issue": 27472,
      "status": "done",
      "branch": "claude/project-thread-6ml89f",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27639",
      "session": "session_017ihL2c5z3q4h8inrn5kwqV",
      "premise_still_valid": true,
      "summary": "security-owd-unset's fix line now names security-external-owner-unmapped's two ways out on a custom object with no sharingModel that the external rule judges, instead of recommending 'private'. The two ways out are public_read_write with a beforeUpdate hook, or declaring owner_id (or created_by when there is no owner_id) and having the driver return it, then 'private'/'public_read'. Severity, message and path are unchanged, and every other object keeps today's hint byte for byte. The wording and the judgement are shared, not copied: validate-external-owner-sharing.ts now exports PUBLIC_READ_WRITE_WAY_OUT, declareOwnershipColumnWayOut(column), externalOwnershipGap(obj, readOnlyDatasources) and readOnlyExternalDatasources(stack), and both rules call them, so the external rule's own findings are byte-identical. os explain security-owd-unset gains a paragraph on the writable-external exception that points at os explain security-external-owner-unmapped. Deliberate scope reading: the swap applies where the external rule fires. An external object that already declares its ownership column, or sits on an allowWrites:false datasource, keeps the 'private' recommendation, which is correct there. This is stated in the PR's Acceptance notes. Draft PR head 4e9b79ca, assignee marchtian; CI was in_progress at report time.",
      "tests": "Tests ran on code tree c4b4bd65 (HEAD~1); head 4e9b79ca adds only the changeset. (1) pnpm --filter '@objectstack/lint^...' build under os-verify-lock: VERDICT command-exit 0. (2) pnpm --filter @objectstack/lint typecheck && pnpm --filter @objectstack/lint test under lock: VERDICT command-exit 0; Test Files 177 passed (177); Tests 7604 passed | 5 skipped (7609); typecheck ran tsc --noEmit plus check:test-typecheck. (3) Targeted vitest on validate-external-owner-sharing / validate-security-posture / rule-explanations / validate-security-posture.runtime-surface: Test Files 4 passed (4), Tests 228 passed (228). The first run of (3) was red on the receiver meta-test (#5017): the new local ownershipGap was not excused. Fixed by adding it to PLUMBING. (4) Ablation, after the fix was committed: scripts/ablation-replace.mjs replaced 'const ownershipGap = externalOwnershipGap(obj, readOnlyDatasources);' with undefined. Anchor x1->x0, blob ce9dd0f8e857->f178c04309b6. Result: VERDICT command-exit 1, Tests 2 failed | 24 passed (26), the failures being 'spike_owned gets this rule's ways out…' and 'names created_by…'. Restored: blob == HEAD (ce9dd0f8e857), git diff HEAD empty. The test imports src/ relatively, so no dist rebuild was needed. NOT MEASURED, declared to CI: packages/cli/test/explain-rule-id.test.ts (unit). The CLI closure is not built here, so the file fails to load on the @objectstack/types entry before any assertion runs. It pins the managed-object hint, which is unchanged.",
      "gates": "node scripts/pm/dispatch-gates.mjs --commands at 4e9b79ca derived 58 commands. All 58 were run, with each exit captured before any pipe. --ran with 'CMD :: exit N' records: '58 derived famil(ies) accounted for — 55 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3)'. 55 exited 0, including check:docs-transcript-drift ('4 declared transcript value(s) across 471 page(s) … equal what the registry derives today'), check:doc-authoring, check:nul-bytes, check:cross-package-test-inputs, check:type-check-coverage and check:type-check-debt. Three were NOT MEASURED with exit 3 (prerequisite): check-plugin-teardown-shape --self-test (a shallow clone cannot reach its pinned control commit), check:dual-build-cjs-loads and check:lean-entry-closure (no full build in this container). CI families: PR check-runs at 4e9b79ca read 18 in_progress and 3 skipped at report time. None are concluded and none are cited as green.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "3 — mcp__github__issue_read x2 (get, get_comments), plus ToolSearch loads only. No MCP write tool was called. pull_request_read was loaded but never called; PR state was read through REST gh api.",
      "api_writes": "2 relay/stamped writes: (1) fleet-write relay pr_create, request fw-20261029T155019Z-b48d06, run 40905592721 success: POST pulls -> #27639, 8179 bytes sent and 8179 stored, identical, assignees marchtian; (2) post-stamped issue comment on #27472 (this report). git push x3 to claude/project-thread-6ml89f (not REST).",
      "line_budget": "6 files, +166 / -21 (187 changed lines): lint src +~75, tests +~70, changeset +16. Far under the 3000 human-merge threshold.",
      "deviations": [
        "Branch claude/project-thread-6ml89f instead of claude/issue-27472-*, and worktree via 'worktree add -B' (PM-set platform exception). origin/claude/project-thread-6ml89f did not exist before the first push.",
        "Comment layout: the PM's stamp line comes first, then 'os-dev-report' alone on line 2. os-dev.md wants os-dev-report as the first line, and the PM's dispatch wants the stamp as the first non-empty line, so both are kept in that order.",
        "The card's literal 'object with external.writable: true' is read as 'the objects security-external-owner-unmapped judges'. Writable external objects that declare their ownership column, or sit on an allowWrites:false datasource, keep the 'private' hint. Stated in the PR Acceptance notes.",
        "The CLI unit pin explain-rule-id.test.ts was not run locally (CLI closure unbuilt); declared to CI."
      ],
      "files_changed": [
        ".changeset/27472-owd-unset-external-hint.md",
        "packages/lint/src/rule-explanations.ts",
        "packages/lint/src/validate-external-owner-sharing.ts",
        "packages/lint/src/validate-external-owner-sharing.test.ts",
        "packages/lint/src/validate-security-posture.ts",
        "packages/lint/src/validate-security-posture.test.ts"
      ],
      "open_questions": [],
      "out_of_scope_findings": []
    }
  4. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
    Landed · 2026-10-29T16:58Z

    PR #27639 merged through the queue as 1346052d31c78b5ce4e76a0da75865440173813e (head 78e68ad4, seat ACCEPT on the PR after an independent full Fable review and a delta review). security-owd-unset no longer recommends 'private' on an external object with no provable owner anchor: a writable binding gets security-external-owner-unmapped's two ways out, and a read-only binding gets 'public_read'. The stale lint header from #27476's review (should-fix 5) is fixed in the same PR. Carried: a read-only binding whose columnMap targets owner_id on a map-ignoring plugin driver keeps the 'private' hint; that rides with #27661.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:specpm:epicpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions