From 2bdf281433f4092cb008172ebe344db486e77dd3 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 17 Sep 2026 23:59:42 -0700 Subject: [PATCH] fix: avoid retaining foreign SSH file frames before metadata (#21167) * fix: avoid retaining foreign SSH file frames before metadata * test(ssh): exercise empty metadata through the streaming mux fixture * fix(ssh): fail the file read when beforeResolve never runs Moving the metadata install from .then() to beforeResolve moved it from a mandatory callback to an optional one, and handleResponse clears the request timer before beforeResolve runs. That left "response fulfilled, metadata never installed" with no deadline: the read never settled, holding its notification and dispose closures until mux disposal. Before this PR the same state failed after the 60s inactivity deadline. Unreachable with the concrete mux, which calls resolve on the line after beforeResolve, but the hook is optional in the type and nothing enforces the pairing. The guard is a no-op on every real path: empty, missing streamId, cap-exceeded and alloc-failure all settle first, and the success path sets metadataReady. Found during review of #21167; raised at https://github.com/stablyai/orca/pull/21167#issuecomment-5726058832 --------- Co-authored-by: m4air Co-authored-by: Claude --- .../ssh-file-metadata-retention/README.md | 79 ++++++ .../before.config.mjs | 20 ++ .../ssh-file-metadata-retention/fix.patch | 138 +++++++++ .../main-before-electron-results.json | 140 +++++++++ .../main-before-node-results.json | 140 +++++++++ .../main-context.patch | 18 ++ .../main-fixed-electron-results.json | 140 +++++++++ .../main-fixed-node-results.json | 140 +++++++++ .../relay-fixture.mjs | 226 +++++++++++++++ .../scenario.test.mjs | 267 ++++++++++++++++++ .../source-versions.json | 225 +++++++++++++++ .../ssh-file-metadata-retention/sources.cjs | 84 ++++++ .../validation.json | 264 +++++++++++++++++ .../vitest.config.mjs | 31 ++ .../worktree-before-electron-results.json | 140 +++++++++ .../worktree-before-node-results.json | 140 +++++++++ .../worktree-fixed-electron-results.json | 140 +++++++++ .../worktree-fixed-node-results.json | 140 +++++++++ .../ssh-filesystem-provider-stream.test.ts | 63 +++-- .../providers/ssh-filesystem-provider.test.ts | 8 - src/main/ssh/ssh-filesystem-stream-reader.ts | 125 ++++---- .../ssh-filesystem-stream-retention.test.ts | 221 +++++++++++++++ 22 files changed, 2793 insertions(+), 96 deletions(-) create mode 100644 docs/audits/ssh-file-metadata-retention/README.md create mode 100644 docs/audits/ssh-file-metadata-retention/before.config.mjs create mode 100644 docs/audits/ssh-file-metadata-retention/fix.patch create mode 100644 docs/audits/ssh-file-metadata-retention/main-before-electron-results.json create mode 100644 docs/audits/ssh-file-metadata-retention/main-before-node-results.json create mode 100644 docs/audits/ssh-file-metadata-retention/main-context.patch create mode 100644 docs/audits/ssh-file-metadata-retention/main-fixed-electron-results.json create mode 100644 docs/audits/ssh-file-metadata-retention/main-fixed-node-results.json create mode 100644 docs/audits/ssh-file-metadata-retention/relay-fixture.mjs create mode 100644 docs/audits/ssh-file-metadata-retention/scenario.test.mjs create mode 100644 docs/audits/ssh-file-metadata-retention/source-versions.json create mode 100644 docs/audits/ssh-file-metadata-retention/sources.cjs create mode 100644 docs/audits/ssh-file-metadata-retention/validation.json create mode 100644 docs/audits/ssh-file-metadata-retention/vitest.config.mjs create mode 100644 docs/audits/ssh-file-metadata-retention/worktree-before-electron-results.json create mode 100644 docs/audits/ssh-file-metadata-retention/worktree-before-node-results.json create mode 100644 docs/audits/ssh-file-metadata-retention/worktree-fixed-electron-results.json create mode 100644 docs/audits/ssh-file-metadata-retention/worktree-fixed-node-results.json create mode 100644 src/main/ssh/ssh-filesystem-stream-retention.test.ts diff --git a/docs/audits/ssh-file-metadata-retention/README.md b/docs/audits/ssh-file-metadata-retention/README.md new file mode 100644 index 00000000000..592e069e66a --- /dev/null +++ b/docs/audits/ssh-file-metadata-retention/README.md @@ -0,0 +1,79 @@ +# SSH file readers retain unrelated streams before metadata + +The file reader queued every file-stream notification while awaiting its own metadata. A delayed read therefore retained payloads from other reads that had already completed. The fix installs metadata through the mux's existing synchronous `beforeResolve` callback and ignores notifications until the read has a stream identity. Listeners still register before the request, and own frames adjacent to the response are processed correctly. + +This is conditional transient retention during a pending metadata request. The proof establishes a source mechanism and its correction; it does not identify an affected host, measure a natural native I/O stall, or attribute #19831 to SSH. + +## Actual producer and ownership chain + +1. Desktop `filesystem-read-handlers.ts` calls the selected `SshFilesystemProvider.readFile` for `fs:readFile`; this route does not serialize reads. Runtime previews also use the provider with caller-specific caps. An AI-vault scan has an eight-operation gate, which still permits repeated completions in other slots while one operation waits. +2. `readFileViaStream` subscribes to chunk/end/error notifications before sending `fs.readFileStream`. Previously it appended all such notifications until the metadata promise's `.then` callback ran, even when they belonged to other streams. +3. Relay `FilesystemHandler` forwards the path and request context to `readRelayFileStreamMetadata`. The producer awaits `stat` before acquiring a stream slot. For unknown MIME types, its prefix probe also precedes registration. After opening/registering the file, it schedules its pump with `setImmediate` and returns metadata. +4. `RelayDispatcher` publishes the small metadata response in its control lane; the writer prioritizes control before bulk. The saturated-writer control verifies metadata precedes chunks after drain. +5. The mux runs `beforeResolve` synchronously during response dispatch, before resolving the request promise. Its decoder can dispatch adjacent notifications before any `.then` callback runs. The fix installs the stream ID and buffer at that synchronous boundary, eliminating the need to save foreign frames. + +The producer, mux, dispatcher, decoder, writer, file I/O, and stream registry are actual source in the portable proof. The fixture connects both ends through an in-memory duplex transport, uses real temporary files, and supplies the filesystem handler's small path/client/pacing adapter. It does not launch an SSH process, Electron window, native PTY, or network server. + +## Bounds and payload sharing + +- The relay allows **16 concurrent registered streams**, with a **four-chunk ACK window** per paced stream. Chunks are 256 KiB. A metadata operation waiting before registration occupies no stream slot. Other transfers can complete and reuse slots repeatedly. +- Reader size caps are **10 MiB text / 50 MiB binary**, optionally tightened by the caller. They apply after that reader's metadata and do not charge foreign history accumulated before it. +- The metadata request has a **30,000 ms deadline**. After metadata, the reader uses a **60,000 ms inactivity deadline**, reset by its own chunks and integrated with suspend/resume. Connection disposal also releases subscriptions. These timers and transport throughput bound ordinary retention duration; suspension/event-loop stalls can delay timers. No indefinite native stall was established. +- The decoder limits each turn to 64 frames / 4 ms and bounds retained framing bytes. Those limits do not bound arrays owned by subscribers after frames are parsed. +- The mux passes the **same parsed params object** to all subscribers. Four waiting readers add four wrappers per frame, but share its payload. The result is not four copied payloads or quadratic payload-byte growth. + +## Comparative results + +All **80 portable cases pass**: ten controls × baseline/fixed × audited-worktree/named-main graph × Node/Electron. Node is 26.6; Electron 43.7 uses Node 24.21. Reports record exact versions, all 59 selected source hashes, the observed reader hash, and proof artifact hashes. + +| Observation | Before | Fixed | +| ----------------------------------------------------------------------- | --------------------------------: | -------: | +| Four waiting readers; 16 completed 2 MiB transfers | 576 wrappers | 0 | +| Unique shared params objects retained | 144 | 0 | +| Logical base64 bytes, counted once per unique params object | 44,739,584 | 0 | +| Peak registered streams in that workload | 1 | 1 | +| ACKs processed | 128 | 128 | +| Reader history after metadata completion, handled disposal, or deadline | released | released | +| Actual pump with ACK delivery withheld | stops after 4 chunks | same | +| Sixteen active streams, then a seventeenth request | refused; later admission succeeds | same | +| Saturated writer, then drain | metadata before own chunks | same | +| Response plus own chunk/end in one decoder turn | correct result | same | +| Unpaced producer / ordinary completion | correct result | same | +| Canonical LF vs synthetic CRLF source/patch reads | 66 reads agree | same | + +The primary portable workload deliberately gates four request handlers **immediately before invoking the actual relay file producer**. The request remains pending while other real transfers complete. This controlled adapter delay is distinct from a native `stat` already in progress; production source establishes that awaiting `stat` occurs at the same pre-registration phase. It does not measure how often or how long native metadata I/O delays occur on a user's machine. + +The baseline observation adds only `WeakRef(pending)` to expose the closed-over array. It does not add a strong owner. Shared params identity is checked across all waiting readers. Heap deltas support the object/byte accounting but are neither exact object sizes nor RSS. After disposal, the test consumes lazy `Error.stack` and retains only error codes: externally retained unmaterialized V8 error stacks can themselves retain callback context, so the release claim is after normal error handling. + +Every successful transfer checks payload length and SHA-256. Stream capacity, pacing, cancellation, and output assertions run identically for both variants. The controlled producer ignores ACK pacing in one case; this tests existing unpaced behavior, not every historical relay binary. + +## Source graphs and publication + +`source-versions.json` records the full 59-module import graph and five additional actual caller hashes. Both graphs select the same file-reader source variant. The audited worktree and named main `291b4ddd6f1c1af480169885e0fda7f9c78ff053` otherwise differ only in the previously published SSH writer consumed-prefix correction. + +`main-context.patch` reconstructs that single context difference in memory. The loader accepts either of its two exact recorded checkout hashes and reconstructs the selected graph. This lets the same artifact run on this worktree or the independent main publication without depending on another memory PR. `fix.patch` is the separate, single-product-file change under review. Every other graph/caller source is hash-fenced; unknown production imports fail. Dedicated portable tests omit unrelated global Vitest setup files. + +The current reader baseline and relay file producer are also byte-identical to `v1.4.198` (`e0826956fcfc532f5a1e55b5e081f2e57e553c43`). That named version has synchronous `beforeResolve` and control-first writer scheduling. Its comparison is limited to the recorded paths; the proof does not execute a whole historical application. + +No wire field, opcode, host execution verdict, stream cap, timeout, fallback, or native process lifetime changes. Existing MethodNotFound fallback and malformed-metadata / tighter-cap / empty-image / adjacent-error handling are covered through the actual mux by the permanent regression suite. + +## Reproduce + +Choose either graph (`worktree` or `main`) and variant (`before` or `fixed`): + +```sh +ORCA_BACKGROUND_LAUNCH=1 ORCA_SSH_READER_GRAPH=main ORCA_SSH_READER_VARIANT=fixed pnpm exec vitest run --config docs/audits/ssh-file-metadata-retention/vitest.config.mjs +``` + +For Electron, invoke the installed Electron binary with `ELECTRON_RUN_AS_NODE=1` and `ORCA_BACKGROUND_LAUNCH=1`, passing `node_modules/vitest/vitest.mjs` and the same arguments. Reports are separate for every graph/variant/runtime. Set `ORCA_SSH_READER_OUTPUT` to an alternative file path to preserve captured reports. + +Permanent tests and the intentional baseline failure: + +```sh +ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config config/vitest.config.ts src/main/ssh/ssh-filesystem-stream-retention.test.ts src/main/providers/ssh-filesystem-provider-stream.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/main/ssh/ssh-channel-multiplexer.test.ts src/relay/fs-handler-stream.test.ts +ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config docs/audits/ssh-file-metadata-retention/before.config.mjs src/main/ssh/ssh-filesystem-stream-retention.test.ts src/main/providers/ssh-filesystem-provider-stream.test.ts +``` + +The baseline keeps all 64 observed foreign frame objects while metadata remains pending, causing exactly the new lifetime assertion to fail; the other 22 tests pass. The initial four-suite run passed 70 tests. Detailed quality/typecheck results are in `validation.json`. Full-file casting diagnostics are the same 15 inherited assertions in the original reader and provider test, verified by exact diagnostic/source-span comparison; the changed-code gate reports no new findings. No lint rule was suppressed and no unrelated wire validation behavior was changed to satisfy that baseline cleanup. + +The expanded five-suite run passes **124 tests**, including the general provider suite. The empty-file control uses the existing streaming fixture, which invokes the mux's `beforeResolve` callback before resolving metadata, and verifies all stream listeners are released. The older generic fixture omitted that callback and reproduced the CI timeout; actual-mux empty metadata controls already passed. This correction changes test setup only. diff --git a/docs/audits/ssh-file-metadata-retention/before.config.mjs b/docs/audits/ssh-file-metadata-retention/before.config.mjs new file mode 100644 index 00000000000..0f4b29b2e14 --- /dev/null +++ b/docs/audits/ssh-file-metadata-retention/before.config.mjs @@ -0,0 +1,20 @@ +import { resolve } from 'node:path' +import { createRequire } from 'node:module' +import base from '../../../config/vitest.config.ts' +const { loadSources, versions } = createRequire(import.meta.url)('./sources.cjs') +const loaded = loadSources({ variant: 'before' }) +const target = resolve(loaded.root, versions.sourcePath) +export default { + ...base, + plugins: [ + { + name: 'ssh-file-metadata-baseline', + enforce: 'pre', + transform(_source, id) { + return resolve(id.split('?')[0]) === target + ? { code: loaded.sources.get(target), map: null } + : null + } + } + ] +} diff --git a/docs/audits/ssh-file-metadata-retention/fix.patch b/docs/audits/ssh-file-metadata-retention/fix.patch new file mode 100644 index 00000000000..7ffe13335fe --- /dev/null +++ b/docs/audits/ssh-file-metadata-retention/fix.patch @@ -0,0 +1,138 @@ +--- a/src/main/ssh/ssh-filesystem-stream-reader.ts ++++ b/src/main/ssh/ssh-filesystem-stream-reader.ts +@@ -77,7 +77 @@ +- // Why: chunk/end/error frames may arrive in the same dispatch tick as the +- // metadata response. Queue them until streamIdRef is set, then drain. +- type PendingFrame = +- | { kind: 'chunk'; params: Record } +- | { kind: 'end'; params: Record } +- | { kind: 'error'; params: Record } +- const pending: PendingFrame[] = [] ++ // Install metadata during response dispatch, before adjacent stream frames. +@@ -232,13 +225,0 @@ +- const drainPending = (): void => { +- while (!settled && pending.length > 0) { +- const frame = pending.shift()! +- if (frame.kind === 'chunk') { +- handleChunk(frame.params) +- } else if (frame.kind === 'end') { +- handleEnd(frame.params) +- } else { +- handleStreamError(frame.params) +- } +- } +- } +- +@@ -248 +228,0 @@ +- pending.push({ kind: 'chunk', params }) +@@ -257 +236,0 @@ +- pending.push({ kind: 'end', params }) +@@ -266 +244,0 @@ +- pending.push({ kind: 'error', params }) +@@ -287,51 +265,55 @@ +- .request('fs.readFileStream', { filePath, flowControl: 'ack' }) +- .then((rawMetadata) => { +- if (settled) { +- return +- } +- const metadata = rawMetadata as StreamMetadataResponse +- isBinary = metadata.isBinary +- isImage = metadata.isImage +- mimeType = metadata.mimeType +- resultEncoding = metadata.resultEncoding ?? RESULT_ENCODING_BASE64 +- +- if (metadata.empty) { +- succeed({ +- content: '', +- isBinary: metadata.isBinary, +- ...(metadata.isImage !== undefined ? { isImage: metadata.isImage } : {}), +- ...(metadata.mimeType !== undefined ? { mimeType: metadata.mimeType } : {}) +- }) +- return +- } +- +- if (typeof metadata.streamId !== 'number') { +- fail(new StreamProtocolError('Metadata missing streamId for non-empty stream')) +- return +- } +- +- const cap = sshFileStreamReadCap(metadata.isBinary, limits) +- if (metadata.totalSize < 0 || metadata.totalSize > cap) { +- streamIdRef.current = metadata.streamId +- fail( +- new FileReadCapExceededError( +- `Reported totalSize ${metadata.totalSize} exceeds client cap ${cap}` +- ) +- ) +- return +- } +- +- totalSize = metadata.totalSize +- totalChunks = totalSize === 0 ? 0 : Math.ceil(totalSize / STREAM_CHUNK_SIZE) +- try { +- buffer = Buffer.alloc(totalSize) +- } catch (err) { +- streamIdRef.current = metadata.streamId +- fail(new Error(`Failed to allocate ${totalSize} bytes: ${(err as Error).message}`)) +- return +- } +- streamIdRef.current = metadata.streamId +- metadataReady = true +- inactivity.reset() +- drainPending() +- }) ++ .request( ++ 'fs.readFileStream', ++ { filePath, flowControl: 'ack' }, ++ { ++ beforeResolve: (rawMetadata) => { ++ if (settled) { ++ return ++ } ++ const metadata = rawMetadata as StreamMetadataResponse ++ isBinary = metadata.isBinary ++ isImage = metadata.isImage ++ mimeType = metadata.mimeType ++ resultEncoding = metadata.resultEncoding ?? RESULT_ENCODING_BASE64 ++ ++ if (metadata.empty) { ++ succeed({ ++ content: '', ++ isBinary: metadata.isBinary, ++ ...(metadata.isImage !== undefined ? { isImage: metadata.isImage } : {}), ++ ...(metadata.mimeType !== undefined ? { mimeType: metadata.mimeType } : {}) ++ }) ++ return ++ } ++ ++ if (typeof metadata.streamId !== 'number') { ++ fail(new StreamProtocolError('Metadata missing streamId for non-empty stream')) ++ return ++ } ++ ++ const cap = sshFileStreamReadCap(metadata.isBinary, limits) ++ if (metadata.totalSize < 0 || metadata.totalSize > cap) { ++ streamIdRef.current = metadata.streamId ++ fail( ++ new FileReadCapExceededError( ++ `Reported totalSize ${metadata.totalSize} exceeds client cap ${cap}` ++ ) ++ ) ++ return ++ } ++ ++ totalSize = metadata.totalSize ++ totalChunks = totalSize === 0 ? 0 : Math.ceil(totalSize / STREAM_CHUNK_SIZE) ++ try { ++ buffer = Buffer.alloc(totalSize) ++ } catch (err) { ++ streamIdRef.current = metadata.streamId ++ fail(new Error(`Failed to allocate ${totalSize} bytes: ${(err as Error).message}`)) ++ return ++ } ++ streamIdRef.current = metadata.streamId ++ metadataReady = true ++ inactivity.reset() ++ } ++ } ++ ) diff --git a/docs/audits/ssh-file-metadata-retention/main-before-electron-results.json b/docs/audits/ssh-file-metadata-retention/main-before-electron-results.json new file mode 100644 index 00000000000..3bdfbee7446 --- /dev/null +++ b/docs/audits/ssh-file-metadata-retention/main-before-electron-results.json @@ -0,0 +1,140 @@ +{ + "variant": "before", + "graph": "main", + "runtime": { + "node": "24.21.0", + "electron": "43.7.0" + }, + "sources": { + "src/main/ssh/ssh-filesystem-stream-reader.ts": "b8586de1412df98428939a0a4870c5f8ccab22f506a188b2eb1c7a38c317b1ef", + "src/main/ssh/ssh-channel-multiplexer.ts": "9ecd88963fec72901596bd25d2cb4c8666ef41d3c8a968242c6eef52095ee108", + "src/relay/fs-stream-registry.ts": "d723dd0b6419a7937225bdacbc572ef004c49d850cbc616e9168e48f5e63b8fd", + "src/relay/protocol.ts": "faebaded7e8c8b98f021c791b4519d78d58be3879937fe2ed2cef68b22404060", + "src/relay/fs-handler-file-read.ts": "2d70577839cb150ad0977981d54904b1a25bd8b5de7cfe19e018c08c36f41cd2", + "src/relay/dispatcher.ts": "6dca32ec33e410fa9226d6c0a4a548ed06fc5bbf1a65ccdbcae7cc43a9328d18", + "src/relay/relay-frame-decoder.ts": "7044cd142b21f847ee0dc4aee085fe18434b4bcb34ba1eb8bc1fb48a0ed7919e", + "src/main/ssh/ssh-file-stream-read-cap.ts": "83b714af0c87da4c855762ffb827f529302ebc426ab5cde59458206a64cb07b1", + "src/main/ssh/ssh-file-stream-inactivity-deadline.ts": "0554419b22356efa60065b15640d8bf0d8d166bf345e30b492b2305767ea603a", + "src/relay/fs-handler-utils.ts": "12c54b9647ddc9e4ae9924f65aadf0d00e9553050858beae232c02077a94d93a", + "src/main/ssh/relay-protocol.ts": "644aa6f2087b5867d41006bfdcec78ffba693157a2912feb8f82b120f5647b34", + "src/main/ssh/ssh-multiplexer-transport-writer.ts": "433a2cc400b5ed9743871ca03aac06d31a079ef23c42c2a2759f78967312d071", + "src/relay/dispatcher-notification-publication.ts": "d21c8575f5f4694a806b0e05cce4a1595f7116220f48d9068eba7c23aed7a546", + "src/shared/relay-frame-decoder-contract.ts": "f58279099fe4dbbdfde9e3e038a916ae077e71d8ac6aa871b59a64c6fbaeaad8", + "src/shared/relay-frame-decoder.ts": "88c0ecbc06b93efe6733094b8c9e4509ab1cbd139f8cb3257e9fb74a0ed39194", + "src/main/system-power-lifecycle.ts": "cdeb82463a530123be4d42374925a0b349446437c02bff2c287786864515de11", + "src/relay/dispatcher-pty-publication.ts": "2a91aeab16069e4d053c453947c5cd5d24c9e9ac4d36f19d23377689313cc851", + "src/relay/dispatcher-contract.ts": "7c397f9d51c0e3eca604e9075cddb6609a5e114284999009e8a86146e766c081", + "src/relay/fs-handler-list-files.ts": "cca88ec47512840837c84c63baf7ddfc7c216810cb2a5f63fa2babbeef3ff189", + "src/shared/text-search.ts": "c291574e05874440601a6a043b5d065ca172072f4146568d9c9818829abbc185", + "src/shared/image-file-extensions.ts": "8460080a80e2a09fd64466faee1b87df4d8e4803e33bc2f9dd8b3b031a54d536", + "src/shared/ripgrep-process-availability.ts": "2a8e21ae2c9af146f1f226bdfad1e45916cfb2671fb11aa4586d6a337b8cdec2", + "src/shared/search-subprocess-lines.ts": "b860de84a4081fb86515bb31910f0f1d4b0a6606370490c4f60aa9d2e9d5eee3", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts": "3e5fe7a1e3537505baf42869449193fc0448339d851254a49527ec2a1c7cbb50", + "src/shared/relay-frame-buffer.ts": "f89fcd33489894c34f79ba8fd5e5634487555a7c34fc8e7c054229ae1c20e0be", + "src/relay/dispatcher-producer-capacity.ts": "ae201a7e09acbcc013410b92868df1e964f95952f8ab3a462d177f488292965b", + "src/shared/text-search-paths.ts": "a977e1b1591b38d7f08652e3d0a64c95b9379e8339ab9d26d6325f7ec90fcda3", + "src/shared/text-search-glob-patterns.ts": "b34072078cf81813e37c37228b830c974dfadb5284ba608538deb56594d64a69", + "src/shared/string-utils.ts": "fdb48c18f2f7272ed25949eb74beca182c3336d32681191c4b0a4a077db02c22", + "src/shared/text-search-match-accumulator.ts": "1229c755abc94608e211c6e403d529905f577a991871eadfed5eb18821d6e88b", + "src/shared/json-text-structure-limit.ts": "29ded95f7e054d839dc82d118cb324d2144c70267f293d6b54f188159ada5eb9", + "src/shared/search-match-count.ts": "6fde43e886fe142c23aff2b80b3f1ebdf613a083051a11e19d7813363db88f62", + "src/shared/file-listing-cancellation.ts": "c9e1eed636fa2140071fbc2089ce02f738cce725de5069f04bf307fea611b63d", + "src/shared/quick-open-path-search.ts": "2556d6c34f50e573b2fe046f7c64f782362b3bda1241098772bd360975910a98", + "src/shared/quick-open-filter.ts": "9a09a02764d15622932083a3e550fc7215c066d30a2868f5d548b9105b780d81", + "src/relay/dispatcher-producer-transport.ts": "c48087ed5f330311fb438ae3df75ff03bf34df83156f3987b8905598e21780e5", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/shared/file-name-sort.ts": "5aafddf218c453276dcf1afdccf9cd7f82c50160f5df2dd6d1b2779ce0c0cc4a", + "src/shared/clipboard-text.ts": "50f3155063244d6d12ac1493c80029a75df256c031f986f76da64a94152d0c38", + "src/relay/dispatcher-rpc-routing.ts": "c11b3ff9096bd4877bba0ebc8a70a63092a00015fde3d8f81f3b8fb54087479b", + "src/relay/dispatcher-client-writer.ts": "9ed3ee7ad2758f2d10cd0a0ba06bf77dc3318317b8e24f0eaa6cfe235fd34be4", + "src/shared/utf8-byte-limits.ts": "01574b287b2d6ab0758112887e356c0d270b25db1b72651925ba8329eb4224e6", + "src/shared/event-loop-yield.ts": "d291b71f09eb88f24a849d0ae310f946deb7f93b0b6b490b6ea5a605ab7a53cf", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/relay/dispatcher-writer-sink.ts": "12931e899bd882347b72cd03f3690e8d0e94bf2aa1ce9a560112f52cfdc14405", + "src/relay/dispatcher-writer-drain-arm.ts": "c9f1582197d0bb9ea30793f0c792a176ada3f25fb79e03474a70d084920d0416", + "src/relay/dispatcher-writer-lane-scheduler.ts": "8b54b850f8aa88944160b796e3b357fbbc618c782590b4baf6444eda77bd5e5e", + "src/relay/dispatcher-writer-admission.ts": "12e05ba04de5687c0a0db1e44eddf342cb0c21b9653eef2aeff7b5ccd2039797", + "src/shared/timer-delay.ts": "7e529ed30d1b25521f5d72d7c2a6d05d16efe66faf2619b859d67b330cca767f", + "src/shared/skill-install-failure.ts": "571ffbaad47304bbdef294e6445d70d179db40495005fdd0ce385491666ec4eb", + "src/relay/dispatcher-frame-codec.ts": "2728b1e9e8465350c23cede5b64287db77a3d90488d8f8b1e5721bc69d7af7a9", + "src/shared/terminal-unavailable-cause.ts": "b6ce6d4b5b666ef72d8403883d2cc167f2998c524a17155638c261000c424e91", + "src/shared/runtime-capability-degradation.ts": "fc9a5d3814c390c1296ecf72d5ef317b8b8624ea2578e4ceb2b32ee642b13a3f", + "src/relay/dispatcher-capacity-signals.ts": "125a94f04f8b3102956231dc800281ed7ee9bb538b2b93f005825c2dd59e5c63", + "src/relay/dispatcher-client-lifecycle.ts": "6884223ddb2be888cfd437ca1490e14a4c95655c5c831e6812ce4a7876e9c18b", + "src/relay/dispatcher-client-state.ts": "8cdabcb8df6b16b7cc84d8a33f5171cca1807b6d9dea4b374e3c2bb4fc10bfdb", + "src/relay/legacy-relay-publication-ledger.ts": "153926b90d370669d91fd9a59d246f63ed0c01f2aec6e8b7a890a0d0ad7378ba", + "src/relay/client-request-aborts.ts": "a4f458ce767c5315aa01fa9cc8c0a3008dcb445481a401462f14ec3dbbd9e20c" + }, + "observedReaderSha256": "7a5c9c14faf63197765fc5a2900e1d3488f94aaab6757425b7ef87597479e963", + "controls": [ + { + "name": "held-metadata-foreign-history", + "readers": 4, + "entries": [144, 144, 144, 144], + "wrappers": 576, + "uniqueParams": 144, + "logicalBase64BytesByUniqueParams": 44739584, + "sharedAcrossReaders": true, + "decodedTransferBytes": 33554432, + "peakRegisteredStreams": 1, + "maxConcurrentStreams": 16, + "ackWindow": 4, + "ackCount": 128, + "observedHeapDelta": 45298640, + "released": true + }, + { + "name": "ordinary-completion", + "passed": true + }, + { + "name": "transport-disposal", + "passed": true + }, + { + "name": "metadata-request-deadline", + "milliseconds": 30000, + "relayContextAborted": true, + "released": true + }, + { + "name": "unpaced-relay", + "passed": true + }, + { + "name": "real-pump-credit-window", + "chunksBeforeAck": 4, + "totalChunks": 6 + }, + { + "name": "actual-stream-capacity", + "slots": 16, + "rejectedSeventeenth": true, + "admittedAfterCompletion": true + }, + { + "name": "saturated-writer-metadata-order", + "wireOrder": ["probe.prime", "response", "fs.streamChunk", "fs.streamChunk", "fs.streamEnd"] + }, + { + "name": "same-turn-response-and-own-frames", + "passed": true + }, + { + "name": "canonical-crlf-source-control", + "reads": 66, + "passed": true + } + ], + "artifactHashes": { + "sources.cjs": "790ef573e61fbb7f68741d1e1cfb0e4b4c79a2dde4292e7ec9dc8dcf1d94940f", + "relay-fixture.mjs": "9856e17d83b812fb6b6717cf8207ccc808165c517df8d5c76bc020e0e6f51079", + "scenario.test.mjs": "7a619c631403d0ab03610171fc6109a9b52cde2d23b9d022354c3d6bb1b154af", + "vitest.config.mjs": "4ef63c6eba6d8e443f18f787bceb0b1f40b90ee750d63a3b27239ed05bf5d849", + "before.config.mjs": "a0a8ec50388d194d60cdf7019bba18fffb2c4691404924c3675757d3b3984962", + "fix.patch": "9681d47063d4b76bc4f9567eacf5dddb3ec221b715ce0bedddfcb493e0558fc6", + "main-context.patch": "4ef0bf173e6575a548b1d217248432fedbcfd68bcb350980e73987df3aba81e9", + "source-versions.json": "7df5afb4523280364b1eecaf465131ac94822fbb868dc4aaddb42586dff9f21d" + } +} diff --git a/docs/audits/ssh-file-metadata-retention/main-before-node-results.json b/docs/audits/ssh-file-metadata-retention/main-before-node-results.json new file mode 100644 index 00000000000..cdf0128daef --- /dev/null +++ b/docs/audits/ssh-file-metadata-retention/main-before-node-results.json @@ -0,0 +1,140 @@ +{ + "variant": "before", + "graph": "main", + "runtime": { + "node": "26.6.0", + "electron": null + }, + "sources": { + "src/main/ssh/ssh-filesystem-stream-reader.ts": "b8586de1412df98428939a0a4870c5f8ccab22f506a188b2eb1c7a38c317b1ef", + "src/main/ssh/ssh-channel-multiplexer.ts": "9ecd88963fec72901596bd25d2cb4c8666ef41d3c8a968242c6eef52095ee108", + "src/relay/fs-stream-registry.ts": "d723dd0b6419a7937225bdacbc572ef004c49d850cbc616e9168e48f5e63b8fd", + "src/relay/protocol.ts": "faebaded7e8c8b98f021c791b4519d78d58be3879937fe2ed2cef68b22404060", + "src/relay/fs-handler-file-read.ts": "2d70577839cb150ad0977981d54904b1a25bd8b5de7cfe19e018c08c36f41cd2", + "src/relay/dispatcher.ts": "6dca32ec33e410fa9226d6c0a4a548ed06fc5bbf1a65ccdbcae7cc43a9328d18", + "src/relay/relay-frame-decoder.ts": "7044cd142b21f847ee0dc4aee085fe18434b4bcb34ba1eb8bc1fb48a0ed7919e", + "src/main/ssh/ssh-file-stream-read-cap.ts": "83b714af0c87da4c855762ffb827f529302ebc426ab5cde59458206a64cb07b1", + "src/main/ssh/ssh-file-stream-inactivity-deadline.ts": "0554419b22356efa60065b15640d8bf0d8d166bf345e30b492b2305767ea603a", + "src/relay/fs-handler-utils.ts": "12c54b9647ddc9e4ae9924f65aadf0d00e9553050858beae232c02077a94d93a", + "src/main/ssh/relay-protocol.ts": "644aa6f2087b5867d41006bfdcec78ffba693157a2912feb8f82b120f5647b34", + "src/main/ssh/ssh-multiplexer-transport-writer.ts": "433a2cc400b5ed9743871ca03aac06d31a079ef23c42c2a2759f78967312d071", + "src/relay/dispatcher-notification-publication.ts": "d21c8575f5f4694a806b0e05cce4a1595f7116220f48d9068eba7c23aed7a546", + "src/shared/relay-frame-decoder-contract.ts": "f58279099fe4dbbdfde9e3e038a916ae077e71d8ac6aa871b59a64c6fbaeaad8", + "src/shared/relay-frame-decoder.ts": "88c0ecbc06b93efe6733094b8c9e4509ab1cbd139f8cb3257e9fb74a0ed39194", + "src/main/system-power-lifecycle.ts": "cdeb82463a530123be4d42374925a0b349446437c02bff2c287786864515de11", + "src/relay/dispatcher-pty-publication.ts": "2a91aeab16069e4d053c453947c5cd5d24c9e9ac4d36f19d23377689313cc851", + "src/relay/dispatcher-contract.ts": "7c397f9d51c0e3eca604e9075cddb6609a5e114284999009e8a86146e766c081", + "src/relay/fs-handler-list-files.ts": "cca88ec47512840837c84c63baf7ddfc7c216810cb2a5f63fa2babbeef3ff189", + "src/shared/text-search.ts": "c291574e05874440601a6a043b5d065ca172072f4146568d9c9818829abbc185", + "src/shared/image-file-extensions.ts": "8460080a80e2a09fd64466faee1b87df4d8e4803e33bc2f9dd8b3b031a54d536", + "src/shared/ripgrep-process-availability.ts": "2a8e21ae2c9af146f1f226bdfad1e45916cfb2671fb11aa4586d6a337b8cdec2", + "src/shared/search-subprocess-lines.ts": "b860de84a4081fb86515bb31910f0f1d4b0a6606370490c4f60aa9d2e9d5eee3", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts": "3e5fe7a1e3537505baf42869449193fc0448339d851254a49527ec2a1c7cbb50", + "src/shared/relay-frame-buffer.ts": "f89fcd33489894c34f79ba8fd5e5634487555a7c34fc8e7c054229ae1c20e0be", + "src/relay/dispatcher-producer-capacity.ts": "ae201a7e09acbcc013410b92868df1e964f95952f8ab3a462d177f488292965b", + "src/shared/text-search-paths.ts": "a977e1b1591b38d7f08652e3d0a64c95b9379e8339ab9d26d6325f7ec90fcda3", + "src/shared/text-search-glob-patterns.ts": "b34072078cf81813e37c37228b830c974dfadb5284ba608538deb56594d64a69", + "src/shared/string-utils.ts": "fdb48c18f2f7272ed25949eb74beca182c3336d32681191c4b0a4a077db02c22", + "src/shared/text-search-match-accumulator.ts": "1229c755abc94608e211c6e403d529905f577a991871eadfed5eb18821d6e88b", + "src/shared/json-text-structure-limit.ts": "29ded95f7e054d839dc82d118cb324d2144c70267f293d6b54f188159ada5eb9", + "src/shared/search-match-count.ts": "6fde43e886fe142c23aff2b80b3f1ebdf613a083051a11e19d7813363db88f62", + "src/shared/file-listing-cancellation.ts": "c9e1eed636fa2140071fbc2089ce02f738cce725de5069f04bf307fea611b63d", + "src/shared/quick-open-path-search.ts": "2556d6c34f50e573b2fe046f7c64f782362b3bda1241098772bd360975910a98", + "src/shared/quick-open-filter.ts": "9a09a02764d15622932083a3e550fc7215c066d30a2868f5d548b9105b780d81", + "src/relay/dispatcher-producer-transport.ts": "c48087ed5f330311fb438ae3df75ff03bf34df83156f3987b8905598e21780e5", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/shared/file-name-sort.ts": "5aafddf218c453276dcf1afdccf9cd7f82c50160f5df2dd6d1b2779ce0c0cc4a", + "src/shared/clipboard-text.ts": "50f3155063244d6d12ac1493c80029a75df256c031f986f76da64a94152d0c38", + "src/relay/dispatcher-rpc-routing.ts": "c11b3ff9096bd4877bba0ebc8a70a63092a00015fde3d8f81f3b8fb54087479b", + "src/relay/dispatcher-client-writer.ts": "9ed3ee7ad2758f2d10cd0a0ba06bf77dc3318317b8e24f0eaa6cfe235fd34be4", + "src/shared/utf8-byte-limits.ts": "01574b287b2d6ab0758112887e356c0d270b25db1b72651925ba8329eb4224e6", + "src/shared/event-loop-yield.ts": "d291b71f09eb88f24a849d0ae310f946deb7f93b0b6b490b6ea5a605ab7a53cf", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/relay/dispatcher-writer-sink.ts": "12931e899bd882347b72cd03f3690e8d0e94bf2aa1ce9a560112f52cfdc14405", + "src/relay/dispatcher-writer-drain-arm.ts": "c9f1582197d0bb9ea30793f0c792a176ada3f25fb79e03474a70d084920d0416", + "src/relay/dispatcher-writer-lane-scheduler.ts": "8b54b850f8aa88944160b796e3b357fbbc618c782590b4baf6444eda77bd5e5e", + "src/relay/dispatcher-writer-admission.ts": "12e05ba04de5687c0a0db1e44eddf342cb0c21b9653eef2aeff7b5ccd2039797", + "src/shared/timer-delay.ts": "7e529ed30d1b25521f5d72d7c2a6d05d16efe66faf2619b859d67b330cca767f", + "src/shared/skill-install-failure.ts": "571ffbaad47304bbdef294e6445d70d179db40495005fdd0ce385491666ec4eb", + "src/relay/dispatcher-frame-codec.ts": "2728b1e9e8465350c23cede5b64287db77a3d90488d8f8b1e5721bc69d7af7a9", + "src/shared/terminal-unavailable-cause.ts": "b6ce6d4b5b666ef72d8403883d2cc167f2998c524a17155638c261000c424e91", + "src/shared/runtime-capability-degradation.ts": "fc9a5d3814c390c1296ecf72d5ef317b8b8624ea2578e4ceb2b32ee642b13a3f", + "src/relay/dispatcher-capacity-signals.ts": "125a94f04f8b3102956231dc800281ed7ee9bb538b2b93f005825c2dd59e5c63", + "src/relay/dispatcher-client-lifecycle.ts": "6884223ddb2be888cfd437ca1490e14a4c95655c5c831e6812ce4a7876e9c18b", + "src/relay/dispatcher-client-state.ts": "8cdabcb8df6b16b7cc84d8a33f5171cca1807b6d9dea4b374e3c2bb4fc10bfdb", + "src/relay/legacy-relay-publication-ledger.ts": "153926b90d370669d91fd9a59d246f63ed0c01f2aec6e8b7a890a0d0ad7378ba", + "src/relay/client-request-aborts.ts": "a4f458ce767c5315aa01fa9cc8c0a3008dcb445481a401462f14ec3dbbd9e20c" + }, + "observedReaderSha256": "7a5c9c14faf63197765fc5a2900e1d3488f94aaab6757425b7ef87597479e963", + "controls": [ + { + "name": "held-metadata-foreign-history", + "readers": 4, + "entries": [144, 144, 144, 144], + "wrappers": 576, + "uniqueParams": 144, + "logicalBase64BytesByUniqueParams": 44739584, + "sharedAcrossReaders": true, + "decodedTransferBytes": 33554432, + "peakRegisteredStreams": 1, + "maxConcurrentStreams": 16, + "ackWindow": 4, + "ackCount": 128, + "observedHeapDelta": 45477336, + "released": true + }, + { + "name": "ordinary-completion", + "passed": true + }, + { + "name": "transport-disposal", + "passed": true + }, + { + "name": "metadata-request-deadline", + "milliseconds": 30000, + "relayContextAborted": true, + "released": true + }, + { + "name": "unpaced-relay", + "passed": true + }, + { + "name": "real-pump-credit-window", + "chunksBeforeAck": 4, + "totalChunks": 6 + }, + { + "name": "actual-stream-capacity", + "slots": 16, + "rejectedSeventeenth": true, + "admittedAfterCompletion": true + }, + { + "name": "saturated-writer-metadata-order", + "wireOrder": ["probe.prime", "response", "fs.streamChunk", "fs.streamChunk", "fs.streamEnd"] + }, + { + "name": "same-turn-response-and-own-frames", + "passed": true + }, + { + "name": "canonical-crlf-source-control", + "reads": 66, + "passed": true + } + ], + "artifactHashes": { + "sources.cjs": "790ef573e61fbb7f68741d1e1cfb0e4b4c79a2dde4292e7ec9dc8dcf1d94940f", + "relay-fixture.mjs": "9856e17d83b812fb6b6717cf8207ccc808165c517df8d5c76bc020e0e6f51079", + "scenario.test.mjs": "7a619c631403d0ab03610171fc6109a9b52cde2d23b9d022354c3d6bb1b154af", + "vitest.config.mjs": "4ef63c6eba6d8e443f18f787bceb0b1f40b90ee750d63a3b27239ed05bf5d849", + "before.config.mjs": "a0a8ec50388d194d60cdf7019bba18fffb2c4691404924c3675757d3b3984962", + "fix.patch": "9681d47063d4b76bc4f9567eacf5dddb3ec221b715ce0bedddfcb493e0558fc6", + "main-context.patch": "4ef0bf173e6575a548b1d217248432fedbcfd68bcb350980e73987df3aba81e9", + "source-versions.json": "7df5afb4523280364b1eecaf465131ac94822fbb868dc4aaddb42586dff9f21d" + } +} diff --git a/docs/audits/ssh-file-metadata-retention/main-context.patch b/docs/audits/ssh-file-metadata-retention/main-context.patch new file mode 100644 index 00000000000..e1093f07364 --- /dev/null +++ b/docs/audits/ssh-file-metadata-retention/main-context.patch @@ -0,0 +1,18 @@ +--- a/src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts ++++ b/src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts +@@ -6 +6 @@ +- entries: (T | undefined)[] ++ entries: T[] +@@ -23 +22,0 @@ +- queue.entries[queue.head] = undefined +@@ -25,5 +24,2 @@ +- if ( +- queue.head === queue.entries.length || +- (queue.head >= 1024 && queue.head * 2 >= queue.entries.length) +- ) { +- queue.entries = queue.entries.slice(queue.head) ++ if (queue.head === queue.entries.length) { ++ queue.entries.length = 0 +@@ -36 +32 @@ +- const entries = queue.entries.slice(queue.head).filter((entry): entry is T => entry !== undefined) ++ const entries = queue.entries.slice(queue.head) diff --git a/docs/audits/ssh-file-metadata-retention/main-fixed-electron-results.json b/docs/audits/ssh-file-metadata-retention/main-fixed-electron-results.json new file mode 100644 index 00000000000..1c9c0933590 --- /dev/null +++ b/docs/audits/ssh-file-metadata-retention/main-fixed-electron-results.json @@ -0,0 +1,140 @@ +{ + "variant": "fixed", + "graph": "main", + "runtime": { + "node": "24.21.0", + "electron": "43.7.0" + }, + "sources": { + "src/main/ssh/ssh-filesystem-stream-reader.ts": "333b0ba796edbd0a07b483cc65fb9f0c46f6f5488361bce08c488ea72543825a", + "src/main/ssh/ssh-channel-multiplexer.ts": "9ecd88963fec72901596bd25d2cb4c8666ef41d3c8a968242c6eef52095ee108", + "src/relay/fs-stream-registry.ts": "d723dd0b6419a7937225bdacbc572ef004c49d850cbc616e9168e48f5e63b8fd", + "src/relay/protocol.ts": "faebaded7e8c8b98f021c791b4519d78d58be3879937fe2ed2cef68b22404060", + "src/relay/fs-handler-file-read.ts": "2d70577839cb150ad0977981d54904b1a25bd8b5de7cfe19e018c08c36f41cd2", + "src/relay/dispatcher.ts": "6dca32ec33e410fa9226d6c0a4a548ed06fc5bbf1a65ccdbcae7cc43a9328d18", + "src/relay/relay-frame-decoder.ts": "7044cd142b21f847ee0dc4aee085fe18434b4bcb34ba1eb8bc1fb48a0ed7919e", + "src/main/ssh/ssh-file-stream-read-cap.ts": "83b714af0c87da4c855762ffb827f529302ebc426ab5cde59458206a64cb07b1", + "src/main/ssh/ssh-file-stream-inactivity-deadline.ts": "0554419b22356efa60065b15640d8bf0d8d166bf345e30b492b2305767ea603a", + "src/relay/fs-handler-utils.ts": "12c54b9647ddc9e4ae9924f65aadf0d00e9553050858beae232c02077a94d93a", + "src/main/ssh/relay-protocol.ts": "644aa6f2087b5867d41006bfdcec78ffba693157a2912feb8f82b120f5647b34", + "src/main/ssh/ssh-multiplexer-transport-writer.ts": "433a2cc400b5ed9743871ca03aac06d31a079ef23c42c2a2759f78967312d071", + "src/relay/dispatcher-notification-publication.ts": "d21c8575f5f4694a806b0e05cce4a1595f7116220f48d9068eba7c23aed7a546", + "src/shared/relay-frame-decoder-contract.ts": "f58279099fe4dbbdfde9e3e038a916ae077e71d8ac6aa871b59a64c6fbaeaad8", + "src/shared/relay-frame-decoder.ts": "88c0ecbc06b93efe6733094b8c9e4509ab1cbd139f8cb3257e9fb74a0ed39194", + "src/main/system-power-lifecycle.ts": "cdeb82463a530123be4d42374925a0b349446437c02bff2c287786864515de11", + "src/relay/dispatcher-pty-publication.ts": "2a91aeab16069e4d053c453947c5cd5d24c9e9ac4d36f19d23377689313cc851", + "src/relay/dispatcher-contract.ts": "7c397f9d51c0e3eca604e9075cddb6609a5e114284999009e8a86146e766c081", + "src/relay/fs-handler-list-files.ts": "cca88ec47512840837c84c63baf7ddfc7c216810cb2a5f63fa2babbeef3ff189", + "src/shared/text-search.ts": "c291574e05874440601a6a043b5d065ca172072f4146568d9c9818829abbc185", + "src/shared/image-file-extensions.ts": "8460080a80e2a09fd64466faee1b87df4d8e4803e33bc2f9dd8b3b031a54d536", + "src/shared/ripgrep-process-availability.ts": "2a8e21ae2c9af146f1f226bdfad1e45916cfb2671fb11aa4586d6a337b8cdec2", + "src/shared/search-subprocess-lines.ts": "b860de84a4081fb86515bb31910f0f1d4b0a6606370490c4f60aa9d2e9d5eee3", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts": "3e5fe7a1e3537505baf42869449193fc0448339d851254a49527ec2a1c7cbb50", + "src/shared/relay-frame-buffer.ts": "f89fcd33489894c34f79ba8fd5e5634487555a7c34fc8e7c054229ae1c20e0be", + "src/relay/dispatcher-producer-capacity.ts": "ae201a7e09acbcc013410b92868df1e964f95952f8ab3a462d177f488292965b", + "src/shared/text-search-paths.ts": "a977e1b1591b38d7f08652e3d0a64c95b9379e8339ab9d26d6325f7ec90fcda3", + "src/shared/text-search-glob-patterns.ts": "b34072078cf81813e37c37228b830c974dfadb5284ba608538deb56594d64a69", + "src/shared/string-utils.ts": "fdb48c18f2f7272ed25949eb74beca182c3336d32681191c4b0a4a077db02c22", + "src/shared/text-search-match-accumulator.ts": "1229c755abc94608e211c6e403d529905f577a991871eadfed5eb18821d6e88b", + "src/shared/json-text-structure-limit.ts": "29ded95f7e054d839dc82d118cb324d2144c70267f293d6b54f188159ada5eb9", + "src/shared/search-match-count.ts": "6fde43e886fe142c23aff2b80b3f1ebdf613a083051a11e19d7813363db88f62", + "src/shared/file-listing-cancellation.ts": "c9e1eed636fa2140071fbc2089ce02f738cce725de5069f04bf307fea611b63d", + "src/shared/quick-open-path-search.ts": "2556d6c34f50e573b2fe046f7c64f782362b3bda1241098772bd360975910a98", + "src/shared/quick-open-filter.ts": "9a09a02764d15622932083a3e550fc7215c066d30a2868f5d548b9105b780d81", + "src/relay/dispatcher-producer-transport.ts": "c48087ed5f330311fb438ae3df75ff03bf34df83156f3987b8905598e21780e5", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/shared/file-name-sort.ts": "5aafddf218c453276dcf1afdccf9cd7f82c50160f5df2dd6d1b2779ce0c0cc4a", + "src/shared/clipboard-text.ts": "50f3155063244d6d12ac1493c80029a75df256c031f986f76da64a94152d0c38", + "src/relay/dispatcher-rpc-routing.ts": "c11b3ff9096bd4877bba0ebc8a70a63092a00015fde3d8f81f3b8fb54087479b", + "src/relay/dispatcher-client-writer.ts": "9ed3ee7ad2758f2d10cd0a0ba06bf77dc3318317b8e24f0eaa6cfe235fd34be4", + "src/shared/utf8-byte-limits.ts": "01574b287b2d6ab0758112887e356c0d270b25db1b72651925ba8329eb4224e6", + "src/shared/event-loop-yield.ts": "d291b71f09eb88f24a849d0ae310f946deb7f93b0b6b490b6ea5a605ab7a53cf", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/relay/dispatcher-writer-sink.ts": "12931e899bd882347b72cd03f3690e8d0e94bf2aa1ce9a560112f52cfdc14405", + "src/relay/dispatcher-writer-drain-arm.ts": "c9f1582197d0bb9ea30793f0c792a176ada3f25fb79e03474a70d084920d0416", + "src/relay/dispatcher-writer-lane-scheduler.ts": "8b54b850f8aa88944160b796e3b357fbbc618c782590b4baf6444eda77bd5e5e", + "src/relay/dispatcher-writer-admission.ts": "12e05ba04de5687c0a0db1e44eddf342cb0c21b9653eef2aeff7b5ccd2039797", + "src/shared/timer-delay.ts": "7e529ed30d1b25521f5d72d7c2a6d05d16efe66faf2619b859d67b330cca767f", + "src/shared/skill-install-failure.ts": "571ffbaad47304bbdef294e6445d70d179db40495005fdd0ce385491666ec4eb", + "src/relay/dispatcher-frame-codec.ts": "2728b1e9e8465350c23cede5b64287db77a3d90488d8f8b1e5721bc69d7af7a9", + "src/shared/terminal-unavailable-cause.ts": "b6ce6d4b5b666ef72d8403883d2cc167f2998c524a17155638c261000c424e91", + "src/shared/runtime-capability-degradation.ts": "fc9a5d3814c390c1296ecf72d5ef317b8b8624ea2578e4ceb2b32ee642b13a3f", + "src/relay/dispatcher-capacity-signals.ts": "125a94f04f8b3102956231dc800281ed7ee9bb538b2b93f005825c2dd59e5c63", + "src/relay/dispatcher-client-lifecycle.ts": "6884223ddb2be888cfd437ca1490e14a4c95655c5c831e6812ce4a7876e9c18b", + "src/relay/dispatcher-client-state.ts": "8cdabcb8df6b16b7cc84d8a33f5171cca1807b6d9dea4b374e3c2bb4fc10bfdb", + "src/relay/legacy-relay-publication-ledger.ts": "153926b90d370669d91fd9a59d246f63ed0c01f2aec6e8b7a890a0d0ad7378ba", + "src/relay/client-request-aborts.ts": "a4f458ce767c5315aa01fa9cc8c0a3008dcb445481a401462f14ec3dbbd9e20c" + }, + "observedReaderSha256": "333b0ba796edbd0a07b483cc65fb9f0c46f6f5488361bce08c488ea72543825a", + "controls": [ + { + "name": "held-metadata-foreign-history", + "readers": 4, + "entries": [0, 0, 0, 0], + "wrappers": 0, + "uniqueParams": 0, + "logicalBase64BytesByUniqueParams": 0, + "sharedAcrossReaders": false, + "decodedTransferBytes": 33554432, + "peakRegisteredStreams": 1, + "maxConcurrentStreams": 16, + "ackWindow": 4, + "ackCount": 128, + "observedHeapDelta": 513692, + "released": true + }, + { + "name": "ordinary-completion", + "passed": true + }, + { + "name": "transport-disposal", + "passed": true + }, + { + "name": "metadata-request-deadline", + "milliseconds": 30000, + "relayContextAborted": true, + "released": true + }, + { + "name": "unpaced-relay", + "passed": true + }, + { + "name": "real-pump-credit-window", + "chunksBeforeAck": 4, + "totalChunks": 6 + }, + { + "name": "actual-stream-capacity", + "slots": 16, + "rejectedSeventeenth": true, + "admittedAfterCompletion": true + }, + { + "name": "saturated-writer-metadata-order", + "wireOrder": ["probe.prime", "response", "fs.streamChunk", "fs.streamChunk", "fs.streamEnd"] + }, + { + "name": "same-turn-response-and-own-frames", + "passed": true + }, + { + "name": "canonical-crlf-source-control", + "reads": 66, + "passed": true + } + ], + "artifactHashes": { + "sources.cjs": "790ef573e61fbb7f68741d1e1cfb0e4b4c79a2dde4292e7ec9dc8dcf1d94940f", + "relay-fixture.mjs": "9856e17d83b812fb6b6717cf8207ccc808165c517df8d5c76bc020e0e6f51079", + "scenario.test.mjs": "7a619c631403d0ab03610171fc6109a9b52cde2d23b9d022354c3d6bb1b154af", + "vitest.config.mjs": "4ef63c6eba6d8e443f18f787bceb0b1f40b90ee750d63a3b27239ed05bf5d849", + "before.config.mjs": "a0a8ec50388d194d60cdf7019bba18fffb2c4691404924c3675757d3b3984962", + "fix.patch": "9681d47063d4b76bc4f9567eacf5dddb3ec221b715ce0bedddfcb493e0558fc6", + "main-context.patch": "4ef0bf173e6575a548b1d217248432fedbcfd68bcb350980e73987df3aba81e9", + "source-versions.json": "7df5afb4523280364b1eecaf465131ac94822fbb868dc4aaddb42586dff9f21d" + } +} diff --git a/docs/audits/ssh-file-metadata-retention/main-fixed-node-results.json b/docs/audits/ssh-file-metadata-retention/main-fixed-node-results.json new file mode 100644 index 00000000000..6e0629f9a29 --- /dev/null +++ b/docs/audits/ssh-file-metadata-retention/main-fixed-node-results.json @@ -0,0 +1,140 @@ +{ + "variant": "fixed", + "graph": "main", + "runtime": { + "node": "26.6.0", + "electron": null + }, + "sources": { + "src/main/ssh/ssh-filesystem-stream-reader.ts": "333b0ba796edbd0a07b483cc65fb9f0c46f6f5488361bce08c488ea72543825a", + "src/main/ssh/ssh-channel-multiplexer.ts": "9ecd88963fec72901596bd25d2cb4c8666ef41d3c8a968242c6eef52095ee108", + "src/relay/fs-stream-registry.ts": "d723dd0b6419a7937225bdacbc572ef004c49d850cbc616e9168e48f5e63b8fd", + "src/relay/protocol.ts": "faebaded7e8c8b98f021c791b4519d78d58be3879937fe2ed2cef68b22404060", + "src/relay/fs-handler-file-read.ts": "2d70577839cb150ad0977981d54904b1a25bd8b5de7cfe19e018c08c36f41cd2", + "src/relay/dispatcher.ts": "6dca32ec33e410fa9226d6c0a4a548ed06fc5bbf1a65ccdbcae7cc43a9328d18", + "src/relay/relay-frame-decoder.ts": "7044cd142b21f847ee0dc4aee085fe18434b4bcb34ba1eb8bc1fb48a0ed7919e", + "src/main/ssh/ssh-file-stream-read-cap.ts": "83b714af0c87da4c855762ffb827f529302ebc426ab5cde59458206a64cb07b1", + "src/main/ssh/ssh-file-stream-inactivity-deadline.ts": "0554419b22356efa60065b15640d8bf0d8d166bf345e30b492b2305767ea603a", + "src/relay/fs-handler-utils.ts": "12c54b9647ddc9e4ae9924f65aadf0d00e9553050858beae232c02077a94d93a", + "src/main/ssh/relay-protocol.ts": "644aa6f2087b5867d41006bfdcec78ffba693157a2912feb8f82b120f5647b34", + "src/main/ssh/ssh-multiplexer-transport-writer.ts": "433a2cc400b5ed9743871ca03aac06d31a079ef23c42c2a2759f78967312d071", + "src/relay/dispatcher-notification-publication.ts": "d21c8575f5f4694a806b0e05cce4a1595f7116220f48d9068eba7c23aed7a546", + "src/shared/relay-frame-decoder-contract.ts": "f58279099fe4dbbdfde9e3e038a916ae077e71d8ac6aa871b59a64c6fbaeaad8", + "src/shared/relay-frame-decoder.ts": "88c0ecbc06b93efe6733094b8c9e4509ab1cbd139f8cb3257e9fb74a0ed39194", + "src/main/system-power-lifecycle.ts": "cdeb82463a530123be4d42374925a0b349446437c02bff2c287786864515de11", + "src/relay/dispatcher-pty-publication.ts": "2a91aeab16069e4d053c453947c5cd5d24c9e9ac4d36f19d23377689313cc851", + "src/relay/dispatcher-contract.ts": "7c397f9d51c0e3eca604e9075cddb6609a5e114284999009e8a86146e766c081", + "src/relay/fs-handler-list-files.ts": "cca88ec47512840837c84c63baf7ddfc7c216810cb2a5f63fa2babbeef3ff189", + "src/shared/text-search.ts": "c291574e05874440601a6a043b5d065ca172072f4146568d9c9818829abbc185", + "src/shared/image-file-extensions.ts": "8460080a80e2a09fd64466faee1b87df4d8e4803e33bc2f9dd8b3b031a54d536", + "src/shared/ripgrep-process-availability.ts": "2a8e21ae2c9af146f1f226bdfad1e45916cfb2671fb11aa4586d6a337b8cdec2", + "src/shared/search-subprocess-lines.ts": "b860de84a4081fb86515bb31910f0f1d4b0a6606370490c4f60aa9d2e9d5eee3", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts": "3e5fe7a1e3537505baf42869449193fc0448339d851254a49527ec2a1c7cbb50", + "src/shared/relay-frame-buffer.ts": "f89fcd33489894c34f79ba8fd5e5634487555a7c34fc8e7c054229ae1c20e0be", + "src/relay/dispatcher-producer-capacity.ts": "ae201a7e09acbcc013410b92868df1e964f95952f8ab3a462d177f488292965b", + "src/shared/text-search-paths.ts": "a977e1b1591b38d7f08652e3d0a64c95b9379e8339ab9d26d6325f7ec90fcda3", + "src/shared/text-search-glob-patterns.ts": "b34072078cf81813e37c37228b830c974dfadb5284ba608538deb56594d64a69", + "src/shared/string-utils.ts": "fdb48c18f2f7272ed25949eb74beca182c3336d32681191c4b0a4a077db02c22", + "src/shared/text-search-match-accumulator.ts": "1229c755abc94608e211c6e403d529905f577a991871eadfed5eb18821d6e88b", + "src/shared/json-text-structure-limit.ts": "29ded95f7e054d839dc82d118cb324d2144c70267f293d6b54f188159ada5eb9", + "src/shared/search-match-count.ts": "6fde43e886fe142c23aff2b80b3f1ebdf613a083051a11e19d7813363db88f62", + "src/shared/file-listing-cancellation.ts": "c9e1eed636fa2140071fbc2089ce02f738cce725de5069f04bf307fea611b63d", + "src/shared/quick-open-path-search.ts": "2556d6c34f50e573b2fe046f7c64f782362b3bda1241098772bd360975910a98", + "src/shared/quick-open-filter.ts": "9a09a02764d15622932083a3e550fc7215c066d30a2868f5d548b9105b780d81", + "src/relay/dispatcher-producer-transport.ts": "c48087ed5f330311fb438ae3df75ff03bf34df83156f3987b8905598e21780e5", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/shared/file-name-sort.ts": "5aafddf218c453276dcf1afdccf9cd7f82c50160f5df2dd6d1b2779ce0c0cc4a", + "src/shared/clipboard-text.ts": "50f3155063244d6d12ac1493c80029a75df256c031f986f76da64a94152d0c38", + "src/relay/dispatcher-rpc-routing.ts": "c11b3ff9096bd4877bba0ebc8a70a63092a00015fde3d8f81f3b8fb54087479b", + "src/relay/dispatcher-client-writer.ts": "9ed3ee7ad2758f2d10cd0a0ba06bf77dc3318317b8e24f0eaa6cfe235fd34be4", + "src/shared/utf8-byte-limits.ts": "01574b287b2d6ab0758112887e356c0d270b25db1b72651925ba8329eb4224e6", + "src/shared/event-loop-yield.ts": "d291b71f09eb88f24a849d0ae310f946deb7f93b0b6b490b6ea5a605ab7a53cf", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/relay/dispatcher-writer-sink.ts": "12931e899bd882347b72cd03f3690e8d0e94bf2aa1ce9a560112f52cfdc14405", + "src/relay/dispatcher-writer-drain-arm.ts": "c9f1582197d0bb9ea30793f0c792a176ada3f25fb79e03474a70d084920d0416", + "src/relay/dispatcher-writer-lane-scheduler.ts": "8b54b850f8aa88944160b796e3b357fbbc618c782590b4baf6444eda77bd5e5e", + "src/relay/dispatcher-writer-admission.ts": "12e05ba04de5687c0a0db1e44eddf342cb0c21b9653eef2aeff7b5ccd2039797", + "src/shared/timer-delay.ts": "7e529ed30d1b25521f5d72d7c2a6d05d16efe66faf2619b859d67b330cca767f", + "src/shared/skill-install-failure.ts": "571ffbaad47304bbdef294e6445d70d179db40495005fdd0ce385491666ec4eb", + "src/relay/dispatcher-frame-codec.ts": "2728b1e9e8465350c23cede5b64287db77a3d90488d8f8b1e5721bc69d7af7a9", + "src/shared/terminal-unavailable-cause.ts": "b6ce6d4b5b666ef72d8403883d2cc167f2998c524a17155638c261000c424e91", + "src/shared/runtime-capability-degradation.ts": "fc9a5d3814c390c1296ecf72d5ef317b8b8624ea2578e4ceb2b32ee642b13a3f", + "src/relay/dispatcher-capacity-signals.ts": "125a94f04f8b3102956231dc800281ed7ee9bb538b2b93f005825c2dd59e5c63", + "src/relay/dispatcher-client-lifecycle.ts": "6884223ddb2be888cfd437ca1490e14a4c95655c5c831e6812ce4a7876e9c18b", + "src/relay/dispatcher-client-state.ts": "8cdabcb8df6b16b7cc84d8a33f5171cca1807b6d9dea4b374e3c2bb4fc10bfdb", + "src/relay/legacy-relay-publication-ledger.ts": "153926b90d370669d91fd9a59d246f63ed0c01f2aec6e8b7a890a0d0ad7378ba", + "src/relay/client-request-aborts.ts": "a4f458ce767c5315aa01fa9cc8c0a3008dcb445481a401462f14ec3dbbd9e20c" + }, + "observedReaderSha256": "333b0ba796edbd0a07b483cc65fb9f0c46f6f5488361bce08c488ea72543825a", + "controls": [ + { + "name": "held-metadata-foreign-history", + "readers": 4, + "entries": [0, 0, 0, 0], + "wrappers": 0, + "uniqueParams": 0, + "logicalBase64BytesByUniqueParams": 0, + "sharedAcrossReaders": false, + "decodedTransferBytes": 33554432, + "peakRegisteredStreams": 1, + "maxConcurrentStreams": 16, + "ackWindow": 4, + "ackCount": 128, + "observedHeapDelta": -669752, + "released": true + }, + { + "name": "ordinary-completion", + "passed": true + }, + { + "name": "transport-disposal", + "passed": true + }, + { + "name": "metadata-request-deadline", + "milliseconds": 30000, + "relayContextAborted": true, + "released": true + }, + { + "name": "unpaced-relay", + "passed": true + }, + { + "name": "real-pump-credit-window", + "chunksBeforeAck": 4, + "totalChunks": 6 + }, + { + "name": "actual-stream-capacity", + "slots": 16, + "rejectedSeventeenth": true, + "admittedAfterCompletion": true + }, + { + "name": "saturated-writer-metadata-order", + "wireOrder": ["probe.prime", "response", "fs.streamChunk", "fs.streamChunk", "fs.streamEnd"] + }, + { + "name": "same-turn-response-and-own-frames", + "passed": true + }, + { + "name": "canonical-crlf-source-control", + "reads": 66, + "passed": true + } + ], + "artifactHashes": { + "sources.cjs": "790ef573e61fbb7f68741d1e1cfb0e4b4c79a2dde4292e7ec9dc8dcf1d94940f", + "relay-fixture.mjs": "9856e17d83b812fb6b6717cf8207ccc808165c517df8d5c76bc020e0e6f51079", + "scenario.test.mjs": "7a619c631403d0ab03610171fc6109a9b52cde2d23b9d022354c3d6bb1b154af", + "vitest.config.mjs": "4ef63c6eba6d8e443f18f787bceb0b1f40b90ee750d63a3b27239ed05bf5d849", + "before.config.mjs": "a0a8ec50388d194d60cdf7019bba18fffb2c4691404924c3675757d3b3984962", + "fix.patch": "9681d47063d4b76bc4f9567eacf5dddb3ec221b715ce0bedddfcb493e0558fc6", + "main-context.patch": "4ef0bf173e6575a548b1d217248432fedbcfd68bcb350980e73987df3aba81e9", + "source-versions.json": "7df5afb4523280364b1eecaf465131ac94822fbb868dc4aaddb42586dff9f21d" + } +} diff --git a/docs/audits/ssh-file-metadata-retention/relay-fixture.mjs b/docs/audits/ssh-file-metadata-retention/relay-fixture.mjs new file mode 100644 index 00000000000..2c545e5df89 --- /dev/null +++ b/docs/audits/ssh-file-metadata-retention/relay-fixture.mjs @@ -0,0 +1,226 @@ +import { afterEach, beforeEach, expect, vi } from 'vitest' +import { mkdtemp, writeFile, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { createHash, randomBytes } from 'node:crypto' +import { writeFileSync, readFileSync } from 'node:fs' +import { SshChannelMultiplexer } from '../../../src/main/ssh/ssh-channel-multiplexer' +import { readFileViaStream } from '../../../src/main/ssh/ssh-filesystem-stream-reader' +import { RelayDispatcher } from '../../../src/relay/dispatcher' +import { RelayStreamRegistry } from '../../../src/relay/fs-stream-registry' +import { readRelayFileStreamMetadata } from '../../../src/relay/fs-handler-file-read' + +import { createRequire } from 'node:module' +const { loadSources } = createRequire(import.meta.url)('./sources.cjs') +const sourceInfo = loadSources() +const gates = new Map() +const candidate = process.env.ORCA_SSH_READER_VARIANT !== 'before' +const graph = process.env.ORCA_SSH_READER_GRAPH ?? 'worktree' +const artifactNames = [ + 'sources.cjs', + 'relay-fixture.mjs', + 'scenario.test.mjs', + 'vitest.config.mjs', + 'before.config.mjs', + 'fix.patch', + 'main-context.patch', + 'source-versions.json' +] +const report = { + variant: candidate ? 'fixed' : 'before', + graph, + runtime: { node: process.versions.node, electron: process.versions.electron ?? null }, + sources: sourceInfo.hashes, + observedReaderSha256: sourceInfo.observedReaderSha256, + controls: [] +} +const nextTurn = () => new Promise((resolve) => setImmediate(resolve)) +let directory +let fixtures +let heldPaths +function gate(path) { + let release + const promise = new Promise((resolve) => { + release = resolve + }) + gates.set(path, { promise, release }) +} +async function heldFiles(count) { + const paths = [] + for (let i = 0; i < count; i++) { + const path = join(directory, `held-${i}.png`) + await writeFile(path, '') + gate(path) + paths.push(path) + heldPaths.push(path) + } + return paths +} +function connect({ pacing = true, passAcks = true, blockFirstWrite = false } = {}) { + let receive + let drain + let blocked = blockFirstWrite + const registry = new RelayStreamRegistry() + const stats = { peakStreams: 0, chunks: 0, ends: 0, acks: 0, contexts: [], wireOrder: [] } + const dispatcher = new RelayDispatcher( + (data) => { + if (data[0] === 1) { + const message = JSON.parse(data.subarray(13).toString()) + stats.wireOrder.push(message.method ?? 'response') + } + receive(data) + if (blocked) { + blocked = false + return false + } + }, + { + waitWriteDrain(callback) { + drain = callback + return () => {} + } + } + ) + const mux = new SshChannelMultiplexer({ + write(data) { + dispatcher.feed(data) + }, + onData(callback) { + receive = callback + }, + onClose() {} + }) + dispatcher.onRequest('fs.readFileStream', async (params, context) => { + stats.contexts.push(context) + await gates.get(params.filePath)?.promise + const result = await readRelayFileStreamMetadata( + params.filePath, + dispatcher, + registry, + context, + { clientId: context.clientId, paceWithAcks: pacing && params.flowControl === 'ack' } + ) + stats.peakStreams = Math.max(stats.peakStreams, registry.size()) + return result + }) + dispatcher.onNotification('fs.streamAck', (params) => { + stats.acks++ + if (passAcks) { + registry.recordAck(params.streamId, params.seq) + } + }) + dispatcher.onNotification('fs.cancelStream', (params) => registry.abort(params.streamId)) + mux.onNotificationByMethod('fs.streamChunk', () => { + stats.chunks++ + }) + mux.onNotificationByMethod('fs.streamEnd', () => { + stats.ends++ + }) + const fixture = { + mux, + dispatcher, + registry, + stats, + drain() { + drain?.() + } + } + fixtures.push(fixture) + return fixture +} +function snapshot(paths) { + const rows = paths.map((path) => globalThis.__sshPendingReaders.get(path)?.deref() ?? []) + const unique = new Set(rows.flatMap((row) => row.map((frame) => frame.params))) + const bytes = [...unique].reduce( + (sum, params) => sum + (typeof params.data === 'string' ? params.data.length : 0), + 0 + ) + return { + readers: paths.length, + entries: rows.map((row) => row.length), + wrappers: rows.reduce((sum, row) => sum + row.length, 0), + uniqueParams: unique.size, + logicalBase64BytesByUniqueParams: bytes, + sharedAcrossReaders: + rows.length > 1 && + rows[0].length > 0 && + rows.every((row) => row.every((frame, index) => frame.params === rows[0][index]?.params)) + } +} +async function collect() { + for (let i = 0; i < 5; i++) { + await nextTurn() + global.gc() + } + await nextTurn() +} +async function assertReleased(paths) { + await collect() + for (const path of paths) { + expect(globalThis.__sshPendingReaders.get(path)?.deref()).toBeUndefined() + } +} +async function makePayload(size) { + const path = join(directory, 'payload.png') + const bytes = randomBytes(size) + await writeFile(path, bytes) + return { path, hash: createHash('sha256').update(bytes).digest('hex'), size } +} +async function successfulRead(mux, payload) { + const result = await readFileViaStream(mux, payload.path) + expect(result.isImage).toBe(true) + const bytes = Buffer.from(result.content, 'base64') + expect(bytes.length).toBe(payload.size) + expect(createHash('sha256').update(bytes).digest('hex')).toBe(payload.hash) +} +beforeEach(async () => { + expect(process.env.ORCA_BACKGROUND_LAUNCH).toBe('1') + directory = await mkdtemp(join(tmpdir(), 'orca-ssh-reader-')) + fixtures = [] + heldPaths = [] + globalThis.__sshPendingReaders = new Map() +}) +afterEach(async () => { + vi.useRealTimers() + for (const entry of gates.values()) { + entry.release() + } + gates.clear() + for (const fixture of fixtures) { + fixture.mux.dispose() + fixture.dispatcher.dispose() + await fixture.registry.disposeAll() + } + await nextTurn() + await rm(directory, { recursive: true, force: true }) + report.artifactHashes = Object.fromEntries( + artifactNames.map((name) => [ + name, + createHash('sha256') + .update(readFileSync(new URL(name, import.meta.url))) + .digest('hex') + ]) + ) + writeFileSync( + process.env.ORCA_SSH_READER_OUTPUT ?? + new URL( + `./${graph}-${report.variant}-${process.versions.electron ? 'electron' : 'node'}-results.json`, + import.meta.url + ), + `${JSON.stringify(report, null, 2)}\n` + ) +}) + +export { + candidate, + report, + nextTurn, + gates, + heldFiles, + connect, + snapshot, + collect, + assertReleased, + makePayload, + successfulRead +} diff --git a/docs/audits/ssh-file-metadata-retention/scenario.test.mjs b/docs/audits/ssh-file-metadata-retention/scenario.test.mjs new file mode 100644 index 00000000000..5e5c9455200 --- /dev/null +++ b/docs/audits/ssh-file-metadata-retention/scenario.test.mjs @@ -0,0 +1,267 @@ +import { readFileSync } from 'node:fs' +import { createRequire } from 'node:module' +import { describe, expect, it, vi } from 'vitest' +import { SshChannelMultiplexer } from '../../../src/main/ssh/ssh-channel-multiplexer' +import { readFileViaStream } from '../../../src/main/ssh/ssh-filesystem-stream-reader' +import { + encodeJsonRpcFrame, + MAX_CONCURRENT_STREAMS, + STREAM_ACK_WINDOW_CHUNKS, + STREAM_CHUNK_SIZE, + RelayErrorCode +} from '../../../src/relay/protocol' +import { + candidate, + report, + nextTurn, + gates, + heldFiles, + connect, + snapshot, + collect, + assertReleased, + makePayload, + successfulRead +} from './relay-fixture.mjs' +describe('actual SSH mux, relay dispatcher and file producer ownership', () => { + it('retains shared foreign frames while four metadata request handlers are deliberately held', async () => { + const paths = await heldFiles(4) + const { mux, stats } = connect() + const pending = paths.map((path) => readFileViaStream(mux, path)) + const payload = await makePayload(2 * 1024 * 1024) + await collect() + const startHeap = process.memoryUsage().heapUsed + for (let i = 0; i < 16; i++) { + await successfulRead(mux, payload) + } + await collect() + const retained = snapshot(paths) + expect(stats.chunks).toBe(128) + expect(stats.ends).toBe(16) + expect(stats.acks).toBe(128) + expect(stats.peakStreams).toBe(1) + expect(retained.entries).toEqual(Array(4).fill(candidate ? 0 : 144)) + expect(retained.uniqueParams).toBe(candidate ? 0 : 144) + expect(retained.logicalBase64BytesByUniqueParams).toBe( + candidate ? 0 : 128 * Math.ceil(STREAM_CHUNK_SIZE / 3) * 4 + ) + expect(retained.sharedAcrossReaders).toBe(!candidate) + const heapDelta = process.memoryUsage().heapUsed - startHeap + for (const path of paths) { + gates.get(path).release() + } + expect(await Promise.all(pending)).toEqual( + Array.from({ length: 4 }, () => ({ + content: '', + isBinary: true, + isImage: true, + mimeType: 'image/png' + })) + ) + await assertReleased(paths) + report.controls.push({ + name: 'held-metadata-foreign-history', + ...retained, + decodedTransferBytes: 16 * payload.size, + peakRegisteredStreams: stats.peakStreams, + maxConcurrentStreams: MAX_CONCURRENT_STREAMS, + ackWindow: STREAM_ACK_WINDOW_CHUNKS, + ackCount: stats.acks, + observedHeapDelta: heapDelta, + released: true + }) + }) + it('finishes normally without a held metadata request and releases reader state', async () => { + const { mux } = connect() + const payload = await makePayload(STREAM_CHUNK_SIZE + 17) + for (let i = 0; i < 3; i++) { + await successfulRead(mux, payload) + } + await assertReleased([payload.path]) + report.controls.push({ name: 'ordinary-completion', passed: true }) + }) + it('cleans up all pending metadata listeners when transport is disposed', async () => { + const paths = await heldFiles(2) + const { mux } = connect() + const pending = paths.map((path) => + readFileViaStream(mux, path).catch((error) => { + void error.stack + return error.code + }) + ) + await successfulRead(mux, await makePayload(STREAM_CHUNK_SIZE + 1)) + expect(snapshot(paths).wrappers).toBe(candidate ? 0 : 6) + mux.dispose('connection_lost') + const results = await Promise.all(pending) + expect(results).toEqual(['CONNECTION_LOST', 'CONNECTION_LOST']) + await assertReleased(paths) + report.controls.push({ name: 'transport-disposal', passed: true }) + }) + it('retains no reader history after the 30 second request deadline while relay work remains pending', async () => { + const paths = await heldFiles(1) + vi.useFakeTimers({ + toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'Date'] + }) + const { mux, stats } = connect() + const pending = readFileViaStream(mux, paths[0]).catch((error) => error) + await successfulRead(mux, await makePayload(STREAM_CHUNK_SIZE)) + expect(snapshot(paths).wrappers).toBe(candidate ? 0 : 2) + // Keep the actual mux/relay health timers active on the fake clock as well. + for (let i = 0; i < 6; i++) { + await vi.advanceTimersByTimeAsync(5000) + } + expect((await pending).code).toBe('SSH_MUX_REQUEST_TIMEOUT') + expect(stats.contexts[0].signal.aborted).toBe(true) + vi.useRealTimers() + await assertReleased(paths) + report.controls.push({ + name: 'metadata-request-deadline', + milliseconds: 30000, + relayContextAborted: true, + released: true + }) + }) + it('supports a relay that ignores optional chunk pacing', async () => { + const paths = await heldFiles(1) + const { mux, stats } = connect({ pacing: false }) + const pending = readFileViaStream(mux, paths[0]) + await successfulRead(mux, await makePayload(2 * STREAM_CHUNK_SIZE + 7)) + expect(stats.chunks).toBe(3) + expect(snapshot(paths).wrappers).toBe(candidate ? 0 : 4) + gates.get(paths[0]).release() + await pending + await assertReleased(paths) + report.controls.push({ name: 'unpaced-relay', passed: true }) + }) + it('actually stops the pump after four chunks until acknowledgements resume', async () => { + const { mux, registry, stats } = connect({ passAcks: false }) + const payload = await makePayload(6 * STREAM_CHUNK_SIZE) + const pending = successfulRead(mux, payload) + for (let i = 0; i < 200 && stats.chunks < 4; i++) { + await new Promise((resolve) => setTimeout(resolve, 2)) + } + expect(stats.chunks).toBe(4) + await new Promise((resolve) => setTimeout(resolve, 25)) + expect(stats.chunks).toBe(4) + registry.recordAck(1, 3) + await pending + expect(stats.chunks).toBe(6) + report.controls.push({ name: 'real-pump-credit-window', chunksBeforeAck: 4, totalChunks: 6 }) + }) + it('enforces the real 16 slot limit and admits another file after completion', async () => { + const { mux, registry, stats } = connect({ passAcks: false }) + const payload = await makePayload(5 * STREAM_CHUNK_SIZE) + const pending = Array.from({ length: 16 }, () => successfulRead(mux, payload)) + for (let i = 0; i < 500 && stats.chunks < 64; i++) { + await new Promise((resolve) => setTimeout(resolve, 2)) + } + expect(registry.size()).toBe(16) + expect(stats.chunks).toBe(64) + const error = await readFileViaStream(mux, payload.path).catch((error) => error) + expect(error.code).toBe(RelayErrorCode.TooManyStreams) + for (let id = 1; id <= 16; id++) { + registry.recordAck(id, 3) + } + await Promise.all(pending) + expect(registry.size()).toBe(0) + const small = await makePayload(1) + await successfulRead(mux, small) + report.controls.push({ + name: 'actual-stream-capacity', + slots: 16, + rejectedSeventeenth: true, + admittedAfterCompletion: true + }) + }) + it('writes metadata before own chunks when the relay writer resumes from saturation', async () => { + const fixture = connect({ blockFirstWrite: true }) + fixture.dispatcher.notifyClient(1, 'probe.prime') + const payload = await makePayload(STREAM_CHUNK_SIZE + 1) + const pending = successfulRead(fixture.mux, payload) + for (let i = 0; i < 100 && fixture.stats.peakStreams < 1; i++) { + await new Promise((resolve) => setTimeout(resolve, 2)) + } + await nextTurn() + expect(fixture.stats.peakStreams).toBe(1) + expect(fixture.stats.wireOrder).toEqual(['probe.prime']) + fixture.drain() + await pending + expect(fixture.stats.wireOrder).toEqual([ + 'probe.prime', + 'response', + 'fs.streamChunk', + 'fs.streamChunk', + 'fs.streamEnd' + ]) + report.controls.push({ + name: 'saturated-writer-metadata-order', + wireOrder: fixture.stats.wireOrder + }) + }) + it('handles response and own chunk/end in one decoder dispatch turn', async () => { + let receive + let requestId + const mux = new SshChannelMultiplexer({ + write(data) { + if (data[0] === 1) { + const message = JSON.parse(data.subarray(13).toString()) + if (message.method === 'fs.readFileStream') { + requestId = message.id + } + } + }, + onData(callback) { + receive = callback + }, + onClose() {} + }) + const pending = readFileViaStream(mux, 'coalesced.png') + const data = Buffer.from('adjacent\0frame') + receive( + Buffer.concat([ + encodeJsonRpcFrame( + { + jsonrpc: '2.0', + id: requestId, + result: { streamId: 7, totalSize: data.length, isBinary: true } + }, + 1, + 0 + ), + encodeJsonRpcFrame( + { + jsonrpc: '2.0', + method: 'fs.streamChunk', + params: { streamId: 7, seq: 0, data: data.toString('base64') } + }, + 2, + 0 + ), + encodeJsonRpcFrame( + { jsonrpc: '2.0', method: 'fs.streamEnd', params: { streamId: 7 } }, + 3, + 0 + ) + ]) + ) + expect(await pending).toEqual({ content: data.toString('base64'), isBinary: true }) + mux.dispose() + await assertReleased(['coalesced.png']) + report.controls.push({ name: 'same-turn-response-and-own-frames', passed: true }) + }) +}) + +it('reconstructs both sources identically from synthetic CRLF checkout and patch reads', () => { + const { loadSources } = createRequire(import.meta.url)('./sources.cjs') + let reads = 0 + const observed = loadSources({ + read(filename) { + reads += 1 + return readFileSync(filename, 'utf8').replace(/\r?\n/g, '\r\n') + } + }) + const ordinary = loadSources() + expect(observed.hashes).toEqual(ordinary.hashes) + expect([...observed.sources]).toEqual([...ordinary.sources]) + report.controls.push({ name: 'canonical-crlf-source-control', reads, passed: true }) +}) diff --git a/docs/audits/ssh-file-metadata-retention/source-versions.json b/docs/audits/ssh-file-metadata-retention/source-versions.json new file mode 100644 index 00000000000..66f6a28a3d4 --- /dev/null +++ b/docs/audits/ssh-file-metadata-retention/source-versions.json @@ -0,0 +1,225 @@ +{ + "sourcePath": "src/main/ssh/ssh-filesystem-stream-reader.ts", + "baselineSha256": "b8586de1412df98428939a0a4870c5f8ccab22f506a188b2eb1c7a38c317b1ef", + "fixedSha256": "333b0ba796edbd0a07b483cc65fb9f0c46f6f5488361bce08c488ea72543825a", + "contextPath": "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts", + "worktreeGraph": { + "src/relay/dispatcher.ts": "6dca32ec33e410fa9226d6c0a4a548ed06fc5bbf1a65ccdbcae7cc43a9328d18", + "src/main/ssh/ssh-channel-multiplexer.ts": "9ecd88963fec72901596bd25d2cb4c8666ef41d3c8a968242c6eef52095ee108", + "src/main/ssh/ssh-filesystem-stream-reader.ts": "333b0ba796edbd0a07b483cc65fb9f0c46f6f5488361bce08c488ea72543825a", + "src/relay/fs-handler-file-read.ts": "2d70577839cb150ad0977981d54904b1a25bd8b5de7cfe19e018c08c36f41cd2", + "src/relay/protocol.ts": "faebaded7e8c8b98f021c791b4519d78d58be3879937fe2ed2cef68b22404060", + "src/relay/fs-stream-registry.ts": "d723dd0b6419a7937225bdacbc572ef004c49d850cbc616e9168e48f5e63b8fd", + "src/relay/dispatcher-notification-publication.ts": "d21c8575f5f4694a806b0e05cce4a1595f7116220f48d9068eba7c23aed7a546", + "src/relay/fs-handler-utils.ts": "12c54b9647ddc9e4ae9924f65aadf0d00e9553050858beae232c02077a94d93a", + "src/relay/relay-frame-decoder.ts": "7044cd142b21f847ee0dc4aee085fe18434b4bcb34ba1eb8bc1fb48a0ed7919e", + "src/main/ssh/ssh-file-stream-inactivity-deadline.ts": "0554419b22356efa60065b15640d8bf0d8d166bf345e30b492b2305767ea603a", + "src/main/ssh/ssh-file-stream-read-cap.ts": "83b714af0c87da4c855762ffb827f529302ebc426ab5cde59458206a64cb07b1", + "src/main/ssh/relay-protocol.ts": "644aa6f2087b5867d41006bfdcec78ffba693157a2912feb8f82b120f5647b34", + "src/main/ssh/ssh-multiplexer-transport-writer.ts": "433a2cc400b5ed9743871ca03aac06d31a079ef23c42c2a2759f78967312d071", + "src/shared/relay-frame-decoder-contract.ts": "f58279099fe4dbbdfde9e3e038a916ae077e71d8ac6aa871b59a64c6fbaeaad8", + "src/relay/dispatcher-pty-publication.ts": "2a91aeab16069e4d053c453947c5cd5d24c9e9ac4d36f19d23377689313cc851", + "src/main/system-power-lifecycle.ts": "cdeb82463a530123be4d42374925a0b349446437c02bff2c287786864515de11", + "src/relay/dispatcher-contract.ts": "7c397f9d51c0e3eca604e9075cddb6609a5e114284999009e8a86146e766c081", + "src/shared/relay-frame-decoder.ts": "88c0ecbc06b93efe6733094b8c9e4509ab1cbd139f8cb3257e9fb74a0ed39194", + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts": "6173ddd6640a930d0fa586fed91c1adc0d865a33f81af9a1ed5a0817c08e90cc", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/shared/search-subprocess-lines.ts": "b860de84a4081fb86515bb31910f0f1d4b0a6606370490c4f60aa9d2e9d5eee3", + "src/relay/fs-handler-list-files.ts": "cca88ec47512840837c84c63baf7ddfc7c216810cb2a5f63fa2babbeef3ff189", + "src/shared/text-search.ts": "c291574e05874440601a6a043b5d065ca172072f4146568d9c9818829abbc185", + "src/shared/ripgrep-process-availability.ts": "2a8e21ae2c9af146f1f226bdfad1e45916cfb2671fb11aa4586d6a337b8cdec2", + "src/shared/image-file-extensions.ts": "8460080a80e2a09fd64466faee1b87df4d8e4803e33bc2f9dd8b3b031a54d536", + "src/relay/dispatcher-producer-capacity.ts": "ae201a7e09acbcc013410b92868df1e964f95952f8ab3a462d177f488292965b", + "src/relay/dispatcher-producer-transport.ts": "c48087ed5f330311fb438ae3df75ff03bf34df83156f3987b8905598e21780e5", + "src/shared/relay-frame-buffer.ts": "f89fcd33489894c34f79ba8fd5e5634487555a7c34fc8e7c054229ae1c20e0be", + "src/shared/quick-open-path-search.ts": "2556d6c34f50e573b2fe046f7c64f782362b3bda1241098772bd360975910a98", + "src/shared/quick-open-filter.ts": "9a09a02764d15622932083a3e550fc7215c066d30a2868f5d548b9105b780d81", + "src/shared/file-listing-cancellation.ts": "c9e1eed636fa2140071fbc2089ce02f738cce725de5069f04bf307fea611b63d", + "src/relay/dispatcher-rpc-routing.ts": "c11b3ff9096bd4877bba0ebc8a70a63092a00015fde3d8f81f3b8fb54087479b", + "src/relay/dispatcher-client-writer.ts": "9ed3ee7ad2758f2d10cd0a0ba06bf77dc3318317b8e24f0eaa6cfe235fd34be4", + "src/shared/text-search-match-accumulator.ts": "1229c755abc94608e211c6e403d529905f577a991871eadfed5eb18821d6e88b", + "src/shared/json-text-structure-limit.ts": "29ded95f7e054d839dc82d118cb324d2144c70267f293d6b54f188159ada5eb9", + "src/shared/text-search-paths.ts": "a977e1b1591b38d7f08652e3d0a64c95b9379e8339ab9d26d6325f7ec90fcda3", + "src/shared/text-search-glob-patterns.ts": "b34072078cf81813e37c37228b830c974dfadb5284ba608538deb56594d64a69", + "src/shared/search-match-count.ts": "6fde43e886fe142c23aff2b80b3f1ebdf613a083051a11e19d7813363db88f62", + "src/shared/string-utils.ts": "fdb48c18f2f7272ed25949eb74beca182c3336d32681191c4b0a4a077db02c22", + "src/shared/file-name-sort.ts": "5aafddf218c453276dcf1afdccf9cd7f82c50160f5df2dd6d1b2779ce0c0cc4a", + "src/shared/clipboard-text.ts": "50f3155063244d6d12ac1493c80029a75df256c031f986f76da64a94152d0c38", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/shared/event-loop-yield.ts": "d291b71f09eb88f24a849d0ae310f946deb7f93b0b6b490b6ea5a605ab7a53cf", + "src/shared/utf8-byte-limits.ts": "01574b287b2d6ab0758112887e356c0d270b25db1b72651925ba8329eb4224e6", + "src/relay/dispatcher-frame-codec.ts": "2728b1e9e8465350c23cede5b64287db77a3d90488d8f8b1e5721bc69d7af7a9", + "src/shared/timer-delay.ts": "7e529ed30d1b25521f5d72d7c2a6d05d16efe66faf2619b859d67b330cca767f", + "src/shared/skill-install-failure.ts": "571ffbaad47304bbdef294e6445d70d179db40495005fdd0ce385491666ec4eb", + "src/shared/terminal-unavailable-cause.ts": "b6ce6d4b5b666ef72d8403883d2cc167f2998c524a17155638c261000c424e91", + "src/relay/dispatcher-writer-sink.ts": "12931e899bd882347b72cd03f3690e8d0e94bf2aa1ce9a560112f52cfdc14405", + "src/relay/dispatcher-writer-lane-scheduler.ts": "8b54b850f8aa88944160b796e3b357fbbc618c782590b4baf6444eda77bd5e5e", + "src/relay/dispatcher-writer-drain-arm.ts": "c9f1582197d0bb9ea30793f0c792a176ada3f25fb79e03474a70d084920d0416", + "src/relay/dispatcher-writer-admission.ts": "12e05ba04de5687c0a0db1e44eddf342cb0c21b9653eef2aeff7b5ccd2039797", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/relay/dispatcher-capacity-signals.ts": "125a94f04f8b3102956231dc800281ed7ee9bb538b2b93f005825c2dd59e5c63", + "src/shared/runtime-capability-degradation.ts": "fc9a5d3814c390c1296ecf72d5ef317b8b8624ea2578e4ceb2b32ee642b13a3f", + "src/relay/dispatcher-client-lifecycle.ts": "6884223ddb2be888cfd437ca1490e14a4c95655c5c831e6812ce4a7876e9c18b", + "src/relay/dispatcher-client-state.ts": "8cdabcb8df6b16b7cc84d8a33f5171cca1807b6d9dea4b374e3c2bb4fc10bfdb", + "src/relay/legacy-relay-publication-ledger.ts": "153926b90d370669d91fd9a59d246f63ed0c01f2aec6e8b7a890a0d0ad7378ba", + "src/relay/client-request-aborts.ts": "a4f458ce767c5315aa01fa9cc8c0a3008dcb445481a401462f14ec3dbbd9e20c" + }, + "mainGraph": { + "src/main/ssh/ssh-filesystem-stream-reader.ts": "333b0ba796edbd0a07b483cc65fb9f0c46f6f5488361bce08c488ea72543825a", + "src/main/ssh/ssh-channel-multiplexer.ts": "9ecd88963fec72901596bd25d2cb4c8666ef41d3c8a968242c6eef52095ee108", + "src/relay/fs-stream-registry.ts": "d723dd0b6419a7937225bdacbc572ef004c49d850cbc616e9168e48f5e63b8fd", + "src/relay/protocol.ts": "faebaded7e8c8b98f021c791b4519d78d58be3879937fe2ed2cef68b22404060", + "src/relay/fs-handler-file-read.ts": "2d70577839cb150ad0977981d54904b1a25bd8b5de7cfe19e018c08c36f41cd2", + "src/relay/dispatcher.ts": "6dca32ec33e410fa9226d6c0a4a548ed06fc5bbf1a65ccdbcae7cc43a9328d18", + "src/relay/relay-frame-decoder.ts": "7044cd142b21f847ee0dc4aee085fe18434b4bcb34ba1eb8bc1fb48a0ed7919e", + "src/main/ssh/ssh-file-stream-read-cap.ts": "83b714af0c87da4c855762ffb827f529302ebc426ab5cde59458206a64cb07b1", + "src/main/ssh/ssh-file-stream-inactivity-deadline.ts": "0554419b22356efa60065b15640d8bf0d8d166bf345e30b492b2305767ea603a", + "src/relay/fs-handler-utils.ts": "12c54b9647ddc9e4ae9924f65aadf0d00e9553050858beae232c02077a94d93a", + "src/main/ssh/relay-protocol.ts": "644aa6f2087b5867d41006bfdcec78ffba693157a2912feb8f82b120f5647b34", + "src/main/ssh/ssh-multiplexer-transport-writer.ts": "433a2cc400b5ed9743871ca03aac06d31a079ef23c42c2a2759f78967312d071", + "src/relay/dispatcher-notification-publication.ts": "d21c8575f5f4694a806b0e05cce4a1595f7116220f48d9068eba7c23aed7a546", + "src/shared/relay-frame-decoder-contract.ts": "f58279099fe4dbbdfde9e3e038a916ae077e71d8ac6aa871b59a64c6fbaeaad8", + "src/shared/relay-frame-decoder.ts": "88c0ecbc06b93efe6733094b8c9e4509ab1cbd139f8cb3257e9fb74a0ed39194", + "src/main/system-power-lifecycle.ts": "cdeb82463a530123be4d42374925a0b349446437c02bff2c287786864515de11", + "src/relay/dispatcher-pty-publication.ts": "2a91aeab16069e4d053c453947c5cd5d24c9e9ac4d36f19d23377689313cc851", + "src/relay/dispatcher-contract.ts": "7c397f9d51c0e3eca604e9075cddb6609a5e114284999009e8a86146e766c081", + "src/relay/fs-handler-list-files.ts": "cca88ec47512840837c84c63baf7ddfc7c216810cb2a5f63fa2babbeef3ff189", + "src/shared/text-search.ts": "c291574e05874440601a6a043b5d065ca172072f4146568d9c9818829abbc185", + "src/shared/image-file-extensions.ts": "8460080a80e2a09fd64466faee1b87df4d8e4803e33bc2f9dd8b3b031a54d536", + "src/shared/ripgrep-process-availability.ts": "2a8e21ae2c9af146f1f226bdfad1e45916cfb2671fb11aa4586d6a337b8cdec2", + "src/shared/search-subprocess-lines.ts": "b860de84a4081fb86515bb31910f0f1d4b0a6606370490c4f60aa9d2e9d5eee3", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts": "3e5fe7a1e3537505baf42869449193fc0448339d851254a49527ec2a1c7cbb50", + "src/shared/relay-frame-buffer.ts": "f89fcd33489894c34f79ba8fd5e5634487555a7c34fc8e7c054229ae1c20e0be", + "src/relay/dispatcher-producer-capacity.ts": "ae201a7e09acbcc013410b92868df1e964f95952f8ab3a462d177f488292965b", + "src/shared/text-search-paths.ts": "a977e1b1591b38d7f08652e3d0a64c95b9379e8339ab9d26d6325f7ec90fcda3", + "src/shared/text-search-glob-patterns.ts": "b34072078cf81813e37c37228b830c974dfadb5284ba608538deb56594d64a69", + "src/shared/string-utils.ts": "fdb48c18f2f7272ed25949eb74beca182c3336d32681191c4b0a4a077db02c22", + "src/shared/text-search-match-accumulator.ts": "1229c755abc94608e211c6e403d529905f577a991871eadfed5eb18821d6e88b", + "src/shared/json-text-structure-limit.ts": "29ded95f7e054d839dc82d118cb324d2144c70267f293d6b54f188159ada5eb9", + "src/shared/search-match-count.ts": "6fde43e886fe142c23aff2b80b3f1ebdf613a083051a11e19d7813363db88f62", + "src/shared/file-listing-cancellation.ts": "c9e1eed636fa2140071fbc2089ce02f738cce725de5069f04bf307fea611b63d", + "src/shared/quick-open-path-search.ts": "2556d6c34f50e573b2fe046f7c64f782362b3bda1241098772bd360975910a98", + "src/shared/quick-open-filter.ts": "9a09a02764d15622932083a3e550fc7215c066d30a2868f5d548b9105b780d81", + "src/relay/dispatcher-producer-transport.ts": "c48087ed5f330311fb438ae3df75ff03bf34df83156f3987b8905598e21780e5", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/shared/file-name-sort.ts": "5aafddf218c453276dcf1afdccf9cd7f82c50160f5df2dd6d1b2779ce0c0cc4a", + "src/shared/clipboard-text.ts": "50f3155063244d6d12ac1493c80029a75df256c031f986f76da64a94152d0c38", + "src/relay/dispatcher-rpc-routing.ts": "c11b3ff9096bd4877bba0ebc8a70a63092a00015fde3d8f81f3b8fb54087479b", + "src/relay/dispatcher-client-writer.ts": "9ed3ee7ad2758f2d10cd0a0ba06bf77dc3318317b8e24f0eaa6cfe235fd34be4", + "src/shared/utf8-byte-limits.ts": "01574b287b2d6ab0758112887e356c0d270b25db1b72651925ba8329eb4224e6", + "src/shared/event-loop-yield.ts": "d291b71f09eb88f24a849d0ae310f946deb7f93b0b6b490b6ea5a605ab7a53cf", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/relay/dispatcher-writer-sink.ts": "12931e899bd882347b72cd03f3690e8d0e94bf2aa1ce9a560112f52cfdc14405", + "src/relay/dispatcher-writer-drain-arm.ts": "c9f1582197d0bb9ea30793f0c792a176ada3f25fb79e03474a70d084920d0416", + "src/relay/dispatcher-writer-lane-scheduler.ts": "8b54b850f8aa88944160b796e3b357fbbc618c782590b4baf6444eda77bd5e5e", + "src/relay/dispatcher-writer-admission.ts": "12e05ba04de5687c0a0db1e44eddf342cb0c21b9653eef2aeff7b5ccd2039797", + "src/shared/timer-delay.ts": "7e529ed30d1b25521f5d72d7c2a6d05d16efe66faf2619b859d67b330cca767f", + "src/shared/skill-install-failure.ts": "571ffbaad47304bbdef294e6445d70d179db40495005fdd0ce385491666ec4eb", + "src/relay/dispatcher-frame-codec.ts": "2728b1e9e8465350c23cede5b64287db77a3d90488d8f8b1e5721bc69d7af7a9", + "src/shared/terminal-unavailable-cause.ts": "b6ce6d4b5b666ef72d8403883d2cc167f2998c524a17155638c261000c424e91", + "src/shared/runtime-capability-degradation.ts": "fc9a5d3814c390c1296ecf72d5ef317b8b8624ea2578e4ceb2b32ee642b13a3f", + "src/relay/dispatcher-capacity-signals.ts": "125a94f04f8b3102956231dc800281ed7ee9bb538b2b93f005825c2dd59e5c63", + "src/relay/dispatcher-client-lifecycle.ts": "6884223ddb2be888cfd437ca1490e14a4c95655c5c831e6812ce4a7876e9c18b", + "src/relay/dispatcher-client-state.ts": "8cdabcb8df6b16b7cc84d8a33f5171cca1807b6d9dea4b374e3c2bb4fc10bfdb", + "src/relay/legacy-relay-publication-ledger.ts": "153926b90d370669d91fd9a59d246f63ed0c01f2aec6e8b7a890a0d0ad7378ba", + "src/relay/client-request-aborts.ts": "a4f458ce767c5315aa01fa9cc8c0a3008dcb445481a401462f14ec3dbbd9e20c" + }, + "callerHashes": { + "src/main/providers/ssh-filesystem-provider.ts": "de6051f43ea272fe0a15b7ef5c6df5c8caedfa7adaada205386c4f19be7f04f2", + "src/main/ipc/filesystem/filesystem-read-handlers.ts": "cf012e6a7049f803936c75a32619cf1f209053d4b8951f67f7e4e39218448a6a", + "src/main/runtime/runtime-file-commands-mobile-file-list-limit.ts": "27c4b20397471fe036f8fdfe0f76f089bf6bbf9e6d4ae9f23b41bc49359bc8bc", + "src/main/ai-vault/remote-session-scan-concurrency.ts": "1b7147a2b5d793d7f78059c2ae67c41f531ebfa37a9bd292091401d21143e36a", + "src/relay/fs-handler.ts": "bc8c57bdf91e5d34b2fb42d8fd00260873224c7b04d69e5aafae149a377c4235" + }, + "mainRef": "291b4ddd6f1c1af480169885e0fda7f9c78ff053", + "mainOriginalSourceHashes": { + "src/main/ssh/ssh-filesystem-stream-reader.ts": "b8586de1412df98428939a0a4870c5f8ccab22f506a188b2eb1c7a38c317b1ef", + "src/main/ssh/ssh-channel-multiplexer.ts": "9ecd88963fec72901596bd25d2cb4c8666ef41d3c8a968242c6eef52095ee108", + "src/relay/fs-stream-registry.ts": "d723dd0b6419a7937225bdacbc572ef004c49d850cbc616e9168e48f5e63b8fd", + "src/relay/protocol.ts": "faebaded7e8c8b98f021c791b4519d78d58be3879937fe2ed2cef68b22404060", + "src/relay/fs-handler-file-read.ts": "2d70577839cb150ad0977981d54904b1a25bd8b5de7cfe19e018c08c36f41cd2", + "src/relay/dispatcher.ts": "6dca32ec33e410fa9226d6c0a4a548ed06fc5bbf1a65ccdbcae7cc43a9328d18", + "src/relay/relay-frame-decoder.ts": "7044cd142b21f847ee0dc4aee085fe18434b4bcb34ba1eb8bc1fb48a0ed7919e", + "src/main/ssh/ssh-file-stream-read-cap.ts": "83b714af0c87da4c855762ffb827f529302ebc426ab5cde59458206a64cb07b1", + "src/main/ssh/ssh-file-stream-inactivity-deadline.ts": "0554419b22356efa60065b15640d8bf0d8d166bf345e30b492b2305767ea603a", + "src/relay/fs-handler-utils.ts": "12c54b9647ddc9e4ae9924f65aadf0d00e9553050858beae232c02077a94d93a", + "src/main/ssh/relay-protocol.ts": "644aa6f2087b5867d41006bfdcec78ffba693157a2912feb8f82b120f5647b34", + "src/main/ssh/ssh-multiplexer-transport-writer.ts": "433a2cc400b5ed9743871ca03aac06d31a079ef23c42c2a2759f78967312d071", + "src/relay/dispatcher-notification-publication.ts": "d21c8575f5f4694a806b0e05cce4a1595f7116220f48d9068eba7c23aed7a546", + "src/shared/relay-frame-decoder-contract.ts": "f58279099fe4dbbdfde9e3e038a916ae077e71d8ac6aa871b59a64c6fbaeaad8", + "src/shared/relay-frame-decoder.ts": "88c0ecbc06b93efe6733094b8c9e4509ab1cbd139f8cb3257e9fb74a0ed39194", + "src/main/system-power-lifecycle.ts": "cdeb82463a530123be4d42374925a0b349446437c02bff2c287786864515de11", + "src/relay/dispatcher-pty-publication.ts": "2a91aeab16069e4d053c453947c5cd5d24c9e9ac4d36f19d23377689313cc851", + "src/relay/dispatcher-contract.ts": "7c397f9d51c0e3eca604e9075cddb6609a5e114284999009e8a86146e766c081", + "src/relay/fs-handler-list-files.ts": "cca88ec47512840837c84c63baf7ddfc7c216810cb2a5f63fa2babbeef3ff189", + "src/shared/text-search.ts": "c291574e05874440601a6a043b5d065ca172072f4146568d9c9818829abbc185", + "src/shared/image-file-extensions.ts": "8460080a80e2a09fd64466faee1b87df4d8e4803e33bc2f9dd8b3b031a54d536", + "src/shared/ripgrep-process-availability.ts": "2a8e21ae2c9af146f1f226bdfad1e45916cfb2671fb11aa4586d6a337b8cdec2", + "src/shared/search-subprocess-lines.ts": "b860de84a4081fb86515bb31910f0f1d4b0a6606370490c4f60aa9d2e9d5eee3", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts": "3e5fe7a1e3537505baf42869449193fc0448339d851254a49527ec2a1c7cbb50", + "src/shared/relay-frame-buffer.ts": "f89fcd33489894c34f79ba8fd5e5634487555a7c34fc8e7c054229ae1c20e0be", + "src/relay/dispatcher-producer-capacity.ts": "ae201a7e09acbcc013410b92868df1e964f95952f8ab3a462d177f488292965b", + "src/shared/text-search-paths.ts": "a977e1b1591b38d7f08652e3d0a64c95b9379e8339ab9d26d6325f7ec90fcda3", + "src/shared/text-search-glob-patterns.ts": "b34072078cf81813e37c37228b830c974dfadb5284ba608538deb56594d64a69", + "src/shared/string-utils.ts": "fdb48c18f2f7272ed25949eb74beca182c3336d32681191c4b0a4a077db02c22", + "src/shared/text-search-match-accumulator.ts": "1229c755abc94608e211c6e403d529905f577a991871eadfed5eb18821d6e88b", + "src/shared/json-text-structure-limit.ts": "29ded95f7e054d839dc82d118cb324d2144c70267f293d6b54f188159ada5eb9", + "src/shared/search-match-count.ts": "6fde43e886fe142c23aff2b80b3f1ebdf613a083051a11e19d7813363db88f62", + "src/shared/file-listing-cancellation.ts": "c9e1eed636fa2140071fbc2089ce02f738cce725de5069f04bf307fea611b63d", + "src/shared/quick-open-path-search.ts": "2556d6c34f50e573b2fe046f7c64f782362b3bda1241098772bd360975910a98", + "src/shared/quick-open-filter.ts": "9a09a02764d15622932083a3e550fc7215c066d30a2868f5d548b9105b780d81", + "src/relay/dispatcher-producer-transport.ts": "c48087ed5f330311fb438ae3df75ff03bf34df83156f3987b8905598e21780e5", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/shared/file-name-sort.ts": "5aafddf218c453276dcf1afdccf9cd7f82c50160f5df2dd6d1b2779ce0c0cc4a", + "src/shared/clipboard-text.ts": "50f3155063244d6d12ac1493c80029a75df256c031f986f76da64a94152d0c38", + "src/relay/dispatcher-rpc-routing.ts": "c11b3ff9096bd4877bba0ebc8a70a63092a00015fde3d8f81f3b8fb54087479b", + "src/relay/dispatcher-client-writer.ts": "9ed3ee7ad2758f2d10cd0a0ba06bf77dc3318317b8e24f0eaa6cfe235fd34be4", + "src/shared/utf8-byte-limits.ts": "01574b287b2d6ab0758112887e356c0d270b25db1b72651925ba8329eb4224e6", + "src/shared/event-loop-yield.ts": "d291b71f09eb88f24a849d0ae310f946deb7f93b0b6b490b6ea5a605ab7a53cf", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/relay/dispatcher-writer-sink.ts": "12931e899bd882347b72cd03f3690e8d0e94bf2aa1ce9a560112f52cfdc14405", + "src/relay/dispatcher-writer-drain-arm.ts": "c9f1582197d0bb9ea30793f0c792a176ada3f25fb79e03474a70d084920d0416", + "src/relay/dispatcher-writer-lane-scheduler.ts": "8b54b850f8aa88944160b796e3b357fbbc618c782590b4baf6444eda77bd5e5e", + "src/relay/dispatcher-writer-admission.ts": "12e05ba04de5687c0a0db1e44eddf342cb0c21b9653eef2aeff7b5ccd2039797", + "src/shared/timer-delay.ts": "7e529ed30d1b25521f5d72d7c2a6d05d16efe66faf2619b859d67b330cca767f", + "src/shared/skill-install-failure.ts": "571ffbaad47304bbdef294e6445d70d179db40495005fdd0ce385491666ec4eb", + "src/relay/dispatcher-frame-codec.ts": "2728b1e9e8465350c23cede5b64287db77a3d90488d8f8b1e5721bc69d7af7a9", + "src/shared/terminal-unavailable-cause.ts": "b6ce6d4b5b666ef72d8403883d2cc167f2998c524a17155638c261000c424e91", + "src/shared/runtime-capability-degradation.ts": "fc9a5d3814c390c1296ecf72d5ef317b8b8624ea2578e4ceb2b32ee642b13a3f", + "src/relay/dispatcher-capacity-signals.ts": "125a94f04f8b3102956231dc800281ed7ee9bb538b2b93f005825c2dd59e5c63", + "src/relay/dispatcher-client-lifecycle.ts": "6884223ddb2be888cfd437ca1490e14a4c95655c5c831e6812ce4a7876e9c18b", + "src/relay/dispatcher-client-state.ts": "8cdabcb8df6b16b7cc84d8a33f5171cca1807b6d9dea4b374e3c2bb4fc10bfdb", + "src/relay/legacy-relay-publication-ledger.ts": "153926b90d370669d91fd9a59d246f63ed0c01f2aec6e8b7a890a0d0ad7378ba", + "src/relay/client-request-aborts.ts": "a4f458ce767c5315aa01fa9cc8c0a3008dcb445481a401462f14ec3dbbd9e20c" + }, + "reportedVersionComparison": { + "commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43", + "sourceHashes": { + "src/main/ssh/ssh-filesystem-stream-reader.ts": "b8586de1412df98428939a0a4870c5f8ccab22f506a188b2eb1c7a38c317b1ef", + "src/main/ssh/ssh-channel-multiplexer.ts": "480c722b27dd1ffb8c70bfca8fb3568294ff2777b7b02607548df93bb280f6ae", + "src/main/ssh/ssh-file-stream-inactivity-deadline.ts": "0554419b22356efa60065b15640d8bf0d8d166bf345e30b492b2305767ea603a", + "src/main/ssh/ssh-file-stream-read-cap.ts": "83b714af0c87da4c855762ffb827f529302ebc426ab5cde59458206a64cb07b1", + "src/main/ssh/ssh-multiplexer-transport-writer.ts": "4a73e2194f15ec0604802fe6810742f34930fc55e542458b6d8ab7344ee841a2", + "src/shared/relay-frame-decoder.ts": "88c0ecbc06b93efe6733094b8c9e4509ab1cbd139f8cb3257e9fb74a0ed39194", + "src/main/providers/ssh-filesystem-provider.ts": "03d53c8be02737024f5c5f4f08d614a276a8d03f0e599c331be9d0eaa85c2d80", + "src/main/ipc/filesystem/filesystem-read-handlers.ts": "2491d39f0576a961a249ebbf94e563f2eefe7a0cf899bbb6bc77b7a9e7a2aa30", + "src/main/runtime/runtime-file-commands-mobile-file-list-limit.ts": "27c4b20397471fe036f8fdfe0f76f089bf6bbf9e6d4ae9f23b41bc49359bc8bc", + "src/main/ai-vault/remote-session-scan-concurrency.ts": "175944c836a683a41c7d6446d45794eb9e0dd021414926370c64bc5fd574ad34", + "src/relay/fs-handler.ts": "b4f4121c3081b8c98749c4d1cb45fd3355f0c053c9cc078f539cb71210976533", + "src/relay/fs-handler-file-read.ts": "2d70577839cb150ad0977981d54904b1a25bd8b5de7cfe19e018c08c36f41cd2", + "src/relay/fs-stream-registry.ts": "d723dd0b6419a7937225bdacbc572ef004c49d850cbc616e9168e48f5e63b8fd", + "src/relay/dispatcher.ts": "6dca32ec33e410fa9226d6c0a4a548ed06fc5bbf1a65ccdbcae7cc43a9328d18", + "src/relay/dispatcher-rpc-routing.ts": "c11b3ff9096bd4877bba0ebc8a70a63092a00015fde3d8f81f3b8fb54087479b", + "src/relay/dispatcher-client-lifecycle.ts": "6884223ddb2be888cfd437ca1490e14a4c95655c5c831e6812ce4a7876e9c18b", + "src/relay/dispatcher-notification-publication.ts": "d21c8575f5f4694a806b0e05cce4a1595f7116220f48d9068eba7c23aed7a546", + "src/relay/dispatcher-client-writer.ts": "9ed3ee7ad2758f2d10cd0a0ba06bf77dc3318317b8e24f0eaa6cfe235fd34be4", + "src/relay/dispatcher-writer-lane-scheduler.ts": "8b54b850f8aa88944160b796e3b357fbbc618c782590b4baf6444eda77bd5e5e", + "src/relay/dispatcher-writer-sink.ts": "12931e899bd882347b72cd03f3690e8d0e94bf2aa1ce9a560112f52cfdc14405", + "src/relay/dispatcher-writer-admission.ts": "12e05ba04de5687c0a0db1e44eddf342cb0c21b9653eef2aeff7b5ccd2039797", + "src/relay/protocol.ts": "678f9d6dac998385ca10e94da9864fe3451c82fcc88b9c28490dfb42e99606a4", + "src/main/ssh/relay-protocol.ts": "644aa6f2087b5867d41006bfdcec78ffba693157a2912feb8f82b120f5647b34" + } + } +} diff --git a/docs/audits/ssh-file-metadata-retention/sources.cjs b/docs/audits/ssh-file-metadata-retention/sources.cjs new file mode 100644 index 00000000000..08a2f0424ed --- /dev/null +++ b/docs/audits/ssh-file-metadata-retention/sources.cjs @@ -0,0 +1,84 @@ +const assert = require('node:assert/strict') +const { readFileSync } = require('node:fs') +const { createHash } = require('node:crypto') +const path = require('node:path') +const { applyPatch, parsePatch, reversePatch } = require('diff') +const root = path.resolve(__dirname, '../../..') +const canonicalLf = (text) => text.replaceAll('\r\n', '\n') +const sha256 = (text) => createHash('sha256').update(text).digest('hex') +const readText = (filename) => canonicalLf(readFileSync(filename, 'utf8')) +const versions = JSON.parse(readText(path.join(__dirname, 'source-versions.json'))) + +function checkedPatch(name, expectedPath, read) { + const patches = parsePatch(canonicalLf(read(path.join(__dirname, name)))) + assert.equal(patches.length, 1) + assert.equal(patches[0].newFileName, `b/${expectedPath}`) + assert.equal(patches[0].oldFileName, `a/${expectedPath}`) + return patches[0] +} + +function observePending(source) { + return source.replace( + ' const pending: PendingFrame[] = []', + ' const pending: PendingFrame[] = []; globalThis.__sshPendingReaders.set(filePath, new WeakRef(pending))' + ) +} + +function loadSources({ + graph = process.env.ORCA_SSH_READER_GRAPH ?? 'worktree', + variant = process.env.ORCA_SSH_READER_VARIANT ?? 'fixed', + read = readText +} = {}) { + assert.ok(graph === 'worktree' || graph === 'main') + assert.ok(variant === 'before' || variant === 'fixed') + const targetPatch = checkedPatch('fix.patch', versions.sourcePath, read) + const contextPatch = checkedPatch('main-context.patch', versions.contextPath, read) + const sources = new Map() + const hashes = {} + const selected = graph === 'main' ? versions.mainGraph : versions.worktreeGraph + for (const [relative, expected] of Object.entries(selected)) { + const filename = path.join(root, relative) + let text = canonicalLf(read(filename)) + if (relative === versions.sourcePath) { + assert.equal(sha256(text), versions.fixedSha256, 'Fixed reader drift') + if (variant === 'before') { + text = applyPatch(text, reversePatch(targetPatch)) + assert.notEqual(text, false, 'Reader patch no longer reverses') + assert.equal(sha256(text), versions.baselineSha256) + } + } else if (relative === versions.contextPath) { + const actual = sha256(text) + assert.ok( + actual === versions.worktreeGraph[relative] || actual === versions.mainGraph[relative], + 'Unaudited writer context' + ) + if (actual !== expected) { + text = applyPatch(text, graph === 'main' ? contextPatch : reversePatch(contextPatch)) + assert.notEqual(text, false, 'Writer context no longer reconstructs') + } + assert.equal(sha256(text), expected) + } else { + assert.equal(sha256(text), expected, `Graph source drift: ${relative}`) + } + hashes[relative] = sha256(text) + sources.set(filename, text) + } + for (const [relative, expected] of Object.entries(versions.callerHashes)) { + assert.equal( + sha256(canonicalLf(read(path.join(root, relative)))), + expected, + `Caller drift: ${relative}` + ) + } + const reader = sources.get(path.join(root, versions.sourcePath)) + return { + root, + sources, + hashes, + observedReaderSha256: sha256(observePending(reader)), + graph, + variant + } +} + +module.exports = { loadSources, observePending, readText, versions } diff --git a/docs/audits/ssh-file-metadata-retention/validation.json b/docs/audits/ssh-file-metadata-retention/validation.json new file mode 100644 index 00000000000..862ccc5d497 --- /dev/null +++ b/docs/audits/ssh-file-metadata-retention/validation.json @@ -0,0 +1,264 @@ +{ + "productTests": { + "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config config/vitest.config.ts src/main/ssh/ssh-filesystem-stream-retention.test.ts src/main/providers/ssh-filesystem-provider-stream.test.ts src/main/providers/ssh-filesystem-provider.test.ts src/main/ssh/ssh-channel-multiplexer.test.ts src/relay/fs-handler-stream.test.ts", + "passed": 124, + "files": 5, + "newTests": 9, + "exitCode": 0 + }, + "baselineOverlay": { + "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config docs/audits/ssh-file-metadata-retention/before.config.mjs src/main/ssh/ssh-filesystem-stream-retention.test.ts src/main/providers/ssh-filesystem-provider-stream.test.ts", + "passed": 22, + "expectedFailed": 1, + "failure": "64 foreign frame params objects remain reachable before this reader receives metadata.", + "exitCode": 1 + }, + "transportIntegration": { + "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config config/vitest.config.ts src/relay/fs-stream-pty-echo-backpressure.integration.test.ts", + "passed": 3, + "exitCode": 0 + }, + "typecheck": { + "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm tc:node", + "exitCode": 0 + }, + "portableProof": { + "cases": 80, + "reports": [ + { + "file": "worktree-before-node-results.json", + "controls": 10, + "runtime": { + "node": "26.6.0", + "electron": null + }, + "heapDelta": 45478552 + }, + { + "file": "worktree-before-electron-results.json", + "controls": 10, + "runtime": { + "node": "24.21.0", + "electron": "43.7.0" + }, + "heapDelta": 45298880 + }, + { + "file": "worktree-fixed-node-results.json", + "controls": 10, + "runtime": { + "node": "26.6.0", + "electron": null + }, + "heapDelta": -663256 + }, + { + "file": "worktree-fixed-electron-results.json", + "controls": 10, + "runtime": { + "node": "24.21.0", + "electron": "43.7.0" + }, + "heapDelta": 520252 + }, + { + "file": "main-before-node-results.json", + "controls": 10, + "runtime": { + "node": "26.6.0", + "electron": null + }, + "heapDelta": 45477336 + }, + { + "file": "main-before-electron-results.json", + "controls": 10, + "runtime": { + "node": "24.21.0", + "electron": "43.7.0" + }, + "heapDelta": 45298640 + }, + { + "file": "main-fixed-node-results.json", + "controls": 10, + "runtime": { + "node": "26.6.0", + "electron": null + }, + "heapDelta": -669752 + }, + { + "file": "main-fixed-electron-results.json", + "controls": 10, + "runtime": { + "node": "24.21.0", + "electron": "43.7.0" + }, + "heapDelta": 513692 + } + ], + "sourceGraphModules": 59, + "additionalCallerSources": 5, + "canonicalCrLfReads": 66 + }, + "quality": [ + { + "label": "ordinary lint", + "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm exec oxlint --no-ignore --deny-warnings src/main/ssh/ssh-filesystem-stream-reader.ts src/main/ssh/ssh-filesystem-stream-retention.test.ts src/main/providers/ssh-filesystem-provider-stream.test.ts docs/audits/ssh-file-metadata-retention/sources.cjs docs/audits/ssh-file-metadata-retention/relay-fixture.mjs docs/audits/ssh-file-metadata-retention/scenario.test.mjs docs/audits/ssh-file-metadata-retention/vitest.config.mjs docs/audits/ssh-file-metadata-retention/before.config.mjs", + "exitCode": 0 + }, + { + "label": "casting", + "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm exec oxlint --no-ignore --deny-warnings --config config/oxlint-code-quality-casting.json src/main/ssh/ssh-filesystem-stream-reader.ts src/main/ssh/ssh-filesystem-stream-retention.test.ts src/main/providers/ssh-filesystem-provider-stream.test.ts docs/audits/ssh-file-metadata-retention/sources.cjs docs/audits/ssh-file-metadata-retention/relay-fixture.mjs docs/audits/ssh-file-metadata-retention/scenario.test.mjs docs/audits/ssh-file-metadata-retention/vitest.config.mjs docs/audits/ssh-file-metadata-retention/before.config.mjs", + "exitCode": 1 + }, + { + "label": "type-aware", + "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm exec oxlint --no-ignore --deny-warnings --type-aware --config config/oxlint-code-quality-type-aware.json src/main/ssh/ssh-filesystem-stream-reader.ts src/main/ssh/ssh-filesystem-stream-retention.test.ts src/main/providers/ssh-filesystem-provider-stream.test.ts docs/audits/ssh-file-metadata-retention/sources.cjs docs/audits/ssh-file-metadata-retention/relay-fixture.mjs docs/audits/ssh-file-metadata-retention/scenario.test.mjs docs/audits/ssh-file-metadata-retention/vitest.config.mjs docs/audits/ssh-file-metadata-retention/before.config.mjs", + "exitCode": 0 + }, + { + "label": "native code quality", + "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm exec oxlint --no-ignore --deny-warnings --config config/oxlint-code-quality-native-plugins.json src/main/ssh/ssh-filesystem-stream-reader.ts src/main/ssh/ssh-filesystem-stream-retention.test.ts src/main/providers/ssh-filesystem-provider-stream.test.ts docs/audits/ssh-file-metadata-retention/sources.cjs docs/audits/ssh-file-metadata-retention/relay-fixture.mjs docs/audits/ssh-file-metadata-retention/scenario.test.mjs docs/audits/ssh-file-metadata-retention/vitest.config.mjs docs/audits/ssh-file-metadata-retention/before.config.mjs", + "exitCode": 0 + }, + { + "label": "anti-slop", + "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm exec oxlint --no-ignore --deny-warnings --config config/oxlint-anti-slop.json src/main/ssh/ssh-filesystem-stream-reader.ts src/main/ssh/ssh-filesystem-stream-retention.test.ts src/main/providers/ssh-filesystem-provider-stream.test.ts docs/audits/ssh-file-metadata-retention/sources.cjs docs/audits/ssh-file-metadata-retention/relay-fixture.mjs docs/audits/ssh-file-metadata-retention/scenario.test.mjs docs/audits/ssh-file-metadata-retention/vitest.config.mjs docs/audits/ssh-file-metadata-retention/before.config.mjs", + "exitCode": 0 + } + ], + "inheritedCasting": { + "baselineFindings": 15, + "currentFindings": 15, + "sameRuleAndExactAssertionSpans": true, + "findings": [ + { + "path": "src/main/providers/ssh-filesystem-provider-stream.test.ts", + "rule": "typescript(consistent-type-assertions)", + "spans": ["mux as never"] + }, + { + "path": "src/main/providers/ssh-filesystem-provider-stream.test.ts", + "rule": "typescript(consistent-type-assertions)", + "spans": ["new Error('Method not found') as Error & { code: number }"] + }, + { + "path": "src/main/ssh/ssh-filesystem-stream-reader.ts", + "rule": "typescript(consistent-type-assertions)", + "spans": ["err as Error"] + }, + { + "path": "src/main/ssh/ssh-filesystem-stream-reader.ts", + "rule": "typescript(consistent-type-assertions)", + "spans": ["err as Error"] + }, + { + "path": "src/main/ssh/ssh-filesystem-stream-reader.ts", + "rule": "typescript(consistent-type-assertions)", + "spans": ["err as { code?: unknown }"] + }, + { + "path": "src/main/ssh/ssh-filesystem-stream-reader.ts", + "rule": "typescript(consistent-type-assertions)", + "spans": ["new Error(message) as Error & { code: string }"] + }, + { + "path": "src/main/ssh/ssh-filesystem-stream-reader.ts", + "rule": "typescript(consistent-type-assertions)", + "spans": ["new Error(message) as Error & { code: string }"] + }, + { + "path": "src/main/ssh/ssh-filesystem-stream-reader.ts", + "rule": "typescript(consistent-type-assertions)", + "spans": ["params.code as string | undefined"] + }, + { + "path": "src/main/ssh/ssh-filesystem-stream-reader.ts", + "rule": "typescript(consistent-type-assertions)", + "spans": ["params.data as string"] + }, + { + "path": "src/main/ssh/ssh-filesystem-stream-reader.ts", + "rule": "typescript(consistent-type-assertions)", + "spans": ["params.message as string | undefined"] + }, + { + "path": "src/main/ssh/ssh-filesystem-stream-reader.ts", + "rule": "typescript(consistent-type-assertions)", + "spans": ["params.seq as number"] + }, + { + "path": "src/main/ssh/ssh-filesystem-stream-reader.ts", + "rule": "typescript(consistent-type-assertions)", + "spans": ["params.streamId as number | undefined"] + }, + { + "path": "src/main/ssh/ssh-filesystem-stream-reader.ts", + "rule": "typescript(consistent-type-assertions)", + "spans": ["params.streamId as number | undefined"] + }, + { + "path": "src/main/ssh/ssh-filesystem-stream-reader.ts", + "rule": "typescript(consistent-type-assertions)", + "spans": ["params.streamId as number | undefined"] + }, + { + "path": "src/main/ssh/ssh-filesystem-stream-reader.ts", + "rule": "typescript(consistent-type-assertions)", + "spans": ["rawMetadata as StreamMetadataResponse"] + } + ], + "baselineSourceHashes": { + "src/main/providers/ssh-filesystem-provider-stream.test.ts": "422bcaa7293925217f9c61197599f26e9f0f0993cb562ec004a48a1b27d43fa3", + "src/main/ssh/ssh-filesystem-stream-reader.ts": "b8586de1412df98428939a0a4870c5f8ccab22f506a188b2eb1c7a38c317b1ef" + } + }, + "changedCodeGate": { + "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm run check:code-quality:changed", + "exitCode": 0, + "newFindings": 0, + "observedGlobalChangedFiles": 392, + "baseline": "2fccacadbe23", + "scope": "Primary worktree including concurrent global evidence changes" + }, + "whitespace": { + "fullContentsChecked": true, + "zeroContextPatches": true, + "diagnostics": 0 + }, + "formatting": { + "idempotent": true + }, + "emptyFileFixtureCiCorrection": { + "failedHead": "458e11e9f3f5981a85fc1006083c19738a8b26e3", + "jobUrl": "https://github.com/stablyai/orca/actions/runs/35186075669/job/105088389138", + "cause": "General provider test double resolved empty metadata without invoking the synchronous beforeResolve callback. Real-mux empty metadata cases already passed.", + "correction": "Move the existing empty-file control to the existing streaming fixture, which models beforeResolve; also assert all stream/disposal listeners are released. No product change.", + "localCounterexample": { + "failed": 1, + "skipped": 53, + "timeoutMs": 1000, + "logSha256": "a53af8ea05b06fabf918ea12f5c81f635dbfec97709688ce1139f9efc619a438" + }, + "fixedFiveSuites": { + "passed": 124, + "exitCode": 0, + "logSha256": "bab1beb5ed50ff0611b67eda8a96b1176a86442baa50abc07516402e987e31db" + }, + "baselineOverlay": { + "expectedFailed": 1, + "passed": 22, + "exitCode": 1, + "logSha256": "044aaae5fd3943263616d32eafafbfbb0fc4fb742f754feb233a8e6b49160b6b" + }, + "otherCiFailure": { + "jobUrl": "https://github.com/stablyai/orca/actions/runs/35186075669/job/105088388326", + "path": "src/main/windows/windows-pty-job.win32.test.ts", + "failure": "ConPTY job ownership: grandchild never reported its pid", + "mainAndPublishedBlob": "c5f408f73115a11821b06fafe04ca28eaa15acb7", + "scope": "Untouched Windows native test; missing PID cause not established. No retry or timing-threshold change." + } + } +} diff --git a/docs/audits/ssh-file-metadata-retention/vitest.config.mjs b/docs/audits/ssh-file-metadata-retention/vitest.config.mjs new file mode 100644 index 00000000000..b1f6975e0df --- /dev/null +++ b/docs/audits/ssh-file-metadata-retention/vitest.config.mjs @@ -0,0 +1,31 @@ +import { resolve, sep } from 'node:path' +import { createRequire } from 'node:module' +import base from '../../../config/vitest.config.ts' +const { loadSources, observePending } = createRequire(import.meta.url)('./sources.cjs') +const loaded = loadSources() +export default { + ...base, + test: { + ...base.test, + setupFiles: [], + include: ['docs/audits/ssh-file-metadata-retention/scenario.test.mjs'], + maxWorkers: 1 + }, + plugins: [ + { + name: 'ssh-file-metadata-source-graph', + enforce: 'pre', + transform(_source, id) { + const absolute = resolve(id.split('?')[0]) + const source = loaded.sources.get(absolute) + if (source !== undefined) { + return { code: observePending(source), map: null } + } + if (absolute.startsWith(resolve(loaded.root, 'src') + sep) && absolute.endsWith('.ts')) { + throw new Error(`Unreviewed source import: ${absolute}`) + } + return null + } + } + ] +} diff --git a/docs/audits/ssh-file-metadata-retention/worktree-before-electron-results.json b/docs/audits/ssh-file-metadata-retention/worktree-before-electron-results.json new file mode 100644 index 00000000000..1bec84156d8 --- /dev/null +++ b/docs/audits/ssh-file-metadata-retention/worktree-before-electron-results.json @@ -0,0 +1,140 @@ +{ + "variant": "before", + "graph": "worktree", + "runtime": { + "node": "24.21.0", + "electron": "43.7.0" + }, + "sources": { + "src/relay/dispatcher.ts": "6dca32ec33e410fa9226d6c0a4a548ed06fc5bbf1a65ccdbcae7cc43a9328d18", + "src/main/ssh/ssh-channel-multiplexer.ts": "9ecd88963fec72901596bd25d2cb4c8666ef41d3c8a968242c6eef52095ee108", + "src/main/ssh/ssh-filesystem-stream-reader.ts": "b8586de1412df98428939a0a4870c5f8ccab22f506a188b2eb1c7a38c317b1ef", + "src/relay/fs-handler-file-read.ts": "2d70577839cb150ad0977981d54904b1a25bd8b5de7cfe19e018c08c36f41cd2", + "src/relay/protocol.ts": "faebaded7e8c8b98f021c791b4519d78d58be3879937fe2ed2cef68b22404060", + "src/relay/fs-stream-registry.ts": "d723dd0b6419a7937225bdacbc572ef004c49d850cbc616e9168e48f5e63b8fd", + "src/relay/dispatcher-notification-publication.ts": "d21c8575f5f4694a806b0e05cce4a1595f7116220f48d9068eba7c23aed7a546", + "src/relay/fs-handler-utils.ts": "12c54b9647ddc9e4ae9924f65aadf0d00e9553050858beae232c02077a94d93a", + "src/relay/relay-frame-decoder.ts": "7044cd142b21f847ee0dc4aee085fe18434b4bcb34ba1eb8bc1fb48a0ed7919e", + "src/main/ssh/ssh-file-stream-inactivity-deadline.ts": "0554419b22356efa60065b15640d8bf0d8d166bf345e30b492b2305767ea603a", + "src/main/ssh/ssh-file-stream-read-cap.ts": "83b714af0c87da4c855762ffb827f529302ebc426ab5cde59458206a64cb07b1", + "src/main/ssh/relay-protocol.ts": "644aa6f2087b5867d41006bfdcec78ffba693157a2912feb8f82b120f5647b34", + "src/main/ssh/ssh-multiplexer-transport-writer.ts": "433a2cc400b5ed9743871ca03aac06d31a079ef23c42c2a2759f78967312d071", + "src/shared/relay-frame-decoder-contract.ts": "f58279099fe4dbbdfde9e3e038a916ae077e71d8ac6aa871b59a64c6fbaeaad8", + "src/relay/dispatcher-pty-publication.ts": "2a91aeab16069e4d053c453947c5cd5d24c9e9ac4d36f19d23377689313cc851", + "src/main/system-power-lifecycle.ts": "cdeb82463a530123be4d42374925a0b349446437c02bff2c287786864515de11", + "src/relay/dispatcher-contract.ts": "7c397f9d51c0e3eca604e9075cddb6609a5e114284999009e8a86146e766c081", + "src/shared/relay-frame-decoder.ts": "88c0ecbc06b93efe6733094b8c9e4509ab1cbd139f8cb3257e9fb74a0ed39194", + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts": "6173ddd6640a930d0fa586fed91c1adc0d865a33f81af9a1ed5a0817c08e90cc", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/shared/search-subprocess-lines.ts": "b860de84a4081fb86515bb31910f0f1d4b0a6606370490c4f60aa9d2e9d5eee3", + "src/relay/fs-handler-list-files.ts": "cca88ec47512840837c84c63baf7ddfc7c216810cb2a5f63fa2babbeef3ff189", + "src/shared/text-search.ts": "c291574e05874440601a6a043b5d065ca172072f4146568d9c9818829abbc185", + "src/shared/ripgrep-process-availability.ts": "2a8e21ae2c9af146f1f226bdfad1e45916cfb2671fb11aa4586d6a337b8cdec2", + "src/shared/image-file-extensions.ts": "8460080a80e2a09fd64466faee1b87df4d8e4803e33bc2f9dd8b3b031a54d536", + "src/relay/dispatcher-producer-capacity.ts": "ae201a7e09acbcc013410b92868df1e964f95952f8ab3a462d177f488292965b", + "src/relay/dispatcher-producer-transport.ts": "c48087ed5f330311fb438ae3df75ff03bf34df83156f3987b8905598e21780e5", + "src/shared/relay-frame-buffer.ts": "f89fcd33489894c34f79ba8fd5e5634487555a7c34fc8e7c054229ae1c20e0be", + "src/shared/quick-open-path-search.ts": "2556d6c34f50e573b2fe046f7c64f782362b3bda1241098772bd360975910a98", + "src/shared/quick-open-filter.ts": "9a09a02764d15622932083a3e550fc7215c066d30a2868f5d548b9105b780d81", + "src/shared/file-listing-cancellation.ts": "c9e1eed636fa2140071fbc2089ce02f738cce725de5069f04bf307fea611b63d", + "src/relay/dispatcher-rpc-routing.ts": "c11b3ff9096bd4877bba0ebc8a70a63092a00015fde3d8f81f3b8fb54087479b", + "src/relay/dispatcher-client-writer.ts": "9ed3ee7ad2758f2d10cd0a0ba06bf77dc3318317b8e24f0eaa6cfe235fd34be4", + "src/shared/text-search-match-accumulator.ts": "1229c755abc94608e211c6e403d529905f577a991871eadfed5eb18821d6e88b", + "src/shared/json-text-structure-limit.ts": "29ded95f7e054d839dc82d118cb324d2144c70267f293d6b54f188159ada5eb9", + "src/shared/text-search-paths.ts": "a977e1b1591b38d7f08652e3d0a64c95b9379e8339ab9d26d6325f7ec90fcda3", + "src/shared/text-search-glob-patterns.ts": "b34072078cf81813e37c37228b830c974dfadb5284ba608538deb56594d64a69", + "src/shared/search-match-count.ts": "6fde43e886fe142c23aff2b80b3f1ebdf613a083051a11e19d7813363db88f62", + "src/shared/string-utils.ts": "fdb48c18f2f7272ed25949eb74beca182c3336d32681191c4b0a4a077db02c22", + "src/shared/file-name-sort.ts": "5aafddf218c453276dcf1afdccf9cd7f82c50160f5df2dd6d1b2779ce0c0cc4a", + "src/shared/clipboard-text.ts": "50f3155063244d6d12ac1493c80029a75df256c031f986f76da64a94152d0c38", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/shared/event-loop-yield.ts": "d291b71f09eb88f24a849d0ae310f946deb7f93b0b6b490b6ea5a605ab7a53cf", + "src/shared/utf8-byte-limits.ts": "01574b287b2d6ab0758112887e356c0d270b25db1b72651925ba8329eb4224e6", + "src/relay/dispatcher-frame-codec.ts": "2728b1e9e8465350c23cede5b64287db77a3d90488d8f8b1e5721bc69d7af7a9", + "src/shared/timer-delay.ts": "7e529ed30d1b25521f5d72d7c2a6d05d16efe66faf2619b859d67b330cca767f", + "src/shared/skill-install-failure.ts": "571ffbaad47304bbdef294e6445d70d179db40495005fdd0ce385491666ec4eb", + "src/shared/terminal-unavailable-cause.ts": "b6ce6d4b5b666ef72d8403883d2cc167f2998c524a17155638c261000c424e91", + "src/relay/dispatcher-writer-sink.ts": "12931e899bd882347b72cd03f3690e8d0e94bf2aa1ce9a560112f52cfdc14405", + "src/relay/dispatcher-writer-lane-scheduler.ts": "8b54b850f8aa88944160b796e3b357fbbc618c782590b4baf6444eda77bd5e5e", + "src/relay/dispatcher-writer-drain-arm.ts": "c9f1582197d0bb9ea30793f0c792a176ada3f25fb79e03474a70d084920d0416", + "src/relay/dispatcher-writer-admission.ts": "12e05ba04de5687c0a0db1e44eddf342cb0c21b9653eef2aeff7b5ccd2039797", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/relay/dispatcher-capacity-signals.ts": "125a94f04f8b3102956231dc800281ed7ee9bb538b2b93f005825c2dd59e5c63", + "src/shared/runtime-capability-degradation.ts": "fc9a5d3814c390c1296ecf72d5ef317b8b8624ea2578e4ceb2b32ee642b13a3f", + "src/relay/dispatcher-client-lifecycle.ts": "6884223ddb2be888cfd437ca1490e14a4c95655c5c831e6812ce4a7876e9c18b", + "src/relay/dispatcher-client-state.ts": "8cdabcb8df6b16b7cc84d8a33f5171cca1807b6d9dea4b374e3c2bb4fc10bfdb", + "src/relay/legacy-relay-publication-ledger.ts": "153926b90d370669d91fd9a59d246f63ed0c01f2aec6e8b7a890a0d0ad7378ba", + "src/relay/client-request-aborts.ts": "a4f458ce767c5315aa01fa9cc8c0a3008dcb445481a401462f14ec3dbbd9e20c" + }, + "observedReaderSha256": "7a5c9c14faf63197765fc5a2900e1d3488f94aaab6757425b7ef87597479e963", + "controls": [ + { + "name": "held-metadata-foreign-history", + "readers": 4, + "entries": [144, 144, 144, 144], + "wrappers": 576, + "uniqueParams": 144, + "logicalBase64BytesByUniqueParams": 44739584, + "sharedAcrossReaders": true, + "decodedTransferBytes": 33554432, + "peakRegisteredStreams": 1, + "maxConcurrentStreams": 16, + "ackWindow": 4, + "ackCount": 128, + "observedHeapDelta": 45298880, + "released": true + }, + { + "name": "ordinary-completion", + "passed": true + }, + { + "name": "transport-disposal", + "passed": true + }, + { + "name": "metadata-request-deadline", + "milliseconds": 30000, + "relayContextAborted": true, + "released": true + }, + { + "name": "unpaced-relay", + "passed": true + }, + { + "name": "real-pump-credit-window", + "chunksBeforeAck": 4, + "totalChunks": 6 + }, + { + "name": "actual-stream-capacity", + "slots": 16, + "rejectedSeventeenth": true, + "admittedAfterCompletion": true + }, + { + "name": "saturated-writer-metadata-order", + "wireOrder": ["probe.prime", "response", "fs.streamChunk", "fs.streamChunk", "fs.streamEnd"] + }, + { + "name": "same-turn-response-and-own-frames", + "passed": true + }, + { + "name": "canonical-crlf-source-control", + "reads": 66, + "passed": true + } + ], + "artifactHashes": { + "sources.cjs": "790ef573e61fbb7f68741d1e1cfb0e4b4c79a2dde4292e7ec9dc8dcf1d94940f", + "relay-fixture.mjs": "9856e17d83b812fb6b6717cf8207ccc808165c517df8d5c76bc020e0e6f51079", + "scenario.test.mjs": "7a619c631403d0ab03610171fc6109a9b52cde2d23b9d022354c3d6bb1b154af", + "vitest.config.mjs": "4ef63c6eba6d8e443f18f787bceb0b1f40b90ee750d63a3b27239ed05bf5d849", + "before.config.mjs": "a0a8ec50388d194d60cdf7019bba18fffb2c4691404924c3675757d3b3984962", + "fix.patch": "9681d47063d4b76bc4f9567eacf5dddb3ec221b715ce0bedddfcb493e0558fc6", + "main-context.patch": "4ef0bf173e6575a548b1d217248432fedbcfd68bcb350980e73987df3aba81e9", + "source-versions.json": "7df5afb4523280364b1eecaf465131ac94822fbb868dc4aaddb42586dff9f21d" + } +} diff --git a/docs/audits/ssh-file-metadata-retention/worktree-before-node-results.json b/docs/audits/ssh-file-metadata-retention/worktree-before-node-results.json new file mode 100644 index 00000000000..b16ca49d1c6 --- /dev/null +++ b/docs/audits/ssh-file-metadata-retention/worktree-before-node-results.json @@ -0,0 +1,140 @@ +{ + "variant": "before", + "graph": "worktree", + "runtime": { + "node": "26.6.0", + "electron": null + }, + "sources": { + "src/relay/dispatcher.ts": "6dca32ec33e410fa9226d6c0a4a548ed06fc5bbf1a65ccdbcae7cc43a9328d18", + "src/main/ssh/ssh-channel-multiplexer.ts": "9ecd88963fec72901596bd25d2cb4c8666ef41d3c8a968242c6eef52095ee108", + "src/main/ssh/ssh-filesystem-stream-reader.ts": "b8586de1412df98428939a0a4870c5f8ccab22f506a188b2eb1c7a38c317b1ef", + "src/relay/fs-handler-file-read.ts": "2d70577839cb150ad0977981d54904b1a25bd8b5de7cfe19e018c08c36f41cd2", + "src/relay/protocol.ts": "faebaded7e8c8b98f021c791b4519d78d58be3879937fe2ed2cef68b22404060", + "src/relay/fs-stream-registry.ts": "d723dd0b6419a7937225bdacbc572ef004c49d850cbc616e9168e48f5e63b8fd", + "src/relay/dispatcher-notification-publication.ts": "d21c8575f5f4694a806b0e05cce4a1595f7116220f48d9068eba7c23aed7a546", + "src/relay/fs-handler-utils.ts": "12c54b9647ddc9e4ae9924f65aadf0d00e9553050858beae232c02077a94d93a", + "src/relay/relay-frame-decoder.ts": "7044cd142b21f847ee0dc4aee085fe18434b4bcb34ba1eb8bc1fb48a0ed7919e", + "src/main/ssh/ssh-file-stream-inactivity-deadline.ts": "0554419b22356efa60065b15640d8bf0d8d166bf345e30b492b2305767ea603a", + "src/main/ssh/ssh-file-stream-read-cap.ts": "83b714af0c87da4c855762ffb827f529302ebc426ab5cde59458206a64cb07b1", + "src/main/ssh/relay-protocol.ts": "644aa6f2087b5867d41006bfdcec78ffba693157a2912feb8f82b120f5647b34", + "src/main/ssh/ssh-multiplexer-transport-writer.ts": "433a2cc400b5ed9743871ca03aac06d31a079ef23c42c2a2759f78967312d071", + "src/shared/relay-frame-decoder-contract.ts": "f58279099fe4dbbdfde9e3e038a916ae077e71d8ac6aa871b59a64c6fbaeaad8", + "src/relay/dispatcher-pty-publication.ts": "2a91aeab16069e4d053c453947c5cd5d24c9e9ac4d36f19d23377689313cc851", + "src/main/system-power-lifecycle.ts": "cdeb82463a530123be4d42374925a0b349446437c02bff2c287786864515de11", + "src/relay/dispatcher-contract.ts": "7c397f9d51c0e3eca604e9075cddb6609a5e114284999009e8a86146e766c081", + "src/shared/relay-frame-decoder.ts": "88c0ecbc06b93efe6733094b8c9e4509ab1cbd139f8cb3257e9fb74a0ed39194", + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts": "6173ddd6640a930d0fa586fed91c1adc0d865a33f81af9a1ed5a0817c08e90cc", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/shared/search-subprocess-lines.ts": "b860de84a4081fb86515bb31910f0f1d4b0a6606370490c4f60aa9d2e9d5eee3", + "src/relay/fs-handler-list-files.ts": "cca88ec47512840837c84c63baf7ddfc7c216810cb2a5f63fa2babbeef3ff189", + "src/shared/text-search.ts": "c291574e05874440601a6a043b5d065ca172072f4146568d9c9818829abbc185", + "src/shared/ripgrep-process-availability.ts": "2a8e21ae2c9af146f1f226bdfad1e45916cfb2671fb11aa4586d6a337b8cdec2", + "src/shared/image-file-extensions.ts": "8460080a80e2a09fd64466faee1b87df4d8e4803e33bc2f9dd8b3b031a54d536", + "src/relay/dispatcher-producer-capacity.ts": "ae201a7e09acbcc013410b92868df1e964f95952f8ab3a462d177f488292965b", + "src/relay/dispatcher-producer-transport.ts": "c48087ed5f330311fb438ae3df75ff03bf34df83156f3987b8905598e21780e5", + "src/shared/relay-frame-buffer.ts": "f89fcd33489894c34f79ba8fd5e5634487555a7c34fc8e7c054229ae1c20e0be", + "src/shared/quick-open-path-search.ts": "2556d6c34f50e573b2fe046f7c64f782362b3bda1241098772bd360975910a98", + "src/shared/quick-open-filter.ts": "9a09a02764d15622932083a3e550fc7215c066d30a2868f5d548b9105b780d81", + "src/shared/file-listing-cancellation.ts": "c9e1eed636fa2140071fbc2089ce02f738cce725de5069f04bf307fea611b63d", + "src/relay/dispatcher-rpc-routing.ts": "c11b3ff9096bd4877bba0ebc8a70a63092a00015fde3d8f81f3b8fb54087479b", + "src/relay/dispatcher-client-writer.ts": "9ed3ee7ad2758f2d10cd0a0ba06bf77dc3318317b8e24f0eaa6cfe235fd34be4", + "src/shared/text-search-match-accumulator.ts": "1229c755abc94608e211c6e403d529905f577a991871eadfed5eb18821d6e88b", + "src/shared/json-text-structure-limit.ts": "29ded95f7e054d839dc82d118cb324d2144c70267f293d6b54f188159ada5eb9", + "src/shared/text-search-paths.ts": "a977e1b1591b38d7f08652e3d0a64c95b9379e8339ab9d26d6325f7ec90fcda3", + "src/shared/text-search-glob-patterns.ts": "b34072078cf81813e37c37228b830c974dfadb5284ba608538deb56594d64a69", + "src/shared/search-match-count.ts": "6fde43e886fe142c23aff2b80b3f1ebdf613a083051a11e19d7813363db88f62", + "src/shared/string-utils.ts": "fdb48c18f2f7272ed25949eb74beca182c3336d32681191c4b0a4a077db02c22", + "src/shared/file-name-sort.ts": "5aafddf218c453276dcf1afdccf9cd7f82c50160f5df2dd6d1b2779ce0c0cc4a", + "src/shared/clipboard-text.ts": "50f3155063244d6d12ac1493c80029a75df256c031f986f76da64a94152d0c38", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/shared/event-loop-yield.ts": "d291b71f09eb88f24a849d0ae310f946deb7f93b0b6b490b6ea5a605ab7a53cf", + "src/shared/utf8-byte-limits.ts": "01574b287b2d6ab0758112887e356c0d270b25db1b72651925ba8329eb4224e6", + "src/relay/dispatcher-frame-codec.ts": "2728b1e9e8465350c23cede5b64287db77a3d90488d8f8b1e5721bc69d7af7a9", + "src/shared/timer-delay.ts": "7e529ed30d1b25521f5d72d7c2a6d05d16efe66faf2619b859d67b330cca767f", + "src/shared/skill-install-failure.ts": "571ffbaad47304bbdef294e6445d70d179db40495005fdd0ce385491666ec4eb", + "src/shared/terminal-unavailable-cause.ts": "b6ce6d4b5b666ef72d8403883d2cc167f2998c524a17155638c261000c424e91", + "src/relay/dispatcher-writer-sink.ts": "12931e899bd882347b72cd03f3690e8d0e94bf2aa1ce9a560112f52cfdc14405", + "src/relay/dispatcher-writer-lane-scheduler.ts": "8b54b850f8aa88944160b796e3b357fbbc618c782590b4baf6444eda77bd5e5e", + "src/relay/dispatcher-writer-drain-arm.ts": "c9f1582197d0bb9ea30793f0c792a176ada3f25fb79e03474a70d084920d0416", + "src/relay/dispatcher-writer-admission.ts": "12e05ba04de5687c0a0db1e44eddf342cb0c21b9653eef2aeff7b5ccd2039797", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/relay/dispatcher-capacity-signals.ts": "125a94f04f8b3102956231dc800281ed7ee9bb538b2b93f005825c2dd59e5c63", + "src/shared/runtime-capability-degradation.ts": "fc9a5d3814c390c1296ecf72d5ef317b8b8624ea2578e4ceb2b32ee642b13a3f", + "src/relay/dispatcher-client-lifecycle.ts": "6884223ddb2be888cfd437ca1490e14a4c95655c5c831e6812ce4a7876e9c18b", + "src/relay/dispatcher-client-state.ts": "8cdabcb8df6b16b7cc84d8a33f5171cca1807b6d9dea4b374e3c2bb4fc10bfdb", + "src/relay/legacy-relay-publication-ledger.ts": "153926b90d370669d91fd9a59d246f63ed0c01f2aec6e8b7a890a0d0ad7378ba", + "src/relay/client-request-aborts.ts": "a4f458ce767c5315aa01fa9cc8c0a3008dcb445481a401462f14ec3dbbd9e20c" + }, + "observedReaderSha256": "7a5c9c14faf63197765fc5a2900e1d3488f94aaab6757425b7ef87597479e963", + "controls": [ + { + "name": "held-metadata-foreign-history", + "readers": 4, + "entries": [144, 144, 144, 144], + "wrappers": 576, + "uniqueParams": 144, + "logicalBase64BytesByUniqueParams": 44739584, + "sharedAcrossReaders": true, + "decodedTransferBytes": 33554432, + "peakRegisteredStreams": 1, + "maxConcurrentStreams": 16, + "ackWindow": 4, + "ackCount": 128, + "observedHeapDelta": 45478552, + "released": true + }, + { + "name": "ordinary-completion", + "passed": true + }, + { + "name": "transport-disposal", + "passed": true + }, + { + "name": "metadata-request-deadline", + "milliseconds": 30000, + "relayContextAborted": true, + "released": true + }, + { + "name": "unpaced-relay", + "passed": true + }, + { + "name": "real-pump-credit-window", + "chunksBeforeAck": 4, + "totalChunks": 6 + }, + { + "name": "actual-stream-capacity", + "slots": 16, + "rejectedSeventeenth": true, + "admittedAfterCompletion": true + }, + { + "name": "saturated-writer-metadata-order", + "wireOrder": ["probe.prime", "response", "fs.streamChunk", "fs.streamChunk", "fs.streamEnd"] + }, + { + "name": "same-turn-response-and-own-frames", + "passed": true + }, + { + "name": "canonical-crlf-source-control", + "reads": 66, + "passed": true + } + ], + "artifactHashes": { + "sources.cjs": "790ef573e61fbb7f68741d1e1cfb0e4b4c79a2dde4292e7ec9dc8dcf1d94940f", + "relay-fixture.mjs": "9856e17d83b812fb6b6717cf8207ccc808165c517df8d5c76bc020e0e6f51079", + "scenario.test.mjs": "7a619c631403d0ab03610171fc6109a9b52cde2d23b9d022354c3d6bb1b154af", + "vitest.config.mjs": "4ef63c6eba6d8e443f18f787bceb0b1f40b90ee750d63a3b27239ed05bf5d849", + "before.config.mjs": "a0a8ec50388d194d60cdf7019bba18fffb2c4691404924c3675757d3b3984962", + "fix.patch": "9681d47063d4b76bc4f9567eacf5dddb3ec221b715ce0bedddfcb493e0558fc6", + "main-context.patch": "4ef0bf173e6575a548b1d217248432fedbcfd68bcb350980e73987df3aba81e9", + "source-versions.json": "7df5afb4523280364b1eecaf465131ac94822fbb868dc4aaddb42586dff9f21d" + } +} diff --git a/docs/audits/ssh-file-metadata-retention/worktree-fixed-electron-results.json b/docs/audits/ssh-file-metadata-retention/worktree-fixed-electron-results.json new file mode 100644 index 00000000000..c40cf2fe652 --- /dev/null +++ b/docs/audits/ssh-file-metadata-retention/worktree-fixed-electron-results.json @@ -0,0 +1,140 @@ +{ + "variant": "fixed", + "graph": "worktree", + "runtime": { + "node": "24.21.0", + "electron": "43.7.0" + }, + "sources": { + "src/relay/dispatcher.ts": "6dca32ec33e410fa9226d6c0a4a548ed06fc5bbf1a65ccdbcae7cc43a9328d18", + "src/main/ssh/ssh-channel-multiplexer.ts": "9ecd88963fec72901596bd25d2cb4c8666ef41d3c8a968242c6eef52095ee108", + "src/main/ssh/ssh-filesystem-stream-reader.ts": "333b0ba796edbd0a07b483cc65fb9f0c46f6f5488361bce08c488ea72543825a", + "src/relay/fs-handler-file-read.ts": "2d70577839cb150ad0977981d54904b1a25bd8b5de7cfe19e018c08c36f41cd2", + "src/relay/protocol.ts": "faebaded7e8c8b98f021c791b4519d78d58be3879937fe2ed2cef68b22404060", + "src/relay/fs-stream-registry.ts": "d723dd0b6419a7937225bdacbc572ef004c49d850cbc616e9168e48f5e63b8fd", + "src/relay/dispatcher-notification-publication.ts": "d21c8575f5f4694a806b0e05cce4a1595f7116220f48d9068eba7c23aed7a546", + "src/relay/fs-handler-utils.ts": "12c54b9647ddc9e4ae9924f65aadf0d00e9553050858beae232c02077a94d93a", + "src/relay/relay-frame-decoder.ts": "7044cd142b21f847ee0dc4aee085fe18434b4bcb34ba1eb8bc1fb48a0ed7919e", + "src/main/ssh/ssh-file-stream-inactivity-deadline.ts": "0554419b22356efa60065b15640d8bf0d8d166bf345e30b492b2305767ea603a", + "src/main/ssh/ssh-file-stream-read-cap.ts": "83b714af0c87da4c855762ffb827f529302ebc426ab5cde59458206a64cb07b1", + "src/main/ssh/relay-protocol.ts": "644aa6f2087b5867d41006bfdcec78ffba693157a2912feb8f82b120f5647b34", + "src/main/ssh/ssh-multiplexer-transport-writer.ts": "433a2cc400b5ed9743871ca03aac06d31a079ef23c42c2a2759f78967312d071", + "src/shared/relay-frame-decoder-contract.ts": "f58279099fe4dbbdfde9e3e038a916ae077e71d8ac6aa871b59a64c6fbaeaad8", + "src/relay/dispatcher-pty-publication.ts": "2a91aeab16069e4d053c453947c5cd5d24c9e9ac4d36f19d23377689313cc851", + "src/main/system-power-lifecycle.ts": "cdeb82463a530123be4d42374925a0b349446437c02bff2c287786864515de11", + "src/relay/dispatcher-contract.ts": "7c397f9d51c0e3eca604e9075cddb6609a5e114284999009e8a86146e766c081", + "src/shared/relay-frame-decoder.ts": "88c0ecbc06b93efe6733094b8c9e4509ab1cbd139f8cb3257e9fb74a0ed39194", + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts": "6173ddd6640a930d0fa586fed91c1adc0d865a33f81af9a1ed5a0817c08e90cc", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/shared/search-subprocess-lines.ts": "b860de84a4081fb86515bb31910f0f1d4b0a6606370490c4f60aa9d2e9d5eee3", + "src/relay/fs-handler-list-files.ts": "cca88ec47512840837c84c63baf7ddfc7c216810cb2a5f63fa2babbeef3ff189", + "src/shared/text-search.ts": "c291574e05874440601a6a043b5d065ca172072f4146568d9c9818829abbc185", + "src/shared/ripgrep-process-availability.ts": "2a8e21ae2c9af146f1f226bdfad1e45916cfb2671fb11aa4586d6a337b8cdec2", + "src/shared/image-file-extensions.ts": "8460080a80e2a09fd64466faee1b87df4d8e4803e33bc2f9dd8b3b031a54d536", + "src/relay/dispatcher-producer-capacity.ts": "ae201a7e09acbcc013410b92868df1e964f95952f8ab3a462d177f488292965b", + "src/relay/dispatcher-producer-transport.ts": "c48087ed5f330311fb438ae3df75ff03bf34df83156f3987b8905598e21780e5", + "src/shared/relay-frame-buffer.ts": "f89fcd33489894c34f79ba8fd5e5634487555a7c34fc8e7c054229ae1c20e0be", + "src/shared/quick-open-path-search.ts": "2556d6c34f50e573b2fe046f7c64f782362b3bda1241098772bd360975910a98", + "src/shared/quick-open-filter.ts": "9a09a02764d15622932083a3e550fc7215c066d30a2868f5d548b9105b780d81", + "src/shared/file-listing-cancellation.ts": "c9e1eed636fa2140071fbc2089ce02f738cce725de5069f04bf307fea611b63d", + "src/relay/dispatcher-rpc-routing.ts": "c11b3ff9096bd4877bba0ebc8a70a63092a00015fde3d8f81f3b8fb54087479b", + "src/relay/dispatcher-client-writer.ts": "9ed3ee7ad2758f2d10cd0a0ba06bf77dc3318317b8e24f0eaa6cfe235fd34be4", + "src/shared/text-search-match-accumulator.ts": "1229c755abc94608e211c6e403d529905f577a991871eadfed5eb18821d6e88b", + "src/shared/json-text-structure-limit.ts": "29ded95f7e054d839dc82d118cb324d2144c70267f293d6b54f188159ada5eb9", + "src/shared/text-search-paths.ts": "a977e1b1591b38d7f08652e3d0a64c95b9379e8339ab9d26d6325f7ec90fcda3", + "src/shared/text-search-glob-patterns.ts": "b34072078cf81813e37c37228b830c974dfadb5284ba608538deb56594d64a69", + "src/shared/search-match-count.ts": "6fde43e886fe142c23aff2b80b3f1ebdf613a083051a11e19d7813363db88f62", + "src/shared/string-utils.ts": "fdb48c18f2f7272ed25949eb74beca182c3336d32681191c4b0a4a077db02c22", + "src/shared/file-name-sort.ts": "5aafddf218c453276dcf1afdccf9cd7f82c50160f5df2dd6d1b2779ce0c0cc4a", + "src/shared/clipboard-text.ts": "50f3155063244d6d12ac1493c80029a75df256c031f986f76da64a94152d0c38", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/shared/event-loop-yield.ts": "d291b71f09eb88f24a849d0ae310f946deb7f93b0b6b490b6ea5a605ab7a53cf", + "src/shared/utf8-byte-limits.ts": "01574b287b2d6ab0758112887e356c0d270b25db1b72651925ba8329eb4224e6", + "src/relay/dispatcher-frame-codec.ts": "2728b1e9e8465350c23cede5b64287db77a3d90488d8f8b1e5721bc69d7af7a9", + "src/shared/timer-delay.ts": "7e529ed30d1b25521f5d72d7c2a6d05d16efe66faf2619b859d67b330cca767f", + "src/shared/skill-install-failure.ts": "571ffbaad47304bbdef294e6445d70d179db40495005fdd0ce385491666ec4eb", + "src/shared/terminal-unavailable-cause.ts": "b6ce6d4b5b666ef72d8403883d2cc167f2998c524a17155638c261000c424e91", + "src/relay/dispatcher-writer-sink.ts": "12931e899bd882347b72cd03f3690e8d0e94bf2aa1ce9a560112f52cfdc14405", + "src/relay/dispatcher-writer-lane-scheduler.ts": "8b54b850f8aa88944160b796e3b357fbbc618c782590b4baf6444eda77bd5e5e", + "src/relay/dispatcher-writer-drain-arm.ts": "c9f1582197d0bb9ea30793f0c792a176ada3f25fb79e03474a70d084920d0416", + "src/relay/dispatcher-writer-admission.ts": "12e05ba04de5687c0a0db1e44eddf342cb0c21b9653eef2aeff7b5ccd2039797", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/relay/dispatcher-capacity-signals.ts": "125a94f04f8b3102956231dc800281ed7ee9bb538b2b93f005825c2dd59e5c63", + "src/shared/runtime-capability-degradation.ts": "fc9a5d3814c390c1296ecf72d5ef317b8b8624ea2578e4ceb2b32ee642b13a3f", + "src/relay/dispatcher-client-lifecycle.ts": "6884223ddb2be888cfd437ca1490e14a4c95655c5c831e6812ce4a7876e9c18b", + "src/relay/dispatcher-client-state.ts": "8cdabcb8df6b16b7cc84d8a33f5171cca1807b6d9dea4b374e3c2bb4fc10bfdb", + "src/relay/legacy-relay-publication-ledger.ts": "153926b90d370669d91fd9a59d246f63ed0c01f2aec6e8b7a890a0d0ad7378ba", + "src/relay/client-request-aborts.ts": "a4f458ce767c5315aa01fa9cc8c0a3008dcb445481a401462f14ec3dbbd9e20c" + }, + "observedReaderSha256": "333b0ba796edbd0a07b483cc65fb9f0c46f6f5488361bce08c488ea72543825a", + "controls": [ + { + "name": "held-metadata-foreign-history", + "readers": 4, + "entries": [0, 0, 0, 0], + "wrappers": 0, + "uniqueParams": 0, + "logicalBase64BytesByUniqueParams": 0, + "sharedAcrossReaders": false, + "decodedTransferBytes": 33554432, + "peakRegisteredStreams": 1, + "maxConcurrentStreams": 16, + "ackWindow": 4, + "ackCount": 128, + "observedHeapDelta": 520252, + "released": true + }, + { + "name": "ordinary-completion", + "passed": true + }, + { + "name": "transport-disposal", + "passed": true + }, + { + "name": "metadata-request-deadline", + "milliseconds": 30000, + "relayContextAborted": true, + "released": true + }, + { + "name": "unpaced-relay", + "passed": true + }, + { + "name": "real-pump-credit-window", + "chunksBeforeAck": 4, + "totalChunks": 6 + }, + { + "name": "actual-stream-capacity", + "slots": 16, + "rejectedSeventeenth": true, + "admittedAfterCompletion": true + }, + { + "name": "saturated-writer-metadata-order", + "wireOrder": ["probe.prime", "response", "fs.streamChunk", "fs.streamChunk", "fs.streamEnd"] + }, + { + "name": "same-turn-response-and-own-frames", + "passed": true + }, + { + "name": "canonical-crlf-source-control", + "reads": 66, + "passed": true + } + ], + "artifactHashes": { + "sources.cjs": "790ef573e61fbb7f68741d1e1cfb0e4b4c79a2dde4292e7ec9dc8dcf1d94940f", + "relay-fixture.mjs": "9856e17d83b812fb6b6717cf8207ccc808165c517df8d5c76bc020e0e6f51079", + "scenario.test.mjs": "7a619c631403d0ab03610171fc6109a9b52cde2d23b9d022354c3d6bb1b154af", + "vitest.config.mjs": "4ef63c6eba6d8e443f18f787bceb0b1f40b90ee750d63a3b27239ed05bf5d849", + "before.config.mjs": "a0a8ec50388d194d60cdf7019bba18fffb2c4691404924c3675757d3b3984962", + "fix.patch": "9681d47063d4b76bc4f9567eacf5dddb3ec221b715ce0bedddfcb493e0558fc6", + "main-context.patch": "4ef0bf173e6575a548b1d217248432fedbcfd68bcb350980e73987df3aba81e9", + "source-versions.json": "7df5afb4523280364b1eecaf465131ac94822fbb868dc4aaddb42586dff9f21d" + } +} diff --git a/docs/audits/ssh-file-metadata-retention/worktree-fixed-node-results.json b/docs/audits/ssh-file-metadata-retention/worktree-fixed-node-results.json new file mode 100644 index 00000000000..e3a75af6824 --- /dev/null +++ b/docs/audits/ssh-file-metadata-retention/worktree-fixed-node-results.json @@ -0,0 +1,140 @@ +{ + "variant": "fixed", + "graph": "worktree", + "runtime": { + "node": "26.6.0", + "electron": null + }, + "sources": { + "src/relay/dispatcher.ts": "6dca32ec33e410fa9226d6c0a4a548ed06fc5bbf1a65ccdbcae7cc43a9328d18", + "src/main/ssh/ssh-channel-multiplexer.ts": "9ecd88963fec72901596bd25d2cb4c8666ef41d3c8a968242c6eef52095ee108", + "src/main/ssh/ssh-filesystem-stream-reader.ts": "333b0ba796edbd0a07b483cc65fb9f0c46f6f5488361bce08c488ea72543825a", + "src/relay/fs-handler-file-read.ts": "2d70577839cb150ad0977981d54904b1a25bd8b5de7cfe19e018c08c36f41cd2", + "src/relay/protocol.ts": "faebaded7e8c8b98f021c791b4519d78d58be3879937fe2ed2cef68b22404060", + "src/relay/fs-stream-registry.ts": "d723dd0b6419a7937225bdacbc572ef004c49d850cbc616e9168e48f5e63b8fd", + "src/relay/dispatcher-notification-publication.ts": "d21c8575f5f4694a806b0e05cce4a1595f7116220f48d9068eba7c23aed7a546", + "src/relay/fs-handler-utils.ts": "12c54b9647ddc9e4ae9924f65aadf0d00e9553050858beae232c02077a94d93a", + "src/relay/relay-frame-decoder.ts": "7044cd142b21f847ee0dc4aee085fe18434b4bcb34ba1eb8bc1fb48a0ed7919e", + "src/main/ssh/ssh-file-stream-inactivity-deadline.ts": "0554419b22356efa60065b15640d8bf0d8d166bf345e30b492b2305767ea603a", + "src/main/ssh/ssh-file-stream-read-cap.ts": "83b714af0c87da4c855762ffb827f529302ebc426ab5cde59458206a64cb07b1", + "src/main/ssh/relay-protocol.ts": "644aa6f2087b5867d41006bfdcec78ffba693157a2912feb8f82b120f5647b34", + "src/main/ssh/ssh-multiplexer-transport-writer.ts": "433a2cc400b5ed9743871ca03aac06d31a079ef23c42c2a2759f78967312d071", + "src/shared/relay-frame-decoder-contract.ts": "f58279099fe4dbbdfde9e3e038a916ae077e71d8ac6aa871b59a64c6fbaeaad8", + "src/relay/dispatcher-pty-publication.ts": "2a91aeab16069e4d053c453947c5cd5d24c9e9ac4d36f19d23377689313cc851", + "src/main/system-power-lifecycle.ts": "cdeb82463a530123be4d42374925a0b349446437c02bff2c287786864515de11", + "src/relay/dispatcher-contract.ts": "7c397f9d51c0e3eca604e9075cddb6609a5e114284999009e8a86146e766c081", + "src/shared/relay-frame-decoder.ts": "88c0ecbc06b93efe6733094b8c9e4509ab1cbd139f8cb3257e9fb74a0ed39194", + "src/main/ssh/ssh-multiplexer-writer-lane-scheduler.ts": "6173ddd6640a930d0fa586fed91c1adc0d865a33f81af9a1ed5a0817c08e90cc", + "src/shared/pty-write-settlement.ts": "0726aba75f2ef127d41f47a0685fb42daab34fc139d5b9f727079aec88b702e6", + "src/shared/search-subprocess-lines.ts": "b860de84a4081fb86515bb31910f0f1d4b0a6606370490c4f60aa9d2e9d5eee3", + "src/relay/fs-handler-list-files.ts": "cca88ec47512840837c84c63baf7ddfc7c216810cb2a5f63fa2babbeef3ff189", + "src/shared/text-search.ts": "c291574e05874440601a6a043b5d065ca172072f4146568d9c9818829abbc185", + "src/shared/ripgrep-process-availability.ts": "2a8e21ae2c9af146f1f226bdfad1e45916cfb2671fb11aa4586d6a337b8cdec2", + "src/shared/image-file-extensions.ts": "8460080a80e2a09fd64466faee1b87df4d8e4803e33bc2f9dd8b3b031a54d536", + "src/relay/dispatcher-producer-capacity.ts": "ae201a7e09acbcc013410b92868df1e964f95952f8ab3a462d177f488292965b", + "src/relay/dispatcher-producer-transport.ts": "c48087ed5f330311fb438ae3df75ff03bf34df83156f3987b8905598e21780e5", + "src/shared/relay-frame-buffer.ts": "f89fcd33489894c34f79ba8fd5e5634487555a7c34fc8e7c054229ae1c20e0be", + "src/shared/quick-open-path-search.ts": "2556d6c34f50e573b2fe046f7c64f782362b3bda1241098772bd360975910a98", + "src/shared/quick-open-filter.ts": "9a09a02764d15622932083a3e550fc7215c066d30a2868f5d548b9105b780d81", + "src/shared/file-listing-cancellation.ts": "c9e1eed636fa2140071fbc2089ce02f738cce725de5069f04bf307fea611b63d", + "src/relay/dispatcher-rpc-routing.ts": "c11b3ff9096bd4877bba0ebc8a70a63092a00015fde3d8f81f3b8fb54087479b", + "src/relay/dispatcher-client-writer.ts": "9ed3ee7ad2758f2d10cd0a0ba06bf77dc3318317b8e24f0eaa6cfe235fd34be4", + "src/shared/text-search-match-accumulator.ts": "1229c755abc94608e211c6e403d529905f577a991871eadfed5eb18821d6e88b", + "src/shared/json-text-structure-limit.ts": "29ded95f7e054d839dc82d118cb324d2144c70267f293d6b54f188159ada5eb9", + "src/shared/text-search-paths.ts": "a977e1b1591b38d7f08652e3d0a64c95b9379e8339ab9d26d6325f7ec90fcda3", + "src/shared/text-search-glob-patterns.ts": "b34072078cf81813e37c37228b830c974dfadb5284ba608538deb56594d64a69", + "src/shared/search-match-count.ts": "6fde43e886fe142c23aff2b80b3f1ebdf613a083051a11e19d7813363db88f62", + "src/shared/string-utils.ts": "fdb48c18f2f7272ed25949eb74beca182c3336d32681191c4b0a4a077db02c22", + "src/shared/file-name-sort.ts": "5aafddf218c453276dcf1afdccf9cd7f82c50160f5df2dd6d1b2779ce0c0cc4a", + "src/shared/clipboard-text.ts": "50f3155063244d6d12ac1493c80029a75df256c031f986f76da64a94152d0c38", + "src/shared/cross-platform-path.ts": "d72a91065f535824b17f2d3285f45f6d2f40ec2a2ac7ffb4ae17edaa4f9a97f3", + "src/shared/event-loop-yield.ts": "d291b71f09eb88f24a849d0ae310f946deb7f93b0b6b490b6ea5a605ab7a53cf", + "src/shared/utf8-byte-limits.ts": "01574b287b2d6ab0758112887e356c0d270b25db1b72651925ba8329eb4224e6", + "src/relay/dispatcher-frame-codec.ts": "2728b1e9e8465350c23cede5b64287db77a3d90488d8f8b1e5721bc69d7af7a9", + "src/shared/timer-delay.ts": "7e529ed30d1b25521f5d72d7c2a6d05d16efe66faf2619b859d67b330cca767f", + "src/shared/skill-install-failure.ts": "571ffbaad47304bbdef294e6445d70d179db40495005fdd0ce385491666ec4eb", + "src/shared/terminal-unavailable-cause.ts": "b6ce6d4b5b666ef72d8403883d2cc167f2998c524a17155638c261000c424e91", + "src/relay/dispatcher-writer-sink.ts": "12931e899bd882347b72cd03f3690e8d0e94bf2aa1ce9a560112f52cfdc14405", + "src/relay/dispatcher-writer-lane-scheduler.ts": "8b54b850f8aa88944160b796e3b357fbbc618c782590b4baf6444eda77bd5e5e", + "src/relay/dispatcher-writer-drain-arm.ts": "c9f1582197d0bb9ea30793f0c792a176ada3f25fb79e03474a70d084920d0416", + "src/relay/dispatcher-writer-admission.ts": "12e05ba04de5687c0a0db1e44eddf342cb0c21b9653eef2aeff7b5ccd2039797", + "src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a", + "src/relay/dispatcher-capacity-signals.ts": "125a94f04f8b3102956231dc800281ed7ee9bb538b2b93f005825c2dd59e5c63", + "src/shared/runtime-capability-degradation.ts": "fc9a5d3814c390c1296ecf72d5ef317b8b8624ea2578e4ceb2b32ee642b13a3f", + "src/relay/dispatcher-client-lifecycle.ts": "6884223ddb2be888cfd437ca1490e14a4c95655c5c831e6812ce4a7876e9c18b", + "src/relay/dispatcher-client-state.ts": "8cdabcb8df6b16b7cc84d8a33f5171cca1807b6d9dea4b374e3c2bb4fc10bfdb", + "src/relay/legacy-relay-publication-ledger.ts": "153926b90d370669d91fd9a59d246f63ed0c01f2aec6e8b7a890a0d0ad7378ba", + "src/relay/client-request-aborts.ts": "a4f458ce767c5315aa01fa9cc8c0a3008dcb445481a401462f14ec3dbbd9e20c" + }, + "observedReaderSha256": "333b0ba796edbd0a07b483cc65fb9f0c46f6f5488361bce08c488ea72543825a", + "controls": [ + { + "name": "held-metadata-foreign-history", + "readers": 4, + "entries": [0, 0, 0, 0], + "wrappers": 0, + "uniqueParams": 0, + "logicalBase64BytesByUniqueParams": 0, + "sharedAcrossReaders": false, + "decodedTransferBytes": 33554432, + "peakRegisteredStreams": 1, + "maxConcurrentStreams": 16, + "ackWindow": 4, + "ackCount": 128, + "observedHeapDelta": -663256, + "released": true + }, + { + "name": "ordinary-completion", + "passed": true + }, + { + "name": "transport-disposal", + "passed": true + }, + { + "name": "metadata-request-deadline", + "milliseconds": 30000, + "relayContextAborted": true, + "released": true + }, + { + "name": "unpaced-relay", + "passed": true + }, + { + "name": "real-pump-credit-window", + "chunksBeforeAck": 4, + "totalChunks": 6 + }, + { + "name": "actual-stream-capacity", + "slots": 16, + "rejectedSeventeenth": true, + "admittedAfterCompletion": true + }, + { + "name": "saturated-writer-metadata-order", + "wireOrder": ["probe.prime", "response", "fs.streamChunk", "fs.streamChunk", "fs.streamEnd"] + }, + { + "name": "same-turn-response-and-own-frames", + "passed": true + }, + { + "name": "canonical-crlf-source-control", + "reads": 66, + "passed": true + } + ], + "artifactHashes": { + "sources.cjs": "790ef573e61fbb7f68741d1e1cfb0e4b4c79a2dde4292e7ec9dc8dcf1d94940f", + "relay-fixture.mjs": "9856e17d83b812fb6b6717cf8207ccc808165c517df8d5c76bc020e0e6f51079", + "scenario.test.mjs": "7a619c631403d0ab03610171fc6109a9b52cde2d23b9d022354c3d6bb1b154af", + "vitest.config.mjs": "4ef63c6eba6d8e443f18f787bceb0b1f40b90ee750d63a3b27239ed05bf5d849", + "before.config.mjs": "a0a8ec50388d194d60cdf7019bba18fffb2c4691404924c3675757d3b3984962", + "fix.patch": "9681d47063d4b76bc4f9567eacf5dddb3ec221b715ce0bedddfcb493e0558fc6", + "main-context.patch": "4ef0bf173e6575a548b1d217248432fedbcfd68bcb350980e73987df3aba81e9", + "source-versions.json": "7df5afb4523280364b1eecaf465131ac94822fbb868dc4aaddb42586dff9f21d" + } +} diff --git a/src/main/providers/ssh-filesystem-provider-stream.test.ts b/src/main/providers/ssh-filesystem-provider-stream.test.ts index dd3e004b362..82defb9d0cc 100644 --- a/src/main/providers/ssh-filesystem-provider-stream.test.ts +++ b/src/main/providers/ssh-filesystem-provider-stream.test.ts @@ -1,10 +1,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { SshMultiplexerRequestOptions } from '../ssh/ssh-channel-multiplexer' import { SshFilesystemProvider } from './ssh-filesystem-provider' import { SSH_FILE_STREAM_INACTIVITY_TIMEOUT_MS } from '../ssh/ssh-file-stream-inactivity-deadline' import { publishSystemResume, publishSystemSuspend } from '../system-power-lifecycle' type MockMultiplexer = { request: ReturnType + _response: ReturnType notify: ReturnType onNotification: ReturnType onNotificationByMethod: ReturnType @@ -16,10 +18,22 @@ type MockMultiplexer = { } function createMockMux(): MockMultiplexer { + const response = vi.fn().mockResolvedValue(undefined) const methodHandlers = new Map) => void>>() const disposeHandlers = new Set<(reason: 'shutdown' | 'connection_lost') => void>() return { - request: vi.fn().mockResolvedValue(undefined), + _response: response, + request: vi.fn( + async ( + method: string, + params?: Record, + options?: SshMultiplexerRequestOptions + ) => { + const result: unknown = await response(method, params) + options?.beforeResolve?.(result) + return result + } + ), notify: vi.fn(), onNotification: vi.fn(), onNotificationByMethod: vi.fn( @@ -69,15 +83,21 @@ describe('SshFilesystemProvider readFile streaming', () => { vi.useRealTimers() }) + it('returns empty metadata and releases its stream listeners', async () => { + mux._response.mockResolvedValue({ totalSize: 0, isBinary: false, empty: true }) + const result = await provider.readFile('/home/user/empty.txt') + expect(result).toEqual({ content: '', isBinary: false }) + expect(mux._listenerCount()).toBe(0) + }) + it('streams via fs.readFileStream and reassembles utf-8 text', async () => { const text = 'hello world' const totalSize = Buffer.byteLength(text, 'utf-8') - mux.request.mockImplementation(async (method: string) => { + mux._response.mockImplementation(async (method: string) => { if (method !== 'fs.readFileStream') { throw new Error(`unexpected method ${method}`) } - // Why: setImmediate fires after the metadata-resolution .then has set - // streamIdRef, ensuring subscribed handlers see a matching streamId. + // The relay schedules chunks after publishing metadata. setImmediate(() => { mux._emitMethod('fs.streamChunk', { streamId: 1, @@ -96,16 +116,19 @@ describe('SshFilesystemProvider readFile streaming', () => { }) const result = await provider.readFile('/home/user/file.txt') - expect(mux.request).toHaveBeenCalledWith('fs.readFileStream', { - filePath: '/home/user/file.txt', - flowControl: 'ack' - }) + expect(mux.request.mock.calls[0]?.slice(0, 2)).toEqual([ + 'fs.readFileStream', + { + filePath: '/home/user/file.txt', + flowControl: 'ack' + } + ]) expect(result).toEqual({ content: text, isBinary: false }) }) it('falls back to legacy fs.readFile on -32601 method-not-found', async () => { const legacyResult = { content: 'legacy', isBinary: false } - mux.request.mockImplementation(async (method: string) => { + mux._response.mockImplementation(async (method: string) => { if (method === 'fs.readFileStream') { const err = new Error('Method not found') as Error & { code: number } err.code = -32601 @@ -124,7 +147,7 @@ describe('SshFilesystemProvider readFile streaming', () => { it('rejects when chunk arrives out of order', async () => { const totalSize = 256 * 1024 * 2 - mux.request.mockImplementation(async () => { + mux._response.mockImplementation(async () => { setImmediate(() => { mux._emitMethod('fs.streamChunk', { streamId: 1, @@ -144,7 +167,7 @@ describe('SshFilesystemProvider readFile streaming', () => { }) it('rejects when totalSize exceeds client cap without allocating', async () => { - mux.request.mockResolvedValue({ + mux._response.mockResolvedValue({ streamId: 1, totalSize: 51 * 1024 * 1024, isBinary: true, @@ -156,7 +179,7 @@ describe('SshFilesystemProvider readFile streaming', () => { }) it('applies a caller binary cap before allocating the stream buffer', async () => { - mux.request.mockResolvedValue({ + mux._response.mockResolvedValue({ streamId: 2, totalSize: 2, isBinary: true, @@ -172,7 +195,7 @@ describe('SshFilesystemProvider readFile streaming', () => { it('rejects on fs.streamError notification', async () => { const totalSize = 1024 - mux.request.mockImplementation(async () => { + mux._response.mockImplementation(async () => { setImmediate(() => { mux._emitMethod('fs.streamError', { streamId: 7, @@ -193,7 +216,7 @@ describe('SshFilesystemProvider readFile streaming', () => { it('cancels and cleans up a stream that stalls after metadata', async () => { vi.useFakeTimers() - mux.request.mockResolvedValue({ + mux._response.mockResolvedValue({ streamId: 9, totalSize: 1, isBinary: false, @@ -213,7 +236,7 @@ describe('SshFilesystemProvider readFile streaming', () => { it('keeps a long stream alive while chunks continue arriving', async () => { vi.useFakeTimers() const chunkSize = 256 * 1024 - mux.request.mockResolvedValue({ + mux._response.mockResolvedValue({ streamId: 10, totalSize: chunkSize + 1, isBinary: true, @@ -246,7 +269,7 @@ describe('SshFilesystemProvider readFile streaming', () => { it('grants an active stream a fresh inactivity window after system resume', async () => { vi.useFakeTimers() - mux.request.mockResolvedValue({ + mux._response.mockResolvedValue({ streamId: 11, totalSize: 1, isBinary: false, @@ -276,7 +299,7 @@ describe('SshFilesystemProvider readFile streaming', () => { it('keeps metadata received during suspend paused until resume', async () => { vi.useFakeTimers() - mux.request.mockResolvedValue({ + mux._response.mockResolvedValue({ streamId: 12, totalSize: 1, isBinary: false, @@ -310,7 +333,7 @@ describe('SshFilesystemProvider readFile streaming', () => { it('rejects on chunk count mismatch at streamEnd', async () => { const totalSize = 256 * 1024 * 3 - mux.request.mockImplementation(async () => { + mux._response.mockImplementation(async () => { setImmediate(() => { mux._emitMethod('fs.streamChunk', { streamId: 1, @@ -335,7 +358,7 @@ describe('SshFilesystemProvider readFile streaming', () => { // count matches (2), so the old code resolved with a zero-filled tail. The // exact-length check must reject this. const totalSize = 256 * 1024 * 2 - mux.request.mockImplementation(async () => { + mux._response.mockImplementation(async () => { setImmediate(() => { mux._emitMethod('fs.streamChunk', { streamId: 1, @@ -362,7 +385,7 @@ describe('SshFilesystemProvider readFile streaming', () => { it('rejects a short non-final chunk before later chunks arrive', async () => { const totalSize = 256 * 1024 * 2 - mux.request.mockImplementation(async () => { + mux._response.mockImplementation(async () => { setImmediate(() => { mux._emitMethod('fs.streamChunk', { streamId: 1, diff --git a/src/main/providers/ssh-filesystem-provider.test.ts b/src/main/providers/ssh-filesystem-provider.test.ts index b9cb21c3354..a20dfb98d9b 100644 --- a/src/main/providers/ssh-filesystem-provider.test.ts +++ b/src/main/providers/ssh-filesystem-provider.test.ts @@ -77,14 +77,6 @@ describe('SshFilesystemProvider', () => { }) }) - describe('readFile', () => { - it('short-circuits on empty:true metadata without subscribing to chunks', async () => { - mux.request.mockResolvedValue({ totalSize: 0, isBinary: false, empty: true }) - const result = await provider.readFile('/home/user/empty.txt') - expect(result).toEqual({ content: '', isBinary: false }) - }) - }) - describe('readTerminalArtifact', () => { it('sends fs.readTerminalArtifact request with verification metadata', async () => { mux.request.mockResolvedValue({ content: '{}', isBinary: false }) diff --git a/src/main/ssh/ssh-filesystem-stream-reader.ts b/src/main/ssh/ssh-filesystem-stream-reader.ts index 568ab195dc8..8c5b8632fa7 100644 --- a/src/main/ssh/ssh-filesystem-stream-reader.ts +++ b/src/main/ssh/ssh-filesystem-stream-reader.ts @@ -74,13 +74,7 @@ export async function readFileViaStream( let bytesReceived = 0 let settled = false - // Why: chunk/end/error frames may arrive in the same dispatch tick as the - // metadata response. Queue them until streamIdRef is set, then drain. - type PendingFrame = - | { kind: 'chunk'; params: Record } - | { kind: 'end'; params: Record } - | { kind: 'error'; params: Record } - const pending: PendingFrame[] = [] + // Install metadata during response dispatch, before adjacent stream frames. let metadataReady = false const inactivity = createSshFileStreamInactivityDeadline(() => { @@ -229,23 +223,9 @@ export async function readFileViaStream( fail(err) } - const drainPending = (): void => { - while (!settled && pending.length > 0) { - const frame = pending.shift()! - if (frame.kind === 'chunk') { - handleChunk(frame.params) - } else if (frame.kind === 'end') { - handleEnd(frame.params) - } else { - handleStreamError(frame.params) - } - } - } - unsubscribers.push( mux.onNotificationByMethod('fs.streamChunk', (params) => { if (!metadataReady) { - pending.push({ kind: 'chunk', params }) return } handleChunk(params) @@ -254,7 +234,6 @@ export async function readFileViaStream( unsubscribers.push( mux.onNotificationByMethod('fs.streamEnd', (params) => { if (!metadataReady) { - pending.push({ kind: 'end', params }) return } handleEnd(params) @@ -263,7 +242,6 @@ export async function readFileViaStream( unsubscribers.push( mux.onNotificationByMethod('fs.streamError', (params) => { if (!metadataReady) { - pending.push({ kind: 'error', params }) return } handleStreamError(params) @@ -284,56 +262,67 @@ export async function readFileViaStream( void mux // Why: flowControl declares this client acks each chunk, letting a new // relay pace the pump. Old relays ignore the extra param and flood. - .request('fs.readFileStream', { filePath, flowControl: 'ack' }) - .then((rawMetadata) => { - if (settled) { - return - } - const metadata = rawMetadata as StreamMetadataResponse - isBinary = metadata.isBinary - isImage = metadata.isImage - mimeType = metadata.mimeType - resultEncoding = metadata.resultEncoding ?? RESULT_ENCODING_BASE64 + .request( + 'fs.readFileStream', + { filePath, flowControl: 'ack' }, + { + beforeResolve: (rawMetadata) => { + if (settled) { + return + } + const metadata = rawMetadata as StreamMetadataResponse + isBinary = metadata.isBinary + isImage = metadata.isImage + mimeType = metadata.mimeType + resultEncoding = metadata.resultEncoding ?? RESULT_ENCODING_BASE64 - if (metadata.empty) { - succeed({ - content: '', - isBinary: metadata.isBinary, - ...(metadata.isImage !== undefined ? { isImage: metadata.isImage } : {}), - ...(metadata.mimeType !== undefined ? { mimeType: metadata.mimeType } : {}) - }) - return - } + if (metadata.empty) { + succeed({ + content: '', + isBinary: metadata.isBinary, + ...(metadata.isImage !== undefined ? { isImage: metadata.isImage } : {}), + ...(metadata.mimeType !== undefined ? { mimeType: metadata.mimeType } : {}) + }) + return + } - if (typeof metadata.streamId !== 'number') { - fail(new StreamProtocolError('Metadata missing streamId for non-empty stream')) - return - } + if (typeof metadata.streamId !== 'number') { + fail(new StreamProtocolError('Metadata missing streamId for non-empty stream')) + return + } - const cap = sshFileStreamReadCap(metadata.isBinary, limits) - if (metadata.totalSize < 0 || metadata.totalSize > cap) { - streamIdRef.current = metadata.streamId - fail( - new FileReadCapExceededError( - `Reported totalSize ${metadata.totalSize} exceeds client cap ${cap}` - ) - ) - return - } + const cap = sshFileStreamReadCap(metadata.isBinary, limits) + if (metadata.totalSize < 0 || metadata.totalSize > cap) { + streamIdRef.current = metadata.streamId + fail( + new FileReadCapExceededError( + `Reported totalSize ${metadata.totalSize} exceeds client cap ${cap}` + ) + ) + return + } - totalSize = metadata.totalSize - totalChunks = totalSize === 0 ? 0 : Math.ceil(totalSize / STREAM_CHUNK_SIZE) - try { - buffer = Buffer.alloc(totalSize) - } catch (err) { - streamIdRef.current = metadata.streamId - fail(new Error(`Failed to allocate ${totalSize} bytes: ${(err as Error).message}`)) - return + totalSize = metadata.totalSize + totalChunks = totalSize === 0 ? 0 : Math.ceil(totalSize / STREAM_CHUNK_SIZE) + try { + buffer = Buffer.alloc(totalSize) + } catch (err) { + streamIdRef.current = metadata.streamId + fail(new Error(`Failed to allocate ${totalSize} bytes: ${(err as Error).message}`)) + return + } + streamIdRef.current = metadata.streamId + metadataReady = true + inactivity.reset() + } + } + ) + // Why: beforeResolve is an optional hook; if a mux ever resolves without running + // it, metadata never installs and no deadline is armed. Fail instead of hanging. + .then(() => { + if (!settled && !metadataReady) { + fail(new StreamProtocolError('Metadata response resolved without stream identity')) } - streamIdRef.current = metadata.streamId - metadataReady = true - inactivity.reset() - drainPending() }) .catch((err) => { fail(err as Error) diff --git a/src/main/ssh/ssh-filesystem-stream-retention.test.ts b/src/main/ssh/ssh-filesystem-stream-retention.test.ts new file mode 100644 index 00000000000..15400212f78 --- /dev/null +++ b/src/main/ssh/ssh-filesystem-stream-retention.test.ts @@ -0,0 +1,221 @@ +import { afterEach, expect, it, vi } from 'vitest' +import { SshChannelMultiplexer } from './ssh-channel-multiplexer' +import { + FileReadCapExceededError, + readFileViaStream, + StreamProtocolError +} from './ssh-filesystem-stream-reader' +import { SshFilesystemProvider } from '../providers/ssh-filesystem-provider' +import { + encodeJsonRpcFrame, + MessageType, + parseJsonRpcMessage, + type JsonRpcMessage +} from './relay-protocol' + +const muxes: SshChannelMultiplexer[] = [] +afterEach(() => { + for (const mux of muxes.splice(0)) { + mux.dispose() + } +}) + +function createConnection() { + let receive = (_data: Buffer): void => {} + let sequence = 1 + const sent: JsonRpcMessage[] = [] + const mux = new SshChannelMultiplexer({ + write(data) { + if (data[0] === MessageType.Regular) { + sent.push(parseJsonRpcMessage(data.subarray(13))) + } + }, + onData(callback) { + receive = callback + }, + onClose() {} + }) + muxes.push(mux) + return { + mux, + sent, + feed(...messages: JsonRpcMessage[]) { + receive(Buffer.concat(messages.map((message) => encodeJsonRpcFrame(message, sequence++, 0)))) + } + } +} + +async function collect(): Promise { + if (!global.gc) { + throw new Error('Retention test requires --expose-gc') + } + for (let turn = 0; turn < 8; turn += 1) { + await new Promise((resolve) => setImmediate(resolve)) + global.gc() + } +} + +function sendForeignFrames(connection: ReturnType): WeakRef[] { + const refs: WeakRef[] = [] + const stop = connection.mux.onNotificationByMethod('fs.streamChunk', (params) => { + refs.push(new WeakRef(params)) + }) + for (let index = 0; index < 64; index += 1) { + connection.feed({ + jsonrpc: '2.0', + method: 'fs.streamChunk', + params: { streamId: index + 10, seq: 0, data: 'eA==' } + }) + } + stop() + return refs +} + +it('releases foreign stream frames while its own metadata is still pending', async () => { + const connection = createConnection() + const pending = readFileViaStream(connection.mux, '/held.txt') + const refs = sendForeignFrames(connection) + try { + expect(refs).toHaveLength(64) + await collect() + expect(refs.filter((ref) => ref.deref())).toHaveLength(0) + expect(connection.sent).toHaveLength(1) + } finally { + connection.feed({ + jsonrpc: '2.0', + id: 1, + result: { empty: true, totalSize: 0, isBinary: false } + }) + await pending + } +}) + +it('installs metadata before its own chunk and end in the same decoder turn', async () => { + const connection = createConnection() + const pending = readFileViaStream(connection.mux, '/own.txt') + const text = 'same turn 漢\u0000' + const data = Buffer.from(text) + connection.feed( + { + jsonrpc: '2.0', + id: 1, + result: { streamId: 3, totalSize: data.length, isBinary: false, resultEncoding: 'utf-8' } + }, + { + jsonrpc: '2.0', + method: 'fs.streamChunk', + params: { streamId: 3, seq: 0, data: data.toString('base64') } + }, + { jsonrpc: '2.0', method: 'fs.streamEnd', params: { streamId: 3 } } + ) + await expect(pending).resolves.toEqual({ content: text, isBinary: false }) + expect(connection.sent).toContainEqual({ + jsonrpc: '2.0', + method: 'fs.streamAck', + params: { streamId: 3, seq: 0 } + }) +}) + +it('preserves empty image metadata while ignoring earlier unknown stream identifiers', async () => { + const connection = createConnection() + const pending = readFileViaStream(connection.mux, '/empty.png') + connection.feed( + { jsonrpc: '2.0', method: 'fs.streamChunk', params: { streamId: -1, seq: 0, data: 'eA==' } }, + { + jsonrpc: '2.0', + id: 1, + result: { empty: true, totalSize: 0, isBinary: true, isImage: true, mimeType: 'image/png' } + } + ) + await expect(pending).resolves.toEqual({ + content: '', + isBinary: true, + isImage: true, + mimeType: 'image/png' + }) + expect(connection.sent).toHaveLength(1) +}) + +it('rejects metadata missing its stream identifier', async () => { + const connection = createConnection() + const pending = readFileViaStream(connection.mux, '/invalid.txt') + connection.feed({ jsonrpc: '2.0', id: 1, result: { totalSize: 1, isBinary: false } }) + await expect(pending).rejects.toBeInstanceOf(StreamProtocolError) + expect(connection.sent).toHaveLength(1) +}) + +it.each([-1, 51 * 1024 * 1024])( + 'rejects invalid or oversized totalSize %d and cancels the identified stream', + async (totalSize) => { + const connection = createConnection() + const pending = readFileViaStream(connection.mux, '/invalid.png') + connection.feed({ jsonrpc: '2.0', id: 1, result: { streamId: 3, totalSize, isBinary: true } }) + await expect(pending).rejects.toBeInstanceOf(FileReadCapExceededError) + expect(connection.sent).toContainEqual({ + jsonrpc: '2.0', + method: 'fs.cancelStream', + params: { streamId: 3 } + }) + } +) + +it('preserves a tighter caller cap before accepting adjacent data', async () => { + const connection = createConnection() + const pending = readFileViaStream(connection.mux, '/small.txt', { maxTextBytes: 1 }) + connection.feed( + { jsonrpc: '2.0', id: 1, result: { streamId: 3, totalSize: 2, isBinary: false } }, + { jsonrpc: '2.0', method: 'fs.streamChunk', params: { streamId: 3, seq: 0, data: 'eHg=' } } + ) + await expect(pending).rejects.toBeInstanceOf(FileReadCapExceededError) + expect(connection.sent).toHaveLength(2) +}) + +it('preserves an adjacent own-stream error after metadata', async () => { + const connection = createConnection() + const pending = readFileViaStream(connection.mux, '/removed.txt') + connection.feed( + { jsonrpc: '2.0', id: 1, result: { streamId: 3, totalSize: 1, isBinary: false } }, + { + jsonrpc: '2.0', + method: 'fs.streamError', + params: { streamId: 3, code: 'ENOENT', message: 'gone' } + } + ) + await expect(pending).rejects.toMatchObject({ code: 'ENOENT', message: 'gone' }) +}) + +it('preserves the provider fallback when an older relay has no streaming method', async () => { + const connection = createConnection() + const provider = new SshFilesystemProvider('test', connection.mux) + const pending = provider.readFile('/legacy.txt') + try { + connection.feed({ jsonrpc: '2.0', id: 1, error: { code: -32601, message: 'Method not found' } }) + await new Promise((resolve) => setImmediate(resolve)) + expect(connection.sent).toContainEqual({ + jsonrpc: '2.0', + id: 2, + method: 'fs.readFile', + params: { filePath: '/legacy.txt' } + }) + connection.feed({ jsonrpc: '2.0', id: 2, result: { content: 'legacy', isBinary: false } }) + await expect(pending).resolves.toEqual({ content: 'legacy', isBinary: false }) + } finally { + provider.dispose() + } +}) + +// Why: the metadata install moved from the mandatory resolve path to the optional +// beforeResolve hook, and the request timer is cleared before that hook runs. A mux +// that ignores the hook must fail the read, not leave it pending with no deadline. +it('fails the read when a multiplexer resolves without running beforeResolve', async () => { + const connection = createConnection() + vi.spyOn(connection.mux, 'request').mockResolvedValue({ + totalSize: 10, + isBinary: false, + streamId: 7 + }) + + await expect(readFileViaStream(connection.mux, '/no-hook.txt')).rejects.toBeInstanceOf( + StreamProtocolError + ) +})