| name | remote-compute-ssh |
| description | Discover and use SSH compute hosts. Load when you need to run remote commands or submit long-running 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 (call_command), 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.
Registered hosts
Run await host.compute.list() to see all registered hosts.
Each host entry shows:
- Display name
- Provider ID (e.g.,
ssh:biowulf, ssh:192.168.1.100)
- Shape (e.g.,
direct_ssh, slurm, pbs)
- Connection status
Session-active host
The user may enable one host for this conversation via the ≡ panel in the composer.
Always check which host is active before creating a handle:
const activeHosts = await host.compute.list_compute()
const c = activeHosts[0] ? host.compute.create(activeHosts[0]) : null
API reference
const hosts = await host.compute.list()
const activeHosts = await host.compute.list_compute()
const c = host.compute.create('ssh:<alias>')
const result = await c.call_command('<shell command>', '<one-line intent for the approval card>', {
login_shell: true,
timeout_seconds: 60
})
const info = await host.compute.details('ssh:<alias>', { mode: 'read' })
await host.compute.details('ssh:<alias>', { mode: 'append', text: '\n## Note\nlearned X on <date>' })
await host.compute.details('ssh:<alias>', {
mode: 'replace',
text: '<new full doc>',
old_text: info.doc
})
API reference (async jobs)
Use submit_job 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 — you never poll or block.
const activeHosts = await host.compute.list_compute()
const c = host.compute.create('ssh:<alias>')
const job = await c.submit_job(
'<one-line intent for the approval card>',
'<shell command>',
{
timeout_seconds: 3600,
inputs: [
{ src: 'in.dat', dst_filename: 'in.dat' },
{ remote_path: 'ssh:<alias>/<abs_path>' }
],
outputs: [
'*.result',
{ glob: '*.json', visibility: 'featured' },
{ glob: '*.log', visibility: 'hidden' },
{ glob: 'checkpoints/**', residency: 'remote' }
],
harvest: {
exclude: ['work/**'],
max_file_mb: 100,
max_total_mb: 500
}
}
)
print(job.job_id)
End the cell here. 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.
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
attach_job().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.attach_job(job.job_id)
const s = await handle.status()
submit_job 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 timeout_seconds |
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
attach_job(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.attach_job(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.submit_job(). 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.call_command('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 remote_path
inputs to the next job — no local round-trip:
const big_output_uri = r.left_on_remote[0].uri
const job2 = await c.submit_job(
'process big.h5 output from job 1',
'python process.py --input big.h5 --out summary.csv',
{
inputs: [
{ remote_path: 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.submit_job(
`AlphaFold seed ${seed}`,
`python fold.py --seed ${seed} --in input.fasta --out ranked.pdb`,
{
inputs: [{ src: 'input.fasta', dst_filename: 'input.fasta' }],
outputs: [{ glob: '*.pdb', visibility: 'featured' }],
timeout_seconds: 3600
}
)
jobs.push(job.job_id)
}
print(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.set_concurrency_limit(2)
const s = await c.status()
call_command error handling
try {
const r = await c.call_command('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.call_command('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.