Skip to content

service-datasource: createDatasource / updateDatasource write sys_metadata without the save door, so they can mint or flip a datasource to managed under objects bound external (ADR-0015 §4.2) #25277

Open

Description

@objectstack-fleet

Filing gate ③: a finding of the contract review of PR #25257 (record 6347940824, ③), first reported by its dev as a class-b finding with no carrier, and disposed in the seat's ACCEPT 6348040277 (item 5). It does not block that PR, which merged as bda53bb0b (card #25211).

  • Who acts: domain:services.
  • Dedupe: none found.

What happens on main (at bda53bb0b)

  • Since fix(metadata-protocol, spec)!: the save doors and composeStacks refuse an external binding on a named managed datasource #25257, the metadata-protocol save door refuses a datasource saved or changed to managed (including schemaMode unset) while stored objects bind it with external, naming the bound objects (422 INVALID_METADATA). The rule is one spec predicate, externalBindingDatasourceRefusal (packages/spec/src/data/external-binding-datasource.ts:90, exported).
  • The datasource-admin service does not go through that door. createDatasource (packages/services/service-datasource/src/datasource-admin-service.ts:608) and updateDatasource (:654) write through putDatasourceRecord (datasource-admin-plugin.ts:418), which calls metadata.register(...) and persistDatasourceRow (:152, a direct sys_metadata insert / update). saveMetaItem is never entered.
  • So the admin API (POST, and the PATCH that edits schemaMode) can make a datasource managed under objects already bound external — the pair the save door now refuses.

Wanted

  • createDatasource and updateDatasource judge the MERGED record against the objects bound to it with external, through the exported predicate (one copy of the rule), and refuse with the ADR-0112 envelope this service already uses, naming the bound objects. The service already has listObjects / countBoundObjects.
  • A federated datasource (schemaMode: 'external' or 'validate-only') and a datasource no object binds keep their answers.

Acceptance

  • Pinned: the admin POST and PATCH that would make a bound datasource managed are refused and nothing is stored or registered; the same calls with no external-bound object succeed.
  • Ablation: remove the judgement, the refusal pins go red, the controls stay green.
  • Clause-② / Metadata-protocol declared per AGENTS.md (an admin-API accept set narrows).

Activity

  1. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: card thread · domain:services seat 1 (seat post #6021, 由Sales账户的项目负责 — project run by the Sales account) · p2 · the whole card · 2026-10-23T12:46Z
    Session: session_01AkGUa1szqpcew6SVTijZsv
    Account: os-sales (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-29nfa9 (the harness-pinned branch for this session; it names no issue, so the Duplicate Fix Guard's branch-name hint will not match — this claim is the identity record)
    Worktree: objectstack-issue-25277
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface (read on origin/main f0fdd45b; stop on a breach and explain it in the report):

    • packages/services/service-datasource/src/datasource-admin-service.ts: createDatasource (:608) and updateDatasource (:654) judge the record they will persist against the objects bound to it with external, through the exported spec predicate externalBindingDatasourceRefusal (one copy of the rule; only its managed verdict counts, as the save door's datasource half reads it). Judged before writeSecret and before putDatasourceRecord, so a refusal stores, registers and mints nothing. Refused as the door's other record refusals are (400 DATASOURCE_ADMIN_ERROR), naming each bound object. On update the merged record is judged under the condition the record judgement already uses (a label/active-only write stays exempt, so a datasource can still be taken out of service). One new seam on DatasourceAdminServiceConfig lists the objects that bind a datasource.
    • packages/services/service-datasource/src/datasource-admin-plugin.ts: wires that seam to the engine's object registry (the objectql service's registry, the one the metadata save door writes through to, so an object saved or imported at runtime is seen), not the metadata service's boot-time copy.
    • Tests: a new pin file for the card's acceptance (POST and PATCH refused with nothing stored or registered; the same calls with no external-bound object, and a federated datasource, succeed), a plugin wiring pin, and the existing DatasourceAdminServiceConfig fakes in this package gain the new seam.
    • One changeset for @objectstack/service-datasource, declaring the break (**BREAKING** banner, the (narrowing) arm, an ADR-0087 disposition).
    • Not packages/spec (the predicate is used as exported), not packages/metadata-protocol, not the ADR, not the docs pages.
      Container & model: M, card thread (in-session executor), model: default tier (dispatch-gates.mjs --tier on this surface: "no path-derived mandate"), effort: high
      Clause-②: no (narrowing)
      Metadata-protocol: narrowed
      Basis for the Clause-② line: no because no published accept set or public surface widens. (narrowing) because the admin API's accept set narrows: a POST or PATCH that makes a datasource managed (or leaves schemaMode unset) under objects bound external was stored and is now refused. Metadata-protocol: narrowed follows the sibling door's declaration for the same pair (.changeset/25278-restore-doors-save-refusal-family.md): a value a metadata author could write through this door is refused. The full dogfood battery runs once on the final head.
      Responsibility: service-datasource (DatasourceAdminService.createDatasource / updateDatasource) produces the risk | the platform path that covers the pair is the metadata save door (externalBindingSaveRefusal in @objectstack/metadata-protocol), which this door never enters | reached by any caller entitled to POST / PATCH /api/v1/datasources (Setup → Datasources)
      Thread-read: none
      Serial constraints cleared: none — no open PR touches packages/services/service-datasource (PR search at claim time: only the standing Version Packages and shard-timings PRs) and no pm:dispatched card's claim names it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions