Skip to content

controlled_by_parent: the owner of a detail row cannot update their own row when they only read the master — no equivalent of the "activity owner always has full access" rule (hotclm obligations today, HotCRM tasks next) #25760

Closed

Description

@zhuangjianguo

Premise checked before dispatch (domain:spec seat 2, 2026-10-26): readings and corrections in comment 6409904490 (#25760 (comment)). The goal below is unchanged.

Ruled: 6398566164 · letter 1 · 2026-10-25T13:58:53Z (opt-in sharing.controlledByParent.ownerMayEdit: true; the detail row's owner writes with read on the master; default unchanged; ADR-0055 §3 amendment + spec key + server gate + lint arm)

Filing class: ③ platform gap, filed at destination from an app ruling. Reader: domain:server / area:access triage. Unlabelled, no assignee. Origin: objectstack-ai/hotclm#64, ruled D by the maintainer on 2026-10-25 (ruling comment objectstack-ai/hotclm#64 (comment)) — "this is platform-shaped; file it here and let the app wait", under the maintainer's rule that metadata apps never build platform-shaped capabilities themselves.

Context

hotclm's clm_obligation is a controlled_by_parent detail of clm_contract. An obligation has an owner (the employee accountable for performing it), lives only under a signed contract, and its owner is usually the contract's requester, whose contract edit window closed at submitted. DESIGN.md §04 grants the requester RU(本人负责) on obligations, and the requester permission set carries an RLS update policy owner == current_user.id on clm_obligation.

Measured (17.7.0, REST and browser — hotclm#64 and hotclm#87 finding 5)

PATCH /api/v1/data/clm_obligation/<id> {"status":"done"}   (caller owns the obligation AND the parent contract; parent is `active`)
→ 403 [Security] Access denied: update on 'clm_obligation' requires edit access to its master record
       (master 'clm_contract' not editable by this user (row-level security))
POST /api/v1/security/explain {object: clm_obligation, operation: update} → allowed: true

The refusal is correct under ADR-0055 §3 (packages/plugins/plugin-security/src/security-plugin-cbp-write-gate.ts): editing a detail requires edit on its master, and the detail's own owner RLS is composed AND (ADR-0055 line "composes with the detail's own tenant/owner RLS (all AND-ed)"), so it can only narrow, never grant. An owner policy declared on a controlled_by_parent detail is therefore unreachable for any caller who merely reads the master — for 100% of hotclm's obligation rows, by construction.

The gap

There is no way to declare "the person this detail row is assigned to may update it, with read (not edit) on the master". That is a standard pattern, not an app quirk:

  • Salesforce, which ADR-0055 frames as the model it closes gaps against, keeps "the owner of an activity always has full access to it" even under Activities OWD = Controlled by Parent.
  • HotCRM's tasks / activities hang under accounts and are assigned to reps who can read, not edit, the account — the same wall.
  • Nothing in the write gate today consults the detail row's owner (grep of the gate for owner / opt-in: none).

What is asked

A per-object, opt-in declaration on a controlled_by_parent detail (shape is the platform's to choose), e.g. one of:

  1. sharing: { controlledByParent: { ownerMayEdit: true } } — the detail row's owner passes the write gate with read on the master; everyone else still needs master edit. Closest to the Salesforce rule. ← RULED (6398566164).
  2. controlledByParent: { writeRequires: 'read' | 'edit' } (default edit, preserving ADR-0055) — with 'read', the detail's own object permission + RLS policies decide the write, as they do for a private object. (not chosen)

Either keeps ADR-0055 §1/§2 (one required master_detail, read derived from the master) untouched and changes only the §3 write question for objects that opt in. os lint should keep flagging an owner-based update policy on a detail that has not opted in, since today that policy is dead code.

App side after it ships: hotclm adds one line on clm_obligation and one sentence in DESIGN.md §04; no other change.

Dedupe

search_issues on this repo for controlled_by_parent + owner/assigned semantics: 42 results, all about enforcing or regressing the master-edit gate (#25012, #25044, #25049, #25068, #25095, #24962, #24986, #23093, #22721, #19853 …); 0 about letting a detail row's owner write their own row. ADR docs/adr/0055-master-detail-controlled-by-parent.md has no owner exemption.

Not asked

  • No change to the default: existing details keep requiring master edit.
  • No app-side workaround in hotclm (per its AGENTS.md → Platform gaps); hotclm#64 is Blocked-by this card and 我负责的履约 stays read-only for requesters until it lands.

Activity

  1. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    Contributor

    Triage: first touch — enhancement · security · priority:p2 · domain:spec · area:access · needs-user-decision · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-25T09:58Z

    ⛔ Not a claim, ⛔ not a dispatch.

    • For the maintainer: this widens who may write a controlled_by_parent detail row. That is a new protocol key on a security boundary, plus an ADR-0055 §3 amendment, so it is on the human floor. hotclm's ruling D only sent the gap here.
    • Triage leaning: option 1, ownerMayEdit: true. Only the detail row's own owner passes with read on the master. Option 2 (writeRequires: 'read') opens the write to everyone whose object permission and RLS allow it, which is a wider door. Both are opt-in, and the default (master edit) is unchanged.
    • Workload: ADR amendment S, spec key S, the write gate in server M, and the lint arm that keeps flagging an owner policy on a detail that has not opted in.

    Generated by Claude Code

  2. os-elon-musk commented on Oct 25, 2026

    @os-elon-musk
    Collaborator

    Ruling: batch #14 item 2 · letter 1 · maintainer 「同意」 2026-10-25T13:58:53Z

    Option 1 = a per-object opt-in on a controlled_by_parent detail, sharing.controlledByParent.ownerMayEdit: true. With it, the detail row's own owner passes the write gate with read on the master; everyone else still needs master edit. The default is unchanged: existing details keep requiring master edit. Option 2 (writeRequires: 'read') is not chosen: it opens the write to everyone the detail's own object permission and RLS admit, a wider door than the need.

    Scope the ruling sets: ADR-0055 §3 amendment recording the opt-in (Tier H); the spec key; the write gate in plugin-security reading the detail row's owner only when the object opts in; a lint arm that keeps flagging an owner-based update policy on a detail that has not opted in (today that policy is dead code). App side after it ships: hotclm adds one line on clm_obligation and one sentence in DESIGN.md §04; hotclm#64 stays Blocked-by this card until then.

    Workload: ADR S, spec key S, server gate M, lint S. Security face: landing needs a GOVERNED_APPROVER. 裁不派:this comment records the ruling; dispatch is the domain:spec and domain:server lanes'. Brief: 总监决裁 批#14.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    Contributor

    Premise check before dispatch · domain:spec seat 2 · 2026-10-26T06:40Z

    Read-only premise reading on origin/main d5f4c2a2a (dispatch-runbook 派发前置). The goal of this card is unchanged; only the readings below move the landing point or the method, and the dispatch order carries them.

    holds, one correction: object.zod.ts has NO top-level sharing key today (only sharingModel :~3024, retired externalSharingModel, publicSharing). The ruled spelling therefore adds a new sharing strictObject key; ownerMayEdit has 0 hits repo-wide.
    Suggested route (yours to change, report why): exemption in packages/plugins/plugin-security/src/security-plugin-cbp-write-gate.ts before the chain call at :~467 (detail row already read at :~366), applying only when the detail opts in AND the caller holds master READ; denial points :~807/:~852. State in the card/PR whether insert and delete are in scope (the ruling says update). New lint arm in packages/lint/src/validate-security-posture.ts (beside the cbp helpers :~306-372) flagging an owner-predicate update policy on a cbp detail that has not opted in; explanation in rule-explanations.ts. ADR amendment after the existing ## Amendment (2026-09-07, #11082) pattern in docs/adr/0055-master-detail-controlled-by-parent.md (§3 rule line :~61).

    Each count above is a re-measurable reading, not a ruling; the dev re-measures before relying on it.

  4. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 1 · 2026-10-26T06:53Z
    Session: session_01RyMY3prh9GRva1Tt37Di3i
    Account: zhuangjianguo
    Branch: claude/project-thread-h906g2 (card thread; the platform assigns the thread's branch and it can push only that one, written as measured instead of claude/issue-25760-<slug>)
    Worktree: objectstack-issue-25760
    Domain: domain:spec
    Seat: domain:spec#2
    File surface: packages/spec/src/data/object.zod.ts (+ object.form.ts, generated spec artefacts), packages/plugins/plugin-security/src/security-plugin-cbp-write-gate.ts (+ cbp-relation), packages/lint/src/validate-security-posture.ts, rule-explanations.ts, docs/adr/0055-master-detail-controlled-by-parent.md, packages/plugins/plugin-security/src/security-plugin.ts (CbpRelation field + host method), packages/lint/src/index.ts (barrel export), packages/spec/liveness/object.json, regenerated docs and spec artefacts, four one-line pin moves and three new test files, one changeset; amended by the seat after the thread reported them as forced by gates or wiring; amended again for the CI patch round at 3e4b01d5: packages/metadata-core/src/object-schema-fls-references.ts (classify the new sharing key), packages/platform-objects/src/apps/translations/{en,zh-CN,ja-JP,es-ES}.metadata-forms.generated.ts and their two leaf-count pins; then packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts (the /meta/types property-count pin, object 47 → 48) and, ruled by the seat, an explicit timeout on one load-bound test in packages/core/src/security/auth-gate.test.ts (no code change in core) (stop on breach; explain in the report)
    Container & model: L, mode:thread, model: CONTRACT_REVIEW_TIER — MANDATORY: docs/adr/0055-master-detail-controlled-by-parent.md ⇢ 'docs/adr/**', effort: xhigh
    Clause-②: yes
    Metadata-protocol: added
    Responsibility: n/a — not a defect card
    Thread-read: 6409904490
    Premise: corrected 6409904490
    Serial constraints cleared: none (no open PR touches the cbp gate, object.zod.ts or ADR-0055)

  5. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 25760,
      "status": "done",
      "branch": "claude/project-thread-h906g2",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/26005",
      "session": "session_01XT1qCAHDq5w7zUftxENNeE",
      "premise_still_valid": true,
      "summary": "Ruling 6398566164 letter 1 implemented: ObjectSchema gains the closed block sharing.controlledByParent.ownerMayEdit (refused at parse off a controlled_by_parent detail; one exported reader controlledByParentOwnerMayEdit). On an opted-in detail, assertControlledByParentWrite admits a by-id UPDATE by the caller whose userId is the stored row's owner_id (the registry's injected owner column, confirmed present by the relation resolver) when the caller can READ the master row under its own context; insert, delete, predicate (multi) update, delegated writes, unowned rows and a body re-pointing the master keep the master-EDIT rule, as the PR body states (ruling names update; insert/delete out of scope, not foreclosed). Lint arm security-controlled-by-parent-owner-policy-without-opt-in (warning) flags an update/all policy keyed FIELD == current_user.id on a detail without the opt-in, with os explain. ADR-0055 amended in place (governed, Tier H). Head dece711427afd6fc5c4692fb903aa4f7605c7117; draft PR #26005 assigned to zhuangjianguo; body carries Clause-②: yes (widening), Metadata-protocol: added, Ruling-ref, and the 维护者速读(草稿) section.",
      "claim_surface": "Beyond the Claim's named files, each forced by a gate or the wiring, none a design widening: packages/plugins/plugin-security/src/security-plugin.ts (CbpRelation.ownerMayEdit field, the relation host's new report method — the relation's type lives there); packages/lint/src/index.ts (the barrel export rule-id-barrel-exports.test.ts requires); packages/spec/liveness/object.json (check:liveness requires every authorable key declared); regenerated content/docs/references/**, docs/audits/** strictness counts, packages/spec/{api-surface,authorable-surface,export-origins,liveness/state-counts} (check:generated); four existing pins moved by one line each (two compose-stacks config-object pins list sharing; the lint barrel pin wants the rule id on one line; the #5017 meta-guard table gained the arm's reads); three new test files.",
      "tests": "Dependency closure built first (pnpm --filter '@objectstack/plugin-security^...' --filter '@objectstack/lint^...' build, exit 0; spec rebuilt before check:exported-any). Under the verify lock (OS_VERIFY_LOCK_SLOT=issue-25760): pnpm --filter @objectstack/spec test → 'Test Files 722 passed | 1 skipped (723) · Tests 21899 passed | 1 skipped | 1 todo (21901)', typecheck exit 0; pnpm --filter @objectstack/plugin-security test → 'Test Files 224 passed (224) · Tests 4700 passed | 45 skipped (4745)', typecheck exit 0 (after typing the warn mock; first run had 1 TS error in the new rig file); pnpm --filter @objectstack/lint test → 'Test Files 164 passed (164) · Tests 7363 passed (7363)', typecheck exit 0 (after putting the rule id on one line; first run red on rule-id-barrel-exports). New suites alone: spec src/data/controlled-by-parent-owner-may-edit.test.ts 14/14; plugin-security src/controlled-by-parent-owner-may-edit.test.ts 17/17 over the real SecurityPlugin + ObjectQL + SqlDriver + SharingService; lint validate-security-posture-owner-may-edit + validate-security-posture + rule-explanations 206/206. Ablation (rel.ownerMayEdit short-circuited to false in security-plugin-cbp-write-gate.ts, run, file restored with git checkout): 'Tests 4 failed | 13 passed (17)' — red: the owner's admitted update, the delegated case's own-pass control, the body naming the row's own contract, checkControlledByParentWrite allow; every refusal pin stayed green. test:repo (vitest repo project) not run locally — CI's Test Core.",
      "gates": "Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths): 104 families before the regenerated docs widened the change set, 124 after; every command run with its exit code captured before any pipe (status=$? then printed), 122 exit 0. Highlights: check:generated 0 (after regenerating the 5 artefacts it proved stale), check:liveness 0, check:api-surface 0, check:authorable-surface 0, check:docs 0, check:exported-any 0 (after the spec rebuild it demands), check-adr-0087-registration 0, check-changeset-no-major 0, check-empty-changeset 0, check:adr-anchors 0, check-adr-symbol-anchors 0, check:nul-bytes 0, check:platform-checklist 0, check:engine-double-contract 0, check:error-status-conformance 0, check:doc-security-posture 0, check-plugin-teardown-shape --self-test 0 (after fetching its pinned fixture commit 621a4876 into the shallow clone). NOT MEASURED locally, both a recorded exit 3 (prerequisite: the whole farm's dist/, not built here): pnpm check:dual-build-cjs-loads, pnpm check:i18n — CI check-runs are their verdict. --ran reconciliation: 'Run reconciliation — 124 derived, 122 run, 2 NOT-MEASURED, 0 UNRUN' exit 0. CI families on PR #26005: in_progress at report time (PR just opened); the check-runs on the PR are the record, not re-run locally.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "0 — none",
      "api_writes": "2 REST writes, both through the fleet relay (POST /repos/objectstack-ai/objectstack/dispatches as objectstack-fleet[bot]): pr_create → #26005 (run 40294937965, read-back identical, assignees zhuangjianguo, no labels: a changeset is present so skip-changeset does not apply); comment → this os-dev-report on #25760 via scripts/pm/post-stamped.mjs. Plus git push to origin/claude/project-thread-h906g2 twice (WIP 0fb96f51, final dece7114).",
      "deviations": [
        "Two derived gates NOT MEASURED locally (exit 3, full-farm dist prerequisite): check:dual-build-cjs-loads, check:i18n — declared, CI runs them.",
        "One single-file vitest run (lint src/rule-id-barrel-exports.test.ts, 4/4) was run outside the lock while the lock held the full suites; the full lint suite was re-run under the lock afterwards and is the evidence cited.",
        "The gate runner ran unlocked (check:* families) concurrently with the locked suites, per the lock's own sibling-work note; every exit code is the command's own."
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "class: c · reach: docs · content/docs/permissions/{authorization,sharing-rules,rls}.mdx do not mention sharing.controlledByParent.ownerMayEdit (the generated reference page content/docs/references/data/object.mdx does, through the key's describe) · dedupe: cbp ownerMayEdit permissions docs · carrier: PR body (Acceptance notes) · noted, not filed",
        "class: b · reach: named producer · the owner-column half of the opt-in (ownership other than 'user') is reported at runtime by the relation resolver rather than refused at parse, because resolveInjectedSystemColumns imports object.zod.ts (the cycle the attached-block refusal stops at too) · dedupe: ownerMayEdit owner column parse refusal · carrier: PR body + ADR amendment · noted, not filed",
        "class: c · reach: shallow clone · scripts/check-plugin-teardown-shape.mjs --self-test pins its positive control to commit 621a4876, so a shallow seat clone answers exit 3 until that commit is fetched · dedupe: teardown-shape self-test shallow clone · carrier: this report · noted, not filed"
      ]
    }

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 25760,
      "status": "done",
      "branch": "claude/project-thread-h906g2",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/26005",
      "session": "session_01XT1qCAHDq5w7zUftxENNeE",
      "premise_still_valid": true,
      "summary": "Ruling 6398566164 letter 1 implemented: ObjectSchema gains the closed block sharing.controlledByParent.ownerMayEdit (refused at parse off a controlled_by_parent detail; one exported reader controlledByParentOwnerMayEdit). On an opted-in detail, assertControlledByParentWrite admits a by-id UPDATE by the caller whose userId is the stored row's owner_id (the registry's injected owner column, confirmed present by the relation resolver) when the caller can READ the master row under its own context; insert, delete, predicate (multi) update, delegated writes, unowned rows and a body re-pointing the master keep the master-EDIT rule, as the PR body states (ruling names update; insert/delete out of scope, not foreclosed). Lint arm security-controlled-by-parent-owner-policy-without-opt-in (warning) flags an update/all policy keyed FIELD == current_user.id on a detail without the opt-in, with os explain. ADR-0055 amended in place (governed, Tier H). Head e0883a097f7103fc2598249bf4377909a140eb01 (dece7114 + the CI-fix commit 3e4b01d5 + merge of origin/main 39ca5878 + the deferred regeneration e0883a09); draft PR #26005 assigned to zhuangjianguo; body carries Clause-②: yes (widening), Metadata-protocol: added, Ruling-ref, and the 维护者速读(草稿) section. Patch round after CI on dece7114 went red: metadata-core's OBJECT_REFERENCE_POSITIONS now classifies sharing (keep); the platform-objects translation bundles regenerated, their six new leaves authored in zh-CN, ja-JP and es-ES (the #19403 regime refuses an untranslated metadata-form leaf) with the two leaf-count pins moved; then main merged in (no rebase, one conflict: the lifecycle pin, 683 = main's 680 + the three sharing labels) and content/docs/references/data/object.mdx regenerated on the merged tree.",
      "claim_surface": "Beyond the Claim's named files, each forced by a gate or the wiring, none a design widening: packages/plugins/plugin-security/src/security-plugin.ts (CbpRelation.ownerMayEdit field, the relation host's new report method — the relation's type lives there); packages/lint/src/index.ts (the barrel export rule-id-barrel-exports.test.ts requires); packages/spec/liveness/object.json (check:liveness requires every authorable key declared); regenerated content/docs/references/**, docs/audits/** strictness counts, packages/spec/{api-surface,authorable-surface,export-origins,liveness/state-counts} (check:generated); four existing pins moved by one line each (two compose-stacks config-object pins list sharing; the lint barrel pin wants the rule id on one line; the #5017 meta-guard table gained the arm's reads); three new test files. Patch round, each forced by a CI red on dece7114: packages/metadata-core/src/object-schema-fls-references.ts (the key classification its guard demands); packages/platform-objects/src/apps/translations/{en,zh-CN,ja-JP,es-ES}.metadata-forms.generated.ts (regenerated by the repo tooling, six leaves hand-translated) and the two pins object-collapsed-sections-echo-decisions.test.ts (107→113, advanced 96→102) and object-lifecycle-panel-echo-decisions.test.ts (679→683 after the merge). Merge commit 3bccc3b6 and regeneration commit e0883a09 (object.mdx only).",
      "tests": "Dependency closure built first (pnpm --filter '@objectstack/plugin-security^...' --filter '@objectstack/lint^...' build, exit 0; spec rebuilt before check:exported-any). Under the verify lock (OS_VERIFY_LOCK_SLOT=issue-25760): pnpm --filter @objectstack/spec test → 'Test Files 722 passed | 1 skipped (723) · Tests 21899 passed | 1 skipped | 1 todo (21901)', typecheck exit 0; pnpm --filter @objectstack/plugin-security test → 'Test Files 224 passed (224) · Tests 4700 passed | 45 skipped (4745)', typecheck exit 0 (after typing the warn mock; first run had 1 TS error in the new rig file); pnpm --filter @objectstack/lint test → 'Test Files 164 passed (164) · Tests 7363 passed (7363)', typecheck exit 0 (after putting the rule id on one line; first run red on rule-id-barrel-exports). New suites alone: spec src/data/controlled-by-parent-owner-may-edit.test.ts 14/14; plugin-security src/controlled-by-parent-owner-may-edit.test.ts 17/17 over the real SecurityPlugin + ObjectQL + SqlDriver + SharingService; lint validate-security-posture-owner-may-edit + validate-security-posture + rule-explanations 206/206. Ablation (rel.ownerMayEdit short-circuited to false in security-plugin-cbp-write-gate.ts, run, file restored with git checkout): 'Tests 4 failed | 13 passed (17)' — red: the owner's admitted update, the delegated case's own-pass control, the body naming the row's own contract, checkControlledByParentWrite allow; every refusal pin stayed green. test:repo (vitest repo project) not run locally — CI's Test Core. Patch round, under the lock on the merged tree after rebuilding the closure (turbo build exit 0): pnpm --filter @objectstack/metadata-core test → 'Test Files 19 passed (19) · Tests 800 passed', typecheck 0; pnpm --filter @objectstack/platform-objects test → 'Test Files 95 passed (95)' (3 files red before the translations and pin moves), typecheck 0; pnpm --filter @objectstack/core test → 'Test Files 119 passed (119)' (CI's Test Core 5/8 had @objectstack/core#test exit 1 on dece7114; the failing test's name lies beyond the 5000-line tail the GitHub log API serves and the blob host is denied from the seat, so the merged head's CI is its verdict); spec test + typecheck, lint test + typecheck, plugin-security test + typecheck all exit 0 again on e0883a09; check:generated, check:docs, check:i18n exit 0.",
      "gates": "Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) on head e0883a09: 124 families (unchanged by the patch and the merge); every command run under the verify lock with its exit code captured before any pipe (status=$? then printed); 124 exit 0 — including pnpm check:dual-build-cjs-loads and pnpm check:i18n, NOT MEASURED in the first round and measured now that the closure build covers them. --ran reconciliation: 'Run reconciliation — 124 derived, 124 run, 0 NOT-MEASURED, 0 UNRUN' exit 0. CI on dece7114: Test Core (8/8) red (metadata-core guard), Type Check · consumer gates red (check:i18n drift), Test Core (1/8) red (whole leg died in 80 s, consistent with platform-objects' #19403 pins), Test Core (5/8) red (@objectstack/core#test, name unreadable, see tests); the other families green. CI on 3e4b01d5 never ran (PR in conflict with main). CI on e0883a09: pending at report time; the check-runs on the PR are the record.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "0 writes. 22 read-only calls, all to root-cause the CI reds on dece7114: mcp__github__pull_request_read ×3 (get_check_runs), mcp__github__get_job_logs ×15, mcp__github__actions_list ×1 (run artifacts), mcp__github__actions_get ×3 (job steps, artifact download URLs — the blob host is denied, nothing downloaded). No MCP write tool was called.",
      "api_writes": "3 REST writes, all through the fleet relay (POST /repos/objectstack-ai/objectstack/dispatches as objectstack-fleet[bot]): pr_create → #26005 (run 40294937965, read-back identical, assignees zhuangjianguo, no labels: a changeset is present so skip-changeset does not apply); comment 6411992806 → the first os-dev-report on #25760 via scripts/pm/post-stamped.mjs; this refreshed os-dev-report via the same tool. Plus git push to origin/claude/project-thread-h906g2 four times (WIP 0fb96f51, dece7114, 3e4b01d5, e0883a09).",
      "deviations": [
        "Claim surface widened in the patch round by CI reds, not by design: metadata-core's key classification, the platform-objects bundles with six hand-translated leaves and two count pins — stated in the PR body's Verification and here.",
        "CI's Test Core (5/8) failure of @objectstack/core#test on dece7114 could not be named: the GitHub log API serves the last 5000 lines only and the full-log/artifact blob host is denied from the seat; core's test project is green locally on the full diff and on the merged tree. If it reds again on e0883a09 the name must come from the fresh run.",
        "One single-file vitest run (lint src/rule-id-barrel-exports.test.ts, 4/4) was run outside the lock while the lock held the full suites; the full lint suite was re-run under the lock afterwards and is the evidence cited.",
        "The first-round gate runner ran unlocked concurrently with the locked suites; the final sweep on e0883a09 ran under the lock after every build. Every exit code is the command's own."
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "class: c · reach: docs · content/docs/permissions/{authorization,sharing-rules,rls}.mdx do not mention sharing.controlledByParent.ownerMayEdit (the generated reference page content/docs/references/data/object.mdx does, through the key's describe) · dedupe: cbp ownerMayEdit permissions docs · carrier: PR body (Acceptance notes) · noted, not filed",
        "class: b · reach: named producer · the owner-column half of the opt-in (ownership other than 'user') is reported at runtime by the relation resolver rather than refused at parse, because resolveInjectedSystemColumns imports object.zod.ts (the cycle the attached-block refusal stops at too) · dedupe: ownerMayEdit owner column parse refusal · carrier: PR body + ADR amendment · noted, not filed",
        "class: c · reach: shallow clone · scripts/check-plugin-teardown-shape.mjs --self-test pins its positive control to commit 621a4876, so a shallow seat clone answers exit 3 until that commit is fetched · dedupe: teardown-shape self-test shallow clone · carrier: this report · noted, not filed",
        "class: c · reach: seat tooling · mcp__github__get_job_logs returns at most the last 5000 lines whatever tail_lines says, and the full-log and artifact blob host is denied by the seat proxy, so a failure early in a Test Core shard is unreadable from a seat; the shard plan can be recomputed locally with scripts/ci/select-shard-packages.sh + partition-test-shards.mjs but CI's affected set differs · dedupe: job log tail cap · carrier: this report · noted, not filed"
      ]
    }

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 25760,
      "status": "done",
      "branch": "claude/project-thread-h906g2",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/26005",
      "session": "session_01XT1qCAHDq5w7zUftxENNeE",
      "premise_still_valid": true,
      "summary": "Ruling 6398566164 letter 1 implemented: ObjectSchema gains the closed block sharing.controlledByParent.ownerMayEdit (refused at parse off a controlled_by_parent detail; one exported reader controlledByParentOwnerMayEdit). On an opted-in detail, assertControlledByParentWrite admits a by-id UPDATE by the caller whose userId is the stored row's owner_id (the registry's injected owner column, confirmed present by the relation resolver) when the caller can READ the master row under its own context; insert, delete, predicate (multi) update, delegated writes, unowned rows and a body re-pointing the master keep the master-EDIT rule, as the PR body states (ruling names update; insert/delete out of scope, not foreclosed). Lint arm security-controlled-by-parent-owner-policy-without-opt-in (warning) flags an update/all policy keyed FIELD == current_user.id on a detail without the opt-in, with os explain. ADR-0055 amended in place (governed, Tier H). Head 07f1f087aabd8c394f99dc1b7197bcc167bab2ee = dece7114 + 3e4b01d5 (CI fixes: metadata-core classification, platform-objects bundles and pins) + 3bccc3b6 (merge of origin/main 39ca5878, no rebase, one conflict) + e0883a09 (deferred object.mdx regeneration) + 118132da (metadata-protocol's served-schema pin 47 → 48) + 07f1f087 (an explicit 30 s timeout on core's auth-gate BOUNDARY test, seat ruling A). Draft PR #26005 assigned to zhuangjianguo; body carries Clause-②: yes (widening), Metadata-protocol: added, Ruling-ref, and the 维护者速读(草稿) section. CI on 07f1f087 is green: 48 check-runs completed, every non-skipped one success.",
      "claim_surface": "Beyond the Claim's named files, each forced by a gate or the wiring, none a design widening: packages/plugins/plugin-security/src/security-plugin.ts (CbpRelation.ownerMayEdit field, the relation host's new report method — the relation's type lives there); packages/lint/src/index.ts (the barrel export rule-id-barrel-exports.test.ts requires); packages/spec/liveness/object.json (check:liveness requires every authorable key declared); regenerated content/docs/references/**, docs/audits/** strictness counts, packages/spec/{api-surface,authorable-surface,export-origins,liveness/state-counts} (check:generated); four existing pins moved by one line each (two compose-stacks config-object pins list sharing; the lint barrel pin wants the rule id on one line; the #5017 meta-guard table gained the arm's reads); three new test files. Patch rounds, each forced by a CI red: packages/metadata-core/src/object-schema-fls-references.ts (the key classification its guard demands); packages/platform-objects/src/apps/translations/{en,zh-CN,ja-JP,es-ES}.metadata-forms.generated.ts (regenerated by the repo tooling, six leaves hand-translated) and the two pins object-collapsed-sections-echo-decisions.test.ts (107→113, advanced 96→102) and object-lifecycle-panel-echo-decisions.test.ts (679→683 after the merge); packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts (the /meta/types served property-count control, object 47→48 with its history line). BREACH, stated: packages/core/src/security/auth-gate.test.ts is outside the claim surface and outside this diff's inputs; its BOUNDARY test (about 133k paths × 6 methods through the allowlist predicate) ran 5410 ms on Test Core shard 5 three runs in a row (dece7114, e0883a09, 118132da), over vitest's 5 s default, 1289 ms here; the seat ruled A (an explicit 30 s timeout on that one test with a comment naming the measurement; no skip, no quarantine, no change to what it asserts) and claim 6410093517 is amended for it. Merge commit 3bccc3b6 and regeneration commit e0883a09 (object.mdx only).",
      "tests": "Dependency closure built first (pnpm --filter '@objectstack/plugin-security^...' --filter '@objectstack/lint^...' build, exit 0; spec rebuilt before check:exported-any). Under the verify lock (OS_VERIFY_LOCK_SLOT=issue-25760): pnpm --filter @objectstack/spec test → 'Test Files 722 passed | 1 skipped (723) · Tests 21899 passed | 1 skipped | 1 todo (21901)', typecheck exit 0; pnpm --filter @objectstack/plugin-security test → 'Test Files 224 passed (224) · Tests 4700 passed | 45 skipped (4745)', typecheck exit 0 (after typing the warn mock; first run had 1 TS error in the new rig file); pnpm --filter @objectstack/lint test → 'Test Files 164 passed (164) · Tests 7363 passed (7363)', typecheck exit 0 (after putting the rule id on one line; first run red on rule-id-barrel-exports). New suites alone: spec src/data/controlled-by-parent-owner-may-edit.test.ts 14/14; plugin-security src/controlled-by-parent-owner-may-edit.test.ts 17/17 over the real SecurityPlugin + ObjectQL + SqlDriver + SharingService; lint validate-security-posture-owner-may-edit + validate-security-posture + rule-explanations 206/206. Ablation (rel.ownerMayEdit short-circuited to false in security-plugin-cbp-write-gate.ts, run, file restored with git checkout): 'Tests 4 failed | 13 passed (17)' — red: the owner's admitted update, the delegated case's own-pass control, the body naming the row's own contract, checkControlledByParentWrite allow; every refusal pin stayed green. Patch rounds, under the lock on the merged tree after rebuilding the closure (turbo build exit 0): pnpm --filter @objectstack/metadata-core test → 'Test Files 19 passed (19) · Tests 800 passed', typecheck 0; pnpm --filter @objectstack/platform-objects test → 'Test Files 95 passed (95)' (3 files red before the translations and pin moves), typecheck 0; spec, lint, plugin-security test + typecheck all exit 0 again on e0883a09; check:generated, check:docs, check:i18n exit 0. On 118132da: pnpm --filter @objectstack/metadata-protocol test → 266 files green (1 failed of 21557 before the pin moved), typecheck 0. On 07f1f087: core src/security/auth-gate.test.ts alone 'Tests 33 passed (33)'; pnpm --filter @objectstack/core test → 'Test Files 119 passed (119) · Tests 2768 passed (2768)' (55.8 s); pnpm --filter @objectstack/core test:repo → 'Test Files 6 passed (6) · Tests 83 passed (83)'; typecheck 0. CI is the record for the shard-5 load: the BOUNDARY test's 5410 ms came from one early read of job 121550325337's log (run on 118132da); on 07f1f087 Test Core (5/8) is success (job 121560048225).",
      "gates": "Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) on head 07f1f087: 126 families (124 on e0883a09; 125 on 118132da, adding pnpm check:durability-log-level; 126 on 07f1f087, adding pnpm check:kernel-hook-pairs); every command run under the verify lock with its exit code captured before any pipe (status=$? then printed); 126 exit 0 on 07f1f087 (the e0883a09 sweep was 124/124 exit 0 too). The 07f1f087 sweep: families 1–13 in the first runner (12:05–12:06Z), families 14–126 in the second, which held the verify lock 50m57s with 0 queued and waited 0 s (the lock excludes other locked runs only; the figures are shared-box seconds). --ran reconciliation on 07f1f087: 'Run reconciliation — 126 derived, 126 run, 0 NOT-MEASURED, 0 UNRUN' exit 0. CI history on the PR: dece7114 red on Test Core (1/8), (5/8), (8/8) and Type Check · consumer gates; 3e4b01d5 never ran (conflict with main); e0883a09 red on Test Core (5/8) (core, the BOUNDARY timeout) and (7/8) (metadata-protocol pin); 118132da red on Test Core (5/8) only; 07f1f087 (run 40319281239 and its siblings): Test Core (1/8)–(8/8) success, Build Core, Build Docs, Build UI Deps, Build & E2E UI, Type Check UI, Type Check · workspace / consumer gates / source gates / debt ledger, Lint · ESLint / repo & tooling gates / docs & code guards / CI, release & engine gates / PM dispatch-gates / the scoped lanes, Check Changeset, Check PR Size, Governed Surface Queue Guard, Spec property liveness, the claim and single-writer guards — all success; Lint UI, Test UI, Dogfood and Temporal lanes skipped by path filter.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "0 writes. 28 read-only calls, all to read CI on the PR and root-cause its reds: mcp__github__pull_request_read ×5 (get_check_runs), mcp__github__get_job_logs ×18, mcp__github__actions_list ×2 (run artifacts, workflow runs), mcp__github__actions_get ×3 (job steps, artifact download URLs — the blob host is denied, nothing downloaded). No MCP write tool was called.",
      "api_writes": "4 REST writes, all through the fleet relay (POST /repos/objectstack-ai/objectstack/dispatches as objectstack-fleet[bot]): pr_create → #26005 (run 40294937965, read-back identical, assignees zhuangjianguo, no labels: a changeset is present so skip-changeset does not apply); comment 6411992806 (the first os-dev-report on #25760) and comment 6414737027 (the refreshed report on e0883a09, posted seconds before the seat's hold request arrived) via scripts/pm/post-stamped.mjs; this report via the same tool. Plus git push to origin/claude/project-thread-h906g2 six times (WIP 0fb96f51, dece7114, 3e4b01d5, e0883a09, 118132da, 07f1f087). The PR body was written once at pr_create and never patched.",
      "deviations": [
        "Claim surface widened by CI reds, not by design: metadata-core's key classification, the platform-objects bundles with six hand-translated leaves and two count pins, metadata-protocol's served-schema pin — stated in the PR body's Verification and here.",
        "Claim surface BREACHED on packages/core/src/security/auth-gate.test.ts under seat ruling A: an explicit 30 s timeout on one BOUNDARY test that the loaded Test Core shard 5 refused at vitest's 5 s default three runs in a row (5410 ms there, 1289 ms here). Test-only, no skip, no quarantine, no assertion changed; the file is otherwise untouched. The commit message and the in-file comment name the measurement.",
        "The shard-5 failure could not be named from the dece7114 or e0883a09 runs (the GitHub log API serves the last 5000 lines only and the full-log/artifact blob host is denied from the seat); it was named from one early read of the 118132da run's shard-5 log, scheduled about 12 minutes after the push, before the tail moved past it.",
        "The 07f1f087 gate sweep ran in two runners on the same head: the first was reaped after family 13 (a detached background process on the seat does not outlive the turn), the second ran families 14–126; families 1–13 keep the first runner's exit codes. Every exit code is the command's own, captured before any pipe.",
        "One single-file vitest run (lint src/rule-id-barrel-exports.test.ts, 4/4) was run outside the lock while the lock held the full suites; the full lint suite was re-run under the lock afterwards and is the evidence cited.",
        "The first-round gate runner ran unlocked concurrently with the locked suites; every later sweep ran under the lock after every build."
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "class: c · reach: docs · content/docs/permissions/{authorization,sharing-rules,rls}.mdx do not mention sharing.controlledByParent.ownerMayEdit (the generated reference page content/docs/references/data/object.mdx does, through the key's describe) · dedupe: cbp ownerMayEdit permissions docs · carrier: PR body (Acceptance notes) · noted, not filed",
        "class: b · reach: named producer · the owner-column half of the opt-in (ownership other than 'user') is reported at runtime by the relation resolver rather than refused at parse, because resolveInjectedSystemColumns imports object.zod.ts (the cycle the attached-block refusal stops at too) · dedupe: ownerMayEdit owner column parse refusal · carrier: PR body + ADR amendment · noted, not filed",
        "class: c · reach: named producer · core's auth-gate BOUNDARY test enumerates about 133k paths × 6 methods through the allowlist predicate (1.3 s idle, 5.4 s on a Test Core shard that carries core beside spec's test and test:repo); the 30 s timeout is a ceiling on load, a cheaper enumeration (a smaller corpus or a sampled walk) is core's owner's call · dedupe: auth-gate boundary enumeration cost · carrier: this report + commit 07f1f087 · noted, not filed",
        "class: c · reach: shallow clone · scripts/check-plugin-teardown-shape.mjs --self-test pins its positive control to commit 621a4876, so a shallow seat clone answers exit 3 until that commit is fetched · dedupe: teardown-shape self-test shallow clone · carrier: this report · noted, not filed",
        "class: c · reach: seat tooling · mcp__github__get_job_logs returns at most the last 5000 lines whatever tail_lines says, and the full-log and artifact blob host is denied by the seat proxy, so a failure early in a Test Core shard is readable only by one early read while the job is in progress; the shard plan can be recomputed locally with scripts/ci/select-shard-packages.sh + partition-test-shards.mjs but CI's affected set differs · dedupe: job log tail cap · carrier: this report · noted, not filed",
        "class: c · reach: seat tooling · a detached background process (nohup … &) started from a seat turn is reaped a few minutes after the turn ends, so a long gate sweep must run as a harness-tracked background command or inside the turn · dedupe: seat background process reaped · carrier: this report · noted, not filed"
      ]
    }

    Generated by Claude Code

  8. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    Contributor

    ACCEPT → PR #26005 comment 6417897066 (domain:spec seat 2); contract review PASS comment 6417851199 on head 07f1f08. Tier H (docs/adr/**): waits for an APPROVED review from os-zhuang or hotlong on this head; the PR stays draft until then. Fixes #25760 closes this card on merge.

  9. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    Contributor

    Landed · domain:spec seat 2

    reading value
    PR #26005 merged 2026-10-26T15:38:02Z, merge commit 5e113b395a8098aa4260a50424f969ce352fbff8 (after os-zhuang's APPROVED review 5654114202)
    origin/main 6b9355299, contains the merge commit (git merge-base --is-ancestor)

    State: Fixes #25760 closed this card as completed; pm:dispatched removed in this stroke.

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 guardsdomain:specenhancementNew feature or requestpriority:p2Medium: important, M3security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions