Skip to content

feat(native): merge train batch 6, native image builds (#583, #606, #607, #612, #628, #629, #630) - #639

Merged
roodboi merged 215 commits into
nextfrom
claude/train-batch-6
Nov 7, 2026
Merged

roodboi merged 215 commits into
nextfrom
claude/train-batch-6

Conversation

@roodboi

@roodboi roodboi commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

What this is

Merge train batch 6 into next: the native image builds stack and its fixes, as one batch. Builds stay off by default: they run only with HACK_NATIVE_BUILDS=1, which native up, restart and doctor all check. Each component's reviewed head is merged with git merge --no-ff, in stack order. Integration conflicts are resolved in the batch's merge commits, described below. Hosted CI on this branch is the full-suite gate for the combined result.

PR Title Linear Head included Review
#583 feat(native): pin a BuildKit client and scan Dockerfile image references HACK-1270 f21d5a04 Codex code + security: no findings; 0 open threads
#606 feat(native): build Compose images in the guest engine (runtime build-image) HACK-1270 460ddff2 Codex code + security: no findings; 0 open threads
#607 feat(native): admit build storage, budget build cache and reclaim superseded build images HACK-1270 9f33a1d0 Codex code + security: no findings; 0 open threads
#612 feat(native): build Compose build services in native up behind HACK_NATIVE_BUILDS HACK-1270 7f42e328 Codex clean at 8ec7460e; 7f42e328 adds #628's squash (re-review requested)
#628 fix(native): admit native build fields and name services in build refusals HACK-1925 f07ea583 Codex code + security clean; already squashed into #612's branch
#629 fix(native): keep the VM responsive during image builds and stop a silent guest HACK-1924 a0e3e797 Codex code + security: no findings; 0 open threads
#630 fix(native): restore a sourceless graph after its project files were touched HACK-1914 a93db842 Codex clean at 009989c7; a93db842 only merges #629 into it (re-review requested)

Security properties in the merged code

  • Build arguments: they reach the BuildKit client only through a private 0600 bake definition, never its environment. The client environment is cleared to an allowlist. Names must be plain identifiers, and BuildKit-interpreted names such as BUILDKIT_*, BUILDX_* and SOURCE_DATE_EPOCH are refused. A build argument equal to a managed secret value is refused without echoing the value. Build arguments are not secrets, matching Docker.
  • Source policy: a source policy from build_policy::document, set through EXPERIMENTAL_BUILDKIT_SOURCE_POLICY in a private file, applies to every build. It allows only the scanner's digest-pinned base images and denies other images, oci-layout, http, https, git and blob sources. Pinned bases whose config has OnBuild triggers are refused.
  • Dockerfile scanner:
    • remote ADD sources are refused, including the JSON form, multiple sources and variables;
    • JSON-form arguments are read as BuildKit reads them, and a lenient-decode differential fails closed;
    • heredocs are recognized as BuildKit's lexer does, including whitespace after <<;
    • a stage named scratch is refused;
    • ARG names must be plain identifiers;
    • platform arguments must resolve to linux/arm64;
    • expanded values are size-bounded.
  • Build output: forwarded output is sanitized and then redacted, including Go-quoted and JSON-escaped forms and values split across a line cut. It is bounded by size.

Integration changes

Validation on this branch (6337e0ed)

  • Clean CLI typecheck: pass. Biome on the 20 changed files: no errors.
  • RUSTUP_TOOLCHAIN=1.97.1 bun run check:local: pass. All-features clippy -D warnings (Zig 0.15.2): pass.
  • Cargo --lib: 1346 pass. --features native-config-plan provider::graph: 673 pass. Contracts: project_contract 66, compose_fields_contract 10, interpolation_budget_contract 1, local_contract 14.
  • bun run test:models (TLA+ 1.7.4): exit 0, 172 controls verified.
  • Focused bun tests, bounded runner, the changed test files: 242 pass, 0 fail.
  • docs:prepare + docs:check, privacy:check, gitleaks over origin/next..HEAD: pass.

Host evidence: 11 of 11 builds passed on an M5 host with the relay (#629). Still open: the source-policy host check (a pinned build succeeds, and a base trigger that pulls an unpinned image or fetches https fails with "denied by policy") and the bake --allow fs.read / metadata shape on the pinned buildx 0.34.1. Both are listed in #606.

After this merges, each component PR is closed with a link to the merge commit.

🤖 Generated with Claude Code


See it in action
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.

roodboi and others added 30 commits November 5, 2026 11:11
A 14-service retaining cleanup sent every container stop at once. Each stop
holds one engine connection for up to its grace period, and the batch failed
at the connect stage while the provider host held most of its descriptor
limit, leaving the graph in cleanup-intent with containers running.

The stop batch now runs through at most four workers that share one queue
and are all joined before it returns. A connect that fails or times out sent
no request, so it is retried with capped backoff inside the batch deadline
and a 30-second window from that stop's first connect failure. Once one stop
exhausts the window, later connect failures return at once. Response,
timeout and transport failures are still never retried. A stop whose worker
panicked or never ran is reported with the worker stage.

The existing stop budget already covers fully serialized grace periods, so
bounded dispatch fits it. A partial failure keeps the reached stops, and the
next cleanup stops only the unreached instances.

Refs HACK-1302

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Native Compose review no longer aborts on an unsupported port. Each port
or expose entry outside the modeled subset becomes an invalid_port error
diagnostic at services.<name>.ports[<i>] (or .expose[<i>]), collected with
the rest of the review; the entry is omitted from the plan so authored text
is never echoed, and enrollment stays incompatible.

Import preview reviews each environment entry on its own: only an
interpolated or ambiguous entry is refused at its pointer, and literal
siblings stay mapped.

Doctor's compose backend check names the isolatedNetworks adaptation for
an external network, warns (with nativeUpRefusals: ["basic_build"]) for a
basic build that native image-project up refuses, and the config
validate/plan input-family refusal now names the next step.

Refs HACK-1907

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`hack doctor --migrate-env-config` deleted the committed hack.env.json and
edited the tracked Compose file in non-interactive runs, because every step
accepted its default. A run that can't prompt now refuses with
E_INTERACTIVE_REQUIRED before any fix step writes, naming each git-tracked
file it would delete or edit, unless --accept-tracked-env-changes is passed.
Interactive runs list those files and ask per step; tracked legacy files get
their own removal prompt.

Native doctor now runs the same project-file migration instead of pointing
to v4. The implicit migration (env exec, sessions, lifecycle env) reports each
moved key value-free and removes a fully delivered, untracked legacy dotenv
file that Compose no longer names, so no plaintext copy stays behind.
Refusals no longer claim .hack/.env files exist.

Refs HACK-1904

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bels and nested defaults

Native Compose review and execution now accept four Compose forms that
previously refused the whole project:

- depends_on.<svc>.required: false. An optional edge to a service the
  selected profiles disable is skipped with optional_dependency_inactive,
  as in Compose. An active optional target is enforced like a required one
  (fail closed) and reported as optional_dependency_enforced. restart: true
  and missing targets keep their refusals.
- stdin_open and tty. stdin_open keeps an open, unattached stdin
  (OpenStdin true, AttachStdin/StdinOnce/Tty false). tty is accepted, but
  no pseudo-terminal is allocated (terminal_not_allocated warning), so logs
  stay multiplexed.
- caddy hostnames with caddy.tls internal and no caddy.reverse_proxy. The
  Compose router proxies nothing for this form, so native records it as
  inert: no route, hostname claim or relay, an inert_route_declaration
  warning, and graph admission accepts exactly those two labels. Runtime
  host metadata still advertises the hostnames, as on Compose, but the TS
  route map and additionalHostnames adaptation exclude inert services.
  Every other partial or extra caddy label set is still refused.
- Nested interpolation defaults such as ${A:-${B:-}}. Inner :- and :+
  expressions resolve recursively, up to four levels, within the shared
  1 MiB budget. Unselected words are validated without requiring inputs.
  ?, unbraced $NAME and $(...) inside a word stay refused. A reference to
  a managed value of the same service now refuses with
  execution_environment_owner instead of resolving to a default or empty.

New plan fields are omitted when unused, so existing plan IDs do not
change. Older bundles refuse plans that use them (deny_unknown_fields).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Native down always retained data, and a Compose edit that changes graph
resources left retained data that restore cannot adopt, with no frontend
way to start fresh. `hack down --remove-data` (Compose parity:
`down --volumes`) now discards it explicitly:

- Native Compose-file projects only. Authored native-config projects and
  the Compose backend refuse the option as a usage error.
- The project must be stopped; a running graph refuses and names
  `hack down` first. Removal runs no lifecycle hooks and never stops
  compute itself.
- It asks once after a read-only check of the exact saved run; `--yes`
  skips the question and `--json` requires `--yes`
  (E_INTERACTIVE_REQUIRED before any runtime request otherwise).
- Under the restart lock (a pending restart refuses), after the original
  frontend finalization, it requests `graph cleanup --remove-data`, which
  deletes only receipt-owned volumes behind a durable cleanup intent and
  releases shared dependency caches without deleting them.
- The run mapping is retired only after the receipt is `removed`, every
  owned volume is recorded and observed absent and caches are only
  released. A replaced mapping is never removed.
- Any failure keeps the mapping; a rerun resumes a durable cleanup intent
  (compute absent) or an already removed receipt without replaying
  deletion. Refusals carry at most a closed native code.

Restore refusals that say retained data was not adopted now name the two
remedies: revert the change, or run `hack down --remove-data`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Native image-project up and restart now resolve every HACK_NATIVE_*
selection from three places, once per command:

- the committed .hack/native/adaptation.json, plus optional
  adaptation.<env>.json, holding the existing closed adaptation schema;
- a per-machine settings file outside every project share,
  ${XDG_CONFIG_HOME:-$HOME/.config}/hack/native/<project>/settings.json,
  for the share grant, AWS profile, host dependency files, HTTPS,
  prepared base and approved hosts (closed v1 schema, 0700 directory,
  0600 owner-only regular file, refused inside the project root);
- the existing variables, which still win per setting, whole-value.

Per-env files are picked by the run's effective env inside native start
and restart preflight, so fresh, retained and restarted runs select the
same files. Without --env the Compose-backend pre-check counts a network
as isolated only when every candidate adaptation isolates it.

Native doctor adds a value-free "native settings" check: the origin of
each setting (env, machine, project, unset) and closed warnings for
configuration native does not apply (internal DNS/TLS, internal
extra_hosts, routes without probes, ambient COMPOSE_PROFILES, an AWS
region without a profile, unread .hack/native files, missing share
grant).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ations

The private graph environment envelope gains version 2 with a `files` map:
per service, one to four `{name, target, mode: "0400", content_b64}`
members. Version 1 with files, version 2 without them, and malformed,
duplicate or unknown member fields refuse, so an older runtime refuses
file delivery instead of dropping it.

Members are decoded into zeroized memory, bounded to 16 KiB each and to
the existing 32 KiB per-service payload together with values, and staged
in the same per-container tmpfs allocation as values.json: a root-only
0700 `files` directory with each member owned by the container's numeric
UID at mode 0400. They are bound read-only at their declared absolute
targets. Targets under /proc, /sys, /dev, /run and /var/run, nested or
duplicate targets, and any compiled mount or tmpfs that equals, contains
or sits under a target refuse before effects. Members live as long as the
allocation and are retired with it by the existing cleanup.

A service may receive files without values. It then keeps its own process
(no launcher wrap) and needs only a numeric image or Compose user. Live
owner restore and normalized serve-restore deliver fresh members; graph
exec and one-off runs refuse envelopes carrying files. Errors and receipts
stay value-free.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A host dependency selection may now be version 2, where one entry names
services: [...] instead of a single service. Each listed service expands
to its own version 1 entry with the same binding, ports, aliases and host
authority, in order, and the expanded entries are then validated exactly
as version 1: the 128-binding limit counts expanded entries, duplicates
refuse, and an entry sets exactly one of service and services. Version 1
files are unchanged and both forms give the same dependency plan.

Startup, restart preflight and the stopped-restart intent reader all
expand before validation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compose-file native graphs refused every image that declares a VOLUME,
which blocked stock database images even when a named volume covered the
path. The graph now admits an image VOLUME when exactly one explicit,
whole (no subpath) named volume owned by the same receipt is mounted at
each declared target. Missing, duplicate, bind, subpath, tmpfs and
dependency-cache covers refuse with graph_image_volume_uncovered before
any container is created; foreign OS, architecture or image identity
still refuse with graph_image. Start, restart, restore and one-off share
the predicate through verify_images. It reads only reviewed config, adds
no capability and does not change plan or receipt identities.

The per-graph volume budget rises from 8 to 16 (dependency caches still
count, since each volume costs per-graph inspection and cleanup work;
caches keep their separate 64-volume owner limit).

Native doctor adds a warning-only "native data services" check:
native_data_owner_handoff_unqualified for a service that runs a
published image's own entrypoint (no build, no entrypoint override) with
a named data volume and no explicit user:, and native_retained_data_mutable_image for
one on a latest or untagged image without a digest. Reports carry codes
and service names only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Groundwork for native image builds; no command builds images yet and native
up still refuses build services.

- provider-pins.json pins buildx 0.34.1 (darwin-arm64, Developer ID signed)
  by SHA-256. `runtime prepare-builder --archive <file>` checks the pin
  before creating any state, copies the file into private candidate state,
  rehashes the owned copy, verifies its code signature and records a tree
  digest. An existing tree is re-verified and reused; an interrupted
  preparation is retained and refused. `runtime probe` reports
  `builder_prepared` and refuses a changed tree.
- provider/build_scan.rs is a pure scanner that reports the images a build
  of the selected target can pull (FROM, COPY --from, RUN --mount from=,
  ONBUILD triggers of internal stages). It follows the Dockerfile frontend
  embedded in the pinned engine: directives, escape token, continuations,
  heredoc bodies, global ARG substitution with literal build-argument
  overrides, stage names and indexes, target reachability. It refuses what
  it cannot resolve exactly (syntax directives, variable modifiers, quotes,
  uncertain platform arguments, non-arm64 platforms, stage/image name
  collisions, unknown instructions and flags, non-Docker-Hub references,
  size and count bounds) and never echoes Dockerfile text or values.

Refs HACK-1270

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ependencies

Native up and restart now declare the effective internal.extra_hosts and
checkout-local extra-host aliases on every service, as the Compose override
does, and require a reviewed host dependency binding for every profile-active
(service, alias) pair. An alias without a binding, a non-host-gateway target,
an inexact name or a conflicting service mapping refuses value-free instead
of being silently ignored. Lifecycle hooks that change the aliases refuse
after hook cleanup.

Host dependency selections gain a declarative version 2 form: one entry per
binding applies to every active service that declares its aliases (or to
listed services) and expands to version 1 bindings before discovery, review
and the existing limits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ctrl-C or SIGTERM on a ready native Compose-import graph whose up.after
completed now runs down.before, graph cleanup, lifecycle-process
retirement and down.after, matching hack down on the Compose backend and
the authored native path. A second interrupt cancels a running down hook
and forces cleanup. A failed down.before still stops the graph and is
reported as E_LIFECYCLE_FAILED. Ctrl-C during up.after runs no down hook.

A private value-free journal beside the run mapping (run, writer
generation, phase; compare-and-swap under a lock) records intent before
each hook and known completion after it exits. hack down skips completed
phases, runs owed ones once after the frontend's finalization, and
refuses unknown completion without replay; the next up refuses it too.
Projects without down hooks keep the attached graph owner and write no
journal.

Adds the native-foreground-down-hooks TLA+ model (1,246 states) with a
journal-guard negative control, unknown-generation and owe-withdrawal
controls, and stop and crash-recovery reachability controls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The privacy check flags Linux home paths; the fixture only needs a nested
absolute target with a space.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HACK_NATIVE_GCLOUD_ADC=file selects a gcloud Application Default
Credentials adapter on the native Compose foreground. It runs after the
login hooks, beside the AWS adapter. Services that bind
~/.config/gcloud/application_default_credentials.json read-only (short or
long syntax; ~, ${HOME}, $HOME or the absolute home) lose that mount from
public Compose. They receive the same bytes as a private mode-0400 file
member at the authored target through the version 2 private envelope.

The adapter reads only the fixed home ADC path: no symlink, owned by the
user, not group- or world-writable, 1 to 16 KiB of UTF-8 JSON, unchanged
during the read. It admits authorized_user and impersonated user
credentials and refuses service_account keys, external accounts and any
credential_source. GOOGLE_APPLICATION_CREDENTIALS must equal the target;
CLOUDSDK_CONFIG, metadata-host and token-file selectors refuse. One
`gcloud auth application-default print-access-token` proves the login
(output discarded); an ADC change during startup refuses. The run mapping
records only gcloud {mode: "file"}. Restart and retained restore keep it,
and exec rechecks selectors without delivering anything.

The AWS and gcloud adapters now share one private CLI helper: clean
environment, stdout only with a 64 KiB cap, own process group, SIGKILL on
a 30 s timeout, zero-filled buffers, no retry.

AWS credentials stay a startup snapshot. The foreground now prints one
value-free stderr line ten minutes before expiry and one at expiry, naming
only the service count and the local time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… adopt

The native Compose review refused Hack's own `hack.service.one-shot: "true"`
label as an ownership label, while `hack config import --dry-run` accepts it
as a native job. The review now consumes that exact key and literal value as
a completion role: the plan marks the service `one_shot`, the frontend gives
it a `completed` readiness goal, and the graph driver admits the marker only
with completion readiness and adds no container label. Ownership namespaces
and every other `hack.*` key stay refused; an invalid value, an unconditional
restart policy or routing labels on a one-shot refuse with their own codes.

`hack config adopt`, including `--dry-run`, still requires the Compose
backend because it reads that backend's existing containers and volumes. Its
refusal on another backend now names the rerun, for example
`HACK_RUNTIME_BACKEND=compose hack config adopt --dry-run`.

Refs HACK-1906

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
prepareNativeProjectServices refused every service labeled
hack.service.one-shot before the native review could classify it, so the
reviewed completion goal was unreachable through hack up. The frontend
label allowlist now admits only the exact key with the literal value
"true" (map form) or the exact entry hack.service.one-shot=true (list
form), matching the native review. Other values, bare keys, sibling keys
and ownership labels still refuse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nested default and alternative words were built in full before the
top-level resolve charged them, so repeated or nested references could
allocate far past the 1 MiB budget (multiplying per nesting level) before
execution_input_budget refused. Each literal, escape and nested value is
now checked against the remaining budget before it is appended; the
budget is still charged once, when the finished value is appended at top
level.

Tests: a peak-heap contract proves single-level and nested repetition
refuse without building past the budget, and an exact-fit case proves
nested output is charged exactly once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A one-off job keeps the parent service's mounts, but its image check saw
only the new job container, so an image VOLUME covered by the parent's
owned named volume was always refused with graph_image_volume_uncovered.
Verify against the parent receipt's volume resources plus the job
container; parent containers are still not re-inspected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Native Compose review now parses port entries that use one interpolation
per component with a literal default, such as
"${BIND:-127.0.0.1}:${PORT:-8108}:8108", instead of reporting them as
invalid. Review stays value-free: native publication uses the literal
default, warns with interpolated_port_default at services.<s>.ports[<i>],
and records only the variable names (published_reference,
host_ip_reference; omitted when unused, so literal plans keep their
identity).

A reference without a literal default, a nested default, an interpolated
target or protocol, or a reference spanning components is a located
unresolved_port_binding error, so the rest of the review still runs and a
profile-gated service no longer blocks the default plan.

Short-form splitting follows Docker's port-spec rule after interpolation,
so bracketed and bare IPv6 hosts parse. ::1 is kept, :: is narrowed to ::1,
and a specific non-loopback host stays an unsupported_host_binding error,
now located at its entry with a clearer message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A contract key with `services: [...]` migrates only to a service scope,
and service values never reach Compose interpolation; Hack passes Compose
only global values under their own names. Compose reads `.hack/.env` for
interpolation on its own, so deleting it after the migration dropped a
`${KEY}` value that launches still needed.

The migration now keeps `.hack/.env` when a service-only key in it is
named in the Compose file or another env_file it lists (or is a COMPOSE_
setting), and says why in both the implicit and doctor reports, without
values. Compose input this check can't fully read keeps the file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`hack down --remove-data` took the shared native operation lock as a bare
directory, so a removal killed while waiting for finalization or cleanup
left a lock every rerun refused, and SIGINT/SIGTERM bypassed its release.

Removal now links a complete owner record (operation, pid, nonce) into the
same lock directory. A rerun reclaims the lock only when that recorded
owner is gone (ESRCH), under a separate recovery guard that re-reads the
owner, following the projects-registry lock protocol. Live, reused or
inaccessible owners, owner-less locks (restart, filesystem recovery) and a
busy guard still refuse. The command turns SIGINT/SIGTERM into an abort
signal that kills and awaits the owned runtime request and ends the
finalization wait, so the lock is released before the command fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
x-hack-disposable-cache names regenerable directories inside a volume,
and an empty list is valid; everything outside those directories is
retained data. Doctor no longer treats such a volume as a cache, so a
database volume with a declared cache directory keeps both advisories.
Only hack.dependencies.cache-volume volumes are excluded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An optional dependency on a profile-disabled service is dropped from the
execution graph, but review still applied its condition requirements: a
service_healthy edge without a target healthcheck raised
missing_healthcheck, and a service_completed_successfully edge to an
always-restarting target raised nonterminating_completion_dependency.
Either made the whole plan enrollment-incompatible although the target is
never started or awaited. Review now stops after the
optional_dependency_inactive warning for such edges; active edges and
required edges to disabled targets are validated as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Native Compose-file graph containers ran with every capability dropped,
so stock root entrypoints that chown their data directory and then switch
users (database images) failed on first start.

Compose-file services now get exactly moby's default capability set for
the pinned engine (docker-29.5.2), lowered explicitly as CapDrop ALL plus
the effective CapAdd set, with no-new-privileges kept. cap_add and
cap_drop follow Docker's normalization and precedence. cap_add accepts the
default names plus NET_ADMIN, NET_BROADCAST, SYS_NICE, IPC_LOCK and
IPC_OWNER; review refuses ALL and capabilities that act on the shared VM
kernel or on Hack's in-container helpers. privileged stays refused.
Services without capability fields keep their existing plan identity.

Authored native services, the persistent-data process policy and Hack's
helper containers keep every capability dropped. The guide documents the
security model: the development VM is the isolation boundary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A global `ARG BUILDPLATFORM=...` or `ARG TARGETPLATFORM=...` default
replaces the frontend's platform value, and the frontend expands
`--platform` against the global arguments. The scanner accepted
`--platform=$BUILDPLATFORM` by spelling alone, so such a stage could
select another platform. The spelling is now accepted only while no
global default overrides the argument; an overridden one must expand to
linux/arm64 like any other value. A bare global ARG still keeps the
frontend's value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-slim

The native file and storage helper image was the last place next still
shipped Bun 1.4.2. Catalog the registry-derived oven/bun:1.4.4-slim index,
platform manifests, configs and Bun/libc byte pins for linux/arm64 and
linux/amd64, and use the 1.4.4-slim tag for new VM file preparation.
Selection refuses any catalog entry whose Bun version is not current.

Saved storage records and VM file manifests written under the earlier
1.4.2 helper still parse, so saved stop and recovery stay available. That
artifact is no longer cataloged: storage enrolled under it refuses
verification as artifact drift and is never relabeled or re-seeded.

Refs HACK-1758

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ble maps

The native post-hook alias check passed the configuration snapshot read
before `up.before`, so a hook that edited `internal.extra_hosts` in
hack.config.json compared stale static aliases and startup proceeded. The
check now re-reads and validates project configuration itself
(`requireNativeProjectInternalExtraHostsUnchanged` takes no snapshot) and
refuses after hook cleanup when the effective aliases changed.

`readLocalExtraHosts` collapsed every read error into an empty map, so an
existing but unreadable checkout-local `.internal/extra-hosts.json` silently
dropped configured aliases. It now uses `readOptionalTextFile`: an absent
map is empty, an unreadable one refuses with `E_CONFIG_INVALID`. This also
covers Compose `up` and the `hack internal extra-hosts` commands, which no
longer overwrite a map they could not read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A down hook run by a foreground Ctrl-C now leads its own process group
with stdin closed. Canceling it (a second interrupt) sends SIGTERM to the
whole group, then SIGKILL once the shell exits or after a two-second
grace, and output draining after cancellation is bounded by the same
grace. A hook such as `sleep 600; echo finished` no longer leaves its
child running with the output pipes open, which blocked the forced stop
and the journal settlement. Hooks run by `hack down` keep the caller's
process group and terminal stdin.

The frontend's stop listeners now stay installed until down.after and the
finalization acknowledgement settle (disposed in an enclosing finally), so
a further interrupt cancels a running down.after through the force signal
instead of terminating the frontend with an after-started journal record.

Real-process regressions cover shell plus sleep child cancellation,
SIGTERM-ignoring descendants, an escaped process holding the pipes, the
non-owned path, and a second Ctrl-C during a real down.after through
startNativeProject.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Engine copy-up seeds an empty named volume from the image's VOLUME
directory before a read-only mount applies, so a read-only cover still
lets the image write into that volume, which another service may share.
Image VOLUME targets now need a writable owned named-volume cover; a
read-only or malformed ReadOnly cover refuses with
graph_image_volume_uncovered for services and one-off jobs alike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Go's encoding/json folds some non-ASCII runes onto ASCII field names (for
example U+0131 dotless i, U+017F long s and U+212A Kelvin sign), with rules
that have changed between Go versions. So "OnBuıld", "archıtecture" or
"diff_ıds" could name a field for the engine while a check here missed it.

Every object Hack reads from image metadata now refuses, fail closed and
value-free, when any of its member names contains a non-ASCII character:
- the image config top level, and its config and container_config scopes;
- platform fields;
- the archive config and its rootfs;
- manifest, index, descriptor and platform objects.

ASCII case-fold matching and the duplicate and ambiguity refusals are kept.
The special U+017F and U+212A folds are removed, since those keys now refuse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@roodboi

roodboi commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

f88495e closes the parser differential reported against go_json.rs. Go's encoding/json can fold non-ASCII runes onto ASCII field names (dotless ı U+0131, long s U+017F, Kelvin sign U+212A, with rules that vary across Go versions), so OnBuıld, archıtecture or diff_ıds could name a field for the engine while our checks missed it.

Every image-metadata object Hack reads now refuses, fail closed and value-free, when any of its member names contains a non-ASCII character:

  • the image config top level, and its config and container_config scopes;
  • platform fields;
  • the archive config and its rootfs;
  • manifest, index, descriptor and platform objects.

ASCII case-fold matching and the duplicate and ambiguity refusals are unchanged. The special U+017F and U+212A folds are removed, because those keys now refuse.

Tests:

  • image_configs_with_non_ascii_member_names_refuse;
  • manifests_with_non_ascii_member_names_refuse, which also accepts a real-looking OCI index and a Docker v2 manifest;
  • the extended image_load archive-config test, covering archıtecture, oſ, diff_ıds and Kind in rootfs, with a real-looking config still passing;
  • the go_json unit test.

Checks: check:local and all-features clippy pass; the full all-features lib passes (1604); gitleaks is clean.

@codex review

🤖 Addressed by Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: f88495e0f6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ Codex Security Review · Automatically triggered

Here are some automated security review suggestions for this pull request.

Reviewed commit: f88495e0f6

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

resolve_build_base fetched only the config and handed BuildKit a pinned
docker-image reference, so acquisition's archive budget never covered base
layers. The selected manifest's config and layer sizes are now summed (with
overflow checked) and must fit the per-image compressed archive budget
(ImagePolicy archive_limit, the budget ensure-image applies). The manifest
must also have 1 to MAX_LAYERS layers. Either check fails before any build,
with a coded refusal naming the pinned base reference and the declared total.
Descriptor sizes stay required unsigned integers, so a missing, negative or
fractional size refuses.

Bounded unpacked acquisition of build bases is tracked in HACK-1945.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@roodboi

roodboi commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0d13545e9a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 0d13545e9a

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Stage names are stored lowercase, and BuildKit lowercases a FROM base name
before its stage lookup. The FROM lookup compared the expanded name
unchanged, so `FROM alpine AS Base` followed by `FROM Base` was refused. It
now lowercases the name the way stage_by_name already does for COPY --from,
RUN --mount from= and the build target. A name that matches no earlier
stage still falls through as an image reference, where uppercase is
invalid, and a stage named Scratch is still refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@roodboi

roodboi commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: 6c0dd7d8e2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…ts input

Docker runs credential helpers through Go's os/exec, which ignores a broken
pipe when the helper exits without reading the server address it was given.
Hack refused that case, so a helper that answered and exited quickly could
fail a registry pull depending on timing, and the registry identity-token
test failed intermittently on loaded CI runners. A broken pipe on that write
is no longer an error; the helper's exit status and reply decide the result.
Other write errors still refuse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ Codex Security Review · Automatically triggered

Here are some automated security review suggestions for this pull request.

Reviewed commit: 6c0dd7d8e2

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

…ition

BuildKit fetches a build base's layers itself, so the preflight only
summed their sizes. A malicious base could carry foreign-layer urls or
non-distributable layer types that direct acquisition refuses, and BuildKit
would then fetch them from the guest's own namespace, outside the build
network's isolation and the source policy.

Before any build, the base's config descriptor and every layer descriptor
now go through the checks direct acquisition applies:
- the config: an image-config media type, then Fetch::checked with the
  metadata bound;
- every layer: a distributable gzip, zstd or uncompressed media type
  (layer_extension), then Fetch::checked with the archive ceiling, which
  covers a valid digest, a size within limits and no urls.

An empty urls array is treated as absent, matching acquisition. Refusals are
coded native_build_base_image, name the base reference and the reason, and
never echo URLs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@roodboi

roodboi commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 32758d943b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 32758d943b

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@roodboi

roodboi commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

#640 (credential helper EPIPE race fix, one file) is now merged into this batch so the Ubuntu credential-helper test is stable; it will close with this batch.

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: fe683f9388

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: fe683f9388

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

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.

1 participant