Skip to content

An object overlay write over a packaged object is silently discarded by registerObject's ownership rule while saveMetaItem reports success #6995

Description

@os-zhuang

Out-of-scope finding measured while scoping #6853 (registry heal / map-vs-contributors split). Recorded, not claimed. Pre-existing.

Fact (measured on origin/main @ 51f2bb8c3)

applyObjectRegistryMutation (packages/metadata-protocol/src/protocol.ts:7870) mirrors an accepted object overlay write into the registry with:

this.engine.registry.registerItem(request.type, request.item, 'name');
try {
    this.engine.registry.registerObject(
        { ...(request.item as Record< string, unknown >), _provenance: 'org' } as any,
        request.packageId || 'sys_metadata',
    );
} catch (err: any) {
    console.warn(`[Protocol] registerObject failed for ${request.name}: ${err?.message ?? err}`);
}

(generics spaced so the GitHub body sanitizer does not eat them)

SchemaRegistry.registerObject (packages/objectql/src/registry.ts:1148) throws when an own contributor from a different package already holds the name. So whenever the row's package_id does not equal the packaged owner's package id — including the ordinary package-less row, which normalises to the sys_metadata sentinel — the second half of the write is thrown away, warn-logged, and the save still reports success.

The two registry views then disagree permanently:

  • metadata['object'][name] (written by registerItem) holds the overlay body, so GET /api/v1/meta/object/:name serves it;
  • objectContributors still holds the package body, and that is what registry.getObject resolves — the surface assertObjectRegistered and every data-CRUD dispatch reads.

Measured with the protocol-delete-object-registry-heal.test.ts harness (real SchemaRegistry, real SysMetadataRepository), a packaged myapp_invoice owned by app.myapp plus a package-less overlay save carrying only name + overlay_only:

[save warning] [Protocol] registerObject failed for myapp_invoice: Object "myapp_invoice" is already
               owned by package "app.myapp". Package "sys_metadata" cannot claim ownership.
[contributors] packageId=app.myapp ownership=own provenance=package
               fields=[ ... name, amount, packaged_only ]      <- the PACKAGE body, untouched
[metadata map] plain key "myapp_invoice" present               <- the OVERLAY body
[getObject]    fields=[ ... name, amount, packaged_only ]      <- data plane never saw the overlay

The save's own receipt reports success, and nothing in the response says the runtime discarded half of it.

Reachability

  • Save path: needs the documented operator escape hatch (OS_METADATA_WRITABLE=object) or an equivalent door, because both the protocol gate (protocol.ts:8434) and SysMetadataRepository.assertAllowed (sys-metadata-repository.ts:1037) refuse an overlay over an artifact-backed object otherwise (measured: NOT_OVERRIDABLE/403 on both tenant and control-plane kernels).
  • Boot path: loadMetaFromDb (protocol.ts:11753) makes the same registerObject call for every stored object row. There the throw is counted — errors++ plus a Failed to hydrate warning (protocol.ts:11789) — so the boot leg is at least honest about it; the save leg is not.

Dispositions worth pricing (no recommendation forced)

  1. Fail the save. Turn the swallowed throw into a named refusal (the overlay cannot claim ownership of a name another package owns) so declared = enforced and the caller learns the write will not take effect. Cost: a write that "worked" before now 4xx-es; needs an error code + wording decision.
  2. Register the overlay as a distinct contributor rather than as a competing own, so the ownership rule is never reached and both layers coexist. This subsumes the finding but is the same contributor-model contract change Registry heal restores the metadata-map view but never re-registers a packaged contributor definition an overlay save replaced #6853 is escalating — see that card's option set.
  3. Document the split and leave it. Cheapest; leaves a success receipt for a write the runtime ignores.

Related

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions