From 59cd13cb824e661dd6d5805e6ef616cd2b8b281e Mon Sep 17 00:00:00 2001 From: dawn <90008@klbr.net> Date: Sat, 3 Oct 2026 19:15:50 +0300 Subject: [PATCH] remote: tell the model to connect instead of running ssh, in a few lines Signed-off-by: dawn <90008@klbr.net> --- robot-docs/extensions.md | 2 + .../harness/extensions/remote/extension.gleam | 65 ++++++------------- 2 files changed, 22 insertions(+), 45 deletions(-) diff --git a/robot-docs/extensions.md b/robot-docs/extensions.md index 1cd35769..ed61f95d 100644 --- a/robot-docs/extensions.md +++ b/robot-docs/extensions.md @@ -179,6 +179,8 @@ Optional. A signed HTTP hook wakes one persistent session, with an opt-in settin the `remote` extension boots this session's Python kernel on a remote host over one SSH connection, so every harness tool is callable on it, and it is enabled by default. `rem = await remote.connect()` stages albedo's python bundle on the target, starts the kernel there through SSH with `ControlMaster`/`ControlPersist` (one multiplexed TCP connection: no re-authentication per call), and answers the remote kernel's host-route calls against this session's daemon. +the model's instructions are short on purpose: `remote.connect(host)` is ssh for the kernel, to use instead of `run("ssh", host, command)`, with one example of `rem.run` and the points where it differs from local (awaiting remote calls, the sign-in command, degraded mode). sessions kept running ssh as `run` jobs because the longer text described how it worked and never said when to use it. + the ssh options, control path, agent socket discovery, login-shell wrapper and bundle staging live in `priv/python/albedo_ssh.py`, which the daemon's own remote kernels use too (kernel.md, "remote kernels"), so both ride the same control sockets and the same staged bundle under `~/.albedo-remote/`. a session whose workspace is itself on another host needs no `remote.connect()`: its kernel already runs there. machine tools run on the remote kernel: `rem.run(program, *args)` starts a supervised job on the host immediately and synchronously, like local run — the handle exists right away, `job.tail()`, `job.id`, `job.exit_code`, `job.duration`, `job.waited`, and `job.timed_out` answer synchronously from the mirrored output stream, `await job` waits for completion, and `await job.stop()` stops it. pipes run entirely on the host, chained before the one await: `job = rem.run("journalctl", "-u", "nginx", "--no-pager").pipe("rg", "error")`, `await job`, then `job.tail(lines=40)`. `tail(lines=)` and `exit_code` answer from the mirror; other job methods, like `head(lines=)`, are remote calls to await, and properties such as `job.pipeline` do not cross. session tools run where the daemon runs: `await rem.work.*` and `await rem.skills.*` relay over the connection to the local daemon's ledger and catalog. both kernels load the same content-hashed bundle, so a tool's remote shape matches its local one. every other call returns a reference that settles on its first await: values cross as real objects — dataclasses and list subclasses are rebuilt, so remote output prints exactly like local output — and a result that cannot cross comes back as a live reference whose methods are further remote calls. references passed back into remote calls stay references, not copies, including ones still in flight. one rule remains: a call whose local counterpart is synchronous (`rem.files.read`) still needs `await`, because the value itself crosses the network; handles and their state never do. diff --git a/src/albedo/harness/extensions/remote/extension.gleam b/src/albedo/harness/extensions/remote/extension.gleam index 78522591..211f8354 100644 --- a/src/albedo/harness/extensions/remote/extension.gleam +++ b/src/albedo/harness/extensions/remote/extension.gleam @@ -7,51 +7,26 @@ pub fn extension() -> harness_extension.Extension { "remote", "Run this session's Python kernel on a remote host over SSH; every tool is callable on it.", "remote", - "remote.connect(host=None) boots this session's Python kernel on a remote host over" - <> " one SSH connection (ControlMaster) and returns a connection handle: rem =" - <> " await remote.connect(). Every harness tool is then callable on it. Machine tools" - <> " run on the remote kernel: rem.run(program, *args) starts a supervised job there whose" - <> " handle supports await, tail(), poll(), and stop(), as in rem = await" - <> " remote.connect(\"devbox\"); job = rem.run(\"git\", \"status\"," - <> " cwd=\"proj/albedo\"); await job; job.tail(lines=20), or a pipe that runs entirely on the" - <> " host: job = rem.run(\"journalctl\", \"-u\", \"nginx\", \"--no-pager\").pipe(\"rg\"," - <> " \"error\"); await job; job.tail(lines=40). tail(lines=) and exit_code answer from the mirror;" - <> " other job methods are remote calls to await: await job.head(lines=20). await" - <> " rem.files.read(...)" - <> " and await rem.files.find(...) operate on the remote filesystem. Session tools" - <> " run where the daemon runs: await rem.work.* and await rem.skills.* relay over" - <> " the connection to this session's daemon. rem.run(...) starts the job" - <> " immediately and synchronously, like local run: the handle exists right away," - <> " job.tail(), job.id, job.exit_code, job.duration, job.waited, and" - <> " job.timed_out answer synchronously from the mirrored output stream," - <> " await job waits for completion, and await job.stop() stops it. A remote" - <> " job that finishes with its result unread wakes the session by itself," - <> " naming the host it ran on; awaiting it, reading its result, or stopping" - <> " it first means no wake. Every other call" - <> " returns a reference that settles on its first await: await rem.files.find(...)" - <> " resolves to its value, await rem.work.list() to the daemon's answer. Results" - <> " cross as real objects, so remote output looks exactly like local output; a" - <> " result that cannot cross is a live reference whose methods are further remote" - <> " calls, and references passed back into remote calls stay references rather than" - <> " becoming copies. A call whose local counterpart is synchronous (rem.files.read)" - <> " still needs await, because the value itself crosses the network; handles and" - <> " their state never do. await rem.tools() lists what the remote namespace holds;" - <> " await rem.read(path) and await rem.write(path, content) move one file's text" - <> " directly, and await rem.show_image(path_or_bytes) returns a remote image with" - <> " this cell's result under the same limits as show_image. remote.connections() lists open connections (awaiting it also works) and await" - <> " remote.close_all() ends them all. Failures raise RemoteError." - <> " The target resolves per call from a host=" - <> " argument, then remote.configure(host, remote_cwd=None), then $ALBEDO_SSH" - <> " (user@host[:/path]), or the \"remote\" section of extensions.json (host," - <> " remoteCwd, python). If the host is reachable but its kernel cannot boot, the" - <> " connection still returns, prints a warning, and answers only rem.run(program," - <> " *args, cwd=None, env=None, stdin=None, timeout=300), rem.read(path), rem.write(path," - <> " content), and rem.show_image(path_or_bytes). The degraded-mode warning gives an explicit rem.shell(command) escape" - <> " hatch for pipes; it is unavailable in kernel mode. Degraded stdin takes only" - <> " text or bytes, not a file or job; .pipe() is unavailable, and stopping SSH" - <> " cannot prove the remote process stopped. Connections are" - <> " scoped: a lost SSH channel invalidates the connection rather than silently" - <> " reconnecting, and await rem.close() ends it.", + "remote.connect(host) is ssh for this kernel: to work on another machine, use it" + <> " instead of run(\"ssh\", host, command). rem = await remote.connect(\"deploy@box\")" + <> " takes what ssh takes (an ~/.ssh/config alias, user@host:/abs/cwd, or" + <> " ssh://user@host:port/abs/cwd) and signs in once for the whole connection, where" + <> " each raw ssh job may sign in again (for the user, a hardware-key touch each)." + <> " rem.run is run() on that host, pipes included, and its jobs wake the session like" + <> " local ones: job = rem.run(\"journalctl\", \"-u\", \"nginx\", \"--no-pager\").pipe(\"rg\"," + <> " \"error\"); await job; job.tail(lines=40). Every other tool runs there too and" + <> " crosses the network, so await it even where the local call is synchronous:" + <> " await rem.files.read(path), await rem.files.find(...), await job.head(lines=20);" + <> " job.tail() and job.exit_code answer without one. await rem.read(path) and" + <> " await rem.write(path, content) move one file, await rem.show_image(path) shows a" + <> " remote image, await rem.tools() lists what is there, and rem.work and rem.skills" + <> " reach this session's daemon. If connect says the host needs a person to sign" + <> " in, give the user the command it names. If python can't start there, connect" + <> " warns and leaves rem.run plus rem.shell(\"cmd | cmd\"). remote.configure(host)" + <> " sets the default target (else $ALBEDO_SSH, or the \"remote\" section of" + <> " extensions.json); remote.connections() lists connections, and await rem.close()" + <> " and await remote.close_all() end them. A dropped connection stays dropped:" + <> " connect again. Failures raise RemoteError.", ["python"], ) } -- knotmirror 0.0.1