| Field | Value |
|---|---|
| RFC | 0145 |
| Title | registrationSource as a per-type artifact capability facet |
| Status | Accepted |
| Author(s) | David Tufts (@davidscotttufts), on a finding from the openwop-app reference host |
| Created | 2026-08-10 |
| Updated | 2026-08-10 |
| Affects | schemas/capabilities.schema.json (artifactTypes.types.*), spec/v1/artifact-type-packs.md §"Per-type facets", spec/v1/host-capabilities.md §host.artifactTypes |
| Compatibility | additive per COMPATIBILITY.md §2.1 — one optional facet on a per-type entry; no field becomes required, changes type, or is removed |
| Supersedes | — |
| Superseded by | — |
Status note.
Accepted(2026-08-10). The bar was a host advertising the facet truthfully, and it is met: openwop-app advertisesregistrationSourcefor all 30 registered types (cbfbf9d4a), derived from the same registry fieldvalidateArtifactreturns — so requirement 3 holds by construction rather than by agreement, since advert and event read one source and cannot drift apart. Witnessed by leg B against a live host, sabotage-verified host-side (flipping the advert to emit the opposite provenance reds exactly the agreement assertion; revert → green), and re-run through the adopter's pinned dependency at suite1.71.0(1013095cf), so the evidence is re-derivable from a clean checkout of that repo rather than from an adjacent corpus tree. Amends RFC 0075's facet set. The path here is worth recording: the reference host droppedregistrationSourcefrom its advert during the RFC 0144 migration because it was undeclared, and asked for it to be declared upstream rather than re-adding it unilaterally — this RFC is the upstream half of that request, and the host closed the loop by advertising it once it existed.
Summary
artifact-type-packs.md §"Schema distribution" makes serving a type's canonical schema URL a MUST for host-registered (no-pack) types and only a SHOULD for pack-backed ones — because for a host-registered type the served URL is the only resolution path. So whether a type is pack-backed or host-native changes what a consumer may rely on. Discovery does not disclose which. A consumer either fetches and hopes, or installs the pack to discover there wasn't one. This RFC adds registrationSource ("pack" | "host") to the per-type facet set, mirroring the field artifact.created already carries.
Motivation
The asymmetry is normative and currently invisible
artifact-type-packs.md:87, verbatim: "Serving is a MUST for host-registered (no-pack) types (RFC 0075 / P1-3). When a host advertises a schemaVersion for a type that is not backed by an installed pack … the canonical served URL is the _only_ resolution path, so serving it MUST be honored … Pack-publishing hosts are unaffected (the schema travels in the tarball; serving stays a SHOULD for them)."
Two consumers of the same advert therefore have different guarantees available, and no way to tell which one they have:
| Type is… | Schema resolvable via | Serving the canonical URL |
|---|---|---|
| pack-backed | the signed tarball or the served URL | SHOULD |
| host-registered | the served URL only | MUST |
A consumer that needs the schema — a peer host forwarding an artifact, a UI resolving a stored artifact's shape — has to guess which regime applies. Guessing wrong in one direction means fetching a URL that was never promised; in the other, installing a pack that does not exist.
It is already first-class on the wire
run-event-payloads.schema.json $defs.artifactCreated.registrationSource is enum: ["pack", "host"], and artifact-type-packs.md §"Binding" already requires a host to set it alongside registered: true. The concept, the vocabulary, and the normative meaning all exist. Only the discovery surface is missing — so a consumer learns the provenance after an artifact is created, when what it needed was to know before resolving a schema.
Why this passes the test schemaEndpoint failed
During the RFC 0144 migration the reference host emitted an undeclared top-level schemaEndpoint key and asked whether to keep it. It was dropped, on the ground that §"Schema distribution" fixes the canonical URL as {HostBase}/schemas/artifacts/{artifactTypeId}.schema.json — derivable from an artifactTypeId the consumer must already hold. A discovery key that restates a computable value earns nothing and enshrines one host's invention as protocol surface.
registrationSource is the mirror image, and the same test decides it the other way:
- Not computable. Nothing in the identifier, the URL convention, or the rest of the advert discloses whether a pack backs the type.
- It gates a normative difference, not a convenience — MUST vs SHOULD on the only resolution path.
The host applied that test to itself and dropped its own per-type schemaUrl for the same reason it dropped schemaEndpoint; it then asked for registrationSource to be declared upstream rather than re-added locally. That is the correct order, and this RFC is the upstream half.
Proposal
Add registrationSource to the per-type facet set (artifact-type-packs.md §"Per-type facets", host-capabilities.md §host.artifactTypes), and declare it on the per-type entry in capabilities.schema.json:
"artifactTypes": {
"supported": true,
"types": {
"vendor.acme.cad.model": { "validated": true, "registrationSource": "pack" },
"app.audit": { "validated": true, "registrationSource": "host", "schemaVersion": 1 }
}
}
Normative requirements
1. registrationSource is OPTIONAL and takes exactly "pack" or "host", matching artifact.created.registrationSource. 2. Absent ⇒ unspecified provenance, exactly as on the event. It MUST NOT be read as a default of "pack", and a host MUST NOT be treated as non-conformant for omitting it. 3. A host that advertises registrationSource for a type MUST advertise the value it would emit on artifact.created for that type. The two surfaces MUST NOT disagree — a discovery advert of "pack" with an event payload of "host" is a false advertisement, not a permitted divergence. 4. Advertising "host" for a type does not by itself create the §"Schema distribution" serving obligation — that obligation attaches to advertising a schemaVersion for a no-pack type, and is unchanged by this RFC. This facet discloses which regime applies; it does not move the boundary.
What this does NOT do
- No new obligation. Every MUST it makes legible predates it.
- No inference. A host MUST NOT derive
registrationSourcefrom an identifier's shape —vendor.*does not imply"pack", and a bare legacy id (RFC 0141) implies nothing. - No global form. The facet is per-type only. Provenance is a property of a type, not of a host; a host with both pack-backed and host-native types cannot answer globally, which is the same argument that made the rest of the facet set per-type (RFC 0075 / P1-1).
Compatibility
Additive per COMPATIBILITY.md §2.1. One optional property on the per-type entry object. The entry is additionalProperties: false, so declaring the facet is strictly permissive — it admits a document that was previously rejected and invalidates none. No existing property changes meaning or type; no error code changes; a host that never advertises it stays conformant with today's semantics (unspecified provenance).
Conformance
artifact-type-registration-source.test.ts — always-on corpus legs:
- the per-type entry declares
registrationSourcewithenum: ["pack","host"], and it is not required; - a per-type entry carrying
registrationSource: "host"validates, and one carrying an out-of-enum value ("registry") does not — the enum is load-bearing here, unlike RFC 0136'sformat, because this facet has exactly two meanings and an unrecognised third is a wire error rather than a hint to ignore; - both prose sites list the facet, so the schema and its normative surface cannot drift apart — the defect RFC 0144 exists to close.
Leg B (behavioral, requirement 3) — added 2026-08-10. Reads the advertised registrationSource for a type, drives the RFC 0142 host-sample seam to produce one artifact of it, and asserts the emitted registrationSource equals the advertised value. Three gating calls, each deliberate: a type that advertises nothing is inapplicable, not gated — the facet is OPTIONAL (requirement 2) and strict mode must not coerce an advertisement, the same call RFC 0142 makes for store; a type that advertises the facet but does not emit is also inapplicable, because requirement 3 is a statement about agreement between two surfaces and such a type has only one, so asserting would red a host telling the truth everywhere it speaks; and seam-absent returns quietly rather than firing its own behaviorGate, because reaching that point means the type advertises store, so store: true + no seam is already strict-red under RFC 0142 leg B — gating again would double-report one defect, and gating on that seam for a 0145 advert would coerce hosts into wiring 0142's surface to advertise an unrelated facet. Non-vacuity proven against a stub across six modes (agree → pass; disagree → red; event omitting the field → red, since absent asserts unspecified provenance and therefore disagrees with an advert naming one; no advert / no emission / no seam → correctly inapplicable). Not yet witnessed against a live host — see G1.
Alternatives considered
1. Leave it undeclared; let hosts emit it under additionalProperties. Rejected — the per-type entry is additionalProperties: false, so it is not merely undeclared but forbidden; and an undeclared field with normative meaning is exactly the defect RFC 0144 was written to close. Re-creating it one RFC later would be self-refuting. 2. Infer provenance from the identifier. Rejected — vendor.acme. is a registry namespace, not a statement about installation, and RFC 0141 legacy identifiers carry no namespace at all. This is the same wrong-surface inference RFC 0136 requirement 4 forbids for format. 3. A global registrationSource. Rejected — a host with both pack-backed and host-native types would have to advertise a false intersection, which is the argument that made the whole facet set per-type. 4. Fold into RFC 0144's schema without an RFC. Rejected, and this is the one worth stating plainly: RFC 0144 declared five families as their prose already documented them*. registrationSource is not in RFC 0075's facet list. Adding it by editing that schema would put a wire field on the wire with no normative surface behind it — the defect 0144 exists to fix, run in reverse.
Unresolved questions
1. Whether registered: false (unregistered) types should be expressible here at all. Today they cannot appear in types meaningfully — the facet set describes registered behaviour — and the unregistered tier is deliberately first-class and un-advertised. Left alone. 2. Whether a future facet should disclose which pack backs a type (name + version) rather than only that one does. Deferred: it is a larger surface, and the MUST/SHOULD asymmetry this RFC closes does not depend on pack identity.
Open spec gaps
| ID | Gap |
|---|---|
| G1 | Cross-surface agreement (requirement 3) — CLOSED 2026-08-10. Leg B asserts a host's advertised registrationSource for a type equals the value it emits on artifact.created, driven through the RFC 0142 host-sample seam. It was deliberately withheld when this RFC landed because, against a corpus where no host advertised the facet, it would have gone green by finding nothing. Closed in two steps, deliberately counted separately: witnessed against a live host advertising the facet — non-vacuous because a host-side flip to the opposite provenance reds exactly the agreement assertion, so the pass is the two surfaces agreeing rather than an inapplicable return; then reproducible, re-run through the adopter's pinned @openwop/openwop-conformance@1.71.0 (1013095cf), which is what makes the evidence re-derivable by anyone checking out that repo instead of by someone holding both trees at once. A witness that only its author can reproduce is a weaker artifact than it looks, which is why the second step was not folded into the first. |
| G2 | Vendored-schema staleness — narrowed twice; detection still open. The reference host discovered during the RFC 0144 migration that its hand-copied capabilities.schema.json carried 81 properties where the corpus had 88, so it had been validating against a contract that predated the very declaration it was checking — green, and wrong, with nothing warning it in either direction. A file copy has no version. Two narrowings have landed. (1) The published tarball pins the contract schemas at stable paths and conformance/README.md §"Resolving the contract" states the guidance — depend on the package, do not hand-copy — which makes staleness a lockfile fact for anyone taking that path. (2) The tarball's schemas/ now carries CORPUS-STAMP.json (suiteVersion + corpusCommit), written at prepack and living inside the directory because the directory is what gets copied: package.json's version is exactly what a cp -R schemas/ vendor/ discards. A host that still vendors can compare its stamp against the installed package's with a plain file read — no network, no sibling checkout, no pinning a branch — which is what the host-side guard could not do before. What remains open: nothing forces that comparison. The corpus cannot see a host's local files, so a host that copies and never checks is still silent, and the failure is now avoidable and self-detectable rather than detected. Closing it outright would need the comparison to be somewhere the protocol can require it, which is wire surface and its own RFC. The reference host gated its side of this in ad81cf9e3 (14 → 16 schemas), and its stated scope — the suite certifies conformance against a contract that no longer exists, and stays green doing it — is the harm class worth naming. Sibling of RFC 0144 G3. |
References
spec/v1/artifact-type-packs.md§"Schema distribution — source of truth and runtime mirror" (:87) — the MUST/SHOULD asymmetry this facet disclosesspec/v1/artifact-type-packs.md§"Per-type facets (RFC 0075 / P1-1)" — the facet set amendedschemas/run-event-payloads.schema.json$defs.artifactCreated.registrationSource— the existing wire vocabulary this mirrors- RFC 0144 — declared
artifactTypes; this RFC adds a facet to it through prose first, per RFC 0144 §A's rule - openwop-app
e65ff6888(2026-08-10) — the migration that dropped the undeclared field and raised the ask