Repository navigation
fix(native): merge train batch 7 (#636, #638, #637) - #641
Conversation
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>
Merge refs/remotes/pr/636 (4d25fa5: alias de-duplication, refusals naming the alias, up/Ctrl-C output) onto claude/train-batch-6. Resolutions: - native-project-restart-preflight.ts: keep #608 dependencyOwners and add #636 internalExtraHostSources to the options. - project.ts (handleNativeUp): keep #597 resolveNativeHostSettings values (allowedHosts, preparedBase, https, aws) and add internalExtraHostSources. - project.ts (restart preflight): pass both dependencyOwners and internalExtraHostSources. - native-project-start.ts imports: drop NATIVE_GRAPH_UNRESERVED; the image path inspects with `graph inspect`, which refuses `graph_unreserved` (#615), not the native provider's `native_graph_unreserved`. - native-project-start.ts unreserved startup: one finalInspectionFailure that requires #615's `graph_unreserved` refusal plus #636's guards (a settled failure, not a restore, no mapping attempted) and marks graph cleanup confirmed. The message joins both: "nothing was created and no cleanup is needed" followed by #615's startup diagnostic. - native-project-start.ts ready/stop output: #636's ready summary and confirmed-stop line ride #611's progress events instead of a second deps.notice channel. The ready event carries the run id and the endpoint lines; the stopped event carries the retention line when a mapping was published. project.ts renders them under the single ready and stopped lines. The notice dependency and its helpers are removed. Semantic adaptations: - nativeReadySummary returns only the indented endpoint lines; the ready line (with services and run id) is rendered from the progress event. - #636 tests use `graph_unreserved` and observe progress events; the #615 test expects the joined message. docs/guides/native-candidate.md describes one ready/stop output. Tests (base + ours + theirs = merged): native-project-start 98+14+5=117, native-project-internal-hosts 12+0+4=16, internal-extra-hosts-worktree 9+0+1=10, native-startup-logs 7+0+1=8, native-compose-interpolation 0+0+2=2. Focused run: 210 pass, 0 fail across 7 files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge refs/remotes/pr/638 (08f2d8c, stacked on #636: Docker-style relative entrypoint and health-check resolution in the process wrappers, named process refusals, doctor process-wrapper check). Resolutions: - runtime-core error.rs: keep HEAD's ErrorMessage (fixed-literal flag) and add #638's ProcessField, ProcessRefusal and ProcessRefusalsDiagnostic. - native-project-start.ts cleanupUnconfirmed: keep HEAD's startupDiagnostic() form; #636's nothingReserved() was already folded into finalInspectionFailure by the #636 merge. - native-project-start.ts nothing-reserved report: append #638's named process refusals (refusedProcesses helper) after the startup diagnostic in the unified finalInspectionFailure. - doctor.ts: keep #609 amd64 emulation and #597 native settings checks, report fields and JSON, and add #638's native process wrappers check, report field and JSON; imports from both. Semantic adaptations: - process_refusals.rs: Refused::new takes a &'static str message, since HEAD's CandidateError::new accepts only literals or owned Strings (and marks literals as fixed, which these value-free refusals are). - #638's runtime-refusal test uses `graph_unreserved` and expects the unified nothing-reserved message. Tests (base + ours + theirs = merged): native-project-start 103+14+4=121, native-project-process 12+0+2=14, native-process-wrappers 0+0+8=8; Rust #[test]: image_process 7+0+3=10, launcher 7+0+1=8, process_refusals 0+0+1=1, error.rs 0+2+0=2. Focused run: 264 pass, 0 fail across 8 files; cargo check --all-targets --all-features passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge refs/remotes/pr/637 (592a9b1: dependency-cache graphs reconcile after edits; per-cache identity bound to the cache's own inputs and full mount layout; caches with a writable non-initializer consumer scoped to one instance, the fix against cross-worktree cache poisoning). Resolutions: - runtime-core error.rs: keep HEAD's ErrorMessage and #638's process refusal types, add #637's PlanChangeField/PlanChangeDiagnostic; the CandidateError gains plan_change beside process_refusals, and with_plan_change sits beside HEAD's with_service. - native-runtime-client.ts: NativeFailure keeps HEAD's fixed message, service and process refusals and adds planChange; replies carry both nativeFailureDetail and planChange. - native-runtime-client.ts nativeFailureMessage: #637's closed plan-change guidance replaces the generic "inspect owned state" sentence, and HEAD's value-free runtime note and stop-failure summary still follow it. - tests/native-runtime-client.test.ts: keep both fixture branches (fixed-message/unsafe-fixed and plan-change/plan-change-unsafe). Security check: the merged dependency_cache.rs is #637's file unchanged; resolve_with_legacy is the only cache-identity source (config.rs prepare_delivery) and plan_reconcile's ordinary_layout keeps sending instance-scoped caches through reconciliation. No HEAD change adds a cache-volume mount kind outside instance_scoped_volumes, and #610 refuses restart policies on cache initializers. Tests (base + ours + theirs = merged): native-runtime-client 19+1+1=21, native-project-retained-images 13+2+1=16, native-project-restore 7+0+1=8, native-plan-change 0+0+2=2; Rust #[test]: plan_reconcile 6+0+8=14, dependency_cache 6+0+1=7, hostname_change 2+0+1=3. Focused bun run: 62 pass, 0 fail; cargo lib dependency_cache 7, plan_reconcile 14, hostname_change 3, restore 15, error 2 pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HEAD's fixed-message flag and service name (#615), #638's process_refusals and #637's plan_change together grew CandidateError from 104 to 120 bytes, so StopBatchFailure (error plus a boxed slice) reached 136 bytes and process_refusals::Refused 128. Clippy's result_large_err then failed check:local and the all-features clippy gate. - cause_code is an Option<Box<str>> instead of Option<String>; with_cause_code keeps its String argument. - process_refusals and plan_change share one boxed RefusalDiagnostics, serialized flat with serde(flatten), so the reply JSON keeps both top-level keys. Callers use with_process_refusals, with_plan_change and the process_refusals()/plan_change() accessors. - A new error.rs test checks the flat serialization and that CandidateError stays at most 104 bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The native settings doctor test asserted that no path under its temp root appears in doctor output. The native home sits inside that root, and next's hack.local route check legitimately reports its own receipt path there. The test now checks that no absolute project or machine-settings path (and none of the setting values) appears, which is what settings reporting promises. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A project with hack.env.default.yaml but no hack.env.<env>.yaml can start with --env <env> (for example to pick adaptation.<env>.json); the base values apply. Startup now records that case as baseOnlyEnvironment, next to the existing unmanagedEnvironment marker, so restart preflight, the next retained up, fresh exec and down hooks accept the unchanged files. The availability rule compares what startup found with what exists now: an overlay that existed must still exist, a base-only run must still lack the overlay, and an unmanaged run must still have no managed env file. A legacy mapping without a marker keeps the overlay requirement. A retained run keeps its recorded marker and is never relabeled. The mapping validator accepts the complete selector set plus any subset of gcloud, runtimeMetadata and one environment marker, instead of a listed set of combinations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Restart preflight, retained up and fresh exec now say which managed env file appeared or was removed since the run started, by its project-relative name only, and how to apply it: run hack down, then hack up, for an appeared file; restore a removed overlay to keep the run, or start again without it. hack down stays the way to apply such a change. Its hooks use the selection the run recorded: no values for a run started without managed env files, and the base values for a run started base-only, even after an overlay or env file appeared. A run started with its overlay refuses its down hooks before cleanup when the overlay is gone, naming it and asking to restore it, as it did before without the name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Picks up batch 5's review fixes: the doctor test scope, base-only env selections and named env-change refusals. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Picks up batch 5's review fixes and next with batch 5 and Kit's CI changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The engine and BuildKit decode image configurations with Go's encoding/json, which matches object keys to fields case-insensitively and keeps the last of repeated keys. Exact-key lookups here could therefore miss what the engine acts on: a base config with "onbuild" or "ONBUILD" passed the stored-trigger refusal while BuildKit still ran its triggers. A new go_json reader keeps every member and returns the one member a Go field would receive, refusing when more than one key names it. - ONBUILD: case variants count at the top level and under config and the legacy container_config (those keys are also matched case-insensitively). A repeated or conflicting field, or one that is neither an array nor null, refuses. An empty array still passes. - Platform (os, architecture, variant) in config_platform, and architecture, os and rootfs.diff_ids in archive validation, are read the same way. Repetition refuses. - Manifests and indexes: every member this module reads (schemaVersion, mediaType, manifests, config, layers, descriptor digest, size, mediaType, platform, urls, and platform os, architecture, variant) must be spelled exactly once, so index platform selection cannot diverge from the engine's. 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>
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>
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>
…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>
Picks up batch 6's review fixes: Go-style image config key reading, non-ASCII key refusal, the build base size bound and case-insensitive stage names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
Picks up batch 6's final review fixes, including #640. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
next holds batch 6 (#639) with the same tree as the batch 6 head this branch already contains, so the merge keeps this branch's content. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. More of your lovely PRs please. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
What this is
Merge train batch 7 into
next: three real-project trial fixes. Each component PR's reviewed head is merged withgit merge --no-ff, in this order (#638 is stacked on #636). 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.4d25fa5a08f2d8c3592a9b1dIntegration changes
uppass both feat(native): owner-scoped host dependencies and automatic listener refresh #608'sdependencyOwnersand fix(native): compile host aliases like execution and name unbound aliases #636'sinternalExtraHostSources.finalInspectionFailure: fix(native): report service exits, runtime reasons and interleaved logs #615's check plus fix(native): compile host aliases like execution and name unbound aliases #636's guards (a settled failure, not a restore, no mapping attempted), with the combined message "nothing was created and no cleanup is needed" followed by fix(native): report service exits, runtime reasons and interleaved logs #615's diagnostic.native_graph_unreserved, but image projects inspect withgraph inspect, which only returnsgraph_unreserved. fix(native): compile host aliases like execution and name unbound aliases #636's check therefore could never fire, so its tests now usegraph_unreserved.upprints one ready line,Project is ready (SERVICES; run RUN). …, with HTTPS URLs and published ports under it. Ctrl-C prints one stopped line, which states that data was retained when a run mapping was published.planChange. Its guidance replaces "inspect owned state before retrying". The cache-poisoning fix holds in the merged code:dependency_cache.rsis fix(native): reconcile edited graphs that have a dependency cache #637's file unchanged;plan_reconcile::ordinary_layoutstill routes caches with a writable consumer through reconciliation;instance_scoped_volumes.dbcf2bcc): the three error additions together pushedCandidateErrorpast clippy's large-error bound.cause_codeis now a boxedstr, and process refusals and the plan change share one boxed, flat-serializedRefusalDiagnostics. A test pins the size and the JSON shape.Validation on this branch (
458e0a93)RUSTUP_TOOLCHAIN=1.97.1 bun run check:local: pass. All-features clippy-D warnings(Zig 0.15.2): pass.--lib: 1375 pass.--features native-config-planprovider::graph: 688 pass. Contracts:project_contract66,compose_fields_contract10,interpolation_budget_contract1,local_contract14.bun run test:models(TLA+ 1.7.4): exit 0, 177 controls verified.docs:prepare+docs:check,privacy:check, gitleaks overorigin/next..HEAD: pass.Not run locally: the full bun suite, the Linux cargo jobs, docker-e2e and native host qualification. Hosted CI covers the first three.
After this merges, each component PR is closed with a link to the merge commit.
🤖 Generated with Claude Code
Open in [code]smith · Need help on this PR? Tag
@codesmith-botwith what you need.