# Vectis > Local virtual machines for GitHub Actions, controlled by people and agents. # Vectis Source: https://vectis.kerd.dev/docs Vectis runs every GitHub Actions job in a fresh virtual machine on your Apple Silicon Mac and deletes it when the job ends. You prepare an Ubuntu, macOS or Windows guest once; each job gets a clean copy. Three parts work together: - **The Vectis app and its background service** run on your Mac. The service owns the VMs, the disks and the local credentials. The app, the `vectis` CLI and the `vectis-mcp` server for agents are all clients of the same service. - **Your account at [vectis.kerd.dev/connect](https://vectis.kerd.dev/connect)** links your Macs, your GitHub accounts and your repositories. It also relays commands when you control one Mac from another. - **The Vectis GitHub App** lets Vectis see queued jobs for the repositories you connect and register a single-use runner for each one. Build files and VM disks never leave your Mac. The only cloud traffic is coordination: which jobs are waiting, which Mac takes them, and what happened. ## Start here 1. [Install Vectis](/docs/get-started/install) and check the requirements. 2. Follow the [quickstart](/docs/get-started/quickstart) from an empty Mac to your first job. 3. Point your workflows at Vectis with the right [runs-on label](/docs/guides/repositories#choose-the-runs-on-label). ## For agents Agents use the same tools as you. See [Agents](/docs/guides/agents) to install the skill and connect the MCP server. Append `.md` to any page URL to read its Markdown source; [`llms.txt`](/docs/llms.txt) indexes every page and [`llms-full.txt`](/docs/llms-full.txt) contains all of them. ## Run it yourself Vectis is open source. [Self-hosting](/docs/self-hosting/cloud) explains how to build it from source and run your own backend, website and GitHub App. --- # Install Vectis Source: https://vectis.kerd.dev/docs/get-started/install ## Requirements - A Mac with Apple Silicon, running macOS 15 or later. Intel Macs are not supported. A macOS 27 guest needs macOS 27 on the Mac. - Free disk space for the guests you prepare, on the internal disk or an external drive: | Guest | Free space to prepare | Notes | | ------------------------- | --------------------------------------------- | ---------------------------------------------------------- | | Ubuntu 24.04 | 8 GiB or more | Grows as software is installed | | macOS 27 | 70 GiB, or 40 GiB with your own restore image | Apple's restore image is downloaded unless you provide one | | Windows 11 (experimental) | 30 GiB or more | Needs your own installation media and QEMU | - Every running VM also needs space for its copy of the disk. Copies share unchanged blocks with the image, so they start small. - A VM may use at most 75% of the Mac's memory, so macOS always keeps room for itself. ## Install the app 1. Download [Vectis for macOS](https://github.com/kerddotdev/vectis/releases/latest/download/Vectis-arm64.dmg). 2. Open the disk image and drag **Vectis** to **Applications**. Open it from there, not from the disk image. 3. Choose **Install and start service** on the first screen. This registers the Vectis background service for your user, so it runs after you close the window and after you log in again. macOS may ask you to allow Vectis in **System Settings > General > Login Items & Extensions**. If you keep images or VMs on an external drive, macOS also asks whether Vectis may access that volume; allow it, or VMs on that drive cannot start. Vectis keeps its state in `~/.vectis`: the database, the service log and the local connection details. VM images and disks live in the folders you choose for each environment. ## Command line tools The app contains the `vectis` CLI and the `vectis-mcp` server for agents. In the app, open **Diagnostics** and choose **Install** under **Command line tools**. This places two small launchers in `~/.local/bin` that run the tools inside the app, so they always match the installed version. If `~/.local/bin` is not on your PATH, the app tells you. Add this line to `~/.zshrc`, then open a new terminal: ```sh export PATH="$HOME/.local/bin:$PATH" ``` Check the installation: ```sh vectis --version vectis status --json ``` ## Updates The app checks for new releases in the background and downloads them. When an update is ready, **Restart to update** appears in the sidebar. Vectis installs it only when no VM or operation is running: it stops the idle service, installs the update and starts the service again. While jobs are running, the sidebar says the update will install when they finish. The CLI and MCP server update with the app. ## Uninstall 1. In the app, open **Overview**, choose **Stop service if idle**, then **Remove login service** from the same menu. Or run `vectis service stop --if-idle` and `vectis service uninstall`. 2. Delete Vectis from Applications, and `~/.local/bin/vectis` and `~/.local/bin/vectis-mcp` if you installed them. 3. To remove all data, delete `~/.vectis` and the image and VM folders you chose. The Mac stays listed under **Macs** at [vectis.kerd.dev/connect](https://vectis.kerd.dev/connect) as offline and receives no more jobs. Remove the login service before deleting the app; otherwise macOS keeps trying to start a service that no longer exists. --- # Quickstart Source: https://vectis.kerd.dev/docs/get-started/quickstart This walkthrough uses an Ubuntu guest, the fastest to prepare. Each step shows the app first and the CLI equivalent after it. You need the app [installed](/docs/get-started/install) with its service running, and a GitHub repository where you can change workflows. ## 1. Prepare an Ubuntu environment In the app, open **Environments**, choose **Add environment**, select **Ubuntu 24.04** and pick two folders: one for the base image and one for the disposable VMs. Keep the suggested CPU, memory and disk, or adjust them. The ID you choose, for example `ubuntu`, becomes part of the runner label. ```sh vectis environment prepare-linux ubuntu \ --image-directory ~/VMs/images --storage-path ~/VMs/clones --wait --timeout 3600000 ``` Vectis downloads the official Ubuntu cloud image, checks its SHA-256 and installs Git, Docker and the GitHub runner's dependencies. This takes a few minutes. Follow it in **Overview**. Details: [Prepare Ubuntu](/docs/guides/linux-setup). ## 2. Connect this Mac to your account In **Connections**, choose **Connect in browser** under **This Mac**. Your browser opens [vectis.kerd.dev/connect](https://vectis.kerd.dev/connect). Sign in, check that the verification code matches the one in the app, and approve. Back in the app, choose **Finish connecting**. ```sh vectis cloud pair --json # open the link it prints, compare the code, approve vectis cloud finish --json ``` Approve only requests you started yourself, and never share the link. ## 3. Link GitHub and install the App On the connect page, open **Overview** and follow the steps: 1. **Link GitHub** proves which GitHub account is yours. 2. **Install App** installs the Vectis GitHub App on the account or organization that owns your repository. Choose the repositories it may access. 3. **Link again to refresh** so Vectis sees the new installation. The CLI prints the same page with `vectis github connect`. ## 4. Connect the repository In the app, open **Repositories** and choose **Connect a repository**. Pick your GitHub account, the repository and the `ubuntu` environment. You can also do this on the connect page under **Repositories**, where you also choose which Mac runs the jobs. ```sh vectis github accounts --json vectis repository connect my-repo --account --environment ubuntu --wait ``` For a repository owned by an organization, add `--owner `. Public repositories have extra requirements; see [Repositories and labels](/docs/guides/repositories#public-repositories). ## 5. Point a workflow at Vectis Set `runs-on` to the label shown next to the connection, `vectis-`: ```yaml jobs: test: runs-on: vectis-ubuntu steps: - uses: actions/checkout@v5 - run: echo "Hello from a disposable VM" ``` ## 6. Turn on automatic runners In **Repositories**, switch **Automatic** on for the connection. ```sh vectis repository enable-auto --wait ``` Push the workflow. Your Mac notices the queued job within a minute, boots a fresh VM, registers a single-use runner, runs the job and deletes the VM. Watch the operation in **Overview** and the job under the repository in **Repositories**, or on GitHub. If the job keeps waiting, work through [Troubleshooting](/docs/guides/troubleshooting). ## Next - Add a [macOS guest](/docs/guides/macos-setup) for Xcode builds. - Move existing workflows over with [workflow migration](/docs/guides/migrations). - Let an agent operate Vectis with the [skill and MCP server](/docs/guides/agents). --- # Your account Source: https://vectis.kerd.dev/docs/guides/account [vectis.kerd.dev/connect](https://vectis.kerd.dev/connect) is your Vectis account. You sign in there to approve Macs and remote controllers, link GitHub and manage repository connections. It never runs jobs itself. ## Overview A checklist from an empty account to a working setup: connect a Mac, link GitHub, install the App, connect a repository. Each step turns done when Vectis observes it. ## Macs Every Mac connected to your account, whether it is online (seen in the last 90 seconds), paused or offline, and the environments it reports with their state. Macs connect from the app (**Connections > Connect in browser**) or with `vectis cloud pair`; you approve the request on this page after comparing the verification code. **Remove this Mac** deletes it from your account together with its repository connections, runner records and operation history. The Mac keeps its environments, VMs and local data. It stops receiving jobs and remote commands immediately and reports "Removed from account"; run `vectis cloud disconnect` on it, or use **Disconnect** in the app's Connections view, to clear the saved connection. After that it can be connected again like a new Mac. ## GitHub The GitHub accounts you linked and the Vectis App installations each one can use. Linking proves which GitHub account is yours; it does not start jobs or change workflows. **Install App** opens GitHub's installation page, where you choose the account or organization. One linked account can reach several installations, so repeat this for every organization whose repositories you want to run. After installing the App or changing its repository access on GitHub, use **Link or refresh account** so Vectis sees the change. **Unlink this account** removes the link and the repository connections that depend on it. The Vectis GitHub App stays installed until you remove it in your GitHub settings. ## Repositories All repository connections across your Macs, grouped by repository, with each connection's `runs-on` label. **Connect a repository** creates a connection on any of your Macs; **Disable connection** removes one. Automatic runners are turned on per connection in the app or the CLI. ## Remote access The apps, CLIs and agents you allowed to control your Macs. Access lasts up to 90 days. **Revoke** ends it immediately; that client has to sign in again. ## Account Your sign-in, and **Delete account**. Deleting removes every Mac, repository connection, linked GitHub account, remote access grant and operation record, then deletes the sign-in itself. It cannot be undone, and jobs already queued on GitHub stay there. Your Macs keep working locally; run `vectis cloud disconnect` on each one afterwards. ## Approvals Pairing and sign-in requests open this page with a verification code. Approve only requests you started yourself and whose code matches, and never share the link. Requests expire after ten minutes. --- # Agents Source: https://vectis.kerd.dev/docs/guides/agents Agents use the same service and the same commands as you: the `vectis` CLI with JSON output, or the `vectis-mcp` server. The Vectis skill teaches them the workflow and the rules, such as never approving a pairing request on your behalf. First [install the command line tools](/docs/get-started/install#command-line-tools) from the app, so `vectis` and `vectis-mcp` are on your PATH. ## Claude Code Install the plugin, which contains the skill and registers the MCP server: ```text /plugin marketplace add kerddotdev/vectis /plugin install vectis@vectis ``` Or add only the MCP server: ```sh claude mcp add vectis -- vectis-mcp ``` ## Codex Add the server to `~/.codex/config.toml`: ```toml [mcp_servers.vectis] command = "vectis-mcp" ``` Copy [`skills/vectis`](https://github.com/kerddotdev/vectis/tree/main/skills/vectis) into your Codex skills directory to add the skill. ## Cursor and other MCP clients Any client that starts stdio MCP servers can run `vectis-mcp`. For Cursor, add to `~/.cursor/mcp.json`: ```json { "mcpServers": { "vectis": { "command": "vectis-mcp" } } } ``` The skill is a plain [`SKILL.md`](https://github.com/kerddotdev/vectis/blob/main/skills/vectis/SKILL.md) that works with any agent that reads skills. ## MCP tools | Tool | Use | | -------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | `vectis_capabilities` | Supported commands and queries, with the command schema | | `vectis_status` | Machine, environments, VMs, recent operations and cloud connection | | `vectis_command` | Submit a command with an idempotency key | | `vectis_activities`, `vectis_activity` | What the machine was asked to do, and one intent with the operations that carried it | | `vectis_operation`, `vectis_wait` | Read one operation, or wait for it; a wait that times out returns the current state | | `vectis_logs` | The newest service log lines | | `vectis_repositories`, `vectis_jobs`, `vectis_github_accounts` | Connections with their labels, job results, linked accounts | | `vectis_storage`, `vectis_doctor` | Disk usage and runtime diagnostics | | `vectis_github_connect` | The page that links GitHub and installs the App | | `vectis_service`, `vectis_cloud` | Login service and pairing, on the Mac itself only | | `vectis_machines` | Your Macs, after `vectis login` | To let an agent control another Mac, sign in once with `vectis login` and start the server with `vectis-mcp --machine `. A remote session never falls back to the local Mac. `vectis_status` returns a compact summary: machine and cloud state, runner capacity, environments, non-stopped instances, counts within the status window, and the newest 20 operations and activities with shortened messages. It omits full operation results. Read `vectis_activities` for unfinished activities and the newest 100 terminal activities, `vectis_activity` for one retained activity and all its steps, or `vectis_operation` for a full result. These detail tools query individual records, locally and remotely, and remain available when a record falls outside the status window. Tool results are returned once in `structuredContent`, with an empty `content` array. ## Rules agents follow - Humans approve pairing, sign-in and GitHub linking in the browser. Agents hand over the link and wait. - An accepted operation is not a finished one. Agents report the final status, and follow `nextStep` for `action_required`. - Agents reuse an idempotency key only to retry the exact same command. ## Documentation for agents [`llms.txt`](/docs/llms.txt) indexes every page, [`llms-full.txt`](/docs/llms-full.txt) contains all of them, and appending `.md` to any page URL returns its Markdown. --- # The Vectis app Source: https://vectis.kerd.dev/docs/guides/app The app is a window onto the background service. Closing it leaves the service and its jobs running. Everything in the app is also available in the `vectis` CLI and to agents through `vectis-mcp`. | View | What it shows and does | CLI | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | | **Overview** | The service, recent VMs, what needs you and the latest activity. Pause or resume new VMs, stop or reconcile VMs. The menu stops the service when idle or removes the login service. | `status`, `pause`, `resume` | | **Activity** | Everything this Mac was asked to do, grouped by day or repository, with search and filters. Open one to see its steps, dismiss it, or stop the work it owns. | `activity`, `operation` | | **Environments** | Your guest images. Prepare Ubuntu, macOS or Windows, register an existing image, start a clean VM, and set default CPU, memory and VM folder. | `environment` | | **Repositories** | Your repositories, each with the environments it runs on, their `runs-on` label, the **Automatic** switch, **Start runner**, recent jobs, workflow migration and disconnecting. | `repository`, `runner`, `job`, `migration` | | **Connections** | Connect this Mac to your account, link GitHub, and sign in to control your other Macs. | `cloud`, `github`, `login` | | **Storage** | Disk usage of images and VMs, including blocks shared between copies. | `storage` | | **Diagnostics** | Host support, runtime components, the service log and the command line tools installer. | `doctor`, `logs` | | **Settings** | How many VMs this Mac runs at once, and whether it takes on new VMs. | `machine configure`, `pause`, `resume` | Keyboard: **Cmd+1** to **Cmd+7** switch views, **Cmd+,** opens Settings, **Cmd+B** toggles the sidebar. ## Activities and their steps An activity is one thing you or GitHub asked for: preparing an environment, running a job, connecting a repository. The operations underneath it are the steps that carried it out, and you see them as **Steps** when you open an activity. Preparing macOS, for example, is one activity whose steps are the installation, the setup console, the guest SSH enrollment and its verification. An activity's status is the status of the work Vectis did. For a job, the GitHub result is a separate badge: a runner can finish cleanly while the job it ran fails, and both facts are shown. ## What the app tells you Every view follows the service by itself: a change you make anywhere, and a change GitHub reports, appear without refreshing or switching views. A setting that applies immediately, such as the **Automatic** switch, is confirmed by a notification once the service has applied it. Work that takes longer, such as preparing an image or running a job, is confirmed as started and then continues in **Activity**. Notifications about something that worked disappear on their own; a failure, or anything waiting for you, stays until you dismiss it. While the app runs, native macOS notifications report new local activities that need you or fail, even when the window is hidden or minimized or another Mac is selected. They are suppressed while focused; clicking opens the local activity. Startup history is not replayed. Closing the last window stops notifications. The desktop-local **Notify me when Vectis needs me or work fails** preference defaults on; its Settings toggle will be added separately. ## Control another Mac After signing in under **Connections > Other Macs**, the machine switcher at the bottom of the sidebar selects which Mac the views control. File pickers work only for this Mac; for another Mac, type paths that exist on it. Service installation and pairing only work on the Mac itself. See [Remote control](/docs/guides/remote-control). ## Updates The app updates itself from GitHub Releases and restarts the service only when it is idle. See [Updates](/docs/get-started/install#updates). ## Machine resource settings **Settings** controls the concurrent runner limit (1 to 16, default 5). The shared VM memory budget is also configurable through `vectis machine configure --memory-budget-mib `, independently or alongside `--max-runners `. It defaults to 75 percent of host RAM and accepts 2048 MiB to total host RAM. Lowering it gates future starts without stopping current VMs. Environment CPU and memory values are per-VM ceilings. CPU overcommit is allowed; memory admission uses measured VM usage with startup floors, and warning or critical host memory pressure blocks new VMs. `vectis status --json` exposes `runnerCapacity.budget` for the effective budget, committed memory, host RAM and pressure, plus `pressureKnown` to distinguish unknown pressure. See [VM storage and resources](/docs/guides/storage#shared-memory-budget) for accounting and recommended ceilings. --- # Derive an environment Source: https://vectis.kerd.dev/docs/guides/environments `environment.provision` boots a writable clone of a ready environment, preinstalls the pinned GitHub Actions runner, runs an optional setup script once, shuts down the guest cleanly and registers a new environment. It preserves the parent's base image and runner label. Connect repositories to the new environment ID to use its tools. ```sh vectis environment provision ubuntu-project --from ubuntu-24-arm64 \ --image-directory /absolute/path/to/images/ubuntu-project \ --name "Ubuntu project tools" \ --setup-script /absolute/path/to/setup.sh \ --wait --timeout 3600000 --json ``` The image directory must be a new absolute path with an existing writable parent directory. Vectis refuses existing files and directories, including empty ones. The derived environment inherits CPU, memory, disposable VM storage, guest access and effective network settings. Provisioning needs an idle, unpaused machine without unresolved runners or guest setups, and uses the normal VM resource and free-memory checks. Without `--setup-script`, provisioning only preinstalls the runner and prepares the image for cloning. `--no-runner` skips preinstallation and does not advertise a baked runner in the new manifest; jobs use the verified download fallback. Scripts are limited to 64 KiB of UTF-8. The CLI reads the file and sends its contents as data; the service uploads it into a guest file. Bash runs it as the SSH user on Linux and macOS. Windows uses `powershell.exe` (Windows PowerShell 5.1), not PowerShell Core or Bash. A nonzero script exit fails the operation. Linux and macOS preinstall the runner in the SSH user's home without requiring sudo. On Linux, provisioning checks the runner's linked libraries and the system libraries required by the pinned runner before attempting dependency installation. Prepared Vectis images already contain these dependencies, so provisioning skips the installer and its package index refresh. If libraries are missing, provisioning attempts the installer only after `sudo -n true` succeeds; otherwise it fails with a diagnostic explaining the missing dependencies and sudo permission. Installer failures are fatal, and a successful installation must pass the library checks again. Linux provisioning checks `sudo -n true` before uploading setup scripts or downloading the runner, including with `--no-runner`. It needs passwordless sudo for identity cleanup, as fresh Vectis images provide. Older Linux images without it fail with `provision_requires_guest_sudo` and next step: "Prepare a new Linux environment with this Vectis version, then provision from it." Vectis never requests a guest password. Do not put credentials into setup scripts: remote submissions pass through the cloud command queue. ## Runner and image manifest New Linux and Windows preparations retain the pinned runner too. Linux and macOS use `$HOME/.vectis-runner/`, and Windows uses `C:\vectis\runner\`. Linux preparation downloads and extracts the runner in a `vectis` login environment, then runs the dependency installer as root. Downloads retry and verify SHA-256 before extraction. The guest account owns the runner directory so the disposable VM can configure and run it in place. Linux preparation reports script failures through a serial-console marker with the failing step and exit status. The host stops the guest and reports `action_required` with `guest_preparation_failed` instead of waiting for the overall one-hour limit. Failures and timeouts retain up to 4 KiB of redacted serial output in the operation result's `diagnostics`, available through `operation get --json`; timeouts use `preparation_timeout`. The private `preparation.log` retains a larger bounded log. Resume with `environment resume ` after fixing the reported cause. The optional `Environment.image` manifest records `parentId`, a `revision`, `runnerVersion` when installed, and `provisionedAt`. When `runnerVersion` matches Vectis's pinned version, jobs start that runner directly without copying or downloading it. Existing environments without that manifest, and manifests for other runner versions, retain the verified download path and its 600-second install timeout. A manifest is an installation record, not evidence of a successful GitHub job. Linux provisioning clears the temporary guest's machine ID, D-Bus machine ID, SSH host keys and stored instance ID before shutdown. The root boot unit replaces SSH host keys and the instance ID from the read-only `VECTISIDENTITY` disk before SSH starts, but does not reset `/etc/machine-id`. A provisioning boot can persist that machine ID, so the privileged cleanup is required to prevent sharing it among clones. macOS retains the existing per-bundle machine identifier, hardware model, auxiliary storage and pinned SSH identity, as ordinary bundle clones already do. Windows retains its existing guest identity behavior. Network enforcement is inherited: Linux and macOS NAT do not filter egress; Windows defaults to enforced internet-only access. ## Cancellation, cleanup and parents ```sh vectis operation get --json vectis operation cancel --json vectis environment remove ubuntu-project --json ``` Shutdown first uses the hypervisor: Apple Virtualization `requestStop()` for Linux and macOS, or QMP `system_powerdown` for Windows. Neither requires guest sudo. Vectis waits for the VM process to exit successfully before freezing the image, with a two-minute shutdown timeout. Guest shutdown commands are only a fallback when the hypervisor request is unavailable, such as with an older Apple helper; that fallback requires guest shutdown permission. Acknowledging a shutdown request is not evidence that the VM stopped. The operation reports cloning, booting, waiting for the guest, Linux sudo verification, provisioning, Linux identity cleanup, shutdown and freezing stages. Cancellation stops the owned VM and deletes the partial output. Script failure or a shutdown timeout never registers the image. A forced stop is used only for failure, cancellation or a shutdown timeout, and its disk is discarded. A failed attempt can be retried with a new operation key after cleanup. Guest command failures retain `exitCode` and a redacted `diagnostics` string in the operation result, available through `operation get --json`. Diagnostics contain the last 4 KiB of stderr followed by the last 4 KiB of stdout, within the 16 KiB diagnostic limit. Step markers such as `vectis-provision: download` identify the failing preinstallation or shutdown step. Connection failures retain the available SSH diagnostic instead of inventing a guest exit code. Diagnostics remain available when cleanup needs attention or is retried after a restart. Treat diagnostic output as potentially sensitive even after redaction. After a service crash, Vectis retains ownership records and requires verified VM exit before removing partial files. If cleanup is unconfirmed, the operation reports `action_required` and provisioning continues to reserve the host. Reconcile the interrupted instance and restart the service to retry cleanup. A completed registration is recovered without deleting its image. Windows derived images use QCOW2 overlays with an absolute parent backing path, plus cloned UEFI variables and TPM state. Keep the parent's files in place. All operating systems refuse `environment.remove` for a parent with registered derived environments; remove descendants first. Removing a definition preserves its disk, so any retained Windows overlays still require their backing files even after unregistering. ## CLI, MCP and remote control MCP's `vectis_command` accepts the same command: ```json { "type": "environment.provision", "id": "ubuntu-project", "from": "ubuntu-24-arm64", "imageDirectory": "/absolute/path/on/host/images/ubuntu-project", "setupScript": "set -eu\nprintf 'ready\\n' > \"$HOME/project-ready\"", "runner": true } ``` Remote provisioning follows the existing preparation commands: `--machine ` selects the host, `--image-directory` is a path on that host, and `--setup-script` is read on the calling CLI machine before submission. Cancellation uses the same remote operation contract. The existing cloud queue limits the entire serialized command to 64 KiB, including JSON escaping and field overhead; near-limit setup scripts must be submitted locally. Desktop recognizes provisioning activity and cancellation; the environment provisioning form is separate UI work. --- # Limits and boundaries Source: https://vectis.kerd.dev/docs/guides/limits These values describe the current release. Settings you can change are marked as such. ## Runners and capacity | Limit | Value | | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | Time a runner waits for and runs a job | 6 hours | | Time allowed for installing the runner in the guest | 10 minutes | | Idle runner drain when capacity is no longer needed | 5 minutes, including manual runners without a requested job | | GitHub reports a listening runner offline or missing | 2 consecutive minutes | | Continuous runner status read failures | 15 minutes before offline recovery | | Automatic interrupted-runner cleanup | 24 hours, retry delays doubling from 1 minute to 30 minutes | | Concurrent VMs | 5 by default, 1 to 16 with `vectis machine configure --max-runners` or **Concurrent VMs** in the app | | CPU ceiling per VM | At most the host's CPU cores; summed vCPUs may exceed host cores | | Shared VM memory admission budget | 75% of host RAM by default; `machine configure --memory-budget-mib` accepts 2048 MiB to total host RAM | | Startup memory estimate | Linux 2048 MiB; macOS and Windows 4096 MiB; capped by each VM's ceiling | | Host memory pressure | Warning or critical blocks new starts; unknown is reported and treated as normal | | Concurrent macOS VMs | 2 (an Apple limit) | An automatic runner also waits while an environment is being prepared, while the Mac is paused, or when unresolved instances leave insufficient capacity. A runner that has been accepted holds its slot and startup memory estimate before its VM starts. Each non-stopped VM commits the larger of its measured process RSS and startup estimate; failed measurements fall back to its configured ceiling. Probes are cached for five seconds. `runnerCapacity.budget` reports the effective memory budget, committed memory, host RAM, pressure and whether pressure is known. The budget gates admission; it does not reclaim memory or stop VMs that later grow beyond it. ## Environments The warm pool is opt-in: `environment configure --warm-pool ` accepts 0 to 4, default 0. Idle pool VMs consume host RAM and disk space under the same admission budget and pressure gate as other VMs. Pool filling leaves one runner slot and a startup memory estimate free, so the actual size can be smaller than requested, including zero at a one-runner limit. Owned warm VMs yield to jobs and count as reclaimable capacity, while their memory stays visible in `runnerCapacity.budget`. They contain no runner registration, JIT configuration or job credentials. Idle VMs are recycled after 30 minutes and drained on pause, disable or service shutdown. See [Runners and jobs](/docs/guides/runners#optional-warm-pool). | Environment | Default CPU | Default memory | Default disk | | ----------- | ----------- | -------------- | ------------ | | Ubuntu | 2 | 4096 MiB | 32 GiB | | macOS | 2 | 4096 MiB | 64 GiB | | Windows | 4 | 8192 MiB | 96 GiB | Override them with `--cpu`, `--memory-mib` and `--disk-gib`. Minimum disk sizes are 16 GiB for Ubuntu, 40 GiB for macOS and 64 GiB for Windows. A VM's configured memory ceiling can never exceed 75% of host memory. Ceilings remain fixed for a running VM, but unused memory is allocated lazily and summed ceilings may exceed host RAM. For a suitable host, consider Linux 8 vCPUs / 12288 MiB, Windows 8 / 16384 MiB, and macOS 6 / 12288 MiB; preparation defaults are unchanged. See [Storage and resources](/docs/guides/storage). ## Cloud and automation | Limit | Value | | -------------------------------------------------- | ------------------------------------------------- | | Connected Macs per account | 100 | | Active runner leases per Mac | 100 | | Lifetime of a runner's just-in-time configuration | 15 minutes | | Automatic runner failures per job | 3 | | Total automatic runner attempts per job | 10 | | Wait after an action-required or missing operation | 10 minutes | | Wait after a failed attempt | 60 seconds | | Unclaimed automatic runner request, Mac offline | Expires after 2 minutes | | Unclaimed automatic runner request, Mac online | Expires after 10 minutes | | Scan for jobs Vectis missed | At most every 5 minutes per repository connection | | Jobs listed per repository connection | The latest 100 | | Lines returned by one service log read | At most 1000 | | Runner diagnostics | At most 16 KiB | | Pairing requests | Expire after 10 minutes | | Controller grants | Expire after 90 days | ## History and retention On the Mac, finished activity history expires after 30 days. Beyond the newest 300 completions, entries older than 24 hours expire too. Unfinished work stays. See [Troubleshooting](/docs/guides/troubleshooting#an-operation-failed-or-needs-you). In the cloud: | Record | Kept | | ---------------------------------------- | ------------------------------- | | GitHub webhook deliveries | 30 days | | Jobs, finished or not | 90 days since the last update | | Operations | 90 days | | Runner leases | 30 days | | Controllers | 30 days after the grant expires | | Expired pairing and GitHub link requests | 1 day after they expire | | Workflow migration previews | 24 hours | Operations and runner leases that have not finished are not removed. GitHub ends every job long before 90 days, so a job record that has not changed in that time is removed even if Vectis never saw it finish. ## Hosts and guests - The host is an Apple Silicon Mac running macOS 15 or later. Intel Macs are not supported. - Guests are ARM64 only: Ubuntu 24.04, macOS 26 or 27, and Windows 11 ARM64. A macOS 27 guest needs a macOS 27 host. - Windows support is experimental. --- # Prepare a Linux environment Source: https://vectis.kerd.dev/docs/guides/linux-setup The guided Linux path creates an Ubuntu 24.04 ARM64 base image with a local `vectis` user, pinned SSH access, Git, Docker the pinned Actions runner and its dependencies. It runs through the same local service from desktop, CLI and MCP. It requires an Apple Silicon Mac and the configured Apple helper, which runs the VM through Apple Virtualization. The service converts the downloaded disk itself; `qemu-img` is not needed. Choose existing writable directories for base images and disposable VMs. They may be on an external drive. The image volume needs at least 8 GiB free; actual usage grows as software is installed. Virtual disk capacity is separate from allocated host storage. ```sh vectis environment prepare-linux ubuntu-24-arm64 \ --image-directory /absolute/path/to/images \ --storage-path /absolute/path/to/vms \ --cpu 2 --memory-mib 4096 --disk-gib 32 --json vectis operation wait --timeout 3600000 --json ``` Use **Environments > Prepare a guest image**, with **Ubuntu 24.04 ARM64** selected, in the desktop. MCP accepts `environment.prepare-linux` with the same fields. Preparation currently reserves the host for one setup at a time: stop existing VMs and resolve interrupted runners first. Normal VM admission is blocked during preparation; pausing, diagnostics and cancellation remain available. The service downloads a pinned official Ubuntu QCOW2 image, checks its exact size and SHA-256, converts it to a bootable raw disk, and performs guest configuration locally. The source revision is recorded beside the completed image. Installed package versions are available inside the guest at `/etc/vectis/toolchain-versions.txt`. No host home directory, Docker socket or GitHub App key is shared with the guest. Each disposable instance receives its own SSH host key through private, read-only setup media. Images prepared with a shared SSH host key must be prepared again before starting runners; Vectis reports `action_required` for those images. To keep the same environment ID, and with it the `vectis-` runner label and existing repository connections, remove the old environment and prepare the new image under that ID. Removing an environment keeps its image files on disk. ## Network access Apple Virtualization NAT does not enforce egress filtering. The guest can reach the internet, the host and the local network. Status reports `network: { egress: "open", enforcement: "not_enforced" }`, including for existing environments without an explicit setting. `vectis environment configure --egress open --json` is accepted. Requesting `internet_only` fails with `egress_unsupported`: "Apple Virtualization NAT cannot restrict guest network access yet; this environment can reach the host and local network." Windows environments support enforced internet-only egress. Inspect effective state with `vectis status --json`, `vectis environment list --json` or MCP `vectis_status`; MCP uses the same configuration command. ## What the image contains A prepared image is a runner host, not a copy of a GitHub-hosted runner. GitHub's Ubuntu ARM64 image is built by 63 provisioning steps and carries hundreds of preinstalled tools, which does not belong in a disposable VM on a personal Mac. What a workflow actually depends on is narrower, because `actions/setup-*` actions download toolchains at job time. That is the layer Vectis matches, and it takes the list from the same place GitHub does: the `apt` section of [the Ubuntu 24.04 ARM64 toolset](https://github.com/actions/runner-images/blob/main/images/ubuntu/toolsets/toolset-2404-arm64.json) in `actions/runner-images`. So a prepared guest has Git, Docker, OpenSSH and the official runner's dependencies, the archive and shell tools that actions invoke (`tar`, `gzip`, `xz-utils`, `zip`, `unzip`, `zstd`, `p7zip-full`, `jq`, `curl`, `wget`, `rsync`, `shellcheck`), a C and C++ toolchain with the headers native modules build against (`gcc`, `g++`, `make`, `pkg-config`, `autoconf`, `automake`, `libtool`, `libssl-dev`, `libsqlite3-dev`, `libicu-dev`), and the shared libraries prebuilt binaries link against, `libatomic1` among them. Two packages differ from GitHub's list: `p7zip-rar` is left out because it is non-free, and `netcat` and `upx` are installed under their real package names. The complete inventory of a prepared image is in the guest at `/etc/vectis/toolchain-versions.txt`. Preparation updates `ca-certificates` from the Ubuntu security archive and fails unless it is at least `20260816ubuntu0.24.04.1`. That release distrusts the `emSign ECC Root CA - G3` root after the October 2026 eMudhra intermediate key compromise, so `curl`, `git`, `apt` and Docker in the guest reject chains through those intermediates. An image prepared before this check keeps its older store. Derive an environment whose setup script runs `sudo -n apt-get update && sudo -n apt-get install -y ca-certificates`, or prepare a new one. Deliberately absent: language runtimes and their version managers, databases, browsers and drivers, cloud CLIs, Android and Java SDKs. A job that needs one installs it in a step, or runs in a container with `container:` in the workflow, which works because the guest runs Docker. Nothing a job installs survives it, since every VM starts from the same base image. To bake shared tools into a new base, [derive an environment](/docs/guides/environments) with `environment.provision`; the original stays immutable. Docker container jobs (`container:`) and service containers (`services:`) are supported on the Ubuntu environment and covered by the environment validation workflow. ## Run x86_64 Linux tools Enable Rosetta per Linux environment to run x86_64 Linux binaries and Docker images requested with `docker run --platform linux/amd64`. The Mac must have Rosetta installed, and the Linux image must have been prepared with Rosetta guest support. Older images need re-preparation; enabling the option alone does not update an image. Prepare a new Linux environment and reconnect its repositories if the existing image lacks support. ```sh # Run on the Mac if Rosetta is not installed: softwareupdate --install-rosetta vectis environment configure ubuntu-24-arm64 --x64 rosetta --json # Run inside a job: docker run --rm --platform linux/amd64 ubuntu:24.04 uname -m # Disable translation for future instances: vectis environment configure ubuntu-24-arm64 --x64 off --json ``` Vectis shares only [Apple's read-only Rosetta runtime](https://developer.apple.com/documentation/virtualization/running-intel-binaries-in-linux-vms) with an opted-in Linux guest. A conditional boot unit mounts it read-only at `/run/rosetta` and registers an x86_64 ELF handler with `binfmt_misc`. It registers after `systemd-binfmt` has finished, so the normal boot registration cannot clear the handler. The `CF` flags preserve binary credentials and keep the interpreter available in container mount namespaces. The mount attempt is the unit condition: a missing tag skips registration without failing the unit, including on Ubuntu kernels that do not expose a sysfs tag file. Instance startup verifies the guest handler before reporting success. Missing host support reports `rosetta_not_installed` or `rosetta_unsupported`; a missing or disabled guest handler reports `rosetta_guest_unprepared` with the next step "Prepare the Linux environment again". Translation does not provide x86_64 libraries for standalone dynamically linked binaries; the binary needs compatible libraries, for example inside an amd64 container. The runner and guest kernel stay ARM64. Jobs requesting `X64` labels still do not match, and `setup-*` actions still install ARM64 tools by default. Rosetta does not change label matching or workflow migration. The setting applies to future instances. Like other environment configuration changes, enabling or disabling it changes `environmentRevision`, invalidates previous environment evidence and prevents preparing runner leases from completing against the old revision. Those attempts need cleanup and a fresh lease. ## Cancel and resume ```sh vectis operation cancel --json vectis operation get --json vectis environment resume --json ``` A stopped or failed setup reports `action_required` and its durable `setupId`. Partial downloads are retained and verified on continuation. A cancelled or failed guest configuration restarts from the clean downloaded image. Previously completed stages do not require manually recreating keys or directories. After a service crash, the previous guest must have verifiable process-exit evidence before its disk can be rewritten. Vectis will not kill an unrelated process or assume that a stored PID still belongs to it. The private `preparation.log` remains in the operation's reported image directory for diagnosis. Completed setup registers the environment automatically. It does not connect a repository or prove a GitHub job has run. Continue with [Repositories and labels](/docs/guides/repositories), then [Runners and jobs](/docs/guides/runners). New preparations retain the pinned runner under `$HOME/.vectis-runner/` as the guest user and record it in `Environment.image`. Jobs use it directly when the recorded version matches. [Environment provisioning](/docs/guides/environments) requires passwordless sudo for Linux identity cleanup and checks it before any runner download or setup script upload. Older images without it fail with `provision_requires_guest_sudo`; prepare a new Linux environment with this Vectis version, then provision from it. Shutdown uses the hypervisor and does not require guest sudo. --- # Install a macOS guest Source: https://vectis.kerd.dev/docs/guides/macos-setup Vectis downloads macOS 27.2 (build 26B123) directly from Apple and verifies its pinned SHA-256 and exact size before installation. The selected revision does not change when Apple publishes a new release. You can instead supply an existing Apple macOS 26 or 27 IPSW. A macOS 27 guest needs a host running macOS 27 or later; the Apple virtualization helper checks host and restore-image compatibility. Guest SSH enrollment uses a generated local connection script and explicit guest verification. Xcode is not installed implicitly. Choose existing image and disposable VM directories. The image volume needs at least 70 GiB free for automatic download and installation, or 40 GiB with an existing IPSW. Stop existing VMs and resolve interrupted runner operations first. The installer uses a new bundle; it never replaces a registered base image. ```sh vectis environment install-macos macos-27-arm64 \ --image-directory /absolute/path/to/images \ --storage-path /absolute/path/to/vms \ --cpu 2 --memory-mib 4096 --disk-gib 64 --json vectis operation wait --timeout 3600000 --json ``` For existing media, add `--restore-path /absolute/path/to/restore.ipsw`. In desktop **Environments > Prepare a guest image**, select **macOS 27 ARM64**. Leave the optional restore path empty to download, or choose an existing image. CLI, desktop and MCP all use the same durable service operation. Closing the desktop does not cancel installation. A completed restore reports `action_required`, a `setupId` and the installed bundle path. This means the operating system is installed, not that a runner is ready. ```sh vectis environment open-macos-setup --json ``` This opens the guest console on the host Mac. Create a local `vectis` account without Apple ID and enable guest Remote Login. Keep FileVault disabled inside this CI guest so fresh clones can boot unattended. This does not change host encryption. Close the console or cancel its operation to stop the guest. Reopening the same setup continues from its existing disk. ## Connect and verify the guest Keep the setup console open with guest Remote Login enabled. Desktop offers **Connect guest SSH on host** beside the running setup operation. This opens a generated connection script in Terminal on the host. From the CLI: ```sh vectis environment connect-macos-guest --open-terminal --json ``` Without `--open-terminal`, the operation returns the generated `scriptPath` for a human to open locally. Non-interactive agents can prepare enrollment without launching a password prompt. Remote control opens Terminal on the host, not on the controller machine. The script uses a dedicated guest key and an isolated known-hosts file. Confirm the guest host-key fingerprint in Terminal, then enter only the guest `vectis` password there. To inspect the guest's fingerprint, run `ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub` inside the guest. The password is never sent to Vectis or the cloud. Do not disable host-key checking or supply a general-purpose host identity. After the script finishes, choose **Verify guest SSH**, or run: ```sh vectis environment verify-macos-guest --wait --json ``` Verification checks the pinned SSH connection, ARM64 architecture, clock synchronization, macOS 26 or 27, guest FileVault being off, and the guest's developer toolchain. Successful verification still returns `action_required`: shut down the guest from its Apple menu, then choose **Finish macOS setup**, or run: ```sh vectis environment finish-macos-setup --wait --json ``` Vectis registers the environment only after the same verified setup session has stopped. Reopening its console requires another verification before finishing. A registered environment still needs repository linking and a real Actions verification job. Each runner checks guest readiness again before GitHub registration. ## Network access Apple Virtualization NAT does not enforce egress filtering. The guest can reach the internet, the host and the local network. Status reports `network: { egress: "open", enforcement: "not_enforced" }`, including for existing environments without an explicit setting. `vectis environment configure --egress open --json` is accepted. Requesting `internet_only` fails with `egress_unsupported`: "Apple Virtualization NAT cannot restrict guest network access yet; this environment can reach the host and local network." Windows environments support enforced internet-only egress. Inspect effective state with `vectis status --json`, `vectis environment list --json` or MCP `vectis_status`; MCP uses the same configuration command. ## What the guest contains A macOS guest is whatever you install in its setup console. Vectis restores Apple's image, enrolls SSH and verifies the guest; it cannot install Xcode for you, because Apple does not permit redistributing it and it needs an Apple ID and tens of gigabytes. One thing is not optional. Without a developer toolchain the guest has no working `git`, so `actions/checkout` fails on its first step and almost no workflow gets further. Verification refuses such a guest and tells you what to install. While the setup console is open, either run `xcode-select --install` for Apple's Command Line Tools, which bring `git`, `clang`, `make` and the macOS SDK, or install the full Xcode and open it once to accept its license. Jobs that build with Xcode, sign, or need a specific Xcode version need the full install, not the Command Line Tools. Everything else follows the same rule as the other guests: `actions/setup-*` actions download toolchains at job time, and anything your jobs always need, such as Homebrew formulas or a specific Ruby, can be installed once into a [derived environment](/docs/guides/environments) with `environment.provision`, or installed in a workflow step. ## Guest TLS trust In October 2026 the private keys of eight eMudhra intermediate certificate authorities were found to be recoverable. Their chains end at the `emSign ECC Root CA - G3` root and, through Sectigo cross-signatures, at `AAA Certificate Services`. Browsers block these chains, but Apple's `curl` and `git` read `/etc/ssl/cert.pem` and check neither revocation nor Certificate Transparency. On macOS 27.2 both still accepted the compromised chains on 2026-10-21. Vectis handles this for runner jobs automatically. Before each runner starts, it writes a copy of `/etc/ssl/cert.pem` without the two roots into the runner's private directory and exports it as `SSL_CERT_FILE`, `CURL_CA_BUNDLE` and `GIT_SSL_CAINFO`, which jobs inherit. No sudo is needed and the guest trust store is untouched. If the source is unreadable or the filter cannot be verified, the runner does not start. Tools that ignore those variables and read their own bundle are unaffected by this filtering; tools that use Apple's system trust depend on Apple's own block. The manual step below stays optional, for interactive use inside the guest. Until Apple updates `/etc/ssl/cert.pem`, remove the two roots from it while the setup console is open, before you finish setup. This changes only the guest, never the host. The Mozilla bundle that `curl` publishes no longer contains either root. ```sh set -euo pipefail work=$(mktemp -d) awk -v dir="$work" '/-----BEGIN CERTIFICATE-----/ { n++; file = sprintf("%s/%04d.crt", dir, n) } file { print > file } /-----END CERTIFICATE-----/ { close(file); file = "" }' /etc/ssl/cert.pem : > "$work/cert.pem" for cert in "$work"/*.crt; do case "$(/usr/bin/openssl x509 -in "$cert" -noout -fingerprint -sha256 | cut -d= -f2 | tr -d ':')" in 86A1ECBA089C4A8D3BBE2734C612BA341D813E043CF9E8A862CD5C57A36BBE6B | D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4) /usr/bin/openssl x509 -in "$cert" -noout -subject ;; *) cat "$cert" >> "$work/cert.pem" ;; esac done sudo cp /etc/ssl/cert.pem /etc/ssl/cert.pem.before-emudhra sudo install -m 0644 "$work/cert.pem" /etc/ssl/cert.pem rm -rf "$work" ``` The loop prints the two removed subjects. Afterwards `curl https://dv-g3b-emudhra.we-have-a-problem.com/` fails with a certificate error, and `curl https://github.com/` still succeeds. Apps that use Apple's system trust instead of this file depend on Apple's own block. ## Interruption and recovery ```sh vectis operation cancel --json vectis environment resume-macos --json ``` Interrupted downloads retain their pinned source and partial bytes for the same setup. Completed download bytes are verified again before reuse. After a restore interruption, the service verifies the previous process has stopped. A completed restore is preserved. A failed restore retries into a new bundle and retains interrupted files for inspection; it does not overwrite uncertain disk state. Inspect the private `preparation.log` in the operation's image directory for errors. A setup console is a local interactive operation. Remote callers can request it, but must use the host's screen to complete guest OS steps. No VNC or guest password is exposed through the cloud. ## Discard an unused setup To permanently remove a stopped, unregistered setup, confirm its exact environment ID: ```sh vectis environment discard-macos --environment --json ``` Desktop offers the same confirmation beside the setup operation. This deletes the current attempt's owned image directory, including its guest disk and setup logs. It preserves downloaded or supplied source IPSWs, registered environments, and older interrupted attempt directories. Active or registered setups and directories without matching ownership metadata cannot be discarded. ## Provision an existing guest Use [environment provisioning](/docs/guides/environments) to create a new `base.bundle` from a ready macOS environment. The runner is installed without sudo under `$HOME/.vectis-runner/`, and optional setup scripts run as the SSH user with Bash. Provisioning does not install Xcode or change the parent bundle. Provisioning uses this bounded shutdown sequence instead of the virtual power button, which can open a confirmation dialog in a logged-in macOS session: 1. Over SSH as the guest user, run `osascript -e 'tell application "loginwindow" to «event aevtshut»'`, then wait up to 60 seconds for the VM to stop. Apple's SDK identifies `shut` as `kAEShutDown`; `rsdn` is `kAEShowShutdownDialog` and would request confirmation. The command does not invoke sudo. [Apple documents the loginwindow shutdown event](https://developer.apple.com/library/archive/qa/qa1134/_index.html), but on a cold-booted clone with no logged-in console session (root-owned `/dev/console`) the AppleEvent was observed to fail with -10810, and provisioning then uses the fallbacks below. Guest applications can also cancel shutdown. 2. If the VM is still running and `sudo -n true` succeeds, run `sudo -n /sbin/shutdown -h now` and wait up to another 60 seconds. SSH enrollment does not grant passwordless sudo, and provisioning never prompts for a password. 3. If the guest remains running, complete two separate `sync` commands over SSH, wait 5 seconds, then force-stop the owned provisioning VM through the Apple helper. Each SSH command and helper stop has a 30-second limit. A failed or timed-out sync prevents image registration; failed or cancelled provisioning removes its partial output after confirmed VM cleanup. Unconfirmed cleanup requires inspection. The final operation result reports `shutdown` as `loginwindow`, `sudo`, or `forced_stop_after_sync`. The last value explicitly means the image was frozen after a forced stop following two successful syncs, not after an observed clean guest shutdown. This fallback applies only to the derived provisioning image, never ordinary runner VMs or user data. Inspect it with `vectis operation get --json`. --- # Migrate repository workflows Source: https://vectis.kerd.dev/docs/guides/migrations ## Preview a single workflow To see how Vectis would rewrite one workflow file without connecting anything, map its labels yourself: ```sh vectis migration preview .github/workflows/test.yml --map ubuntu-24.04-arm=vectis-ubuntu --wait --json ``` The result contains `changed`, the proposed YAML as `source`, and `findings` for jobs it left alone. Only selected label scalars change; quoting, comments, whitespace and line endings remain intact. Scalars that cannot be safely replaced require manual review. Nothing is written. The optional `jobs` summary lists each job with `job`, `changed`, `reasons` for retained runner selections, and static `needs` dependencies. A matrix job can have `changed: true` while some selections stay unchanged. Dynamic or aliased dependencies omit `needs`; an empty array means no dependencies. Workflow-level rejections may omit the summary. Repository analysis includes the same summary in each workflow file. ## Migrate a connected repository Connect a personal or organization repository to a prepared ARM64 environment first. Repository migration currently uses the paired machine's verified GitHub account and App installation. Public repositories must require GitHub approval for all external contributors before publication. Organization repositories require current repository administrator access for the verified GitHub identity. Private repositories must disable fork pull request workflows or require approval for those workflows before publication. ```sh vectis repository list --json vectis migration analyze --wait --json ``` Analysis reads workflow files from the default branch at a pinned commit. Its operation result contains a `previewId`, the original and proposed YAML, per-job findings, and whether the environment has successful runner evidence. The cloud retains the preview for 24 hours. The app offers **Analyze workflow migration** in a connection's menu under **Repositories** and shows the result in Overview. MCP accepts the same `migration.analyze` command. Review the proposed changes before creating the PR: ```sh vectis migration publish --wait --json ``` Publication requires an observed successful GitHub job and completed runner cleanup on the same binding and environment revision. Image metadata or configuration changes invalidate earlier evidence and previews. This revision check detects changes to the prepared image; it is not a substitute for source image checksum verification. Complete another test job after replacing an image, then analyze again. The publisher checks the repository ID, current default branch and pinned base commit. It writes only changed workflow files on `vectis/migrate-workflows`, preserving the rest of the tree. It opens a regular PR and never merges it. The App needs Contents, Workflows and Pull requests write permissions for publication; analysis uses a separate read-only token. Repeated requests return the existing PR, including a closed one, instead of recreating rejected changes. An interrupted request can be retried with the same preview after inspecting the branch and PR. A conflicting migration branch is left untouched. A moved default branch requires a fresh analysis. Analysis maps `ubuntu-24.04-arm`, `windows-11-arm`, `macos-27`, `macos-26` and `macos-latest` to the matching environment. x64 labels such as `ubuntu-latest` are reported as findings, not rewritten. Static scalar selections and simple static matrices are supported. Dynamic expressions, expanded matrices, reusable workflows, aliased events and privileged event patterns require manual review. x64 labels never imply ARM64 compatibility. Check toolchain versions and action compatibility in the PR even when the runner architecture matches. Review findings can coexist with safe changes: unsupported jobs keep their original runners. An empty migration cannot be published. To withdraw a proposal, close its PR without merging. After merging, revert the migration commit through normal GitHub review to restore the previous workflow. Automated PR closure and revert creation are not exposed by Vectis yet. --- # Remote control Source: https://vectis.kerd.dev/docs/guides/remote-control The host service keeps ownership of VMs and jobs. Remote clients send commands through Convex; hosts connect outward and do not need a public port. A remote controller can manage machines owned by the same Vectis account. Connect each Mac to your account first, as in the [quickstart](/docs/get-started/quickstart#2-connect-this-mac-to-your-account). ## Sign in from the CLI With the [command line tools](/docs/get-started/install#command-line-tools) installed, run: ```sh vectis login --name "My CLI" --json ``` Open the returned private link yourself, sign in, compare the verification code, and approve remote control. Do not share the link. The command returns `action_required` until you finish the browser step. Then run: ```sh vectis login finish --json vectis machine list --json vectis status --machine --json vectis doctor --machine --json ``` Use the cloud machine `id` from `machine list`, not its `localId`. Add the same `--home` to every command when using a separate controller directory. This directory does not require a running local service. Controller credentials stay in Keychain, expire after 90 days, and can be revoked from the browser connection page or with `vectis logout`. ## Submit and observe commands Use `--machine` for each remote command. Omitting it selects the local service. An unavailable remote machine never causes a fallback to local execution. ```sh vectis pause --machine --key --json vectis operation get --machine --json vectis operation wait --machine --timeout 60000 --json vectis operation cancel --machine --json ``` An accepted command is durably queued, not completed. Hosts claim commands and report execution separately. Retry the same request key only with the same command. Cancellation before delivery prevents execution; cancellation after delivery requires the host to observe the request and finish cleanup. Already completed operations keep their actual outcome. Automatic runner requests that remain unclaimed are checked after two minutes and cancelled if their machine is offline; an online machine has at most ten minutes to claim. The queued job can then be requested by another eligible machine on the next demand pass, after a 60-second retry delay. Expiry counts toward the three-failure budget; successful runners serving other jobs do not. Automatic admission has a separate cap of ten total attempts per job. Action-required or deleted previous operations can retry after ten minutes; only action-required operations count as failures. Read-only requests require an online host. Their cloud responses are bounded and expire after 60 seconds. Completed command details are retrieved from the host; when it is offline, the cloud summary explicitly reports that the full result remains local. GitHub logs and artifacts retain normal GitHub behavior. File paths, image directories, CPU and memory limits refer to the selected host. Remote service installation, startup, and shutdown are not available: configure the background service on that host locally. ## Desktop In **Connections > Other Macs**, choose **Sign in with browser**, approve the request, then **Finish sign-in**. Select an online Mac from **Control machine** at the bottom of the sidebar. The same environment, repository, storage, diagnostic, and runner controls now address that host. Switching hosts clears forms and previous host data. Local file pickers are disabled while a remote host is selected; enter paths that exist on that host. **This Mac (local)** restores direct local control. Closing the desktop leaves host services and jobs running. ## MCP After `vectis login`, start the MCP server for a specific Mac: ```sh vectis-mcp --machine ``` `vectis_machines` lists your Macs from a local session. Start with `vectis_capabilities` to inspect the selected target and available commands. Remote sessions omit local service and machine-pairing tools. `vectis_command`, reads, and `vectis_wait` use the same remote client as the CLI and desktop. Cancelling a wait does not cancel its operation. --- # Repositories and labels Source: https://vectis.kerd.dev/docs/guides/repositories A connection ties one GitHub repository to one environment on one Mac. A repository can have several connections, for example Ubuntu on one Mac and macOS on another. ## Before you connect - The Mac is [connected to your account](/docs/get-started/quickstart#2-connect-this-mac-to-your-account). - Your GitHub account is linked, and the Vectis GitHub App is installed on the repository's owner with access to the repository. - The environment exists on that Mac. App and CLI connections require it to be ready; web connections can be created before setup finishes. ## Connect **App:** **Repositories > Connect a repository**, then pick the GitHub account, an optional organization, the repository and the environment. **Web:** **Repositories > Connect a repository** at [vectis.kerd.dev/connect](https://vectis.kerd.dev/connect), where you also pick the Mac. **CLI:** ```sh vectis github accounts --json vectis repository readiness / --environment --json vectis repository connect --account --environment [--owner ] --wait vectis repository list --json ``` Readiness returns ordered checks with a status, an optional error code, a next step and a symbolic fix target. `ready` means no check is blocked or unknown. A missing connection or disabled automatic runners needs attention but does not block connecting. Readiness never connects a repository, changes GitHub policy or enables runners. `repository list` shows each connection's ID (the binding ID used by other commands) and its `runsOn` label. Connecting the same repository and environment again reuses the connection. ## Capacity mode Connections default to `demand`, which starts capacity for individually admitted queued jobs. The preview `scale_set` mode lets this Mac long-poll one GitHub runner scale set session and create runners from GitHub's assigned-job level. Automatic runners must be enabled in either mode. ```sh vectis repository capacity-mode scale-set vectis repository enable-auto vectis repository list --json vectis status --json ``` `repository list` includes `capacityMode`; listener state and error codes appear in `status.scaleSets`. Use the reported label or scale set name, and check the workflow against the preview integration before relying on it. See [scale set capacity](/docs/guides/runners#scale-set-capacity-preview) for lifecycle and verification limits. Scale set preview requires actors `any` and approval `automatic`. Keep restrictive policies in demand mode; switching to scale sets refuses unsupported policies with `scale_set_policy_unsupported`. GitHub's external pull request approval remains a maintainer decision. Pausing, disabling automatic runners, disconnecting or changing mode stops new scale set intake and attempts to close its session without interrupting running jobs. ```sh vectis repository disable-auto vectis repository capacity-mode demand ``` ## Who can use this Mac Each connection can limit the numeric GitHub user IDs that may use its runner capacity, and require a manual start. Existing connections allow any actor and use automatic approval unless a policy is set. Automatic runner admission must still be enabled separately. ```sh gh api users/ --jq .id vectis repository access --actors --approval required vectis job list --json vectis runner run --job-id --key --wait ``` Use `--actors any` to allow all actors and `--approval automatic` to remove the manual approval requirement. Actor lists accept at most 100 positive integer user IDs and remove duplicates. Both the original run actor and the triggering actor must be allowed. Starting a specific job manually approves this Mac's capacity for that run ID and attempt; it cannot override the actor list or approve an external pull request on GitHub. Unknown actor IDs leave the job waiting. Job lists expose `access` as `approval_required`, `actor_not_allowed`, or `actor_unknown` when admission is blocked. Vectis checks before starting a VM and again through GitHub's public job-started hook inside Linux, macOS and Windows guests. This second check matters because GitHub can assign a different matching queued job to an ephemeral runner. Every assignment must match the repository ID. Required approvals also match the approved run ID and attempt. A rejected assignment emits an error annotation and stops the runner before any further workflow step runs. The job shows as failed on GitHub with that annotation. The host destroys the VM and fails the operation with `runner_assignment_rejected` and the policy reason. On rejection, the hook writes a marker file and forcibly terminates only its own Runner.Worker and Runner.Listener ancestors. If writing the marker or identifying or stopping those ancestors fails, the hook blocks instead of returning control to the Worker. The host checks the marker every 10 seconds over a separate guest connection and stops the VM through normal cleanup. This relies on GitHub's public job-started hook and was verified with synthetic process trees, not a real runner. Actor-restricted assignments fail closed when the event does not identify the triggering actor. In particular, reruns are refused because their original event sender cannot attest the new triggering actor's numeric ID. Bash guests need `jq` or `python3` for this check; Ubuntu images include both, while macOS setup must provide one. Windows uses PowerShell's JSON parser. Policy changes apply to new admission and registration; a listening runner retains the policy granted when it registered. ## Choose the runs-on label Each environment has one Vectis label: `vectis-`. The app, the web page and `repository list` show it next to every connection. ```yaml runs-on: vectis-ubuntu ``` A job reaches Vectis when its `runs-on` labels include the Vectis label and nothing Vectis does not offer. Besides the Vectis label, a job may list only these, which every Vectis runner also carries: - `self-hosted` - the guest's OS: `Linux`, `macOS` or `Windows` - `ARM64` ```yaml runs-on: [self-hosted, Linux, ARM64, vectis-ubuntu] # also works runs-on: [self-hosted, linux, x64, vectis-ubuntu] # never runs on Vectis: x64 runs-on: ubuntu-latest # GitHub-hosted, not Vectis ``` Guests are ARM64. Jobs that need x86-64 binaries or containers do not work unchanged; [workflow migration](/docs/guides/migrations) only moves jobs that can run on ARM64. ## Organization repositories Select your own linked GitHub account and give the organization separately (`--owner `, or the owner field in the app and on the web). Your GitHub account needs **admin** permission on the repository. Organization membership alone is not enough. Vectis checks this again whenever it registers a runner, reads jobs or opens a migration pull request. ## Public repositories Anyone can open a pull request against a public repository, and its workflows would run on your Mac. Vectis therefore requires this GitHub setting before it connects a public repository or registers a runner for it: **Settings > Actions > General > Approval for running fork pull request workflows from contributors: Require approval for all external contributors.** A maintainer then approves each outside contributor's run on GitHub after reviewing the change. Vectis never changes this setting and never approves a run. Forks do not inherit your runners. Review outside code the way you would for any self-hosted runner: approval is the protection, not a guarantee against hostile code. Private repositories must disable fork pull request workflows or enable **Require approval for fork pull request workflows** in the same settings page. Vectis checks this when connecting a repository and before registering each runner. ## Disconnect **App:** the connection's menu, **Disconnect repository**. **Web:** **Disable connection**. **CLI:** `vectis repository disconnect `. Disconnecting stops new runners for that connection. Running jobs finish normally. The GitHub App installation and your workflows stay as they are; jobs that still ask for the label wait until another connection provides it. ## Repository cache volumes Cache volumes are opt-in. A connection can retain tool downloads and package caches between jobs on the same machine and environment: ```sh vectis repository cache enable --size-gib 20 vectis repository cache disable vectis repository cache clear ``` The default size cap is 20 GiB; `--size-gib` accepts whole GiB from 1 to 1024. Omitting it preserves the binding's existing size, or uses 20 for a new setting. Disable affects future jobs and preserves the existing cache. To resize, disable, wait for running jobs to finish, clear, then enable with the new size. Clearing refuses active or unreconciled cache instances. Disconnecting a repository deletes its saved generations and prevents running forks from writing back. A disconnect made in the cloud is applied when the host receives the authoritative binding list, including after reconnecting. The host stores a sparse raw `cache.img` under `/cache//-/`. Without a custom storage path, environment storage is `/instances`. Numeric installation and repository IDs establish ownership; repository names do not. Environments are isolated to keep guest filesystems separate. Each VM receives an APFS clone of the current generation as its own writable **block device** in the instance directory. The host never mounts the guest filesystem or shares a host folder with it. APFS clone support is required; there is no full-copy fallback. The cap applies to each disk's logical capacity, not the total host storage: concurrent forks, one retained previous generation and changed copy-on-write blocks can use additional space. First use formats the cache inside the guest. Linux identifies the `VECTISCACHE` virtio device, checks its size and that it has no mounted filesystems, formats blank disks as ext4, then mounts at `/vectis-cache`. macOS excludes the root APFS physical store, requires a single other physical disk with the exact configured size, and initializes only a blank disk as APFS at `/Volumes/VECTISCACHE`. Unknown layouts and unexpected existing filesystems fail closed. Windows identifies the separate NVMe disk by serial `VECTISCACHE`, excludes boot and system disks, checks its size and initializes only RAW disks as NTFS. Windows assigns an available drive letter. Guest setup requires passwordless `sudo` on Linux/macOS or an elevated Windows runner account. Cache-enabled runners check `sudo -n true` on Linux/macOS or the effective administrator token on Windows immediately after guest readiness, before runner installation or GitHub registration. Older Linux images may lack passwordless sudo even though they can run jobs without a cache. A failed privilege check ends the operation as `failed` with `cache_volume_requires_guest_sudo`, discards the private fork and leaves the saved generation intact. Prepare or provision a new environment with this Vectis version, or disable the repository cache. Vectis remembers the observed privilege result for that environment image and guest identity across service restarts. Repository readiness reports `cache.guest_sudo` as attention for a failed probe while caching is enabled; an unprobed image reports unknown. New image manifests or a different image path or guest identity require a fresh probe. Known failures stop subsequent local requests before VM startup. The cloud demand scheduler does not retry this failure for the same GitHub job ID, including after the environment is repaired: start a runner manually for the queued job or rerun the GitHub job to create new demand. Before the runner listener starts, it exports these paths to jobs: | Variable | Directory inside the cache | | ---------------------- | -------------------------- | | `RUNNER_TOOL_CACHE` | `toolcache` | | `npm_config_cache` | `npm` | | `npm_config_store_dir` | `pnpm` | | `YARN_CACHE_FOLDER` | `yarn` | | `PIP_CACHE_DIR` | `pip` | | `GOMODCACHE` | `gomod` | | `GOCACHE` | `go-build` | Workflows and setup actions can override these variables. Cargo is deliberately unchanged: `CARGO_HOME` also contains executables and configuration. The runner itself and the checkout remain on the disposable boot disk. ### Write-back authority The cloud grants `cacheWrite: true` only for a known job whose workflow run is a `push`, `schedule`, or `workflow_dispatch`, targets the repository's current default branch, and has `head_repository.id` equal to the binding's repository ID. The demand runner's job ID identifies the stored workflow run ID before preparation. Vectis reads `/actions/runs/{run_id}` and the verified repository's `default_branch` with a repository-scoped Actions read token, separate from the runner administration token. The run's `head_branch` must match that default branch, its repository and head repository IDs must match the binding, and its attempt must match the known job attempt. Unreadable or missing metadata denies write-back. Pull requests, `pull_request_target`, forks, other branches and runners started without a specific job ID always discard their forks. Scale set runners have no known workflow run at prepare time and always receive `cacheWrite: false`, including when cache volumes are enabled. They can read the saved generation but discard their changes. The job-started hook fences writable grants to their run ID and attempt so another queued job cannot reuse that grant. The event allowlist is exact: | Workflow event | Write-back | | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | `push` | Allowed only on the current default branch with the same head repository | | `schedule` | Allowed only on the current default branch with the same head repository | | `workflow_dispatch` | Allowed only on the current default branch with the same head repository | | Every other event, including `pull_request`, `pull_request_target`, `workflow_run`, `repository_dispatch` and `merge_group` | Denied | Runner operation results include `cache.attached`, `cache.writeBack` (`granted` or `denied`), an optional bounded `cache.reason`, and `cache.promoted`. These fields are available in operation and activity reads through the CLI, MCP and remote clients. A granted write-back does not mean promotion succeeded. For example, `metadata_access_denied` identifies denied metadata access, `non_default_branch` identifies a branch mismatch, and `promotion_failed` identifies a host promotion error. `promoted: true` is recorded only after the fork replaces the saved generation; a later flush error can still require reconciliation. A writable fork becomes the new generation only after the listener finishes normally, the guest successfully unmounts or offlines the cache, and the VM process exits. The host flushes the fork, atomically promotes it, then deletes the instance directory. Runner failures, cancellation and guest connection loss before promotion leave the current generation intact. A cleanup error after the rename can report `promoted: true` while still requiring reconciliation. A failed workflow step can still populate the cache if the runner lifecycle and unmount complete normally. Promotions are serialized by the host and use atomic rename. Concurrent writers use **last-completed-promotion wins**; changes from parallel forks are not merged. One previous generation is retained, and a missing current image can recover from it. Crashed instances never replay a promotion: reconcile them through the normal instance and runner commands. A cache is shared job input, not a secret store. Any admitted job, including an approved pull request, can read the generation attached to its own fork. Keep credentials and sensitive artifacts out of cache directories. Cache write restrictions do not approve fork workflows or bypass GitHub's maintainer approval policy. These paths and scripts have synthetic test coverage; real Linux, macOS and Windows guest verification is still required before claiming native cache reuse. Repository cache jobs use a cold VM start so the private cache disk is attached before boot. They do not claim generic warm pool VMs; idle pool VMs yield when the job needs their capacity. Jobs without a cache volume can still use the warm pool. --- # Runners and jobs Source: https://vectis.kerd.dev/docs/guides/runners Every job runs on its own runner in its own VM. Vectis boots a copy of the environment or takes over an unused warm VM, registers a GitHub runner that accepts exactly one job, runs it, then deletes the VM and the registration. ## Optional warm pool Keep up to four pre-booted, SSH-ready VMs per environment to skip cloning, booting and waiting for SSH when a job arrives. The pool is opt-in and defaults to zero on Linux, macOS and Windows: ```sh vectis environment configure --warm-pool 1 --wait vectis environment configure --warm-pool 0 --wait ``` Warm VMs use real host RAM and disk space while idle. They follow the shared memory budget, normal-pressure gate and macOS two-VM limit. Vectis leaves a runner slot and another startup estimate free, and does not fill the pool while a runner is waiting for a VM or preparation is busy. The requested size is an upper bound, not a capacity guarantee. With a one-runner limit, no warm VM is kept. Idle warm VMs have no runner registration, JIT configuration or job credentials. Registration happens only after a runner atomically takes over the VM and checks SSH. Unhealthy VMs are stopped before falling back to a cold start; used job VMs never return to the pool. `status` and MCP snapshots distinguish idle pool instances with `role: "warm"` and background startup with `role: "warming"`, separately from the VM's lifecycle status. They are not running jobs. Reported runner capacity treats owned pool VMs as reclaimable while the budget still reports their RAM usage. Idle warm VMs are stopped and replaced after 30 minutes. A real job can reclaim warm VMs belonging to other environments when it needs memory or a macOS slot. Pausing, disabling the pool or shutting down the service drains idle VMs; changing environment defaults replaces them. After a crash, pool VMs require the same confirmed-exit reconciliation as other interrupted instances. ## Automatic runners Turn on **Automatic** for a connection in **Repositories**, or: ```sh vectis repository enable-auto vectis repository disable-auto ``` In the default `demand` capacity mode, with automatic runners on, the Mac looks for queued jobs that match the connection's label every heartbeat, about every 30 seconds, and scans GitHub for missed jobs at most every five minutes. It starts runners when it is connected and not paused, one per queued job, and a new job also starts one right away instead of waiting for the next heartbeat. A Mac runs up to five runners at once by default; change it in **Settings** or with `vectis machine configure --max-runners ` (1 to 16). CPU and memory settings on an environment are per-VM ceilings. CPU ceilings may add up to more than the host's cores; each VM must still fit the host's core count. Memory admission uses a shared budget, defaulting to 75 percent of host RAM, rather than adding up the ceilings. Set it with `vectis machine configure --memory-budget-mib ` (2048 MiB to total host RAM). Each non-stopped VM commits the larger of its measured process RSS and a startup floor: 2048 MiB for Linux, 4096 MiB for macOS or Windows, capped by that VM's memory ceiling. Measurements are cached for five seconds. An unavailable footprint measurement uses the configured ceiling; a fresh VM without a PID reserves its startup estimate. Accepted runners reserve the estimate before boot, conservatively using the largest ready estimate until their environment is known. Starting another VM must fit that estimate into the remaining budget, and host memory pressure must be normal. Warning or critical pressure returns `memory_pressure`; a full budget returns `memory_budget_exceeded`. Unknown pressure is treated as normal and reported with `pressureKnown: false`. Capacity is calculated separately for each ready environment. `vectis status --json` includes `runnerCapacity.environments` and `runnerCapacity.budget` with the effective budget, committed memory, host RAM and pressure. The overall available count is the maximum environment count, not their sum. macOS retains its shared two-VM limit. Interrupted VMs hold memory until verified stopped. An action-required runner whose VM is verified stopped releases its local slot while cleanup remains unresolved. Existing VMs continue running when pressure rises or the budget is lowered; the budget gates new starts rather than enforcing a hard runtime memory cap. Generous ceilings let a build grow when resources are available: for example Linux with 8 vCPUs / 12288 MiB, Windows with 8 / 16384 MiB, or macOS with 6 / 12288 MiB, when the host supports them. Unused guest memory is allocated lazily, so a ceiling is not an immediate reservation. Touched memory can remain allocated until the disposable VM exits. These are recommendations, not changes to preparation defaults. In demand mode, before booting a VM, and again before registering the runner, Vectis checks with GitHub that the job is still queued. GitHub may hand the runner a different job with the same labels. If the original job is still queued after a runner succeeded, Vectis can request another runner without consuming the failure budget. Automatic admission stops after three failures or ten total attempts per job. Failures include failed operations, requests that expire before claim, and action-required operations. Failed and expired requests wait 60 seconds before retrying; action-required operations wait ten minutes. A deleted previous operation can retry after ten minutes without inventing a failure. A person's cancellation stays final unless GitHub queues the job again after an approval or concurrency wait. Automatic runners are off until you turn them on, per connection. These CLI commands wait for the setting to be applied without needing `--wait`, and return a nonzero exit code if it fails or needs attention. ## Scale set capacity (preview) Each repository connection chooses a capacity mode independently. `demand` uses per-job admission and is the default. `scale_set` uses GitHub's runner scale set queue, with one host-owned session per binding. Enable the preview with: ```sh vectis repository capacity-mode scale-set vectis repository enable-auto vectis status --json ``` The host long-polls with a queue-scoped token. The cloud keeps the GitHub App key and opens, refreshes and closes sessions. The token never goes to a guest or appears in status. GitHub's `totalAssignedJobs` is the capacity level, not a count to add for each notification. The host subtracts this binding's accepted and running scale set requests and limits new requests to the environment's free local capacity. Request keys and message progress are persisted before acknowledgement so queue redelivery cannot repeat an accepted request. Scale set runners request JIT configuration for their session generation without a job ID. GitHub assigns the job after registration. They skip demand-mode per-job queued checks and drain after five idle minutes, after confirming they are not busy. Scale set reservations use the same per-environment slots as demand runners, including the shared memory budget and host pressure. Runners without cache volumes can take an idle warm pool VM; cache-enabled runners cold-start with their private cache disk attached. Scale set grants always have `cacheWrite: false` because the workflow run is unknown at prepare time; changes to their private cache forks are discarded. The same VM resource limits and guest isolation apply to Linux, macOS and Windows. Linux and macOS NAT do not enforce egress filtering; Windows defaults to enforced internet-only egress. Preview scale sets support only unrestricted actors with automatic approval. Actor allowlists and required manual capacity approval need demand mode; they report `scale_set_policy_unsupported` rather than weakening policy. GitHub's maintainer approval rules for external pull requests still apply. A conflicting session reports `scale_set_session_conflict`; Vectis does not take over another listener's session. Unsupported responses and authorization failures also leave the listener `action_required` with a code. `status.scaleSets` reports `connecting`, `listening`, `action_required` or `stopped` per binding, plus the last assigned level and reserved runners when known. MCP `vectis_status` includes the same listener states. A queue 401 triggers one serialized cloud token refresh and a retry; transient failures use abortable backoff and honor `Retry-After`. Pause, disable automatic runners, disconnect the repository or switch back to demand to stop polling and attempt to close the session. Existing jobs finish normally. A failed session close is bounded and best effort; GitHub may retain it until expiry. After resolving a listener error, disable and re-enable automatic runners to retry. ```sh vectis repository disable-auto vectis repository capacity-mode demand ``` These are setting commands and wait for confirmation without `--wait`. The equivalent MCP command is `vectis_command` with `{key, command: {type: "repository.capacity-mode", bindingId, mode: "scale_set"}}`; the protocol uses `scale_set`, while the CLI uses `scale-set`. Remote commands replay through the same local service path. ## Manual runners Start one runner yourself with **Start runner** on the connection, or: ```sh vectis runner run --key --json vectis runner run --job-id --json # only if that job is still queued ``` A manual runner without a job ID waits for work and drains after 5 idle minutes. The listener has a six-hour maximum lifetime, including job execution. An automatic runner request that the Mac does not pick up expires so another Mac connected to the same repository can take the job: after 2 minutes with result code `machine_offline` if the Mac went offline, or after 10 minutes with `claim_timeout` if it stayed online. The job is requested again on the next heartbeat. Every runner is drained after 5 minutes of inactivity if its requested job no longer needs queued capacity. Manual runners without a requested job drain after the same idle window. Vectis confirms the runner is not busy, stops the VM and releases the lease, reporting `cancelled` with result code `runner_idle`. Busy runners and runners whose jobs still need queued capacity keep listening. A locally listening runner that GitHub reports offline or missing for 2 consecutive minutes is stopped with retryable failure code `runner_offline`. Continuous status read failures for 15 minutes trigger the same recovery. Cleanup must be confirmed before the operation ends; unconfirmed cleanup stays `action_required`. ## Read phase timings Inspect a runner with `vectis operation get --json`, or use `vectis activity get --json` to include its child operations. The runner operation's `result.phases` retains phase timestamps as `stage` changes. CLI output is always JSON; `--json` makes it compact. MCP `vectis_operation` and `vectis_activity` return these details, locally and through remote inspection, including retained operations outside the status window. MCP `vectis_status` returns a compact summary without operation results or phase timestamps; use the detail tools to read timings. Each phase has a `name`, an ISO UTC `startedAt`, and an optional `endedAt`. Subtract the timestamps to get its duration: `00:00:00.000Z` to `00:00:12.300Z` is 12.3 seconds. A closed phase measures an attempt, including a failed or cancelled one; use the operation status to determine its outcome. A missing end means the phase is still running or was interrupted before its end was recorded. Older operations may have no phases. | Phase | Measured interval | | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `queued_locally` | Local operation acceptance to lifecycle task start, including local admission checks. This excludes waiting in GitHub or the cloud. | | `job_check` | One check that the requested job still needs capacity, before startup, before registration, or while idle. | | `vm_start` | The `environment.start` child, including validation, cloning and spawning, until it confirms the VM is running. | | `warm_takeover` | Claim an idle pool VM; followed by a `guest_ready` SSH health check. | | `vm_start_and_guest_ready` | Rosetta startup, including guest address discovery, pinned SSH readiness, clock and architecture checks, and Rosetta verification inside `environment.start`. | | `guest_ready` | Guest readiness and the pinned SSH connection. After `vm_start_and_guest_ready`, this is a readiness recheck; the initial wait is already included in startup. | | `runner_install` | Downloading, verifying and extracting the runner. | | `runner_preinstalled` | The environment manifest matches the pinned runner version, so installation is skipped. | | `registration` | Preparing the GitHub JIT registration and validating its grant. | | `waiting_for_job` | Registration end to the guest's job-started hook timestamp, or listener exit if no job is observed. | | `job` | The guest's job-started hook timestamp until the guest listener exits. | | `cleanup` | Stopping the VM and releasing the runner, including registration reconciliation attempted by this task. Later recovery retries are not included. | Runners record `runner_preinstalled` instead of `runner_install` when the environment manifest matches the pinned runner version. After policy verification, the guest job-started hook atomically writes epoch milliseconds to `vectis-actions-runner/_vectis/job-started`. The existing policy gate poll reads it every 10 seconds and once after listener exit, without adding guest connections. A late marker corrects an earlier busy observation, including for short jobs that finish between polls. Guest readiness verifies clock skew is within 60 seconds; the hook timestamp is clamped between registration end and observation time, capped at listener exit when already known. If the marker is missing, invalid or unreadable, job timing falls back to GitHub's busy-status poll, normally every 30 seconds. That fallback uses the host's observation time and can miss short jobs. Bash uses GNU `date` milliseconds or Perl `Time::HiRes` on macOS; a timestamp failure leaves the policy decision unchanged and uses the busy fallback. Windows uses PowerShell 5.1-compatible .NET timestamp and file APIs. The measured start is the hook boundary, and the end is listener exit, including runner teardown. Clock skew still affects guest timestamps; never infer job success or an exact GitHub queue duration from these timings. History is bounded to 16 entries. When idle checks repeat, the oldest completed `job_check` entries are removed to preserve startup and cleanup. Idle checks overlap `waiting_for_job` or `job`; do not sum overlapping phases as total elapsed time. ## Read job results A runner operation that succeeded means the VM ran and was cleaned up. It does not say whether the job passed. The job's result comes from GitHub: - **App:** open a connection in **Repositories** to see its recent jobs. - **CLI:** `vectis job list --json` returns the latest 100 jobs with their GitHub status and conclusion. Job states arrive through GitHub webhooks. Vectis checks for failed deliveries every two minutes and requests one automatic redelivery per webhook, usually recovering missed updates within a few minutes without a connected Mac. If one still seems missing or stale: ```sh vectis job scan --wait # find queued and running jobs Vectis missed vectis job refresh --wait # reread one job from GitHub ``` ## Rerun, cancel and dispatch Control a connected repository's GitHub Actions runs from the CLI or MCP `vectis_command`: ```sh vectis run rerun-failed --wait vectis run cancel --wait vectis workflow dispatch ci.yml --ref main --input target=staging --wait ``` `workflow` accepts a file name or numeric ID, `ref` is a branch or tag, and `--input key=value` can be repeated for up to 25 string inputs. Vectis limits the complete command JSON to 65,535 bytes, including keys and metadata. The workflow must enable `workflow_dispatch`. Like other intents, these commands return an operation immediately unless `--wait` is supplied. The same typed commands work through local and remote clients; the desktop recognizes their operations but has no run control buttons yet. The GitHub App needs **Actions: write**. Existing installations must approve the updated permissions in GitHub **Settings -> Applications**. A missing approval returns `github_app_permission_required` with instructions. After correcting a rejected request, submit a new operation key; replaying the old key returns its stored rejection. Public repositories require approval for every external contributor before rerun or dispatch; cancellation does not require that policy. Rerun requires a completed run with failed jobs. After GitHub accepts a rerun or cancellation, Vectis rereads the run with backoff for up to 5 seconds for rerun or 20 seconds for cancellation. Both requests finish as `succeeded` with `result.state: "accepted"`: - `result.cancellation: "observed"` means Vectis observed the run stopped (`completed` or `cancelled`). `"requested"` means GitHub accepted cancellation, but Vectis has not observed it stop. The message is "GitHub accepted the cancellation; the run is still stopping." - `result.rerun: "observed"` means Vectis observed a newer attempt. `"requested"` means GitHub accepted the rerun, but the newer attempt is not yet visible. A failed reread does not undo GitHub's acceptance. Request success does not mean the jobs passed. Dispatch reports the run ID when GitHub returns it. An empty 204 response means accepted with an unknown run ID. GitHub has no idempotency keys for these requests. Vectis saves a cloud receipt before submission and never repeats a POST for the same machine and operation key while that receipt is retained, including remote replay. The daily retention sweep removes receipts older than 30 days since their last update. Do not replay old keys after that window: their duplicate protection has expired. Only an ambiguous POST response (such as a network error, 5xx response or missing status) leaves rerun or cancellation `action_required` with `run_command_unconfirmed`. Check the run on GitHub before submitting a new key; never blindly retry. Cancelling the Vectis operation stops waiting and can leave the GitHub outcome unconfirmed. ## Read a job's log Read a failed job directly through Vectis, using its GitHub job ID and repository binding: ```sh vectis job logs --binding vectis job logs --binding --follow ``` The first command prints the retained log. `--follow` starts polling every 2 seconds, doubles the interval after empty chunks up to 30 seconds, and resets it to 2 seconds when text arrives. It stops when the job has completed and its available log has been read. Stop following with Ctrl-C. GitHub may only publish logs after the job finishes. Add `--machine ` for a remote host; `--json` prints one response per chunk. Agents can use MCP `vectis_job_logs` with `{bindingId, jobId, after?}`. Pass the returned `next` as `after` to read the next chunk. Both cursors are absolute, nonnegative integer byte offsets in GitHub's UTF-8 log. The response includes `text`, `next`, the observed job `status`, `complete` and `truncated`. `complete` is true only when the job is completed and the cursor reaches the end. `truncated` means earlier unread bytes fell outside the retained window and were skipped. Logs are fetched from GitHub on demand without storing log archives. Remote reads use temporary inspection responses that expire after 60 seconds. Vectis reads within the last 4 MiB, uses UTF-8 safe chunks of at most 32 KiB, and preserves GitHub's masking. Downloads request one chunk at a time with bounded HTTP Range requests when supported; otherwise they stream the file with a 64 MiB download limit and retain only its tail. GitHub access, expired logs or the download limit can prevent a read. ## Cancel Cancel a runner from its operation in **Overview**, or: ```sh vectis operation cancel vectis operation wait ``` Cancelling stops the VM, then removes the GitHub registration. The cancel request returns before cleanup finishes; the original operation reports the outcome. Pausing the machine only stops new VMs. ## When a runner is lost GitHub reports "The self-hosted runner lost communication with the server" when a runner stops answering mid-job. If Vectis loses its connection to the VM, or the runner process exits unsuccessfully, it collects evidence before it stops the VM, because stopping destroys it. The failed operation keeps it as `diagnostics`, shown in the activity's details in the app and in `vectis operation get --json`: - the runner's last output, or the SSH error that ended the connection; - the host's view of the VM network: its ARP entry and interfaces; - from the guest, if it still answers: uptime, memory, the kernel log tail (when readable) and the runner's own log tail. An unreachable guest is reported as such, with SSH's reason (timeout, no route, refused). The text is capped at 16 KiB, contains no runner credentials, and is not collected for cancelled or successful runners. On Windows guests only uptime and the runner log are read, and that path is untested. ## Recover after a crash If the Mac or the service stopped abruptly, an operation or VM can end up `action_required`. Interrupted runner operations report `runner_recovery_pending`. On startup and during periodic recovery, Vectis checks the owned VM's exit evidence. Once the instance is stopped or absent, it reconciles the cloud lease and releases the registration automatically. A VM that may still be running, or an unconfirmed cloud release, keeps the operation unresolved. If deleting an owned VM's files fails after its process exits, Vectis retains an exit receipt so reconciliation can retry the file cleanup. ```sh vectis instance reconcile --wait vectis runner reconcile --wait ``` The app offers the same actions as **Reconcile** in **Overview**. Until an interrupted VM is reconciled, the Mac starts no new VMs and offers no runner slots. Do not start new runners to work around an unresolved one. For interrupted runners, `runner_registration_uncertain` and `runner_cleanup_required`, recovery retries while connected to the cloud, with delays of 1, 2, 4, 8, 16 and then 30 minutes, capped at 30 minutes. The retry schedule survives service restarts. After 24 hours, the operation remains `action_required` with a message and a next step for manual reconciliation. A stopped runner can still be busy on GitHub, usually for up to 15 minutes until GitHub releases its job; `runner_busy` leaves cleanup unresolved until a later retry succeeds. A refused assignment retains its policy reason during cleanup. After manual or automatic reconciliation succeeds, the runner operation ends as `failed` with `runner_assignment_rejected` and the original refusal message. Offline runners retain `failed` with `runner_offline` after delayed cleanup. Other interrupted runners end as `cancelled` with `runner_interrupted`; this does not establish the GitHub job result. Requests interrupted before startup or registration began end as `failed` with `runner_interrupted`. Runner reconciliation also works when registration stopped before creating a cloud lease. After confirming VM cleanup, Vectis records the request as released in the cloud so a delayed registration cannot create a duplicate runner. If the cloud cannot confirm this, the operation stays unresolved. Registration failures confirmed to have no remaining cloud registration end as retryable `failed` with `runner_registration_failed`. A lost response is reconciled before retrying; unavailable reconciliation stays `action_required`. Approval-required results retain their existing manual next steps. --- # VM storage and resources Source: https://vectis.kerd.dev/docs/guides/storage Choose an existing writable directory for VM working disks. It can be on an external drive. Keep prepared base images wherever you prefer; `basePath` locates the source and `storagePath` chooses where disposable instances are created. Example `environment.json` for an already prepared Linux image: ```json { "id": "ubuntu", "name": "Ubuntu ARM64", "os": "linux", "basePath": "/Volumes/CI/Images/ubuntu-arm64.img", "storagePath": "/Volumes/CI/VMs", "cpu": 2, "memoryMiB": 4096, "state": "ready" } ``` Register it and start a VM: ```sh vectis environment register --file environment.json --wait --json vectis environment start ubuntu --wait --json ``` The original base image is preserved. The instance receives its own working disk and directory. Stopping an owned instance removes its disposable working directory, preserving the selected parent directory and base image. For Linux runner access, use an image from [Linux preparation](/docs/guides/linux-setup). Its registration includes `sshUser`, `sshKeyPath` and `sshHostKeyMode: "instance"`. Custom Linux images must install the SSH identity from Vectis instance setup media before starting SSH; a shared image host key is not accepted. ## Change defaults or one VM Change defaults for future instances: ```sh vectis environment configure ubuntu --cpu 4 --memory-mib 8192 --storage-path /Volumes/CI/VMs --wait --json ``` Override just one start without changing defaults: ```sh vectis environment start ubuntu --cpu 2 --memory-mib 4096 --wait --json ``` Running instances keep the resources and location they started with. Changing a path does not move existing disks. CPU and memory settings are per-VM ceilings, fixed until the VM stops. Vectis allows CPU overcommit: their sum may exceed host cores, while each VM must fit the host core count. Unused guest memory is allocated lazily. For a suitable host, generous ceilings such as Linux 8 vCPUs / 12288 MiB, Windows 8 / 16384 MiB, and macOS 6 / 12288 MiB let builds grow without reserving the full ceiling at admission. A single VM ceiling still cannot exceed 75 percent of host RAM. ## Shared memory budget `vectis machine configure --memory-budget-mib 36864` sets the shared admission budget, for example 36 GiB on a 48 GiB host. The allowed range is 2048 MiB to total host RAM; an unset budget defaults to 75 percent of host RAM. Combine it with `--max-runners ` or change either independently. These machine settings persist across service restarts and apply to local, MCP and remote commands. Admission counts every non-stopped VM at the larger of its measured process RSS and a startup floor: 2048 MiB for Linux, 4096 MiB for macOS or Windows, capped by its memory ceiling. Fresh VMs without a PID and accepted runners reserve that estimate before boot. An unavailable measurement falls back to the configured ceiling. Footprints and host pressure are cached for five seconds. A new VM starts only if its estimate fits the remaining budget and host memory pressure is normal. Warning or critical returns `memory_pressure`; insufficient budget returns `memory_budget_exceeded`, both with next steps. Unknown pressure is treated as normal and exposed as `pressureKnown: false`. Startup separately checks available host memory against the same estimate, rather than the full ceiling. Inspect `vectis status --json` for `runnerCapacity.budget` and per-environment slots. This budget gates new starts; running VMs can grow beyond it and are not stopped when it is lowered or pressure rises. Memory touched by a guest may remain allocated until the disposable VM exits. Free host memory or stop idle VMs before retrying a refused start. The two-macOS-VM limit remains unchanged. If a selected directory disappears, Vectis reports `storage_unavailable`. Reconnect the drive before retrying. It does not silently recreate a missing selected directory on the system disk. Storage is currently identified by path, so verify that the intended drive is mounted there. ## Understand disk usage ```sh vectis storage --json ``` The report separates base images from active and interrupted instances and includes a host-file breakdown when inspection succeeds. | Metric | Meaning | | ---------------------- | ------------------------------------------------------------------------- | | `virtualCapacityBytes` | The virtual disk capacity exposed to the guest | | `fileBytes` | The apparent size of image and support files on the host | | `allocatedBytes` | Filesystem-reported allocated blocks | | `entries` | Usage grouped by files or directories immediately inside the VM directory | | `guestBreakdown` | Availability of guest filesystem inspection | Sparse files can have a large capacity and use fewer host blocks. Copy-on-write clones can share blocks, so summing `allocatedBytes` does not measure exclusive physical consumption. Guest filesystem breakdown is currently unavailable: the report cannot yet tell you how much space is used by guest packages, caches, or build files inside a disk image. ## Background access to external storage A path that works from Terminal may still be inaccessible to the installed background runtime. Keep the volume mounted and check the runtime's access under **System Settings > Privacy & Security > Files and Folders**. Selecting a folder in the desktop does not by itself prove that the independent service has permission to read it. Vectis does not grant Full Disk Access or change system privacy settings automatically. Storage measurements run in a separate owned process with a deadline. A blocked or unavailable volume returns an unavailable result with next steps instead of leaving the measurement pending indefinitely. Concurrent requests share the current measurement, and stopping the service cancels it. This protects inspection; it does not bypass permission requirements for VM startup or image preparation. Before starting a VM, Vectis also checks image reads and storage-directory access in a separate process. If access cannot be confirmed within five seconds, startup returns `action_required` with `storage_access_required`. No VM or work directory is created by that failed preflight. Resolve the permission or mount issue, then retry with a new operation key. This check cannot guarantee that a volume will remain connected or accessible throughout a later job. --- # Troubleshooting Source: https://vectis.kerd.dev/docs/guides/troubleshooting ## A job stays queued Start with `vectis repository readiness --binding --json` (MCP `vectis_readiness`). Follow each blocked check's `code` and `nextStep`; an `unknown` check could not be verified. Disabled automatic runners appear as attention. Readiness checks prerequisites, so also check capacity and workflow labels below. 1. **Is the Mac available?** `vectis status --json` shows `machine.paused` and `cloud.state`. A paused Mac, a Mac whose cloud connection is not `connected`, or a Mac that is asleep takes no new jobs. Resume it with `vectis resume`. 2. **Is the Mac busy?** A Mac runs up to its **Concurrent VMs** setting at once (5 by default; change it with `vectis machine configure --max-runners `), as long as all VMs together fit within the host's CPU cores and 75% of its memory. It starts none while an environment is being prepared or an interrupted instance is unresolved. The job starts when a slot and resources are free. See [Limits and boundaries](/docs/guides/limits). 3. **Are automatic runners on?** Without them nobody starts a runner. Turn on **Automatic** for the connection, or start one with `vectis runner run`. 4. **Do the labels match?** The workflow's `runs-on` must contain the connection's `runsOn` label and nothing besides `self-hosted`, the OS label and `ARM64`. See [the label rules](/docs/guides/repositories#choose-the-runs-on-label). 5. **Is the environment ready?** `vectis status --json` lists environments with their `state`. 6. **Is the run waiting for approval?** Runs from outside contributors on public repositories wait until a maintainer approves them on GitHub. 7. **Can the GitHub App reach the repository?** The App must be installed on the owner with access to the repository. Link GitHub again on the connect page to refresh what Vectis sees. 8. **Did Vectis miss the job?** Run `vectis job scan --wait`. Jobs with more than 100 labels or a label longer than 100 characters are recorded but cannot match Vectis runners. Unknown GitHub job statuses are retained as waiting until a scan or refresh supplies a known status. ## An operation failed or needs you Every operation ends with a status, a message and, when you need to act, a `nextStep`: ```sh vectis operation get --json vectis logs --lines 200 --json ``` Displayed logs, stored operation details and retained runner and preparation diagnostics redact recognized credentials. For slow runners, inspect `result.phases` on the runner operation, either directly or inside `vectis activity get --json`. Compare `vm_start`, `guest_ready`, `runner_install` and `registration` to find startup costs; `cleanup` measures the stop and release attempt. Each duration is `endedAt - startedAt`. A missing end is still running or interrupted, and older records may omit phases. Job timing starts at the first GitHub busy observation, normally polled every 30 seconds, so short jobs can be missed. These local timings do not measure the full GitHub queue. See [Read phase timings](/docs/guides/runners#read-phase-timings) for the complete phase list, overlap and retention rules. The app shows the same in **Overview** and the service log in **Diagnostics**. `action_required` means completion is unconfirmed; follow its `nextStep`. Runner cleanup may still be retried automatically. GitHub reads, runner deletion and installation token creation retry transient failures up to four attempts within a 45-second budget per request. Retries respect GitHub rate limit headers. `github_unavailable` means GitHub remained unavailable; `github_rate_limited` means the rate limit could not be cleared within that budget. Wait and retry. Side-effecting POST requests are never automatically retried; an ambiguous run command keeps `run_command_unconfirmed`, and an ambiguous runner registration still requires reconciliation. An operation that stays in **Needs you** after you have read it, usually because the service restarted before it finished, can be closed with **Dismiss** in the app or `vectis operation cancel `. It ends as `cancelled` and nothing else changes. Two cases keep their own way out instead: an interrupted runner is reconciled once its VM is confirmed stopped, and an image preparation is resumed or discarded. After a restart, interrupted runners report `runner_recovery_pending`. Once the VM is verified stopped or absent, Vectis automatically reconciles the cloud lease and releases the runner. Recovery also covers `runner_cleanup_required` and `runner_registration_uncertain`. Connected retries use exponential backoff from 1 minute to 30 minutes for up to 24 hours, preserving the schedule across service restarts. GitHub can keep a stopped runner busy until it releases the job, usually within 15 minutes; `runner_busy` means cleanup must wait. An unconfirmed VM exit or cloud release keeps the operation `action_required`. After the retry window, use `vectis runner reconcile ` once the VM is confirmed stopped; the result includes this next step. If Vectis refused the assigned job, the cleanup result keeps the original reason in `cause`. Successful reconciliation ends that runner operation as `failed` with `runner_assignment_rejected` and the original refusal message. Offline runners end as retryable `failed` with `runner_offline`, including after delayed cleanup. Other interrupted runners end as `cancelled` with `runner_interrupted`; check GitHub separately for the job result. A request interrupted before startup began ends as `failed` with `runner_interrupted`. `vectis status --json` groups related operations into `activities`. A preparation keeps one activity across setup attempts; a runner activity includes its VM start and cleanup. Preparation status follows the latest attempt, while a runner follows its root operation. A successful discard cancels the preparation activity; the discard operation itself stays `succeeded`. Operation IDs and request keys stay unchanged. Status includes every unfinished operation and activity, including `action_required`, plus the newest 100 terminal operations and 100 terminal activities. It includes every non-stopped instance and the newest 20 stopped instances. These are display windows, not the complete retained history. Use `vectis operation get --json` or `vectis activity get --json` for an older retained record; waits poll only the requested operation. `vectis activity list` reads activity summaries directly. The desktop fetches an opened activity's full steps separately. Stopped instance records older than 24 hours are removed beyond the newest 20, using their stop time when available and creation time for older records. This removes only stopped records, not guest files, unfinished work or request-key receipts. Replaying a retained stop request still returns its original operation; an expired operation's key remains reserved. Runner activities keep two separate optional facts: `subject.requestedJob` is the job that requested capacity, and `subject.actualJob` is the job observed on that registered runner. GitHub can assign a different job. Each may include `name`, `workflowName` and `htmlUrl` when GitHub provides them; a missing actual job means its identity is still unknown. Job failure does not make a successful runner lifecycle fail, and job success does not hide a cleanup failure. Idle runners drain after 5 minutes when their requested job no longer needs capacity; manual runners without a requested job use the same window. GitHub reporting offline or missing for 2 consecutive minutes, or continuous status read failures for 15 minutes, stops a listening runner with `runner_offline`. Registration attempts with confirmed cleanup can fail retryably with `runner_registration_failed`; unresolved registration still requires reconciliation. Actual job observation continues after the runner operation finishes and across service restarts. The service watches up to 20 of the newest eligible leases for activities created within the last 48 hours, stopping when the actual job completes. Already stored observations remain in activity history. Use `vectis activity cancel --json` to request cancellation of every background task that activity still owns, then inspect its final status. It can also close an activity that only waits for a person, with the same preparation and runner recovery restrictions as `operation cancel`. The command works through remote control and MCP too. Finished activity history expires after 30 days. Beyond the newest 300 completions, entries older than 24 hours expire too; the entire recent 24-hour tail stays. Unfinished or actively owned work stays. Terminal settings and automatic scans expire after 24 hours. Resource records, prepared environments and disks are unaffected. Reusing a pruned operation's request key returns `operation_expired`; use a new key for new work. During an app update, an older service may omit `activities` and `revision`. Clients treat absent activities as an empty list and an absent revision as unknown change state. ## A job fails because a tool is missing in the guest A prepared environment is a runner host, not a copy of a GitHub-hosted runner. It provides what a workflow needs to install its own toolchain: git, Docker, archive tools, a C and C++ toolchain and the libraries prebuilt binaries link against. Language runtimes, databases, browsers and cloud CLIs are not there. Each guide lists what its guest contains: [Linux](/docs/guides/linux-setup#what-the-image-contains), [macOS](/docs/guides/macos-setup#what-the-guest-contains), [Windows](/docs/guides/windows-setup#what-the-guest-contains). Three ways out, in the order worth trying: 1. Use the matching `actions/setup-*` action. That is how hosted runners get their toolchains too. 2. Install it in a step. On Linux, `sudo apt-get install -y ` works without an `apt-get update` first. 3. Run the job in a container with `container:` in the workflow. The Linux guest runs Docker, so a job can bring a complete image of its own. Nothing a job installs survives it: every VM is a fresh clone of the base image. On Linux the exact contents are in the guest at `/etc/vectis/toolchain-versions.txt`, on Windows at `C:\ProgramData\Vectis\toolchain.json`. ## The Mac says it was removed from its account `vectis status --json` reports `cloud.state` as `removed`, and the app shows "Removed from account". Someone deleted this Mac on the account page, so its credential no longer works and Vectis stops retrying. Nothing local is lost. 1. Clear the saved connection with `vectis cloud disconnect`, or use **Disconnect** in the app's Connections view. 2. Connect again with `vectis cloud pair` or **Connect in browser** when you want the Mac back on an account. Repository connections are not restored automatically; connect the repositories again afterwards. ## A VM does not start - **Diagnostics** shows runtime configuration. `vectis doctor --json` adds typed `checks` for host support, executable runtime components and a writable service home, alongside the existing fields. Missing Windows-only components need attention when no Windows environment exists; otherwise they block. The app configures its own; from a source checkout, set them as described in [Build from source](/docs/self-hosting/build-from-source). - The CPU and memory of all running VMs must fit the Mac. A VM may use at most 75% of its memory. - VMs on an external drive need the drive connected and macOS permission for Vectis to access it. - An interrupted VM blocks new ones until it is reconciled; see [Recover after a crash](/docs/guides/runners#recover-after-a-crash). ## Reporting a problem Open an issue on [GitHub](https://github.com/kerddotdev/vectis/issues/new/choose) with `vectis --version`, `vectis doctor --json` and the relevant part of `vectis logs`. Remove anything private first. Report security problems privately as described in the [security policy](https://github.com/kerddotdev/vectis/blob/main/SECURITY.md). --- # Install a Windows guest Source: https://vectis.kerd.dev/docs/guides/windows-setup Windows support is **experimental**. It installs Windows 11 ARM64 from your own ISO with QEMU and Apple's Hypervisor framework, and it does not fall back to slow CPU emulation. The Vectis app does not include QEMU, the software TPM or UEFI firmware; you install them yourself as described below. ## Prerequisites Vectis needs four programs and two firmware files: - **QEMU 11.1.1 with both Vectis patches for TPM compatibility and network isolation.** Stock QEMU is not accepted. Build it by following [the QEMU build notes](https://github.com/kerddotdev/vectis/blob/main/native/qemu/README.md); `qemu-img` comes from the same build or from Homebrew (`brew install qemu`). - **swtpm**, the software TPM: `brew install swtpm`. - **ARM64 UEFI firmware with Secure Boot support** and a blank **raw** variable-store template that matches it. The QEMU build notes name a pinned source and explain how to convert a QCOW2 template to raw. Do not reuse another VM's variable store or TPM state. - **The VirtIO driver ISO** for ARM64 Windows, from the [virtio-win project](https://github.com/virtio-win/virtio-win-pkg-scripts). - **A Windows 11 ARM64 ISO** from Microsoft, in English. The service records runtime paths when it is registered. While no VM is running, register it again with the paths set: ```sh export VECTIS_QEMU=/absolute/path/to/qemu-system-aarch64 export VECTIS_QEMU_IMG=/absolute/path/to/qemu-img export VECTIS_SWTPM=/absolute/path/to/swtpm vectis service update ``` **Diagnostics** in the app shows whether the service found them. Choose existing directories for the base image and disposable VMs. Preparation requires an idle host, at least 30 GiB of free image storage, at least 2 CPU cores, 4096 MiB of guest memory and a virtual disk of at least 64 GiB. CPU and memory allocations must fit the host's limits. A thin virtual disk grows as Windows writes to it. Use installation media you obtained from trusted publishers. Vectis fingerprints these local files and rejects changed media on resume; a local fingerprint does not establish publisher authenticity. The current unattended recipe targets English installation media and the selected Windows image name, normally `Windows 11 Pro`. You must accept the applicable Windows license terms. Vectis does not provide a Windows license, activate Windows or bypass activation requirements. ## Start installation In the app, open **Environments > Add environment**, select **Windows 11** and provide the media paths and resource limits. The equivalent CLI command is: ```sh vectis environment install-windows windows-11-arm64 \ --iso-path /absolute/path/to/windows-arm64.iso \ --drivers-path /absolute/path/to/virtio-win.iso \ --firmware-path /absolute/path/to/uefi-code.fd \ --firmware-vars-path /absolute/path/to/blank-uefi-vars.fd \ --image-directory /absolute/path/to/images \ --storage-path /absolute/path/to/vms \ --cpu 4 --memory-mib 8192 --disk-gib 96 \ --image-name 'Windows 11 Pro' --accept-license --json vectis operation wait --timeout 3600000 --json ``` The service creates a dedicated disk, UEFI variables, TPM state and setup identity. The local administrator password is generated into macOS Keychain. Temporary answer files and setup media contain guest credentials and are private local files: do not share them or upload them in diagnostics. The recipe creates a local `vectis` account, disables automatic login after bootstrap, installs OpenSSH Server, restricts guest SSH to the QEMU host address and uses dedicated pinned keys. It does not share host directories or credentials with the guest. Windows setup and OpenSSH installation can require internet access. Windows environments default to `internet_only` egress, reported as `network: { egress: "internet_only", enforcement: "enforced" }` in status. Guests use IPv4 internet access. Host addresses, loopback, private networks and link-local destinations are blocked by the patched QEMU runtime. Virtual DNS, DHCP and the owned SSH forward on host loopback remain available; guest administrator changes cannot disable the host filter. You can deliberately select weaker networking for future instances: ```sh vectis environment configure windows-11-arm64 --egress open --json # Restore the default restriction for future instances: vectis environment configure windows-11-arm64 --egress internet_only --json ``` `open` disables the host egress filter and allows the guest to reach host and local network services. Status reports `network: { egress: "open", enforcement: "not_enforced" }`. It still requires the patched Vectis QEMU runtime and keeps IPv6 disabled. Installation always uses internet-only egress. Changing the setting affects future instances, invalidates previous environment evidence and leaves running instances unchanged. Inspect it with `vectis status --json`, `vectis environment list --json` or MCP `vectis_status`; MCP configuration uses the same `environment.configure` command. The service reports success only after checking guest ARM64 architecture, clock synchronization and the setup marker over SSH, then observing a completed guest shutdown. It removes temporary setup media after completion. A successful setup registers the base environment; connecting a repository and observing its first Actions job are separate steps. ## What the guest contains Fresh installation and `environment provision` install the same pinned, SHA-256-verified Windows toolchain. Provisioning runs under Windows PowerShell 5.1 and requires an elevated guest administrator: - [Git for Windows 2.55.0.5 ARM64](https://github.com/git-for-windows/git/releases/tag/v2.55.0.windows.5), including Bash at `C:\Program Files\Git\bin\bash.exe` and GNU tar at `C:\Program Files\Git\usr\bin\tar.exe`. Bash and Git are on the machine PATH. The installer includes Git LFS; Bash and GNU tar are part of its base payload. Optional desktop integration and automatic Git updates are omitted. Git is configured system-wide with `http.sslBackend schannel`, so it verifies certificates against the Windows certificate store and Microsoft's distrust list instead of the CA bundle that ships with Git. Revocation checking stays on. - [zstd 1.5.7](https://github.com/facebook/zstd/releases/tag/v1.5.7) at `C:\Program Files\zstd`, on the machine PATH. This release has no Windows ARM64 build, so Vectis uses the official x64 executable through Windows 11 ARM64's x64 emulation and verifies that it runs. - [PowerShell 7.6.6 ARM64](https://github.com/PowerShell/PowerShell/releases/tag/v7.6.6), installed by MSI at `C:\Program Files\PowerShell\7`, with `pwsh` on the machine PATH. Automatic Microsoft Update integration and PowerShell remoting are not enabled. Windows PowerShell 5.1 remains the provisioning shell. The [Actions cache toolkit prefers GNU tar at the standard Git installation path](https://github.com/actions/toolkit/blob/main/packages/cache/src/internal/tar.ts), and uses zstd when available. Windows' built-in bsdtar remains available, but is not the cache toolkit's preferred executable after provisioning. In a lead-reported measurement on the same Windows 11 ARM64 guest with a 679 MB pnpm store, GNU tar with zstd compressed the store in 6.5-6.8 seconds, compared with 11.7-14.0 seconds for bsdtar with gzip. These are local archive compression timings, not end-to-end Actions cache save or restore timings. Existing MinGit files stay in `C:\Program Files\MinGit` for callers using absolute paths. Only its exact machine PATH entries (`cmd` and `bin`, ignoring case and trailing slashes) are removed. The full Git's `bin`, `cmd` and `usr\bin` entries take priority, and other machine PATH entries are preserved. MinGit is no longer installed in fresh images. PATH checks accept multiple matches and verify only the first resolved Git, Bash, tar, zstd and pwsh executable. Both full Git `bin\git.exe` and `cmd\git.exe` are accepted; tar must resolve to the installed GNU tar. Downloads use basic parsing to avoid the [Windows PowerShell 5.1 web-content confirmation prompt](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-webrequest?view=powershell-5.1), with bounded retries and SHA-256 verification before installation or extraction. Repeating provisioning checks the pinned versions and required binaries before deciding whether to install them again. Temporary downloads are removed even on failure. Languages, build tools, SDKs and package managers still come from workflow steps or a custom provisioning script. Nothing installed during a job survives that job's disposable VM. ## Disposable guest performance settings Vectis configures a Microsoft Defender policy exclusion for the entire `C:\` drive inside the disposable Windows guest. It takes effect at the next boot, including with Tamper Protection enabled; it has no effect during the preparation boot that writes the policy. Realtime protection may remain enabled after reboot while excluding `C:\` from scanning. The VM boundary isolates the host from untrusted job code. Each job gets a fresh disposable VM, so reducing guest antivirus scanning is an acceptable performance tradeoff for this CI environment. Host protection stays enabled. Do not apply these settings to the host or a general-purpose Windows machine. AMSI can nondeterministically reject Defender-changing PowerShell cmdlets with `ScriptContainedMaliciousContent`. Even a single `Add-MpPreference -ExclusionPath 'C:\'` command that succeeded once was blocked later in the same guest. Separating cmdlets into individual processes is not reliable. Vectis therefore never uses `Add-MpPreference` or `Set-MpPreference` for configuration. Fresh installation and provisioning write only registry policies through individual `reg.exe` commands in `cmd.exe`, before the toolchain script. The fresh-install seed stores each command in a separate `.cmd` file. Both exclusion writes are required: ```cmd reg.exe add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender\Exclusions" /v Exclusions_Paths /t REG_DWORD /d 1 /f reg.exe add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender\Exclusions\Paths" /v "C:\\" /t REG_SZ /d 0 /f ``` The realtime, behavior, IOAV, script and on-access protection policy keys, cloud reporting and sample submission policies remain best-effort writes. [Tamper Protection can block or silently ignore attempts to disable protection](https://learn.microsoft.com/en-us/defender-endpoint/tamper-protection-overview). Modern Defender ignores `DisableAntiSpyware`. Successful policy writes do not prove realtime protection is off. The mandatory toolchain manifest records `Defender.Mode: policy-exclusion`, `Defender.PolicyExclusionPath: ["C:\\"]` and `Defender.AppliesAt: next-boot`. These fields describe configuration intent, not an observed effective exclusion. Preparation does not depend on `Get-MpPreference` or `Get-MpComputerStatus`: before reboot, a missing effective exclusion is expected. Read-only checks remain useful after boot. Windows Update is disabled through `NoAutoUpdate` policy and `wuauserv`; Windows Search indexing (`WSearch`) and SysMain are disabled and stopped. Vectis also attempts to disable and stop Update Orchestrator (`UsoSvc`) and Update Medic (`WaaSMedicSvc`). Windows can protect those two services: blocked changes produce a warning and their actual state is recorded rather than reported as disabled. HVCI / Memory Integrity's `Enabled` value is set to `0` when present; running HVCI is recorded as requiring a reboot, and needs a real guest check after reboot. The toolchain steps emit `vectis-provision: ` markers. Versions, Windows build, next-boot Defender policy configuration, service states, Update policy and HVCI state are merged into `C:\ProgramData\Vectis\toolchain.json`, preserving unrelated manifest fields. ### Verify the next boot Provisioning ends with a hypervisor shutdown, with a guest shutdown fallback, and confirms the VM has exited before registering the derived image. Fresh installation does not reboot inside the bootstrap script: `windows-installation` observes its prepared marker, verifies the runner, issues `shutdown.exe /s /t 3`, waits for the VM to exit, and only then writes `image.json` and finalizes the environment. Both paths therefore save a stopped image after writing the policy. Every job boots a new instance from that image, applying the exclusion at that boot. Tools and custom setup scripts running before this shutdown do not yet benefit from a newly written exclusion. Vectis removes the obsolete `Vectis-VerifySecurity` startup task and its script and receipts when provisioning existing images. It does not reapply Defender settings or verify them automatically at runner start. The manifest records preparation-time configuration, not proof of the current boot's state. After the lead boots a newly provisioned image or clone, check in the guest: ```powershell Get-MpComputerStatus | Select-Object RealTimeProtectionEnabled, IsTamperProtected (Get-MpPreference).ExclusionPath Get-Content C:\ProgramData\Vectis\toolchain.json -Raw Get-Service wuauserv, UsoSvc, WaaSMedicSvc, WSearch, SysMain Get-CimInstance -Namespace 'root\Microsoft\Windows\DeviceGuard' -ClassName Win32_DeviceGuard | Select-Object SecurityServicesRunning ``` After boot, `ExclusionPath` should contain `C:\`; realtime protection may remain `True` with Tamper Protection enabled. The lead confirmed this policy exclusion after reboot in the same Windows 11 ARM64 guest: with a 679 MB pnpm store, `pnpm install` took 24.5 seconds with the exclusion versus 91-100 seconds without it. This is measured guest evidence for that setup, not a timing guarantee for every job. Check running HVCI and service states separately, and measure dependency installation and cache save/restore timings for the workload being evaluated. ## Interruption and recovery ```sh vectis operation cancel --json vectis environment resume-windows --json ``` Closing the desktop does not cancel the service operation. Cancellation preserves the owned disk for diagnosis and resume. The previous VM must have a matching exit receipt before another attempt is admitted. An uncertain process state blocks new VM starts. An interrupted operation reports `action_required` with the setup ID and private directory. Its `setup.ppm` screenshot and `preparation.log` can help diagnose guest setup failures. The preparation log contains host runtime output; guest command failures also emit a bounded full PowerShell error record to stderr. The bootstrap records its failing stage, error message and script line in `C:\ProgramData\Vectis\diagnostic.json`. When SSH remains reachable, Vectis saves a bounded copy as `bootstrap-diagnostic.json` in the private setup directory before stopping the VM. Only the known stage appears in the operation message; the detailed error stays local. Older setup media may have only the generic failure marker. Keep screenshots, diagnostics and setup credentials local. Resume uses the same setup identity and disk, not a second fresh installation. It regenerates setup media from the current service recipe and retries a failed bootstrap once if SSH is reachable. A new bootstrap attempt clears old completion and failure markers before running. A changed environment configuration requires a separate setup. If SSH cannot start or the firewall blocks it, automatic bootstrap retry cannot reach the guest. The setup may need human console recovery; repeatedly resuming does not fix unreachable SSH. Preserve the setup disk and inspect its screenshot and local diagnostics before deciding how to recover. Do not treat a stopped or unreachable guest as a completed environment, or disable pinned host-key checking to reach it. ## Derive a prepared image Fresh Windows preparations preinstall the verified pinned Actions runner at `C:\vectis\runner\` and record its version in `Environment.image`, so jobs start it in place. Existing ready environments can gain the runner or repository tools with [environment provisioning](/docs/guides/environments). Setup scripts run as the guest SSH user under Windows PowerShell 5.1 (`powershell.exe`). A derived disk is a QCOW2 overlay: keep its parent backing image in place and remove registered descendants before their parent. --- # CLI reference Source: https://vectis.kerd.dev/docs/reference/cli This page is generated from `vectis --help` when the documentation is built. Install the command line tools from the app as described in [Install Vectis](/docs/get-started/install#command-line-tools). In a source checkout, run `pnpm vectis` instead. See [Operations and output](/docs/reference/operations) for keys, waiting, exit codes and errors. ## Service ```text service install Install and start the macOS login service for this home service uninstall Remove the login service, preserving all data service start Start the installed or an independent background service service run Run the service in the foreground service stop [--if-idle] Stop the service; --if-idle refuses to interrupt active work service status Inspect the login service registration for this home service update Adopt this runtime when idle; restore the previous one on failure service recover-update Restore the previous runtime after an interrupted update status Inspect the machine, unfinished work and bounded recent history storage Inspect image and VM disk usage doctor Inspect host and runtime prerequisites logs [--lines ] Read the newest service log lines (default 200, at most 1000) capabilities List supported commands and queries with the command schema pause | resume Stop or allow admission of new VMs ``` ## Cloud and GitHub ```text cloud pair Begin browser-approved pairing of this machine with your account cloud finish Complete an approved pairing cloud disconnect Remove this machine's saved cloud connection and credential github connect Print the page that links a GitHub account and installs the App github accounts List verified GitHub accounts available to this machine login [--name ] Begin browser-approved remote control of your machines login finish Complete an approved remote control login logout Revoke this CLI's remote access and remove its credentials machine list List your machines and when they were last seen machine configure [--max-runners ] [--memory-budget-mib ] Set runner limit (1-16, default 5) and shared VM memory budget Memory budget: 2048 MiB to host RAM, default 75% of host RAM ``` ## Repositories and jobs ```text repository readiness [--environment ] Check readiness before connecting repository readiness --binding Check an existing binding repository list List repositories connected to this machine, with their runs-on label repository connect Connect with --account and --environment , optional --owner repository enable-auto Start runners automatically for matching queued jobs repository disable-auto Stop starting runners automatically repository capacity-mode demand|scale-set Choose runner capacity mode (scale sets are preview) repository disconnect Stop future runners without interrupting running jobs runner run Start one disposable runner, optionally only for --job-id runner reconcile Clean up an interrupted runner after verifying its VM stopped repository cache enable|disable|clear [--size-gib N] Configure or clear an opt-in repository cache disk (default 20 GiB) repository access --actors any| --approval automatic|required Actor IDs are numeric GitHub user IDs: gh api users/ --jq .id job list Read recent GitHub job states for a connected repository job logs --binding [--follow] Read the retained log; follow backs off from 2 to 30 seconds when empty job scan Discover missing jobs through the GitHub API job refresh Recover one job state directly from GitHub ``` ## Workflow run control ```text run rerun-failed Rerun failed jobs of a completed GitHub run run cancel Request cancellation of an active GitHub run workflow dispatch --ref Dispatch a workflow file or numeric ID Successful rerun/cancel results distinguish requested from observed GitHub state. Ambiguous POST responses require checking GitHub before retrying with a new key. Run command receipts expire after 30 days; do not replay older keys. ``` ## Workflow migration ```text migration preview --map = Rewrite runs-on labels locally without writing files migration analyze Prepare a reviewable migration preview for a repository migration publish Create or recover its pull request; never merges ``` ## Environments ```text environment provision Derive a new image with --from --image-directory [--name ] [--setup-script ] [--no-runner] environment prepare-linux Prepare Ubuntu 24.04 with --image-directory and --storage-path environment resume Continue an interrupted Ubuntu preparation environment install-macos Install macOS 27 with --image-directory and --storage-path, optional --restore-path environment open-macos-setup Open the guest console for Setup Assistant environment connect-macos-guest Prepare guest SSH enrollment, optional --open-terminal environment verify-macos-guest Verify guest SSH and unattended startup prerequisites environment finish-macos-setup Register the verified, stopped macOS setup environment resume-macos Inspect or retry an interrupted macOS installation environment discard-macos --environment Permanently discard an unregistered setup environment install-windows Experimental: install Windows 11 from your ISO, drivers and firmware environment resume-windows Continue an interrupted Windows installation environment register --file Register an existing prepared environment JSON file environment list List environments and effective network egress environment configure Set --cpu, --memory-mib, --storage-path, --x64 rosetta|off or --egress internet_only|open for future VMs --warm-pool keeps 0-4 SSH-ready VMs (default 0, uses RAM) environment start Start a clean VM, optionally overriding --cpu, --memory-mib, --storage-path environment remove Remove an idle definition, preserving its disk instance stop Stop a VM owned by this service instance reconcile Recheck an interrupted VM safely ``` ## Activities and operations ```text activity list List unfinished and recent activities; filter with --kind, --status, --repository, --limit activity get Inspect one activity with the operations that belong to it activity cancel Cancel work an activity owns, or close one that only waits for you operation get Inspect one retained operation, including outside the status window operation wait Wait for an operation's terminal state; on --timeout, print it as it is operation cancel Cancel an active operation, or close one that only waits for you command --file Submit any protocol command as JSON version Print the Vectis version ``` ## Options ```text --home State directory (default: ~/.vectis, or ~/.vectis-dev for development builds) --machine Send commands and queries to a remote machine after vectis login --json Print compact JSON (output is always JSON) --key Idempotency key; retrying with the same key never repeats work --kind Filter activities by what asked for them --status Filter activities by status --repository Filter activities by repository --wait Wait for the operation's terminal state (automatic for setting commands) --timeout Wait timeout (default: 120000) --name Environment or login name (defaults: Ubuntu 24.04 ARM64, macOS 27 ARM64, Windows 11 ARM64, Vectis CLI) --image-directory Existing directory for prepared base images --storage-path Directory for VM clones --cpu Virtual CPUs (defaults: 2, Windows 4) --memory-mib Memory (defaults: 4096, Windows 8192; at most 75% of host memory) --disk-gib Disk capacity (defaults: Ubuntu 32, macOS 64, Windows 96) --restore-path Local macOS restore image instead of the pinned download --open-terminal Open Terminal with the guest enrollment script --iso-path, --drivers-path, --firmware-path, --firmware-vars-path Windows installation media --image-name Windows edition in the ISO (default: Windows 11 Pro) --accept-license Accept the Windows license terms for this installation --account GitHub account ID from vectis github accounts --owner Repository owner when it differs from the account --environment Environment ID --job-id Require a matching queued GitHub job before starting a runner --ref Git ref for workflow dispatch --input = Workflow input; repeat for up to 25 inputs --map = Label mapping for migration preview; repeat for more labels --file JSON input file --if-idle Refuse to stop while VMs or operations are active -v, --version Print the Vectis version -h, --help Show this help ``` Exit codes: 0 success, 1 failure or cancellation, 3 action required or still pending. Setting commands always wait: pause/resume, repository enable-auto/disable-auto/capacity-mode, repository cache, environment register/configure/remove, instance stop/reconcile, operation cancel, and activity cancel. Their wait timeout reports wait_cancelled with exit code 3; the operation may still be running. Errors are JSON: {"error":{"code","message","nextStep"}}. ## Examples ```text vectis status --json vectis environment prepare-linux ubuntu --image-directory ~/VMs/images --storage-path ~/VMs/clones --wait vectis repository list --json vectis runner run --key --json vectis operation wait --timeout 3600000 --json ``` Documentation: https://vectis.kerd.dev/docs --- # Operations and output Source: https://vectis.kerd.dev/docs/reference/operations ## Operations Every change is an operation recorded by the service before anything happens. Its `status` is one of: | Status | Meaning | | ----------------- | --------------------------------------------------- | | `accepted` | Recorded; work has not started | | `running` | In progress | | `action_required` | Stopped safely; `nextStep` says what you need to do | | `succeeded` | Finished, including cleanup | | `failed` | Finished without success; `message` says why | | `cancelled` | Cancelled, with cleanup finished | Operations have a `message`, often a `nextStep`, and for some commands a `result`. ## Idempotency keys Each command carries a key (`--key` in the CLI, `key` in MCP). Submitting the same key and command again returns the original operation instead of doing the work twice, which makes retries after a timeout or a dropped connection safe. The CLI generates a random key when you omit it. Reusing a key for a different command is rejected. ## Waiting Commands marked `activity: "setting"` in capabilities wait automatically in the CLI: pause/resume, repository enable-auto/disable-auto, environment register/configure/remove, instance stop/reconcile and operation cancel. They return the terminal operation, with exit code 1 for failure or cancellation and 3 for action required. If their `--timeout` expires, they report a `wait_cancelled` error and exit with 3; the operation may still be running. MCP command submission keeps its existing behavior. `--wait` or `vectis operation wait ` waits until the operation leaves `accepted` and `running`, for up to `--timeout` milliseconds (default 120000). If time runs out, the CLI prints the operation as it is and exits with 3; the operation keeps going. MCP `vectis_wait` waits up to 120 seconds per call and returns `timedOut: true` in the same case. Stopping a wait never cancels the operation; use `vectis operation cancel ` for that. ## Output and exit codes The CLI always prints JSON: indented by default, compact with `--json`. | Exit code | Meaning | | --------- | ------------------------------------------------------------------ | | 0 | Success | | 1 | The operation failed or was cancelled, or the command was rejected | | 3 | The operation needs you (`action_required`) or is still running | Errors are printed as: ```json { "error": { "code": "machine_paused", "message": "The machine is paused.", "nextStep": "Run vectis resume." } } ``` `code` is stable for scripts and agents; `nextStep` is written for people. ## Runner labels Every runner carries `self-hosted`, its OS label (`Linux`, `macOS` or `Windows`), `ARM64` and `vectis-`. A job reaches it when its `runs-on` includes `vectis-` and only labels from that list. See [Repositories and labels](/docs/guides/repositories#choose-the-runs-on-label). ## Environment variables | Variable | Effect | | ------------------------------------------------ | ----------------------------------------------------------------------- | | `VECTIS_HOME` | State directory, like `--home` | | `VECTIS_APPLE_HELPER`, `VECTIS_KEYCHAIN_HELPER` | Native helpers; set automatically by the app and its command line tools | | `VECTIS_QEMU`, `VECTIS_QEMU_IMG`, `VECTIS_SWTPM` | Windows runtime executables | Source checkouts also read `CONVEX_URL` and `VECTIS_WEB_URL`; see [Build from source](/docs/self-hosting/build-from-source). --- # Build from source Source: https://vectis.kerd.dev/docs/self-hosting/build-from-source Use Node.js 24.22.1 or newer within the Node 24 release line, and pnpm 10.24.0. Native VM execution requires an Apple Silicon Mac. To use Vectis rather than develop it, [install the app](/docs/get-started/install) instead. ## Development builds Everything run from a checkout is a **development build**: it is named Vectis Dev, keeps its state in `~/.vectis-dev` instead of `~/.vectis`, and connects to the cloud deployment configured in the repository's `.env.local`: ```sh cp .env.example .env.local # then fill in CONVEX_URL and VECTIS_WEB_URL ``` Without those values everything local works, and cloud commands report `cloud_unconfigured`. To connect to your own backend, see [Run your own cloud](/docs/self-hosting/cloud) and [Private development environment](/docs/self-hosting/development-environment). ## Run an isolated service From the repository root: ```sh pnpm install --frozen-lockfile pnpm build pnpm vectis service start --home /tmp/vectis-demo --json pnpm vectis status --home /tmp/vectis-demo --json pnpm vectis doctor --home /tmp/vectis-demo --json ``` Keep using the same `--home` for a given service. It contains the SQLite database, service log, and authenticated local connection metadata. It is separate from your chosen VM storage directory. Do not share `connection.json`; it contains a local access token. The service runs independently of the CLI. Stop it with: ```sh pnpm vectis service stop --home /tmp/vectis-demo --json ``` ## Start at login on macOS After building the checkout, install a per-user login service: ```sh pnpm vectis service install --home /absolute/path/to/vectis-state --json pnpm vectis service status --home /absolute/path/to/vectis-state --json ``` Stop an already running manual service before installation. Installation registers a LaunchAgent for the current macOS user and waits for an authenticated response before reporting that it is running. macOS may require background item approval in System Settings. A registration whose startup times out remains available for inspection, retry, or removal. The registration stores absolute paths to Node, this built checkout, the state directory, and any configured `VECTIS_APPLE_HELPER`, `VECTIS_QEMU`, `VECTIS_QEMU_IMG`, `VECTIS_SWTPM`, and `VECTIS_KEYCHAIN_HELPER` executables. Set these before installation. Other shell variables and secrets are not copied. Keep the checkout and runtime paths available when running from source. For standalone installations, [package](/docs/self-hosting/packaging) the runtime. To adopt another runtime, use `vectis service update` from the new package while the service is idle. An abnormal exit permits launchd to restart the service. An authenticated `service stop` exits cleanly and leaves it stopped until `service start` or the next login. Logging out ends this per-user service. Closing the terminal does not. This behavior follows Apple's [LaunchAgent lifecycle](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html). Remove the login registration after stopping the service: ```sh pnpm vectis service stop --home /absolute/path/to/vectis-state --json pnpm vectis service uninstall --home /absolute/path/to/vectis-state --json ``` Stopping the service stops its owned VMs. Uninstalling the registration preserves state, images, and chosen VM storage. `service status` reports registration and launchd loading; use `status` for the live machine and operations. The MCP `vectis_service` tool exposes the same login-service controls and can discover registration without a running local API. Before replacing a runtime or app, use `service stop --if-idle`. This refuses to stop while a job, preparation, pending command, or unreconciled VM remains. Pause new work and retry after active work finishes. The desktop's **Stop service if idle** button and MCP `vectis_service` with `action: "stop", ifIdle: true` use the same check. Older services that lack this operation reject it; the client does not fall back to interrupting their work. ## Prepared Linux images For development, the Apple helper can start a prepared, bootable ARM64 Linux raw disk with EFI. A raw filesystem partition alone is not a bootable VM disk. Build the helper with Xcode's Swift tools, then sign the development binary with its virtualization entitlement: ```sh swift build --package-path native/apple codesign --force --sign - --entitlements native/apple/entitlements.plist native/apple/.build/out/Products/Debug/vectis-vm ``` Set `VECTIS_APPLE_HELPER` to the absolute executable path before starting the service. A previously running service must be restarted to pick up runtime configuration. Use `environment register --file environment.json` to register the prepared image. See the [storage guide](/docs/guides/storage) for the file format. Registering an environment does not install its operating system or toolchains. Once its guest SSH access is prepared, the shared runner lifecycle installs the official runner and executes jobs in disposable instances. See [Runners and jobs](/docs/guides/runners) for manual and automatic runners. ## Operation outcomes Mutating commands return an operation ID. `accepted` means the request was recorded. Use `--wait` or `operation wait ` to observe completion. Exit code `1` indicates failure; `3` indicates `action_required`. `pause` prevents new VM starts and leaves running work alone. `instance stop ` stops a VM owned by the service. After a service crash, an interrupted instance requires verified process-exit evidence before Vectis removes its working directory. VM startup runs as a cancellable operation. Use `operation cancel ` to withdraw an unfinished start, then wait for that original operation to finish. Cancellation being requested does not prove cleanup has completed. While image access or cleanup is still pending, Vectis keeps the reservation and refuses another VM start. Pause and cancellation requests remain available. If macOS is waiting for file-access approval, the underlying file operation may need that prompt resolved before cleanup can finish; a cancelled start will not proceed to boot afterward. ## Connect your machine Pairing requires the native Keychain helper. Set `VECTIS_KEYCHAIN_HELPER` to its absolute executable path before starting both the service and CLI. An installed login service captures this path at installation. ```sh pnpm vectis cloud pair --home /absolute/path/to/vectis-state --json ``` Open the returned `/connect` link yourself. Compare the verification code with the CLI output, sign in, and approve the machine. Treat the pairing link as private and never approve a link supplied by someone else. The request expires after ten minutes. A source checkout uses the deployment configured by `CONVEX_URL` and `VECTIS_WEB_URL` in `.env.local` (see `.env.example`); packaged apps use the deployment they were built for. Then finish the connection: ```sh pnpm vectis cloud finish --home /absolute/path/to/vectis-state --json pnpm vectis status --home /absolute/path/to/vectis-state --json ``` `pending` means browser approval has not completed. `linked` means the credential and local configuration were saved; inspect the separate `cloud.state` to confirm the relay is connected. Finishing pairing reloads the cloud connection without restarting the service or stopping VMs. Retries reuse the pending credential or verify the saved connection. Pairing does not silently replace an existing machine connection. The service and CLI keep raw credentials in macOS Keychain. The web approval contains only credential fingerprints. An MCP process configured with the same Keychain helper exposes `vectis_cloud` for the same pair and finish actions. Human approval is always explicit. This connects a machine to Vectis. Continue with the [quickstart](/docs/get-started/quickstart#3-link-github-and-install-the-app) to link GitHub and connect repositories. --- # Run your own cloud Source: https://vectis.kerd.dev/docs/self-hosting/cloud The hosted service at `vectis.kerd.dev` is one deployment of the code in this repository. You can run the same stack yourself, for a fork or for private development. A deployment has four parts: | Part | What it does | | ----------------- | ----------------------------------------------------------------------------------------------- | | Convex | Accounts, machines, repository connections, GitHub webhooks, runner leases, and remote commands | | Clerk | Sign-in for the connect page | | Cloudflare Worker | Serves the website, the docs, and `/connect`, and forwards GitHub browser callbacks to Convex | | GitHub App | Repository access and just-in-time runner registration | Vectis builds come in two flavors. **Development** builds (every source checkout) read `.env.local`, use `~/.vectis-dev`, and are named Vectis Dev. **Production** builds are packaged with `--flavor production`, read `.env.production.local` at package time, and use `~/.vectis`. Keep one set of Convex, Clerk, Worker, and GitHub App per flavor; never point a development build at production data. [`.env.example`](https://github.com/kerddotdev/vectis/blob/main/.env.example) lists every variable. Copy it to `.env.local` or `.env.production.local` and fill it in as you go. Commands below use development; add `--prod` to Convex commands and use `.env.production.local` for production. ## 1. Convex Create a Convex project and a development deployment. This writes `CONVEX_DEPLOYMENT`, `CONVEX_URL`, and `CONVEX_SITE_URL` to `.env.local`: ```sh pnpm exec convex dev --once ``` For production, deploy with `pnpm exec convex deploy` and copy the production URLs into `.env.production.local`. Create the key that signs short-lived machine tokens, store it on the deployment, and delete the local copy. Load the file first so the commands can use its values: ```sh set -a; . ./.env.local; set +a openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out machine.pem pnpm exec convex env set VECTIS_MACHINE_PRIVATE_KEY -- "$(cat machine.pem)" rm machine.pem pnpm exec convex env set VECTIS_MACHINE_ISSUER "$CONVEX_SITE_URL" pnpm exec convex env set VECTIS_WEB_URL "$VECTIS_WEB_URL" pnpm exec convex env set VECTIS_GITHUB_APP_NAME "Vectis" ``` `VECTIS_WEB_URL` is the HTTPS origin of your Worker (step 3), without a trailing slash. ## 2. Clerk 1. Create a Clerk application. Development uses its development instance; production needs a production instance on your own domain, which Clerk walks you through (DNS records for `clerk.`). 2. In **Integrations**, activate **Convex**. This creates the `convex` JWT template that Convex verifies. 3. Copy the publishable key into `CLERK_PUBLISHABLE_KEY`, and set the issuer on Convex: ```sh pnpm exec convex env set CLERK_JWT_ISSUER_DOMAIN https://your-app.clerk.accounts.dev ``` 4. Add a webhook in Clerk that sends `user.deleted` to `${CONVEX_SITE_URL}/clerk/webhook`, then store its signing secret: ```sh pnpm exec convex env set CLERK_WEBHOOK_SECRET whsec_your_signing_secret ``` Without it, deleting an account inside Clerk would leave its machines and repository connections behind. Vectis verifies every delivery signature and ignores other events. For production, also turn on bot sign-up protection. Anyone can create an account on a public deployment. ## 3. Cloudflare Worker The Worker in `apps/web` serves the static site and forwards `/api/github/*` browser callbacks to `CONVEX_SITE_URL`. Sign in with `pnpm --filter @vectis/web exec wrangler login`, then deploy: ```sh pnpm site:deploy:dev # the "development" environment on workers.dev pnpm site:deploy # the "production" environment on its custom domain ``` The production route in `apps/web/wrangler.jsonc` is `vectis.kerd.dev`; change it to your own domain in a fork. The deploy script passes `CONVEX_SITE_URL` to the Worker, so the configuration file holds no deployment URLs. To keep a development deployment private, follow [Private development environment](/docs/self-hosting/development-environment). ## 4. GitHub App Vectis creates its GitHub App through GitHub's manifest flow, so the App's private key, client secret, and webhook secret are written straight into Convex and never touch your machine. The Worker from step 3 must already serve `VECTIS_WEB_URL`, because GitHub returns to it. Run this from the repository root; for production, load `.env.production.local` instead and add `--prod` to `convex run`: ```sh set -a; . ./.env.local; set +a state=$(openssl rand -hex 32) digest=$(printf %s "$state" | shasum -a 256 | cut -d " " -f 1) owner=your-github-login pnpm exec convex run githubAppSetup:create \ "{\"stateDigest\":\"$digest\",\"ownerId\":$(gh api "users/$owner" --jq .id),\"ownerLogin\":\"$owner\",\"organization\":false}" open "$VECTIS_WEB_URL/api/github/app/setup?state=$state" ``` Set `"organization": true` when the owner is an organization. The setup link expires after an hour and works once. A deployment holds exactly one App. For a development deployment, set `VECTIS_GITHUB_APP_NAME` to something like `Vectis [DEV]` and `VECTIS_GITHUB_APP_PUBLIC` to `false` before creating it, so only the owner account can install it. After GitHub creates the App, open its settings to upload a logo and add privacy policy and support URLs. GitHub delivers webhooks directly to `CONVEX_SITE_URL`, which verifies every delivery signature. ## 5. Build clients Clients connect to the deployment they were built for. With the variables in place: ```sh pnpm package:cli --output /absolute/path/to/new-package --helpers /absolute/path/to/native-binaries --flavor production ``` A production package reads `CONVEX_URL` and `VECTIS_WEB_URL` from the environment or `.env.production.local` and bakes them in. Released apps ignore those variables at run time, so a stray `CONVEX_URL` in a user's shell cannot redirect them. ## Keeping deployments compatible Released apps keep running against the backend after you deploy a newer one. Deploy Convex changes that older clients can still use, and remove old behavior only after the releases that depend on it are gone. --- # Private development environment Source: https://vectis.kerd.dev/docs/self-hosting/development-environment A development environment lets you test pairing, GitHub linking, and remote control against your own backend without touching production. It uses the same parts as [your own cloud](/docs/self-hosting/cloud), with three extra locks so nobody else can use it. ## Layout | Part | Development | | ---------------------- | ---------------------------------------------------------------------- | | Website and `/connect` | The `development` Worker on `vectis-dev..workers.dev` | | Backend | Your Convex development deployment | | Sign-in | Your Clerk development instance | | GitHub App | A private App named like `Vectis [DEV]` | | Local builds | Every source checkout: `pnpm vectis`, `pnpm mcp`, `pnpm desktop:start` | Source checkouts read `CONVEX_URL` and `VECTIS_WEB_URL` from `.env.local`, so once the file is filled in, pairing and login links point at the development environment automatically. ## Set it up 1. Follow [Run your own cloud](/docs/self-hosting/cloud) with development values. Set `VECTIS_WEB_URL` to `https://vectis-dev..workers.dev`. 2. Deploy the website with `pnpm site:deploy:dev`. 3. **Lock the website.** In the Cloudflare dashboard, open **Workers & Pages**, select `vectis-dev`, go to **Settings > Domains & Routes**, and choose **Enable Cloudflare Access** for `workers.dev`. Edit the `vectis-dev - Production` policy in Zero Trust so it allows only your email address. GitHub webhooks go straight to Convex, so nothing needs a bypass. 4. **Lock sign-in.** In the Clerk development instance, open **Configure > Restrictions**, turn on the allowlist with your email addresses, and enforce it on sign-in as well as sign-up. Nobody else can create an account or sign in, so nobody else can approve a pairing or control a machine. 5. **Lock the GitHub App.** Create it with `VECTIS_GITHUB_APP_PUBLIC=false`, so it can only be installed on the account that owns it. ## Check the locks - In a private browser window, `https://vectis-dev..workers.dev/connect` shows the Cloudflare Access login instead of the page. - Signing in or signing up with another email address is refused. - The App's public page on GitHub offers installation only to its owner. ## Use it ```sh export VECTIS_HOME="$(mktemp -d)" pnpm build pnpm vectis service start pnpm vectis cloud pair ``` The pairing link opens your development `/connect` page. Pass `--home` or `VECTIS_HOME` to keep test machines separate from `~/.vectis-dev`. --- # Package and sign Source: https://vectis.kerd.dev/docs/self-hosting/packaging The portable macOS ARM64 development package includes its own pinned Node runtime, CLI, MCP server, background service, Apple virtualization helper, and Keychain helper. It runs outside the source checkout without Node or pnpm on the user's PATH. The CLI payload is an ad-hoc-signed development artifact, not a notarized public release. An optional Windows runtime can include QEMU, qemu-img, swtpm, and their libraries. Firmware and guest operating system images remain separate. Do not disable Gatekeeper to distribute this artifact. ## Build Build the native helpers and the TypeScript applications with the pinned project tools. Pass the directory containing both `vectis-vm` and `vectis-keychain`: ```sh swift build --package-path native/apple --configuration release pnpm package:cli --output /absolute/path/to/new-package --helpers /absolute/path/to/native-binaries pnpm package:verify /absolute/path/to/new-package ``` The output directory must not already exist. The build downloads Node 24.22.2 from nodejs.org and checks its pinned SHA-256 and byte length. The Node license and dependency license files stay in the package. Package contents use an explicit allowlist; environment files and internal documents are excluded. An inspection rejects external dependency symlinks. To stage the Windows runtime from locally built or installed binaries, provide the matching QEMU source tree and a new output directory: ```sh pnpm package:windows-runtime --qemu /absolute/path/to/qemu-system-aarch64 --qemu-img /absolute/path/to/qemu-img --swtpm /absolute/path/to/swtpm --qemu-source /absolute/path/to/qemu-source --output /absolute/path/to/windows-runtime pnpm package:cli --output /absolute/path/to/new-package --helpers /absolute/path/to/native-binaries --windows-runtime /absolute/path/to/windows-runtime ``` Staging copies the ARM64 binaries and non-system libraries, rewrites library references within the bundle, and records source hashes. It does not modify the original installations. This development bundle still requires complete corresponding sources and third-party license materials before redistribution; `BUILD.json` records that limitation. Collect the pinned sources and verify them against the runtime you intend to package: ```sh pnpm package:runtime-sources --runtime /absolute/path/to/windows-runtime --output /absolute/path/to/source-materials pnpm package:verify-runtime-sources --runtime /absolute/path/to/windows-runtime --sources /absolute/path/to/source-materials ``` Verification checks component versions, source and upstream patch coverage against the runtime's bundled SPDX records, archive checksums and sizes, notice and recipe files, and the custom QEMU patch. Missing or mismatched materials fail verification. The DMG packager performs the same verification on its staged copies before creating the image. This checks technical completeness and integrity; it does not grant a license or certify legal compliance. `package:verify` runs isolated service start, pause, stop, restart, resume, MCP stdio, and cleanup checks using the packaged runtime with no Node on PATH. It preserves failed-test state if service shutdown cannot be verified. Run it after moving the package outside the checkout to check relocation too. ## Use Keep the package directory intact and add its `bin` directory to PATH. Do not symlink individual launchers. Both launchers resolve the packaged runtime relative to themselves: ```sh /absolute/path/to/package/bin/vectis --help /absolute/path/to/package/bin/vectis service start --home /absolute/path/to/test-state --json /absolute/path/to/package/bin/vectis-mcp --home /absolute/path/to/test-state ``` The launchers configure the packaged Apple and Keychain helpers and any bundled Windows runtime automatically. Explicit `VECTIS_APPLE_HELPER`, `VECTIS_KEYCHAIN_HELPER`, `VECTIS_QEMU`, `VECTIS_QEMU_IMG`, and `VECTIS_SWTPM` overrides remain available for development. Service installation records absolute paths, so stop and uninstall its login registration before moving or removing an installed package. Uninstalling that registration preserves images, configuration, and credentials. The package does not grant GitHub access or connect a cloud account automatically. Follow the [quickstart](/docs/get-started/quickstart) and the [remote control guide](/docs/guides/remote-control). ## Desktop development app Building desktop or headless app bundles requires Xcode 26 or later for the Icon Composer source in `assets/brand/vectis.icon`. Packaging compiles both the modern asset catalog and a macOS 15 `.icns` fallback. The web mark and reusable palette live beside it under `assets/brand/web`. Build the renderer before creating a fresh portable payload, then wrap it with the pinned Electron version: ```sh pnpm desktop:build pnpm package:cli --output /absolute/path/to/new-payload --helpers /absolute/path/to/native-binaries pnpm package:desktop --package /absolute/path/to/new-payload --output /absolute/path/to/new-desktop-output ``` The result is `Vectis-darwin-arm64/Vectis.app` for a production payload (`pnpm package:cli --flavor production`) or `Vectis Dev-darwin-arm64/Vectis Dev.app` for a development one. The app also contains the `vectis` and `vectis-mcp` launchers in `Contents/Resources/bin`. The app finds its Node runtime and native helpers inside its own Resources directory. Installing the service from the desktop registers that independent runtime with macOS; closing the desktop leaves it running. The default desktop build is for local development verification. Add `--sign` to use an available Developer ID Application identity. Add `--keychain-profile ` together with `--sign` to submit the signed app for notarization using an existing notarytool profile. Signing errors fail the build; signing alone does not mean Apple has notarized the app. For a team App Store Connect API key, use `--sign --notarize` with `APPLE_API_KEY` (an absolute `.p8` file path), `APPLE_API_KEY_ID`, and `APPLE_API_ISSUER` in the process environment. A supplied Keychain profile takes precedence. Merely setting these variables does not enable notarization. Keep the key outside the source checkout and package payload; never commit it or pass its contents as a command argument. Keep the app at its installed location while its login service is registered. The portable payload and app contain their own copies of the runtime, so the payload can be removed after packaging if no service uses it. macOS file privacy permissions also apply to the signed background service. A successful CLI test from a terminal does not establish that the login service can access the same images or storage directory. Verify that access separately before relying on unattended VM startup. ## Electron-free signed runtime The standalone runtime can also be packaged as a native macOS application bundle without Electron. Build the small launcher, then wrap a portable payload: ```sh swift build --package-path native/apple --configuration release --product vectis-launcher pnpm package:headless --package /absolute/path/to/payload --launcher /absolute/path/to/vectis-launcher --output /absolute/path/to/new-runtime --identity YOUR_DEVELOPER_ID_IDENTITY pnpm package:verify /absolute/path/to/new-runtime ``` The output contains `Vectis Runtime.app` and `bin/vectis` and `bin/vectis-mcp`. Keep them together. Without `--identity`, this is an ad-hoc development build. Signing alone does not notarize the bundle. Add `--notarize` with the same API key environment variables described above, or `--keychain-profile ` with `--identity`. The packager submits to Apple, requires acceptance, staples and validates the ticket, and checks Gatekeeper before recording notarization success. Third-party redistribution requirements still apply to any included Windows runtime. ## Replace an installed runtime Keep the previous package at its original location. Run the new package's CLI against the existing state directory: ```sh /absolute/path/to/new-package/bin/vectis service update --home /absolute/path/to/state --json ``` This validates the new runtime paths, refuses active work, waits for the old service to stop, and replaces its login registration. The new service must respond before the update succeeds. If startup fails, Vectis attempts to restore and start the previous registration. VM images, credentials, and configuration stay in the same locations. After an interrupted update, use `service recover-update` with the same home. The saved previous registration remains available until recovery succeeds. Other registration changes are blocked while recovery is pending, and concurrent clients cannot replace the registration simultaneously. Keep both packages until the operation completes. The desktop's **Use this app's runtime** and **Recover runtime update** buttons use the same operations. MCP exposes `vectis_service` with `action: "update"` or `action: "recoverUpdate"`. Updates adopt the runtime running that client; they do not download a release. This recovery restores the runtime registration, not a backup of application data or a general database downgrade. ## Release artifacts Create the release artifacts from an already notarized desktop app. Include the matching collected runtime sources when Windows binaries are bundled: ```sh pnpm package:release --app /absolute/path/to/Vectis.app --output /absolute/path/to/new-directory --identity YOUR_DEVELOPER_ID_IDENTITY ``` Use the API key environment variables above or `--keychain-profile`. The command verifies the app signature and stapled ticket, then writes: - `Vectis-arm64.dmg`: the app with an Applications shortcut, signed and notarized. Development builds produce `Vectis-Dev-arm64.dmg`. - `Vectis--arm64-mac.zip` and `latest-mac.yml`: the update payload and feed that installed production apps read from GitHub Releases. Development builds have no update feed. It does not publish anything. Production apps look for updates on the `kerddotdev/vectis` GitHub Releases, so a fork must change `scripts/release/desktop-identity.ts`. Drag the app to Applications before opening it and installing the service. Do not install the background service directly from a mounted disk image.