Skip to content

settings: a cluster says what it creates - #39

Merged
github-actions[bot] merged 1 commit into
developfrom
feat/a-cluster-says-what-it-creates
Sep 12, 2026
Merged

github-actions[bot] merged 1 commit into
developfrom
feat/a-cluster-says-what-it-creates

Conversation

@traviswu-bigstack

Copy link
Copy Markdown
Contributor

What

The instance profile becomes an operator setting, so create_instance can work.
ADR 0016 slice 3.

instance-profile.json beside the action level and the credential, holding
flavor, image, network and an optional project. It loads through the
configure path slice 1 introduced, which means both of that slice's test
layers apply to it.

Why

Lab validation on the 1cc R630 found ConfigureInstanceProfile had no
non-test caller
. No flag and no file supplied a profile, so every create
refused with "this agent has no flavor configured; it cannot create anything
until its operator sets one"
— a good message naming something an operator
could not act on.

It shipped documented, exercised by tests, and unreachable, because every test
that found it green built its own Registry and so never asked whether
anything builds one in production. Slice 1 (#38) made that catchable; this is
the first new setting through it.

ADR 0016's own slice table names the test: a written profile reaches the
registry; absent refuses a create naming the file.

Reviewer notes

Mode policy — the action level's rule, not the credential's. Refused if
group- or world-writable, unconstrained on readability. The profile holds no
secret: flavour, image and network are ids of shared cloud resources, and an
operator should be able to read what their own agent creates without root. But
whoever can write it chooses the image, and choosing the image is choosing what
code runs on the instance. Write access is the escalation; read access reveals
nothing.

A partial profile is broken, not unset. Absent is the ordinary state of a
cluster that has not opted in and reports "not configured". A file naming two
of three is something the operator wrote and this agent cannot honour: the
setting is not applied, the startup line says which field is missing, and
creates refuse. ADR 0016's rule — a broken setting disables what it enables and
never more — with the message naming the field rather than the file in general.

Unknown fields are rejected rather than ignored. The likely typo is
flavour, the spelling this codebase's own prose uses. Ignoring it would leave
Flavor empty and refuse every create with a message about a flavour the
operator can see in their own file.

project is carried but not cross-checked at startup. nova takes the
project from the credential's scope, so the field is a declaration for the
operator, not a value the request sends. A local file-to-file check against the
credential's project would compare two declarations, neither authoritative;
the check that matters already exists and is authoritative — internal/openstack
refuses outright if Keystone reports a different scope. Adding a third statement
to catch a disagreement between the other two is not worth a Keystone call at
boot.

Validation is syntactic at load, semantic at use. Confirming a flavour
exists needs the credential and a network call, which configure does not make.
So an operator learns at startup that their file parses, has all three fields
and is not world-writable — and does not learn until their first create that a
uuid does not exist, is in another project, or names a network they cannot
attach. The startup line prints the three values, so a transposed id is at least
visible.

executorFilled becomes a map from placeholder to the file that supplies
it
, so the refusal names instance-profile.json. {dc} carries no file until
cube-cos-api access becomes a setting (slice 4) and falls back to the shorter
wording rather than inventing a path.

Registry.Profile() is ConfigureInstanceProfile's observable counterpart,
added for the reason Writers() was in slice 1: without it, a profile on disk
reaching the registry is visible only by creating against a real cloud, so
nothing could assert the wiring without a network — which is how this defect
survived.

Break and restore

break result
profile removed from settings layer 1 fails naming the file an operator would write for nothing; layer 2 also fails
declared and loaded, apply dropped layer 1 passes, layer 2 fails — the discriminating case
one placeholder loses its file attribution passed at first — see below

The third break did not fail anything. TestAnUnconfiguredCreateRefusesNamingTheFile
resolves a real create_instance, and a Post tool's body is a map — so the
resolver reaches its placeholders in whatever order Go iterates, and any one of
the three satisfied "the message names the file". Found by breaking it, not by
reading it. Added TestEveryProfilePlaceholderNamesTheProfileFile, which checks
each placeholder individually and is deterministic; the break then fails it.

Not in this slice

The SaaS-side approval statement still says "flavour, image, network and
project are the cluster's own settings, not chosen by the assistant"
rather
than naming values. It is authored in cube-ai-advisor, which does not know the
profile — the executor holds it. Making it name real values means publishing the
profile to the SaaS, which is a design change, not a rename.

Docs

bigstack-handbook — as-built beside the other slices in
kb/cube-ai-advisor/architecture/read-only-plane-end-to-end.md.

ConfigureInstanceProfile had no non-test caller. No flag and no file supplied
a profile, so every create refused with "this agent has no flavor configured;
it cannot create anything until its operator sets one" -- a message naming
something the operator could not act on. Lab validation on the 1cc R630 was
the first thing to notice, because every test that found the profile green
built its own Registry.

The profile becomes a setting in the list slice 1 introduced, so it loads
through the one path and the two layers that catch an unloaded or unwired
setting apply to it. instance-profile.json holds flavor, image and network,
with project optional; it is refused if group- or world-writable and not if
readable, the action level's rule rather than the credential's -- it holds no
secret, but whoever may write it chooses the image, and choosing the image is
choosing what code runs.

Unknown fields are rejected rather than ignored. The likely typo is "flavour",
the spelling this codebase's own prose uses, and ignoring it would leave the
field empty and refuse every create with a message about a flavour the
operator can see in their file.

executorFilled becomes a map from placeholder to the file that supplies it, so
an unconfigured create names instance-profile.json rather than leaving an
operator to find it. {dc} carries no file until cube-cos-api access becomes a
setting, and falls back to the shorter wording rather than inventing a path.

Registry.Profile is ConfigureInstanceProfile's observable counterpart, added
for the reason Writers was: without it a profile on disk reaching the registry
is visible only by creating against a real cloud, which is how this shipped
documented, tested and never called.

Signed-off-by: Travis Wu <travis.wu@bigstack.co>
@traviswu-bigstack
traviswu-bigstack force-pushed the feat/a-cluster-says-what-it-creates branch from e416d85 to 183bc0c Compare September 12, 2026 04:55
@traviswu-bigstack
traviswu-bigstack marked this pull request as ready for review September 12, 2026 04:58
@traviswu-bigstack traviswu-bigstack added the done Apply to a PR to trigger the fast-forward merge label Sep 12, 2026
@github-actions
github-actions Bot merged commit 183bc0c into develop Sep 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

done Apply to a PR to trigger the fast-forward merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant