Scotty runs Pi in a persistent Cloudflare Sandbox workspace, presents its live worklog at an
authenticated /s/<id> URL, checkpoints the workspace to R2, and can resume, beam down, or
permanently destroy the session.
Browser CLI
owner/client cookie root bearer token
| |
+-------------------------+-----------------------------+
v
+---------------------------+
| Cloudflare Worker |
| Hono API + static assets |
| browser/CLI auth boundary |
+-----+-----------+---------+
| |
+---------+ +----------------------+
v v
+------------------------+ +-----------------------+
| Auth Durable Object | | Runner control plane |
| one owner + epoch | | registry + runner DOs |
| paired browser clients | +-----------+-----------+
+------------------------+ |
v
trusted Linux runner
(registration/control)
one Durable Object per Cloudflare session
|
v
+---------------------------+
| Sandbox Durable Object |
| AUTHORITATIVE |
| session state |
| credentials + lifecycle |
+-----+----------+----------+
| |
projects | | immutable checkpoints
v v
+-----------+ +-----------+
| KV | | R2 |
| non-secret| | backups |
| projection| +-----------+
+-----------+
|
v
+------------------------------------+
| Cloudflare Sandbox + Container |
| persistent workspace |
| scotty-pi-shell + Pi RPC worklog |
| session-bound sentinels only |
+------------------+-----------------+
|
v
+------------------------------------+
| ContainerProxy allowlisted egress |
| sentinel -> credential substitution |
+------------------+-----------------+
|
GitHub + Pi providers
The Sandbox Durable Object is the source of truth. KV is only a non-secret list, repository, and stats projection; R2 stores immutable backups. The Container application runs the workspace and Pi process but does not own session state or real credentials. The trusted-runner lane currently supports registration and lifecycle control; runner-backed session creation remains disabled until it has the native Pi RPC worklog transport.
worker/— Hono API, Sandbox Durable Object, credential-isolating egress proxy, direct Pi RPC lifecycle and worklog, and trusted-runner control plane.cli/— Effect-native Bun CLI, embeddedscotty tui, and embeddedscotty skillsguide.tui/— internal terminal fleet console and shared desktop-sidecar source modules; these compile into Scotty artifacts and are not a separately installed product.desktop/— macOS GPUI viewport for switching among existing warm Scotty sessions.assets/brand/— app icons, favicons, hero/social art, and agent glyphs.e2e/— direct static contract checks, the local-live harness and helper tests, deployed route checks, and an explicitly gated deployed canary.spikes/— executable probes for the upstream Sandbox contracts.
Repository code is untrusted. Real Pi provider and GitHub credentials stay in Worker secrets or per-session Durable Object storage. The container receives session-bound sentinels only. ContainerProxy replaces sentinels on allowlisted egress, sanitizes OAuth refresh responses before they return to the container, and denies all other outbound traffic.
Browser authority is separate from the root credential. SCOTTY_TOKEN is accepted only as a CLI
bearer and break-glass recovery credential; it is never accepted from a cookie, browser URL, or
?t=. The singleton Auth Durable Object stores exactly one owner client ID plus an ownership
epoch. Other browsers are standard clients. Pairing creates standard access, ownership transfer is
bound to one existing target browser, and root recovery revokes every browser credential before
creating a fresh owner. Raw client, pairing, transfer, and recovery secrets are never persisted.
For each paired device, the Auth Durable Object retains a server client ID, credential digest,
neutral or user-supplied label, scopes, created, expiry, and last-seen times, optional user agent,
and revocation time. The default scotty tui label contains no hostname.
The browser never receives container credentials. For Cloudflare sessions, the Worker authenticates the terminal WebSocket and attaches it to the Sandbox native PTY running Pi. Pi and Codex receive only session-bound provider and GitHub sentinels.
Residual limitation: any allowed package registry is still a potential source/prompt exfiltration channel. Keep ALLOWED_HOSTS in worker/src/egress.ts minimal for the target repository.
Read AGENTS.md before changing Scotty. It defines the public-contract, state,
credential, Effect v4, Alchemy, formatting, and verification invariants. When changing a
non-trivial Effect or Alchemy pattern, follow its source-first instructions before editing.
Requirements are the Node version pinned in .nvmrc, npm, Bun, and Git. Docker is
required only for the real local Sandbox loop, the Wrangler rollback probe, and production
deployment. Cloudflare authentication is required only for disposable deployed probes or
production deployment.
For a normal local checkout:
git submodule update --init vendor/effect vendor/alchemy
npm ci --no-audit --no-fund
npm run checkOn macOS, build the ad-hoc-signed development desktop bundle with:
npm run build:desktop
open dist/Scotty.appDesktop uses the same mode-0600 paired-client config as scotty tui at
~/.config/scotty/tui.json. See desktop/README.md for fixture testing,
pairing, platform requirements, and distribution limitations.
For a fresh Linux agent environment, .agents/setup installs the pinned Node
version, initializes the reference-source submodules, and runs npm ci. A resumed agent can use
.agents/resume to fail fast when its cached environment is incomplete:
./.agents/setup
./.agents/resumeStart with the smallest test that owns the contract. Keep it running while editing, format the touched files, then run the full gate once before handoff.
- Run the focused test once before editing to establish the baseline.
- Start its watch mode and make the smallest contract-preserving change.
- Format only the touched files while iterating.
- Rerun the focused test plus its nearest package suite.
- Run
npm run checkbefore handing off, committing, or opening a PR.
# Worker or Durable Object: one pass, then watch mode
npx vitest run worker/test/sandbox-runtime.test.ts
npx vitest worker/test/sandbox-runtime.test.ts
# Effect CLI, Bun CLI, Node E2E, or operations
npx vitest run cli/effect-test/command-tree.test.ts
bun test cli/test/cli.test.ts
npm run test:e2e:local-live:helpers
node --test scripts/sessions-shell.test.mjs
# Format touched files and finish with the complete repository gate
npx oxfmt --disable-nested-config --write README.md worker/src/sandbox-runtime.ts
npm run checkReplace these paths with the files under change. npm run check verifies pinned Effect and Pi
packages, formatting, lint, every typecheck and test suite, and the repository secret scan. The
default suites do not use Cloudflare, Pi provider, or GitHub credentials.
Use the repository-local lab when you need to run the real CLI against the production Worker in Wrangler local mode with Docker-backed Sandbox support:
npm run lab -- start
npm run lab -- exec RUN_ID -- doctor --json
npm run lab -- stop RUN_IDstart requires Docker, authenticated gh, Bun, and mode-0600 ~/.pi/agent/auth.json. It uses
isolated temporary Wrangler state and CLI HOME, passes a run-specific Wrangler worker name, and
prints a JSON run ID and host after /health passes. Commands forwarded through exec use the
actual CLI and can exercise real local Sandbox lifecycles; doctor alone does not create one.
stop removes only Sandbox containers named for that worker. The private .scotty-lab/run.json
manifest contains no credentials; ephemeral
mode-0600 files under the system temporary directory hold the generated root token, Wrangler
inputs, and redacted startup log until stop. exec forwards arguments directly to
cli/scotty.ts without a shell and preserves its stdio and exit status.
Every future complexity slice must follow the before/after representative-flow gate in
docs/scotty-lab.md and halt immediately on unexplained divergence.
When a change crosses the real Worker/Sandbox/Pi boundary, also run the local-live loop. It uses temporary Wrangler state and Docker containers and does not touch deployed Scotty resources:
DOCKER_HOST="unix://${HOME}/.colima/default/docker.sock" \
npm run test:e2e:local-live -- --no-open --no-holdThis requires a healthy Docker daemon, authenticated gh, and mode-0600
~/.pi/agent/auth.json. It proves both fresh-start auth hydration and warm-session reseeding. Add
--require-response only when the local network permits model traffic from the container.
A Wrangler dry run remains as a local rollback probe. It builds the Sandbox image and therefore requires a healthy Docker daemon:
npx wrangler deploy --dry-run --config worker/wrangler.jsoncFor interactive local Worker development only, use:
npx wrangler dev --config worker/wrangler.jsoncWrangler is not a production infrastructure or deployment path.
The standalone CLI owns installation. Run scotty init --name NAME; it asks Alchemy to authenticate
the selected Cloudflare profile, shows the target account and resources, deploys only after
confirmation, generates and uploads the root Worker secret without putting it in Alchemy state, and
stores the local pointer in mode-0600 ~/.scotty.json. The installation name is required and is
never inferred from a username, machine, repository, or Cloudflare account.
For a clean first run:
- Run
scotty init --name NAMEand confirm the displayed Cloudflare account and resource names. - Sign in to OpenAI or OpenAI Codex with Pi, then run
scotty auth sync. - Run
scotty doctor --json. - Run
scotty owner recoveron the browser that will own the installation. - Open
/devicesin that owner browser and create a one-use pairing link. - On each terminal or desktop device, run
scotty tui pair ORIGINand paste the link when asked. - Run
scotty tui, or build and open the desktop app.
auth sync uses the account, Worker name, and origin saved by init. It fails before reading local
Pi credentials if Cloudflare does not match that saved installation. The pairing prompt does not
echo its one-use credential. See desktop/README.md for the desktop build.
On a replacement machine, run scotty recover --name NAME. Cloudflare profile ownership is the
recovery authority. The CLI first discovers and displays the resource mapping. It rotates only the
root token after confirmation. It writes a mode-0600 recovery journal before the remote change, so
a stopped command can reuse the same token. A pre-existing deployment whose physical or Alchemy
logical names differ from the generic convention can be recovered with a private
--adoption-manifest PATH; .scotty-adoption.json is ignored by Git.
Use scotty deploy for normal updates. It reads the managed installation from ~/.scotty.json,
checks the current Docker context, and shows the Alchemy resource plan. It asks for confirmation
only when the plan has changes. A non-interactive deployment with changes needs --yes. Deployment
never generates or changes the root token. On interactive macOS, Scotty offers to start Colima when
the current Docker context is unavailable. It never changes DOCKER_HOST.
Use scotty uninstall to remove the Container application and both Workers. It removes the local
config only after the remote work succeeds. KV and R2 remain by default. Pass --delete-data only
when the session index and every backup must also be deleted. Both modes stop all active sessions.
Alchemy declares the Worker, Durable Objects, Container application, KV namespace, R2 bucket,
assets, bindings, migrations, and retained-resource policy. Defaults are derived from the
installation name: scotty-NAME-worker, scotty-NAME-runner, scotty-NAME-sandbox,
scotty-NAME-sessions, and scotty-NAME-backups. No Cloudflare account ID, workers.dev hostname,
Container UUID, or runner instance name is committed.
Production deploys are local-only and forward-only. The guarded wrapper refuses CI, any branch
other than main, a dirty worktree, or a local main that differs from origin/main.
-
Fast-forward a clean local
mainto the reviewed GitHub state.git switch main git fetch origin main git merge --ff-only origin/main git status --short --branch
-
Ensure Cloudflare authentication is available. Docker is required only for an intentional Container release. On macOS with Colima:
colima start default DOCKER_HOST="unix://${HOME}/.colima/default/docker.sock" docker info -
Run the guarded deployment for the installation. Omit
SCOTTY_ADOPTION_MANIFESTwhen the installation uses Scotty's default resource names. Production requires the installation's explicit Hatch/Evidence preview topology and always enables Evidence. Put that topology in the private adoption manifest, or provideSCOTTY_PREVIEW_BASEandSCOTTY_PREVIEW_ZONE_ID.DOCKER_HOST="unix://${HOME}/.colima/default/docker.sock" \ SCOTTY_INSTALLATION_NAME=home \ SCOTTY_ADOPTION_MANIFEST="${HOME}/.config/scotty/production-adoption.json" \ npm run deploy:production
The default command requires the Container plan to be a no-op and does not open Docker. When the release intentionally changes the Container image or configuration, review that plan and authorize it explicitly:
DOCKER_HOST="unix://${HOME}/.colima/default/docker.sock" \ SCOTTY_INSTALLATION_NAME=home \ SCOTTY_ADOPTION_MANIFEST="${HOME}/.config/scotty/production-adoption.json" \ npm run deploy:production -- --container
-
Require the command to finish successfully. It runs
npm run check, audits the current Container inventory, builds a dependency-minimal image context, and runs an Alchemy plan before applying anything. A normal release stops unlessSandboxContaineris a no-op. An explicitly authorized Container release waits for the exact rollout and health counters to converge. Both paths audit the deployed inventory again. -
Verify the connected installation from the freshly built CLI:
npm run build:cli ./dist/scotty doctor --json
Do not substitute a direct Wrangler production upload for this runbook. A Worker upload alone does
not prove that the Container rollout converged or that runtime inventory remained healthy. If the
guard fails, fix the reported Git, test, audit, or rollout condition and rerun the same command; do
not bypass it with a direct Alchemy or Wrangler deployment. Do not add --container merely to get
past a failed no-op check; first confirm that the image or Container configuration is intended to
change.
On an ARM Mac, the emulated linux/amd64 image build can rarely stop during npm ci with a
segmentation fault or exit code 139. Let the guarded command finish its rollout settlement and final
audit. If the guard proves that production remains healthy, rerun the same guarded command once.
There is no automatic retry because a failed deployment can leave unclear provider state. If the
second build fails, stop and diagnose the Docker VM or architecture emulation. Do not retry with a
direct Alchemy or Wrangler command.
Normal guarded output redacts Cloudflare account IDs, resource IDs, physical resource names, and Worker URLs. It also prints one short message when the Container image builds, artifacts upload, the Cloudflare update applies, the rollout settles, and the final audit runs.
The current Cloudflare gate is forward-only: the full local suite and Colima-backed image build must
pass with the pinned Pi version, then the guarded deployment and deployed canary must prove
beam up → Pi worklog → snapshot → resume → vaporize.
Install the current signed release on macOS or Linux with GitHub CLI:
case "$(uname -s)-$(uname -m)" in
Darwin-arm64) asset=scotty-darwin-arm64 ;;
Darwin-x86_64) asset=scotty-darwin-x64 ;;
Linux-aarch64 | Linux-arm64) asset=scotty-linux-arm64 ;;
Linux-x86_64) asset=scotty-linux-x64 ;;
*) echo "Unsupported platform" >&2; exit 1 ;;
esac
scotty_download_dir=$(mktemp -d)
gh release download --repo Yeshwanthyk/scotty --pattern "$asset" --dir "$scotty_download_dir"
mkdir -p "${HOME}/.local/bin"
install -m 0755 "$scotty_download_dir/$asset" "${HOME}/.local/bin/scotty"
"${HOME}/.local/bin/scotty" --versionAfter the first install, scotty upgrade verifies the signed release manifest and executable hash
before replacing the current binary. Add ${HOME}/.local/bin to PATH to invoke it as scotty.
Contributors can instead run npm run build:cli and use ./dist/scotty directly.
npm run build:cli
./dist/scotty init --name home
./dist/scotty recover --name home
./dist/scotty deploy
./dist/scotty doctor --json
./dist/scotty owner recover
./dist/scotty beam up "fix the failing tests" --title "Fix tests" --repo owner/project --provider cloudflare --json
./dist/scotty beam down SESSION_ID --json
./dist/scotty beam vaporize SESSION_ID --yes --json
./dist/scotty inspect SESSION_ID --json
./dist/scotty steer SESSION_ID "check the focused tests" --json
./dist/scotty upgrade
./dist/scotty uninstall
./dist/scotty skillsHatch is the authenticated live app for the current sandbox. The sandbox agent starts it with
scotty_hatch ensure and keeps that process running. Open it from the paired session shell's
Open Hatch control, or open the session URL with /hatch/open appended. Do not copy or share the
wildcard preview URL, handoff token, route nonce, or Hatch cookie.
Browser evidence uses a separate temporary server on a different port from Hatch. Before changing visible behavior, define one bounded flow with at most three observable checks. Run that exact viewport, action, and assertion graph once with video disabled. Make the change, then run the same graph with video enabled. Both runs use Scotty's fixed local evidence runner with headed Chromium on an isolated X display inside the sandbox. It captures each step as PNG and, when requested, ffmpeg records the same live pixels as WebM; it does not stitch screenshots or use rrweb replay. Stop only the temporary server and leave Hatch running.
The agent's latest update must include the exact structured scotty-hatch:<hatchId> reference and
both scotty-evidence:<jobId> references from the same conversation. Summary then shows the live
Hatch control and one private Showcase link. Showcase contains matched before/after screenshots,
passed assertions, and the actual WebM recorded by the after browser run. Do not blindly retry a
failed evidence run; change the failure cause or session state first.
For a trusted Linux VPS, first build or pull the pinned runtime image and sign in with gh.
Then run the repeatable user-service setup:
./dist/scotty runner setup \
--name "$RUNNER_NAME" \
--root /home/runner/.local/state/scotty-runner \
--image sha256:<64-lowercase-hex> \
--codex-auth /home/runner/.codex/auth.json \
--source-binary /absolute/path/to/dist/scottyinspect passively reads a warm session without starting or waking its container. steer takes a
fresh passive snapshot and submits one bounded prompt against that exact epoch and session revision;
stale or ambiguous outcomes are never retried automatically. On a local machine these commands use
the configured Worker and root bearer token. Inside a Scotty sandbox they instead use the exact
https://scotty.internal origin without loading or forwarding root credentials or source identity.
The source Sandbox Durable Object derives authority only from the Cloudflare container context and
allows a different target only when both authoritative session records have exactly the same
repository identity. This coordination is request-scoped; it has no mailbox or persisted
coordination state.
The installed cloudflare/sandbox:0.12.3 HTTPS interceptor's trust of the reserved origin cannot be
proven by local tests. A deployed same-repository inspect/steer canary remains a production gate; the
local suite is not production proof.
The command uses the installation in ~/.scotty.json, registers the required name with the
control plane, receives a one-time runner credential, imports the current GitHub CLI login,
installs runner-only credential files, writes and restarts the hardened systemd user service, and
fails if the service is not active. Pass --replace only when moving or reinstalling an existing
runner; that rotates its credential and disconnects the old machine. Use scotty runner list to
inspect registrations and scotty runner remove NAME --yes after all assigned sessions are gone.
The runner credential is never accepted as a command argument or stored in Worker configuration.
Runner-backed session creation remains disabled until the runner link has a native Pi RPC worklog
transport; registration and lifecycle control are intentionally available first.
Run scotty owner recover once on the intended primary browser after a fresh deployment or when
moving to a replacement laptop. Keep SCOTTY_TOKEN in a password manager or another protected
recovery location. scotty attach <id> opens the Pi worklog at the clean session URL and requires
an already paired browser. Sleeping sessions must be resumed from Home before the worklog opens.
The repository does not ship a fake or default offline E2E suite. The direct static contract and
local-live helper tests are included in npm run test:all:
npm run test:e2e:static
npm run test:e2e:local-live:helpersThe real local Worker, Sandbox, and Pi loop is explicit and requires Docker, gh auth, and Pi
credentials; see e2e/README.md:
npm run test:e2e:local-live -- --no-open --no-holdThe non-mutating deployed route check and destructive deployed canary are also explicit:
npm run test:e2e:deployed-routes
npm run test:e2e:deployedThe canary uses the stage-isolated spikes/infra/full-stack-canary.run.ts stack and requires every
stage-scoped gate documented in e2e/README.md. Its production Worker host check fails closed.
