@gajae-code/coding-agent 0.18.7 → 0.18.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +377 -1
- package/dist/types/async/endpoint-id.d.ts +16 -0
- package/dist/types/async/job-manager.d.ts +1 -1
- package/dist/types/async/support.d.ts +0 -14
- package/dist/types/capability/mcp.d.ts +2 -0
- package/dist/types/capability/skill.d.ts +2 -0
- package/dist/types/capability/types.d.ts +2 -0
- package/dist/types/cli/public-command-errors.d.ts +3 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/commands/mcp-serve.d.ts +4 -0
- package/dist/types/config/autorouting-tier-map.d.ts +348 -9
- package/dist/types/config/file-lock.d.ts +1 -0
- package/dist/types/config/model-bindings-applier.d.ts +2 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -3
- package/dist/types/config/model-registry.d.ts +4 -9
- package/dist/types/config/skill-settings-defaults.d.ts +2 -0
- package/dist/types/cursor-delete-path.d.ts +1 -0
- package/dist/types/cursor-mcp-name.d.ts +8 -0
- package/dist/types/customization/inventory.d.ts +2 -0
- package/dist/types/debug/report-bundle-parent.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
- package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
- package/dist/types/discovery/helpers.d.ts +6 -1
- package/dist/types/discovery/mcp-json.d.ts +1 -0
- package/dist/types/eval/py/kernel.d.ts +18 -0
- package/dist/types/export/html/css-color.d.ts +2 -0
- package/dist/types/export/html/html-export-destination.d.ts +2 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +5 -0
- package/dist/types/extensibility/extensions/types.d.ts +7 -0
- package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
- package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
- package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
- package/dist/types/extensibility/skill-management.d.ts +4 -1
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -3
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
- package/dist/types/gjc-runtime/sidecar-bootstrap-env.d.ts +6 -0
- package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
- package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +8 -5
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
- package/dist/types/internal-urls/skill-path.d.ts +1 -0
- package/dist/types/lsp/config.d.ts +8 -5
- package/dist/types/lsp/workspace-path.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +7 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -0
- package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
- package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
- package/dist/types/modes/controllers/input-controller.d.ts +9 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
- package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
- package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
- package/dist/types/progress/collect-project-progress.d.ts +38 -0
- package/dist/types/progress/progress-contract.d.ts +154 -0
- package/dist/types/progress/progress-snapshot.d.ts +11 -0
- package/dist/types/progress/project-progress.d.ts +141 -0
- package/dist/types/progress/render-progress.d.ts +28 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -1
- package/dist/types/runtime-mcp/mcp-redirect.d.ts +9 -0
- package/dist/types/runtime-mcp/oauth-public-url.d.ts +3 -0
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
- package/dist/types/runtime-mcp/project-host-values.d.ts +27 -0
- package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
- package/dist/types/runtime-mcp/types.d.ts +6 -0
- package/dist/types/sdk/acp/adapter.d.ts +1 -1
- package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
- package/dist/types/sdk/broker/broker.d.ts +24 -2
- package/dist/types/sdk/broker/doctor-restart.d.ts +5 -0
- package/dist/types/sdk/broker/ensure.d.ts +14 -1
- package/dist/types/sdk/broker/session-index.d.ts +12 -6
- package/dist/types/sdk/bus/index.d.ts +12 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
- package/dist/types/sdk/client/client.d.ts +2 -0
- package/dist/types/sdk/host/query/handlers.d.ts +3 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/session-list.d.ts +1 -1
- package/dist/types/sdk/session.d.ts +8 -0
- package/dist/types/session/agent-session.d.ts +38 -9
- package/dist/types/session/artifacts.d.ts +2 -2
- package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +26 -1
- package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
- package/dist/types/session/internal/task-artifact-owner-access.d.ts +2 -0
- package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
- package/dist/types/session/session-manager.d.ts +31 -5
- package/dist/types/session/session-retirement.d.ts +31 -7
- package/dist/types/session/session-storage.d.ts +69 -2
- package/dist/types/session/task-artifact-owner-codec.d.ts +2 -0
- package/dist/types/setup/credential-auto-import.d.ts +1 -1
- package/dist/types/setup/credential-import.d.ts +6 -4
- package/dist/types/skill-state/active-state.d.ts +38 -0
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +3 -0
- package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/isolation-dir.d.ts +26 -0
- package/dist/types/tools/ast-edit-write-path.d.ts +13 -0
- package/dist/types/tools/bash-skill-urls.d.ts +1 -1
- package/dist/types/tools/browser/cdp-url.d.ts +2 -0
- package/dist/types/tools/github-side-effect.d.ts +22 -0
- package/dist/types/tools/index.d.ts +3 -0
- package/dist/types/tools/monitor-mutation.d.ts +9 -0
- package/dist/types/tools/python.d.ts +9 -1
- package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
- package/dist/types/tools/restricted-cwd.d.ts +1 -0
- package/dist/types/tools/todo-write.d.ts +6 -0
- package/dist/types/utils/tools-checksum.d.ts +7 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +21 -0
- package/scripts/verify-managed-empty-stop.ts +28 -0
- package/src/async/endpoint-id.ts +26 -0
- package/src/async/job-manager.ts +247 -132
- package/src/async/support.ts +0 -25
- package/src/capability/index.ts +15 -0
- package/src/capability/mcp.ts +2 -0
- package/src/capability/skill.ts +2 -0
- package/src/capability/types.ts +2 -0
- package/src/cli/auth-broker-cli.ts +1 -1
- package/src/cli/customize-doctor.ts +15 -2
- package/src/cli/doctor/repairs.ts +4 -1
- package/src/cli/doctor-supervisor.ts +3 -0
- package/src/cli/public-command-errors.ts +21 -1
- package/src/cli/public-command-registry.ts +28 -6
- package/src/cli/setup-cli.ts +4 -4
- package/src/cli/skills-cli.ts +5 -0
- package/src/commands/deep-interview.ts +47 -8
- package/src/commands/mcp-serve.ts +7 -0
- package/src/commands/sdk.ts +455 -397
- package/src/config/autorouting-tier-map.ts +117 -7
- package/src/config/file-lock.ts +316 -79
- package/src/config/mcp-schema.json +4 -0
- package/src/config/model-bindings-applier.ts +20 -0
- package/src/config/model-profile-activation.ts +240 -46
- package/src/config/model-registry.ts +47 -153
- package/src/config/skill-settings-defaults.ts +7 -0
- package/src/coordinator-mcp/policy.ts +3 -2
- package/src/coordinator-mcp/server.ts +2 -0
- package/src/cursor-delete-path.ts +10 -0
- package/src/cursor-mcp-name.ts +15 -0
- package/src/cursor.ts +13 -2
- package/src/customization/inventory.ts +4 -13
- package/src/dap/config.ts +2 -2
- package/src/debug/report-bundle-parent.ts +22 -0
- package/src/debug/report-bundle.ts +3 -0
- package/src/debug/system-info.ts +13 -1
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +10 -6
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
- package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
- package/src/defaults/gjc-defaults.test.ts +18 -1
- package/src/defaults/gjc-defaults.ts +3 -0
- package/src/defaults/gjc-skills.generated.ts +58 -36
- package/src/discovery/agents.ts +6 -1
- package/src/discovery/builtin.ts +13 -1
- package/src/discovery/helpers.ts +171 -29
- package/src/discovery/mcp-json.ts +26 -12
- package/src/discovery/ssh.ts +1 -1
- package/src/discovery/substitute-plugin-root.ts +3 -1
- package/src/edit/file-read-cache.ts +1 -1
- package/src/eval/py/executor.ts +796 -154
- package/src/eval/py/kernel.ts +79 -13
- package/src/eval/py/tool-bridge.ts +4 -0
- package/src/export/html/css-color.ts +9 -0
- package/src/export/html/html-export-destination.ts +16 -0
- package/src/export/html/index.ts +7 -10
- package/src/export/html/template.generated.ts +1 -1
- package/src/extensibility/extensions/function-hooks-internal.ts +27 -0
- package/src/extensibility/extensions/runner.ts +15 -1
- package/src/extensibility/extensions/types.ts +7 -0
- package/src/extensibility/gjc-plugins/mcp-policy.ts +2 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +85 -8
- package/src/extensibility/plugins/manager.ts +3 -2
- package/src/extensibility/plugins/marketplace/cache.ts +15 -0
- package/src/extensibility/plugins/marketplace/manager.ts +11 -3
- package/src/extensibility/plugins/plugin-link-path.ts +12 -0
- package/src/extensibility/runtime-skill-discovery.ts +20 -2
- package/src/extensibility/skill-management.ts +9 -2
- package/src/extensibility/skills.ts +6 -1
- package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
- package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
- package/src/gjc-runtime/deep-interview-stage.ts +98 -21
- package/src/gjc-runtime/gc-runtime.ts +206 -9
- package/src/gjc-runtime/launch-worktree.ts +70 -9
- package/src/gjc-runtime/psmux-detect.ts +28 -4
- package/src/gjc-runtime/ralplan-runtime.ts +447 -52
- package/src/gjc-runtime/session-state-sidecar.ts +3 -2
- package/src/gjc-runtime/sidecar-bootstrap-env.ts +19 -0
- package/src/gjc-runtime/state-argv.ts +2 -0
- package/src/gjc-runtime/state-migrations.ts +91 -29
- package/src/gjc-runtime/state-runtime.ts +3716 -135
- package/src/gjc-runtime/state-writer.ts +105 -16
- package/src/gjc-runtime/tmux-common.ts +8 -5
- package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
- package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
- package/src/gjc-runtime/workflow-manifest.ts +54 -21
- package/src/harness-control-plane/operate.ts +12 -4
- package/src/harness-control-plane/owner.ts +5 -1
- package/src/hashline/parser.ts +48 -9
- package/src/hashline/recovery.ts +13 -4
- package/src/internal-urls/docs-index.generated.ts +20 -19
- package/src/internal-urls/skill-path.ts +19 -0
- package/src/internal-urls/skill-protocol.ts +2 -0
- package/src/lsp/config.ts +12 -63
- package/src/lsp/edits.ts +6 -0
- package/src/lsp/workspace-path.ts +51 -0
- package/src/main.ts +195 -31
- package/src/memories/index.ts +88 -7
- package/src/modes/acp/acp-agent.ts +88 -4
- package/src/modes/components/custom-editor.ts +14 -0
- package/src/modes/components/customization/customization-dashboard.ts +12 -0
- package/src/modes/components/extensions/extension-dashboard.ts +2 -2
- package/src/modes/components/extensions/state-manager.ts +15 -5
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/hook-selector.ts +1 -1
- package/src/modes/components/model-selector.ts +1 -4
- package/src/modes/components/provider-onboarding-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +4 -0
- package/src/modes/controllers/event-controller.ts +11 -4
- package/src/modes/controllers/input-controller.ts +76 -31
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -16
- package/src/modes/controllers/selector-controller.ts +24 -15
- package/src/modes/controllers/todo-command-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +5 -1
- package/src/modes/runtime-init.ts +4 -0
- package/src/modes/utils/abort-message.ts +3 -1
- package/src/modes/utils/interrupt-ownership.ts +12 -0
- package/src/modes/utils/keybinding-matchers.ts +4 -0
- package/src/modes/utils/macos-option-forwarding.ts +80 -0
- package/src/progress/collect-project-progress.ts +153 -0
- package/src/progress/progress-contract.ts +172 -0
- package/src/progress/progress-snapshot.ts +140 -0
- package/src/progress/project-progress.ts +446 -0
- package/src/progress/render-progress.ts +209 -0
- package/src/prompts/system/master-mode.md +1 -0
- package/src/prompts/tools/skill-discovery.md +1 -1
- package/src/runtime-mcp/client.ts +4 -2
- package/src/runtime-mcp/config-writer.ts +10 -6
- package/src/runtime-mcp/config.ts +1 -0
- package/src/runtime-mcp/json-rpc.ts +2 -1
- package/src/runtime-mcp/manager.ts +29 -11
- package/src/runtime-mcp/mcp-redirect.ts +73 -0
- package/src/runtime-mcp/oauth-flow.ts +14 -2
- package/src/runtime-mcp/oauth-public-url.ts +14 -0
- package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
- package/src/runtime-mcp/pool.ts +6 -0
- package/src/runtime-mcp/project-host-values.ts +46 -0
- package/src/runtime-mcp/smithery-registry.ts +22 -3
- package/src/runtime-mcp/transports/http.ts +4 -3
- package/src/runtime-mcp/types.ts +6 -0
- package/src/sdk/acp/adapter.ts +95 -8
- package/src/sdk/broker/broker-exit.ts +52 -23
- package/src/sdk/broker/broker.ts +81 -6
- package/src/sdk/broker/doctor-restart.ts +4 -0
- package/src/sdk/broker/ensure.ts +31 -2
- package/src/sdk/broker/lifecycle.ts +711 -66
- package/src/sdk/broker/session-index.ts +191 -81
- package/src/sdk/bus/chat-daemon-cli.ts +38 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +238 -43
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
- package/src/sdk/cli/session-cli.ts +17 -3
- package/src/sdk/client/client.ts +28 -8
- package/src/sdk/host/query/handlers.ts +9 -1
- package/src/sdk/host/session-runtime.test.ts +2705 -303
- package/src/sdk/host/session-runtime.ts +490 -143
- package/src/sdk/host/surface-policy.ts +1 -0
- package/src/sdk/index.ts +8 -0
- package/src/sdk/lifecycle-session.ts +3 -1
- package/src/sdk/prompt-deadline-manager.ts +15 -0
- package/src/sdk/protocol/operation-inventory.generated.json +146 -0
- package/src/sdk/protocol/operation-registry.ts +9 -2
- package/src/sdk/router/session-router.ts +82 -18
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +69 -45
- package/src/session/agent-session.ts +679 -238
- package/src/session/artifacts.ts +176 -71
- package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
- package/src/session/internal/managed-session-scope.ts +3779 -462
- package/src/session/internal/managed-session-storage.ts +501 -78
- package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
- package/src/session/internal/task-artifact-owner-access.ts +19 -3
- package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
- package/src/session/session-manager.ts +951 -133
- package/src/session/session-retirement.ts +573 -14
- package/src/session/session-storage.ts +447 -15
- package/src/session/task-artifact-owner-codec.ts +71 -8
- package/src/session/task-artifact-owner-retirement.ts +21 -19
- package/src/setup/credential-auto-import.ts +12 -3
- package/src/setup/credential-import.ts +176 -5
- package/src/setup/paseo/announce.ts +6 -1
- package/src/skill-state/active-state.ts +145 -2
- package/src/skill-state/workflow-mutation-guard.ts +102 -10
- package/src/slash-commands/builtin-registry.ts +39 -9
- package/src/slash-commands/helpers/mcp.ts +8 -4
- package/src/slash-commands/helpers/progress-report.ts +23 -0
- package/src/stt/setup.ts +3 -5
- package/src/task/commands.ts +3 -1
- package/src/task/executor.ts +4 -0
- package/src/task/index.ts +6 -0
- package/src/task/isolation-dir.ts +143 -0
- package/src/task/worktree.ts +16 -7
- package/src/tools/ask.ts +205 -6
- package/src/tools/ast-edit-write-path.ts +68 -0
- package/src/tools/ast-edit.ts +10 -1
- package/src/tools/bash-allowed-prefixes.ts +15 -0
- package/src/tools/bash-skill-urls.ts +56 -3
- package/src/tools/bash.ts +48 -79
- package/src/tools/browser/cdp-url.ts +18 -0
- package/src/tools/browser.ts +8 -1
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/eval.ts +56 -36
- package/src/tools/gh.ts +10 -0
- package/src/tools/github-side-effect.ts +112 -0
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/index.ts +3 -0
- package/src/tools/monitor-mutation.ts +27 -0
- package/src/tools/monitor.ts +7 -0
- package/src/tools/python.ts +188 -66
- package/src/tools/recipe/index.ts +9 -0
- package/src/tools/recipe/recipe-guard.ts +19 -0
- package/src/tools/restricted-cwd.ts +10 -0
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +28 -1
- package/src/tools/todo-write.ts +10 -1
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/tools/write.ts +1 -0
- package/src/utils/external-editor.ts +3 -2
- package/src/utils/tools-checksum.ts +34 -0
- package/src/utils/tools-manager.ts +24 -6
- package/src/web/insane/url-guard.ts +2 -1
- package/src/web/search/providers/codex.ts +3 -2
|
@@ -8,7 +8,9 @@ import type {
|
|
|
8
8
|
NativeExactFileIdentity,
|
|
9
9
|
NativeExactUnlinkResult,
|
|
10
10
|
NativeOwnerOnlySecurityResult,
|
|
11
|
+
RecoveryFsFile,
|
|
11
12
|
RecoveryFsIdentity,
|
|
13
|
+
RecoveryFsResult,
|
|
12
14
|
RecoveryFsRoot,
|
|
13
15
|
} from "@gajae-code/natives";
|
|
14
16
|
import { isEnoent, logger } from "@gajae-code/utils";
|
|
@@ -49,6 +51,7 @@ export const MANAGED_ARTIFACT_MAX_DEPTH = 32;
|
|
|
49
51
|
export const MANAGED_ARTIFACT_MAX_FILES = 50_000;
|
|
50
52
|
export const MANAGED_ARTIFACT_MAX_FILE_BYTES = 128 * 1024 * 1024;
|
|
51
53
|
export const MANAGED_ARTIFACT_MAX_TOTAL_BYTES = 512 * 1024 * 1024;
|
|
54
|
+
export const MANAGED_SESSION_READ_RANGE_MAX_BYTES = 64 * 1024 * 1024;
|
|
52
55
|
const REPLACEMENT_CLEANUP_RECEIPT_MAX_BYTES = 64 * 1024;
|
|
53
56
|
const REPLACEMENT_CLEANUP_RECEIPT_SCAN_LIMIT = MANAGED_ARTIFACT_MAX_FILES;
|
|
54
57
|
export const MANAGED_ARTIFACT_COPY_BATCH_SIZE = 256;
|
|
@@ -390,6 +393,15 @@ const replacementPublisherOwnerId = randomUUID();
|
|
|
390
393
|
const activeReplacementAttempts = new Set<string>();
|
|
391
394
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
392
395
|
|
|
396
|
+
/** Exact quarantine names used when native exact-unlink leaves a scrubbed lock inode. */
|
|
397
|
+
export function isManagedLockQuarantineName(name: string): boolean {
|
|
398
|
+
const prefix = ".gjc-lock-";
|
|
399
|
+
const suffix = ".stale";
|
|
400
|
+
return (
|
|
401
|
+
name.startsWith(prefix) && name.endsWith(suffix) && UUID_PATTERN.test(name.slice(prefix.length, -suffix.length))
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
393
405
|
function replacementReceiptPublisher(bytes: Uint8Array): ReplacementReceiptPublisher | undefined {
|
|
394
406
|
let record: Record<string, unknown> | undefined;
|
|
395
407
|
try {
|
|
@@ -874,43 +886,24 @@ async function hashReaperFile(pathname: string, expected: fs.BigIntStats): Promi
|
|
|
874
886
|
}
|
|
875
887
|
}
|
|
876
888
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
sha256: string,
|
|
881
|
-
allowHardLink: boolean,
|
|
882
|
-
): NativeExactUnlinkResult {
|
|
883
|
-
const parent = fs.lstatSync(path.dirname(pathname), { bigint: true });
|
|
884
|
-
if (!parent.isDirectory() || parent.isSymbolicLink()) return { ok: false, code: "parent_mismatch" };
|
|
885
|
-
const identity: NativeExactFileIdentity = {
|
|
886
|
-
dev: stat.dev,
|
|
887
|
-
ino: stat.ino,
|
|
888
|
-
nlink: stat.nlink,
|
|
889
|
-
parentDev: parent.dev,
|
|
890
|
-
parentIno: parent.ino,
|
|
891
|
-
size: stat.size,
|
|
892
|
-
mtimeNs: stat.mtimeNs,
|
|
893
|
-
sha256,
|
|
894
|
-
quarantineName: remnantReapQuarantineName(pathname),
|
|
895
|
-
...(allowHardLink ? { allowHardLink: true, requireHardLink: true } : {}),
|
|
896
|
-
};
|
|
897
|
-
return nativeSessionStorage().exactUnlinkDirect(pathname, identity);
|
|
889
|
+
interface ReaperParentIdentity {
|
|
890
|
+
readonly dev: bigint;
|
|
891
|
+
readonly ino: bigint;
|
|
898
892
|
}
|
|
899
893
|
|
|
900
|
-
|
|
894
|
+
function exactReaperUnlinkSync(
|
|
901
895
|
pathname: string,
|
|
902
896
|
stat: fs.BigIntStats,
|
|
903
897
|
sha256: string,
|
|
904
898
|
allowHardLink: boolean,
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
if (!parent.isDirectory() || parent.isSymbolicLink()) return { ok: false, code: "parent_mismatch" };
|
|
899
|
+
parentIdentity: ReaperParentIdentity,
|
|
900
|
+
): NativeExactUnlinkResult {
|
|
908
901
|
const identity: NativeExactFileIdentity = {
|
|
909
902
|
dev: stat.dev,
|
|
910
903
|
ino: stat.ino,
|
|
911
904
|
nlink: stat.nlink,
|
|
912
|
-
parentDev:
|
|
913
|
-
parentIno:
|
|
905
|
+
parentDev: parentIdentity.dev,
|
|
906
|
+
parentIno: parentIdentity.ino,
|
|
914
907
|
size: stat.size,
|
|
915
908
|
mtimeNs: stat.mtimeNs,
|
|
916
909
|
sha256,
|
|
@@ -957,10 +950,12 @@ export function reapScrubbedProtocolRemnantsSync(
|
|
|
957
950
|
const cutoff = Date.now() - minAgeMs;
|
|
958
951
|
let reaped = 0;
|
|
959
952
|
let failures = 0;
|
|
953
|
+
// Immutable expected identity only; native reopens and revalidates the parent per unlink.
|
|
954
|
+
let parentIdentity: ReaperParentIdentity | undefined;
|
|
960
955
|
for (const name of names) {
|
|
961
956
|
const terminalRemnant = SCRUBBED_REMNANT_PREFIXES.some(prefix => name.startsWith(prefix));
|
|
962
|
-
const replacementCandidate = REPLACEMENT_STAGING_NAME.test(name);
|
|
963
|
-
const quarantineCandidate = REAPER_QUARANTINE_NAME.test(name);
|
|
957
|
+
const replacementCandidate = !terminalRemnant && REPLACEMENT_STAGING_NAME.test(name);
|
|
958
|
+
const quarantineCandidate = !terminalRemnant && REAPER_QUARANTINE_NAME.test(name);
|
|
964
959
|
if (!terminalRemnant && !replacementCandidate && !quarantineCandidate) continue;
|
|
965
960
|
const alias = terminalRemnant
|
|
966
961
|
? undefined
|
|
@@ -980,9 +975,29 @@ export function reapScrubbedProtocolRemnantsSync(
|
|
|
980
975
|
if (named.mtimeMs > cutoff) continue;
|
|
981
976
|
const sha256 = terminalRemnant ? EMPTY_FILE_SHA256 : hashReaperFileSync(pathname, named);
|
|
982
977
|
if (!sha256) continue;
|
|
983
|
-
|
|
978
|
+
if (!parentIdentity) {
|
|
979
|
+
const parent = fs.lstatSync(path.dirname(pathname), { bigint: true });
|
|
980
|
+
if (!parent.isDirectory() || parent.isSymbolicLink()) {
|
|
981
|
+
failures += 1;
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
parentIdentity = { dev: parent.dev, ino: parent.ino };
|
|
985
|
+
}
|
|
986
|
+
const removed = exactReaperUnlinkSync(pathname, named, sha256, !terminalRemnant, parentIdentity);
|
|
984
987
|
if (removed.ok) reaped += 1;
|
|
985
|
-
else
|
|
988
|
+
else {
|
|
989
|
+
let parentKindMismatch = false;
|
|
990
|
+
if (removed.code === "reparse_point") {
|
|
991
|
+
try {
|
|
992
|
+
const parent = fs.lstatSync(path.dirname(pathname), { bigint: true });
|
|
993
|
+
parentKindMismatch = !parent.isDirectory() || parent.isSymbolicLink();
|
|
994
|
+
} catch (error) {
|
|
995
|
+
// Preserve lookup-error accounting without changing or retrying the native result.
|
|
996
|
+
if (!isEnoent(error)) failures += 1;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
if (parentKindMismatch || !isBenignReaperRace(removed)) failures += 1;
|
|
1000
|
+
}
|
|
986
1001
|
} catch (error) {
|
|
987
1002
|
if (!isEnoent(error)) failures += 1;
|
|
988
1003
|
}
|
|
@@ -1016,10 +1031,11 @@ export async function reapScrubbedProtocolRemnants(
|
|
|
1016
1031
|
let reaped = 0;
|
|
1017
1032
|
let failures = 0;
|
|
1018
1033
|
let scanned = 0;
|
|
1034
|
+
let parentIdentity: ReaperParentIdentity | undefined;
|
|
1019
1035
|
for (const name of names) {
|
|
1020
1036
|
const terminalRemnant = SCRUBBED_REMNANT_PREFIXES.some(prefix => name.startsWith(prefix));
|
|
1021
|
-
const replacementCandidate = REPLACEMENT_STAGING_NAME.test(name);
|
|
1022
|
-
const quarantineCandidate = REAPER_QUARANTINE_NAME.test(name);
|
|
1037
|
+
const replacementCandidate = !terminalRemnant && REPLACEMENT_STAGING_NAME.test(name);
|
|
1038
|
+
const quarantineCandidate = !terminalRemnant && REAPER_QUARANTINE_NAME.test(name);
|
|
1023
1039
|
if (!terminalRemnant && !replacementCandidate && !quarantineCandidate) continue;
|
|
1024
1040
|
if (++scanned % SCRUBBED_REMNANT_REAP_BATCH_SIZE === 0) await Bun.sleep(0);
|
|
1025
1041
|
const alias = terminalRemnant
|
|
@@ -1038,9 +1054,29 @@ export async function reapScrubbedProtocolRemnants(
|
|
|
1038
1054
|
if (named.mtimeMs > cutoff) continue;
|
|
1039
1055
|
const sha256 = terminalRemnant ? EMPTY_FILE_SHA256 : await hashReaperFile(pathname, named);
|
|
1040
1056
|
if (!sha256) continue;
|
|
1041
|
-
|
|
1057
|
+
if (!parentIdentity) {
|
|
1058
|
+
const parent = await fsp.lstat(path.dirname(pathname), { bigint: true });
|
|
1059
|
+
if (!parent.isDirectory() || parent.isSymbolicLink()) {
|
|
1060
|
+
failures += 1;
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
parentIdentity = { dev: parent.dev, ino: parent.ino };
|
|
1064
|
+
}
|
|
1065
|
+
const removed = await exactReaperUnlinkSync(pathname, named, sha256, !terminalRemnant, parentIdentity);
|
|
1042
1066
|
if (removed.ok) reaped += 1;
|
|
1043
|
-
else
|
|
1067
|
+
else {
|
|
1068
|
+
let parentKindMismatch = false;
|
|
1069
|
+
if (removed.code === "reparse_point") {
|
|
1070
|
+
try {
|
|
1071
|
+
const parent = await fsp.lstat(path.dirname(pathname), { bigint: true });
|
|
1072
|
+
parentKindMismatch = !parent.isDirectory() || parent.isSymbolicLink();
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
// Preserve lookup-error accounting without changing or retrying the native result.
|
|
1075
|
+
if (!isEnoent(error)) failures += 1;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
if (parentKindMismatch || !isBenignReaperRace(removed)) failures += 1;
|
|
1079
|
+
}
|
|
1044
1080
|
} catch (error) {
|
|
1045
1081
|
if (!isEnoent(error)) failures += 1;
|
|
1046
1082
|
}
|
|
@@ -1163,7 +1199,7 @@ type RetainedManagedReplacer = {
|
|
|
1163
1199
|
expectedSha256: string,
|
|
1164
1200
|
): { ok: boolean; code?: string };
|
|
1165
1201
|
};
|
|
1166
|
-
type LockRecord = {
|
|
1202
|
+
export type LockRecord = {
|
|
1167
1203
|
attemptId: string;
|
|
1168
1204
|
pid: number;
|
|
1169
1205
|
bootId?: string;
|
|
@@ -1244,8 +1280,8 @@ export function retainManagedDirectoryAuthority(
|
|
|
1244
1280
|
expected?: { dev: bigint; ino: bigint },
|
|
1245
1281
|
): RecoveryFsRoot | undefined {
|
|
1246
1282
|
assertManagedDirectoryRoot(root);
|
|
1247
|
-
|
|
1248
|
-
const
|
|
1283
|
+
const resolved = canonicalizeManagedPathWithinRoot(root.canonicalPath, directory);
|
|
1284
|
+
const relative = managedRelativePath(root, resolved).join("/");
|
|
1249
1285
|
if (process.platform !== "linux") return undefined;
|
|
1250
1286
|
const named = fs.lstatSync(resolved, { bigint: true });
|
|
1251
1287
|
if (!named.isDirectory() || named.isSymbolicLink()) throw new Error("Managed directory authority is unavailable");
|
|
@@ -1261,7 +1297,6 @@ export function retainManagedDirectoryAuthority(
|
|
|
1261
1297
|
retainedRoot.identity.ino !== root.ino.toString()
|
|
1262
1298
|
)
|
|
1263
1299
|
throw new Error("Managed root authority changed");
|
|
1264
|
-
const relative = path.relative(root.canonicalPath, resolved).split(path.sep).join("/");
|
|
1265
1300
|
const rootRecovery = rootAuthority.recoveryReaperMetrics();
|
|
1266
1301
|
const rootRecoveryUnhealthy = !rootRecovery.ok || Boolean(rootRecovery.code);
|
|
1267
1302
|
if (rootRecoveryUnhealthy)
|
|
@@ -1330,14 +1365,79 @@ function ensureManagedRoot(root: ManagedDirectoryRoot): void {
|
|
|
1330
1365
|
assertManagedDirectoryRoot(root);
|
|
1331
1366
|
}
|
|
1332
1367
|
|
|
1333
|
-
|
|
1334
|
-
|
|
1368
|
+
/** Resolve aliases above a managed root without following descendant symlinks. */
|
|
1369
|
+
function canonicalizeManagedPathWithinRoot(rootPath: string, pathname: string): string {
|
|
1370
|
+
const canonicalRootPath = path.resolve(rootPath);
|
|
1371
|
+
const namedRoot = fs.lstatSync(canonicalRootPath);
|
|
1372
|
+
if (!namedRoot.isDirectory() || namedRoot.isSymbolicLink())
|
|
1373
|
+
throw new Error(`Managed root authority changed: ${canonicalRootPath}`);
|
|
1374
|
+
const resolvedPath = path.resolve(pathname);
|
|
1375
|
+
const filesystemRoot = path.parse(resolvedPath).root;
|
|
1376
|
+
const components = path.relative(filesystemRoot, resolvedPath).split(path.sep).filter(Boolean);
|
|
1377
|
+
let currentPath = filesystemRoot;
|
|
1378
|
+
let reachedManagedRoot = currentPath === canonicalRootPath;
|
|
1379
|
+
if (reachedManagedRoot) currentPath = canonicalRootPath;
|
|
1380
|
+
|
|
1381
|
+
for (let index = 0; index < components.length; index++) {
|
|
1382
|
+
const component = components[index]!;
|
|
1383
|
+
const nextPath = path.join(currentPath, component);
|
|
1384
|
+
if (!reachedManagedRoot) {
|
|
1385
|
+
let canonicalPrefix: string;
|
|
1386
|
+
try {
|
|
1387
|
+
canonicalPrefix = fs.realpathSync.native(nextPath);
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
1390
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
1391
|
+
throw new Error(`Managed path escapes configured root: ${pathname}`, { cause: error });
|
|
1392
|
+
throw error;
|
|
1393
|
+
}
|
|
1394
|
+
if (canonicalPrefix === canonicalRootPath) {
|
|
1395
|
+
reachedManagedRoot = true;
|
|
1396
|
+
currentPath = canonicalRootPath;
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
currentPath = nextPath;
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
let named: fs.Stats;
|
|
1404
|
+
try {
|
|
1405
|
+
named = fs.lstatSync(nextPath);
|
|
1406
|
+
} catch (error) {
|
|
1407
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
1408
|
+
if (code === "ENOENT") return path.join(currentPath, ...components.slice(index));
|
|
1409
|
+
throw error;
|
|
1410
|
+
}
|
|
1411
|
+
if (named.isSymbolicLink()) throw new Error(`Managed path contains symlink: ${nextPath}`);
|
|
1412
|
+
if (index < components.length - 1 && !named.isDirectory())
|
|
1413
|
+
throw new Error(`Managed path component is not a directory: ${nextPath}`);
|
|
1414
|
+
currentPath = nextPath;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
if (!reachedManagedRoot) throw new Error(`Managed path escapes configured root: ${pathname}`);
|
|
1418
|
+
return currentPath;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
function relativeWithinManagedRoot(rootPath: string, pathname: string): readonly string[] {
|
|
1422
|
+
const canonicalPath = canonicalizeManagedPathWithinRoot(rootPath, pathname);
|
|
1423
|
+
const relative = path.relative(rootPath, canonicalPath);
|
|
1424
|
+
if (process.platform === "win32") {
|
|
1425
|
+
const canonicalRoot = path.win32.normalize(rootPath);
|
|
1426
|
+
const canonicalCandidate = path.win32.normalize(canonicalPath);
|
|
1427
|
+
const rootPrefix = canonicalRoot.endsWith(path.win32.sep) ? canonicalRoot : `${canonicalRoot}${path.win32.sep}`;
|
|
1428
|
+
if (canonicalCandidate !== canonicalRoot && !canonicalCandidate.startsWith(rootPrefix))
|
|
1429
|
+
throw new Error(`Managed path escapes configured root: ${pathname}`);
|
|
1430
|
+
}
|
|
1335
1431
|
if (relative === "") return [];
|
|
1336
1432
|
if (path.isAbsolute(relative) || relative.split(path.sep).includes(".."))
|
|
1337
1433
|
throw new Error(`Managed path escapes configured root: ${pathname}`);
|
|
1338
1434
|
return relative.split(path.sep);
|
|
1339
1435
|
}
|
|
1340
1436
|
|
|
1437
|
+
function managedRelativePath(root: ManagedDirectoryRoot, pathname: string): readonly string[] {
|
|
1438
|
+
return relativeWithinManagedRoot(root.canonicalPath, pathname);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1341
1441
|
const PROCESS_START_ID = randomUUID();
|
|
1342
1442
|
|
|
1343
1443
|
class ManagedSecurityError extends Error {
|
|
@@ -1429,7 +1529,12 @@ function secureExistingManagedDirectory(pathname: string, kind: "directory" | "f
|
|
|
1429
1529
|
);
|
|
1430
1530
|
assertManagedPathIdentity(pathname, kind, named);
|
|
1431
1531
|
if (verified.ok) return;
|
|
1432
|
-
|
|
1532
|
+
// owner_mismatch: directory owner is not the current user. Repair attempts ownership transfer.
|
|
1533
|
+
// If the user lacks SeTakeOwnershipPrivilege (non-elevated), the repair will fail with
|
|
1534
|
+
// io_error, which is caught as a security error with clear context below.
|
|
1535
|
+
if (verified.code !== "acl_verify_failed" && verified.code !== "owner_mismatch") {
|
|
1536
|
+
throw securityError(pathname, verified);
|
|
1537
|
+
}
|
|
1433
1538
|
const repaired = validateNativeSecurityResult(
|
|
1434
1539
|
nativeSessionStorage().repairOwnerOnlyPathSecurityExpected(
|
|
1435
1540
|
pathname,
|
|
@@ -1440,7 +1545,16 @@ function secureExistingManagedDirectory(pathname: string, kind: "directory" | "f
|
|
|
1440
1545
|
"verify",
|
|
1441
1546
|
kind,
|
|
1442
1547
|
);
|
|
1443
|
-
if (!repaired.ok)
|
|
1548
|
+
if (!repaired.ok) {
|
|
1549
|
+
// If initial verification was owner_mismatch and repair failed (typically io_error
|
|
1550
|
+
// when the user lacks SeTakeOwnershipPrivilege), provide clear context.
|
|
1551
|
+
if (verified.code === "owner_mismatch" && repaired.code === "io_error") {
|
|
1552
|
+
throw new Error(
|
|
1553
|
+
`${kind} owner mismatch: unable to take ownership (need administrator privileges or file ownership): ${pathname}`,
|
|
1554
|
+
);
|
|
1555
|
+
}
|
|
1556
|
+
throw securityError(pathname, repaired);
|
|
1557
|
+
}
|
|
1444
1558
|
assertManagedPathIdentity(pathname, kind, named);
|
|
1445
1559
|
}
|
|
1446
1560
|
|
|
@@ -1452,11 +1566,18 @@ function secureManagedDirectory(pathname: string, created: boolean, policy: Mana
|
|
|
1452
1566
|
secure(pathname, "directory");
|
|
1453
1567
|
}
|
|
1454
1568
|
|
|
1569
|
+
/** Exact, identity-bound read access retained for the lifetime of one managed stream. */
|
|
1570
|
+
export interface ManagedSessionReadLease {
|
|
1571
|
+
readRange(start: number, length: number): Uint8Array;
|
|
1572
|
+
close(): void;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1455
1575
|
/** Internal retained-root capability for one managed descendant subtree. */
|
|
1456
1576
|
export class ManagedSessionDescendantStore {
|
|
1457
1577
|
readonly #root: ManagedDirectoryRoot;
|
|
1458
1578
|
readonly #baseDir: string;
|
|
1459
1579
|
readonly #policy: ManagedSessionSecurityPolicy;
|
|
1580
|
+
readonly #access: "read-only" | "read-write";
|
|
1460
1581
|
readonly #authority: RecoveryFsRoot | undefined;
|
|
1461
1582
|
#ownsAuthority = false;
|
|
1462
1583
|
#closed = false;
|
|
@@ -1475,16 +1596,52 @@ export class ManagedSessionDescendantStore {
|
|
|
1475
1596
|
policy?: ManagedSessionSecurityPolicy,
|
|
1476
1597
|
profileAgentDir?: string,
|
|
1477
1598
|
expectedSubtreeRoot?: ManagedDirectoryRoot,
|
|
1599
|
+
access: "read-only" | "read-write" = "read-write",
|
|
1478
1600
|
) {
|
|
1479
|
-
|
|
1601
|
+
if (access !== "read-only" && access !== "read-write") throw new Error("managed_store_access_invalid");
|
|
1602
|
+
if (access === "read-only" && retained) throw new Error("managed_read_store_cannot_borrow_authority");
|
|
1603
|
+
if (access === "read-only" && !expectedSubtreeRoot)
|
|
1604
|
+
throw new Error("managed_read_store_requires_existing_identity");
|
|
1605
|
+
assertManagedDirectoryRoot(root);
|
|
1606
|
+
let canonicalBaseDir: string;
|
|
1607
|
+
try {
|
|
1608
|
+
canonicalBaseDir = canonicalizeManagedPathWithinRoot(root.canonicalPath, baseDir);
|
|
1609
|
+
} catch (error) {
|
|
1610
|
+
// If canonicalization fails with a symlink error on a retained path,
|
|
1611
|
+
// the binding has changed: the path no longer resolves to the original inode.
|
|
1612
|
+
if (retained && error instanceof Error && error.message.startsWith("Managed path contains symlink:")) {
|
|
1613
|
+
throw new Error("Managed descendant root binding changed");
|
|
1614
|
+
}
|
|
1615
|
+
throw error;
|
|
1616
|
+
}
|
|
1617
|
+
let canonicalAuthorityBaseDir: string;
|
|
1618
|
+
if (retained) {
|
|
1619
|
+
try {
|
|
1620
|
+
canonicalAuthorityBaseDir = canonicalizeManagedPathWithinRoot(
|
|
1621
|
+
root.canonicalPath,
|
|
1622
|
+
retained.authorityBaseDir,
|
|
1623
|
+
);
|
|
1624
|
+
} catch (error) {
|
|
1625
|
+
// If canonicalization fails with a symlink error on a retained path,
|
|
1626
|
+
// the binding has changed: the path no longer resolves to the original inode.
|
|
1627
|
+
if (error instanceof Error && error.message.startsWith("Managed path contains symlink:")) {
|
|
1628
|
+
throw new Error("Managed descendant root binding changed");
|
|
1629
|
+
}
|
|
1630
|
+
throw error;
|
|
1631
|
+
}
|
|
1632
|
+
} else {
|
|
1633
|
+
canonicalAuthorityBaseDir = canonicalBaseDir;
|
|
1634
|
+
}
|
|
1635
|
+
managedRelativePath(root, canonicalBaseDir);
|
|
1480
1636
|
|
|
1481
1637
|
this.#root = root;
|
|
1482
|
-
this.#baseDir =
|
|
1638
|
+
this.#baseDir = canonicalBaseDir;
|
|
1483
1639
|
this.#policy = policy ?? "default";
|
|
1640
|
+
this.#access = access;
|
|
1484
1641
|
this.#profileAgentDir = profileAgentDir ?? root.canonicalPath;
|
|
1485
|
-
this.#authorityBaseDir =
|
|
1642
|
+
this.#authorityBaseDir = canonicalAuthorityBaseDir;
|
|
1486
1643
|
if (retained) {
|
|
1487
|
-
const relative =
|
|
1644
|
+
const relative = relativeWithinManagedRoot(canonicalAuthorityBaseDir, this.#baseDir).join("/");
|
|
1488
1645
|
// For the root case (authorityBaseDir === baseDir, relative === ""), the
|
|
1489
1646
|
// stable identity() result carries the exact root dev/inode without
|
|
1490
1647
|
// snapshotting the entire live session tree. snapshotManagedTree("")
|
|
@@ -1558,7 +1715,7 @@ export class ManagedSessionDescendantStore {
|
|
|
1558
1715
|
dev: canonicalFileId(subtreeStat.dev),
|
|
1559
1716
|
ino: canonicalFileId(subtreeStat.ino),
|
|
1560
1717
|
});
|
|
1561
|
-
if (process.platform === "linux") {
|
|
1718
|
+
if (process.platform === "linux" && access === "read-write") {
|
|
1562
1719
|
const before = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
1563
1720
|
const authority = nativeSessionStorage().openRecoveryFsRoot(this.#baseDir);
|
|
1564
1721
|
const retained = authority.identity();
|
|
@@ -1606,19 +1763,27 @@ export class ManagedSessionDescendantStore {
|
|
|
1606
1763
|
child.dev.toString(),
|
|
1607
1764
|
child.ino.toString(),
|
|
1608
1765
|
);
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1766
|
+
try {
|
|
1767
|
+
const derived = new ManagedSessionDescendantStore(
|
|
1768
|
+
this.#root,
|
|
1769
|
+
resolved,
|
|
1770
|
+
{
|
|
1771
|
+
authority: retainedChild,
|
|
1772
|
+
authorityBaseDir: resolved,
|
|
1773
|
+
},
|
|
1774
|
+
this.#policy,
|
|
1775
|
+
this.#profileAgentDir,
|
|
1776
|
+
);
|
|
1777
|
+
derived.#ownsAuthority = true;
|
|
1778
|
+
return derived;
|
|
1779
|
+
} catch (error) {
|
|
1780
|
+
retainedChild.close();
|
|
1781
|
+
throw error;
|
|
1782
|
+
}
|
|
1619
1783
|
}
|
|
1620
1784
|
|
|
1621
1785
|
retainAuthority(): RecoveryFsRoot | undefined {
|
|
1786
|
+
this.#assertWritable();
|
|
1622
1787
|
if (!this.#authority) return undefined;
|
|
1623
1788
|
return this.#authority.retainManagedDirectory(
|
|
1624
1789
|
"",
|
|
@@ -1673,7 +1838,7 @@ export class ManagedSessionDescendantStore {
|
|
|
1673
1838
|
captureDirectoryIdentity(relativePath: string): { dev: string; ino: string } {
|
|
1674
1839
|
this.#assertBound();
|
|
1675
1840
|
const resolved = this.#resolve(relativePath);
|
|
1676
|
-
if (!this.#authority) this.#assertPathBackedDirectoryChain(resolved);
|
|
1841
|
+
if (this.#access === "read-only" || !this.#authority) this.#assertPathBackedDirectoryChain(resolved);
|
|
1677
1842
|
const named = fs.lstatSync(resolved, { bigint: true });
|
|
1678
1843
|
if (!named.isDirectory() || named.isSymbolicLink()) throw new Error("managed_directory_identity_unavailable");
|
|
1679
1844
|
const dev = canonicalFileId(named.dev);
|
|
@@ -1714,9 +1879,10 @@ export class ManagedSessionDescendantStore {
|
|
|
1714
1879
|
|
|
1715
1880
|
#assertBound(): void {
|
|
1716
1881
|
if (!this.#authority) {
|
|
1717
|
-
const named = fs.
|
|
1882
|
+
const named = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
1718
1883
|
if (
|
|
1719
1884
|
!named.isDirectory() ||
|
|
1885
|
+
named.isSymbolicLink() ||
|
|
1720
1886
|
canonicalFileId(named.dev) !== this.#subtreeRoot.dev ||
|
|
1721
1887
|
canonicalFileId(named.ino) !== this.#subtreeRoot.ino
|
|
1722
1888
|
)
|
|
@@ -2019,7 +2185,12 @@ export class ManagedSessionDescendantStore {
|
|
|
2019
2185
|
}
|
|
2020
2186
|
}
|
|
2021
2187
|
|
|
2188
|
+
#assertWritable(): void {
|
|
2189
|
+
if (this.#access === "read-only") throw new Error("managed_store_read_only");
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2022
2192
|
#beforeMutation(): void {
|
|
2193
|
+
this.#assertWritable();
|
|
2023
2194
|
this.#assertBound();
|
|
2024
2195
|
// Reaping is scheduled BEFORE reconciliation, not after. The receipt scan
|
|
2025
2196
|
// throws `managed_replace_cleanup_receipt_limit_exceeded` once the bound
|
|
@@ -2591,12 +2762,18 @@ export class ManagedSessionDescendantStore {
|
|
|
2591
2762
|
canonicalFileId(rootBefore.ino) !== this.#subtreeRoot.ino
|
|
2592
2763
|
)
|
|
2593
2764
|
throw new Error("Managed descendant root binding changed");
|
|
2594
|
-
let fd: number
|
|
2765
|
+
let fd: number;
|
|
2595
2766
|
try {
|
|
2596
2767
|
fd = fs.openSync(
|
|
2597
2768
|
resolved,
|
|
2598
2769
|
fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | (fs.constants.O_NOFOLLOW ?? 0),
|
|
2599
2770
|
);
|
|
2771
|
+
} catch (error) {
|
|
2772
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
2773
|
+
throw error;
|
|
2774
|
+
}
|
|
2775
|
+
let descriptor: SessionStorageStat;
|
|
2776
|
+
try {
|
|
2600
2777
|
const opened = fs.fstatSync(fd, { bigint: true });
|
|
2601
2778
|
const named = fs.lstatSync(resolved, { bigint: true });
|
|
2602
2779
|
if (
|
|
@@ -2618,7 +2795,7 @@ export class ManagedSessionDescendantStore {
|
|
|
2618
2795
|
)
|
|
2619
2796
|
throw new Error("Managed descendant root binding changed");
|
|
2620
2797
|
this.#assertBound();
|
|
2621
|
-
|
|
2798
|
+
descriptor = managedAppendReceiptFromIdentity({
|
|
2622
2799
|
dev: canonicalFileId(opened.dev),
|
|
2623
2800
|
ino: canonicalFileId(opened.ino),
|
|
2624
2801
|
nlink: opened.nlink,
|
|
@@ -2627,11 +2804,13 @@ export class ManagedSessionDescendantStore {
|
|
|
2627
2804
|
ctimeNs: opened.ctimeNs,
|
|
2628
2805
|
}).descriptor;
|
|
2629
2806
|
} catch (error) {
|
|
2630
|
-
|
|
2807
|
+
try {
|
|
2808
|
+
fs.closeSync(fd);
|
|
2809
|
+
} catch {}
|
|
2631
2810
|
throw error;
|
|
2632
|
-
} finally {
|
|
2633
|
-
if (fd !== undefined) fs.closeSync(fd);
|
|
2634
2811
|
}
|
|
2812
|
+
fs.closeSync(fd);
|
|
2813
|
+
return descriptor;
|
|
2635
2814
|
}
|
|
2636
2815
|
const stat = this.#authority.stat(this.#relative(resolved));
|
|
2637
2816
|
if (!stat.ok) {
|
|
@@ -2657,7 +2836,8 @@ export class ManagedSessionDescendantStore {
|
|
|
2657
2836
|
if (!Number.isSafeInteger(start) || start < 0 || !Number.isSafeInteger(length) || length < 0)
|
|
2658
2837
|
throw new RangeError("Invalid managed range read");
|
|
2659
2838
|
if (start > Number.MAX_SAFE_INTEGER - length) throw new RangeError("Managed range read start overflows");
|
|
2660
|
-
if (length >
|
|
2839
|
+
if (length > MANAGED_SESSION_READ_RANGE_MAX_BYTES)
|
|
2840
|
+
throw new RangeError("Managed range read exceeds the bounded maximum");
|
|
2661
2841
|
this.#assertPathBackedReadRelative(relativePath);
|
|
2662
2842
|
this.#assertBound();
|
|
2663
2843
|
const rootBefore = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
@@ -2681,6 +2861,7 @@ export class ManagedSessionDescendantStore {
|
|
|
2681
2861
|
resolved,
|
|
2682
2862
|
fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | (fs.constants.O_NOFOLLOW ?? 0),
|
|
2683
2863
|
);
|
|
2864
|
+
let snapshot: SessionStorageRangeSnapshot;
|
|
2684
2865
|
try {
|
|
2685
2866
|
const before = fs.fstatSync(fd, { bigint: true });
|
|
2686
2867
|
if (!before.isFile() || before.nlink > 1n) throw new Error("source_changed");
|
|
@@ -2727,7 +2908,10 @@ export class ManagedSessionDescendantStore {
|
|
|
2727
2908
|
named.isSymbolicLink() ||
|
|
2728
2909
|
named.dev !== before.dev ||
|
|
2729
2910
|
named.ino !== before.ino ||
|
|
2730
|
-
named.nlink !== before.nlink
|
|
2911
|
+
named.nlink !== before.nlink ||
|
|
2912
|
+
named.size !== before.size ||
|
|
2913
|
+
named.mtimeNs !== before.mtimeNs ||
|
|
2914
|
+
named.ctimeNs !== before.ctimeNs
|
|
2731
2915
|
)
|
|
2732
2916
|
throw new Error("source_changed");
|
|
2733
2917
|
const rootAfter = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
@@ -2739,7 +2923,7 @@ export class ManagedSessionDescendantStore {
|
|
|
2739
2923
|
)
|
|
2740
2924
|
throw new Error("Managed descendant root binding changed");
|
|
2741
2925
|
this.#assertBound();
|
|
2742
|
-
|
|
2926
|
+
snapshot = {
|
|
2743
2927
|
bytes,
|
|
2744
2928
|
stat: {
|
|
2745
2929
|
dev: canonicalFileId(after.dev),
|
|
@@ -2753,10 +2937,133 @@ export class ManagedSessionDescendantStore {
|
|
|
2753
2937
|
isFile: true,
|
|
2754
2938
|
},
|
|
2755
2939
|
};
|
|
2756
|
-
}
|
|
2757
|
-
|
|
2940
|
+
} catch (error) {
|
|
2941
|
+
try {
|
|
2942
|
+
fs.closeSync(fd);
|
|
2943
|
+
} catch {}
|
|
2944
|
+
throw error;
|
|
2945
|
+
}
|
|
2946
|
+
fs.closeSync(fd);
|
|
2947
|
+
return snapshot;
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
/** Open an identity-bound stream lease; Darwin uses exact bounded range reads per chunk. */
|
|
2951
|
+
openReadLease(
|
|
2952
|
+
relativePath: string,
|
|
2953
|
+
expectedDescriptor: Pick<SessionStorageStat, "dev" | "ino" | "nlink" | "size" | "mtimeNs" | "ctimeNs">,
|
|
2954
|
+
): ManagedSessionReadLease {
|
|
2955
|
+
if (!Number.isSafeInteger(expectedDescriptor.size) || expectedDescriptor.size < 0)
|
|
2956
|
+
throw new RangeError("Invalid managed read identity size");
|
|
2957
|
+
this.#assertPathBackedReadRelative(relativePath);
|
|
2958
|
+
this.#assertBound();
|
|
2959
|
+
if (!this.#authority) {
|
|
2960
|
+
this.readRangeExpectedSync(relativePath, 0, 0, expectedDescriptor);
|
|
2961
|
+
let closed = false;
|
|
2962
|
+
return {
|
|
2963
|
+
readRange: (start, length) => {
|
|
2964
|
+
if (closed) throw new Error("closed");
|
|
2965
|
+
if (start + length > expectedDescriptor.size) throw new Error("range_not_present");
|
|
2966
|
+
return this.readRangeExpectedSync(relativePath, start, length, expectedDescriptor).bytes;
|
|
2967
|
+
},
|
|
2968
|
+
close: () => {
|
|
2969
|
+
if (closed) return;
|
|
2970
|
+
closed = true;
|
|
2971
|
+
this.readRangeExpectedSync(relativePath, 0, 0, expectedDescriptor);
|
|
2972
|
+
},
|
|
2973
|
+
};
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
const relative = this.#relative(this.#resolve(relativePath));
|
|
2977
|
+
const authority = this.#authority;
|
|
2978
|
+
const matchesExpected = (identity: RecoveryFsIdentity | undefined): boolean => {
|
|
2979
|
+
if (!identity) return false;
|
|
2980
|
+
const actual = managedFileIdentityFromNative(identity);
|
|
2981
|
+
return (
|
|
2982
|
+
actual.dev === expectedDescriptor.dev &&
|
|
2983
|
+
actual.ino === expectedDescriptor.ino &&
|
|
2984
|
+
(expectedDescriptor.nlink === undefined || actual.nlink === expectedDescriptor.nlink) &&
|
|
2985
|
+
actual.size === expectedDescriptor.size &&
|
|
2986
|
+
actual.mtimeNs === expectedDescriptor.mtimeNs &&
|
|
2987
|
+
actual.ctimeNs === expectedDescriptor.ctimeNs
|
|
2988
|
+
);
|
|
2989
|
+
};
|
|
2990
|
+
const assertCurrentBinding = (): void => {
|
|
2991
|
+
this.#assertBound();
|
|
2992
|
+
const named = authority.stat(relative);
|
|
2993
|
+
if (!named.ok || !matchesExpected(named.identity)) throw new Error("source_changed");
|
|
2994
|
+
this.#assertBound();
|
|
2995
|
+
};
|
|
2996
|
+
|
|
2997
|
+
let file: RecoveryFsFile | undefined;
|
|
2998
|
+
try {
|
|
2999
|
+
file = authority.openFile(relative);
|
|
3000
|
+
const opened = file.identity();
|
|
3001
|
+
if (!opened.ok || !matchesExpected(opened.identity)) throw new Error("source_changed");
|
|
3002
|
+
assertCurrentBinding();
|
|
3003
|
+
} catch (error) {
|
|
3004
|
+
try {
|
|
3005
|
+
file?.close();
|
|
3006
|
+
} catch {
|
|
3007
|
+
// Preserve the identity/open failure; closing is still attempted.
|
|
3008
|
+
}
|
|
3009
|
+
throw error;
|
|
2758
3010
|
}
|
|
3011
|
+
|
|
3012
|
+
let closed = false;
|
|
3013
|
+
return {
|
|
3014
|
+
readRange: (start, length) => {
|
|
3015
|
+
if (closed) throw new Error("closed");
|
|
3016
|
+
if (
|
|
3017
|
+
!Number.isSafeInteger(start) ||
|
|
3018
|
+
start < 0 ||
|
|
3019
|
+
!Number.isSafeInteger(length) ||
|
|
3020
|
+
length < 0 ||
|
|
3021
|
+
start > Number.MAX_SAFE_INTEGER - length
|
|
3022
|
+
)
|
|
3023
|
+
throw new RangeError("Invalid managed range read");
|
|
3024
|
+
if (length > 1024 * 1024) throw new RangeError("Managed read chunk exceeds the bounded maximum");
|
|
3025
|
+
if (start + length > expectedDescriptor.size) throw new Error("range_not_present");
|
|
3026
|
+
assertCurrentBinding();
|
|
3027
|
+
const before = file!.identity();
|
|
3028
|
+
if (!before.ok || !matchesExpected(before.identity)) throw new Error("source_changed");
|
|
3029
|
+
const chunk = file!.readChunk(start, length);
|
|
3030
|
+
if (!chunk.ok || !chunk.data || !chunk.identity) throw new Error(chunk.code ?? "managed_read_failed");
|
|
3031
|
+
if (!matchesExpected(chunk.identity)) throw new Error("source_changed");
|
|
3032
|
+
if (chunk.data.byteLength !== length) throw new Error("range_not_present");
|
|
3033
|
+
const after = file!.identity();
|
|
3034
|
+
if (!after.ok || !matchesExpected(after.identity)) throw new Error("source_changed");
|
|
3035
|
+
assertCurrentBinding();
|
|
3036
|
+
return chunk.data;
|
|
3037
|
+
},
|
|
3038
|
+
close: () => {
|
|
3039
|
+
if (closed) return;
|
|
3040
|
+
closed = true;
|
|
3041
|
+
let failure: unknown;
|
|
3042
|
+
try {
|
|
3043
|
+
assertCurrentBinding();
|
|
3044
|
+
} catch (error) {
|
|
3045
|
+
failure = error;
|
|
3046
|
+
}
|
|
3047
|
+
let result: RecoveryFsResult | undefined;
|
|
3048
|
+
try {
|
|
3049
|
+
result = file!.close();
|
|
3050
|
+
} catch (error) {
|
|
3051
|
+
failure ??= error;
|
|
3052
|
+
}
|
|
3053
|
+
if (failure === undefined && (!result?.ok || !matchesExpected(result.identity)))
|
|
3054
|
+
failure = new Error(result?.code ?? "source_changed");
|
|
3055
|
+
if (failure === undefined) {
|
|
3056
|
+
try {
|
|
3057
|
+
assertCurrentBinding();
|
|
3058
|
+
} catch (error) {
|
|
3059
|
+
failure = error;
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
if (failure !== undefined) throw failure;
|
|
3063
|
+
},
|
|
3064
|
+
};
|
|
2759
3065
|
}
|
|
3066
|
+
|
|
2760
3067
|
/** Read an exact managed file without exposing its pathname as authority. */
|
|
2761
3068
|
readExpected(relativePath: string): ManagedFileSnapshot | null {
|
|
2762
3069
|
this.#assertBound();
|
|
@@ -2793,6 +3100,85 @@ export class ManagedSessionDescendantStore {
|
|
|
2793
3100
|
};
|
|
2794
3101
|
}
|
|
2795
3102
|
|
|
3103
|
+
/** Read one complete managed file through retained authority after descriptor-size admission. */
|
|
3104
|
+
readExpectedBounded(
|
|
3105
|
+
relativePath: string,
|
|
3106
|
+
maxBytes: number,
|
|
3107
|
+
admitSize?: (size: number, descriptor: ManagedFileIdentity) => void,
|
|
3108
|
+
): ManagedFileSnapshot | null {
|
|
3109
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new Error("invalid_capture_limit");
|
|
3110
|
+
this.#assertBound();
|
|
3111
|
+
const resolved = this.#resolve(relativePath);
|
|
3112
|
+
if (!this.#authority) {
|
|
3113
|
+
try {
|
|
3114
|
+
this.#assertPathBackedDirectoryChain(resolved);
|
|
3115
|
+
const captured = captureManagedFileNoFollowBounded(resolved, maxBytes, admitSize);
|
|
3116
|
+
this.#assertBound();
|
|
3117
|
+
return captured;
|
|
3118
|
+
} catch (error) {
|
|
3119
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
3120
|
+
throw error;
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
let descriptor: SessionStorageRangeSnapshot;
|
|
3124
|
+
try {
|
|
3125
|
+
descriptor = this.readRangeExpectedSync(this.#relative(resolved), 0, 0);
|
|
3126
|
+
} catch (error) {
|
|
3127
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
3128
|
+
throw error;
|
|
3129
|
+
}
|
|
3130
|
+
const { stat } = descriptor;
|
|
3131
|
+
if (stat.nlink !== 1n) throw new Error("source_changed");
|
|
3132
|
+
if (!Number.isSafeInteger(stat.size) || stat.size < 0) throw new Error("source_changed");
|
|
3133
|
+
if (stat.size > maxBytes) throw new Error("artifact_capacity_exceeded");
|
|
3134
|
+
const lease = this.openReadLease(this.#relative(resolved), stat);
|
|
3135
|
+
let failed = false;
|
|
3136
|
+
let failure: unknown;
|
|
3137
|
+
let snapshot: ManagedFileSnapshot | undefined;
|
|
3138
|
+
try {
|
|
3139
|
+
admitSize?.(stat.size, {
|
|
3140
|
+
dev: stat.dev,
|
|
3141
|
+
ino: stat.ino,
|
|
3142
|
+
nlink: stat.nlink,
|
|
3143
|
+
size: stat.size,
|
|
3144
|
+
mtimeNs: stat.mtimeNs,
|
|
3145
|
+
ctimeNs: stat.ctimeNs,
|
|
3146
|
+
});
|
|
3147
|
+
const bytes = Buffer.alloc(stat.size);
|
|
3148
|
+
for (let offset = 0; offset < bytes.byteLength; ) {
|
|
3149
|
+
const length = Math.min(1024 * 1024, bytes.byteLength - offset);
|
|
3150
|
+
bytes.set(lease.readRange(offset, length), offset);
|
|
3151
|
+
offset += length;
|
|
3152
|
+
}
|
|
3153
|
+
snapshot = {
|
|
3154
|
+
bytes,
|
|
3155
|
+
identity: {
|
|
3156
|
+
dev: stat.dev,
|
|
3157
|
+
ino: stat.ino,
|
|
3158
|
+
nlink: stat.nlink,
|
|
3159
|
+
size: stat.size,
|
|
3160
|
+
mtimeNs: stat.mtimeNs,
|
|
3161
|
+
ctimeNs: stat.ctimeNs,
|
|
3162
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
3163
|
+
},
|
|
3164
|
+
};
|
|
3165
|
+
} catch (error) {
|
|
3166
|
+
failed = true;
|
|
3167
|
+
failure = error;
|
|
3168
|
+
}
|
|
3169
|
+
try {
|
|
3170
|
+
lease.close();
|
|
3171
|
+
} catch (error) {
|
|
3172
|
+
if (!failed) {
|
|
3173
|
+
failed = true;
|
|
3174
|
+
failure = error;
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
if (failed) throw failure;
|
|
3178
|
+
if (!snapshot) throw new Error("Managed descendant snapshot is unavailable");
|
|
3179
|
+
return snapshot;
|
|
3180
|
+
}
|
|
3181
|
+
|
|
2796
3182
|
/** Remove an exact captured file without reopening its pathname as authority. */
|
|
2797
3183
|
removeExpected(relativePath: string, expected: ManagedFileSnapshot): void {
|
|
2798
3184
|
this.#beforeMutation();
|
|
@@ -3028,6 +3414,7 @@ export class ManagedSessionDescendantStore {
|
|
|
3028
3414
|
sourceStoreRelativePath: string;
|
|
3029
3415
|
},
|
|
3030
3416
|
): ManagedFileSnapshot {
|
|
3417
|
+
this.#assertWritable();
|
|
3031
3418
|
this.#assertBound();
|
|
3032
3419
|
const sourceResolved = this.#resolve(sourceRelativePath);
|
|
3033
3420
|
const destinationResolved = this.#resolve(destinationRelativePath);
|
|
@@ -3297,7 +3684,7 @@ function sameReplacementIdentity(
|
|
|
3297
3684
|
);
|
|
3298
3685
|
}
|
|
3299
3686
|
|
|
3300
|
-
function
|
|
3687
|
+
export function parseManagedLockRecord(bytes: Uint8Array): LockRecord | undefined {
|
|
3301
3688
|
try {
|
|
3302
3689
|
const value: unknown = JSON.parse(Buffer.from(bytes).toString("utf8"));
|
|
3303
3690
|
if (!value || typeof value !== "object") return undefined;
|
|
@@ -3320,7 +3707,7 @@ function parseLock(pathname: string): LockRecord | undefined {
|
|
|
3320
3707
|
try {
|
|
3321
3708
|
const stat = fs.lstatSync(pathname);
|
|
3322
3709
|
if (!stat.isFile() || stat.isSymbolicLink()) return undefined;
|
|
3323
|
-
return
|
|
3710
|
+
return parseManagedLockRecord(fs.readFileSync(pathname));
|
|
3324
3711
|
} catch {
|
|
3325
3712
|
return undefined;
|
|
3326
3713
|
}
|
|
@@ -3329,7 +3716,7 @@ function parseLock(pathname: string): LockRecord | undefined {
|
|
|
3329
3716
|
function captureLock(pathname: string): { record: LockRecord; snapshot: ManagedFileSnapshot } | undefined {
|
|
3330
3717
|
try {
|
|
3331
3718
|
const snapshot = captureManagedFileNoFollow(pathname);
|
|
3332
|
-
const record =
|
|
3719
|
+
const record = parseManagedLockRecord(snapshot.bytes);
|
|
3333
3720
|
return record ? { record, snapshot } : undefined;
|
|
3334
3721
|
} catch {
|
|
3335
3722
|
return undefined;
|
|
@@ -3437,17 +3824,41 @@ export function captureManagedFilePrefixNoFollow(pathname: string, maxBytes: num
|
|
|
3437
3824
|
return captureManagedFileNoFollowLimit(pathname, maxBytes);
|
|
3438
3825
|
}
|
|
3439
3826
|
|
|
3827
|
+
/** Captures a complete no-follow file only after descriptor-size admission and before allocating its bytes. */
|
|
3828
|
+
export function captureManagedFileNoFollowBounded(
|
|
3829
|
+
pathname: string,
|
|
3830
|
+
maxBytes: number,
|
|
3831
|
+
admitSize?: (size: number, descriptor: ManagedFileIdentity) => void,
|
|
3832
|
+
): ManagedFileSnapshot {
|
|
3833
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new Error("invalid_capture_limit");
|
|
3834
|
+
return captureManagedFileNoFollowLimit(pathname, maxBytes, true, admitSize);
|
|
3835
|
+
}
|
|
3836
|
+
|
|
3440
3837
|
/** Captures header/hash/copy input from one no-follow descriptor and rechecks the pathname before use. */
|
|
3441
3838
|
export function captureManagedFileNoFollow(pathname: string): ManagedFileSnapshot {
|
|
3442
3839
|
return captureManagedFileNoFollowLimit(pathname);
|
|
3443
3840
|
}
|
|
3444
3841
|
|
|
3445
|
-
function captureManagedFileNoFollowLimit(
|
|
3842
|
+
function captureManagedFileNoFollowLimit(
|
|
3843
|
+
pathname: string,
|
|
3844
|
+
maxBytes?: number,
|
|
3845
|
+
rejectOversized = false,
|
|
3846
|
+
admitSize?: (size: number, descriptor: ManagedFileIdentity) => void,
|
|
3847
|
+
): ManagedFileSnapshot {
|
|
3446
3848
|
const fd = fs.openSync(pathname, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | (fs.constants.O_NOFOLLOW ?? 0));
|
|
3849
|
+
let failed = false;
|
|
3850
|
+
let failure: unknown;
|
|
3851
|
+
let snapshot: ManagedFileSnapshot | undefined;
|
|
3447
3852
|
try {
|
|
3448
3853
|
const before = fs.fstatSync(fd, { bigint: true });
|
|
3449
|
-
if (!before.isFile() || before.nlink
|
|
3450
|
-
|
|
3854
|
+
if (!before.isFile() || (rejectOversized ? before.nlink !== 1n : before.nlink > 1n))
|
|
3855
|
+
throw new Error("source_changed");
|
|
3856
|
+
const fileSize = Number(before.size);
|
|
3857
|
+
if (!Number.isSafeInteger(fileSize) || fileSize < 0) throw new Error("source_changed");
|
|
3858
|
+
if (rejectOversized && maxBytes !== undefined && fileSize > maxBytes)
|
|
3859
|
+
throw new Error("artifact_capacity_exceeded");
|
|
3860
|
+
admitSize?.(fileSize, identity(before));
|
|
3861
|
+
const captureSize = maxBytes === undefined ? fileSize : Math.min(fileSize, maxBytes);
|
|
3451
3862
|
const bytes = Buffer.alloc(captureSize);
|
|
3452
3863
|
let offset = 0;
|
|
3453
3864
|
while (offset < bytes.byteLength) {
|
|
@@ -3460,10 +3871,22 @@ function captureManagedFileNoFollowLimit(pathname: string, maxBytes?: number): M
|
|
|
3460
3871
|
const named = fs.lstatSync(pathname, { bigint: true });
|
|
3461
3872
|
if (!named.isFile() || named.isSymbolicLink() || !sameIdentity(identity(before), identity(named)))
|
|
3462
3873
|
throw new Error("source_changed");
|
|
3463
|
-
|
|
3464
|
-
}
|
|
3874
|
+
snapshot = { bytes, identity: identity(before, createHash("sha256").update(bytes).digest("hex")) };
|
|
3875
|
+
} catch (error) {
|
|
3876
|
+
failed = true;
|
|
3877
|
+
failure = error;
|
|
3878
|
+
}
|
|
3879
|
+
try {
|
|
3465
3880
|
fs.closeSync(fd);
|
|
3881
|
+
} catch (error) {
|
|
3882
|
+
if (!failed) {
|
|
3883
|
+
failed = true;
|
|
3884
|
+
failure = error;
|
|
3885
|
+
}
|
|
3466
3886
|
}
|
|
3887
|
+
if (failed) throw failure;
|
|
3888
|
+
if (!snapshot) throw new Error("Managed file capture is unavailable");
|
|
3889
|
+
return snapshot;
|
|
3467
3890
|
}
|
|
3468
3891
|
|
|
3469
3892
|
/** Streams a managed file once while retaining only a bounded header prefix and the full descriptor-bound digest. */
|