refactor(memory-safety): scope bounds to wired ingress and add a ratchet

Audit of the merged OOM foundation found 13 modules with no production
consumer and BoundedMap with none at all: the bounds landed ahead of the
code that uses them.

- Delete the 13 unwired modules; they return with the slice that consumes them.
- Migrate secure-path-hardening-cache, github/conflict-summary-cache and
  runtime/orca-runtime onto BoundedMap, deleting both duplicate
  setBoundedMapEntry helpers so the primitive is net-negative on lines.
- Move the six generic primitives to src/shared/memory-safety/ with a README
  stating the bound-at-ingress doctrine; domain limits stay with their domain.
- Add check:bounded-ingress, a baseline-locked ratchet that fails new
  unbounded readFile/response body/JSON.parse/fan-out unless it uses a
  memory-safety helper or carries a bounded-by: justification.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-07-25 02:34:05 -07:00
co-authored by Orca
parent f009500677
commit 002d959075
75 changed files with 1081 additions and 1944 deletions
+728
View File
@@ -0,0 +1,728 @@
{
"allowed": [
"mobile/src/cache/home-snapshot-cache.ts:json-parse",
"mobile/src/cache/home-snapshot-cache.ts:json-stringify",
"mobile/src/components/CustomKeyModal.tsx:json-parse",
"mobile/src/components/CustomKeyModal.tsx:json-stringify",
"mobile/src/components/MobileRichMarkdownEditor.tsx:json-parse",
"mobile/src/components/mobile-rich-markdown-editor-html.ts:json-stringify",
"mobile/src/components/pr-sidebar/MermaidDiagram.tsx:json-stringify",
"mobile/src/files/mobile-file-preview-source.ts:json-stringify",
"mobile/src/session/MobileNativeChatView.tsx:json-stringify",
"mobile/src/session/mobile-image-source-picker.ts:response-text",
"mobile/src/session/mobile-native-chat-permission.ts:json-parse",
"mobile/src/session/mobile-native-chat-question.ts:json-stringify",
"mobile/src/session/mobile-terminal-records.ts:json-stringify",
"mobile/src/session/use-mobile-native-chat-ask-dismiss.ts:json-stringify",
"mobile/src/storage/codex-reset-attempt-journal.ts:json-parse",
"mobile/src/storage/codex-reset-attempt-journal.ts:json-stringify",
"mobile/src/storage/preferences.ts:json-parse",
"mobile/src/storage/preferences.ts:json-stringify",
"mobile/src/storage/session-view-preferences.ts:json-parse",
"mobile/src/storage/session-view-preferences.ts:json-stringify",
"mobile/src/terminal/TerminalWebView.tsx:json-parse",
"mobile/src/terminal/TerminalWebView.tsx:json-stringify",
"mobile/src/terminal/terminal-accessory-layout.ts:json-parse",
"mobile/src/terminal/terminal-accessory-layout.ts:json-stringify",
"mobile/src/terminal/terminal-webview-engine.generated.ts:json-parse",
"mobile/src/terminal/terminal-webview-engine.generated.ts:json-stringify",
"mobile/src/terminal/terminal-webview-html.ts:json-parse",
"mobile/src/terminal/terminal-webview-html.ts:json-stringify",
"mobile/src/terminal/terminal-webview-url-tap.ts:json-stringify",
"mobile/src/transport/browser-screencast-protocol.ts:json-parse",
"mobile/src/transport/host-credential-cleanup.ts:json-parse",
"mobile/src/transport/host-credential-cleanup.ts:json-stringify",
"mobile/src/transport/host-store.ts:json-parse",
"mobile/src/transport/host-store.ts:json-stringify",
"mobile/src/transport/mobile-e2ee-v2-physical-channel.ts:json-parse",
"mobile/src/transport/mobile-e2ee-v2-physical-channel.ts:json-stringify",
"mobile/src/transport/mobile-relay-credential-bundle.ts:json-parse",
"mobile/src/transport/mobile-relay-credential-bundle.ts:json-stringify",
"mobile/src/transport/mobile-relay-credential-rotation.ts:json-stringify",
"mobile/src/transport/mobile-relay-direct-upgrade-journal.ts:json-parse",
"mobile/src/transport/mobile-relay-direct-upgrade-journal.ts:json-stringify",
"mobile/src/transport/mobile-relay-direct-upgrade.ts:json-stringify",
"mobile/src/transport/mobile-relay-e2ee-link.ts:json-parse",
"mobile/src/transport/mobile-relay-e2ee-link.ts:json-stringify",
"mobile/src/transport/mobile-relay-host-overlay-store.ts:json-parse",
"mobile/src/transport/mobile-relay-host-overlay-store.ts:json-stringify",
"mobile/src/transport/mobile-relay-invite-director.ts:json-parse",
"mobile/src/transport/mobile-relay-invite-director.ts:json-stringify",
"mobile/src/transport/mobile-relay-pairing-journal-store.ts:json-parse",
"mobile/src/transport/mobile-relay-pairing-journal-store.ts:json-stringify",
"mobile/src/transport/mobile-relay-pairing-journal.ts:json-stringify",
"mobile/src/transport/mobile-relay-pairing-recovery.ts:json-stringify",
"mobile/src/transport/mobile-relay-physical-client.ts:json-parse",
"mobile/src/transport/mobile-relay-physical-client.ts:json-stringify",
"mobile/src/transport/mobile-relay-resume-director.ts:json-parse",
"mobile/src/transport/mobile-relay-resume-director.ts:json-stringify",
"mobile/src/transport/mobile-relay-resume-director.ts:response-text",
"mobile/src/transport/mobile-relay-rpc-session.ts:json-parse",
"mobile/src/transport/mobile-relay-rpc-session.ts:json-stringify",
"mobile/src/transport/pairing.ts:json-parse",
"mobile/src/transport/pre-profile-pairing-coordinator.ts:json-stringify",
"mobile/src/transport/rpc-client.ts:json-parse",
"mobile/src/transport/rpc-client.ts:json-stringify",
"mobile/src/transport/socket-event-debug.ts:json-stringify",
"mobile/src/transport/terminal-stream-protocol.ts:json-parse",
"mobile/src/transport/websocket-payload-bytes.ts:response-text",
"mobile/src/worktree/last-visited-worktree-repo.ts:json-parse",
"src/cli/bundled-skill-guides.ts:json-parse",
"src/cli/bundled-skill-guides.ts:json-stringify",
"src/cli/bundled-skill-guides.ts:read-file",
"src/cli/format.ts:json-stringify",
"src/cli/handlers/agent-hooks.ts:json-parse",
"src/cli/handlers/agent-hooks.ts:json-stringify",
"src/cli/handlers/agent-hooks.ts:read-file",
"src/cli/handlers/automations.ts:json-parse",
"src/cli/handlers/browser-env.ts:json-stringify",
"src/cli/handlers/browser-interact.ts:json-stringify",
"src/cli/handlers/browser-nav.ts:json-stringify",
"src/cli/handlers/browser-storage.ts:json-stringify",
"src/cli/handlers/browser-tab.ts:json-stringify",
"src/cli/handlers/emulator.ts:json-parse",
"src/cli/handlers/emulator.ts:json-stringify",
"src/cli/handlers/introspection.ts:json-stringify",
"src/cli/handlers/orchestration.ts:json-stringify",
"src/cli/handlers/skills.ts:json-stringify",
"src/cli/handlers/vm.ts:json-stringify",
"src/cli/handlers/vm.ts:read-file",
"src/cli/linear-request-builders.ts:read-file",
"src/cli/runtime/mac-app-update-bundle.ts:read-file",
"src/cli/runtime/metadata.ts:json-parse",
"src/cli/runtime/metadata.ts:read-file",
"src/cli/runtime/serve-update-supervisor.ts:json-parse",
"src/cli/runtime/serve-update-supervisor.ts:json-stringify",
"src/cli/runtime/serve-update-supervisor.ts:read-file",
"src/cli/runtime/transport.ts:json-parse",
"src/cli/runtime/transport.ts:json-stringify",
"src/cli/workspace-format.ts:json-stringify",
"src/main/active-view-preference.ts:json-parse",
"src/main/active-view-preference.ts:json-stringify",
"src/main/active-view-preference.ts:read-file",
"src/main/agent-hooks/hooks-json-read.ts:json-parse",
"src/main/agent-hooks/hooks-json-read.ts:read-file",
"src/main/agent-hooks/install-telemetry.ts:json-stringify",
"src/main/agent-hooks/installer-utils-remote.ts:json-parse",
"src/main/agent-hooks/installer-utils-remote.ts:json-stringify",
"src/main/agent-hooks/installer-utils-remote.ts:read-file",
"src/main/agent-hooks/installer-utils.ts:json-stringify",
"src/main/agent-hooks/installer-utils.ts:read-file",
"src/main/agent-hooks/managed-hook-local-filesystem.ts:read-file",
"src/main/agent-hooks/managed-hook-lock-claims.ts:json-stringify",
"src/main/agent-hooks/managed-hook-lock-claims.ts:read-file",
"src/main/agent-hooks/managed-hook-lock-records.ts:json-parse",
"src/main/agent-hooks/managed-hook-lock-records.ts:json-stringify",
"src/main/agent-hooks/managed-hook-lock-records.ts:read-file",
"src/main/agent-hooks/managed-hook-owner-identity.ts:read-file",
"src/main/agent-hooks/server.ts:json-parse",
"src/main/agent-hooks/server.ts:json-stringify",
"src/main/agent-hooks/server.ts:read-file",
"src/main/agent-hooks/wsl-hook-fs-adapter.ts:read-file",
"src/main/agent-hooks/wsl-hook-relay-deps.ts:read-file",
"src/main/agent-hooks/wsl-hook-relay-launch.ts:read-file",
"src/main/agent-state-file-reader.ts:json-parse",
"src/main/agent-trust-presets.ts:json-parse",
"src/main/agent-trust-presets.ts:json-stringify",
"src/main/agent-trust-presets.ts:read-file",
"src/main/ai-vault/cached-session-list.ts:json-stringify",
"src/main/ai-vault/remote-session-scanner-codex-index.ts:read-file",
"src/main/ai-vault/remote-session-scanner-discovery.ts:unbounded-fanout",
"src/main/ai-vault/remote-session-scanner.ts:read-file",
"src/main/ai-vault/remote-session-scanner.ts:unbounded-fanout",
"src/main/ai-vault/session-parse-cache-persistence.ts:json-parse",
"src/main/ai-vault/session-parse-cache-persistence.ts:json-stringify",
"src/main/ai-vault/session-parse-cache-persistence.ts:read-file",
"src/main/ai-vault/session-scanner-antigravity-fixtures.ts:json-stringify",
"src/main/ai-vault/session-scanner-claude-subagents.ts:json-parse",
"src/main/ai-vault/session-scanner-claude-subagents.ts:read-file",
"src/main/ai-vault/session-scanner-codex-fixtures.ts:json-stringify",
"src/main/ai-vault/session-scanner-devin-parser.ts:json-parse",
"src/main/ai-vault/session-scanner-devin-parser.ts:read-file",
"src/main/ai-vault/session-scanner-gemini-parsers.ts:json-parse",
"src/main/ai-vault/session-scanner-gemini-parsers.ts:read-file",
"src/main/ai-vault/session-scanner-graph-parsers.ts:json-parse",
"src/main/ai-vault/session-scanner-graph-parsers.ts:read-file",
"src/main/ai-vault/session-scanner-grok-parser.ts:json-parse",
"src/main/ai-vault/session-scanner-grok-parser.ts:read-file",
"src/main/ai-vault/session-scanner-incremental-fixtures.ts:json-stringify",
"src/main/ai-vault/session-scanner-kimi-parser.ts:json-parse",
"src/main/ai-vault/session-scanner-kimi-parser.ts:read-file",
"src/main/ai-vault/session-scanner-kimi-paths.ts:json-parse",
"src/main/ai-vault/session-scanner-opencode-sqlite.ts:json-parse",
"src/main/ai-vault/session-scanner-secondary-parsers.ts:json-parse",
"src/main/ai-vault/session-scanner-secondary-parsers.ts:read-file",
"src/main/ai-vault/session-scanner-test-fixtures.ts:json-stringify",
"src/main/ai-vault/session-scanner-values.ts:json-parse",
"src/main/ai-vault/session-scanner-values.ts:read-file",
"src/main/ai-vault/session-scanner.ts:read-file",
"src/main/amp/hook-service.ts:json-stringify",
"src/main/amp/hook-service.ts:read-file",
"src/main/attribution/terminal-attribution.ts:read-file",
"src/main/automations/external-manager.ts:json-parse",
"src/main/automations/external-manager.ts:read-file",
"src/main/automations/hermes-cron-output.ts:read-file",
"src/main/automations/hermes-cron-output.ts:unbounded-fanout",
"src/main/azure-devops/azure-devops-api-request.ts:response-text",
"src/main/azure-devops/pull-request-creation.ts:json-stringify",
"src/main/bitbucket/client.ts:response-text",
"src/main/browser/agent-browser-bridge.ts:json-parse",
"src/main/browser/agent-browser-bridge.ts:json-stringify",
"src/main/browser/agent-browser-bridge.ts:read-file",
"src/main/browser/browser-clicked-link-routing.ts:json-stringify",
"src/main/browser/browser-cookie-import.ts:json-parse",
"src/main/browser/browser-cookie-import.ts:read-file",
"src/main/browser/browser-manager.ts:json-stringify",
"src/main/browser/browser-session-registry.ts:json-parse",
"src/main/browser/browser-session-registry.ts:json-stringify",
"src/main/browser/browser-session-registry.ts:read-file",
"src/main/browser/cdp-bridge.ts:json-parse",
"src/main/browser/cdp-bridge.ts:json-stringify",
"src/main/browser/cdp-ws-proxy-test-harness.ts:json-parse",
"src/main/browser/cdp-ws-proxy-test-harness.ts:json-stringify",
"src/main/browser/cdp-ws-proxy.ts:json-parse",
"src/main/browser/cdp-ws-proxy.ts:json-stringify",
"src/main/browser/popup-origin-bar-window.ts:json-stringify",
"src/main/browser/snapshot-engine.ts:json-parse",
"src/main/browser/snapshot-engine.ts:json-stringify",
"src/main/claude-accounts/managed-auth-path.ts:read-file",
"src/main/claude-accounts/oauth-refresh.ts:json-parse",
"src/main/claude-accounts/oauth-refresh.ts:json-stringify",
"src/main/claude-accounts/oauth-refresh.ts:response-text",
"src/main/claude-accounts/runtime-auth-service.ts:json-parse",
"src/main/claude-accounts/runtime-auth-service.ts:json-stringify",
"src/main/claude-accounts/runtime-auth-service.ts:read-file",
"src/main/claude-accounts/service.ts:json-parse",
"src/main/claude-accounts/service.ts:json-stringify",
"src/main/claude-accounts/service.ts:read-file",
"src/main/claude-usage/scanner.ts:json-parse",
"src/main/claude-usage/scanner.ts:unbounded-fanout",
"src/main/claude-usage/store.ts:json-parse",
"src/main/claude-usage/store.ts:json-stringify",
"src/main/claude-usage/store.ts:read-file",
"src/main/claude/hook-settings.ts:json-stringify",
"src/main/cli/cli-installer.ts:read-file",
"src/main/cli/linux-bare-orca-dispatcher.ts:read-file",
"src/main/cli/linux-terminal-orca-cli-shim.ts:read-file",
"src/main/cli/wsl-cli-registration-registry.ts:json-parse",
"src/main/cli/wsl-cli-registration-registry.ts:json-stringify",
"src/main/cli/wsl-cli-registration-registry.ts:read-file",
"src/main/codex-accounts/codex-auth-identity.ts:json-parse",
"src/main/codex-accounts/host-codex-managed-home-ownership.ts:read-file",
"src/main/codex-accounts/legacy-shared-auth-migration.ts:json-stringify",
"src/main/codex-accounts/legacy-shared-auth-migration.ts:read-file",
"src/main/codex-accounts/runtime-home-service.ts:json-parse",
"src/main/codex-accounts/runtime-home-service.ts:json-stringify",
"src/main/codex-accounts/runtime-home-service.ts:read-file",
"src/main/codex-accounts/service.ts:json-parse",
"src/main/codex-accounts/service.ts:json-stringify",
"src/main/codex-accounts/service.ts:read-file",
"src/main/codex-usage/scanner.ts:json-parse",
"src/main/codex-usage/store.ts:json-parse",
"src/main/codex-usage/store.ts:json-stringify",
"src/main/codex-usage/store.ts:read-file",
"src/main/codex/codex-app-server-grant-bridge.ts:json-parse",
"src/main/codex/codex-app-server-grant-bridge.ts:json-stringify",
"src/main/codex/codex-app-server-grant-entry.ts:json-parse",
"src/main/codex/codex-app-server-grant-entry.ts:json-stringify",
"src/main/codex/codex-app-server-session.ts:json-parse",
"src/main/codex/codex-app-server-session.ts:json-stringify",
"src/main/codex/codex-home-paths.ts:json-parse",
"src/main/codex/codex-home-paths.ts:json-stringify",
"src/main/codex/codex-home-paths.ts:read-file",
"src/main/codex/codex-hook-identity.ts:json-stringify",
"src/main/codex/codex-real-home-hook-install.ts:read-file",
"src/main/codex/codex-session-backfill-audit.ts:json-stringify",
"src/main/codex/codex-session-backfill-marker.ts:json-parse",
"src/main/codex/codex-session-backfill-marker.ts:json-stringify",
"src/main/codex/codex-session-backfill-marker.ts:read-file",
"src/main/codex/codex-session-bridge.ts:json-parse",
"src/main/codex/codex-session-bridge.ts:read-file",
"src/main/codex/codex-session-index-heal-state.ts:json-parse",
"src/main/codex/codex-session-index-heal-state.ts:json-stringify",
"src/main/codex/codex-session-index-heal-state.ts:read-file",
"src/main/codex/codex-trust-config-rollback.ts:read-file",
"src/main/codex/codex-trust-grant-ledger.ts:json-parse",
"src/main/codex/codex-trust-grant-ledger.ts:json-stringify",
"src/main/codex/codex-trust-grant-ledger.ts:read-file",
"src/main/codex/config-settings-baseline.ts:json-stringify",
"src/main/codex/config-toml-trust.ts:json-stringify",
"src/main/codex/config-toml-trust.ts:read-file",
"src/main/codex/hook-service.ts:json-stringify",
"src/main/codex/hook-service.ts:read-file",
"src/main/codex/hook-trust-promotion.ts:json-parse",
"src/main/codex/hook-trust-promotion.ts:json-stringify",
"src/main/codex/hook-trust-promotion.ts:read-file",
"src/main/codex/wsl-codex-session-bridge.ts:json-parse",
"src/main/computer/desktop-script-action.ts:json-stringify",
"src/main/computer/desktop-script-provider-client.ts:json-parse",
"src/main/computer/desktop-script-provider-client.ts:json-stringify",
"src/main/computer/desktop-script-provider-test-harness.ts:json-parse",
"src/main/computer/desktop-script-provider-test-harness.ts:json-stringify",
"src/main/computer/macos-computer-use-permission-status.ts:json-parse",
"src/main/computer/macos-computer-use-permission-status.ts:read-file",
"src/main/computer/macos-native-provider-client.ts:json-parse",
"src/main/computer/macos-native-provider-client.ts:json-stringify",
"src/main/crash-reporting/crash-report-store.ts:json-parse",
"src/main/crash-reporting/crash-report-store.ts:json-stringify",
"src/main/crash-reporting/crash-report-store.ts:read-file",
"src/main/daemon/client.ts:json-parse",
"src/main/daemon/client.ts:read-file",
"src/main/daemon/daemon-entry.ts:read-file",
"src/main/daemon/daemon-file-log.ts:json-stringify",
"src/main/daemon/daemon-health.ts:json-parse",
"src/main/daemon/daemon-health.ts:read-file",
"src/main/daemon/daemon-host-relocation.ts:json-parse",
"src/main/daemon/daemon-host-relocation.ts:json-stringify",
"src/main/daemon/daemon-host-relocation.ts:read-file",
"src/main/daemon/daemon-init.ts:read-file",
"src/main/daemon/daemon-pty-adapter.ts:json-stringify",
"src/main/daemon/daemon-spawner.ts:json-parse",
"src/main/daemon/daemon-spawner.ts:json-stringify",
"src/main/daemon/daemon-spawner.ts:read-file",
"src/main/daemon/daemon-stream-backlog-probe.ts:json-stringify",
"src/main/daemon/history-manager.ts:json-parse",
"src/main/daemon/history-manager.ts:json-stringify",
"src/main/daemon/history-manager.ts:read-file",
"src/main/daemon/ndjson.ts:json-parse",
"src/main/daemon/ndjson.ts:json-stringify",
"src/main/daemon/terminal-history-file-reader.ts:json-parse",
"src/main/devin/hook-config-json.ts:read-file",
"src/main/devin/hook-settings.ts:json-stringify",
"src/main/diagnostics/main-thread-churn-probe.ts:json-stringify",
"src/main/emulator/emulator-probe.ts:json-stringify",
"src/main/emulator/serve-sim-accessibility-tree.ts:json-parse",
"src/main/emulator/serve-sim-accessibility-tree.ts:response-text",
"src/main/emulator/serve-sim-detached-session.ts:json-parse",
"src/main/emulator/serve-sim-detached-session.ts:read-file",
"src/main/emulator/serve-sim-execution.ts:json-parse",
"src/main/emulator/serve-sim-execution.ts:read-file",
"src/main/emulator/simctl-simulator-devices.ts:json-parse",
"src/main/ghostty/index.ts:json-stringify",
"src/main/ghostty/index.ts:read-file",
"src/main/ghostty/theme-resolution.ts:read-file",
"src/main/git/huge-folder-ignore.ts:read-file",
"src/main/git/repo.ts:read-file",
"src/main/git/status.ts:read-file",
"src/main/git/worktree-include-file.ts:read-file",
"src/main/git/worktree.ts:read-file",
"src/main/gitea/client.ts:response-text",
"src/main/gitea/pull-request-creation.ts:json-stringify",
"src/main/github/client.ts:json-parse",
"src/main/github/client.ts:read-file",
"src/main/github/conflict-summary-cache.ts:json-stringify",
"src/main/github/issues.ts:json-parse",
"src/main/github/local-git-config-signature.ts:read-file",
"src/main/github/merged-pr-commit-membership.ts:json-parse",
"src/main/github/project-view.ts:json-parse",
"src/main/github/project-view/internals.ts:json-parse",
"src/main/github/project-view/mutations.ts:json-parse",
"src/main/github/project-view/project-error-classification.ts:json-parse",
"src/main/github/rate-limit.ts:json-parse",
"src/main/github/work-item-details.ts:json-parse",
"src/main/github/work-item-details.ts:json-stringify",
"src/main/gitlab/client.ts:json-parse",
"src/main/gitlab/issues.ts:json-parse",
"src/main/gitlab/merge-request-creation-lookup.ts:json-parse",
"src/main/gitlab/merge-request-creation-lookup.ts:json-stringify",
"src/main/gitlab/merge-request-creation.ts:read-file",
"src/main/gitlab/work-item-details.ts:json-parse",
"src/main/hermes/hook-service.ts:json-stringify",
"src/main/hermes/hook-service.ts:read-file",
"src/main/hooks.ts:read-file",
"src/main/ipc/ai-vault.ts:json-stringify",
"src/main/ipc/app.ts:json-parse",
"src/main/ipc/crash-reporting.ts:json-stringify",
"src/main/ipc/developer-permissions.ts:unbounded-fanout",
"src/main/ipc/ephemeral-vm-runtime-handlers.ts:json-stringify",
"src/main/ipc/feedback.ts:json-stringify",
"src/main/ipc/filesystem-mutations.ts:read-file",
"src/main/ipc/filesystem-watcher.ts:json-stringify",
"src/main/ipc/filesystem.ts:read-file",
"src/main/ipc/notebook.ts:json-stringify",
"src/main/ipc/notification-authorization-status.ts:json-parse",
"src/main/ipc/notifications.ts:read-file",
"src/main/ipc/pet.ts:json-parse",
"src/main/ipc/pet.ts:read-file",
"src/main/ipc/repos.ts:read-file",
"src/main/ipc/shell.ts:read-file",
"src/main/ipc/ssh.ts:json-stringify",
"src/main/ipc/terminal-render-desync-evidence.ts:json-stringify",
"src/main/ipc/watcher-removal-gate.ts:json-stringify",
"src/main/ipc/workspace-cleanup-activity.ts:read-file",
"src/main/ipc/workspace-ports.ts:json-stringify",
"src/main/ipc/worktree-base-directory-poller.ts:unbounded-fanout",
"src/main/ipc/worktree-base-directory-watch-targets.ts:read-file",
"src/main/ipc/worktree-common-git-directory.ts:read-file",
"src/main/ipc/worktree-head-identity-reader.ts:read-file",
"src/main/ipc/worktree-remote.ts:read-file",
"src/main/ipc/worktrees.ts:read-file",
"src/main/jira/client.ts:json-parse",
"src/main/jira/client.ts:json-stringify",
"src/main/jira/client.ts:read-file",
"src/main/jira/client.ts:response-text",
"src/main/jira/issues.ts:json-stringify",
"src/main/keybindings/keybinding-file.ts:json-parse",
"src/main/keybindings/keybinding-file.ts:json-stringify",
"src/main/keybindings/keybinding-file.ts:read-file",
"src/main/kimi/hook-service.ts:read-file",
"src/main/linear/client.ts:json-parse",
"src/main/linear/client.ts:json-stringify",
"src/main/linear/client.ts:read-file",
"src/main/linear/issues.ts:unbounded-fanout",
"src/main/local-worktree-filesystem.ts:read-file",
"src/main/minimax/minimax-cookie-store.ts:read-file",
"src/main/native-chat/transcript-line-decoders-grok.ts:json-parse",
"src/main/native-chat/transcript-record-blocks.ts:json-stringify",
"src/main/observability/bundle.ts:json-parse",
"src/main/observability/bundle.ts:json-stringify",
"src/main/observability/bundle.ts:read-file",
"src/main/observability/diagnostic-upload-http.ts:json-parse",
"src/main/observability/diagnostic-upload-http.ts:json-stringify",
"src/main/observability/local-file-sink.ts:json-stringify",
"src/main/opencode-usage/scanner.ts:json-parse",
"src/main/opencode-usage/scanner.ts:json-stringify",
"src/main/opencode-usage/store.ts:json-parse",
"src/main/opencode-usage/store.ts:json-stringify",
"src/main/opencode-usage/store.ts:read-file",
"src/main/opencode/hook-service.ts:json-parse",
"src/main/opencode/hook-service.ts:json-stringify",
"src/main/opencode/hook-service.ts:read-file",
"src/main/orca-profiles/profile-cloud-client.ts:json-stringify",
"src/main/orca-profiles/profile-cloud-client.ts:response-text",
"src/main/orca-profiles/profile-cloud-org-members-client.ts:json-stringify",
"src/main/orca-profiles/profile-cloud-org-members-client.ts:response-text",
"src/main/orca-profiles/profile-cloud-session-mutation.ts:json-parse",
"src/main/orca-profiles/profile-cloud-session-mutation.ts:read-file",
"src/main/orca-profiles/profile-cloud-session-store.ts:json-parse",
"src/main/orca-profiles/profile-cloud-session-store.ts:json-stringify",
"src/main/orca-profiles/profile-cloud-session-store.ts:read-file",
"src/main/orca-profiles/profile-index-store.ts:json-parse",
"src/main/orca-profiles/profile-index-store.ts:json-stringify",
"src/main/orca-profiles/profile-index-store.ts:read-file",
"src/main/orca-profiles/profile-project-state-file.ts:json-parse",
"src/main/orca-profiles/profile-project-state-file.ts:json-stringify",
"src/main/orca-profiles/profile-project-state-file.ts:read-file",
"src/main/persistence.ts:json-parse",
"src/main/persistence.ts:json-stringify",
"src/main/persistence.ts:read-file",
"src/main/pi/agent-status-extension-source.ts:json-stringify",
"src/main/pi/agent-status-extension-source.ts:read-file",
"src/main/pi/titlebar-extension-service.ts:read-file",
"src/main/ports/local-workspace-port-scanner.ts:json-parse",
"src/main/ports/local-workspace-port-scanner.ts:read-file",
"src/main/project-groups/nested-repo-discovery.ts:read-file",
"src/main/providers/ssh-filesystem-provider.ts:read-file",
"src/main/providers/ssh-git-provider.ts:json-stringify",
"src/main/providers/types.ts:read-file",
"src/main/providers/windows-foreground-process-rows.ts:json-parse",
"src/main/pty/shell-startup-env.ts:read-file",
"src/main/rate-limits/claude-fetcher.ts:json-parse",
"src/main/rate-limits/claude-fetcher.ts:read-file",
"src/main/rate-limits/claude-fetcher.ts:response-text",
"src/main/rate-limits/claude-oauth-usage-error.ts:response-text",
"src/main/rate-limits/codex-fetcher.ts:json-parse",
"src/main/rate-limits/codex-fetcher.ts:json-stringify",
"src/main/rate-limits/codex-fetcher.ts:read-file",
"src/main/rate-limits/codex-fetcher.ts:response-text",
"src/main/rate-limits/gemini-cli-oauth-extractor.ts:json-parse",
"src/main/rate-limits/gemini-cli-oauth-extractor.ts:read-file",
"src/main/rate-limits/gemini-oauth-sources.ts:json-parse",
"src/main/rate-limits/gemini-oauth-sources.ts:json-stringify",
"src/main/rate-limits/gemini-oauth-sources.ts:read-file",
"src/main/rate-limits/gemini-oauth-sources.ts:response-text",
"src/main/rate-limits/gemini-usage-fetcher.test-fixtures.ts:json-stringify",
"src/main/rate-limits/gemini-usage-fetcher.ts:json-stringify",
"src/main/rate-limits/gemini-usage-fetcher.ts:response-text",
"src/main/rate-limits/grok-auth.ts:json-parse",
"src/main/rate-limits/grok-auth.ts:read-file",
"src/main/rate-limits/grok-fetcher.ts:response-text",
"src/main/rate-limits/kimi-fetcher.ts:json-parse",
"src/main/rate-limits/kimi-fetcher.ts:read-file",
"src/main/rate-limits/kimi-fetcher.ts:response-text",
"src/main/rate-limits/minimax-fetcher.ts:response-text",
"src/main/rate-limits/opencode-go-usage-fetcher.ts:response-text",
"src/main/remote-agent-trust-presets.ts:json-parse",
"src/main/remote-agent-trust-presets.ts:json-stringify",
"src/main/remote-agent-trust-presets.ts:read-file",
"src/main/repo-icon-autodetect.ts:json-parse",
"src/main/repo-icon-autodetect.ts:read-file",
"src/main/runtime/agent-session-claim-identity.ts:read-file",
"src/main/runtime/claude-agent-teams-shim-env.ts:read-file",
"src/main/runtime/device-registry.ts:json-parse",
"src/main/runtime/device-registry.ts:read-file",
"src/main/runtime/e2ee-keypair.ts:json-parse",
"src/main/runtime/e2ee-keypair.ts:read-file",
"src/main/runtime/orca-runtime-files.ts:json-stringify",
"src/main/runtime/orca-runtime-files.ts:read-file",
"src/main/runtime/orca-runtime.ts:json-stringify",
"src/main/runtime/orca-runtime.ts:read-file",
"src/main/runtime/orchestration/coordinator.ts:json-parse",
"src/main/runtime/orchestration/db.ts:json-parse",
"src/main/runtime/orchestration/db.ts:json-stringify",
"src/main/runtime/orchestration/lifecycle-reconciliation.ts:json-parse",
"src/main/runtime/orchestration/lifecycle-reconciliation.ts:json-stringify",
"src/main/runtime/relay/relay-control-client.ts:json-stringify",
"src/main/runtime/relay/relay-control-protocol.ts:json-parse",
"src/main/runtime/relay/relay-http-client.ts:json-stringify",
"src/main/runtime/relay/relay-http-client.ts:response-text",
"src/main/runtime/relay/relay-revoke-outbox.ts:json-parse",
"src/main/runtime/relay/relay-revoke-outbox.ts:read-file",
"src/main/runtime/rpc/dispatcher.ts:json-stringify",
"src/main/runtime/rpc/e2ee-channel.ts:json-stringify",
"src/main/runtime/rpc/methods/orchestration-gates.ts:json-parse",
"src/main/runtime/rpc/methods/orchestration.ts:json-parse",
"src/main/runtime/rpc/methods/orchestration.ts:json-stringify",
"src/main/runtime/rpc/relay-transport.ts:json-stringify",
"src/main/runtime/rpc/ws-fallback-port-store.ts:json-parse",
"src/main/runtime/rpc/ws-fallback-port-store.ts:json-stringify",
"src/main/runtime/rpc/ws-fallback-port-store.ts:read-file",
"src/main/runtime/runtime-metadata.ts:json-parse",
"src/main/runtime/runtime-metadata.ts:read-file",
"src/main/runtime/runtime-rpc.ts:json-parse",
"src/main/runtime/runtime-rpc.ts:json-stringify",
"src/main/runtime/runtime-rpc.ts:unbounded-fanout",
"src/main/runtime/tls-certificate.ts:read-file",
"src/main/runtime/windows-mobile-firewall.ts:json-parse",
"src/main/serve-update-handoff.ts:json-parse",
"src/main/serve-update-handoff.ts:json-stringify",
"src/main/serve-update-handoff.ts:read-file",
"src/main/server/serve-readiness.ts:json-stringify",
"src/main/skills/claude-plugin-skill-sources.ts:json-parse",
"src/main/skills/skill-bundle-artifacts.ts:read-file",
"src/main/skills/skill-package-identity.ts:json-stringify",
"src/main/source-control/hosted-review-api-request.ts:response-text",
"src/main/source-control/pull-request-template.ts:read-file",
"src/main/speech/openai-api-key-store.ts:json-parse",
"src/main/speech/openai-api-key-store.ts:read-file",
"src/main/speech/openai-transcription-client.ts:response-text",
"src/main/speech/stt-worker.ts:json-parse",
"src/main/ssh/relay-protocol.ts:json-parse",
"src/main/ssh/relay-protocol.ts:json-stringify",
"src/main/ssh/ssh-agent-identity-filter.ts:read-file",
"src/main/ssh/ssh-auth-resolution.ts:read-file",
"src/main/ssh/ssh-config-include-expander.ts:read-file",
"src/main/ssh/ssh-control-socket.ts:json-stringify",
"src/main/ssh/ssh-git-response-stream-reader.ts:json-parse",
"src/main/ssh/ssh-port-forward.ts:unbounded-fanout",
"src/main/ssh/ssh-relay-deploy.ts:json-stringify",
"src/main/ssh/ssh-relay-versioned-install.ts:read-file",
"src/main/ssh/ssh-remote-cli-format.ts:json-stringify",
"src/main/ssh/ssh-remote-commands.ts:read-file",
"src/main/ssh/ssh-remote-orca-cli.ts:json-stringify",
"src/main/ssh/ssh-remote-orchestration-send.ts:json-stringify",
"src/main/ssh/ssh-remote-platform.ts:json-stringify",
"src/main/ssh/system-ssh-file-transfer.ts:json-stringify",
"src/main/ssh/system-ssh-file-transfer.ts:read-file",
"src/main/startup/configure-process.ts:json-parse",
"src/main/startup/configure-process.ts:read-file",
"src/main/startup/ensure-virtual-display.ts:read-file",
"src/main/startup/gpu-fallback-marker.ts:json-parse",
"src/main/startup/gpu-fallback-marker.ts:json-stringify",
"src/main/startup/gpu-fallback-marker.ts:read-file",
"src/main/startup/startup-diagnostics.ts:json-stringify",
"src/main/startup/windows-user-data-acl.ts:json-parse",
"src/main/startup/windows-user-data-acl.ts:json-stringify",
"src/main/startup/windows-user-data-acl.ts:read-file",
"src/main/stats/collector.ts:json-parse",
"src/main/stats/collector.ts:json-stringify",
"src/main/stats/collector.ts:read-file",
"src/main/system-fonts.ts:json-parse",
"src/main/telemetry/consent.ts:json-stringify",
"src/main/terminal-history.ts:json-parse",
"src/main/terminal-history.ts:json-stringify",
"src/main/terminal-history.ts:read-file",
"src/main/updater-changelog.ts:response-text",
"src/main/updater-lifecycle-diagnostics.ts:json-stringify",
"src/main/updater-nudge.ts:response-text",
"src/main/updater-prerelease-feed.ts:response-text",
"src/main/warp-themes/index.ts:read-file",
"src/main/workspace-space-analysis.ts:unbounded-fanout",
"src/main/worktree-removal-safety.ts:read-file",
"src/relay/agent-exec-handler.ts:json-stringify",
"src/relay/external-automations-handler.ts:json-parse",
"src/relay/external-automations-handler.ts:read-file",
"src/relay/fs-handler-file-read.ts:read-file",
"src/relay/fs-handler.ts:json-stringify",
"src/relay/fs-handler.ts:read-file",
"src/relay/fs-stream-registry.ts:unbounded-fanout",
"src/relay/git-handler-status-ops.ts:read-file",
"src/relay/git-handler.ts:json-stringify",
"src/relay/git-working-file-read.ts:read-file",
"src/relay/plugin-overlay.ts:read-file",
"src/relay/port-scan-handler.ts:read-file",
"src/relay/protocol.ts:json-parse",
"src/relay/protocol.ts:json-stringify",
"src/relay/pty-handler.ts:json-parse",
"src/relay/pty-handler.ts:json-stringify",
"src/relay/pty-handler.ts:unbounded-fanout",
"src/relay/pty-shell-launch.ts:read-file",
"src/relay/pty-shell-utils.ts:read-file",
"src/relay/relay-handshake.ts:read-file",
"src/relay/relay-watcher-removal-fence.ts:unbounded-fanout",
"src/relay/windows-port-scan.ts:json-parse",
"src/relay/workspace-session-handler.ts:json-parse",
"src/relay/workspace-session-handler.ts:json-stringify",
"src/relay/workspace-session-handler.ts:read-file",
"src/relay/wsl-hook-fs-bridge.ts:read-file",
"src/renderer/src/components/GitHubItemDialog.tsx:json-stringify",
"src/renderer/src/components/PullRequestPage.tsx:json-stringify",
"src/renderer/src/components/TaskPage.tsx:json-parse",
"src/renderer/src/components/TaskPage.tsx:json-stringify",
"src/renderer/src/components/Terminal.tsx:unbounded-fanout",
"src/renderer/src/components/automations/AutomationsPage.tsx:json-stringify",
"src/renderer/src/components/browser-pane/BrowserPane.tsx:json-parse",
"src/renderer/src/components/browser-pane/BrowserPane.tsx:json-stringify",
"src/renderer/src/components/diff-comments/useDiffCommentDecorator.tsx:json-stringify",
"src/renderer/src/components/editor/CombinedDiffViewer.tsx:json-stringify",
"src/renderer/src/components/editor/IpynbViewer.tsx:json-stringify",
"src/renderer/src/components/editor/editor-autosave-controller.ts:unbounded-fanout",
"src/renderer/src/components/editor/ipynb-parse.ts:json-parse",
"src/renderer/src/components/editor/ipynb-parse.ts:json-stringify",
"src/renderer/src/components/editor/markdown-document-list-request.ts:json-stringify",
"src/renderer/src/components/editor/markdown-export-extract.ts:response-text",
"src/renderer/src/components/editor/raw-markdown-html.ts:json-stringify",
"src/renderer/src/components/editor/rich-markdown-html-superscript-link.ts:json-parse",
"src/renderer/src/components/editor/useEditorPanelContentState.ts:json-stringify",
"src/renderer/src/components/editor/useLocalImageSrc.ts:read-file",
"src/renderer/src/components/feature-wall/feature-wall-completion-persistence.ts:json-parse",
"src/renderer/src/components/feature-wall/feature-wall-completion-persistence.ts:json-stringify",
"src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx:json-stringify",
"src/renderer/src/components/floating-terminal/floating-terminal-panel-bounds.ts:json-parse",
"src/renderer/src/components/floating-terminal/floating-terminal-panel-bounds.ts:json-stringify",
"src/renderer/src/components/floating-terminal/floating-terminal-trigger-position.ts:json-parse",
"src/renderer/src/components/floating-terminal/floating-terminal-trigger-position.ts:json-stringify",
"src/renderer/src/components/github-project/column-widths.ts:json-parse",
"src/renderer/src/components/github-project/column-widths.ts:json-stringify",
"src/renderer/src/components/github-project/columns.ts:json-parse",
"src/renderer/src/components/github-project/columns.ts:json-stringify",
"src/renderer/src/components/github-project/project-visible-table-cache.ts:json-stringify",
"src/renderer/src/components/landing-preflight-dismissal.ts:json-parse",
"src/renderer/src/components/landing-preflight-dismissal.ts:json-stringify",
"src/renderer/src/components/linear-issue-attribute-filter-dropdowns.tsx:json-stringify",
"src/renderer/src/components/native-chat/native-chat-interactive-prompt.ts:json-parse",
"src/renderer/src/components/native-chat/native-chat-pending-occurrence.ts:json-stringify",
"src/renderer/src/components/native-chat/native-chat-session-assembler.ts:json-stringify",
"src/renderer/src/components/native-chat/native-chat-session-option-enrichment.ts:json-stringify",
"src/renderer/src/components/native-chat/native-chat-skill-discovery-context.ts:json-stringify",
"src/renderer/src/components/pet/PetOverlay.tsx:json-parse",
"src/renderer/src/components/pet/PetOverlay.tsx:json-stringify",
"src/renderer/src/components/pr-comments-resolution-prompt.ts:json-stringify",
"src/renderer/src/components/quick-open-file-list.ts:json-stringify",
"src/renderer/src/components/right-sidebar/ChecksPanel.tsx:json-stringify",
"src/renderer/src/components/right-sidebar/PortsPanel.tsx:json-stringify",
"src/renderer/src/components/right-sidebar/SourceControlTextGenerationDialog.tsx:json-stringify",
"src/renderer/src/components/right-sidebar/ai-vault-view-options-persistence.ts:json-parse",
"src/renderer/src/components/right-sidebar/ai-vault-view-options-persistence.ts:json-stringify",
"src/renderer/src/components/right-sidebar/checks-panel-git-status-snapshot.ts:json-stringify",
"src/renderer/src/components/right-sidebar/checks-panel-polling.ts:json-stringify",
"src/renderer/src/components/right-sidebar/file-explorer-operation-owner.ts:json-stringify",
"src/renderer/src/components/right-sidebar/push-target-upstream-refresh-cache.ts:json-stringify",
"src/renderer/src/components/right-sidebar/useFileDeletion.ts:unbounded-fanout",
"src/renderer/src/components/right-sidebar/useFileExplorerWatch.ts:json-stringify",
"src/renderer/src/components/right-sidebar/useGitHistoryCommitActions.ts:json-stringify",
"src/renderer/src/components/right-sidebar/useSavedSourceControlAgentActionAutoStart.ts:json-stringify",
"src/renderer/src/components/settings/RepositoryHostSetupsSection.tsx:json-stringify",
"src/renderer/src/components/settings/RuntimeEnvironmentsPane.tsx:unbounded-fanout",
"src/renderer/src/components/settings/SourceControlActionRecipeRow.tsx:json-stringify",
"src/renderer/src/components/settings/mcp-config-inspection.ts:read-file",
"src/renderer/src/components/settings/repository-source-control-ai-global-ux.ts:json-stringify",
"src/renderer/src/components/settings/repository-source-control-ai-persist-queue.ts:json-stringify",
"src/renderer/src/components/settings/source-control-action-recipe-draft-state.ts:json-stringify",
"src/renderer/src/components/settings/source-control-ai-action-recipe-draft.ts:json-stringify",
"src/renderer/src/components/setup-guide/setup-guide-progress-readiness.ts:json-stringify",
"src/renderer/src/components/sidebar/WorktreeList.tsx:unbounded-fanout",
"src/renderer/src/components/sidebar/linear-agent-skill-runtime.ts:json-stringify",
"src/renderer/src/components/sidebar/workspace-board-task-status-sync.ts:json-stringify",
"src/renderer/src/components/sidebar/workspace-status-drag-data.ts:json-parse",
"src/renderer/src/components/sidebar/workspace-status-drag-data.ts:json-stringify",
"src/renderer/src/components/status-bar/SshStatusSegment.tsx:unbounded-fanout",
"src/renderer/src/components/status-bar/StatusBar.tsx:json-stringify",
"src/renderer/src/components/task-page-cache-selectors.ts:json-stringify",
"src/renderer/src/components/terminal-pane/terminal-view-attributes-publisher.ts:json-stringify",
"src/renderer/src/hooks/useIpcEvents.ts:unbounded-fanout",
"src/renderer/src/lib/ai-vault-session-drag.ts:json-parse",
"src/renderer/src/lib/ai-vault-session-drag.ts:json-stringify",
"src/renderer/src/lib/app-font-family.ts:json-stringify",
"src/renderer/src/lib/execute-open-editor-path-move.ts:unbounded-fanout",
"src/renderer/src/lib/feature-education-telemetry.ts:json-parse",
"src/renderer/src/lib/feature-education-telemetry.ts:json-stringify",
"src/renderer/src/lib/react-error-boundary-reporting.ts:json-stringify",
"src/renderer/src/lib/workspace-file-drag.ts:json-parse",
"src/renderer/src/lib/workspace-file-drag.ts:json-stringify",
"src/renderer/src/lib/workspace-port-actions.ts:json-stringify",
"src/renderer/src/lib/worktree-operation-generation.ts:json-stringify",
"src/renderer/src/lib/worktree-operation-route.ts:json-stringify",
"src/renderer/src/lib/worktree-runtime-owner-index.ts:json-stringify",
"src/renderer/src/runtime/runtime-file-client.ts:read-file",
"src/renderer/src/runtime/sync-runtime-graph.ts:json-stringify",
"src/renderer/src/runtime/web-session-intent-owner.ts:json-stringify",
"src/renderer/src/store/slices/linear.ts:json-stringify",
"src/renderer/src/store/slices/pull-request-generation.ts:json-stringify",
"src/renderer/src/store/slices/repos.ts:json-stringify",
"src/renderer/src/store/slices/terminal-tab-retirement.ts:json-stringify",
"src/renderer/src/store/slices/workspace-cleanup.ts:json-stringify",
"src/renderer/src/web/web-pairing.ts:json-parse",
"src/renderer/src/web/web-preload-api.ts:json-parse",
"src/renderer/src/web/web-preload-api.ts:json-stringify",
"src/renderer/src/web/web-runtime-client.ts:json-parse",
"src/renderer/src/web/web-runtime-client.ts:json-stringify",
"src/renderer/src/web/web-runtime-client.ts:response-text",
"src/renderer/src/web/web-runtime-environment.ts:json-parse",
"src/renderer/src/web/web-runtime-environment.ts:json-stringify",
"src/shared/agent-hook-listener.ts:json-parse",
"src/shared/agent-hook-listener.ts:json-stringify",
"src/shared/agent-question-answered-intent.ts:json-parse",
"src/shared/agent-status-types.ts:json-parse",
"src/shared/browser-annotation-viewport-bridge.ts:json-stringify",
"src/shared/browser-screencast-protocol.ts:json-parse",
"src/shared/browser-screencast-protocol.ts:json-stringify",
"src/shared/claude-statusline-rate-limits.ts:json-parse",
"src/shared/codex-reset-credit-attempt-ledger.ts:json-stringify",
"src/shared/codex-reset-credit-scope.ts:json-stringify",
"src/shared/commit-message-agent-spec.ts:json-parse",
"src/shared/emulator-keyboard-frame.ts:json-stringify",
"src/shared/emulator-touch-frame.ts:json-stringify",
"src/shared/ephemeral-vm-recipe-lifecycle-payload.ts:json-stringify",
"src/shared/ephemeral-vm-recipe-runner.ts:json-stringify",
"src/shared/ephemeral-vm-recipes.ts:json-parse",
"src/shared/ephemeral-vm-runtime-store.ts:json-parse",
"src/shared/external-automation-jobs-file.ts:json-parse",
"src/shared/fetch-response-body.ts:json-parse",
"src/shared/fetch-response-body.ts:response-text",
"src/shared/git-effective-upstream.ts:json-stringify",
"src/shared/git-publish-target-status.ts:json-stringify",
"src/shared/git-status-line-stats-cache.ts:json-stringify",
"src/shared/in-flight-promise-dedupe.ts:json-stringify",
"src/shared/linear-issue-attribute-filter.ts:json-stringify",
"src/shared/map-with-concurrency.ts:unbounded-fanout",
"src/shared/mcp-config.ts:json-parse",
"src/shared/mobile-relay-pairing-fixtures.ts:json-stringify",
"src/shared/native-chat-ask.ts:json-parse",
"src/shared/native-chat-tool-summary.ts:json-stringify",
"src/shared/pairing.ts:json-parse",
"src/shared/pairing.ts:json-stringify",
"src/shared/pr-checks-fix-prompt.ts:json-stringify",
"src/shared/pull-request-generation.ts:json-parse",
"src/shared/remote-runtime-client.ts:json-stringify",
"src/shared/remote-runtime-request-frames.ts:json-parse",
"src/shared/remote-runtime-request-websocket.ts:json-stringify",
"src/shared/runtime-bootstrap.ts:json-parse",
"src/shared/runtime-environment-store.ts:json-parse",
"src/shared/secure-file.ts:json-stringify",
"src/shared/serve-update-handoff.ts:json-parse",
"src/shared/setup-script-import-codex-environment.ts:json-parse",
"src/shared/setup-script-import-codex-environment.ts:read-file",
"src/shared/setup-script-imports.ts:json-parse",
"src/shared/setup-script-imports.ts:read-file",
"src/shared/setup-script-package-manager-suggestion.ts:json-parse",
"src/shared/setup-script-package-manager-suggestion.ts:read-file",
"src/shared/source-control-ai.ts:json-stringify",
"src/shared/source-control-commit-failure.ts:json-stringify",
"src/shared/source-control-conflict-prompts.ts:json-stringify",
"src/shared/source-control-push-failure.ts:json-stringify",
"src/shared/terminal-stream-protocol.ts:json-parse",
"src/shared/terminal-stream-protocol.ts:json-stringify",
"src/shared/text-search.ts:json-parse"
]
}
+158
View File
@@ -0,0 +1,158 @@
// Ratchet: unbounded reads of untrusted/unpredictable input are the OOM crash class (#9872, #9984).
// Existing occurrences are frozen in a baseline; new ones must use a src/shared/memory-safety helper
// or carry a `bounded-by:` justification comment on the line or the line above.
//
// Run: node config/scripts/check-bounded-ingress.mjs [--update-baseline]
import fs from 'node:fs/promises'
import path from 'node:path'
import process from 'node:process'
import { pathToFileURL } from 'node:url'
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts'])
const SKIP_PATH_PARTS = new Set([
'node_modules',
'dist',
'out',
'.git',
'__snapshots__',
'memory-safety'
])
const BASELINE_PATH = 'config/bounded-ingress-baseline.json'
// Each pattern names an unbounded way to pull data into memory.
const PATTERNS = [
{
id: 'read-file',
re: /\breadFile(?:Sync)?\s*\(/,
hint: 'use readNodeFileWithinLimit (memory-safety/node-bounded-file-reader)'
},
{
id: 'response-text',
re: /\.(?:text|json|arrayBuffer|blob)\s*\(\s*\)/,
hint: 'bound the response body before materializing it'
},
{
id: 'json-parse',
re: /\bJSON\.parse\s*\(/,
hint: 'guard with assertJsonTextWithinStructureLimits (memory-safety/json-text-structure-limit)'
},
{
id: 'json-stringify',
re: /\bJSON\.stringify\s*\(/,
hint: 'use stringifyJsonWithinLimit (memory-safety/node-bounded-json-stringify) when the value can be large'
},
{
id: 'unbounded-fanout',
re: /Promise\.all\s*\(\s*[\w.]+\s*\.map\s*\(/,
hint: 'use mapWithConcurrency so fan-out cannot grow with input size'
}
]
const ALLOW_RE = /bounded-by:/i
export function findViolations(relativePath, source) {
const lines = source.split('\n')
const found = []
for (let i = 0; i < lines.length; i += 1) {
const line = lines[i]
if (ALLOW_RE.test(line) || (i > 0 && ALLOW_RE.test(lines[i - 1]))) {
continue
}
for (const pattern of PATTERNS) {
if (pattern.re.test(line)) {
found.push({ file: relativePath, line: i + 1, id: pattern.id, hint: pattern.hint })
}
}
}
return found
}
export function violationKey(violation) {
return `${violation.file}:${violation.id}`
}
function isSkipped(relative) {
if (relative.includes('.test.') || relative.includes('.spec.') || relative.includes('.bench.')) {
return true
}
return relative.split('/').some((part) => SKIP_PATH_PARTS.has(part))
}
async function collect(root, dir, out = []) {
let entries
try {
entries = await fs.readdir(dir, { withFileTypes: true })
} catch {
return out
}
for (const entry of entries) {
const full = path.join(dir, entry.name)
const relative = path.relative(root, full).split(path.sep).join('/')
if (entry.isDirectory()) {
if (!SKIP_PATH_PARTS.has(entry.name)) {
await collect(root, full, out)
}
} else if (
entry.isFile() &&
SOURCE_EXTENSIONS.has(path.extname(entry.name)) &&
!isSkipped(relative)
) {
out.push({ full, relative })
}
}
return out
}
export async function run(root, { updateBaseline = false } = {}) {
const files = [
...(await collect(root, path.join(root, 'src'))),
...(await collect(root, path.join(root, 'mobile/src')))
]
const violations = []
for (const file of files) {
const source = await fs.readFile(file.full, 'utf8')
violations.push(...findViolations(file.relative, source))
}
const baselineFile = path.join(root, BASELINE_PATH)
if (updateBaseline) {
const keys = [...new Set(violations.map(violationKey))].sort()
await fs.writeFile(baselineFile, `${JSON.stringify({ allowed: keys }, null, 2)}\n`)
return { added: [], baselineSize: keys.length, updated: true }
}
let allowed = new Set()
try {
allowed = new Set(JSON.parse(await fs.readFile(baselineFile, 'utf8')).allowed)
} catch {
// No baseline yet: every occurrence is reported so the first run can seed one.
}
const added = violations.filter((v) => !allowed.has(violationKey(v)))
return { added, baselineSize: allowed.size, updated: false }
}
const invokedDirectly = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
if (invokedDirectly) {
const root = path.resolve(import.meta.dirname, '../..')
const updateBaseline = process.argv.includes('--update-baseline')
const { added, baselineSize, updated } = await run(root, { updateBaseline })
if (updated) {
console.log(`bounded-ingress baseline updated (${baselineSize} allowed entries)`)
process.exit(0)
}
if (added.length > 0) {
console.error('Unbounded ingress introduced in files not covered by the baseline:\n')
for (const v of added.slice(0, 40)) {
console.error(` ${v.file}:${v.line} [${v.id}]\n ${v.hint}`)
}
if (added.length > 40) {
console.error(` ... and ${added.length - 40} more`)
}
console.error(
'\nSee src/shared/memory-safety/README.md. If the size is genuinely bounded already,' +
' add a `bounded-by:` comment on or above the line explaining what bounds it.'
)
process.exit(1)
}
console.log(`bounded-ingress: no new unbounded reads (${baselineSize} baseline entries)`)
}
@@ -0,0 +1,45 @@
import { describe, expect, it } from 'vitest'
import { findViolations, violationKey } from './check-bounded-ingress.mjs'
describe('check-bounded-ingress', () => {
it('flags an unbounded file read and JSON parse', () => {
const found = findViolations(
'src/main/thing.ts',
['const buf = await readFile(p)', 'return JSON.parse(buf.toString())'].join('\n')
)
expect(found.map((v) => v.id)).toEqual(['read-file', 'json-parse'])
expect(found[0].line).toBe(1)
})
it('honors a bounded-by justification on the line and the line above', () => {
const sameLine = findViolations(
'src/main/thing.ts',
'const buf = await readFile(p) // bounded-by: fixture path under 1KB'
)
const lineAbove = findViolations(
'src/main/thing.ts',
['// bounded-by: size checked by the caller', 'const buf = await readFile(p)'].join('\n')
)
expect(sameLine).toEqual([])
expect(lineAbove).toEqual([])
})
it('flags unbounded response bodies and fan-out', () => {
const found = findViolations(
'src/main/thing.ts',
['const body = await response.text()', 'await Promise.all(items.map((i) => load(i)))'].join(
'\n'
)
)
expect(found.map((v) => v.id)).toEqual(['response-text', 'unbounded-fanout'])
})
it('keys violations by file and rule so a baseline survives line moves', () => {
expect(violationKey({ file: 'src/a.ts', id: 'read-file', line: 12 })).toBe('src/a.ts:read-file')
expect(violationKey({ file: 'src/a.ts', id: 'read-file', line: 99 })).toBe('src/a.ts:read-file')
})
it('ignores lines with no ingress pattern', () => {
expect(findViolations('src/main/thing.ts', 'const total = a + b')).toEqual([])
})
})
+3 -2
View File
@@ -11,7 +11,7 @@
"main": "./out/main/index.js",
"scripts": {
"format": "oxfmt --write .",
"lint": "oxlint && pnpm run lint:switch-exhaustiveness && node config/scripts/check-styled-scrollbars.mjs && pnpm run check:reliability-gates && pnpm run check:max-lines-ratchet && pnpm run verify:bundled-skill-guides && pnpm run verify:skill-bundle-manifest && pnpm run verify:localization-catalog && pnpm run verify:localization-coverage",
"lint": "oxlint && pnpm run lint:switch-exhaustiveness && node config/scripts/check-styled-scrollbars.mjs && pnpm run check:reliability-gates && pnpm run check:max-lines-ratchet && pnpm run check:bounded-ingress && pnpm run verify:bundled-skill-guides && pnpm run verify:skill-bundle-manifest && pnpm run verify:localization-catalog && pnpm run verify:localization-coverage",
"lint:react-doctor": "oxlint --config config/oxlint-react-doctor.json",
"lint:react-doctor:changed": "node config/scripts/lint-react-doctor-changed.mjs",
"lint:switch-exhaustiveness": "oxlint --type-aware --config config/oxlint-switch-exhaustiveness.json src/main src/preload src/shared src/relay src/cli src/renderer/src config tests --quiet",
@@ -104,7 +104,8 @@
"bench:multi-workspace-typing": "pnpm run ensure:electron-runtime && node config/scripts/run-multi-workspace-typing-bench.mjs",
"bench:cold-park-reveal": "pnpm run ensure:electron-runtime && node tools/benchmarks/terminal-cold-park-reveal-bench.mjs",
"bench:cold-park-resource": "pnpm run ensure:electron-runtime && node tools/benchmarks/terminal-cold-park-resource-bench.mjs",
"bench:compare": "node config/scripts/compare-benchmark-artifacts.mjs"
"bench:compare": "node config/scripts/compare-benchmark-artifacts.mjs",
"check:bounded-ingress": "node config/scripts/check-bounded-ingress.mjs"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.2",
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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
@@ -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
+2 -2
View File
@@ -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<typeof BoundedFileReader>()
return {
...actual,
+11 -32
View File
@@ -1,4 +1,5 @@
import type { PRConflictSummary } from '../../shared/types'
import { BoundedMap } from '../../shared/memory-safety/bounded-map'
// Why 60s: the hottest coordinator cadences that re-derive a CONFLICTING PR
// (10s mergeability-pending, 2.5s manual-pending) previously each ran a
@@ -31,8 +32,10 @@ type CachedSummary = {
staleAt: number | null
}
const baseOidCache = new Map<string, CachedBaseTipResolution>()
const summaryCache = new Map<string, CachedSummary>()
const baseOidCache = new BoundedMap<string, CachedBaseTipResolution>({
maxEntries: BASE_OID_CACHE_MAX
})
const summaryCache = new BoundedMap<string, CachedSummary>({ maxEntries: SUMMARY_CACHE_MAX })
const inFlightBaseOidResolves = new Map<string, Promise<FreshBaseTipResolution>>()
const inFlightSummaryDerivations = new Map<string, Promise<PRConflictSummary | undefined>>()
@@ -62,16 +65,11 @@ export function readFreshBaseTipResolution(baseKey: string): FreshBaseTipResolut
}
export function storeResolvedBaseTip(baseKey: string, oid: string): void {
setBoundedMapEntry(baseOidCache, baseKey, { oid, resolvedAt: Date.now() }, BASE_OID_CACHE_MAX)
baseOidCache.set(baseKey, { oid, resolvedAt: Date.now() })
}
export function rememberUnresolvedBaseTip(baseKey: string): void {
setBoundedMapEntry(
baseOidCache,
baseKey,
{ oid: null, resolvedAt: Date.now() },
BASE_OID_CACHE_MAX
)
baseOidCache.set(baseKey, { oid: null, resolvedAt: Date.now() })
}
export function readCachedSummary(summaryKey: string): CachedSummary | null {
@@ -87,15 +85,10 @@ export function readCachedSummary(summaryKey: string): CachedSummary | null {
}
export function storeCachedSummary(summaryKey: string, value: PRConflictSummary | undefined): void {
setBoundedMapEntry(
summaryCache,
summaryKey,
{
value,
staleAt: value === undefined ? Date.now() + CONFLICT_SUMMARY_BASE_FETCH_WINDOW_MS : null
},
SUMMARY_CACHE_MAX
)
summaryCache.set(summaryKey, {
value,
staleAt: value === undefined ? Date.now() + CONFLICT_SUMMARY_BASE_FETCH_WINDOW_MS : null
})
}
export function dedupeBaseOidResolve(
@@ -128,20 +121,6 @@ function dedupeInFlight<T>(
return promise
}
function setBoundedMapEntry<K, V>(map: Map<K, V>, key: K, value: V, maxEntries: number): void {
if (map.has(key)) {
map.delete(key)
}
map.set(key, value)
while (map.size > maxEntries) {
const oldest = map.keys().next()
if (oldest.done) {
return
}
map.delete(oldest.value)
}
}
export function __resetPRConflictSummaryDerivationCachesForTests(): void {
baseOidCache.clear()
summaryCache.clear()
+11 -20
View File
@@ -10,6 +10,7 @@ import {
normalizeTerminalTitle
} from '../../shared/agent-detection'
import { extractOscTitleScanTail } from '../../shared/osc-title-scan-tail'
import { BoundedMap } from '../../shared/memory-safety/bounded-map'
import { extractLastOsc7Uri, extractOscScanTail } from '../daemon/osc7-uri-extraction'
import { parseFileUriPathParts } from '../daemon/osc7-file-uri'
import type { AgentStatus } from '../../shared/agent-detection'
@@ -2886,13 +2887,17 @@ export class OrcaRuntimeService {
// same repo remote/ref locks. This queue serializes all fetch shapes for one
// canonical repo+remote while still letting same-shape callers share promises.
private remoteFetchQueueTail = new Map<string, Promise<RemoteFetchResult>>()
private fetchLastCompletedAt = new Map<string, number>()
private fetchLastCompletedAt = new BoundedMap<string, number>({
maxEntries: REMOTE_FETCH_CACHE_MAX
})
// Why: `getCanonicalFetchKey` is awaited from every freshness probe and
// every getOrStartRemoteFetch call. Without memoization the warm-cache hot
// path spawns a `git rev-parse --git-common-dir` subprocess per touch
// (twice in createLocalWorktree). Cache by `<repoPath>::<remote>` so the
// canonical key is resolved at most once per repo+remote in the process.
private canonicalFetchKeyCache = new Map<string, string>()
private canonicalFetchKeyCache = new BoundedMap<string, string>({
maxEntries: REMOTE_FETCH_CACHE_MAX
})
private optimisticReconcileTokens = new Map<string, string>()
private removeManagedWorktreeInFlight = new Map<string, RuntimeWorktreeRemovalInFlight>()
private preservedBranchCleanupByWorktreeId = new Map<string, PreservedBranchCleanupTarget>()
@@ -19530,7 +19535,7 @@ export class OrcaRuntimeService {
const cacheKey = `${runtimeKey}::${repoPath}::${remote}`
const cached = this.canonicalFetchKeyCache.get(cacheKey)
if (cached !== undefined) {
setBoundedMapEntry(this.canonicalFetchKeyCache, cacheKey, cached, REMOTE_FETCH_CACHE_MAX)
this.canonicalFetchKeyCache.set(cacheKey, cached)
return cached
}
let resolved = cacheKey
@@ -19547,7 +19552,7 @@ export class OrcaRuntimeService {
// Fall through to the caller-provided path. The fetch still runs from
// repoPath; this key only controls cache sharing.
}
setBoundedMapEntry(this.canonicalFetchKeyCache, cacheKey, resolved, REMOTE_FETCH_CACHE_MAX)
this.canonicalFetchKeyCache.set(cacheKey, resolved)
return resolved
}
@@ -19572,7 +19577,7 @@ export class OrcaRuntimeService {
return null
}
if (Date.now() - lastAt < FETCH_FRESHNESS_MS) {
setBoundedMapEntry(this.fetchLastCompletedAt, key, lastAt, REMOTE_FETCH_CACHE_MAX)
this.fetchLastCompletedAt.set(key, lastAt)
return lastAt
}
this.fetchLastCompletedAt.delete(key)
@@ -19580,7 +19585,7 @@ export class OrcaRuntimeService {
}
private rememberFreshFetchCompletedAt(key: string, completedAt = Date.now()): void {
setBoundedMapEntry(this.fetchLastCompletedAt, key, completedAt, REMOTE_FETCH_CACHE_MAX)
this.fetchLastCompletedAt.set(key, completedAt)
}
async getOrStartRemoteFetch(
@@ -30447,20 +30452,6 @@ const REMOTE_FETCH_TIMEOUT_MS = 60_000
const REMOTE_FETCH_CACHE_MAX = 512
const DRIFT_PROBE_SUBJECT_LIMIT = 5
function setBoundedMapEntry<K, V>(map: Map<K, V>, key: K, value: V, maxEntries: number): void {
if (map.has(key)) {
map.delete(key)
}
map.set(key, value)
while (map.size > maxEntries) {
const oldest = map.keys().next()
if (oldest.done) {
return
}
map.delete(oldest.value)
}
}
function getExplicitWorktreeIdSelector(selector: string | undefined): string | null {
if (!selector?.startsWith('id:')) {
return null
@@ -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
@@ -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
@@ -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
+1 -1
View File
@@ -64,7 +64,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
+1 -1
View File
@@ -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'
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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. */
-19
View File
@@ -1,19 +0,0 @@
export const MAX_CUSTOM_PET_FILE_BYTES = 64 * 1024 * 1024
// Why: sprite processing holds decoded image, canvas, ImageData, PNG, and
// optional bitmap copies at once, so encoded bytes alone are not a safe bound.
export const MAX_CUSTOM_PET_SHEET_PIXELS = 4 * 1024 * 1024
export const MAX_CUSTOM_PET_SHEET_DIMENSION = 8_192
export const MAX_CUSTOM_PET_DETECTED_FRAMES = 128
export function isCustomPetSheetSizeSafe(width: number, height: number): boolean {
return (
Number.isSafeInteger(width) &&
Number.isSafeInteger(height) &&
width > 0 &&
height > 0 &&
width <= MAX_CUSTOM_PET_SHEET_DIMENSION &&
height <= MAX_CUSTOM_PET_SHEET_DIMENSION &&
width * height <= MAX_CUSTOM_PET_SHEET_PIXELS
)
}
+1 -1
View File
@@ -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).
+3 -3
View File
@@ -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,
+2 -2
View File
@@ -1,8 +1,8 @@
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
+1 -1
View File
@@ -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
@@ -1,59 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
assertFilesystemDirectoryWithinLimit,
createFilesystemDirectoryLimitState,
FILESYSTEM_DIRECTORY_LIMIT_MESSAGE,
FILESYSTEM_DIRECTORY_MAX_ENTRIES,
FILESYSTEM_DIRECTORY_MAX_RETAINED_BYTES,
resolveFilesystemDirectoryListingLimits,
trackFilesystemDirectoryEntry
} from './filesystem-directory-listing-limit'
describe('filesystem directory listing limit', () => {
it('accepts ordinary complete listings', () => {
expect(() =>
assertFilesystemDirectoryWithinLimit([
{ name: 'src' },
{ name: 'README.md' },
{ name: '文件.txt' }
])
).not.toThrow()
})
it('rejects the first entry beyond the count limit', () => {
const state = createFilesystemDirectoryLimitState({
maxEntries: 2,
maxRetainedBytes: FILESYSTEM_DIRECTORY_MAX_RETAINED_BYTES
})
trackFilesystemDirectoryEntry(state, { name: 'one' })
trackFilesystemDirectoryEntry(state, { name: 'two' })
expect(() => trackFilesystemDirectoryEntry(state, { name: 'three' })).toThrow(
FILESYSTEM_DIRECTORY_LIMIT_MESSAGE
)
expect(state.entries).toBe(3)
})
it('rejects names beyond the retained-byte limit', () => {
const state = createFilesystemDirectoryLimitState({
maxEntries: FILESYSTEM_DIRECTORY_MAX_ENTRIES,
maxRetainedBytes: 100
})
expect(() => trackFilesystemDirectoryEntry(state, { name: 'xx' })).toThrow(
FILESYSTEM_DIRECTORY_LIMIT_MESSAGE
)
})
it('never lets callers raise the process-wide ceilings', () => {
expect(
resolveFilesystemDirectoryListingLimits({
maxEntries: Number.MAX_SAFE_INTEGER,
maxRetainedBytes: Number.MAX_SAFE_INTEGER
})
).toEqual({
maxEntries: FILESYSTEM_DIRECTORY_MAX_ENTRIES,
maxRetainedBytes: FILESYSTEM_DIRECTORY_MAX_RETAINED_BYTES
})
})
})
@@ -1,75 +0,0 @@
export const FILESYSTEM_DIRECTORY_MAX_ENTRIES = 100_000
export const FILESYSTEM_DIRECTORY_MAX_RETAINED_BYTES = 12 * 1024 * 1024
export const FILESYSTEM_DIRECTORY_LIMIT_MESSAGE =
'This folder is too large to list safely (limit: 100,000 items or a 12 MB listing).'
export type FilesystemDirectoryListingLimits = {
maxEntries: number
maxRetainedBytes: number
}
type NamedDirectoryEntry = { name: string }
export type FilesystemDirectoryLimitState = {
entries: number
retainedBytes: number
limits: FilesystemDirectoryListingLimits
}
export function resolveFilesystemDirectoryListingLimits(
requested?: Partial<FilesystemDirectoryListingLimits>
): FilesystemDirectoryListingLimits {
return {
maxEntries: clampLimit(requested?.maxEntries, FILESYSTEM_DIRECTORY_MAX_ENTRIES),
maxRetainedBytes: clampLimit(
requested?.maxRetainedBytes,
FILESYSTEM_DIRECTORY_MAX_RETAINED_BYTES
)
}
}
export function createFilesystemDirectoryLimitState(
requested?: Partial<FilesystemDirectoryListingLimits>
): FilesystemDirectoryLimitState {
return {
entries: 0,
retainedBytes: 0,
limits: resolveFilesystemDirectoryListingLimits(requested)
}
}
export function trackFilesystemDirectoryEntry(
state: FilesystemDirectoryLimitState,
entry: NamedDirectoryEntry
): void {
state.entries += 1
state.retainedBytes += estimateFilesystemDirectoryEntryBytes(entry)
if (
state.entries > state.limits.maxEntries ||
state.retainedBytes > state.limits.maxRetainedBytes
) {
throw new Error(FILESYSTEM_DIRECTORY_LIMIT_MESSAGE)
}
}
export function assertFilesystemDirectoryWithinLimit(
entries: readonly NamedDirectoryEntry[],
requested?: Partial<FilesystemDirectoryListingLimits>
): void {
const state = createFilesystemDirectoryLimitState(requested)
for (const entry of entries) {
trackFilesystemDirectoryEntry(state, entry)
}
}
export function estimateFilesystemDirectoryEntryBytes(entry: NamedDirectoryEntry): number {
// Why: this covers worst-case JSON escaping plus each result object's fixed overhead.
return entry.name.length * 6 + 96
}
function clampLimit(value: number | undefined, maximum: number): number {
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) {
return maximum
}
return Math.min(value, maximum)
}
@@ -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)
+1 -1
View File
@@ -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 }
-165
View File
@@ -1,165 +0,0 @@
import { describe, expect, it } from 'vitest'
import { DatabaseSync } from 'node:sqlite'
import {
formatHermesSessionMessagesWithinLimits,
HERMES_RUN_PAGE_OUTPUT_OMITTED_ERROR,
HERMES_SESSION_RUN_METADATA_MAX_BYTES,
HERMES_SESSION_RUN_SELECT_SQL,
HERMES_SESSION_TRANSCRIPT_TRUNCATED_ERROR,
hydrateHermesRunPageWithinLimits
} from './hermes-run-output-limits'
describe('HERMES_SESSION_RUN_SELECT_SQL', () => {
it('preserves exact-limit text metadata and rejects oversized non-numeric fields in SQL', () => {
const database = new DatabaseSync(':memory:')
database.exec(`CREATE TABLE sessions (
id TEXT PRIMARY KEY,
title TEXT,
started_at,
ended_at,
model TEXT,
message_count,
input_tokens,
output_tokens
)`)
const exactTitle = 't'.repeat(HERMES_SESSION_RUN_METADATA_MAX_BYTES)
const exactModel = 'm'.repeat(HERMES_SESSION_RUN_METADATA_MAX_BYTES)
database
.prepare(
`INSERT INTO sessions (
id, title, started_at, ended_at, model, message_count, input_tokens, output_tokens
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
)
.run('run-1', exactTitle, 1, 2, exactModel, 3, 4, 5)
const exact = database.prepare(HERMES_SESSION_RUN_SELECT_SQL).get('run-1')
expect(exact).toEqual({
title: exactTitle,
started_at: 1,
ended_at: 2,
model: exactModel,
message_count: 3,
input_tokens: 4,
output_tokens: 5
})
database.exec(`UPDATE sessions SET
title = title || 'overflow',
model = model || 'overflow',
started_at = zeroblob(1048576),
ended_at = zeroblob(1048576),
message_count = zeroblob(1048576),
input_tokens = zeroblob(1048576),
output_tokens = zeroblob(1048576)
WHERE id = 'run-1'`)
const bounded = database.prepare(HERMES_SESSION_RUN_SELECT_SQL).get('run-1')
expect(Buffer.byteLength(String(bounded?.title))).toBe(HERMES_SESSION_RUN_METADATA_MAX_BYTES)
expect(Buffer.byteLength(String(bounded?.model))).toBe(HERMES_SESSION_RUN_METADATA_MAX_BYTES)
expect(bounded).toMatchObject({
started_at: null,
ended_at: null,
message_count: null,
input_tokens: null,
output_tokens: null
})
database.close()
})
})
describe('formatHermesSessionMessagesWithinLimits', () => {
it('preserves transcript formatting below the limits', () => {
const result = formatHermesSessionMessagesWithinLimits([
{
role: 'assistant',
tool_name: ' terminal ',
reasoning_content: ' because ',
content: ' done '
},
{ role: null, content: ' ' }
])
expect(result).toEqual({
content: [
'## assistant / terminal',
'',
'### Reasoning',
'',
'because',
'',
'done',
'',
'---',
'',
'## message',
'',
'(empty)'
].join('\n'),
truncated: false
})
})
it('keeps UTF-8 output within the byte budget and explains truncation', () => {
const result = formatHermesSessionMessagesWithinLimits(
[{ role: 'assistant', content: '🐋'.repeat(100) }],
{ maxBytes: 160 }
)
expect(result.truncated).toBe(true)
expect(Buffer.byteLength(result.content ?? '')).toBeLessThanOrEqual(160)
expect(result.content).toContain(HERMES_SESSION_TRANSCRIPT_TRUNCATED_ERROR)
expect(result.content).not.toContain('\ufffd')
})
it('stops pulling rows after the message limit', () => {
let rowsPulled = 0
function* messages(): Generator<Record<string, unknown>> {
for (let index = 0; index < 100; index += 1) {
rowsPulled += 1
yield { role: 'user', content: `message ${index}` }
}
}
const result = formatHermesSessionMessagesWithinLimits(messages(), {
maxBytes: 1024,
maxMessages: 2
})
expect(rowsPulled).toBe(3)
expect(result.truncated).toBe(true)
expect(result.content).toContain('message 0')
expect(result.content).toContain('message 1')
expect(result.content).not.toContain('message 2')
})
})
describe('hydrateHermesRunPageWithinLimits', () => {
it('bounds concurrency, preserves order, and omits aggregate overflow', async () => {
let active = 0
let maxActive = 0
const refs = ['first', 'second', 'third', 'fourth']
const runs = (await hydrateHermesRunPageWithinLimits(
refs,
async (id) => {
active += 1
maxActive = Math.max(maxActive, active)
await new Promise<void>((resolve) => setTimeout(resolve, 1))
active -= 1
return { id, output_content: id.slice(0, 3) }
},
{ maxConcurrent: 2, maxRetainedBytes: 5 }
)) as { id: string; output_content: string | null; error?: string }[]
expect(maxActive).toBe(2)
expect(runs.map((run) => run.id)).toEqual(refs)
expect(runs[0]?.output_content).toBe('fir')
expect(runs.slice(1)).toEqual(
refs.slice(1).map((id) => ({
id,
output_content: null,
error: HERMES_RUN_PAGE_OUTPUT_OMITTED_ERROR
}))
)
})
})
-254
View File
@@ -1,254 +0,0 @@
export const HERMES_PRIMARY_OUTPUT_MAX_BYTES = 5 * 1024 * 1024
export const HERMES_SESSION_TRANSCRIPT_MAX_BYTES = 8 * 1024 * 1024
export const HERMES_SESSION_TRANSCRIPT_MAX_MESSAGES = 10_000
export const HERMES_RUN_PAGE_MAX_RETAINED_BYTES = 32 * 1024 * 1024
export const HERMES_RUN_PAGE_MAX_RUNS = 100
export const HERMES_RUN_HYDRATION_CONCURRENCY = 2
export const HERMES_SESSION_RUN_METADATA_MAX_BYTES = 16 * 1024
export const HERMES_RUN_PAGE_OUTPUT_OMITTED_ERROR =
'Run output omitted because this history page exceeds the memory limit'
export const HERMES_SESSION_TRANSCRIPT_TRUNCATED_ERROR =
'Session transcript truncated because it exceeds the 8 MiB or 10,000-message history limit'
const HERMES_SESSION_TRANSCRIPT_FIELD_MAX_BYTES = HERMES_SESSION_TRANSCRIPT_MAX_BYTES + 1
const HERMES_SESSION_TRANSCRIPT_TRUNCATION_NOTICE = `[${HERMES_SESSION_TRANSCRIPT_TRUNCATED_ERROR}.]`
const HERMES_SESSION_TRANSCRIPT_SOURCE_TRUNCATED_COLUMN = 'orca_source_truncated'
export const HERMES_SESSION_RUN_SELECT_SQL = `SELECT
CASE WHEN typeof(title) = 'text'
THEN CAST(substr(CAST(title AS BLOB), 1, ${HERMES_SESSION_RUN_METADATA_MAX_BYTES}) AS TEXT)
ELSE NULL END AS title,
CASE WHEN typeof(started_at) IN ('integer', 'real') THEN started_at ELSE NULL END AS started_at,
CASE WHEN typeof(ended_at) IN ('integer', 'real') THEN ended_at ELSE NULL END AS ended_at,
CASE WHEN typeof(model) = 'text'
THEN CAST(substr(CAST(model AS BLOB), 1, ${HERMES_SESSION_RUN_METADATA_MAX_BYTES}) AS TEXT)
ELSE NULL END AS model,
CASE WHEN typeof(message_count) IN ('integer', 'real') THEN message_count ELSE NULL END AS message_count,
CASE WHEN typeof(input_tokens) IN ('integer', 'real') THEN input_tokens ELSE NULL END AS input_tokens,
CASE WHEN typeof(output_tokens) IN ('integer', 'real') THEN output_tokens ELSE NULL END AS output_tokens
FROM sessions
WHERE id = ?`
export const HERMES_SESSION_TRANSCRIPT_SELECT_SQL = `SELECT
CAST(substr(CAST(role AS BLOB), 1, ${HERMES_SESSION_TRANSCRIPT_FIELD_MAX_BYTES}) AS TEXT) AS role,
CAST(substr(CAST(content AS BLOB), 1, ${HERMES_SESSION_TRANSCRIPT_FIELD_MAX_BYTES}) AS TEXT) AS content,
CAST(substr(CAST(tool_name AS BLOB), 1, ${HERMES_SESSION_TRANSCRIPT_FIELD_MAX_BYTES}) AS TEXT) AS tool_name,
CAST(substr(CAST(reasoning AS BLOB), 1, ${HERMES_SESSION_TRANSCRIPT_FIELD_MAX_BYTES}) AS TEXT) AS reasoning,
CAST(substr(CAST(reasoning_content AS BLOB), 1, ${HERMES_SESSION_TRANSCRIPT_FIELD_MAX_BYTES}) AS TEXT) AS reasoning_content,
CASE WHEN
length(CAST(role AS BLOB)) > ${HERMES_SESSION_TRANSCRIPT_MAX_BYTES}
OR length(CAST(content AS BLOB)) > ${HERMES_SESSION_TRANSCRIPT_MAX_BYTES}
OR length(CAST(tool_name AS BLOB)) > ${HERMES_SESSION_TRANSCRIPT_MAX_BYTES}
OR length(CAST(reasoning AS BLOB)) > ${HERMES_SESSION_TRANSCRIPT_MAX_BYTES}
OR length(CAST(reasoning_content AS BLOB)) > ${HERMES_SESSION_TRANSCRIPT_MAX_BYTES}
THEN 1 ELSE 0 END AS ${HERMES_SESSION_TRANSCRIPT_SOURCE_TRUNCATED_COLUMN}
FROM messages
WHERE session_id = ?
ORDER BY timestamp, id
LIMIT ${HERMES_SESSION_TRANSCRIPT_MAX_MESSAGES + 1}`
type HermesRunPageLimits = {
maxConcurrent?: number
maxRetainedBytes?: number
}
type HermesSessionTranscriptLimits = {
maxBytes?: number
maxMessages?: number
}
export type FormattedHermesSessionMessages = {
content: string | null
truncated: boolean
}
export async function hydrateHermesRunPageWithinLimits<T>(
refs: readonly T[],
hydrate: (ref: T) => Promise<unknown>,
limits: HermesRunPageLimits = {}
): Promise<unknown[]> {
const maxConcurrent = clampFiniteLimit(limits.maxConcurrent, 1, HERMES_RUN_HYDRATION_CONCURRENCY)
const maxRetainedBytes = clampFiniteLimit(
limits.maxRetainedBytes,
0,
HERMES_RUN_PAGE_MAX_RETAINED_BYTES
)
const results: unknown[] = []
let retainedBytes = 0
for (let start = 0; start < refs.length; start += maxConcurrent) {
const batch = await Promise.all(refs.slice(start, start + maxConcurrent).map(hydrate))
for (const run of batch) {
const runBytes = hermesRunOutputByteLength(run)
if (retainedBytes + runBytes <= maxRetainedBytes) {
results.push(run)
retainedBytes += runBytes
} else {
results.push(omitHermesRunOutput(run))
}
}
}
return results
}
export function formatHermesSessionMessagesWithinLimits(
messages: Iterable<Record<string, unknown>>,
limits: HermesSessionTranscriptLimits = {}
): FormattedHermesSessionMessages {
const maxBytes = clampFiniteLimit(limits.maxBytes, 0, HERMES_SESSION_TRANSCRIPT_MAX_BYTES)
const maxMessages = clampFiniteLimit(
limits.maxMessages,
0,
HERMES_SESSION_TRANSCRIPT_MAX_MESSAGES
)
const chunks: string[] = []
let retainedBytes = 0
let messageCount = 0
let truncated = false
transcript: for (const message of messages) {
if (messageCount >= maxMessages) {
truncated = true
break
}
for (const part of formatHermesSessionMessageParts(message, messageCount > 0)) {
const partBytes = Buffer.byteLength(part)
if (retainedBytes + partBytes <= maxBytes) {
chunks.push(part)
retainedBytes += partBytes
continue
}
const prefix = takeUtf8Prefix(part, maxBytes - retainedBytes)
if (prefix) {
chunks.push(prefix)
retainedBytes += Buffer.byteLength(prefix)
}
truncated = true
break transcript
}
messageCount += 1
if (message[HERMES_SESSION_TRANSCRIPT_SOURCE_TRUNCATED_COLUMN] === 1) {
truncated = true
break
}
}
if (truncated) {
appendTranscriptTruncationNotice(chunks, retainedBytes, maxBytes)
}
return { content: chunks.length > 0 ? chunks.join('') : null, truncated }
}
function clampFiniteLimit(value: number | undefined, minimum: number, maximum: number): number {
if (value === undefined || !Number.isFinite(value)) {
return maximum
}
return Math.max(minimum, Math.min(maximum, Math.floor(value)))
}
function hermesRunOutputByteLength(run: unknown): number {
if (!run || typeof run !== 'object' || Array.isArray(run)) {
return 0
}
const content = (run as Record<string, unknown>).output_content
return typeof content === 'string' ? Buffer.byteLength(content) : 0
}
function formatHermesSessionMessageParts(
message: Record<string, unknown>,
includeSeparator: boolean
): string[] {
const role = typeof message.role === 'string' ? message.role : 'message'
const content = typeof message.content === 'string' ? message.content.trim() : ''
const toolName = typeof message.tool_name === 'string' ? message.tool_name.trim() : ''
const reasoning =
typeof message.reasoning_content === 'string'
? message.reasoning_content.trim()
: typeof message.reasoning === 'string'
? message.reasoning.trim()
: ''
const parts = [
includeSeparator ? '\n\n---\n\n' : '',
`## ${role}${toolName ? ` / ${toolName}` : ''}`
]
if (reasoning) {
parts.push('\n\n### Reasoning\n\n', reasoning)
}
parts.push('\n\n', content || '(empty)')
return parts
}
function appendTranscriptTruncationNotice(
chunks: string[],
retainedBytes: number,
maxBytes: number
): void {
const separator = chunks.length > 0 ? '\n\n---\n\n' : ''
const notice = `${separator}${HERMES_SESSION_TRANSCRIPT_TRUNCATION_NOTICE}`
const boundedNotice = takeUtf8Prefix(notice, maxBytes)
const noticeBytes = Buffer.byteLength(boundedNotice)
const targetBytes = maxBytes - noticeBytes
trimUtf8Chunks(chunks, retainedBytes, targetBytes)
if (boundedNotice) {
chunks.push(boundedNotice)
}
}
function trimUtf8Chunks(chunks: string[], retainedBytes: number, targetBytes: number): void {
let bytes = retainedBytes
while (bytes > targetBytes && chunks.length > 0) {
const lastIndex = chunks.length - 1
const last = chunks[lastIndex]!
const lastBytes = Buffer.byteLength(last)
const allowedBytes = Math.max(0, lastBytes - (bytes - targetBytes))
if (allowedBytes === 0) {
chunks.pop()
bytes -= lastBytes
continue
}
const prefix = takeUtf8Prefix(last, allowedBytes)
chunks[lastIndex] = prefix
bytes = bytes - lastBytes + Buffer.byteLength(prefix)
}
}
function takeUtf8Prefix(value: string, maxBytes: number): string {
if (maxBytes <= 0) {
return ''
}
if (Buffer.byteLength(value) <= maxBytes) {
return value
}
let low = 0
let high = Math.min(value.length, maxBytes)
while (low < high) {
const middle = Math.ceil((low + high) / 2)
if (Buffer.byteLength(value.slice(0, middle)) <= maxBytes) {
low = middle
} else {
high = middle - 1
}
}
if (low > 0 && isHighSurrogate(value.charCodeAt(low - 1))) {
low -= 1
}
return value.slice(0, low)
}
function isHighSurrogate(code: number): boolean {
return code >= 0xd800 && code <= 0xdbff
}
function omitHermesRunOutput(run: unknown): unknown {
if (!run || typeof run !== 'object' || Array.isArray(run)) {
return { output_content: null, error: HERMES_RUN_PAGE_OUTPUT_OMITTED_ERROR }
}
const record = run as Record<string, unknown>
const existingError = typeof record.error === 'string' && record.error ? record.error : null
return {
...record,
output_content: null,
error: existingError
? `${existingError}; ${HERMES_RUN_PAGE_OUTPUT_OMITTED_ERROR}`
: HERMES_RUN_PAGE_OUTPUT_OMITTED_ERROR
}
}
@@ -1,36 +0,0 @@
import { describe, expect, it } from 'vitest'
import { HermesRunRefRetainer } from './hermes-run-ref-retention'
describe('HermesRunRefRetainer', () => {
it('preserves every ref and exact order below the limit', () => {
const retainer = new HermesRunRefRetainer<{ id: string; run_at: string }>(3)
retainer.add({ id: 'middle', run_at: '2026-05-15T09:00:00Z' })
retainer.add({ id: 'newest', run_at: '2026-05-16T09:00:00Z' })
retainer.add({ id: 'oldest', run_at: '2026-05-14T09:00:00Z' })
expect(retainer.finish()).toEqual({
refs: [
{ id: 'newest', run_at: '2026-05-16T09:00:00Z' },
{ id: 'middle', run_at: '2026-05-15T09:00:00Z' },
{ id: 'oldest', run_at: '2026-05-14T09:00:00Z' }
],
saturated: false
})
})
it('retains the exact newest window and reports saturation', () => {
const retainer = new HermesRunRefRetainer<{ id: string; run_at: string }>(3)
for (const day of [1, 5, 2, 6, 3, 4]) {
retainer.add({ id: `day-${day}`, run_at: `2026-05-0${day}T09:00:00Z` })
}
expect(retainer.finish()).toEqual({
refs: [
{ id: 'day-6', run_at: '2026-05-06T09:00:00Z' },
{ id: 'day-5', run_at: '2026-05-05T09:00:00Z' },
{ id: 'day-4', run_at: '2026-05-04T09:00:00Z' }
],
saturated: true
})
})
})
-78
View File
@@ -1,78 +0,0 @@
export const HERMES_RUN_REF_MAX_ENTRIES = 10_000
export const HERMES_RUN_REF_ID_MAX_BYTES = 4 * 1024
export const HERMES_SESSION_RUN_REFS_SELECT_SQL = `SELECT
CASE WHEN length(CAST(id AS BLOB)) <= ${HERMES_RUN_REF_ID_MAX_BYTES} THEN id ELSE NULL END AS id,
CASE WHEN typeof(started_at) IN ('integer', 'real') THEN started_at ELSE NULL END AS started_at
FROM sessions
WHERE id LIKE ? ESCAPE '\\'
ORDER BY started_at DESC
LIMIT ${HERMES_RUN_REF_MAX_ENTRIES + 1}`
export type HermesSortableRunRef = {
id: string
run_at: string | null
}
export type BoundedHermesRunRefs<T> = {
refs: T[]
saturated: boolean
}
export class HermesRunRefRetainer<T extends HermesSortableRunRef> {
private readonly retained: T[] = []
private readonly pending: T[] = []
private readonly maxEntries: number
private seen = 0
constructor(maxEntries = HERMES_RUN_REF_MAX_ENTRIES) {
this.maxEntries = Number.isFinite(maxEntries)
? Math.max(0, Math.min(HERMES_RUN_REF_MAX_ENTRIES, Math.floor(maxEntries)))
: HERMES_RUN_REF_MAX_ENTRIES
}
add(ref: T): void {
this.seen += 1
if (this.maxEntries === 0) {
return
}
this.pending.push(ref)
if (this.pending.length >= this.maxEntries) {
this.flush()
}
}
finish(): BoundedHermesRunRefs<T> {
this.flush()
return {
refs: this.retained.slice(),
saturated: this.seen > this.maxEntries
}
}
private flush(): void {
if (this.pending.length === 0) {
return
}
for (const ref of this.pending) {
this.retained.push(ref)
}
this.pending.length = 0
this.retained.sort(compareHermesRunRefsNewestFirst)
if (this.retained.length > this.maxEntries) {
this.retained.length = this.maxEntries
}
}
}
export function compareHermesRunRefsNewestFirst(
left: HermesSortableRunRef,
right: HermesSortableRunRef
): number {
const leftTime = left.run_at ? Date.parse(left.run_at) : Number.NaN
const rightTime = right.run_at ? Date.parse(right.run_at) : Number.NaN
if (Number.isFinite(leftTime) && Number.isFinite(rightTime)) {
return rightTime - leftTime
}
return right.id.localeCompare(left.id)
}
-13
View File
@@ -1,13 +0,0 @@
import { measureUtf8ByteLength } from './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'
export function assertHtmlToPdfInputWithinMemoryLimit(
html: string,
maxBytes = HTML_TO_PDF_MAX_INPUT_BYTES
): void {
if (measureUtf8ByteLength(html, { stopAfterBytes: maxBytes }).exceededLimit) {
throw new Error(HTML_TO_PDF_MEMORY_LIMIT_ERROR)
}
}
@@ -1,52 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import {
createLinuxProcTextReadBudget,
LINUX_PROC_NETWORK_TABLE_MAX_BYTES,
readLinuxProcNetworkTable,
readLinuxProcTextWithinBudget
} from './linux-proc-port-scan-limits'
describe('Linux proc port scan limits', () => {
it('applies the network-table byte cap before retaining content', async () => {
const readFile = vi.fn(async () => Buffer.from('table'))
await expect(readLinuxProcNetworkTable('/proc/net/tcp', readFile)).resolves.toBe('table')
expect(readFile).toHaveBeenCalledWith('/proc/net/tcp', LINUX_PROC_NETWORK_TABLE_MAX_BYTES)
})
it('shares one retained-byte budget across process metadata files', async () => {
const budget = createLinuxProcTextReadBudget(5)
const readFile = vi
.fn<(filePath: string, maxBytes: number) => Promise<Buffer>>()
.mockResolvedValueOnce(Buffer.from('abc'))
.mockResolvedValueOnce(Buffer.from('de'))
await expect(readLinuxProcTextWithinBudget('/proc/1/comm', budget, readFile, 4)).resolves.toBe(
'abc'
)
await expect(
readLinuxProcTextWithinBudget('/proc/1/cmdline', budget, readFile, 4)
).resolves.toBe('de')
await expect(
readLinuxProcTextWithinBudget('/proc/2/cmdline', budget, readFile, 4)
).resolves.toBeUndefined()
expect(readFile.mock.calls).toEqual([
['/proc/1/comm', 4],
['/proc/1/cmdline', 2]
])
expect(budget.remainingBytes).toBe(0)
})
it('does not debit failed metadata reads', async () => {
const budget = createLinuxProcTextReadBudget(4)
const readFile = vi.fn(async () => {
throw new Error('oversized')
})
await expect(
readLinuxProcTextWithinBudget('/proc/1/cmdline', budget, readFile)
).resolves.toBeUndefined()
expect(budget.remainingBytes).toBe(4)
})
})
-55
View File
@@ -1,55 +0,0 @@
import { readNodeFileWithinLimit } from './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
export const LINUX_PROC_PROCESS_METADATA_MAX_BYTES = 8 * 1024 * 1024
export const LINUX_PROC_PROCESS_METADATA_FILE_MAX_BYTES = 64 * 1024
export type LinuxProcTextReadBudget = { remainingBytes: number }
type LinuxProcTextReader = (filePath: string, maxBytes: number) => Promise<Buffer>
const readBoundedNodeFile: LinuxProcTextReader = async (filePath, maxBytes) =>
(await readNodeFileWithinLimit(filePath, maxBytes)).buffer
export function createLinuxProcTextReadBudget(
maxBytes = LINUX_PROC_PROCESS_METADATA_MAX_BYTES
): LinuxProcTextReadBudget {
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
throw new RangeError('Linux proc text budget must be a non-negative safe integer')
}
return { remainingBytes: maxBytes }
}
export async function readLinuxProcNetworkTable(
filePath: string,
readFile: LinuxProcTextReader = readBoundedNodeFile
): Promise<string | null> {
try {
return (await readFile(filePath, LINUX_PROC_NETWORK_TABLE_MAX_BYTES)).toString('utf8')
} catch {
return null
}
}
export async function readLinuxProcTextWithinBudget(
filePath: string,
budget: LinuxProcTextReadBudget,
readFile: LinuxProcTextReader = readBoundedNodeFile,
perFileMaxBytes = LINUX_PROC_PROCESS_METADATA_FILE_MAX_BYTES
): Promise<string | undefined> {
const maxBytes = Math.min(perFileMaxBytes, budget.remainingBytes)
if (maxBytes <= 0) {
return undefined
}
try {
const content = await readFile(filePath, maxBytes)
if (content.byteLength > maxBytes) {
return undefined
}
budget.remainingBytes -= content.byteLength
return content.toString('utf8')
} catch {
return undefined
}
}
@@ -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
+1 -1
View File
@@ -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
+58
View File
@@ -0,0 +1,58 @@
# 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.
## 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` | Reading/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.
## 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.
@@ -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,
@@ -1,37 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
assertMobileFileDirectoryWithinLimit,
MOBILE_FILE_DIRECTORY_LIMIT_MESSAGE,
MOBILE_FILE_DIRECTORY_MAX_ENTRIES,
MOBILE_FILE_DIRECTORY_MAX_RETAINED_BYTES
} from './mobile-file-directory-limit'
describe('mobile file directory limit', () => {
it('accepts a complete directory listing within both limits', () => {
expect(() =>
assertMobileFileDirectoryWithinLimit([
{ name: 'src' },
{ name: 'README.md' },
{ name: 'package.json' }
])
).not.toThrow()
})
it('rejects rather than truncating an excessive entry count', () => {
const entries = Array.from({ length: MOBILE_FILE_DIRECTORY_MAX_ENTRIES + 1 }, () => ({
name: 'x'
}))
expect(() => assertMobileFileDirectoryWithinLimit(entries)).toThrow(
MOBILE_FILE_DIRECTORY_LIMIT_MESSAGE
)
})
it('rejects a listing whose names exceed the retained-byte limit', () => {
const name = 'x'.repeat(MOBILE_FILE_DIRECTORY_MAX_RETAINED_BYTES / 2)
expect(() => assertMobileFileDirectoryWithinLimit([{ name }])).toThrow(
MOBILE_FILE_DIRECTORY_LIMIT_MESSAGE
)
})
})
-43
View File
@@ -1,43 +0,0 @@
// Why: normal repositories stay complete while pathological fan-out/name payloads fail before retention.
export const MOBILE_FILE_DIRECTORY_MAX_ENTRIES = 10_000
export const MOBILE_FILE_DIRECTORY_MAX_RETAINED_BYTES = 4 * 1024 * 1024
export const MOBILE_FILE_DIRECTORY_LIMIT_MESSAGE =
'This folder is too large to show safely on mobile (limit: 10,000 items or a 4 MB listing).'
type NamedDirectoryEntry = { name: string }
export type MobileFileDirectoryLimitState = {
entries: number
retainedBytes: number
}
export function createMobileFileDirectoryLimitState(): MobileFileDirectoryLimitState {
return { entries: 0, retainedBytes: 0 }
}
export function trackMobileFileDirectoryEntry(
state: MobileFileDirectoryLimitState,
entry: NamedDirectoryEntry
): void {
state.entries += 1
state.retainedBytes += estimateMobileDirectoryEntryBytes(entry)
if (
state.entries > MOBILE_FILE_DIRECTORY_MAX_ENTRIES ||
state.retainedBytes > MOBILE_FILE_DIRECTORY_MAX_RETAINED_BYTES
) {
throw new Error(MOBILE_FILE_DIRECTORY_LIMIT_MESSAGE)
}
}
export function assertMobileFileDirectoryWithinLimit(
entries: readonly NamedDirectoryEntry[]
): void {
const state = createMobileFileDirectoryLimitState()
for (const entry of entries) {
trackMobileFileDirectoryEntry(state, entry)
}
}
export function estimateMobileDirectoryEntryBytes(entry: NamedDirectoryEntry): number {
return entry.name.length * 2 + 64
}
@@ -2,7 +2,7 @@ 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[] = []
+1 -1
View File
@@ -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
@@ -1,171 +0,0 @@
import { createHash } from 'node:crypto'
import {
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
truncateSync,
writeFileSync
} from 'node:fs'
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 {
PersistedStateSecretCapacityError,
assertPersistedStateSecretWithinLimit,
readPersistedStateJsonFileSync,
replacePersistedStateJsonWithinLimit,
restorePersistedStateBackupSync,
stringifyPrettyPersistedStateWithinLimit,
stringifyPersistedStateWithinLimit,
updatePersistedStateHashWithJsonRange
} from './persisted-state-file-bounds'
describe('persisted state file bounds', () => {
let root = ''
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), 'orca-state-bounds-'))
})
afterEach(() => {
rmSync(root, { recursive: true, force: true })
})
it('reads and parses a state file exactly at the byte limit', () => {
const path = join(root, 'state.json')
const json = `{"value":"${'x'.repeat(20)}"}`
writeFileSync(path, json)
expect(
readPersistedStateJsonFileSync<{ value: string }>(path, Buffer.byteLength(json))
).toEqual({
byteLength: Buffer.byteLength(json),
value: { value: 'x'.repeat(20) }
})
})
it('rejects an oversized sparse state file before reading its body', () => {
const path = join(root, 'state.json')
writeFileSync(path, '')
truncateSync(path, 1025)
expect(() => readPersistedStateJsonFileSync(path, 1024)).toThrow(NodeFileReadTooLargeError)
})
it('rejects structurally amplified state before parsing it', () => {
const path = join(root, 'state.json')
const json = '{"rows":[{},{}]}'
writeFileSync(path, json)
expect(() =>
readPersistedStateJsonFileSync(path, Buffer.byteLength(json), {
structuralTokens: 7,
nestingDepth: 3
})
).toThrow('JSON structure')
})
it('matches native compact JSON exactly at the output boundary', () => {
const state = { quote: '"', unicode: '🐋', nested: [1, true, null] }
const native = JSON.stringify(state)
expect(stringifyPersistedStateWithinLimit(state, Buffer.byteLength(native))).toEqual({
byteLength: Buffer.byteLength(native),
serialized: native
})
expect(() => stringifyPersistedStateWithinLimit(state, Buffer.byteLength(native) - 1)).toThrow(
JsonStringifyByteLimitError
)
})
it('matches native pretty JSON and enforces its whitespace-inclusive boundary', () => {
const state = { nested: { value: 'x' }, list: [1, 2] }
const native = JSON.stringify(state, null, 2)
expect(stringifyPrettyPersistedStateWithinLimit(state, Buffer.byteLength(native))).toEqual({
byteLength: Buffer.byteLength(native),
serialized: native
})
expect(() =>
stringifyPrettyPersistedStateWithinLimit(state, Buffer.byteLength(native) - 1)
).toThrow(JsonStringifyByteLimitError)
})
it('bounds secret plaintext before encryption can expand it', () => {
assertPersistedStateSecretWithinLimit('🐋', 4)
expect(() => assertPersistedStateSecretWithinLimit('🐋x', 4)).toThrow(
PersistedStateSecretCapacityError
)
})
it('checks replacement growth before constructing the next payload', () => {
const serialized = '{"value":"slot"}'
const exactBytes =
Buffer.byteLength(serialized) - Buffer.byteLength('slot') + Buffer.byteLength('expanded')
expect(
replacePersistedStateJsonWithinLimit({
serialized,
currentBytes: Buffer.byteLength(serialized),
search: 'slot',
replacement: 'expanded',
maxBytes: exactBytes
})
).toEqual({ byteLength: exactBytes, serialized: '{"value":"expanded"}' })
expect(() =>
replacePersistedStateJsonWithinLimit({
serialized,
currentBytes: Buffer.byteLength(serialized),
search: 'slot',
replacement: 'expanded',
maxBytes: exactBytes - 1
})
).toThrow(JsonStringifyByteLimitError)
})
it('hashes bounded string ranges without splitting UTF-16 surrogate pairs', () => {
const value = `prefix-${'x'.repeat(8)}🐋-${'y'.repeat(8)}-suffix`
const expected = createHash('sha1').update(value).digest('hex')
const actual = createHash('sha1')
updatePersistedStateHashWithJsonRange(actual, value, 0, value.length, 2)
expect(actual.digest('hex')).toBe(expected)
})
it('atomically restores only a valid in-limit backup', () => {
const backupPath = join(root, 'backup.json')
const targetPath = join(root, 'profile', 'orca-data.json')
writeFileSync(backupPath, '{"repos":[{"id":"recovered"}]}')
restorePersistedStateBackupSync(backupPath, targetPath, 1024)
expect(JSON.parse(readFileSync(targetPath, 'utf8'))).toEqual({
repos: [{ id: 'recovered' }]
})
const originalTarget = readFileSync(targetPath)
writeFileSync(backupPath, '{{invalid')
expect(() => restorePersistedStateBackupSync(backupPath, targetPath, 1024)).toThrow()
expect(readFileSync(targetPath)).toEqual(originalTarget)
expect(
readdirSync(join(root, 'profile')).filter((name) => name.endsWith('.recovery.tmp'))
).toEqual([])
})
it('leaves the target untouched when a backup exceeds the cap', () => {
const backupPath = join(root, 'backup.json')
const targetPath = join(root, 'orca-data.json')
writeFileSync(targetPath, '{"original":true}')
writeFileSync(backupPath, '')
truncateSync(backupPath, 1025)
expect(() => restorePersistedStateBackupSync(backupPath, targetPath, 1024)).toThrow(
NodeFileReadTooLargeError
)
expect(readFileSync(targetPath, 'utf8')).toBe('{"original":true}')
})
})
-222
View File
@@ -1,222 +0,0 @@
import { randomUUID, type Hash } from 'node:crypto'
import { mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs'
import { dirname } from 'node:path'
import {
NodeFileReadTooLargeError,
readNodeFileSyncWithinLimit,
type BoundedNodeFileRead
} from './node-bounded-file-reader'
import {
JsonStringifyByteLimitError,
stringifyJsonWithinByteLimit
} from './node-bounded-json-stringify'
import {
assertJsonTextStructureWithinLimits,
type JsonTextStructureLimits
} from './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
export const ORCA_PERSISTED_STATE_HASH_CHUNK_CODE_UNITS = 64 * 1024
export const ORCA_PERSISTED_STATE_JSON_LIMITS: JsonTextStructureLimits = {
structuralTokens: 4_000_000,
nestingDepth: 256
}
export type PersistedStateJsonRead<T> = {
byteLength: number
value: T
}
export class PersistedStateSecretCapacityError extends Error {
constructor(
readonly observedBytes: number,
readonly maxBytes = ORCA_PERSISTED_STATE_SECRET_MAX_BYTES
) {
super(`Persisted state secret exceeds ${maxBytes} bytes`)
this.name = 'PersistedStateSecretCapacityError'
}
}
export function isPersistedStateFileCapacityError(
error: unknown
): error is NodeFileReadTooLargeError {
return error instanceof NodeFileReadTooLargeError
}
export function readPersistedStateJsonFileSync<T>(
filePath: string,
maxBytes = ORCA_PERSISTED_STATE_MAX_BYTES,
structureLimits: JsonTextStructureLimits = ORCA_PERSISTED_STATE_JSON_LIMITS
): PersistedStateJsonRead<T> {
const { buffer } = readPersistedStateFileBytesSync(filePath, maxBytes)
return {
byteLength: buffer.byteLength,
value: parsePersistedStateJsonBuffer<T>(buffer, structureLimits)
}
}
export function readPersistedStateFileBytesSync(
filePath: string,
maxBytes = ORCA_PERSISTED_STATE_MAX_BYTES
): BoundedNodeFileRead {
return readNodeFileSyncWithinLimit(filePath, maxBytes)
}
export function parsePersistedStateJsonBuffer<T>(
buffer: Buffer,
structureLimits: JsonTextStructureLimits = ORCA_PERSISTED_STATE_JSON_LIMITS
): T {
const serialized = buffer.toString('utf8')
assertJsonTextStructureWithinLimits(serialized, structureLimits)
return JSON.parse(serialized) as T
}
export function stringifyPersistedStateWithinLimit(
value: unknown,
maxBytes = ORCA_PERSISTED_STATE_MAX_BYTES
): { byteLength: number; serialized: string } {
return stringifyJsonWithinByteLimit(value, maxBytes)
}
export function stringifyPrettyPersistedStateWithinLimit(
value: unknown,
maxBytes = ORCA_PERSISTED_STATE_MAX_BYTES
): { byteLength: number; serialized: string } {
return stringifyJsonWithinByteLimit(value, maxBytes, 2)
}
export function encodePersistedStateJsonStringContent(
value: string,
maxBytes = ORCA_PERSISTED_STATE_MAX_BYTES
): string {
const { serialized } = stringifyJsonWithinByteLimit(value, maxBytes)
return serialized.slice(1, -1)
}
export function assertPersistedStateSecretWithinLimit(
value: string,
maxBytes = ORCA_PERSISTED_STATE_SECRET_MAX_BYTES
): void {
const observedBytes = Buffer.byteLength(value, 'utf8')
if (observedBytes > maxBytes) {
throw new PersistedStateSecretCapacityError(observedBytes, maxBytes)
}
}
export function replacedPersistedStateJsonByteLength(options: {
currentBytes: number
maxBytes?: number
replacement: string
search: string
}): number {
const maxBytes = options.maxBytes ?? ORCA_PERSISTED_STATE_MAX_BYTES
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
throw new RangeError('Persisted state JSON byte limit must be a non-negative safe integer')
}
if (!Number.isSafeInteger(options.currentBytes) || options.currentBytes < 0) {
throw new RangeError('Persisted state JSON byte count must be a non-negative safe integer')
}
if (options.currentBytes > maxBytes) {
throw new JsonStringifyByteLimitError(options.currentBytes, maxBytes)
}
const nextBytes =
options.currentBytes -
Buffer.byteLength(options.search, 'utf8') +
Buffer.byteLength(options.replacement, 'utf8')
if (!Number.isSafeInteger(nextBytes) || nextBytes < 0 || nextBytes > maxBytes) {
throw new JsonStringifyByteLimitError(nextBytes, maxBytes)
}
return nextBytes
}
export function replacePersistedStateJsonWithinLimit(options: {
currentBytes: number
maxBytes?: number
replacement: string
search: string
serialized: string
}): { byteLength: number; serialized: string } {
const byteLength = replacedPersistedStateJsonByteLength(options)
const searchIndex = options.serialized.indexOf(options.search)
if (searchIndex === -1) {
throw new Error('Persisted state JSON replacement slot is missing')
}
if (options.serialized.includes(options.search, searchIndex + options.search.length)) {
throw new Error('Persisted state JSON replacement slot is ambiguous')
}
return {
byteLength,
serialized: options.serialized.replace(options.search, () => options.replacement)
}
}
export function updatePersistedStateHashWithJsonRange(
hash: Pick<Hash, 'update'>,
value: string,
start = 0,
end = value.length,
chunkCodeUnits = ORCA_PERSISTED_STATE_HASH_CHUNK_CODE_UNITS
): void {
if (
!Number.isSafeInteger(start) ||
!Number.isSafeInteger(end) ||
start < 0 ||
end < start ||
end > value.length
) {
throw new RangeError('Persisted state hash range is invalid')
}
if (!Number.isSafeInteger(chunkCodeUnits) || chunkCodeUnits <= 0) {
throw new RangeError('Persisted state hash chunk size must be a positive safe integer')
}
let offset = start
while (offset < end) {
let nextOffset = Math.min(end, offset + chunkCodeUnits)
if (
nextOffset < end &&
isHighSurrogate(value.charCodeAt(nextOffset - 1)) &&
isLowSurrogate(value.charCodeAt(nextOffset))
) {
nextOffset += 1
}
hash.update(value.slice(offset, nextOffset), 'utf8')
offset = nextOffset
}
}
export function restorePersistedStateBackupSync(
sourcePath: string,
targetPath: string,
maxBytes = ORCA_PERSISTED_STATE_MAX_BYTES
): number {
const read = readValidatedPersistedStateBytesSync(sourcePath, maxBytes)
mkdirSync(dirname(targetPath), { recursive: true })
const temporaryPath = `${targetPath}.${process.pid}.${randomUUID()}.recovery.tmp`
try {
writeFileSync(temporaryPath, read.buffer)
renameSync(temporaryPath, targetPath)
} catch (error) {
rmSync(temporaryPath, { force: true })
throw error
}
return read.buffer.byteLength
}
function readValidatedPersistedStateBytesSync(
filePath: string,
maxBytes: number
): BoundedNodeFileRead {
const read = readPersistedStateFileBytesSync(filePath, maxBytes)
parsePersistedStateJsonBuffer(read.buffer)
return read
}
function isHighSurrogate(code: number): boolean {
return code >= 0xd800 && code <= 0xdbff
}
function isLowSurrogate(code: number): boolean {
return code >= 0xdc00 && code <= 0xdfff
}
-5
View File
@@ -1,5 +0,0 @@
export const PR_REFRESH_QUEUE_ENTRY_LIMIT = 1_024
export const PR_REFRESH_ALIAS_LIMIT = 64
export const PR_REFRESH_VISIBLE_CANDIDATE_LIMIT = 1_024
export const PR_REFRESH_RETRY_STATE_LIMIT = 1_024
export const PR_REFRESH_ACTIVE_SCOPE_LIMIT = 1_024
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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 =
@@ -1,82 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import {
createQuickOpenListingBudget,
QuickOpenSubprocessPathAccumulator,
resolveQuickOpenResultLimit,
retainQuickOpenPath,
QUICK_OPEN_LISTING_MAX_PATH_BYTES,
QUICK_OPEN_LISTING_MAX_RETAINED_PATH_BYTES,
QUICK_OPEN_LISTING_MAX_RETAINED_PATHS,
QUICK_OPEN_LISTING_MAX_RESULTS
} from './quick-open-listing-limits'
describe('Quick Open listing limits', () => {
it('uses one hard result cap while preserving smaller requested limits', () => {
expect(resolveQuickOpenResultLimit()).toBe(QUICK_OPEN_LISTING_MAX_RESULTS)
expect(resolveQuickOpenResultLimit(17)).toBe(17)
expect(resolveQuickOpenResultLimit(QUICK_OPEN_LISTING_MAX_RESULTS + 1)).toBe(
QUICK_OPEN_LISTING_MAX_RESULTS
)
expect(resolveQuickOpenResultLimit(0)).toBe(0)
})
it('keeps the production memory ceilings explicit', () => {
expect(QUICK_OPEN_LISTING_MAX_RESULTS).toBe(20_001)
expect(QUICK_OPEN_LISTING_MAX_RETAINED_PATHS).toBe(100_000)
expect(QUICK_OPEN_LISTING_MAX_RETAINED_PATH_BYTES).toBe(32 * 1024 * 1024)
expect(QUICK_OPEN_LISTING_MAX_PATH_BYTES).toBe(64 * 1024)
})
it('accepts the exact retained path boundaries without charging duplicates', () => {
const paths = new Set<string>()
const budget = createQuickOpenListingBudget({
maxRetainedPaths: 2,
maxRetainedPathBytes: 3,
maxPathBytes: 2
})
expect(retainQuickOpenPath(paths, 'ab', budget)).toBe(true)
expect(retainQuickOpenPath(paths, 'ab', budget)).toBe(false)
expect(retainQuickOpenPath(paths, 'c', budget)).toBe(true)
expect(budget).toMatchObject({ retainedPathCount: 2, retainedPathBytes: 3 })
expect(() => retainQuickOpenPath(paths, '', budget)).toThrow('2 retained paths')
})
it('rejects path-byte overflow without mutating the retained budget', () => {
const paths = new Set<string>()
const budget = createQuickOpenListingBudget({
maxRetainedPaths: 3,
maxRetainedPathBytes: 2,
maxPathBytes: 2
})
retainQuickOpenPath(paths, 'ab', budget)
expect(() => retainQuickOpenPath(paths, 'c', budget)).toThrow('2 retained path bytes')
expect(paths).toEqual(new Set(['ab']))
expect(budget).toMatchObject({ retainedPathCount: 1, retainedPathBytes: 2 })
})
it('bounds one fragmented subprocess path and recovers after overflow', () => {
const onPath = vi.fn(() => true)
const fields = new QuickOpenSubprocessPathAccumulator(0, 3)
expect(fields.push(Buffer.from('ab'), onPath)).toBe('continue')
expect(fields.push(Buffer.from('cd'), onPath)).toBe('path-too-large')
expect(fields.push(Buffer.from('ok\0'), onPath)).toBe('continue')
expect(onPath).toHaveBeenCalledTimes(1)
expect(onPath).toHaveBeenCalledWith('ok')
})
it('stops within a multi-path chunk without visiting later fields', () => {
const visited: string[] = []
const fields = new QuickOpenSubprocessPathAccumulator(0, 16)
expect(
fields.push(Buffer.from('one\0two\0three\0'), (path) => {
visited.push(path)
return path !== 'two'
})
).toBe('stopped')
expect(visited).toEqual(['one', 'two'])
})
})
-142
View File
@@ -1,142 +0,0 @@
import { GrowingByteBuffer } from './growing-byte-buffer'
export const QUICK_OPEN_LISTING_MAX_RESULTS = 20_001
export const QUICK_OPEN_LISTING_MAX_RETAINED_PATHS = 100_000
export const QUICK_OPEN_LISTING_MAX_RETAINED_PATH_BYTES = 32 * 1024 * 1024
export const QUICK_OPEN_LISTING_MAX_PATH_BYTES = 64 * 1024
export type QuickOpenListingBudget = {
retainedPathCount: number
retainedPathBytes: number
maxRetainedPaths: number
maxRetainedPathBytes: number
maxPathBytes: number
}
export function resolveQuickOpenResultLimit(requested?: number): number {
if (requested === undefined || requested === Number.POSITIVE_INFINITY) {
return QUICK_OPEN_LISTING_MAX_RESULTS
}
if (!Number.isFinite(requested)) {
return 0
}
return Math.min(Math.max(Math.trunc(requested), 0), QUICK_OPEN_LISTING_MAX_RESULTS)
}
export function createQuickOpenListingBudget(
limits: Partial<
Pick<QuickOpenListingBudget, 'maxRetainedPaths' | 'maxRetainedPathBytes' | 'maxPathBytes'>
> = {}
): QuickOpenListingBudget {
const maxRetainedPaths = limits.maxRetainedPaths ?? QUICK_OPEN_LISTING_MAX_RETAINED_PATHS
const maxRetainedPathBytes =
limits.maxRetainedPathBytes ?? QUICK_OPEN_LISTING_MAX_RETAINED_PATH_BYTES
const maxPathBytes = limits.maxPathBytes ?? QUICK_OPEN_LISTING_MAX_PATH_BYTES
for (const [name, value] of Object.entries({
maxRetainedPaths,
maxRetainedPathBytes,
maxPathBytes
})) {
if (!Number.isSafeInteger(value) || value < 0) {
throw new RangeError(`${name} must be a non-negative safe integer`)
}
}
return {
retainedPathCount: 0,
retainedPathBytes: 0,
maxRetainedPaths,
maxRetainedPathBytes,
maxPathBytes
}
}
export function retainQuickOpenPath(
paths: Set<string>,
path: string,
budget: QuickOpenListingBudget
): boolean {
if (paths.has(path)) {
return false
}
const pathBytes = Buffer.byteLength(path, 'utf8')
if (pathBytes > budget.maxPathBytes) {
throw new Error(`Quick Open file path exceeded ${budget.maxPathBytes} bytes`)
}
if (budget.retainedPathCount >= budget.maxRetainedPaths) {
throw new Error(`Quick Open file listing exceeded ${budget.maxRetainedPaths} retained paths`)
}
if (pathBytes > budget.maxRetainedPathBytes - budget.retainedPathBytes) {
throw new Error(
`Quick Open file listing exceeded ${budget.maxRetainedPathBytes} retained path bytes`
)
}
budget.retainedPathCount++
budget.retainedPathBytes += pathBytes
paths.add(path)
return true
}
export type QuickOpenPathAccumulatorResult = 'continue' | 'stopped' | 'path-too-large'
export class QuickOpenSubprocessPathAccumulator {
private readonly field = new GrowingByteBuffer()
constructor(
private readonly delimiter: number,
private readonly maxPathBytes = QUICK_OPEN_LISTING_MAX_PATH_BYTES
) {
if (!Number.isInteger(delimiter) || delimiter < 0 || delimiter > 0xff) {
throw new RangeError('Quick Open path delimiter must be one byte')
}
if (!Number.isSafeInteger(maxPathBytes) || maxPathBytes < 0) {
throw new RangeError('Quick Open path limit must be a non-negative safe integer')
}
}
push(
rawChunk: Buffer | string,
onPath: (path: string) => boolean
): QuickOpenPathAccumulatorResult {
const chunk = Buffer.isBuffer(rawChunk) ? rawChunk : Buffer.from(rawChunk, 'utf8')
let cursor = 0
while (cursor < chunk.length) {
const delimiter = chunk.indexOf(this.delimiter, cursor)
const end = delimiter === -1 ? chunk.length : delimiter
const segmentBytes = end - cursor
if (this.field.byteLength + segmentBytes > this.maxPathBytes) {
this.clear()
return 'path-too-large'
}
if (delimiter !== -1 && this.field.byteLength === 0) {
if (!onPath(chunk.toString('utf8', cursor, end))) {
return 'stopped'
}
} else if (segmentBytes > 0) {
// Why: copying prevents a short residual path from retaining the whole read buffer.
this.field.append(chunk.subarray(cursor, end))
if (delimiter !== -1 && !onPath(this.take())) {
return 'stopped'
}
} else if (delimiter !== -1 && !onPath(this.take())) {
return 'stopped'
}
if (delimiter === -1) {
return 'continue'
}
cursor = delimiter + 1
}
return 'continue'
}
finish(): string | null {
return this.field.byteLength > 0 ? this.take() : null
}
clear(): void {
this.field.clear()
}
private take(): string {
return this.field.takeString()
}
}
-12
View File
@@ -1,12 +0,0 @@
import { assertJsonTextStructureWithinLimits } from './json-text-structure-limit'
export const RELAY_JSON_MAX_STRUCTURAL_TOKENS = 1_000_000
export const RELAY_JSON_MAX_NESTING_DEPTH = 128
export function parseRelayJsonText<T>(text: string): T {
assertJsonTextStructureWithinLimits(text, {
structuralTokens: RELAY_JSON_MAX_STRUCTURAL_TOKENS,
nestingDepth: RELAY_JSON_MAX_NESTING_DEPTH
})
return JSON.parse(text) as T
}
+1 -1
View File
@@ -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[] = []
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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 =
| {
+3 -3
View File
@@ -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,
+23 -48
View File
@@ -1,74 +1,49 @@
import { BoundedMap } from './memory-safety/bounded-map'
export type SecurePathHardeningCacheBounds = {
maxEntries: number
maxKeyBytes: number
maxTotalKeyBytes: number
}
type RetainedSecurePath<T> = {
value: T
keyBytes: number
}
// Key-byte-weighted LRU over the shared BoundedMap. The wrapper keeps two behaviors a generic map
// does not model: a path over EITHER the per-key or the aggregate ceiling is rejected, and the cache
// is disabled outright when maxEntries <= 0.
export class SecurePathHardeningCache<T> {
private readonly entries = new Map<string, RetainedSecurePath<T>>()
private retainedKeyBytes = 0
private readonly disabled: boolean
private readonly map: BoundedMap<string, T>
constructor(private readonly bounds: SecurePathHardeningCacheBounds) {}
constructor(bounds: SecurePathHardeningCacheBounds) {
this.disabled = bounds.maxEntries <= 0
this.map = new BoundedMap<string, T>({
maxEntries: Math.max(1, bounds.maxEntries),
maxBytes: bounds.maxTotalKeyBytes,
maxEntryBytes: Math.min(bounds.maxKeyBytes, bounds.maxTotalKeyBytes),
sizeOf: (_value, path) => Buffer.byteLength(path, 'utf8')
})
}
get(path: string): T | undefined {
const retained = this.entries.get(path)
if (!retained) {
return undefined
}
this.entries.delete(path)
this.entries.set(path, retained)
return retained.value
return this.map.get(path)
}
set(path: string, value: T): boolean {
const keyBytes = Buffer.byteLength(path, 'utf8')
this.delete(path)
if (
keyBytes > this.bounds.maxKeyBytes ||
keyBytes > this.bounds.maxTotalKeyBytes ||
this.bounds.maxEntries <= 0
) {
return false
}
while (
this.entries.size >= this.bounds.maxEntries ||
this.retainedKeyBytes + keyBytes > this.bounds.maxTotalKeyBytes
) {
const oldest = this.entries.keys().next().value
if (oldest === undefined) {
return false
}
this.delete(oldest)
}
this.entries.set(path, { value, keyBytes })
this.retainedKeyBytes += keyBytes
return true
return this.disabled ? false : this.map.set(path, value)
}
delete(path: string): void {
const retained = this.entries.get(path)
if (!retained) {
return
}
this.entries.delete(path)
this.retainedKeyBytes -= retained.keyBytes
this.map.delete(path)
}
clear(): void {
this.entries.clear()
this.retainedKeyBytes = 0
this.map.clear()
}
state(): { entries: number; keyBytes: number; paths: string[] } {
return {
entries: this.entries.size,
keyBytes: this.retainedKeyBytes,
paths: [...this.entries.keys()]
entries: this.map.size,
keyBytes: this.map.retainedBytes,
paths: this.map.keys()
}
}
}
+1 -1
View File
@@ -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'
@@ -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'
+1 -1
View File
@@ -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
@@ -1,106 +0,0 @@
import { describe, expect, it } from 'vitest'
import { getUtf8ByteLength } from './utf8-byte-limits'
import {
admitSshConnectionState,
admitSshDetectedPorts,
SSH_CONNECTION_ERROR_MAX_UTF8_BYTES,
SSH_DETECTED_PORTS_MAX_ENTRIES,
SSH_DETECTED_PORT_ADVERTISED_URL_MAX_UTF8_BYTES,
SSH_DETECTED_PORT_PROCESS_NAME_MAX_UTF8_BYTES,
SSH_RETAINED_IDENTIFIER_MAX_UTF8_BYTES
} from './ssh-retained-payload-admission'
describe('SSH retained payload admission', () => {
it('keeps ordinary connection state while stripping unknown payload fields', () => {
const admitted = admitSshConnectionState(
{
targetId: 'ssh-a',
status: 'connected',
error: null,
reconnectAttempt: 2,
connectionGeneration: 3,
supportsFolderDownload: true,
remotePlatform: 'linux',
unexpected: 'x'.repeat(1024)
},
'ssh-a'
)
expect(admitted).toEqual({
targetId: 'ssh-a',
status: 'connected',
error: null,
reconnectAttempt: 2,
connectionGeneration: 3,
supportsFolderDownload: true,
remotePlatform: 'linux'
})
})
it('caps connection errors without splitting a UTF-8 code point', () => {
const admitted = admitSshConnectionState(
{
targetId: 'ssh-a',
status: 'error',
error: `${'x'.repeat(SSH_CONNECTION_ERROR_MAX_UTF8_BYTES - 1)}🙂tail`,
reconnectAttempt: 0
},
'ssh-a'
)
expect(admitted).not.toBeNull()
expect(getUtf8ByteLength(admitted?.error ?? '')).toBeLessThanOrEqual(
SSH_CONNECTION_ERROR_MAX_UTF8_BYTES
)
expect(admitted?.error?.endsWith('\ud83d')).toBe(false)
})
it('rejects mismatched and oversized target identifiers', () => {
const state = {
targetId: 'ssh-a',
status: 'connected',
error: null,
reconnectAttempt: 0
}
expect(admitSshConnectionState(state, 'ssh-b')).toBeNull()
expect(
admitSshConnectionState(
{ ...state, targetId: 'x'.repeat(SSH_RETAINED_IDENTIFIER_MAX_UTF8_BYTES + 1) },
'x'.repeat(SSH_RETAINED_IDENTIFIER_MAX_UTF8_BYTES + 1)
)
).toBeNull()
})
it('caps port rows and their retained strings', () => {
const rows = Array.from({ length: SSH_DETECTED_PORTS_MAX_ENTRIES + 10 }, (_, index) => ({
port: 1000 + index,
host: '127.0.0.1',
pid: index + 1,
processName: '🙂'.repeat(SSH_DETECTED_PORT_PROCESS_NAME_MAX_UTF8_BYTES),
advertisedUrl: `https://example.test/${'x'.repeat(
SSH_DETECTED_PORT_ADVERTISED_URL_MAX_UTF8_BYTES
)}`,
unexpected: 'retained only without admission'
}))
const admitted = admitSshDetectedPorts(rows)
expect(admitted).toHaveLength(SSH_DETECTED_PORTS_MAX_ENTRIES)
expect(getUtf8ByteLength(admitted[0].processName ?? '')).toBeLessThanOrEqual(
SSH_DETECTED_PORT_PROCESS_NAME_MAX_UTF8_BYTES
)
expect(admitted[0].advertisedUrl).toBeUndefined()
expect(admitted[0]).not.toHaveProperty('unexpected')
})
it('drops malformed rows instead of retaining their payloads', () => {
expect(
admitSshDetectedPorts([
{ port: 0, host: '127.0.0.1' },
{ port: 3000, host: '' },
{ port: 3001, host: '127.0.0.1', processName: 'node' }
])
).toEqual([{ port: 3001, host: '127.0.0.1', processName: 'node' }])
})
})
@@ -1,138 +0,0 @@
import type { EnrichedDetectedPort, SshConnectionState, SshConnectionStatus } from './ssh-types'
import { clampUtf8TextPrefix, measureUtf8ByteLength } from './utf8-byte-limits'
export const SSH_RETAINED_IDENTIFIER_MAX_UTF8_BYTES = 1024
export const SSH_CONNECTION_ERROR_MAX_UTF8_BYTES = 16 * 1024
export const SSH_CREDENTIAL_DETAIL_MAX_UTF8_BYTES = 16 * 1024
export const SSH_DETECTED_PORTS_MAX_ENTRIES = 50
export const SSH_DETECTED_PORT_HOST_MAX_UTF8_BYTES = 1024
export const SSH_DETECTED_PORT_PROCESS_NAME_MAX_UTF8_BYTES = 4 * 1024
export const SSH_DETECTED_PORT_ADVERTISED_URL_MAX_UTF8_BYTES = 2048
const CONNECTION_STATUSES = new Set<SshConnectionStatus>([
'disconnected',
'connecting',
'auth-failed',
'deploying-relay',
'connected',
'reconnecting',
'reconnection-failed',
'error'
])
export function isSshRetainedIdentifier(value: unknown): value is string {
return (
typeof value === 'string' &&
value.length > 0 &&
!measureUtf8ByteLength(value, {
stopAfterBytes: SSH_RETAINED_IDENTIFIER_MAX_UTF8_BYTES
}).exceededLimit
)
}
export function admitSshConnectionState(
value: unknown,
expectedTargetId: string
): SshConnectionState | null {
if (!value || typeof value !== 'object' || !isSshRetainedIdentifier(expectedTargetId)) {
return null
}
const input = value as Record<string, unknown>
if (
(input.targetId !== undefined &&
(!isSshRetainedIdentifier(input.targetId) || input.targetId !== expectedTargetId)) ||
typeof input.status !== 'string' ||
!CONNECTION_STATUSES.has(input.status as SshConnectionStatus) ||
!isNonNegativeSafeInteger(input.reconnectAttempt) ||
(input.error !== null && typeof input.error !== 'string')
) {
return null
}
const error = clampSshConnectionError(input.error)
return {
targetId: expectedTargetId,
status: input.status as SshConnectionStatus,
error,
reconnectAttempt: input.reconnectAttempt,
...(isNonNegativeSafeInteger(input.connectionGeneration)
? { connectionGeneration: input.connectionGeneration }
: {}),
...(typeof input.supportsFolderDownload === 'boolean'
? { supportsFolderDownload: input.supportsFolderDownload }
: {}),
...(input.remotePlatform === 'linux' ||
input.remotePlatform === 'darwin' ||
input.remotePlatform === 'win32'
? { remotePlatform: input.remotePlatform }
: {})
}
}
export function clampSshConnectionError(error: string | null): string | null {
return typeof error === 'string'
? clampUtf8TextPrefix(error, SSH_CONNECTION_ERROR_MAX_UTF8_BYTES)
: null
}
export function admitSshDetectedPorts(value: unknown): EnrichedDetectedPort[] {
if (!Array.isArray(value)) {
return []
}
const retained: EnrichedDetectedPort[] = []
const scanLimit = Math.min(value.length, SSH_DETECTED_PORTS_MAX_ENTRIES)
for (let index = 0; index < scanLimit; index += 1) {
const port = admitDetectedPort(value[index])
if (port) {
retained.push(port)
}
}
return retained
}
function admitDetectedPort(value: unknown): EnrichedDetectedPort | null {
if (!value || typeof value !== 'object') {
return null
}
const input = value as Record<string, unknown>
if (
!Number.isSafeInteger(input.port) ||
(input.port as number) < 1 ||
(input.port as number) > 65_535 ||
!isStringWithinLimit(input.host, SSH_DETECTED_PORT_HOST_MAX_UTF8_BYTES)
) {
return null
}
const processName =
typeof input.processName === 'string'
? clampUtf8TextPrefix(input.processName, SSH_DETECTED_PORT_PROCESS_NAME_MAX_UTF8_BYTES)
: undefined
const advertisedUrl = isStringWithinLimit(
input.advertisedUrl,
SSH_DETECTED_PORT_ADVERTISED_URL_MAX_UTF8_BYTES
)
? input.advertisedUrl
: undefined
return {
port: input.port as number,
host: input.host,
...(isNonNegativeSafeInteger(input.pid) && input.pid > 0 ? { pid: input.pid } : {}),
...(processName ? { processName } : {}),
...(advertisedUrl ? { advertisedUrl } : {}),
...(input.advertisedProtocol === 'http' || input.advertisedProtocol === 'https'
? { advertisedProtocol: input.advertisedProtocol }
: {})
}
}
function isNonNegativeSafeInteger(value: unknown): value is number {
return Number.isSafeInteger(value) && (value as number) >= 0
}
function isStringWithinLimit(value: unknown, maxBytes: number): value is string {
return (
typeof value === 'string' &&
value.length > 0 &&
!measureUtf8ByteLength(value, { stopAfterBytes: maxBytes }).exceededLimit
)
}
-34
View File
@@ -1,34 +0,0 @@
export const MAX_TERMINAL_COLS = 500
export const MAX_TERMINAL_ROWS = 500
export function terminalSizeAdmissionError(
cols: unknown,
rows: unknown,
field: string,
options: { allowMissing?: boolean } = {}
): string | null {
for (const [name, value, max] of [
['cols', cols, MAX_TERMINAL_COLS],
['rows', rows, MAX_TERMINAL_ROWS]
] as const) {
if (options.allowMissing && value === undefined) {
continue
}
if (!Number.isSafeInteger(value) || (value as number) < 1 || (value as number) > max) {
return `${field}.${name} must be an integer from 1 through ${max}`
}
}
return null
}
export function isValidTerminalSize(cols: unknown, rows: unknown): boolean {
return terminalSizeAdmissionError(cols, rows, 'terminal size') === null
}
export function normalizeTerminalSize(
cols: unknown,
rows: unknown,
fallback: { cols: number; rows: number } = { cols: 80, rows: 24 }
): { cols: number; rows: number } {
return isValidTerminalSize(cols, rows) ? { cols: cols as number, rows: rows as number } : fallback
}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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'
@@ -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
@@ -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<Repo, 'id' | 'connectionId' | 'executionHostId'>