Skip to content

feat: Krater Compute, the application form and project teams - #9

Merged
jaspermayone merged 84 commits into
mainfrom
claude/application-form
Oct 22, 2026
Merged

jaspermayone merged 84 commits into
mainfrom
claude/application-form

Conversation

@Adambomb210

Copy link
Copy Markdown
Collaborator

Title: Krater Compute, the application form and project teams

Base: main ← Head: claude/application-form (patchworklabsorg/krater)

main holds only the initial commit, so this PR carries everything on the branch:

If PR #1 merges first, this diff shrinks to the compute and form work.

What the branch adds

  • Krater Compute replaces SkyPilot with Krater's own layer over Vast.ai (docs/compute/):
    • Scheduler: waits for an offer under the member's price and bids on interruptible machines by default.
    • Pauses: handles outbid pauses (hold or move).
    • Spend: accrues spend and enforces a hard budget stop.
    • Reconcile: checks Vast's real charges every hour, and sends corrections to Quilt.
    • Settings: compute settings in an admin panel, with per-project overrides.
    • Prices and quota: keeps a price history and tracks Vast's daily search quota.
  • Go runner: runs on every rented machine, with no org key on it.
    • It runs the member's image and command, and delivers the project's secrets.
    • It uploads checkpoints and outputs through presigned URLs, and logs every minute.
    • It sends heartbeats.
  • Boxes: interactive SSH machines with stop, resume and move. /workspace is saved before a stop. They stop when idle and after a max runtime.
  • The application form: Ganymede's application questions, with word limits.
    • The estimator gives two estimates, proof of concept and finished product, at Vast prices, and teams can override it.
    • Only the proof of concept is approved; more budget comes through an amendment.
  • Project teams: a lead plus invited Ganymede members.
    • Teammates edit, submit, run compute and manage secrets, and are invited to the Slack channel.
    • Teammates can't review the project.
    • The team is credited in the gallery.
  • Guardian gate (Jasper's guardian verification, merged): no budget grant while a team member is a minor without a
    verified guardian in Weave. Joining a funded project runs the gate too.
  • Limit orders: one price for interruptible jobs.
    • Krater bids the offer's minimum plus a margin, never above the limit.
    • When outbid, the job holds its paused machine and moves only if a machine at or under the limit is free.
    • The web form defaults to a limit order.
  • Provider seam (krater/providers): what Krater can call on a provider.
    • It defines capabilities, a protocol, neutral errors and a registry.
    • It records the provider on each rental, and validation is gated by capability.
    • It ships with a report on integrating the OSPool (docs/compute/ospool.md) and the AWS/GCP mapping (docs/compute/providers.md).
  • Provider-neutral rework (merged from claude/provider-neutral):
    • Provider ids are strings: provider_instance_id, provider_offer_id, provider_machine_id.
    • A neutral Phase replaces reads of Vast's status fields.
    • Launch is split: Krater builds a neutral Launch, and each provider turns it into its own create request.
    • /start gives the runner a self_stop descriptor (vast_api or shutdown); the legacy fields are still sent.
    • Members' messages name the provider from ProviderInfo.title.
    • Still Vast-shaped (docs/compute/providers.md): the offer, instance and charge types, and one provider per deployment.
  • Runner self-stop (dead-man switch): after runner_self_stop_minutes with no answer from Krater (default 3 h,
    overridable per project), the runner stops its own machine through Vast's API with the container's own key. Krater,
    once it is back, starts a job's machine again or shows a box as stopped.
  • Member CLI (cli/, krater): sign-in through Weave (PKCE), submit (including --limit), list, logs and
    logs -f, wait, download, ssh, stop/resume/cancel, secrets.
  • Gallery setting: an admin "Public gallery" card sets how members under 18 are named (default: first name only).

Migrations, in order (Alembic, single head c8f2a4d6e1b3)

PR #1's chain up to 83af68b415ca (quilt outbox), then:

  1. a7c1e5d9f2b4: stop storing member data from Weave (refactor: stop storing copies of member data from weave #7)
  2. f736452b1d88: compute settings
  3. a4c9e2d71b03: GPU price samples
  4. 23eb52c373d1: compute jobs, instances and events
  5. b81f0c6d2e57: compute spend estimate
  6. b9a50b19751b: spend reconciliations
  7. c2d8e5f4a910: compute runner
  8. d5e1a7b3c204: compute storage quota
  9. 4cd4032b89cb: compute boxes
  10. e7a2c9d1f305: compute run seconds
  11. f3b8d4e60a12: drop SkyPilot columns
  12. a90b9c697fd0: project secrets
  13. b6d3f9a1c2e7: cancel queued skypilot_reconcile tasks
  14. c4e8a2f7b913: vast_search_quota
  15. b2d4f6a8c0e1: merge point (refactor: stop storing copies of member data from weave #7 into compute)
  16. e3a9c6b2d714: project_members; drops credited_builder_ids (credited builders become accepted members)
  17. f5b2d8e1a936: the application form's answers
  18. 2d64a8c637d2: runner self-stop
  19. efcb0ce55d26: limit orders
  20. a7c3e91f0b42: compute instance provider
  21. b5d1e7a3c920: site settings (gallery minors)
  22. c8f2a4d6e1b3: provider ids as strings (its downgrade refuses while any id isn't an integer)

How it was tested

  • Automated: 1075 Python tests against Postgres, 129 CLI tests, and the Go runner tests (go vet, -race).
    ruff check and ruff format --check are clean. There is one Alembic head.
  • Staging end to end (local full stack against Weave main, 2026-10-14). The UI was driven by script with real Weave sign-in. It covered:
    • sign-in gates;
    • the form and teams;
    • Slack channel, review and feed (signed simulated clicks);
    • the guardian gate in both directions;
    • changes requested, then a resubmission;
    • admin budget, cap and overrides;
    • compute on a fake Vast;
    • the real Go runner through the Tailscale Funnel: secrets, a 20 MB checkpoint byte-exact, logs and the CLI download;
    • amendment, completion and the gallery;
    • withdraw, with its channel archived.
  • Paid Vast rounds on staging:
    • 2026-10-11: $0.019. Covered the entrypoint override, the full job path, charges, a box on plain Ubuntu, a natural pause and a non-root image.
    • 2026-10-14 morning: $0.065. A job from the CLI with its secret, output and checkpoint byte-exact, and an SSH box with stop, resume and cancel. Krater's reconcile matched Vast's charges exactly.
  • Self-stop on a real Vast machine (2026-10-14 evening, $0.040): passed.
    • Setup: a 30-minute override, with the portal and worker stopped right after a heartbeat.
    • At 31 minutes the runner stopped the command, and Vast accepted the stop from the container key. The instance was stopped, not destroyed.
    • Back up, Krater recorded self_stopped, paused the job and started the machine (200). The runner reported again within seconds.
    • Cancelling destroyed the machine, and Vast listed 0 instances.
    • Two earlier tries on interruptible machines were taken back by Vast within minutes, and Krater correctly read them as outbid. The passing run used an on-demand machine.
  • Self-stop on interruptible machines (2026-10-15, on this branch's merged code, $0.052): not proven. Three attempts.
    • Vast took back a GTX 1080 Ti after 12 minutes, and a Titan Xp after 7 minutes and again after 19. Each time, Krater correctly read it as outbid.
    • A V100 host never started the container.
    • Shown on real machines: string ids end to end, the new runner, and the reconcile matching Vast's charges.
  • Provider-neutral rework on staging (fake Vast): migration up and down on staging's real data, the E2E steps,
    a job and a box rented through the new launch path, the CLI, and /start returning the descriptor.
  • Final review: three reviewers (compute and runner, forms and web, CLI and API); confirmed findings fixed with tests.
    • Fixed: a cancelled job revived by a mid-pass move or rent, and a self-stopped machine whose restart wasn't retried.
    • Fixed: Patchwork IDs shown on the public gallery for members Weave doesn't return, and file downloads authorized from sign-in roles.
    • Fixed: a team join racing the guardian gate.
    • Fixed: a box confirming an unsaved /workspace, a final upload not retried, and proxy 403s ending jobs.
    • Fixed: CLI downloads failing after 15 minutes, and terminal escape sequences in logs.
    • Fixed: several inputs that caused 500s.

Open items

  • Rotate the Vast API key before production. The staging test key has been used in logs and tests.
  • Production deploy: feat(krater): krater staging on weft, production ready behind one switch infra#32 runs Krater on alastor with Krater Compute in the module, off until
    the Vast key is rotated (then compute.enable). Its database is the schema krater in patchwork_central on skein, not a
    Postgres of its own (docs/dev/production.md 0.1). The DNS records are in feat: add krater and krater-storage.patchworklabs.org dns#12. After this merges,
    set infra's fallback rev to the merge commit.
  • The project-secrets key in production: feat(krater): krater staging on weft, production ready behind one switch infra#32 has the slot (krater-project-secrets-key.age, portal only), but the key isn't made yet.
  • Storage quota is checked per upload request, not reserved, so parallel uploads can exceed it.
  • Weave token checks have no negative cache. Revoked CLI tokens act for up to 60 s.
  • Teammates removed from a team stay in the project's Slack channel.
  • Admins can set compute overrides on their own project. SPEC bars this only for budget and hourly cap. Needs a decision.
  • The e3a9c6b2d714 migration turns credited builders into accepted members without consent or a guardian check, and its downgrade drops them. This doesn't matter while production has no projects.
  • A runner restarted while Krater is still down uses the 3-hour self-stop default, not the project's setting. Seen on a real machine on 2026-10-15: Vast restarted the container mid-window.
  • A host that can't start the container stays provisioning for 30 minutes, then the job fails blaming the image, instead of moving to another machine.
  • Production Weave: OAuth app, roles and Slack integration. KRATER_TRUSTED_PROXY_COUNT behind Cloudflare and Traefik.
  • OSPool: Adam contacts OSG (docs/compute/ospool.md).
  • Provider-neutral, still to do: neutral offer, instance and charge types; a provider per job. Best shaped by a real
    second provider.

🤖 Generated with Claude Code

Adambomb210 and others added 28 commits October 22, 2026 00:14
The client covers what Krater Compute needs: search offers, create, show,
list, change bid, stop/start, destroy, logs and charges. It follows the
facts from the 2026-10-11 spike: every path has its trailing slash and a
redirect is an error (Vast's 308 does nothing), instances are listed
through the paged v1 API, create returns only the instance id (the
response's instance_api_key is dropped), log downloads go out without the
org key, and money is parsed as Decimal. Hourly rates are stored as integer
micro-dollars because bids need sub-cent precision.

FakeVast serves the same API through httpx.MockTransport and copies the
behaviours that cost money to get wrong (308 on a missing slash, 410 on
the v0 list, 401 without the key, the 512-offer search cap). Only the
worker will build a client; the fake is never used in production.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every policy number in the Krater Compute design is now a global default
in compute_settings (one row, seeded with the values approved on
2026-10-11: reliability floor 0.95, bid margin 10%, pause grace 5 min,
move after 30 min, box idle 30 min, destroy after 24 h, reconciliation
alert at 10%), and admins can override any of them per project.
effective_policy() resolves a project's numbers for the compute loops.

Admins edit both on /admin/compute and /admin/compute/projects/{id}.
Changes need a reason, are bounds-checked (move after can't undercut the
pause grace, for any project), and write an AuditEvent with old and new
values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vast gives client accounts no price history (its market metrics answer
host accounts only, and its public feed needs a data license for bulk
use), so the worker samples it: one search per tracked GPU model and
rental type, 1-GPU offers at or above the global reliability floor, and
one gpu_price_samples row with the offer count and the min, p10 and median
full hourly cost (GPU price plus 32 GB of the host's disk) in integer
micro-dollars. A search that hits Vast's 512-offer cap is flagged. One
failing model is skipped; a rate limit ends the run. Without a usable
Vast client (no key, or the fake in production) the task does nothing.

CLAUDE.md now records the micro-dollar rate unit and that only the worker
talks to Vast.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A worker that started before the key file was readable, or whose key was
rotated, kept using no client or the old key until a restart. Live mode
without a readable key now logs an error on every call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
compute_jobs, compute_instances and compute_events, and a scheduler pass
the worker runs every minute under a procrastinate lock, only with the new
KRATER_COMPUTE_MODE=vast (which production allows only with the live Vast
API). Each pass:

- settles creates whose response was lost: the instance row and its
  unique label are committed before Vast is called, so the machine is
  found by label, or given up after 5 minutes;
- sweeps instances with Krater's label prefix that Krater has no live row
  for, and destroys them;
- watches live instances: running, paused when outbid, the bid raised in
  steps after the pause grace up to the job's max, moved after "move
  after", max runtime, never started, vanished, and finished jobs;
- starts queued jobs at the cheapest offer at or under the target (bid =
  min_bid + margin, clamped to the max), within the project's hourly cap,
  holding budget for the max runtime under the project's row lock. After
  max wait the job keeps waiting, raises to its max, goes on demand, or
  cancels, as the member chose.

Every destroy is confirmed by show_instance and retried until it is.
Members submit and cancel jobs through krater.services.compute_jobs; the
runner, spend loop, reconciliation, boxes and UI come next. Until the
runner exists the container runs the command in a shell that never exits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…label prefix

Withdrawing a project (or approving its completion) only cancelled its
queued jobs, so running machines kept billing until their max runtime,
after the budget was reclaimed. The scheduler now cancels and destroys
them on its next pass.

Production with KRATER_COMPUTE_MODE=vast refuses the default label prefix:
the sweep destroys unknown instances with its prefix, so two deployments
sharing a Vast account must not share one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every scheduler pass, before it changes any state, each billing instance
accrues its rented price since the last pass: disk from create (the image
pull bills disk only), the GPU only while running, as Vast bills it. Job
spend is the sum of its instances; project spend is written as a new
vast_estimate SpendSnapshot whenever it grows, which feeds the project
page, the budget alerts and Quilt's spend.recorded events unchanged. The
estimate only grows; bandwidth is left to the hourly reconciliation.

Then each project with active jobs is warned at the warn percentage and,
projected one minute ahead, stopped at its ceiling: every active job
fails with "budget reached" and its machines are destroyed. The warning
and teardown audit events (and so the Slack alerts) now live in
krater.services.budget_alerts, shared with the SkyPilot reconciler, whose
behaviour is unchanged. The project page shows the budget banner in vast
mode too.

spend_snapshots.source is a non-native enum whose CHECK constraint
autogenerate doesn't compare, so migration b81f0c6d2e57 replaces it by
hand; a test inserts a vast_estimate snapshot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An hourly task (under the scheduler's lock) reads /api/v0/charges for the
days recent instances ran, sums each instance's charges (rows span days
and are rounded to $0.001, so only cumulative totals are compared), and
replaces the instance's estimate with Vast's figure. Each check is kept per
project in spend_reconciliations; a difference past the project's alert
threshold (and over a cent) writes an AuditEvent and shows on
/admin/compute. Charges for Krater-labelled instances Krater never rented
are flagged too.

A corrected total above the latest snapshot becomes a new vast_estimate
snapshot and a Quilt spend.recorded event. A lower one stays in Krater's
own records: Quilt takes increases only until patchworklabsorg/quilt#17.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vast passes `args` to the image's ENTRYPOINT, so an image with its own
ENTRYPOINT would have swallowed `sh -c ...`, exited, and been restarted by
Vast in a billing loop. In args mode Vast's `onstart` field is the
entrypoint override (its CLI sends --entrypoint as onstart), so Krater now
sends runtype args, onstart sh and args [-c, script]. Recorded as needing
one paid check with an image that has an ENTRYPOINT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Charges for a running machine may lag what it has run, and the snapshot
only rises, so lowering a live estimate to Vast's figure would make the
budget stop fire late by that lag. A live instance now takes Vast's figure
only when it is higher; a destroyed one takes it either way. Unknown
charges are flagged once per source instead of every hour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Krater Compute's runner gets only presigned URLs for its own job's keys,
never storage credentials. ObjectStore gains presign_put, list_prefix and
multipart uploads (create, presign a part, complete), so a checkpoint
larger than one part goes up in parts under Cloudflare's request-body
cap in front of the storage host. Checkpoints and outputs get their own
bucket (KRATER_COMPUTE_S3_BUCKET, default krater-compute), which the
deploy-time ensure_bucket now creates too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each rented instance gets its own runner token (Krater stores only its
sha256), sent to the machine as KRATER_JOB_TOKEN with KRATER_RUNNER_URL.
It works only while that instance is live and not being destroyed, so a
moved job's old machine is locked out. /runner/v1/ (Bearer token only,
no session or CSRF, its own rate-limit group) lets the runner:

- start: learn its command and whether this is a resume (a later attempt,
  or a restart of the container on the same machine);
- heartbeat: report GPU use and sessions, and learn when to stop;
- exit: report the exit code, once; the scheduler then destroys;
- list, download and upload its own job's checkpoints and outputs through
  presigned URLs, multipart above 64 MB.

The container now runs a bootstrap that fetches the runner from the
portal with curl, wget or Python, checks its sha256, runs it, and then
waits forever. The scheduler fails and destroys a running machine whose
runner hasn't started within the runner start deadline (10 min, also
after a resume) or stops sending heartbeats (30 min); both are new
compute settings. Without a runner binary to serve, nothing is rented.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
runner/ is a standard-library-only Go program, built by a new Dockerfile
stage as a static linux/amd64 binary that the portal serves. On the
machine it reports start, restores the job's checkpoints, runs the
command with sh -c (KRATER_RESUMED, KRATER_CHECKPOINT_DIR and
KRATER_OUTPUT_DIR set), sends heartbeats with nvidia-smi's GPU use,
stops the command when Krater says so, uploads changed checkpoints on a
timer (skipping .tmp/.partial names) and checkpoints and outputs at the
end through presigned URLs, multipart above 64 MB, and reports the exit
code. A refused token makes it give up at once.

Go tests run against a fake Krater-and-storage server, with -race. The
bootstrap was also run end to end in alpine, python:3.12-slim and
debian:bookworm-slim against a fake runner API: the first two ran the
command and reported its exit code, the third explained it has no curl,
wget or Python, and a tampered checksum was refused. docs/compute/runner.md
documents the contract for members.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runner usually reports /start within seconds of the container
starting, before the scheduler's next pass even notices Vast's running,
so comparing runner_started_at with the time the pass saw running failed
almost every real job at the 10-minute deadline. Each instance now has
runner_starts_needed: 1 at first, and the runner's start count plus one
whenever a pause is seen (Vast restarts the container on resume). Tests
cover a start before the pass sees running, a restart reported before the
resume is seen, and a stale start from before a pause.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A runner holds its token and could otherwise ask for unlimited upload
URLs on storage that alastor's other services share.

- A per-project storage quota (a compute setting, 50 GB by default,
  migration d5e1a7b3c204): Krater sums the project's objects and refuses
  URLs past it; replacing a file counts only the difference.
- Every presigned PUT and part URL is signed for its exact size. Checked
  against a throwaway SeaweedFS 4.47: the signed size gets 200, a bigger
  or smaller body 403.
- An hourly task aborts multipart uploads whose job has ended (an hour's
  grace) or that belong to no job. SeaweedFS lists uploads without a start
  time, so job state decides, not age.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`python -m krater.compute.submit` queues, lists and cancels jobs as the
project's submitter, through the same service and checks the member UI
will use; run it where the worker runs. scripts/dev/compute_watchdog.py
is the independent backstop for a paid test: it destroys every instance
with one label prefix (and verifies each by id) once Vast's charges for
them pass a cap or a deadline passes, and refuses a broad prefix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Interactive boxes need a stopped state (the GPU is freed, the disk kept)
for both jobs and instances; a stopped box's job still counts as active,
so its disk keeps billing against the budget and holds. Migration
4cd4032b89cb adds it to the hand-maintained enum CHECK constraints, the
stop/resume and SSH columns on compute_instances, and two settings: the
box sync deadline and the resume timeout (10 minutes each).

Members add SSH public keys at /account/ssh-keys. Only ssh-ed25519,
ecdsa-sha2-nistp* and ssh-rsa keys are accepted, only if the base64
decodes to a blob of the same type, and only `type base64` is stored,
so nothing can add options or a second line to a box's authorized_keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A box is a job with no command that keeps a workspace open:

- It launches in Vast's SSH mode (ssh_direct), its runner started in the
  background by Vast's startup script, all in launch.create_request and
  marked pending the paid check. Krater records where to SSH (the direct
  22/tcp port when mapped) and hands the runner its owner's SSH keys.
- Krater refuses to start boxes while the Vast account has any SSH keys,
  since those may reach every machine.
- Idle (runner reports GPU under 5%, CPU under 10%, no session) for the
  box idle time, or stopped by the member: the next heartbeat asks the
  runner to save /workspace, and Krater stops the machine once it
  confirms or the sync deadline passes. A stopped box bills disk only,
  stays an active job for budgets, and is destroyed after the destroy
  time, with a warning an hour before.
- Resume restarts the same machine; the runner must start again. If the
  machine's GPU is taken, Krater takes the start back and tells the member
  when the workspace was last saved; moving it elsewhere is their choice,
  never automatic. A box started outside Krater is billed and stopped.
- A box whose runner goes silent after it has run is stopped, not
  destroyed, so its disk survives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For a box the runner writes its owner's keys as the whole of
~/.ssh/authorized_keys (dropping anything that isn't one `type base64`
line), restores /workspace, reports open SSH sessions counted from
/proc/net/tcp and tcp6 with each heartbeat, saves /workspace on a timer
and whenever Krater asks before stopping the box, then confirms. A file
the storage quota refuses no longer stops the rest of an upload pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With KRATER_COMPUTE_MODE=vast, a project's team runs jobs and boxes from
/projects/{id}/compute: a list with each job's state and why, a submit
form (GPU models or minimum memory, disk, target and max price,
interruptible, max wait and what happens after, max runtime) next to the
latest sampled prices, and a job page with its machine, the SSH command
for a box (Jupyter through an SSH tunnel), stop, resume, move and
cancel, and its event log. Reviewers and admins can look; only the team
can launch. The project page links there.

/pricing/history draws Krater's own price samples per GPU and rental
type: the cheapest offer as a line and the 10th-50th percentile band in
the same hue (a three-colour version failed the colour-vision check in
dark mode), server-rendered SVG with a legend, a hover crosshair and
tooltip from a small script, and the same numbers as a table. Checked by
eye in light and dark mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Max runtime counted wall time since the first start, so a box stopped
  overnight was destroyed on resume. Instances now add up time actually
  running (run_seconds, migration e7a2c9d1f305; reset when a box is
  resumed), and a box at its max runtime is stopped, never destroyed.
  Jobs no longer lose runtime to pauses either.
- Vast keeps reporting running for a while after a stop. A stopped box
  seen running now counts as a member restart only if Vast's intended
  state is running too, so Krater's own stop isn't billed as an hour of
  GPU. FakeVast can lag the same way.
- An error from the account SSH-key check no longer stops the whole pass:
  boxes are refused for that pass, jobs still start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing is removed yet; this only moves and renames, so the deletion that
follows is a pure deletion.

- The public GPU price catalog (SkyPilot's Vast CSV, fetched from GitHub)
  that /pricing and the budget estimator read now lives in
  krater/pricing/catalog.py with its own fake, chosen by pricing_mode.
- LAUNCHABLE_STATUSES and hourly_cap_cents move to
  krater/services/launch_rules.py.
- Renamed settings: pricing_mode, pricing_catalog_url and its timeout,
  compute_max_hourly_cost_cents, budget_warn_percent. Each still reads its
  old KRATER_SKYPILOT_* env name for one release (pydantic AliasChoices;
  the new name wins), so Krater can deploy before its env files change.
  Production now refuses a fake pricing catalog by pricing_mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Krater Compute replaces it (docs/compute/). Deleted:

- krater/skypilot, krater/skypilot_policy, services/skypilot_sync.py,
  services/launch_policy.py, the /internal/skypilot/policy route and its
  rate-limit group, the skypilot_reconcile task and its CLI;
- the skypilot and auth-proxy compose services, their volume and the
  Caddy route for the SkyPilot hostname;
- the nightly SkyPilot contract workflow and its scripts, the SkyPilot
  docs and the tests for all of the above; pyyaml, which only the launch
  gate used.

KRATER_COMPUTE_MODE is now off or vast, and off by default so a developer
with a live Vast key can't rent by accident. The project page shows the
budget banner from krater.services.budget_alerts and links to Compute;
the admin cap form no longer mentions SkyPilot. The worker (only) can
mount a Vast key file in compose. The public price catalog behind
/pricing stays (krater/pricing/catalog.py), as does the
skypilot_cost_report spend source, which existing rows use. The
projects.skypilot_* columns are dropped in the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing reads them since SkyPilot was removed (migration f3b8d4e60a12;
the downgrade restores them empty). SpendSource.skypilot_cost_report and
the old audit action names stay, since existing rows use them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SPEC, CLAUDE.md, the production and staging runbooks, the pricing doc and
the member guide to running when compute is cheap now describe Krater
Compute; small references in the Slack, storage, Quilt and Weave docs are
fixed; HANDOFF and FUTURE are marked historical where they describe
SkyPilot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…duction-only checks

/pricing linked to /pricing/history even with compute off, where the
worker has no Vast key and the history stays empty. The link now shows
with KRATER_COMPUTE_MODE=vast or once samples exist.

docs/compute/README.md lists what the staging round can't check because
production sits behind Cloudflare: that a datacenter IP can fetch the
runner without a challenge, and that size-signed presigned uploads
survive the proxy.

Checked locally: the real procrastinate worker against FakeVast ran the
scheduler, price history, reconciliation and the rest under their locks
with no errors, rented the queued job and held its budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…price-history searches

Staging, 2026-10-11: Vast twice answered a create with {"success": false} and no message, and rented the
machine anyway. Krater requeued the job and the sweep destroyed the machine a minute later, in a loop. Such an
answer is now unconfirmed, like a lost one: the next pass finds the machine by its label. A refusal without a
message names the answer's fields (never their values).

The price-history run hit Vast's rate limit after 33 searches in 4 s; searches are now 1 s apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adambomb210 and others added 26 commits October 22, 2026 00:15
`krater ssh box -- -i key nvidia-smi` built `ssh -p N -i key nvidia-smi root@host`, so ssh took the command for
the host name. Found running a box on a real Vast machine (staging, 2026-10-14).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On staging (2026-10-14) Krater went down with a box running, and the box billed for four hours with nobody
watching: the runner only logged its failed heartbeats. Now a runner that hears nothing from Krater for the new
runner_self_stop_minutes policy (3 hours by default, admin-adjustable and per project) stops its own machine through
Vast's API with the container's own key. It stops rather than destroys, so the disk keeps what the job or member did
meanwhile. Back up, Krater tells a self-stop from an outbid machine by the runner's silence, starts a job's machine
again (it resumes from the checkpoints on its disk) and shows a box as stopped.

Also: the scheduler's first pass after Krater itself was down failed every running job for silent heartbeats before
any could get through. A live instance not looked at for 5 minutes now restarts its runner deadlines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A limit order is one price (target = max) that holds when outbid: Krater
bids the offer's minimum plus the margin, raises up to the limit, and once
past "move after" keeps the paused machine and its disk unless another
machine at or under the limit is free. Krater holds the order itself, so
it works the same with any provider.

- compute_jobs.on_outbid (move | hold); the API takes limit_usd_per_hour
  and on_outbid; the new-job form defaults to a limit order.
- krater submit --limit, --expires, --on-outbid; prices shown as "limit".
- Fix _raise_bid re-raising to the same bid every pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Krater Compute now reaches its provider through `krater.providers`:
capabilities it branches on (never a provider's name), the protocol the
scheduler, reconciler and price history call, neutral errors that Vast's
subclass, and a registry keyed by KRATER_COMPUTE_MODE. Vast is the only
provider and behaves as before.

- compute_instances.provider; a pass watches, settles and reconciles
  only its provider's rows; instance ids are unique per provider.
- Job validation refuses what the active provider can't do (boxes,
  interruptible, on demand, limit orders, raise_bid, on_demand).
- The runner's switch can be turned off (-1) for a provider whose
  machines can't stop themselves; reconcile runs only with charges.
- The runner no longer says "Krater stops this machine" after stopping
  itself; self-stop reasons read "30 minutes" rather than "0.5 hours".
- docs/compute/providers.md: adding a provider, what is still
  Vast-shaped, AWS/GCP mapping, and the OSPool report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…is stopping

- A <job> can be its name: the active job of that name wins over ended
  ones, and several ended ones are listed.
- krater download JOB DIR (--to still works).
- krater stop says the box is stopping, not "running".
- krater show gives the ssh command only while the box runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A site setting on /admin (audited, reason required): full name, first
name only (the default), or not named ("and 1 more team member"). A
member whose age Weave doesn't say counts as under 18 here, since the
gallery is public. New site_settings table (one row) for settings that
aren't compute policy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/compute/ospool.md: integrate as a gateway (Patchwork is the only identity
OSG sees), the three gates (egress, eligibility, automated submission), the
access-point routes, identity and minors (one public ProjectName, pseudonymous
ids in job ads), budgets via HTCondor GPU-hours at a shadow rate, the mapping
onto the provider seam with corrected HTCondor states and holds, the Krater
changes it needs, a no-egress fallback, testing, a plan, questions for OSG,
alternatives, and an egress test job. providers.md now summarises and links it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the egress test

Spend accrues at an instance's hourly rate and the cap checks read the offer's,
so $0 offers would leave every ceiling and alert silent until reconcile.
htcondor2 doesn't spool by default, so a remote worker must submit with
spool=True and upload the runner and token. The egress test now signs its PUT
for the exact size, defines its URLs, records the site through
job_machine_attrs, and says which failures mean blocked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ly Krater's own answers

A box confirmed /workspace as saved even when the push failed, and a job reported its exit after a failed final
upload, so outputs could be lost behind a success. Both now retry for a bounded time; a box stays unconfirmed (Krater
records it unsaved at its sync deadline) and a job's log says what is missing. Any 403 or 4xx from a proxy in front of
Krater (an HTML WAF page, Funnel) no longer ends a job or resets the dead-man switch: only 2xx or Krater's JSON
answers count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t, revoke on logout

- wait and logs -f retry 5xx/429/timeouts with backoff instead of exiting 1 like a failed job;
- download refreshes its signed URLs after a 403, re-downloads same-size files unless --skip-existing (checkpoints
  keep their size), and skips dot segments or unwritable names instead of crashing;
- non-finite prices are refused; job log text is stripped of terminal control sequences (--raw opts out);
- a refresh retries with a token another invocation rotated, and tells a Weave outage from an expired sign-in; the
  credentials file is written through a unique temp file;
- logout revokes the tokens at Weave before deleting them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ry IDs, input 500s

- The scheduler re-reads a job under a row lock before renting for it or requeueing a moved one: a cancel landing
  mid-pass was overwritten, and a box cancelled while moving rented a new machine.
- A self-stopped job's machine start is retried every pass (Vast never resumes a deliberately stopped machine).
- The public gallery leaves unnamed anyone Weave doesn't return, instead of printing their Patchwork ID.
- Job pages and file downloads check Weave live (cached directory answer on GET), not the roles from sign-in.
- Accepting a team invitation takes the project row lock grants take, so a join can't race past the guardian gate.
- Storage names accept what ML tools write (epoch=3-step=100.ckpt) and refuse . segments.
- The label prefix must be specific (the sweep destroys unknown instances with it).
- API: non-finite prices, non-string commands and huge GPU lists are 422s, not 500s; estimator too.
- A blank repo or demo link clears it; reconcile keeps the accrual mark when the estimate stands; money
  ceilings use integers; the watchdog keeps destroying through Vast errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s for

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…offer_id, provider_machine_id

Vast's ids are integers, but AWS's (i-0abc...) and HTCondor's (1234.0) aren't. compute_instances' vast_instance_id,
offer_id and machine_id become strings named for any provider (alembic c8f2a4d6e1b3; last_vast_status becomes
last_provider_status; the per-provider unique constraint takes the naming convention's name). The downgrade refuses
while a row holds a non-integer id. Checked up, refused, down and up again on a copy of staging's data.

The provider protocol and the Vast shapes carry string ids; krater.vast turns them back into integers at the wire
and refuses anything but plain digits, so an id can never change a request path. The runner's /start keeps its
vast_instance_id integer for runners already deployed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in /start

Built from the provider's self_stop_method (vast_api or shutdown), never from its name. The instance id is a
string. The legacy vast_instance_id and vast_api_url fields are still sent, and a runner without a descriptor
uses them as before. An unknown method or an unusable id stops the command but leaves the machine running.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
krater.providers.Phase (provisioning, running, interrupted, stopped, gone) replaces the scheduler's reads of
is_running, intended_status and cur_state. krater.vast maps its statuses to a phase, plus stopping for the
lag after a stop; a table test covers the combinations seen on staging. Vast behaves as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scheduler's and the reconciler's reasons and events said Vast; they now take the active provider's title.
Quilt's spend-correction reason names no provider.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without one, a runner told to stop itself would fall back to Vast's API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Vast's

krater.compute.launch builds a neutral Launch (image, disk, label, bootstrap, box, bid, env);
ProviderInfo.create_request turns it into the provider's create request. Vast's args mode with sh as the
entrypoint, and its ssh_direct mode for boxes, now live in krater.vast.launch. The request Vast gets is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-shaped

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Infra's Krater module no longer runs krater-db or a backup timer: the
database is the schema `krater` on skein, written by bin/new-krater-secrets,
and skein's nightly dump is the backup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jaspermayone
jaspermayone force-pushed the claude/application-form branch from 16de89a to d6e5a38 Compare October 22, 2026 04:15
@jaspermayone
jaspermayone merged commit de3df05 into main Oct 22, 2026
4 checks passed
@jaspermayone
jaspermayone deleted the claude/application-form branch October 22, 2026 04:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants