diff --git a/.github/CLAUDE.md b/.github/CLAUDE.md index 85761cc28..0ac64a4c9 100644 --- a/.github/CLAUDE.md +++ b/.github/CLAUDE.md @@ -67,8 +67,8 @@ source contracts only; it never builds release binaries or container images. Release Please maintains the version and changelog PR. Publishing the resulting stable GitHub release triggers the heavy release workflows: -- `release.yml` builds and smokes Linux and Windows archives, builds and scans - the container, verifies and attaches artifacts, publishes npm and MCP +- `release.yml` builds and smokes Linux, macOS, and Windows archives, builds and + scans the container, verifies and attaches artifacts, publishes npm and MCP Registry metadata, and signs/attests release outputs. - `build-incus-image.yml` uses the central hosted Incus image workflow and publishes checksum-verified image assets plus the rolling Incus alias. @@ -82,6 +82,9 @@ ARM64 workflow, installer, and package contracts are explicitly enabled for Labby through the pinned fleet policy and repository contract. Keep that opt-in visible when adding ARM64 jobs or artifacts; QEMU and cross-platform emulation still require a deliberate implementation and verification plan. +The supported binary artifacts are Linux x86_64, macOS arm64, and Windows +x86_64. Keep each target native to its GitHub-hosted runner; do not add +emulation, cross-platform image matrices, or QEMU setup. ## Editing rules diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 8361cb58d..da97f8113 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -5,7 +5,7 @@ name: release-please # On every green CI run on main, release-please maintains a "release PR" that # bumps `.release-please-manifest.json` and updates CHANGELOG.md. Merging that # PR creates the git tag (vX.Y.Z) and a draft GitHub Release. Publishing that -# release triggers the GitHub-hosted release workflows for Linux/Windows +# release triggers the GitHub-hosted release workflows for Linux/macOS/Windows # archives, the GHCR image, and the Incus artifact. # # release-type is "simple", NOT "rust". release-please's rust/cargo-workspace diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2f5763108..bb4a85584 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -100,6 +100,10 @@ jobs: runner: '"windows-latest"' binary: labby.exe archive: lab-x86_64-pc-windows-msvc.zip + - target: aarch64-apple-darwin + runner: '"macos-15"' + binary: labby + archive: lab-aarch64-apple-darwin.tar.gz steps: - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd with: @@ -111,6 +115,8 @@ jobs: - name: Install Rust and kache if: runner.os != 'Windows' uses: ./.github/actions/setup-rust-kache + with: + targets: ${{ matrix.target }} - uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 if: runner.os == 'Windows' with: @@ -133,7 +139,11 @@ jobs: mkdir -p dist cp "target/${{ matrix.target }}/release/${{ matrix.binary }}" "dist/${{ matrix.binary }}" tar -C dist -czf "${{ matrix.archive }}" "${{ matrix.binary }}" - sha256sum "${{ matrix.archive }}" > "${{ matrix.archive }}.sha256" + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "${{ matrix.archive }}" > "${{ matrix.archive }}.sha256" + else + shasum -a 256 "${{ matrix.archive }}" > "${{ matrix.archive }}.sha256" + fi - name: Smoke packaged Code Mode runner (Unix) if: runner.os != 'Windows' diff --git a/README.md b/README.md index 992b007f4..f76f2fc46 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,13 @@ labby mcp `labby serve` starts the hosted HTTP runtime: `/v1` product APIs, `/mcp` streamable HTTP MCP, auth routes, OAuth relay endpoints, and static Labby web assets when an export is available. `labby mcp` is the stdio MCP entrypoint for -local MCP clients. +local MCP clients. A client configured to launch `labby mcp` does not need an +HTTP URL: when a `labby serve` daemon is reachable, the stdio process becomes a +transparent bridge to that daemon and uses its gateway configuration, upstream +connections, and OAuth state. If no daemon is found and no explicit target is +set, it starts a standalone local gateway instead. See the +[local bridge guide](./docs/surfaces/TRANSPORT.md#local-bridge-to-the-running-daemon) +for client configuration and `LABBY_SERVER_URL` fail-closed behavior. ### Manage Upstream MCP Gateways @@ -543,8 +549,8 @@ provider tools. It does not install ACP adapters or mount ACP-specific state. Release Please maintains the version/changelog pull request and creates the stable tag plus draft GitHub release when that pull request merges. Publishing -the release triggers the heavy GitHub-hosted x86_64 workflows. They build Linux -and Windows archives with checksums, build and scan the GHCR image, build and +the release triggers the heavy GitHub-hosted workflows. They build Linux, +macOS, and Windows archives with checksums, build and scan the GHCR image, build and smoke the Incus image, publish the npm launcher, and publish Labby's `server.json` metadata to the official MCP Registry. diff --git a/crates/labby-codemode/src/runner_backend.rs b/crates/labby-codemode/src/runner_backend.rs index 6b1c504c5..48b5951aa 100644 --- a/crates/labby-codemode/src/runner_backend.rs +++ b/crates/labby-codemode/src/runner_backend.rs @@ -199,7 +199,7 @@ fn invalid_param(message: impl Into) -> ToolError { } } -#[cfg(test)] +#[cfg(all(test, target_os = "linux"))] mod tests { use super::*; diff --git a/crates/labby/tests/ci_changed_paths.rs b/crates/labby/tests/ci_changed_paths.rs index 4e15f462b..d2092f99f 100644 --- a/crates/labby/tests/ci_changed_paths.rs +++ b/crates/labby/tests/ci_changed_paths.rs @@ -1240,6 +1240,15 @@ fn draft_releases_are_surfaced_without_being_auto_published() { fn release_tool_downloads_are_version_and_digest_pinned() { let release = fs::read_to_string(repo_root().join(".github/workflows/release.yml")) .expect("read release workflow"); + for target in [ + "target: x86_64-unknown-linux-gnu", + "target: aarch64-apple-darwin", + "target: x86_64-pc-windows-msvc", + ] { + assert!(release.contains(target), "release matrix missing {target}"); + } + assert!(release.contains("runner: '\"macos-15\"'")); + assert!(!release.contains("x86_64-apple-darwin")); assert!(!release.contains("/latest/download/")); assert!(!release.contains("mcp-publisher")); assert!(!release.contains("registry.modelcontextprotocol.io")); diff --git a/docs/TECH.md b/docs/TECH.md index 3d0e4373d..1e9e17ad4 100644 --- a/docs/TECH.md +++ b/docs/TECH.md @@ -18,7 +18,7 @@ The workspace metadata in the root `Cargo.toml` is authoritative: - Cargo resolver 3 - workspace version shared by the Rust crates - AGPL-3.0-only license -- release targets: Linux x86_64 GNU and Windows x86_64 MSVC +- release targets: Linux x86_64 GNU, macOS arm64, and Windows x86_64 MSVC `rust-toolchain.toml` pins the toolchain used locally and in CI. The matching `rust-version` in `Cargo.toml` is the minimum version Cargo will accept. diff --git a/docs/runtime/CICD.md b/docs/runtime/CICD.md index dea8fc0ba..e04e60f27 100644 --- a/docs/runtime/CICD.md +++ b/docs/runtime/CICD.md @@ -181,7 +181,7 @@ land the required code/tests and the baseline update together. - Required fast jobs run only when their category is enabled; `ci-gate` is the stable required check for branch protection - Native Windows workspace and Palette jobs use GitHub-hosted runners, bounded timeouts, and keyed Cargo caches; they report portability regressions without blocking `ci-gate` - Heavy release work starts only from a published stable GitHub release - - Release Linux jobs use GitHub-hosted x86_64 runners; native Windows artifacts use GitHub-hosted Windows + - Release Linux jobs use GitHub-hosted x86_64 runners; native macOS and Windows artifacts use GitHub-hosted runners The pinned fleet policy and repository contract set `allow-arm64: true` for Labby. This removes the former fleet-wide ARM64 token rejection while keeping @@ -203,11 +203,13 @@ documented in [Actions runner setup](./ACTIONS_RUNNER.md). | Platform | Target | |----------|--------| | Linux x86_64 | `x86_64-unknown-linux-gnu` | +| macOS arm64 | `aarch64-apple-darwin` | | Windows x86_64 | `x86_64-pc-windows-msvc` | -Windows is a supported platform. Official Windows release artifacts are built -on native GitHub-hosted Windows runners using the MSVC target. Linux-to-Windows -GNU cross-compilation may be useful experimentally, but it is not the release +macOS and Windows are supported platforms. Official macOS artifacts are built +on a native GitHub-hosted Apple Silicon runner. Official Windows artifacts are +built on native GitHub-hosted Windows runners using the MSVC target. +Cross-compilation may be useful experimentally, but it is not the release support contract. ## Integration Tests @@ -252,7 +254,7 @@ partially published release. - **Surface:** GitHub Releases - **Container surface:** GitHub Container Registry (`ghcr.io/dinglebear-ai/labby`) -- **Artifacts per release:** one binary archive per supported target (Linux x86_64 and Windows x86_64) +- **Artifacts per release:** one binary archive per supported target (Linux x86_64, macOS arm64, and Windows x86_64) - **Checksums:** every binary archive has a SHA-256 checksum file - **Package registries:** the `@dinglebear/labby` npm launcher and `server.json` MCP Registry metadata publish from the same validated version. diff --git a/docs/services/UPSTREAM.md b/docs/services/UPSTREAM.md index 4c0206b29..af1a67f52 100644 --- a/docs/services/UPSTREAM.md +++ b/docs/services/UPSTREAM.md @@ -638,7 +638,7 @@ Add one or more `[[upstream]]` entries to `~/.config/labby/config.toml`. Set bearer-token env vars named by `bearer_token_env` in `~/.labby/.env` or the process environment. -### 3. Start `lab` +### 3. Start `labby` For local stdio clients: @@ -652,21 +652,27 @@ For network MCP clients: labby serve ``` -### 4. Point the client at `lab`, not the upstreams +### 4. Point the client at `labby`, not the upstreams Example `.mcp.json` for stdio: ```json { "mcpServers": { - "lab": { + "labby": { "command": "labby", - "args": ["serve"] + "args": ["mcp"] } } } ``` +This is the local stdio bridge: the client does not need an HTTP URL. If a +`labby serve` daemon is already running, `labby mcp` forwards the session to +that daemon; otherwise it starts a standalone local gateway. See the +[transport guide](../surfaces/TRANSPORT.md#local-bridge-to-the-running-daemon) +for explicit-target and fallback behavior. + Example HTTP MCP endpoint: ```text diff --git a/docs/surfaces/TRANSPORT.md b/docs/surfaces/TRANSPORT.md index 2dc5b3e71..1f233fa4b 100644 --- a/docs/surfaces/TRANSPORT.md +++ b/docs/surfaces/TRANSPORT.md @@ -21,6 +21,52 @@ labby mcp Stdio is intended for local editor and desktop clients. Protocol messages use stdin/stdout; logs must never be written to stdout. +### Local bridge to the running daemon + +Use `labby mcp` when the MCP client can launch a local command but should use +the same gateway state as a long-running `labby serve` daemon. The client talks +to `labby mcp` over stdin/stdout, so the client configuration does not need an +HTTP URL: + +```json +{ + "mcpServers": { + "labby": { + "command": "labby", + "args": ["mcp"] + } + } +} +``` + +Start the daemon once, either in a terminal or as the installed user service: + +```bash +labby serve --host 127.0.0.1 --port 8765 +``` + +When `labby mcp` starts, it probes the local daemon. If one is reachable, it +becomes a transparent stdio-to-MCP bridge: tools, resources, prompts, +notifications, cancellation, and task responses are forwarded to the daemon. +The bridge does not create a second gateway manager, upstream pool, or OAuth +state, so the local client sees the same configuration and connections as the +web UI and HTTP clients. + +If no explicit remote target is configured and no daemon is found, `labby mcp` +starts a standalone local gateway instead. This is useful for a fully local +setup, but it has its own configuration and runtime state. To require one +specific daemon and prevent that standalone fallback, set `LABBY_SERVER_URL`: + +```bash +LABBY_SERVER_URL=http://127.0.0.1:8765 labby mcp +``` + +Explicit targets fail closed when they are invalid, unreachable, unauthorized, +or not a compatible Labby daemon. For a daemon on another host, use an HTTPS +`LABBY_SERVER_URL` and the daemon's matching `LABBY_MCP_HTTP_TOKEN`; use the +HTTP MCP endpoint directly when the client cannot launch local commands. The +full target and token rules are in [Environment](../runtime/ENV.md#remote-gateway-cli-usage). + ## Streamable HTTP Run: diff --git a/packages/labby-mcp/README.md b/packages/labby-mcp/README.md index 992b007f4..f76f2fc46 100644 --- a/packages/labby-mcp/README.md +++ b/packages/labby-mcp/README.md @@ -223,7 +223,13 @@ labby mcp `labby serve` starts the hosted HTTP runtime: `/v1` product APIs, `/mcp` streamable HTTP MCP, auth routes, OAuth relay endpoints, and static Labby web assets when an export is available. `labby mcp` is the stdio MCP entrypoint for -local MCP clients. +local MCP clients. A client configured to launch `labby mcp` does not need an +HTTP URL: when a `labby serve` daemon is reachable, the stdio process becomes a +transparent bridge to that daemon and uses its gateway configuration, upstream +connections, and OAuth state. If no daemon is found and no explicit target is +set, it starts a standalone local gateway instead. See the +[local bridge guide](./docs/surfaces/TRANSPORT.md#local-bridge-to-the-running-daemon) +for client configuration and `LABBY_SERVER_URL` fail-closed behavior. ### Manage Upstream MCP Gateways @@ -543,8 +549,8 @@ provider tools. It does not install ACP adapters or mount ACP-specific state. Release Please maintains the version/changelog pull request and creates the stable tag plus draft GitHub release when that pull request merges. Publishing -the release triggers the heavy GitHub-hosted x86_64 workflows. They build Linux -and Windows archives with checksums, build and scan the GHCR image, build and +the release triggers the heavy GitHub-hosted workflows. They build Linux, +macOS, and Windows archives with checksums, build and scan the GHCR image, build and smoke the Incus image, publish the npm launcher, and publish Labby's `server.json` metadata to the official MCP Registry. diff --git a/packages/labby-mcp/lib/platform.js b/packages/labby-mcp/lib/platform.js index aeb676d64..b6464ce23 100644 --- a/packages/labby-mcp/lib/platform.js +++ b/packages/labby-mcp/lib/platform.js @@ -23,7 +23,17 @@ function targetFor(platform = process.platform, arch = process.arch) { }; } - throw new Error(`Unsupported platform ${platform}/${arch}. Supported targets: linux/x64, win32/x64.`); + if (platform === "darwin" && arch === "arm64") { + return { + asset: "lab-aarch64-apple-darwin.tar.gz", + binary: "labby", + archiveType: "tar.gz", + }; + } + + throw new Error( + `Unsupported platform ${platform}/${arch}. Supported targets: linux/x64, darwin/arm64, win32/x64.`, + ); } function releaseVersion(env = process.env) { diff --git a/packages/labby-mcp/scripts/check-package.js b/packages/labby-mcp/scripts/check-package.js index b101ca647..27bb5232c 100644 --- a/packages/labby-mcp/scripts/check-package.js +++ b/packages/labby-mcp/scripts/check-package.js @@ -318,7 +318,7 @@ function supportedTargets(platform) { const tuples = [ ["linux", "x64"], ["win32", "x64"], - ["darwin", "x64"], + ["darwin", "arm64"], ["linux", "ppc64"], ]; const targets = []; diff --git a/packages/labby-mcp/test/platform.test.js b/packages/labby-mcp/test/platform.test.js index 86088cee7..71e607027 100644 --- a/packages/labby-mcp/test/platform.test.js +++ b/packages/labby-mcp/test/platform.test.js @@ -20,6 +20,11 @@ test("maps supported platforms to release assets", () => { binary: "labby.exe", archiveType: "zip", }); + assert.deepEqual(targetFor("darwin", "arm64"), { + asset: "lab-aarch64-apple-darwin.tar.gz", + binary: "labby", + archiveType: "tar.gz", + }); }); test("rejects unsupported platforms", () => { diff --git a/scripts/install.sh b/scripts/install.sh index 25fb92403..ca7b0b825 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -53,7 +53,13 @@ target_triple() { *) fail "unsupported platform ${os}/${arch}; supported: Linux/x86_64" ;; esac ;; - *) fail "unsupported platform ${os}/${arch}; supported: Linux/x86_64" ;; + Darwin) + case "$arch" in + arm64) echo "aarch64-apple-darwin" ;; + *) fail "unsupported platform ${os}/${arch}; supported: macOS/arm64" ;; + esac + ;; + *) fail "unsupported platform ${os}/${arch}; supported: Linux/x86_64 and macOS/arm64" ;; esac }