Skip to content

fix(native): run stock image processes in wrappers like Docker, naming refusals - #638

Closed
roodboi wants to merge 2 commits into
claude/hack-1938-alias-wrapper-diagnosticsfrom
claude/hack-1939-wrapper-relative-exec
Closed

roodboi wants to merge 2 commits into
claude/hack-1938-alias-wrapper-diagnosticsfrom
claude/hack-1939-wrapper-relative-exec

Conversation

@roodboi

@roodboi roodboi commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Native's process wrappers now run stock images the way Docker does. Every setting a wrapper still cannot run is named by service and field, all at once and before any runtime state is reserved. Compose-declared cases are refused before the VM starts, and doctor predicts them.

Stacked on #636. The base is claude/hack-1938-alias-wrapper-diagnostics. After #636 merges, retarget this PR to next. Only the commits after 4d25fa5a belong to this PR: 516c7238 (the change) and 08f2d8c3. The second commit addresses the Codex finding: wrapper preflights in up and restart check only profile-active services, and doctor lists profile-gated services as information only.

Problem (HACK-1939 item 2)

Project-wide host aliases put a host-dependency release barrier around every service, and private environment values put a launcher around their services. Both refused stock images whose entrypoint or health-check executable is a bare name (docker-entrypoint.sh, CMD mysqladmin) with environment_entrypoint or environment_health. The refusals came one service per run, about 72 s each, without naming the service.

Runtime (runtime-core, Zig launcher, relay-guest)

  • Docker-style exec: the environment launcher (startup, health and exec modes) and the relay --await-release/--check-release barrier exec the selected process like Docker:
    • a name containing / runs as given;
    • a bare name is resolved through the container's own PATH after the environment is applied (Docker's default PATH when none is set);
    • no host PATH is consulted and no shell runs.
  • Image process materialized for dependency services: the selected process now comes from the pinned image for host-dependency services as well, not only for private environment delivery. Before this change, a dependency-only service with no Compose entrypoint could not keep its image ENTRYPOINT behind the barrier.
  • Command-only and shell-form cases:
    • A command-only image (no ENTRYPOINT) becomes the same argv.
    • An explicit entrypoint: [] plus a command becomes that command.
    • A CMD-SHELL or string health test becomes the exec form Docker runs: the image SHELL, or /bin/sh -c by default.
  • Refusals collected before reservation: every wrapped service is checked before the graph directory is created. A refusal keeps its code and adds a value-free process_refusals diagnostic {version: 1, refusals: [{service, field}]}. Fields are a closed set: entrypoint, healthcheck, user, stop_signal, mounts.

CLI

  • Pre-boot refusal: native up refuses, before the VM starts, the Compose-declared settings no wrapper can run, naming every service and field. These are an empty process, a health test without an executable, a named user, an unsupported stop signal, and a reserved volume target. Restart preflight runs the same check before stopping the current graph. Example:
    • Native up cannot wrap these services' processes; nothing was started. Refused: service cache user (use a numeric UID or UID:GID); service shell user (use a numeric UID or UID:GID).
  • Runtime refusals named: runtime process_refusals are parsed in their closed form only, and named in the nothing-reserved startup report from fix(native): compile host aliases like execution and name unbound aliases #636.
  • Doctor: the native backend gains a native process wrappers check. It predicts the Compose-declared cases for every service native may wrap: all services when project-wide aliases exist, services with host-gateway extra hosts, and services with bare environment keys. Settings inherited from images are checked by the runtime against the pinned image.

Host proof

Candidate bundle built with scripts/build-native-candidate.sh from 516c7238. It ran with a fresh isolated HOME, HACK_HOME and native home, HACK_DAEMON_AUTOSTART_DISABLE=1, and one VM at a time. All fixtures were synthetic.

  1. HACK-1938 alias-dup repro:
    • Setup: extra_hosts: ["${UNSET_PREFIX:+x-}api.example.test:host-gateway"] plus internal.extra_hosts {api.example.test: host-gateway}, with a version 2 binding to a host listener.
    • Result: ready in about 9 to 22 s. On next.10 the same setup failed with invalid_extra_hosts after about 146 s.
    • Inside the container, wget http://api.example.test:8080/ returned the host page, and /etc/hosts holds exactly one api.example.test entry.
    • up printed Native project ready (run …); Ctrl-C printed the confirmed-stop line and exited 130.
  2. Stock redis:7.4-alpine with both wrappers:
    • Setup: no Compose entrypoint, so the image's ENTRYPOINT ["docker-entrypoint.sh"] is used. The services carry a project-wide alias (relay barrier) and a managed env value (launcher). cache has health ["CMD", "redis-cli", "ping"]; shell has the string health test "redis-cli ping".
    • Both services were running/healthy in 13 s.
    • Container PID 1 argv: /sbin/docker-init -- /run/hack-relay-guest --await-release <gen> -- /run/hack-environment-launcher … docker-entrypoint.sh redis-server.
    • redis-cli ping returned PONG, the managed value was delivered (checked in-container, never printed), and the alias resolved to the host listener.
    • Redis needed user: "999:1000": its entrypoint's root-to-redis setpriv handoff fails because native drops all capabilities. That is the existing native_data_owner_handoff_unqualified advisory and unrelated to this change.
  3. Named user user: redis on both services:
    • doctor warned in native process wrappers, naming both services.
    • up refused in 0 s with the line above, and the pool stayed stopped.

Validation

Rust, run from a short-path worktree with cargo +1.97.1:

Gate Result
bun run check:local (fmt and clippy -D warnings) pass
clippy -D warnings with environment-launcher,native-config-plan pass
relay-guest clippy pass
cargo test --lib graph:: with environment-launcher,native-config-plan 661 pass, 0 fail
bun run test:local 1194 pass, 1 fail

The one test:local failure is provider::resources::tests::explicit_root_binding_requires_exact_executable. It fails identically on a clean origin/next checkout on this host, so it is not caused by this change.

New Rust tests cover:

  • relative entrypoint and health argv preserved for PATH lookup;
  • command-only images;
  • empty-process refusal without partial mutation;
  • CMD-SHELL to exec-form conversion, including the image SHELL;
  • field-tagged launcher refusals;
  • the collector and its JSON contract.

TypeScript:

  • Focused bounded-runner tests: 185 pass, 0 fail across native-process-wrappers, native-project-process, native-project-start, native-project-restart, native-refusal-cli and native-compose-interpolation.
  • Doctor-related tests: 179 pass, 1 skipped, 1 fail. The failure, native-ca-doctor-cli "unknown owner", needs brew on PATH and fails identically without this change.
  • bun run --cwd packages/cli typecheck and bun run --cwd packages/cli check pass in a clean checkout outside /private/tmp.
  • Gitleaks finds nothing, staged or in history.
  • The full bun suite was not run locally; CI runs it.

Remaining gaps

  • Named users: a user name like postgres still cannot be wrapped, because private files need an exact UID. Native refuses it by name instead of guessing.
  • Unpredictable before boot: settings inherited from an image (a named image USER, an image with neither ENTRYPOINT nor CMD) are refused by the runtime with all services named, after the VM boots but before any reservation.
  • Artifact change: the relay guest artifact changes. I did not test restoring a graph retained by an older bundle with this bundle.

Refs HACK-1939

🤖 Generated with Claude Code

…g refusals

Project-wide host aliases put a host-dependency release barrier around every
service, and private environment values put a launcher around their
services. Both refused stock images whose entrypoint or health-check
executable is a bare name (docker-entrypoint.sh, CMD mysqladmin), one
service per run, without naming the service.

Runtime:
- The environment launcher and the relay release barrier exec the selected
  process like Docker: a name with "/" runs as given and a bare name is
  resolved through the container's own PATH (Docker's default when unset),
  never a host PATH and never a shell.
- The selected process is materialized from the pinned image for
  host-dependency services too, not only for private environment delivery,
  so the barrier runs the image's ENTRYPOINT and CMD. A command-only image
  becomes the same argv, and a CMD-SHELL health check becomes the exec form
  Docker runs (the image SHELL, /bin/sh -c by default).
- Every wrapped service is checked before any runtime state is reserved.
  A refusal keeps its code and lists each service and closed field
  (entrypoint, healthcheck, user, stop_signal, mounts) in a new value-free
  process_refusals diagnostic.

CLI:
- Native up refuses Compose-declared settings no wrapper can run (empty
  process, a health test without an executable, a named user, an
  unsupported stop signal, a reserved volume target) before the VM starts,
  naming every service and field. Restart preflight checks the same before
  stopping the current graph.
- Runtime process_refusals are parsed in their closed form and named in the
  startup failure report.
- Native doctor adds a "native process wrappers" check that predicts these
  cases for every service native may wrap.

Refs HACK-1939

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

linear-code Bot commented Nov 7, 2026

Copy link
Copy Markdown

HACK-1939

@roodboi

roodboi commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-07T02:42:44.825113Z 08f2d8c Manual request
🔒 Security Review ✅ Completed 2026-11-07T02:45:02.963422Z 08f2d8c Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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: 516c723811

ℹ️ 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".

Comment thread src/backends/native-project-start.ts Resolved
@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: 516c723811

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.

The wrapper preflight checked every authored service, so a service behind
an unselected profile with an unsupported wrapper setting blocked the whole
up and restart even though it never runs. Apply Compose profile activation
before the check in the start and restart preflights. Doctor reports
profile-gated services as information only and never warns on them.

Refs HACK-1939

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. More of your lovely PRs please.

Reviewed commit: 08f2d8c3eb

ℹ️ 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: 08f2d8c3eb

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 added a commit that referenced this pull request Nov 7, 2026
## What this is

Merge train batch 7 into `next`: three real-project trial fixes. Each
component PR's reviewed head is merged with `git 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.

| PR | Title | Linear | Head included | Review at that head |
| --- | --- | --- | --- | --- |
| #636 | fix(native): compile host aliases like execution and name
unbound aliases | HACK-1939 | `4d25fa5a` | Codex code + security: no
findings; 0 open threads |
| #638 | fix(native): run stock image processes in wrappers like Docker,
naming refusals | HACK-1939 | `08f2d8c3` | Codex code + security: no
findings; 0 open threads |
| #637 | fix(native): reconcile edited graphs that have a dependency
cache | HACK-1937 | `592a9b1d` | Codex code + security: no findings (the
security review confirms the cache-poisoning fix); 0 open threads;
real-VM proof passed |

## Integration changes

- **#636 with batches 1–6:**
- The restart preflight and native `up` pass both #608's
`dependencyOwners` and #636's `internalExtraHostSources`.
- #615 and #636 both added a "nothing was reserved" startup report. They
are now one `finalInspectionFailure`: #615's check plus #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
#615's diagnostic.
- #636 matched `native_graph_unreserved`, but image projects inspect
with `graph inspect`, which only returns `graph_unreserved`. #636's
check therefore could never fire, so its tests now use
`graph_unreserved`.
- #636's ready summary and confirmed-stop line ride #611's progress
events, so `up` prints 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.
- **#638 with #615 / #622:** the error type keeps the fixed-message type
and adds #638's process-refusal types. The unified startup report
appends #638's named process refusals after the startup diagnostic.
Doctor keeps the amd64 emulation and settings checks next to #638's
process-wrapper check.
- **#637 with #615 / #638:** failures keep the fixed message, service
and process refusals, and add #637's `planChange`. Its guidance replaces
"inspect owned state before retrying". The cache-poisoning fix holds in
the merged code:
  - `dependency_cache.rs` is #637's file unchanged;
- `plan_reconcile::ordinary_layout` still routes caches with a writable
consumer through reconciliation;
- no batch change mounts a cache volume outside
`instance_scoped_volumes`.
- **Size fix (`dbcf2bcc`):** the three error additions together pushed
`CandidateError` past clippy's large-error bound. `cause_code` is now a
boxed `str`, and process refusals and the plan change share one boxed,
flat-serialized `RefusalDiagnostics`. A test pins the size and the JSON
shape.
- **#640** (credential-helper EPIPE race fix) was folded into batch 6,
so it is not listed here.

## Validation on this branch (`458e0a93`)

- Clean CLI typecheck: pass. Biome on 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`: 1375 pass. `--features native-config-plan`
`provider::graph`: 688 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, 177 controls verified.
- Focused bun tests, bounded runner, the changed test files: 260 pass, 0
fail.
- `docs:prepare` + `docs:check`, `privacy:check`, gitleaks over
`origin/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](https://claude.com/claude-code)

<!-- codesmith:footer -->
---
<a
href="https://backend.blacksmith.sh/track/run-proof?expires=1796634294&installation_model_id=17053&pr_number=641&ref=codesmith_pr_footer&repository=hack-dance%2Fhack&return_to=https%3A%2F%2Fgithub.com%2Fhack-dance%2Fhack%2Fpull%2F641&variant=run_proof_on_this_pr&signature=daf0979b3cf0e8fb0d4344fa34460d87b7d75f22ce9fa22f3da3d8c483a575a9"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/proof-run-proof-on-this-pr-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/proof-run-proof-on-this-pr-light.svg"><img
alt="Run Proof on this PR"
src="https://pr-comments-assets.blacksmith.sh/codesmith/proof-run-proof-on-this-pr-dark-3x.png"
width="168" height="28"></picture></a>
<sup><a
href="https://app.blacksmith.sh/hack-dance/codesmith/hack/pr/641?autoLogin=true&ref=codesmith_pr_footer">Open
in [code]smith</a> · Need help on this PR? Tag
<code>@codesmith-bot</code> with what you need.</sup>

<!-- codesmith:autofix:disabled -->
<!-- /codesmith:footer -->

---------

Co-authored-by: Dimitri Kennedy <344842+roodboi@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@roodboi

roodboi commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

Merged into next through the train batch PR #641 (squash commit 76460f8). This PR's reviewed head 08f2d8c3eb207a7393fe13326153cb7c85824b57 was included unchanged. #641 describes the integration resolutions. Closing as merged by batch; the branch is kept.

🤖 Addressed by Claude Code

@roodboi roodboi closed this Nov 7, 2026
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