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

state-models:
name: Runtime state models
Expand Down
31 changes: 29 additions & 2 deletions docs/reference/native-config-compiler.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ versions refuse; omitted fields retain their documented defaults.
"web": {
"image": "example/web:1",
"command": { "exec": ["web", "--port", "3000"] },
"entrypoint": { "exec": [] },
"init": true,
"shutdown": { "signal": "SIGTERM", "grace": "45s" },
"restart": { "kind": "on-failure", "max_retries": 3 },
"working_directory": "/app",
"mounts": [{ "source": ".", "target": "/app", "access": "read-only" }],
"environment": { "TOKEN": { "env_ref": "TOKEN" } }
Expand All @@ -60,6 +64,30 @@ versions refuse; omitted fields retain their documented defaults.
- Omitted command preserves image defaults. `{ "exec": ["program", "argument"] }`
and `{ "shell": "explicit shell source" }` are distinct. Empty commands and NUL
bytes refuse. Argument order is preserved.
- Optional `entrypoint` uses the same explicit `exec` or `shell` tags. Its
`{ "exec": [] }` form clears the image entrypoint; an empty `command.exec`
still refuses. A nonempty argv needs a nonempty executable. Omission preserves
image defaults. Optional `init` is a strict boolean; explicit false stays false.
- Optional `shutdown` has `signal`, `grace`, or both. Signals use the 31 canonical
`SIG`-prefixed Linux names enumerated in the schema; aliases, numeric
signals and realtime signal tokens refuse. Grace is a positive integer in
`ms`, `s`, `m` or `h`, normalized to milliseconds up to 4,294,967,295 ms.
Empty objects, nulls and unknown fields refuse. Omitted signal/grace remains
omitted rather than guessing an image default.
- Optional `restart` is `{ "kind": "no" }`, `{ "kind": "always" }`,
`{ "kind": "unless-stopped" }` or `{ "kind": "on-failure" }`. Only
`on-failure` accepts `max_retries`, a positive integer up to 4,294,967,295.
Jobs reject `always` and `unless-stopped`, including inactive jobs, to preserve
their successful-exit completion contract. Omitted restart means no automatic
restart without adding a serialized default to the plan.
- These process fields are validated intent. Backend signal support, entrypoint
clearing, init behavior, restart execution and shutdown precision require
separate runtime qualification. Planning accepts grace values above 30 seconds;
an existing backend admission limit is not an authored-format restriction.
Local settings cannot supply workload process definitions. All four fields
remain absent when omitted, preserving existing plans and hashes. Compiler
protocol capability `process_plan_version: 1` is required when authored process
settings are present, even when the workload is inactive.
- Mounts select exactly one relative `source` or declared `storage`, an absolute
container `target` and explicit `access`: `read-only` or `read-write`. Targets
must be unique after lexical normalization. Mount order is preserved.
Expand Down Expand Up @@ -107,8 +135,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,
network/security/resources, cache protocols, backend options, arbitrary extensions,
Network/security/resources, cache protocols, advanced build options, 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
qualify a migrated advanced project.
Expand Down
36 changes: 36 additions & 0 deletions packages/config-compiler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,3 +327,39 @@ unchanged. The shared 8 MiB report budget counts binding reports and every expan
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.

## Workload process policy

`process_plan_version: 1` adds optional service/job `entrypoint`, `init`,
`shutdown`, and `restart` fields. Each field preserves omission; the compiler
does not supply a new default or rewrite image behavior. Explicit `init: false`
and explicit no-restart intent remain distinct from omission in the plan and
semantic identity. Every declaration is validated before profile filtering.

`entrypoint` has exactly one form: `{exec: ["program", "argument"]}` or
`{shell: "explicit shell text"}`. The distinct entrypoint type also permits
`{exec: []}` to clear an inherited image entrypoint. A nonempty exec list requires
a nonempty first argument; later arguments can be empty strings. Shell text must
be nonempty. No argument or shell text can contain NUL bytes. Normal commands,
readiness commands, and host commands still reject empty exec lists. Entrypoint
intent is retained independently of the normal workload command.

`shutdown` contains optional `signal` and `grace`, with at least one field required.
Signal is a canonical named Linux signal from the generated `ShutdownSignal`
enum, including the 31 ordinary names from `SIGHUP` through `SIGSYS`. Numeric
signals, prefixless names, aliases (`SIGIOT`, `SIGCLD`, `SIGPOLL`, `SIGUNUSED`),
and realtime syntax refuse. Grace uses the existing positive integer duration
parser for `ms`, `s`, `m`, or `h`, normalized to milliseconds within the supported
u32 millisecond range. Authored grace is not capped by a backend's execution
timeout; a backend must preserve or explicitly refuse unsupported intent during
admission. The plan does not prove that a signal can be delivered by a runtime.

`restart` is a tagged object with `kind: "no"`, `"always"`, `"unless-stopped"`,
or `"on-failure"`. Only `on-failure` may include `max_retries`, a positive u32
integer; omission is retained as symbolic intent. Jobs reject perpetual `always`
and `unless-stopped` policies even when inactive. Jobs permit `no` and
`on-failure` as authored intent, without claiming that a backend executes job
retries. The generated schema includes this job restriction and entrypoint
clearing distinction. Null, unknown fields, tuple forms, ambiguous tags, and
invalid scalar types refuse with redacted diagnostics. Compilation performs no
entrypoint execution, init launch, signal delivery, or restart supervision.
186 changes: 185 additions & 1 deletion packages/config-compiler/generated/hack.project.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,45 @@
}
]
},
"Entrypoint": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"exec": {
"items": {
"type": "string"
},
"prefixItems": [
{
"minLength": 1,
"type": "string"
}
],
"type": "array"
}
},
"required": [
"exec"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"shell": {
"minLength": 1,
"type": "string"
}
},
"required": [
"shell"
],
"type": "object"
}
],
"description": "Unlike a normal command, an explicitly empty exec list clears the image entrypoint."
},
"EnvironmentSelection": {
"additionalProperties": false,
"properties": {
Expand Down Expand Up @@ -715,6 +754,69 @@
}
]
},
"Restart": {
"description": "Retry count only applies to failure-triggered restart. Jobs cannot request perpetual restart.",
"oneOf": [
{
"additionalProperties": false,
"properties": {
"kind": {
"const": "no",
"type": "string"
}
},
"required": [
"kind"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"kind": {
"const": "always",
"type": "string"
}
},
"required": [
"kind"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"kind": {
"const": "unless-stopped",
"type": "string"
}
},
"required": [
"kind"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"kind": {
"const": "on-failure",
"type": "string"
},
"max_retries": {
"format": "uint32",
"maximum": 4294967295,
"minimum": 1,
"type": "integer"
}
},
"required": [
"kind"
],
"type": "object"
}
]
},
"RouteAlias": {
"anyOf": [
{
Expand Down Expand Up @@ -779,6 +881,58 @@
],
"type": "string"
},
"Shutdown": {
"additionalProperties": false,
"description": "Omitted fields preserve image/backend defaults; at least one authored field is required.",
"minProperties": 1,
"properties": {
"grace": {
"pattern": "^[0-9]*[1-9][0-9]*(?:ms|s|m|h)$",
"type": "string"
},
"signal": {
"$ref": "#/$defs/ShutdownSignal"
}
},
"type": "object"
},
"ShutdownSignal": {
"description": "Portable signal spelling; numeric IDs and backend aliases are intentionally not accepted.",
"enum": [
"SIGHUP",
"SIGTERM",
"SIGINT",
"SIGQUIT",
"SIGILL",
"SIGTRAP",
"SIGABRT",
"SIGBUS",
"SIGFPE",
"SIGKILL",
"SIGUSR1",
"SIGSEGV",
"SIGUSR2",
"SIGPIPE",
"SIGALRM",
"SIGSTKFLT",
"SIGCHLD",
"SIGCONT",
"SIGSTOP",
"SIGTSTP",
"SIGTTIN",
"SIGTTOU",
"SIGURG",
"SIGXCPU",
"SIGXFSZ",
"SIGVTALRM",
"SIGPROF",
"SIGWINCH",
"SIGIO",
"SIGPWR",
"SIGSYS"
],
"type": "string"
},
"Source": {
"additionalProperties": false,
"properties": {
Expand Down Expand Up @@ -869,6 +1023,9 @@
},
"type": "array"
},
"entrypoint": {
"$ref": "#/$defs/Entrypoint"
},
"environment": {
"additionalProperties": {
"$ref": "#/$defs/EnvironmentValue"
Expand All @@ -879,6 +1036,9 @@
"image": {
"type": "string"
},
"init": {
"type": "boolean"
},
"mounts": {
"default": [],
"items": {
Expand All @@ -896,6 +1056,12 @@
"readiness": {
"$ref": "#/$defs/Readiness"
},
"restart": {
"$ref": "#/$defs/Restart"
},
"shutdown": {
"$ref": "#/$defs/Shutdown"
},
"working_directory": {
"type": "string"
}
Expand Down Expand Up @@ -942,7 +1108,25 @@
},
"jobs": {
"additionalProperties": {
"$ref": "#/$defs/Workload"
"allOf": [
{
"$ref": "#/$defs/Workload"
},
{
"properties": {
"restart": {
"properties": {
"kind": {
"enum": [
"no",
"on-failure"
]
}
}
}
}
}
]
},
"default": {},
"type": "object"
Expand Down
6 changes: 5 additions & 1 deletion packages/config-compiler/generated/native-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export type Source = { root?: string, mode?: SourceMode, };
export type EnvironmentSelection = { default_overlay?: string, };
export type Build = { context: string, dockerfile?: string, target?: string, };
export type Command = { exec: Array<string>, } | { shell: string, };
export type Entrypoint = { exec: Array<string>, } | { shell: string, };
export type ShutdownSignal = "SIGHUP" | "SIGTERM" | "SIGINT" | "SIGQUIT" | "SIGILL" | "SIGTRAP" | "SIGABRT" | "SIGBUS" | "SIGFPE" | "SIGKILL" | "SIGUSR1" | "SIGSEGV" | "SIGUSR2" | "SIGPIPE" | "SIGALRM" | "SIGSTKFLT" | "SIGCHLD" | "SIGCONT" | "SIGSTOP" | "SIGTSTP" | "SIGTTIN" | "SIGTTOU" | "SIGURG" | "SIGXCPU" | "SIGXFSZ" | "SIGVTALRM" | "SIGPROF" | "SIGWINCH" | "SIGIO" | "SIGPWR" | "SIGSYS";
export type Shutdown = { signal?: ShutdownSignal, grace?: string, };
export type Restart = { "kind": "no", } | { "kind": "always", } | { "kind": "unless-stopped", } | { "kind": "on-failure", max_retries?: number, };
export type True = true;
export type EnvironmentValue = { literal: string, } | { default: string, } | { env_ref: string, } | { unset: True, } | { endpoint: EndpointReference, };
export type StorageKind = "persistent";
Expand All @@ -54,7 +58,7 @@ export type ServiceCondition = "started" | "ready";
export type JobCondition = "completed";
export type Dependency = { service: string, condition: ServiceCondition, } | { job: string, condition: JobCondition, };
export type Readiness = { "kind": "exec", command: Command, interval: string, timeout: string, retries: number, } | { "kind": "http", port: number, path: string, interval: string, timeout: string, retries: number, } | { "kind": "tcp", port: number, interval: string, timeout: string, retries: number, };
export type Workload = { image?: string, build?: Build, command?: Command, working_directory?: string, mounts?: Array<Mount>, environment?: { [key in string]: EnvironmentValue }, depends_on?: Array<Dependency>, profiles?: Array<string>, readiness?: Readiness, };
export type Workload = { image?: string, build?: Build, command?: Command, entrypoint?: Entrypoint, init?: boolean, shutdown?: Shutdown, restart?: Restart, working_directory?: string, mounts?: Array<Mount>, environment?: { [key in string]: EnvironmentValue }, depends_on?: Array<Dependency>, profiles?: Array<string>, readiness?: Readiness, };
export type Project = { schema_version: 1, name: string, source?: Source, services?: { [key in string]: Workload }, jobs?: { [key in string]: Workload }, storage?: { [key in string]: Storage }, profiles?: Array<string>, environment?: EnvironmentSelection, worktree?: WorktreePolicy, host?: HostConfig, routes?: Routes, open?: OpenConfig, host_bindings?: { [key in string]: HostBindingTarget }, };
export type Plan = { plan_version: 1, name: string, source: Source, environment: EnvironmentSelection, worktree: WorktreePolicy, host?: HostConfig, routes?: Routes, open?: OpenConfig, host_bindings?: { [key in string]: HostBindingTarget }, selected_profiles: Array<string>, storage: { [key in string]: Storage }, services: { [key in string]: Workload }, jobs: { [key in string]: Workload }, };
export type Diagnostic = { code: string, message: string, pointer: string, line: number, column: number, };
Expand Down
Loading
Loading