feat(wings): add auth, policy, and MOCITO install path#9458
Conversation
Greptile SummaryThis PR introduces the
Confidence Score: 3/5The PR is a large opt-in feature with several security-sensitive paths; the Most of the issues flagged in earlier review rounds have been addressed — credentials TOCTOU fixed, schema-mismatch surfaced at warn, IP-allow-list retry restored to original URL, MOCITO env now fully applied, policy org-keyed cache, locked-artifact fallback-or-false, ExistingInstallRestoredError returning Ok(false). The unresolved src/cli/wings/inspect.rs (registry hostname not validated before attaching Bearer token), src/wings/policy.rs (minimum_artifact_age_seconds and allowed_source_hosts unevaluated) Important Files Changed
Reviews (65): Last reviewed commit: "Report restored Wings installs as fallba..." | Re-trigger Greptile |
There was a problem hiding this comment.
Code Review
This pull request introduces 'mise wings', a new experimental feature that enables routing tool-install asset fetches through a paid regional cache. The changes include new CLI commands for authentication (login, logout, whoami, status), persistent credential management, and hooks into the HTTP client to transparently rewrite URLs and inject authentication headers. My review identified a security vulnerability regarding file permissions for stored credentials, a potential issue with misleading error handling during file reads, and a performance optimization opportunity in the URL rewriting logic.
| let raw = crate::file::read_to_string(&path) | ||
| .map_err(|e| eyre::eyre!(CredentialsError::Malformed(e.to_string())))?; |
There was a problem hiding this comment.
Mapping all I/O errors from read_to_string to CredentialsError::Malformed is misleading. If the file exists but cannot be read due to permission issues or other system errors, it should propagate the original error rather than claiming the file is malformed.
| let raw = crate::file::read_to_string(&path) | |
| .map_err(|e| eyre::eyre!(CredentialsError::Malformed(e.to_string())))?; | |
| let raw = crate::file::read_to_string(&path)?; |
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.5.6 x -- echo |
23.4 ± 1.3 | 19.9 | 30.3 | 1.05 ± 0.09 |
mise x -- echo |
22.3 ± 1.5 | 19.5 | 34.7 | 1.00 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.5.6 env |
20.7 ± 1.0 | 18.8 | 25.9 | 1.00 |
mise env |
21.2 ± 0.9 | 19.0 | 25.5 | 1.02 ± 0.06 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.5.6 hook-env |
21.8 ± 0.9 | 20.0 | 25.4 | 1.00 |
mise hook-env |
24.1 ± 1.4 | 20.8 | 29.3 | 1.10 ± 0.08 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.5.6 ls |
18.4 ± 1.2 | 16.4 | 23.3 | 1.00 |
mise ls |
19.1 ± 1.5 | 16.2 | 26.6 | 1.03 ± 0.11 |
xtasks/test/perf
| Command | mise-2026.5.6 | mise | Variance |
|---|---|---|---|
| install (cached) | 131ms | 136ms | -3% |
| ls (cached) | 65ms | 66ms | -1% |
| bin-paths (cached) | 67ms | 69ms | -2% |
| task-ls (cached) | 519ms | 520ms | +0% |
Mirroring the privacy revert in jdx/mise#9458: the mise binary's `wings.enabled` default went back to `false` after Bugbot HIGH + Greptile P1+security correctly flagged that auto-activating CI exchanges the runner's OIDC token to a third-party cache without explicit consent — surprising for any workflow with `id-token: write` (common for SLSA / AWS-OIDC / Sigstore / etc.). So this action goes back to `wings_enabled: true` as the explicit opt-in. Sets `MISE_WINGS_ENABLED=1` so subsequent `mise install` commands route through the cache. The "missing id-token: write" warning is back too — it's actionable for opted-in users, and not relevant for the default-off path.
Adds a single new input — `wings_enabled` — that gates the [mise-wings](https://mise-wings.en.dev) asset cache for tool installs. Existing workflows are unaffected: default `false` is a no-op. | Input | Default | Description | |---|---|---| | `wings_enabled` | `false` | Route tool-install URLs through the wings cache when `true` | ## How it works When `wings_enabled: true`, the action exports `MISE_WINGS_ENABLED=1`. Authentication is fully automatic — mise itself owns the GHA OIDC → wings session exchange. No `mise wings login` step in workflow YAML, no long-lived secrets to rotate. When mise (built with wings support — see jdx/mise#9458) sees `MISE_WINGS_ENABLED=1` and detects the GHA OIDC env vars (`ACTIONS_ID_TOKEN_REQUEST_URL` + `ACTIONS_ID_TOKEN_REQUEST_TOKEN`), it: 1. Fetches the runner's OIDC token, scoped to the wings deployment audience 2. POSTs it to `https://api.<host>/auth` to mint a wings CI session JWT 3. Caches the JWT in-process for the rest of the workflow 4. Transparently rewrites `registry.npmjs.org` / `github.com` / `api.github.com` URLs to the wings cache subdomains and attaches the JWT as a Bearer header ## Why opt-in (not opt-out) The default-off posture is deliberate. Many workflows already declare `permissions: id-token: write` for unrelated reasons (SLSA provenance, AWS OIDC, Sigstore, npm provenance). If `wings_enabled` defaulted to `true`, those workflows would silently send the runner's OIDC identity claims to a third-party cache without explicit consent. Cursor Bugbot HIGH + Greptile P1+security flagged a prior "default true" iteration of this PR as a privacy regression. Explicit opt-in keeps the gate visible in the workflow YAML. ## Workflow requirements ```yaml permissions: id-token: write # required for OIDC jobs: build: steps: - uses: jdx/mise-action@<sha> with: wings_enabled: true ``` The action emits a clear warning when `wings_enabled: true` but `id-token: write` is missing — without that hint, the user would see "wings configured but doing nothing" and have no clue why. ## Notes - Older mise binaries see `MISE_WINGS_ENABLED` and silently ignore it — forward-compatible. - `setupMise` fetches the mise binary itself with `curl`, which doesn't go through mise's HTTP layer; the wings rewriter only kicks in once the resulting mise binary runs `mise install`. The action sets the env var before any `mise` subcommand runs.
Adds a single new input — `wings_enabled` — that gates the [mise-wings](https://mise-wings.en.dev) asset cache for tool installs. Existing workflows are unaffected: default `false` is a no-op. | Input | Default | Description | |---|---|---| | `wings_enabled` | `false` | Route tool-install URLs through the wings cache when `true` | ## How it works When `wings_enabled: true`, the action exports `MISE_WINGS_ENABLED=1`. Authentication is fully automatic — mise itself owns the GHA OIDC → wings session exchange. No `mise wings login` step in workflow YAML, no long-lived secrets to rotate. When mise (built with wings support — see jdx/mise#9458) sees `MISE_WINGS_ENABLED=1` and detects the GHA OIDC env vars (`ACTIONS_ID_TOKEN_REQUEST_URL` + `ACTIONS_ID_TOKEN_REQUEST_TOKEN`), it: 1. Fetches the runner's OIDC token, scoped to the wings deployment audience 2. POSTs it to `https://api.<host>/auth` to mint a wings CI session JWT 3. Caches the JWT in-process for the rest of the workflow 4. Transparently rewrites `registry.npmjs.org` / `github.com` / `api.github.com` URLs to the wings cache subdomains and attaches the JWT as a Bearer header ## Why opt-in (not opt-out) The default-off posture is deliberate. Many workflows already declare `permissions: id-token: write` for unrelated reasons (SLSA provenance, AWS OIDC, Sigstore, npm provenance). If `wings_enabled` defaulted to `true`, those workflows would silently send the runner's OIDC identity claims to a third-party cache without explicit consent. Cursor Bugbot HIGH + Greptile P1+security flagged a prior "default true" iteration of this PR as a privacy regression. Explicit opt-in keeps the gate visible in the workflow YAML. ## Workflow requirements ```yaml permissions: id-token: write # required for OIDC jobs: build: steps: - uses: jdx/mise-action@<sha> with: wings_enabled: true ``` The action emits a clear warning when `wings_enabled: true` but `id-token: write` is missing — without that hint, the user would see "wings configured but doing nothing" and have no clue why. ## Notes - Older mise binaries see `MISE_WINGS_ENABLED` and silently ignore it — forward-compatible. - `setupMise` fetches the mise binary itself with `curl`, which doesn't go through mise's HTTP layer; the wings rewriter only kicks in once the resulting mise binary runs `mise install`. The action sets the env var before any `mise` subcommand runs.
## Summary Adds two new inputs that gate the mise-wings asset cache for tool installs. Existing workflows are unaffected: default `wings_enabled: false` is a no-op. | Input | Default | Description | |---|---|---| | `wings_enabled` | `false` | Route tool-install URLs through the wings cache when `true` | ## How it works When `wings_enabled: true`, the action exports `MISE_WINGS_ENABLED=1`. Authentication is **fully automatic** — mise itself owns the GHA OIDC → wings session exchange. No `mise wings login` step in workflow YAML, no long-lived secrets to rotate. When mise (built with wings support — see [jdx/mise#9458](jdx/mise#9458)) sees `MISE_WINGS_ENABLED=1` and detects the GHA OIDC env vars (`ACTIONS_ID_TOKEN_REQUEST_URL` + `ACTIONS_ID_TOKEN_REQUEST_TOKEN`), it: 1. Fetches the runner's OIDC token, scoped to the wings deployment audience 2. POSTs it to `https://api.<host>/auth` to mint a wings CI session JWT 3. Caches the JWT in-process for the rest of the workflow run 4. Transparently rewrites `registry.npmjs.org` / `github.com` / `api.github.com` URLs to the corresponding wings cache subdomains and attaches the JWT as a Bearer header ## Why opt-in (not opt-out) The default-off posture is deliberate. Many workflows already declare `permissions: id-token: write` for unrelated reasons (SLSA provenance, AWS OIDC, Sigstore, npm provenance, etc.). If `wings_enabled` defaulted to `true`, those workflows would silently send the runner's OIDC identity claims to a third-party cache without explicit consent. Cursor Bugbot HIGH + Greptile P1+security correctly flagged the previous "default true" iteration of this PR as a privacy regression. Explicit opt-in keeps the gate visible in the workflow YAML. ## Workflow requirements ```yaml permissions: id-token: write # required for OIDC jobs: build: steps: - uses: jdx/mise-action@<sha> with: wings_enabled: true ``` The action emits a clear warning when `wings_enabled: true` but `id-token: write` is missing — without that hint, the user would see "wings configured but doing nothing" and have no clue why. ## Test plan - [x] `npm run all` — format + lint + package, clean - [x] `dist/index.js` rebuilt and contains the wings hook (greppable: `MISE_WINGS_ENABLED`, `setupWings`) - [ ] End-to-end: a workflow with `wings_enabled: true`, `permissions: id-token: write`, an active wings subscription, and a recent enough `mise` binary. The mise repo's own `docs.yml` will exercise this path once [jdx/mise#9458](jdx/mise#9458) is merged. - [ ] Default-off path: a workflow without the `wings_enabled` input behaves identically to today. ## Out of scope - Older mise binaries will see `MISE_WINGS_ENABLED` and silently ignore it (no wings client code) — that's intended; the action doesn't gate on mise version. - Self-hosted runners: `permissions: id-token: write` only does anything on GitHub-hosted runners by default. Self-hosted runners need extra config; the warning above is conservative enough for both cases. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Introduces an opt-in path that can cause OIDC-based authentication to a third-party cache and alters tool download routing when enabled. Default-off behavior limits impact, but misconfiguration could create confusing cache bypass or unexpected network/token exchange behavior. > > **Overview** > Adds a new **experimental** `wings_enabled` action input (default `false`) to opt workflows into the mise-wings asset cache by exporting `MISE_WINGS_ENABLED=1`. > > When enabled, the action now runs `setupWings()` early to set the env var and warn if GitHub OIDC env vars are missing (i.e., `permissions: id-token: write` not configured), while leaving existing/default behavior unchanged. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 969042f. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
a2aa825 to
8e610ac
Compare
e53fd78 to
bad80d5
Compare
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
| } | ||
|
|
||
| let fallback_blocked = fallback_blocked(tv); | ||
| let locked_artifact = locked_wings_artifact(tv)?; | ||
| if let Some(artifact) = locked_artifact { | ||
| let Some(token) = crate::wings::auth::session_token().await? else { | ||
| bail!( | ||
| "mise.lock pins a wings artifact for {}, but wings authentication is not available; run `mise wings login` or configure GitHub Actions OIDC", | ||
| tv.style() | ||
| ); | ||
| }; | ||
| ctx.pr.set_message("wings pull locked oci".into()); | ||
| return pull_and_install(ctx, tv, &artifact, &token) | ||
| .await |
There was a problem hiding this comment.
Locked-artifact path hard-fails without auth even when fallback is not blocked
fallback_blocked is computed on line ~53 but is never consulted in this branch. When wings.enabled = true (opt-in via project mise.toml), a team member who hasn't run mise wings login and has no CI OIDC cannot install any tool whose wings_artifact_ref/digest is already pinned in mise.lock, even when wings.required is not configured. The PR description says "fallback remains the default behavior", but that promise isn't kept once an artifact is pinned.
Concrete scenario: a developer installs node via wings (lock gets wings_artifact_ref), commits mise.lock, and a new hire without wings credentials runs mise install. They receive the hard bail! despite wings.required never being set. Changing the bail! to return fallback_or_false(tv, fallback_blocked, "...") would honor the project-level fallback setting for users who haven't opted in to wings personally, while still enforcing the lock for teams that have set wings.required = ["*"].
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit dfacc4f. Configure here.
| tv.style() | ||
| ); | ||
| return Ok(()); | ||
| } |
There was a problem hiding this comment.
Restored-install error silently claims Wings success on fresh installs
Medium Severity
When install_mocito_artifact fails with ExistingInstallRestoredError, pull_and_install returns Ok(()), causing try_install to return Ok(true). This tells the caller the Wings install succeeded, so the normal backend installer never runs. On a fresh install where create_install_dirs already created empty dirs, the "restored existing install" is just those empty directories — the user ends up with an empty install dir that appears successful. The ExistingInstallRestoredError path also skips record_wings_lock_info. Returning Ok(false) instead of Ok(()) would allow the normal installer to fall back correctly.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit dfacc4f. Configure here.
| return Ok(()); | ||
| } | ||
| return Err(e); | ||
| } | ||
| record_wings_lock_info(tv, artifact, &policy_decision); | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[derive(Debug, Deserialize)] | ||
| struct WingsManifest { | ||
| #[serde(rename = "artifactType")] | ||
| artifact_type: String, |
There was a problem hiding this comment.
ExistingInstallRestoredError silently returns Ok(true) instead of Ok(false)
When install_mocito_artifact fails with ExistingInstallRestoredError (staging→install rename fails but backup restore succeeds), pull_and_install returns Ok(()). Back in try_install, the if let Err(e) = pull_and_install(...) branch is never taken, so execution falls through to Ok(true) — telling backend/mod.rs that wings fully succeeded.
The practical effect for a wings.required user doing a reinstall: the old (pre-upgrade) artifact stays in place, no error or fallback fires, and the lockfile is not updated. The user believes the new approved artifact is installed, but the old one is silently retained. Returning Ok(false) instead would let fallback_or_false apply the wings.required policy and either fail loudly or trigger the normal installer, giving the user a clear signal that the install didn't complete.
## Summary Adds two new inputs that gate the mise-wings asset cache for tool installs. Existing workflows are unaffected: default `wings_enabled: false` is a no-op. | Input | Default | Description | |---|---|---| | `wings_enabled` | `false` | Route tool-install URLs through the wings cache when `true` | ## How it works When `wings_enabled: true`, the action exports `MISE_WINGS_ENABLED=1`. Authentication is **fully automatic** — mise itself owns the GHA OIDC → wings session exchange. No `mise wings login` step in workflow YAML, no long-lived secrets to rotate. When mise (built with wings support — see [jdx/mise#9458](jdx/mise#9458)) sees `MISE_WINGS_ENABLED=1` and detects the GHA OIDC env vars (`ACTIONS_ID_TOKEN_REQUEST_URL` + `ACTIONS_ID_TOKEN_REQUEST_TOKEN`), it: 1. Fetches the runner's OIDC token, scoped to the wings deployment audience 2. POSTs it to `https://api.<host>/auth` to mint a wings CI session JWT 3. Caches the JWT in-process for the rest of the workflow run 4. Transparently rewrites `registry.npmjs.org` / `github.com` / `api.github.com` URLs to the corresponding wings cache subdomains and attaches the JWT as a Bearer header ## Why opt-in (not opt-out) The default-off posture is deliberate. Many workflows already declare `permissions: id-token: write` for unrelated reasons (SLSA provenance, AWS OIDC, Sigstore, npm provenance, etc.). If `wings_enabled` defaulted to `true`, those workflows would silently send the runner's OIDC identity claims to a third-party cache without explicit consent. Cursor Bugbot HIGH + Greptile P1+security correctly flagged the previous "default true" iteration of this PR as a privacy regression. Explicit opt-in keeps the gate visible in the workflow YAML. ## Workflow requirements ```yaml permissions: id-token: write # required for OIDC jobs: build: steps: - uses: jdx/mise-action@<sha> with: wings_enabled: true ``` The action emits a clear warning when `wings_enabled: true` but `id-token: write` is missing — without that hint, the user would see "wings configured but doing nothing" and have no clue why. ## Test plan - [x] `npm run all` — format + lint + package, clean - [x] `dist/index.js` rebuilt and contains the wings hook (greppable: `MISE_WINGS_ENABLED`, `setupWings`) - [ ] End-to-end: a workflow with `wings_enabled: true`, `permissions: id-token: write`, an active wings subscription, and a recent enough `mise` binary. The mise repo's own `docs.yml` will exercise this path once [jdx/mise#9458](jdx/mise#9458) is merged. - [ ] Default-off path: a workflow without the `wings_enabled` input behaves identically to today. ## Out of scope - Older mise binaries will see `MISE_WINGS_ENABLED` and silently ignore it (no wings client code) — that's intended; the action doesn't gate on mise version. - Self-hosted runners: `permissions: id-token: write` only does anything on GitHub-hosted runners by default. Self-hosted runners need extra config; the warning above is conservative enough for both cases. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Introduces an opt-in path that can cause OIDC-based authentication to a third-party cache and alters tool download routing when enabled. Default-off behavior limits impact, but misconfiguration could create confusing cache bypass or unexpected network/token exchange behavior. > > **Overview** > Adds a new **experimental** `wings_enabled` action input (default `false`) to opt workflows into the mise-wings asset cache by exporting `MISE_WINGS_ENABLED=1`. > > When enabled, the action now runs `setupWings()` early to set the env var and warn if GitHub OIDC env vars are missing (i.e., `permissions: id-token: write` not configured), while leaving existing/default behavior unchanged. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 969042fe52fbe7bea08e2d7019b51ebd1b238014. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
## Summary Adds two new inputs that gate the mise-wings asset cache for tool installs. Existing workflows are unaffected: default `wings_enabled: false` is a no-op. | Input | Default | Description | |---|---|---| | `wings_enabled` | `false` | Route tool-install URLs through the wings cache when `true` | ## How it works When `wings_enabled: true`, the action exports `MISE_WINGS_ENABLED=1`. Authentication is **fully automatic** — mise itself owns the GHA OIDC → wings session exchange. No `mise wings login` step in workflow YAML, no long-lived secrets to rotate. When mise (built with wings support — see [jdx/mise#9458](jdx/mise#9458)) sees `MISE_WINGS_ENABLED=1` and detects the GHA OIDC env vars (`ACTIONS_ID_TOKEN_REQUEST_URL` + `ACTIONS_ID_TOKEN_REQUEST_TOKEN`), it: 1. Fetches the runner's OIDC token, scoped to the wings deployment audience 2. POSTs it to `https://api.<host>/auth` to mint a wings CI session JWT 3. Caches the JWT in-process for the rest of the workflow run 4. Transparently rewrites `registry.npmjs.org` / `github.com` / `api.github.com` URLs to the corresponding wings cache subdomains and attaches the JWT as a Bearer header ## Why opt-in (not opt-out) The default-off posture is deliberate. Many workflows already declare `permissions: id-token: write` for unrelated reasons (SLSA provenance, AWS OIDC, Sigstore, npm provenance, etc.). If `wings_enabled` defaulted to `true`, those workflows would silently send the runner's OIDC identity claims to a third-party cache without explicit consent. Cursor Bugbot HIGH + Greptile P1+security correctly flagged the previous "default true" iteration of this PR as a privacy regression. Explicit opt-in keeps the gate visible in the workflow YAML. ## Workflow requirements ```yaml permissions: id-token: write # required for OIDC jobs: build: steps: - uses: jdx/mise-action@<sha> with: wings_enabled: true ``` The action emits a clear warning when `wings_enabled: true` but `id-token: write` is missing — without that hint, the user would see "wings configured but doing nothing" and have no clue why. ## Test plan - [x] `npm run all` — format + lint + package, clean - [x] `dist/index.js` rebuilt and contains the wings hook (greppable: `MISE_WINGS_ENABLED`, `setupWings`) - [ ] End-to-end: a workflow with `wings_enabled: true`, `permissions: id-token: write`, an active wings subscription, and a recent enough `mise` binary. The mise repo's own `docs.yml` will exercise this path once [jdx/mise#9458](jdx/mise#9458) is merged. - [ ] Default-off path: a workflow without the `wings_enabled` input behaves identically to today. ## Out of scope - Older mise binaries will see `MISE_WINGS_ENABLED` and silently ignore it (no wings client code) — that's intended; the action doesn't gate on mise version. - Self-hosted runners: `permissions: id-token: write` only does anything on GitHub-hosted runners by default. Self-hosted runners need extra config; the warning above is conservative enough for both cases. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Introduces an opt-in path that can cause OIDC-based authentication to a third-party cache and alters tool download routing when enabled. Default-off behavior limits impact, but misconfiguration could create confusing cache bypass or unexpected network/token exchange behavior. > > **Overview** > Adds a new **experimental** `wings_enabled` action input (default `false`) to opt workflows into the mise-wings asset cache by exporting `MISE_WINGS_ENABLED=1`. > > When enabled, the action now runs `setupWings()` early to set the env var and warn if GitHub OIDC env vars are missing (i.e., `permissions: id-token: write` not configured), while leaving existing/default behavior unchanged. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 969042fe52fbe7bea08e2d7019b51ebd1b238014. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->


Summary
Adds the first opt-in mise-wings client surface to mise. Existing installs do not change unless
wings.enabled = trueorMISE_WINGS_ENABLED=1is set and mise can obtain a Wings session.This PR is the upstream
misecounterpart to the Wings MOCITO artifact work. Wings resolves and serves approved tool artifacts through an OCI registry; mise owns local version resolution, signed policy verification, digest/referrer verification, extraction, and PATH/env wiring.Included
mise wings login/logout/whoami/statuswith generated CLI docs/manpage outputmise wings inventoryto upload the current installed-tool security snapshot to/v1/wings/inventorymise wings inspect manifest|referrers|sbomfor authenticated OCI artifact inspectionwings.enabled,wings.required, hiddenwings.staging, and per-toolwings = falseMISE_STATE_DIR/wings/credentials.jsonwith 0600 permissions on Unixid-token: writeis availableregistry.<wings-host>for resolver-approved artifactsapplication/vnd.mise.tool.v1andapplication/vnd.mise.tool.config.v1+json/v1/wings/policybundle fetch/cache/verification before Wings installsmise.lockrecording of the exact Wings artifact ref, artifact digest, and evaluated policy versionmise.lockreplay of pinned Wings artifact refs/digests before resolver calls, so older approved artifact revisions stay installable by digestnamespace,tool,version,platform,stripComponents,bin,binLinks, andenvenvpersistence into the install directory and activation through the normalToolset::env_from_tools()path, including existingMISE_ADD_PATHhandlingbinLinksinstallation into.mise-bins, following aqua/GitHub selected-bin behavior so bundled helper binaries do not leak onto PATHBehavior
When Wings is active, mise first honors any current-platform Wings artifact pin in
mise.lockby pulling that exact OCI manifest digest. If no Wings artifact is pinned and mise can identify an installable source artifact URL for a tool version, it posts tohttps://api.<wings-host>/v1/catalog/resolve.allow: mise pulls the returned OCI manifest/config/payloads fromhttps://registry.<wings-host>/v2/..., verifies descriptors and evidence referrers, verifies/evaluates the signed org policy bundle, records the selected artifact/policy inmise.lock, and installs the artifact into the normal install directory.pending: mise blocks, polls the resolver, and surfaces the worker-reported percentage/message.blocked: mise fails the install with the policy reason.Fallback remains the default behavior. Teams that want stricter enforcement can set
wings.required = ["node", "github:*"]orwings.required = ["*"]; matched installs fail if Wings cannot provide an artifact. A tool-levelwings = falseremains an explicit opt-out for that tool.The old URL rewriting path (
npm.<wings.host>,gh.<wings.host>,gh-api.<wings.host>) is removed.mise oci build/run/pushis unrelated to this path; Wings has its own registry client and does not depend on themise ociCLI feature.Package-manager backends such as
npm,pipx, andgemare intentionally not faked here with metadata/source blobs. They should activate through Wings once the worker can emit actual installable MOCITO artifacts for those backend recipes.mise wings inventoryposts installed tool identity, version, platform, Wings artifact digest when present, and sha256/http source evidence when available. It uses the enrolled device id for dev credentials or a generated local id for CI/non-device sessions, and deliberately excludes paths, hostnames, usernames, env values, command arguments, and package-manager logs.Validation
cargo check -qcargo test -q wings::policycargo test -q wings::artifact::testscargo test -q wings::inventorycargo test -q wings::cicargo test -q wings::artifact::tests::resolver_retries_rate_limits_and_server_errorscargo test -q wingscargo test -q lockfile::testscargo test -q toolset::toolset_envmise run render:schemamise run render:usagehksuite passed on the original branch, includingcargo fmt --all -- --check,cargo check --all-features, prettier, shellcheck, shfmt, actionlint, markdownlint, taplo, and schema checksFollow-ups
The larger Wings path still needs worker-side package-manager recipes/rebuild invalidation before npm/pipx/gem tools can be served as useful installable OCI artifacts.
Note
High Risk
Adds a new authenticated install flow that can change how tools are resolved/installed when
wings.enabledis set, including network auth (device + GHA OIDC), policy enforcement, and lockfile pinning. Mistakes here could break installs or inadvertently affect credential handling/host routing.Overview
Adds a new opt-in mise-wings integration gated by
wings.enabled/MISE_WINGS_ENABLED, including amise wingsCLI (login/logout/status/whoami,inventory, andinspectcommands) plus generated docs/manpage entries.Tool installation now attempts
crate::wings::artifact::try_installbefore the normal backend installer, supporting resolver allow/pending/blocked responses, OCI registry pulls with digest/size verification, and policy evaluation;wings.requiredcan enforce “no fallback,” and per-toolwings = falseopts out.Extends
mise.lockplatform metadata to persist/replaywings_artifact_ref,wings_artifact_digest, andwings_policy_version, wires Wings-provided env into tool environment resolution, and updates GitHubdocsworkflow permissions/env to enable OIDC-based Wings auth.Reviewed by Cursor Bugbot for commit 0c3bcb1. Bugbot is set up for automated code reviews on this repo. Configure here.