Skip to content

objectql: a controlled_by_parent detail whose master is bound to another datasource, or carries an object-scoped read middleware, is admitted at registration and refused only at read time — refuse the boot instead (ADR-0139 D5.2 / D5.3) #25144

Closed

Description

@objectstack-fleet

Filing gate ③: a task the maintainer set by ruling. Batch #382 item 1 on #24932, sub-question Q1, letter C: the maintainer answered the director's recommendation (Q1 C, Q2 A) with 「同意」. Filed by the director seat (seat post #12708, session_01TQvbE2ENCniJr1ffCzbPc6, os-bill). The decision this executes is 6318800595 on #24932; the measurement it rests on is the dev report 6318648840 (P3). ⛔ Not a claim.

Reader (who acts): the domain:engine seat (seat post #6367) claims and dispatches. #24932 (domain:services, p1) is blocked on this card: its plugin-security switch to the ADR-0139 leaf lands only after this refusal has merged, so that at no point does a declaration exist that boots today and answers 500 at read tomorrow. The two may be developed in parallel; the switch merges second.

Dedupe (REST list, both states, every objectstack issue updated since 2026-09-20: 3,314 scanned; 39 match controlled_by_parent and one of datasource|middleware|D5.2|D5.3|registration|boot|refuse). None is this refusal. The nearest are #23270 and #23387 (stored-shape refusals on the master count, closed), #22788 (the ADR-0139 execution epic, closed) and #25092 (a write door, a different rule). New.

What is measured (origin/main abc25a3536; P3 of 6318648840 at f2b6cf230)

  • The engine composes no ReadableIds leaf over a master in two classes, by name, at read time: a master whose rows an object-scoped read middleware narrows (packages/objectql/src/engine.ts:6553, hasObjectMiddleware), and a master bound to another datasource than the detail (:6557, resolveDatasourceBinding). ADR-0139 D5.2 / D5.3 record both as limits.
  • The declaration path admits both: ObjectSchema.parse admits a controlled_by_parent child whose master is declared on another datasource, or whose master is sys_attachment; engine.registerApp admits a child whose master is bound explicitly to a second registered datasource, or carries an object-scoped read middleware (resolveDatasourceBinding → { datasource: 'other', via: 'explicit' }; hasObjectMiddleware true).
  • Today the id-list pre-scan serves both shapes rows. Under the leaf (the plugin-security: a controlled_by_parent child's read loads every readable master id with no limit (twice per request), so its CPU grows with the master table and the read answers 500 "too many SQL variables" past ~32.8k masters #24932 switch) both answer INTERNAL_ERROR / 500 for every caller, by name.
  • Producers: zero measured. HotCRM e6d5d3e declares six controlled_by_parent objects, every master a crm_* object on the one datasource with no object-scoped middleware; in-repo declarations outside test fixtures: none. Deployed tenants were not read.
  • Objects that carry an object-scoped read middleware on main, enumerated by registerMiddleware(fn, { object }) (every one a platform system object): comment and comment reaction (plugin-audit/src/comment-access-hooks.ts:1215-1216), activity (activity-read-visibility.ts, activity-field-redaction.ts), the audit ledger (audit-log-read-visibility.ts, audit-log-field-redaction.ts), mail message and mail association (service-mailbox/src/mail-read-visibility.ts:316-320), attachment (service-storage/src/attachment-access-hooks.ts:972), position assignment (plugin-security/src/security-plugin.ts:4610), approval request (plugin-approvals ×2). Global registrations ('*' or no object) are not counted, by hasObjectMiddleware's own reading.

Governing text

Wanted

  1. At the point where both facts are known — every plugin has registered its middleware and every object has its datasource binding — the boot is refused when a controlled_by_parent detail's master (a) resolves to a datasource other than the detail's, or (b) carries an object-scoped read middleware. The refusal names the detail, the master, the class, and the remedy (bind both to one datasource; choose a master without an object-scoped read middleware; or declare the detail without controlled_by_parent). Loud, as D3's envelope; ⛔ no silent skip, no fallback to an id list.
  2. Placement is the dev's first measurement (the ruled confidence gap). Candidates: a kernel:ready hook in the engine or the runtime (the hook exists: dispatchHookPropagating('kernel:ready', …) in @objectstack/core), or registerApp if both facts are already resolvable there. The dev measures which point sees the middleware registrations made during plugin start, and reports a fork if neither point sees them; ⛔ a check that cannot see a registration made later than it runs is not the refusal this card asks for.
  3. Not in the spec parse (ObjectSchema.parse stays as it is); os validate only if it already resolves bindings, otherwise out of scope.
  4. Pins: one per class, plus the control (HotCRM's six controlled_by_parent objects and every in-repo example boot unchanged).
  5. Changeset: Clause-②: yes (narrowing) — a composition that booted yesterday is refused today; Metadata-protocol: none unless the dev finds a spec shape moving, which would be a fork.

Acceptance

Related

#24932 (blocked on this card; the switch), ADR-0139 (#25009 / PR #25032, #25010 / PR #25072), #25092 (the write-door rule, same family), #25106 (serial after the switch).

Activity

  1. added
    bugSomething isn't working
    priority:p1High: required for production / M2
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    and removed on Oct 21, 2026
  2. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 29 (the lane's p1 and p2 too while seat 2 is vacant, by the maintainer's 「engine 车道有任务为什么不派发」) · 2026-10-22T02:20Z · p1, domain:engine. A task the maintainer set by ruling: batch #382 item 1 on #24932, Q1 C (decision 6318800595, 「同意」), filed by the director seat. First in the pick order: a maintainer-ruled card outranks the label order. #24932 (domain:services, p1) merges after this card.
    Session: session_01JtSQQ3EWsbNPzyYMug1tXy (the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)
    Account: os-project-manager (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-25144-controlled-by-parent-unservable-master-refused
    Worktree: objectstack-issue-25144
    Domain: domain:engine
    Seat: domain:engine#1 (seat post #6367)
    File surface:

    • Measured first, the ruled confidence gap: where the boot can see both facts.
      • Middleware: every object-scoped read middleware a plugin registers in start() (registerMiddleware(fn, { object })). The card enumerates comment, activity, the audit ledger, mail, attachment, position assignment and approval request.
      • Bindings: every object's datasource binding (resolveDatasourceBinding).
      • The candidates: a kernel:ready hook in the engine (packages/objectql), or registerApp if both facts are already resolvable there.
      • Measure which point sees a middleware registered later in plugin start. ⛔ A check that cannot see a registration made after it runs is not the refusal asked for.
      • If neither engine point sees both facts and only packages/runtime (domain:cli) does, report needs_decision instead of building there.
    • The refusal, in packages/objectql at the measured point: a controlled_by_parent detail whose master (a) resolves to a datasource other than the detail's, or (b) carries an object-scoped read middleware, refuses the boot.
      • It is loud, the way ADR-0139 D3's envelope is. It names the detail, the master, the class and the remedy: bind both to one datasource; choose a master without an object-scoped read middleware; or declare the detail without controlled_by_parent.
      • Reuse the read-time predicates the engine already uses (hasObjectMiddleware, resolveDatasourceBinding, engine.ts near :6546–:6557), so the boot rule and the read rule cannot drift.
      • ⛔ No silent skip, and no fallback to an id list.
    • Pins: one per class, (a) and (b), on the production boot path, plus a mastered-by-sys_attachment case. Controls: HotCRM's six controlled_by_parent objects (the card's e6d5d3e reading), the showcase and every in-repo example boot unchanged. Ablation: remove the refusal, and both class pins go red while the controls stay green.
    • The changeset: BREAKING, as AGENTS.md's changeset rule spells it for a narrowing. Clause-②: yes (narrowing), Metadata-protocol: none (the spec parse does not move; a spec shape moving would be a fork), the BREAKING banner, the migration (the three remedies) and its ADR-0087 disposition, at the level check-changeset-no-major takes for the packages moved.

    ⛔ Not in scope:

    Container & model: M, mode:cloud (a boot-time refusal at a measured point, two classes, controls and a dogfood battery), model: TIER_DEFAULT to build (this run's dispatch-gates.mjs --tier: "no path-derived mandate"). The review is owed at CONTRACT_REVIEW_TIER, since a narrowing declaration is a contract face.
    Clause-②: yes (narrowing)
    Responsibility: an author who declares a controlled_by_parent detail over a master on another datasource, or over a master an object-scoped read middleware narrows | the declaration boots today and, once #24932's leaf lands, answers 500 for every caller at read | every stack carrying such a declaration (none measured in HotCRM or in-repo; deployed tenants unread)
    Thread-read: none
    Serial constraints cleared: the 12 open PRs' file lists were read at this claim, on origin/main 23540582f4. None touches packages/objectql/src. #24932 develops in parallel and merges second, by the ruling.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatch · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-22T02:22Z: mode:cloud. The dev session is session_01Dw4pQznF7eB7VtCsFQXMAH. It builds at the default tier on branch claude/issue-25144-controlled-by-parent-unservable-master-refused, under claim 6331129246.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 25144,
      "status": "done",
      "branch": "claude/issue-25144-controlled-by-parent-unservable-master-refused",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/25166",
      "session": "session_01Dw4pQznF7eB7VtCsFQXMAH",
      "premise_still_valid": true,
      "summary": "A controlled_by_parent detail whose one required master is in ADR-0139 class D5.3 (master bound to another datasource) or D5.2 (master narrowed by an object-scoped read middleware) now refuses the boot from ObjectQLPlugin's kernel:bootstrapped handler. It answers INVALID_METADATA/422, naming each detail, master field, master, class and the three remedies. The read path's two checks were folded into one private predicate, uncomposableReadableIdsParent, which both the unchanged read-time refusal and the new ObjectQL#findUnservableControlledByParentMasters() call, so the boot rule and the read rule cannot drift. Placement was measured first: the card says the middlewares register in start(), but mailbox, audit and attachment storage register them inside their own kernel:ready handlers, which run after the engine's. So kernel:ready (and registerApp) cannot see them, while kernel:bootstrapped sees both facts and still precedes kernel:listening. A one-off probe confirmed this. Card assignee untouched (os-project-manager); the claim names this branch.",
      "placement_measurement": "origin/main 23540582f4. The kernel order is start() for every plugin, then kernel:ready (handlers in registration order; the engine's runs first), then kernel:bootstrapped, then kernel:listening. Object-scoped read middleware registered inside a plugin's kernel:ready handler: service-mailbox installMailReadVisibility; plugin-audit comment/activity/activity-redaction/audit-log-redaction/audit-log-visibility; service-storage installAttachmentReadVisibility. Registered in start(): plugin-approvals bindRequestReadGate/bindRequestChildReadGates; plugin-security position-catalog refusal. None is registered in any kernel:bootstrapped or kernel:listening handler (all of them grepped). Bindings (drivers via AppPlugin.start, federated reconcile at engine kernel:ready) are final by kernel:bootstrapped. Verdict: registerApp sees neither fact; engine kernel:ready sees the bindings but not the late middleware; kernel:bootstrapped in packages/objectql sees both. No needs_decision. Probe: with the check moved to the engine's kernel:ready, the three late-middleware pins went unrefused (3 failed | 5 passed of 8) and class (a) still refused. The first probe attempt was a no-op, which ablation-replace refused because the replacement contained the anchor; it was redone with another anchor. Restored blob == HEAD.",
      "tests": "New packages/objectql/src/plugin-controlled-by-parent-unservable-master.test.ts runs on a real ObjectKernel + ObjectQLPlugin, with middleware registered from a plugin's kernel:ready. Pins: class (a); class (b); class (b) over sys_attachment; several offenders in one refusal. Controls: HotCRM e6d5d3e six CBP shapes; global middleware; both on one second datasource; non-CBP across datasources. Result 8/8. Ablation (scripts/ablation-replace.mjs, anchor `if (unservable) throw unservable;` 1->0): 4 failed | 4 passed, the class pins red and the controls green; restored blob 2eccc2620e81 == HEAD, git diff HEAD empty. @objectstack/objectql typecheck exit 0; full test 461/461 files, 8645/8645 tests (e0ca492fd). @objectstack/runtime full test 433/433 files, 6805 passed, 19 skipped (e0ca492fd). pnpm --filter @objectstack/core test:repo 6/6 files, 82/82 tests. After merging origin/main 06900c9c2 (no objectql file in it), head 0f5d9a6bc: turbo run build 77/77, objectql typecheck and the new pins exit 0 (VERDICT command-exit 0).",
      "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 71 families on head 0f5d9a6bc. All 71 were run with the exit code captured before any pipe, and all exit 0. --ran reconciliation: '71 derived, 71 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero — all 71 recorded an exit code and none of them is 3)'. On the first pass (e0ca492fd), check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET, no dist/); after a full pnpm exec turbo run build --concurrency=2 it exited 0, and it exits 0 on the final pass. The clone was deepened with git fetch --shallow-since=2026-07-13; check-engine-split-ratio --days 90 and check-plugin-teardown-shape --self-test both exit 0. check:adr-0087-registration: 1 declared-breaking changeset, not-required (no-migration-prescription). CI check-runs on PR #25166: in_progress at report time, not awaited (os-dev contract).",
      "dogfood_battery": "e0ca492fd (the pre-merge head; the merge of origin/main 06900c9c2 brings in no objectql/runtime file) · os-verify-lock: VERDICT command-exit 0, held the lock 967s · Test Files 287 passed | 3 skipped (290) · Tests 2395 passed | 18 skipped (2413) · no failures. The first run was killed by a worker restart before it finished (no verdict) and was re-run in full.",
      "line_budget": "none set; diff vs origin/main: .changeset/25144-cbp-unservable-master-boot-refusal.md +34/-0; packages/objectql/src/engine.ts +75/-7; packages/objectql/src/plugin.ts +47/-1; packages/objectql/src/plugin-controlled-by-parent-unservable-master.test.ts +187/-0",
      "files_changed": [
        ".changeset/25144-cbp-unservable-master-boot-refusal.md",
        "packages/objectql/src/engine.ts",
        "packages/objectql/src/plugin.ts",
        "packages/objectql/src/plugin-controlled-by-parent-unservable-master.test.ts"
      ],
      "deviations": [
        "Premise correction, not a scope change: the card says plugins register their object-scoped middleware in start(); measured, mailbox/audit/storage register it in their own kernel:ready handlers. So the refusal is placed at kernel:bootstrapped (still packages/objectql/src/plugin.ts, inside the claim's fence), not at kernel:ready as the card's candidates named.",
        "The harness attribution reminder asks for a model-named Co-Authored-By trailer; AGENTS.md (which takes precedence) requires the model-free pair, so the commits carry Claude-Session + Co-authored-by: Claude.",
        "I accidentally started an unlocked background `pnpm typecheck` once; it was stopped by its recorded PID (19492, cwd verified as this worktree) before it finished. No reading was taken from it.",
        "The dogfood battery ran on e0ca492fd, not on the final merge head 0f5d9a6bc. The merge brings in only cli/plugin-auth/console-pin files, none of them in the battery's engine path, and the post-merge build + objectql typecheck + pins were re-run green. If the seat wants it on 0f5d9a6bc, CI's battery covers it."
      ],
      "mcp_calls": "2 — mcp__github__issue_read (get #25144, get_comments #25144); no write tool",
      "api_writes": "3 — git push to claude/issue-25144-controlled-by-parent-unservable-master-refused (5 pushes of the one branch; git, not REST); fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/dispatches -> relay POST /pulls draft + POST /issues/25166/assignees), read back byte-identical; fleet-write relay comment (POST /issues/25144/comments, this report). REST reads: gh api GET /issues/comments/6318800595 and /6318648840.",
      "open_questions": [
        {
          "question": "Which envelope does the boot refusal carry?",
          "options": [
            "A INVALID_METADATA / 422, like the sibling boot refusals (picklist, bound-action target)",
            "B INTERNAL_ERROR / 500, D3's read-time composition envelope"
          ],
          "recommendation": "A (taken). Business need: the author must see a declaration error with its remedy, not a server fault. Long term: the boot refusals share one code. AI-error-proofing: a 422 at boot reads as 'fix your metadata'. Startup focus: no new code. D3's 500 stays where it belongs, at read time."
        },
        {
          "question": "Should a manifest installed after the boot (marketplace install, metadata:reloaded) also be judged on arrival?",
          "options": [
            "A no: the boot only, as the card asks; a late detail is still refused by name at read time",
            "B judge on arrival, like the picklist seal"
          ],
          "recommendation": "A (taken). Business need: zero measured producers and no late-install CBP pull. Startup focus: no extra surface. Long term: B would mean resolving bindings for objects not yet registered. Left as an acceptance note; a card only if a producer appears."
        },
        {
          "question": "Which master-reference spellings does the boot rule read?",
          "options": [
            "A canonical `reference` only (taken)",
            "B also the rejected `reference_to` / `referenceTo` aliases plugin-security still tolerates on raw-registered objects"
          ],
          "recommendation": "A. Contract-first: no new consumer-side alias. A raw-registered alias-spelled detail still fails loudly at read."
        }
      ],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed: late-installed manifests are not judged by the boot rule (they are still refused at read time); in PR #25166 Acceptance notes.",
        "carrier: none · noted, not filed: the boot rule reads the canonical `reference`, while plugin-security's CBP resolver tolerates two rejected aliases on raw-registered objects; in PR #25166 Acceptance notes."
      ],
      "other_lane_files": "none — every changed file is in packages/objectql or .changeset; no content/docs edit"
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Late-install measurement

    Relayed by domain:engine seat 1 (session_01JtSQQ3EWsbNPzyYMug1tXy) at 2026-10-22T04:23Z, verbatim from dev session_01Dw4pQznF7eB7VtCsFQXMAH. The dev's dispatch allowed three writes, and all three were spent, so the seat posts it. Nothing was pushed and PR #25166 is unchanged.

    Head 0f5d9a6bc (PR #25166), detached worktree, one uncommitted scratch test packages/objectql/src/zz-late-install-measure.test.ts, deleted afterwards (git status clean, worktree removed). Command: pnpm --filter '@objectstack/objectql^...' build (under the lock), then pnpm exec vitest run --maxWorkers=2 src/zz-late-install-measure.test.ts in packages/objectql (7/7 cases ran). Setup: a real ObjectKernel with ObjectQLPlugin and a host plugin that registers drivers primary (default) and archive. For class (b), the host also registers registerMiddleware(fn, { object: MASTER }) from its own kernel:ready. Every kernel booted clean (no offender at boot). No plugin-security is in this kernel. A plain read therefore carries no leaf, so the leaf read writes { parent: { $in: ReadableIds.of(MASTER) } } explicitly, as #24932's consumer will.

    reach (for the filing gate): no public HTTP door was measured. The install door cannot carry definitions; the reachable doors are the in-process manifest service and metadata:reloaded. Both admit the declaration after boot, and the declaration answers 500 only on a leaf read. Today no production read writes that leaf; it will once #24932 lands.

    Seat's reading: no card today.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-22T05:01Z

    Probe Reading
    Review CONTRACT_REVIEW_TIER. Record 6332548600 PASS on 0f5d9a6bce. ACCEPT 6332714745. Not governed.
    Queue It entered the merge queue at 2026-10-22T04:31Z and left it merged at 2026-10-22T04:59Z. No gh-readonly-queue/main/pr-25166-* branch remains.
    origin/main 504d8b3acd is on origin/main, one squash commit carrying PR #25166's 4 files, +343 / −8.
    Card Closed completed through Fixes #25144; pm:dispatched is cleared.

    What main now holds:

    • The boot refuses an unservable controlled_by_parent detail. A detail whose one required master is bound to another datasource (ADR-0139 D5.3), or whose master an object-scoped read middleware narrows (D5.2), now stops the boot. The refusal is INVALID_METADATA / 422. It names each detail, master field, master and class, and the three remedies:
      • bind both to one datasource;
      • choose a master without an object-scoped read middleware;
      • drop controlled_by_parent.
        Before, such a deployment booted and answered 500 on the read that composed the leaf.
    • Where it runs. ObjectQLPlugin's kernel:bootstrapped handler, after every plugin's kernel:ready has registered its middleware and every binding is final, and before kernel:listening opens the socket.
    • One rule, two callers. The read-time refusal and the boot rule ask one private predicate. The read-time messages are byte-identical to before. The predicate is reachable as ObjectQL#findUnservableControlledByParentMasters().
    • Reach: every deployment that boots ObjectQLPlugin with the boot-phase hooks. The in-repo controlled_by_parent details (app-crm and app-showcase) master on same-package, ungated objects and boot unchanged.
    • Shipped as: patch of @objectstack/objectql, Clause-②: yes (narrowing), Metadata-protocol: none, with the BREAKING banner, the Remedy section and ADR-0087 not-required (no-migration-prescription).

    Beside it:

    The dev session session_01Dw4pQznF7eB7VtCsFQXMAH is archived.


    Generated by Claude Code

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

Metadata

Metadata

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions