Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
22af78c
feat: reconcile deterministic mise export with current main
ORESoftware Aug 5, 2026
e9b6646
chore: sync deterministic Nix fix into mise export
ORESoftware Aug 5, 2026
ba8f84d
chore: sync frozen-rendezvous canary into deterministic mise export
ORESoftware Aug 5, 2026
be3c21e
chore: sync zed-lock v0.1.1 integration into deterministic mise export
ORESoftware Aug 5, 2026
c345f58
chore: sync current main into deterministic mise export
ORESoftware Aug 8, 2026
ad06282
chore(DEN-1462): stage semantic current-main reconstruction
ORESoftware Aug 8, 2026
5556999
fix(DEN-1462): compose export CLI with current task commands
ORESoftware Aug 8, 2026
622e925
Merge current main into deterministic mise export
github-actions[bot] Aug 8, 2026
9fcd902
chore(DEN-1462): remove completed current-main reconstruction workflow
ORESoftware Aug 8, 2026
ecbeee9
chore(DEN-1462): stage ownership-state integrity hardening
ORESoftware Aug 8, 2026
1ff0ec3
fix(DEN-1462): generate valid ownership-state regression
ORESoftware Aug 8, 2026
67a4b9e
fix(DEN-1462): preserve exporter ownership-state integrity
github-actions[bot] Aug 8, 2026
e0bb22a
fix(DEN-1462): harden ownership and reserved source state
ORESoftware Aug 8, 2026
4063a65
fix(DEN-1462): isolate reserved export-state source hardening
ORESoftware Aug 8, 2026
f427fab
ci(DEN-1462): run focused reserved-source patch on Windows
ORESoftware Aug 8, 2026
b3cf177
test(DEN-1462): reject reserved export state as a plan source
ORESoftware Aug 8, 2026
688a9a2
fix(DEN-1462): use structural reserved-source patching
ORESoftware Aug 8, 2026
817d0e4
fix(DEN-1462): guard reserved exporter state at the public API
ORESoftware Aug 8, 2026
9dd3a37
test(DEN-1462): certify reserved exporter state at the public API
ORESoftware Aug 8, 2026
c21f26c
chore(DEN-1462): remove completed exporter hardening workflow
ORESoftware Aug 8, 2026
50edd5c
Merge current main publish-ignore facade into deterministic mise export
ORESoftware Aug 8, 2026
e08910e
style(DEN-1462): apply rustfmt to reserved-state CLI regression
ORESoftware Aug 8, 2026
21394d0
chore(DEN-1462): stage current-main semantic merge
ORESoftware Aug 8, 2026
7c5445b
chore(DEN-1462): remove completed semantic-sync workflow
ORESoftware Aug 8, 2026
3b6ae4b
chore(DEN-1462): stage checkout-lock serialization hardening
ORESoftware Aug 8, 2026
81e4d89
chore(DEN-1462): remove failed lock-hardening carrier
ORESoftware Aug 8, 2026
851866e
fix(DEN-1462): serialize public exporter writes
ORESoftware Aug 8, 2026
7d3c8ad
test(DEN-1462): certify exporter and recovery lock serialization
ORESoftware Aug 8, 2026
67f1304
chore(DEN-1462): stage startup recovery serialization
ORESoftware Aug 8, 2026
6a8ecfb
fix(DEN-1462): align recovery-lock patch with live main
ORESoftware Aug 8, 2026
dd676c8
fix(DEN-1462): structurally patch startup recovery locking
ORESoftware Aug 8, 2026
69b4f78
chore(DEN-1462): run final startup recovery serialization
ORESoftware Aug 8, 2026
9a4c3cc
ci(DEN-1462): remove completed recovery-lock materializer
ORESoftware Aug 8, 2026
c0040b4
ci(DEN-1462): remove completed recovery-lock v2 materializer
ORESoftware Aug 8, 2026
8607af1
fix(DEN-1462): serialize startup recovery with checkout ownership
ORESoftware Aug 8, 2026
b96d95b
ci(DEN-1462): trigger checkout-owned recovery repair
ORESoftware Aug 8, 2026
04eefaf
ci(DEN-1462): remove suppressed recovery-lock carrier
ORESoftware Aug 8, 2026
5ad549c
fix(DEN-1462): serialize startup recovery by checkout
ORESoftware Aug 8, 2026
aaf4268
Merge current main into deterministic mise exporter
ORESoftware Aug 9, 2026
93f9c9d
chore(DEN-1462): stage current-main exporter repair
ORESoftware Aug 9, 2026
f39bcd7
Merge current main into deterministic mise exporter
github-actions[bot] Aug 9, 2026
e836742
chore(DEN-1462): trigger exact-head checks
ORESoftware Aug 9, 2026
3434716
Merge current main and consume Windows lock fix
ORESoftware Aug 9, 2026
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
32 changes: 31 additions & 1 deletion .cli-flags.toml
Original file line number Diff line number Diff line change
Expand Up @@ -293,14 +293,44 @@ aliases = ["un"]
help = "Remove materialized dependencies while retaining the lockfile."

[commands.env]
help = "Import and verify project-local developer environments."
help = "Import, verify, and export project-local developer environments."

[commands.env.commands.import]
help = "Import manager-native state as a normalized EnvironmentPlan."

[commands.env.commands.verify]
help = "Verify config/lock coverage and normalized environment identity."

[commands.env.commands.export]
help = "Project a schema-v2 EnvironmentPlan into deterministic manager configuration."

[commands.env.commands.export.flags.env_plan]
env = "ZED_PKG_ENV_PLAN"
aliases = ["plan"]
type = "string"
help = "Project-local schema-v2 environment plan path."

[commands.env.commands.export.flags.env_output]
env = "ZED_PKG_ENV_OUTPUT"
aliases = ["output"]
type = "string"
default = ".mise.toml"
help = "Project-local manager export path."

[commands.env.commands.export.flags.env_check]
env = "ZED_PKG_ENV_CHECK"
aliases = ["check"]
type = "bool"
default = "false"
help = "Verify the deterministic manager projection without writing."

[commands.env.commands.export.flags.env_write]
env = "ZED_PKG_ENV_WRITE"
aliases = ["write"]
type = "bool"
default = "false"
help = "Transactionally write a Zed-owned manager projection."

[commands.task]
help = "List, inspect, graph, or execute native schema-v2 project tasks."

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ registry hosts both on S3/Cloudflare R2.
| `zed uninstall [<org>/<name> ...]` (`zed un`) | Transactionally remove all or selected materialized packages while retaining the manifest and lockfile for a frozen reinstall |
| `zed env import mise [--config PATH] [--lock PATH] [--frozen] [--json]` | Import the supported project-local mise tool/lock subset as the shared normalized `EnvironmentPlan`; never loads parent/global config or executes hooks |
| `zed env verify mise [--config PATH] [--lock PATH] --frozen [--json]` | Fail closed on missing lock coverage, drift, malformed checksums, unsupported semantics, or non-portable frozen state and report the stable plan digest |
| `zed env export mise --plan PATH [--output .mise.toml] [--check|--write] [--json]` | Deterministically project a schema-v2 environment plan into conflict-safe project-local mise TOML without invoking mise or executing project code |
| `zed env import asdf [--config .tool-versions] [--lock .zed/asdf.lock.toml] [--frozen] [--json]` | Import project-local asdf selections and optional immutable plugin/artifact provenance without invoking asdf or plugin code |
| `zed env verify asdf [--config .tool-versions] [--lock .zed/asdf.lock.toml] --frozen [--json]` | Verify exact asdf tool, plugin revision, artifact SHA-256, platform, and normalized plan identity without reading parent/global configuration |
| `zed task list\|info\|graph\|run ...` | Use the shared schema-v2 runtime to discover, inspect, graph, dry-run, execute, confirm, parallelize, and content-cache project tasks; `zed-task` remains a compatibility binary |
Expand Down
122 changes: 122 additions & 0 deletions docs/mise-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Deterministic mise export

`zed env export mise` projects a schema-v2 Zed environment plan into a
project-local mise configuration without invoking mise, loading global state,
or executing project code.

```sh
# Print deterministic TOML without writing files.
zed env export mise --plan zed-env.toml

# Verify that the checked-in manager view is current.
zed env export mise --plan zed-env.toml --output .mise.toml --check

# Create or update a Zed-owned manager view transactionally.
zed env export mise --plan zed-env.toml --output .mise.toml --write
```

`--check` and `--write` are mutually exclusive. Without either flag, the
command prints the deterministic TOML document to standard output.

## Supported projection

The first write-capable slice exports only state with an exact, tested mise
representation:

- ordered single- and multi-version tools;
- backend-qualified tool keys and core-tool aliases;
- `version`, portable project-relative `path`, `prefix`, and `ref` selectors;
- typed backend options and tool OS constraints;
- scalar environment values;
- typed project and task variables;
- task descriptions, aliases, ordered command arrays, dependencies,
post-dependencies, readiness dependencies, environment, variables,
task-local tools, directories, sources, outputs, one shell string, usage,
confirmation, cache, timeout, and output flags; and
- lockfile platform settings.

The exporter preserves multi-version and command order. Set-like platform
lists are sorted and deduplicated by the normalized environment contract.

## Fail-closed boundaries

The command rejects fields rather than silently omitting them when no certified
mapping exists. Current explicit boundaries include:

- activation hooks other than `activation = "none"`;
- environment-plan system packages;
- imported manager provenance and lock identity;
- extension maps;
- resolved versions, immutable sources, and artifact checksums that belong in
`mise.lock` rather than `mise.toml`;
- complex top-level or task environment tables/arrays, which mise may interpret
as directives rather than literal values;
- grouped and structured task invocations until the current mise run-entry wire
form is certified; and
- shell program-plus-argument vectors, because the current mise task field is
one shell string.

Diagnostics include the exact environment-plan path that cannot be represented.
Complete current `mise.lock` export is tracked independently so config export
cannot accidentally erase provenance.

## Secrets

The plan contract does not yet expose a portable secret-reference type. Export
therefore rejects literal environment, variable, and tool-option names that
look credential-bearing at any nesting depth, including password, secret, token,
private/access key, API key, credential, and authorization names. The generated file, sidecar,
logs, and diagnostics never print rejected values.

## Ownership and conflicts

Write mode records deterministic ownership in:

```text
.zed/mise-export-state.json
```

The sidecar contains only schema version, project-relative plan/output paths,
and SHA-256 identities. It contains no timestamps, random identifiers,
absolute paths, credentials, or environment values.

The exporter follows these rules:

1. A missing output can be created.
2. An existing file whose bytes already equal the deterministic projection can
be adopted safely.
3. A differing existing file with no matching Zed ownership record is treated
as hand-authored and is never overwritten.
4. A Zed-owned file whose current digest differs from the recorded digest is a
user edit and is never overwritten.
5. A Zed-owned unchanged file may be replaced only by the same project-relative
plan recorded in the sidecar.

Output and state changes share `ProjectTransaction`, including crash recovery
and rollback. Plan, output, state, and staging identities are compared with
portable ASCII case-folding so a Linux-generated ownership file cannot become
ambiguous on Windows or macOS. Project, output, and state paths reject absolute/home/drive/UNC,
parent traversal, and symlink escape.

## Determinism

The plan identity is:

```text
SHA256("zed-pkg:mise-export-plan:v1\0" || canonical-environment-plan-json)
```

The output identity is SHA-256 over the exact generated TOML bytes. Print,
check, and write modes all render the normalized semantic plan, so set-like
presentation order cannot diverge under one plan digest. Repeated generation of the
same normalized plan is byte-identical across supported platforms.

## Next gates

- Bind the complete current `mise.lock` contract to export and verification.
- Translate portable manager identities into native `EnvironmentLock`.
- Certify structured task run entries and sandbox capabilities.
- Add conflict-aware import → export → import semantic identity tests on Linux,
macOS, and Windows.

Tracking: DEN-1462, DEN-1461, DEN-1481.
64 changes: 62 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ impl From<CompletionShell> for clap_complete::Shell {

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum EnvironmentManagerArg {
/// Import or verify project-local mise configuration.
/// Import, verify, or export project-local mise configuration.
Mise,
/// Import or verify project-local asdf configuration and Zed-owned provenance.
Asdf,
Expand Down Expand Up @@ -443,6 +443,26 @@ pub enum EnvCmd {
#[arg(long, env = "ZED_PKG_ENV_JSON")]
json: bool,
},
/// Export a schema-v2 EnvironmentPlan as deterministic mise TOML.
Export {
#[arg(value_enum)]
manager: EnvironmentManagerArg,
/// Project-local schema-v2 EnvironmentPlan (.toml or .json).
#[arg(long, env = "ZED_PKG_ENV_PLAN")]
plan: PathBuf,
/// Project-local mise output path.
#[arg(long, env = "ZED_PKG_ENV_OUTPUT", default_value = ".mise.toml")]
output: PathBuf,
/// Verify that the output already equals the deterministic projection.
#[arg(long, env = "ZED_PKG_ENV_CHECK")]
check: bool,
/// Transactionally create/update a Zed-owned manager view.
#[arg(long, env = "ZED_PKG_ENV_WRITE")]
write: bool,
/// Emit a machine-readable export result.
#[arg(long, env = "ZED_PKG_ENV_JSON")]
json: bool,
},
/// Verify manager config/lock coverage and the normalized plan digest.
Verify {
#[arg(value_enum)]
Expand Down Expand Up @@ -597,7 +617,7 @@ mod tests {

use clap::{CommandFactory, Parser};

use super::{AuthCmd, Cli, Cmd};
use super::{AuthCmd, Cli, Cmd, EnvCmd};

#[test]
fn flat_and_nested_auth_spellings_dispatch_identically() {
Expand Down Expand Up @@ -742,6 +762,46 @@ mod tests {
}
}

#[test]
fn environment_export_is_typed_and_rejects_ambiguous_write_modes() {
let cli = Cli::try_parse_from([
"zed",
"env",
"export",
"mise",
"--plan",
"zed-env.toml",
"--output",
".mise.toml",
"--check",
"--json",
])
.unwrap();
assert!(matches!(cli.cmd, Cmd::Env { .. }));

assert!(matches!(
Cli::try_parse_from([
"zed",
"env",
"export",
"mise",
"--plan",
"zed-env.toml",
"--check",
"--write",
])
.unwrap()
.cmd,
Cmd::Env {
cmd: EnvCmd::Export {
check: true,
write: true,
..
}
}
));
}

#[test]
fn task_commands_are_typed_and_reject_zero_concurrency() {
for args in [
Expand Down
79 changes: 79 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,85 @@ pub mod interactive;
pub mod lock_waiter;
pub mod managed_install;
pub mod manifestless;
#[path = "mise_export.rs"]
mod mise_export_impl;
pub mod mise_export {
use std::path::{Component, Path};

use anyhow::{Context, Result, ensure};

pub use super::mise_export_impl::{
MiseExportAction, MiseExportMode, MiseExportReport, print_export,
};

const EXPORT_STATE_PATH: &str = ".zed/mise-export-state.json";

/// Render, verify, or write one project-local mise projection.
///
/// The ownership sidecar is never accepted as an input plan, including
/// through portable case aliases or an in-project symlink. Mutating writes
/// are serialized with every other checkout operation so transaction
/// recovery cannot observe another writer's active staging directory.
pub fn export_mise(
cwd: &Path,
plan_arg: &Path,
output_arg: &Path,
mode: MiseExportMode,
) -> Result<MiseExportReport> {
reject_reserved_state_plan(cwd, plan_arg)?;
if mode == MiseExportMode::Write {
return super::project_lock::with_lock(cwd, "export mise environment", || {
super::mise_export_impl::export_mise(cwd, plan_arg, output_arg, mode)
});
}
super::mise_export_impl::export_mise(cwd, plan_arg, output_arg, mode)
}

fn reject_reserved_state_plan(cwd: &Path, plan_arg: &Path) -> Result<()> {
let root = cwd
.canonicalize()
.with_context(|| format!("failed to resolve project root {}", cwd.display()))?;

let lexical_relative = if plan_arg.is_absolute() {
plan_arg.strip_prefix(&root).ok()
} else {
Some(plan_arg)
};
if let Some(relative) = lexical_relative {
ensure_not_reserved(relative)?;
}

let candidate = if plan_arg.is_absolute() {
plan_arg.to_path_buf()
} else {
root.join(plan_arg)
};
if let Ok(canonical) = candidate.canonicalize()
&& let Ok(relative) = canonical.strip_prefix(&root)
{
ensure_not_reserved(relative)?;
}
Ok(())
}

fn ensure_not_reserved(relative: &Path) -> Result<()> {
let mut parts = Vec::new();
for component in relative.components() {
match component {
Component::CurDir => {}
Component::Normal(part) => parts.push(part.to_string_lossy()),
Component::ParentDir => parts.push("..".into()),
Component::RootDir | Component::Prefix(_) => return Ok(()),
}
}
let folded = parts.join("/").to_ascii_lowercase();
ensure!(
folded != EXPORT_STATE_PATH,
"environment plan cannot target reserved export state `{EXPORT_STATE_PATH}`"
);
Ok(())
}
}
pub mod mise_lock;
pub mod nix_bundle_write;
pub mod nix_export_bundle;
Expand Down
26 changes: 26 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use zed_cli::fetch;
use zed_cli::git_submodules as submodules;
use zed_cli::global;
use zed_cli::managed_install;
use zed_cli::mise_export::{self, MiseExportMode};
use zed_cli::nix_bundle_write;
use zed_cli::nix_export_plan;
use zed_cli::ops;
Expand Down Expand Up @@ -267,6 +268,31 @@ fn run(cli: Cli) -> anyhow::Result<()> {
asdf_environment::print_import(&imported, json)
}
},
EnvCmd::Export {
manager: EnvironmentManagerArg::Mise,
plan,
output,
check,
write,
json,
} => {
if check && write {
anyhow::bail!("the arguments '--check' and '--write' cannot be used together");
}
let mode = if check {
MiseExportMode::Check
} else if write {
MiseExportMode::Write
} else {
MiseExportMode::Print
};
let exported = mise_export::export_mise(&cwd, &plan, &output, mode)?;
mise_export::print_export(&exported, json)
}
EnvCmd::Export {
manager: EnvironmentManagerArg::Asdf,
..
} => anyhow::bail!("asdf export is not implemented; use `zed env export mise`"),
EnvCmd::Verify {
manager,
config,
Expand Down
Loading
Loading