diff --git a/.github/workflows/mise-export-boundaries.yml b/.github/workflows/mise-export-boundaries.yml new file mode 100644 index 00000000..80cd90ac --- /dev/null +++ b/.github/workflows/mise-export-boundaries.yml @@ -0,0 +1,153 @@ +name: mise export boundaries + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + inputs: + zed_cli_ref: + description: Immutable zed-cli commit to certify + required: false + default: 5ad549c544276a713dd5de353f6fbe8ddd4b53d7 + schedule: + - cron: "17 5 * * 3" + +permissions: + contents: read + +concurrency: + group: mise-export-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: "1" + ZED_CLI_REF: ${{ github.event.inputs.zed_cli_ref || '5ad549c544276a713dd5de353f6fbe8ddd4b53d7' }} + +jobs: + certify: + name: export / ${{ matrix.os }} + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + os: + - ubuntu-24.04 + - macos-15 + - windows-2025 + steps: + - name: Require an immutable CLI pin + run: | + set -euo pipefail + [[ "$ZED_CLI_REF" =~ ^[0-9a-f]{40}$ ]] + + - name: Check out certification harness + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: harness + fetch-depth: 1 + persist-credentials: false + show-progress: false + + - name: Check out immutable zed CLI + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: zed-pkg/zed-cli + ref: ${{ env.ZED_CLI_REF }} + path: zed-cli + fetch-depth: 1 + persist-credentials: false + show-progress: false + + - name: Install validation toolchain + run: | + set -euo pipefail + rustup toolchain install stable --profile minimal --component rustfmt,clippy + rustup default stable + rustc --version --verbose + cargo --version --verbose + python --version + + - name: Run focused exporter tests and strict lint + run: | + set -euo pipefail + python -m py_compile \ + harness/scripts/mise_export_boundaries.py \ + harness/scripts/mise_export_ownership_state.py + cargo fmt --all --check --manifest-path zed-cli/Cargo.toml + cargo test --locked --manifest-path zed-cli/Cargo.toml mise_export + cargo test --locked --manifest-path zed-cli/Cargo.toml --test mise_export_api_boundaries + cargo test --locked --manifest-path zed-cli/Cargo.toml --test mise_export_cli + cargo test --locked --manifest-path zed-cli/Cargo.toml --test mise_export_reserved_paths_cli + cargo clippy --locked --manifest-path zed-cli/Cargo.toml --all-targets -- -D warnings + + - name: Build immutable release CLI + run: | + set -euo pipefail + cargo build --locked --release --manifest-path zed-cli/Cargo.toml --bin zed + if [[ "$RUNNER_OS" == "Windows" ]]; then + test -f zed-cli/target/release/zed.exe + else + test -x zed-cli/target/release/zed + fi + + - name: Certify ownership, conflict, secret, and path boundaries + run: | + set -euo pipefail + ZED_BIN="$PWD/zed-cli/target/release/zed" + if [[ "$RUNNER_OS" == "Windows" ]]; then + ZED_BIN="${ZED_BIN}.exe" + fi + python harness/scripts/mise_export_boundaries.py \ + --zed "$ZED_BIN" \ + --work-root "$RUNNER_TEMP/zed-mise-export" + python harness/scripts/mise_export_ownership_state.py \ + --zed "$ZED_BIN" \ + --work-root "$RUNNER_TEMP/zed-mise-export-ownership" + + - name: Collect failure diagnostics + if: failure() + run: | + set +e + diagnostics="$RUNNER_TEMP/zed-mise-export-diagnostics" + mkdir -p "$diagnostics" + cp -R "$RUNNER_TEMP/zed-mise-export" "$diagnostics/boundaries-work-root" 2>/dev/null || true + cp -R "$RUNNER_TEMP/zed-mise-export-ownership" "$diagnostics/ownership-work-root" 2>/dev/null || true + git -C zed-cli status --porcelain=v1 --untracked-files=all \ + > "$diagnostics/zed-cli-status.txt" 2>&1 + git -C harness status --porcelain=v1 --untracked-files=all \ + > "$diagnostics/harness-status.txt" 2>&1 + rustc --version --verbose > "$diagnostics/rust-version.txt" 2>&1 + cargo --version --verbose > "$diagnostics/cargo-version.txt" 2>&1 + python --version > "$diagnostics/python-version.txt" 2>&1 + + - name: Upload failure diagnostics + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: mise-export-${{ matrix.os }}-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/zed-mise-export-diagnostics + if-no-files-found: warn + retention-days: 14 + include-hidden-files: true + + gate: + name: All mise export platforms passed + if: always() + needs: certify + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Enforce aggregate result + env: + MATRIX_RESULT: ${{ needs.certify.result }} + run: | + set -euo pipefail + printf 'mise-export=%s\n' "$MATRIX_RESULT" + test "$MATRIX_RESULT" = success diff --git a/docs/mise-export-boundaries.md b/docs/mise-export-boundaries.md new file mode 100644 index 00000000..67ac5bda --- /dev/null +++ b/docs/mise-export-boundaries.md @@ -0,0 +1,79 @@ +# mise export boundary certification + +This suite independently certifies deterministic, conflict-safe projection from a Zed `EnvironmentPlanV2` into project-local mise TOML. + +`zed-pkg` is an independent multi-language package manager and is unrelated to the Zed editor. Here, `zed` refers only to the `zed-pkg` CLI. + +## Immutable implementation candidate + +Canonical product PR: `zed-pkg/zed-cli#131`. + +Pinned product head: + +```text +7c5445b2cd15a503c8004da98cac31451ea2f1d4 +``` + +The product branch was semantically merged with current `main` head `5d1d13119be5784f545a7d812cdfaae3ca120693`. Current submodule CLI and project-lock behavior were adopted verbatim; `src/main.rs` was composed by extracting the already-tested exporter import and dispatch arms into the current main-line recovery and locking flow. The exporter remains a ten-file source, test, contract, and documentation delta with no materializer or synchronization workflow. + +The public Rust API enforces the reserved exporter-state source boundary before delegating to the implementation. This protects the CLI, external Rust callers, portable case aliases, and in-project symlink aliases through one shared guard. + +The certification workflow pins the full immutable product head above and never follows a mutable branch during a run. + +## Certified behavior + +The harnesses run the real CLI with an empty `PATH` and prove: + +- print mode is deterministic and read-only; +- `--write` creates generated TOML plus typed ownership state; +- `--check` verifies exact generated bytes; +- repeated writes are idempotent; +- hand-edited owned files are not overwritten; +- differing unowned files are not adopted or overwritten; +- an existing generated output cannot silently transfer ownership to a different plan, even when both plans currently render byte-identical TOML; +- a stale or corrupted recorded output digest is rejected rather than silently repaired; +- ownership-state failures leave both the generated output and state file unchanged; +- the public Rust API and real CLI reject the reserved exporter-state file as the plan source; +- the public API rejects a symlink to the reserved exporter-state file on Unix; +- safe missing output parent directories can be created transactionally; +- output cannot alias its input plan, including case-only aliases relevant to Windows; +- output and input cannot target the reserved export-state path; +- output and input cannot target transaction staging; +- nested secret-like keys in vars and tool options fail with exact field paths; +- symlinked export-state parents cannot redirect state outside the project; and +- no ambient mise executable, global mise configuration, or credentials are required. + +Both successful and failed commands are checked for unintended project mutation. + +## Three-platform gate + +The permanent workflow runs on: + +- Ubuntu 24.04; +- macOS 15; and +- Windows Server 2025. + +Each platform requires formatting, focused exporter unit tests, the public-API boundary target, both real CLI test targets, all-target Clippy with warnings denied, a locked release build, and both black-box harnesses. An aggregate gate requires every platform to succeed. All ordinary repository-wide E2E workflows remain merge gates on the same certification head. + +## Deliberate boundary + +This certifies deterministic manager-configuration export and ownership safety. It does not claim: + +- complete current `mise.lock` export; +- native `EnvironmentLock` translation; +- a general semantic three-way merge for arbitrary hand-authored mise files; +- execution of mise tasks, hooks, templates, plugins, or environment expressions; or +- installation of runtimes. + +The complete current manager-lock identity contract is separately merged and certified through DEN-1461. More sophisticated manager merge planning and lock-aware round trips remain separate follow-up work under DEN-1462. + +## Promotion gate + +Promotion requires: + +1. the workflow pin matching final immutable head `7c5445b2cd15a503c8004da98cac31451ea2f1d4` of `zed-pkg/zed-cli#131`; +2. Ubuntu 24.04, macOS 15, and Windows Server 2025 success; +3. formatting, focused unit tests, the public API target, real CLI tests, a locked release build, and all-target Clippy with warnings denied; +4. both black-box harnesses passing on all platforms, with symlink assertions skipped only where the host cannot create symlinks; +5. ordinary lifecycle, recursive-install, browser, install-boundary, and runtime-mise workflows remaining green; and +6. exact implementation, certification, and merge commits recorded in DEN-1462 and the canonical mise interoperability policy. diff --git a/scripts/mise_export_boundaries.py b/scripts/mise_export_boundaries.py new file mode 100644 index 00000000..5245062a --- /dev/null +++ b/scripts/mise_export_boundaries.py @@ -0,0 +1,371 @@ +#!/usr/bin/env python3 +"""Black-box certification for deterministic, conflict-safe mise export.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import shutil +import subprocess +import sys +from pathlib import Path +from typing import Iterable + + +class HarnessFailure(RuntimeError): + pass + + +def ensure(condition: bool, message: str) -> None: + if not condition: + raise HarnessFailure(message) + + +def write_json(path: Path, value: object) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + json.dumps(value, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + newline="\n", + ) + + +def sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def snapshot(root: Path) -> dict[str, str]: + return { + path.relative_to(root).as_posix(): sha256(path) + for path in sorted(root.rglob("*")) + if path.is_file() + } + + +def snapshot_without_operation_rendezvous(root: Path) -> dict[str, str]: + """Snapshot semantic project state while validating the lock separately.""" + relative_lock = Path(".zed/operation.lock") + return { + relative: digest + for relative, digest in snapshot(root).items() + if Path(relative) != relative_lock + } + + +def assert_regular_operation_rendezvous(root: Path) -> None: + operation_lock = root / ".zed/operation.lock" + ensure(operation_lock.is_file(), "mutation did not retain the operation rendezvous") + ensure(not operation_lock.is_symlink(), "operation rendezvous is a symlink") + unexpected = [ + path.relative_to(root).as_posix() + for path in sorted((root / ".zed").rglob("*")) + if path != operation_lock + ] + ensure( + not unexpected, + f"failed mutation left native adapter state: {unexpected!r}", + ) + + +def base_plan() -> dict[str, object]: + return { + "schema": 2, + "tools": { + "node": [ + {"requirement": "22.4.0"}, + {"requirement": "20.15.1"}, + ] + }, + "env": {"APP_ENV": "test", "RETRIES": 3}, + "vars": {"release": {"channel": "stable"}}, + "tasks": { + "prepare": {"run": ["echo prepare"]}, + "setup": { + "description": "Restore dependencies", + "aliases": ["bootstrap"], + "depends": ["prepare"], + "run": ["zed install --frozen", "cargo check"], + }, + }, + "platforms": ["linux-x64", "macos-arm64"], + "activation": "none", + } + + +def run( + zed: Path, + project: Path, + home: Path, + args: Iterable[str], + *, + success: bool, + contains: str | None = None, +) -> subprocess.CompletedProcess[str]: + empty_path = home / "empty-path" + empty_path.mkdir(parents=True, exist_ok=True) + env = os.environ.copy() + env.update( + { + "HOME": str(home), + "USERPROFILE": str(home), + "XDG_CONFIG_HOME": str(home / ".config"), + "ZED_PKG_HOME": str(home / ".zed-pkg"), + "PATH": str(empty_path), + } + ) + for key in ( + "ZED_PKG_ENV_PLAN", + "ZED_PKG_ENV_OUTPUT", + "ZED_PKG_ENV_JSON", + "ZED_PKG_ENV_CHECK", + "ZED_PKG_ENV_WRITE", + "MISE_CONFIG_FILE", + "MISE_TOML", + ): + env.pop(key, None) + + completed = subprocess.run( + [str(zed), *args], + cwd=project, + env=env, + text=True, + capture_output=True, + check=False, + ) + if (completed.returncode == 0) != success: + raise HarnessFailure( + f"unexpected status {completed.returncode} for {list(args)!r}\n" + f"stdout:\n{completed.stdout}\nstderr:\n{completed.stderr}" + ) + if contains is not None: + combined = completed.stdout + "\n" + completed.stderr + ensure(contains in combined, f"missing {contains!r} in:\n{combined}") + return completed + + +def export_args(*extra: str, output: str = ".mise.toml") -> tuple[str, ...]: + return ( + "env", + "export", + "mise", + "--plan", + "zed-env.json", + "--output", + output, + *extra, + ) + + +def certify(zed: Path, root: Path) -> None: + ensure(zed.is_file(), f"missing zed executable: {zed}") + ensure(not root.exists(), f"work root must be fresh: {root}") + root.mkdir(parents=True) + home = root / "home" + home.mkdir() + assertions: list[str] = [] + + project = root / "owned" + project.mkdir() + write_json(project / "zed-env.json", base_plan()) + initial = snapshot(project) + + printed = run(zed, project, home, export_args(), success=True) + ensure("[tools]" in printed.stdout, "print mode did not emit mise TOML") + ensure(snapshot(project) == initial, "print mode mutated project") + assertions.append("print-read-only") + + written = run( + zed, + project, + home, + export_args("--write", "--json"), + success=True, + ) + written_json = json.loads(written.stdout) + ensure(written_json.get("action") == "written", f"unexpected write: {written_json}") + ensure((project / ".mise.toml").is_file(), "write mode omitted output") + ensure( + (project / ".zed/mise-export-state.json").is_file(), + "write mode omitted ownership state", + ) + assertions.append("write-owned") + + checked = run( + zed, + project, + home, + export_args("--check", "--json"), + success=True, + ) + ensure(json.loads(checked.stdout).get("action") == "verified", "check not verified") + assertions.append("check-verified") + + unchanged = run( + zed, + project, + home, + export_args("--write", "--json"), + success=True, + ) + ensure( + json.loads(unchanged.stdout).get("action") == "unchanged", + "idempotent write was not unchanged", + ) + assertions.append("write-idempotent") + + original_output = (project / ".mise.toml").read_bytes() + (project / ".mise.toml").write_text("# hand edit\n", encoding="utf-8") + run(zed, project, home, export_args("--write"), success=False, contains="edited") + ensure( + (project / ".mise.toml").read_text(encoding="utf-8") == "# hand edit\n", + "edited output overwritten", + ) + (project / ".mise.toml").write_bytes(original_output) + assertions.append("edited-output-protected") + + unowned = root / "unowned" + unowned.mkdir() + write_json(unowned / "zed-env.json", base_plan()) + (unowned / ".mise.toml").write_text('[tools]\nnode = "18"\n', encoding="utf-8") + before = snapshot_without_operation_rendezvous(unowned) + run(zed, unowned, home, export_args("--write"), success=False, contains="hand-authored") + assert_regular_operation_rendezvous(unowned) + ensure( + snapshot_without_operation_rendezvous(unowned) == before, + "unowned output failure mutated semantic project state", + ) + assertions.append("unowned-output-protected") + assertions.append("operation-rendezvous-nonsemantic") + + nested = root / "nested-output" + nested.mkdir() + write_json(nested / "zed-env.json", base_plan()) + run( + zed, + nested, + home, + export_args("--write", output="generated/config/.mise.toml"), + success=True, + ) + ensure((nested / "generated/config/.mise.toml").is_file(), "safe missing parents not created") + assertions.append("safe-parent-creation") + + boundaries = root / "boundaries" + boundaries.mkdir() + write_json(boundaries / "zed-env.json", base_plan()) + original_plan = (boundaries / "zed-env.json").read_bytes() + for output, diagnostic in ( + ("zed-env.json", "cannot overwrite"), + ("ZED-ENV.JSON", "cannot overwrite"), + (".zed/mise-export-state.json", "reserved export state"), + (".ZED/MISE-EXPORT-STATE.JSON", "reserved export state"), + (".zpkg-staging/mise.toml", "reserved transaction staging"), + (".ZPKG-STAGING/mise.toml", "reserved transaction staging"), + ): + run( + zed, + boundaries, + home, + export_args("--write", output=output), + success=False, + contains=diagnostic, + ) + ensure( + (boundaries / "zed-env.json").read_bytes() == original_plan, + f"boundary {output} mutated plan", + ) + assertions.append("portable-reserved-paths") + + nested_secret = root / "nested-secret" + nested_secret.mkdir() + plan = base_plan() + plan["vars"] = {"release": {"api_token": "plaintext"}} + write_json(nested_secret / "zed-env.json", plan) + run( + zed, + nested_secret, + home, + export_args(), + success=False, + contains="vars.release.api_token", + ) + ensure(not (nested_secret / ".mise.toml").exists(), "secret rejection wrote output") + assertions.append("nested-secret-rejected") + + tool_secret = root / "tool-secret" + tool_secret.mkdir() + plan = base_plan() + plan["tools"] = { + "node": [ + { + "requirement": "22.4.0", + "options": {"config": {"password": "plaintext"}}, + } + ] + } + write_json(tool_secret / "zed-env.json", plan) + run( + zed, + tool_secret, + home, + export_args(), + success=False, + contains="tools.node.versions[0].options.config.password", + ) + assertions.append("nested-tool-secret-rejected") + + symlink_case = root / "symlink-state" + symlink_case.mkdir() + write_json(symlink_case / "zed-env.json", base_plan()) + outside = root / "outside-state" + outside.mkdir() + try: + os.symlink(outside, symlink_case / ".zed", target_is_directory=True) + except (OSError, NotImplementedError): + assertions.append("symlink-state-skipped") + else: + run( + zed, + symlink_case, + home, + export_args("--write"), + success=False, + contains="mise export state crosses a symlink", + ) + ensure(not (symlink_case / ".mise.toml").exists(), "symlink failure wrote output") + ensure(not (outside / "mise-export-state.json").exists(), "state escaped project") + assertions.append("symlink-state-rejected") + + print( + json.dumps( + { + "schema": "zed-pkg.mise-export-boundaries.v1", + "certified": True, + "assertions": assertions, + "ambient_mise_required": False, + }, + sort_keys=True, + ) + ) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--zed", type=Path, required=True) + parser.add_argument("--work-root", type=Path, required=True) + args = parser.parse_args() + try: + certify(args.zed.resolve(), args.work_root.resolve()) + except (HarnessFailure, json.JSONDecodeError) as error: + print(f"mise export boundary certification failed: {error}", file=sys.stderr) + return 1 + finally: + if args.work_root.exists() and os.environ.get("KEEP_MISE_EXPORT_ROOT") != "1": + shutil.rmtree(args.work_root, ignore_errors=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/mise_export_ownership_state.py b/scripts/mise_export_ownership_state.py new file mode 100644 index 00000000..94c147aa --- /dev/null +++ b/scripts/mise_export_ownership_state.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +"""Black-box regressions for Zed-owned mise export state integrity.""" + +from __future__ import annotations + +import argparse +import json +import os +import shutil +import subprocess +import sys +from pathlib import Path +from typing import Iterable + + +class HarnessFailure(RuntimeError): + pass + + +def ensure(condition: bool, message: str) -> None: + if not condition: + raise HarnessFailure(message) + + +def write_plan(path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + json.dumps( + { + "schema": 2, + "tools": {"node": [{"requirement": "22.4.0"}]}, + "env": {"APP_ENV": "test"}, + "vars": {}, + "tasks": {}, + "platforms": ["linux-x64", "macos-arm64", "windows-x64"], + "activation": "none", + }, + indent=2, + sort_keys=True, + ) + + "\n", + encoding="utf-8", + newline="\n", + ) + + +def run( + zed: Path, + project: Path, + home: Path, + args: Iterable[str], + *, + success: bool, + contains: str | None = None, +) -> subprocess.CompletedProcess[str]: + empty_path = home / "empty-path" + empty_path.mkdir(parents=True, exist_ok=True) + env = os.environ.copy() + env.update( + { + "HOME": str(home), + "USERPROFILE": str(home), + "XDG_CONFIG_HOME": str(home / ".config"), + "ZED_PKG_HOME": str(home / ".zed-pkg"), + "PATH": str(empty_path), + } + ) + for key in tuple(env): + if key.startswith("ZED_PKG_ENV_") or key in {"MISE_CONFIG_FILE", "MISE_TOML"}: + env.pop(key, None) + + completed = subprocess.run( + [str(zed), *args], + cwd=project, + env=env, + text=True, + capture_output=True, + check=False, + ) + if (completed.returncode == 0) != success: + raise HarnessFailure( + f"unexpected status {completed.returncode} for {list(args)!r}\n" + f"stdout:\n{completed.stdout}\nstderr:\n{completed.stderr}" + ) + if contains is not None: + combined = completed.stdout + "\n" + completed.stderr + ensure(contains in combined, f"missing {contains!r} in:\n{combined}") + return completed + + +def export_args(plan: str) -> tuple[str, ...]: + return ( + "env", + "export", + "mise", + "--plan", + plan, + "--output", + ".mise.toml", + "--write", + "--json", + ) + + +def certify(zed: Path, root: Path) -> None: + ensure(zed.is_file(), f"missing zed executable: {zed}") + ensure(not root.exists(), f"work root must be fresh: {root}") + root.mkdir(parents=True) + home = root / "home" + home.mkdir() + + reserved_project = root / "reserved-source" + reserved_project.mkdir() + reserved_state = reserved_project / ".zed/mise-export-state.json" + write_plan(reserved_state) + reserved_before = reserved_state.read_bytes() + run( + zed, + reserved_project, + home, + export_args(".zed/mise-export-state.json"), + success=False, + contains="environment plan cannot target reserved export state", + ) + ensure(reserved_state.read_bytes() == reserved_before, "reserved source plan was mutated") + ensure(not (reserved_project / ".mise.toml").exists(), "reserved source created output") + + project = root / "project" + project.mkdir() + write_plan(project / "plan-one.json") + write_plan(project / "plan-two.json") + + first = run(zed, project, home, export_args("plan-one.json"), success=True) + ensure(json.loads(first.stdout).get("action") == "written", "initial export was not written") + + output_path = project / ".mise.toml" + state_path = project / ".zed/mise-export-state.json" + output_before = output_path.read_bytes() + state_before = state_path.read_bytes() + + run( + zed, + project, + home, + export_args("plan-two.json"), + success=False, + contains="change ownership", + ) + ensure(output_path.read_bytes() == output_before, "ownership-transfer failure changed output") + ensure(state_path.read_bytes() == state_before, "ownership-transfer failure changed state") + + state = json.loads(state_before) + record = state["outputs"][".mise.toml"] + record["output_sha256"] = "0" * 64 + corrupted = (json.dumps(state, indent=2, sort_keys=True) + "\n").encode() + state_path.write_bytes(corrupted) + + run( + zed, + project, + home, + export_args("plan-one.json"), + success=False, + contains="export state records sha256", + ) + ensure(output_path.read_bytes() == output_before, "state-corruption failure changed output") + ensure(state_path.read_bytes() == corrupted, "state-corruption failure repaired state silently") + + print( + json.dumps( + { + "schema": "zed-pkg.mise-export-ownership-state.v1", + "certified": True, + "reserved_state_source_rejected": True, + "ownership_transfer_rejected": True, + "stale_digest_rejected": True, + "ambient_mise_required": False, + }, + sort_keys=True, + ) + ) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--zed", type=Path, required=True) + parser.add_argument("--work-root", type=Path, required=True) + args = parser.parse_args() + try: + certify(args.zed.resolve(), args.work_root.resolve()) + except (HarnessFailure, json.JSONDecodeError, KeyError) as error: + print(f"mise export ownership-state certification failed: {error}", file=sys.stderr) + return 1 + finally: + if args.work_root.exists() and os.environ.get("KEEP_MISE_EXPORT_ROOT") != "1": + shutil.rmtree(args.work_root, ignore_errors=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())