Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 39 additions & 6 deletions docs/reference/native-compose-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ documents can contain decrypted values and remain in owned private files with
restricted permissions. They are never part of the public plan or JSON output.
Do not copy these documents into source control or attach them to diagnostics.

## Finite host up hooks
## Finite host lifecycle hooks

Whole-project `up` and `restart` run `host.up.before` before engine startup, then
`host.up.after` after the exact workloads and routes are ready. Each phase runs in
Expand All @@ -123,7 +123,7 @@ the compiler's effective bindings. `env_target` can select the default host or a
declared workload's host view. Managed references read that immutable baseline;
literal overrides, empty defaults and explicit unset destinations retain their
meaning. HTTP/HTTPS external endpoint bindings are supported. Other endpoint
owners remain refused. `run` currently refuses projects with either nonempty up
owners remain refused. `run` currently refuses projects with any nonempty lifecycle
hooks instead of assigning new lifecycle semantics to a one-off command.

Each hook phase has a separate budget equal to `HACK_COMPOSE_STARTUP_TIMEOUT_MS`;
Expand All @@ -149,16 +149,41 @@ operation pending. The generation becomes ready only after those checks pass.
Before spawning, Hack synchronizes a private hook intent under the same instance
mutation lock used for engine effects. An interrupted or unverified hook retains
that intent and blocks `up`, `restart` and `run` without replay. Saved `ps --json`
reports `beforeHooksPending` for either phase and `hostHookPhase` as `before`,
`after`, or null, even if no engine generation was created. Older token-only
reports `beforeHooksPending` for every phase and `hostHookPhase` as `before`,
`after`, `down.before`, `down.after`, or null, even if no engine generation was created. Older token-only
receipts remain uncertain before intents. An after intent also binds its exact
pending startup operation and generation. Saved
`down` can stop a retained engine generation but reports incomplete cleanup while
hook intent remains. `down --recover` can recover a verified dead CLI mutation
owner; it cannot prove hook process ownership, clear hook uncertainty or rerun a
hook. Explicit recovery for interrupted hooks remains a later lifecycle slice.
An uncertain hook also prevents routing claim retirement after an owned stop.
Down hooks and persistent host processes remain unsupported.
Normal `down` runs `host.down.before` before stopping the owned engine and
`host.down.after` only after fresh exact container, network and proxy dispatch
absence. Hostname claims remain held through after hooks. A hook failure returns
its exit status and leaves stop pending; a before failure runs no engine mutation.
Private target values for both phases are captured under the instance lock before
any hook journal or spawn. Each phase receives its own finite budget.

The private saved generation binds down hooks to its immutable source revision,
profiles, effective overlay (including no overlay) and original explicit overlay
selection. Normal hook-enabled down rechecks source, local, routing and managed
environment freshness before engine effects and final claim retirement. Hooks
cannot rebind that generation. Any pre-existing pending operation or unknown hook
intent refuses normal hook-enabled down before private value acquisition. Changed
or malformed inputs require explicit recovery. Generations saved without this
binding retain saved-only stop semantics; newly authored hooks cannot attach to an
already-running old generation. Completed stopped generations do not replay hooks.

`down --recover` skips authored down hooks, compiler/source parsing and private
value acquisition. A successful hook-enabled engine recovery reports
`hostHooksSkipped: true`; it does not report skipped or previously failed hooks as
successful. A known finite nonzero with proven group absence clears only its exact
hook intent, so later explicit engine recovery can retire claims after fresh absence.
Unknown completion preserves host intent, pending generation and claims even after
owned engine stop, and still reports incomplete. Persistent host processes and
explicit uncertain-hook recovery remain unsupported. `restart` retains its current
up/recreate contract; down hooks run on explicit `down`.

## Saved operations and recovery

Expand Down Expand Up @@ -229,7 +254,7 @@ from orphan generation files by this recovery path.
## Remaining coverage

This slice explicitly refuses foreground or partial-service startup, non-plain
logs, pruning options, all `host.down` hooks, persistent host
logs, pruning options, persistent host
processes, browser opening, route
bindings, typed host/gateway endpoints, TCP endpoint derivation, and HTTP/TCP
readiness. It preserves ordinary project DNS and outbound networking and adds no
Expand All @@ -249,3 +274,11 @@ HACK_E2E_CLI_BIN=./dist/hack HACK_E2E_DOCKER=1 HACK_E2E_REQUIRE_DOCKER=1 \
Container checks do not establish normal browser routing, native trust, host
lifecycle recovery, persistent host ownership, or interactive TTY acceptance.
Those remain separate NC03 gates.

The registered `native-config-down-hooks` Docker scenario requires the current
compiled CLI and a cached exact Bun image. It checks finite before/after order and
managed host isolation around production ownership probes, exit 17 with pending
stop, malformed-source/env recovery with skipped hooks, and the same owned volume
with a preserved data counter across stops. Final cleanup removes only that
freshly verified isolated fixture volume. PTY, cancellation, timeout, orphan
completion and routed claim retirement require their separate lifecycle controls.
24 changes: 20 additions & 4 deletions docs/reference/native-compose-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ checks that reservation, reserved delivery identity and the document budget, the
synchronizes one private immutable `compose.json` and its manifest. Directory
permissions are `0700`; private files are `0600`. Current/pending receipts anchor
the exact manifest and document device, inode and digest. Input revisions and
document digests stay in private files and are absent from returned generation
identity and engine labels.
document digests stay in private files. The private generation handle exposes its
non-enumerable immutable input revision only for saved down-hook binding validation; public
reports and engine labels omit it.
All rendered resources also carry the exact random store owner nonce, so a new
store cannot silently adopt resources with the same stable Compose namespace.
Container resources carry the reserved workload discriminator `service` or `job`
Expand Down Expand Up @@ -56,7 +57,9 @@ does not prune them or delete persistent data.
Saved mode reads only saved ownership state and Git/filesystem identity. It neither
reads authored contents nor resolves environment values. `loadCurrent`,
`loadPending` and `withLease` support pinned observation. A retained-data `down`
uses saved generation identity without requiring current credentials. Explicit
uses saved generation identity. The adapter may opt into fresh finite down hooks
only when the immutable document carries their saved source-selection binding.
Explicit recovery and generations without that binding require no current credentials. Explicit
pending recovery records a recovery token and clears uncertainty only after the
caller verifies its owned stop completed. No store API deletes persistent data,
prunes generations, or removes abandoned leases.
Expand Down Expand Up @@ -99,7 +102,20 @@ host intent and retains the pending engine effect. Uncertain completion retains
both. Final source/environment checks and workload/route readiness precede the
ready receipt; an after hook cannot rebind the running generation.

Saved observation exposes `beforeHooksPending` for either phase and
Finite `down.before` and `down.after` use a separate `downHooks` callback contract,
accepted only for a fresh normal down with current running generation, null pending
operation, null host intent and a freshness callback. Recovery never replays these
callbacks. Each intent binds phase, exact down operation/token and generation; an
up/run/recovery callback cannot clear it. The adapter captures both phases' private
values before the stop journal. Before failure prevents engine effects. After runs
only after exact owned engine/proxy absence while route claims remain held. A known
finite nonzero clears only its exact hook intent and retains stop pending; unknown
completion retains both. Completion requires null host intent at both receipt
checks around the finalizer. The finalizer rechecks source/env freshness and whole
owner/proxy absence before claim retirement; a later failure or crash preserves the
pending recovery generation, including the retirement/receipt publication gap.

Saved observation exposes `beforeHooksPending` for every phase and
`hostHookPhase`. A legacy token-only receipt decodes as an uncertain before
intent. Unknown phases refuse without rewriting or clearing the receipt.
Saved retaining down may
Expand Down
5 changes: 2 additions & 3 deletions docs/reference/native-compose-renderer.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,11 @@ execution owner still fences source and environment generations before apply.

The default renderer refuses nonempty host declarations. The command owner can
set `beforeHooksOwned: true` for its validated and journaled finite `host.up.before`
and `host.up.after` execution. The flag retains its original name. This admits that
`host.up.after`, `host.down.before` and `host.down.after` execution. The flag retains its original name. This admits that
bounded host contract without placing host commands or
host environment values in the Compose document. Shared effective-binding checks
serve both guest rendering and the hook owner; each supplies its selected baseline
and allowed endpoint policy. Down phases and persistent processes still
refuse. The flag itself provides no execution or freshness proof.
and allowed endpoint policy. Persistent processes still refuse. The flag itself provides no execution or freshness proof.

The component performs no filesystem or process operations, engine requests,
environment lookup, decryption, hook execution, or registry/cache inspection. Its
Expand Down
17 changes: 16 additions & 1 deletion src/lib/native-compose-after-hooks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HackCliError } from "./cli-result.ts";
import { resolveComposeStartupTimeoutMs } from "./compose-startup-budget.ts";
import {
type NativeComposeHook,
NativeComposeHostHookError,
selectNativeComposeAfterHooks,
} from "./native-compose-host-contract.ts";
Expand All @@ -18,9 +19,23 @@ export async function prepareNativeComposeAfterHooks(opts: {
readonly projectRoot: string;
readonly signal: AbortSignal;
readonly json: boolean;
}) {
return await prepareNativeComposeFiniteHooks({
...opts,
hooks: selectNativeComposeAfterHooks(opts.inputs.result.plan),
});
}

/** Capture a finite phase's target values before its journal; no commands or values are persisted. */
export async function prepareNativeComposeFiniteHooks(opts: {
readonly inputs: Inputs;
readonly hooks: readonly NativeComposeHook[];
readonly projectRoot: string;
readonly signal: AbortSignal;
readonly json: boolean;
}) {
await opts.inputs.assertFresh();
const hooks = selectNativeComposeAfterHooks(opts.inputs.result.plan);
const hooks = opts.hooks;
const values = new Map<string, Readonly<Record<string, string>>>();
for (const hook of hooks) {
values.set(hook.name, await opts.inputs.resolveHostValues(hook.name));
Expand Down
Loading
Loading