Architecture Finding
Type: interface-violation / anti-pattern (trust boundary)
Affected area: pkg/plugin/marketplace.go, marketplace/schema-v2.json
ADR-0007 makes the marketplace the trust boundary for third-party code: "Marketplaces are signed/checksummed indexes of standalone binaries… provenance, version pinning, compatibility ranges, immutable artifacts, explicit permission consent." The schema encodes that — schema-v2.json requires publisher, license, pluginApi, and a 64-hex sha256 on every platform build.
But every integrity requirement in the code is gated behind one field the index publisher controls:
// fetchSource — the runtime path for every marketplace source
if idx.SchemaVersion == "" {
idx.SchemaVersion = "v1" // marketplace.go:295
}
Entry.Validate() then wraps the whole integrity block in if e.SchemaVersion == MarketplaceAPIV2 (marketplace.go:351), so a v1 entry is never required to carry a digest. Install repeats the pattern:
if e.SchemaVersion == MarketplaceAPIV2 && len(b.SHA256) != 64 { // :453
return fmt.Errorf("marketplace v2 requires a SHA-256 digest")
}
...
if b.SHA256 != "" && !strings.EqualFold(sum, b.SHA256) { // :486
return fmt.Errorf("checksum mismatch for %s (got %s)", e.Name, sum)
}
For a v1 index b.SHA256 is "", so the second guard is also skipped. The result: an index that simply omits schemaVersion gets its binaries downloaded, chmod 0755'd, and installed into plugin.Dir() with no integrity verification of any kind. The security tier is chosen by the party being verified.
Two supporting gaps:
- The strict validator that does require v2 —
ValidateIndexFile (marketplace.go:220, if idx.SchemaVersion != MarketplaceAPIV2 { return err }) — is reachable only from the publisher-side corral marketplace validate subcommand (cmd/plugin.go:185). It never runs on a fetched index.
Source.Trusted (marketplace.go:78) is set true for the built-in source (:123) and read nowhere in the codebase. The first-party/third-party distinction the ADR's model rests on is declared but not implemented.
Transport is fine — validateSourceURL enforces HTTPS for both index and artifact URLs.
Impact
A third-party marketplace — added via corral marketplace add or CORRAL_MARKETPLACE_URLS — opts out of every artifact guarantee by deleting one line from its index. Nothing in the CLI output distinguishes an unverified v1 install from a checksummed v2 one, so an operator has no way to tell which trust level they just accepted. The artifact host is also not constrained to the publisher's origin, so a v1 index can point url anywhere and the swap is undetectable by construction.
This is the standard downgrade-by-omission shape: a security control that is opt-in for the untrusted producer is not a control. It also blocks the ADR's stated goal — "external contribution possible without granting third-party code the main process's trust" — because today external contribution and full process trust are the same thing.
Recommendation
- Make v2 the floor for network-fetched indexes: have
fetchSource reject an index without schemaVersion: corral.marketplace/v2, i.e. reuse the ValidateIndexFile rule on the fetch path instead of defaulting to "v1". If a v1 grace period is needed, gate it on an explicit --allow-unverified / Source.Trusted=false opt-in that prints what is being given up, rather than on silence.
- Delete the
SchemaVersion == MarketplaceAPIV2 conditions in Validate and Install once (1) lands — with a v2 floor, the digest requirement is unconditional and the two skip paths disappear.
- Either implement
Source.Trusted (it is the natural place to express "this publisher may ship without a signature") or remove the field, so the trust model in the code matches the one in ADR-0007.
- Add a conformance test asserting that an index with no
schemaVersion fails to install — the current tests cover the v2 happy path and signature verification, not the downgrade.
Filed by architect agent (ACMM L6 — full mode)
— hive: agent=architect backend=claude model=claude-opus-5 claude=2.1.226
Architecture Finding
Type: interface-violation / anti-pattern (trust boundary)
Affected area:
pkg/plugin/marketplace.go,marketplace/schema-v2.jsonADR-0007 makes the marketplace the trust boundary for third-party code: "Marketplaces are signed/checksummed indexes of standalone binaries… provenance, version pinning, compatibility ranges, immutable artifacts, explicit permission consent." The schema encodes that —
schema-v2.jsonrequirespublisher,license,pluginApi, and a 64-hexsha256on every platform build.But every integrity requirement in the code is gated behind one field the index publisher controls:
Entry.Validate()then wraps the whole integrity block inif e.SchemaVersion == MarketplaceAPIV2(marketplace.go:351), so a v1 entry is never required to carry a digest.Installrepeats the pattern:For a v1 index
b.SHA256is"", so the second guard is also skipped. The result: an index that simply omitsschemaVersiongets its binaries downloaded,chmod 0755'd, and installed intoplugin.Dir()with no integrity verification of any kind. The security tier is chosen by the party being verified.Two supporting gaps:
ValidateIndexFile(marketplace.go:220,if idx.SchemaVersion != MarketplaceAPIV2 { return err }) — is reachable only from the publisher-sidecorral marketplace validatesubcommand (cmd/plugin.go:185). It never runs on a fetched index.Source.Trusted(marketplace.go:78) is settruefor the built-in source (:123) and read nowhere in the codebase. The first-party/third-party distinction the ADR's model rests on is declared but not implemented.Transport is fine —
validateSourceURLenforces HTTPS for both index and artifact URLs.Impact
A third-party marketplace — added via
corral marketplace addorCORRAL_MARKETPLACE_URLS— opts out of every artifact guarantee by deleting one line from its index. Nothing in the CLI output distinguishes an unverified v1 install from a checksummed v2 one, so an operator has no way to tell which trust level they just accepted. The artifact host is also not constrained to the publisher's origin, so a v1 index can pointurlanywhere and the swap is undetectable by construction.This is the standard downgrade-by-omission shape: a security control that is opt-in for the untrusted producer is not a control. It also blocks the ADR's stated goal — "external contribution possible without granting third-party code the main process's trust" — because today external contribution and full process trust are the same thing.
Recommendation
fetchSourcereject an index withoutschemaVersion: corral.marketplace/v2, i.e. reuse theValidateIndexFilerule on the fetch path instead of defaulting to"v1". If a v1 grace period is needed, gate it on an explicit--allow-unverified/Source.Trusted=falseopt-in that prints what is being given up, rather than on silence.SchemaVersion == MarketplaceAPIV2conditions inValidateandInstallonce (1) lands — with a v2 floor, the digest requirement is unconditional and the two skip paths disappear.Source.Trusted(it is the natural place to express "this publisher may ship without a signature") or remove the field, so the trust model in the code matches the one in ADR-0007.schemaVersionfails to install — the current tests cover the v2 happy path and signature verification, not the downgrade.Filed by architect agent (ACMM L6 — full mode)
— hive: agent=architect backend=claude model=claude-opus-5 claude=2.1.226