@gajae-code/coding-agent 0.18.7 → 0.18.8
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 +279 -1
- package/dist/types/async/job-manager.d.ts +1 -1
- 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/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/export/html/css-color.d.ts +2 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- 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 +3 -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/ralplan-runtime.d.ts +0 -5
- 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 +11 -1
- 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/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/oauth-public-url.d.ts +1 -0
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -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 +18 -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 +62 -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/slash-commands/helpers/progress-report.d.ts +8 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/tools/bash-skill-urls.d.ts +1 -1
- package/dist/types/tools/index.d.ts +3 -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/job-manager.ts +228 -122
- package/src/capability/index.ts +17 -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/public-command-errors.ts +21 -1
- 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/sdk.ts +28 -8
- package/src/config/autorouting-tier-map.ts +117 -7
- package/src/config/file-lock.ts +307 -68
- 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/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 +3 -0
- 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/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 +10 -0
- package/src/discovery/helpers.ts +159 -20
- package/src/discovery/mcp-json.ts +6 -0
- 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 +380 -120
- package/src/export/html/css-color.ts +9 -0
- package/src/export/html/index.ts +5 -4
- package/src/export/html/template.generated.ts +1 -1
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +7 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -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 +6 -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 +14 -4
- package/src/gjc-runtime/ralplan-runtime.ts +447 -52
- 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 +83 -8
- 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 +18 -17
- package/src/internal-urls/skill-path.ts +19 -0
- package/src/internal-urls/skill-protocol.ts +2 -0
- package/src/lsp/edits.ts +6 -0
- package/src/lsp/workspace-path.ts +51 -0
- package/src/main.ts +195 -31
- 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/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 +74 -29
- package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
- package/src/modes/controllers/selector-controller.ts +28 -15
- package/src/modes/interactive-mode.ts +4 -0
- 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/config-writer.ts +10 -6
- package/src/runtime-mcp/config.ts +1 -0
- package/src/runtime-mcp/oauth-flow.ts +5 -0
- package/src/runtime-mcp/oauth-public-url.ts +8 -0
- package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
- package/src/runtime-mcp/smithery-registry.ts +22 -3
- package/src/runtime-mcp/transports/http.ts +2 -2
- package/src/runtime-mcp/types.ts +6 -0
- package/src/sdk/acp/adapter.ts +93 -8
- package/src/sdk/broker/broker-exit.ts +52 -23
- package/src/sdk/broker/broker.ts +75 -6
- package/src/sdk/broker/ensure.ts +2 -0
- 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 +2647 -294
- package/src/sdk/host/session-runtime.ts +426 -109
- 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 +75 -12
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +67 -38
- package/src/session/agent-session.ts +623 -224
- package/src/session/artifacts.ts +174 -70
- package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
- package/src/session/internal/managed-session-scope.ts +3457 -462
- package/src/session/internal/managed-session-storage.ts +376 -61
- 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 +939 -133
- package/src/session/session-retirement.ts +573 -14
- package/src/session/session-storage.ts +417 -14
- 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 +132 -2
- package/src/slash-commands/builtin-registry.ts +39 -9
- 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/tools/ask.ts +205 -6
- package/src/tools/ast-edit.ts +4 -0
- 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/descriptors.ts +5 -1
- package/src/tools/eval.ts +56 -36
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/index.ts +3 -0
- package/src/tools/python.ts +187 -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/tools-checksum.ts +34 -0
- package/src/utils/tools-manager.ts +24 -6
- 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;
|
|
@@ -1429,7 +1465,12 @@ function secureExistingManagedDirectory(pathname: string, kind: "directory" | "f
|
|
|
1429
1465
|
);
|
|
1430
1466
|
assertManagedPathIdentity(pathname, kind, named);
|
|
1431
1467
|
if (verified.ok) return;
|
|
1432
|
-
|
|
1468
|
+
// owner_mismatch: directory owner is not the current user. Repair attempts ownership transfer.
|
|
1469
|
+
// If the user lacks SeTakeOwnershipPrivilege (non-elevated), the repair will fail with
|
|
1470
|
+
// io_error, which is caught as a security error with clear context below.
|
|
1471
|
+
if (verified.code !== "acl_verify_failed" && verified.code !== "owner_mismatch") {
|
|
1472
|
+
throw securityError(pathname, verified);
|
|
1473
|
+
}
|
|
1433
1474
|
const repaired = validateNativeSecurityResult(
|
|
1434
1475
|
nativeSessionStorage().repairOwnerOnlyPathSecurityExpected(
|
|
1435
1476
|
pathname,
|
|
@@ -1440,7 +1481,16 @@ function secureExistingManagedDirectory(pathname: string, kind: "directory" | "f
|
|
|
1440
1481
|
"verify",
|
|
1441
1482
|
kind,
|
|
1442
1483
|
);
|
|
1443
|
-
if (!repaired.ok)
|
|
1484
|
+
if (!repaired.ok) {
|
|
1485
|
+
// If initial verification was owner_mismatch and repair failed (typically io_error
|
|
1486
|
+
// when the user lacks SeTakeOwnershipPrivilege), provide clear context.
|
|
1487
|
+
if (verified.code === "owner_mismatch" && repaired.code === "io_error") {
|
|
1488
|
+
throw new Error(
|
|
1489
|
+
`${kind} owner mismatch: unable to take ownership (need administrator privileges or file ownership): ${pathname}`,
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
throw securityError(pathname, repaired);
|
|
1493
|
+
}
|
|
1444
1494
|
assertManagedPathIdentity(pathname, kind, named);
|
|
1445
1495
|
}
|
|
1446
1496
|
|
|
@@ -1452,11 +1502,18 @@ function secureManagedDirectory(pathname: string, created: boolean, policy: Mana
|
|
|
1452
1502
|
secure(pathname, "directory");
|
|
1453
1503
|
}
|
|
1454
1504
|
|
|
1505
|
+
/** Exact, identity-bound read access retained for the lifetime of one managed stream. */
|
|
1506
|
+
export interface ManagedSessionReadLease {
|
|
1507
|
+
readRange(start: number, length: number): Uint8Array;
|
|
1508
|
+
close(): void;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1455
1511
|
/** Internal retained-root capability for one managed descendant subtree. */
|
|
1456
1512
|
export class ManagedSessionDescendantStore {
|
|
1457
1513
|
readonly #root: ManagedDirectoryRoot;
|
|
1458
1514
|
readonly #baseDir: string;
|
|
1459
1515
|
readonly #policy: ManagedSessionSecurityPolicy;
|
|
1516
|
+
readonly #access: "read-only" | "read-write";
|
|
1460
1517
|
readonly #authority: RecoveryFsRoot | undefined;
|
|
1461
1518
|
#ownsAuthority = false;
|
|
1462
1519
|
#closed = false;
|
|
@@ -1475,12 +1532,18 @@ export class ManagedSessionDescendantStore {
|
|
|
1475
1532
|
policy?: ManagedSessionSecurityPolicy,
|
|
1476
1533
|
profileAgentDir?: string,
|
|
1477
1534
|
expectedSubtreeRoot?: ManagedDirectoryRoot,
|
|
1535
|
+
access: "read-only" | "read-write" = "read-write",
|
|
1478
1536
|
) {
|
|
1537
|
+
if (access !== "read-only" && access !== "read-write") throw new Error("managed_store_access_invalid");
|
|
1538
|
+
if (access === "read-only" && retained) throw new Error("managed_read_store_cannot_borrow_authority");
|
|
1539
|
+
if (access === "read-only" && !expectedSubtreeRoot)
|
|
1540
|
+
throw new Error("managed_read_store_requires_existing_identity");
|
|
1479
1541
|
managedRelativePath(root, baseDir);
|
|
1480
1542
|
|
|
1481
1543
|
this.#root = root;
|
|
1482
1544
|
this.#baseDir = path.resolve(baseDir);
|
|
1483
1545
|
this.#policy = policy ?? "default";
|
|
1546
|
+
this.#access = access;
|
|
1484
1547
|
this.#profileAgentDir = profileAgentDir ?? root.canonicalPath;
|
|
1485
1548
|
this.#authorityBaseDir = retained?.authorityBaseDir ?? this.#baseDir;
|
|
1486
1549
|
if (retained) {
|
|
@@ -1558,7 +1621,7 @@ export class ManagedSessionDescendantStore {
|
|
|
1558
1621
|
dev: canonicalFileId(subtreeStat.dev),
|
|
1559
1622
|
ino: canonicalFileId(subtreeStat.ino),
|
|
1560
1623
|
});
|
|
1561
|
-
if (process.platform === "linux") {
|
|
1624
|
+
if (process.platform === "linux" && access === "read-write") {
|
|
1562
1625
|
const before = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
1563
1626
|
const authority = nativeSessionStorage().openRecoveryFsRoot(this.#baseDir);
|
|
1564
1627
|
const retained = authority.identity();
|
|
@@ -1606,19 +1669,27 @@ export class ManagedSessionDescendantStore {
|
|
|
1606
1669
|
child.dev.toString(),
|
|
1607
1670
|
child.ino.toString(),
|
|
1608
1671
|
);
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1672
|
+
try {
|
|
1673
|
+
const derived = new ManagedSessionDescendantStore(
|
|
1674
|
+
this.#root,
|
|
1675
|
+
resolved,
|
|
1676
|
+
{
|
|
1677
|
+
authority: retainedChild,
|
|
1678
|
+
authorityBaseDir: resolved,
|
|
1679
|
+
},
|
|
1680
|
+
this.#policy,
|
|
1681
|
+
this.#profileAgentDir,
|
|
1682
|
+
);
|
|
1683
|
+
derived.#ownsAuthority = true;
|
|
1684
|
+
return derived;
|
|
1685
|
+
} catch (error) {
|
|
1686
|
+
retainedChild.close();
|
|
1687
|
+
throw error;
|
|
1688
|
+
}
|
|
1619
1689
|
}
|
|
1620
1690
|
|
|
1621
1691
|
retainAuthority(): RecoveryFsRoot | undefined {
|
|
1692
|
+
this.#assertWritable();
|
|
1622
1693
|
if (!this.#authority) return undefined;
|
|
1623
1694
|
return this.#authority.retainManagedDirectory(
|
|
1624
1695
|
"",
|
|
@@ -1673,7 +1744,7 @@ export class ManagedSessionDescendantStore {
|
|
|
1673
1744
|
captureDirectoryIdentity(relativePath: string): { dev: string; ino: string } {
|
|
1674
1745
|
this.#assertBound();
|
|
1675
1746
|
const resolved = this.#resolve(relativePath);
|
|
1676
|
-
if (!this.#authority) this.#assertPathBackedDirectoryChain(resolved);
|
|
1747
|
+
if (this.#access === "read-only" || !this.#authority) this.#assertPathBackedDirectoryChain(resolved);
|
|
1677
1748
|
const named = fs.lstatSync(resolved, { bigint: true });
|
|
1678
1749
|
if (!named.isDirectory() || named.isSymbolicLink()) throw new Error("managed_directory_identity_unavailable");
|
|
1679
1750
|
const dev = canonicalFileId(named.dev);
|
|
@@ -1714,9 +1785,10 @@ export class ManagedSessionDescendantStore {
|
|
|
1714
1785
|
|
|
1715
1786
|
#assertBound(): void {
|
|
1716
1787
|
if (!this.#authority) {
|
|
1717
|
-
const named = fs.
|
|
1788
|
+
const named = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
1718
1789
|
if (
|
|
1719
1790
|
!named.isDirectory() ||
|
|
1791
|
+
named.isSymbolicLink() ||
|
|
1720
1792
|
canonicalFileId(named.dev) !== this.#subtreeRoot.dev ||
|
|
1721
1793
|
canonicalFileId(named.ino) !== this.#subtreeRoot.ino
|
|
1722
1794
|
)
|
|
@@ -2019,7 +2091,12 @@ export class ManagedSessionDescendantStore {
|
|
|
2019
2091
|
}
|
|
2020
2092
|
}
|
|
2021
2093
|
|
|
2094
|
+
#assertWritable(): void {
|
|
2095
|
+
if (this.#access === "read-only") throw new Error("managed_store_read_only");
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2022
2098
|
#beforeMutation(): void {
|
|
2099
|
+
this.#assertWritable();
|
|
2023
2100
|
this.#assertBound();
|
|
2024
2101
|
// Reaping is scheduled BEFORE reconciliation, not after. The receipt scan
|
|
2025
2102
|
// throws `managed_replace_cleanup_receipt_limit_exceeded` once the bound
|
|
@@ -2657,7 +2734,8 @@ export class ManagedSessionDescendantStore {
|
|
|
2657
2734
|
if (!Number.isSafeInteger(start) || start < 0 || !Number.isSafeInteger(length) || length < 0)
|
|
2658
2735
|
throw new RangeError("Invalid managed range read");
|
|
2659
2736
|
if (start > Number.MAX_SAFE_INTEGER - length) throw new RangeError("Managed range read start overflows");
|
|
2660
|
-
if (length >
|
|
2737
|
+
if (length > MANAGED_SESSION_READ_RANGE_MAX_BYTES)
|
|
2738
|
+
throw new RangeError("Managed range read exceeds the bounded maximum");
|
|
2661
2739
|
this.#assertPathBackedReadRelative(relativePath);
|
|
2662
2740
|
this.#assertBound();
|
|
2663
2741
|
const rootBefore = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
@@ -2727,7 +2805,10 @@ export class ManagedSessionDescendantStore {
|
|
|
2727
2805
|
named.isSymbolicLink() ||
|
|
2728
2806
|
named.dev !== before.dev ||
|
|
2729
2807
|
named.ino !== before.ino ||
|
|
2730
|
-
named.nlink !== before.nlink
|
|
2808
|
+
named.nlink !== before.nlink ||
|
|
2809
|
+
named.size !== before.size ||
|
|
2810
|
+
named.mtimeNs !== before.mtimeNs ||
|
|
2811
|
+
named.ctimeNs !== before.ctimeNs
|
|
2731
2812
|
)
|
|
2732
2813
|
throw new Error("source_changed");
|
|
2733
2814
|
const rootAfter = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
@@ -2757,6 +2838,124 @@ export class ManagedSessionDescendantStore {
|
|
|
2757
2838
|
fs.closeSync(fd);
|
|
2758
2839
|
}
|
|
2759
2840
|
}
|
|
2841
|
+
|
|
2842
|
+
/** Open an identity-bound stream lease; Darwin uses exact bounded range reads per chunk. */
|
|
2843
|
+
openReadLease(
|
|
2844
|
+
relativePath: string,
|
|
2845
|
+
expectedDescriptor: Pick<SessionStorageStat, "dev" | "ino" | "nlink" | "size" | "mtimeNs" | "ctimeNs">,
|
|
2846
|
+
): ManagedSessionReadLease {
|
|
2847
|
+
if (!Number.isSafeInteger(expectedDescriptor.size) || expectedDescriptor.size < 0)
|
|
2848
|
+
throw new RangeError("Invalid managed read identity size");
|
|
2849
|
+
this.#assertPathBackedReadRelative(relativePath);
|
|
2850
|
+
this.#assertBound();
|
|
2851
|
+
if (!this.#authority) {
|
|
2852
|
+
this.readRangeExpectedSync(relativePath, 0, 0, expectedDescriptor);
|
|
2853
|
+
let closed = false;
|
|
2854
|
+
return {
|
|
2855
|
+
readRange: (start, length) => {
|
|
2856
|
+
if (closed) throw new Error("closed");
|
|
2857
|
+
if (start + length > expectedDescriptor.size) throw new Error("range_not_present");
|
|
2858
|
+
return this.readRangeExpectedSync(relativePath, start, length, expectedDescriptor).bytes;
|
|
2859
|
+
},
|
|
2860
|
+
close: () => {
|
|
2861
|
+
if (closed) return;
|
|
2862
|
+
closed = true;
|
|
2863
|
+
this.readRangeExpectedSync(relativePath, 0, 0, expectedDescriptor);
|
|
2864
|
+
},
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
const relative = this.#relative(this.#resolve(relativePath));
|
|
2869
|
+
const authority = this.#authority;
|
|
2870
|
+
const matchesExpected = (identity: RecoveryFsIdentity | undefined): boolean => {
|
|
2871
|
+
if (!identity) return false;
|
|
2872
|
+
const actual = managedFileIdentityFromNative(identity);
|
|
2873
|
+
return (
|
|
2874
|
+
actual.dev === expectedDescriptor.dev &&
|
|
2875
|
+
actual.ino === expectedDescriptor.ino &&
|
|
2876
|
+
(expectedDescriptor.nlink === undefined || actual.nlink === expectedDescriptor.nlink) &&
|
|
2877
|
+
actual.size === expectedDescriptor.size &&
|
|
2878
|
+
actual.mtimeNs === expectedDescriptor.mtimeNs &&
|
|
2879
|
+
actual.ctimeNs === expectedDescriptor.ctimeNs
|
|
2880
|
+
);
|
|
2881
|
+
};
|
|
2882
|
+
const assertCurrentBinding = (): void => {
|
|
2883
|
+
this.#assertBound();
|
|
2884
|
+
const named = authority.stat(relative);
|
|
2885
|
+
if (!named.ok || !matchesExpected(named.identity)) throw new Error("source_changed");
|
|
2886
|
+
this.#assertBound();
|
|
2887
|
+
};
|
|
2888
|
+
|
|
2889
|
+
let file: RecoveryFsFile | undefined;
|
|
2890
|
+
try {
|
|
2891
|
+
file = authority.openFile(relative);
|
|
2892
|
+
const opened = file.identity();
|
|
2893
|
+
if (!opened.ok || !matchesExpected(opened.identity)) throw new Error("source_changed");
|
|
2894
|
+
assertCurrentBinding();
|
|
2895
|
+
} catch (error) {
|
|
2896
|
+
try {
|
|
2897
|
+
file?.close();
|
|
2898
|
+
} catch {
|
|
2899
|
+
// Preserve the identity/open failure; closing is still attempted.
|
|
2900
|
+
}
|
|
2901
|
+
throw error;
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
let closed = false;
|
|
2905
|
+
return {
|
|
2906
|
+
readRange: (start, length) => {
|
|
2907
|
+
if (closed) throw new Error("closed");
|
|
2908
|
+
if (
|
|
2909
|
+
!Number.isSafeInteger(start) ||
|
|
2910
|
+
start < 0 ||
|
|
2911
|
+
!Number.isSafeInteger(length) ||
|
|
2912
|
+
length < 0 ||
|
|
2913
|
+
start > Number.MAX_SAFE_INTEGER - length
|
|
2914
|
+
)
|
|
2915
|
+
throw new RangeError("Invalid managed range read");
|
|
2916
|
+
if (length > 1024 * 1024) throw new RangeError("Managed read chunk exceeds the bounded maximum");
|
|
2917
|
+
if (start + length > expectedDescriptor.size) throw new Error("range_not_present");
|
|
2918
|
+
assertCurrentBinding();
|
|
2919
|
+
const before = file!.identity();
|
|
2920
|
+
if (!before.ok || !matchesExpected(before.identity)) throw new Error("source_changed");
|
|
2921
|
+
const chunk = file!.readChunk(start, length);
|
|
2922
|
+
if (!chunk.ok || !chunk.data || !chunk.identity) throw new Error(chunk.code ?? "managed_read_failed");
|
|
2923
|
+
if (!matchesExpected(chunk.identity)) throw new Error("source_changed");
|
|
2924
|
+
if (chunk.data.byteLength !== length) throw new Error("range_not_present");
|
|
2925
|
+
const after = file!.identity();
|
|
2926
|
+
if (!after.ok || !matchesExpected(after.identity)) throw new Error("source_changed");
|
|
2927
|
+
assertCurrentBinding();
|
|
2928
|
+
return chunk.data;
|
|
2929
|
+
},
|
|
2930
|
+
close: () => {
|
|
2931
|
+
if (closed) return;
|
|
2932
|
+
closed = true;
|
|
2933
|
+
let failure: unknown;
|
|
2934
|
+
try {
|
|
2935
|
+
assertCurrentBinding();
|
|
2936
|
+
} catch (error) {
|
|
2937
|
+
failure = error;
|
|
2938
|
+
}
|
|
2939
|
+
let result: RecoveryFsResult | undefined;
|
|
2940
|
+
try {
|
|
2941
|
+
result = file!.close();
|
|
2942
|
+
} catch (error) {
|
|
2943
|
+
failure ??= error;
|
|
2944
|
+
}
|
|
2945
|
+
if (failure === undefined && (!result?.ok || !matchesExpected(result.identity)))
|
|
2946
|
+
failure = new Error(result?.code ?? "source_changed");
|
|
2947
|
+
if (failure === undefined) {
|
|
2948
|
+
try {
|
|
2949
|
+
assertCurrentBinding();
|
|
2950
|
+
} catch (error) {
|
|
2951
|
+
failure = error;
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
if (failure !== undefined) throw failure;
|
|
2955
|
+
},
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2760
2959
|
/** Read an exact managed file without exposing its pathname as authority. */
|
|
2761
2960
|
readExpected(relativePath: string): ManagedFileSnapshot | null {
|
|
2762
2961
|
this.#assertBound();
|
|
@@ -2793,6 +2992,85 @@ export class ManagedSessionDescendantStore {
|
|
|
2793
2992
|
};
|
|
2794
2993
|
}
|
|
2795
2994
|
|
|
2995
|
+
/** Read one complete managed file through retained authority after descriptor-size admission. */
|
|
2996
|
+
readExpectedBounded(
|
|
2997
|
+
relativePath: string,
|
|
2998
|
+
maxBytes: number,
|
|
2999
|
+
admitSize?: (size: number, descriptor: ManagedFileIdentity) => void,
|
|
3000
|
+
): ManagedFileSnapshot | null {
|
|
3001
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new Error("invalid_capture_limit");
|
|
3002
|
+
this.#assertBound();
|
|
3003
|
+
const resolved = this.#resolve(relativePath);
|
|
3004
|
+
if (!this.#authority) {
|
|
3005
|
+
try {
|
|
3006
|
+
this.#assertPathBackedDirectoryChain(resolved);
|
|
3007
|
+
const captured = captureManagedFileNoFollowBounded(resolved, maxBytes, admitSize);
|
|
3008
|
+
this.#assertBound();
|
|
3009
|
+
return captured;
|
|
3010
|
+
} catch (error) {
|
|
3011
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
3012
|
+
throw error;
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
let descriptor: SessionStorageRangeSnapshot;
|
|
3016
|
+
try {
|
|
3017
|
+
descriptor = this.readRangeExpectedSync(this.#relative(resolved), 0, 0);
|
|
3018
|
+
} catch (error) {
|
|
3019
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
3020
|
+
throw error;
|
|
3021
|
+
}
|
|
3022
|
+
const { stat } = descriptor;
|
|
3023
|
+
if (stat.nlink !== 1n) throw new Error("source_changed");
|
|
3024
|
+
if (!Number.isSafeInteger(stat.size) || stat.size < 0) throw new Error("source_changed");
|
|
3025
|
+
if (stat.size > maxBytes) throw new Error("artifact_capacity_exceeded");
|
|
3026
|
+
const lease = this.openReadLease(this.#relative(resolved), stat);
|
|
3027
|
+
let failed = false;
|
|
3028
|
+
let failure: unknown;
|
|
3029
|
+
let snapshot: ManagedFileSnapshot | undefined;
|
|
3030
|
+
try {
|
|
3031
|
+
admitSize?.(stat.size, {
|
|
3032
|
+
dev: stat.dev,
|
|
3033
|
+
ino: stat.ino,
|
|
3034
|
+
nlink: stat.nlink,
|
|
3035
|
+
size: stat.size,
|
|
3036
|
+
mtimeNs: stat.mtimeNs,
|
|
3037
|
+
ctimeNs: stat.ctimeNs,
|
|
3038
|
+
});
|
|
3039
|
+
const bytes = Buffer.alloc(stat.size);
|
|
3040
|
+
for (let offset = 0; offset < bytes.byteLength; ) {
|
|
3041
|
+
const length = Math.min(1024 * 1024, bytes.byteLength - offset);
|
|
3042
|
+
bytes.set(lease.readRange(offset, length), offset);
|
|
3043
|
+
offset += length;
|
|
3044
|
+
}
|
|
3045
|
+
snapshot = {
|
|
3046
|
+
bytes,
|
|
3047
|
+
identity: {
|
|
3048
|
+
dev: stat.dev,
|
|
3049
|
+
ino: stat.ino,
|
|
3050
|
+
nlink: stat.nlink,
|
|
3051
|
+
size: stat.size,
|
|
3052
|
+
mtimeNs: stat.mtimeNs,
|
|
3053
|
+
ctimeNs: stat.ctimeNs,
|
|
3054
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
3055
|
+
},
|
|
3056
|
+
};
|
|
3057
|
+
} catch (error) {
|
|
3058
|
+
failed = true;
|
|
3059
|
+
failure = error;
|
|
3060
|
+
}
|
|
3061
|
+
try {
|
|
3062
|
+
lease.close();
|
|
3063
|
+
} catch (error) {
|
|
3064
|
+
if (!failed) {
|
|
3065
|
+
failed = true;
|
|
3066
|
+
failure = error;
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
if (failed) throw failure;
|
|
3070
|
+
if (!snapshot) throw new Error("Managed descendant snapshot is unavailable");
|
|
3071
|
+
return snapshot;
|
|
3072
|
+
}
|
|
3073
|
+
|
|
2796
3074
|
/** Remove an exact captured file without reopening its pathname as authority. */
|
|
2797
3075
|
removeExpected(relativePath: string, expected: ManagedFileSnapshot): void {
|
|
2798
3076
|
this.#beforeMutation();
|
|
@@ -3028,6 +3306,7 @@ export class ManagedSessionDescendantStore {
|
|
|
3028
3306
|
sourceStoreRelativePath: string;
|
|
3029
3307
|
},
|
|
3030
3308
|
): ManagedFileSnapshot {
|
|
3309
|
+
this.#assertWritable();
|
|
3031
3310
|
this.#assertBound();
|
|
3032
3311
|
const sourceResolved = this.#resolve(sourceRelativePath);
|
|
3033
3312
|
const destinationResolved = this.#resolve(destinationRelativePath);
|
|
@@ -3297,7 +3576,7 @@ function sameReplacementIdentity(
|
|
|
3297
3576
|
);
|
|
3298
3577
|
}
|
|
3299
3578
|
|
|
3300
|
-
function
|
|
3579
|
+
export function parseManagedLockRecord(bytes: Uint8Array): LockRecord | undefined {
|
|
3301
3580
|
try {
|
|
3302
3581
|
const value: unknown = JSON.parse(Buffer.from(bytes).toString("utf8"));
|
|
3303
3582
|
if (!value || typeof value !== "object") return undefined;
|
|
@@ -3320,7 +3599,7 @@ function parseLock(pathname: string): LockRecord | undefined {
|
|
|
3320
3599
|
try {
|
|
3321
3600
|
const stat = fs.lstatSync(pathname);
|
|
3322
3601
|
if (!stat.isFile() || stat.isSymbolicLink()) return undefined;
|
|
3323
|
-
return
|
|
3602
|
+
return parseManagedLockRecord(fs.readFileSync(pathname));
|
|
3324
3603
|
} catch {
|
|
3325
3604
|
return undefined;
|
|
3326
3605
|
}
|
|
@@ -3329,7 +3608,7 @@ function parseLock(pathname: string): LockRecord | undefined {
|
|
|
3329
3608
|
function captureLock(pathname: string): { record: LockRecord; snapshot: ManagedFileSnapshot } | undefined {
|
|
3330
3609
|
try {
|
|
3331
3610
|
const snapshot = captureManagedFileNoFollow(pathname);
|
|
3332
|
-
const record =
|
|
3611
|
+
const record = parseManagedLockRecord(snapshot.bytes);
|
|
3333
3612
|
return record ? { record, snapshot } : undefined;
|
|
3334
3613
|
} catch {
|
|
3335
3614
|
return undefined;
|
|
@@ -3437,17 +3716,41 @@ export function captureManagedFilePrefixNoFollow(pathname: string, maxBytes: num
|
|
|
3437
3716
|
return captureManagedFileNoFollowLimit(pathname, maxBytes);
|
|
3438
3717
|
}
|
|
3439
3718
|
|
|
3719
|
+
/** Captures a complete no-follow file only after descriptor-size admission and before allocating its bytes. */
|
|
3720
|
+
export function captureManagedFileNoFollowBounded(
|
|
3721
|
+
pathname: string,
|
|
3722
|
+
maxBytes: number,
|
|
3723
|
+
admitSize?: (size: number, descriptor: ManagedFileIdentity) => void,
|
|
3724
|
+
): ManagedFileSnapshot {
|
|
3725
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new Error("invalid_capture_limit");
|
|
3726
|
+
return captureManagedFileNoFollowLimit(pathname, maxBytes, true, admitSize);
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3440
3729
|
/** Captures header/hash/copy input from one no-follow descriptor and rechecks the pathname before use. */
|
|
3441
3730
|
export function captureManagedFileNoFollow(pathname: string): ManagedFileSnapshot {
|
|
3442
3731
|
return captureManagedFileNoFollowLimit(pathname);
|
|
3443
3732
|
}
|
|
3444
3733
|
|
|
3445
|
-
function captureManagedFileNoFollowLimit(
|
|
3734
|
+
function captureManagedFileNoFollowLimit(
|
|
3735
|
+
pathname: string,
|
|
3736
|
+
maxBytes?: number,
|
|
3737
|
+
rejectOversized = false,
|
|
3738
|
+
admitSize?: (size: number, descriptor: ManagedFileIdentity) => void,
|
|
3739
|
+
): ManagedFileSnapshot {
|
|
3446
3740
|
const fd = fs.openSync(pathname, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | (fs.constants.O_NOFOLLOW ?? 0));
|
|
3741
|
+
let failed = false;
|
|
3742
|
+
let failure: unknown;
|
|
3743
|
+
let snapshot: ManagedFileSnapshot | undefined;
|
|
3447
3744
|
try {
|
|
3448
3745
|
const before = fs.fstatSync(fd, { bigint: true });
|
|
3449
|
-
if (!before.isFile() || before.nlink
|
|
3450
|
-
|
|
3746
|
+
if (!before.isFile() || (rejectOversized ? before.nlink !== 1n : before.nlink > 1n))
|
|
3747
|
+
throw new Error("source_changed");
|
|
3748
|
+
const fileSize = Number(before.size);
|
|
3749
|
+
if (!Number.isSafeInteger(fileSize) || fileSize < 0) throw new Error("source_changed");
|
|
3750
|
+
if (rejectOversized && maxBytes !== undefined && fileSize > maxBytes)
|
|
3751
|
+
throw new Error("artifact_capacity_exceeded");
|
|
3752
|
+
admitSize?.(fileSize, identity(before));
|
|
3753
|
+
const captureSize = maxBytes === undefined ? fileSize : Math.min(fileSize, maxBytes);
|
|
3451
3754
|
const bytes = Buffer.alloc(captureSize);
|
|
3452
3755
|
let offset = 0;
|
|
3453
3756
|
while (offset < bytes.byteLength) {
|
|
@@ -3460,10 +3763,22 @@ function captureManagedFileNoFollowLimit(pathname: string, maxBytes?: number): M
|
|
|
3460
3763
|
const named = fs.lstatSync(pathname, { bigint: true });
|
|
3461
3764
|
if (!named.isFile() || named.isSymbolicLink() || !sameIdentity(identity(before), identity(named)))
|
|
3462
3765
|
throw new Error("source_changed");
|
|
3463
|
-
|
|
3464
|
-
}
|
|
3766
|
+
snapshot = { bytes, identity: identity(before, createHash("sha256").update(bytes).digest("hex")) };
|
|
3767
|
+
} catch (error) {
|
|
3768
|
+
failed = true;
|
|
3769
|
+
failure = error;
|
|
3770
|
+
}
|
|
3771
|
+
try {
|
|
3465
3772
|
fs.closeSync(fd);
|
|
3773
|
+
} catch (error) {
|
|
3774
|
+
if (!failed) {
|
|
3775
|
+
failed = true;
|
|
3776
|
+
failure = error;
|
|
3777
|
+
}
|
|
3466
3778
|
}
|
|
3779
|
+
if (failed) throw failure;
|
|
3780
|
+
if (!snapshot) throw new Error("Managed file capture is unavailable");
|
|
3781
|
+
return snapshot;
|
|
3467
3782
|
}
|
|
3468
3783
|
|
|
3469
3784
|
/** Streams a managed file once while retaining only a bounded header prefix and the full descriptor-bound digest. */
|