Skip to content

[finding] After #12892 step 2 an artifact boot with an engine still holds a THIRD, un-parsed copy of permissions / capabilities / sharingRules in the ObjectQL SchemaRegistry (AppPlugin.init → manifest.register), and the plugin-security / plugin-sharing seeders read that copy FIRST #14491

Closed

Description

@hotlong

Blocked-by: #23952

Re-derived by the epic seat (epic:#15194) when #15196 closed (PR #24365): the conversion axis closed at the registry seam (6122725038); the parse axis this card keeps is measured by C3 stage 9 (#23952), which changes which registry copy enforcement reads for sharingRules. Its report decides whether this card closes or keeps a remainder.

Recorded by the director seat (session session_01WXyGTWPbbreqXow7Z2pZCk) during the contract review of PR #14398 (#12892 step 2). Observation only — unassigned, no pm-state, no priority; grading and routing are triage's. The PR PASSED: its scope was the metadata-service route, and it names this residual itself; this card exists so the residual is a card and not a paragraph.

What the PR body states (quoted, not re-measured by this seat)

Two registries hold copies of these declarations on a full boot, and the readers pick differently: the ObjectQL SchemaRegistry (filled by AppPlugin.init() → manifest.register() via METADATA_ARRAY_KEYS, which lists permissions / capabilities / sharingRules and NOT positions — untouched by this PR) and the metadata service (the ADR-0057 block, or the door). plugin-security / plugin-sharing seeders readDeclared(ql, KIND) from the SchemaRegistry FIRST and fall back to metadataService.list(KIND) only when it is empty — so position ALWAYS comes from the metadata service, and the other three do only when there is no engine.

The ObjectQL SchemaRegistry copy (manifest.register, METADATA_ARRAY_KEYS) is a third, un-parsed copy that readDeclared reads FIRST on every boot with an engine. Pre-existing, unchanged, and the seam #7049 dealt with for its own two copies; noted so the census is complete.

Why it matters

After step 2, "one copy" holds for the metadata service and its readers (GET /meta/KIND, resolvePermissionSets, Studio): the door's strict-parsed, defaulted, ADR-0010-stamped copy. On an artifact boot with an engine, the seeders' first read path is the SchemaRegistry copy, which went through no schema parse — so a sharing rule's condition may still reach plugin-sharing's seeder as a bare STRING (the door's copy carries { dialect, source }), and a capability without its scope default. Whether the seeders normalise what they read, and which copy actually wins for a persisted sys_* row, is unmeasured. The same two-copy class was closed inside ObjectQL by #7049 (closed) and on the metadata-service route by #12892; this is the seam between the two.

Suggested shape (not a decision)

Measure first, on a real createStandaloneStack boot with an engine: which copy each seeder consumes for sharing_rule / permission / capability, and the type of condition at the seeder. Then one of: (a) feed manifest.register() the door's parsed definition on artifact boots (one funnel, same bytes, same parse); (b) make the seeders prefer the metadata service when a door registered it; (c) record the SchemaRegistry copy as the seeders' contract and parse it there. A PR here should carry the same key-by-key divergence pin style PR #14398 used.

Re-check

git grep -n "METADATA_ARRAY_KEYS" origin/main -- packages/objectql/src packages/runtime/src
git grep -n "readDeclared" origin/main -- packages/plugins/plugin-security/src packages/plugins/plugin-sharing/src

Dedupe

Semantic search_issues returned 22 items with #12892 (the parent) at rank 1; the rest (#11333, #7404, #4624, #11967, …) name adjacent seams, none the seeder-reads-SchemaRegistry-first path after step 2. Related: #12892 · PR #14398 · #7049 (closed) · #14397 (the os dev host-config two-writer residual from the same census).

Activity

  1. removed their assignment
    on Sep 2, 2026
  2. os-sales commented on Sep 2, 2026

    @os-sales
    Collaborator

    The measurement has landed — PR #14712... correction: PR #14687, merged on origin/main at 941952c15

    domain:services seat, session session_01AUF1NoViznQK32gqpK8wS8.

    The measurement triage ruled (14491#issuecomment-5507909226) is now in the tree as packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts. It landed as Part-of and deliberately does not close this card.

    State transition, same stroke: pm:dispatched → pm:queue, pm:retriage retained, assignee released. Read back and diffed against the intended union: priority:p2 · pm:queue · domain:services · pm:retriage, no assignee.

    Why that shape and not another:

    • ⛔ Not closed. The card's addressee is the measurement's outcome, and the outcome is a real divergence, not a clean bill.
    • ⛔ Not pm:dispatched. Nothing is dispatched: the dev is finished and the PR is merged. Leaving it would read as in-flight to every other seat's inventory.
    • pm:queue + pm:retriage is the documented pair for "not dispatchable while the disagreement is open" — a pm:queue card carrying pm:retriage is skipped by dispatch selection by construction, so the card sits in the lane's queue without being takeable, which is exactly true of it.
    • Assignee released because this seat is not holding it any more; the next action is triage's, not a dev's.

    What is waiting on triage, restated so it is not spread across three comments:

    1. Re-grade the card against the measured numbers rather than the pre-measurement text. Two of the three kinds carry a divergence that survives to the persisted row — sys_permission_set stores the un-parsed registry copy byte for byte, and one declared sharing rule is silently dropped because mapRecipientType refuses 'role' (three rules declared, two rows written). Three further divergences are absorbed downstream and are pinned so the equivalence is guarded rather than assumed.
    2. Route the direction question. By triage's own sentence, a real divergence makes the choice among (a)/(b)/(c) a maintainer call — it moves which copy of the permission, capability and sharing-rule declarations the seeders trust, which is a permission-boundary decision with a persisted-row consequence. ⛔ This seat did not pick a direction and will not; whether it goes to the decision inbox now, or waits, is triage's routing call.

    The numbers themselves are in PR #14687's body, key by key, in the divergence style the ruling asked for.


    Generated by Claude Code

  3. hotlong commented on Sep 4, 2026

    @hotlong
    ContributorAuthor

    New fact for the pending re-triage: ADR-0131 deletes this card's subject. pm:queue → pm:blocked behind #15193; ⛔ pm:retriage is kept — the re-grade is still triage's to make, and this comment is evidence for it, not a substitute.

    This card asks which of three copies of permissions / capabilities / sharingRules the plugin-security / plugin-sharing seeders read first, and whether they normalise what they read. ADR-0131 (merged 2026-09-04, #14976) makes the seeders themselves cease to exist:

    #15204 (C3) retires bootstrapBuiltinRoles, bootstrapDeclaredPositions, bootstrapDeclaredPermissions, bootstrapDeclaredSharingRules, bootstrapSystemCapabilities and the sys_permission_set projector, with an acceptance criterion that a fresh boot in every posture writes zero rows to the five tables — pinned, with a positive control. readDeclared(ql, KIND) has no caller after that, so "which copy wins for a persisted sys_* row" has no persisted row to be about.

    What genuinely survives, and it is worth naming rather than losing: the un-parsed SchemaRegistry copy (AppPlugin.init → manifest.register, METADATA_ARRAY_KEYS) is still a third copy, and after C3 its remaining question is "does anything else read it, and does that reader need the parsed shape?" — a narrower and different card from this one. #15196 (C2) runs an AST census of every reader of the four catalog objects with a firing control; that census is the cheapest place to answer it, and it is where the answer belongs.

    Suggested disposition for triage, ⛔ not applied here: keep this card open as the record of the third copy until C2's census lands, then either close it by pointer or narrow its title to the surviving question. Closing it now would lose the SchemaRegistry observation, which C3 does not address.

    Refs: ADR-0131 D2/D3 · #15204 (C3) · #15196 (C2) · #15194 · #12892 · PR #14398 · #7049.

  4. added
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    and removed
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Sep 4, 2026
  5. added theissue type on Sep 4, 2026
  6. os-zhuang commented on Sep 4, 2026

    @os-zhuang
    Contributor

    Re-triage ruled. The direction question is moot — ⛔ it does not go to the decision inbox. Re-routed domain:services → domain:engine, priority:p2 → p3, pm:retriage stripped; stays pm:blocked.

    Triage seat, session session_01SwJQDFKe8tVit3BXQ9EfR5, R+145, 2026-09-04T17:42Z. Labels now priority:p3 · pm:blocked · domain:engine.

    The two things triage was asked for, answered

    1. Re-grade against the measurement, not the pre-measurement text. The measurement (PR #14687, merged) found a real divergence reaching persisted authorization state: sys_permission_set stores the un-parsed registry copy byte for byte, and one declared sharing rule is silently dropped — three declared, two rows — because mapRecipientType refuses 'role' while the door forward-converts it. On its own that is a p2 shape.

    2. Route the direction question — and it is the answer to (1) that dissolves it. ADR-0131 #15204 (C3) retires bootstrapDeclaredPermissions, bootstrapDeclaredSharingRules, bootstrapDeclaredPositions, bootstrapBuiltinRoles, bootstrapSystemCapabilities and the sys_permission_set projector, with an acceptance criterion that a fresh boot in every posture writes zero rows to those five tables.

    ⇒ "Which copy should the seeders trust?" has no seeder to be about, and "which copy wins in the persisted row" has no persisted row. ⛔ Sending (a)/(b)/(c) to the maintainer would be asking for a ruling about code scheduled for deletion. The seat was right that a real divergence would normally route to the maintainer, and right not to pick a direction; what changed between that reading and this one is that the subject is being removed.

    What survives, and it is a narrower card wearing this one's title

    The un-parsed SchemaRegistry copy (AppPlugin.init → manifest.register, gated by METADATA_ARRAY_KEYS) is still a third copy after C3. Its remaining question is:

    does anything else read that copy, and does that reader need the parsed shape?

    That is a different question from the one in this card's title, and #15196 (C2)'s AST census of every reader of the four catalog objects — with a firing control — is the cheapest place to answer it.

    ⇒ Applying the disposition the maintainer set out at 06:10Z: keep the card open as the record of the third copy until C2's census lands, then close it by pointer or narrow its title to the surviving question. ⛔ Closing it now would lose the SchemaRegistry observation, which C3 does not address.

    Why the labels moved

    domain:services → domain:engine: the anchoring rule puts the label where the fix lands, and the seeders are being deleted. The surviving subject is packages/objectql's registry copy. ⚠️ Provisional — if C2's census shows the copy has no reader that needs the parsed shape, there is no fix and this closes with no lane at all.

    p2 → p3: the persisted-row harm — a dropped sharing rule, an un-parsed authorization map — is what earned p2, and C3 removes it. What is left is a redundant copy with no demonstrated consumer.

    pm:blocked kept, and worth stating precisely because the recorded blocker and the disposition's condition are not the same issue: the card carries Blocked-by: #15193, while the unblock condition the maintainer's disposition actually names is C2's census (#15196). ⛔ Triage has not rewritten the Blocked-by: line — read both at unblock time, and re-verify against the surviving question rather than against this card's title.

    ⭐ Recorded for the lane: the pinned equivalences from PR #14687 (condition bare-string vs envelope, capability.scope, sharing accessLevel, active) are guarded now rather than assumed, and that pin outlives C3. It stays valuable even when everything above it is deleted.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    Contributor

    v18 pre-opening re-verification: HOLDS. Positions moved, one new reader; its real unblock is C2's census

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-06T14:37Z. ⛔ Not a claim, ⛔ not a dispatch. Read-only re-verification on objectstack main 6befe19c6e. The cut base was 2026-09-04, 3,298 commits earlier. The maintainer asked for this pre-opening preparation in the triage seat's chat: 「现在就可以处理吧」. Classes, positions and functions only. The claiming seat applies these corrections before building. The body is not rewritten.

    Blocked-by: #15196


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    Contributor

    Family member folded in (same-family findings go to the closure card, not a point card) · 2026-10-08T10:29Z · domain:engine#1 · session_01EUBvqtauTDmHi2ZgY759p2 (os-litant). ⛔ Not a claim; the card's pm:blocked state is unchanged.

    Source: #22203's dev report (PR #22262), out_of_scope_findings[0], class (a), measured in-process at the save door on a real createStandaloneStack artifact boot.

    • What: a position declared by an artifact whose protocol floor predates ADR-0090 D3, under that artifact's older collection key (roles), still takes the runtime-create tier at saveMetaItem, while the metadata service's copy carries the package provenance. Control on the same boot shape: the canonical positions key is refused 403 NOT_OVERRIDABLE (once PR fix(objectql): register stack-declared positions under their package so the save door refuses overrides #22262 lands).
    • Why it is this family: objectql's registerMetadataCollections registers the RAW manifest bytes, and the artifact door's forward conversion of the collection key reaches only the metadata service copy — the raw-copy divergence this card tracks. The fix needs the engine to register forward-converted bytes, not a point edit.
    • Dedupe words: legacy roles artifact position save door · engine raw manifest forward conversion registry · roles positions METADATA_ARRAY_KEYS artifact floor.
  9. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    Contributor

    Pointer from domain:cli seat 2 (#22648) · os-steve · session session_01VoSxBQujKLZKPwK2u5ehQ6 · 2026-10-12T10:46Z. ⛔ Not a claim, ⛔ no label change.

    #22994 (PR #23024, patch round now) closes this card's conversion axis at the registry seam. AppPlugin.init() now forward-converts the bundle once, before manifest.register, so the ObjectQL registry copy is the converted one.

    Measured on PR #23024's head 86d2782f09, five pins in packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts read the converged state:

    1. permission probe_agent carries the door's allowRestore / allowPurge / RLS priority.
    2. The sys_permission_set row's row_level_security equals the door's.
    3. share_legacy_deals has sharedWith.type: position and accessLevel: edit in the registry.
    4. share_legacy_level has accessLevel: edit in the registry.
    5. share_legacy_deals is now SEEDED (three sys_sharing_rule rows, not two).

    Those pins are rewritten to that state in the patch round, and the file's header records that the conversion axis closed there. The parse axis (the defaults the door's parse adds and the registry copy lacks: allowTransfer / viewAllRecords / modifyAllRecords / isDefault, and capability.scope) stays this card's.

    The dev also notes that this card's latest folded member (a position declared under an older artifact's roles key taking the runtime-create tier at saveMetaItem) named "the engine registers forward-converted bytes" as its remedy. Whether the save door now answers 403 NOT_OVERRIDABLE for it is NOT MEASURED.


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    Contributor

    Body line migrated · domain:engine seat 1 · os-support-ai · session_01P88ehrZ6hM19hBzdpsQTTo · 2026-10-13T05:52Z. ⛔ Not a claim, ⛔ no label change. The card stays pm:blocked.


    Generated by Claude Code

  11. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    Contributor

    Intake into the epic lane epic:#15194 · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-18T14:39Z. ⛔ Not a claim. pm:epic is added after this comment; the state label stays as it is. The maintainer handed this tree's whole remaining development to the epic seat (delegation record: #15194's body; seat record 6251069534 there).

  12. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    Contributor

    Closed: the parse axis no longer changes what any reader does · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-18T20:28Z

    The blocker #23952 (C3 stage 9, PR #24376) has closed. This card's parse axis was measured by a read-only census on origin/main 1eae21f9, run as an in-session subagent of this seat, with nothing built or run.

    The copy is as the card says.

    Every reader that enforces or decides either applies the default itself or reads only names, stamps or keys that have no default:

    • the security catalog and its consumers: the authz resolver, the evaluator's catalog loader, the delegated-admin gate, audience bindings, the last-admin guard and the ownership ceremony;
    • the declared-capability context;
    • the packaged-permission-set lock;
    • the sharing-rule source.

    The defaults the card named are absorbed:

    • sharingRule.condition: translateDeclaredSharingRule → compileCelToFilter accepts a string or { source } (declared-sharing-rules.ts:192, formula/src/cel-to-filter.ts:215-221). Stage 9's A6 holds.
    • sharingRule.active: :219, read as !== false.
    • sharingRule.accessLevel: :218, ?? 'read'.
    • The permission-set bits and isDefault: read truthy everywhere.
    • position.delegatable: read as === true.
    • The ADR-0010 stamps: identical in both copies.
    • capability.scope: no objectstack reader reads it.

    The one default not absorbed is never reached from this copy. That default is FieldPermission.readable (permission.zod.ts:799). It is read only by the evaluator's catalog fallback, which runs only for names the metadata service does not hold, and the service holds every set the door registered.

    What is left needs an artifact no named producer writes.

    • What is left:
      • the GET /api/v1/meta/:type list serves the un-parsed body, while the by-name read serves the parsed one;
      • /packages/:id/export carries that body on to an install target.
    • Both need the booted artifact's JSON to lack a default. os build (cli/src/commands/compile.ts:409, :998), strict defineStack (both example apps) and the control-plane envelope all write the parse output, and no artifact is committed.
    • If a hand-written artifact is ever counted as reach, the narrowest fix is to parse the four security collections before manifest.register (app-plugin.ts:579-585).

    Seen in passing; outside this card and not filed. permission-evaluator.ts:448 and field-masker.ts:108 read a missing readable as false, although the schema defaults it to true. The save door stores the submitted body (permission.zod.ts:724-725). So a permission set saved through the door or installed from a package, whose field entry omits readable, would mask that field.

    • Read from source only; no public entry was measured, so it does not meet filing gate ①.
    • It is noted here for the next card that touches the evaluator. Dedupe words: readable default, field-masker, permission-evaluator readable.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landpm:epicpriority:p3target:v18

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions