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
29 changes: 23 additions & 6 deletions .github/workflows/layered-workspaces.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ on:
default: false
type: boolean
run_real_framework_handoffs:
description: "Qualify pinned Go, pnpm, npm, Python, and CMake repositories through A -> B -> C macOS NFS lanes"
description: "Qualify pinned Go, Go workspace, Yarn, Bun, pnpm, npm, Python, uv, and CMake repositories through A -> B -> C macOS NFS lanes"
required: false
default: false
type: boolean
Expand All @@ -60,6 +60,8 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test -p trail-environment-adapter-sdk --locked
- run: cargo test -p trail-environment-adapter-sdk --example ecosystem-build-adapter --locked
- run: python3 -m unittest scripts/test_check_external_build_system_handoff.py
- run: cargo test -p trail view_core_ --no-default-features
- if: ${{ runner.os != 'Windows' }}
run: cargo test -p trail workspace_layer --no-default-features
Expand Down Expand Up @@ -105,7 +107,7 @@ jobs:
- run: cargo test -p trail real_cmake_configure_build_and_clean_stay_lane_private -- --nocapture
env:
TRAIL_RUN_FUSE_COW_TESTS: "1"
- run: cargo test -p trail real_python_venvs_embed_lane_paths_and_remain_isolated -- --nocapture
- run: cargo test -p trail real_python_venvs_use_direct_private_bindings_and_remain_isolated -- --nocapture
env:
TRAIL_RUN_FUSE_COW_TESTS: "1"
- run: cargo test -p trail failed_mounted_initializer_preserves_previous_generation_and_real_uppers -- --nocapture
Expand Down Expand Up @@ -207,7 +209,7 @@ jobs:
- run: cargo test -p trail real_cmake_configure_build_and_clean_stay_lane_private -- --nocapture
env:
TRAIL_RUN_NFS_COW_TESTS: "1"
- run: cargo test -p trail real_python_venvs_embed_lane_paths_and_remain_isolated -- --nocapture
- run: cargo test -p trail real_python_venvs_use_direct_private_bindings_and_remain_isolated -- --nocapture
env:
TRAIL_RUN_NFS_COW_TESTS: "1"
- run: cargo test -p trail failed_mounted_initializer_preserves_previous_generation_and_real_uppers -- --nocapture
Expand Down Expand Up @@ -266,7 +268,7 @@ jobs:
strategy:
fail-fast: false
matrix:
framework: [go, pnpm, npm, python, cmake]
framework: [go, go-workspace, yarn, bun, pnpm, npm, python, uv, cmake, cmake-modern]
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -276,22 +278,37 @@ jobs:
path: ${{ runner.temp }}/trail-real-framework-candidate
- name: Restore candidate executable permission
run: chmod 0755 "${{ runner.temp }}/trail-real-framework-candidate/trail"
- if: ${{ matrix.framework == 'go' }}
- if: ${{ matrix.framework == 'go' || matrix.framework == 'go-workspace' }}
uses: actions/setup-go@v5
with:
go-version: "1.26.x"
- if: ${{ matrix.framework == 'pnpm' || matrix.framework == 'npm' }}
- if: ${{ matrix.framework == 'yarn' || matrix.framework == 'bun' || matrix.framework == 'pnpm' || matrix.framework == 'npm' }}
uses: actions/setup-node@v4
with:
node-version: "22"
- if: ${{ matrix.framework == 'yarn' }}
run: corepack enable && corepack prepare yarn@1.22.22 --activate
- if: ${{ matrix.framework == 'bun' }}
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.10"
- if: ${{ matrix.framework == 'npm' }}
run: npm install --global npm@11.12.1
- if: ${{ matrix.framework == 'python' || matrix.framework == 'uv' }}
uses: astral-sh/setup-uv@v6
with:
version: "0.11.19"
- if: ${{ matrix.framework == 'pnpm' }}
run: corepack enable && corepack prepare pnpm@10.14.0 --activate
- if: ${{ matrix.framework == 'cmake-modern' }}
run: brew install ninja ccache
- name: Qualify ${{ matrix.framework }} A -> B -> C handoff
run: scripts/verify-real-framework-handoff.sh "${{ matrix.framework }}"
env:
TRAIL_BIN: ${{ runner.temp }}/trail-real-framework-candidate/trail
TRAIL_FRAMEWORK_EVIDENCE_DIR: ${{ runner.temp }}/trail-real-framework-${{ matrix.framework }}
TRAIL_FRAMEWORK_WORK_ROOT: ${{ runner.temp }}/trail-real-framework-work-${{ matrix.framework }}
TRAIL_CMAKE_CONFIGURE_PRESET: ${{ matrix.framework == 'cmake-modern' && 'dev' || '' }}
- uses: actions/upload-artifact@v4
if: ${{ always() }}
with:
Expand Down
22 changes: 21 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,33 @@ All notable changes to Trail are documented in this file. Trail follows

## [Unreleased]

### Added

- Environment support now includes contained Go multi-module workspaces, real frozen
Yarn Classic and Bun handoffs, project-aware `uv sync --frozen`, and modern
CMake/Ninja/preset/toolchain/ccache/vcpkg planning. Node native addons and lifecycle
scripts require an exact committed deny-by-default approval with platform/toolchain
identity and sandboxed output bounds.
- Versioned experimental Bazel, Gradle, Maven, and Nix adapter packages exercise the
common protocol-v2 host lifecycle. Nix records pure locked `/nix/store` results and a
digest-pinned builder as provider-owned immutable identities while Trail creates only
lane-private profile/state; it never copies the store or executes Nix in the adapter.
- Canonical ecosystem certification evidence now binds repository/tool/distribution
identities, A → B → C ancestry, deterministic plans, caches/private outputs,
semantic validations, identity invalidation, and hashes of every raw report. Public
environment plans expose adapter implementation and distribution digests consistently
through Rust, CLI JSON, HTTP, MCP, and OpenAPI.

### Fixed

- Managed lane commands now derive fixed policy, resolved executable, cache,
and output bindings from each active environment adapter instead of injecting
Cargo/npm defaults globally. Go, pnpm/npm/Yarn/Bun, Python, and CMake commands
receive isolated framework-native caches and exact tool paths, while inactive
frameworks no longer leak variables into the command.
frameworks no longer leak variables into the command. Cargo-managed commands
also discard inherited profile, target, wrapper, and Rust-flag overrides before
applying the pinned lane policy, so compatible dependency fingerprints remain
reusable across lanes.
- Managed execution now rejects environment-bearing materialized lanes before
emitting an impossible resolution command and recommends a new
`--workdir-mode auto` lane. Layered workspace backends remain required for
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ A layered lane keeps two kinds of state separate:
readiness, merge, and Git export can preserve them.
- Framework artifacts are dependency installs, compiler targets, bundles,
generated caches, and other tool output. Trail discovers them through Cargo,
Node, Next.js, Vite, repository v2, or adapter-v3 contracts; they do not enter
Go, Node, Python, CMake, repository v2, or adapter-plugin contracts; they do not enter
source history unless an explicit declared source export is authorized.

The reusable artifact path is content addressed. Discovery reads source markers
Expand All @@ -208,6 +208,15 @@ Node components receive package-manager caches plus a direct private
`TRAIL_CMAKE_BUILD_DIR`. Inactive frameworks inject no cache, tool, or output
variables into the command.

The built-ins cover Go modules and contained `go.work` graphs, npm/pnpm/Yarn
Classic/Bun frozen installs, hash-locked and `uv.lock` Python projects, and
CMake/Ninja/presets/ccache with pinned vcpkg manifest authority. Yarn Berry/PnP,
unfrozen Python inputs, unsafe CMake includes, and unapproved Node lifecycle scripts
fail closed. Experimental protocol-v2 example packages locally qualify Bazel, Gradle,
Maven, and Nix without adding framework-specific execution paths to Trail core; see
[ecosystem environment certification](docs/lanes/ecosystem-environment-certification.md)
for exact platform evidence and pending hosted gates.

```sh
trail env discover fix-login
trail env plan fix-login
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ These docs are written from the current Rust code, CLI definitions, exported mod
- [Events, traces, and spans](lanes/events-traces-and-spans.md)
- [Tests, evals, gates, and readiness](lanes/tests-evals-gates-and-readiness.md)
- [Handoff, review, and merge](lanes/handoff-review-and-merge.md)
- [Ecosystem environment certification](lanes/ecosystem-environment-certification.md)

## Integrations

Expand Down
19 changes: 15 additions & 4 deletions docs/design/environment-adapter-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -1264,9 +1264,20 @@ explicit source-export contracts.

Bazel/Nix-like provider stores use a protocol-v2 plugin metadata component containing
sorted `verified_external` entries. Each entry binds a provider token, opaque bounded
reference, SHA-256 digest, and platform identity. It has no action, cache, output,
runtime allocation, or Trail-owned cleanup. OCI runtime declarations remain restricted
to `oci_image`, so a generic store reference cannot be relabeled as a container image.
reference, SHA-256 digest, and platform identity. It has no action, cache, runtime
allocation, or Trail-owned cleanup. A provider-store plan may additionally declare only
lane-scoped `writable_private`, non-reused, never-published client state; Trail creates
that state without invoking the adapter. OCI runtime declarations remain restricted to
`oci_image`, so a generic store reference cannot be relabeled as a container image.

The locally qualified Nix example requires a strict committed `trail.nix.toml` marker and a
matching pinned `flake.lock`. The marker records `locked = true`, `pure = true`, exact
Nix version, digest-pinned OCI builder/platform, and package/check `/nix/store` paths plus
NAR SHA-256 digests. Changed lock bytes, builder substitution, unlocked/impure markers,
malformed store references, and writable/shared external identities fail planning. The
host records metadata and lane-private profile/state only; qualification performs the
actual `nix build --offline --no-write-lock-file --option pure-eval true` outside the
adapter and verifies lock bytes are unchanged.

The implemented `trail/cmake-build@1` adapter covers the safe first slice: discovery,
deterministic host/tool compatibility identity, atomic lane-private build-directory
Expand Down Expand Up @@ -1319,7 +1330,7 @@ trail lane exec <lane> -- "$TRAIL_VENV_PYTHON" -m pytest

The supported install contracts are `uv.lock`, a hash-pinned
`requirements.lock`, or a verified Trail-managed hash-bearing requirements snapshot.
`uv.lock` uses `uv sync --frozen --no-install-project`; requirements snapshots use
`uv.lock` uses contained project-aware `uv sync --frozen --offline`; requirements snapshots use
`uv pip sync --require-hashes`. A plain `requirements.txt`, Poetry/PDM/Pipenv lock, or
unhashed requirements file is rejected until an adapter can implement its exact frozen
installation semantics.
Expand Down
9 changes: 9 additions & 0 deletions docs/design/guardrails-security-and-redaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ Repository v2 and adapter v3 requests may narrow a certified profile but cannot
widen it. V1/v2 adapters keep their legacy authority and cannot obtain v3
resolution, source-export, or attestation privileges through omitted fields.

Provider-owned external artifacts are untrusted claims until their complete typed
identity passes host validation. Protocol-v2 plans cannot combine them with actions or
caches and cannot relabel a generic store reference as an OCI image. Their only optional
filesystem state is a layerless, lane-scoped `writable_private` directory with no reuse,
gate, or publication; Trail creates it without executing the adapter. Nix qualification
additionally binds a pinned lock digest, pure/locked marker, digest-pinned builder image,
exact store paths, platform, and NAR SHA-256 digests. Trail never treats a marker as
authority to fetch, execute, access a host store, or clean provider content.

Secret bytes are never key material. A phase that receives a secret is tainted;
its output must remain lane private and cannot enter shared CAS, a reusable
materialization, an attestation, or a source export. Candidate ingestion also
Expand Down
11 changes: 9 additions & 2 deletions docs/design/universal-lane-environments.md
Original file line number Diff line number Diff line change
Expand Up @@ -627,8 +627,10 @@ Conformance fixtures also cover ecosystems that have no built-in adapter. A
Maven/Gradle-like checksum graph plus private build state and an unknown custom generator
compile through repository v2. Bazel/Nix-like content stores compile through the generic
plugin `verified_external` identity: Trail records provider/reference/digest/platform
metadata but creates no layer, cache, runtime, or cleanup claim. These are compositions
of common contracts, not framework switches in the lane backend.
metadata but creates no layer, cache, runtime, or cleanup claim. Such a plan may pair
the immutable external identity with layerless lane-private client state (for example a
Nix profile), but only with no reuse or publication and with no adapter action. These are
compositions of common contracts, not framework switches in the lane backend.

Each lane pins a generation independently. Syncing lane A cannot change the active
generation, private upper, services, or secret handles of lane B. A new artifact may be
Expand Down Expand Up @@ -886,6 +888,11 @@ policy decisions, stale reasons, operation links, and redaction rules. Long-runn
build, verification, and runtime operations use Trail operations with progress events
and cancellation rather than blocking opaque requests.

`EnvironmentPlanReport` includes both `adapter_implementation_version` and
`adapter_distribution_digest`. Automation must bind both values before accepting a plan:
the canonical adapter identity alone does not prove which executable/package bytes
produced it. CLI JSON, HTTP, MCP structured content, and OpenAPI expose the same fields.

The shared Rust operation layer now exposes artifact inspection, attach/sample/full/
reproducibility-evidence verification, quarantine list/show/resolve, bounded content
reachability, workspace/envelope CAS accounting, resolution reports, and source-export
Expand Down
80 changes: 80 additions & 0 deletions docs/lanes/ecosystem-environment-certification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Ecosystem environment certification

Trail distinguishes implementation support from certification evidence. Recognition or
successful planning is not enough to claim that a framework can perform a safe Agent
A → B → C handoff. Certification requires a pinned repository and tools, exact semantic
checkpoint ancestry, deterministic plans, real validation, identity-input invalidation,
private-output isolation, and hashes for every authoritative raw report.

## Current status

| Ecosystem variant | Trail contract | Local real-repository evidence | Hosted status |
| --- | --- | --- | --- |
| Go module and `go.work` multi-module | built-in `trail/go-vendor@1`/`@2` | qualified on macOS NFS-COW | opt-in matrix; not promoted to required CI |
| npm, pnpm, Yarn Classic, Bun | built-in `trail/node@1` | qualified on macOS NFS-COW | opt-in matrix; Yarn Berry/PnP remains unsupported |
| Python hash locks and `uv.lock` projects | built-in `trail/python-venv@1` | qualified on macOS NFS-COW | opt-in matrix; Poetry/PDM/Pipenv locks remain unsupported |
| CMake and modern CMake/Ninja/presets/ccache/vcpkg | built-in `trail/cmake-build@1` | qualified on macOS NFS-COW | opt-in matrix; Conan remains recognized but unsupported |
| Approved Node lifecycle/native addon | built-in Node approval contract | qualified on macOS NFS-COW with real denied-network/write checks | hosted promotion pending |
| Bazel, Gradle, Maven | protocol-v2 example plugin packages | qualified locally with pinned real repositories and offline construction | experimental packages; hosted promotion pending |
| Nix | protocol-v2 example plugin package plus external immutable identities | qualified locally with a pinned `NixOS/templates` revision and digest-pinned Linux/arm64 builder | experimental package; hosted promotion pending |

“Qualified locally” describes passing evidence for the named platform. It is not a
cross-platform claim. A row becomes hosted-certified only after its owning workflow is
green and required for the release; skipped or unavailable native backends are not
passing evidence.

## Canonical evidence

The external-system checker accepts exactly 26 raw JSON reports for a three-lane run:
the installed distribution and conformance result; spawn, checkpoint, repeated plan,
sync, and semantic validation for A/B/C; plus the same identity-authority invalidation
reports. It verifies raw hashes again when reading the sealed `evidence.json`:

```sh
python3 scripts/check-external-build-system-handoff.py \
/path/to/certification-v1 nix owner/repository <revision> external-build.nix
python3 scripts/check-external-build-system-handoff.py \
--verify /path/to/certification-v1
```

The built-in framework harness uses the equivalent
`scripts/check-real-framework-handoff.py` contract. Evidence directories are generated
qualification artifacts and are not committed to the Trail source tree.

## External package workflow

Build one shared example executable and package it with an ecosystem-specific manifest:

```sh
CARGO_TARGET_DIR=/path/out cargo build \
-p trail-environment-adapter-sdk --example ecosystem-build-adapter --locked
TRAIL_ECOSYSTEM_ADAPTER_BIN=/path/out/debug/examples/ecosystem-build-adapter \
scripts/build-ecosystem-adapter-package.sh nix /new/package-directory
trail env plugin inspect /new/package-directory --format json
trail env plugin install /new/package-directory --format json
```

The Bazel, Gradle, and Maven packages declare an offline process-tree action, host-owned
performance caches where applicable, and lane-private mutable output. The Nix package is
metadata-only: it runs no process and receives no cache, network, secret, Docker socket,
or host-store access. It requires a strict `trail.nix.toml` marker whose `flake.lock`
digest matches the pinned source and records:

- `locked = true` and `pure = true`;
- exact Nix version, digest-pinned builder image, and platform;
- package and check `/nix/store/...` references with NAR SHA-256 digests.

Trail records those provider-owned identities and creates only lane-private profile and
client-state directories. Qualification separately proves the reported paths using
`nix build --offline --no-write-lock-file --option pure-eval true`; changing lock bytes
invalidates the Trail component even when JSON meaning and Nix store results are
unchanged.

## Promotion rule

Do not promote a status because a synthetic fixture passes. Promotion requires the
common malicious-package suite, deterministic planning, exact distribution binding,
real-tool validation, native lane isolation, authority invalidation, and sealed raw
evidence. Record the tested repository revision, tool/image digest, operating system,
architecture, and layered backend. Keep unsupported variants fail-closed and name the
missing contract explicitly.
14 changes: 13 additions & 1 deletion docs/reference/cli/integrations-and-maintenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,19 @@ On macOS, `nfs-cow` provides the same write-time copy-up behavior through the
built-in loopback NFS client and requires no kernel extension.
On Windows, `dokan-cow` exposes the same layered semantics through Dokan 2.x.

### Prewarm or promote a lane environment
### Inspect, prewarm, or promote a lane environment

```sh
trail env discover <LANE>
trail env graph <LANE>
trail env plan <LANE> [--component <ID>] [--adapter <ADAPTER>] [--path <ROOT>]
```

`env plan --format json` returns the same `EnvironmentPlanReport` used by HTTP
and MCP. Its required `adapter_identity`, `adapter_implementation_version`, and
`adapter_distribution_digest` fields bind the logical adapter, implementation
version, and exact built-in or installed package distribution that produced the
plan. Automation should compare all three before accepting reusable state.

```sh
trail env sync all <LANE>
Expand Down
Loading
Loading