| name | remote-compute-ssh |
| description | Evaluate and use SSH Remote Compute before choosing where to run GPU, high-memory, parallel, batch, model-inference, bioinformatics, or other long-running scientific work; supports short remote commands and asynchronous jobs with automatic harvest and analysis. |
| license | Apache-2.0 |
This skill covers remote compute over SSH: listing hosts, creating handles, running short
remote commands (callCommand), reading/writing host knowledge docs, and the full async
job lifecycle — submit → harvest → analysis turn → publish artifacts.
Where host.compute runs: host.compute lives ONLY on the control-plane REPL kernel — run
every example below with the repl_execute tool (JavaScript), the same kernel that hosts
host.mcp. The python/r data kernels have NO host.compute (SSH and approvals stay outside
the sandbox workspace); calling it from a python/r cell will fail with host.compute is undefined.
Choose an execution location
Only Compute Hosts enabled for this Session are visible or callable. Discover them in one catalog;
each entry has role selected or available. A non-empty selected pool is an execution instruction:
run tool-backed task work on one or more selected hosts as the task requires. The pool has no
priority and does not imply automatic multi-host scheduling. If no host is selected, choose from the
available entries. Read details() only for candidates that need closer evaluation.
Never guess or reuse a provider id absent from the catalog. A user naming a disabled host does not
make it callable; explain that it must first be enabled for this Session. If no eligible host is
usable, explain the blocker and ask the user how to proceed.
const hosts = await host.compute.listHosts()
const selectedHosts = hosts.filter((host) => host.role === 'selected')
const candidates = selectedHosts.length > 0 ? selectedHosts : hosts
Each list item is a compact summary with provider_id, display_name, shape, status, and role
(last_probe_ok, probe_failed, or not_probed). last_probe_ok means the most recent persisted
Probe succeeded; it does not assert live connectivity. Knowledge documents and resource probe
snapshots are deliberately excluded from discovery results.
API reference
const hosts = await host.compute.listHosts()
const visibleHosts = await host.compute.listRegistered()
const selectedHosts = await host.compute.listPreferred()
const c = host.compute.create('ssh:<alias>')
const result = await c.callCommand('<shell command>', '<one-line intent for the approval card>', {
loginShell: true,
timeoutSeconds: 60
})
const info = await host.compute.details('ssh:<alias>', { mode: 'read' })
host..(, {
: ,
:
})
host..(, {
: ,
: ,
: info.
})
With loginShell: true, the remote Bash login profiles run first and then Open Science attempts to
source ~/.bashrc when it is readable. A .bashrc can deliberately return early for non-interactive
shells, so variables declared after such a guard are not available. A missing .bashrc is a no-op.
Set loginShell: false to run the command without either initialization step. Initialization failures
are reported through the normal command result/error behavior.
API reference (async jobs)
Use submitJob for long-running computations (minutes to hours). It returns immediately with a
job_id; the job runs on the remote host in the background. When the job finishes, the app
automatically harvests the outputs and initiates a new analysis turn. Do not poll for completion;
perform only the single bounded immediate-failure check below, then return control to the user.
const c = host.compute.create('ssh:<alias>')
const job = await c.submitJob(
'<one-line intent for the approval card>',
'<shell command>',
{
timeoutSeconds: 3600,
inputs: [
{ src: 'in.dat', dstFilename: 'in.dat' },
{ remotePath: 'ssh:<alias>/<abs_path>' }
],
outputs: [
'*.result',
{ glob: '*.json', visibility: 'featured' },
{ glob: '*.log', visibility: 'hidden' },
{ glob: 'checkpoints/**', residency: 'remote' }
],
harvest: {
exclude: ['work/**'],
: ,
:
}
}
)
( (resolve, ))
initial = c.(job.).()
initial
Immediate failure check after submission
Wait exactly once for 2 seconds (setTimeout(..., 2000)), then call .result() exactly once. The
result read is non-blocking for submitted and running jobs and includes status, stdout, stderr,
and error details already persisted by dispatch. This catches syntax errors, missing executables,
and other scripts that fail as soon as they start without waiting for a long-running job or starting
a second harvest. Do not wait again and do not turn this into a polling loop: after printing the
snapshot, end the cell and let the app own the rest of the lifecycle.
End the cell after that one check. Do NOT write a polling loop. The app runs the poller and harvest in the
background. When the job finishes, the app automatically starts a new analysis turn in this
conversation — the conversation is NOT locked while the job runs, so the user can keep chatting.
Harvest safety boundaries
- Declared output files are selected before
stdout and stderr; logs use the remaining per-job budget.
- The app rejects model-supplied limits above 100 MiB per file or 500 MiB per job.
- Harvest also preserves a fixed 2 GiB of free local disk space. Files that do not fit remain remote.
Behavior boundaries
- While the job runs: the conversation is open. The user can send messages; you can handle
other tasks. No blocking wait.
- When the job finishes: the app initiates a new analysis turn automatically. You do not
trigger this — it happens without any action on your part.
- Do NOT write a loop calling
attachJob().status() to wait for completion. That is the
app's job, not yours. Writing such a loop would block the conversation for the entire job
duration.
Check job status (non-blocking read, for informational use)
const handle = c.attachJob(job.job_id)
const s = await handle.status()
To stop one active job, request durable cancellation through the same handle:
await c.attachJob(job.job_id).cancel()
submitJob status values
| status | meaning |
|---|
submitted | accepted; background dispatch in progress |
running | remote process confirmed alive (pid recorded) |
success | exit code 0 |
failed | non-zero exit (job_failed) or process vanished (process_vanished) |
timeout | exceeded timeoutSeconds |
error | never reached the remote host (host_unreachable / dispatch_failed) |
Workflow: the analysis turn
When the app initiates the analysis turn, it provides the job_id, status, and
featured_files (workspace-relative paths under hpc/<job_id>/featured/). In this turn:
- Call
attachJob(job_id).result() to get the full result dict.
- Inspect the outputs, run any analysis needed.
- Call
write_artifact_file to publish outputs worth keeping as artifacts.
const c = host.compute.create('ssh:<alias>')
const r = await c.attachJob(job_id).result()
Files land in the workspace at hpc/<job_id>/ and are readable directly:
import pandas as pd
df = pd.read_csv('hpc/<job_id>/featured/results.csv')
Publish artifacts
Harvest only lands files in the workspace — it does NOT publish artifacts automatically.
Call write_artifact_file in the analysis turn to publish outputs worth keeping:
for (const path of r.featured_files) {
await host.mcp('artifacts', 'write_artifact_file', { path })
}
When the job fails
Read r.exit_code and r.stderr_tail. An infrastructure failure (wrong partition, env not
activated, missing module, OOM, walltime) is yours to fix — adjust command, record the fix,
fresh c.submitJob(). A harvest failure (r.stderr_tail notes it, r.remote_workdir is
preserved) means some files were not downloaded — the remote workdir is kept so you can
c.callCommand('ls ...', intent='...') to inspect what's there.
Chaining jobs via left_on_remote
Large outputs declared with residency: 'remote' or files that exceed the size threshold stay
on the remote host and appear in r.left_on_remote. Use their URIs directly as remotePath
inputs to the next job — no local round-trip:
const big_output_uri = r.left_on_remote[0].uri
const job2 = await c.submitJob(
'process big.h5 output from job 1',
'python process.py --input big.h5 --out summary.csv',
{
inputs: [
{ remotePath: big_output_uri }
],
outputs: ['summary.csv']
}
)
Submitting several jobs
Submit a batch and let each job's analysis turn handle its results independently. The app
triggers a separate analysis turn for each job as it finishes (or merges simultaneous
completions into one turn with multiple job_ids):
const c = host.compute.create('ssh:gpu-cluster')
const jobs = []
for (const seed of [0, 1, 2, 3, 4]) {
const job = await c.submitJob(
`AlphaFold seed ${seed}`,
`python fold.py --seed ${seed} --in input.fasta --out ranked.pdb`,
{
inputs: [{ src: 'input.fasta', dstFilename: 'input.fasta' }],
outputs: [{ glob: '*.pdb', visibility: 'featured' }],
timeoutSeconds: 3600
}
)
jobs.push(job.job_id)
}
return jobs
The app triggers one analysis turn per job completion (or a merged turn for simultaneous
completions). Do NOT write a loop collecting all results — each analysis turn handles
its job independently.
Session concurrency control
Cap how many non-terminal jobs run at once across all providers in this conversation. Jobs that
would exceed the cap enter a queued state and auto-dispatch when a slot frees up. These two
methods live on the handle returned by create(), but they are session-scoped — they act on
the whole conversation, not on the handle's bound provider.
const c = host.compute.create('ssh:<alias>')
await c.setConcurrencyLimit(2)
const s = await c.status()
callCommand error handling
try {
const r = await c.callCommand('cmd', '<intent>')
} catch (e) {
const code = e.error_code || ''
if (code === 'host_unreachable') {
} else if (code === 'approval_denied') {
} else if (code === 'timeout') {
}
}
Typical first-contact workflow
await host.compute.details(provider_id, { mode: 'read' }) — a ## Resources skeleton means
first contact; populated sections mean prior sessions did the legwork, trust them.
- Bind once:
const c = host.compute.create(provider_id).
- Run one batched probe:
await c.callCommand('id; module avail 2>&1 | head -40', '<intent>').
- Append what you learned via
await host.compute.details(..., { mode: 'append' }).
What to record in the knowledge doc
The knowledge doc is the only state that survives across sessions. Record:
- Scheduler type and any known partition/account combinations that worked.
- Environment activation commands (e.g.
module load X/<ver>, conda activate <env>).
- Verified invocations tagged
verified <date>; user-provided info tagged per user <date>.
- Gotchas specific to this host or provider.
Do NOT record per-job state, transient errors, or facts about your project — those belong
elsewhere. When a session ends without new host-specific learnings, write nothing.