Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
96379d6
feat: preview basic Compose builds in native config import
Oct 8, 2026
5934593
chore: reconcile basic-build preview with native readiness
Oct 8, 2026
4e5b031
chore: align build preview with completed compiler cleanup
Oct 8, 2026
e846691
feat: add retained basic build adoption proof
Oct 8, 2026
30fc85d
fix: refuse private inputs before retained build capture
Oct 8, 2026
5687b49
refactor: derive adoption claim version from manifest
Oct 8, 2026
3e82ef2
fix: project private retained build source fields explicitly
Oct 8, 2026
1606d17
test: narrow private mapper candidate assertion
Oct 8, 2026
2afdf2d
test: select local inheritance in retained source controls
Oct 8, 2026
0b10773
refactor: extract retained build validation predicates
Oct 8, 2026
508afe0
test: expose fixed retained reprepare stage diagnostics
Oct 8, 2026
527058d
refactor: isolate retained saved receipt correspondence
Oct 8, 2026
9c12860
chore: reconcile retained basic builds with current next
Oct 8, 2026
d93abe4
feat: reconcile retained builds with owned network admission
Oct 8, 2026
9e248d1
test: maintain retained basic build worktree acceptance
Oct 8, 2026
8ea4c9d
test: fence retained build adoption previews
Oct 8, 2026
611b9e8
test: simplify retained build fixture checks
Oct 8, 2026
3811d05
test: finish retained build fixture lint cleanup
Oct 8, 2026
0cef4ff
chore: reconcile retained build adoption with next
Oct 8, 2026
9b3e7ce
chore: reconcile canonical native fixture controls
Oct 8, 2026
a94fd58
fix: retain owned build image graph in adoption fixture
Oct 8, 2026
3bc063c
test: preserve fixed builder graph tuple types
Oct 8, 2026
38a545b
style: align retained build fixture control syntax
Oct 8, 2026
6a3087b
chore(native): reconcile retained build with plural bridge adoption
Oct 8, 2026
03968cd
merge: reconcile retained basic builds with completed jobs
Oct 9, 2026
a3cd379
test: qualify retained build fixture driver and COPY evidence
Oct 9, 2026
94b5a5a
test: preserve single-object build cleanup and lint continuity
Oct 9, 2026
9736e51
test(runtime): synchronize abandoned relay fence observation
Oct 9, 2026
0189e97
test(e2e): expose closed retained-job start substages
Oct 9, 2026
104c916
Merge commit 'fe06bfc14b86c4313015a533bfd2c15c95dc9a47' into feat/nat…
Oct 9, 2026
a372aca
test: preserve retained build parent presence and compose labels
Oct 9, 2026
7f75476
test: recheck captured compose release during image cleanup
Oct 9, 2026
41ff785
test: sort retained build fixture imports
Oct 9, 2026
91d1bda
Merge commit 'c7057c059767e201753bb54a21660eae2c2908a5' into feat/nat…
Oct 9, 2026
0757e88
test: preserve retained build recovery-stop admission
Oct 9, 2026
e4c043f
Merge commit '7c810a55772603f7d1daf44aa82d51c382c097b1' into feat/nat…
Oct 9, 2026
cf0bf3a
test: align retained build diagnostic formatting
Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

103 changes: 102 additions & 1 deletion docs/reference/native-compose-adoption.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,106 @@ same-identity byte repair, separate rollback and
exact owned cleanup. It requires the current compiled CLI and companion compiler;
it does not qualify completed jobs or container recreation.

## Retained basic builds

The distinct version 9 owner handles existing basic build-only services with
qualified named data volumes and the existing default bridge. Context, relative
Dockerfile and target use the closed import mapping, with current local files
additionally verified. A service must omit `pull_policy`: explicit `build`
requires builder execution and cannot be satisfied by starting an old image.
The owner never builds, pulls, creates a container or substitutes an image during
the format switch or retained execution. Explicit rebuild/recreation requests
refuse. Image-only binding APIs and versions 1–5 retain their prior contracts.
Versions for custom networks, completed jobs and retained files are separate;
their combinations with this first build proof refuse, as do profiles, readiness,
managed/generated inputs, typed locals and literal-dollar build paths.
Pure preview can map a qualified custom bridge alongside a basic build; retained
build adoption refuses that intersection before opening context files, including
inactive workloads.

Included context files, the Dockerfile, optional root and Dockerfile-specific
ignore files, and their safe filesystem identities are privately pinned. The
names-only private env/local layout refusal precedes the context walk and is
rechecked afterwards; known private material added mid-walk is never opened as
an included file. A Dockerfile-specific ignore file takes precedence, while presence and bytes of
both files remain bound. The pinned `@balena/dockerignore` Moby port handles only
the qualified case-sensitive grammar: literal normalized paths, `!` negation,
bare `**`, blank lines and comments. Other globs, escapes, BOMs and ambiguous
paths refuse. Every possible adopted-owned path must be excluded by the effective
rules, including Git markers, `.hack/.internal`, `.hack/.branch` and the switched
authored files. Excluded subtrees are not read. Parent negations include
descendants: `**` followed by `!.hack` does not isolate future private outputs.
Such a context refuses unless later literal exclusions close those paths.
Root and `.hack` contexts can qualify through these exact exclusions.

The maintained `native-compose-adoption-build-worktrees` acceptance explicitly
requires Buildx's default Docker driver, `DOCKER_BUILDKIT=1` and
`compose build --builder default` for fixture bootstrap. It never creates or
bootstraps a separate builder. Its private fixed-stage evidence retains the
complete fatal-decoded synthetic COPY reply before applying the unchanged file
and hash oracle. This separates builder qualification from source/image guards;
it does not infer which builder caused an earlier failure or prove cache ownership.

One acquisition is limited to 16 builds, depth 32, 256 captured entries, 4096
directory names, 16 MiB of bytes and a 48 KiB private proof; the existing stable
file owner also limits each file to 1 MiB. Included byte, identity or mode changes,
included additions/removals, ignore presence changes and unsafe paths refuse.
Same-inode exact byte repair can restore a saved proof; it does not repair a
strict prepared authored-source timestamp or make editor races atomic.

The selected read-only Compose query supplies each original image reference.
Container IDs and birth, image IDs and birth, and current tag resolution must
match the privately saved observation, along with the existing config hashes,
mounts, network identity and volume creation identities. No image environment,
command or layer contents are read. These facts attest the current retained
image and current included source separately; they do not establish which source
historically built that image. Missing images or retargeted tags refuse before
effects. No image ownership for removal is granted by this proof.

Preparation, dry-run and saved execution use the same closed owner. Public output
contains field provenance and counts, never context hashes, image references or
private capabilities. Version 9 requires a finite remaining mutation deadline,
the whole original selection and the existing signal/process-group owner.
Starts, stops and recovery consume original IDs; source or image drift retains
pending evidence. Rollback restores the exact original authored inputs after
verified stop and keeps their original data. Older upgraded owners that know
only versions 1–5 refuse the new proof; this is not a universal old-launcher
fence. Current Moby/BuildKit ignore parity and maintained two-worktree SQL,
image/ID/birth, recovery and rollback acceptance qualify this slice separately
from pure preview and synthetic model controls. Full NC04 remains open.

The maintained `native-compose-adoption-build-worktrees` scenario requires
explicit selection. It bootstraps two disposable Postgres images, checks the
complete `COPY .` projection for root/Dockerfile-specific and default `.hack`
contexts, and then permits only captured metadata queries and journaled original-ID
starts/stops. Format switch and saved consumption cannot reach a builder. Its
source and candidate drift controls retain pending ownership, preserve the other
worktree's SQL row, and restore both original configurations through rollback.
The fixture issues a recovery-stop transport capability only for the exact
`down --recover --json` invocation. It can stop the original IDs while the active
generation retains its interrupted start journal; publication, generation,
complete service selection and daemon checks still precede each effect. Private
per-invocation evidence records closed operation/substage names and CLI exit/code
classifications before caller assertions, without reply values or arbitrary errors.
Exact fixture image removal requires its captured new ID/birth, sole tag, fixture
label, unchanged daemon and no remaining container references. A local final
image may expose one digest for that exact repository and captured image ID;
other digest aliases refuse. The fixture privately journals the exact new
image graph after each bootstrap build. Only fixture-labelled, untagged parents
on its complete chain to the captured original base qualify for disposal, in
child-before-parent order with nonforce `image rm --no-prune`. A builder exposing
no parent qualifies only its single final object. Unexplained new images, foreign
labels or references retain the failed fixture. Only an absent `Parent` key
defaults to empty; an explicit empty parent is preserved and malformed present
values still refuse.
The known Compose project, service and version labels must match the exact
fixture project, `db` service and selected Compose release. Other label names
remain refused. Full original image inventory
and tags must be restored; inventory bounds are not relaxed for new objects.
General builder cache is retained; cache reclamation and historical image-from-source provenance remain
unqualified. Fixture source/model checks alone do not establish live builder or
data-preservation acceptance.

The version 4 typed-local slice reads optional `.hack/hack.local.json` at the
selected checkout and verified inherited primary in the same issued private source
acquisition. It accepts only `schema_version: 1` and an optional `environment`
Expand Down Expand Up @@ -227,7 +327,8 @@ shared network ID nor a partial member observation can stand in for the other
bridge. Engine ID, resource inventories and source
bytes are rechecked, and acquisition compares two complete private observations.
The Docker routing environment is captured for later comparison. Queries never
request container environment or image configuration. This retained-resource authority
request container environment or full image configuration; version 9 additionally
reads the minimal image reference, ID and birth described above. This retained-resource authority
keeps the original bridge and container IDs through stop, saved restart,
recovery and rollback; it does not authorize a later newly created native
generation to take over those identities. That transition needs a separate
Expand Down
6 changes: 4 additions & 2 deletions docs/reference/native-config-import.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,10 @@ workload's `pull_policy: build` is a separate supported acquisition requirement.
Combined `build` and `image` refuse because the native model requires exactly one
source. Invalid builds cannot fall back to an authored image or a default policy.

This expands read-only preview only. Retained-container adoption still uses its
separate image-only mapping and refuses builds. The mapper runs no Compose
This expands read-only preview only. The image-only retained baseline remains
closed. A separate [proof-bearing basic-build owner](native-compose-adoption.md#retained-basic-builds)
qualifies current included source and exact existing images before adoption; a
complete pure preview never supplies that authority. The mapper runs no Compose
normalization, builder or runtime command, and does not validate Dockerfile
contents, path existence or filesystem identity. The authoritative compiler still
must validate the whole private candidate before a complete CLI preview; actual
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"typescript": "^5"
},
"dependencies": {
"@balena/dockerignore": "1.0.2",
"@charmland/lipgloss": "2.0.0-beta.3-0e280f3",
"@clack/prompts": "1.0.0-alpha.9",
"elysia": "^1.4.9",
Expand Down
5 changes: 4 additions & 1 deletion src/commands/config-adopt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,10 @@ async function adoptPrepared(
if (opts.signal.aborted) {
throw new Error("Legacy adoption cancelled; values omitted.");
}
if (legacyComposeRetainedOrdered(input.retainedPlan)) {
if (
legacyComposeRetainedOrdered(input.retainedPlan) ||
input.retainedBuild
) {
return await runLegacyComposeRetainedOperation({
input,
operation: "stop",
Expand Down
94 changes: 88 additions & 6 deletions src/lib/native-compose-adoption-binding.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { resolve } from "node:path";
import { isRecord } from "./guards.ts";
import {
acquireLegacyComposeBuildSource,
type LegacyComposeBuildSourceProof,
} from "./native-compose-adoption-build.ts";
import {
inspectLegacyComposeRetainedBuildImages,
type LegacyComposeRetainedBuildImage,
} from "./native-compose-adoption-build-images.ts";
import {
legacyComposeAdoptionCandidateSupported,
legacyComposeAdoptionLayoutSupported,
Expand All @@ -8,6 +16,7 @@ import { retainLegacyAdoptionLocalRefusal } from "./native-compose-adoption-loca
import {
type LegacyComposeStorageIntent,
planLegacyComposeAdoption,
planLegacyComposeRetainedBasicBuildAdoption,
} from "./native-compose-adoption-plan.ts";
import {
hasLegacyComposeGeneratedSources,
Expand Down Expand Up @@ -858,6 +867,10 @@ export type LegacyComposeAdoptionBinding = {
readonly compose: NativeConfigImportSourceIdentity;
};
readonly projection?: Readonly<ProjectedPreparation>;
readonly build?: {
readonly source: LegacyComposeBuildSourceProof;
readonly images: readonly LegacyComposeRetainedBuildImage[];
};
}>;
};
function translate(error: unknown, signal?: AbortSignal): never {
Expand Down Expand Up @@ -886,12 +899,36 @@ function translate(error: unknown, signal?: AbortSignal): never {
* consume this binding explicitly and recheck it on the same engine under its
* lease. Repeated checks cannot atomically freeze Docker or external editors.
*/
export async function acquireLegacyComposeAdoptionBinding(input: {
type BindingSelection = {
readonly projectRoot: string;
readonly signal?: AbortSignal;
readonly timeoutMs?: number;
readonly binary?: string;
}): Promise<LegacyComposeAdoptionBinding> {
};
export async function acquireLegacyComposeAdoptionBinding(
input: BindingSelection
): Promise<LegacyComposeAdoptionBinding> {
return await acquireBinding(input, "image-only");
}

/** Distinct current-source/image proof; ordinary image-only admission is unchanged. */
export async function acquireLegacyComposeRetainedBasicBuildBinding(
input: BindingSelection
): Promise<LegacyComposeAdoptionBinding> {
return await acquireBinding(input, "basic-build");
}

/** Preparation self-selects a qualified owner; no caller-provided binding or bypass is accepted. */
export async function acquireLegacyComposeAdoptionPreparationBinding(
input: BindingSelection
): Promise<LegacyComposeAdoptionBinding> {
return await acquireBinding(input, "preparation");
}

async function acquireBinding(
input: BindingSelection,
purpose: "image-only" | "basic-build" | "preparation"
): Promise<LegacyComposeAdoptionBinding> {
let signal: AbortSignal | undefined;
try {
const selected = selection(input);
Expand All @@ -909,22 +946,34 @@ export async function acquireLegacyComposeAdoptionBinding(input: {
if (!source.ok) {
refuse("E_LEGACY_COMPOSE_BINDING_UNSUPPORTED");
}
const planned = planLegacyComposeAdoption({
const ordinary = planLegacyComposeAdoption({
configText: source.configText,
composeText: source.composeText,
});
const basic =
purpose === "basic-build" ||
(purpose === "preparation" && !ordinary.intent);
const planned = basic
? planLegacyComposeRetainedBasicBuildAdoption(source)
: ordinary;
const intent = planned.intent;
if (!intent) {
refuse("E_LEGACY_COMPOSE_BINDING_UNSUPPORTED");
}
const buildSource = basic
? await acquireLegacyComposeBuildSource({ source, signal })
: undefined;
const mapped = mapLegacyNativeStorageAdoption({
configText: source.configText,
composeText: source.composeText,
});
const candidate = mapped.candidate;
const candidate = buildSource?.candidate ?? mapped.candidate;
const jobFamily =
candidate && legacyComposeRetainedPlan(candidate).requiresV7 === true;
if (jobFamily && !legacyComposeAdoptionCandidateSupported(candidate)) {
if (
jobFamily &&
(buildSource || !legacyComposeAdoptionCandidateSupported(candidate))
) {
refuse("E_LEGACY_COMPOSE_BINDING_UNSUPPORTED");
}
let projection: LegacyComposeAdoptionProjection | undefined;
Expand All @@ -942,7 +991,7 @@ export async function acquireLegacyComposeAdoptionBinding(input: {
signal,
})))
) {
if (jobFamily) {
if (buildSource || jobFamily) {
refuse("E_LEGACY_COMPOSE_BINDING_UNSUPPORTED");
}
projection = await LegacyComposeAdoptionProjection.acquire({
Expand All @@ -964,6 +1013,9 @@ export async function acquireLegacyComposeAdoptionBinding(input: {
await projection.assertFresh({ signal: selectedSignal });
return;
}
if (buildSource) {
await buildSource.assertFresh({ signal: selectedSignal });
}
if (await hasLegacyComposeGeneratedSources(root, selectedSignal)) {
refuse("E_LEGACY_COMPOSE_BINDING_CHANGED");
}
Expand All @@ -988,6 +1040,14 @@ export async function acquireLegacyComposeAdoptionBinding(input: {
timeoutMs,
composeFiles: projected?.composeFiles,
});
const buildImages = buildSource
? await inspectLegacyComposeRetainedBuildImages({
binding: baseline,
composeFile: baseline.composeFile,
signal,
timeoutMs,
})
: undefined;
freezeImportValue(baseline);
const assertFresh = async (current: {
readonly projectRoot: string;
Expand Down Expand Up @@ -1019,6 +1079,19 @@ export async function acquireLegacyComposeAdoptionBinding(input: {
if (JSON.stringify(observed) !== JSON.stringify(baseline)) {
refuse("E_LEGACY_COMPOSE_BINDING_CHANGED");
}
if (
buildImages &&
JSON.stringify(
await inspectLegacyComposeRetainedBuildImages({
binding: observed,
composeFile: observed.composeFile,
signal: currentSignal,
timeoutMs,
})
) !== JSON.stringify(buildImages)
) {
refuse("E_LEGACY_COMPOSE_BINDING_CHANGED");
}
await source.assertFresh({ signal: currentSignal });
await layoutSupported(currentSignal);
cancelled(signal);
Expand Down Expand Up @@ -1050,13 +1123,22 @@ export async function acquireLegacyComposeAdoptionBinding(input: {
binding: baseline,
sourceFiles: source.sourceFiles,
...(projected ? { projection: projected } : {}),
...(buildSource && buildImages
? {
build: Object.freeze({
source: buildSource.proof,
images: buildImages,
}),
}
: {}),
};
for (const key of [
"configText",
"composeText",
"binding",
"sourceFiles",
"projection",
"build",
]) {
Object.defineProperty(result, key, { enumerable: false });
}
Expand Down
Loading
Loading