Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
81835c6
feat(cloud): add the Cloudflare cloud contract and Worker scaffold
kerdofficial Nov 10, 2026
f7ead8f
feat(web): use the cloud Worker from /connect and the site proxy
kerdofficial Nov 10, 2026
f799fe9
docs: describe the Cloudflare cloud deployment
kerdofficial Nov 10, 2026
69ecbd5
test(cloud): type the generated GitHub App key pair
kerdofficial Nov 10, 2026
e0c0c12
feat(cloud): machine hub, sync, operations and event-driven runner de…
kerdofficial Nov 10, 2026
090079a
feat(client): pair and control machines through the cloud Worker
kerdofficial Nov 10, 2026
43d1a92
fix(github): use manual redirects so GitHub calls run in Workers
kerdofficial Nov 10, 2026
7554660
feat(cloud): GitHub webhooks, job state and redelivery on D1
kerdofficial Nov 10, 2026
7c3afb4
feat(cloud): runner leases and scale sets on D1
kerdofficial Nov 10, 2026
e0904a8
feat(client): relay to the cloud over HTTP and one hibernating WebSocket
kerdofficial Nov 10, 2026
adb0fbc
feat(cloud): pairing, controllers, accounts and repository bindings o…
kerdofficial Nov 10, 2026
9c18d21
feat(cloud): run control receipts and workflow migrations on D1
kerdofficial Nov 10, 2026
3230c0c
chore: remove the Convex backend
kerdofficial Nov 10, 2026
dd08422
chore(cloud): configure D1 databases and Clerk issuers
kerdofficial Nov 10, 2026
fbdbe67
fix(cloud): send a User-Agent to GitHub and drop unsupported redirect…
kerdofficial Nov 10, 2026
4b6d626
docs(cloud): run the cloud Worker on Workers Paid
kerdofficial Nov 10, 2026
09cdf99
chore: serve development deployments from kerd.dev hostnames
kerdofficial Nov 10, 2026
68c10c6
test(cli): configure a memory budget every CI host can hold
kerdofficial Nov 10, 2026
eded4d4
fix(web): show when the account overview cannot load
kerdofficial Nov 10, 2026
0412315
fix(cloud): report job markers for the repositories the relay tracks
kerdofficial Nov 10, 2026
6f89f25
fix(client): report an unconfirmed environment as a typed error
kerdofficial Nov 10, 2026
af2ea7f
fix(cloud): cap every active operation so sync returns all of them
kerdofficial Nov 10, 2026
b268df0
fix(cloud): apply the active operation cap to automatic runner demand
kerdofficial Nov 10, 2026
a1b0df4
test(cloud): sync returns every operation at the submission limit
kerdofficial Nov 10, 2026
317d8e2
fix(cloud): keep sweeping full retention batches within a run
kerdofficial Nov 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 14 additions & 13 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,29 @@
# builds. Both files are gitignored. Variables set in the process environment take precedence.
#
# Source checkouts of the CLI, MCP server, desktop app and website read these files directly.
# Packaged apps bake CONVEX_URL and VECTIS_WEB_URL in at package time and ignore the environment.
# Packaged apps bake VECTIS_CLOUD_URL and VECTIS_WEB_URL in at package time and ignore the
# environment.

# Convex. `pnpm exec convex dev` writes the first three for your development deployment.
CONVEX_DEPLOYMENT=dev:your-deployment-123
CONVEX_URL=https://your-deployment-123.convex.cloud
CONVEX_SITE_URL=https://your-deployment-123.convex.site
# The cloud Worker (apps/cloud). Development builds may use the local `pnpm --filter
# @vectis/cloud dev` origin, http://127.0.0.1:8787; production builds require HTTPS.
VECTIS_CLOUD_URL=https://cloud-dev.example.com

# Clerk. `clerk env pull` writes these for the selected instance. The secret key is only used by
# the Clerk CLI and never reaches a build.
CLERK_PUBLISHABLE_KEY=pk_test_replace_with_your_publishable_key
CLERK_SECRET_KEY=sk_test_replace_with_your_secret_key

# The HTTPS origin that serves the website and /connect for this deployment, without a path.
VECTIS_WEB_URL=https://vectis-dev.your-account.workers.dev
VECTIS_WEB_URL=https://dev.example.com

# Convex deployment variables. These are not read from this file: set them on the deployment
# with `pnpm exec convex env set <name> <value>` (add `--prod` for production).
# Cloud Worker settings are not read from this file. Plain values live in the `vars` of
# apps/cloud/wrangler.jsonc for each environment; the secret is set with
# `pnpm --filter @vectis/cloud exec wrangler secret put CLERK_WEBHOOK_SECRET --env <env>`
# (locally in apps/cloud/.dev.vars).
#
# CLERK_JWT_ISSUER_DOMAIN Clerk Frontend API URL, for example https://your-app.clerk.accounts.dev
# VECTIS_MACHINE_ISSUER The deployment's .convex.site URL; issues machine tokens
# VECTIS_MACHINE_PRIVATE_KEY RS256 PKCS#8 PEM used to sign machine tokens
# VECTIS_WEB_URL Same value as above; used for GitHub callbacks and redirects
# VECTIS_WEB_URL Same value as above; CORS origin, GitHub callbacks and redirects
# VECTIS_CLOUD_URL The Worker's own origin; GitHub webhook and manifest URLs
# CLERK_ISSUER Clerk Frontend API URL, for example https://your-app.clerk.accounts.dev
# VECTIS_GITHUB_APP_NAME GitHub App name, for example "Vectis [DEV]"
# VECTIS_GITHUB_APP_PUBLIC "false" keeps the GitHub App installable only by its owner
# CLERK_WEBHOOK_SECRET Signing secret of the Clerk user.deleted webhook (whsec_...)
# CLERK_WEBHOOK_SECRET Secret: signing secret of the Clerk user.deleted webhook (whsec_...)
2 changes: 1 addition & 1 deletion .github/labels.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
{
"name": "area:cloud",
"color": "d4c5f9",
"description": "Convex backend, pairing, remote control, and the GitHub App."
"description": "Cloud Worker, pairing, remote control, and the GitHub App."
},
{
"name": "area:web",
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/deploy-site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- run: pnpm site:build
env:
VECTIS_FLAVOR: production
CONVEX_URL: ${{ vars.CONVEX_URL }}
VECTIS_CLOUD_URL: ${{ vars.VECTIS_CLOUD_URL }}
CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }}
- run: pnpm --filter @vectis/web exec wrangler deploy --env production --dry-run --outdir ../../dist/site-worker
- id: artifact
Expand Down Expand Up @@ -67,8 +67,7 @@ jobs:
- run: node scripts/site-artifact.ts verify
env:
VECTIS_SITE_ARTIFACT_DIGEST: ${{ needs.build.outputs.digest }}
- run: pnpm --filter @vectis/web exec wrangler deploy ../../dist/site-worker/worker.js --config wrangler.jsonc --env production --no-bundle --var VECTIS_CONVEX_SITE:${CONVEX_SITE_URL}
- run: pnpm --filter @vectis/web exec wrangler deploy ../../dist/site-worker/worker.js --config wrangler.jsonc --env production --no-bundle
env:
CONVEX_SITE_URL: ${{ vars.CONVEX_SITE_URL }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
12 changes: 9 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,15 @@ jobs:
node-version: 24.22.2
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm exec convex deploy -y
# Migrations are additive, so the running Worker keeps working until the new one deploys.
- run: pnpm --filter @vectis/cloud exec wrangler d1 migrations apply DB --env production --remote
env:
CONVEX_DEPLOY_KEY: ${{ secrets.CONVEX_DEPLOY_KEY }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- run: pnpm --filter @vectis/cloud exec wrangler deploy --env production
env:
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

desktop:
name: desktop
Expand All @@ -57,7 +63,7 @@ jobs:
timeout-minutes: 120
environment: release
env:
CONVEX_URL: ${{ vars.CONVEX_URL }}
VECTIS_CLOUD_URL: ${{ vars.VECTIS_CLOUD_URL }}
VECTIS_WEB_URL: ${{ vars.VECTIS_WEB_URL }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down
2 changes: 1 addition & 1 deletion .oxfmtrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"ignorePatterns": ["convex/_generated/**"]
"ignorePatterns": ["apps/cloud/worker-configuration.d.ts"]
}
2 changes: 1 addition & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"ignorePatterns": ["convex/_generated/**"]
"ignorePatterns": ["apps/cloud/worker-configuration.d.ts"]
}
22 changes: 11 additions & 11 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Vectis

Vectis runs repository-scoped GitHub Actions runners in disposable local virtual machines. A local background service owns processes, virtual machines, credentials, and durable execution state. The desktop app, CLI, and MCP server are clients of the same local contract. A Convex backend owns cloud identity, the GitHub App, and remote commands. CI execution always stays on the host.
Vectis runs repository-scoped GitHub Actions runners in disposable local virtual machines. A local background service owns processes, virtual machines, credentials, and durable execution state. The desktop app, CLI, and MCP server are clients of the same local contract. A Cloudflare Worker (`apps/cloud`, D1 and one Durable Object per machine) owns cloud identity, the GitHub App, and remote commands. CI execution always stays on the host.

## What we never compromise on

Expand Down Expand Up @@ -51,7 +51,7 @@ Before calling a change done, walk this list and say which entries applied:

- **Contract.** Commands are variants of `Command` in `packages/protocol/src/index.ts` and are listed in `capabilities`. Read-only remote queries go through `LocalQuery` in `packages/protocol/src/controller.ts`.
- **Service.** `apps/server/src/service.ts` handles each command; local HTTP endpoints live in `apps/server/src/http.ts` with a matching `VectisClient` method in `packages/client/src/index.ts`.
- **Cloud.** Cloud-backed commands go through `packages/client/src/cloud-relay.ts` and a Convex function in `convex/`. Regenerate `convex/_generated` after changing Convex functions.
- **Cloud.** Cloud-backed commands go through `packages/client/src/cloud-relay.ts` and a route of the cloud Worker in `apps/cloud`. Machine and human routes are typed in `packages/protocol/src/cloud.ts`; schema changes are new files in `apps/cloud/migrations`.
- **CLI.** Help text and parsing in `apps/cli/src/main.ts`. The help text also generates the public CLI reference.
- **Desktop.** `apps/desktop/src/renderer/lib/operations.ts` (labels, cancellable and preparation commands) and the relevant view in `apps/desktop/src/renderer/views`.
- **MCP.** `vectis_command` derives its schema from `Command` and usually needs no change. New tools belong in `apps/mcp/src/server.ts`.
Expand All @@ -63,19 +63,19 @@ Before calling a change done, walk this list and say which entries applied:
## Development

- Node 24 and pnpm (`packageManager` in `package.json`) with the committed lockfile. `pnpm install` installs.
- `pnpm build` compiles the headless apps, packages, Convex code, and scripts into `dist/`. Anything run from `dist/` needs a fresh build.
- `pnpm build` compiles the headless apps, packages, and scripts into `dist/`. Anything run from `dist/` needs a fresh build.
- `pnpm vectis --help` runs the built CLI. `pnpm mcp` runs the built MCP server. Pass `--home "$(mktemp -d)"`.
- `pnpm desktop:start` launches the development desktop (`Vectis Dev`) with a standalone Node runtime for the background service. Set `VECTIS_HOME` first.
- `pnpm web:dev` runs the Astro website. `pnpm docs:dev` runs the Blume documentation. `pnpm site:build` builds both and stages them into `apps/web/site` with the docs under `/docs`.
- Web, docs, and desktop builds download General Sans from Fontshare into the gitignored `packages/design/fonts`. Its license forbids committing the font files. `VECTIS_ALLOW_FONT_FALLBACK=1` builds without them.
- The Swift helpers live in `native/apple`: `swift build --package-path native/apple`, then ad-hoc sign `vectis-vm` with `native/apple/entitlements.plist`. QEMU build notes are in `native/qemu/README.md`.
- Source checkouts are development builds. They read `CONVEX_URL`, `VECTIS_WEB_URL` and `CLERK_PUBLISHABLE_KEY` from the repository `.env.local` (see `.env.example`); `pnpm exec convex dev` fills in the Convex values. Production packages (`pnpm package:cli --flavor production`) and `pnpm site:deploy` read `.env.production.local` and bake the values in.
- Never deploy (Convex, Cloudflare, `site:deploy`, `site:deploy:dev`), publish packages, push tags or create releases unless asked. [RELEASING.md](RELEASING.md) describes how CI and releases run.
- Source checkouts are development builds. They read `VECTIS_CLOUD_URL`, `VECTIS_WEB_URL` and `CLERK_PUBLISHABLE_KEY` from the repository `.env.local` (see `.env.example`). `pnpm --filter @vectis/cloud dev` runs the cloud Worker locally with a local D1 database. Production packages (`pnpm package:cli --flavor production`) and `pnpm site:deploy` read `.env.production.local` and bake the values in.
- Never deploy (the cloud Worker, D1 migrations with `--remote`, `site:deploy`, `site:deploy:dev`), publish packages, push tags or create releases unless asked. [RELEASING.md](RELEASING.md) describes how CI and releases run.

## Verifying

- Smallest proof that the change works: `pnpm exec vitest run <files>` for the tests you touched, plus `pnpm typecheck` or `pnpm lint` when types or lint rules are affected. `pnpm check` runs format, lint, typecheck, and tests together.
- Tests are colocated `*.test.ts` files; Convex tests live in `tests/convex` and use `convex-test`.
- Tests are colocated `*.test.ts` files. Cloud Worker tests live in `apps/cloud/test` and run inside workerd through `@cloudflare/vitest-plugin` (vitest project `cloud`).
- Every test uses its own temporary home (`mkdtemp`) and synthetic data. Stub launchd, the Keychain, and cloud deployments the way existing tests do.
- Test authentication, ownership, failure recovery, migration preservation, and real protocol boundaries. Do not add tests that only mirror the implementation or assert static UI markup.
- Native VM runs and real GitHub job evidence are reported separately from synthetic tests. Never claim a VM boot, a GitHub job, signing, or a deployment without evidence.
Expand All @@ -90,23 +90,23 @@ Before calling a change done, walk this list and say which entries applied:

## How it works

Clients reach the service over an authenticated loopback HTTP API: `127.0.0.1`, a random port, and a bearer token in the home's `connection.json`. Cross-origin browser requests are rejected. Commands become durable operations in SQLite, keyed for idempotency, and the runner package supervises VMs, disks, and guest processes. When a host is paired with the cloud, the service keeps an outgoing authenticated relay to Convex: remote commands arrive through it and replay locally under the same idempotency rules, and GitHub webhooks, runner leases, and job state flow through Convex. The host never exposes an inbound port.
Clients reach the service over an authenticated loopback HTTP API: `127.0.0.1`, a random port, and a bearer token in the home's `connection.json`. Cross-origin browser requests are rejected. Commands become durable operations in SQLite, keyed for idempotency, and the runner package supervises VMs, disks, and guest processes. When a host is paired with the cloud, the service keeps an outgoing authenticated WebSocket to its machine hub in the cloud Worker plus HTTPS calls: the cloud signals changes over the socket, the service syncs, remote commands replay locally under the same idempotency rules, and GitHub webhooks, runner leases, and job state flow through the Worker. The cloud is event-driven; nothing polls it on a timer except a 15-minute safety sync. The host never exposes an inbound port.

## Where code lives

- `apps/server` - local service: HTTP API, SQLite store, command dispatch, image preparation, cloud relay startup.
- `apps/cli`, `apps/mcp` - headless control surfaces.
- `apps/desktop` - Electron shell and React renderer over the shared local client.
- `apps/web` - Astro website, the browser `/connect` account page, and a Cloudflare Worker that proxies GitHub callbacks to Convex.
- `apps/web` - Astro website, the browser `/connect` account page, and a Cloudflare Worker that proxies GitHub callbacks to the cloud Worker.
- `apps/docs` - public Blume documentation. Public Markdown lives only in `apps/docs/docs`; the CLI reference is generated from the built CLI help.
- `packages/protocol` - Effect Schema contracts shared by every surface and Convex. No runtime logic.
- `packages/protocol` - Effect Schema contracts shared by every surface and the cloud Worker. No runtime logic.
- `packages/client` - local and remote clients, cloud relay, pairing, Keychain and LaunchAgent helpers.
- `packages/runner` - VM runtime and supervision, QEMU, the Apple helper bridge, guest preparation, storage inspection.
- `packages/github`, `packages/migration` - GitHub App contracts, job and runner logic, workflow migration previews.
- `packages/design` - design tokens, fonts, and the shared site header and footer.
- `convex` - cloud identity, authorization, GitHub integration, and remote operations.
- `apps/cloud` - cloud Worker: identity, authorization, GitHub integration, and remote operations on D1 and Durable Objects.
- `native/apple`, `native/qemu` - Apple virtualization and Keychain helpers, QEMU build notes.
- `scripts` - docs generation, site staging, packaging, and verification. `tests/convex` - Convex tests.
- `scripts` - docs generation, site staging, packaging, and verification.
- `skills/vectis` - portable agent guidance for using Vectis.

## Taste
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ A background service on your Mac owns the VMs, disks and credentials. When GitHu

## Self-hosting and building from source

Vectis is MIT licensed and every part of it is in this repository: the service, the apps, the Convex backend, the website and the GitHub App manifest. See [Build from source](https://vectis.kerd.dev/docs/self-hosting/build-from-source) and [Run your own cloud](https://vectis.kerd.dev/docs/self-hosting/cloud).
Vectis is MIT licensed and every part of it is in this repository: the service, the apps, the cloud Worker, the website and the GitHub App manifest. See [Build from source](https://vectis.kerd.dev/docs/self-hosting/build-from-source) and [Run your own cloud](https://vectis.kerd.dev/docs/self-hosting/cloud).

```sh
pnpm install
Expand Down
15 changes: 7 additions & 8 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,21 @@ Rulesets need a public repository on the free plan. After the repository is publ
| Secret | `APPLE_API_KEY_P8` | Contents of the App Store Connect API key (`.p8`) |
| Variable | `APPLE_API_KEY_ID`, `APPLE_API_ISSUER` | The API key ID and issuer ID |
| Variable | `MACOS_SIGNING_IDENTITY` | For example `Developer ID Application: Name (TEAMID)` |
| Variable | `CONVEX_URL`, `VECTIS_WEB_URL` | The production deployment baked into the apps |
| Variable | `VECTIS_CLOUD_URL`, `VECTIS_WEB_URL` | The production deployment baked into the apps |

`production` (deployment branches and tags: `main`, `v*`):

| Kind | Name | Value |
| -------- | -------------------------------------------------------- | ----------------------------------------------- |
| Secret | `CONVEX_DEPLOY_KEY` | Production deploy key from the Convex dashboard |
| Secret | `CLOUDFLARE_API_TOKEN` | Token that can deploy Workers on the account |
| Variable | `CLOUDFLARE_ACCOUNT_ID` | Cloudflare account ID |
| Variable | `CONVEX_URL`, `CONVEX_SITE_URL`, `CLERK_PUBLISHABLE_KEY` | Production values for the website build |
| Kind | Name | Value |
| -------- | ------------------------------------------- | ------------------------------------------------------ |
| Secret | `CLOUDFLARE_API_TOKEN` | Token that can deploy Workers and migrate D1 databases |
| Variable | `CLOUDFLARE_ACCOUNT_ID` | Cloudflare account ID |
| Variable | `VECTIS_CLOUD_URL`, `CLERK_PUBLISHABLE_KEY` | Production values for the website build |

## Cut a release

1. Open a pull request titled `chore(release): vX.Y.Z` that only changes `version` in the root `package.json`. Merge it.
2. Tag the merge commit and push the tag: `git tag vX.Y.Z && git push origin vX.Y.Z`. A tag with a hyphen, such as `v0.2.0-rc.1`, becomes a prerelease.
3. Approve the `release` deployment. The workflow checks the tag against the version, runs `check`, deploys the Convex backend, builds, signs and notarizes the app in the macOS VM, publishes the GitHub release with generated notes, and redeploys the website so the changelog and download pick up the release.
3. Approve the `release` deployment. The workflow checks the tag against the version, runs `check`, applies D1 migrations and deploys the cloud Worker, builds, signs and notarizes the app in the macOS VM, publishes the GitHub release with generated notes, and redeploys the website so the changelog and download pick up the release.
4. Installed apps find the update within four hours.

Backend changes must keep working with the previous release, because installed apps update at their own pace.
4 changes: 2 additions & 2 deletions apps/cli/src/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ test("machine configure settles without --wait and status prints runner capacity
"--max-runners",
"3",
"--memory-budget-mib",
"8192",
"2048",
"--key",
"cli-configure",
"--json",
Expand All @@ -99,7 +99,7 @@ test("machine configure settles without --wait and status prints runner capacity
);
expect(server.service.snapshot().machine).toMatchObject({
maxRunners: 3,
memoryBudgetMiB: 8192,
memoryBudgetMiB: 2048,
});
output.mockClear();
vi.resetModules();
Expand Down
Loading
Loading