Skip to content

feat(ssh): wire SshConnection through the work and transport close ledgers (#16741 T2 P2) - #24401

Merged
OrcaWin merged 3 commits into
mainfrom
OrcaWin/node-rt-p3-t2-p2
Oct 1, 2026
Merged

OrcaWin merged 3 commits into
mainfrom
OrcaWin/node-rt-p3-t2-p2

Conversation

@OrcaWin

@OrcaWin OrcaWin commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator
Files Added Deleted Net
Test 18 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1467 $\color{#cf222e}{\Huge{\mathbf{−}}}$​37 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1430
Prod 12 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1054 $\color{#cf222e}{\Huge{\mathbf{−}}}$​384 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​670

This is the P2 slice of the T2 port of #16741 (SSH core), building on #24210, which added the work ledger and transport close ledger without using them. This PR has SshConnection actually use them.

ELI5

An SSH connection now keeps a list of everything it has opened (commands, file transfers, terminals, forwarded sockets) and crosses each item off only when it has really closed. A later caller can then wait for "everything on this connection is closed" instead of guessing. Nothing a user does today waits on that yet, so behavior does not change.

What Changed

Before: SshConnection opened channels and transports, but nothing could tell when they had all physically closed. Closing a connection was fire-and-forget.

After: behavior is the same for users. Underneath:

  • Work tracking. Every operation goes through the connection's work ledger and stays tracked until its channel emits close, including channels that open after their caller gave up. That covers exec, shell (the new plain-SSH terminals), SFTP, directory and file uploads, downloads, file writes, upload sessions and system-SSH commands.
  • Transport tracking. Every ssh2 client and proxy process the connection allocates is tracked until it emits close, including clients whose login failed.
  • New methods, not yet called in production:
    • subscribeTransportClosure: a one-shot notice once the connection is disconnected, every tracked transport has closed and all work has drained. A system-SSH connection never gets this notice, because its child process's closure cannot be proven from here.
    • disconnectAndDrain(signal): for single-lifetime connections. It stops new work, disconnects, and waits until everything above has closed. If closure cannot be proven, for example on system SSH or with a connect still in progress, it cleans up and then refuses rather than claiming success.
    • getExecutionDestination: the host, port, user, accepted host-key fingerprint and proxy route that the current ssh2 login actually reached.
    • getTransportGeneration, plus prepareForwardRoute / openForwardSocket / forwardOut / forwardStreamLocal for forwarding callers in later slices.
    • An automaticReconnect constructor option, on by default.
  • System-SSH input. Ending the input of a system-SSH command now sends end-of-file to the ssh child process, as an ssh2 channel already does.
  • Channel errors stay visible. Tracking a channel adds an error listener. When that is the channel's only error listener, the error is logged as [ssh] Unhandled <kind> channel error for <target>: <message> instead of disappearing. Before this, such an error crashed the process.

Channel or client closure is treated as local evidence only. It is never reported as proof that a remote process exited, so the live / unverifiable / exited rule is unchanged.

Why

Later T2 and T3 work (connection-manager drain, reset, ownership moves) needs to know when a connection's work and transports are really gone. Tracking at the SshConnection boundary catches every caller without changing any of them, including the plain-SSH mode added in #24147, whose shell() and SFTP calls go through the same ledger.

  • Bodies kept intact. Each public operation calls its existing body through the ledger instead of re-indenting it into a wrapper, which would have churned hundreds of lines.
  • Logic moved to small modules. ssh-connection.ts ends at 1,872 lines, below main's 1,917, so it does not grow under its max-lines exemption. The public API is unchanged.
    • ssh-connection-transport-closure.ts and ssh-connection-close-drain.ts: the new closure gating and close drain.
    • ssh-channel-open.ts: the existing channel-open waiter and session-limit retry.
    • ssh-connection-file-transfers.ts: the existing transfer bodies, reading the connection through getters so each read sees the live transport.
    • ssh-forward-channel-lifetime.ts: the forward-route checks.
  • Not taken from feat(orcad): consolidate Bun runtime and SSH/Remote migration #16741, left for later slices:
    • the public fenceWorkForReset (T3); disconnectAndDrain fences through the ledger directly instead;
    • isEphemeralRuntimeSshOwner (T6);
    • assertProfileLifetimeAdmission (P8b);
    • the four connection-manager drain tests (P3);
    • the string-stdin and output-limit options for execCommand, because main already streams stdin.

Linked Issue

Part of the Node runtime plan, Phase 3, T2 slice P2. It ports from #16741 (head a68b6f3, merge-base 277c289) and builds on #24210.

Visual Proof

N/A. This is internal SSH connection bookkeeping with no UI or interaction change.

Testing

  • New tests: an unhandled channel error is reported, an owner-handled one is not, and SshConnection logs an unhandled exec channel error with the target and channel kind.

  • Ported tests: destination parsing and capture, disconnect drain, transport closure, work drain (system-SSH cases split into their own file), forward-channel and upload-session lifetimes, system-SSH command input. Also the feat(orcad): consolidate Bun runtime and SSH/Remote migration #16741 updates to the channel-open, direct-startup, file-transfer, host-key-store-wiring and reconnect-ladder tests, and to the multi-factor authentication and system-transport integration tests.

  • Regression: every ssh-connection* and ssh-relay-session* test, plus the rest of src/main/ssh, the SSH providers and the SSH IPC tests (300+ files) pass. That includes the plain-SSH session, shell and SFTP provider tests.

  • The work-drain tests use the ledger's own fence until T3 adds the public one.

  • macOS only, locally; CI covers Linux and Windows.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

AI Disclosure

Review

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • SSH and remote hosts: loss of contact is still reported as unverifiable, never as exit.
  • Wire: nothing new is sent to relays or clients.
  • Cross-platform: the change is in main-process SSH code and has no path or shortcut impact.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

m4air added 2 commits October 1, 2026 08:45
…dgers (#16741 T2 P2)

Every operation SshConnection admits (exec, shell, sftp, file transfers, upload
sessions, forwarded channels and sockets, system-SSH commands) now runs through
the connection's work ledger, and every ssh2 client and proxy process it
allocates is tracked until it physically closes. Ordinary connect, reconnect
and disconnect behavior is unchanged.

Adds:
- subscribeTransportClosure: one-shot notice once the connection is disposed,
  every allocated transport has emitted 'close' and tracked work has drained.
  System-SSH startup is never proven closed from here.
- disconnectAndDrain(signal): for owned single-lifetime transports; fences new
  work, disconnects, and waits for physical close of the client, proxy, every
  allocated client and all fenced work. Refuses (after cleaning up) when the
  transport cannot be proven, e.g. system SSH or a connect still in flight.
- getExecutionDestination: the ssh2 endpoint, accepted host-key fingerprint and
  proxy-route digest proven by the current handshake (ssh-connection-destination).
- getTransportGeneration, prepareForwardRoute, openForwardSocket, forwardOut,
  forwardStreamLocal for later forwarding callers.
- An automaticReconnect constructor option (default on).

Channel close is local lifetime evidence only, never a remote-exit verdict.

Porting note (source: #16741 head a68b6f3, merge-base 277c289):
- Taken: the ledger hunks of ssh-connection.ts, ssh-connection-destination,
  ssh-forward-channel-lifetime, ssh-upload-session-lifetime, the system-SSH
  facade EOF hunk, and their tests.
- Adapted: operation bodies became private *Untracked methods called through
  the ledger instead of being re-indented; closure gating and the close drain
  moved to ssh-connection-transport-closure / ssh-connection-close-drain; the
  destination parser uses type guards instead of a cast. disconnectAndDrain
  fences through the ledger directly. Main's plain-SSH shell() goes through the
  ledger too. execCommand takes Pick<SshConnection, 'exec' |
  'usesSystemSshTransport'>; its string-stdin/maxOutputBytes hunk is not taken
  because main already streams stdin. Work-drain tests fence the private ledger
  until T3 adds the public fence; system-SSH drain cases split into their own
  file.
- Left for later slices: fenceWorkForReset (T3), isEphemeralRuntimeSshOwner
  (T6), assertProfileLifetimeAdmission (P8b), and the four manager drain cases
  in ssh-connection-disconnect-drain.test.ts (P3).
…channel errors

ssh-connection.ts no longer grows under its max-lines exemption: it is 1872
lines, below main's 1917. The public API is unchanged.

- ssh-channel-open.ts: the channel-open waiter and session-limit retry.
- ssh-connection-file-transfers.ts: the uploadDirectory, downloadFile, upload
  session, writeFile and writeBuffer bodies, reading the connection through a
  small getter-based host so each read still sees the live transport.
- ssh-forward-channel-lifetime.ts: the forward client and stream-local checks.

The lifetime tracker's 'error' listener no longer hides errors. When it is a
channel's only error listener, the error is reported: SshConnection logs
"[ssh] Unhandled <kind> channel error for <target>: <message>", and other
callers fall back to a generic [ssh] warning. Nothing throws, so an orphaned
channel error still cannot crash the process.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — two defer-able observations to confirm before this connects up with later slices.

Reviewed changes

  • Work-ledger wiring in SshConnection — exec, shell, sftp, uploadDirectory, downloadFile, writeFile, writeBuffer, upload sessions and system-ssh commands now run through workLedger, with each original body kept as a private *Untracked method.
  • Transport-close ledger — every ssh2 Client and proxy process is tracked until its close, including failed startups.
  • New, not-yet-called surface — subscribeTransportClosure, disconnectAndDrain, getExecutionDestination, getTransportGeneration, prepareForwardRoute/openForwardSocket/forwardOut/forwardStreamLocal, plus an automaticReconnect constructor option (default true) and a resetConnectionFenced latch.
  • System-ssh EOF — the exec-channel Duplex now forwards final to proc.stdin.end, so EOF reaches the remote reader.
  • Destination capture — the accepted host-key fingerprint, endpoint and proxy-route digest from a successful ssh2 handshake are published as a frozen snapshot.
  • Tests — new drain/closure/destination/upload-session/forward-lifetime suites; updates to channel-open, direct-startup, file-transfer, host-key-store, reconnect-ladder, MFA and system-transport tests.

All 3,175 tests in src/main/ssh pass locally.

ℹ️ Post-disconnect operations now reject with AbortError

The ledger's admission hook was wired as if (this.disposed) throw createSshOperationAbortError(), so once disconnect() runs, every new operation — exec, sftp, uploadDirectory, writeFile, upload sessions, forwarding — rejects with AbortError instead of the previous 'Not connected' / 'Connection disposed'. That is a wider contract change than the PR's "behavior is the same for users" framing, and only the upload-session path has a test pinning it. Worth confirming that callers which distinguish "transport lost → reconnect" from "user cancelled" do not now read a disposed connection as a cancellation.

Technical details
# Disposed-connection operations change error type

## Affected sites
- `src/main/ssh/ssh-connection.ts` — `workLedger` constructor passes `assertAdmission` that throws `createSshOperationAbortError()` when `this.disposed`; `admit()` calls it for `run`, `beginSession` and `beginChannelOpen`.
- `src/main/ssh/ssh-connection.ts` (`exec`/`sftp`/`uploadDirectory`/`downloadFile`/`writeFile`/`writeBuffer`/`openFileUploadSession`/`forwardOut`/`forwardStreamLocal`) — previously reached `execUntracked` etc. and threw `'Not connected'` after disconnect.
- `src/main/ssh/ssh-connection-file-transfer.test.ts:185` — only coverage of the new behavior (underlying transfer not called; rejects `AbortError`).

## Required outcome
- Confirm that treating a disposed connection as "operation aborted" is intended for all callers, or keep the prior `'Not connected'` classification for non-abort cases.

## Suggested approach (optional)
- If the change is intended, add a direct test for `exec`/`sftp` after `disconnect()` so the contract is pinned, and note the taxonomy change in the PR description.

## Open questions for the human
- Do any relay/reconnect paths key on `'Not connected'` or `'Connection disposed'` to decide recovery?

ℹ️ unprovenStartupTransport is sticky even when the connection ends up on ssh2

unprovenStartupTransport is set in doSystemSshProbe, spawnAndWaitForSystemSsh and connectTrackedViaSystemSsh and never cleared. The GSSAPI path can run the system-SSH probe, catch its failure, reset useSystemSshTransport = false, and then connect successfully over ssh2 — leaving the flag permanently true for that instance. From then on disconnectAndDrain always refuses with ssh_connection_close_transport_unproven and subscribeTransportClosure never fires, even though the live transport is provable ssh2. Nothing consumes these yet, but the first later slice that does will silently refuse to drain such a host.

Technical details
# Sticky unproven flag after a failed system-SSH probe

## Affected sites
- `src/main/ssh/ssh-connection.ts:1274` (`doSystemSshProbe`), `:1435` (`spawnAndWaitForSystemSsh`), `:2078` (`connectTrackedViaSystemSsh`) — set `unprovenStartupTransport = true`; no assignment back to `false` exists.
- `src/main/ssh/ssh-connection.ts:1029-1048` — GSSAPI attempt probes system SSH, and on a non-disposed failure resets `useSystemSshTransport` and falls through to the ssh2 path.
- `src/main/ssh/ssh-connection.ts:2135-2147` (`disconnectAndDrain`) and `:229-238` (`transportClosure` predicate) read the flag.

## Required outcome
- Decide whether a cleaned-up, failed system-SSH probe should permanently disqualify the connection from proven closure, or whether the flag can be scoped to a currently-live system transport.

## Open questions for the human
- Is "any system-ssh child ever spawned" the intended granularity, or only "a system-ssh transport is live at drain time"?

ℹ️ Nitpicks

  • getTransportGeneration()'s doc says it is "for callers that compare transport incarnations rather than connect attempts", but it returns connectGeneration, which also advances on failed attempts and on disconnect(). A caller using it as a transport-incarnation key would see a new value with no new transport.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Two CI gates are red on 15329c7: pnpm run typecheck and pnpm run check:code-quality:changed. Both look mechanical, but the PR cannot merge green until they are fixed.

Reviewed changes (delta since the prior review at a3e2cee)

  • Channel-open extraction — openSshSessionChannelWithRetry and waitForSshChannelOpen moved out of ssh-connection.ts into ssh-channel-open.ts; the logic is a faithful move, now taking the ledger work item and an onUnhandledError reporter.
  • File-transfer extraction — the five transfer bodies moved into ssh-connection-file-transfers.ts behind a SshFileTransferHost getter interface; the SshConnection methods are thin workLedger.run wrappers. ssh-connection.ts is now 1,872 lines.
  • Forward guards — assertSshForwardClient / assertSshStreamLocalForwardAllowed extracted into ssh-forward-channel-lifetime.ts.
  • Unhandled channel errors — trackSshConnectionChannelLifetime now reports via an onUnhandledError callback when the tracker's listener is the channel's only error listener; SshConnection supplies a kind-labelled reporter ([ssh] Unhandled exec channel error for <target>: …). New ssh-connection-channel-error.test.ts covers the handled and unhandled cases.
  • Doc fix — getTransportGeneration no longer claims to be a distinct transport-incarnation key (addresses an earlier nitpick).

The refactor is behavior-preserving and I found no new logic bug in it. The full src/main/ssh suite (3,180 tests) passes.

⚠️ pnpm run typecheck fails with a consistent-type-assertions-adjacent type error

pnpm tc:node fails on a new test file; pnpm run typecheck runs the same config/tsconfig.node.json project in CI (.github/workflows/pr.yml:387), so this blocks the typecheck job.

Technical details
# TS2739 in ssh-upload-session-lifetime.test.ts

## Affected sites
- `src/main/ssh/ssh-upload-session-lifetime.test.ts:79` — `channel.bind({})` passes `{}` where `bind(resource: SshConnectionWorkChannel)` requires `on`/`once`/`removeListener`.
- `src/main/ssh/ssh-upload-session-lifetime.ts` — `beginChannelOpen().bind` parameter type comes from `SshConnectionWorkChannel` in `ssh-connection-work-ledger.ts`.

## Required outcome
- Pass a value satisfying `SshConnectionWorkChannel` (e.g. `new EventEmitter()` with `import { EventEmitter } from 'node:events'`) so `pnpm tc:node` passes.

## Open questions for the human
- None; behavior is unchanged (the resource is only stored, never invoked).

⚠️ pnpm run check:code-quality:changed fails with 17 new type-assertion findings

CI runs this gate at .github/workflows/pr.yml:225; it reports 22 typescript(consistent-type-assertions) findings (17 unique lines) across six test files added or changed by this PR. Per AGENTS.md, unavoidable casts need a line-specific // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: … rationale, or the cast should be removed.

Technical details
# Consistent type assertions in new SSH test files

## Affected sites
- `src/main/ssh/ssh-connection-channel-error.test.ts:52`
- `src/main/ssh/ssh-connection-disconnect-drain.test.ts:47,193,205`
- `src/main/ssh/ssh-connection-reconnect-ladder.test.ts:271`
- `src/main/ssh/ssh-connection-transport-closure.test.ts:69,87,154`
- `src/main/ssh/ssh-connection-work-drain.test.ts:79,88,116,129,163,175`
- `src/main/ssh/ssh-forward-channel-lifetime.test.ts:10,21,70`

## Required outcome
- The changed-code quality gate passes.

## Suggested approach (optional)
- Add the SAFETY-commented `oxlint-disable-next-line` where the cast is genuinely unavoidable, or replace the cast with a typed stub (`new EventEmitter()`, a small `ClientChannel` fixture) where it is not.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/main/ssh/ssh-upload-session-lifetime.test.ts Outdated
@greptile-apps

greptile-apps Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 3/5

[High risk] Refactors SSH connection lifecycle and resource cleanup.

The PR does not appear safe to merge while forwarding remains outside the ledger and confirmed late channel closure can still leave drains failed.

Findings

  1. P1 Production forwards bypass tracking ▶
  2. P1 Late closure cannot clear timeout ▶
  3. P1 Forwarded work bypasses tracking ▶
  4. P1 Confirmed closure still fails drain ▶

Summary

The PR connects SSH operations and transports to closure ledgers, adds drain and destination APIs, and forwards end-of-input to system-SSH command processes. Since the previous review, changes chiefly tighten a forwarding parameter type and adjust test fixtures and lint annotations.

Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  C[SshConnection] --> W[Work ledger]
  C --> T[Transport close ledger]
  W --> D[Disconnect and drain]
  T --> D
  D --> P[Physical closure confirmed]
Loading

Reviews (3) · Last reviewed commit: "fix(ssh): name the forwarded local socke..."

Comment thread src/main/ssh/ssh-connection.ts Outdated
Comment on lines +296 to +299
forwardOut(client: SshClient, localSocket: object, ...args: Parameters<SshClient['forwardOut']>) {
assertSshForwardClient(this.client, client)
const report = this.reportUnhandledChannelError('forward')
forwardTrackedSshChannel(this.workLedger, client, localSocket, report, ...args)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Production forwards bypass tracking Existing SSH port forwards and browser tunnels call client.forwardOut directly instead of using this tracked wrapper. If the SSH client closes while one of those forwards still has an open socket or a pending channel callback, the work ledger cannot see it. subscribeTransportClosure or disconnectAndDrain can then report completion before that forwarding work is finished.

Knowledge Base Used: Remote connectivity and execution

Comment on lines +67 to +73
const markOpenUnconfirmed = (error: Error): Error => {
work.markUnverifiable(error)
if (!trackRemoteCommandTermination) {
return error
}
unconfirmedOpenError = Object.assign(error, { sshChannelCloseConfirmed: false })
return unconfirmedOpenError

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Late closure cannot clear timeout When a channel open times out or exceeds its abort grace, this records a lasting work-ledger failure. If the callback later returns a channel and that channel physically closes, settling it does not clear the failure. A later drain still rejects even though the channel's closure has been observed.

Knowledge Base Used: Remote connectivity and execution

@greptile-apps

This comment has been minimized.

Comment thread src/main/ssh/ssh-connection.ts Outdated
openForwardSocket<T extends NodeJS.EventEmitter>(open: () => T): T {
return openTrackedSshSocket(this.workLedger, open, this.reportUnhandledChannelError('socket'))
}
forwardOut(client: SshClient, localSocket: object, ...args: Parameters<SshClient['forwardOut']>) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Forwarded work bypasses tracking Existing SSH port forwards and browser tunnels call client.forwardOut directly instead of this tracked method. If a forward is opening or active during disconnectAndDrain, its socket and channel are absent from the work ledger. The drain can therefore report completion without accounting for that forward.

Knowledge Base Used: Remote connectivity and execution

}
}
const rejectAfterClose = (value: T): void => {
const abortError = markOpenUnconfirmed(createSshOperationAbortError())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Confirmed closure still fails drain If an in-flight channel open is aborted during disconnectAndDrain, this call marks it unverifiable before waiting for close. The fenced ledger retains that failure permanently, so the drain rejects even when the channel closes within the grace period and its closure is confirmed.

@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

📝 Walkthrough

Walkthrough

The changes add tracked lifetimes for SSH channel opens, forwarded channels, upload sessions, and file-transfer operations. They add work fencing and transport-close draining, plus validated execution-destination snapshots for successful SSH2 connections. System SSH command streams now end child stdin when input ends. Tests cover channel errors, reconnects, cancellation, transport closure, and work draining.

Priority: ⬇️ Low

Merge Risk: 🔵 Low · up to 3e5bb

A forwarded-socket close race can leave the new drain API waiting on a stale ledger entry. Fix the tracking before relying on drain completion.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 3e5bb

The change affects SSH shutdown guarantees and authenticated destination identity. Identity checks restrict the new destination information to the current successful connection. No newly introduced security vulnerability was verified, but complete shutdown coverage is not established for every forwarding path.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The demonstrated change is scoped to resources and execution identity owned by an individual SSH connection. Inspected entrypoints bind forwarding to that connection's current client; the evidence does not establish an expansion to another connection or broader execution authority.

Trust Boundaries and Controls

  • observed — The new destination snapshot crosses the authentication-to-execution-identity boundary only after a successful current handshake and a non-rejected host-key decision. Its getter additionally requires an undisposed, connected ssh2 connection with matching client and generation. New connection attempts and disconnect invalidate the stored snapshot.

Resilience and Maintainability Implications

  • observed — Shutdown evidence covers resources admitted through the tracking APIs, not arbitrary use of the exposed raw client. The existing port-forward provider calls client.forwardOut directly and maintains its own socket cleanup. Consequently, connection drain alone must not be treated as certification that every forwarding resource or listener has closed.

Hardening Proposals

  • proposed — Before using drain as a migration or security teardown gate, define its ownership scope explicitly and coordinate raw-client forwarding owners. For generic event-emitter resources, establish already-closed state or retain closure history before allowing repeated tracking, rather than assuming a previously emitted close event will recur.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.13% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 32 functions across 30 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change: wiring SshConnection through the work and transport close ledgers.
Description check ✅ Passed The description is comprehensive. It includes the user impact, implementation details, rationale, linked issue context, testing results, visual proof, cross-platform notes, and checklist status. The e…
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: ded1e127-679b-4c73-a52c-0d4a67576cc5

📥 Commits

Reviewing files that changed from the base of the PR and between c991893 and 15329c7.

📒 Files selected for processing (30)
  • src/main/ssh/ssh-channel-open.ts
  • src/main/ssh/ssh-connection-channel-error.test.ts
  • src/main/ssh/ssh-connection-channel-lifetime.test.ts
  • src/main/ssh/ssh-connection-channel-lifetime.ts
  • src/main/ssh/ssh-connection-channel-open.test.ts
  • src/main/ssh/ssh-connection-close-drain.ts
  • src/main/ssh/ssh-connection-destination-capture.test.ts
  • src/main/ssh/ssh-connection-destination.test.ts
  • src/main/ssh/ssh-connection-destination.ts
  • src/main/ssh/ssh-connection-direct-startup.test.ts
  • src/main/ssh/ssh-connection-disconnect-drain.test.ts
  • src/main/ssh/ssh-connection-file-transfer.test.ts
  • src/main/ssh/ssh-connection-file-transfers.ts
  • src/main/ssh/ssh-connection-host-key-store-wiring.test.ts
  • src/main/ssh/ssh-connection-reconnect-ladder.test.ts
  • src/main/ssh/ssh-connection-system-work-drain.test.ts
  • src/main/ssh/ssh-connection-test-fixtures.ts
  • src/main/ssh/ssh-connection-transport-closure.test.ts
  • src/main/ssh/ssh-connection-transport-closure.ts
  • src/main/ssh/ssh-connection-work-drain.test.ts
  • src/main/ssh/ssh-connection.ts
  • src/main/ssh/ssh-forward-channel-lifetime.test.ts
  • src/main/ssh/ssh-forward-channel-lifetime.ts
  • src/main/ssh/ssh-multi-factor-authentication.test.ts
  • src/main/ssh/ssh-relay-exec-command.ts
  • src/main/ssh/ssh-relay-exec-input.test.ts
  • src/main/ssh/ssh-system-transport.integration.test.ts
  • src/main/ssh/ssh-upload-session-lifetime.test.ts
  • src/main/ssh/ssh-upload-session-lifetime.ts
  • src/main/ssh/system-ssh-command.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review.

export function forwardTrackedSshChannel(
ledger: SshConnectionWorkLedger,
client: Client,
localSocket: object,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Type the forwarded localSocket as NodeJS.EventEmitter instead of object. In both signatures, object fails the anti-slop(no-object-parameters) static-analysis check. That makes CI fail. The type also hides the real contract: the lifetime tracker needs on, once, and removeListener. If a caller passes a value without them, the tracker marks the value unverifiable and never settles its ledger entry.

  • src/main/ssh/ssh-forward-channel-lifetime.ts#L55-L55: change the forwardTrackedSshChannel parameter localSocket: object to localSocket: NodeJS.EventEmitter.
  • src/main/ssh/ssh-connection.ts#L296-L296: change the SshConnection.forwardOut parameter localSocket: object to localSocket: NodeJS.EventEmitter.
🧰 Tools
🪛 GitHub Check: static analysis

[failure] 55-55: anti-slop(no-object-parameters)
src/main/ssh/ssh-forward-channel-lifetime.ts:55:16: Parameter localSocket uses the broad object type. Accept a named owner type; parse external input at its boundary before calling this function.

📍 Affects 2 files
  • src/main/ssh/ssh-forward-channel-lifetime.ts#L55-L55 (this comment)
  • src/main/ssh/ssh-connection.ts#L296-L296

Source: Linters/SAST tools

…ssion test stub

The forwarded local socket now takes SshConnectionWorkChannel, the event
surface the ledger tracks, instead of a broad object. The upload-session
lifetime test binds an EventEmitter rather than an untyped {}.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes (delta since the prior Pullfrog review at 15329c7)

  • Typecheck fix — ssh-upload-session-lifetime.test.ts now binds a real EventEmitter (channel.bind(new EventEmitter()), plus the node:events import) instead of {}.
  • Cast cleanup — every typescript/consistent-type-assertions finding from the prior review now carries a line-specific // oxlint-disable-next-line … -- SAFETY: rationale.
  • Forward socket typing — forwardOut and forwardTrackedSshChannel now take localSocket: SshConnectionWorkChannel rather than object; forwardOut also declares its void return. Production-only type tightening with no behavioral change.
  • Test stub typing — the reconnect-ladder DNS failure is built via Object.assign(new Error(…), { code: 'EAI_AGAIN' }) instead of a NodeJS.ErrnoException cast.

I re-ran both gates the prior review left red: pnpm tc:node exits 0 and pnpm run check:code-quality:changed reports 0 new findings across all 30 changed files. The changes are mechanical and introduce no new logic. The two earlier deferrable observations (the post-disconnect AbortError taxonomy and the sticky unprovenStartupTransport flag) remain open design questions with no production consumer yet; they are intentionally not re-raised here.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · Preserve closure state across repeated socket tracking. · ssh-forward-channel-lifetime.ts:55-78

src/main/ssh/ssh-forward-channel-lifetime.ts:55-78
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Preserve closure state across repeated socket tracking.

openForwardSocket tracks the socket once, then forwardOut creates a second ledger entry for the same socket. If the socket emits close between these calls, the second tracker subscribes too late. The close event is not replayed, so the new entry can remain pending and make disconnectAndDrain wait for a close that cannot occur. Check the socket state and retain closure state across tracking calls.

Suggested fix
 type TrackableChannel = SshConnectionWorkChannel & {
   closed?: unknown
+  destroyed?: unknown
 }
 
+const closedChannels = new WeakSet<object>()
+
 ...
   }
   const channel = value
   work.bind(channel)
-  if (channel.closed === true) {
+  if (
+    channel.closed === true ||
+    channel.destroyed === true ||
+    closedChannels.has(channel)
+  ) {
     work.close()
     return
   }
 ...
   channel.once('close', () => {
+    closedChannels.add(channel)
     channel.removeListener('error', onError)
     work.close()
   })

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 67610fd9-7139-4a08-990e-2d5a3e5af3c5

📥 Commits

Reviewing files that changed from the base of the PR and between 15329c7 and 3e5bb01.

📒 Files selected for processing (9)
  • src/main/ssh/ssh-connection-channel-error.test.ts
  • src/main/ssh/ssh-connection-disconnect-drain.test.ts
  • src/main/ssh/ssh-connection-reconnect-ladder.test.ts
  • src/main/ssh/ssh-connection-transport-closure.test.ts
  • src/main/ssh/ssh-connection-work-drain.test.ts
  • src/main/ssh/ssh-connection.ts
  • src/main/ssh/ssh-forward-channel-lifetime.test.ts
  • src/main/ssh/ssh-forward-channel-lifetime.ts
  • src/main/ssh/ssh-upload-session-lifetime.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/main/ssh/ssh-connection-channel-error.test.ts
  • src/main/ssh/ssh-forward-channel-lifetime.test.ts
  • src/main/ssh/ssh-connection-transport-closure.test.ts
  • src/main/ssh/ssh-connection-disconnect-drain.test.ts
  • src/main/ssh/ssh-connection-work-drain.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.

@OrcaWin
OrcaWin merged commit 4e8edc8 into main Oct 1, 2026
59 checks passed
Chihen-Tai added a commit to Chihen-Tai/orca that referenced this pull request Oct 1, 2026
GitHub reported a conflict after main moved SshConnection's file transfers
into ssh-connection-file-transfers.ts (stablyai#24401).

Conflict, in ssh-connection.ts: took main's delegation through
fileTransferHost() and the work ledger, keeping this branch's download
observer type on downloadFile. This branch's transfer behavior moved into
main's new module:
- downloadSshFile: SFTP downloads go through downloadFileViaSftp, and
  system-SSH downloads use a signal linked to the caller's cancel, both
  reporting bytes.
- createSshFileUploadSession: uploads link the caller's cancel signal and
  pass onRemoteCreated / onBytesTransferred; sessions expose
  removeCreatedEntry (SFTP, and system SSH except on Windows hosts).

sftp-upload.ts auto-merged past its 300-line limit, since main split
uploadDirectory while this branch added upload observers. The two observer
listeners moved unchanged into sftp-upload-observers.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chihen-Tai added a commit to Chihen-Tai/orca that referenced this pull request Oct 1, 2026
Main's openTrackedSshUploadSession (stablyai#24401) returned only uploadFile and
close, so the optional removeCreatedEntry that SFTP and POSIX system-SSH
sessions provide was dropped. With it gone, a cancelled SSH import's rollback
kept every folder it created and deleted files by path after the identity
check, instead of the remote's guarded non-recursive removal. Type checking
missed it because the method is optional.

The wrapper now forwards removeCreatedEntry when the session has one, inside
the session lifetime so the connection's reset fence waits for it. When the
session has none (Windows hosts), it adds no stub, which keeps the
rollback's Windows fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OrcaWin added a commit that referenced this pull request Oct 2, 2026
* Revert "feat(orcad): source-side dormant export of a relay-hosted SSH target (#16741 T6-8) (#24519)"

This reverts commit 783101b.

* Revert "feat(ssh): update, roll back, recover and stop a managed orcad server (#16741 T6-5 follow-up) (#24463)"

This reverts commit 38c2d1d.

* Revert "feat(ssh): deploy and pair an empty managed orcad server over SSH (#16741 T6-5) (#24453)"

This reverts commit 8b76683.

* Revert "fix(ssh): orcad GC honors the activation journal; readiness requires proven daemon coverage (#16741 T6 follow-up) (#24451)"

This reverts commit d3f8c50.

* Revert "feat(ssh): remote orcad stop by request file and journaled decommission (#16741 T6-4) (#24449)"

This reverts commit 43d9b43.

* Revert "feat(orcad): supervisable server: stop requests, managed stop receipts and a lifetime that keeps its lock on failed teardown (#16741 T6-3) (#24433)"

This reverts commit b093d3a.

* Revert "feat(ssh): crash-safe orcad activation, rollback and recovery (#16741 T6-2) (#24423)"

This reverts commit 1a9ac0e.

* Revert "feat(runtime): SSH access links for paired servers in a downgrade-safe sidecar (#16741 T5-1+T5-2) (#24420)"

This reverts commit 99db2bf.

* Revert "feat(relay): capability-gated owner reset with a durable preparation journal (#16741 T3 R1) (#24418)"

This reverts commit 34a582b.

* Revert "feat(ssh): track connection-manager drains, test probes and provider continuations (#16741 T2 P3+P8a) (#24407)"

This reverts commit d53063d.

* Revert "feat(daemon): idle retirement, session census and recovery-only provider (#16741 T2 P4b) (#24409)"

This reverts commit ff212db.

* Revert "feat(ssh): add pty.resumeClient and split SSH PTY process listing (#16741 T2 P5+P6) (#24414)"

This reverts commit 92cb717.

* Revert "feat(relay): await owned watcher and agent children on shutdown (#16741 T2 P1) (#24400)"

This reverts commit 6b36e4f.

* Revert "feat(session): retry failed renderer session writes and verify local folder PTYs (#16741 T2 P9) (#24406)"

This reverts commit d23ecef.

* Revert "feat(ssh): remote orcad primitives on the pinned Node runtime (#16741 T6-1) (#24419)"

This reverts commit dd87ae5.

* Revert "fix(runtime): fence runtime-environment subscriptions and status probes by identity (#16741 T5-3) (#24421)"

This reverts commit ece9e4d.

* Revert "feat(orcad): migration manifest and dormant-state contracts (#16741 T6-7) (#24422)"

This reverts commit 3fbdaba.

* Revert "feat(ssh): wire SshConnection through the work and transport close ledgers (#16741 T2 P2) (#24401)"

This reverts commit 4e8edc8.

* Revert "feat(profiles): carry markdown frontmatter visibility in project transfers (#16741 T2 P7) (#24405)"

This reverts commit 60c9326.

* Revert "fix(runtime): project the PTY incarnation onto mobile session tabs (#24413)"

This reverts commit 99e0303.

* Revert "feat(daemon): tag daemon stream data with the PTY incarnation id (#16741 T2 P4a) (#24402)"

This reverts commit 817af76.

* Revert "feat(ssh): port the SSH connection work ledger and transport close ledger (#16741 T2) (#24210)"

This reverts commit c991893.

* Revert "feat(relay): fence and drain file and git response streams on shutdown (#24185)"

This reverts commit dc08ffe.

* Revert "refactor(runtime-rpc): extract the Node WebSocket lifecycle; opt-in pinned port (#24186)"

This reverts commit a789233.

* Revert "feat(relay): route relay handlers through work admission; producer publication drain (#24181)"

This reverts commit 0b812bd.

* Revert "feat(relay): land the #16741 T1 seam (work drain, publication drain, release gate) (#24156)"

This reverts commit 3aa2d3a.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Chihen-Tai added a commit to Chihen-Tai/orca that referenced this pull request Oct 2, 2026
Main reverted the 26 Phase 3 PRs (stablyai#24559), including stablyai#24401, which had
moved SshConnection's file transfers into ssh-connection-file-transfers.ts
behind a work ledger. This branch had moved its transfer behavior into that
module, and added the forwarding of removeCreatedEntry through its tracked
upload session.

Resolution:
- ssh-connection-file-transfers.ts, ssh-upload-session-lifetime.ts and its
  test: take main's deletion.
- ssh-connection.ts: git auto-merged downloadFile and openFileUploadSession
  to main's plain bodies with no conflict marker, silently dropping this
  branch's download progress and cancel, upload cancel signal and
  callbacks, and removeCreatedEntry. Both methods are restored to this
  branch's in-class version from before stablyai#24401 (identical to 03a44db),
  with their imports; main's other changes in the file are kept.

The 26 tests that fail in native-chat, cross-version e2e and the agent
session import also fail on main itself at 66799f7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant