Skip to content

feat(mcp): SSH agent clones pinned to the checked address behind the Terminals gate - #249

Closed
mubeda wants to merge 11 commits into
mainfrom
v015b/217-mcp-ssh-clone
Closed

mubeda wants to merge 11 commits into
mainfrom
v015b/217-mcp-ssh-clone

Conversation

@mubeda

@mubeda mubeda commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

Closes #217

Implements the approved design docs/plans/2026-11-01-mcp-ssh-agent-clone-design.md. The user chose option A1 on 2026-11-01, and Codex reviewed the design over 3 rounds.

Update 2026-11-02 (back in v0.15.0)

  • The design's Status line now reads "Approved by Mauro 2026-11-02 (option A1; Codex review, 3 rounds)".
  • Merged main (feat(web): resizable Git Changes panes and next/previous change across files #241), with no conflicts.
  • Fixed the Windows x64 failure (backend::tests::an_unparsable_operation_id_reads_the_same_on_both_transports: "SQLite online backup WAL checkpoint remained busy (163/163 pages)"). Refs Tests: harden load-sensitive web, device, desktop and queue tests seen in the v0.15 batch #263.
    • Cause: update preparation calls Database::checkpoint_wal. That ran PRAGMA wal_checkpoint(TRUNCATE) once and refused any busy result. Here every committed page was already in the database file, and only the WAL reset waited on a reader.
    • Fix: a busy result now passes only with non-negative counts and every page moved. Two cases still fail: busy with -1 counts, which means another checkpointer held the lock and nothing moved (found by Codex), and pages left in the WAL.
    • Tests:
      • a_checkpoint_that_moved_every_page_succeeds_while_a_reader_holds_the_wal failed before the fix with exactly CI's error, remained busy (52/52 pages), and passes now.
      • a_checkpoint_that_leaves_committed_pages_behind_still_fails passes.
      • a_busy_checkpoint_is_complete_only_when_every_page_moved covers each result case.
    • Checks: persistence:: and maintenance:: lib tests (122) pass, as do --test production_maintenance (16), cargo fmt --all --check and Clippy with -D warnings.
    • Codex: the review found the -1 case (NEEDS-FIX), which is now fixed and covered by the case test.

Changes

  • The gate (A1): outside agents may clone ssh://[user@]host[:port]/path only if they already pass the Terminals gate: MCP on, Terminals on, effective ceiling Full access, and terminal:operate.
    • The terminal tools and the clone path share one predicate, mcp::control::acts_as_server_user.
    • Live settings and live session access are read for every clone, so turning Terminals off or a Take over applies to the next clone, through both the tool and the direct RPCs.
  • Refusals:
    • Every other agent gets clone_ssh_not_allowed, naming the https:// equivalent for GitHub, GitLab, Bitbucket and Codeberg on port 22.
    • scp-like user@host:path and git+ssh:// are refused with the ssh:// spelling.
    • An SSH URL whose rewrite differs is refused, so the clone uses the URL as written.
  • The pinned ssh command:
    • It runs with -F none and HostName=<checked public address>. HostKeyAlias is the host on port 22, or [host]:port on other ports.
    • The person's known_hosts is read but never written: StrictHostKeyChecking=yes, BatchMode=yes, UpdateHostKeys=no.
    • Only keys loaded in the ssh-agent are offered: IdentityFile=none with IdentityAgent=SSH_AUTH_SOCK. There is no proxy, control master or forwarding.
    • Stalls end on their own: ConnectTimeout=30, ServerAliveInterval=15, ServerAliveCountMax=4.
    • Paths are escaped for OpenSSH's own parser, then for Git's sh -c.
    • An inherited GIT_SSH_COMMAND, GIT_SSH, GIT_SSH_VARIANT or core.sshCommand is never used.
  • Agent selection:
    • Linux and macOS: SSH_AUTH_SOCK when it is a socket.
    • Windows: Git's ssh with SSH_AUTH_SOCK, or the System32 OpenSSH client with the openssh-ssh-agent pipe.
    • With no agent, the clone fails with clone_ssh_unavailable.
  • Failure messages: they name the plan's alias, port and known_hosts file. For an unknown host they include a trust command that uses the plan's HostKeyAlias. The reason is kept in the direct sourceControl.cloneRepository RPC too.
  • Process ownership: ssh runs as Git's child in the existing supervised process group, so cancel stops it. A test asserts the ssh child has exited.

Validation

  • node scripts/run-msvc.mjs cargo test -p bibcode-server --lib -- clone_policy mcp_tools_tests::clone: 34 pass (17 policy, 17 clone integration), 3 runs in a row.
  • Real-ssh tests ran on both Windows clients (Git's OpenSSH 10.6 and Windows OpenSSH 9.5). They cover ssh -G reading the generated command, a silent server ending the clone without a cancel, and the trust command.
  • Reverting to the HTTPS-only refusal, or removing the %% escaping, failed 14 tests. The reason test failed before its fix.
  • cargo fmt --all --check, clippy -D warnings, vp check, vp run typecheck: pass.
  • Manual runbook steps added to docs/testing/cross-platform-validation.md (step 15) and docs/testing/ssh-environments.md: real sshd, ssh-add -c, a host certificate, and ports 22 and 2222.
  • Not run on this host: the Linux and macOS paths. CI's Linux lib tests are their first run.

Review

  • Codex on the implementation, 3 rounds. They found the cancel test not proving ssh stopped, the RPC dropping the reason, and a Windows pid race in the test. All are fixed.
  • UI.md: one wording fix, so the refusal says how to get access.
  • Not run: an advisor review, because no advisor tool was available in this session.

Profiler coverage: ssh is a child of the supervised clone process group (cancel, the 24 h bound and reaping cover it); vcs.clone is already long-running. No URL user, key or known_hosts content is logged.
Docs: docs/reference/mcp.md, docs/user/mcp.md, docs/architecture/rpc-and-orchestration.md, plus the runbooks above.

Known limitations (from the design)

  • A server that trickles data holds a clone until Cancel or the 24 h bound.
  • An ssh-add -c confirmation dialog belongs to the person's agent.
  • Host certificates verify on port 22 only.
  • Jump hosts fail closed.
  • A known_hosts path set elsewhere in ssh_config is not read.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YWW672NbkSQaa8cTJBLP3P

mubeda and others added 8 commits November 1, 2026 18:30
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWW672NbkSQaa8cTJBLP3P
The user approved option A1 on 2026-11-01. Revision 1 resolves the
Codex REVISE findings: transport and pin come from the written URL
(git clone ignores the enclosing repository's insteadOf), host
certificates on non-default ports are a fail-closed limitation,
ssh-add -c confirmation is documented as the agent's own prompt,
ServerAliveInterval/CountMax bound silent servers, and the unknown-host
recovery command uses the plan's port and known_hosts file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWW672NbkSQaa8cTJBLP3P
Escape known_hosts paths for OpenSSH's argv_split and %-token expansion
before shell quoting, keep the agent socket out of the command with
IdentityAgent=SSH_AUTH_SOCK, give the recovery command the plan's
HostKeyAlias so certificate fallbacks save the plain key, and start the
confirmation-refusal test agent with SSH_ASKPASS_REQUIRE=never.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWW672NbkSQaa8cTJBLP3P
)

An outside agent that passes the Terminals gate (Terminals on, Full access,
terminal:operate, read live) may clone ssh:// URLs with the person's SSH
agent. ssh runs through BiBCode's own GIT_SSH_COMMAND: -F none,
HostName=<checked address>, HostKeyAlias=<host> ([host]:port off 22),
strict known_hosts checking that never writes, agent keys only, no proxy,
multiplexing or forwarding, ConnectTimeout and ServerAlive for stalls.
Known_hosts paths get OpenSSH option escaping then shell quoting. Every
other agent gets clone_ssh_not_allowed naming the https:// URL. An SSH URL
an enclosing insteadOf rewrites is refused, since git clone uses the URL as
written. ssh's own refusals get actionable advice; a missing agent or an
ssh lacking an option is clone_ssh_unavailable.

The Terminals gate is one predicate shared by the terminal tools and the
clone policy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWW672NbkSQaa8cTJBLP3P
Approve the design, describe the SSH plan, gate and reasons in the MCP
reference, user guide and architecture, and add the real-sshd, agent,
certificate and ssh-add -c steps to the validation runbooks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWW672NbkSQaa8cTJBLP3P
…cy (#217)

The stall test now asserts ssh connected and the failure carries no policy
reason, so a refusal before ssh runs cannot pass it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWW672NbkSQaa8cTJBLP3P
…ory (#217)

The direct RPC rebuilt transfer failures from their detail and dropped
reason: clone_ssh_unavailable; it now keeps the reason as the plan's
refusals do. The cancel test now checks that the fake ssh's sleeping child
has exited (its OS pid, the Windows pid under MSYS), not only that the
folder is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWW672NbkSQaa8cTJBLP3P
)

MSYS changes a forked child's Windows pid when it execs, so the fake ssh
now waits (up to 10 s) until /proc/<child>/exename is sleep before reading
its winpid; the cancel test then watches the real sleeper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWW672NbkSQaa8cTJBLP3P
mubeda and others added 3 commits November 2, 2026 17:06
…eset waits on a reader

PRAGMA wal_checkpoint(TRUNCATE) reports busy when every committed page is
already in the database file but a reader keeps the WAL from being reset.
Update preparation refused that, which failed the Windows desktop lane
("WAL checkpoint remained busy (163/163 pages)"). A busy result now passes
only with non-negative counts and every page moved; -1 counts (another
checkpointer held the lock) and pages left in the WAL still fail.

Refs #263

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWW672NbkSQaa8cTJBLP3P
@mubeda

mubeda commented Nov 2, 2026

Copy link
Copy Markdown
Owner Author

Merged into v015/batch, which will ship as the single "BiBCode v0.15.0" PR to main. Its issues are closed by that PR. The branch is kept.

@mubeda mubeda closed this Nov 2, 2026
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Nov 3, 2026
@mubeda
mubeda deleted the v015b/217-mcp-ssh-clone branch November 5, 2026 02:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP clone: allow SSH agent clones pinned to the checked public address

1 participant