Skip to content

feat(wings): add auth, policy, and MOCITO install path#9458

Closed
jdx wants to merge 43 commits into
mainfrom
feat/wings-cli
Closed

feat(wings): add auth, policy, and MOCITO install path#9458
jdx wants to merge 43 commits into
mainfrom
feat/wings-cli

Conversation

@jdx

@jdx jdx commented Apr 28, 2026

Copy link
Copy Markdown
Owner

Summary

Adds the first opt-in mise-wings client surface to mise. Existing installs do not change unless wings.enabled = true or MISE_WINGS_ENABLED=1 is set and mise can obtain a Wings session.

This PR is the upstream mise counterpart 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/status with generated CLI docs/manpage output
  • mise wings inventory to upload the current installed-tool security snapshot to /v1/wings/inventory
  • mise wings inspect manifest|referrers|sbom for authenticated OCI artifact inspection
  • wings.enabled, wings.required, hidden wings.staging, and per-tool wings = false
  • schema-versioned dev credentials at MISE_STATE_DIR/wings/credentials.json with 0600 permissions on Unix
  • refresh-token rotation with in-process coordination
  • GitHub Actions OIDC exchange for short-lived CI sessions when id-token: write is available
  • install-time Wings catalog resolution for backends with installable artifact URLs
  • OCI registry pulls from registry.<wings-host> for resolver-approved artifacts
  • MOCITO manifest/config consumption with application/vnd.mise.tool.v1 and application/vnd.mise.tool.config.v1+json
  • descriptor digest and size verification for the manifest, config blob, and every payload layer
  • OCI referrers index and referrer-manifest verification for Wings evidence attached to the selected platform manifest digest
  • signed /v1/wings/policy bundle fetch/cache/verification before Wings installs
  • local policy evaluation against source checksum, scan, approval, managed, and referrer evidence
  • mise.lock recording of the exact Wings artifact ref, artifact digest, and evaluated policy version
  • mise.lock replay of pinned Wings artifact refs/digests before resolver calls, so older approved artifact revisions stay installable by digest
  • one or more payload layers extracted in manifest order
  • MOCITO config validation for namespace, tool, version, platform, stripComponents, bin, binLinks, and env
  • MOCITO env persistence into the install directory and activation through the normal Toolset::env_from_tools() path, including existing MISE_ADD_PATH handling
  • binLinks installation into .mise-bins, following aqua/GitHub selected-bin behavior so bundled helper binaries do not leak onto PATH
  • pending-job polling with worker progress messages
  • removal of the old transparent hostname rewrite/cache-subdomain model
  • docs, manpage, schema, and workflow text updates for the resolver/OCI/inventory model

Behavior

When Wings is active, mise first honors any current-platform Wings artifact pin in mise.lock by 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 to https://api.<wings-host>/v1/catalog/resolve.

  • allow: mise pulls the returned OCI manifest/config/payloads from https://registry.<wings-host>/v2/..., verifies descriptors and evidence referrers, verifies/evaluates the signed org policy bundle, records the selected artifact/policy in mise.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.
  • resolver unavailable/timeout: mise falls back to the normal backend installer by default.

Fallback remains the default behavior. Teams that want stricter enforcement can set wings.required = ["node", "github:*"] or wings.required = ["*"]; matched installs fail if Wings cannot provide an artifact. A tool-level wings = false remains 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/push is unrelated to this path; Wings has its own registry client and does not depend on the mise oci CLI feature.

Package-manager backends such as npm, pipx, and gem are 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 inventory posts 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 -q
  • cargo test -q wings::policy
  • cargo test -q wings::artifact::tests
  • cargo test -q wings::inventory
  • cargo test -q wings::ci
  • cargo test -q wings::artifact::tests::resolver_retries_rate_limits_and_server_errors
  • cargo test -q wings
  • cargo test -q lockfile::tests
  • cargo test -q toolset::toolset_env
  • mise run render:schema
  • mise run render:usage
  • commit hook hk suite passed on the original branch, including cargo fmt --all -- --check, cargo check --all-features, prettier, shellcheck, shfmt, actionlint, markdownlint, taplo, and schema checks

Follow-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.enabled is 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 a mise wings CLI (login/logout/status/whoami, inventory, and inspect commands) plus generated docs/manpage entries.

Tool installation now attempts crate::wings::artifact::try_install before the normal backend installer, supporting resolver allow/pending/blocked responses, OCI registry pulls with digest/size verification, and policy evaluation; wings.required can enforce “no fallback,” and per-tool wings = false opts out.

Extends mise.lock platform metadata to persist/replay wings_artifact_ref, wings_artifact_digest, and wings_policy_version, wires Wings-provided env into tool environment resolution, and updates GitHub docs workflow 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.

@greptile-apps

greptile-apps Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces the mise wings subsystem: opt-in (wings.enabled = false by default) OCI-backed tool installation via a Wings catalog resolver, along with mise wings login/logout/status/whoami/inventory/inspect CLI commands and lockfile pinning of Wings artifact refs and policy versions.

  • Core install path (src/wings/artifact.rs): catalog resolve with pending/blocked handling, OCI manifest/config/layer download with sha256 digest verification, MOCITO config validation, staging→install atomic swap with backup/restore, and lockfile recording of artifact ref + policy version.
  • Auth, credentials, policy (auth.rs, credentials.rs, ci.rs, policy.rs): dev login (credential file with 0o600 from open), GHA OIDC auto-mint, host-keyed credential validation, refresh-token rotation with concurrent-logout guard, signed policy JWS fetch/cache/evaluate with observe-mode fallback when no trust root is configured.
  • Env and HTTP plumbing (toolset_env.rs, http.rs, backend/mod.rs): MOCITO env vars merged into tool activation unconditionally (self-guarded by install marker); IP allow-list retry uses original pre-rewrite URL.

Confidence Score: 3/5

The PR is a large opt-in feature with several security-sensitive paths; the inspect commands still forward the wings session JWT to any caller-supplied registry hostname without validating against the expected wings registry, which was flagged in a prior round and remains unresolved.

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 inspect.rs token-to-arbitrary-registry path is the main remaining concern for this round. Additionally, minimum_artifact_age_seconds and allowed_source_hosts are deserialized from the signed policy bundle but never evaluated, silently bypassing those org-configured constraints in Enforce mode.

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

Filename Overview
src/wings/artifact.rs Core Wings install path: OCI pull, MOCITO extraction, digest/referrer verification, env file write, staging→install swap. Most previously-flagged issues resolved (locked-artifact auth fallback, ExistingInstallRestoredError Ok(false), env application, backup-cleanup warning, deadline guard). Registry hostname validation in place for install path.
src/wings/policy.rs Policy fetch/verify/evaluate. Observe-mode fallback when no public key configured; org-keyed cache lookup (not all-file scan). minimum_artifact_age_seconds and allowed_source_hosts deserialized but never evaluated in Enforce mode.
src/wings/credentials.rs TOCTOU fix applied (OpenOptions with mode 0o600 on Unix). Schema-mismatch errors now log at warn level. Host mismatch guard in session_token_from_credentials.
src/wings/auth.rs Session token acquisition with host validation and concurrent logout guard (credentials::cached() checked after acquiring lock). CI OIDC path only fires when wings.enabled.
src/http.rs IP allow-list retry now uses original_url with use_url_replacements=false, correctly bypassing wings rewrite on the retry attempt. GitHub forbidden check uses request_url (post-replacement) correctly avoiding false triggers when wings is active.
src/toolset/toolset_env.rs Outer wings.enabled gate removed; installed_env called unconditionally for every tool (self-guards with has_wings_install_marker). MOCITO env merging implemented with correct MISE_ADD_PATH path concatenation and backend-wins precedence.
src/lockfile.rs wings_artifact_ref, wings_artifact_digest, wings_policy_version added to PlatformInfo with correct merge semantics (drop on URL change, preserve on same URL). Roundtrip tests included.
src/cli/wings/inspect.rs Inspect commands accept arbitrary registry hostnames and attach wings Bearer token without validating the registry matches registry.. Install path has this check; inspect path does not (flagged in prior review round).
src/backend/mod.rs try_install plumbed before install_version_. Ok(true)→direct tv return, Ok(false)→normal installer, Err→cleanup_install_dirs_on_error. Old tv clone for cleanup preserved in the Ok(false) branch.

Fix All in Claude Code

Reviews (65): Last reviewed commit: "Report restored Wings installs as fallba..." | Re-trigger Greptile

Comment thread src/wings/credentials.rs
Comment thread src/http.rs Outdated
Comment thread settings.toml Outdated
Comment thread src/wings/credentials.rs
Comment thread src/cli/wings/login.rs Outdated
Comment thread src/wings/http_hooks.rs Outdated
Comment thread src/cli/wings/whoami.rs

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/wings/credentials.rs Outdated
Comment thread src/wings/credentials.rs Outdated
Comment on lines +126 to +127
let raw = crate::file::read_to_string(&path)
.map_err(|e| eyre::eyre!(CredentialsError::Malformed(e.to_string())))?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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)?;

Comment thread src/wings/url.rs Outdated
Comment thread src/http.rs Outdated
Comment thread src/wings/http_hooks.rs Outdated
Comment thread src/wings/credentials.rs
Comment thread src/wings/http_hooks.rs Outdated
@github-actions

github-actions Bot commented Apr 29, 2026

Copy link
Copy Markdown

Hyperfine Performance

mise x -- echo

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%

Comment thread settings.toml Outdated
Comment thread settings.toml Outdated
Comment thread src/cli/wings/login.rs Outdated
jdx added a commit to jdx/mise-action that referenced this pull request Apr 29, 2026
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.
Comment thread schema/mise.json
Comment thread src/cli/wings/status.rs
Comment thread src/cli/wings/status.rs Outdated
Comment thread src/wings/http_hooks.rs Outdated
jdx added a commit to jdx/mise-action that referenced this pull request Apr 29, 2026
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.
jdx added a commit to jdx/mise-action that referenced this pull request Apr 29, 2026
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.
jdx added a commit to jdx/mise-action that referenced this pull request Apr 29, 2026
## 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 -->
@jdx jdx force-pushed the feat/wings-cli branch 2 times, most recently from a2aa825 to 8e610ac Compare May 5, 2026 18:05
Comment thread src/http.rs Outdated
@jdx jdx changed the title feat(wings): mise wings login/logout/whoami/status + transparent cache routing feat(wings): add CLI auth and opt-in cache routing May 5, 2026
@jdx jdx force-pushed the feat/wings-cli branch from 8e610ac to d8b7396 Compare May 5, 2026 19:26
Comment thread .github/workflows/test.yml Outdated
@jdx jdx force-pushed the feat/wings-cli branch from d8b7396 to c6a5ce4 Compare May 5, 2026 19:44
Comment thread src/http.rs Outdated
Comment thread src/wings/http_hooks.rs Outdated
@jdx jdx force-pushed the feat/wings-cli branch 2 times, most recently from e53fd78 to bad80d5 Compare May 6, 2026 20:54
Comment thread src/wings/policy.rs
@jdx jdx force-pushed the feat/wings-cli branch from d2de1f0 to 34fe1ea Compare May 13, 2026 00:00
Comment thread src/wings/inventory.rs Outdated
Comment thread src/http.rs
Comment thread docs/.vitepress/cli_commands.ts
Comment thread src/wings/artifact.rs
Comment thread src/http.rs
@socket-security

socket-security Bot commented May 13, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcargo/​jsonwebtoken@​9.3.11009997100100

View full report

Comment thread src/lockfile.rs
Comment thread src/wings/artifact.rs
Comment thread src/lockfile.rs Outdated
Comment thread src/wings/artifact.rs
Comment thread src/wings/policy.rs
Comment thread src/wings/artifact.rs
Comment on lines +55 to +68
}

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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 = ["*"].

Fix in Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread src/wings/artifact.rs
tv.style()
);
return Ok(());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit dfacc4f. Configure here.

Comment thread src/wings/artifact.rs Outdated
Comment on lines +649 to +660
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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Fix in Claude Code

@jdx jdx closed this Jun 6, 2026
github-actions Bot added a commit to step-security/mise-action that referenced this pull request Jun 9, 2026
## 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 -->
github-actions Bot added a commit to step-security/mise-action that referenced this pull request Jun 9, 2026
## 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 -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant