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 @@ -54,6 +54,9 @@ jobs:
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
bunx ultracite check scripts/check-native-endpoint-plan-cli.ts src/lib/native-endpoint-plan-protocol.ts tests/native-endpoint-plan-transport.test.ts tests/native-endpoint-project.test.ts
bun test tests/native-endpoint-plan-transport.test.ts tests/native-endpoint-project.test.ts
bun scripts/check-native-endpoint-plan-cli.ts

state-models:
name: Runtime state models
Expand Down
74 changes: 70 additions & 4 deletions docs/reference/native-config-compiler.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ 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 `..`.

Container shutdown/restart policies, endpoint references,
Container shutdown/restart policies,
network/security/resources, cache protocols, backend options, arbitrary extensions,
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
Expand Down Expand Up @@ -177,7 +177,72 @@ 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.
runtime. Execution remains a later integration step.

## Endpoint references and host bindings

Environment destinations can reference a route, service or logical host binding:

```json
{
"API": { "endpoint": { "kind": "service", "name": "api", "port": 3000, "protocol": "http" } },
"APP_ORIGIN": { "endpoint": { "kind": "route", "name": "web" } },
"SEARCH": { "endpoint": { "kind": "host_binding", "name": "search" } }
}
```

Service references require a declared service, a port from 1 to 65,535 and an
explicit `http`, `https` or `tcp` protocol. Jobs cannot be service targets. Route
references name an entry in `routes.http`. Unknown references and active consumers
of inactive service targets refuse; disabled declarations still receive static
validation. A route endpoint uses the selected origin from `routing_resolution`.
The port and protocol of a direct service endpoint remain typed rather than being
turned into a guessed URL or credentials-bearing connection string.

Optional project `host_bindings` declares targets in a separate logical namespace:

```json
{
"host_bindings": {
"search": { "kind": "host", "port": 9200, "protocol": "http" },
"remote": { "kind": "external", "hostname": "search.example.com", "port": 443, "protocol": "https" }
}
}
```

`host` identifies an endpoint on the machine running the project. Its report
retains `context:"host"` or `context:"workload"`; an execution backend must choose
the appropriate loopback or guest-to-host address. `external` preserves a validated
hostname, port and protocol. Neither definition starts a tunnel, grants ownership
of a process, probes connectivity or configures DNS. Commands, credentials,
resource IDs and source paths are not binding targets. External hostnames cannot
contain credentials, schemes, ports, paths, queries or fragments.

Local `host_bindings` merges by logical name: project, inherited verified primary,
then checkout. A local `null` removes that binding; a later target restores it.
There is no recursive patch or command injection. Actual resolution reports
`host_binding_resolution:{bindings:{NAME:{target,origin}},removed:{NAME:origin}}`,
where `origin` is `project`, `primary_local` or `checkout_local`.
Context-free `--file` validation keeps local-only binding references symbolic.
Project-aware validation rejects missing or removed referenced bindings before
reading managed environment documents. CI, slim and inheritance opt-out rules
apply to binding inheritance just as they do to other local settings.

`config plan` returns endpoint bindings as
`{kind:"endpoint",reference:{...},target:{...}}`. An existing managed key at the
destination produces `env_endpoint_collision`, keeps the managed binding intact,
and makes the plan incomplete with exit 1. Endpoint and unset tags cannot share
an entry. A direct service endpoint in a host invocation produces
`unsupported_endpoint_context` rather than assuming guest DNS works on the host.
This is a planning refusal, not a failure to launch a process.

Endpoint support requires the sidecar's `endpoint_plan_version:1` capability.
Consumers validate report targets against their authored references and existing
routing/binding reports. Authored identity includes project binding declarations;
local binding selection affects only resolution identity. The shared output budget
covers expanded binding reports and endpoint entries. No managed values, ciphertext,
keys or host paths enter these reports. Late hook-produced bindings and runtime
address delivery still require separate execution and generation-fence work.

## Host declarations

Expand Down Expand Up @@ -379,8 +444,9 @@ The optional `.hack/hack.local.json` is a separate versioned document:
{"schema_version":1,"environment":{"default_overlay":null}}
```

Local settings permit `environment.default_overlay`, `routes.domain` and
`open.prefer`. Omission inherits; overlay null selects base, while domain/open null
Local settings permit `environment.default_overlay`, `routes.domain`,
`open.prefer` and `host_bindings`. Omission inherits; overlay null selects base,
binding null removes a logical target, 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,
Expand Down
59 changes: 58 additions & 1 deletion packages/config-compiler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ are rejected. Each document is limited to 1 MiB, their combined decoded size to
These are parser resource bounds, not runtime workload limits.

A local document permits `schema_version: 1` and optional `environment`,
`routes`, and `open`. Environment permits only `default_overlay`. Omission inherits, `null` selects base,
`routes`, `open`, and `host_bindings`. 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 @@ -270,3 +270,60 @@ bindings. Oversized expansion returns fixed redacted `plan_too_large`; this is a
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.

## Structured endpoints and local host bindings

`endpoint_plan_version: 1` adds one exclusive authored environment form:
`{endpoint: {kind, ...}}`. References are `{kind: "route", name}`, a declared
service `{kind: "service", name, port, protocol}`, or a logical binding
`{kind: "host_binding", name}`. Ports are explicit integers from 1–65535 and
protocol is `http`, `https`, or `tcp`. References accept no credentials, path,
query, command, or arbitrary scope. A route selects its centrally derived project
origin, not its OAuth alias. Services and routes must exist in the authored
namespace and cannot target jobs. Every reference is validated before profile
filtering; an active invocation cannot reference an inactive service or route.

Project `host_bindings` is an optional map from canonical logical names (lowercase
letters/digits, single separating hyphens, at most 63 bytes) to either
`{kind: "host", port, protocol}` or
`{kind: "external", hostname, port, protocol}`. External hostname is literal,
canonical lowercase DNS (including a single label), strict dotted IPv4, or
compressed hexadecimal IPv6 in brackets. It has no authority port, credentials,
path, wildcard, query, fragment, or normalization. Explicit external loopback
addresses retain their meaning in the calling context; they do not imply host
gateway access. Use the typed `host` intent for that purpose.

Local maps merge by logical name: project, verified primary when inheritance is
enabled, then current checkout. Local `null` removes that binding, including an
inherited binding; a later target readds it. Null is not a project target and the
whole map cannot be null. Binding definitions contain no process command, resource
ID, source replacement, or credential. Empty maps explicitly enable binding
reporting. Ignored primary maps are still validated and enter resolution identity.

Context-free compile keeps logical host-binding references symbolic, allowing
local provisioning without tracked host addresses. Actual resolve requires every
referenced binding, including those in inactive invocations, to exist after local
merging. Missing references refuse with `unknown_host_binding`; tombstoned
references refuse with `removed_host_binding` at the removing local document's
original location. The authored plan retains only project definitions. Optional
`host_binding_resolution: {bindings, removed}` reports effective typed targets and
each winning/removing `project`, `primary_local`, or `checkout_local` source.
The probe performs this merge/reference check while deferring routing expansion.

Metadata planning produces `{kind: "endpoint", reference, target}` without a
string `value`. Route targets contain a centrally derived `origin`; direct service
targets remain symbolic `{kind: "service", name, port, protocol}`. Typed host
targets add `context: "host" | "workload"`; they require backend-qualified loopback
or gateway translation during execution. External targets retain the canonical
hostname, port and protocol. Direct service references in host invocations produce
an incomplete `unsupported_endpoint_context` diagnostic and omit the destination,
because guest service names are not host addresses.

Endpoints cannot replace a same-key managed baseline entry: the plan preserves
that managed entry, reports `env_endpoint_collision`, and remains incomplete.
Combining endpoint and unset forms refuses; unsetting another key grants no
replacement permission. Existing remapped managed-reference collision rules stay
unchanged. The shared 8 MiB report budget counts binding reports and every expanded
endpoint before insertion. No new fields alter old hashes or replies when absent.
This compiler does not resolve DNS, execute hooks, start services, or prove endpoint
reachability; native execution and backend translation remain separate work.
156 changes: 121 additions & 35 deletions packages/config-compiler/generated/hack.local.schema.json
Original file line number Diff line number Diff line change
@@ -1,68 +1,154 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "LocalConfig",
"type": "object",
"properties": {
"environment": {
"$ref": "#/$defs/LocalEnvironment",
"default": {}
},
"open": {
"$ref": "#/$defs/LocalOpen"
"$defs": {
"EndpointProtocol": {
"enum": [
"http",
"https",
"tcp"
],
"type": "string"
},
"routes": {
"$ref": "#/$defs/LocalRoutes"
"HostBindingTarget": {
"description": "`host` is the host loopback/gateway intent; an external hostname is a literal address.",
"oneOf": [
{
"additionalProperties": false,
"properties": {
"kind": {
"const": "host",
"type": "string"
},
"port": {
"format": "uint16",
"maximum": 65535,
"minimum": 1,
"type": "integer"
},
"protocol": {
"$ref": "#/$defs/EndpointProtocol"
}
},
"required": [
"kind",
"port",
"protocol"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"hostname": {
"maxLength": 253,
"minLength": 1,
"type": "string"
},
"kind": {
"const": "external",
"type": "string"
},
"port": {
"format": "uint16",
"maximum": 65535,
"minimum": 1,
"type": "integer"
},
"protocol": {
"$ref": "#/$defs/EndpointProtocol"
}
},
"required": [
"kind",
"hostname",
"port",
"protocol"
],
"type": "object"
}
]
},
"schema_version": {
"type": "integer",
"format": "uint32",
"maximum": 1,
"minimum": 1
}
},
"additionalProperties": false,
"required": [
"schema_version"
],
"$defs": {
"LocalEnvironment": {
"type": "object",
"additionalProperties": false,
"properties": {
"default_overlay": {
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
"type": [
"string",
"null"
],
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
]
}
},
"additionalProperties": false
"type": "object"
},
"LocalOpen": {
"type": "object",
"additionalProperties": false,
"properties": {
"prefer": {
"$ref": "#/$defs/OpenPreference"
}
},
"additionalProperties": false
"type": "object"
},
"LocalRoutes": {
"type": "object",
"additionalProperties": false,
"properties": {
"domain": {
"type": "string"
}
},
"additionalProperties": false
"type": "object"
},
"OpenPreference": {
"type": "string",
"enum": [
"auto",
"alias",
"dev"
]
],
"type": "string"
}
}
},
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"environment": {
"$ref": "#/$defs/LocalEnvironment",
"default": {}
},
"host_bindings": {
"additionalProperties": {
"anyOf": [
{
"$ref": "#/$defs/HostBindingTarget"
},
{
"type": "null"
}
]
},
"propertyNames": {
"maxLength": 63,
"minLength": 1,
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
"type": "string"
},
"type": "object"
},
"open": {
"$ref": "#/$defs/LocalOpen"
},
"routes": {
"$ref": "#/$defs/LocalRoutes"
},
"schema_version": {
"format": "uint32",
"maximum": 1,
"minimum": 1,
"type": "integer"
}
},
"required": [
"schema_version"
],
"title": "LocalConfig",
"type": "object"
}
Loading
Loading