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 }