compute-env-setup
Setting up compute environments on a remote provider
Overview
What's invariant across every backend is what a job needs: a software stack (specific package versions, often with load-bearing install ordering), possibly some large model weights placed where the tool will find them, and a resource shape. What varies — a lot — is how a given provider materialises those three things and how an env name gets resolved to them at submit time. This skill is about keeping one declarative spec that says what the environment is, and treating how it gets built and addressed on this provider as something you figure out once per provider. The work ranges from minutes (another conda env on a host that has ten) to days (GPU image + weight cache + egress on a fresh backend), and most of the long tail is diagnosing why the documented invocation doesn't work even though every import succeeds.
Before building anything: compute_details({provider, mode:"read"}). The env, or a near-match you can extend, may already exist. The doc you get back is also where you'll record what you set up.
Provider shapes
These are not exhaustive and they blend at the edges (a Slurm node can run Apptainer containers built from the same Dockerfile a cloud backend uses). The point is to recognise which shape you're in, because that determines what "build", "register", and "resolve" mean.
Direct SSH host (conda or venv). You have a shell on the machine. There is no image, no runner script, no renderer — you are the renderer: read the spec's pip_phases and run them in order after conda create -n <name> python=<X>. (When the spec's base is a Docker image string, treat it as documentation of the python + CUDA versions you need, not something to pull.) Weights live in scratch or home; download once, point the tool's cache env var there. The env name is the conda env name — name it exactly what you want --env to accept, there's no aliasing layer. submit_job activates it (conda run -n <name> …) and runs. "Registering" is implicit: once the host is added as a Claude Science SSH provider, its probe lists conda envs, and you append a ### env: block to compute_details so the next agent knows what's there without re-probing. Lowest ceremony; often right for a personal GPU box.
Scheduler cluster (Slurm, PBS, LSF). Shared filesystem, login node, compute nodes via srun/sbatch, usually no root. Software is module load <name> or Apptainer/Singularity containers in a shared path. For containers: apptainer pull <name>.sif docker://<ref> if the image was built elsewhere; apptainer build --fakeroot <name>.sif <name>.def only if the cluster enables unprivileged user namespaces (many don't — build off-cluster and pull). Set APPTAINER_CACHEDIR/APPTAINER_TMPDIR to scratch first or the layer cache will blow your home quota. For modules: write the modulefile under a personal tree ($HOME/modulefiles/) and module use $HOME/modulefiles in the job preamble — you almost certainly can't write the system MODULEPATH. Weights go in shared scratch; note that scratch is usually purge-on-idle, so record the purge window and consider mirroring to a project/group quota. Tier becomes scheduler directives, and on most clusters --account, --partition, and --time are mandatory alongside --gres=gpu:<type>:1 --cpus-per-task --mem. Compute nodes often have no internet — egress is not a fallback here; pre-stage everything from the login or data-transfer node.
Container via bridge runner. The compute happens inside ephemeral containers launched by some service (a sandboxing API, Kubernetes, a cloud batch system), but Claude Science talks to it through a small persistent bridge host — an SSH-reachable box that holds credentials and a runner script. Building means producing a container image (Dockerfile → registry, or the service's image builder) and caching it where the service can pull it. Weights are mounted read-only from object storage or a volume the service supports. The runner script (advanced_runner.py is the worked example) holds a literal ENV_TABLE = {"<name>": {"image": <ref>, "tier": …, "mounts": …, "egress": …}} and translates --env <name> into the service's launch call. "Registering" is adding an entry to that table and redeploying the script. This shape exists because the service's own API is either not directly reachable from Claude Science or needs credentials you don't want in every agent process.
Managed API with native adapter (byoc). Modal, RunPod, a cloud Batch service — anything where Claude Science talks to the provider's SDK directly instead of going through a bridge host. Each provider's adapter ships a directory of bundled env definitions that are SDK code (not a spec to translate); you build one by calling build_env(name) inside the compute_provider kernel — a confined Python shell where the SDK is already authenticated. What comes back is the provider's opaque image reference plus any volumes the env mounts; those strings are all the job surface needs. Weights populate inside the same kernel so multi-gigabyte downloads happen on the provider's network rather than the local allowlist. Name resolution is the same ### env:<name>@<specHash> ledger block as everywhere else — the content hash means a definition change is a cache miss and an unchanged one warm-reuses without rebuilding. Once the adapter exists, this shape has the least friction of any of them.