Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
110 changes: 110 additions & 0 deletions .github/workflows/tool-profile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
name: frozen offline tool profiles

on:
pull_request:
paths:
- .tool-cli-flags.toml
- docs/tool-profile.md
- src/tool_profile.rs
- src/bin/zed-tool.rs
- src/lib.rs
- tests/tool_profile_cli.rs
- .github/workflows/tool-profile.yml
push:
branches: [main]
paths:
- .tool-cli-flags.toml
- docs/tool-profile.md
- src/tool_profile.rs
- src/bin/zed-tool.rs
- src/lib.rs
- tests/tool_profile_cli.rs
- .github/workflows/tool-profile.yml
workflow_dispatch:

permissions:
contents: read

concurrency:
group: tool-profile-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

defaults:
run:
shell: bash

jobs:
tool-profile:
name: frozen profile / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-15, windows-2025]
steps:
- name: Check out exact candidate
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
persist-credentials: false
show-progress: false

- name: Install Rust quality components
uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30
with:
toolchain: stable
components: rustfmt,clippy

- name: Validate staged flags-to-environment contract
run: |
set -euo pipefail
python - <<'PY'
import tomllib
from pathlib import Path

data = tomllib.loads(Path('.tool-cli-flags.toml').read_text())
observed = set()
for flag in data.get('flags', {}).values():
observed.add(flag['env'])
for command in data.get('commands', {}).values():
for flag in command.get('flags', {}).values():
observed.add(flag['env'])
expected = {
'ZED_TOOL_LOCK',
'ZED_TOOL_JSON',
'ZED_TOOL_PORTABLE',
'ZED_TOOL_PLAN_DIGEST',
'ZED_TOOL_TARGET',
'ZED_TOOL_OFFLINE',
'ZED_TOOL_PROFILE',
'ZED_PKG_HOME',
}
assert observed == expected, (observed, expected)
PY

- name: Formatting, unit, and compiled CLI tests
run: |
set -euo pipefail
cargo fmt --all --check
cargo test --locked --lib tool_profile::tests
cargo test --locked --test tool_profile_cli

- name: Current store and lock compatibility
run: |
set -euo pipefail
cargo test --locked --lib store::tests
cargo test --locked --lib project_lock::tests
cargo test --locked --test frozen_offline_prefetch

- name: Strict Clippy
run: |
set -euo pipefail
cargo clippy --locked --bin zed-tool --test tool_profile_cli -- -D warnings

- name: Require a clean checkout
run: |
set -euo pipefail
git diff --exit-code
git status --porcelain=v1 --untracked-files=all | tee /tmp/tool-profile-status.txt
test ! -s /tmp/tool-profile-status.txt
79 changes: 79 additions & 0 deletions .tool-cli-flags.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Dedicated flags-to-environment contract for the staged frozen tool-profile
# CLI. Canonical `zed tool` routing must reuse these exact variable names.

[parse]
command_env = "ZED_TOOL_COMMAND"
unknown_options_env = "ZED_TOOL_UNKNOWN_OPTIONS"
errors_env = "ZED_TOOL_PARSE_ERRORS"
allow_unknown = false

[help]
url = "https://github.com/zed-pkg/zed-cli/blob/main/docs/tool-profile.md"

[flags.lock]
env = "ZED_TOOL_LOCK"
aliases = ["lock"]
type = "string"
default = ".zed/environment.lock.toml"
help = "Project-local EnvironmentLock TOML or JSON."

[flags.json]
env = "ZED_TOOL_JSON"
aliases = ["json"]
type = "bool"
default = "false"
help = "Emit one stable JSON document."

[commands.verify]
help = "Validate exact lock structure, portability, and optional plan identity."

[commands.verify.flags.portable]
env = "ZED_TOOL_PORTABLE"
aliases = ["portable"]
type = "bool"
default = "false"
help = "Require a machine-independent portable lock."

[commands.verify.flags.plan_digest]
env = "ZED_TOOL_PLAN_DIGEST"
aliases = ["plan-digest"]
type = "string"
help = "Require one normalized EnvironmentPlan SHA-256."

[commands.list]
help = "List exact tool variants selected for one target."

[commands.list.flags.target]
env = "ZED_TOOL_TARGET"
aliases = ["target"]
type = "string"
help = "Exact locked target identity."

[commands.install]
help = "Replay exact cached artifacts into a project-local executable profile."

[commands.install.flags.target]
env = "ZED_TOOL_TARGET"
aliases = ["target"]
type = "string"
help = "Exact locked target identity."

[commands.install.flags.offline]
env = "ZED_TOOL_OFFLINE"
aliases = ["offline"]
type = "bool"
default = "false"
help = "Require cache-only installation; mandatory for this staged slice."

[commands.install.flags.profile]
env = "ZED_TOOL_PROFILE"
aliases = ["profile"]
type = "string"
default = ".zed/tools"
help = "Project-local tool profile root."

[commands.install.flags.home]
env = "ZED_PKG_HOME"
aliases = ["home"]
type = "string"
help = "Zed content-addressed store and cache root."
102 changes: 102 additions & 0 deletions docs/tool-profile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Frozen offline tool profiles

`zed-tool` is the staged native-tool replay surface for exact
`EnvironmentLock` files. It does not resolve versions, contact a registry,
execute a manager or plugin, run an install script, or modify a global PATH.

The first slice proves that manager-neutral locked artifacts can be verified,
extracted through Zed's existing content-addressed store, and activated in one
project-local profile without requiring mise or another environment manager.

## Commands

```console
zed-tool [--lock .zed/environment.lock.toml] [--json] verify [--portable] [--plan-digest SHA]
zed-tool [--lock .zed/environment.lock.toml] [--json] list --target TARGET
zed-tool [--lock .zed/environment.lock.toml] [--json] install --target TARGET --offline [--profile .zed/tools] [--home PATH]
```

The dedicated flags contract is `.tool-cli-flags.toml`:

- `ZED_TOOL_LOCK`
- `ZED_TOOL_JSON`
- `ZED_TOOL_PORTABLE`
- `ZED_TOOL_PLAN_DIGEST`
- `ZED_TOOL_TARGET`
- `ZED_TOOL_OFFLINE`
- `ZED_TOOL_PROFILE`
- `ZED_PKG_HOME`

A future canonical `zed tool` route must reuse these names.

## Frozen replay model

Installation performs these steps:

1. Canonicalize the current project and read one project-relative lock without
following a symlink outside the checkout.
2. Parse TOML or JSON through `zed_interfaces::EnvironmentLock` and require
portable validation.
3. Select exactly one locked variant per logical tool for the requested target.
Multiple active versions are rejected until precedence is explicitly
modeled and certified.
4. Reject every artifact format except `tar_gz` and `zip` in this first slice.
5. Find the authenticated bytes at
`$ZED_PKG_HOME/cache/<sha256>.tar.gz`; the extension is the historical cache
convention and the extractor still identifies gzip or ZIP by magic bytes.
6. Require the cached file's exact locked size and SHA-256, then pass it through
Zed's existing per-digest locked, traversal-safe, atomic store extraction.
7. Resolve every locked executable below the authenticated install root,
rejecting missing files, symlink traversal, non-regular files, and missing
Unix execute bits.
8. Reject duplicate executable or alias ownership. Collision checks are ASCII
case-insensitive so a profile remains safe on Windows.
9. Stage a complete profile under `.zed/tools/v1/<target>`, fsync its state,
and atomically replace the previous owned profile while holding the
checkout-local operation lock.
10. Record the active profile's artifact digests as a distinct live store
reference, so package installs and tool profiles do not overwrite each
other's GC ownership.

On Unix, profile commands are symlinks to immutable store files. On Windows,
they are exact copied executable/script files. A logical command without an
extension inherits a locked `.exe`, `.com`, `.cmd`, or `.bat` extension on
Windows.

## Profile state

The deterministic `profile.json` records only:

- schema;
- normalized environment-lock SHA-256;
- target;
- logical tool name, original requirement, exact version, and backend;
- artifact SHA-256;
- relative install root; and
- exposed command names and relative locked source paths.

It contains no source URL, credential, environment value, timestamp, hostname,
process identifier, user-home path, or absolute store path. Replaying the same
lock and target against an intact profile reports `unchanged` and does not
rewrite profile state.

## Fail-closed boundaries

The staged installer rejects:

- online mode;
- missing cached bytes;
- wrong artifact size or SHA-256;
- unsupported archive formats;
- missing target variants or multiple variants for one logical tool/target;
- executable and alias collisions;
- unsafe project, lock, profile, target, install-root, or executable paths;
- symlinked profile parents or authenticated artifact paths;
- non-regular or non-executable locked commands;
- lock/plan digest drift; and
- profile replacement or reference-recording failures.

A failed profile commit restores the prior active profile. Download, version
selection, signatures, multiple active versions, backend plugins, update and
outdated operations, lazy shims, shell activation, and `zed dev` integration are
separate certified slices under DEN-1437 and DEN-1442.
Loading
Loading