Skip to content

Commit b2de5bc

Browse files
authored
Merge pull request #19 from KadenMc/feat/aexp-airgapped-ssh
feat: rework aexp.airgapped from login-node daemon to direct SSH
2 parents 5e8a2ec + 229bf7e commit b2de5bc

11 files changed

Lines changed: 1911 additions & 1241 deletions

File tree

CHANGELOG.md

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.4.0] - 2026-05-20
11+
12+
### Added
13+
14+
- **`aexp airgapped init` CLI command.** One-shot setup that writes the
15+
`AEXP_RELAY_SSH_HOST` / `AEXP_RELAY_REMOTE_REPO` env keys into the
16+
`aexp` MCP server's `env` block in `.mcp.json` (idempotent; `--force`
17+
to overwrite a conflict), then prints the `~/.ssh/config` snippet to
18+
paste in plus the remaining manual steps. Collapses the airgapped
19+
setup to one command + paste a block + `ssh` once + `/mcp` reconnect.
20+
21+
### Changed
22+
23+
- **`aexp.airgapped` reworked from a login-node daemon to direct SSH
24+
(BREAKING).** The relay now runs each whitelisted op as a per-call
25+
`ssh <host> "cd <repo> && <git ...>"` from the user's local machine —
26+
no daemon, no file queue, no heartbeat.
27+
- `RelayClient` now takes `ssh_host` / `remote_repo` (or
28+
`$AEXP_RELAY_SSH_HOST` / `$AEXP_RELAY_REMOTE_REPO`) instead of
29+
`queue` / `cwd`.
30+
- `request()` signature changed: `cwd` removed; `ssh_host`,
31+
`remote_repo`, `approve` added. `validate_request()` now takes
32+
`(op, args)` and no longer validates a `cwd`.
33+
- Removed: the `Daemon` class, `ensure_queue`, `DEFAULT_QUEUE`,
34+
`RelayCrashedError`, the `daemon` / `install-helpers` CLI verbs, and
35+
the `AEXP_RELAY_CWD_NAMES` env var.
36+
- `RelayDownError` now means "SSH could not reach the login node".
37+
- Consent-required ops (`wandb_sync`) now require an explicit
38+
`approve=True` / `--approve` instead of the file-based approve/reject.
39+
- `ssh` is invoked with `-n` and `stdin=subprocess.DEVNULL` so it never
40+
inherits the caller's stdin. Without this, the relay hangs when
41+
called from a long-lived process whose stdin is a never-closing pipe
42+
(an MCP server's stdio transport is exactly this): ssh stays alive
43+
after the remote command finishes, waiting on a stdin EOF that never
44+
comes.
45+
- Timeout errors now surface ssh's captured partial stderr; the
46+
`AEXP_RELAY_SSH_VERBOSE=1` env var adds `ssh -vv` for diagnosing
47+
connection / auth hangs.
48+
- New: an `aexp airgapped` CLI subcommand group, `mcp__aexp__airgapped_*`
49+
MCP tools, a `check_connection()` helper, and a local-side audit log
50+
at `~/.aexp/airgapped-relay.log`.
51+
1052
## [0.3.0]
1153

1254
### Release summary
@@ -109,8 +151,8 @@ brittleness session:
109151
`Path("notebooks/...").resolve()`).
110152
- **`aexp.airgapped`** — a file-queue bridge between a no-internet
111153
compute node and an internet-having login node sharing `$HOME`,
112-
designed for secure HPC where SSH from the agent's runtime is
113-
forbidden. A daemon under `tmux` on the login node services a closed
154+
designed for secure HPC where the agent's runtime is network-isolated.
155+
A daemon under `tmux` on the login node services a closed
114156
whitelist (`git_pull / push / fetch / status / rebase` auto-approved,
115157
`wandb_sync` consent-gated) via atomic-rename JSON requests on a
116158
shared filesystem. `RelayClient` exposes the git verbs as semantic

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ The design bet: agents already know how to run experiments. What they need is a
131131
| | |
132132
|---|---|
133133
| **Sandbox scaffolding** | `/aexp-new-sandbox` (or `aexp new-sandbox --slug ...`) creates `notebooks/_sandbox/<YYYY-MM-DD>_<slug>/` with a directional-experiment README template, a `helpers.py` skeleton, and (on first use) a sandbox-root README + `.gitignore` for large outputs. Sandbox subdirs are deliberately **outside** the H→E→F enforcement chain — agent-autonomous-write territory for free-form exploration that hasn't yet earned a tracked artifact. The `aexp.sandbox.setup_sandbox_notebook` first-cell helper closes the kernel-cwd-vs-repo-root trap on remote Jupyter setups. See [docs/sandbox.md](docs/sandbox.md). |
134-
| **Airgapped relay** (opt-in import) | `from aexp.airgapped import RelayClient` exposes a file-queue bridge between a no-internet compute node and an internet-having login node sharing `$HOME`. Designed for secure HPC sites where SSH from the agent's runtime is forbidden. The daemon (under `tmux` on the login node) services a closed whitelist — `git_pull / push / fetch / status / rebase` auto-approved, `wandb_sync` consent-gated — via atomic-rename JSON requests. `RelayClient` exposes the git verbs as semantic methods (`.pull()`, `.push(branch=...)`, etc.) so consumers don't hand-construct args. See [docs/airgapped.md](docs/airgapped.md). |
134+
| **Airgapped relay** (opt-in; **skip unless your compute machine has no internet**) | If `git pull` works where you run Jupyter, you don't need this — it's not imported at package init and zero-cost to ignore. Otherwise (network-isolated compute with a sibling node that has internet and shares `$HOME` — common at HPC sites, also some clinical / government / research-lab setups): `from aexp.airgapped import RelayClient` runs whitelisted git/wandb commands on that sibling node over SSH from the laptop, against the shared-`$HOME` repo. Closed whitelist (`git_pull / push / fetch / status / rebase` auto-approved, `wandb_sync` consent-gated). One-shot setup via `aexp airgapped init`. Three surfaces: Python `RelayClient`, `aexp airgapped` CLI, and `mcp__aexp__airgapped_*` MCP tools. See [docs/airgapped.md](docs/airgapped.md). |
135135

136136
---
137137

@@ -246,7 +246,7 @@ So a session can end cleanly with a broken `supporting_runs` citation still pres
246246
| [docs/queue.md](docs/queue.md) | Queue, runner-script materialization, sp resolution, drift-proof provenance, cross-machine sync |
247247
| [docs/threads.md](docs/threads.md) | Threads (`T###`) — forward-looking research concerns broader than a hypothesis: lifecycle, linkage to H/E/F, required template sections |
248248
| [docs/sandbox.md](docs/sandbox.md) | Sandbox scaffolding — `notebooks/_sandbox/` layout, the `/aexp-new-sandbox` slash command, the notebook first-cell convention, promotion path to tracked artifacts |
249-
| [docs/airgapped.md](docs/airgapped.md) | Airgapped relay — file-queue bridge between a no-internet compute node and an internet-having login node sharing `$HOME`; daemon bootstrap, client API, whitelist, hardening |
249+
| [docs/airgapped.md](docs/airgapped.md) | Airgapped relay — per-call SSH bridge that runs whitelisted git/wandb commands on an internet-having login node; SSH/ControlMaster setup, client API, CLI, MCP tools, whitelist, consent |
250250

251251
---
252252

@@ -266,7 +266,7 @@ src/aexp/
266266
schema.py # pydantic + dataclass types
267267
mcp_server.py # FastMCP server — optional [mcp] extra
268268
sandbox.py # scaffold notebooks/_sandbox/<date>_<slug>/ + setup_sandbox_notebook
269-
airgapped/ # opt-in: no-internet compute ↔ login-node relay (RelayClient, daemon)
269+
airgapped/ # opt-in: SSH relay to a login node for airgapped compute (RelayClient)
270270
hooks/ # Claude Code hooks (session_start, enforce_hef_chain, kb_write_guard, stop_validate)
271271
slash_commands/ # /aexp-* templates
272272
trackers/ # TrackerAdapter ABC + noop + wandb adapters

0 commit comments

Comments
 (0)