From 7ad26e647ee3aad0462c35fc59a877fc55a82367 Mon Sep 17 00:00:00 2001 From: ilay alog Date: Thu, 3 Sep 2026 18:54:04 -0400 Subject: [PATCH 01/17] Add per-machine budgets, idle resource reclamation, multi-tab lock, and ops runbook - Enforce rolling 24-hour resource budgets (2h remote browser, 2h container) with transparent server-side alarm multiplexing and structured error codes (EBUDGET, EIDLE, EOWNER, ECAPACITY). - Implement server-side idle reclamation: remote Chrome closes after 5m of inactivity/tab concealment; containers stop 5m after last exec. - Browser Worker: introduce BrowserLease Durable Object, serialize concurrent creates, retain and retry failed setup rollback, and rate-limit per IP and subject. - Computer Worker: introduce RuntimeLease Durable Object, multiplex container alarms, cap single write and batch request sizes, and sandbox published site origins. - Web client: introduce machine lock with take-over support, gate keyboard and tool access via inert container on inactive tabs, and stream activity heartbeats. - CI/CD & Docs: add GitHub Actions workflows, issue/PR templates, CONTRIBUTING.md, SECURITY.md, docs/OPERATIONS.md, and updated self-hosting guides. --- .github/ISSUE_TEMPLATE/bug_report.md | 39 ++ .github/ISSUE_TEMPLATE/feature_request.md | 25 ++ .github/PULL_REQUEST_TEMPLATE.md | 23 ++ .github/workflows/ci.yml | 91 +++++ .github/workflows/deploy-workers.yml | 123 +++++++ CONTRIBUTING.md | 133 +++++++ README.md | 36 +- SECURITY.md | 105 ++++++ docs/OPERATIONS.md | 404 +++++++++++++++++++++ docs/SELF_HOSTING.md | 65 +++- docs/agent-skills/browser.md | 23 +- docs/agent-skills/cloud.md | 24 +- docs/features/cloud-kernel.md | 12 + docs/features/session-restore-presence.md | 5 + docs/features/shared-browser.md | 52 +-- shared/session-limits.ts | 172 +++++++++ web/.env.example | 7 +- web/e2e/desktop.e2e.ts | 3 + web/e2e/fakeComputer.ts | 3 + web/server/index.js | 24 +- web/server/index.test.ts | 12 + web/src/apps/browser/cdp.ts | 5 +- web/src/apps/browser/session.test.ts | 268 +++++++++++++- web/src/apps/browser/session.ts | 223 +++++++++++- web/src/desktop/Desktop.tsx | 129 ++++--- web/src/kernel/activity.test.ts | 34 ++ web/src/kernel/activity.ts | 111 ++++++ web/src/kernel/cloudExec.test.ts | 73 ++++ web/src/kernel/cloudExec.ts | 25 +- web/src/kernel/fs.ts | 2 + web/src/kernel/machineLock.test.ts | 108 ++++++ web/src/kernel/machineLock.ts | 76 +++- web/src/kernel/manualContent.ts | 4 +- web/src/styles/desktop.css | 24 +- web/src/tools/agentAction.test.ts | 16 + web/src/tools/agentAction.ts | 3 + web/src/tools/browserTools.ts | 6 +- web/src/tools/cloudExec.test.ts | 17 + web/vite.config.ts | 18 +- workers/browser-session/src/index.test.ts | 215 +++++++++-- workers/browser-session/src/index.ts | 331 ++--------------- workers/browser-session/src/lease.test.ts | 288 +++++++++++++++ workers/browser-session/src/lease.ts | 333 +++++++++++++++++ workers/browser-session/src/upstream.ts | 188 ++++++++++ workers/browser-session/src/worker.ts | 200 ++++++++++ workers/browser-session/wrangler.jsonc | 53 ++- workers/computer/package.json | 3 +- workers/computer/smoke-live.ts | 85 +++++ workers/computer/src/alarms.ts | 70 ++++ workers/computer/src/handler.ts | 243 +++++++++++-- workers/computer/src/index.test.ts | 228 +++++++++++- workers/computer/src/index.ts | 99 ++++- workers/computer/src/runtimeLease.test.ts | 123 +++++++ workers/computer/src/runtimeLease.ts | 163 +++++++++ workers/computer/src/sessionLimits.test.ts | 90 +++++ workers/computer/src/syncRetry.test.ts | 67 +++- workers/computer/src/syncRetry.ts | 32 +- workers/computer/wrangler.jsonc | 76 +++- 58 files changed, 4813 insertions(+), 597 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/deploy-workers.yml create mode 100644 CONTRIBUTING.md create mode 100644 SECURITY.md create mode 100644 docs/OPERATIONS.md create mode 100644 shared/session-limits.ts create mode 100644 web/src/kernel/activity.test.ts create mode 100644 web/src/kernel/activity.ts create mode 100644 web/src/kernel/cloudExec.test.ts create mode 100644 web/src/tools/agentAction.test.ts create mode 100644 workers/browser-session/src/lease.test.ts create mode 100644 workers/browser-session/src/lease.ts create mode 100644 workers/browser-session/src/upstream.ts create mode 100644 workers/browser-session/src/worker.ts create mode 100644 workers/computer/smoke-live.ts create mode 100644 workers/computer/src/alarms.ts create mode 100644 workers/computer/src/runtimeLease.test.ts create mode 100644 workers/computer/src/runtimeLease.ts create mode 100644 workers/computer/src/sessionLimits.test.ts diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..3aee933 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,39 @@ +--- +name: Bug report +about: Something is broken in the OS, a Worker, or the site +title: "" +labels: bug +assignees: "" +--- + +## What happened + + + +## Steps to reproduce + +1. +2. +3. + +## Expected + +## Where + +- [ ] `web/` (the OS in the tab) +- [ ] `workers/browser-session` +- [ ] `workers/computer` +- [ ] `shared/` (capability / limits contract) +- [ ] Hosted site (`https://computer.webmcp.com`) + +## Environment + +- Browser and version (Chrome 151+ with `--enable-features=WebMCP`, or ChatGPT's browser): +- OS: +- Commit / URL: +- Tool call involved (wire name, e.g. `cloud_exec`), if any: + +## Evidence + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..e73920d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,25 @@ +--- +name: Feature request +about: Propose a new tool, app, or capability +title: "" +labels: enhancement +assignees: "" +--- + +## Problem + + + +## Proposal + + + +## Alternatives considered + +## Scope + +- [ ] `web/` only (runs in the tab, no paid resources) +- [ ] Needs a Worker change (`workers/browser-session` or `workers/computer`) +- [ ] Changes the shared contract (`shared/`), i.e. requires a coordinated redeploy +- [ ] Changes resource budgets or cost exposure diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..1fe6387 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,23 @@ +## What + + + +## Why + +## How to verify + + + +## Checklist + +- [ ] Tests pass locally for every package I touched + (`cd web && bun test src server`, `cd workers/computer && bun test src`, + `cd workers/browser-session && bun test src`) +- [ ] Typecheck passes (`bunx tsc --noEmit` in each touched package) +- [ ] `cd web && bun run build` passes if `web/` changed +- [ ] New or changed tools use `snake_case` wire names and declare an invocation class +- [ ] Tests live next to the code they cover +- [ ] No secrets, account IDs, tokens, or user data in the diff +- [ ] `shared/` changes: both Workers and the site are updated together and + `docs/OPERATIONS.md` / `docs/SELF_HOSTING.md` reflect new limits +- [ ] Docs updated (`README.md`, `docs/features/`, `docs/agent-skills/`) where behaviour changed diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9838778 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,91 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + BUN_VERSION: "1.3" + +jobs: + web: + name: web (test, typecheck, build) + runs-on: ubuntu-latest + defaults: + run: + working-directory: web + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: ${{ env.BUN_VERSION }} + - name: Cache Bun + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-web-${{ hashFiles('web/bun.lock') }} + restore-keys: | + bun-${{ runner.os }}-web- + - run: bun install --frozen-lockfile + - name: Test + run: bun test src server + - name: Typecheck + run: bunx tsc --noEmit + - name: Build + run: bun run build + + computer: + name: workers/computer (test, typecheck) + runs-on: ubuntu-latest + defaults: + run: + working-directory: workers/computer + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: ${{ env.BUN_VERSION }} + - name: Cache Bun + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-computer-${{ hashFiles('workers/computer/bun.lock') }} + restore-keys: | + bun-${{ runner.os }}-computer- + - run: bun install --frozen-lockfile + - name: Test + run: bun test src + - name: Typecheck + run: bunx tsc --noEmit + + browser-session: + name: workers/browser-session (test, typecheck) + runs-on: ubuntu-latest + defaults: + run: + working-directory: workers/browser-session + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: ${{ env.BUN_VERSION }} + - name: Cache Bun + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-browser-session-${{ hashFiles('workers/browser-session/bun.lock') }} + restore-keys: | + bun-${{ runner.os }}-browser-session- + - run: bun install --frozen-lockfile + - name: Test + run: bun test src + - name: Typecheck + run: bunx tsc --noEmit diff --git a/.github/workflows/deploy-workers.yml b/.github/workflows/deploy-workers.yml new file mode 100644 index 0000000..8bb7311 --- /dev/null +++ b/.github/workflows/deploy-workers.yml @@ -0,0 +1,123 @@ +name: Deploy Workers + +# Manual deploy of the Cloudflare Workers. The GitHub environment is named after the +# `environment` input, so protection rules (required reviewers, branch restrictions) +# configured on the `staging` / `production` environments apply automatically. +# +# Required environment secrets: +# CLOUDFLARE_API_TOKEN - Wrangler deploy token (Workers Scripts, Containers, R2, +# Durable Objects edit on the nekuda.ai account) +# CLOUDFLARE_ACCOUNT_ID - target Cloudflare account (keep it in each GitHub environment) +# +# Runtime secrets (GATEWAY_SIGNING_SECRET, CF_ACCOUNT_ID, BROWSER_RENDERING_API_TOKEN, +# PUBLIC_SITE_ORIGIN) are NOT set here. Set them once with `wrangler secret put`; see +# docs/OPERATIONS.md. + +on: + workflow_dispatch: + inputs: + environment: + description: Target environment + type: choice + required: true + default: staging + options: + - staging + - production + worker: + description: Which Worker(s) to deploy + type: choice + required: true + default: both + options: + - both + - computer + - browser-session + +concurrency: + group: deploy-${{ inputs.environment }} + cancel-in-progress: false + +permissions: + contents: read + +env: + BUN_VERSION: "1.3" + WRANGLER_VERSION: "4.128.0" + # Top-level config is production; `--env staging` selects the staging env block. + WRANGLER_ENV_FLAG: ${{ inputs.environment == 'staging' && '--env staging' || '' }} + +jobs: + validate-ref: + name: Validate deployment ref + runs-on: ubuntu-latest + steps: + - name: Production deploys must use main + if: ${{ inputs.environment == 'production' && github.ref != 'refs/heads/main' }} + run: | + echo "Production deployments must be dispatched from main." >&2 + exit 1 + + computer: + name: Deploy webmcp-computer-cloud (${{ inputs.environment }}) + needs: validate-ref + if: ${{ inputs.worker == 'both' || inputs.worker == 'computer' }} + # ubuntu-latest ships Docker; the computer Worker builds workers/computer/Dockerfile + # at deploy time. + runs-on: ubuntu-latest + environment: ${{ inputs.environment }} + defaults: + run: + working-directory: workers/computer + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: ${{ env.BUN_VERSION }} + - name: Cache Bun + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-computer-${{ hashFiles('workers/computer/bun.lock') }} + restore-keys: | + bun-${{ runner.os }}-computer- + - run: bun install --frozen-lockfile + - name: Check (tests + typecheck) + run: bun run check + - name: Docker is available + run: docker version + - name: Deploy + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + run: bunx wrangler@${{ env.WRANGLER_VERSION }} deploy ${{ env.WRANGLER_ENV_FLAG }} + + browser-session: + name: Deploy webmcp-computer-browser-session (${{ inputs.environment }}) + needs: validate-ref + if: ${{ inputs.worker == 'both' || inputs.worker == 'browser-session' }} + runs-on: ubuntu-latest + environment: ${{ inputs.environment }} + defaults: + run: + working-directory: workers/browser-session + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: ${{ env.BUN_VERSION }} + - name: Cache Bun + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-browser-session-${{ hashFiles('workers/browser-session/bun.lock') }} + restore-keys: | + bun-${{ runner.os }}-browser-session- + - run: bun install --frozen-lockfile + - name: Check (tests + typecheck) + run: bun run check + - name: Deploy + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + run: bunx wrangler@${{ env.WRANGLER_VERSION }} deploy ${{ env.WRANGLER_ENV_FLAG }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ca58504 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,133 @@ +# Contributing + +WebMCP Computer is MIT licensed. By contributing you agree your work is released under +the same licence (see `LICENSE`). + +## Layout + +| Path | What | Package root | +| --- | --- | --- | +| `web/` | The OS: Vite + React 18 app, plus the `/api/session` broker in `web/server/` | yes | +| `workers/browser-session/` | Token-holding Browser Run session Worker | yes | +| `workers/computer/` | Cloud workspace Worker: DO SQLite filesystem, Container, R2 publishing | yes | +| `shared/` | Capability and budget contracts imported by all three | no (imported by path) | +| `docs/` | Brief, feature specs, seeded agent manual, testing charter, WebMCP reference | no | + +Three independent package roots, three lockfiles. There is no root workspace. + +## Setup + +Requires Bun 1.3.x. + +```sh +cd web && bun install --frozen-lockfile && cd .. +cd workers/computer && bun install --frozen-lockfile && cd ../.. +cd workers/browser-session && bun install --frozen-lockfile && cd ../.. +``` + +Run the OS locally: + +```sh +cd web && bun run dev # http://localhost:5173 +``` + +Native WebMCP needs Chrome 151+ started with `--enable-features=WebMCP`, or ChatGPT's +browser. Without it the OS still renders and tools still register through the SDK's +fallback, but agent calls will not arrive. + +## Tests and typecheck + +Run exactly what CI runs (`.github/workflows/ci.yml`): + +```sh +cd web && bun test src server && bunx tsc --noEmit && bun run build +cd workers/computer && bun test src && bunx tsc --noEmit # or: bun run check +cd workers/browser-session && bun test src && bunx tsc --noEmit # or: bun run check +``` + +`bun run check` in each Worker runs tests then `tsc --noEmit`. + +End-to-end tests (`cd web && bun run test:e2e`) need a local Chrome with native WebMCP; see +`docs/testing/README.md`. They are not run in CI. + +## Conventions + +- **TypeScript strict** everywhere. No `any` unless quarantined with a comment. +- **Wire names are `snake_case`** for tools, tool arguments, and JSON fields sent over the + network (`cloud_exec`, `retry_after_ms` style). Internal TypeScript uses `camelCase`. +- **Every tool declares an invocation class** via annotations: `ask` (read-only), `act` + (reversible, visible), `transact` (consequential). Register through + `@nekuda/webmcp-sdk` (`defineTool` + `registerTools`); return errors as MCP + `{ content, isError: true }` results, not thrown exceptions. +- **Tests next to code.** `foo.ts` is covered by `foo.test.ts` in the same directory. + Bun's test runner picks them up from `src` (and `server` in `web/`). +- **No UI kit.** Hand-rolled CSS, React 18, Zustand. Do not add component libraries. +- **Shared contract changes are coordinated.** Anything in `shared/` is imported by the + site and both Workers; change it together and note it in the PR so both Workers are + redeployed with the site. +- **No secrets in source.** Wrangler configs declare required secrets; values are set with + `wrangler secret put`. Never put a secret behind a `VITE_` variable. +- **Docs are part of the change.** Feature behaviour lives in `docs/features/`; anything an + agent needs to know lives in `docs/agent-skills/` (seeded into `~/skills/` in the OS). +- Keep the Cloudflare pins: `bunx wrangler@4.128.0` and `@cloudflare/computer` at the + version in `workers/computer/package.json`. + +## Running the OS against staging Workers + +The local Vite server serves `/api/session` itself (see `sitesLocalApi` in +`web/vite.config.ts`). It mints capabilities with +`WEBMCP_COMPUTER_DEV_GATEWAY_SIGNING_SECRET` and hands out +`VITE_BROWSER_WORKER_URL` / `VITE_COMPUTER_WORKER_URL`. + +Copy `web/.env.example` to `web/.env` and set: + +```sh +# web/.env +WEBMCP_COMPUTER_DEV_GATEWAY_SIGNING_SECRET= +VITE_BROWSER_WORKER_URL=https://browser-staging.webmcp.com +VITE_COMPUTER_WORKER_URL=https://cloud-staging.webmcp.com +``` + +The dev secret must equal the `GATEWAY_SIGNING_SECRET` set on the staging Workers, or every +Worker call returns 401. Ask a maintainer for it; never commit it. `web/.env` is +gitignored. + +Alternatively, point at local Workers: run `bunx wrangler@4.128.0 dev` in each Worker +directory (ports 8787 and 8788 by default), leave `web/.env` at the example defaults, and +set the same dev secret on both Workers via `.dev.vars`. + +You can also override Worker URLs at runtime with `?browser_worker=` / +`?computer_worker=` query parameters or the `webmcp_computer.browser_worker` / +`webmcp_computer.computer_worker` localStorage keys. Production accepts these only when +they point at `127.0.0.1` or `localhost`. + +## Pull requests + +- Branch from `main`; target `main`. Keep PRs focused; split unrelated changes. +- Fill in `.github/PULL_REQUEST_TEMPLATE.md`. The checklist there is the review gate. +- CI must be green on all three jobs. +- Changes to Worker names, rate-limit namespace IDs, R2 buckets, `max_instances`, or + anything in `shared/session-limits.ts` need a maintainer review and an + `docs/OPERATIONS.md` update. +- Deploys are manual (`.github/workflows/deploy-workers.yml`) and maintainer-only. + +## Reporting security issues + +See `SECURITY.md`. Do not open public issues for vulnerabilities. + +## Embedding the demo video in the README + +GitHub strips `