feat: rework aexp.airgapped from login-node daemon to direct SSH#19
Merged
Conversation
BREAKING CHANGE: aexp.airgapped's transport changes from a file-queue plus login-node daemon to per-call SSH from the user's local machine. The relay now runs each whitelisted op as `ssh <host> "cd <repo> && <git ...>"` -- no daemon, no file queue, no heartbeat, nothing persistent on the remote side. API changes: - RelayClient takes ssh_host / remote_repo (or $AEXP_RELAY_SSH_HOST / $AEXP_RELAY_REMOTE_REPO) instead of queue / cwd. - request() drops cwd; adds ssh_host, remote_repo, approve. validate_request() now takes (op, args). - Removed: Daemon, ensure_queue, DEFAULT_QUEUE, RelayCrashedError, the daemon / install-helpers CLI verbs, AEXP_RELAY_CWD_NAMES. - RelayDownError now means "SSH could not reach the login node". - Consent ops (wandb_sync) require explicit approve=True / --approve. New surfaces: - `aexp airgapped` CLI group (status / pull / push / fetch / repo-status / rebase / wandb-sync / init), wired into the top-level CLI; `python -m aexp.airgapped` still works. - mcp__aexp__airgapped_* MCP tools (7) in mcp_server.py. - `aexp airgapped init` -- one-shot setup: writes the relay env keys into .mcp.json, prints the ~/.ssh/config block + remaining steps. - check_connection() helper; local-side audit log at ~/.aexp/airgapped-relay.log. Robustness: - ssh runs with `-n` and stdin=subprocess.DEVNULL so it never inherits the caller's stdin. Without this the relay hangs when called from a long-lived process whose stdin is a never-closing pipe (an MCP server's stdio transport): ssh stays alive after the remote command finishes, waiting on a stdin EOF that never comes. - Timeout errors surface ssh's captured partial stderr; AEXP_RELAY_SSH_VERBOSE=1 adds `ssh -vv` for diagnosis. tests/test_airgapped.py reworked (57 tests). docs/airgapped.md rewritten; README + CHANGELOG updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bump version 0.3.0 -> 0.4.0 for the aexp.airgapped daemon-to-SSH rework (breaking change to the airgapped surface). Renames the CHANGELOG [Unreleased] block to [0.4.0]. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Reworks
aexp.airgappedfrom a file-queue + login-node daemon to per-call SSH from the user's local machine. The relay now runs each whitelisted op asssh <host> "cd <repo> && <git ...>"-- no daemon, no file queue, no heartbeat, nothing persistent on the remote side.BREAKING for
aexp.airgappedconsumers (the surface shipped in #17; no production code depended on the daemon API -- electricrag only referenced it from docs).API changes
RelayClienttakesssh_host/remote_repo(or$AEXP_RELAY_SSH_HOST/$AEXP_RELAY_REMOTE_REPO) instead ofqueue/cwd.request()dropscwd; addsssh_host,remote_repo,approve.validate_request()now takes(op, args).Daemon,ensure_queue,DEFAULT_QUEUE,RelayCrashedError, thedaemon/install-helpersCLI verbs,AEXP_RELAY_CWD_NAMES.RelayDownErrornow means "SSH could not reach the login node".wandb_sync) require an explicitapprove=True/--approve.New surfaces
aexp airgappedCLI group --status / pull / push / fetch / repo-status / rebase / wandb-sync / init, wired into the top-level CLI;python -m aexp.airgappedstill works.mcp__aexp__airgapped_*MCP tools (7) inmcp_server.py.aexp airgapped init-- one-shot setup: writes the relay env keys into.mcp.json, prints the~/.ssh/configblock + remaining manual steps.check_connection()helper; a local-side audit log at~/.aexp/airgapped-relay.log.Robustness
sshruns with-nandstdin=subprocess.DEVNULLso it never inherits the caller's stdin. Without this the relay hangs when called from a long-lived process whose stdin is a never-closing pipe -- an MCP server's stdio transport is exactly this:sshstays alive after the remote command finishes, waiting on a stdin EOF that never comes. (Found via a full local reproduction driving the real MCP server; regression test included.)AEXP_RELAY_SSH_VERBOSE=1addsssh -vvfor diagnosis.Verification
tests/test_airgapped.pyreworked -- 57 tests: SSH transport, remote-command shlex quoting, ssh-vs-git failure disambiguation, consent gating,init, and a regression test that ssh never inherits the caller's stdin. All green; ruff + mypy clean on the airgapped package.RelayClient, CLI, MCP tools) -- clean 7/7 sweep.docs/airgapped.mdrewritten;README+CHANGELOGupdated.🤖 Generated with Claude Code