Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
126 commits
Select commit Hold shift + click to select a range
f991436
Cache installs, bound job time, and check shell scripts and compose i…
ishu86 Sep 24, 2026
80d4932
Keep pre-release tags off latest and pass image metadata through the …
ishu86 Sep 24, 2026
85eada4
Build the images on pull requests that can break them.
ishu86 Sep 24, 2026
b55ddcb
Audit the pinned Python and npm dependencies weekly and when a pin ch…
ishu86 Sep 24, 2026
b34ff92
Check Host and Origin on the MCP HTTP door.
ishu86 Sep 24, 2026
f6b9e77
Let MCP optional parameters accept an explicit null.
ishu86 Sep 24, 2026
e6eb2bf
Run MCP tools in a worker thread so one slow call does not stall the …
ishu86 Sep 24, 2026
9c2894d
Drop an unused headers parameter and a dead branch in the MCP server.
ishu86 Sep 24, 2026
c1121c6
Let the no-credential-write test allow a read-only credential tool.
ishu86 Sep 24, 2026
a4f710b
Count overlapping injections and refuse routes that raced one.
ishu86 Sep 24, 2026
e64824b
Give deskd's /login longer than its own worst case before cased gives…
ishu86 Sep 24, 2026
545eba3
Return from /exec when bash exits and kill the whole command on timeout.
ishu86 Sep 24, 2026
67aaf9d
Answer bad deskd input with 400 and report failed navigations as such.
ishu86 Sep 24, 2026
cd1ee55
Read the watchdog's page text and URL in one evaluate.
ishu86 Sep 24, 2026
cc3babf
Share page-target discovery between Tab and the capture worker.
ishu86 Sep 24, 2026
950f9c6
Export ~/.case/env in bin/case and survive an empty auth array on bas…
ishu86 Sep 24, 2026
810d69f
Ignore node_modules and Python caches at any depth in the build context.
ishu86 Sep 24, 2026
a4183ca
Start Xvfb at depth 24 whatever depth DESK_RESOLUTION names.
ishu86 Sep 24, 2026
7891f75
Trap TERM from the first line of start.sh and fit shutdown into 10s.
ishu86 Sep 24, 2026
3916223
Pin the rest of deskd's transitive dependencies.
ishu86 Sep 24, 2026
647df12
Write pip.conf before the desktop COPYs and describe the LOOK re-seed.
ishu86 Sep 24, 2026
8e0a8b3
Ship tzdata with the control plane.
ishu86 Sep 24, 2026
99843c6
Let the case cursor theme inherit Adwaita.
ishu86 Sep 24, 2026
acffd71
Say in the README that a proxy in front of MCP needs its hostname all…
ishu86 Sep 24, 2026
1868721
Keep the Drive tests off the developer's threads.json.
ishu86 Sep 24, 2026
6a2e196
Run every Drive test file from disk instead of a hand-kept list.
ishu86 Sep 24, 2026
75f6b4e
Keep one aborted request or a down cased from killing Drive.
ishu86 Sep 24, 2026
16d49eb
Act on the picked computer in Files, Credentials, Schedules and teach.
ishu86 Sep 24, 2026
48aabf8
Hand ntfy messages off without waiting for the turn they start.
ishu86 Sep 24, 2026
409d128
Honor retry-after from the SDKs' Headers object.
ishu86 Sep 24, 2026
670d280
Make withRateRetry the only retry policy for provider requests.
ishu86 Sep 24, 2026
c455c20
Clip live Anthropic tool results the way history clips them.
ishu86 Sep 24, 2026
9fe25f5
Bound Anthropic turns by billed tokens and warn them at 80% too.
ishu86 Sep 24, 2026
e088a3d
Show the model the png that handoff_get and click return.
ishu86 Sep 24, 2026
ba39bbb
Drop a failed round's partial output before it is replayed.
ishu86 Sep 24, 2026
c8a01ee
Answer a refused /live upgrade and forward the bytes behind it.
ishu86 Sep 24, 2026
a9e1010
Escape the schedules error before putting it in innerHTML.
ishu86 Sep 24, 2026
c852a34
Keep steers and queued prompts on the running thread, not the viewed …
ishu86 Sep 24, 2026
d5c487c
Mark the Drive token cookie Secure when it is set over HTTPS.
ishu86 Sep 24, 2026
0be51b5
Refuse an over-cap file before reading all of it.
ishu86 Sep 24, 2026
54763d5
Stop histTrim re-serializing the whole history on every pass.
ishu86 Sep 24, 2026
677c0df
Read each screenshot and attachment once per turn, not once per round.
ishu86 Sep 24, 2026
97eb076
Parse the streaming reply once a frame and repaint the sidebar on cha…
ishu86 Sep 24, 2026
f05c56e
Remove the CASE_LOCAL flag, which nothing read.
ishu86 Sep 24, 2026
bb44e29
Let the model pass button to computer_action.
ishu86 Sep 24, 2026
ea63623
Drop exports and ids nothing uses, and a comment naming a gone file.
ishu86 Sep 24, 2026
6088278
Show failed tools as failed when a thread is reopened.
ishu86 Sep 24, 2026
4ee8a5c
Say where Drive's threads.json really lives by default.
ishu86 Sep 24, 2026
5aae0b0
Keep a lost wake race from deleting the computer's volume.
ishu86 Sep 24, 2026
b03d7ec
Stop the container when a wake fails after starting it.
ishu86 Sep 24, 2026
c994b16
Make admission and the claim that counts against it one step.
ishu86 Sep 24, 2026
2502fc7
Pass the usage stats opt-out through compose.
ishu86 Sep 24, 2026
1227d2a
Leave deleted computers out of the usage stats count.
ishu86 Sep 24, 2026
1a12880
Keep a schedule row that no longer computes from raising every sweep.
ishu86 Sep 24, 2026
b385a24
Let the last schedule run on a computer be the one that sleeps it.
ishu86 Sep 24, 2026
0b20c77
Keep the session keeper off a box someone started using mid-probe.
ishu86 Sep 24, 2026
d37629d
Notify on a failed session check instead of opening a stuck login.
ishu86 Sep 24, 2026
757bf58
Treat an empty numeric env line as unset.
ishu86 Sep 24, 2026
df74363
Answer a non-integer timeout_s or ref with a 400, not a 500.
ishu86 Sep 24, 2026
40ef1ea
Serve a schedule's runs through run_json and 404 an unknown schedule.
ishu86 Sep 24, 2026
96e8290
Resolve run owners from the name map even when it is empty.
ishu86 Sep 24, 2026
a18c18f
Raise a blocker again once its handoff expires, and poll each desk al…
ishu86 Sep 24, 2026
99cafc2
Run each sweeper step on its own so one failure can't starve the rest.
ishu86 Sep 24, 2026
53b1c00
Look up the live relay's computer off the event loop.
ishu86 Sep 24, 2026
be95029
Count the live view and the desk door as activity on a computer.
ishu86 Sep 24, 2026
06ffc4c
Say when destroy leaves a volume behind, and label new volumes.
ishu86 Sep 24, 2026
ec13bc8
Drop the Docker ping before every call and reconcile from one listing.
ishu86 Sep 24, 2026
0385a1c
Reuse deskd connections with one requests Session per thread.
ishu86 Sep 24, 2026
0a74ea5
Index runs by schedule and count computers in SQL for /health.
ishu86 Sep 24, 2026
0bd72ae
Stop desktops in parallel when cased shuts down.
ishu86 Sep 24, 2026
c0a7450
Drop the index statements _migrate repeated from SCHEMA.
ishu86 Sep 24, 2026
9dec92c
Name audit files by the UTC day, like their lines and the prune.
ishu86 Sep 24, 2026
a2ef4fd
Bound SSE subscriber queues and let emit outlive the event loop.
ishu86 Sep 24, 2026
e0d4a36
Start the stock schedule brain where case-mcp.json's paths resolve.
ishu86 Sep 24, 2026
ad5478c
Pass the solver proxy, brain timeout and session keeper settings thro…
ishu86 Sep 24, 2026
233ee38
Give the Drive pages an SPDX header and check them in CI.
ishu86 Sep 24, 2026
c3b64e7
Keep deskd's pins in one file and test deskd against them in CI.
ishu86 Sep 24, 2026
7c297c8
Accept only approve or deny as an approval answer.
ishu86 Sep 24, 2026
5b8ba47
Fail a handoff on the human's deny, not on a page saying denied.
ishu86 Sep 24, 2026
dd248b6
Check a submitted code against the page text only.
ishu86 Sep 24, 2026
7a1bc7d
Report a refused code from /auth/submit_challenge.
ishu86 Sep 24, 2026
e5f1d5a
Define the attempt and handoff status sets once, in store.
ishu86 Sep 24, 2026
264d507
Let the active-attempt queries use their indexes, and prune old attem…
ishu86 Sep 24, 2026
7ca69e9
Keep why an attempt failed.
ishu86 Sep 24, 2026
e655bcd
Close an attempt's open challenge whenever the attempt ends.
ishu86 Sep 24, 2026
bc01705
Stop raise_challenge from orphaning or churning challenge handoffs.
ishu86 Sep 24, 2026
74cc929
Type an auto TOTP code at most once per 30s window per attempt.
ishu86 Sep 24, 2026
8624dbe
Check the page gate before advancing a desk-solved page challenge.
ishu86 Sep 24, 2026
2d8090b
Raise a late "check your email" wall as a device challenge.
ishu86 Sep 24, 2026
9154cd3
Judge an advance-path captcha solve by the gate when deskd holds no l…
ishu86 Sep 24, 2026
6095871
Keep a click's result when its follow-up snapshot fails.
ishu86 Sep 24, 2026
7fb8f7c
Refuse wait_for with network_idle plus a selector or text.
ishu86 Sep 24, 2026
bf3518f
Name the stored element the same way the walk names it.
ishu86 Sep 24, 2026
9ef6ac2
Quote the proof selector with json.dumps.
ishu86 Sep 24, 2026
45f5838
Correct the comment on who can mint links.
ishu86 Sep 24, 2026
2dfcda7
Claim a challenge in one write.
ishu86 Sep 24, 2026
5b3bcd0
Upload files to the page in 256 KiB chunks.
ishu86 Sep 24, 2026
6bfbadc
Parse the Cookie header in one place.
ishu86 Sep 24, 2026
ff0b604
Use handoffs' continuation and attempt-id helpers in assist.
ishu86 Sep 24, 2026
846e88b
Read a VERIFY_JS answer in one place.
ishu86 Sep 24, 2026
e6e22c2
Record a login outcome through one helper.
ishu86 Sep 24, 2026
45c60e5
Drop the unused captcha Solver scaffolding.
ishu86 Sep 24, 2026
7b40503
Send the same headers on every Assist gone and done page.
ishu86 Sep 24, 2026
af626a5
Reflow the host-install settings paragraph in the README.
ishu86 Sep 24, 2026
aff1829
Give each unit test process its own CASE_HOME.
ishu86 Sep 24, 2026
fa72206
Run every unit test file through one shared runner.
ishu86 Sep 24, 2026
78fd113
Share the test fixtures that several files copied.
ishu86 Sep 24, 2026
e2d319c
Name the interval at which an auth wait re-reads the attempt.
ishu86 Sep 24, 2026
d436f8d
Stop the captcha, navigate and browse tests from sleeping for real.
ishu86 Sep 24, 2026
13bbfe3
Drop dead code from the captcha tests and check an unverified login e…
ishu86 Sep 24, 2026
3696339
Lint the Python tree in CI.
ishu86 Sep 24, 2026
b1fc275
Say in CONTRIBUTING that unit tests run one file per process.
ishu86 Sep 24, 2026
a4dd1bf
Say in the README that an approval takes only approve or deny.
ishu86 Sep 24, 2026
45d3c2d
Describe the injection gate's check on routes that were already running.
ishu86 Sep 24, 2026
d4140dc
Audit cased's and deskd's pins in separate runs.
ishu86 Sep 24, 2026
1d38fdc
Pin shellcheck for CI and spell out bin/case's colima stop as an if.
ishu86 Sep 24, 2026
40f7003
Refuse a resume or challenge submit while another injection is typing.
ishu86 Sep 24, 2026
504cd94
Send a message typed mid-turn to the thread it was typed into.
ishu86 Sep 24, 2026
a3d3b12
Let go of an /exec's pipes once it returns, even if a background job …
ishu86 Sep 24, 2026
804e80d
Keep the scheduler lock off a computer's final sleep.
ishu86 Sep 24, 2026
fe85d33
Send a queued prompt only once its own thread is on screen.
ishu86 Sep 24, 2026
f7493a0
Hold a prompt for a thread until that thread has finished loading.
ishu86 Sep 24, 2026
e69986f
Leave a thread that failed to load unopened, so it can be retried.
ishu86 Sep 24, 2026
47a64f7
Start a new task for prompts queued to a thread that is gone.
ishu86 Sep 24, 2026
b2ee130
Hand queued prompts to a new task only when their thread is really gone.
ishu86 Sep 24, 2026
a8cddb2
Hold a prompt whose thread failed to load instead of blocking the queue.
ishu86 Sep 24, 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
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
.git
.venv
__pycache__
**/__pycache__
*.pyc
**/*.pyc
.pytest_cache
.worktrees
.claude
Expand All @@ -11,6 +13,7 @@ __pycache__
*.key
*.log
node_modules
**/node_modules
web/web-ui/threads.json
.env
*.env.local
Expand Down
31 changes: 25 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ CASE_MAX_RUNNING=4
# engine sees (on a Mac that is the VM, not the Mac). A create/wake past it 409s.
CASE_MAX_RAM_MB=

# Drive talks to cased on loopback / the compose network. No SSH.
CASE_LOCAL=1

# Drive home: threads.json, drive/shots (and later drive/inbox). Compose sets
# CASE_HOME=/data so they share the ui-data volume. Unset = ~/.case.
# Drive home: drive/shots and drive/inbox. Compose sets CASE_HOME=/data, the
# ui-data volume. Unset = ~/.case.
# CASE_HOME=

# Drive threads. Compose sets /data/threads.json. Unset = web/web-ui/threads.json.
# CASE_THREADS=

# Per-turn input-token ceiling for Drive chat. Default 2000000.
# CASE_TURN_TOKENS=2000000

Expand All @@ -30,6 +30,9 @@ CASE_LOCAL=1
# Desktop resolution for new/woken computers (WxH or WxHxDEPTH).
# DESK_RESOLUTION=1280x800x24

# 1 = desktops mirror their Chromium and websockify logs to docker logs.
# DESK_DEBUG=1

# Browser Drive still takes the key per request (x-openai-key / x-anthropic-key).
# The box key below is used by phone chat and schedules.
# CASE_DRIVE_PROVIDER=openai
Expand All @@ -42,6 +45,13 @@ CASE_LOCAL=1
# on PATH.
# CASE_BRAIN_URL=
# CASE_BRAIN_CMD=
# Seconds a scheduled run may take before it is stopped. Default 1800.
# CASE_BRAIN_TIMEOUT=1800

# Session keeper: how often saved logins with a probe_url are checked, and how
# recently a computer must have been used for the check to skip it. Seconds.
# CASE_SESSION_KEEPER_INTERVAL_S=21600
# CASE_SESSION_KEEPER_BUSY_S=900

# Phone chat over Telegram: make a bot with @BotFather, paste its token, start
# the ui, send /start to the bot and it replies with the chat id to put here.
Expand All @@ -62,7 +72,7 @@ CASE_LOCAL=1
# unset on a laptop; those links and buttons are omitted.
# CASE_PUBLIC_HOST=

# Extra hostnames Drive and cased will answer to (comma-separated). Only needed
# Extra hostnames Drive, cased and MCP will answer to (comma-separated). Only needed
# when something fronts them under another name; any other Host gets a 403.
# CASE_PUBLIC_HOST is allowed without being repeated here.
# CASE_ALLOWED_HOSTS=
Expand All @@ -76,3 +86,12 @@ CASE_LOCAL=1
# CASE_DBC_AUTHTOKEN=
# CASE_DBC_USERNAME=
# CASE_DBC_PASSWORD=
# Proxy the solver should use to load the page (host:port), and its type.
# CASE_DBC_PROXY=
# CASE_DBC_PROXYTYPE=HTTP

# Anonymous usage stats: counts of computers and schedules, and whether
# scheduled runs succeed. Never a name, prompt, domain or credential. Either
# line turns them off.
# CASE_TELEMETRY=0
# DO_NOT_TRACK=1
43 changes: 43 additions & 0 deletions .github/workflows/audit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# SPDX-License-Identifier: MIT
# Known-vulnerability check for the pinned dependencies. Weekly, because an
# advisory lands on a pin that has not changed; and on pull requests that change
# a pin, so a bump is checked before it merges.
name: audit

on:
schedule:
- cron: "17 6 * * 1"
workflow_dispatch:
pull_request:
paths:
- requirements*.txt
- image/requirements.txt
- web/package*.json
- .github/workflows/audit.yml

permissions:
contents: read

jobs:
audit:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "22"

- name: Python
run: |
python -m venv .venv
.venv/bin/pip install pip-audit==2.10.1
# separate runs: cased and deskd pin different fastapi, so one resolve fails
.venv/bin/pip-audit -r requirements-dev.txt
.venv/bin/pip-audit -r image/requirements.txt

- name: Drive
run: npm --prefix web audit --omit=dev
49 changes: 45 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,36 @@ on:
permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
cache: pip
cache-dependency-path: requirements*.txt
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "22"
cache: npm
cache-dependency-path: web/package-lock.json

- name: Install deps
run: |
python -m venv .venv
.venv/bin/pip install -r requirements-dev.txt

- name: Lint
run: .venv/bin/ruff check --select F,E9 control-plane mcp image tests

- name: Unit tests (no Docker)
run: |
# Everything except the acceptance suite, which needs a real desktop
Expand All @@ -47,6 +59,30 @@ jobs:
npm --prefix web ci --omit=dev # serve.mjs imports openai at module load
npm --prefix web test

- name: Shell scripts
run: .venv/bin/shellcheck -e SC1090 image/start.sh bin/case # SC1090: ~/.case/env is the user's

- name: Compose file
run: docker compose config -q

# deskd runs on Debian bookworm's Python in the image, against its own pins, not
# the control plane's. Test it there too.
deskd:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.11"
cache: pip
cache-dependency-path: image/requirements.txt
- name: deskd tests on the image's pins
run: |
python -m venv .venv
.venv/bin/pip install -r image/requirements.txt httpx==0.28.1
.venv/bin/python tests/test_deskd.py

# Case is dual-licensed and sold commercially, which is only possible if the
# project can sublicense every contribution. Deliberately no third-party CLA
# bot: the one everyone used was archived in March 2026, and a licensing gate
Expand All @@ -55,6 +91,7 @@ jobs:
cla:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Contributor has signed the CLA
Expand Down Expand Up @@ -88,16 +125,20 @@ jobs:

spdx:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Every source file declares its license
run: |
# A plain pathspec's * crosses directories, so web/*.mjs covers web/ at any depth.
# The list is built first so a git failure fails the step instead of an empty loop.
files=$(git ls-files \
'control-plane/*.py' 'image/*.py' 'image/*.sh' \
'mcp/*.py' 'tests/*.py' 'web/*.mjs' 'web/*.html' 'bin/case')
missing=0
while read -r f; do
for f in $files; do
grep -q 'SPDX-License-Identifier' "$f" || { echo "no SPDX header: $f"; missing=1; }
done < <(git ls-files \
'control-plane/*.py' 'image/*.py' 'image/*.sh' \
'mcp/*.py' 'tests/*.py' 'web/**/*.mjs' 'bin/case')
done
[ "$missing" = 0 ] || {
echo "New files take the SPDX id of their directory. See LICENSE.md."
exit 1
Expand Down
62 changes: 62 additions & 0 deletions .github/workflows/images.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# SPDX-License-Identifier: MIT
# Build the three images on changes that can break them, so a bad base digest,
# noVNC checksum or pin shows up on the pull request instead of at release.
# Nothing is pushed; publish-image.yml does that from tags.
name: images

on:
push:
branches: [main]
paths:
- image/**
- Dockerfile.*
- .dockerignore
- compose.yaml
- requirements.txt
- web/package*.json
- .github/workflows/images.yml
pull_request:
paths:
- image/**
- Dockerfile.*
- .dockerignore
- compose.yaml
- requirements.txt
- web/package*.json
- .github/workflows/images.yml

permissions:
contents: read

concurrency:
group: images-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- name: desk
context: ./image
file: ./image/Dockerfile
- name: control
context: .
file: ./Dockerfile.cased
- name: ui
context: .
file: ./Dockerfile.ui
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: ${{ matrix.context }}
file: ${{ matrix.file }}
platforms: linux/amd64
cache-from: type=gha,scope=check-${{ matrix.name }}
cache-to: type=gha,mode=max,scope=check-${{ matrix.name }}
push: false
6 changes: 4 additions & 2 deletions .github/workflows/publish-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,17 @@ jobs:
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest,enable=${{ github.ref_type == 'tag' }}
type=raw,value=latest,enable=${{ github.ref_type == 'tag' && !contains(github.ref_name, '-') }}
type=raw,value=edge,enable=${{ github.ref_type != 'tag' }}

- name: One manifest for both arches
env:
META_JSON: ${{ steps.meta.outputs.json }}
run: |
cd /tmp/digests
# shellcheck disable=SC2046
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< '${{ steps.meta.outputs.json }}') \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$META_JSON") \
$(printf '${{ env.IMAGE }}@sha256:%s ' *)

- name: Show what landed
Expand Down
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,14 @@ for f in tests/test_*.py; do
case "$f" in tests/test_acceptance.py) continue ;; esac
.venv/bin/python "$f" || exit 1
done
.venv/bin/ruff check --select F,E9 control-plane mcp image tests
npm --prefix web ci
npm --prefix web test
```

Unit tests run one file per process, as CI does: each file sets up its own vault
when it is imported, so a single pytest run across the files shares one and fails.

## Acceptance tests (Docker)

```bash
Expand Down
33 changes: 23 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ to the same topic. It marks its own posts so it does not read them as new tasks.

When one handoff is waiting, your next message answers it. If several are
waiting, prefix the answer with its handoff ID, such as `h_ab12 483920`.
An approval takes only `approve` or `deny`; any other reply is refused and the
handoff keeps waiting.
With no handoff waiting, a message steers the current Phone task or starts a
new task on the first computer returned by cased. Create a computer before
sending your first task.
Expand All @@ -242,7 +244,7 @@ Choose a computer's size under **+ New computer**, then **SIZE**. The default is

| Setting | What it controls | Compose default |
| --- | --- | --- |
| `CASE_MAX_RUNNING` | Maximum number of awake computers | 4 |
| `CASE_MAX_RUNNING` | Maximum number of awake computers | 4 (8 without Compose) |
| `CASE_MAX_RAM_MB` | Total RAM that awake computers may reserve, in MB | 75% of the memory visible to cased |

On macOS with Compose, that memory comes from the Docker VM. A 4 GB VM has room
Expand Down Expand Up @@ -277,7 +279,9 @@ Set `CASE_TOKEN` to that value in `.env`. Add your proxy hostname to
`CASE_TOKEN` protects Drive and the REST API. Open Drive through the HTTPS proxy
with `?token=<your-token>` on the first visit. MCP on port 8788 has no built-in
client authentication: keep it local or configure authentication at its proxy.
Setting `CASE_TOKEN` alone does not protect the MCP endpoint.
Setting `CASE_TOKEN` alone does not protect the MCP endpoint. MCP checks `Host`
against the same list as cased and Drive, so a proxy in front of it needs its
hostname in `CASE_ALLOWED_HOSTS` too.

For ntfy Assist links and approval buttons, set `CASE_PUBLIC_HOST` to the public
hostname of your cased proxy, without a scheme. That hostname is allowed without
Expand Down Expand Up @@ -326,13 +330,20 @@ Start cased, then Drive:

```bash
bin/case up
CASE_LOCAL=1 CASE_URL=http://127.0.0.1:8787 node web/web-ui/serve.mjs
CASE_URL=http://127.0.0.1:8787 node web/web-ui/serve.mjs
```

Open the [computers page](http://127.0.0.1:4174/deploy). Drive runs in the
foreground in this terminal. Host processes use environment variables, not the
Compose `.env` file. If cased runs directly on macOS, set `CASE_MAX_RAM_MB`
explicitly; its automatic memory budget requires Linux's `/proc/meminfo`.
Compose `.env` file; `bin/case` also reads `~/.case/env` if it exists. If cased
runs directly on macOS, set `CASE_MAX_RAM_MB` explicitly; its automatic memory
budget requires Linux's `/proc/meminfo`.

A few settings only apply here. `CASE_PORT` moves cased off 8787.
`CASE_VNC_PORT` pins every desktop's noVNC to one host port for a reverse proxy.
`CASE_BRAIN_BIN` points the scheduler at a `claude` binary that is not on `PATH`,
and `CASE_MCP_CONFIG` replaces `case-mcp.json`. On macOS, `CASE_CPU` and
`CASE_MEM` size the Colima VM that `bin/case up` starts (default 4 CPUs, 4 GB).

For a client that uses stdio MCP, [case-mcp.json](case-mcp.json) starts
`mcp/case_mcp.py` with Python. It needs the installed Python dependencies and
Expand Down Expand Up @@ -375,11 +386,13 @@ installation, or the `case-home` volume mounted at `/data` in Compose. Back up
the database and key together, along with the desktop volumes you want to keep.
Treat these backups as sensitive data.

Drive keeps `threads.json`, screenshots under `drive/shots`, and attachments
under `drive/inbox` in its home directory. This is `~/.case` by default; Compose
uses the `ui-data` volume mounted at `/data`. Deleting a thread does not remove
its screenshots or attachments. Files added through the plus menu stay on the
Drive host for the model to read; they are not copied onto the desktop computer.
Drive keeps screenshots under `drive/shots` and attachments under `drive/inbox`
in its home directory, `~/.case` by default. Its threads are in
`web/web-ui/threads.json` by default, or the file `CASE_THREADS` names. Compose
keeps all of them on the `ui-data` volume mounted at `/data`. Deleting a thread
does not remove its screenshots or attachments. Files added through the plus
menu stay on the Drive host for the model to read; they are not copied onto the
desktop computer.

</details>

Expand Down
Loading
Loading