diff --git a/src/main/agent-state-file-reader.test.ts b/src/main/agent-state-file-reader.test.ts index 34638fcbc06..66d39d07510 100644 --- a/src/main/agent-state-file-reader.test.ts +++ b/src/main/agent-state-file-reader.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, rmSync, truncateSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' -import { NodeFileReadTooLargeError } from '../shared/node-bounded-file-reader' +import { NodeFileReadTooLargeError } from '../shared/memory-safety/node-bounded-file-reader' import { MAX_AGENT_STATE_FILE_BYTES, MAX_AGENT_STATE_JSON_NESTING_DEPTH, diff --git a/src/main/agent-state-file-reader.ts b/src/main/agent-state-file-reader.ts index 0980fb5f935..1448de55f5c 100644 --- a/src/main/agent-state-file-reader.ts +++ b/src/main/agent-state-file-reader.ts @@ -1,5 +1,5 @@ -import { readNodeFileSyncWithinLimit } from '../shared/node-bounded-file-reader' -import { assertJsonTextStructureWithinLimits } from '../shared/json-text-structure-limit' +import { readNodeFileSyncWithinLimit } from '../shared/memory-safety/node-bounded-file-reader' +import { assertJsonTextStructureWithinLimits } from '../shared/memory-safety/json-text-structure-limit' export const MAX_AGENT_STATE_FILE_BYTES = 4 * 1024 * 1024 export const MAX_AGENT_STATE_JSON_STRUCTURAL_TOKENS = 1_000_000 diff --git a/src/main/daemon/terminal-history-file-reader.ts b/src/main/daemon/terminal-history-file-reader.ts index 237eca57bdd..4a67715fc46 100644 --- a/src/main/daemon/terminal-history-file-reader.ts +++ b/src/main/daemon/terminal-history-file-reader.ts @@ -1,7 +1,7 @@ import { readNodeFileSyncWithinLimit, readNodeFileWithinLimit -} from '../../shared/node-bounded-file-reader' +} from '../../shared/memory-safety/node-bounded-file-reader' export function readTerminalHistoryBuffer(filePath: string, maxBytes: number): Buffer { return readNodeFileSyncWithinLimit(filePath, maxBytes).buffer diff --git a/src/main/git/status.test.ts b/src/main/git/status.test.ts index 2cdc41dc367..e6b0795f0aa 100644 --- a/src/main/git/status.test.ts +++ b/src/main/git/status.test.ts @@ -1,7 +1,7 @@ /* eslint-disable max-lines -- Why: git status/discard/chunking behavior is verified together here to keep the command contract readable in one place. */ import { beforeEach, describe, expect, it, vi } from 'vitest' import type * as NodeFs from 'node:fs' -import type * as BoundedFileReader from '../../shared/node-bounded-file-reader' +import type * as BoundedFileReader from '../../shared/memory-safety/node-bounded-file-reader' import path from 'node:path' import { MAX_RENDERED_DIFF_COMBINED_CHARACTERS, @@ -63,7 +63,7 @@ vi.mock('fs', () => ({ existsSync: existsSyncMock })) -vi.mock('../../shared/node-bounded-file-reader', async (importOriginal) => { +vi.mock('../../shared/memory-safety/node-bounded-file-reader', async (importOriginal) => { const actual = await importOriginal() return { ...actual, diff --git a/src/renderer/src/components/terminal-pane/pty-eager-buffer-clamp.ts b/src/renderer/src/components/terminal-pane/pty-eager-buffer-clamp.ts index 0d4087e8288..347dd8372ac 100644 --- a/src/renderer/src/components/terminal-pane/pty-eager-buffer-clamp.ts +++ b/src/renderer/src/components/terminal-pane/pty-eager-buffer-clamp.ts @@ -1,4 +1,4 @@ -import { clampUtf8TextTail } from '../../../../shared/utf8-byte-limits' +import { clampUtf8TextTail } from '../../../../shared/memory-safety/utf8-byte-limits' export type EagerBufferChunk = { data: string diff --git a/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.test.ts b/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.test.ts index 7acbd6a5ff9..ae10944f857 100644 --- a/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.test.ts @@ -1,7 +1,7 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import { TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT } from '../../../../shared/terminal-scrollback-limits' import type { TerminalLayoutSnapshot } from '../../../../shared/types' -import { getUtf8ByteLength } from '../../../../shared/utf8-byte-limits' +import { getUtf8ByteLength } from '../../../../shared/memory-safety/utf8-byte-limits' const LEAF_ID = '11111111-1111-4111-8111-111111111111' as const const LEAF_ID_2 = '22222222-2222-4222-8222-222222222222' as const diff --git a/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.ts b/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.ts index 4e6d1698787..f6c1a228988 100644 --- a/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.ts +++ b/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.ts @@ -7,7 +7,7 @@ import { mergeCapturedLeafState } from './merge-captured-leaf-state' import { resolveTerminalLayoutActiveLeafId } from './terminal-layout-leaf-ids' import { TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT } from '../../../../shared/terminal-scrollback-limits' import { serializeWithAbsoluteCursor } from '../../../../shared/terminal-serialize-absolute-cursor' -import { measureUtf8ByteLength } from '../../../../shared/utf8-byte-limits' +import { measureUtf8ByteLength } from '../../../../shared/memory-safety/utf8-byte-limits' const MAX_BUFFER_BYTES = TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT diff --git a/src/shared/agent-hook-listener.ts b/src/shared/agent-hook-listener.ts index 4e6432a7458..4ec929a3f0d 100644 --- a/src/shared/agent-hook-listener.ts +++ b/src/shared/agent-hook-listener.ts @@ -63,7 +63,7 @@ import { resolveGrokSessionsDir } from './grok-session-paths' import { sweepStaleAgentHookEndpointTemps } from './agent-hook-endpoint-temp-cleanup' -import { assertJsonTextStructureWithinLimits } from './json-text-structure-limit' +import { assertJsonTextStructureWithinLimits } from './memory-safety/json-text-structure-limit' /** Maximum request body size accepted by the listener (1 MB). */ export const HOOK_REQUEST_MAX_BYTES = 1_000_000 diff --git a/src/shared/agent-status-types.ts b/src/shared/agent-status-types.ts index 1968ee0890f..b2367b74d59 100644 --- a/src/shared/agent-status-types.ts +++ b/src/shared/agent-status-types.ts @@ -9,7 +9,7 @@ import { normalizeOptionalMultilineField, normalizePromptField } from './agent-status-field-normalization' -import { assertJsonTextStructureWithinLimits } from './json-text-structure-limit' +import { assertJsonTextStructureWithinLimits } from './memory-safety/json-text-structure-limit' export { AGENT_STATUS_MAX_FIELD_LENGTH } from './agent-status-field-normalization' diff --git a/src/shared/browser-screencast-protocol.ts b/src/shared/browser-screencast-protocol.ts index 842fe86cb3d..e87b49ca1ae 100644 --- a/src/shared/browser-screencast-protocol.ts +++ b/src/shared/browser-screencast-protocol.ts @@ -1,4 +1,4 @@ -import { assertJsonTextStructureWithinLimits } from './json-text-structure-limit' +import { assertJsonTextStructureWithinLimits } from './memory-safety/json-text-structure-limit' const BROWSER_SCREENCAST_KIND = 0x62 const BROWSER_SCREENCAST_VERSION = 1 diff --git a/src/shared/commit-message-agent-spec.ts b/src/shared/commit-message-agent-spec.ts index 98ed09d681e..29244caa03e 100644 --- a/src/shared/commit-message-agent-spec.ts +++ b/src/shared/commit-message-agent-spec.ts @@ -1,6 +1,6 @@ import type { TuiAgent } from './types' import { isTuiAgentEnabled } from './tui-agent-selection' -import { assertJsonTextStructureWithinLimits } from './json-text-structure-limit' +import { assertJsonTextStructureWithinLimits } from './memory-safety/json-text-structure-limit' /* eslint-disable max-lines -- Why: this is the single registry for non-interactive commit-message agents, their model discovery parsers, and UI capabilities. */ diff --git a/src/shared/ephemeral-vm-recipes.ts b/src/shared/ephemeral-vm-recipes.ts index 0f68bc92f84..07ca904f4c6 100644 --- a/src/shared/ephemeral-vm-recipes.ts +++ b/src/shared/ephemeral-vm-recipes.ts @@ -5,7 +5,7 @@ import { MAX_SSH_RELAY_GRACE_PERIOD_SECONDS, MIN_SSH_RELAY_GRACE_PERIOD_SECONDS } from './ssh-types' -import { assertJsonTextStructureWithinLimits } from './json-text-structure-limit' +import { assertJsonTextStructureWithinLimits } from './memory-safety/json-text-structure-limit' // Why: ephemeral-vm-recipe-doctor imports Node's fs/path, so it must NOT be // re-exported through this barrel — the renderer/web-client imports this module // and would otherwise pull Node built-ins into the browser bundle (build fails). diff --git a/src/shared/ephemeral-vm-runtime-store.ts b/src/shared/ephemeral-vm-runtime-store.ts index ad26bb2950e..167ceef81d4 100644 --- a/src/shared/ephemeral-vm-runtime-store.ts +++ b/src/shared/ephemeral-vm-runtime-store.ts @@ -1,8 +1,8 @@ import { existsSync } from 'node:fs' import { join } from 'node:path' -import { JsonStringifyByteLimitError } from './node-bounded-json-stringify' -import { readNodeFileSyncWithinLimit } from './node-bounded-file-reader' -import { writeSecureJsonFileWithinLimit } from './bounded-secure-json-file' +import { JsonStringifyByteLimitError } from './memory-safety/node-bounded-json-stringify' +import { readNodeFileSyncWithinLimit } from './memory-safety/node-bounded-file-reader' +import { writeSecureJsonFileWithinLimit } from './memory-safety/bounded-secure-json-file' import { hardenExistingSecureFile } from './secure-file' import { EphemeralVmRuntimeRecordSchema, diff --git a/src/shared/external-automation-jobs-file.ts b/src/shared/external-automation-jobs-file.ts index 1013b8a0a53..1f3b9267c3b 100644 --- a/src/shared/external-automation-jobs-file.ts +++ b/src/shared/external-automation-jobs-file.ts @@ -1,8 +1,11 @@ -import { NodeFileReadTooLargeError, readNodeFileWithinLimit } from './node-bounded-file-reader' +import { + NodeFileReadTooLargeError, + readNodeFileWithinLimit +} from './memory-safety/node-bounded-file-reader' import { assertJsonTextStructureWithinLimits, type JsonTextStructureLimits -} from './json-text-structure-limit' +} from './memory-safety/json-text-structure-limit' export const EXTERNAL_AUTOMATION_JOBS_FILE_MAX_BYTES = 8 * 1024 * 1024 export const EXTERNAL_AUTOMATION_JOBS_MAX_ENTRIES = 10_000 diff --git a/src/shared/fetch-response-body.ts b/src/shared/fetch-response-body.ts index e9467fd3a2d..bdd168dde35 100644 --- a/src/shared/fetch-response-body.ts +++ b/src/shared/fetch-response-body.ts @@ -1,7 +1,7 @@ import { assertJsonTextStructureWithinLimits, type JsonTextStructureLimits -} from './json-text-structure-limit' +} from './memory-safety/json-text-structure-limit' const INITIAL_RESPONSE_CAPACITY_BYTES = 64 * 1024 diff --git a/src/shared/git-uncommitted-line-stats.test.ts b/src/shared/git-uncommitted-line-stats.test.ts index 0e2d549868f..8c50f0676a7 100644 --- a/src/shared/git-uncommitted-line-stats.test.ts +++ b/src/shared/git-uncommitted-line-stats.test.ts @@ -7,7 +7,7 @@ const { lstatMock, readFileMock } = vi.hoisted(() => ({ vi.mock('fs/promises', () => ({ lstat: lstatMock })) -vi.mock('./node-bounded-file-reader', () => ({ +vi.mock('./memory-safety/node-bounded-file-reader', () => ({ readNodeFileWithinLimit: async (path: string) => ({ buffer: await readFileMock(path), stats: mockFileStat(0) diff --git a/src/shared/git-uncommitted-line-stats.ts b/src/shared/git-uncommitted-line-stats.ts index 3896d9354c3..e13ceb8f730 100644 --- a/src/shared/git-uncommitted-line-stats.ts +++ b/src/shared/git-uncommitted-line-stats.ts @@ -4,7 +4,7 @@ import { isBinaryBuffer } from './binary-buffer' import { decodeGitCQuotedPath } from './git-cquoted-path' import { DEFAULT_GIT_STATUS_LIMIT } from './git-status-limit' import { iterateNulDelimitedFields } from './nul-delimited-fields' -import { readNodeFileWithinLimit } from './node-bounded-file-reader' +import { readNodeFileWithinLimit } from './memory-safety/node-bounded-file-reader' export type GitLineStats = { added?: number; removed?: number } diff --git a/src/shared/html-to-pdf-memory-limit.ts b/src/shared/html-to-pdf-memory-limit.ts index 061d60eb430..2e195bbd49c 100644 --- a/src/shared/html-to-pdf-memory-limit.ts +++ b/src/shared/html-to-pdf-memory-limit.ts @@ -1,4 +1,4 @@ -import { measureUtf8ByteLength } from './utf8-byte-limits' +import { measureUtf8ByteLength } from './memory-safety/utf8-byte-limits' export const HTML_TO_PDF_MAX_INPUT_BYTES = 32 * 1024 * 1024 export const HTML_TO_PDF_MEMORY_LIMIT_ERROR = 'HTML export exceeds the PDF memory limit' diff --git a/src/shared/linux-proc-port-scan-limits.ts b/src/shared/linux-proc-port-scan-limits.ts index dd1f9fd5c6e..7b4a0fdfb53 100644 --- a/src/shared/linux-proc-port-scan-limits.ts +++ b/src/shared/linux-proc-port-scan-limits.ts @@ -1,4 +1,4 @@ -import { readNodeFileWithinLimit } from './node-bounded-file-reader' +import { readNodeFileWithinLimit } from './memory-safety/node-bounded-file-reader' export const LINUX_PROC_NETWORK_TABLE_MAX_BYTES = 8 * 1024 * 1024 export const LINUX_PROC_LISTENING_SOCKET_MAX_ENTRIES = 2_048 diff --git a/src/shared/markdown-document-listing-limits.ts b/src/shared/markdown-document-listing-limits.ts index 9c9b39c49b3..5c9f111c820 100644 --- a/src/shared/markdown-document-listing-limits.ts +++ b/src/shared/markdown-document-listing-limits.ts @@ -1,5 +1,5 @@ import type { MarkdownDocument } from './types' -import { measureUtf8ByteLength } from './utf8-byte-limits' +import { measureUtf8ByteLength } from './memory-safety/utf8-byte-limits' export const MARKDOWN_DOCUMENT_LISTING_MAX_DOCUMENTS = 20_000 export const MARKDOWN_DOCUMENT_LISTING_MAX_METADATA_BYTES = 8 * 1024 * 1024 diff --git a/src/shared/mcp-config-inspection-limits.ts b/src/shared/mcp-config-inspection-limits.ts index 221a350825a..cd717dcdd5f 100644 --- a/src/shared/mcp-config-inspection-limits.ts +++ b/src/shared/mcp-config-inspection-limits.ts @@ -1,4 +1,4 @@ -import { measureUtf8ByteLength } from './utf8-byte-limits' +import { measureUtf8ByteLength } from './memory-safety/utf8-byte-limits' export const MCP_CONFIG_INSPECTION_MAX_BYTES = 256 * 1024 export const MCP_CONFIG_INSPECTION_MAX_CODE_UNITS = 256 * 1024 diff --git a/src/shared/memory-safety/README.md b/src/shared/memory-safety/README.md new file mode 100644 index 00000000000..50f7224a8b2 --- /dev/null +++ b/src/shared/memory-safety/README.md @@ -0,0 +1,72 @@ +# Memory safety + +Shared primitives that stop unbounded memory growth. Renderer OOM is Orca's dominant crash class +(#9984): heap climbs to V8's ~3.5 GB ceiling and the process is killed. The reported reproduction +(#9872) was a single unbounded `Map` that also spread-copied itself on every update. + +**Read this before adding a new limit, and before writing code that reads or accumulates data.** + +## The rule: bound at ingress, not in the interior + +Put a ceiling where untrusted or unpredictably-large data first enters memory: + +- **Files** — anything read from disk, a repo, or a remote host +- **Transport** — IPC, WebSocket frames, relay/SSH payloads, subprocess output +- **Parsing** — JSON/YAML/CSV/notebook text, base64, image headers +- **Enumeration** — directory walks, provider pagination, session scans +- **Long-lived accumulators** — caches and maps keyed by something a user can grow without limit + (pane IDs, session IDs, paths) that live for the process lifetime + +Do **not** add ceilings to interior data structures whose size is already implied by a bounded +input. A second ceiling behind an existing one is not defense in depth; it is a second thing that +can be wrong, and every ceiling is a new user-visible failure mode when it is hit. + +If you cannot name the untrusted input and the bound it crosses, you do not need a limit. + +### Never copy an accumulator on every update + +The crash that started this (#9872) was not only an unbounded `Map` — it also rebuilt itself on every +status ping (`state = { ...state, [key]: value }`). Copy-per-update turns a large collection into +repeated large allocations, so it reaches the heap ceiling far sooner than its size suggests. Mutate +in place, or use `BoundedMap`. No lint rule catches this; it is on you to notice. + +## What's here + +| Module | Use it for | +| -------------------------------- | ----------------------------------------------------------------------------------------- | +| `bounded-map.ts` | A cache/map that must not grow forever. Count and/or byte ceiling with LRU eviction. | +| `node-bounded-file-reader.ts` | Reading a file whose size you do not control. Checks before allocating and while growing. | +| `node-bounded-json-stringify.ts` | Serializing to JSON when the result could be huge. | +| `bounded-secure-json-file.ts` | Writing a secret-bearing JSON file within a byte ceiling. | +| `json-text-structure-limit.ts` | Guarding `JSON.parse` against depth/token amplification before it runs. | +| `utf8-byte-limits.ts` | Measuring/truncating UTF-8 without materializing the whole string. | + +Domain-specific ceilings (e.g. `git-status-limit.ts`, `terminal-scrollback-limits.ts`) live next to +the code they bound, not here. This folder is only for primitives reused across domains. + +## Rules for a new ceiling + +1. **Set it far above real use.** A limit a normal user reaches is a bug. Ordinary large repos, long + terminal sessions, and big diffs must pass untouched. State the headroom in a comment. +2. **Fail closed, and never crash a remote host.** An unmeasurable size rejects the item; it must not + poison a ledger or throw into a relay running on someone else's machine. +3. **Decide overload behavior explicitly** — reject, evict, or truncate — and document which. + Silent truncation of user data is the worst option; prefer a visible, recoverable failure. +4. **Release on every exit path**, including throw, abort, disconnect, and renderer teardown. Pair + acquisition with `try`/`finally`; do not rely on the happy path. +5. **Test the exact boundary and boundary+1**, through every entry point — including hydration, + restore, and replay paths, not just the create path. + +## Enforcement + +None of this is lint-enforced today: `readFile`, `JSON.parse`/`stringify`, unbounded `fetch` body +reads, `createReadStream`, `readdir`, recursion depth, copy-per-update amplification, and +accumulator growth are all caught by review, not tooling. The rules above are the standard a +reviewer should hold you to. + +## Before you add one + +Prefer fixing the actual growth. The real crash was fixed by capping one map that was already known +to grow, found via the heap-highwater breadcrumbs in `renderer_memory_highwater` (#9984). Measure +first: if you cannot point at evidence that something grows, a new ceiling is speculation, and +speculation here costs review burden, user-visible failure modes, and code that outlives its reason. diff --git a/src/shared/bounded-map.test.ts b/src/shared/memory-safety/bounded-map.test.ts similarity index 100% rename from src/shared/bounded-map.test.ts rename to src/shared/memory-safety/bounded-map.test.ts diff --git a/src/shared/bounded-map.ts b/src/shared/memory-safety/bounded-map.ts similarity index 100% rename from src/shared/bounded-map.ts rename to src/shared/memory-safety/bounded-map.ts diff --git a/src/shared/bounded-secure-json-file.ts b/src/shared/memory-safety/bounded-secure-json-file.ts similarity index 85% rename from src/shared/bounded-secure-json-file.ts rename to src/shared/memory-safety/bounded-secure-json-file.ts index c9da2b9ce6f..46a5b1ec706 100644 --- a/src/shared/bounded-secure-json-file.ts +++ b/src/shared/memory-safety/bounded-secure-json-file.ts @@ -1,5 +1,5 @@ import { stringifyJsonWithinByteLimit } from './node-bounded-json-stringify' -import { writeSecureFile } from './secure-file' +import { writeSecureFile } from '../secure-file' export function writeSecureJsonFileWithinLimit( targetPath: string, diff --git a/src/shared/json-text-structure-limit.test.ts b/src/shared/memory-safety/json-text-structure-limit.test.ts similarity index 100% rename from src/shared/json-text-structure-limit.test.ts rename to src/shared/memory-safety/json-text-structure-limit.test.ts diff --git a/src/shared/json-text-structure-limit.ts b/src/shared/memory-safety/json-text-structure-limit.ts similarity index 100% rename from src/shared/json-text-structure-limit.ts rename to src/shared/memory-safety/json-text-structure-limit.ts diff --git a/src/shared/node-bounded-file-reader.test.ts b/src/shared/memory-safety/node-bounded-file-reader.test.ts similarity index 100% rename from src/shared/node-bounded-file-reader.test.ts rename to src/shared/memory-safety/node-bounded-file-reader.test.ts diff --git a/src/shared/node-bounded-file-reader.ts b/src/shared/memory-safety/node-bounded-file-reader.ts similarity index 100% rename from src/shared/node-bounded-file-reader.ts rename to src/shared/memory-safety/node-bounded-file-reader.ts diff --git a/src/shared/node-bounded-json-stringify.test.ts b/src/shared/memory-safety/node-bounded-json-stringify.test.ts similarity index 100% rename from src/shared/node-bounded-json-stringify.test.ts rename to src/shared/memory-safety/node-bounded-json-stringify.test.ts diff --git a/src/shared/node-bounded-json-stringify.ts b/src/shared/memory-safety/node-bounded-json-stringify.ts similarity index 100% rename from src/shared/node-bounded-json-stringify.ts rename to src/shared/memory-safety/node-bounded-json-stringify.ts diff --git a/src/shared/utf8-byte-limits.ts b/src/shared/memory-safety/utf8-byte-limits.ts similarity index 100% rename from src/shared/utf8-byte-limits.ts rename to src/shared/memory-safety/utf8-byte-limits.ts diff --git a/src/shared/node-bounded-file-reader-sync.test.ts b/src/shared/node-bounded-file-reader-sync.test.ts index 748969e13ba..97f37cf8757 100644 --- a/src/shared/node-bounded-file-reader-sync.test.ts +++ b/src/shared/node-bounded-file-reader-sync.test.ts @@ -2,7 +2,10 @@ import { closeSync, ftruncateSync, mkdtempSync, openSync, rmSync, writeFileSync import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' -import { NodeFileReadTooLargeError, readNodeFileSyncWithinLimit } from './node-bounded-file-reader' +import { + NodeFileReadTooLargeError, + readNodeFileSyncWithinLimit +} from './memory-safety/node-bounded-file-reader' const tempDirectories: string[] = [] diff --git a/src/shared/orca-yaml-file-limit.ts b/src/shared/orca-yaml-file-limit.ts index 8f2641637a1..05f439cba38 100644 --- a/src/shared/orca-yaml-file-limit.ts +++ b/src/shared/orca-yaml-file-limit.ts @@ -1,4 +1,4 @@ -import { measureUtf8ByteLength } from './utf8-byte-limits' +import { measureUtf8ByteLength } from './memory-safety/utf8-byte-limits' export const MAX_ORCA_YAML_BYTES = 256 * 1024 export const MAX_ORCA_YAML_CODE_UNITS = 256 * 1024 diff --git a/src/shared/persisted-state-file-bounds.test.ts b/src/shared/persisted-state-file-bounds.test.ts index e6ee2de1878..6dd997a4321 100644 --- a/src/shared/persisted-state-file-bounds.test.ts +++ b/src/shared/persisted-state-file-bounds.test.ts @@ -10,8 +10,8 @@ import { import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { NodeFileReadTooLargeError } from './node-bounded-file-reader' -import { JsonStringifyByteLimitError } from './node-bounded-json-stringify' +import { NodeFileReadTooLargeError } from './memory-safety/node-bounded-file-reader' +import { JsonStringifyByteLimitError } from './memory-safety/node-bounded-json-stringify' import { PersistedStateSecretCapacityError, assertPersistedStateSecretWithinLimit, diff --git a/src/shared/persisted-state-file-bounds.ts b/src/shared/persisted-state-file-bounds.ts index 4fe8a977bb3..f11a38a1633 100644 --- a/src/shared/persisted-state-file-bounds.ts +++ b/src/shared/persisted-state-file-bounds.ts @@ -5,15 +5,15 @@ import { NodeFileReadTooLargeError, readNodeFileSyncWithinLimit, type BoundedNodeFileRead -} from './node-bounded-file-reader' +} from './memory-safety/node-bounded-file-reader' import { JsonStringifyByteLimitError, stringifyJsonWithinByteLimit -} from './node-bounded-json-stringify' +} from './memory-safety/node-bounded-json-stringify' import { assertJsonTextStructureWithinLimits, type JsonTextStructureLimits -} from './json-text-structure-limit' +} from './memory-safety/json-text-structure-limit' export const ORCA_PERSISTED_STATE_MAX_BYTES = 64 * 1024 * 1024 export const ORCA_PERSISTED_STATE_SECRET_MAX_BYTES = 4 * 1024 * 1024 diff --git a/src/shared/pull-request-generation.ts b/src/shared/pull-request-generation.ts index 073b7f4333f..2c063b703bb 100644 --- a/src/shared/pull-request-generation.ts +++ b/src/shared/pull-request-generation.ts @@ -1,5 +1,5 @@ import { truncateDiffForPrompt } from './commit-message-prompt' -import { assertJsonTextStructureWithinLimits } from './json-text-structure-limit' +import { assertJsonTextStructureWithinLimits } from './memory-safety/json-text-structure-limit' export const GENERATED_PULL_REQUEST_JSON_STRUCTURE_LIMITS = { structuralTokens: 64, diff --git a/src/shared/quick-open-install-rg.ts b/src/shared/quick-open-install-rg.ts index 7815d7d6e34..f8fb07c98f4 100644 --- a/src/shared/quick-open-install-rg.ts +++ b/src/shared/quick-open-install-rg.ts @@ -1,4 +1,4 @@ -import { readNodeFileWithinLimit } from './node-bounded-file-reader' +import { readNodeFileWithinLimit } from './memory-safety/node-bounded-file-reader' import { getProcessOutputFields, iterateProcessOutputLines } from './process-output-field-scanner' const GENERIC_LINUX_RIPGREP_INSTALL = diff --git a/src/shared/relay-json-admission.ts b/src/shared/relay-json-admission.ts index a9d256ba904..e7bb1b0a450 100644 --- a/src/shared/relay-json-admission.ts +++ b/src/shared/relay-json-admission.ts @@ -1,4 +1,4 @@ -import { assertJsonTextStructureWithinLimits } from './json-text-structure-limit' +import { assertJsonTextStructureWithinLimits } from './memory-safety/json-text-structure-limit' export const RELAY_JSON_MAX_STRUCTURAL_TOKENS = 1_000_000 export const RELAY_JSON_MAX_NESTING_DEPTH = 128 diff --git a/src/shared/relay-version-marker.test.ts b/src/shared/relay-version-marker.test.ts index 36955399518..6c6ca3c125e 100644 --- a/src/shared/relay-version-marker.test.ts +++ b/src/shared/relay-version-marker.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, rmSync, truncateSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' -import { NodeFileReadTooLargeError } from './node-bounded-file-reader' +import { NodeFileReadTooLargeError } from './memory-safety/node-bounded-file-reader' import { RELAY_VERSION_MARKER_MAX_BYTES, readRelayVersionMarkerSync } from './relay-version-marker' const roots: string[] = [] diff --git a/src/shared/relay-version-marker.ts b/src/shared/relay-version-marker.ts index 68be008ff8d..a74873f3486 100644 --- a/src/shared/relay-version-marker.ts +++ b/src/shared/relay-version-marker.ts @@ -1,4 +1,4 @@ -import { readNodeFileSyncWithinLimit } from './node-bounded-file-reader' +import { readNodeFileSyncWithinLimit } from './memory-safety/node-bounded-file-reader' export const RELAY_VERSION_MARKER_MAX_BYTES = 4 * 1024 diff --git a/src/shared/remote-runtime-memory-limits.ts b/src/shared/remote-runtime-memory-limits.ts index 62cb62d03cf..2274d86d4f4 100644 --- a/src/shared/remote-runtime-memory-limits.ts +++ b/src/shared/remote-runtime-memory-limits.ts @@ -1,7 +1,7 @@ import { JsonStringifyByteLimitError, stringifyJsonWithinByteLimit -} from './node-bounded-json-stringify' +} from './memory-safety/node-bounded-json-stringify' import { RemoteRuntimeClientError } from './remote-runtime-client-error' export const REMOTE_RUNTIME_MAX_OUTBOUND_JSON_BYTES = 4 * 1024 * 1024 diff --git a/src/shared/remote-runtime-request-frames.ts b/src/shared/remote-runtime-request-frames.ts index 811ba8c26c9..e573e7feca4 100644 --- a/src/shared/remote-runtime-request-frames.ts +++ b/src/shared/remote-runtime-request-frames.ts @@ -4,7 +4,7 @@ import { isKeepaliveFrame } from './runtime-rpc-envelope' import { RemoteRuntimeClientError } from './remote-runtime-client-error' -import { assertJsonTextStructureWithinLimits } from './json-text-structure-limit' +import { assertJsonTextStructureWithinLimits } from './memory-safety/json-text-structure-limit' export const REMOTE_RUNTIME_JSON_STRUCTURE_LIMITS = { structuralTokens: 256 * 1024, diff --git a/src/shared/runtime-bootstrap.ts b/src/shared/runtime-bootstrap.ts index efa224d749b..b299c539787 100644 --- a/src/shared/runtime-bootstrap.ts +++ b/src/shared/runtime-bootstrap.ts @@ -1,5 +1,5 @@ import { join } from 'node:path' -import { assertJsonTextStructureWithinLimits } from './json-text-structure-limit' +import { assertJsonTextStructureWithinLimits } from './memory-safety/json-text-structure-limit' export type RuntimeTransportMetadata = | { diff --git a/src/shared/runtime-environment-store.ts b/src/shared/runtime-environment-store.ts index 4211eb18da8..e8d73547b3c 100644 --- a/src/shared/runtime-environment-store.ts +++ b/src/shared/runtime-environment-store.ts @@ -1,10 +1,10 @@ import { randomUUID } from 'node:crypto' import { existsSync } from 'node:fs' import { join } from 'node:path' -import { JsonStringifyByteLimitError } from './node-bounded-json-stringify' -import { readNodeFileSyncWithinLimit } from './node-bounded-file-reader' +import { JsonStringifyByteLimitError } from './memory-safety/node-bounded-json-stringify' +import { readNodeFileSyncWithinLimit } from './memory-safety/node-bounded-file-reader' import { parsePairingCode, type PairingOffer } from './pairing' -import { writeSecureJsonFileWithinLimit } from './bounded-secure-json-file' +import { writeSecureJsonFileWithinLimit } from './memory-safety/bounded-secure-json-file' import { hardenExistingSecureFile } from './secure-file' import { createEnvironmentFromPairingOffer, diff --git a/src/shared/serve-update-handoff.ts b/src/shared/serve-update-handoff.ts index 3dddd2a38da..05bb5b3b9a4 100644 --- a/src/shared/serve-update-handoff.ts +++ b/src/shared/serve-update-handoff.ts @@ -1,5 +1,5 @@ import { join } from 'node:path' -import { assertJsonTextStructureWithinLimits } from './json-text-structure-limit' +import { assertJsonTextStructureWithinLimits } from './memory-safety/json-text-structure-limit' export const SERVE_UPDATE_HANDOFF_PATH_ENV = 'ORCA_SERVE_UPDATE_HANDOFF_PATH' export const SERVE_UPDATE_HANDOFF_FILE = 'serve-update-handoff.json' diff --git a/src/shared/setup-script-import-codex-environment.ts b/src/shared/setup-script-import-codex-environment.ts index 77a8e48296d..543e20ee089 100644 --- a/src/shared/setup-script-import-codex-environment.ts +++ b/src/shared/setup-script-import-codex-environment.ts @@ -6,7 +6,7 @@ import { SETUP_SCRIPT_IMPORT_MAX_TOML_LINES, SETUP_SCRIPT_IMPORT_MAX_UNSUPPORTED_FIELDS } from './setup-script-import-limits' -import { measureUtf8ByteLength } from './utf8-byte-limits' +import { measureUtf8ByteLength } from './memory-safety/utf8-byte-limits' const CODEX_ENVIRONMENT_PATH = '.codex/environments/environment.toml' diff --git a/src/shared/setup-script-import-limits.ts b/src/shared/setup-script-import-limits.ts index d409586db85..556310f3074 100644 --- a/src/shared/setup-script-import-limits.ts +++ b/src/shared/setup-script-import-limits.ts @@ -1,4 +1,4 @@ -import { measureUtf8ByteLength } from './utf8-byte-limits' +import { measureUtf8ByteLength } from './memory-safety/utf8-byte-limits' export const SETUP_SCRIPT_IMPORT_FILE_MAX_BYTES = 256 * 1024 export const SETUP_SCRIPT_IMPORT_MAX_CODE_UNITS = 256 * 1024 diff --git a/src/shared/ssh-retained-payload-admission.test.ts b/src/shared/ssh-retained-payload-admission.test.ts index 92a4e6c90db..e30e885ae94 100644 --- a/src/shared/ssh-retained-payload-admission.test.ts +++ b/src/shared/ssh-retained-payload-admission.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { getUtf8ByteLength } from './utf8-byte-limits' +import { getUtf8ByteLength } from './memory-safety/utf8-byte-limits' import { admitSshConnectionState, admitSshDetectedPorts, diff --git a/src/shared/ssh-retained-payload-admission.ts b/src/shared/ssh-retained-payload-admission.ts index 21b51787a1d..1cd6afec88c 100644 --- a/src/shared/ssh-retained-payload-admission.ts +++ b/src/shared/ssh-retained-payload-admission.ts @@ -1,5 +1,5 @@ import type { EnrichedDetectedPort, SshConnectionState, SshConnectionStatus } from './ssh-types' -import { clampUtf8TextPrefix, measureUtf8ByteLength } from './utf8-byte-limits' +import { clampUtf8TextPrefix, measureUtf8ByteLength } from './memory-safety/utf8-byte-limits' export const SSH_RETAINED_IDENTIFIER_MAX_UTF8_BYTES = 1024 export const SSH_CONNECTION_ERROR_MAX_UTF8_BYTES = 16 * 1024 diff --git a/src/shared/terminal-stream-protocol.ts b/src/shared/terminal-stream-protocol.ts index 76e82eafce9..acb9c7b6ce8 100644 --- a/src/shared/terminal-stream-protocol.ts +++ b/src/shared/terminal-stream-protocol.ts @@ -1,4 +1,4 @@ -import { assertJsonTextStructureWithinLimits } from './json-text-structure-limit' +import { assertJsonTextStructureWithinLimits } from './memory-safety/json-text-structure-limit' const TERMINAL_STREAM_KIND = 0x74 const TERMINAL_STREAM_VERSION = 1 diff --git a/src/shared/text-search.ts b/src/shared/text-search.ts index d3d9b40226a..74c7080624f 100644 --- a/src/shared/text-search.ts +++ b/src/shared/text-search.ts @@ -11,7 +11,7 @@ * Design doc: docs/design/share-text-search.md. */ import { posix, win32 } from 'node:path' -import { assertJsonTextStructureWithinLimits } from './json-text-structure-limit' +import { assertJsonTextStructureWithinLimits } from './memory-safety/json-text-structure-limit' import { normalizeSearchResult } from './search-match-count' import { escapeRegex } from './string-utils' import type { SearchFileResult, SearchMatch, SearchOptions, SearchResult } from './types' diff --git a/src/shared/workspace-session-terminal-buffers.test.ts b/src/shared/workspace-session-terminal-buffers.test.ts index e5c868cd691..f480b47feb2 100644 --- a/src/shared/workspace-session-terminal-buffers.test.ts +++ b/src/shared/workspace-session-terminal-buffers.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import { FLOATING_TERMINAL_WORKTREE_ID } from './constants' import type { WorkspaceSessionState } from './types' import { TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT } from './terminal-scrollback-limits' -import { getUtf8ByteLength } from './utf8-byte-limits' +import { getUtf8ByteLength } from './memory-safety/utf8-byte-limits' import { pruneLocalTerminalScrollbackBuffers, shouldPreserveTerminalScrollbackBuffers diff --git a/src/shared/workspace-session-terminal-buffers.ts b/src/shared/workspace-session-terminal-buffers.ts index 81ab8315c72..503074a12ea 100644 --- a/src/shared/workspace-session-terminal-buffers.ts +++ b/src/shared/workspace-session-terminal-buffers.ts @@ -2,7 +2,7 @@ import type { Repo, WorkspaceSessionState } from './types' import { FLOATING_TERMINAL_WORKTREE_ID } from './constants' import { getRepoIdFromWorktreeId } from './worktree-id' import { TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT } from './terminal-scrollback-limits' -import { clampUtf8TextTail, measureUtf8ByteLength } from './utf8-byte-limits' +import { clampUtf8TextTail, measureUtf8ByteLength } from './memory-safety/utf8-byte-limits' import { parseExecutionHostId } from './execution-host' export type RepoConnection = Pick