Let a session compose with its environment instead of contradicting it - #31
Merged
Conversation
…ting it
The frozen CreateSession said an environment and a scratch spec together
were "contradictory and refused." The product has never agreed: rainier
new composes --env with --image, --egress, and a trailing command
("the environment supplies everything the flags don't", design §4.3),
and a bare rainier new names no image at all. Recomposing self-hosted
controld over the contract (gate O8) is what surfaced the conflict —
the branch could only refuse those flows or drop their fields silently.
The contract now states the rule the product implements, once, on
PortableSpec: an environment is a template and a session an instance
of it; every field that is set overrides, every field that is unset is
inherited. Image overrides the environment's (and forgoes its snapshot,
which was built from the environment's own image and setup); Cmd is the
session's, since an environment carries none; EgressAllow extends the
environment's list rather than replacing it, because that list is what
the environment needs to work; Repos keeps its nil/empty/list rule,
which the contract already composed this way. Validate refuses only
malformation. A scratch create with no image asks the host for its
default; a host that wants to forbid an override or require an image
does so in its own policy, not here.
controlapp's portableSpecFor layers accordingly, its zero-spec refusal
is gone, and createSpec decides whether setup runs by whether the row
boots the snapshot — so an image override gets the setup the snapshot
would have carried. The composition is pinned as a table in the public
contract's tests, through the service, and at dispatch.
This is a coordinated change to the frozen surface, made because the
first host composed against it showed the rule was a guess the product
contradicts. It makes the contract more permissive, which is the safe
direction for a pre-v1 surface.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiKRhqhzgJ1UDaLBynMyj3
jiashuoz
added a commit
that referenced
this pull request
Sep 2, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiKRhqhzgJ1UDaLBynMyj3
This was referenced Sep 2, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A coordinated change to the frozen
controlcontract, surfaced by the self-hosted recomposition (gate O8, plan #29):CreateSessionsaid an environment and a scratch spec together were "contradictory and refused," butrainier newhas composed--envwith--image,--egress, and a trailing command since Plan 4 (cmd/rainier/main.go:575, design §4.3), and a barerainier newnames no image at all. Recomposing controld over the contract could only refuse those flows or drop their fields silently.control.PortableSpecnow states the rule once: an environment is a template, a session an instance; every set field overrides, every unset field is inherited.Imageoverrides and forgoes the snapshot;Cmdis the session's (an environment carries none);EgressAllowextends the environment's list rather than replacing it;Reposkeeps the nil/empty/list rule the contract already composed this way.CreateSession.Validaterefuses only malformation (a repository reference naming no repository). No combination of environment and spec is refused; a scratch create with no image asks the host for its default. A host that wants to forbid overrides or require an image does so in its own policy — the contract says what is possible, not what a host allows.controlapp:portableSpecForlayers accordingly; the zero-spec refusal is gone;createSpecdecides whether setup runs by whether the row boots the snapshot, so an image override gets the setup the snapshot would have carried.TestCreateSessionComposesEnvironmentAndOverrides, a table), through the service (TestCreateSessionLayersOverridesOnTheEnvironment), and at dispatch (TestCreateSpecSendsSetupUnlessTheRowBootsTheSnapshot).One deliberate product refinement rides along: per-session egress now extends an environment's list instead of replacing it. The environment's egress is what it needs to work (its registry, its init hook's API); a session adding hosts is the intent behind the flag, and silently removing the environment's was the surprising behavior.
No exported identifier is added or removed; the public-control guard's allowlist is unchanged. Merge before the O8 branch, which then drops its interim D2/D11 refusals.
Verification
go test ./control ./controlapp -race -count=1./scripts/check-public-control.shmake verifygit diff --check🤖 Generated with Claude Code
https://claude.ai/code/session_01GiKRhqhzgJ1UDaLBynMyj3