diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md
index 1f7a09df016..8a0f6a6e6cb 100644
--- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md
+++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md
@@ -9,21 +9,16 @@ keeps commands, hashes, runner identities, timings, and failure details.
A checked box means the work has evidence in the detailed ledger. Design approval alone does not
complete a box.
-Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — explicit remote-command
-dialect correction locally green, exact-head native rerun next, 2026-07-17, Codex implementation
-owner.** Exact head
-`34b51eb12640795d1cdd99a17c6fbadda79c2f60`, run `29558649829`, and Windows x64/ARM64 jobs
-`87816177268`/`87816177284` prove pinned launcher control in 734.8928/681.1723 ms and advance the
-injected connection beyond its probe. Both then fail the first Windows staging command because the
-generic system-SSH file channel applies the POSIX `exec /bin/sh -c` wrapper to an already complete
-PowerShell command; no payload byte or transfer metric runs. All four primary non-Windows jobs and
-both Linux oldest-userland supplements pass. Add only an explicit `posix`/`powershell` command
-dialect at the file-channel boundary, preserving the exact POSIX call and using `wrapCommand: false`
-only for PowerShell. `E-M6-WINDOWS-COMMAND-DIALECT-CORRECTION-LOCAL-001` records 40 focused cases,
-285 release contracts, typecheck, full lint/reliability/max-lines, formatting, exact call-site audit,
-diff, and resolver isolation green. Commit/push this exact package and require native full-size,
-cancellation, concurrency, reuse, and exact teardown proof. No product/default caller, Beta,
-fallback, tuple, publication, or signing behavior is authorized.
+Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — explicit Windows
+completion framing locally green, exact-head native rerun next, 2026-07-17, Codex implementation
+owner.** Exact head `ad3c3975c099f0e197661091b1cec9b0a0d6d553`, run `29559577952`, and Windows
+x64/ARM64 jobs `87818965702`/`87818965757` independently time out after 1,200,018/1,200,015 ms with
+one fixture-user command chain retained. The isolated correction replaces final stdin EOF reads with
+an exact `ORCAEND1` frame while preserving local size and remote frame validation. Local evidence is
+green through 54 focused cases, 697 broad relay cases, 285 release contracts, typecheck, full
+lint/reliability/max-lines, formatting, diff, and resolver isolation. Commit/push only this package
+and require fresh x64/ARM64 full-size, cancellation, concurrency, reuse, and exact teardown proof.
+No product/default caller, Beta, fallback, tuple, publication, or signing behavior is authorized.
`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture
correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned
Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain
diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md
index 1d5486fd8ad..31b087f415f 100644
--- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md
+++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md
@@ -8,8 +8,8 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps
Date created: 2026-07-14
Last updated: 2026-07-17
-Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — explicit remote-command dialect correction exact-head native qualification, 2026-07-17, Codex implementation owner**. Exact head `34b51eb12640795d1cdd99a17c6fbadda79c2f60`, run `29558649829`, and x64/ARM64 jobs `87816177268`/`87816177284` select a required `posix`/`powershell` file-channel dialect. `E-M6-WINDOWS-COMMAND-DIALECT-CORRECTION-LOCAL-001` records the correction locally green: POSIX retains `connection.exec(command)` exactly and PowerShell alone uses `{ wrapCommand: false }`. Commit/push only this package and require fresh native x64/ARM64 proof. Legacy remains the sole production path; real Apple/SignPath rehearsals remain deferred.
-Session checkpoint: **COMMAND DIALECT CORRECTION LOCAL GREEN / NATIVE RERUN NEXT — 2026-07-17, Codex implementation owner** — 40 focused cases with two declared live skips, 285 release contracts with eight declared skips, typecheck, lint/reliability/max-lines, formatting, exact call-site audit, diff, and protected-resolver isolation pass. Full-size transfer, cancellation, concurrency, reuse, and exact profile teardown remain unproven. No product caller, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
+Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — explicit Windows completion framing locally green, exact-head native qualification next, 2026-07-17, Codex implementation owner**. Exact correction head `ad3c3975c099f0e197661091b1cec9b0a0d6d553`, run `29559577952`, and Windows x64/ARM64 jobs `87818965702`/`87818965757` independently time out after 1,200,018/1,200,015 ms with a fixture-user command chain still waiting. `E-M6-WINDOWS-FRAMED-COMPLETION-LOCAL-001` replaces the receiver's EOF dependency with an exact `ORCAEND1` frame while retaining local byte-count and remote frame validation; focused, broad, release, typecheck, lint, formatting, diff, and resolver-isolation gates pass. Commit/push only this package and require fresh native full-size/cancellation/concurrency/reuse/teardown proof. Legacy remains the sole production path; real Apple/SignPath rehearsals remain deferred.
+Session checkpoint: **WINDOWS FRAMED COMPLETION LOCAL GREEN / NATIVE RERUN NEXT — 2026-07-17, Codex implementation owner** — 54 focused cases with three declared skips, 697 broad relay cases with ten declared skips, 285 release contracts with eight declared skips, typecheck, lint/reliability/max-lines, formatting, diff, and protected-resolver isolation pass. Both PowerShell receivers are now length-delimited and completion-framed, with no stdin-EOF read; local short/long payload mismatches abort boundedly. Native proof remains required. No product caller, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693)
@@ -20346,6 +20346,79 @@ lint` passes in 18.59 seconds real with the 26 existing warnings, all 41 reliabi
require exact-head x64/ARM64 full-size serial/concurrent/collision/cancellation metrics,
connection reuse, and exact fixture profile/directory teardown before advancing.
+### E-M6-WINDOWS-COMMAND-DIALECT-CI-RED-001 — both architectures retain an EOF-dependent receiver
+
+- Date/owner/source/run: 2026-07-17, Codex implementation owner; exact head
+ `ad3c3975c099f0e197661091b1cec9b0a0d6d553`, artifact run
+ [29559577952](https://github.com/stablyai/orca/actions/runs/29559577952), Windows x64 job
+ [87818965702](https://github.com/stablyai/orca/actions/runs/29559577952/job/87818965702) on
+ `windows-2022`, and native ARM64 job
+ [87818965757](https://github.com/stablyai/orca/actions/runs/29559577952/job/87818965757) on
+ `windows-11-arm`; both use Node 24.18.0 and official OpenSSH 10.0p2. X64 uses PowerShell
+ 5.1.20348.4294 and ARM64 uses 5.1.26100.8655.
+- Preserved gates: each Windows job passes 101 artifact files, 864 runnable cases, and 19 declared
+ skips before the live step, including launcher compilation/lifecycle, exact clean-build identity,
+ runtime smoke, and source/cache boundaries. X64 measures 42 files / 96,527,161 bytes; ARM64
+ measures 42 / 85,213,511. Linux x64/ARM64 jobs `87818965677`/`87818965726`, Darwin x64/ARM64 jobs
+ `87818965712`/`87818965737`, and Linux oldest-userland supplements
+ `87819839680`/`87819839678` pass at the same exact head.
+- Pinned trust: the standalone strict-trust launcher succeeds with sentinel, ended stdout, natural
+ process exit/channel close 0, and zero stderr in 400.4242 ms on x64 and 719.5839 ms on ARM64.
+ This independently preserves the earlier launcher/trust correction before the full-size test.
+- Exact RED: the full-size test times out at its 1,200,000 ms ceiling after 1,200,018 ms on x64 and
+ 1,200,015 ms on ARM64. Neither emits serial/concurrent/collision/cancellation metrics. X64
+ teardown finds fixture-owned `sshd-session.exe`, `cmd.exe`, `conhost.exe`, and `powershell.exe`;
+ ARM64 independently finds `cmd.exe`, `conhost.exe`, and `powershell.exe`. Both then correctly
+ report the still-locked `UsrClass.dat` instead of hiding fixture residue.
+- Classification: command routing now reaches a complete PowerShell receiver, but both control and
+ file protocols still demand a final stdin EOF after already length-delimited bytes. The retained
+ command chains select that shared EOF-dependent completion boundary as the next isolated
+ hypothesis; they do not prove which transfer phase was active, so no full-size or teardown item is
+ credited. Replace only the final EOF check with an explicit fixed completion frame, retain exact
+ local byte-count and remote frame validation, and require fresh x64/ARM64 proof.
+
+### E-M6-WINDOWS-FRAMED-COMPLETION-LOCAL-001 — explicit completion framing is locally green
+
+- Date/owner/base/runner: 2026-07-17, Codex implementation owner; uncommitted correction atop exact
+ native RED head `ad3c3975c099f0e197661091b1cec9b0a0d6d553` on Apple arm64, macOS 26.2 build
+ 25C56, Node 26.5.0, and pnpm 10.24.0. The repository requests Node 24; native jobs remain
+ authoritative.
+- Implementation boundary: Windows staging-control requests and file streams now end with the exact
+ eight-byte ASCII frame `ORCAEND1`; their PowerShell 5.1 receivers read and verify that frame after
+ their already bounded length fields and never wait for stdin EOF. The file destination counts
+ accepted payload bytes locally, aborts boundedly on short/long input, and writes the completion
+ frame only after the declared size is exact. The remote receiver still uses `CreateNew`, exact
+ payload length, fixed 64 KiB buffering, and failure cleanup. POSIX/SFTP, SSH authentication,
+ connection reuse, launcher/trust, product/default, Beta, fallback, tuple, publication, and signing
+ behavior are unchanged.
+- Focused evidence: an initial focused run exposed two outdated test assumptions (closing without
+ the declared payload and asserting close synchronously); all product cases passed. After correcting
+ those framing oracles, `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts
+ --maxWorkers=1 src/main/ssh/ssh-relay-runtime-command-file-destination.test.ts
+ src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts
+ src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts
+ src/main/ssh/ssh-relay-runtime-windows-staging-control.test.ts
+ src/main/ssh/ssh-relay-runtime-windows-tree-transfer.test.ts
+ src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts` exits 0 with four
+ files passed, one live file skipped, 54 runnable cases passed, and three declared skips in 3.26
+ seconds real; maximum RSS is 146,571,264 bytes and peak footprint is 97,474,416 bytes.
+- Broad/release evidence: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts
+ --maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts` exits 0 with 53 files passed, six declared skipped,
+ 697 runnable cases passed, and ten declared skips in 23.32 seconds real; maximum RSS is
+ 301,826,048 bytes and peak footprint is 96,818,888 bytes. The exact release-contract command exits
+ 0 with 51 files, 285 runnable cases, and eight declared skips in 12.44 seconds real; maximum RSS is
+ 195,035,136 bytes and peak footprint is 97,294,144 bytes.
+- Static/isolation evidence: `/usr/bin/time -l pnpm typecheck` exits 0 in 2.86 seconds real with
+ 1,242,497,024-byte maximum RSS and 97,031,904-byte peak footprint. `/usr/bin/time -l pnpm lint`
+ exits 0 in 13.52 seconds real with the existing 26 warnings, all 41 reliability gates, and the
+ 355-entry max-lines ratchet; maximum RSS is 2,110,308,352 bytes and peak footprint is 97,441,720
+ bytes. Targeted `oxfmt --check`, `git diff --check`, exact no-`ReadByte()`/frame source audit, and
+ protected-resolver comparison pass.
+- Residual gate: local Darwin declares the PowerShell/live cases skipped. Commit/push this exact
+ package and require fresh native x64/ARM64 receiver syntax, full-size serial/concurrent/collision/
+ cancellation metrics, connection reuse, and exact fixture profile/directory teardown before
+ advancing.
+
## Accepted Gaps
No product gap is accepted merely because it appears in this list. Each entry requires explicit
@@ -20404,10 +20477,10 @@ The project is not complete until every applicable item below is checked with ev
## Next Required Action
-Commit and push only the explicit command-dialect correction recorded by
-`E-M6-WINDOWS-COMMAND-DIALECT-CORRECTION-LOCAL-001`, then require exact-head Windows x64/ARM64
-full-size transfer, cancellation, concurrency, connection reuse, and exact profile/directory
-teardown evidence. Do not check the broad Milestone 6 Windows system-SSH item or add an
+Commit and push only the explicit Windows completion-framing correction recorded by
+`E-M6-WINDOWS-FRAMED-COMPLETION-LOCAL-001`, then require exact-head Windows x64/ARM64 full-size
+transfer, cancellation, concurrency, connection reuse, and exact profile/directory teardown
+evidence. Do not check the broad Milestone 6 Windows system-SSH item or add an
Electron/startup/product importer, per-target mode wiring, fallback, tuple
enablement, release publication, or default behavior.
Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets
diff --git a/src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts b/src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts
index cb7b06d2f65..e5ecd0f97e6 100644
--- a/src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts
+++ b/src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts
@@ -38,6 +38,8 @@ function decodeHeader(frame: Buffer) {
}
}
+const completionFrame = Buffer.from('ORCAEND1', 'ascii')
+
function decodePowerShellCommand(command: string): string {
const encoded = command.match(/-EncodedCommand\s+([A-Za-z0-9+/=]+)$/u)?.[1]
if (!encoded) {
@@ -89,7 +91,8 @@ describe('SSH relay runtime Windows system-SSH file destination', () => {
expect(script).toContain('[IO.FileMode]::CreateNew')
expect(script).toContain('[IO.FileShare]::None')
expect(script).toContain('New-Object byte[] 65536')
- expect(script).toContain('$inputStream.ReadByte() -ne -1')
+ expect(script).toContain("-ne 'ORCAEND1'")
+ expect(script).not.toContain('$inputStream.ReadByte()')
for (const forbidden of ['ReadToEnd', 'CopyTo(', 'FromBase64String', 'ConvertFrom-Json']) {
expect(script).not.toContain(forbidden)
}
@@ -108,7 +111,10 @@ describe('SSH relay runtime Windows system-SSH file destination', () => {
headerCallback?.()
const destination = await opening
+ await destination.write(Buffer.alloc(65_537, 7))
const closing = destination.close()
+ await vi.waitFor(() => expect(channel.write).toHaveBeenCalledTimes(3))
+ expect(channel.write.mock.calls[2]?.[0]).toEqual(completionFrame)
resolve()
await closing
})
@@ -161,14 +167,33 @@ describe('SSH relay runtime Windows system-SSH file destination', () => {
expect(channel.forceClose).not.toHaveBeenCalled()
})
- it('propagates receiver failure only after EOF and remote settlement', async () => {
+ it('propagates receiver failure only after framed completion and remote settlement', async () => {
const { channel, reject } = createChannel()
const destination = await openDestination(channel, { expectedSize: 0 })
const closing = destination.close()
- expect(channel.end).toHaveBeenCalledOnce()
- reject(new Error('receiver rejected extra byte'))
- await expect(closing).rejects.toThrow('receiver rejected extra byte')
+ await vi.waitFor(() => expect(channel.end).toHaveBeenCalledOnce())
+ expect(channel.write.mock.calls[1]?.[0]).toEqual(completionFrame)
+ reject(new Error('receiver rejected completion frame'))
+ await expect(closing).rejects.toThrow('receiver rejected completion frame')
+ })
+
+ it.each([
+ ['short', 2, Buffer.from([1])],
+ ['long', 1, Buffer.from([1, 2])]
+ ])('rejects a %s payload locally and aborts its channel', async (_kind, expectedSize, bytes) => {
+ const { channel, resolve } = createChannel()
+ channel.requestClose.mockImplementation(() => resolve())
+ const destination = await openDestination(channel, { expectedSize })
+
+ if (bytes.length > expectedSize) {
+ await expect(destination.write(bytes)).rejects.toThrow(/payload size mismatch/i)
+ } else {
+ await destination.write(bytes)
+ await expect(destination.close()).rejects.toThrow(/payload size mismatch/i)
+ }
+ expect(channel.requestClose).toHaveBeenCalledOnce()
+ expect(channel.forceClose).not.toHaveBeenCalled()
})
it.skipIf(process.platform !== 'win32')(
@@ -211,8 +236,12 @@ describe('SSH relay runtime Windows system-SSH file destination', () => {
signal: new AbortController().signal,
openChannel: openPowerShellChannel
})
- await destination.write(bytes)
- await expect(destination.close()).rejects.toThrow(/PowerShell receiver failed/i)
+ if (bytes.length > expectedSize) {
+ await expect(destination.write(bytes)).rejects.toThrow(/payload size mismatch/i)
+ } else {
+ await destination.write(bytes)
+ await expect(destination.close()).rejects.toThrow(/payload size mismatch/i)
+ }
await expect(readFile(remotePath)).rejects.toMatchObject({ code: 'ENOENT' })
}
} finally {
diff --git a/src/main/ssh/ssh-relay-runtime-windows-file-destination.ts b/src/main/ssh/ssh-relay-runtime-windows-file-destination.ts
index 91397e742b6..b71017dacfe 100644
--- a/src/main/ssh/ssh-relay-runtime-windows-file-destination.ts
+++ b/src/main/ssh/ssh-relay-runtime-windows-file-destination.ts
@@ -14,12 +14,14 @@ export type OpenSshRelayRuntimeWindowsFileDestinationOptions = Readonly<{
}>
const HEADER_MAGIC = Buffer.from('ORCARLY1', 'ascii')
+const COMPLETION_MAGIC = Buffer.from('ORCAEND1', 'ascii')
const FIXED_HEADER_BYTES = 20
const MAXIMUM_PATH_BYTES = 32 * 1024
const PAYLOAD_BUFFER_BYTES = 64 * 1024
export const SSH_RELAY_RUNTIME_WINDOWS_FILE_DESTINATION_LIMITS = Object.freeze({
maximumHeaderBytes: FIXED_HEADER_BYTES + MAXIMUM_PATH_BYTES,
+ completionBytes: COMPLETION_MAGIC.length,
maximumPathBytes: MAXIMUM_PATH_BYTES,
payloadBufferBytes: PAYLOAD_BUFFER_BYTES
})
@@ -61,7 +63,9 @@ try {
$outputStream.Write($buffer, 0, $read)
$remaining -= $read
}
- if ($inputStream.ReadByte() -ne -1) { throw 'SSH relay runtime Windows receiver extra byte' }
+ [byte[]]$completion = New-Object byte[] ${COMPLETION_MAGIC.length}
+ Read-Exact $inputStream $completion ${COMPLETION_MAGIC.length}
+ if ([Text.Encoding]::ASCII.GetString($completion) -ne 'ORCAEND1') { throw 'bad completion' }
$outputStream.Flush()
$outputStream.Dispose()
$outputStream = $null
@@ -142,7 +146,6 @@ export async function openSshRelayRuntimeWindowsFileDestination(
try {
// Why: the receiver must authenticate framing before any source buffer can be borrowed.
await destination.write(header)
- return destination
} catch (error) {
try {
await destination.abort(error)
@@ -154,4 +157,51 @@ export async function openSshRelayRuntimeWindowsFileDestination(
}
throw error
}
+
+ let payloadBytes = 0
+ const abortForSizeMismatch = async (): Promise => {
+ const error = new Error('SSH relay runtime Windows file destination payload size mismatch')
+ try {
+ await destination.abort(error)
+ } catch (cleanupError) {
+ throw new AggregateError(
+ [error, cleanupError],
+ 'SSH relay runtime Windows file size cleanup failed'
+ )
+ }
+ throw error
+ }
+ return Object.freeze({
+ write: async (chunk: Buffer): Promise => {
+ if (!Buffer.isBuffer(chunk) || chunk.length === 0) {
+ return destination.write(chunk)
+ }
+ if (payloadBytes + chunk.length > options.expectedSize) {
+ return abortForSizeMismatch()
+ }
+ await destination.write(chunk)
+ payloadBytes += chunk.length
+ },
+ close: async (): Promise => {
+ if (payloadBytes !== options.expectedSize) {
+ return abortForSizeMismatch()
+ }
+ try {
+ // Why: Win32-OpenSSH may retain stdin EOF; an explicit frame ends verified bytes instead.
+ await destination.write(COMPLETION_MAGIC)
+ await destination.close()
+ } catch (error) {
+ try {
+ await destination.abort(error)
+ } catch (cleanupError) {
+ throw new AggregateError(
+ [error, cleanupError],
+ 'SSH relay runtime Windows file completion cleanup failed'
+ )
+ }
+ throw error
+ }
+ },
+ abort: (reason: unknown): Promise => destination.abort(reason)
+ })
}
diff --git a/src/main/ssh/ssh-relay-runtime-windows-staging-control.test.ts b/src/main/ssh/ssh-relay-runtime-windows-staging-control.test.ts
index 1338f22b712..522fd044b49 100644
--- a/src/main/ssh/ssh-relay-runtime-windows-staging-control.test.ts
+++ b/src/main/ssh/ssh-relay-runtime-windows-staging-control.test.ts
@@ -39,7 +39,8 @@ function decodeRequest(frame: Buffer) {
operation: frame.readUInt8(8),
reserved: frame.subarray(9, 12),
remoteRoot: frame.subarray(rootStart, pathStart).toString('utf8'),
- remotePath: frame.subarray(pathStart, pathStart + pathLength).toString('utf8')
+ remotePath: frame.subarray(pathStart, pathStart + pathLength).toString('utf8'),
+ completion: frame.subarray(pathStart + pathLength).toString('ascii')
}
}
@@ -96,7 +97,8 @@ describe('SSH relay runtime Windows system-SSH staging control', () => {
expect(script).toContain('New-Item -ItemType Directory -Path')
expect(script).toContain('[IO.Path]::GetDirectoryName')
expect(script).toContain('[IO.Directory]::Delete')
- expect(script).toContain('$inputStream.ReadByte() -ne -1')
+ expect(script).toContain("-ne 'ORCAEND1'")
+ expect(script).not.toContain('$inputStream.ReadByte()')
for (const forbidden of ['ReadToEnd', 'FromBase64String', 'ConvertFrom-Json']) {
expect(script).not.toContain(forbidden)
}
@@ -111,7 +113,8 @@ describe('SSH relay runtime Windows system-SSH staging control', () => {
operation: opcode,
reserved: Buffer.alloc(3),
remoteRoot: 'C:/Users/测试/.orca-remote/stage',
- remotePath
+ remotePath,
+ completion: 'ORCAEND1'
})
fixture.resolve()
await expect(running).resolves.toBeUndefined()
diff --git a/src/main/ssh/ssh-relay-runtime-windows-staging-control.ts b/src/main/ssh/ssh-relay-runtime-windows-staging-control.ts
index 0de0db3c57c..5bd15059526 100644
--- a/src/main/ssh/ssh-relay-runtime-windows-staging-control.ts
+++ b/src/main/ssh/ssh-relay-runtime-windows-staging-control.ts
@@ -19,6 +19,7 @@ export type RunSshRelayRuntimeWindowsStagingControlOptions = Readonly<{
}>
const REQUEST_MAGIC = Buffer.from('ORCACTL1', 'ascii')
+const COMPLETION_MAGIC = Buffer.from('ORCAEND1', 'ascii')
const FIXED_REQUEST_BYTES = 20
const MAXIMUM_PATH_BYTES = 32 * 1024
const COMMAND_TIMEOUT_MS = 30_000
@@ -32,7 +33,8 @@ const OPERATION_CODES: Record =
export const SSH_RELAY_RUNTIME_WINDOWS_STAGING_CONTROL_LIMITS = Object.freeze({
commandTimeoutMs: COMMAND_TIMEOUT_MS,
maximumPathBytes: MAXIMUM_PATH_BYTES,
- maximumRequestBytes: FIXED_REQUEST_BYTES + MAXIMUM_PATH_BYTES * 2
+ maximumRequestBytes: FIXED_REQUEST_BYTES + MAXIMUM_PATH_BYTES * 2 + COMPLETION_MAGIC.length,
+ completionBytes: COMPLETION_MAGIC.length
})
const RECEIVER_SCRIPT = `
@@ -62,7 +64,9 @@ try {
[byte[]]$pathBytes = New-Object byte[] ([int]$pathLength)
Read-Exact $inputStream $rootBytes ([int]$rootLength)
Read-Exact $inputStream $pathBytes ([int]$pathLength)
- if ($inputStream.ReadByte() -ne -1) { throw 'extra byte' }
+ [byte[]]$completion = New-Object byte[] ${COMPLETION_MAGIC.length}
+ Read-Exact $inputStream $completion ${COMPLETION_MAGIC.length}
+ if ([Text.Encoding]::ASCII.GetString($completion) -ne 'ORCAEND1') { throw 'bad completion' }
$utf8 = New-Object Text.UTF8Encoding($false, $true)
$root = $utf8.GetString($rootBytes)
$path = if ($pathLength -eq 0) { '' } else { $utf8.GetString($pathBytes) }
@@ -159,13 +163,16 @@ function buildRequest(
): Buffer {
const rootBytes = Buffer.from(remoteRoot, 'utf8')
const pathBytes = Buffer.from(remotePath, 'utf8')
- const request = Buffer.alloc(FIXED_REQUEST_BYTES + rootBytes.length + pathBytes.length)
+ const request = Buffer.alloc(
+ FIXED_REQUEST_BYTES + rootBytes.length + pathBytes.length + COMPLETION_MAGIC.length
+ )
REQUEST_MAGIC.copy(request, 0)
request.writeUInt8(OPERATION_CODES[operation], 8)
request.writeUInt32LE(rootBytes.length, 12)
request.writeUInt32LE(pathBytes.length, 16)
rootBytes.copy(request, FIXED_REQUEST_BYTES)
pathBytes.copy(request, FIXED_REQUEST_BYTES + rootBytes.length)
+ COMPLETION_MAGIC.copy(request, FIXED_REQUEST_BYTES + rootBytes.length + pathBytes.length)
return request
}