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
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,42 @@ on:
pull_request:

jobs:
config-compiler:
name: Config compiler (${{ matrix.os }})
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with:
bun-version: "1.4.2"
- uses: dtolnay/rust-toolchain@1.97.1
with:
components: rustfmt, clippy
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: bun install --frozen-lockfile
- run: bun run check:config-compiler && bun run test:config-compiler
- run: bun run build:config-compiler
- name: Independently validate generated schema shape corpus
run: |
python3 -m venv "$RUNNER_TEMP/config-schema"
"$RUNNER_TEMP/config-schema/bin/python" -m pip install -r scripts/config-schema-requirements.txt
"$RUNNER_TEMP/config-schema/bin/python" scripts/check-config-schema.py
- name: Test transport, projections and compiled bundle without host toolchains
run: |
bunx ultracite check scripts/build-config-compiler.ts scripts/check-config-compiler-cli.ts tests/config-compiler-build.test.ts tests/native-config-compiler.test.ts tests/native-config-command.test.ts tests/native-config-dto.test.ts
bun test tests/config-compiler-build.test.ts tests/native-config-compiler.test.ts tests/native-config-command.test.ts tests/native-config-dto.test.ts
bun run --cwd packages/cli typecheck
bun run build
bun scripts/check-config-compiler-cli.ts

state-models:
name: Runtime state models
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ dist
# v5 candidate build and runtime state; never shares the installed v4 home
.hack-local/
packages/runtime-core/target/
packages/config-compiler/target/

# Local planning, historical evidence and retired documentation
/_docs/
Expand Down
1 change: 1 addition & 0 deletions .hack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ hack run --profile toolchain toolchain -- test
hack run --profile toolchain toolchain -- check
hack run --profile toolchain toolchain -- rust
hack run --profile toolchain toolchain -- rust-check
hack run --profile toolchain toolchain -- config-compiler
hack run --profile toolchain toolchain -- build
hack run --profile toolchain toolchain -- exec bun index.ts --help
```
Expand Down
10 changes: 9 additions & 1 deletion .hack/toolchain/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ case "$task" in
check) install_deps; bun run typecheck; exec bun run check ;;
rust-check) cargo fmt --manifest-path packages/runtime-core/Cargo.toml --check; exec cargo clippy --locked --manifest-path packages/runtime-core/Cargo.toml --target-dir /build/rust --all-targets --all-features --jobs 2 -- -D warnings ;;
rust) exec cargo test --all-features --locked --manifest-path packages/runtime-core/Cargo.toml --target-dir /build/rust --jobs 2 "$@" ;;
config-compiler)
install_deps
cargo fmt --manifest-path packages/config-compiler/Cargo.toml --check
cargo clippy --locked --manifest-path packages/config-compiler/Cargo.toml --target-dir /build/config-compiler --all-targets --jobs 2 -- -D warnings
cargo test --locked --manifest-path packages/config-compiler/Cargo.toml --target-dir /build/config-compiler --jobs 2
export HACK_CONFIG_COMPILER_TARGET_DIR=/build/config-compiler
exec bun scripts/build-config-compiler.ts
;;
build)
install_deps
# Bun 1.3.9 stages in cwd; its cross-device fallback can emit zero-filled
Expand All @@ -26,5 +34,5 @@ case "$task" in
exec /app/dist/hack --version
;;
exec) exec "$@" ;;
*) echo "Tasks: models, install, test [paths], check, rust [args], rust-check, build, exec <command>" >&2; exit 2 ;;
*) echo "Tasks: models, install, test [paths], check, rust [args], rust-check, config-compiler, build, exec <command>" >&2; exit 2 ;;
esac
11 changes: 10 additions & 1 deletion docs/guides/native-candidate.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ mise exec -- scripts/build-native-candidate.sh /absolute/new/hack-native-bundle

The destination must not exist. The bundle contains `hack-native`, the static Linux
ARM64 `hack-relay-guest`, compiled normal CLI `hack-cli`, the `hack-v5` entrypoint,
the compiled `hack-config-compiler` and generated `hack.project.schema.json`,
provider pins, this guide, `SHA256SUMS`, and one content-addressed shared MCP bundle
under `mcp/BUNDLE_ID/`. Its manifest and native adapter, owner, and compiled backend
are included in the outer checksums. The relay uses
Expand All @@ -29,7 +30,7 @@ For versioned candidate packages, channel rules and publishing gates are describ
in the [prerelease guide](https://github.com/hack-dance/hack/blob/next/docs/guides/prereleases.md),
and the separate `hack-next` installation path is described in the
[candidate installer guide](https://github.com/hack-dance/hack/blob/next/docs/guides/candidate-install.md).
The build re-signs the final compiled frontend and MCP backend with local ad-hoc signatures and
The build re-signs the compiled frontend, config compiler, and MCP backend with local ad-hoc signatures and
strictly verifies all macOS executables before generating checksums. This checks
code integrity; an ad-hoc signature does not establish a publisher identity or
provide Apple notarization. Verify `SHA256SUMS` after copying the complete bundle.
Expand All @@ -38,6 +39,14 @@ selections, set `artifact` to this bundled file's absolute path and
`artifact_sha256` to its entry in `SHA256SUMS`; the runtime verifies it again before
delivery. The `native-stream-relay` feature does not replace this dependency relay.

The compiler and generated schema stay beside `hack-cli` as one checksummed pair.
Packaging and installation refuse a partial, changed, or aliased pair. The compiler
is executable; the schema is private data. Older bundles without either file remain
installable and selectable. An existing channel with an older retained installer
must explicitly run `upgrade-manager` with the reviewed installer before selecting
a bundle with the new pair, or use a fresh channel root. This upgrades the manager;
it does not migrate the retained runtime homes.

Shared MCP remains opt-in. Select the verified nested bundle with the existing
installer; substitute `--cursor` or `--codex` as needed:

Expand Down
26 changes: 25 additions & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ hack branch open <name> [options]

## `hack config`

Read/write hack.config.json values
Read/write legacy config or validate an explicit native project file

### Usage

Expand All @@ -504,6 +504,7 @@ hack config <subcommand> [options]
| --- | --- |
| `hack config get <key>` | Read a value from hack.config.json |
| `hack config set <key> <value>` | Update a value in hack.config.json |
| `hack config validate` | Validate an explicit native project file without starting workloads |

### Options

Expand Down Expand Up @@ -568,6 +569,29 @@ hack config set <key> <value> [options]
| `--help, -h` | Show help |
| `--version, -v` | Show version |

## `hack config validate`

Validate an explicit native project file without starting workloads

### Usage

```bash
hack config validate [options]
```

Uses the matching bundled Rust compiler. This experimental command does not discover a project, resolve secrets, or adopt native configuration for runtime commands.

### Options

| Option | Description |
| --- | --- |
| `--file <path>` | Required native project JSON file |
| `--profile <names>` | Comma-separated declared native profiles |
| `--json` | Output JSON (machine-readable) |
| `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) |
| `--help, -h` | Show help |
| `--version, -v` | Show version |


## `hack session`

Expand Down
145 changes: 145 additions & 0 deletions docs/reference/native-config-compiler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Native config compiler foundation

This is an experimental, pure compiler for a bounded subset of the planned
`.hack/hack.project.json` format. It does not discover projects, execute workloads,
import Compose, migrate data, decrypt environment values, perform host admission,
or change how existing projects run. A successful compile is syntax and semantic
validation, not backend capability or application acceptance.

The standalone `packages/config-compiler` Rust package has no dependency on the
native runtime, virtualization, Docker, or platform provider APIs. It uses the
repository's pinned Rust 1.97.1 and committed Cargo lockfile when building. The
compiled executable requires no host Rust installation, network, or VM.

## Supported authored core

`schema_version` must be `1`; `name` is required. Services, jobs, storage, profiles
and project environment selection default to empty. Source defaults to
`{"root":".","mode":"host-mounted"}`. Unknown fields, explicit nulls and unsupported
versions refuse; omitted fields retain their documented defaults.

```json
{
"schema_version": 1,
"name": "example",
"services": {
"web": {
"image": "example/web:1",
"command": { "exec": ["web", "--port", "3000"] },
"working_directory": "/app",
"mounts": [{ "source": ".", "target": "/app", "access": "read-only" }],
"environment": { "TOKEN": { "env_ref": "TOKEN" } }
}
}
}
```

- Each workload selects exactly one `image` or `build`. Basic build accepts a
relative `context`, a relative `dockerfile` (default `Dockerfile`) and optional
`target`. Advanced build settings are not accepted in this slice.
- 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.
- 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.
- Declared storage currently accepts only `kind: persistent`, `scope: worktree`.
This is a symbolic resource declaration; it grants no creation or deletion
authority. Cache, shared and external storage remain unsupported.
- Environment values use exactly one of `{literal:"public text"}`,
`{default:"public fallback"}`, `{env_ref:"KEY"}` or `{unset:true}`. Empty strings
are valid. `unset:false`, bare values, nulls and combined tags refuse. Variable
destination names match `[A-Za-z_][A-Za-z0-9_]*`; managed `env_ref` names match
the owning store's `[A-Z_][A-Z0-9_]*`. Public literals are authored configuration;
never copy secrets into them. The compiler never reads managed stores or process
environment. Required references stay symbolic; managed-layer selection, missing
keys, remapping collisions and secret delivery remain later owner/admission checks.
- Project `environment.default_overlay` may select a canonical named overlay;
omission selects base. Names must already match `[a-z0-9]+(?:-[a-z0-9]+)*`;
noncanonical spellings refuse rather than selecting a normalized different name.
Local override files are not accepted by this compiler.
- 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
namespace. Duplicate edges and cycles refuse, including in disabled workloads.
- Readiness accepts tagged `exec`, `http` and `tcp` definitions. Exec adds
`command`; HTTP adds `port` and an absolute `path`; TCP adds `port`. All require
`interval`, `timeout` and positive `retries`. Durations are positive integer
`ms`, `s`, `m` or `h`, normalized to millisecond strings up to 4,294,967,295 ms.
Port range is 1–65,535. Preserving a check does not promise backend enforcement.
- Root `profiles:["dev"]` declares profiles. A workload's `profiles:["dev"]`
assigns membership. Unprofiled workloads are enabled by default; explicit
selections enable matching workloads. Unknown selections and dependencies from
enabled workloads onto disabled workloads refuse. Profiles do not automatically
enable dependencies or hide invalid authored definitions.

Project, workload, storage, build target and profile names are canonical lowercase
ASCII letters/digits followed by letters/digits, `.`, `_` or `-`, at most 63 bytes.
Names and profile lists must be unique in their applicable namespace. Relative
paths use POSIX syntax, stay project-relative, and reject absolute paths, `..`,
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, shutdown/restart policies, host hooks/processes, endpoint references,
network/security/resources, cache protocols, backend options, arbitrary extensions,
and local/worktree policy 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.

## Protocol and diagnostics

The current CLI exposes explicit validation only:

```sh
hack config validate --file .hack/hack.project.json
hack config validate --file .hack/hack.project.json --profile dev,test --json
```

`--file` is required; this command does not switch project discovery or runtime
execution to the native format. `--json` returns the 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}`.
- `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.
- Success exits 0 and emits `{transport_version:1,ok:true,plan,semantic_hash}`.
The plan declares `plan_version:1`. Failure exits 1 and emits
`{transport_version:1,ok:false,diagnostics:[...]}`. One deterministic first
diagnostic contains a stable code, fixed redacted message, JSON pointer and
one-based line/byte-column. Semantic errors use the nearest authored value's
location; errors for a missing property or CLI-selected profile may point to its
containing object. Input contents and parser excerpts never appear in messages.
- Invalid invocation exits 2 with fixed usage on stderr and no JSON on stdout.
- `hack-config-compiler generate DIR` writes deterministic
`hack.project.schema.json` (2020-12) and `native-config.ts` projections.

Duplicate keys, including escaped-equivalent keys in nested objects and arrays,
are rejected before map insertion. Graph cycle checking is iterative. Serialization
orders object keys, profile sets and dependency sets deterministically, materializes
defaults, normalizes portable paths and durations, and preserves semantic command
and mount order. `semantic_hash` is lowercase SHA-256 of the serialized normalized
plan; it contains no managed secret values, ciphertext or host admission metadata.
It is not a freshness fence, artifact signature or resource identity.

The JSON Schema and DTOs describe wire shape. Rust additionally enforces names,
paths, dependencies, cycles, profiles and other contextual rules. The shared shape
corpus is `packages/config-compiler/tests/fixtures/schema-corpus.json`; semantic
negative cases live in Rust tests. TypeScript types cannot enforce runtime limits
or reject extra properties supplied through untyped inputs.

## Development checks

```sh
bun run build:config-compiler
cargo +1.97.1 fmt --manifest-path packages/config-compiler/Cargo.toml --check
cargo +1.97.1 clippy --locked --manifest-path packages/config-compiler/Cargo.toml --all-targets -- -D warnings
cargo +1.97.1 test --locked --manifest-path packages/config-compiler/Cargo.toml
```

Generated projections must match a fresh `generate` run. Cross-platform packaging,
CLI transport, installed-sidecar checks and schema-validator qualification are
separate integration gates; unit tests alone do not establish them.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
"dev": "bun run --cwd packages/cli dev",
"build": "bun run --cwd packages/cli build",
"build:local": "sh scripts/build-hack-local.sh",
"build:config-compiler": "bun scripts/build-config-compiler.ts",
"check:config-compiler": "cargo fmt --manifest-path packages/config-compiler/Cargo.toml --check && cargo clippy --locked --manifest-path packages/config-compiler/Cargo.toml --target-dir .hack-local/config-compiler-target --all-targets --jobs 2 -- -D warnings",
"test:config-compiler": "cargo test --locked --manifest-path packages/config-compiler/Cargo.toml --target-dir .hack-local/config-compiler-target --jobs 2",
"test:local": "cargo test --locked --manifest-path packages/runtime-core/Cargo.toml --target-dir .hack-local/target --jobs 2",
"check:local": "cargo fmt --manifest-path packages/runtime-core/Cargo.toml --check && cargo clippy --locked --manifest-path packages/runtime-core/Cargo.toml --target-dir .hack-local/target --all-targets --jobs 2 -- -D warnings",
"build:runtime-image": "bun run --cwd packages/cli build:runtime-image",
Expand Down
8 changes: 7 additions & 1 deletion packages/cli/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
{
"extends": "../../tsconfig.json",
"include": ["index.ts", "../../src", "../../tests"],
"include": [
"index.ts",
"../../src",
"../../tests",
"../../scripts/build-config-compiler.ts",
"../../scripts/check-config-compiler-cli.ts"
],
"exclude": [
"../../apps",
"../../dist",
Expand Down
Loading
Loading