Skip to content

fix(items): adopt spec #79 — tag the platform defaults, and give the install form a vocabulary - #22

Merged
justinmerrell merged 7 commits into
mainfrom
fix/spec-adr-0018-install-form
Sep 8, 2026
Merged

justinmerrell merged 7 commits into
mainfrom
fix/spec-adr-0018-install-form

Conversation

@justinmerrell

Copy link
Copy Markdown
Contributor

Closes #21.

musher-dev/spec merged #79 (ADR 0018) at 7b79284. This repository fetches the schema bundles from spec@main unpinned on every run, so the break is live now — the next nightly cron would have been the first red run.

What was actually broken

Diffing the component bundle at 7b79284 against its parent 8ff7c2a, the only breaking change is that ComponentPlatformDefault gained a REQUIRED type with one member, SELF_ADDRESS. Six inputs across three items carried only a source. Everything else in #79 is additive, and the two narrowings it landed are inert here — the corpus has zero CONNECTION inputs and no generator beside a platformDefault.

npm test on main fails three suites with six errors, all must have required property 'type'. On this branch: 377/377, typecheck clean.

What else is in here

The same PR gives the fields it is already touching the vocabulary #79 opened, because the numbers that carried the proposal came from this corpus: of 28 USER inputs, 11 are typed by a person, and ten of those eleven reached a deploying user as an undifferentiated text box.

  • formatTIMEZONE on n8n's and openclaw's timezones (the field that motivated the proposal, hand-rolled twice, and the one entry where §6.4's derivation does real work: a picker, not a text box). HOSTNAME on n8nHost, matching its PUBLIC_HOSTNAME source. ENDPOINT_URL on the four public URLs and on open-webui.ollamaBaseUrl. CONNECTION_STRING on mlflow.backendStoreUri.
  • ui.order / ui.prominence — every blueprint here declares parameters: {}, so every form is derived and today sorts by input name as UTF-8 bytes. n8n's form currently leads with editorBaseUrl and splits the URL pair; label-studio's interleaves the custom-domain fields with the secrets. SECONDARY goes on the six platformDefault inputs and nothing else.
  • ui.examples — the shapes that were stated only in prose. The highest-value one is label-studio.csrfTrustedOrigins, whose real constraint (exact origin, scheme included, no trailing slash) has lived in a YAML comment since a2bf352 and is now machine-readable.
  • postgres.postgresPassword gets a generator — issue §3. It was the only required, sensitive input in the corpus without one, and the sole reason postgres was not a zero-input deploy while eleven of thirteen items already are.

Three judgement calls that go against the issue's own suggestions

  • No URI anywhere. §6.3 makes URI the residual member; every candidate here is ENDPOINT_URL or CONNECTION_STRING, so URI would have been the least informative true statement available. mlflow.backendStoreUri's shipped default sqlite:////mlflow/mlflow.db addresses no network service at all — it is a file — so ENDPOINT_URL would be false and CONNECTION_STRING is the member's exact definition.
  • No enum on mlflow.backendStoreUri. §6.4 says a control for a non-empty enum MUST NOT offer a value outside its members. A SQLAlchemy URI's value space is unbounded, and closing it would falsify that item's own listing, which advertises the Postgres + S3 upgrade as a deploy parameter. ui.examples gives the discoverability without the closure — so the corpus still holds zero enums, and ui.enumLabels stays unused.
  • open-webui.ollamaBaseUrl stays PRIMARY. It looks like the six advanced fields but is their opposite: their shipped value is already right, whereas its default names a sibling service that is not part of this deployment. A field whose default cannot work must not sit behind a disclosure, and it has to stay under the switch that governs it. A comment in the file records this so it is not "fixed" later.

Two things the issue does not cover

  • metadata.version. Per 7613630, a component content change that reuses its version resolves against the already-published record and is silently ignored at sync. Every touched component moves; ERR_VERSION_MISMATCH compares listing↔blueprint only, so a blueprint moves only when its listing does. Only postgres's listing changes, because only §3 changes what the storefront claims.

    item component listing blueprint
    n8n 1→2 1 1
    label-studio 2→3 2 2
    openclaw 1→2 1 1
    mlflow 1→2 2 2
    open-webui 2→3 1 1
    postgres 1→2 1→2 1→2
  • Why one PR and not three. The issue's §4 proposes one PR per item for the fix. That does not work here: validate.yml runs npm test over the whole corpus on every pull_request, and lefthook's pre-push does the same. Until all six blocks carry type, the first two of three PRs are red and cannot be pushed without --no-verify; stacking does not help, only the last is green. The README's rule has a stated reason — "so a rejection that only the platform can raise is easy to attribute" — which is about the capability phase; a spec-mandated required field carries no per-item platform risk, and one commit per item preserves the attribution anyway. The three fix(items) commits land first, so the platform's apps/api/config/catalog.ref pin can move to a tip where the corpus and the spec agree even if review on the rest runs long.

Flagged, deliberately not done

  • open-webui.ollamaBaseUrl's default http://ollama:11434 reads as an example wearing a default's clothes, as the issue says. Removing it is a behaviour change (the env var would go unset), not a presentation one. Worth its own issue.
  • items/n8n/listing.yaml says "Webhook, editor, and host URLs default to this deployment's own public https URL", but n8nHost binds a bare host — which format: HOSTNAME now makes visible. Correcting the prose would drag n8n's listing and blueprint to version 2 for a sentence whose meaning did not change.
  • Generated secrets stayed PRIMARY. There is a real case for SECONDARY — a minted value is nothing the user decides — but that is a product judgement about the install form rather than a reading of the spec, and the issue does not propose it. One line per secret if wanted.

Verified / not verified

Verified: full corpus 377/377 and npm run typecheck clean, against the live spec@main bundles the suite fetches on every run. Each item also validated individually. git diff --stat main..HEAD touches nine files and nothing else.

Not verified: anything in the capability phase, which this repository has no way to run — that the platform mints and injects the new postgres password at deploy, and that the six SELF_ADDRESS defaults still resolve the same endpoints platform-side. Also unverified, and unchanged by this PR: the PUBLIC_URL trailing-slash issue tracked in musher-dev/platform#2468, which still leaves label-studio's 403 in place.

🤖 Generated with Claude Code

https://claude.ai/code/session_019pvcJHCqQNnH1jRgFMYUWZ

justinmerrell and others added 7 commits September 8, 2026 18:14
…mezone

spec#79 (ADR 0018) made `platformDefault.type` REQUIRED with one member,
SELF_ADDRESS. n8n's three address inputs -- n8nHost, webhookUrl and
editorBaseUrl -- carried only a `source`, so the component stopped validating
against spec@main the moment that merged. This repository fetches the bundles
unpinned on every run, so the failure is real today, not on the next release.

The same change opened a presentation vocabulary the item could not use before,
and the fields it applies to are exactly the ones being touched:

`format` names what each value is. n8nHost binds PUBLIC_HOSTNAME, a host with no
port, which is now sayable as HOSTNAME; webhookUrl and editorBaseUrl are URLs
addressing a network service, which is ENDPOINT_URL rather than the residual
URI. timezone becomes TIMEZONE -- the field that motivated the proposal, and the
one entry here where the derivation does real work: component 6.4 obliges a
client to offer the IANA identifiers rather than a text box.

`ui.order` and `ui.prominence` fix an install form nobody chose. This item's
blueprint declares `parameters: {}`, so the form is derived, and 6.4's tiebreak
sorts a document declaring no order by input name as UTF-8 bytes -- which today
means editorBaseUrl, n8nHost, timezone, webhookUrl. That leads with the most
advanced field and splits the two URLs. Timezone is the only field a deploying
user has any reason to set, so it goes first, and the three custom-domain
overrides go behind a disclosure. SECONDARY is not isRequired restated: all four
inputs are optional, and three of them are also advanced.

`ui.examples` is never submitted -- 6.4 separates it from `default` on exactly
that point -- so n8nHost carries `n8n.example.com` to show the host-alone form
its label only implies.

The component moves to version 2. Per 7613630 a content change that reuses its
version resolves against the already-published record and is silently ignored at
sync. The listing and blueprint do not move: nothing here changes what the item
does, only how its form asks. Note the listing's "Webhook, editor, and host URLs
default to this deployment's own public https URL" is loosely worded now that
HOSTNAME makes the host/URL distinction explicit, but the sentence's meaning is
unchanged and correcting it would drag both documents to version 2 for prose.

Verified: `items/n8n` 25/25 against the live spec@main bundles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pvcJHCqQNnH1jRgFMYUWZ
…th the email

spec#79 (ADR 0018) made `platformDefault.type` REQUIRED with one member,
SELF_ADDRESS. publicUrl and csrfTrustedOrigins carried only a `source`, so this
component stopped validating against spec@main when that merged. The bundles are
fetched unpinned on every run, so the break is live rather than pending.

Same change, same file, the presentation vocabulary it opened:

publicUrl gets `format: ENDPOINT_URL`. It is a URL addressing a network service,
which is the specific member; URI is the residual one and would have been the
least informative true thing to say.

csrfTrustedOrigins gets no format at all, and a comment saying why. The value is
a comma-separated list, and every format member names a convention the whole
scalar follows -- "https://a,https://b" is not a URL. Component 6.4 binds a
client's rendering, so one told this was a single URL could normalise or trim it
and quietly corrupt a two-origin list. The constraint that actually matters here
-- exact origin, scheme included, no trailing slash -- has lived in a YAML
comment and a description since a2bf352; ui.examples is the first place it can
be stated in a form a client can use, so it carries both a single origin and a
two-origin value.

`ui.order` fixes an install form nobody chose. The blueprint declares
`parameters: {}`, so the form is derived, and 6.4 sorts a document declaring no
order by input name as UTF-8 bytes: adminEmail, adminPassword,
csrfTrustedOrigins, publicUrl, secretKey. That interleaves the two custom-domain
fields with the secrets and splits the pair the comment above them describes as
one decision. Ordered now as the item actually reads: adminEmail first -- it is
the only input in this component a person must type, with no generator, no
platformDefault and no default -- then the two generated credentials, then the
two custom-domain overrides behind a disclosure.

SECONDARY on those two and not on the secrets. The distinction 6.4 draws is not
isRequired restated: the platform has already computed the right value for
publicUrl and csrfTrustedOrigins and a deploying user should leave both blank,
whereas an auto-generated admin password is a value they will want to see and
copy.

The component moves to version 3. Per 7613630 a content change reusing its
version resolves against the already-published record and is silently ignored at
sync. Listing and blueprint stay at 2: the listing's claim that "the public URL,
and the origins the sign-in form is accepted from, both default to this
deployment's own https URL" is exactly as true as it was.

Verified: `items/label-studio` 25/25 against the live spec@main bundles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pvcJHCqQNnH1jRgFMYUWZ
spec#79 (ADR 0018) made `platformDefault.type` REQUIRED with one member,
SELF_ADDRESS. publicOrigin carried only a `source`, so this component stopped
validating against spec@main when that merged; the bundles are fetched unpinned
on every run, so the failure is live.

publicOrigin also gains `format: ENDPOINT_URL`. It is the https origin of this
deployment's own Control UI, written into gateway.controlUi.allowedOrigins at
first boot -- a URL addressing a network service, which is the specific member.
URI is the residual one and says less.

timezone gains `format: TIMEZONE`. This is the field the whole proposal was
argued from: openclaw and n8n hand-rolled the same IANA timezone twice, both
`type: STRING` with `default: UTC` and the intent stated only in prose. Component
6.4 now obliges a client to offer the identifiers 6.3 names, so the same
declaration that was documentation becomes a picker. The vocabulary is IANA's and
revised several times a year, so nothing is restated here -- 6.3 names where it
is published, and every mainstream runtime already carries the database.

`ui.order` fixes a form nobody chose. The blueprint declares `parameters: {}`, so
the form is derived, and 6.4 sorts a document declaring no order by input name as
UTF-8 bytes: gatewayToken, publicOrigin, timezone. That puts the custom-domain
override -- the one field here that should normally be left alone -- ahead of the
only field a deploying user has a reason to set. Ordered now as gateway token,
timezone, then publicOrigin behind a disclosure.

SECONDARY on publicOrigin alone. 6.4 draws it as distinct from isRequired: all
three inputs are optional, but only publicOrigin is one the platform has already
answered correctly and the user should leave blank.

The component moves to version 2. Per 7613630 a content change reusing its
version resolves against the already-published record and is silently ignored at
sync. Listing and blueprint stay at 1 -- the listing's claim that the public
origin is written into the allowlist automatically and overridden only for a
custom domain is unchanged.

Verified: `items/openclaw` 25/25 against the live spec@main bundles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pvcJHCqQNnH1jRgFMYUWZ
…es beside them

spec#79 (ADR 0018) added `HOSTNAME` and `TIMEZONE` to the format vocabulary and
gave `ui` an order, a prominence, examples and enum labels. Nothing here was
broken by it -- mlflow's two inputs validate unchanged -- but they are the two
plainest cases in the corpus of a value whose shape was stated only in prose.

backendStoreUri gets `format: CONNECTION_STRING`, not URI. The shipped default,
`sqlite:////mlflow/mlflow.db`, addresses no network service at all -- it is a
file -- so ENDPOINT_URL would be false for the value the item actually deploys.
SQLAlchemy's URI grammar is the backing store's own connection form, which is
that member's definition; URI is the residual member and would have been true
and useless.

artifactsDestination gets no format, deliberately. Its default `/mlflow/mlruns`
is a bare POSIX path with no scheme, so it is not a URI; the production values
are (s3://, gs://) but are not connection forms either. No member covers "path or
storage URI", and 6.4 binds a client's rendering, so the honest declaration is
none. The shape goes in examples instead.

ui.examples rather than an enum, which is the thing worth recording. The issue
this closes floats `enum` for backendStoreUri on the grounds that sqlite versus
Postgres is a choice rather than free text. It is the wrong tool three times
over. 6.4's derivation says a control for a non-empty enum MUST NOT offer a value
outside its members, and a SQLAlchemy URI's value space is unbounded -- host,
port, database, credentials, driver. It would falsify this item's own listing,
which advertises upgrading the two store URIs to Postgres and S3 as deploy
parameters. And the candidate members are templates, not values: a chooser
offering postgresql://user:pass@host:5432/mlflow hands the user something that
cannot be submitted as offered, which is the exact confusion `examples` was added
to remove -- 6.4 is explicit that an example is never submitted and a default is.
So the corpus still holds zero enums, and ui.enumLabels stays unused.

Both stay PRIMARY. They are required, they carry working defaults, and together
they are this item's entire configuration surface; hiding either behind a
disclosure would contradict the listing. `ui.order` only corrects the derived
form's byte order, which put the artifact store ahead of the metadata store it
depends on.

The component moves to version 2; per 7613630 a content change reusing its
version is silently ignored at sync. Listing and blueprint stay at 2 -- naming a
value's convention does not change what the item does, and taking the enum would
have been the one change here that made the listing stale.

Verified: `items/mlflow` 25/25, full corpus 377/377 against the live spec@main
bundles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pvcJHCqQNnH1jRgFMYUWZ
spec#79 (ADR 0018) opened a presentation vocabulary this item could not use.
Nothing here was broken by it; ollamaBaseUrl was simply one of the two plain
misses the corpus review turned up -- ENDPOINT_URL has existed since v1 and
http://ollama:11434 is the textbook case for it.

The judgement worth recording is the prominence one, because the obvious reading
is wrong. ollamaBaseUrl is optional and looks advanced, which is the shape of the
six platform-default inputs that do become SECONDARY in this change. It is their
opposite. Those six are advanced because the platform has already computed the
right value and a deploying user should leave them blank; this one's default
names a sibling `ollama` service that is NOT part of this deployment, so the
shipped value cannot work. A field whose default is knowingly wrong must not sit
behind a disclosure. It also has to stay directly under the switch that governs
it: both the input's own description and the listing tell the user to turn the
integration on and set the base URL, and a disclosure boundary between the two
breaks that sentence. A comment in the file says so, so the next author does not
quietly "fix" it.

`ui.examples` carries the public form the default does not. The issue this closes
reads the default itself as an example wearing a default's clothes, and it has a
point -- 6.4 separates the two on exactly this, an example is never submitted and
a default is. Removing it is a behaviour change though, not a presentation one:
the env var would go unset. Left alone deliberately, and worth its own issue.

`ui.order` is already what byte order gives, since the input names happen to sort
that way. Declared anyway, so the ordering survives a fourth input rather than
depending on a spelling accident.

The component moves to version 3; per 7613630 a content change reusing its
version is silently ignored at sync. Listing and blueprint stay at 1 -- and stay
true precisely because ollamaBaseUrl stayed PRIMARY. Precedent for a component
moving alone is acc69a8, which took this item from 1 to 2.

Verified: `items/open-webui` 25/25 against the live spec@main bundles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pvcJHCqQNnH1jRgFMYUWZ
postgres is the only item in this catalog that cannot be deployed without typing
something, and the reason is one input. postgresPassword is isRequired, is
isSensitive, and carries no generator -- while label-studio mints both
adminPassword and secretKey exactly the same way, and eleven of the thirteen
items already require nothing typed at all. It is the single place in the corpus
where a secret the platform could mint is handed to a person instead, and the
asymmetry is invisible until the whole corpus is read end to end.

Give it a generator. The shape is the house one, seen eleven times already:
byteLength and encoding. ALPHANUMERIC rather than HEX so the value survives being
pasted into a postgresql:// URL without percent-encoding, which matters more here
than anywhere else in the corpus because this item's whole purpose is to be wired
into something else. 32 bytes matches the other machine credentials -- litellm's
master key, meilisearch's master key, qdrant's API key -- rather than the 24-byte
tier the two human-facing admin passwords use.

The component schema already obliged the rest: the generator branch requires
isSensitive true and suppliedBy USER, both of which were set, and since spec#79
forbids a sibling platformDefault, which there is not. It also gains the
description it never had -- postgres was the only component in the corpus whose
inputs carried a label and nothing else -- and the three inputs gain a ui.order,
since the file is open and the derived form otherwise sorts them
postgresDb, postgresPassword, postgresUser by UTF-8 bytes. All three stay
PRIMARY: postgresDb is optional and ordinary, which is the distinction 6.4 draws
against the six advanced fields elsewhere in this branch.

This drags the listing, which is why all three documents move. "You supply the
database password at deploy" stops being true, so it is reworded to the phrasing
the other ten listings already use, and ERR_VERSION_MISMATCH requires the listing
and blueprint to agree, so the blueprint moves with it. The component moves for
its own reason: per 7613630 a content change reusing its version resolves against
the already-published record and is silently ignored at sync.

Not verified: that the platform mints and injects this at deploy the way the
other eleven generators are minted. The local suite has no capability phase, so
what is checked here is that the document is valid and internally consistent, not
that a live deployment comes up with a working superuser password.

Verified: `items/postgres` 25/25 against the live spec@main bundles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pvcJHCqQNnH1jRgFMYUWZ
… requires

Three synthetic components in the semantic-rule suite build a platformDefault
carrying only a `source`. Since spec#79 that is not a document any conforming
implementation would accept.

They do not fail, and would not have. assertReports runs `diagnose`, the semantic
checker alone; only the well-formed baseline goes through the fetched validator,
and that baseline has no platformDefault. So this is hygiene rather than a fix --
a fixture exercising a semantic rule should not, in passing, model a shape the
structural phase now rejects, or the next author reads it as the current form.

Verified: 377/377, typecheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pvcJHCqQNnH1jRgFMYUWZ
@justinmerrell
justinmerrell merged commit a057da4 into main Sep 8, 2026
1 check passed
@justinmerrell
justinmerrell deleted the fix/spec-adr-0018-install-form branch September 8, 2026 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec#79 requires platformDefault.type — three items break, and thirteen gain an install-form vocabulary

1 participant