Skip to content
63 changes: 58 additions & 5 deletions docs/guides/native-candidate.md
Original file line number Diff line number Diff line change
Expand Up @@ -2337,10 +2337,11 @@ Host dependencies use the equivalent `--minimum-dependency-sockets N` selection;
an exact capacity or a minimum. The combined physical transport limit still applies
to the pool's actual capacities, including any spare slots. A minimum does not resize
an existing pool or guarantee that enough slots are currently free.
Each routed service
must already declare a matching `healthcheck.x-hack-http` (see
[Native HTTP readiness probe](#native-http-readiness-probe)); command healthchecks
and missing probes are not replaced or inferred. Native review still validates
Each routed service needs a matching native route probe (see
[Native HTTP readiness probe](#native-http-readiness-probe)). If no `healthcheck`
key was authored, native infers one on the reviewed Caddy route port without
changing the Compose file. Explicit healthchecks are never replaced; command-only
or disabled checks on routed services still refuse in this increment. Native review still validates
hostnames, ports, networks and labels. Foreground graph ownership supervises and
retires Unix route publishers. Without explicit HTTPS selection, this enrollment does not start a hostname authority
or Caddy HTTPS server. It never installs trust, configures DNS, or widens an existing pool.
Expand Down Expand Up @@ -2859,7 +2860,59 @@ shared-source mode; active project bind mounts still use the explicit project sh

#### Native HTTP readiness probe

`healthcheck.x-hack-http` declares the readiness probe native startup uses for a
For a complete Caddy route with no authored `healthcheck`, native reviews a default
probe: `GET /` on the route port, a 1000 ms timeout, three consecutive
failures, and a 240000 ms initial startup grace. It checks every second until its
first success, then every 30 seconds for the rest of that probe generation.
This allows slow first-page compilation, including a server first answering after
120 seconds. With a full timeout on each request, the grace plus three failed
startup samples takes at most about 246 seconds of probe time, leaving about
54 seconds of the usual 300-second graph budget for earlier startup work.
An inferred probe observed unhealthy names its service and route port. For a
missing listener or HTTP 5xx during boot, check `hack logs <service>` in the same
project/branch, fix the route
port, or declare `healthcheck.x-hack-http` with a longer `start_period_ms` and a
readiness path that answers 2xx.

The 300-second deadline belongs to the whole graph; it is not renewed when a
service starts. Slow earlier services or `service_healthy` dependencies consume
it too. A routed service started after a dependency taking 240 seconds can reach
the graph deadline before its own probe finishes. That path still returns the
generic `graph_timeout`, without naming the waiting services. Use `hack logs`
and inspect the dependencies as well as the route. An explicit probe with a longer
grace does not extend the whole-graph deadline.
A successful TCP connection and complete HTTP/1.0 or HTTP/1.1 response headers are
required. HTTP 200–499, including `/` returning 404, is ready; 5xx, EOF, non-HTTP
bytes, a timeout or partial headers are not. Redirects are not followed. At most
four interim 1xx responses may precede the final response, within the same 8 KiB
header and timeout budgets. The request uses `Host: 127.0.0.1:<port>`.

This proves endpoint readiness, not application correctness. After the first
success, three consecutive failures at the 30-second interval make the service
unhealthy. Status no longer exposes a ready route endpoint, and a new publication
or rebind cannot proceed until healthy evidence returns. This does not stop or restart the
container, withdraw an existing publication, or close its existing relay: route
traffic continues to the application and may receive its error responses. A
later successful check restores health; it does not resume one-second probing.
An inferred probe does not satisfy `depends_on: condition: service_healthy`:
that condition still requires an explicit enabled healthcheck, as on Compose.
Final HTTPS publication verification accepts 2xx–4xx only with a fresh challenge added
by the upstream proxy response handler. Authority denials and routing errors do
not qualify as the application's 404. No readiness decision follows an external
redirect. Explicit probes retain the stricter policy below. The generated
`policy` field is internal and refuses in authored `x-hack-http` declarations.
The native bundle embeds the matching guest probe; its content hash binds the
published helper. An older helper refuses the new policy argument. These inferred
defaults are retained-run compatibility values and cannot change without migration.

Native reserves `X-Hack-Probe-Request` and `X-Hack-Upstream-Proof` for the HTTPS
verification exchange. The challenge is stripped before either the authority or
application sees it. Normal traffic has no challenge, so no non-empty proof
header is emitted. The shared HTTPS reader permits up to four interim 1xx
responses before the final status for both inferred and explicit routes; explicit
final-status and reviewed-redirect rules remain unchanged.

`healthcheck.x-hack-http` declares an explicit readiness probe native startup uses for a
service. It must be the only key in that service's `healthcheck`: it cannot be
combined with `test`, `disable`, `interval`, `timeout`, `start_period`,
`start_interval` or `retries`. Docker Compose ignores the extension field. All six
Expand Down
1 change: 1 addition & 0 deletions docs/reference/v5-model-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ must be attached to the eventual exact integration revision.
| Settled retained Compose route-attempt retirement; pinned references; interrupted deletion | **Coverage gap:** filesystem journal compaction is not modeled; native-route-stop covers a different stop protocol | Instance mutation lock and exact surviving claim witnesses in [journal compaction](../../src/lib/native-compose-route-claims.ts); [claim regressions](../../tests/native-compose-route-claims.test.ts) and [retained lifecycle integration](../../tests/native-compose-adoption-routing-generation.test.ts) | 24 successful operations keep two attempt directories and constant Linux descriptors; four integrated lifecycle operations check directory counts. Injected unlink failures cover same-store and fresh-store cleanup retries. Assumes cooperative same-user writers, the caller's mutation lock and working filesystem synchronization. No process-kill/power-loss durability proof or bound on unresolved/over-budget histories. |
| Graph cleanup container stop batch: bounded in-flight stops, shared worker queue, connect-stage retry, batch-wide unreachable state, deadline expiry, worker panic | **Coverage gap:** no maintained model covers scheduling inside the stop batch; native-route-stop covers a different stop protocol | `dispatch` and `attempt` in [the stop batch](../../packages/runtime-core/src/provider/engine/stop.rs): at most 4 stops in flight, every worker joined; only `connect`/`connect_timeout` failures retry (25 ms doubling to 500 ms, within the batch deadline and 30 s of a stop's first connect failure); once one stop exhausts its window, later connect failures return at once; a deadline reached during backoff keeps the connect stage; `response`, `timeout` and `transport` never retry. Regressions in the same file's `provider::engine::stop` tests (bounded in-flight socket server, late socket, missing socket, 14-stop retry counts, persistent connect failures, oversleeping backoff, non-retried stages, worker panic) and `fourteen_service_connect_failures_keep_resumable_progress_and_retry_only_unstopped` | Tests cover exact attempt counts and the in-flight bound for fixed interleavings; no model explores all worker schedules or proves at most one delivered request per stop. Assumes a failed connect sent no request bytes and that the stop budget covers fully serialized grace periods. Graph-level resume after a partial batch is covered by tests only. Host qualification of the descriptor-pressure root cause (HACK-1302) remains open. |
| Concurrent stop/publication; route claim and relay lifetime | native-route-stop, shared-https-lifetime, previous-boot-shared-https, relay-lifecycle-barrier, relay-authorization, relay-staging, relay-fence | Fenced route removal, relay owner, ready/removed protocol, authenticated observations | Native DNS/TLS/browser behavior; old-boot uncertainty and external routing. |
| Inferred Caddy route readiness without authored probes | Existing probe generation, exec identity and freshness checks are reused; no new lifecycle transition | `http_probe.rs` and compiled `guest/http-probe.c` tests cover TCP refusal, complete HTTP 2xx–4xx, 5xx, EOF, timeout, partial/oversized headers and bounded interim responses. Compiled-helper startup regressions cover no listener and boot-time 5xx reaching a named service failure; route diagnostics name the port and remedies. A compiled production-transition fake-clock test drives graph readiness at 120 s, with the old 60 s grace as a negative control; it injects request outcomes and does not test real elapsed scheduling. A cadence regression covers the switch after first success. Bridge reservation regressions preserve running relay assignments when ready endpoints disappear. Project contracts preserve explicit checks and refuse inferred service_healthy dependencies; TypeScript route/preflight tests require the exact reviewed inferred policy. HTTPS tests require a fresh upstream response challenge for inferred routes | 32 route slots, 1 s startup/30 s steady interval, 1 s deadline, 8 KiB headers, four interim responses, three failed samples and 240 s initial grace. The separate 300 s graph budget includes earlier services and can still return unnamed graph_timeout. Bridge registry tests do not exercise continued route traffic; that remains a real-Caddy/VM check. No model explores HTTP parsing or Caddy handler behavior. TLS fixtures simulate the proxy marker; an actual Caddy/authority/VM run must qualify application 404 versus authority denial, SNI mismatch and missing upstream. |
| Shared host helper instance: overlapping ups, session adoption, stale create decisions, replacement before finalize, epoch-record failure, interrupted cleanup, concurrent down, displaced launches, failed down session stop | shared-helper-instance | `src/lib/project-shared-helpers.ts` (shutdown generation, `assertCurrent`, `SharedHelperSessionOwnership`); `src/lib/project-lifecycle-controller.ts` session decision, the locked creation transaction (revalidate, publish, create, environment, record, stop on failure, all under one lock), launches fenced by the lock and a token check, abort and signal cleanup through the same epoch decision (`runIfOwned`), an unknown exact-presence answer failing the inspection, and the locked conditional finalize; `stopSessionBeforeReleasingHelpers` (down, restart, native retirement keep holders when the session stop fails); `runMuxCommand` bounds every lifecycle and backend mux call so it settles before the lock is released, and reports a timeout as unknown (never absence); `tests/mux-command-timeout.test.ts`; `tests/project-lifecycle-controller-ownership.test.ts`, `tests/project-shared-helpers.test.ts`, `tests/native-project-down.test.ts`, e2e `shared-host-helpers` | Helper holder/rollback and claim protocol are not modeled (claim fixtures cover them); timeout settlement is an assumption the code does not guarantee (a killed tmux client's queued command may still apply, so a late kill or window command can reach a newer session; consequences and the session-ID fence, not in this slice, are in the README); zellij is refused for shared-helper lifecycle sessions (token written after creation); the gap between a down's session stop and its release (an up that captured after the stopping mark can still succeed there) is a documented first-slice limitation; real mux/process/lock-file behavior. |
| Engine restart policies: stop versus pending restart, guest engine restart, stale incarnations, terminal proof | service-restart-policy | Receipt-admitted policy, disarm-before-stop and `restarting` stop preflight in `shutdown.rs`; admission refusals for incarnation-bound delivery classes in `restart_policy.rs`; owner-driven `Rebind` after full retirement, fresh ownership/attachment checks and a new reservation in `bridges/restart_rebind.rs`; route probe sample-generation rearm; Rust tests for admission, endpoint identity, port-busy refusal, disarm and stop preflight; portable selected-child readiness guard plus macOS two-real-child polling regression prevent historical sibling exits from rejecting a replacement or misclassifying its own listener | Real guest engine behavior (crash restart, stop during backoff, disarm before deletion) needs native host evidence; several services, selected-child readiness during sibling failure, backoff timing and retry counting are not modeled. Same-port/route reachability after a policy restart, route probe rearm and per-slot child retirement still require real-VM host evidence. Rebind abstracts retirement/reservation/publication; relay lifecycle, fence and staging models cover their separate failure boundaries. |
| Foreground Ctrl-C down hooks: concurrent `down`, second interrupt, crash between or during hooks, stop during `up.after`, next generation | native-foreground-down-hooks | Value-free compare-and-swap journal in [down hooks](../../src/backends/native-project-down-hooks.ts), foreground orchestration in [foreground stop](../../src/backends/native-project-foreground-stop.ts) and `hack down` in [native down](../../src/backends/native-project-down.ts); [journal](../../tests/native-project-down-hooks.test.ts), [foreground](../../tests/native-project-start-stop-hooks.test.ts) and [down](../../tests/native-project-down-journal.test.ts) regressions | Durable writes are atomic in the model; real fsync/power loss, a stale journal lock after a crash inside the lock, and the live process-group behavior of the graph owner on a host are not covered. One generation and at most two `down` invocations are explored. |
Expand Down
15 changes: 15 additions & 0 deletions packages/runtime-core/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,21 @@ fn build_http_probe() {
.status()
.expect("build host HTTP probe test driver");
assert!(result.success(), "host HTTP probe build failed");
println!("cargo:rerun-if-changed=tests/fixtures/http-probe-clock.c");
let result = Command::new("cc")
.args([
"-std=c11",
"-O2",
"-Wall",
"-Wextra",
"-Werror",
"tests/fixtures/http-probe-clock.c",
"-o",
])
.arg(out.join("http-probe-clock-host"))
.status()
.expect("build fake-clock probe test driver");
assert!(result.success(), "fake-clock HTTP probe build failed");
}

fn build_loopback_identity() {
Expand Down
Loading
Loading