Make it easier to add and stack docker stages to agent images #394
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Motivation
Bottle manifests should be able to add tools, users, packages, and other image customization on top of the image supplied by the agent provider without duplicating or replacing the provider Dockerfile.
The provider remains responsible for choosing the compatible base distro and establishing the prerequisites and runtime contract bot-bottle needs. A bottle may compose ordered customization layers between provider setup and final agent provisioning. Changing the base distro or bypassing provider assumptions remains the existing full-override
agent_provider.dockerfileuse case.Proposed manifest shape
Use
layersrather thanstages: these entries are ordered image build fragments, not Docker multi-stage targets.A layer's
dockerfilemay be an inline literal block or a path to a fragment in the repository. Names are used in diagnostics and should be unique within the resolved bottle.Build order and contract
The effective image is built in this order:
image.layers, in resolved manifest order;Providers must expose a named customization boundary/build target. The builder builds that target, applies every bottle fragment on top of the previous result, and then applies the generated finalization layer.
The finalization layer must not assume fragments preserved provider state. It restores or validates the expected runtime user, home, workdir, certificates, filesystem permissions, entrypoint/command contract, and any other provider-declared invariants needed to launch the agent.
For
extends:, parent layers are applied first and child layers are appended.YAML-subset support
The hand-written YAML subset currently rejects multiline block scalars. Add narrowly scoped literal scalar support for inline Dockerfiles:
|and|-, including their trailing-newline behavior;#comments, indentation, colons, and backslash continuations;serialize_yaml_subsetemit multiline strings as literal blocks and round-trip them correctly;>, anchors, aliases, tags, and unrelated full-YAML features unsupported.Security and build context
FROM; changing or bypassing the provider base is not part ofimage.layers.Caching and diagnostics
Cache identity must include, as applicable:
Build failures identify the bottle, layer name, and layer position. Each backend may use its native image/cache representation, but ordering and invalidation semantics must be consistent.
Acceptance criteria
image.layerslist with unique named entries.|and|-literal blocks without enabling folded scalars or other excluded YAML features.image.layers[], blank lines, Dockerfile comments, colons, indentation, backslash continuations, and trailing-newline behavior.extends:resolves parent layers first and appends child layers deterministically.FROMare rejected with a useful error.USER root, a changedWORKDIR, or a customENTRYPOINT.agent_provider.dockerfilefull-override behavior remains available for users who need to change the base distro or provider assumptions.@didericis-codex give me feedback on this issue/flesh it out more.
I agree with the direction. I'd make this an ordered list of bottle-owned image layers while keeping the provider responsible for the base distro and final runtime contract:
The finalization step matters because a fragment can change
USER,WORKDIR,ENTRYPOINT, certificates, or filesystem permissions. We should explicitly restore the provider's expected user/home/workdir and validate the resulting image rather than assume fragments preserve them.A possible shape:
Ordered stages also give
extends:straightforward semantics: parent stages first, then child stages. Each can become a cached intermediate image shared by the Docker, macOS-container, and Firecracker/Buildah backends.The main implementation constraint is that Dockerfiles cannot literally splice content into another Dockerfile. Providers therefore need to expose a named customization boundary or build target. The builder can build that provider target, apply each bottle fragment as
FROM <previous-stage>, then add the generated bot-bottle finalization layer. I'd keepagent_provider.dockerfileas the full-override escape hatch for changing distro or provider assumptions; stages should be the normal customization path.Details worth including in the acceptance criteria:
FROMin fragments unless bypassing the provider base is explicitly supported;extends:composition, cache invalidation, and fragments that leave hostile state such asUSER rootor a customENTRYPOINT.One naming thought: these are image layers/build fragments rather than Docker multi-stage targets in the usual sense, so
image.layersorimage.buildmay be less ambiguous thanstages.Overall: provider chooses the compatible base, the bottle composes custom layers, and bot-bottle reasserts the runtime contract. That gives the laser/tool-installation use case a concise path without making every bottle duplicate a provider Dockerfile.
@didericis-codex looks good. Agree with changing it to
layers. Also I think we'll need to modify the simpleyamlsubset we created/not sure we've made allowed for the syntax you used there yet. Evaluate how heavy a lift that is assuming I'm correct/we haven't added that yet, otherwise correct me.Confirmed: you're correct.
bot_bottle/yaml_subset.pyexplicitly rejects|/>block scalars today, so the inlinedockerfile: |example will not parse as-is.I would call this a small-to-medium, localized lift rather than a parser rewrite. The existing nested list-of-mappings shape already works; the missing piece is the multiline scalar itself. The care point is that the tokenizer currently strips blank lines, trailing comments, and trailing whitespace before parsing. For Dockerfile content, lines such as
# syntax=..., blank separators, continuations, and indentation need to be captured verbatim.My recommendation is to add only literal block scalar support initially:
|and probably|-for explicit trailing-newline behavior;>, anchors, tags, and the rest of full YAML out of scope;str, so manifest/schema consumers need no special scalar type;serialize_yaml_subsetto emit multiline strings as literal blocks;image.layers[], including blank lines,#comments, backslash continuations, colons, and trailing-newline behavior.That is likely one focused parser/serializer change plus tests. The main risk is regression in the shared YAML-subset parser (it is also used outside manifests), but literal blocks are syntactically explicit, so the new path can be isolated and existing inputs should remain unchanged.
Using only file-backed fragments would avoid this work, but since inline layers are the ergonomic core of the feature, I think the narrow
|implementation is justified. Separately, addingimage.layersto the manifest model/validation and implementing the image-building behavior is the larger part of issue #394.@didericis-codex RE #394 (comment)
Cool, update the issue/edit it and include the acceptance criteria you talked about.
Updated the issue body with the agreed
image.layersdesign and the acceptance criteria, including the narrow|/|-YAML-subset work, ordering andextends:semantics, finalization invariants, build-context and secret constraints, caching, diagnostics, and backend-parity tests.