Repository navigation
fix(metadata-protocol)!: the by-name /meta read refuses an object of a disabled package (404 OBJECT_PACKAGE_DISABLED), as the list and the data plane do - #24208
Conversation
… disabled package getMetaItem and getMetaItemLayered answer 404 OBJECT_PACKAGE_DISABLED, from the data plane's own constructor, for an object whose owning package is disabled. The save's destructive-change check reads the private shared body, so it still sees that object. Claude-Session: https://claude.ai/code/session_01KTccq4q6rvEht5nZtQbLii Co-authored-by: Claude <noreply@anthropic.com>
… a disabled package's object as the protocol does Claude-Session: https://claude.ai/code/session_01KTccq4q6rvEht5nZtQbLii Co-authored-by: Claude <noreply@anthropic.com>
…tcher and the exposure gate Claude-Session: https://claude.ai/code/session_01KTccq4q6rvEht5nZtQbLii Co-authored-by: Claude <noreply@anthropic.com>
… the data plane's refusal again Claude-Session: https://claude.ai/code/session_01KTccq4q6rvEht5nZtQbLii Co-authored-by: Claude <noreply@anthropic.com>
…tifactItem ledger keys its by-name rows on readMetaItem Claude-Session: https://claude.ai/code/session_01KTccq4q6rvEht5nZtQbLii Co-authored-by: Claude <noreply@anthropic.com>
…a local type, not the engine view Claude-Session: https://claude.ai/code/session_01KTccq4q6rvEht5nZtQbLii Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 26 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 39 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d1a14284cab408295948b9af76648b16a75b8163 && git checkout d1a14284cab408295948b9af76648b16a75b8163
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 70cfb12771bc8cc44bff830ea7528714f14d715e 81e9289baa3466f31285059cc907e133b4789953 && git checkout -B drift-repro 70cfb12771bc8cc44bff830ea7528714f14d715e && git merge --no-ff 81e9289baa3466f31285059cc907e133b4789953
node scripts/docs-audit/affected-docs.mjs --json 70cfb12771bc8cc44bff830ea7528714f14d715e
|
Contract reviewServed-tier: ① Derived judgmentsRead at the head; the check-runs' conclusions are the gate verdicts (40 runs: 34 success, 6 skipped, none failed or in progress).
Residue, not a defect: the by-name predicate is the data plane's ( ② Semver level
③ Boundary flagsReport 6238493670, every entry:
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 ·
|
| Item | Reading |
|---|---|
| Shape | Draft PR → main. The body opens Fixes #24097, its only closing keyword (full-body scan). mergeable_state: clean against today's main, which has since taken PRs #24202 and #24203 in protocol.ts. |
| Scope | 11 files (+484 / −34), inside the re-declared claim 6236980302. protocol.ts: the getMetaItem refusal, the private readMetaItem body, the destructive-change prior read, and getMetaItemLayered's check. The runtime dispatcher's object branch and the REST read gate are declared to domain:cli on #6024 and #22648. Plus the #7557 pin rows, the flipped and re-keyed pins, and three changesets. Not governed (check-governed-merges.mjs --pr 24208: 0 of 11). |
| Ruling | The seat's ruling 6236971888 (OQ1 → A, OQ2 → A, items 1–7), implemented as written. The seat checked the record's central claims on this head before adopting it. getMetaItem canonicalises, then refuses type: 'object' of a disabled package, then calls readMetaItem. saveMetaItem's destructive-change read calls this.readMetaItem (:22950). The runtime's protocol read rethrows OBJECT_PACKAGE_DISABLED instead of falling through (meta.ts:1773). |
| Contract review | CONTRACT_REVIEW_TIER, an isolated reviewer: record 6239481977, PASS on this head (narrowing face, every non-prose path). |
| Measured | Item 6: lock.verdict is asked only for presentational types and 'position', never object, so packages/cli is untouched. Item 7: GET /meta/app/NAME of a disabled package answers 200 while the list omits it. That is measured over HTTP and filed by the seat as #24210 (the non-object family), ⛔ not fixed here. Live boot at this head: every by-name object face answers 404 OBJECT_PACKAGE_DISABLED; a destructive PUT answers 409; another package's object answers 200; re-enable restores 200; 0 fail-open warnings. |
| Prose faces | The three changesets, sentence by sentence. Each FROM / TO matches the pins. The rest changeset states the reversal of the #24031 move (a denied operation on a disabled package's object now answers 404 OBJECT_PACKAGE_DISABLED). Each "What changes for you" names the remedy (re-enable the package). |
Clause-② |
no (narrowing), at a line start in the PR body and all three changesets. minor + BREAKING under the launch-window convention, each with not-required (no-migration-prescription). No objectql changeset, since its diff is one test file outside its published files. |
| Gates | Dev union: 68 derived, 68 run, all exit 0. CI on this head: 40 check-runs, 34 success, 6 skipped, 0 failed. Every skip is rostered in EXPECTED_SKIPS: Console Pin Gate, Build Docs, Temporal Conformance, the Dogfood matrix template, Dogfood Verify CLI, Packed-tarball smoke. |
Disposition:
open_questions: [0] A:minor+ BREAKING, the repo's narrowing convention. [1] A: a local optional type for the predicate inpackages/runtime, sopackages/specis not widened for one consumer.out_of_scope_findings[0](non-object by-name reads): filed metadata-protocol: a by-name /meta read of a non-object item (app, view, page, …) serves a disabled package's item while the list omits it; #24097 closes the object half only #24210.out_of_scope_findings[1](/publishedserves a disabled package's object snapshot): carried on metadata-protocol: a by-name /meta read of a non-object item (app, view, page, …) serves a disabled package's item while the list omits it; #24097 closes the object half only #24210, the one-answer-per-door family. The reviewer adds two dispatcher-only registry reads on object API doors, the legacy one-segmentGET /meta/NAMEand/meta/object/NAME/state/FIELD. Both are carried there too.out_of_scope_findings[2](the consumed rest: every /data request's API-exposure gate reads EVERY object's metadata (with a full schema diagnostic) to check ONE object's enable flags — 62-72% of a data request's time, measured #24031 changeset states the opposite): Acceptance notes. A consumed changeset is release-owned, and this PR's rest changeset carries the FROM / TO.- Reviewer note: the
OBJECT_PACKAGE_DISABLEDledger row's comment still says "data plane refuses", while the by-name/metareads now raise it too. That is spec auxiliary text, noted, not filed; it is for the spec lane's next touch of the ledger row. deviations: accepted (the two pins outside the literal surface that this narrowing flips or re-keys; the local type; the commit title; the dev's own clone deepened; attribution per AGENTS.md; no objectql changeset).
Landing: ordinary queue (pr_ready + automerge_enable through the relay), in this stroke.
Generated by Claude Code
Fixes #24097
Clause-②: no (narrowing)
Implements the seat ruling on #24097 (review 6236971888: OQ1 → A, OQ2 → A, items 1–7) under the re-declared claim 6236980302.
What changed
A disabled package's object was refused by the
/meta/objectslist and by the data plane, butGET /meta/object/:namestill served it. Every by-name door now gives the same answer,404 OBJECT_PACKAGE_DISABLED. The authoring check that needs the object still sees it.getMetaItem, the API face (packages/metadata-protocol/src/protocol.ts). The refusal is checked once, right aftercanonicalizeMetaRequestTypeand before any layer is read, so it covers the active read,state: 'draft'andpreviewDrafts, and the plural spelling folds into it. The refusal comes fromobjectPackageDisabledError, a constructor extracted fromassertObjectRegistered, so the data plane and the by-name read throw byte-identical envelopes. The predicate isregistry.isObjectPackageDisabled, optional-called throughisObjectOfDisabledPackage, as the data plane already calls it.getMetaItembody is now the privatereadMetaItem, andgetMetaItemdelegates to it after the refusal.saveMetaItem's destructive-change prior read usesreadMetaItem. There is no new public method, spec key or error code.getMetaItemLayered(?layers=true) reads its layers without going throughgetMetaItem, so it carries the same check itself (item 5).packages/runtime/src/domains/meta.ts(cross-lane, declared todomain:cli). The object branch no longer falls back toregistry.getObjectwhen the protocol refusesOBJECT_PACKAGE_DISABLED. The unscoped kernel's registry-first read skips an object the registry reports as disabled, so the protocol answers it. The predicate is read through a local type, becauseEngineSchemaRegistryView(packages/spec, outside the fence) does not declare it.packages/rest/src/meta-item-read-gate.ts(cross-lane, declared todomain:cli).loadObjectItemreadsOBJECT_PACKAGE_DISABLEDas "no object" and does not log the fail-open warning. Any other thrown read still fails open and warns.Call sites of the by-name object read
"Today" is
origin/main7d6ec91d2; "after" is this branch's head. The case istype: 'object'for an object whose owning package is disabled.apps/console/src/components/FormPage.tsx(/meta/object/NAME)OBJECT_PACKAGE_DISABLED. It uses the object, and the data plane already refuses its rowspackages/app-shell/src/providers/MetadataProvider.tsx(getItem('object', name))packages/app-shell/src/console/ai/AiChatPage.tsxapps/console/src/pages/system/hiddenFields.ts,PagePreview.tsx,ObjectChart.tsx,getObjectSchemausersMetadataFieldsPage,PermissionMatrixEditor,StudioDesignSurface,PackageOwdOverviewPanel)client.list('object', …)already omits the package's objectsPackagesPage.tsx(show or re-enable a package)/api/v1/packagesonlyprotocol.tssaveMetaItemdestructive-change check409 DESTRUCTIVE_CHANGEreadMetaItem, still409 DESTRUCTIVE_CHANGE(pinned; measured over HTTP below)protocol.tsgetMetaItemCachedprotocol.tsapplySeedBodiesadapter, build probespackages/restimport template / export / import-prepare404 OBJECT_PACKAGE_DISABLED; the others keep their best-effort catchpackages/restreference-label lookup (rest-server.ts)packages/restenforceApiAccess→loadObjectItemenable404 OBJECT_PACKAGE_DISABLED, with no warningpackages/runtimedispatcherGET /meta/object/:name(object branch, draft read, layered read)OBJECT_PACKAGE_DISABLEDpackages/coreimport runner (servesReferenceTarget)reference_not_found(fail-closed catch), which matches the data planepackages/services/service-automationconnector pullpackages/cliownership ceremonylock.verdictpackages/cliis touchedMeasurements (ruling items 5–7)
getMetaItemLayered: it does not route throughgetMetaItem. An ablation that removes only its own check turns itsREADERSrow red ("getMetaItemLayered … → refuses": false): without the check it serves the object. Over HTTP below,?layers=trueanswers 404OBJECT_PACKAGE_DISABLED.lock.verdict: the ceremony enumerates candidates by raw SQL oversys_metadata(readMetadataKeyRows,organization-ownership-plan.ts), limited toPRESENTATIONAL_TYPES=view, dashboard, report, translation, email_template. The other caller,organization-ownership-apply.ts, asks only about'position'. Solock.verdictis never asked about anobject, and the refusal, which istype: 'object'only, cannot reach it. Noneeds_decision;packages/cliis untouched.app.probe-24097disabled,GET /api/v1/meta/app/probe_gadget_appanswers 200 whileGET /api/v1/meta/appomits the app (5 → 4 items). This is class (b) with that reach. It is reported to the seat to file the family card and is ⛔ not fixed here.HTTP readings:
pnpm dev:crm -- --freshat this branch's headThese come from a full
pnpm build(76/76) and a seeded admin, against a writable packageapp.probe-24097holding objectprobe_gadgetand appprobe_gadget_app. Code-shippedcom.example.crmrefuses disable with422 WRITABLE_PACKAGE_REQUIRED.GET /meta/object/probe_gadgetOBJECT_PACKAGE_DISABLEDGET /meta/object/probe_gadget?layers=trueOBJECT_PACKAGE_DISABLEDGET /meta/object/probe_gadget?state=draftNO_DRAFTOBJECT_PACKAGE_DISABLEDGET /data/probe_gadgetOBJECT_PACKAGE_DISABLEDGET /meta/objectscontains itGET /meta/app/probe_gadget_appGET /meta/appcontains itPUT /meta/object/probe_gadgetdropping fieldtitleDESTRUCTIVE_CHANGEGET /meta/object/crm_account(another package)The server log carried 0
failing openwarnings.Pins
packages/objectql/src/package-disable-enforcement.test.ts, the Package disable and uninstall never reach the metadata/data layer: a disabled package's objects still serve rows, and uninstall leaves 7 orphanedsys_metadatarows #7557 pin.READERSgains fiveenforces: truerows:getMetaItemobject,objects,state: 'draft',previewDrafts, andgetMetaItemLayered. A row counts onlyOBJECT_PACKAGE_DISABLEDas refusing and rethrows any other error, soNO_DRAFTcannot pass for enforcement. The seed adds a pending draft so the draft row serves before the disable. New cases:code,status,object, and a message identical to the data plane's;409 DESTRUCTIVE_CHANGE, and the stored row keeps the field.packages/runtime/src/domains/meta-object-disabled-package.test.ts, throughHttpDispatcher.dispatch, on unscoped (registry-first) and scoped (protocol-first) kernels: 404OBJECT_PACKAGE_DISABLED,registry.getObjectnever asked for the disabled name, and the control served.packages/rest/src/meta-item-read-gate.object-package-disabled.test.ts:OBJECT_PACKAGE_DISABLEDreads as no object and no warning, any other throw still warns once, and the control.packages/rest/src/rest-exposure-gate-one-object-read.test.ts's rest: every /data request's API-exposure gate reads EVERY object's metadata (with a full schema diagnostic) to check ONE object's enable flags — 62-72% of a data request's time, measured #24031 row "a DENIED operation answers the exposure verdict (405)" now asserts404 OBJECT_PACKAGE_DISABLED, withcreateDatareached once. The ledgerprotocol.lookup-artifact-item-call-sites.test.tsre-keys its three by-name rows fromgetMetaItemtoreadMetaItem; the call sites, arguments and dispositions are unchanged.Verification (head
81e9289ba)@objectstack/objectql437 files / 8239 passed.@objectstack/runtime409 files / 6280 passed.@objectstack/rest293 files / 6016 passed.@objectstack/metadata-protocol: the first full run had 1 failure, thelookupArtifactItemledger, re-keyed above. The rerun at head: 243 files / 20628 passed, 3 files skipped. Runtime rerun at head: 409 / 6280 passed.metadata-protocol,objectql,restandruntimeare clean. The runtime typecheck was rerun after the predicate typing fix.node scripts/pm/dispatch-gates.mjs --commands(no paths) derived 68 commands at81e9289b. All 68 exit 0. Two first refused on the shallow clone (check-engine-split-ratio --days 90,check-plugin-teardown-shape --self-test) and passed after the deepening each prescribed.--ranwith exit codes: "68 derived, 68 run, 0 NOT-MEASURED (a DERIVED zero)".check-platform-checklist --changed-from origin/main: OK, 36 anchors judged, 0 lost. Control-byte scan of the changed files: clean.scripts/ablation-replace.mjs, thenpnpm --filter @objectstack/metadata-protocol build, with the test resolvingdist/throughexports. The on-disk proof is the count ofthrow objectPackageDisabledError(indist/index.js, because a comment or string marker is stripped by the bundler. A first attempt used such a marker; the preflight reported it absent fromdist/, so it was discarded and redone.getMetaItem's check removed: dist 3 → 2. The fourgetMetaItemrows and the envelope case went red, whilegetMetaItemLayeredstayed refusing. Restored: dist 3, 12/12.getMetaItemLayered's check removed: dist 3 → 2. Only the layered row went red. Restored: 12/12.getMetaItem: distawait this.readMetaItem(1 → 0. The destructive pin went red withexpected undefined to be 'DESTRUCTIVE_CHANGE'. Restored: 12/12.blob == HEADand an emptygit status --porcelain.Acceptance notes
GET /meta/:type/:name/published(REST and the dispatcher) asksgetMetaItemLayeredinside atryand falls through to the published snapshot on any throw. For a disabled package's object it therefore serves the snapshot, not the refusal. It was not in the ruling's fence and was not measured over HTTP. Noted, not filed..changeset/pre/24031-exposure-gate-one-object-read.md, already consumed into a prerelease) says a denied operation on a disabled package's object answers the exposure verdict. This PR reverses that, and.changeset/24097-rest-exposure-gate-disabled-package.mdstates the FROM → TO. The consumed changeset is not edited.@objectstack/objectql: its diff is a test file, which is not in its publishedfiles.Generated by Claude Code