mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 00:03:47 +00:00
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:
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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,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. */
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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).
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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
|
||||
}))
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
-1
@@ -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
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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,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}')
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,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,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'])
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,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,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
|
||||
|
||||
@@ -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,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 =
|
||||
| {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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,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
|
||||
)
|
||||
}
|
||||
@@ -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,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
|
||||
|
||||
@@ -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'>
|
||||
|
||||
Reference in New Issue
Block a user