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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ jobs:
bunx ultracite check scripts/check-native-host-plan-cli.ts tests/native-host-plan-transport.test.ts src/lib/native-host-plan-protocol.ts
bun test tests/native-host-plan-transport.test.ts
bun scripts/check-native-host-plan-cli.ts
bunx ultracite check scripts/check-native-routing-plan-cli.ts src/lib/native-routing-plan-protocol.ts src/lib/native-routing-inputs.ts tests/native-routing-plan-transport.test.ts tests/native-routing-inputs.test.ts tests/native-routing-input-race.test.ts tests/native-routing-project.test.ts
bun test tests/native-routing-plan-transport.test.ts tests/native-routing-inputs.test.ts tests/native-routing-input-race.test.ts tests/native-routing-project.test.ts
bun scripts/check-native-routing-plan-cli.ts

state-models:
name: Runtime state models
Expand Down
6 changes: 4 additions & 2 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -580,14 +580,15 @@ Validate native configuration and selected local overlays without starting workl
hack config validate [options]
```

Uses the matching bundled Rust compiler. Without --file, discovers a native project and resolves permitted worktree-local overlay settings. --file validates only the explicit document. Neither mode reads env values, writes state, or starts workloads.
Uses the matching bundled Rust compiler. Without --file, discovers a native project and resolves permitted worktree-local overlay and routing settings. --file validates only the explicit document. Neither mode reads env values, writes state, or starts workloads.

### Options

| Option | Description |
| --- | --- |
| `--file <path>` | Validate only this native project JSON file, without discovery or local overrides |
| `--profile <names>` | Comma-separated declared native profiles |
| `--domain <suffix>` | Select the generated native routing domain suffix |
| `--path, -p <dir>` | Run a project command against a repo path (overrides cwd search) |
| `--env <name|base>` | Apply an optional env overlay by name (use 'base' to bypass overlays) |
| `--json` | Output JSON (machine-readable) |
Expand All @@ -605,13 +606,14 @@ Inspect native environment binding completeness without decrypting or starting w
hack config plan [options]
```

Resolves the native project and permitted local env selection, then inspects managed-env names, winning scopes and secret flags. Managed YAML is parsed by its existing owner; keys and values are never decrypted or returned. Required unresolved refs return a nonzero exit. This experimental report does not establish runtime admission or enable native execution; use config validate for validation without managed-document reads.
Resolves the native project, permitted local env selection and routing preview, then inspects managed-env names, winning scopes and secret flags. Managed YAML is parsed by its existing owner; keys and values are never decrypted or returned. Required unresolved refs return a nonzero exit. This experimental report does not establish runtime admission or enable native execution; use config validate for validation without managed-document reads.

### Options

| Option | Description |
| --- | --- |
| `--profile <names>` | Comma-separated declared native profiles |
| `--domain <suffix>` | Select the generated native routing domain suffix |
| `--path, -p <dir>` | Run a project command against a repo path (overrides cwd search) |
| `--env <name|base>` | Apply an optional env overlay by name (use 'base' to bypass overlays) |
| `--json` | Output JSON (machine-readable) |
Expand Down
101 changes: 90 additions & 11 deletions docs/reference/native-config-compiler.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ versions refuse; omitted fields retain their documented defaults.
command selection.
- Project `worktree.auto_branch` and `worktree.inherit_local` are strict booleans,
both defaulting to true. They appear in the normalized plan. This validation
command uses inheritance policy; it does not create branch instances or execute
`auto_branch` behavior.
command uses inheritance policy and previews the verified linked-worktree branch
namespace for routing. It does not create branch instances.
- Dependencies are `{service:"db",condition:"started"|"ready"}` or
`{job:"init",condition:"completed"}`. References must match the declared kind.
Ready dependencies require explicit readiness. Services and jobs share one name
Expand All @@ -107,12 +107,78 @@ backslashes and drive syntax. Lexical `.` and repeated separators normalize; no
filesystem or symlink resolution occurs. Working directories and mount targets
must be absolute POSIX container paths without `..`.

Routes, container shutdown/restart policies, endpoint references,
Container shutdown/restart policies, endpoint references,
network/security/resources, cache protocols, backend options, arbitrary extensions,
and local settings other than environment selection are not yet implemented. They refuse rather than being
and other local settings are not yet implemented. They refuse rather than being
silently dropped. This foundation does not replace the full native contract or
qualify a migrated advanced project.

## Routing and domain previews

Optional `routes` declares project origins, aliases and named HTTP routes:

```json
{
"schema_version": 1,
"name": "example",
"services": { "web": { "image": "example/web:1" } },
"routes": {
"domain": "hack.local",
"aliases": { "oauth": { "domain": "hack.gy" } },
"oauth_alias": "oauth",
"http": {
"web": { "service": "web", "port": 3000, "hostname": "project" }
}
},
"open": { "prefer": "auto" }
}
```

`routes.domain` is a canonical lowercase DNS suffix. The effective suffix is
explicit `--domain`, checkout local, inherited verified primary local, project,
global `default_domain`, then `hack.local`, in that order. A global domain is
read as one scalar from a bounded regular config file; private global fields are
not sent to the compiler. Invalid or redirected policy files refuse.

Generated origins are `https://[branch.]NAME.SUFFIX`. Branch names are derived
from a verified linked Git worktree and normalized with the existing branch
namespace rules. Primary checkouts, non-Git projects, CI, slim mode and
`worktree.auto_branch:false` omit that namespace. Detached linked worktrees refuse
when a branch is required. `inherit_local:false` disables primary settings without
disabling branch isolation.

`routes.origin` pins an explicit HTTP(S) project origin. An alias selects exactly
one `domain` or `origin`; generated domain aliases follow the same branch namespace,
while explicit origins remain pinned. Origins normalize scheme/host case and
default ports. Credentials, paths (including `/`), queries, fragments, wildcards,
ambiguous numeric hosts and invalid ports refuse. Canonical loopback IPv4, bracketed
IPv6 and `localhost` are allowed. Generated names must be valid DNS labels.

Each `routes.http` entry requires a declared **service**, a port in 1–65,535 and
`hostname`: `project` uses the base origin; another canonical relative DNS name
prefixes each project/alias hostname. IP origins cannot be prefixed. Upstream
`protocol` defaults to `http`; it is separate from the browser origin's scheme.
Unknown services, job targets, duplicate origins and expanded route/alias collisions
refuse, including in inactive declarations. Inactive routes stay in the portable
plan but are omitted from the selected routing report. Expansion is bounded before
allocation and charged to the shared planning output budget.

`open.prefer` accepts `auto` (default), `alias` or `dev`. `auto` selects the
explicit `routes.oauth_alias` when present, otherwise the project origin; `alias`
requires that selection; `dev` selects the project origin. Alias names have no
implicit OAuth meaning. Local `open.prefer` overrides inherited/project settings.
`hack.local` remains the development default; an OAuth provider that requires a
public suffix can use an explicitly selected `hack.gy` or custom-domain alias.
Provider registration and browser acceptance are separate checks.

Project-aware `config validate` and `config plan` return `routing_resolution` with
the selected domain and provenance, project/alias origins, branch namespace, open
choice and selected routes. The CLI cross-checks the report against authored
declarations and request context. Routing changes affect the local resolution hash;
local policy never rewrites the authored semantic hash. This is offline planning:
it does not configure DNS, trust certificates, run hooks, bind ports or admit a
runtime. Endpoint bindings and execution remain later integration steps.

## Host declarations

The optional `host` namespace declares ordered lifecycle hooks and named processes:
Expand Down Expand Up @@ -189,6 +255,7 @@ The CLI supports project-aware and explicit-document validation:
```sh
hack config validate --json
hack config validate --path /path/to/native-project --env base --json
hack config validate --domain hack.gy --json
hack config validate --file .hack/hack.project.json
hack config validate --file .hack/hack.project.json --profile dev,test --json
```
Expand All @@ -200,19 +267,22 @@ explicitly selects base; another canonical name selects that overlay. This selec
a name only: overlay existence, managed metadata, keys and required references are
not inspected, and no env values are read. Runtime/adoption commands remain fenced.

`--file` validates only that document, ignores all local files and performs no
project discovery. It cannot be combined with `--path` or `--env`.
`--file` validates only that document, ignores local/global policy files and performs
no project discovery or origin resolution. It cannot be combined with `--path`,
`--env` or `--domain`.
`--json` returns the authored normalized plan, including
authored public literals and commands. Those values are intentionally visible;
diagnostic redaction does not turn the plan into a secret-safe storage format.

- `hack-config-compiler --protocol` emits
`{"transport_version":1,"authored_version":1,"plan_version":1,"resolve_version":1,"local_version":1,"env_plan_version":1,"host_env_plan_version":1}`.
`{"transport_version":1,"authored_version":1,"plan_version":1,"resolve_version":1,"local_version":1,"env_plan_version":1,"host_env_plan_version":1,"routing_plan_version":1}`.
Project-aware validation requires local resolution capabilities; explicit
metadata planning additionally requires `env_plan_version:1`. Older matching
compilers can still serve their supported validation modes.
Host planning requires `host_env_plan_version:1` before acquiring host metadata;
hostless env-plan-v1 compilers remain supported.
Typed routing and policy resolution require `routing_plan_version:1`; unchanged
non-routing calls remain compatible with older matching compilers.
- `hack-config-compiler compile [--profile NAME]...` reads one UTF-8 JSON document
from stdin through EOF. Input is limited to 1 MiB and 64 nested containers. These
are parser safety bounds, not container resource or workload-count limits.
Expand All @@ -228,11 +298,17 @@ diagnostic redaction does not turn the plan into a secret-safe storage format.
from the authored identity and includes explicit targets from inactive workloads.
- Invalid invocation exits 2 with fixed usage on stderr and no JSON on stdout.
- `hack-config-compiler resolve [--profile NAME]...` reads a versioned request:
`{request_version:1,project:"original JSON text",primary_local?:"original JSON text",checkout_local?:"original JSON text",explicit_overlay?:null|string}`.
`{request_version:1,project:"original JSON text",primary_local?:"original JSON text",checkout_local?:"original JSON text",explicit_overlay?:null|string,global_domain?:string,explicit_domain?:string,branch?:string}`.
JSON texts retain duplicate keys until Rust checks each document. Each document
is limited to 1 MiB, their combined text to 3 MiB, and the encoded request to
20 MiB to allow JSON escaping. Diagnostics add `document` identifying `project`,
`primary_local`, `checkout_local` or `request`.
A routing-v1 compiler also accepts `routing_probe:true` on `resolve` only.
This validates authored/local policy and returns `routing_inputs_required:true`
when routing context is needed, without deriving provisional origins. The CLI
then acquires verified global/branch inputs and sends an ordinary resolve.
The probe is not part of the resolution hash and is never a final plan. Old
non-routing compilers receive no probe flag.
- `hack-config-compiler generate DIR` writes deterministic
`hack.project.schema.json`, `hack.local.schema.json` (2020-12) and
`native-config.ts` projections.
Expand All @@ -243,6 +319,7 @@ diagnostic redaction does not turn the plan into a secret-safe storage format.
hack config plan --json
hack config plan --path /path/to/native-project --env qa --profile dev --json
hack config plan --env base
hack config plan --domain hack.gy --json
```

This project-aware command uses the same validated local selection as `config
Expand Down Expand Up @@ -302,8 +379,9 @@ The optional `.hack/hack.local.json` is a separate versioned document:
{"schema_version":1,"environment":{"default_overlay":null}}
```

Only `environment.default_overlay` is supported in this slice. Omission inherits;
null selects base; a canonical name selects that overlay. Other fields, workload
Local settings permit `environment.default_overlay`, `routes.domain` and
`open.prefer`. Omission inherits; overlay null selects base, while domain/open null
refuses. A canonical overlay name selects that overlay. Other fields, workload
definitions, unversioned documents, duplicate keys and unknown versions refuse.
The effective selection is project default/base, then verified primary local,
current checkout local, then explicit `--env`. Each later present value wins.
Expand All @@ -319,7 +397,8 @@ local settings; primary inheritance requires the project root to be the Git root
Success adds `local_resolution` with selected `overlay` (null means base), `origin`,
worktree policy and `resolution_hash`. Local settings do not rewrite the authored
plan or its `semantic_hash`. The separate hash binds normalized supplied local
documents and explicit selection, including missing versus null. Neither hash
documents, supplied routing context and explicit selection, including missing
versus null. Neither hash
proves an atomic multi-file snapshot or provides an admission/freshness fence.
Future apply must recheck private input generations. Adding defaulted worktree
policy changes hashes relative to the earlier experimental compiler; do not reuse
Expand Down
70 changes: 68 additions & 2 deletions packages/config-compiler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ are rejected. Each document is limited to 1 MiB, their combined decoded size to
3 MiB, and the encoded request to 20 MiB. All JSON parsing has a depth budget of 64.
These are parser resource bounds, not runtime workload limits.

A local document permits only `schema_version: 1` and optional `environment`,
which permits only `default_overlay`. Omission inherits, `null` selects base,
A local document permits `schema_version: 1` and optional `environment`,
`routes`, and `open`. Environment permits only `default_overlay`. Omission inherits, `null` selects base,
and a string selects a canonical overlay matching `[a-z0-9]+(?:-[a-z0-9]+)*`.
No normalization silently changes names.

Expand Down Expand Up @@ -204,3 +204,69 @@ bindings. This may refuse near-limit reports before their final encoding reaches
project location. The bound prevents input-to-output amplification and is shared
across workload and host reporting; it is not a process-count or runtime-resource
limit. No metadata-derived hash or managed value is exposed by the budget.


## Typed routing and domain planning

The protocol advertises `routing_plan_version: 1`. Optional project `routes`
contains `domain`, `origin`, named `aliases`, `oauth_alias`, and an `http` map
(default empty). Each HTTP entry requires a declared service, a port from 1–65535,
and `hostname`; `protocol` defaults to `http` and also accepts `https`. Hostname
`project` uses the project origin; other canonical relative DNS labels prefix its
hostname. Jobs cannot receive HTTP routes. All declarations, including inactive
services, are validated before profile filtering. The portable plan retains every
route; the resolved routing report contains only selected services.

An alias has exactly one of `{domain: "example.test"}` or
`{origin: "http://localhost:3000"}`. `oauth_alias` explicitly selects a declared
alias. Optional project `open.prefer` is `auto` (default), `alias`, or `dev`.
`auto` uses the explicitly selected OAuth alias when present, otherwise the project
origin. `alias` without that selection refuses during resolution; `dev` uses the
project origin. This describes navigation intent, not OAuth-provider acceptance.

Local `routes` permits only optional `domain`; local `open` permits only optional
`prefer`. Null is invalid and empty objects inherit. Resolve and plan requests add
optional `global_domain`, `explicit_domain`, and `branch`. Domain precedence is
explicit request, checkout local, inherited primary local, project, global, then
`hack.local`. Open preference precedence is checkout local, inherited primary local,
project, then `auto`. Every supplied document is validated, including shadowed or
opted-out primary settings. Generated project and domain-alias origins are
`https://[branch.]project.domain`; explicit origins are never rewritten. Branch
must be one canonical DNS label. Project names need that DNS grammar only when
used in generated origins.

Domain suffixes use lowercase ASCII DNS labels and require two or more labels,
except the supported `hack` suffix. Decimal or hexadecimal numeric final labels,
underscores, wildcard labels and trailing dots refuse. Relative route hostnames
have their own DNS-label grammar. Explicit origins accept only HTTP or HTTPS,
normalize hostname case and default ports, and permit localhost, strict IPv4 and
bracketed IPv6. Credentials, paths (including `/`), query, fragment, whitespace,
control characters and legacy numeric-IP forms refuse. IPv6 is normalized to
compressed hexadecimal form. A relative route cannot prefix an IP origin.
Duplicate normalized project/alias origins and route expansions refuse, even for
inactive services or projects with no HTTP entries.

Routing becomes active when authored or effective local routing/open settings,
or any domain/branch request input, is present. Successful resolve and plan then
add `routing_resolution`: effective domain and its source, project origin, alias
origins, selected OAuth alias or null, open preference and its source, open origin,
optional branch, and selected route origins with alias expansions. Supplied domain
and branch choices enter the separate resolution hash, not the authored semantic
hash. Existing calls without routing inputs preserve their previous output and
hashes. Empty authored routing/open objects explicitly enable routing defaults.

Resolve alone supports `routing_probe: true` for an acquisition pass. It validates
original documents and static routing references but defers origin expansion and
context-dependent collisions until the caller acquires the global domain and
verified branch. Active routing returns `routing_inputs_required: true` instead
of `routing_resolution`; inactive routing returns neither field. The probe flag
never enters a hash. Plan refuses the probe field, including `false`; a probe is
not a completed routing plan. Consumers must perform the final normal resolution.

Routing expansion counts output space before allocating repeated route/alias
entries, using the same conservative 8 MiB response safety budget as environment
planning. Environment planning reserves the routing report before copying symbolic
bindings. Oversized expansion returns fixed redacted `plan_too_large`; this is an
input-amplification bound, not a runtime route or workload capacity limit. This
package does not register DNS, acquire certificates, bind ports, configure proxies,
open browsers or claim that an origin is reachable.
Loading
Loading