@gajae-code/coding-agent 0.18.6 → 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 +318 -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 +6 -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/settings.d.ts +4 -0
- 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 +8 -3
- 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/elicitation.d.ts +12 -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/lifecycle.d.ts +9 -0
- package/dist/types/sdk/broker/session-index.d.ts +12 -6
- package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
- 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/bus/telegram-daemon.d.ts +3 -2
- package/dist/types/sdk/client/client.d.ts +2 -0
- package/dist/types/sdk/host/host.d.ts +6 -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 +9 -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 +36 -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 +19 -0
- package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +11 -0
- package/dist/types/session/session-manager.d.ts +31 -5
- package/dist/types/session/session-retirement.d.ts +71 -0
- package/dist/types/session/session-storage.d.ts +62 -48
- package/dist/types/session/session-transcript-header.d.ts +2 -0
- package/dist/types/session/task-artifact-owner-codec.d.ts +109 -0
- package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
- package/dist/types/session/task-artifact-owner.d.ts +10 -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/resident-memory-bench.ts +174 -20
- package/scripts/verify-managed-empty-stop.ts +28 -0
- package/src/async/job-manager.ts +228 -122
- package/src/capability/index.ts +18 -0
- package/src/capability/mcp.ts +2 -0
- package/src/capability/skill.ts +2 -0
- package/src/capability/types.ts +6 -0
- package/src/cli/auth-broker-cli.ts +1 -1
- package/src/cli/config-cli.ts +8 -18
- 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/cli-main.ts +7 -8
- package/src/commands/deep-interview.ts +47 -8
- package/src/commands/sdk.ts +140 -29
- 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/settings.ts +44 -4
- 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 +168 -23
- package/src/discovery/mcp-json.ts +6 -0
- package/src/discovery/ssh.ts +9 -4
- 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 -8
- 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/hindsight/config.ts +9 -3
- package/src/internal-urls/docs-index.generated.ts +19 -18
- 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 +163 -7
- 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/client.ts +13 -4
- package/src/runtime-mcp/config-writer.ts +10 -6
- package/src/runtime-mcp/config.ts +1 -0
- package/src/runtime-mcp/elicitation.ts +142 -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 +83 -7
- package/src/sdk/broker/ensure.ts +2 -0
- package/src/sdk/broker/lifecycle.ts +820 -73
- package/src/sdk/broker/session-index.ts +191 -81
- package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
- package/src/sdk/bus/chat-daemon-cli.ts +38 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
- package/src/sdk/bus/index.ts +241 -46
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
- package/src/sdk/bus/telegram-daemon.ts +4 -2
- package/src/sdk/cli/session-cli.ts +17 -3
- package/src/sdk/client/client.ts +28 -8
- package/src/sdk/host/host.ts +6 -0
- package/src/sdk/host/query/handlers.ts +9 -1
- package/src/sdk/host/session-runtime.test.ts +2805 -411
- package/src/sdk/host/session-runtime.ts +428 -111
- 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/mcp/server.ts +15 -3
- 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 +94 -14
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +81 -38
- package/src/session/agent-session.ts +674 -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 +482 -61
- package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
- package/src/session/internal/task-artifact-owner-access.ts +245 -0
- package/src/session/internal/task-artifact-owner-transcript.ts +614 -0
- package/src/session/session-manager.ts +939 -133
- package/src/session/session-retirement.ts +714 -0
- package/src/session/session-storage.ts +418 -174
- package/src/session/session-transcript-header.ts +17 -0
- package/src/session/task-artifact-owner-codec.ts +641 -0
- package/src/session/task-artifact-owner-retirement.ts +477 -0
- package/src/session/task-artifact-owner.ts +289 -0
- 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
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import * as util from "node:util";
|
|
5
6
|
|
|
6
7
|
import type { RecoveryFsRoot } from "@gajae-code/natives";
|
|
7
8
|
|
|
@@ -12,6 +13,7 @@ type NativeManagedScope = Pick<
|
|
|
12
13
|
| "exactRestore"
|
|
13
14
|
| "exactUnlink"
|
|
14
15
|
| "snapshotDirectoryTree"
|
|
16
|
+
| "snapshotEmptyDirectory"
|
|
15
17
|
| "verifyOwnerOnlyPathSecurity"
|
|
16
18
|
| "verifyOwnerOnlyPathSecurityExpected"
|
|
17
19
|
>;
|
|
@@ -26,28 +28,53 @@ import {
|
|
|
26
28
|
FileSessionStorage,
|
|
27
29
|
type NativeDirectoryTreeSnapshot,
|
|
28
30
|
type SessionStorageFileIdentity,
|
|
31
|
+
type SessionStorageSnapshot,
|
|
29
32
|
type VerifiedSessionDeleteResult,
|
|
30
33
|
type VerifiedSessionDeleteTarget,
|
|
31
34
|
} from "../session-storage";
|
|
35
|
+
import { parseFirstJsonlLine } from "../session-transcript-header";
|
|
36
|
+
import {
|
|
37
|
+
parseTaskArtifactOwnerDeletionEvidence,
|
|
38
|
+
parseTaskArtifactOwnerRetirementOutcome,
|
|
39
|
+
type TaskArtifactOwnerDeletionEvidence,
|
|
40
|
+
type TaskArtifactOwnerStorageContext,
|
|
41
|
+
} from "../task-artifact-owner-codec";
|
|
42
|
+
import { verifyTaskArtifactOwnerPhysicalRetirement } from "../task-artifact-owner-retirement";
|
|
43
|
+
import {
|
|
44
|
+
continuationExtendsPrevious,
|
|
45
|
+
type ManagedGcSessionRetirementReceipt,
|
|
46
|
+
type ManagedGcSessionRetirementState,
|
|
47
|
+
type ManagedGcSessionRetirementTarget,
|
|
48
|
+
managedGcRetirementIdentityRecord,
|
|
49
|
+
managedGcRetirementIdentityValue,
|
|
50
|
+
parseManagedGcRetirementReceipt,
|
|
51
|
+
sameIdentity,
|
|
52
|
+
} from "./managed-gc-retirement-codec";
|
|
32
53
|
import {
|
|
33
54
|
acquireManagedLock,
|
|
34
55
|
assertManagedDirectoryRoot,
|
|
35
56
|
captureManagedFileNoFollow,
|
|
57
|
+
captureManagedFileNoFollowBounded,
|
|
36
58
|
captureManagedFilePrefixNoFollow,
|
|
37
59
|
copyManagedFileNoReplace,
|
|
38
60
|
ensureManagedDirectory,
|
|
39
61
|
fsyncManagedArtifactTree,
|
|
40
62
|
inspectManagedFileNoFollow,
|
|
63
|
+
isManagedLockQuarantineName,
|
|
41
64
|
MANAGED_ARTIFACT_COPY_BATCH_SIZE,
|
|
65
|
+
MANAGED_ARTIFACT_MAX_FILE_BYTES,
|
|
42
66
|
MANAGED_ARTIFACT_MAX_FILES,
|
|
43
67
|
MANAGED_ARTIFACT_MAX_TOTAL_BYTES,
|
|
68
|
+
MANAGED_SESSION_READ_RANGE_MAX_BYTES,
|
|
44
69
|
type ManagedDirectoryRoot,
|
|
45
70
|
type ManagedFileSnapshot,
|
|
46
71
|
ManagedPublishError,
|
|
47
72
|
ManagedSessionDescendantStore,
|
|
48
73
|
type ManagedSessionSecurityPolicy,
|
|
49
74
|
type ManagedStorageLock,
|
|
75
|
+
managedDirectoryRoot,
|
|
50
76
|
managedSecurityFailureClassification,
|
|
77
|
+
parseManagedLockRecord,
|
|
51
78
|
prepareManagedDirectoryRoot,
|
|
52
79
|
publishManagedFileNoReplace,
|
|
53
80
|
publishManagedTombstone,
|
|
@@ -56,6 +83,22 @@ import {
|
|
|
56
83
|
validateManagedArtifactTree,
|
|
57
84
|
validateNativeSecurityResult,
|
|
58
85
|
} from "./managed-session-storage";
|
|
86
|
+
import {
|
|
87
|
+
hasUnsupportedLegacyOwnerTarget as hasUnsupportedLegacyOwnerTargetInternal,
|
|
88
|
+
type ManagedGcOwnerCleanupAuthority,
|
|
89
|
+
type ManagedGcOwnerDeleteFields,
|
|
90
|
+
type ManagedGcOwnerProgress,
|
|
91
|
+
managedGcOwnerDeleteFields as managedGcOwnerDeleteFieldsInternal,
|
|
92
|
+
managedGcOwnerProgressBeforeDelete as managedGcOwnerProgressBeforeDeleteInternal,
|
|
93
|
+
persistManagedGcOwnerTranscriptDeletion,
|
|
94
|
+
persistManagedGcStorageOwnerDisposition as persistManagedGcStorageOwnerDispositionInternal,
|
|
95
|
+
prepareManagedGcOwnerTargets as prepareManagedGcOwnerTargetsInternal,
|
|
96
|
+
publishManagedGcArtifactsRemoved as publishManagedGcArtifactsRemovedInternal,
|
|
97
|
+
retireManagedGcOwnerAfterArtifacts as retireManagedGcOwnerAfterArtifactsInternal,
|
|
98
|
+
taskArtifactOwnerTranscriptResult as taskArtifactOwnerTranscriptResultInternal,
|
|
99
|
+
} from "./managed-task-owner-cleanup";
|
|
100
|
+
import { captureTaskArtifactOwnerDeletionEvidence } from "./task-artifact-owner-access";
|
|
101
|
+
import { taskArtifactOwnerLocatorFromTranscriptBytes } from "./task-artifact-owner-transcript";
|
|
59
102
|
|
|
60
103
|
export const MANAGED_SESSION_LAYOUT_VERSION = 2 as const;
|
|
61
104
|
export const MANAGED_SESSION_IDENTITY_VERSION = 1 as const;
|
|
@@ -74,6 +117,66 @@ export interface ManagedScope {
|
|
|
74
117
|
platform: "posix" | "win32";
|
|
75
118
|
}
|
|
76
119
|
|
|
120
|
+
export interface ManagedGcProtocolPathIdentity {
|
|
121
|
+
readonly path: string;
|
|
122
|
+
readonly dev: string;
|
|
123
|
+
readonly ino: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ManagedGcProtocolFileSnapshot {
|
|
127
|
+
readonly name: string;
|
|
128
|
+
readonly dev: string;
|
|
129
|
+
readonly ino: string;
|
|
130
|
+
readonly nlink: string;
|
|
131
|
+
readonly size: number;
|
|
132
|
+
readonly mtimeNs: string;
|
|
133
|
+
readonly ctimeNs: string;
|
|
134
|
+
readonly mode: number;
|
|
135
|
+
readonly sha256: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export type ManagedGcProtocolRole = "locks" | "receipts" | "tombstones";
|
|
139
|
+
|
|
140
|
+
export interface ManagedGcProtocolDirectorySnapshot {
|
|
141
|
+
readonly role: ManagedGcProtocolRole;
|
|
142
|
+
readonly path: string;
|
|
143
|
+
readonly dev: string;
|
|
144
|
+
readonly ino: string;
|
|
145
|
+
readonly mtimeNs: string;
|
|
146
|
+
readonly ctimeNs: string;
|
|
147
|
+
readonly mode: number;
|
|
148
|
+
readonly files: readonly ManagedGcProtocolFileSnapshot[];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Read-only protocol data; never a deletion capability. */
|
|
152
|
+
export interface ManagedGcProtocolScopeSnapshot {
|
|
153
|
+
readonly configuredRoot: ManagedGcProtocolPathIdentity;
|
|
154
|
+
readonly profile: ManagedGcProtocolPathIdentity;
|
|
155
|
+
readonly sessionsRoot: ManagedGcProtocolPathIdentity;
|
|
156
|
+
readonly scope: ManagedGcProtocolPathIdentity;
|
|
157
|
+
readonly binding: ManagedGcProtocolFileSnapshot;
|
|
158
|
+
readonly protocol: ManagedGcProtocolPathIdentity;
|
|
159
|
+
readonly protocolMtimeNs: string;
|
|
160
|
+
readonly protocolCtimeNs: string;
|
|
161
|
+
readonly protocolMode: number;
|
|
162
|
+
readonly directories: readonly ManagedGcProtocolDirectorySnapshot[];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface ManagedGcProtocolScopeInput {
|
|
166
|
+
readonly scopePath: string;
|
|
167
|
+
readonly scopeIdentity: ManagedGcProtocolPathIdentity;
|
|
168
|
+
readonly bindingIdentity: ManagedGcProtocolFileSnapshot;
|
|
169
|
+
readonly protocolIdentity: ManagedGcProtocolScopeSnapshot["protocol"] & {
|
|
170
|
+
readonly mtimeNs: string;
|
|
171
|
+
readonly ctimeNs: string;
|
|
172
|
+
readonly mode: number;
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type ManagedGcProtocolScopeInspector = (
|
|
177
|
+
inputs: readonly ManagedGcProtocolScopeInput[],
|
|
178
|
+
) => Promise<readonly ManagedGcProtocolScopeSnapshot[]>;
|
|
179
|
+
|
|
77
180
|
/**
|
|
78
181
|
* Opaque managed writer authority captured by a trusted destination. The open
|
|
79
182
|
* transaction must use this authority rather than reacquiring its root from a
|
|
@@ -86,9 +189,23 @@ export interface ManagedCandidateWriteAuthority {
|
|
|
86
189
|
}
|
|
87
190
|
|
|
88
191
|
const managedRoots = new WeakMap<ManagedScope, ReturnType<typeof prepareManagedDirectoryRoot>>();
|
|
192
|
+
const managedScopeConfigurations = new WeakMap<
|
|
193
|
+
ManagedScope,
|
|
194
|
+
Readonly<
|
|
195
|
+
Pick<ManagedScope, "agentDir" | "sessionsRoot" | "canonicalCwd" | "directoryName" | "directoryPath" | "platform">
|
|
196
|
+
>
|
|
197
|
+
>();
|
|
89
198
|
const managedDirectoryIdentities = new WeakMap<ManagedScope, { dev: bigint; ino: bigint }>();
|
|
90
199
|
const managedDirectoryAuthorities = new WeakMap<ManagedScope, RecoveryFsRoot | undefined>();
|
|
91
200
|
const boundManagedWriteAuthorities = new WeakMap<ManagedScope, ManagedCandidateWriteAuthority>();
|
|
201
|
+
const readManagedGcScopeIdentities = new WeakMap<
|
|
202
|
+
ManagedScope,
|
|
203
|
+
{
|
|
204
|
+
readonly configuredRoot: { readonly path: string; readonly dev: bigint; readonly ino: bigint };
|
|
205
|
+
readonly profile: { readonly path: string; readonly dev: bigint; readonly ino: bigint };
|
|
206
|
+
readonly sessions: { readonly path: string; readonly dev: bigint; readonly ino: bigint };
|
|
207
|
+
}
|
|
208
|
+
>();
|
|
92
209
|
|
|
93
210
|
function bindManagedWriteAuthority(scope: ManagedScope, authority: ManagedCandidateWriteAuthority): void {
|
|
94
211
|
if (
|
|
@@ -179,6 +296,7 @@ export type ManagedScopeErrorCode =
|
|
|
179
296
|
| "binding_conflict"
|
|
180
297
|
| "binding_invalid"
|
|
181
298
|
| "migration_busy"
|
|
299
|
+
| "managed_gc_journal_capacity_exceeded"
|
|
182
300
|
| "atomic_unavailable"
|
|
183
301
|
| "invalid_request"
|
|
184
302
|
| "durability_failed"
|
|
@@ -227,9 +345,14 @@ const managedScopeFailureCodes = new Set<ManagedScopeErrorCode>([
|
|
|
227
345
|
"durability_failed",
|
|
228
346
|
"durability_not_provable",
|
|
229
347
|
"migration_busy",
|
|
348
|
+
"managed_gc_journal_capacity_exceeded",
|
|
230
349
|
"capacity_exceeded",
|
|
231
350
|
]);
|
|
232
351
|
|
|
352
|
+
function isManagedGcJournalCapacityError(error: unknown): boolean {
|
|
353
|
+
return error instanceof Error && error.message === "managed_gc_journal_capacity_exceeded";
|
|
354
|
+
}
|
|
355
|
+
|
|
233
356
|
/**
|
|
234
357
|
* A scope whose directory outgrew a scan budget. The binding is untouched and
|
|
235
358
|
* canonical; only the surrounding entry count is over budget, so this must not
|
|
@@ -315,6 +438,7 @@ export type ManagedOpenFailure =
|
|
|
315
438
|
| "source_changed"
|
|
316
439
|
| "unsafe_artifacts"
|
|
317
440
|
| "artifact_capacity_exceeded"
|
|
441
|
+
| "managed_gc_journal_capacity_exceeded"
|
|
318
442
|
| "durability_failed"
|
|
319
443
|
| "atomic_unavailable"
|
|
320
444
|
| "invalid_request"
|
|
@@ -355,6 +479,8 @@ async function deleteSessionVerifiedWithFence(
|
|
|
355
479
|
stage: ManagedVerifiedDeleteTestEvent["stage"],
|
|
356
480
|
lock: ManagedStorageLock,
|
|
357
481
|
target: VerifiedSessionDeleteTarget,
|
|
482
|
+
scope: ManagedScope,
|
|
483
|
+
retiredTarget: RetiredTarget,
|
|
358
484
|
verifyAuthority?: () => void,
|
|
359
485
|
): Promise<VerifiedSessionDeleteResult> {
|
|
360
486
|
lock.assertOwned();
|
|
@@ -363,7 +489,14 @@ async function deleteSessionVerifiedWithFence(
|
|
|
363
489
|
if (hook) await hook({ flow, stage });
|
|
364
490
|
lock.assertOwned();
|
|
365
491
|
verifyAuthority?.();
|
|
366
|
-
|
|
492
|
+
const ownerFields = await managedGcOwnerDeleteFields(scope, retiredTarget, lock);
|
|
493
|
+
assertOwnerTranscriptLocator(target, ownerFields.taskArtifactOwnerDeletionEvidence);
|
|
494
|
+
lock.assertOwned();
|
|
495
|
+
verifyAuthority?.();
|
|
496
|
+
return new FileSessionStorage().deleteSessionVerified(
|
|
497
|
+
{ ...target, ...ownerFields },
|
|
498
|
+
managedGcProtocolInspectorForLock(scope, lock),
|
|
499
|
+
);
|
|
367
500
|
}
|
|
368
501
|
|
|
369
502
|
type NativeIdentity =
|
|
@@ -615,6 +748,17 @@ function resolveManagedScopeInternal(
|
|
|
615
748
|
directoryPath: path.join(sessionsRoot, `v2-${digest}`),
|
|
616
749
|
platform: identity.platform,
|
|
617
750
|
};
|
|
751
|
+
managedScopeConfigurations.set(
|
|
752
|
+
scope,
|
|
753
|
+
Object.freeze({
|
|
754
|
+
agentDir: scope.agentDir,
|
|
755
|
+
sessionsRoot: scope.sessionsRoot,
|
|
756
|
+
canonicalCwd: scope.canonicalCwd,
|
|
757
|
+
directoryName: scope.directoryName,
|
|
758
|
+
directoryPath: scope.directoryPath,
|
|
759
|
+
platform: scope.platform,
|
|
760
|
+
}),
|
|
761
|
+
);
|
|
618
762
|
try {
|
|
619
763
|
const root = fs.lstatSync(sessionsRoot);
|
|
620
764
|
if (!root.isDirectory() || root.isSymbolicLink()) {
|
|
@@ -649,7 +793,10 @@ function resolveManagedScopeInternal(
|
|
|
649
793
|
"verify",
|
|
650
794
|
"directory",
|
|
651
795
|
);
|
|
652
|
-
if (
|
|
796
|
+
if (
|
|
797
|
+
!security.ok &&
|
|
798
|
+
(!allowRepairableAclFailure || (security.code !== "acl_verify_failed" && security.code !== "owner_mismatch"))
|
|
799
|
+
) {
|
|
653
800
|
return {
|
|
654
801
|
kind: "error",
|
|
655
802
|
code: "binding_invalid",
|
|
@@ -677,6 +824,71 @@ export function resolveManagedScopeForWrite(input: ManagedScopeInput): ManagedSc
|
|
|
677
824
|
return resolveManagedScopeInternal(input, true);
|
|
678
825
|
}
|
|
679
826
|
|
|
827
|
+
/** Resolve only an existing, authenticated v2 scope; this path never initializes or repairs storage. */
|
|
828
|
+
export function resolveManagedGcScopeForRead(input: ManagedScopeInput): ManagedScopeResolution {
|
|
829
|
+
const resolved = resolveManagedScope(input);
|
|
830
|
+
if (resolved.kind === "error") return resolved;
|
|
831
|
+
const scope = resolved.scope;
|
|
832
|
+
try {
|
|
833
|
+
const rootPath = configuredRootPath(scope);
|
|
834
|
+
if (
|
|
835
|
+
path.resolve(scope.agentDir) !== scope.agentDir ||
|
|
836
|
+
path.resolve(scope.sessionsRoot) !== scope.sessionsRoot ||
|
|
837
|
+
path.dirname(scope.directoryPath) !== scope.sessionsRoot ||
|
|
838
|
+
scope.directoryName !== `v2-${scopeDigest(scope.platform, scope.canonicalCwd)}`
|
|
839
|
+
)
|
|
840
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
841
|
+
const captureDirectory = (pathname: string) => {
|
|
842
|
+
const stat = fs.lstatSync(pathname, { bigint: true });
|
|
843
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("managed_gc_scope_authority_mismatch");
|
|
844
|
+
const security = validateNativeSecurityResult(
|
|
845
|
+
verifyExistingManagedScopeDirectory(pathname),
|
|
846
|
+
"verify",
|
|
847
|
+
"directory",
|
|
848
|
+
);
|
|
849
|
+
if (!security.ok) throw new Error("managed_gc_scope_authority_mismatch");
|
|
850
|
+
return { path: pathname, dev: stat.dev, ino: stat.ino };
|
|
851
|
+
};
|
|
852
|
+
const configuredRoot = captureDirectory(rootPath);
|
|
853
|
+
const profile = captureDirectory(scope.agentDir);
|
|
854
|
+
const sessions = captureDirectory(scope.sessionsRoot);
|
|
855
|
+
const scopeDirectory = captureDirectory(scope.directoryPath);
|
|
856
|
+
const rootAuthority = managedDirectoryRoot(rootPath);
|
|
857
|
+
if (
|
|
858
|
+
rootAuthority.canonicalPath !== rootPath ||
|
|
859
|
+
rootAuthority.dev !== BigInt.asUintN(64, configuredRoot.dev) ||
|
|
860
|
+
rootAuthority.ino !== BigInt.asUintN(64, configuredRoot.ino)
|
|
861
|
+
)
|
|
862
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
863
|
+
const bindingPath = path.join(scope.directoryPath, MANAGED_SESSION_BINDING_FILE);
|
|
864
|
+
const binding = captureManagedFileNoFollow(bindingPath);
|
|
865
|
+
if (validateBindingRaw(scope, binding.bytes.toString("utf8")))
|
|
866
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
867
|
+
for (const expected of [configuredRoot, profile, sessions, scopeDirectory]) {
|
|
868
|
+
const current = fs.lstatSync(expected.path, { bigint: true });
|
|
869
|
+
if (
|
|
870
|
+
!current.isDirectory() ||
|
|
871
|
+
current.isSymbolicLink() ||
|
|
872
|
+
current.dev !== expected.dev ||
|
|
873
|
+
current.ino !== expected.ino
|
|
874
|
+
)
|
|
875
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
876
|
+
}
|
|
877
|
+
managedRoots.set(scope, rootAuthority);
|
|
878
|
+
managedDirectoryIdentities.set(scope, { dev: scopeDirectory.dev, ino: scopeDirectory.ino });
|
|
879
|
+
managedDirectoryAuthorities.set(scope, undefined);
|
|
880
|
+
readManagedGcScopeIdentities.set(scope, { configuredRoot, profile, sessions });
|
|
881
|
+
return resolved;
|
|
882
|
+
} catch (error) {
|
|
883
|
+
return {
|
|
884
|
+
kind: "error",
|
|
885
|
+
code: "binding_invalid",
|
|
886
|
+
message: "The existing managed GC read authority could not be verified.",
|
|
887
|
+
cause: { classification: managedScopeResidualClassification(error) ?? "managed_gc_scope_authority_mismatch" },
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
680
892
|
function legacyDirectoryNames(
|
|
681
893
|
platform: ManagedScope["platform"],
|
|
682
894
|
canonicalCwd: string,
|
|
@@ -1015,6 +1227,17 @@ export async function ensureManagedScope(
|
|
|
1015
1227
|
const preparedDirectory = fs.lstatSync(scope.directoryPath, { bigint: true });
|
|
1016
1228
|
if (!preparedDirectory.isDirectory() || preparedDirectory.isSymbolicLink()) throw new Error("reparse_point");
|
|
1017
1229
|
managedDirectoryIdentities.set(scope, { dev: preparedDirectory.dev, ino: preparedDirectory.ino });
|
|
1230
|
+
if (!managedDirectoryAuthorities.has(scope)) {
|
|
1231
|
+
const authority = boundManagedWriteAuthorities.get(scope);
|
|
1232
|
+
const retainedAuthority =
|
|
1233
|
+
authority?.retainedAuthority &&
|
|
1234
|
+
authority.retainedDirectory !== undefined &&
|
|
1235
|
+
path.resolve(authority.retainedDirectory) === path.resolve(scope.directoryPath)
|
|
1236
|
+
? authority.retainedAuthority
|
|
1237
|
+
: retainManagedDirectoryAuthority(root, scope.directoryPath, managedDirectoryIdentityForScope(scope));
|
|
1238
|
+
assertRetainedManagedDirectoryIdentity(scope);
|
|
1239
|
+
managedDirectoryAuthorities.set(scope, retainedAuthority);
|
|
1240
|
+
}
|
|
1018
1241
|
return { kind: "resolved", scope };
|
|
1019
1242
|
} catch (error) {
|
|
1020
1243
|
const publication = error instanceof ManagedPublishError ? error : undefined;
|
|
@@ -1815,6 +2038,7 @@ function expectedFailure(error: unknown): ManagedOpenFailure {
|
|
|
1815
2038
|
message === "source_changed" ||
|
|
1816
2039
|
message === "unsafe_artifacts" ||
|
|
1817
2040
|
message === "artifact_capacity_exceeded" ||
|
|
2041
|
+
message === "managed_gc_journal_capacity_exceeded" ||
|
|
1818
2042
|
message === "durability_failed" ||
|
|
1819
2043
|
message === "atomic_unavailable" ||
|
|
1820
2044
|
message === "invalid_request" ||
|
|
@@ -2076,235 +2300,2507 @@ function preparedReceiptMatches(
|
|
|
2076
2300
|
}
|
|
2077
2301
|
}
|
|
2078
2302
|
|
|
2079
|
-
type RetiredTarget = ManagedCandidate
|
|
2303
|
+
type RetiredTarget = ManagedCandidate & {
|
|
2304
|
+
readonly taskArtifactOwnerDeletionEvidence?: TaskArtifactOwnerDeletionEvidence;
|
|
2305
|
+
};
|
|
2080
2306
|
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2307
|
+
const MANAGED_GC_RETIREMENT_PREFIX = "gc-retirement-";
|
|
2308
|
+
const MANAGED_GC_RETIREMENT_RECEIPTS = `${MANAGED_INTERNAL_DIRECTORY}/${MANAGED_RECEIPTS_DIRECTORY}`;
|
|
2309
|
+
const MANAGED_GC_RETIREMENT_LOCKS = `${MANAGED_INTERNAL_DIRECTORY}/${MANAGED_LOCKS_DIRECTORY}`;
|
|
2310
|
+
// History capacity is shared by every transcript in one scope, so publishers must share one lease.
|
|
2311
|
+
const MANAGED_GC_RETIREMENT_SCOPE_LOCK_NAME = `${MANAGED_GC_RETIREMENT_PREFIX}${createHash("sha256")
|
|
2312
|
+
.update("managed-gc-retirement-scope-lock-v1", "utf8")
|
|
2313
|
+
.digest("hex")}`;
|
|
2314
|
+
interface ManagedGcTrustedScope {
|
|
2315
|
+
readonly root: ManagedDirectoryRoot;
|
|
2316
|
+
readonly retainedAuthority: RecoveryFsRoot | undefined;
|
|
2317
|
+
readonly identity: { readonly dev: bigint; readonly ino: bigint };
|
|
2318
|
+
readonly policy: ManagedSessionSecurityPolicy;
|
|
2319
|
+
readonly ownerContext: TaskArtifactOwnerStorageContext;
|
|
2320
|
+
}
|
|
2321
|
+
function managedGcTrustedScope(scope: ManagedScope): ManagedGcTrustedScope {
|
|
2322
|
+
const root = managedRoots.get(scope);
|
|
2323
|
+
const identity = managedDirectoryIdentities.get(scope);
|
|
2324
|
+
const configuration = managedScopeConfigurations.get(scope);
|
|
2325
|
+
if (!root || !identity || !configuration || !managedDirectoryAuthorities.has(scope))
|
|
2326
|
+
throw new Error("managed_gc_scope_authority_unavailable");
|
|
2327
|
+
if (
|
|
2328
|
+
scope.agentDir !== configuration.agentDir ||
|
|
2329
|
+
scope.sessionsRoot !== configuration.sessionsRoot ||
|
|
2330
|
+
scope.canonicalCwd !== configuration.canonicalCwd ||
|
|
2331
|
+
scope.directoryName !== configuration.directoryName ||
|
|
2332
|
+
scope.directoryPath !== configuration.directoryPath ||
|
|
2333
|
+
scope.platform !== configuration.platform ||
|
|
2334
|
+
scope.apiVersion !== 1 ||
|
|
2335
|
+
scope.layoutVersion !== MANAGED_SESSION_LAYOUT_VERSION ||
|
|
2336
|
+
scope.identityVersion !== MANAGED_SESSION_IDENTITY_VERSION
|
|
2337
|
+
)
|
|
2338
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
2339
|
+
const policy: ManagedSessionSecurityPolicy =
|
|
2340
|
+
scope.platform === "win32" ? "windows-existing-verify-first" : "default";
|
|
2341
|
+
assertManagedDirectoryRoot(root);
|
|
2342
|
+
managedDirectoryIdentityForScope(scope);
|
|
2343
|
+
assertRetainedManagedDirectoryIdentity(scope);
|
|
2344
|
+
const readIdentities = readManagedGcScopeIdentities.get(scope);
|
|
2345
|
+
if (readIdentities) {
|
|
2346
|
+
for (const expected of [readIdentities.configuredRoot, readIdentities.profile, readIdentities.sessions]) {
|
|
2347
|
+
const current = fs.lstatSync(expected.path, { bigint: true });
|
|
2100
2348
|
if (
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
(item.provenance !== undefined && item.provenance !== "v2" && item.provenance !== "legacy") ||
|
|
2106
|
-
typeof fields.canonicalPath !== "string" ||
|
|
2107
|
-
typeof fields.dev !== "string" ||
|
|
2108
|
-
typeof fields.ino !== "string" ||
|
|
2109
|
-
typeof fields.size !== "number" ||
|
|
2110
|
-
typeof fields.mtimeMs !== "number" ||
|
|
2111
|
-
typeof fields.mtimeNs !== "string" ||
|
|
2112
|
-
typeof fields.sha256 !== "string"
|
|
2349
|
+
!current.isDirectory() ||
|
|
2350
|
+
current.isSymbolicLink() ||
|
|
2351
|
+
current.dev !== expected.dev ||
|
|
2352
|
+
current.ino !== expected.ino
|
|
2113
2353
|
)
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
identity: {
|
|
2128
|
-
canonicalPath: fields.canonicalPath,
|
|
2129
|
-
dev: BigInt(fields.dev),
|
|
2130
|
-
ino: BigInt(fields.ino),
|
|
2131
|
-
size: fields.size,
|
|
2132
|
-
mtimeMs: fields.mtimeMs,
|
|
2133
|
-
mtimeNs: BigInt(fields.mtimeNs),
|
|
2134
|
-
sha256: fields.sha256,
|
|
2135
|
-
sessionId: item.sessionId,
|
|
2136
|
-
},
|
|
2137
|
-
});
|
|
2354
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
2355
|
+
}
|
|
2356
|
+
for (const pathname of [
|
|
2357
|
+
readIdentities.configuredRoot.path,
|
|
2358
|
+
readIdentities.profile.path,
|
|
2359
|
+
readIdentities.sessions.path,
|
|
2360
|
+
]) {
|
|
2361
|
+
const verified = validateNativeSecurityResult(
|
|
2362
|
+
verifyExistingManagedScopeDirectory(pathname),
|
|
2363
|
+
"verify",
|
|
2364
|
+
"directory",
|
|
2365
|
+
);
|
|
2366
|
+
if (!verified.ok) throw new Error("managed_gc_scope_authority_mismatch");
|
|
2138
2367
|
}
|
|
2139
|
-
return targets;
|
|
2140
|
-
} catch {
|
|
2141
|
-
return undefined;
|
|
2142
2368
|
}
|
|
2369
|
+
return {
|
|
2370
|
+
root,
|
|
2371
|
+
retainedAuthority: managedDirectoryAuthorities.get(scope),
|
|
2372
|
+
identity,
|
|
2373
|
+
policy,
|
|
2374
|
+
ownerContext: Object.freeze({
|
|
2375
|
+
rootAuthority: root,
|
|
2376
|
+
sessionsRoot: scope.sessionsRoot,
|
|
2377
|
+
securityPolicy: policy,
|
|
2378
|
+
profileAgentDir: scope.agentDir,
|
|
2379
|
+
}),
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
/** Build owner-storage authority only from a previously prepared trusted managed scope. */
|
|
2383
|
+
export function taskArtifactOwnerStorageContextForScope(scope: ManagedScope): TaskArtifactOwnerStorageContext {
|
|
2384
|
+
return managedGcTrustedScope(scope).ownerContext;
|
|
2143
2385
|
}
|
|
2144
2386
|
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2387
|
+
function managedGcOwnerCleanupAuthority(scope: ManagedScope, lock: ManagedStorageLock): ManagedGcOwnerCleanupAuthority {
|
|
2388
|
+
return {
|
|
2389
|
+
agentDir: scope.agentDir,
|
|
2390
|
+
sessionsRoot: scope.sessionsRoot,
|
|
2391
|
+
directoryPath: scope.directoryPath,
|
|
2392
|
+
storageContext: taskArtifactOwnerStorageContextForScope(scope),
|
|
2393
|
+
inspectProtocol: managedGcProtocolInspectorForLock(scope, lock),
|
|
2394
|
+
assertOwned: () => lock.assertOwned(),
|
|
2395
|
+
bindTarget: transcriptPath => bindManagedGcSessionRetirementTarget(scope, transcriptPath),
|
|
2396
|
+
readReceipt: async transcriptPath => {
|
|
2397
|
+
lock.assertOwned();
|
|
2398
|
+
const receipt = await readManagedGcSessionRetirementReceipt(scope, transcriptPath);
|
|
2399
|
+
lock.assertOwned();
|
|
2400
|
+
return receipt;
|
|
2401
|
+
},
|
|
2402
|
+
publishReceipt: async receipt => {
|
|
2403
|
+
lock.assertOwned();
|
|
2404
|
+
const published = await publishManagedGcSessionRetirementReceipt(scope, receipt);
|
|
2405
|
+
lock.assertOwned();
|
|
2406
|
+
return published;
|
|
2407
|
+
},
|
|
2408
|
+
findCompletedRetirement: async (evidence, exceptTranscriptPath) => {
|
|
2409
|
+
lock.assertOwned();
|
|
2410
|
+
const receipts = await discoverManagedGcSessionRetirementReceipts({
|
|
2411
|
+
agentDir: scope.agentDir,
|
|
2412
|
+
sessionsRoot: scope.sessionsRoot,
|
|
2413
|
+
});
|
|
2414
|
+
lock.assertOwned();
|
|
2415
|
+
const match = receipts.find(
|
|
2416
|
+
({ scope: source, receipt }) =>
|
|
2417
|
+
source.agentDir === scope.agentDir &&
|
|
2418
|
+
source.sessionsRoot === scope.sessionsRoot &&
|
|
2419
|
+
source.directoryPath !== scope.directoryPath &&
|
|
2420
|
+
receipt.transcriptPath !== exceptTranscriptPath &&
|
|
2421
|
+
receipt.state === "owner_retired" &&
|
|
2422
|
+
receipt.taskArtifactOwnerRetirementOutcome?.kind === "completed" &&
|
|
2423
|
+
deepSame(receipt.taskArtifactOwnerDeletionEvidence, evidence),
|
|
2424
|
+
);
|
|
2425
|
+
return match
|
|
2426
|
+
? {
|
|
2427
|
+
scope: {
|
|
2428
|
+
agentDir: match.scope.agentDir,
|
|
2429
|
+
sessionsRoot: match.scope.sessionsRoot,
|
|
2430
|
+
directoryPath: match.scope.directoryPath,
|
|
2431
|
+
},
|
|
2432
|
+
receipt: match.receipt,
|
|
2433
|
+
}
|
|
2434
|
+
: undefined;
|
|
2435
|
+
},
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2156
2438
|
|
|
2157
|
-
function
|
|
2158
|
-
|
|
2159
|
-
if (!fs.existsSync(pathname)) return undefined;
|
|
2160
|
-
if (!isQuarantinePath(receipt.target, pathname) || !receipt.expectedArtifactsIdentity)
|
|
2161
|
-
throw new Error("durability_failed");
|
|
2162
|
-
const identity = artifactIdentityAt(pathname);
|
|
2163
|
-
if (
|
|
2164
|
-
!identity ||
|
|
2165
|
-
identity.dev !== receipt.expectedArtifactsIdentity.dev ||
|
|
2166
|
-
identity.ino !== receipt.expectedArtifactsIdentity.ino
|
|
2167
|
-
)
|
|
2168
|
-
throw new Error("durability_failed");
|
|
2169
|
-
const tree = snapshotArtifactTree(pathname);
|
|
2170
|
-
if (!artifactTreePayloadAbsent(tree)) throw new Error("durability_failed");
|
|
2171
|
-
return { path: pathname, identity, tree };
|
|
2439
|
+
function hasUnsupportedLegacyOwnerTarget(scope: ManagedScope, target: RetiredTarget): boolean {
|
|
2440
|
+
return hasUnsupportedLegacyOwnerTargetInternal(scope.directoryPath, target);
|
|
2172
2441
|
}
|
|
2173
2442
|
|
|
2174
|
-
function
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
const tree = artifactTreeSnapshot(retained.tree);
|
|
2181
|
-
const expectedRetainedPath =
|
|
2182
|
-
typeof record.detachedArtifactsPath === "string"
|
|
2183
|
-
? record.detachedArtifactsPath
|
|
2184
|
-
: typeof record.plannedArtifactsPath === "string"
|
|
2185
|
-
? deterministicRemovalRoot(record.plannedArtifactsPath)
|
|
2186
|
-
: undefined;
|
|
2187
|
-
if (
|
|
2188
|
-
!expectedRetainedPath ||
|
|
2189
|
-
retained.path !== expectedRetainedPath ||
|
|
2190
|
-
record.artifactsPayloadDurable !== true ||
|
|
2191
|
-
!identity ||
|
|
2192
|
-
typeof identity !== "object" ||
|
|
2193
|
-
Array.isArray(identity) ||
|
|
2194
|
-
!tree ||
|
|
2195
|
-
!artifactTreePayloadAbsent(tree)
|
|
2196
|
-
)
|
|
2197
|
-
throw new Error("durability_failed");
|
|
2198
|
-
const artifactIdentity = identity as Record<string, unknown>;
|
|
2199
|
-
if (
|
|
2200
|
-
typeof artifactIdentity.dev !== "string" ||
|
|
2201
|
-
typeof artifactIdentity.ino !== "string" ||
|
|
2202
|
-
typeof artifactIdentity.size !== "number" ||
|
|
2203
|
-
typeof artifactIdentity.mtimeNs !== "string" ||
|
|
2204
|
-
typeof artifactIdentity.sha256 !== "string"
|
|
2205
|
-
)
|
|
2206
|
-
throw new Error("durability_failed");
|
|
2207
|
-
if (!fs.existsSync(retained.path)) return true;
|
|
2208
|
-
const observed = artifactIdentityAt(retained.path);
|
|
2209
|
-
if (
|
|
2210
|
-
!observed ||
|
|
2211
|
-
observed.dev !== BigInt(artifactIdentity.dev) ||
|
|
2212
|
-
observed.ino !== BigInt(artifactIdentity.ino) ||
|
|
2213
|
-
!artifactTreePayloadAbsent(snapshotArtifactTree(retained.path))
|
|
2214
|
-
)
|
|
2215
|
-
throw new Error("durability_failed");
|
|
2216
|
-
return true;
|
|
2443
|
+
async function prepareManagedGcOwnerTargets(
|
|
2444
|
+
scope: ManagedScope,
|
|
2445
|
+
targets: readonly RetiredTarget[],
|
|
2446
|
+
lock: ManagedStorageLock,
|
|
2447
|
+
): Promise<RetiredTarget[]> {
|
|
2448
|
+
return prepareManagedGcOwnerTargetsInternal(managedGcOwnerCleanupAuthority(scope, lock), targets);
|
|
2217
2449
|
}
|
|
2218
2450
|
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
target: RetiredTarget
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
detachedArtifactsPath?: string;
|
|
2227
|
-
detachedTranscriptPath?: string;
|
|
2228
|
-
transcriptPayloadDurable?: true;
|
|
2229
|
-
retainedArtifactsSuccessorPath?: string;
|
|
2230
|
-
retainedArtifactsPlaceholderPath?: string;
|
|
2231
|
-
retainedArtifactsUnknownPath?: string;
|
|
2232
|
-
retainedTranscriptSuccessorPath?: string;
|
|
2233
|
-
retainedTranscriptPlaceholderPath?: string;
|
|
2234
|
-
retainedTranscriptUnknownPath?: string;
|
|
2451
|
+
async function managedGcOwnerDeleteFields(
|
|
2452
|
+
scope: ManagedScope,
|
|
2453
|
+
target: RetiredTarget,
|
|
2454
|
+
lock: ManagedStorageLock,
|
|
2455
|
+
): Promise<ManagedGcOwnerDeleteFields> {
|
|
2456
|
+
return managedGcOwnerDeleteFieldsInternal(managedGcOwnerCleanupAuthority(scope, lock), target);
|
|
2457
|
+
}
|
|
2235
2458
|
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2459
|
+
async function publishManagedGcArtifactsRemoved(
|
|
2460
|
+
scope: ManagedScope,
|
|
2461
|
+
target: RetiredTarget,
|
|
2462
|
+
lock: ManagedStorageLock,
|
|
2463
|
+
): Promise<ManagedGcSessionRetirementReceipt | undefined> {
|
|
2464
|
+
return publishManagedGcArtifactsRemovedInternal(managedGcOwnerCleanupAuthority(scope, lock), target);
|
|
2465
|
+
}
|
|
2239
2466
|
|
|
2240
|
-
function
|
|
2241
|
-
|
|
2467
|
+
async function retireManagedGcOwnerAfterArtifacts(
|
|
2468
|
+
scope: ManagedScope,
|
|
2242
2469
|
target: RetiredTarget,
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
):
|
|
2246
|
-
return
|
|
2247
|
-
|
|
2248
|
-
|
|
2470
|
+
tombstoneTargets: readonly RetiredTarget[],
|
|
2471
|
+
lock: ManagedStorageLock,
|
|
2472
|
+
): Promise<ManagedGcOwnerProgress> {
|
|
2473
|
+
return retireManagedGcOwnerAfterArtifactsInternal(
|
|
2474
|
+
managedGcOwnerCleanupAuthority(scope, lock),
|
|
2475
|
+
target,
|
|
2476
|
+
tombstoneTargets,
|
|
2249
2477
|
);
|
|
2250
2478
|
}
|
|
2251
2479
|
|
|
2252
|
-
function
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
identity: receipt.target.identity,
|
|
2264
|
-
},
|
|
2265
|
-
...(receipt.expectedArtifactsIdentity ? { expectedArtifactsIdentity: receipt.expectedArtifactsIdentity } : {}),
|
|
2266
|
-
...(receipt.expectedArtifactsTree ? { expectedArtifactsTree: receipt.expectedArtifactsTree } : {}),
|
|
2267
|
-
...(receipt.artifactsPayloadDurable === true ? { artifactsPayloadDurable: true } : {}),
|
|
2268
|
-
...(receipt.artifactsRemovedAttempt !== undefined
|
|
2269
|
-
? { artifactsRemovedAttempt: receipt.artifactsRemovedAttempt }
|
|
2270
|
-
: {}),
|
|
2271
|
-
...(receipt.detachedArtifactsPath ? { detachedArtifactsPath: receipt.detachedArtifactsPath } : {}),
|
|
2272
|
-
...(receipt.detachedTranscriptPath ? { detachedTranscriptPath: receipt.detachedTranscriptPath } : {}),
|
|
2273
|
-
...(receipt.transcriptPayloadDurable === true ? { transcriptPayloadDurable: true } : {}),
|
|
2274
|
-
...(receipt.retainedArtifactsSuccessorPath
|
|
2275
|
-
? { retainedArtifactsSuccessorPath: receipt.retainedArtifactsSuccessorPath }
|
|
2276
|
-
: {}),
|
|
2277
|
-
...(receipt.retainedArtifactsPlaceholderPath
|
|
2278
|
-
? { retainedArtifactsPlaceholderPath: receipt.retainedArtifactsPlaceholderPath }
|
|
2279
|
-
: {}),
|
|
2280
|
-
...(receipt.retainedArtifactsUnknownPath
|
|
2281
|
-
? { retainedArtifactsUnknownPath: receipt.retainedArtifactsUnknownPath }
|
|
2282
|
-
: {}),
|
|
2283
|
-
...(receipt.retainedTranscriptSuccessorPath
|
|
2284
|
-
? { retainedTranscriptSuccessorPath: receipt.retainedTranscriptSuccessorPath }
|
|
2285
|
-
: {}),
|
|
2286
|
-
...(receipt.retainedTranscriptPlaceholderPath
|
|
2287
|
-
? { retainedTranscriptPlaceholderPath: receipt.retainedTranscriptPlaceholderPath }
|
|
2288
|
-
: {}),
|
|
2289
|
-
...(receipt.retainedTranscriptUnknownPath
|
|
2290
|
-
? { retainedTranscriptUnknownPath: receipt.retainedTranscriptUnknownPath }
|
|
2291
|
-
: {}),
|
|
2292
|
-
plannedArtifactsPath: receipt.plannedArtifactsPath,
|
|
2293
|
-
plannedTranscriptPath: receipt.plannedTranscriptPath,
|
|
2294
|
-
};
|
|
2480
|
+
async function persistManagedGcStorageOwnerDisposition(
|
|
2481
|
+
scope: ManagedScope,
|
|
2482
|
+
target: RetiredTarget,
|
|
2483
|
+
deletion: Extract<VerifiedSessionDeleteResult, { kind: "cleanup_pending"; phase: "task_artifact_owner" }>,
|
|
2484
|
+
lock: ManagedStorageLock,
|
|
2485
|
+
): Promise<ManagedGcOwnerProgress> {
|
|
2486
|
+
return persistManagedGcStorageOwnerDispositionInternal(
|
|
2487
|
+
managedGcOwnerCleanupAuthority(scope, lock),
|
|
2488
|
+
target,
|
|
2489
|
+
deletion,
|
|
2490
|
+
);
|
|
2295
2491
|
}
|
|
2296
2492
|
|
|
2297
|
-
|
|
2493
|
+
async function managedGcOwnerProgressBeforeDelete(
|
|
2494
|
+
scope: ManagedScope,
|
|
2495
|
+
target: RetiredTarget,
|
|
2496
|
+
tombstoneTargets: readonly RetiredTarget[],
|
|
2497
|
+
artifactsRemoved: boolean,
|
|
2498
|
+
lock: ManagedStorageLock,
|
|
2499
|
+
): Promise<ManagedGcOwnerProgress> {
|
|
2500
|
+
return managedGcOwnerProgressBeforeDeleteInternal(
|
|
2501
|
+
managedGcOwnerCleanupAuthority(scope, lock),
|
|
2502
|
+
target,
|
|
2503
|
+
tombstoneTargets,
|
|
2504
|
+
artifactsRemoved,
|
|
2505
|
+
);
|
|
2506
|
+
}
|
|
2298
2507
|
|
|
2299
|
-
function
|
|
2508
|
+
async function taskArtifactOwnerTranscriptResult(
|
|
2300
2509
|
scope: ManagedScope,
|
|
2301
|
-
tombstone: string,
|
|
2302
2510
|
target: RetiredTarget,
|
|
2303
|
-
|
|
2304
|
-
):
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2511
|
+
lock: ManagedStorageLock,
|
|
2512
|
+
): Promise<"none" | "retired" | "payload_retired"> {
|
|
2513
|
+
return taskArtifactOwnerTranscriptResultInternal(managedGcOwnerCleanupAuthority(scope, lock), target);
|
|
2514
|
+
}
|
|
2515
|
+
function managedGcScopeStore(scope: ManagedScope, trusted: ManagedGcTrustedScope): ManagedSessionDescendantStore {
|
|
2516
|
+
return new ManagedSessionDescendantStore(
|
|
2517
|
+
trusted.root,
|
|
2518
|
+
scope.directoryPath,
|
|
2519
|
+
trusted.retainedAuthority
|
|
2520
|
+
? { authority: trusted.retainedAuthority, authorityBaseDir: scope.directoryPath }
|
|
2521
|
+
: undefined,
|
|
2522
|
+
trusted.policy,
|
|
2523
|
+
scope.agentDir,
|
|
2524
|
+
{
|
|
2525
|
+
canonicalPath: scope.directoryPath,
|
|
2526
|
+
dev: BigInt.asUintN(64, trusted.identity.dev),
|
|
2527
|
+
ino: BigInt.asUintN(64, trusted.identity.ino),
|
|
2528
|
+
},
|
|
2529
|
+
);
|
|
2530
|
+
}
|
|
2531
|
+
function managedGcScopeReader(scope: ManagedScope, trusted: ManagedGcTrustedScope): ManagedSessionDescendantStore {
|
|
2532
|
+
return new ManagedSessionDescendantStore(
|
|
2533
|
+
trusted.root,
|
|
2534
|
+
scope.directoryPath,
|
|
2535
|
+
undefined,
|
|
2536
|
+
trusted.policy,
|
|
2537
|
+
scope.agentDir,
|
|
2538
|
+
{
|
|
2539
|
+
canonicalPath: scope.directoryPath,
|
|
2540
|
+
dev: BigInt.asUintN(64, trusted.identity.dev),
|
|
2541
|
+
ino: BigInt.asUintN(64, trusted.identity.ino),
|
|
2542
|
+
},
|
|
2543
|
+
"read-only",
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2546
|
+
function managedGcRetirementTranscriptKey(transcriptPath: string): string {
|
|
2547
|
+
return createHash("sha256").update(path.resolve(transcriptPath), "utf8").digest("hex");
|
|
2548
|
+
}
|
|
2549
|
+
function managedGcRetirementReceiptRelativePath(
|
|
2550
|
+
transcriptPath: string,
|
|
2551
|
+
state: ManagedGcSessionRetirementState,
|
|
2552
|
+
attempt?: number,
|
|
2553
|
+
): string {
|
|
2554
|
+
if (state === "owner_pending" && (!Number.isSafeInteger(attempt) || attempt === undefined || attempt < 1))
|
|
2555
|
+
throw new Error("task_artifact_owner_continuation_attempt_invalid");
|
|
2556
|
+
const suffix = state === "owner_pending" ? `${state}-${String(attempt).padStart(8, "0")}` : state;
|
|
2557
|
+
return `${MANAGED_GC_RETIREMENT_RECEIPTS}/${MANAGED_GC_RETIREMENT_PREFIX}${managedGcRetirementTranscriptKey(transcriptPath)}-${suffix}.json`;
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
const MANAGED_GC_RECEIPT_MAX_BYTES = MANAGED_SESSION_READ_RANGE_MAX_BYTES;
|
|
2561
|
+
|
|
2562
|
+
interface ManagedGcDirectoryEvidence {
|
|
2563
|
+
readonly dev: string;
|
|
2564
|
+
readonly ino: string;
|
|
2565
|
+
readonly mtimeNs: string;
|
|
2566
|
+
readonly ctimeNs: string;
|
|
2567
|
+
readonly mode: string;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
interface ManagedGcDirectoryInventory {
|
|
2571
|
+
readonly evidence: ManagedGcDirectoryEvidence;
|
|
2572
|
+
readonly entryCount: number;
|
|
2573
|
+
readonly entries: readonly fs.Dirent[];
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
interface ManagedGcReceiptHistory {
|
|
2577
|
+
readonly entryCount: number;
|
|
2578
|
+
readonly encodedBytes: number;
|
|
2579
|
+
readonly directoryEntryCount: number;
|
|
2580
|
+
readonly directoryEvidence: ManagedGcDirectoryEvidence;
|
|
2581
|
+
readonly directoryInventory: ManagedGcDirectoryInventory;
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
interface ManagedGcDiscoveryScopeFence {
|
|
2585
|
+
readonly scope: ManagedScope;
|
|
2586
|
+
readonly scopeIdentity: { readonly dev: string; readonly ino: string };
|
|
2587
|
+
readonly binding: ManagedFileSnapshot;
|
|
2588
|
+
readonly internalDirectoryIdentity: { readonly dev: string; readonly ino: string };
|
|
2589
|
+
readonly receiptInventory: ManagedGcDirectoryInventory | undefined;
|
|
2590
|
+
readonly openReader: () => ManagedSessionDescendantStore;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
interface MutableManagedGcReceiptHistory {
|
|
2594
|
+
encodedBytes: number;
|
|
2595
|
+
readonly admittedFiles: Set<string>;
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
function managedGcJournalCapacity(): never {
|
|
2599
|
+
throw new Error("managed_gc_journal_capacity_exceeded");
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
function managedGcDirectoryEvidence(stat: fs.BigIntStats): ManagedGcDirectoryEvidence {
|
|
2603
|
+
return {
|
|
2604
|
+
dev: stat.dev.toString(),
|
|
2605
|
+
ino: stat.ino.toString(),
|
|
2606
|
+
mtimeNs: stat.mtimeNs.toString(),
|
|
2607
|
+
ctimeNs: stat.ctimeNs.toString(),
|
|
2608
|
+
mode: stat.mode.toString(),
|
|
2609
|
+
};
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
function managedGcSameDirectoryEvidence(left: ManagedGcDirectoryEvidence, right: ManagedGcDirectoryEvidence): boolean {
|
|
2613
|
+
return (
|
|
2614
|
+
left.dev === right.dev &&
|
|
2615
|
+
left.ino === right.ino &&
|
|
2616
|
+
left.mtimeNs === right.mtimeNs &&
|
|
2617
|
+
left.ctimeNs === right.ctimeNs &&
|
|
2618
|
+
left.mode === right.mode
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
/** Count the selected family before retaining a bounded inventory, preserving unrelated role entries. */
|
|
2623
|
+
function managedGcStreamDirectoryInventory(
|
|
2624
|
+
pathname: string,
|
|
2625
|
+
countEntry: (entry: fs.Dirent) => boolean,
|
|
2626
|
+
retain: (entry: fs.Dirent) => boolean,
|
|
2627
|
+
capacity: () => never = managedGcJournalCapacity,
|
|
2628
|
+
): ManagedGcDirectoryInventory {
|
|
2629
|
+
const beforeStat = fs.lstatSync(pathname, { bigint: true });
|
|
2630
|
+
if (!beforeStat.isDirectory() || beforeStat.isSymbolicLink()) throw new Error("managed_gc_scope_authority_mismatch");
|
|
2631
|
+
const before = managedGcDirectoryEvidence(beforeStat);
|
|
2632
|
+
const scan = (collect: boolean): { count: number; entries: fs.Dirent[] } => {
|
|
2633
|
+
const entries: fs.Dirent[] = [];
|
|
2634
|
+
let count = 0;
|
|
2635
|
+
let retained = 0;
|
|
2636
|
+
const directory = fs.opendirSync(pathname);
|
|
2637
|
+
try {
|
|
2638
|
+
for (let entry = directory.readSync(); entry; entry = directory.readSync()) {
|
|
2639
|
+
if (countEntry(entry) && ++count > MANAGED_ARTIFACT_MAX_FILES) capacity();
|
|
2640
|
+
if (!retain(entry)) continue;
|
|
2641
|
+
if (++retained > MANAGED_ARTIFACT_MAX_FILES) capacity();
|
|
2642
|
+
if (collect) entries.push(entry);
|
|
2643
|
+
}
|
|
2644
|
+
} finally {
|
|
2645
|
+
directory.closeSync();
|
|
2646
|
+
}
|
|
2647
|
+
return { count, entries };
|
|
2648
|
+
};
|
|
2649
|
+
const counted = scan(false);
|
|
2650
|
+
const afterCount = fs.lstatSync(pathname, { bigint: true });
|
|
2651
|
+
if (
|
|
2652
|
+
!afterCount.isDirectory() ||
|
|
2653
|
+
afterCount.isSymbolicLink() ||
|
|
2654
|
+
!managedGcSameDirectoryEvidence(before, managedGcDirectoryEvidence(afterCount))
|
|
2655
|
+
)
|
|
2656
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
2657
|
+
const collected = scan(true);
|
|
2658
|
+
const afterCollect = fs.lstatSync(pathname, { bigint: true });
|
|
2659
|
+
if (
|
|
2660
|
+
collected.count !== counted.count ||
|
|
2661
|
+
!afterCollect.isDirectory() ||
|
|
2662
|
+
afterCollect.isSymbolicLink() ||
|
|
2663
|
+
!managedGcSameDirectoryEvidence(before, managedGcDirectoryEvidence(afterCollect))
|
|
2664
|
+
)
|
|
2665
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
2666
|
+
collected.entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
2667
|
+
return { evidence: before, entryCount: counted.count, entries: collected.entries };
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
function managedGcReceiptDirectoryInventory(
|
|
2671
|
+
scope: ManagedScope,
|
|
2672
|
+
store: ManagedSessionDescendantStore,
|
|
2673
|
+
): ManagedGcDirectoryInventory {
|
|
2674
|
+
const relativePath = MANAGED_GC_RETIREMENT_RECEIPTS;
|
|
2675
|
+
const pathname = path.join(scope.directoryPath, relativePath);
|
|
2676
|
+
const before = store.captureDirectoryIdentity(relativePath);
|
|
2677
|
+
const inventory = managedGcStreamDirectoryInventory(
|
|
2678
|
+
pathname,
|
|
2679
|
+
entry => entry.name.startsWith(MANAGED_GC_RETIREMENT_PREFIX),
|
|
2680
|
+
entry => entry.name.startsWith(MANAGED_GC_RETIREMENT_PREFIX),
|
|
2681
|
+
);
|
|
2682
|
+
const after = store.captureDirectoryIdentity(relativePath);
|
|
2683
|
+
if (
|
|
2684
|
+
before.dev !== after.dev ||
|
|
2685
|
+
before.ino !== after.ino ||
|
|
2686
|
+
before.dev !== inventory.evidence.dev ||
|
|
2687
|
+
before.ino !== inventory.evidence.ino
|
|
2688
|
+
)
|
|
2689
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
2690
|
+
store.assertBound();
|
|
2691
|
+
return inventory;
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
function managedGcReceiptCapacity(): never {
|
|
2695
|
+
throw new Error("managed_gc_receipt_capacity_exceeded");
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
function managedGcJsonStringByteLength(value: string, add: (amount: number) => void): void {
|
|
2699
|
+
add(2);
|
|
2700
|
+
for (let index = 0; index < value.length; index++) {
|
|
2701
|
+
const unit = value.charCodeAt(index);
|
|
2702
|
+
if (unit === 0x22 || unit === 0x5c) add(2);
|
|
2703
|
+
else if (unit < 0x20)
|
|
2704
|
+
add(unit === 0x08 || unit === 0x09 || unit === 0x0a || unit === 0x0c || unit === 0x0d ? 2 : 6);
|
|
2705
|
+
else if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
2706
|
+
const low = value.charCodeAt(index + 1);
|
|
2707
|
+
if (low >= 0xdc00 && low <= 0xdfff) {
|
|
2708
|
+
add(4);
|
|
2709
|
+
index++;
|
|
2710
|
+
} else add(6);
|
|
2711
|
+
} else if (unit >= 0xdc00 && unit <= 0xdfff) add(6);
|
|
2712
|
+
else if (unit <= 0x7f) add(1);
|
|
2713
|
+
else if (unit <= 0x7ff) add(2);
|
|
2714
|
+
else add(3);
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
/** Measure the canonical JSON encoding before JSON.stringify or Buffer allocation. */
|
|
2719
|
+
function managedGcJsonByteLength(value: unknown): number {
|
|
2720
|
+
let bytes = 0;
|
|
2721
|
+
const active = new Set<object>();
|
|
2722
|
+
const add = (amount: number): void => {
|
|
2723
|
+
if (!Number.isSafeInteger(amount) || amount < 0 || amount > MANAGED_GC_RECEIPT_MAX_BYTES - bytes)
|
|
2724
|
+
managedGcReceiptCapacity();
|
|
2725
|
+
bytes += amount;
|
|
2726
|
+
};
|
|
2727
|
+
const visit = (item: unknown, depth: number, inArray = false): void => {
|
|
2728
|
+
if (depth > 32) throw new Error("task_artifact_owner_continuation_corrupt");
|
|
2729
|
+
if (item === null) {
|
|
2730
|
+
add(4);
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
if (typeof item === "string") {
|
|
2734
|
+
managedGcJsonStringByteLength(item, add);
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
if (typeof item === "bigint") {
|
|
2738
|
+
managedGcJsonStringByteLength(item.toString(), add);
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
if (typeof item === "boolean") {
|
|
2742
|
+
add(item ? 4 : 5);
|
|
2743
|
+
return;
|
|
2744
|
+
}
|
|
2745
|
+
if (typeof item === "number") {
|
|
2746
|
+
add(Number.isFinite(item) ? (Object.is(item, -0) ? 1 : String(item).length) : 4);
|
|
2747
|
+
return;
|
|
2748
|
+
}
|
|
2749
|
+
if (typeof item === "undefined" || typeof item === "function" || typeof item === "symbol") {
|
|
2750
|
+
if (inArray) add(4);
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
if (typeof item !== "object") managedGcReceiptCapacity();
|
|
2754
|
+
if (active.has(item)) throw new Error("task_artifact_owner_continuation_corrupt");
|
|
2755
|
+
active.add(item);
|
|
2756
|
+
const array = Array.isArray(item);
|
|
2757
|
+
if (array) {
|
|
2758
|
+
const values = item as unknown[];
|
|
2759
|
+
if (values.length > MANAGED_GC_RECEIPT_MAX_BYTES) managedGcReceiptCapacity();
|
|
2760
|
+
add(2);
|
|
2761
|
+
let emitted = false;
|
|
2762
|
+
for (let index = 0; index < values.length; index++) {
|
|
2763
|
+
if (emitted) add(1);
|
|
2764
|
+
emitted = true;
|
|
2765
|
+
visit(values[index], depth + 1, true);
|
|
2766
|
+
}
|
|
2767
|
+
} else {
|
|
2768
|
+
add(2);
|
|
2769
|
+
let emitted = false;
|
|
2770
|
+
for (const key in item) {
|
|
2771
|
+
if (!Object.hasOwn(item, key)) continue;
|
|
2772
|
+
const child = (item as Record<string, unknown>)[key];
|
|
2773
|
+
if (child === undefined || typeof child === "function" || typeof child === "symbol") continue;
|
|
2774
|
+
if (emitted) add(1);
|
|
2775
|
+
emitted = true;
|
|
2776
|
+
managedGcJsonStringByteLength(key, add);
|
|
2777
|
+
add(1);
|
|
2778
|
+
visit(child, depth + 1);
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
active.delete(item);
|
|
2782
|
+
};
|
|
2783
|
+
visit(value, 0);
|
|
2784
|
+
add(1);
|
|
2785
|
+
return bytes;
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
function validateManagedGcTranscriptPath(scope: ManagedScope, transcriptPath: string): string {
|
|
2789
|
+
if (
|
|
2790
|
+
typeof transcriptPath !== "string" ||
|
|
2791
|
+
!path.isAbsolute(transcriptPath) ||
|
|
2792
|
+
path.resolve(transcriptPath) !== transcriptPath ||
|
|
2793
|
+
path.dirname(transcriptPath) !== scope.directoryPath ||
|
|
2794
|
+
path.extname(transcriptPath) !== ".jsonl" ||
|
|
2795
|
+
!pathIsWithin(scope.sessionsRoot, transcriptPath)
|
|
2796
|
+
)
|
|
2797
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
2798
|
+
return transcriptPath;
|
|
2799
|
+
}
|
|
2800
|
+
function managedGcTargetFromSnapshot(
|
|
2801
|
+
scope: ManagedScope,
|
|
2802
|
+
transcriptPath: string,
|
|
2803
|
+
snapshot: SessionStorageSnapshot,
|
|
2804
|
+
): ManagedGcSessionRetirementTarget {
|
|
2805
|
+
if (
|
|
2806
|
+
!snapshot.stat.isFile ||
|
|
2807
|
+
snapshot.stat.nlink !== 1n ||
|
|
2808
|
+
snapshot.stat.size !== snapshot.bytes.byteLength ||
|
|
2809
|
+
!Number.isSafeInteger(snapshot.stat.size) ||
|
|
2810
|
+
snapshot.stat.size < 0
|
|
2811
|
+
)
|
|
2812
|
+
throw new Error("task_artifact_owner_continuation_identity_invalid");
|
|
2813
|
+
const header = parseFirstJsonlLine(snapshot.bytes);
|
|
2814
|
+
if (
|
|
2815
|
+
header?.type !== "session" ||
|
|
2816
|
+
typeof header.id !== "string" ||
|
|
2817
|
+
header.id.length === 0 ||
|
|
2818
|
+
typeof header.cwd !== "string" ||
|
|
2819
|
+
header.cwd.length === 0
|
|
2820
|
+
)
|
|
2821
|
+
throw new Error("task_artifact_owner_transcript_header_invalid");
|
|
2822
|
+
const cwdIdentity = identityFor(header.cwd);
|
|
2823
|
+
if (!cwdIdentity.ok || cwdIdentity.platform !== scope.platform || cwdIdentity.canonicalPath !== scope.canonicalCwd)
|
|
2824
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
2825
|
+
const locator = taskArtifactOwnerLocatorFromTranscriptBytes(snapshot.bytes, header.id);
|
|
2826
|
+
if (!locator) throw new Error("task_artifact_owner_locator_missing");
|
|
2827
|
+
return Object.freeze({
|
|
2828
|
+
transcriptPath,
|
|
2829
|
+
sessionId: header.id,
|
|
2830
|
+
cwd: header.cwd,
|
|
2831
|
+
transcriptIdentity: {
|
|
2832
|
+
dev: snapshot.stat.dev,
|
|
2833
|
+
ino: snapshot.stat.ino,
|
|
2834
|
+
nlink: snapshot.stat.nlink,
|
|
2835
|
+
size: snapshot.stat.size,
|
|
2836
|
+
mtimeNs: snapshot.stat.mtimeNs,
|
|
2837
|
+
sha256: createHash("sha256").update(snapshot.bytes).digest("hex"),
|
|
2838
|
+
},
|
|
2839
|
+
taskArtifactOwnerLocator: locator,
|
|
2840
|
+
});
|
|
2841
|
+
}
|
|
2842
|
+
/** Bind the first transcript header through FileSessionStorage's descriptor-bound, no-follow snapshot. */
|
|
2843
|
+
export function bindManagedGcSessionRetirementTarget(
|
|
2844
|
+
scope: ManagedScope,
|
|
2845
|
+
transcriptPathValue: string,
|
|
2846
|
+
): ManagedGcSessionRetirementTarget {
|
|
2847
|
+
const trusted = managedGcTrustedScope(scope);
|
|
2848
|
+
const transcriptPath = validateManagedGcTranscriptPath(scope, transcriptPathValue);
|
|
2849
|
+
const store = managedGcScopeReader(scope, trusted);
|
|
2850
|
+
try {
|
|
2851
|
+
store.verifyRootSecurity();
|
|
2852
|
+
store.assertBound();
|
|
2853
|
+
const snapshot = new FileSessionStorage().readSnapshotSync(transcriptPath);
|
|
2854
|
+
store.assertBound();
|
|
2855
|
+
return managedGcTargetFromSnapshot(scope, transcriptPath, snapshot);
|
|
2856
|
+
} finally {
|
|
2857
|
+
store.close();
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
function sameManagedGcTarget(left: ManagedGcSessionRetirementTarget, right: ManagedGcSessionRetirementTarget): boolean {
|
|
2861
|
+
return (
|
|
2862
|
+
left.transcriptPath === right.transcriptPath &&
|
|
2863
|
+
left.sessionId === right.sessionId &&
|
|
2864
|
+
left.cwd === right.cwd &&
|
|
2865
|
+
sameIdentity(left.transcriptIdentity, right.transcriptIdentity) &&
|
|
2866
|
+
util.isDeepStrictEqual(left.taskArtifactOwnerLocator, right.taskArtifactOwnerLocator)
|
|
2867
|
+
);
|
|
2868
|
+
}
|
|
2869
|
+
function assertManagedGcTranscriptUnchangedOrAbsent(
|
|
2870
|
+
scope: ManagedScope,
|
|
2871
|
+
target: ManagedGcSessionRetirementTarget,
|
|
2872
|
+
allowAbsent: boolean,
|
|
2873
|
+
): void {
|
|
2874
|
+
try {
|
|
2875
|
+
const current = bindManagedGcSessionRetirementTarget(scope, target.transcriptPath);
|
|
2876
|
+
if (!sameManagedGcTarget(current, target))
|
|
2877
|
+
throw new Error("task_artifact_owner_continuation_transcript_replaced");
|
|
2878
|
+
} catch (error) {
|
|
2879
|
+
if (allowAbsent && hasFsCode(error, "ENOENT")) return;
|
|
2880
|
+
throw error;
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
function managedGcReceiptRecord(
|
|
2884
|
+
scope: ManagedScope,
|
|
2885
|
+
receipt: ManagedGcSessionRetirementReceipt,
|
|
2886
|
+
attempt?: number,
|
|
2887
|
+
): Record<string, unknown> {
|
|
2888
|
+
const state = receipt.state;
|
|
2889
|
+
const common = {
|
|
2890
|
+
schemaVersion: 1,
|
|
2891
|
+
state,
|
|
2892
|
+
scope: scopeDigest(scope.platform, scope.canonicalCwd),
|
|
2893
|
+
transcriptPath: receipt.transcriptPath,
|
|
2894
|
+
sessionId: receipt.sessionId,
|
|
2895
|
+
cwd: receipt.cwd,
|
|
2896
|
+
transcriptIdentity: managedGcRetirementIdentityRecord(receipt.transcriptIdentity),
|
|
2897
|
+
taskArtifactOwnerDeletionEvidence: receipt.taskArtifactOwnerDeletionEvidence,
|
|
2898
|
+
};
|
|
2899
|
+
const hasOutcomeFields =
|
|
2900
|
+
receipt.ownerRetirementAttempt !== undefined ||
|
|
2901
|
+
receipt.taskArtifactOwnerRetirementOutcome !== undefined ||
|
|
2902
|
+
receipt.taskArtifactOwnerRetirementContinuation !== undefined ||
|
|
2903
|
+
receipt.taskArtifactOwnerPayloadRetired !== undefined ||
|
|
2904
|
+
receipt.taskArtifactOwnerNamespaceRetained !== undefined ||
|
|
2905
|
+
receipt.taskArtifactOwnerRetired !== undefined;
|
|
2906
|
+
if (state === "prepared" || state === "artifacts_removed") {
|
|
2907
|
+
if (
|
|
2908
|
+
hasOutcomeFields ||
|
|
2909
|
+
(state === "prepared" && receipt.artifactsRemoved !== undefined) ||
|
|
2910
|
+
(state === "artifacts_removed" && receipt.artifactsRemoved !== undefined && receipt.artifactsRemoved !== true)
|
|
2911
|
+
)
|
|
2912
|
+
throw new Error("task_artifact_owner_continuation_state_mismatch");
|
|
2913
|
+
return state === "prepared" ? common : { ...common, artifactsRemoved: true };
|
|
2914
|
+
}
|
|
2915
|
+
if (state === "owner_pending") {
|
|
2916
|
+
const outcome = receipt.taskArtifactOwnerRetirementOutcome;
|
|
2917
|
+
if (
|
|
2918
|
+
!outcome ||
|
|
2919
|
+
outcome.kind === "completed" ||
|
|
2920
|
+
(receipt.artifactsRemoved !== undefined && receipt.artifactsRemoved !== true) ||
|
|
2921
|
+
receipt.taskArtifactOwnerRetired !== undefined ||
|
|
2922
|
+
(attempt !== undefined &&
|
|
2923
|
+
receipt.ownerRetirementAttempt !== undefined &&
|
|
2924
|
+
receipt.ownerRetirementAttempt !== attempt) ||
|
|
2925
|
+
(receipt.taskArtifactOwnerRetirementContinuation !== undefined &&
|
|
2926
|
+
!util.isDeepStrictEqual(receipt.taskArtifactOwnerRetirementContinuation, outcome.continuation)) ||
|
|
2927
|
+
(receipt.taskArtifactOwnerPayloadRetired !== undefined &&
|
|
2928
|
+
receipt.taskArtifactOwnerPayloadRetired !== (outcome.kind === "payload_retired")) ||
|
|
2929
|
+
(receipt.taskArtifactOwnerNamespaceRetained !== undefined &&
|
|
2930
|
+
receipt.taskArtifactOwnerNamespaceRetained !== (outcome.kind === "payload_retired"))
|
|
2931
|
+
)
|
|
2932
|
+
throw new Error("task_artifact_owner_continuation_outcome_mismatch");
|
|
2933
|
+
if (!Number.isSafeInteger(attempt) || attempt === undefined || attempt < 1)
|
|
2934
|
+
throw new Error("task_artifact_owner_continuation_attempt_invalid");
|
|
2935
|
+
return {
|
|
2936
|
+
...common,
|
|
2937
|
+
ownerRetirementAttempt: attempt,
|
|
2938
|
+
artifactsRemoved: true,
|
|
2939
|
+
taskArtifactOwnerRetirementOutcome: outcome,
|
|
2940
|
+
taskArtifactOwnerRetirementContinuation: outcome.continuation,
|
|
2941
|
+
...(outcome.kind === "payload_retired"
|
|
2942
|
+
? { taskArtifactOwnerPayloadRetired: true, taskArtifactOwnerNamespaceRetained: true }
|
|
2943
|
+
: {}),
|
|
2944
|
+
};
|
|
2945
|
+
}
|
|
2946
|
+
if (
|
|
2947
|
+
(receipt.artifactsRemoved !== undefined && receipt.artifactsRemoved !== true) ||
|
|
2948
|
+
hasOutcomeFieldsExceptOutcome(receipt) ||
|
|
2949
|
+
(receipt.taskArtifactOwnerRetired !== undefined && receipt.taskArtifactOwnerRetired !== true) ||
|
|
2950
|
+
receipt.taskArtifactOwnerRetirementOutcome?.kind !== "completed"
|
|
2951
|
+
)
|
|
2952
|
+
throw new Error("task_artifact_owner_continuation_outcome_mismatch");
|
|
2953
|
+
return {
|
|
2954
|
+
...common,
|
|
2955
|
+
artifactsRemoved: true,
|
|
2956
|
+
taskArtifactOwnerRetired: true,
|
|
2957
|
+
taskArtifactOwnerRetirementOutcome: receipt.taskArtifactOwnerRetirementOutcome,
|
|
2958
|
+
};
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
function hasOutcomeFieldsExceptOutcome(receipt: ManagedGcSessionRetirementReceipt): boolean {
|
|
2962
|
+
return (
|
|
2963
|
+
receipt.ownerRetirementAttempt !== undefined ||
|
|
2964
|
+
receipt.taskArtifactOwnerRetirementContinuation !== undefined ||
|
|
2965
|
+
receipt.taskArtifactOwnerPayloadRetired !== undefined ||
|
|
2966
|
+
receipt.taskArtifactOwnerNamespaceRetained !== undefined
|
|
2967
|
+
);
|
|
2968
|
+
}
|
|
2969
|
+
function readManagedGcReceiptSnapshot(
|
|
2970
|
+
store: ManagedSessionDescendantStore,
|
|
2971
|
+
relativePath: string,
|
|
2972
|
+
history?: MutableManagedGcReceiptHistory,
|
|
2973
|
+
): ManagedFileSnapshot | null {
|
|
2974
|
+
try {
|
|
2975
|
+
return store.readExpectedBounded(relativePath, MANAGED_GC_RECEIPT_MAX_BYTES, (size, descriptor) => {
|
|
2976
|
+
if (descriptor.nlink !== 1n || descriptor.size !== size) throw new Error("source_changed");
|
|
2977
|
+
if (history) managedGcAdmitHistoryBytes(history, descriptor.size, relativePath);
|
|
2978
|
+
});
|
|
2979
|
+
} catch (error) {
|
|
2980
|
+
if (error instanceof Error && error.message === "artifact_capacity_exceeded") managedGcJournalCapacity();
|
|
2981
|
+
throw error;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
function managedGcAdmitHistoryBytes(history: MutableManagedGcReceiptHistory, size: number, relativePath: string): void {
|
|
2986
|
+
if (history.admittedFiles.has(relativePath)) return;
|
|
2987
|
+
if (history.admittedFiles.size >= MANAGED_ARTIFACT_MAX_FILES) managedGcJournalCapacity();
|
|
2988
|
+
if (size > MANAGED_ARTIFACT_MAX_TOTAL_BYTES - history.encodedBytes) managedGcJournalCapacity();
|
|
2989
|
+
history.encodedBytes += size;
|
|
2990
|
+
history.admittedFiles.add(relativePath);
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
function parseManagedGcReceiptFile(
|
|
2994
|
+
store: ManagedSessionDescendantStore,
|
|
2995
|
+
relativePath: string,
|
|
2996
|
+
scope: ManagedScope,
|
|
2997
|
+
ownerContext: TaskArtifactOwnerStorageContext,
|
|
2998
|
+
target: ManagedGcSessionRetirementTarget,
|
|
2999
|
+
history?: MutableManagedGcReceiptHistory,
|
|
3000
|
+
): ManagedGcSessionRetirementReceipt | undefined {
|
|
3001
|
+
const snapshot = readManagedGcReceiptSnapshot(store, relativePath, history);
|
|
3002
|
+
if (!snapshot) return undefined;
|
|
3003
|
+
let value: unknown;
|
|
3004
|
+
try {
|
|
3005
|
+
value = JSON.parse(snapshot.bytes.toString("utf8"));
|
|
3006
|
+
} catch {
|
|
3007
|
+
throw new Error("task_artifact_owner_continuation_corrupt");
|
|
3008
|
+
}
|
|
3009
|
+
return parseManagedGcRetirementReceipt(value, scopeDigest(scope.platform, scope.canonicalCwd), ownerContext, target);
|
|
3010
|
+
}
|
|
3011
|
+
function targetFromPreparedRecord(value: unknown, transcriptPath: string): ManagedGcSessionRetirementTarget {
|
|
3012
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3013
|
+
throw new Error("task_artifact_owner_continuation_corrupt");
|
|
3014
|
+
const record = value as Record<string, unknown>;
|
|
3015
|
+
if (
|
|
3016
|
+
record.state !== "prepared" ||
|
|
3017
|
+
record.transcriptPath !== transcriptPath ||
|
|
3018
|
+
typeof record.sessionId !== "string" ||
|
|
3019
|
+
typeof record.cwd !== "string"
|
|
3020
|
+
)
|
|
3021
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
3022
|
+
const evidence = parseTaskArtifactOwnerDeletionEvidence(record.taskArtifactOwnerDeletionEvidence);
|
|
3023
|
+
return {
|
|
3024
|
+
transcriptPath,
|
|
3025
|
+
sessionId: record.sessionId,
|
|
3026
|
+
cwd: record.cwd,
|
|
3027
|
+
transcriptIdentity: managedGcRetirementIdentityValue(record.transcriptIdentity),
|
|
3028
|
+
taskArtifactOwnerLocator: evidence.locator,
|
|
3029
|
+
};
|
|
3030
|
+
}
|
|
3031
|
+
function deepSame(left: unknown, right: unknown): boolean {
|
|
3032
|
+
return util.isDeepStrictEqual(left, right);
|
|
3033
|
+
}
|
|
3034
|
+
function readManagedGcPreparedAuthority(
|
|
3035
|
+
scope: ManagedScope,
|
|
3036
|
+
transcriptPath: string,
|
|
3037
|
+
ownerContext: TaskArtifactOwnerStorageContext,
|
|
3038
|
+
store: ManagedSessionDescendantStore,
|
|
3039
|
+
history?: MutableManagedGcReceiptHistory,
|
|
3040
|
+
bootstrap?: {
|
|
3041
|
+
readonly snapshot: ManagedFileSnapshot;
|
|
3042
|
+
readonly value: unknown;
|
|
3043
|
+
readonly history: MutableManagedGcReceiptHistory;
|
|
3044
|
+
},
|
|
3045
|
+
): ManagedGcSessionRetirementReceipt | undefined {
|
|
3046
|
+
const snapshot =
|
|
3047
|
+
bootstrap?.snapshot ??
|
|
3048
|
+
readManagedGcReceiptSnapshot(store, managedGcRetirementReceiptRelativePath(transcriptPath, "prepared"), history);
|
|
3049
|
+
if (!snapshot) return undefined;
|
|
3050
|
+
let value: unknown;
|
|
3051
|
+
if (bootstrap) {
|
|
3052
|
+
value = bootstrap.value;
|
|
3053
|
+
if (history && history !== bootstrap.history)
|
|
3054
|
+
managedGcAdmitHistoryBytes(
|
|
3055
|
+
history,
|
|
3056
|
+
snapshot.identity.size,
|
|
3057
|
+
managedGcRetirementReceiptRelativePath(transcriptPath, "prepared"),
|
|
3058
|
+
);
|
|
3059
|
+
} else {
|
|
3060
|
+
try {
|
|
3061
|
+
value = JSON.parse(snapshot.bytes.toString("utf8"));
|
|
3062
|
+
} catch {
|
|
3063
|
+
throw new Error("task_artifact_owner_continuation_corrupt");
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
const target = targetFromPreparedRecord(value, transcriptPath);
|
|
3067
|
+
const prepared = parseManagedGcRetirementReceipt(
|
|
3068
|
+
value,
|
|
3069
|
+
scopeDigest(scope.platform, scope.canonicalCwd),
|
|
3070
|
+
ownerContext,
|
|
3071
|
+
target,
|
|
3072
|
+
);
|
|
3073
|
+
if (prepared.state !== "prepared") throw new Error("task_artifact_owner_continuation_state_missing");
|
|
3074
|
+
return prepared;
|
|
3075
|
+
}
|
|
3076
|
+
function assertSameManagedGcImmutableAuthority(
|
|
3077
|
+
prepared: ManagedGcSessionRetirementReceipt,
|
|
3078
|
+
next: ManagedGcSessionRetirementReceipt,
|
|
3079
|
+
): void {
|
|
3080
|
+
if (
|
|
3081
|
+
!sameManagedGcTarget(prepared, next) ||
|
|
3082
|
+
!deepSame(prepared.taskArtifactOwnerDeletionEvidence, next.taskArtifactOwnerDeletionEvidence)
|
|
3083
|
+
)
|
|
3084
|
+
throw new Error("task_artifact_owner_continuation_evidence_mismatch");
|
|
3085
|
+
}
|
|
3086
|
+
function readManagedGcSessionRetirementReceiptUnlocked(
|
|
3087
|
+
scope: ManagedScope,
|
|
3088
|
+
transcriptPathValue: string,
|
|
3089
|
+
trusted: ManagedGcTrustedScope,
|
|
3090
|
+
store: ManagedSessionDescendantStore,
|
|
3091
|
+
lock?: ManagedStorageLock,
|
|
3092
|
+
inventory?: ManagedGcDirectoryInventory,
|
|
3093
|
+
targetNames?: readonly string[],
|
|
3094
|
+
preparedBootstrap?: {
|
|
3095
|
+
readonly snapshot: ManagedFileSnapshot;
|
|
3096
|
+
readonly value: unknown;
|
|
3097
|
+
readonly history: MutableManagedGcReceiptHistory;
|
|
3098
|
+
},
|
|
3099
|
+
): {
|
|
3100
|
+
readonly receipt: ManagedGcSessionRetirementReceipt | undefined;
|
|
3101
|
+
readonly prepared: ManagedGcSessionRetirementReceipt | undefined;
|
|
3102
|
+
readonly history: ManagedGcReceiptHistory;
|
|
3103
|
+
} {
|
|
3104
|
+
const transcriptPath = validateManagedGcTranscriptPath(scope, transcriptPathValue);
|
|
3105
|
+
lock?.assertOwned();
|
|
3106
|
+
store.assertBound();
|
|
3107
|
+
const receiptInventory = inventory ?? managedGcReceiptDirectoryInventory(scope, store);
|
|
3108
|
+
const names = targetNames ?? managedGcReceiptNames(scope, transcriptPath, store, receiptInventory);
|
|
3109
|
+
if (names.length > MANAGED_ARTIFACT_MAX_FILES) managedGcJournalCapacity();
|
|
3110
|
+
const prefix = `${MANAGED_GC_RETIREMENT_PREFIX}${managedGcRetirementTranscriptKey(transcriptPath)}-`;
|
|
3111
|
+
const pendingPrefix = `${prefix}owner_pending-`;
|
|
3112
|
+
const historyBytes = preparedBootstrap?.history ?? { encodedBytes: 0, admittedFiles: new Set<string>() };
|
|
3113
|
+
const history = (): ManagedGcReceiptHistory => ({
|
|
3114
|
+
entryCount: names.length,
|
|
3115
|
+
encodedBytes: historyBytes.encodedBytes,
|
|
3116
|
+
directoryEntryCount: receiptInventory.entryCount,
|
|
3117
|
+
directoryEvidence: receiptInventory.evidence,
|
|
3118
|
+
directoryInventory: receiptInventory,
|
|
3119
|
+
});
|
|
3120
|
+
const preparedName = `${prefix}prepared.json`;
|
|
3121
|
+
if (!names.includes(preparedName)) {
|
|
3122
|
+
if (names.length > 0) throw new Error("task_artifact_owner_continuation_state_missing");
|
|
3123
|
+
if (!inventory) {
|
|
3124
|
+
const after = managedGcReceiptDirectoryInventory(scope, store);
|
|
3125
|
+
if (!managedGcSameReceiptDirectoryInventory(receiptInventory, after))
|
|
3126
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
3127
|
+
}
|
|
3128
|
+
return { receipt: undefined, prepared: undefined, history: history() };
|
|
3129
|
+
}
|
|
3130
|
+
const prepared = readManagedGcPreparedAuthority(
|
|
3131
|
+
scope,
|
|
3132
|
+
transcriptPath,
|
|
3133
|
+
trusted.ownerContext,
|
|
3134
|
+
store,
|
|
3135
|
+
historyBytes,
|
|
3136
|
+
preparedBootstrap,
|
|
3137
|
+
);
|
|
3138
|
+
if (!prepared) {
|
|
3139
|
+
throw new Error("task_artifact_owner_continuation_state_missing");
|
|
3140
|
+
}
|
|
3141
|
+
const target = prepared;
|
|
3142
|
+
assertManagedGcTranscriptUnchangedOrAbsent(scope, target, true);
|
|
3143
|
+
const nameSet = new Set(names);
|
|
3144
|
+
if (!nameSet.has(preparedName)) throw new Error("task_artifact_owner_continuation_state_missing");
|
|
3145
|
+
const readState = (state: ManagedGcSessionRetirementState, attempt?: number) =>
|
|
3146
|
+
parseManagedGcReceiptFile(
|
|
3147
|
+
store,
|
|
3148
|
+
managedGcRetirementReceiptRelativePath(transcriptPath, state, attempt),
|
|
3149
|
+
scope,
|
|
3150
|
+
trusted.ownerContext,
|
|
3151
|
+
target,
|
|
3152
|
+
historyBytes,
|
|
3153
|
+
);
|
|
3154
|
+
const artifactsName = path.posix.basename(
|
|
3155
|
+
managedGcRetirementReceiptRelativePath(transcriptPath, "artifacts_removed"),
|
|
3156
|
+
);
|
|
3157
|
+
const retiredName = path.posix.basename(managedGcRetirementReceiptRelativePath(transcriptPath, "owner_retired"));
|
|
3158
|
+
const artifactsRemoved = nameSet.has(artifactsName) ? readState("artifacts_removed") : undefined;
|
|
3159
|
+
const ownerRetired = nameSet.has(retiredName) ? readState("owner_retired") : undefined;
|
|
3160
|
+
if (
|
|
3161
|
+
(nameSet.has(artifactsName) && artifactsRemoved?.state !== "artifacts_removed") ||
|
|
3162
|
+
(nameSet.has(retiredName) && ownerRetired?.state !== "owner_retired")
|
|
3163
|
+
)
|
|
3164
|
+
throw new Error("task_artifact_owner_continuation_state_missing");
|
|
3165
|
+
const attempts = names
|
|
3166
|
+
.filter(name => name.startsWith(pendingPrefix))
|
|
3167
|
+
.map(name => {
|
|
3168
|
+
const suffix = name.slice(pendingPrefix.length);
|
|
3169
|
+
if (!/^[0-9]{8,}\.json$/u.test(suffix)) throw new Error("task_artifact_owner_continuation_corrupt");
|
|
3170
|
+
const attempt = Number(suffix.slice(0, -5));
|
|
3171
|
+
if (!Number.isSafeInteger(attempt) || attempt < 1 || `${String(attempt).padStart(8, "0")}.json` !== suffix)
|
|
3172
|
+
throw new Error("task_artifact_owner_continuation_corrupt");
|
|
3173
|
+
return attempt;
|
|
3174
|
+
})
|
|
3175
|
+
.sort((left, right) => left - right);
|
|
3176
|
+
let previousPending: ManagedGcSessionRetirementReceipt | undefined;
|
|
3177
|
+
let latestPending: ManagedGcSessionRetirementReceipt | undefined;
|
|
3178
|
+
for (let index = 0; index < attempts.length; index++) {
|
|
3179
|
+
const attempt = attempts[index]!;
|
|
3180
|
+
const pending = readState("owner_pending", attempt);
|
|
3181
|
+
if (
|
|
3182
|
+
attempt !== index + 1 ||
|
|
3183
|
+
!pending ||
|
|
3184
|
+
pending.state !== "owner_pending" ||
|
|
3185
|
+
pending.ownerRetirementAttempt !== attempt
|
|
3186
|
+
)
|
|
3187
|
+
throw new Error("task_artifact_owner_continuation_state_missing");
|
|
3188
|
+
assertSameManagedGcImmutableAuthority(prepared, pending);
|
|
3189
|
+
if (previousPending) {
|
|
3190
|
+
const previous = previousPending.taskArtifactOwnerRetirementOutcome;
|
|
3191
|
+
const next = pending.taskArtifactOwnerRetirementOutcome;
|
|
3192
|
+
if (
|
|
3193
|
+
!previous ||
|
|
3194
|
+
previous.kind === "completed" ||
|
|
3195
|
+
!next ||
|
|
3196
|
+
next.kind === "completed" ||
|
|
3197
|
+
!continuationExtendsPrevious(previous.continuation, next.continuation)
|
|
3198
|
+
)
|
|
3199
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
3200
|
+
}
|
|
3201
|
+
previousPending = pending;
|
|
3202
|
+
latestPending = pending;
|
|
3203
|
+
}
|
|
3204
|
+
if ((attempts.length || ownerRetired) && !artifactsRemoved)
|
|
3205
|
+
throw new Error("task_artifact_owner_continuation_state_missing");
|
|
3206
|
+
if (artifactsRemoved) assertSameManagedGcImmutableAuthority(prepared, artifactsRemoved);
|
|
3207
|
+
if (ownerRetired) assertSameManagedGcImmutableAuthority(prepared, ownerRetired);
|
|
3208
|
+
if (ownerRetired) {
|
|
3209
|
+
const outcome = ownerRetired.taskArtifactOwnerRetirementOutcome;
|
|
3210
|
+
if (outcome?.kind !== "completed") throw new Error("task_artifact_owner_continuation_outcome_mismatch");
|
|
3211
|
+
verifyTaskArtifactOwnerPhysicalRetirement(
|
|
3212
|
+
trusted.ownerContext,
|
|
3213
|
+
ownerRetired.taskArtifactOwnerDeletionEvidence,
|
|
3214
|
+
outcome,
|
|
3215
|
+
);
|
|
3216
|
+
}
|
|
3217
|
+
if (!inventory) {
|
|
3218
|
+
const after = managedGcReceiptDirectoryInventory(scope, store);
|
|
3219
|
+
if (!managedGcSameReceiptDirectoryInventory(receiptInventory, after))
|
|
3220
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
3221
|
+
}
|
|
3222
|
+
return { receipt: ownerRetired ?? latestPending ?? artifactsRemoved ?? prepared, prepared, history: history() };
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
function managedGcReceiptNames(
|
|
3226
|
+
scope: ManagedScope,
|
|
3227
|
+
transcriptPath: string,
|
|
3228
|
+
store: ManagedSessionDescendantStore,
|
|
3229
|
+
inventory?: ManagedGcDirectoryInventory,
|
|
3230
|
+
): string[] {
|
|
3231
|
+
const prefix = `${MANAGED_GC_RETIREMENT_PREFIX}${managedGcRetirementTranscriptKey(transcriptPath)}-`;
|
|
3232
|
+
const matching = (inventory ?? managedGcReceiptDirectoryInventory(scope, store)).entries.filter(entry =>
|
|
3233
|
+
entry.name.startsWith(prefix),
|
|
3234
|
+
);
|
|
3235
|
+
if (matching.some(entry => !entry.isFile() || entry.isSymbolicLink()))
|
|
3236
|
+
throw new Error("task_artifact_owner_continuation_corrupt");
|
|
3237
|
+
const names = matching.map(entry => entry.name);
|
|
3238
|
+
const pendingPrefix = `${prefix}owner_pending-`;
|
|
3239
|
+
for (const name of names) {
|
|
3240
|
+
if (
|
|
3241
|
+
name !== `${prefix}prepared.json` &&
|
|
3242
|
+
name !== `${prefix}artifacts_removed.json` &&
|
|
3243
|
+
name !== `${prefix}owner_retired.json` &&
|
|
3244
|
+
!name.startsWith(pendingPrefix)
|
|
3245
|
+
)
|
|
3246
|
+
throw new Error("task_artifact_owner_continuation_corrupt");
|
|
3247
|
+
}
|
|
3248
|
+
return names;
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
function managedGcSameReceiptDirectoryInventory(
|
|
3252
|
+
left: ManagedGcDirectoryInventory,
|
|
3253
|
+
right: ManagedGcDirectoryInventory,
|
|
3254
|
+
): boolean {
|
|
3255
|
+
return (
|
|
3256
|
+
left.entryCount === right.entryCount &&
|
|
3257
|
+
managedGcSameDirectoryEvidence(left.evidence, right.evidence) &&
|
|
3258
|
+
left.entries.length === right.entries.length &&
|
|
3259
|
+
left.entries.every(
|
|
3260
|
+
(entry, index) =>
|
|
3261
|
+
entry.name === right.entries[index]?.name &&
|
|
3262
|
+
entry.isDirectory() === right.entries[index]?.isDirectory() &&
|
|
3263
|
+
entry.isFile() === right.entries[index]?.isFile() &&
|
|
3264
|
+
entry.isSymbolicLink() === right.entries[index]?.isSymbolicLink() &&
|
|
3265
|
+
entry.isBlockDevice() === right.entries[index]?.isBlockDevice() &&
|
|
3266
|
+
entry.isCharacterDevice() === right.entries[index]?.isCharacterDevice() &&
|
|
3267
|
+
entry.isFIFO() === right.entries[index]?.isFIFO() &&
|
|
3268
|
+
entry.isSocket() === right.entries[index]?.isSocket(),
|
|
3269
|
+
)
|
|
3270
|
+
);
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
function managedGcOptionalReceiptDirectoryInventory(
|
|
3274
|
+
scope: ManagedScope,
|
|
3275
|
+
store: ManagedSessionDescendantStore,
|
|
3276
|
+
): ManagedGcDirectoryInventory | undefined {
|
|
3277
|
+
try {
|
|
3278
|
+
store.captureDirectoryIdentity(MANAGED_GC_RETIREMENT_RECEIPTS);
|
|
3279
|
+
} catch (error) {
|
|
3280
|
+
if (hasFsCode(error, "ENOENT")) return undefined;
|
|
3281
|
+
throw error;
|
|
3282
|
+
}
|
|
3283
|
+
return managedGcReceiptDirectoryInventory(scope, store);
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
function managedGcDiscoveryScopeFence(
|
|
3287
|
+
scope: ManagedScope,
|
|
3288
|
+
store: ManagedSessionDescendantStore,
|
|
3289
|
+
scopeIdentity: { readonly dev: string; readonly ino: string },
|
|
3290
|
+
binding: ManagedFileSnapshot,
|
|
3291
|
+
openReader: () => ManagedSessionDescendantStore,
|
|
3292
|
+
): ManagedGcDiscoveryScopeFence {
|
|
3293
|
+
const internalDirectoryIdentity = store.captureDirectoryIdentity(MANAGED_INTERNAL_DIRECTORY);
|
|
3294
|
+
return {
|
|
3295
|
+
scope,
|
|
3296
|
+
scopeIdentity,
|
|
3297
|
+
binding,
|
|
3298
|
+
internalDirectoryIdentity,
|
|
3299
|
+
receiptInventory: managedGcOptionalReceiptDirectoryInventory(scope, store),
|
|
3300
|
+
openReader,
|
|
3301
|
+
};
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
function managedGcAssertDiscoveryScopeFence(fence: ManagedGcDiscoveryScopeFence): void {
|
|
3305
|
+
const store = fence.openReader();
|
|
3306
|
+
try {
|
|
3307
|
+
store.verifyRootSecurity();
|
|
3308
|
+
const currentScope = fs.lstatSync(fence.scope.directoryPath, { bigint: true });
|
|
3309
|
+
if (
|
|
3310
|
+
!currentScope.isDirectory() ||
|
|
3311
|
+
currentScope.isSymbolicLink() ||
|
|
3312
|
+
currentScope.dev.toString() !== fence.scopeIdentity.dev ||
|
|
3313
|
+
currentScope.ino.toString() !== fence.scopeIdentity.ino
|
|
3314
|
+
)
|
|
3315
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3316
|
+
const currentBinding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
|
|
3317
|
+
if (
|
|
3318
|
+
!currentBinding ||
|
|
3319
|
+
!util.isDeepStrictEqual(currentBinding.identity, fence.binding.identity) ||
|
|
3320
|
+
!currentBinding.bytes.equals(fence.binding.bytes)
|
|
3321
|
+
)
|
|
3322
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3323
|
+
const internalDirectoryIdentity = store.captureDirectoryIdentity(MANAGED_INTERNAL_DIRECTORY);
|
|
3324
|
+
if (
|
|
3325
|
+
internalDirectoryIdentity.dev !== fence.internalDirectoryIdentity.dev ||
|
|
3326
|
+
internalDirectoryIdentity.ino !== fence.internalDirectoryIdentity.ino
|
|
3327
|
+
)
|
|
3328
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3329
|
+
const currentReceipts = managedGcOptionalReceiptDirectoryInventory(fence.scope, store);
|
|
3330
|
+
if (
|
|
3331
|
+
(fence.receiptInventory === undefined) !== (currentReceipts === undefined) ||
|
|
3332
|
+
(fence.receiptInventory !== undefined &&
|
|
3333
|
+
currentReceipts !== undefined &&
|
|
3334
|
+
!managedGcSameReceiptDirectoryInventory(fence.receiptInventory, currentReceipts))
|
|
3335
|
+
)
|
|
3336
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
3337
|
+
store.assertBound();
|
|
3338
|
+
} finally {
|
|
3339
|
+
store.close();
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
function assertManagedGcReceiptHistoryDirectoryUnchanged(
|
|
3344
|
+
scope: ManagedScope,
|
|
3345
|
+
store: ManagedSessionDescendantStore,
|
|
3346
|
+
history: ManagedGcReceiptHistory,
|
|
3347
|
+
): void {
|
|
3348
|
+
const current = managedGcReceiptDirectoryInventory(scope, store);
|
|
3349
|
+
if (
|
|
3350
|
+
!managedGcSameReceiptDirectoryInventory(history.directoryInventory, current) ||
|
|
3351
|
+
current.entryCount !== history.directoryEntryCount ||
|
|
3352
|
+
!managedGcSameDirectoryEvidence(current.evidence, history.directoryEvidence)
|
|
3353
|
+
)
|
|
3354
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3357
|
+
async function withManagedGcRetirementJournal<T>(
|
|
3358
|
+
scope: ManagedScope,
|
|
3359
|
+
transcriptPath: string,
|
|
3360
|
+
operation: (
|
|
3361
|
+
trusted: ManagedGcTrustedScope,
|
|
3362
|
+
store: ManagedSessionDescendantStore,
|
|
3363
|
+
lock: ManagedStorageLock,
|
|
3364
|
+
) => Promise<T> | T,
|
|
3365
|
+
): Promise<T> {
|
|
3366
|
+
const trusted = managedGcTrustedScope(scope);
|
|
3367
|
+
validateManagedGcTranscriptPath(scope, transcriptPath);
|
|
3368
|
+
const store = managedGcScopeStore(scope, trusted);
|
|
3369
|
+
let lock: ManagedStorageLock | undefined;
|
|
3370
|
+
let operationError: unknown;
|
|
3371
|
+
let result: T | undefined;
|
|
3372
|
+
let operationCompleted = false;
|
|
3373
|
+
try {
|
|
3374
|
+
store.verifyRootSecurity();
|
|
3375
|
+
store.ensureDirectory(MANAGED_GC_RETIREMENT_RECEIPTS);
|
|
3376
|
+
store.ensureDirectory(MANAGED_GC_RETIREMENT_LOCKS);
|
|
3377
|
+
lock = await acquireManagedLock(
|
|
3378
|
+
path.join(scope.directoryPath, MANAGED_GC_RETIREMENT_LOCKS),
|
|
3379
|
+
MANAGED_GC_RETIREMENT_SCOPE_LOCK_NAME,
|
|
3380
|
+
trusted.root,
|
|
3381
|
+
trusted.policy,
|
|
3382
|
+
);
|
|
3383
|
+
lock.assertOwned();
|
|
3384
|
+
store.assertBound();
|
|
3385
|
+
result = await operation(trusted, store, lock);
|
|
3386
|
+
operationCompleted = true;
|
|
3387
|
+
} catch (error) {
|
|
3388
|
+
operationError = error;
|
|
3389
|
+
}
|
|
3390
|
+
let releaseError: unknown;
|
|
3391
|
+
if (lock) {
|
|
3392
|
+
try {
|
|
3393
|
+
await lock.release();
|
|
3394
|
+
} catch (error) {
|
|
3395
|
+
releaseError = error;
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
store.close();
|
|
3399
|
+
if (operationError !== undefined && releaseError !== undefined)
|
|
3400
|
+
throw new AggregateError(
|
|
3401
|
+
[operationError, releaseError],
|
|
3402
|
+
"managed_gc_retirement_operation_and_lock_release_failed",
|
|
3403
|
+
);
|
|
3404
|
+
if (operationError !== undefined) throw operationError;
|
|
3405
|
+
if (releaseError !== undefined) throw releaseError;
|
|
3406
|
+
if (!operationCompleted) throw new Error("managed_gc_retirement_result_missing");
|
|
3407
|
+
return result as T;
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
/** Read the prepared authority first, then strictly validate every later receipt against it. */
|
|
3411
|
+
export async function readManagedGcSessionRetirementReceipt(
|
|
3412
|
+
scope: ManagedScope,
|
|
3413
|
+
transcriptPath: string,
|
|
3414
|
+
): Promise<ManagedGcSessionRetirementReceipt | undefined> {
|
|
3415
|
+
const result = await withManagedGcRetirementJournal(scope, transcriptPath, (trusted, store, lock) =>
|
|
3416
|
+
readManagedGcSessionRetirementReceiptUnlocked(scope, transcriptPath, trusted, store, lock),
|
|
3417
|
+
);
|
|
3418
|
+
return result.receipt;
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3421
|
+
/** Read an authenticated journal without creating lock/receipt directories or repairing security state. */
|
|
3422
|
+
export async function readManagedGcSessionRetirementReceiptReadOnly(
|
|
3423
|
+
scope: ManagedScope,
|
|
3424
|
+
transcriptPath: string,
|
|
3425
|
+
): Promise<ManagedGcSessionRetirementReceipt | undefined> {
|
|
3426
|
+
const trusted = managedGcTrustedScope(scope);
|
|
3427
|
+
validateManagedGcTranscriptPath(scope, transcriptPath);
|
|
3428
|
+
const store = managedGcScopeReader(scope, trusted);
|
|
3429
|
+
try {
|
|
3430
|
+
store.verifyRootSecurity();
|
|
3431
|
+
store.assertBound();
|
|
3432
|
+
try {
|
|
3433
|
+
store.captureDirectoryIdentity(MANAGED_GC_RETIREMENT_RECEIPTS);
|
|
3434
|
+
} catch (error) {
|
|
3435
|
+
if (hasFsCode(error, "ENOENT")) return undefined;
|
|
3436
|
+
throw error;
|
|
3437
|
+
}
|
|
3438
|
+
return readManagedGcSessionRetirementReceiptUnlocked(scope, transcriptPath, trusted, store).receipt;
|
|
3439
|
+
} finally {
|
|
3440
|
+
store.close();
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
function hasManagedGcRetirementJournalWithoutWorkspace(
|
|
3445
|
+
scope: ManagedScope,
|
|
3446
|
+
onEmptyInventory?: (fence: ManagedGcDiscoveryScopeFence) => void,
|
|
3447
|
+
): boolean {
|
|
3448
|
+
if (
|
|
3449
|
+
path.resolve(scope.canonicalCwd) !== scope.canonicalCwd ||
|
|
3450
|
+
scope.platform !== (process.platform === "win32" ? "win32" : "posix") ||
|
|
3451
|
+
scope.directoryName !== `v2-${scopeDigest(scope.platform, scope.canonicalCwd)}`
|
|
3452
|
+
)
|
|
3453
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3454
|
+
const identity = fs.lstatSync(scope.directoryPath, { bigint: true });
|
|
3455
|
+
if (!identity.isDirectory() || identity.isSymbolicLink()) throw new Error("managed_gc_scope_authority_mismatch");
|
|
3456
|
+
const rootPath = configuredRootPath(scope);
|
|
3457
|
+
const rootIdentity = fs.lstatSync(rootPath, { bigint: true });
|
|
3458
|
+
const assertRoot = (): void => {
|
|
3459
|
+
const security = validateNativeSecurityResult(
|
|
3460
|
+
verifyExistingManagedScopeDirectory(rootPath),
|
|
3461
|
+
"verify",
|
|
3462
|
+
"directory",
|
|
3463
|
+
);
|
|
3464
|
+
const current = fs.lstatSync(rootPath, { bigint: true });
|
|
3465
|
+
if (
|
|
3466
|
+
!security.ok ||
|
|
3467
|
+
!current.isDirectory() ||
|
|
3468
|
+
current.isSymbolicLink() ||
|
|
3469
|
+
current.dev !== rootIdentity.dev ||
|
|
3470
|
+
current.ino !== rootIdentity.ino
|
|
3471
|
+
)
|
|
3472
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3473
|
+
};
|
|
3474
|
+
assertRoot();
|
|
3475
|
+
const rootAuthority = managedDirectoryRoot(rootPath);
|
|
3476
|
+
if (
|
|
3477
|
+
rootAuthority.dev !== BigInt.asUintN(64, rootIdentity.dev) ||
|
|
3478
|
+
rootAuthority.ino !== BigInt.asUintN(64, rootIdentity.ino)
|
|
3479
|
+
)
|
|
3480
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3481
|
+
const store = new ManagedSessionDescendantStore(
|
|
3482
|
+
rootAuthority,
|
|
3483
|
+
scope.directoryPath,
|
|
3484
|
+
undefined,
|
|
3485
|
+
scope.platform === "win32" ? "windows-existing-verify-first" : "default",
|
|
3486
|
+
scope.agentDir,
|
|
3487
|
+
{
|
|
3488
|
+
canonicalPath: scope.directoryPath,
|
|
3489
|
+
dev: BigInt.asUintN(64, identity.dev),
|
|
3490
|
+
ino: BigInt.asUintN(64, identity.ino),
|
|
3491
|
+
},
|
|
3492
|
+
"read-only",
|
|
3493
|
+
);
|
|
3494
|
+
try {
|
|
3495
|
+
store.verifyRootSecurity();
|
|
3496
|
+
const binding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
|
|
3497
|
+
if (!binding || validateBindingRaw(scope, binding.bytes.toString("utf8")))
|
|
3498
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3499
|
+
const readNames = (): ManagedGcDirectoryInventory | undefined => {
|
|
3500
|
+
try {
|
|
3501
|
+
return managedGcReceiptDirectoryInventory(scope, store);
|
|
3502
|
+
} catch (error) {
|
|
3503
|
+
if (hasFsCode(error, "ENOENT")) return undefined;
|
|
3504
|
+
throw error;
|
|
3505
|
+
}
|
|
3506
|
+
};
|
|
3507
|
+
const before = readNames();
|
|
3508
|
+
const currentBinding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
|
|
3509
|
+
const after = readNames();
|
|
3510
|
+
if (
|
|
3511
|
+
!currentBinding ||
|
|
3512
|
+
!util.isDeepStrictEqual(binding.identity, currentBinding.identity) ||
|
|
3513
|
+
!binding.bytes.equals(currentBinding.bytes) ||
|
|
3514
|
+
(before === undefined) !== (after === undefined) ||
|
|
3515
|
+
(before !== undefined && after !== undefined && !managedGcSameReceiptDirectoryInventory(before, after))
|
|
3516
|
+
)
|
|
3517
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3518
|
+
store.verifyRootSecurity();
|
|
3519
|
+
assertRoot();
|
|
3520
|
+
const hasJournal = before?.entries.some(entry => entry.name.startsWith(MANAGED_GC_RETIREMENT_PREFIX)) ?? false;
|
|
3521
|
+
if (!hasJournal && onEmptyInventory) {
|
|
3522
|
+
const internalDirectoryIdentity = store.captureDirectoryIdentity(MANAGED_INTERNAL_DIRECTORY);
|
|
3523
|
+
const policy: ManagedSessionSecurityPolicy =
|
|
3524
|
+
scope.platform === "win32" ? "windows-existing-verify-first" : "default";
|
|
3525
|
+
onEmptyInventory({
|
|
3526
|
+
scope,
|
|
3527
|
+
scopeIdentity: { dev: identity.dev.toString(), ino: identity.ino.toString() },
|
|
3528
|
+
binding,
|
|
3529
|
+
internalDirectoryIdentity,
|
|
3530
|
+
receiptInventory: before,
|
|
3531
|
+
openReader: () =>
|
|
3532
|
+
new ManagedSessionDescendantStore(
|
|
3533
|
+
rootAuthority,
|
|
3534
|
+
scope.directoryPath,
|
|
3535
|
+
undefined,
|
|
3536
|
+
policy,
|
|
3537
|
+
scope.agentDir,
|
|
3538
|
+
{
|
|
3539
|
+
canonicalPath: scope.directoryPath,
|
|
3540
|
+
dev: BigInt.asUintN(64, identity.dev),
|
|
3541
|
+
ino: BigInt.asUintN(64, identity.ino),
|
|
3542
|
+
},
|
|
3543
|
+
"read-only",
|
|
3544
|
+
),
|
|
3545
|
+
});
|
|
3546
|
+
}
|
|
3547
|
+
return hasJournal;
|
|
3548
|
+
} finally {
|
|
3549
|
+
store.close();
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
|
|
3553
|
+
/** Discover complete owner journals under authenticated existing v2 scopes without initializing storage. */
|
|
3554
|
+
export async function discoverManagedGcSessionRetirementReceipts(input: {
|
|
3555
|
+
agentDir: string;
|
|
3556
|
+
sessionsRoot: string;
|
|
3557
|
+
}): Promise<readonly { readonly scope: ManagedScope; readonly receipt: ManagedGcSessionRetirementReceipt }[]> {
|
|
3558
|
+
const agentDir = canonicalizeTrustedPath(input.agentDir);
|
|
3559
|
+
const sessionsRoot = canonicalizeTrustedPath(input.sessionsRoot);
|
|
3560
|
+
const profileStat = fs.lstatSync(agentDir, { bigint: true });
|
|
3561
|
+
const rootStat = fs.lstatSync(sessionsRoot, { bigint: true });
|
|
3562
|
+
for (const [pathname, stat] of [
|
|
3563
|
+
[agentDir, profileStat],
|
|
3564
|
+
[sessionsRoot, rootStat],
|
|
3565
|
+
] as const) {
|
|
3566
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("managed_gc_scope_authority_mismatch");
|
|
3567
|
+
const verified = validateNativeSecurityResult(
|
|
3568
|
+
verifyExistingManagedScopeDirectory(pathname),
|
|
3569
|
+
"verify",
|
|
3570
|
+
"directory",
|
|
3571
|
+
);
|
|
3572
|
+
if (!verified.ok) throw new Error("managed_gc_scope_authority_mismatch");
|
|
3573
|
+
}
|
|
3574
|
+
const inventoryBefore = {
|
|
3575
|
+
dev: rootStat.dev,
|
|
3576
|
+
ino: rootStat.ino,
|
|
3577
|
+
profileDev: profileStat.dev,
|
|
3578
|
+
profileIno: profileStat.ino,
|
|
3579
|
+
};
|
|
3580
|
+
const rootInventory = managedGcStreamDirectoryInventory(
|
|
3581
|
+
sessionsRoot,
|
|
3582
|
+
entry => /^v2-[a-z2-7]{52}$/u.test(entry.name),
|
|
3583
|
+
entry => /^v2-[a-z2-7]{52}$/u.test(entry.name),
|
|
3584
|
+
);
|
|
3585
|
+
if (rootInventory.evidence.dev !== rootStat.dev.toString() || rootInventory.evidence.ino !== rootStat.ino.toString())
|
|
3586
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3587
|
+
const entries = rootInventory.entries;
|
|
3588
|
+
const result: Array<{ readonly scope: ManagedScope; readonly receipt: ManagedGcSessionRetirementReceipt }> = [];
|
|
3589
|
+
const scopeFences: ManagedGcDiscoveryScopeFence[] = [];
|
|
3590
|
+
// Fail closed for the whole root: a partial inventory cannot prove live sibling absence.
|
|
3591
|
+
for (const entry of entries) {
|
|
3592
|
+
if (!/^v2-[a-z2-7]{52}$/u.test(entry.name)) continue;
|
|
3593
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) throw new Error("managed_gc_scope_authority_mismatch");
|
|
3594
|
+
const scopePath = path.join(sessionsRoot, entry.name);
|
|
3595
|
+
const bindingPath = path.join(scopePath, MANAGED_SESSION_BINDING_FILE);
|
|
3596
|
+
const bindingBytes = captureManagedFileNoFollow(bindingPath).bytes;
|
|
3597
|
+
let bindingValue: unknown;
|
|
3598
|
+
try {
|
|
3599
|
+
bindingValue = JSON.parse(bindingBytes.toString("utf8"));
|
|
3600
|
+
} catch {
|
|
3601
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3602
|
+
}
|
|
3603
|
+
if (!isBinding(bindingValue) || bindingValue.identityDigest !== entry.name.slice(3))
|
|
3604
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3605
|
+
const resolved = resolveManagedGcScopeForRead({ cwd: bindingValue.canonicalPath, agentDir, sessionsRoot });
|
|
3606
|
+
if (resolved.kind === "error" && resolved.code === "cwd_missing") {
|
|
3607
|
+
const storedScope: ManagedScope = {
|
|
3608
|
+
apiVersion: 1,
|
|
3609
|
+
layoutVersion: MANAGED_SESSION_LAYOUT_VERSION,
|
|
3610
|
+
identityVersion: MANAGED_SESSION_IDENTITY_VERSION,
|
|
3611
|
+
agentDir,
|
|
3612
|
+
sessionsRoot,
|
|
3613
|
+
canonicalCwd: bindingValue.canonicalPath,
|
|
3614
|
+
legacyLexicalCwd: bindingValue.canonicalPath,
|
|
3615
|
+
directoryName: entry.name,
|
|
3616
|
+
directoryPath: scopePath,
|
|
3617
|
+
platform: bindingValue.platform,
|
|
3618
|
+
};
|
|
3619
|
+
if (
|
|
3620
|
+
!hasManagedGcRetirementJournalWithoutWorkspace(storedScope, fence => {
|
|
3621
|
+
scopeFences.push(fence);
|
|
3622
|
+
})
|
|
3623
|
+
)
|
|
3624
|
+
continue;
|
|
3625
|
+
}
|
|
3626
|
+
if (resolved.kind !== "resolved" || resolved.scope.directoryPath !== scopePath)
|
|
3627
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3628
|
+
const scope = resolved.scope;
|
|
3629
|
+
const trusted = managedGcTrustedScope(scope);
|
|
3630
|
+
const store = managedGcScopeReader(scope, trusted);
|
|
3631
|
+
try {
|
|
3632
|
+
const binding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
|
|
3633
|
+
if (
|
|
3634
|
+
!binding ||
|
|
3635
|
+
validateBindingRaw(scope, binding.bytes.toString("utf8")) ||
|
|
3636
|
+
!binding.bytes.equals(bindingBytes)
|
|
3637
|
+
)
|
|
3638
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3639
|
+
const scopeFence = managedGcDiscoveryScopeFence(
|
|
3640
|
+
scope,
|
|
3641
|
+
store,
|
|
3642
|
+
{ dev: trusted.identity.dev.toString(), ino: trusted.identity.ino.toString() },
|
|
3643
|
+
binding,
|
|
3644
|
+
() => managedGcScopeReader(scope, trusted),
|
|
3645
|
+
);
|
|
3646
|
+
scopeFences.push(scopeFence);
|
|
3647
|
+
const receiptInventory = scopeFence.receiptInventory;
|
|
3648
|
+
if (!receiptInventory) continue;
|
|
3649
|
+
const groups = new Map<string, string[]>();
|
|
3650
|
+
for (const item of receiptInventory.entries) {
|
|
3651
|
+
if (!item.name.startsWith(MANAGED_GC_RETIREMENT_PREFIX)) continue;
|
|
3652
|
+
if (
|
|
3653
|
+
!item.isFile() ||
|
|
3654
|
+
item.isSymbolicLink() ||
|
|
3655
|
+
!/^gc-retirement-[a-f0-9]{64}-(?:prepared|artifacts_removed|owner_retired|owner_pending-[0-9]{8,})\.json$/u.test(
|
|
3656
|
+
item.name,
|
|
3657
|
+
)
|
|
3658
|
+
)
|
|
3659
|
+
throw new Error("task_artifact_owner_continuation_corrupt");
|
|
3660
|
+
const key = item.name.slice(MANAGED_GC_RETIREMENT_PREFIX.length, MANAGED_GC_RETIREMENT_PREFIX.length + 64);
|
|
3661
|
+
let group = groups.get(key);
|
|
3662
|
+
if (!group) {
|
|
3663
|
+
group = [];
|
|
3664
|
+
groups.set(key, group);
|
|
3665
|
+
}
|
|
3666
|
+
group.push(item.name);
|
|
3667
|
+
}
|
|
3668
|
+
for (const [key, group] of groups) {
|
|
3669
|
+
const preparedName = `${MANAGED_GC_RETIREMENT_PREFIX}${key}-prepared.json`;
|
|
3670
|
+
if (!group.includes(preparedName)) throw new Error("task_artifact_owner_continuation_state_missing");
|
|
3671
|
+
const bootstrapHistory: MutableManagedGcReceiptHistory = {
|
|
3672
|
+
encodedBytes: 0,
|
|
3673
|
+
admittedFiles: new Set<string>(),
|
|
3674
|
+
};
|
|
3675
|
+
const preparedFile = readManagedGcReceiptSnapshot(
|
|
3676
|
+
store,
|
|
3677
|
+
`${MANAGED_GC_RETIREMENT_RECEIPTS}/${preparedName}`,
|
|
3678
|
+
bootstrapHistory,
|
|
3679
|
+
);
|
|
3680
|
+
if (!preparedFile) throw new Error("task_artifact_owner_continuation_state_missing");
|
|
3681
|
+
let preparedValue: unknown;
|
|
3682
|
+
try {
|
|
3683
|
+
preparedValue = JSON.parse(preparedFile.bytes.toString("utf8"));
|
|
3684
|
+
} catch {
|
|
3685
|
+
throw new Error("task_artifact_owner_continuation_corrupt");
|
|
3686
|
+
}
|
|
3687
|
+
const transcriptPath =
|
|
3688
|
+
preparedValue && typeof preparedValue === "object"
|
|
3689
|
+
? (preparedValue as Record<string, unknown>).transcriptPath
|
|
3690
|
+
: undefined;
|
|
3691
|
+
if (typeof transcriptPath !== "string" || managedGcRetirementTranscriptKey(transcriptPath) !== key)
|
|
3692
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
3693
|
+
const read = readManagedGcSessionRetirementReceiptUnlocked(
|
|
3694
|
+
scope,
|
|
3695
|
+
transcriptPath,
|
|
3696
|
+
trusted,
|
|
3697
|
+
store,
|
|
3698
|
+
undefined,
|
|
3699
|
+
receiptInventory,
|
|
3700
|
+
group,
|
|
3701
|
+
{ snapshot: preparedFile, value: preparedValue, history: bootstrapHistory },
|
|
3702
|
+
);
|
|
3703
|
+
const receipt = read.receipt;
|
|
3704
|
+
if (!receipt) throw new Error("task_artifact_owner_continuation_state_missing");
|
|
3705
|
+
result.push({ scope, receipt });
|
|
3706
|
+
}
|
|
3707
|
+
const latestInventory = managedGcReceiptDirectoryInventory(scope, store);
|
|
3708
|
+
if (!managedGcSameReceiptDirectoryInventory(receiptInventory, latestInventory))
|
|
3709
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
3710
|
+
} finally {
|
|
3711
|
+
store.close();
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
for (const fence of scopeFences) managedGcAssertDiscoveryScopeFence(fence);
|
|
3715
|
+
const rootInventoryAfter = managedGcStreamDirectoryInventory(
|
|
3716
|
+
sessionsRoot,
|
|
3717
|
+
entry => /^v2-[a-z2-7]{52}$/u.test(entry.name),
|
|
3718
|
+
entry => /^v2-[a-z2-7]{52}$/u.test(entry.name),
|
|
3719
|
+
);
|
|
3720
|
+
if (!managedGcSameReceiptDirectoryInventory(rootInventory, rootInventoryAfter))
|
|
3721
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3722
|
+
const rootAfter = fs.lstatSync(sessionsRoot, { bigint: true });
|
|
3723
|
+
const profileAfter = fs.lstatSync(agentDir, { bigint: true });
|
|
3724
|
+
if (
|
|
3725
|
+
!rootAfter.isDirectory() ||
|
|
3726
|
+
rootAfter.isSymbolicLink() ||
|
|
3727
|
+
rootAfter.dev !== inventoryBefore.dev ||
|
|
3728
|
+
rootAfter.ino !== inventoryBefore.ino ||
|
|
3729
|
+
!profileAfter.isDirectory() ||
|
|
3730
|
+
profileAfter.isSymbolicLink() ||
|
|
3731
|
+
profileAfter.dev !== inventoryBefore.profileDev ||
|
|
3732
|
+
profileAfter.ino !== inventoryBefore.profileIno
|
|
3733
|
+
)
|
|
3734
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3735
|
+
return result.sort((left, right) => left.receipt.transcriptPath.localeCompare(right.receipt.transcriptPath));
|
|
3736
|
+
}
|
|
3737
|
+
|
|
3738
|
+
const MANAGED_GC_PROTOCOL_ROLES: readonly ManagedGcProtocolRole[] = [
|
|
3739
|
+
MANAGED_LOCKS_DIRECTORY,
|
|
3740
|
+
MANAGED_RECEIPTS_DIRECTORY,
|
|
3741
|
+
MANAGED_TOMBSTONES_DIRECTORY,
|
|
3742
|
+
];
|
|
3743
|
+
const MANAGED_GC_PROTOCOL_MAX_ENTRIES = MANAGED_ARTIFACT_MAX_FILES;
|
|
3744
|
+
|
|
3745
|
+
function managedGcProtocolDirectoryStat(pathname: string): fs.BigIntStats {
|
|
3746
|
+
const stat = fs.lstatSync(pathname, { bigint: true });
|
|
3747
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("managed_gc_protocol_directory_invalid");
|
|
3748
|
+
const security = validateNativeSecurityResult(verifyExistingManagedScopeDirectory(pathname), "verify", "directory");
|
|
3749
|
+
if (!security.ok) throw new Error("managed_gc_protocol_directory_invalid");
|
|
3750
|
+
return stat;
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
function managedGcProtocolDirectorySnapshot(
|
|
3754
|
+
scope: ManagedScope,
|
|
3755
|
+
store: ManagedSessionDescendantStore,
|
|
3756
|
+
role: ManagedGcProtocolRole | typeof MANAGED_INTERNAL_DIRECTORY,
|
|
3757
|
+
): { readonly path: string; readonly stat: fs.BigIntStats; readonly names: readonly string[] } {
|
|
3758
|
+
const relativePath = role === MANAGED_INTERNAL_DIRECTORY ? role : `${MANAGED_INTERNAL_DIRECTORY}/${role}`;
|
|
3759
|
+
const pathname = path.join(scope.directoryPath, relativePath);
|
|
3760
|
+
const before = managedGcProtocolDirectoryStat(pathname);
|
|
3761
|
+
const retained = store.captureDirectoryIdentity(relativePath);
|
|
3762
|
+
if (retained.dev !== before.dev.toString() || retained.ino !== before.ino.toString())
|
|
3763
|
+
throw new Error("managed_gc_protocol_directory_identity_mismatch");
|
|
3764
|
+
const inventory = managedGcStreamDirectoryInventory(
|
|
3765
|
+
pathname,
|
|
3766
|
+
entry => role === MANAGED_RECEIPTS_DIRECTORY && entry.name.startsWith(MANAGED_GC_RETIREMENT_PREFIX),
|
|
3767
|
+
() => true,
|
|
3768
|
+
);
|
|
3769
|
+
const names = inventory.entries.map(entry => entry.name);
|
|
3770
|
+
const after = managedGcProtocolDirectoryStat(pathname);
|
|
3771
|
+
if (
|
|
3772
|
+
after.dev !== before.dev ||
|
|
3773
|
+
after.ino !== before.ino ||
|
|
3774
|
+
after.mtimeNs !== before.mtimeNs ||
|
|
3775
|
+
after.ctimeNs !== before.ctimeNs ||
|
|
3776
|
+
after.mode !== before.mode ||
|
|
3777
|
+
!managedGcSameDirectoryEvidence(inventory.evidence, managedGcDirectoryEvidence(before))
|
|
3778
|
+
)
|
|
3779
|
+
throw new Error("managed_gc_protocol_directory_changed");
|
|
3780
|
+
store.assertBound();
|
|
3781
|
+
return { path: pathname, stat: before, names };
|
|
3782
|
+
}
|
|
3783
|
+
|
|
3784
|
+
function managedGcProtocolFileSnapshot(
|
|
3785
|
+
scope: ManagedScope,
|
|
3786
|
+
store: ManagedSessionDescendantStore,
|
|
3787
|
+
relativePath: string,
|
|
3788
|
+
receiptHistories?: Map<string, MutableManagedGcReceiptHistory>,
|
|
3789
|
+
): {
|
|
3790
|
+
readonly snapshot: ManagedFileSnapshot;
|
|
3791
|
+
readonly identity: ManagedGcProtocolFileSnapshot;
|
|
3792
|
+
} {
|
|
3793
|
+
store.assertBound();
|
|
3794
|
+
const pathname = path.join(scope.directoryPath, relativePath);
|
|
3795
|
+
const before = fs.lstatSync(pathname, { bigint: true });
|
|
3796
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n)
|
|
3797
|
+
throw new Error("managed_gc_protocol_file_invalid");
|
|
3798
|
+
const native = nativeScope();
|
|
3799
|
+
const security = validateNativeSecurityResult(
|
|
3800
|
+
process.platform === "win32"
|
|
3801
|
+
? native.verifyOwnerOnlyPathSecurityExpected(pathname, "file", before.dev, before.ino)
|
|
3802
|
+
: native.verifyOwnerOnlyPathSecurity(pathname, "file"),
|
|
3803
|
+
"verify",
|
|
3804
|
+
"file",
|
|
3805
|
+
);
|
|
3806
|
+
if (!security.ok) throw new Error("managed_gc_protocol_file_invalid");
|
|
3807
|
+
let snapshot: ManagedFileSnapshot | null;
|
|
3808
|
+
if (isManagedGcRetirementReceiptPath(relativePath)) {
|
|
3809
|
+
let history: MutableManagedGcReceiptHistory | undefined;
|
|
3810
|
+
if (receiptHistories) {
|
|
3811
|
+
const match =
|
|
3812
|
+
/^gc-retirement-([a-f0-9]{64})-(?:prepared|artifacts_removed|owner_retired|owner_pending-[0-9]{8,})\.json$/u.exec(
|
|
3813
|
+
path.posix.basename(relativePath),
|
|
3814
|
+
);
|
|
3815
|
+
if (!match) throw new Error("task_artifact_owner_continuation_corrupt");
|
|
3816
|
+
history = receiptHistories.get(match[1]!);
|
|
3817
|
+
if (!history) {
|
|
3818
|
+
history = { encodedBytes: 0, admittedFiles: new Set<string>() };
|
|
3819
|
+
receiptHistories.set(match[1]!, history);
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
snapshot = readManagedGcReceiptSnapshot(store, relativePath, history);
|
|
3823
|
+
} else if (isManagedGcCleanupReceiptPath(relativePath)) {
|
|
3824
|
+
try {
|
|
3825
|
+
snapshot = store.readExpectedBounded(relativePath, CLEANUP_RECEIPT_REPLAY_MAX_BYTES);
|
|
3826
|
+
} catch (error) {
|
|
3827
|
+
if (error instanceof Error && error.message === "artifact_capacity_exceeded") cleanupReceiptReplayCapacity();
|
|
3828
|
+
throw error;
|
|
3829
|
+
}
|
|
3830
|
+
} else {
|
|
3831
|
+
snapshot = store.readExpected(relativePath);
|
|
3832
|
+
}
|
|
3833
|
+
if (!snapshot) throw new Error("managed_gc_protocol_file_missing");
|
|
3834
|
+
const after = fs.lstatSync(pathname, { bigint: true });
|
|
3835
|
+
if (
|
|
3836
|
+
!after.isFile() ||
|
|
3837
|
+
after.isSymbolicLink() ||
|
|
3838
|
+
before.dev !== after.dev ||
|
|
3839
|
+
before.ino !== after.ino ||
|
|
3840
|
+
before.nlink !== after.nlink ||
|
|
3841
|
+
before.size !== after.size ||
|
|
3842
|
+
before.mtimeNs !== after.mtimeNs ||
|
|
3843
|
+
before.ctimeNs !== after.ctimeNs ||
|
|
3844
|
+
before.mode !== after.mode ||
|
|
3845
|
+
snapshot.identity.dev !== before.dev ||
|
|
3846
|
+
snapshot.identity.ino !== before.ino ||
|
|
3847
|
+
snapshot.identity.nlink !== before.nlink ||
|
|
3848
|
+
snapshot.identity.size !== Number(before.size) ||
|
|
3849
|
+
snapshot.identity.mtimeNs !== before.mtimeNs ||
|
|
3850
|
+
snapshot.identity.ctimeNs !== before.ctimeNs
|
|
3851
|
+
)
|
|
3852
|
+
throw new Error("managed_gc_protocol_file_changed");
|
|
3853
|
+
store.assertBound();
|
|
3854
|
+
return {
|
|
3855
|
+
snapshot,
|
|
3856
|
+
identity: {
|
|
3857
|
+
name: path.basename(pathname),
|
|
3858
|
+
dev: snapshot.identity.dev.toString(),
|
|
3859
|
+
ino: snapshot.identity.ino.toString(),
|
|
3860
|
+
nlink: snapshot.identity.nlink.toString(),
|
|
3861
|
+
size: snapshot.identity.size,
|
|
3862
|
+
mtimeNs: snapshot.identity.mtimeNs.toString(),
|
|
3863
|
+
ctimeNs: snapshot.identity.ctimeNs.toString(),
|
|
3864
|
+
mode: Number(before.mode & 0o777n),
|
|
3865
|
+
sha256: snapshot.identity.sha256,
|
|
3866
|
+
},
|
|
3867
|
+
};
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
function isManagedGcPendingCleanupReceiptPath(relativePath: string): boolean {
|
|
3871
|
+
const prefix = `${MANAGED_INTERNAL_DIRECTORY}/${MANAGED_TOMBSTONES_DIRECTORY}/`;
|
|
3872
|
+
if (!relativePath.startsWith(prefix)) return false;
|
|
3873
|
+
return /\.cleanup-pending-[1-9][0-9]*\.json$/u.test(relativePath.slice(prefix.length));
|
|
3874
|
+
}
|
|
3875
|
+
|
|
3876
|
+
function isManagedGcCleanupReceiptPath(relativePath: string): boolean {
|
|
3877
|
+
const prefix = `${MANAGED_INTERNAL_DIRECTORY}/${MANAGED_TOMBSTONES_DIRECTORY}/`;
|
|
3878
|
+
if (!relativePath.startsWith(prefix)) return false;
|
|
3879
|
+
const match =
|
|
3880
|
+
/^[a-f0-9]{64}\.[a-f0-9]{64}\.cleanup-(pending|artifacts_removed|completed)-([1-9][0-9]*)\.json$/u.exec(
|
|
3881
|
+
relativePath.slice(prefix.length),
|
|
3882
|
+
);
|
|
3883
|
+
if (!match) return false;
|
|
3884
|
+
const attempt = Number(match[2]);
|
|
3885
|
+
return Number.isSafeInteger(attempt) && String(attempt) === match[2] && (match[1] !== "completed" || attempt === 1);
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
function isManagedGcRetirementReceiptPath(relativePath: string): boolean {
|
|
3889
|
+
return relativePath.startsWith(`${MANAGED_GC_RETIREMENT_RECEIPTS}/${MANAGED_GC_RETIREMENT_PREFIX}`);
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3892
|
+
function isManagedGcCleanupLikeName(name: string): boolean {
|
|
3893
|
+
return name.includes(".cleanup-") && name.endsWith(".json");
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
function managedGcProtocolRecord(
|
|
3897
|
+
bytes: Uint8Array,
|
|
3898
|
+
role: ManagedGcProtocolRole,
|
|
3899
|
+
name: string,
|
|
3900
|
+
): Record<string, unknown> {
|
|
3901
|
+
try {
|
|
3902
|
+
const value: unknown = JSON.parse(Buffer.from(bytes).toString("utf8"));
|
|
3903
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value as Record<string, unknown>;
|
|
3904
|
+
} catch {
|
|
3905
|
+
// A role-specific parser below supplies the bounded protocol location.
|
|
3906
|
+
}
|
|
3907
|
+
throw new Error(`managed_gc_protocol_record_invalid:${role}/${name}`);
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3910
|
+
function managedGcProtocolFileIdentityMatches(
|
|
3911
|
+
left: ManagedGcProtocolFileSnapshot,
|
|
3912
|
+
right: ManagedGcProtocolFileSnapshot,
|
|
3913
|
+
): boolean {
|
|
3914
|
+
return (
|
|
3915
|
+
left.name === right.name &&
|
|
3916
|
+
left.dev === right.dev &&
|
|
3917
|
+
left.ino === right.ino &&
|
|
3918
|
+
left.nlink === right.nlink &&
|
|
3919
|
+
left.size === right.size &&
|
|
3920
|
+
left.mtimeNs === right.mtimeNs &&
|
|
3921
|
+
left.ctimeNs === right.ctimeNs &&
|
|
3922
|
+
left.mode === right.mode &&
|
|
3923
|
+
left.sha256 === right.sha256
|
|
3924
|
+
);
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
function managedGcProtocolPathIdentity(pathname: string): ManagedGcProtocolPathIdentity {
|
|
3928
|
+
const stat = managedGcProtocolDirectoryStat(pathname);
|
|
3929
|
+
return { path: pathname, dev: stat.dev.toString(), ino: stat.ino.toString() };
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3932
|
+
function validateManagedGcProtocolMigrationReceipt(
|
|
3933
|
+
scope: ManagedScope,
|
|
3934
|
+
pathname: string,
|
|
3935
|
+
name: string,
|
|
3936
|
+
bytes: Uint8Array,
|
|
3937
|
+
): void {
|
|
3938
|
+
const match = /^([a-f0-9]{64})(?:\.(prepared|detached|published))?\.json$/u.exec(name);
|
|
3939
|
+
if (!match || match[2] !== undefined) throw new Error("managed_gc_protocol_migration_incomplete");
|
|
3940
|
+
const record = managedGcProtocolRecord(bytes, MANAGED_RECEIPTS_DIRECTORY, name);
|
|
3941
|
+
if (record.state !== "committed") throw new Error("managed_gc_protocol_migration_incomplete");
|
|
3942
|
+
const source = record.source as { path?: unknown } | undefined;
|
|
3943
|
+
const destination = record.destination as { path?: unknown } | undefined;
|
|
3944
|
+
if (typeof source?.path !== "string" || typeof destination?.path !== "string")
|
|
3945
|
+
throw new Error("managed_gc_protocol_migration_invalid");
|
|
3946
|
+
const legacy = inspectCandidate(source.path, "legacy");
|
|
3947
|
+
const managed = inspectCandidate(destination.path, "v2");
|
|
3948
|
+
if ("code" in legacy || "code" in managed || !receiptMatches(pathname, legacy, managed, scope))
|
|
3949
|
+
throw new Error("managed_gc_protocol_migration_authority_mismatch");
|
|
3950
|
+
}
|
|
3951
|
+
|
|
3952
|
+
function validateManagedGcProtocolTombstoneFile(
|
|
3953
|
+
scope: ManagedScope,
|
|
3954
|
+
pathname: string,
|
|
3955
|
+
name: string,
|
|
3956
|
+
bytes: Uint8Array,
|
|
3957
|
+
journals: readonly { readonly scope: ManagedScope; readonly receipt: ManagedGcSessionRetirementReceipt }[],
|
|
3958
|
+
): void {
|
|
3959
|
+
if (/^[a-f0-9]{64}\.json$/u.test(name)) {
|
|
3960
|
+
const targets = retiredTargets(scope, pathname);
|
|
3961
|
+
if (!targets || targets.length === 0) throw new Error("managed_gc_protocol_tombstone_invalid");
|
|
3962
|
+
return;
|
|
3963
|
+
}
|
|
3964
|
+
const match =
|
|
3965
|
+
/^([a-f0-9]{64})\.([a-f0-9]{64})\.cleanup-(pending|artifacts_removed|completed)-([1-9][0-9]*)\.json$/u.exec(name);
|
|
3966
|
+
if (!match || (match[3] === "completed" && match[4] !== "1"))
|
|
3967
|
+
throw new Error("managed_gc_protocol_tombstone_role_invalid");
|
|
3968
|
+
const tombstone = path.join(path.dirname(pathname), `${match[1]}.json`);
|
|
3969
|
+
const targets = retiredTargets(scope, tombstone);
|
|
3970
|
+
if (!targets) throw new Error("managed_gc_protocol_tombstone_authority_missing");
|
|
3971
|
+
const target = targets.find(candidate => stableOperationName(candidate) === match[2]);
|
|
3972
|
+
if (!target) throw new Error("managed_gc_protocol_tombstone_target_missing");
|
|
3973
|
+
const attempt = Number(match[4]);
|
|
3974
|
+
if (match[3] === "pending") {
|
|
3975
|
+
const pending = pendingCleanupReceipt(scope, tombstone, target);
|
|
3976
|
+
if (!pending || pending.attempt < attempt) throw new Error("managed_gc_protocol_tombstone_history_invalid");
|
|
3977
|
+
return;
|
|
3978
|
+
}
|
|
3979
|
+
if (match[3] === "artifacts_removed") {
|
|
3980
|
+
if (!cleanupArtifactsRemovedEvidence(scope, tombstone, target, attempt))
|
|
3981
|
+
throw new Error("managed_gc_protocol_tombstone_artifacts_invalid");
|
|
3982
|
+
return;
|
|
3983
|
+
}
|
|
3984
|
+
const ownerReceipt = target.taskArtifactOwnerDeletionEvidence
|
|
3985
|
+
? journals.find(
|
|
3986
|
+
item => item.scope.directoryPath === scope.directoryPath && item.receipt.transcriptPath === target.path,
|
|
3987
|
+
)?.receipt
|
|
3988
|
+
: undefined;
|
|
3989
|
+
if (target.taskArtifactOwnerDeletionEvidence && !ownerReceipt)
|
|
3990
|
+
throw new Error("task_artifact_owner_continuation_state_missing");
|
|
3991
|
+
const record = managedGcProtocolRecord(bytes, MANAGED_TOMBSTONES_DIRECTORY, name);
|
|
3992
|
+
if (!cleanupCompletedRecordMatches(scope, tombstone, target, ownerReceipt, record))
|
|
3993
|
+
throw new Error("managed_gc_protocol_tombstone_completion_invalid");
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
/** Inspect under a live operation lease; returned snapshots never authorize effects. */
|
|
3997
|
+
export function managedGcProtocolInspectorForLock(
|
|
3998
|
+
scope: ManagedScope,
|
|
3999
|
+
lock: ManagedStorageLock,
|
|
4000
|
+
): ManagedGcProtocolScopeInspector {
|
|
4001
|
+
const inspect = managedGcProtocolScopeInspectorForScope(scope);
|
|
4002
|
+
return async inputs => {
|
|
4003
|
+
lock.assertOwned();
|
|
4004
|
+
managedGcTrustedScope(scope);
|
|
4005
|
+
const snapshots = await inspect(inputs);
|
|
4006
|
+
lock.assertOwned();
|
|
4007
|
+
managedGcTrustedScope(scope);
|
|
4008
|
+
return snapshots;
|
|
4009
|
+
};
|
|
4010
|
+
}
|
|
4011
|
+
|
|
4012
|
+
/** Bind a read-only protocol inventory to an already prepared scope's root/profile/session identities. */
|
|
4013
|
+
export function managedGcProtocolScopeInspectorForScope(scope: ManagedScope): ManagedGcProtocolScopeInspector {
|
|
4014
|
+
const trusted = managedGcTrustedScope(scope);
|
|
4015
|
+
const configuredRoot = managedGcProtocolPathIdentity(trusted.root.canonicalPath);
|
|
4016
|
+
const profile = managedGcProtocolPathIdentity(scope.agentDir);
|
|
4017
|
+
const sessionsRoot = managedGcProtocolPathIdentity(scope.sessionsRoot);
|
|
4018
|
+
const protocol = managedGcProtocolPathIdentity(path.join(scope.directoryPath, MANAGED_INTERNAL_DIRECTORY));
|
|
4019
|
+
const assertAuthority = (): void => {
|
|
4020
|
+
managedGcTrustedScope(scope);
|
|
4021
|
+
for (const expected of [configuredRoot, profile, sessionsRoot, protocol]) {
|
|
4022
|
+
const current = managedGcProtocolPathIdentity(expected.path);
|
|
4023
|
+
if (current.dev !== expected.dev || current.ino !== expected.ino)
|
|
4024
|
+
throw new Error("managed_gc_protocol_root_identity_mismatch");
|
|
4025
|
+
}
|
|
4026
|
+
};
|
|
4027
|
+
return async (inputs: readonly ManagedGcProtocolScopeInput[]) => {
|
|
4028
|
+
assertAuthority();
|
|
4029
|
+
const journals = await discoverManagedGcSessionRetirementReceipts({
|
|
4030
|
+
agentDir: scope.agentDir,
|
|
4031
|
+
sessionsRoot: scope.sessionsRoot,
|
|
4032
|
+
});
|
|
4033
|
+
assertAuthority();
|
|
4034
|
+
if (inputs.length > MANAGED_GC_PROTOCOL_MAX_ENTRIES) throw new Error("managed_gc_protocol_scope_limit_exceeded");
|
|
4035
|
+
const snapshots: ManagedGcProtocolScopeSnapshot[] = [];
|
|
4036
|
+
let totalFiles = 0;
|
|
4037
|
+
for (const input of inputs) {
|
|
4038
|
+
if (
|
|
4039
|
+
path.resolve(input.scopePath) !== input.scopePath ||
|
|
4040
|
+
path.dirname(input.scopePath) !== scope.sessionsRoot ||
|
|
4041
|
+
!/^v2-[a-z2-7]{52}$/u.test(path.basename(input.scopePath)) ||
|
|
4042
|
+
input.scopeIdentity.path !== input.scopePath ||
|
|
4043
|
+
input.protocolIdentity.path !== path.join(input.scopePath, MANAGED_INTERNAL_DIRECTORY)
|
|
4044
|
+
)
|
|
4045
|
+
throw new Error("managed_gc_protocol_scope_path_mismatch");
|
|
4046
|
+
const bindingPath = path.join(input.scopePath, MANAGED_SESSION_BINDING_FILE);
|
|
4047
|
+
const binding = captureManagedFileNoFollow(bindingPath);
|
|
4048
|
+
const bindingStat = fs.lstatSync(bindingPath, { bigint: true });
|
|
4049
|
+
const bindingIdentity: ManagedGcProtocolFileSnapshot = {
|
|
4050
|
+
name: MANAGED_SESSION_BINDING_FILE,
|
|
4051
|
+
dev: binding.identity.dev.toString(),
|
|
4052
|
+
ino: binding.identity.ino.toString(),
|
|
4053
|
+
nlink: binding.identity.nlink.toString(),
|
|
4054
|
+
size: binding.identity.size,
|
|
4055
|
+
mtimeNs: binding.identity.mtimeNs.toString(),
|
|
4056
|
+
ctimeNs: binding.identity.ctimeNs.toString(),
|
|
4057
|
+
mode: Number(bindingStat.mode & 0o777n),
|
|
4058
|
+
sha256: binding.identity.sha256,
|
|
4059
|
+
};
|
|
4060
|
+
if (!managedGcProtocolFileIdentityMatches(bindingIdentity, input.bindingIdentity))
|
|
4061
|
+
throw new Error("managed_gc_protocol_binding_identity_mismatch");
|
|
4062
|
+
const bindingValue: unknown = JSON.parse(binding.bytes.toString("utf8"));
|
|
4063
|
+
if (!isBinding(bindingValue)) throw new Error("managed_gc_scope_authority_mismatch");
|
|
4064
|
+
const resolved = resolveManagedGcScopeForRead({
|
|
4065
|
+
cwd: bindingValue.canonicalPath,
|
|
4066
|
+
agentDir: scope.agentDir,
|
|
4067
|
+
sessionsRoot: scope.sessionsRoot,
|
|
4068
|
+
});
|
|
4069
|
+
let candidateScope: ManagedScope;
|
|
4070
|
+
let candidateTrusted: ManagedGcTrustedScope | undefined;
|
|
4071
|
+
if (resolved.kind === "error" && resolved.code === "cwd_missing") {
|
|
4072
|
+
candidateScope = {
|
|
4073
|
+
apiVersion: 1,
|
|
4074
|
+
layoutVersion: MANAGED_SESSION_LAYOUT_VERSION,
|
|
4075
|
+
identityVersion: MANAGED_SESSION_IDENTITY_VERSION,
|
|
4076
|
+
agentDir: scope.agentDir,
|
|
4077
|
+
sessionsRoot: scope.sessionsRoot,
|
|
4078
|
+
canonicalCwd: bindingValue.canonicalPath,
|
|
4079
|
+
legacyLexicalCwd: bindingValue.canonicalPath,
|
|
4080
|
+
directoryName: path.basename(input.scopePath),
|
|
4081
|
+
directoryPath: input.scopePath,
|
|
4082
|
+
platform: bindingValue.platform,
|
|
4083
|
+
};
|
|
4084
|
+
if (hasManagedGcRetirementJournalWithoutWorkspace(candidateScope))
|
|
4085
|
+
throw new Error("managed_gc_protocol_scope_unrecognized");
|
|
4086
|
+
} else if (resolved.kind === "resolved" && resolved.scope.directoryPath === input.scopePath) {
|
|
4087
|
+
candidateScope = resolved.scope;
|
|
4088
|
+
candidateTrusted = managedGcTrustedScope(candidateScope);
|
|
4089
|
+
} else {
|
|
4090
|
+
throw new Error("managed_gc_protocol_scope_unrecognized");
|
|
4091
|
+
}
|
|
4092
|
+
if (
|
|
4093
|
+
configuredRootPath(candidateScope) !== trusted.root.canonicalPath ||
|
|
4094
|
+
(candidateTrusted &&
|
|
4095
|
+
(candidateTrusted.root.canonicalPath !== trusted.root.canonicalPath ||
|
|
4096
|
+
candidateTrusted.root.dev !== trusted.root.dev ||
|
|
4097
|
+
candidateTrusted.root.ino !== trusted.root.ino)) ||
|
|
4098
|
+
candidateScope.agentDir !== scope.agentDir ||
|
|
4099
|
+
candidateScope.sessionsRoot !== scope.sessionsRoot
|
|
4100
|
+
)
|
|
4101
|
+
throw new Error("managed_gc_protocol_root_mismatch");
|
|
4102
|
+
const scopeIdentity = managedGcProtocolPathIdentity(candidateScope.directoryPath);
|
|
4103
|
+
if (scopeIdentity.dev !== input.scopeIdentity.dev || scopeIdentity.ino !== input.scopeIdentity.ino)
|
|
4104
|
+
throw new Error("managed_gc_protocol_scope_identity_mismatch");
|
|
4105
|
+
const store = candidateTrusted
|
|
4106
|
+
? managedGcScopeReader(candidateScope, candidateTrusted)
|
|
4107
|
+
: new ManagedSessionDescendantStore(
|
|
4108
|
+
trusted.root,
|
|
4109
|
+
candidateScope.directoryPath,
|
|
4110
|
+
undefined,
|
|
4111
|
+
trusted.policy,
|
|
4112
|
+
candidateScope.agentDir,
|
|
4113
|
+
{
|
|
4114
|
+
canonicalPath: candidateScope.directoryPath,
|
|
4115
|
+
dev: BigInt(scopeIdentity.dev),
|
|
4116
|
+
ino: BigInt(scopeIdentity.ino),
|
|
4117
|
+
},
|
|
4118
|
+
"read-only",
|
|
4119
|
+
);
|
|
4120
|
+
try {
|
|
4121
|
+
store.verifyRootSecurity();
|
|
4122
|
+
const checkedBinding = managedGcProtocolFileSnapshot(candidateScope, store, MANAGED_SESSION_BINDING_FILE);
|
|
4123
|
+
if (!managedGcProtocolFileIdentityMatches(checkedBinding.identity, input.bindingIdentity))
|
|
4124
|
+
throw new Error("managed_gc_protocol_binding_identity_mismatch");
|
|
4125
|
+
const protocolDirectory = managedGcProtocolDirectorySnapshot(
|
|
4126
|
+
candidateScope,
|
|
4127
|
+
store,
|
|
4128
|
+
MANAGED_INTERNAL_DIRECTORY,
|
|
4129
|
+
);
|
|
4130
|
+
if (
|
|
4131
|
+
protocolDirectory.path !== input.protocolIdentity.path ||
|
|
4132
|
+
protocolDirectory.stat.dev.toString() !== input.protocolIdentity.dev ||
|
|
4133
|
+
protocolDirectory.stat.ino.toString() !== input.protocolIdentity.ino ||
|
|
4134
|
+
protocolDirectory.stat.mtimeNs.toString() !== input.protocolIdentity.mtimeNs ||
|
|
4135
|
+
protocolDirectory.stat.ctimeNs.toString() !== input.protocolIdentity.ctimeNs ||
|
|
4136
|
+
Number(protocolDirectory.stat.mode & 0o777n) !== input.protocolIdentity.mode ||
|
|
4137
|
+
protocolDirectory.names.length !== MANAGED_GC_PROTOCOL_ROLES.length ||
|
|
4138
|
+
MANAGED_GC_PROTOCOL_ROLES.some(role => !protocolDirectory.names.includes(role))
|
|
4139
|
+
)
|
|
4140
|
+
throw new Error("managed_gc_protocol_roles_invalid");
|
|
4141
|
+
const directories: ManagedGcProtocolDirectorySnapshot[] = [];
|
|
4142
|
+
const receiptHistories = new Map<string, MutableManagedGcReceiptHistory>();
|
|
4143
|
+
for (const role of MANAGED_GC_PROTOCOL_ROLES) {
|
|
4144
|
+
const directory = managedGcProtocolDirectorySnapshot(candidateScope, store, role);
|
|
4145
|
+
const files: ManagedGcProtocolFileSnapshot[] = [];
|
|
4146
|
+
for (const name of directory.names) {
|
|
4147
|
+
if (!name || path.basename(name) !== name || name.includes(".jsonl"))
|
|
4148
|
+
throw new Error(`managed_gc_protocol_entry_invalid:${role}/${name}`);
|
|
4149
|
+
const relative = `${MANAGED_INTERNAL_DIRECTORY}/${role}/${name}`;
|
|
4150
|
+
const cleanupLike = role === MANAGED_TOMBSTONES_DIRECTORY && isManagedGcCleanupLikeName(name);
|
|
4151
|
+
if (cleanupLike && !isManagedGcCleanupReceiptPath(relative))
|
|
4152
|
+
throw new Error("managed_gc_protocol_tombstone_role_invalid");
|
|
4153
|
+
const pendingCleanup =
|
|
4154
|
+
role === MANAGED_TOMBSTONES_DIRECTORY && isManagedGcPendingCleanupReceiptPath(relative);
|
|
4155
|
+
let captured: ReturnType<typeof managedGcProtocolFileSnapshot>;
|
|
4156
|
+
try {
|
|
4157
|
+
if (pendingCleanup)
|
|
4158
|
+
validateManagedGcProtocolTombstoneFile(
|
|
4159
|
+
candidateScope,
|
|
4160
|
+
path.join(directory.path, name),
|
|
4161
|
+
name,
|
|
4162
|
+
new Uint8Array(),
|
|
4163
|
+
journals,
|
|
4164
|
+
);
|
|
4165
|
+
captured = managedGcProtocolFileSnapshot(candidateScope, store, relative, receiptHistories);
|
|
4166
|
+
if (role === MANAGED_LOCKS_DIRECTORY) {
|
|
4167
|
+
if (isManagedLockQuarantineName(name)) {
|
|
4168
|
+
if (
|
|
4169
|
+
captured.snapshot.bytes.byteLength !== 0 ||
|
|
4170
|
+
captured.identity.size !== 0 ||
|
|
4171
|
+
captured.identity.sha256 !== createHash("sha256").digest("hex")
|
|
4172
|
+
)
|
|
4173
|
+
throw new Error("managed_gc_protocol_lock_quarantine_invalid");
|
|
4174
|
+
} else {
|
|
4175
|
+
if (!/^(?:[a-f0-9]{64}|gc-retirement-[a-f0-9]{64})\.lock$/u.test(name))
|
|
4176
|
+
throw new Error("managed_gc_protocol_lock_role_invalid");
|
|
4177
|
+
if (!parseManagedLockRecord(captured.snapshot.bytes))
|
|
4178
|
+
throw new Error("managed_gc_protocol_lock_invalid");
|
|
4179
|
+
}
|
|
4180
|
+
} else if (role === MANAGED_RECEIPTS_DIRECTORY) {
|
|
4181
|
+
if (
|
|
4182
|
+
/^gc-retirement-[a-f0-9]{64}-(?:prepared|artifacts_removed|owner_retired|owner_pending-[0-9]{8,})\.json$/u.test(
|
|
4183
|
+
name,
|
|
4184
|
+
)
|
|
4185
|
+
) {
|
|
4186
|
+
const key = name.slice(
|
|
4187
|
+
MANAGED_GC_RETIREMENT_PREFIX.length,
|
|
4188
|
+
MANAGED_GC_RETIREMENT_PREFIX.length + 64,
|
|
4189
|
+
);
|
|
4190
|
+
if (
|
|
4191
|
+
!journals.some(
|
|
4192
|
+
item =>
|
|
4193
|
+
item.scope.directoryPath === candidateScope.directoryPath &&
|
|
4194
|
+
managedGcRetirementTranscriptKey(item.receipt.transcriptPath) === key,
|
|
4195
|
+
)
|
|
4196
|
+
)
|
|
4197
|
+
throw new Error("task_artifact_owner_continuation_state_missing");
|
|
4198
|
+
} else {
|
|
4199
|
+
validateManagedGcProtocolMigrationReceipt(
|
|
4200
|
+
candidateScope,
|
|
4201
|
+
path.join(directory.path, name),
|
|
4202
|
+
name,
|
|
4203
|
+
captured.snapshot.bytes,
|
|
4204
|
+
);
|
|
4205
|
+
}
|
|
4206
|
+
} else {
|
|
4207
|
+
if (!pendingCleanup)
|
|
4208
|
+
validateManagedGcProtocolTombstoneFile(
|
|
4209
|
+
candidateScope,
|
|
4210
|
+
path.join(directory.path, name),
|
|
4211
|
+
name,
|
|
4212
|
+
captured.snapshot.bytes,
|
|
4213
|
+
journals,
|
|
4214
|
+
);
|
|
4215
|
+
}
|
|
4216
|
+
} catch (error) {
|
|
4217
|
+
if (isManagedGcJournalCapacityError(error)) throw error;
|
|
4218
|
+
const detail = error instanceof Error ? error.message : "unknown";
|
|
4219
|
+
throw new Error(`${detail} [${role}/${name}]`);
|
|
4220
|
+
}
|
|
4221
|
+
files.push(captured.identity);
|
|
4222
|
+
totalFiles++;
|
|
4223
|
+
if (totalFiles > MANAGED_GC_PROTOCOL_MAX_ENTRIES)
|
|
4224
|
+
throw new Error("managed_gc_protocol_entry_limit_exceeded");
|
|
4225
|
+
}
|
|
4226
|
+
const finalDirectory = managedGcProtocolDirectorySnapshot(candidateScope, store, role);
|
|
4227
|
+
if (
|
|
4228
|
+
finalDirectory.stat.dev !== directory.stat.dev ||
|
|
4229
|
+
finalDirectory.stat.ino !== directory.stat.ino ||
|
|
4230
|
+
finalDirectory.stat.mtimeNs !== directory.stat.mtimeNs ||
|
|
4231
|
+
finalDirectory.stat.ctimeNs !== directory.stat.ctimeNs ||
|
|
4232
|
+
finalDirectory.stat.mode !== directory.stat.mode ||
|
|
4233
|
+
finalDirectory.names.length !== directory.names.length ||
|
|
4234
|
+
finalDirectory.names.some((name, index) => name !== directory.names[index])
|
|
4235
|
+
)
|
|
4236
|
+
throw new Error("managed_gc_protocol_directory_changed");
|
|
4237
|
+
directories.push({
|
|
4238
|
+
role,
|
|
4239
|
+
path: directory.path,
|
|
4240
|
+
dev: directory.stat.dev.toString(),
|
|
4241
|
+
ino: directory.stat.ino.toString(),
|
|
4242
|
+
mtimeNs: directory.stat.mtimeNs.toString(),
|
|
4243
|
+
ctimeNs: directory.stat.ctimeNs.toString(),
|
|
4244
|
+
mode: Number(directory.stat.mode & 0o777n),
|
|
4245
|
+
files,
|
|
4246
|
+
});
|
|
4247
|
+
}
|
|
4248
|
+
const finalProtocol = managedGcProtocolDirectorySnapshot(candidateScope, store, MANAGED_INTERNAL_DIRECTORY);
|
|
4249
|
+
if (
|
|
4250
|
+
finalProtocol.stat.dev !== protocolDirectory.stat.dev ||
|
|
4251
|
+
finalProtocol.stat.ino !== protocolDirectory.stat.ino ||
|
|
4252
|
+
finalProtocol.stat.mtimeNs !== protocolDirectory.stat.mtimeNs ||
|
|
4253
|
+
finalProtocol.stat.ctimeNs !== protocolDirectory.stat.ctimeNs ||
|
|
4254
|
+
finalProtocol.stat.mode !== protocolDirectory.stat.mode ||
|
|
4255
|
+
finalProtocol.names.length !== protocolDirectory.names.length ||
|
|
4256
|
+
finalProtocol.names.some((name, index) => name !== protocolDirectory.names[index])
|
|
4257
|
+
)
|
|
4258
|
+
throw new Error("managed_gc_protocol_tree_changed");
|
|
4259
|
+
const comparisonReceiptHistories = new Map<string, MutableManagedGcReceiptHistory>();
|
|
4260
|
+
for (const directory of directories) {
|
|
4261
|
+
const relative = `${MANAGED_INTERNAL_DIRECTORY}/${directory.role}`;
|
|
4262
|
+
const currentDirectory = managedGcProtocolDirectorySnapshot(candidateScope, store, directory.role);
|
|
4263
|
+
const names = currentDirectory.names;
|
|
4264
|
+
if (
|
|
4265
|
+
currentDirectory.stat.dev !== BigInt(directory.dev) ||
|
|
4266
|
+
currentDirectory.stat.ino !== BigInt(directory.ino) ||
|
|
4267
|
+
currentDirectory.stat.mtimeNs !== BigInt(directory.mtimeNs) ||
|
|
4268
|
+
currentDirectory.stat.ctimeNs !== BigInt(directory.ctimeNs) ||
|
|
4269
|
+
names.length !== directory.files.length ||
|
|
4270
|
+
names.some((name, index) => name !== directory.files[index]?.name)
|
|
4271
|
+
)
|
|
4272
|
+
throw new Error("managed_gc_protocol_directory_changed");
|
|
4273
|
+
for (const expected of directory.files) {
|
|
4274
|
+
const actual = managedGcProtocolFileSnapshot(
|
|
4275
|
+
candidateScope,
|
|
4276
|
+
store,
|
|
4277
|
+
`${relative}/${expected.name}`,
|
|
4278
|
+
comparisonReceiptHistories,
|
|
4279
|
+
);
|
|
4280
|
+
if (!managedGcProtocolFileIdentityMatches(actual.identity, expected))
|
|
4281
|
+
throw new Error("managed_gc_protocol_file_changed");
|
|
4282
|
+
}
|
|
4283
|
+
const current = managedGcProtocolDirectoryStat(directory.path);
|
|
4284
|
+
if (
|
|
4285
|
+
current.dev.toString() !== directory.dev ||
|
|
4286
|
+
current.ino.toString() !== directory.ino ||
|
|
4287
|
+
current.mtimeNs.toString() !== directory.mtimeNs ||
|
|
4288
|
+
current.ctimeNs.toString() !== directory.ctimeNs ||
|
|
4289
|
+
Number(current.mode & 0o777n) !== directory.mode
|
|
4290
|
+
)
|
|
4291
|
+
throw new Error("managed_gc_protocol_directory_changed");
|
|
4292
|
+
}
|
|
4293
|
+
const finalBinding = managedGcProtocolFileSnapshot(candidateScope, store, MANAGED_SESSION_BINDING_FILE);
|
|
4294
|
+
if (!managedGcProtocolFileIdentityMatches(finalBinding.identity, input.bindingIdentity))
|
|
4295
|
+
throw new Error("managed_gc_protocol_binding_changed");
|
|
4296
|
+
snapshots.push({
|
|
4297
|
+
configuredRoot,
|
|
4298
|
+
profile,
|
|
4299
|
+
sessionsRoot,
|
|
4300
|
+
scope: scopeIdentity,
|
|
4301
|
+
binding: bindingIdentity,
|
|
4302
|
+
protocol: {
|
|
4303
|
+
path: protocolDirectory.path,
|
|
4304
|
+
dev: protocolDirectory.stat.dev.toString(),
|
|
4305
|
+
ino: protocolDirectory.stat.ino.toString(),
|
|
4306
|
+
},
|
|
4307
|
+
protocolMtimeNs: protocolDirectory.stat.mtimeNs.toString(),
|
|
4308
|
+
protocolCtimeNs: protocolDirectory.stat.ctimeNs.toString(),
|
|
4309
|
+
protocolMode: Number(protocolDirectory.stat.mode & 0o777n),
|
|
4310
|
+
directories,
|
|
4311
|
+
});
|
|
4312
|
+
} finally {
|
|
4313
|
+
store.close();
|
|
4314
|
+
}
|
|
4315
|
+
}
|
|
4316
|
+
const latestJournals = await discoverManagedGcSessionRetirementReceipts({
|
|
4317
|
+
agentDir: scope.agentDir,
|
|
4318
|
+
sessionsRoot: scope.sessionsRoot,
|
|
4319
|
+
});
|
|
4320
|
+
if (!deepSame(latestJournals, journals)) throw new Error("managed_gc_protocol_journal_changed");
|
|
4321
|
+
assertAuthority();
|
|
4322
|
+
return snapshots;
|
|
4323
|
+
};
|
|
4324
|
+
}
|
|
4325
|
+
|
|
4326
|
+
function managedGcReceiptForPublication(
|
|
4327
|
+
scope: ManagedScope,
|
|
4328
|
+
receipt: ManagedGcSessionRetirementReceipt,
|
|
4329
|
+
ownerContext: TaskArtifactOwnerStorageContext,
|
|
4330
|
+
attempt?: number,
|
|
4331
|
+
history?: ManagedGcReceiptHistory,
|
|
4332
|
+
): {
|
|
4333
|
+
readonly target: ManagedGcSessionRetirementTarget;
|
|
4334
|
+
readonly record: Record<string, unknown>;
|
|
4335
|
+
readonly parsed: ManagedGcSessionRetirementReceipt;
|
|
4336
|
+
} {
|
|
4337
|
+
validateManagedGcTranscriptPath(scope, receipt.transcriptPath);
|
|
4338
|
+
if (
|
|
4339
|
+
history &&
|
|
4340
|
+
(history.entryCount >= MANAGED_ARTIFACT_MAX_FILES || history.directoryEntryCount >= MANAGED_ARTIFACT_MAX_FILES)
|
|
4341
|
+
)
|
|
4342
|
+
managedGcJournalCapacity();
|
|
4343
|
+
const candidateRecord = managedGcReceiptRecord(scope, receipt, attempt);
|
|
4344
|
+
const candidateByteLength = managedGcJsonByteLength(candidateRecord);
|
|
4345
|
+
if (history && candidateByteLength > MANAGED_ARTIFACT_MAX_TOTAL_BYTES - history.encodedBytes)
|
|
4346
|
+
managedGcJournalCapacity();
|
|
4347
|
+
const parsed = parseManagedGcRetirementReceipt(
|
|
4348
|
+
candidateRecord,
|
|
4349
|
+
scopeDigest(scope.platform, scope.canonicalCwd),
|
|
4350
|
+
ownerContext,
|
|
4351
|
+
receipt,
|
|
4352
|
+
);
|
|
4353
|
+
const record = managedGcReceiptRecord(scope, parsed, attempt);
|
|
4354
|
+
const encodedByteLength = managedGcJsonByteLength(record);
|
|
4355
|
+
if (encodedByteLength !== candidateByteLength) throw new Error("managed_gc_receipt_encoding_mismatch");
|
|
4356
|
+
return { target: receipt, record, parsed };
|
|
4357
|
+
}
|
|
4358
|
+
|
|
4359
|
+
function publishManagedGcReceiptNoReplace(
|
|
4360
|
+
store: ManagedSessionDescendantStore,
|
|
4361
|
+
lock: ManagedStorageLock,
|
|
4362
|
+
relativePath: string,
|
|
4363
|
+
record: Record<string, unknown>,
|
|
4364
|
+
): void {
|
|
4365
|
+
lock.assertOwned();
|
|
4366
|
+
store.assertBound();
|
|
4367
|
+
const expectedByteLength = managedGcJsonByteLength(record);
|
|
4368
|
+
const serializedJson = JSON.stringify(record, (_key, value: unknown) =>
|
|
4369
|
+
typeof value === "bigint" ? value.toString() : value,
|
|
4370
|
+
);
|
|
4371
|
+
const serialized = `${serializedJson}\n`;
|
|
4372
|
+
const actualByteLength = Buffer.byteLength(serialized, "utf8");
|
|
4373
|
+
if (actualByteLength > MANAGED_GC_RECEIPT_MAX_BYTES) managedGcReceiptCapacity();
|
|
4374
|
+
if (actualByteLength !== expectedByteLength) throw new Error("managed_gc_receipt_encoding_mismatch");
|
|
4375
|
+
const bytes = Buffer.from(serialized, "utf8");
|
|
4376
|
+
try {
|
|
4377
|
+
store.publishNoReplaceSync(relativePath, bytes);
|
|
4378
|
+
} catch (error) {
|
|
4379
|
+
if (!(error instanceof Error) || error.message !== "destination_conflict") throw error;
|
|
4380
|
+
const existing = readManagedGcReceiptSnapshot(store, relativePath);
|
|
4381
|
+
if (!existing?.bytes.equals(bytes)) throw error;
|
|
4382
|
+
}
|
|
4383
|
+
lock.assertOwned();
|
|
4384
|
+
const persisted = readManagedGcReceiptSnapshot(store, relativePath);
|
|
4385
|
+
if (!persisted?.bytes.equals(bytes)) throw new Error("durability_failed");
|
|
4386
|
+
store.assertBound();
|
|
4387
|
+
}
|
|
4388
|
+
|
|
4389
|
+
/** Atomically publish one journal state under the managed scope lock; no GC effects are activated here. */
|
|
4390
|
+
export async function publishManagedGcSessionRetirementReceipt(
|
|
4391
|
+
scope: ManagedScope,
|
|
4392
|
+
receipt: ManagedGcSessionRetirementReceipt,
|
|
4393
|
+
): Promise<ManagedGcSessionRetirementReceipt> {
|
|
4394
|
+
validateManagedGcTranscriptPath(scope, receipt.transcriptPath);
|
|
4395
|
+
return withManagedGcRetirementJournal(scope, receipt.transcriptPath, (trusted, store, lock) => {
|
|
4396
|
+
const currentRead = readManagedGcSessionRetirementReceiptUnlocked(
|
|
4397
|
+
scope,
|
|
4398
|
+
receipt.transcriptPath,
|
|
4399
|
+
trusted,
|
|
4400
|
+
store,
|
|
4401
|
+
lock,
|
|
4402
|
+
);
|
|
4403
|
+
const current = currentRead.receipt;
|
|
4404
|
+
const target = receipt;
|
|
4405
|
+
let attempt: number | undefined;
|
|
4406
|
+
if (!current) {
|
|
4407
|
+
if (receipt.state !== "prepared") throw new Error("task_artifact_owner_continuation_state_order_invalid");
|
|
4408
|
+
assertManagedGcTranscriptUnchangedOrAbsent(scope, target, false);
|
|
4409
|
+
const capturedEvidence = captureTaskArtifactOwnerDeletionEvidence(
|
|
4410
|
+
trusted.ownerContext,
|
|
4411
|
+
target.sessionId,
|
|
4412
|
+
target.taskArtifactOwnerLocator,
|
|
4413
|
+
);
|
|
4414
|
+
if (
|
|
4415
|
+
!capturedEvidence ||
|
|
4416
|
+
!deepSame(
|
|
4417
|
+
capturedEvidence,
|
|
4418
|
+
parseTaskArtifactOwnerDeletionEvidence(receipt.taskArtifactOwnerDeletionEvidence),
|
|
4419
|
+
)
|
|
4420
|
+
)
|
|
4421
|
+
throw new Error("task_artifact_owner_continuation_evidence_mismatch");
|
|
4422
|
+
} else {
|
|
4423
|
+
const prepared = currentRead.prepared;
|
|
4424
|
+
if (!prepared) throw new Error("task_artifact_owner_continuation_state_missing");
|
|
4425
|
+
assertSameManagedGcImmutableAuthority(prepared, receipt);
|
|
4426
|
+
assertManagedGcTranscriptUnchangedOrAbsent(scope, prepared, true);
|
|
4427
|
+
if (receipt.state === "prepared") {
|
|
4428
|
+
managedGcReceiptForPublication(scope, receipt, trusted.ownerContext);
|
|
4429
|
+
return current;
|
|
4430
|
+
}
|
|
4431
|
+
if (receipt.state === "artifacts_removed" && current.state !== "prepared") {
|
|
4432
|
+
managedGcReceiptForPublication(scope, receipt, trusted.ownerContext);
|
|
4433
|
+
return current;
|
|
4434
|
+
}
|
|
4435
|
+
if (receipt.state === "owner_pending") {
|
|
4436
|
+
if (current.state !== "artifacts_removed" && current.state !== "owner_pending")
|
|
4437
|
+
throw new Error("task_artifact_owner_continuation_state_order_invalid");
|
|
4438
|
+
attempt = current.state === "owner_pending" ? (current.ownerRetirementAttempt ?? 0) + 1 : 1;
|
|
4439
|
+
if (current.state === "owner_pending") {
|
|
4440
|
+
const previous = current.taskArtifactOwnerRetirementOutcome;
|
|
4441
|
+
const next = receipt.taskArtifactOwnerRetirementOutcome;
|
|
4442
|
+
if (
|
|
4443
|
+
!previous ||
|
|
4444
|
+
previous.kind === "completed" ||
|
|
4445
|
+
!next ||
|
|
4446
|
+
next.kind === "completed" ||
|
|
4447
|
+
!continuationExtendsPrevious(previous.continuation, next.continuation)
|
|
4448
|
+
)
|
|
4449
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
4450
|
+
}
|
|
4451
|
+
} else if (receipt.state === "owner_retired") {
|
|
4452
|
+
if (current.state === "owner_retired") {
|
|
4453
|
+
const existing = current.taskArtifactOwnerRetirementOutcome;
|
|
4454
|
+
const candidate = managedGcReceiptForPublication(scope, receipt, trusted.ownerContext).parsed;
|
|
4455
|
+
if (!existing || !deepSame(existing, candidate.taskArtifactOwnerRetirementOutcome))
|
|
4456
|
+
throw new Error("task_artifact_owner_continuation_outcome_mismatch");
|
|
4457
|
+
return current;
|
|
4458
|
+
}
|
|
4459
|
+
if (current.state !== "artifacts_removed" && current.state !== "owner_pending")
|
|
4460
|
+
throw new Error("task_artifact_owner_continuation_state_order_invalid");
|
|
4461
|
+
} else if (receipt.state !== "artifacts_removed") {
|
|
4462
|
+
throw new Error("task_artifact_owner_continuation_state_order_invalid");
|
|
4463
|
+
}
|
|
4464
|
+
}
|
|
4465
|
+
assertManagedGcReceiptHistoryDirectoryUnchanged(scope, store, currentRead.history);
|
|
4466
|
+
const { record, parsed } = managedGcReceiptForPublication(
|
|
4467
|
+
scope,
|
|
4468
|
+
receipt,
|
|
4469
|
+
trusted.ownerContext,
|
|
4470
|
+
attempt,
|
|
4471
|
+
currentRead.history,
|
|
4472
|
+
);
|
|
4473
|
+
if (receipt.state === "owner_retired") {
|
|
4474
|
+
const outcome = parsed.taskArtifactOwnerRetirementOutcome;
|
|
4475
|
+
if (outcome?.kind !== "completed") throw new Error("task_artifact_owner_continuation_outcome_mismatch");
|
|
4476
|
+
verifyTaskArtifactOwnerPhysicalRetirement(
|
|
4477
|
+
trusted.ownerContext,
|
|
4478
|
+
parsed.taskArtifactOwnerDeletionEvidence,
|
|
4479
|
+
outcome,
|
|
4480
|
+
);
|
|
4481
|
+
}
|
|
4482
|
+
const relativePath = managedGcRetirementReceiptRelativePath(receipt.transcriptPath, receipt.state, attempt);
|
|
4483
|
+
publishManagedGcReceiptNoReplace(store, lock, relativePath, record);
|
|
4484
|
+
const persistedRead = readManagedGcSessionRetirementReceiptUnlocked(
|
|
4485
|
+
scope,
|
|
4486
|
+
receipt.transcriptPath,
|
|
4487
|
+
trusted,
|
|
4488
|
+
store,
|
|
4489
|
+
lock,
|
|
4490
|
+
);
|
|
4491
|
+
const persisted = persistedRead.receipt;
|
|
4492
|
+
if (!persisted) throw new Error("durability_failed");
|
|
4493
|
+
const expectedState = receipt.state;
|
|
4494
|
+
if (
|
|
4495
|
+
persisted.state !== expectedState ||
|
|
4496
|
+
(expectedState === "owner_pending" && persisted.ownerRetirementAttempt !== attempt) ||
|
|
4497
|
+
!deepSame(persisted.taskArtifactOwnerDeletionEvidence, parsed.taskArtifactOwnerDeletionEvidence)
|
|
4498
|
+
)
|
|
4499
|
+
throw new Error("durability_failed");
|
|
4500
|
+
return persisted;
|
|
4501
|
+
});
|
|
4502
|
+
}
|
|
4503
|
+
function retiredTargets(scope: ManagedScope, pathname: string): readonly RetiredTarget[] | undefined {
|
|
4504
|
+
let value: unknown;
|
|
4505
|
+
try {
|
|
4506
|
+
value = JSON.parse(captureManagedFileNoFollow(pathname).bytes.toString("utf8"));
|
|
4507
|
+
const targets = parseRetiredTargetRecord(scope, value);
|
|
4508
|
+
if (!targets) assertLegacyOwnerFreeRecord(value);
|
|
4509
|
+
return targets;
|
|
4510
|
+
} catch (error) {
|
|
4511
|
+
assertLegacyOwnerFreeRecord(value);
|
|
4512
|
+
if (isOwnerBoundaryFailure(error)) throw error;
|
|
4513
|
+
if (error instanceof SyntaxError) throw new Error("durability_failed", { cause: error });
|
|
4514
|
+
return undefined;
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4517
|
+
|
|
4518
|
+
const OWNER_CONSUMER_UNAVAILABLE = "task_artifact_owner_legacy_scope_unsupported";
|
|
4519
|
+
|
|
4520
|
+
function isOwnerBoundaryFailure(error: unknown): boolean {
|
|
4521
|
+
return error instanceof Error && error.message.startsWith("task_artifact_owner_");
|
|
4522
|
+
}
|
|
4523
|
+
|
|
4524
|
+
function hasOwnerClaims(value: unknown): boolean {
|
|
4525
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
4526
|
+
const record = value as Record<string, unknown>;
|
|
4527
|
+
return (
|
|
4528
|
+
Object.keys(record).some(key => key.startsWith("taskArtifactOwner")) ||
|
|
4529
|
+
(record.target !== undefined && hasOwnerClaims(record.target)) ||
|
|
4530
|
+
(Array.isArray(record.targets) && record.targets.some(hasOwnerClaims))
|
|
4531
|
+
);
|
|
4532
|
+
}
|
|
4533
|
+
|
|
4534
|
+
function assertLegacyOwnerFreeRecord(value: unknown): void {
|
|
4535
|
+
if (hasOwnerClaims(value)) throw new Error(OWNER_CONSUMER_UNAVAILABLE);
|
|
4536
|
+
}
|
|
4537
|
+
|
|
4538
|
+
function assertOwnerTranscriptLocator(
|
|
4539
|
+
target: Pick<
|
|
4540
|
+
VerifiedSessionDeleteTarget,
|
|
4541
|
+
"transcriptPath" | "sessionId" | "transcriptIdentity" | "detachedTranscriptPath"
|
|
4542
|
+
>,
|
|
4543
|
+
evidence?: TaskArtifactOwnerDeletionEvidence,
|
|
4544
|
+
): void {
|
|
4545
|
+
const pathname = target.detachedTranscriptPath ?? target.transcriptPath;
|
|
4546
|
+
let snapshot: SessionStorageSnapshot;
|
|
4547
|
+
try {
|
|
4548
|
+
snapshot = new FileSessionStorage().readSnapshotSync(pathname);
|
|
4549
|
+
} catch (error) {
|
|
4550
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
4551
|
+
throw error;
|
|
4552
|
+
}
|
|
4553
|
+
// Descriptor-bound owner classification is data, not authorization; storage retains its exact identity fence.
|
|
4554
|
+
const locator = taskArtifactOwnerLocatorFromTranscriptBytes(snapshot.bytes, target.sessionId);
|
|
4555
|
+
if ((locator && !evidence) || (evidence && !locator)) throw new Error("task_artifact_owner_locator_missing");
|
|
4556
|
+
if (locator && !deepSame(locator, evidence?.locator))
|
|
4557
|
+
throw new Error("task_artifact_owner_continuation_evidence_mismatch");
|
|
4558
|
+
}
|
|
4559
|
+
|
|
4560
|
+
function parseRetiredTargetRecord(scope: ManagedScope, value: unknown): readonly RetiredTarget[] | undefined {
|
|
4561
|
+
if (!value || typeof value !== "object") return undefined;
|
|
4562
|
+
if (Object.keys(value).some(key => key.startsWith("taskArtifactOwner"))) throw new Error(OWNER_CONSUMER_UNAVAILABLE);
|
|
4563
|
+
const record = value as { schemaVersion?: unknown; state?: unknown; scope?: unknown; targets?: unknown };
|
|
4564
|
+
if (
|
|
4565
|
+
record.schemaVersion !== 2 ||
|
|
4566
|
+
record.state !== "retired" ||
|
|
4567
|
+
record.scope !== scopeDigest(scope.platform, scope.canonicalCwd) ||
|
|
4568
|
+
!Array.isArray(record.targets)
|
|
4569
|
+
)
|
|
4570
|
+
return undefined;
|
|
4571
|
+
const targets: RetiredTarget[] = [];
|
|
4572
|
+
for (const target of record.targets) {
|
|
4573
|
+
if (!target || typeof target !== "object" || Array.isArray(target)) return undefined;
|
|
4574
|
+
const item = target as Record<string, unknown>;
|
|
4575
|
+
if (
|
|
4576
|
+
Object.keys(item).some(
|
|
4577
|
+
key => key.startsWith("taskArtifactOwner") && key !== "taskArtifactOwnerDeletionEvidence",
|
|
4578
|
+
)
|
|
4579
|
+
)
|
|
4580
|
+
throw new Error(OWNER_CONSUMER_UNAVAILABLE);
|
|
4581
|
+
const identity = item.identity;
|
|
4582
|
+
if (!identity || typeof identity !== "object" || Array.isArray(identity)) return undefined;
|
|
4583
|
+
const fields = identity as Record<string, unknown>;
|
|
4584
|
+
if (
|
|
4585
|
+
typeof item.path !== "string" ||
|
|
4586
|
+
typeof item.sessionId !== "string" ||
|
|
4587
|
+
typeof item.cwd !== "string" ||
|
|
4588
|
+
!pathIsWithin(scope.sessionsRoot, item.path) ||
|
|
4589
|
+
(item.provenance !== undefined && item.provenance !== "v2" && item.provenance !== "legacy") ||
|
|
4590
|
+
typeof fields.canonicalPath !== "string" ||
|
|
4591
|
+
typeof fields.dev !== "string" ||
|
|
4592
|
+
typeof fields.ino !== "string" ||
|
|
4593
|
+
typeof fields.size !== "number" ||
|
|
4594
|
+
typeof fields.mtimeMs !== "number" ||
|
|
4595
|
+
typeof fields.mtimeNs !== "string" ||
|
|
4596
|
+
typeof fields.sha256 !== "string"
|
|
4597
|
+
)
|
|
4598
|
+
return undefined;
|
|
4599
|
+
const provenance =
|
|
4600
|
+
item.provenance === "v2" || item.provenance === "legacy"
|
|
4601
|
+
? item.provenance
|
|
4602
|
+
: path.dirname(item.path) === scope.directoryPath
|
|
4603
|
+
? "v2"
|
|
4604
|
+
: "legacy";
|
|
4605
|
+
let taskArtifactOwnerDeletionEvidence: TaskArtifactOwnerDeletionEvidence | undefined;
|
|
4606
|
+
if (Object.hasOwn(item, "taskArtifactOwnerDeletionEvidence")) {
|
|
4607
|
+
try {
|
|
4608
|
+
taskArtifactOwnerDeletionEvidence = parseTaskArtifactOwnerDeletionEvidence(
|
|
4609
|
+
item.taskArtifactOwnerDeletionEvidence,
|
|
4610
|
+
);
|
|
4611
|
+
if (taskArtifactOwnerDeletionEvidence.sessionId !== item.sessionId)
|
|
4612
|
+
throw new Error("owner_session_mismatch");
|
|
4613
|
+
} catch (cause) {
|
|
4614
|
+
throw new Error("task_artifact_owner_legacy_scope_unsupported", { cause });
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
4617
|
+
targets.push({
|
|
4618
|
+
...(taskArtifactOwnerDeletionEvidence ? { taskArtifactOwnerDeletionEvidence } : {}),
|
|
4619
|
+
path: item.path,
|
|
4620
|
+
sessionId: item.sessionId,
|
|
4621
|
+
cwd: item.cwd,
|
|
4622
|
+
provenance,
|
|
4623
|
+
migrationState: provenance === "v2" ? "native_v2" : "legacy_unmigrated",
|
|
4624
|
+
identity: {
|
|
4625
|
+
canonicalPath: fields.canonicalPath,
|
|
4626
|
+
dev: BigInt(fields.dev),
|
|
4627
|
+
ino: BigInt(fields.ino),
|
|
4628
|
+
size: fields.size,
|
|
4629
|
+
mtimeMs: fields.mtimeMs,
|
|
4630
|
+
mtimeNs: BigInt(fields.mtimeNs),
|
|
4631
|
+
sha256: fields.sha256,
|
|
4632
|
+
sessionId: item.sessionId,
|
|
4633
|
+
},
|
|
4634
|
+
});
|
|
4635
|
+
}
|
|
4636
|
+
return targets;
|
|
4637
|
+
}
|
|
4638
|
+
|
|
4639
|
+
/**
|
|
4640
|
+
* Append-only cleanup state machine:
|
|
4641
|
+
* `pending(N)` durably authorizes only its planned quarantine names before detach;
|
|
4642
|
+
* a returned partial result appends `pending(N + 1)` with the observed detached
|
|
4643
|
+
* identity/path. Restart accepts only a contiguous, target-bound sequence.
|
|
4644
|
+
*/
|
|
4645
|
+
type RetainedArtifactsRootReceipt = {
|
|
4646
|
+
path: string;
|
|
4647
|
+
identity: SessionStorageFileIdentity;
|
|
4648
|
+
tree: NativeDirectoryTreeSnapshot;
|
|
4649
|
+
};
|
|
4650
|
+
|
|
4651
|
+
function retainedArtifactsRootReceipt(receipt: CleanupReceipt): RetainedArtifactsRootReceipt | undefined {
|
|
4652
|
+
const pathname = receipt.detachedArtifactsPath ?? deterministicRemovalRoot(receipt.plannedArtifactsPath);
|
|
4653
|
+
if (!fs.existsSync(pathname)) return undefined;
|
|
4654
|
+
if (!isQuarantinePath(receipt.target, pathname) || !receipt.expectedArtifactsIdentity)
|
|
4655
|
+
throw new Error("durability_failed");
|
|
4656
|
+
const identity = artifactIdentityAt(pathname);
|
|
4657
|
+
if (
|
|
4658
|
+
!identity ||
|
|
4659
|
+
identity.dev !== receipt.expectedArtifactsIdentity.dev ||
|
|
4660
|
+
identity.ino !== receipt.expectedArtifactsIdentity.ino
|
|
4661
|
+
)
|
|
4662
|
+
throw new Error("durability_failed");
|
|
4663
|
+
const tree = snapshotArtifactTree(pathname);
|
|
4664
|
+
if (!artifactTreePayloadAbsent(tree)) throw new Error("durability_failed");
|
|
4665
|
+
return { path: pathname, identity, tree };
|
|
4666
|
+
}
|
|
4667
|
+
|
|
4668
|
+
function retainedArtifactsRootMatches(record: Record<string, unknown>): boolean {
|
|
4669
|
+
if (record.retainedArtifactsRoot === undefined) return true;
|
|
4670
|
+
if (!record.retainedArtifactsRoot || typeof record.retainedArtifactsRoot !== "object")
|
|
4671
|
+
throw new Error("durability_failed");
|
|
4672
|
+
const retained = record.retainedArtifactsRoot as Record<string, unknown>;
|
|
4673
|
+
const identity = retained.identity;
|
|
4674
|
+
const tree = artifactTreeSnapshot(retained.tree);
|
|
4675
|
+
const expectedRetainedPath =
|
|
4676
|
+
typeof record.detachedArtifactsPath === "string"
|
|
4677
|
+
? record.detachedArtifactsPath
|
|
4678
|
+
: typeof record.plannedArtifactsPath === "string"
|
|
4679
|
+
? deterministicRemovalRoot(record.plannedArtifactsPath)
|
|
4680
|
+
: undefined;
|
|
4681
|
+
if (
|
|
4682
|
+
!expectedRetainedPath ||
|
|
4683
|
+
retained.path !== expectedRetainedPath ||
|
|
4684
|
+
record.artifactsPayloadDurable !== true ||
|
|
4685
|
+
!identity ||
|
|
4686
|
+
typeof identity !== "object" ||
|
|
4687
|
+
Array.isArray(identity) ||
|
|
4688
|
+
!tree ||
|
|
4689
|
+
!artifactTreePayloadAbsent(tree)
|
|
4690
|
+
)
|
|
4691
|
+
throw new Error("durability_failed");
|
|
4692
|
+
const artifactIdentity = identity as Record<string, unknown>;
|
|
4693
|
+
if (
|
|
4694
|
+
typeof artifactIdentity.dev !== "string" ||
|
|
4695
|
+
typeof artifactIdentity.ino !== "string" ||
|
|
4696
|
+
typeof artifactIdentity.size !== "number" ||
|
|
4697
|
+
typeof artifactIdentity.mtimeNs !== "string" ||
|
|
4698
|
+
typeof artifactIdentity.sha256 !== "string"
|
|
4699
|
+
)
|
|
4700
|
+
throw new Error("durability_failed");
|
|
4701
|
+
if (!fs.existsSync(retained.path)) return true;
|
|
4702
|
+
const observed = artifactIdentityAt(retained.path);
|
|
4703
|
+
if (
|
|
4704
|
+
!observed ||
|
|
4705
|
+
observed.dev !== BigInt(artifactIdentity.dev) ||
|
|
4706
|
+
observed.ino !== BigInt(artifactIdentity.ino) ||
|
|
4707
|
+
!artifactTreePayloadAbsent(snapshotArtifactTree(retained.path))
|
|
4708
|
+
)
|
|
4709
|
+
throw new Error("durability_failed");
|
|
4710
|
+
return true;
|
|
4711
|
+
}
|
|
4712
|
+
|
|
4713
|
+
type CleanupReceipt = {
|
|
4714
|
+
attempt: number;
|
|
4715
|
+
target: RetiredTarget;
|
|
4716
|
+
taskArtifactOwnerTranscriptDeleted?: true;
|
|
4717
|
+
expectedArtifactsIdentity?: SessionStorageFileIdentity;
|
|
4718
|
+
expectedArtifactsTree?: NativeDirectoryTreeSnapshot;
|
|
4719
|
+
artifactsPayloadDurable?: true;
|
|
4720
|
+
artifactsRemovedAttempt?: number;
|
|
4721
|
+
detachedArtifactsPath?: string;
|
|
4722
|
+
detachedTranscriptPath?: string;
|
|
4723
|
+
transcriptPayloadDurable?: true;
|
|
4724
|
+
retainedArtifactsSuccessorPath?: string;
|
|
4725
|
+
retainedArtifactsPlaceholderPath?: string;
|
|
4726
|
+
retainedArtifactsUnknownPath?: string;
|
|
4727
|
+
retainedTranscriptSuccessorPath?: string;
|
|
4728
|
+
retainedTranscriptPlaceholderPath?: string;
|
|
4729
|
+
retainedTranscriptUnknownPath?: string;
|
|
4730
|
+
|
|
4731
|
+
plannedArtifactsPath: string;
|
|
4732
|
+
plannedTranscriptPath: string;
|
|
4733
|
+
};
|
|
4734
|
+
|
|
4735
|
+
function cleanupReceiptPath(
|
|
4736
|
+
tombstone: string,
|
|
4737
|
+
target: RetiredTarget,
|
|
4738
|
+
state: "pending" | "artifacts_removed" | "completed",
|
|
4739
|
+
attempt: number,
|
|
4740
|
+
): string {
|
|
4741
|
+
return path.join(
|
|
4742
|
+
path.dirname(tombstone),
|
|
4743
|
+
`${path.basename(tombstone, ".json")}.${stableOperationName(target)}.cleanup-${state}-${attempt}.json`,
|
|
4744
|
+
);
|
|
4745
|
+
}
|
|
4746
|
+
|
|
4747
|
+
function cleanupReceipt(scope: ManagedScope, tombstone: string, receipt: CleanupReceipt): Record<string, unknown> {
|
|
4748
|
+
return {
|
|
4749
|
+
schemaVersion: 2,
|
|
4750
|
+
state: "cleanup_pending",
|
|
4751
|
+
scope: scopeDigest(scope.platform, scope.canonicalCwd),
|
|
4752
|
+
tombstone,
|
|
4753
|
+
attempt: receipt.attempt,
|
|
4754
|
+
target: {
|
|
4755
|
+
path: receipt.target.path,
|
|
4756
|
+
sessionId: receipt.target.sessionId,
|
|
4757
|
+
cwd: receipt.target.cwd,
|
|
4758
|
+
identity: receipt.target.identity,
|
|
4759
|
+
},
|
|
4760
|
+
...(receipt.taskArtifactOwnerTranscriptDeleted ? { taskArtifactOwnerTranscriptDeleted: true } : {}),
|
|
4761
|
+
...(receipt.expectedArtifactsIdentity ? { expectedArtifactsIdentity: receipt.expectedArtifactsIdentity } : {}),
|
|
4762
|
+
...(receipt.expectedArtifactsTree ? { expectedArtifactsTree: receipt.expectedArtifactsTree } : {}),
|
|
4763
|
+
...(receipt.artifactsPayloadDurable === true ? { artifactsPayloadDurable: true } : {}),
|
|
4764
|
+
...(receipt.artifactsRemovedAttempt !== undefined
|
|
4765
|
+
? { artifactsRemovedAttempt: receipt.artifactsRemovedAttempt }
|
|
4766
|
+
: {}),
|
|
4767
|
+
...(receipt.detachedArtifactsPath ? { detachedArtifactsPath: receipt.detachedArtifactsPath } : {}),
|
|
4768
|
+
...(receipt.detachedTranscriptPath ? { detachedTranscriptPath: receipt.detachedTranscriptPath } : {}),
|
|
4769
|
+
...(receipt.transcriptPayloadDurable === true ? { transcriptPayloadDurable: true } : {}),
|
|
4770
|
+
...(receipt.retainedArtifactsSuccessorPath
|
|
4771
|
+
? { retainedArtifactsSuccessorPath: receipt.retainedArtifactsSuccessorPath }
|
|
4772
|
+
: {}),
|
|
4773
|
+
...(receipt.retainedArtifactsPlaceholderPath
|
|
4774
|
+
? { retainedArtifactsPlaceholderPath: receipt.retainedArtifactsPlaceholderPath }
|
|
4775
|
+
: {}),
|
|
4776
|
+
...(receipt.retainedArtifactsUnknownPath
|
|
4777
|
+
? { retainedArtifactsUnknownPath: receipt.retainedArtifactsUnknownPath }
|
|
4778
|
+
: {}),
|
|
4779
|
+
...(receipt.retainedTranscriptSuccessorPath
|
|
4780
|
+
? { retainedTranscriptSuccessorPath: receipt.retainedTranscriptSuccessorPath }
|
|
4781
|
+
: {}),
|
|
4782
|
+
...(receipt.retainedTranscriptPlaceholderPath
|
|
4783
|
+
? { retainedTranscriptPlaceholderPath: receipt.retainedTranscriptPlaceholderPath }
|
|
4784
|
+
: {}),
|
|
4785
|
+
...(receipt.retainedTranscriptUnknownPath
|
|
4786
|
+
? { retainedTranscriptUnknownPath: receipt.retainedTranscriptUnknownPath }
|
|
4787
|
+
: {}),
|
|
4788
|
+
plannedArtifactsPath: receipt.plannedArtifactsPath,
|
|
4789
|
+
plannedTranscriptPath: receipt.plannedTranscriptPath,
|
|
4790
|
+
};
|
|
4791
|
+
}
|
|
4792
|
+
|
|
4793
|
+
type CleanupArtifactsRemovedEvidence = { retainedArtifactsRootPath?: string };
|
|
4794
|
+
|
|
4795
|
+
function cleanupArtifactsRemovedEvidence(
|
|
4796
|
+
scope: ManagedScope,
|
|
4797
|
+
tombstone: string,
|
|
4798
|
+
target: RetiredTarget,
|
|
4799
|
+
attempt: number,
|
|
4800
|
+
): CleanupArtifactsRemovedEvidence | undefined {
|
|
4801
|
+
try {
|
|
4802
|
+
const value: unknown = JSON.parse(
|
|
4803
|
+
captureManagedFileNoFollow(cleanupReceiptPath(tombstone, target, "artifacts_removed", attempt)).bytes.toString(
|
|
2308
4804
|
"utf8",
|
|
2309
4805
|
),
|
|
2310
4806
|
);
|
|
@@ -2875,133 +5371,231 @@ function artifactTreeSnapshot(value: unknown): NativeDirectoryTreeSnapshot | und
|
|
|
2875
5371
|
return snapshot as unknown as NativeDirectoryTreeSnapshot;
|
|
2876
5372
|
}
|
|
2877
5373
|
|
|
5374
|
+
const CLEANUP_RECEIPT_REPLAY_MAX_BYTES = Math.min(
|
|
5375
|
+
MANAGED_ARTIFACT_MAX_FILE_BYTES,
|
|
5376
|
+
MANAGED_SESSION_READ_RANGE_MAX_BYTES,
|
|
5377
|
+
);
|
|
5378
|
+
const CLEANUP_RECEIPT_REPLAY_MAX_WORK = MANAGED_ARTIFACT_MAX_FILES * 2;
|
|
5379
|
+
const CLEANUP_RECEIPT_REPLAY_CHUNK_BYTES = 64 * 1024;
|
|
5380
|
+
|
|
5381
|
+
interface CleanupReceiptReplayBudget {
|
|
5382
|
+
workUnits: number;
|
|
5383
|
+
directoryEntries: number;
|
|
5384
|
+
receiptReads: number;
|
|
5385
|
+
readBytes: number;
|
|
5386
|
+
}
|
|
5387
|
+
|
|
5388
|
+
function cleanupReceiptReplayCapacity(): never {
|
|
5389
|
+
throw new Error("managed_gc_journal_capacity_exceeded");
|
|
5390
|
+
}
|
|
5391
|
+
|
|
5392
|
+
function chargeCleanupReceiptReplayWork(budget: CleanupReceiptReplayBudget, amount = 1): void {
|
|
5393
|
+
if (!Number.isSafeInteger(amount) || amount < 0 || budget.workUnits > CLEANUP_RECEIPT_REPLAY_MAX_WORK - amount)
|
|
5394
|
+
cleanupReceiptReplayCapacity();
|
|
5395
|
+
budget.workUnits += amount;
|
|
5396
|
+
}
|
|
5397
|
+
|
|
5398
|
+
function admitCleanupReceiptReplayEntry(budget: CleanupReceiptReplayBudget): void {
|
|
5399
|
+
if (budget.directoryEntries >= MANAGED_ARTIFACT_MAX_FILES) cleanupReceiptReplayCapacity();
|
|
5400
|
+
chargeCleanupReceiptReplayWork(budget);
|
|
5401
|
+
budget.directoryEntries++;
|
|
5402
|
+
}
|
|
5403
|
+
|
|
5404
|
+
function admitCleanupReceiptReplayRead(budget: CleanupReceiptReplayBudget, size: number): void {
|
|
5405
|
+
if (
|
|
5406
|
+
!Number.isSafeInteger(size) ||
|
|
5407
|
+
size < 0 ||
|
|
5408
|
+
size > CLEANUP_RECEIPT_REPLAY_MAX_BYTES ||
|
|
5409
|
+
budget.receiptReads >= MANAGED_ARTIFACT_MAX_FILES ||
|
|
5410
|
+
budget.readBytes > MANAGED_ARTIFACT_MAX_TOTAL_BYTES - size
|
|
5411
|
+
)
|
|
5412
|
+
cleanupReceiptReplayCapacity();
|
|
5413
|
+
chargeCleanupReceiptReplayWork(budget, Math.max(1, Math.ceil(size / CLEANUP_RECEIPT_REPLAY_CHUNK_BYTES)));
|
|
5414
|
+
budget.receiptReads++;
|
|
5415
|
+
budget.readBytes += size;
|
|
5416
|
+
}
|
|
5417
|
+
|
|
2878
5418
|
function pendingCleanupReceipt(
|
|
2879
5419
|
scope: ManagedScope,
|
|
2880
5420
|
tombstone: string,
|
|
2881
5421
|
target: RetiredTarget,
|
|
2882
5422
|
): CleanupReceipt | undefined {
|
|
5423
|
+
const budget: CleanupReceiptReplayBudget = { workUnits: 0, directoryEntries: 0, receiptReads: 0, readBytes: 0 };
|
|
2883
5424
|
try {
|
|
2884
5425
|
const prefix = `${path.basename(tombstone, ".json")}.${stableOperationName(target)}.cleanup-pending-`;
|
|
2885
|
-
const
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
5426
|
+
const directory = fs.opendirSync(path.dirname(tombstone));
|
|
5427
|
+
const records: Array<{ readonly name: string; readonly attempt: number }> = [];
|
|
5428
|
+
try {
|
|
5429
|
+
for (;;) {
|
|
5430
|
+
const entry = directory.readSync();
|
|
5431
|
+
if (!entry) break;
|
|
5432
|
+
admitCleanupReceiptReplayEntry(budget);
|
|
5433
|
+
if (!entry.name.startsWith(prefix) || !entry.name.endsWith(".json")) continue;
|
|
5434
|
+
const suffix = entry.name.slice(prefix.length, -".json".length);
|
|
5435
|
+
if (!/^[1-9][0-9]*$/u.test(suffix)) throw new Error("durability_failed");
|
|
5436
|
+
const attempt = Number(suffix);
|
|
5437
|
+
if (!Number.isSafeInteger(attempt) || String(attempt) !== suffix) throw new Error("durability_failed");
|
|
5438
|
+
if (records.length >= MANAGED_ARTIFACT_MAX_FILES) cleanupReceiptReplayCapacity();
|
|
5439
|
+
records.push({ name: entry.name, attempt });
|
|
5440
|
+
}
|
|
5441
|
+
} finally {
|
|
5442
|
+
directory.closeSync();
|
|
5443
|
+
}
|
|
5444
|
+
records.sort((left, right) => left.attempt - right.attempt);
|
|
2898
5445
|
let latest: CleanupReceipt | undefined;
|
|
2899
5446
|
const plannedPaths = new Set<string>();
|
|
2900
|
-
for (const
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
5447
|
+
for (const { name, attempt } of records) {
|
|
5448
|
+
let snapshot: ManagedFileSnapshot;
|
|
5449
|
+
try {
|
|
5450
|
+
snapshot = captureManagedFileNoFollowBounded(
|
|
5451
|
+
path.join(path.dirname(tombstone), name),
|
|
5452
|
+
CLEANUP_RECEIPT_REPLAY_MAX_BYTES,
|
|
5453
|
+
size => admitCleanupReceiptReplayRead(budget, size),
|
|
5454
|
+
);
|
|
5455
|
+
} catch (error) {
|
|
5456
|
+
if (error instanceof Error && error.message === "artifact_capacity_exceeded")
|
|
5457
|
+
cleanupReceiptReplayCapacity();
|
|
5458
|
+
throw error;
|
|
5459
|
+
}
|
|
5460
|
+
const value: unknown = JSON.parse(snapshot.bytes.toString("utf8"));
|
|
5461
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("durability_failed");
|
|
5462
|
+
latest = parseCleanupReceiptRecord(
|
|
5463
|
+
scope,
|
|
5464
|
+
tombstone,
|
|
5465
|
+
target,
|
|
5466
|
+
value as Record<string, unknown>,
|
|
5467
|
+
attempt,
|
|
5468
|
+
latest,
|
|
5469
|
+
plannedPaths,
|
|
5470
|
+
);
|
|
5471
|
+
}
|
|
5472
|
+
return latest;
|
|
5473
|
+
} catch (error) {
|
|
5474
|
+
if (
|
|
5475
|
+
(error as Error).message === "durability_failed" ||
|
|
5476
|
+
(error as Error).message === "managed_gc_journal_capacity_exceeded" ||
|
|
5477
|
+
isOwnerBoundaryFailure(error)
|
|
5478
|
+
)
|
|
5479
|
+
throw error;
|
|
5480
|
+
return undefined;
|
|
5481
|
+
}
|
|
5482
|
+
}
|
|
5483
|
+
|
|
5484
|
+
function parseCleanupReceiptRecord(
|
|
5485
|
+
scope: ManagedScope,
|
|
5486
|
+
tombstone: string,
|
|
5487
|
+
target: RetiredTarget,
|
|
5488
|
+
record: Record<string, unknown>,
|
|
5489
|
+
filenameAttempt: number,
|
|
5490
|
+
latest: CleanupReceipt | undefined,
|
|
5491
|
+
plannedPaths: Set<string>,
|
|
5492
|
+
): CleanupReceipt {
|
|
5493
|
+
if (
|
|
5494
|
+
Object.keys(record).some(
|
|
5495
|
+
key => key.startsWith("taskArtifactOwner") && key !== "taskArtifactOwnerTranscriptDeleted",
|
|
5496
|
+
)
|
|
5497
|
+
)
|
|
5498
|
+
throw new Error(OWNER_CONSUMER_UNAVAILABLE);
|
|
5499
|
+
const attempt = record.attempt;
|
|
5500
|
+
const recorded = record.target as Record<string, unknown> | undefined;
|
|
5501
|
+
const identity = recorded?.identity as Record<string, unknown> | undefined;
|
|
5502
|
+
if (
|
|
5503
|
+
record.schemaVersion !== 2 ||
|
|
5504
|
+
record.state !== "cleanup_pending" ||
|
|
5505
|
+
record.scope !== scopeDigest(scope.platform, scope.canonicalCwd) ||
|
|
5506
|
+
record.tombstone !== tombstone ||
|
|
5507
|
+
typeof attempt !== "number" ||
|
|
5508
|
+
!Number.isSafeInteger(attempt) ||
|
|
5509
|
+
attempt !== filenameAttempt ||
|
|
5510
|
+
attempt !== (latest?.attempt ?? 0) + 1 ||
|
|
5511
|
+
recorded?.path !== target.path ||
|
|
5512
|
+
recorded.sessionId !== target.sessionId ||
|
|
5513
|
+
recorded.cwd !== target.cwd ||
|
|
5514
|
+
identity?.dev !== String(target.identity.dev) ||
|
|
5515
|
+
identity.ino !== String(target.identity.ino) ||
|
|
5516
|
+
identity.size !== target.identity.size ||
|
|
5517
|
+
identity.mtimeNs !== String(target.identity.mtimeNs) ||
|
|
5518
|
+
identity.sha256 !== target.identity.sha256 ||
|
|
5519
|
+
!isQuarantinePath(target, record.plannedArtifactsPath) ||
|
|
5520
|
+
!isQuarantinePath(target, record.plannedTranscriptPath) ||
|
|
5521
|
+
record.plannedArtifactsPath === record.plannedTranscriptPath ||
|
|
5522
|
+
plannedPaths.has(record.plannedArtifactsPath as string) ||
|
|
5523
|
+
plannedPaths.has(record.plannedTranscriptPath as string) ||
|
|
5524
|
+
(record.detachedArtifactsPath !== undefined && !isQuarantinePath(target, record.detachedArtifactsPath)) ||
|
|
5525
|
+
(record.detachedTranscriptPath !== undefined && !isQuarantinePath(target, record.detachedTranscriptPath)) ||
|
|
5526
|
+
(record.retainedArtifactsSuccessorPath !== undefined &&
|
|
5527
|
+
!isRetainedNativePath(target, record.retainedArtifactsSuccessorPath)) ||
|
|
5528
|
+
(record.retainedArtifactsPlaceholderPath !== undefined &&
|
|
5529
|
+
!isRetainedNativePath(target, record.retainedArtifactsPlaceholderPath)) ||
|
|
5530
|
+
(record.retainedArtifactsUnknownPath !== undefined &&
|
|
5531
|
+
!isRetainedNativePath(target, record.retainedArtifactsUnknownPath)) ||
|
|
5532
|
+
(record.retainedTranscriptSuccessorPath !== undefined &&
|
|
5533
|
+
!isRetainedNativePath(target, record.retainedTranscriptSuccessorPath)) ||
|
|
5534
|
+
(record.retainedTranscriptPlaceholderPath !== undefined &&
|
|
5535
|
+
!isRetainedNativePath(target, record.retainedTranscriptPlaceholderPath)) ||
|
|
5536
|
+
(record.retainedTranscriptUnknownPath !== undefined &&
|
|
5537
|
+
!isRetainedNativePath(target, record.retainedTranscriptUnknownPath)) ||
|
|
5538
|
+
(record.artifactsPayloadDurable !== undefined && record.artifactsPayloadDurable !== true) ||
|
|
5539
|
+
(record.artifactsRemovedAttempt !== undefined &&
|
|
5540
|
+
(typeof record.artifactsRemovedAttempt !== "number" ||
|
|
5541
|
+
!Number.isSafeInteger(record.artifactsRemovedAttempt) ||
|
|
5542
|
+
record.artifactsRemovedAttempt < 1 ||
|
|
5543
|
+
record.artifactsRemovedAttempt > (attempt as number))) ||
|
|
5544
|
+
(record.transcriptPayloadDurable !== undefined && record.transcriptPayloadDurable !== true) ||
|
|
5545
|
+
(record.taskArtifactOwnerTranscriptDeleted !== undefined &&
|
|
5546
|
+
(record.taskArtifactOwnerTranscriptDeleted !== true || !target.taskArtifactOwnerDeletionEvidence))
|
|
5547
|
+
)
|
|
5548
|
+
throw new Error("durability_failed");
|
|
5549
|
+
const artifact = record.expectedArtifactsIdentity as Record<string, unknown> | undefined;
|
|
5550
|
+
const expectedArtifactsIdentity = artifact
|
|
5551
|
+
? typeof artifact.dev === "string" &&
|
|
5552
|
+
typeof artifact.ino === "string" &&
|
|
5553
|
+
typeof artifact.size === "number" &&
|
|
5554
|
+
typeof artifact.mtimeNs === "string" &&
|
|
5555
|
+
typeof artifact.sha256 === "string"
|
|
5556
|
+
? {
|
|
5557
|
+
dev: BigInt(artifact.dev),
|
|
5558
|
+
ino: BigInt(artifact.ino),
|
|
5559
|
+
size: artifact.size,
|
|
5560
|
+
mtimeNs: BigInt(artifact.mtimeNs),
|
|
5561
|
+
sha256: artifact.sha256,
|
|
5562
|
+
}
|
|
5563
|
+
: undefined
|
|
5564
|
+
: undefined;
|
|
5565
|
+
if (artifact && !expectedArtifactsIdentity) throw new Error("durability_failed");
|
|
5566
|
+
const expectedArtifactsTree =
|
|
5567
|
+
record.expectedArtifactsTree === undefined ? undefined : artifactTreeSnapshot(record.expectedArtifactsTree);
|
|
5568
|
+
if (record.expectedArtifactsTree !== undefined && !expectedArtifactsTree) throw new Error("durability_failed");
|
|
2968
5569
|
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
};
|
|
2999
|
-
}
|
|
3000
|
-
return latest;
|
|
3001
|
-
} catch (error) {
|
|
3002
|
-
if ((error as Error).message === "durability_failed") throw error;
|
|
3003
|
-
return undefined;
|
|
3004
|
-
}
|
|
5570
|
+
if (
|
|
5571
|
+
latest &&
|
|
5572
|
+
((record.detachedArtifactsPath !== undefined &&
|
|
5573
|
+
![...plannedPaths].some(planned => isAuthorizedArtifactRoot(target, planned, record.detachedArtifactsPath))) ||
|
|
5574
|
+
(record.detachedTranscriptPath !== undefined && !plannedPaths.has(record.detachedTranscriptPath)))
|
|
5575
|
+
)
|
|
5576
|
+
throw new Error("durability_failed");
|
|
5577
|
+
plannedPaths.add(record.plannedArtifactsPath as string);
|
|
5578
|
+
plannedPaths.add(record.plannedTranscriptPath as string);
|
|
5579
|
+
return {
|
|
5580
|
+
taskArtifactOwnerTranscriptDeleted: record.taskArtifactOwnerTranscriptDeleted === true ? true : undefined,
|
|
5581
|
+
attempt,
|
|
5582
|
+
target,
|
|
5583
|
+
expectedArtifactsIdentity,
|
|
5584
|
+
expectedArtifactsTree,
|
|
5585
|
+
artifactsPayloadDurable: record.artifactsPayloadDurable === true ? true : undefined,
|
|
5586
|
+
artifactsRemovedAttempt: record.artifactsRemovedAttempt as number | undefined,
|
|
5587
|
+
detachedArtifactsPath: record.detachedArtifactsPath as string | undefined,
|
|
5588
|
+
detachedTranscriptPath: record.detachedTranscriptPath as string | undefined,
|
|
5589
|
+
transcriptPayloadDurable: record.transcriptPayloadDurable === true ? true : undefined,
|
|
5590
|
+
retainedArtifactsSuccessorPath: record.retainedArtifactsSuccessorPath as string | undefined,
|
|
5591
|
+
retainedArtifactsPlaceholderPath: record.retainedArtifactsPlaceholderPath as string | undefined,
|
|
5592
|
+
retainedArtifactsUnknownPath: record.retainedArtifactsUnknownPath as string | undefined,
|
|
5593
|
+
retainedTranscriptSuccessorPath: record.retainedTranscriptSuccessorPath as string | undefined,
|
|
5594
|
+
retainedTranscriptPlaceholderPath: record.retainedTranscriptPlaceholderPath as string | undefined,
|
|
5595
|
+
retainedTranscriptUnknownPath: record.retainedTranscriptUnknownPath as string | undefined,
|
|
5596
|
+
plannedArtifactsPath: record.plannedArtifactsPath as string,
|
|
5597
|
+
plannedTranscriptPath: record.plannedTranscriptPath as string,
|
|
5598
|
+
};
|
|
3005
5599
|
}
|
|
3006
5600
|
|
|
3007
5601
|
function artifactIdentityForCleanup(target: RetiredTarget): SessionStorageFileIdentity | undefined {
|
|
@@ -3043,6 +5637,7 @@ function nextCleanupReceipt(target: RetiredTarget, pending: CleanupReceipt | und
|
|
|
3043
5637
|
return {
|
|
3044
5638
|
attempt,
|
|
3045
5639
|
target,
|
|
5640
|
+
taskArtifactOwnerTranscriptDeleted: pending?.taskArtifactOwnerTranscriptDeleted,
|
|
3046
5641
|
expectedArtifactsIdentity,
|
|
3047
5642
|
expectedArtifactsTree,
|
|
3048
5643
|
artifactsPayloadDurable: pending?.artifactsPayloadDurable,
|
|
@@ -3076,6 +5671,8 @@ function cleanupPendingEvidence(
|
|
|
3076
5671
|
): CleanupReceipt {
|
|
3077
5672
|
return {
|
|
3078
5673
|
...retry,
|
|
5674
|
+
taskArtifactOwnerTranscriptDeleted:
|
|
5675
|
+
deletion.taskArtifactOwnerTranscriptDeleted ?? active.taskArtifactOwnerTranscriptDeleted,
|
|
3079
5676
|
expectedArtifactsIdentity:
|
|
3080
5677
|
deletion.phase === "artifacts" ? deletion.artifactsIdentity : active.expectedArtifactsIdentity,
|
|
3081
5678
|
expectedArtifactsTree: deletion.phase === "artifacts" ? deletion.artifactsTree : active.expectedArtifactsTree,
|
|
@@ -3119,29 +5716,43 @@ async function continueDetachedArtifactCleanup(
|
|
|
3119
5716
|
lock: ManagedStorageLock,
|
|
3120
5717
|
flow: ManagedVerifiedDeleteTestEvent["flow"],
|
|
3121
5718
|
): Promise<{ deletion: VerifiedSessionDeleteResult; pendingEvidence: CleanupReceipt }> {
|
|
3122
|
-
const deletion = await deleteSessionVerifiedWithFence(
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
5719
|
+
const deletion = await deleteSessionVerifiedWithFence(
|
|
5720
|
+
flow,
|
|
5721
|
+
"artifact-finalization",
|
|
5722
|
+
lock,
|
|
5723
|
+
{
|
|
5724
|
+
sessionsRoot: scope.sessionsRoot,
|
|
5725
|
+
transcriptPath: target.path,
|
|
5726
|
+
sessionId: target.sessionId,
|
|
5727
|
+
cwd: target.cwd,
|
|
5728
|
+
transcriptIdentity: target.identity,
|
|
5729
|
+
transcriptParentIdentity: (() => {
|
|
5730
|
+
const parent = fs.lstatSync(path.dirname(target.path), { bigint: true });
|
|
5731
|
+
return { dev: parent.dev, ino: parent.ino };
|
|
5732
|
+
})(),
|
|
5733
|
+
expectedArtifactsIdentity: pendingEvidence.expectedArtifactsIdentity,
|
|
5734
|
+
expectedArtifactsTree: pendingEvidence.expectedArtifactsTree,
|
|
5735
|
+
detachedArtifactsPath: pendingEvidence.detachedArtifactsPath,
|
|
5736
|
+
retainedArtifactsSuccessorPath: pendingEvidence.retainedArtifactsSuccessorPath,
|
|
5737
|
+
retainedArtifactsPlaceholderPath: pendingEvidence.retainedArtifactsPlaceholderPath,
|
|
5738
|
+
retainedArtifactsUnknownPath: pendingEvidence.retainedArtifactsUnknownPath,
|
|
5739
|
+
detachedTranscriptPath: pendingEvidence.detachedTranscriptPath ?? fallbackDetachedTranscriptPath,
|
|
5740
|
+
retainedTranscriptSuccessorPath: pendingEvidence.retainedTranscriptSuccessorPath,
|
|
5741
|
+
retainedTranscriptPlaceholderPath: pendingEvidence.retainedTranscriptPlaceholderPath,
|
|
5742
|
+
retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
|
|
5743
|
+
plannedArtifactsPath: pendingEvidence.plannedArtifactsPath,
|
|
5744
|
+
plannedTranscriptPath: pendingEvidence.plannedTranscriptPath,
|
|
5745
|
+
},
|
|
5746
|
+
scope,
|
|
5747
|
+
target,
|
|
5748
|
+
);
|
|
5749
|
+
if (deletion.kind === "cleanup_pending" && deletion.phase === "task_artifact_owner") {
|
|
5750
|
+
await persistManagedGcStorageOwnerDisposition(scope, target, deletion, lock);
|
|
5751
|
+
const retry = nextCleanupReceipt(target, pendingEvidence);
|
|
5752
|
+
const pending = cleanupPendingEvidence(retry, pendingEvidence, deletion);
|
|
5753
|
+
await publishCleanupPending(scope, tombstone, pending, lock);
|
|
5754
|
+
return { deletion, pendingEvidence: pending };
|
|
5755
|
+
}
|
|
3145
5756
|
if (deletion.kind === "cleanup_pending") {
|
|
3146
5757
|
if (
|
|
3147
5758
|
deletion.phase !== "artifacts" ||
|
|
@@ -3180,33 +5791,110 @@ async function publishCleanupPending(
|
|
|
3180
5791
|
if (!persisted || persisted.attempt !== receipt.attempt) throw new Error("durability_failed");
|
|
3181
5792
|
}
|
|
3182
5793
|
|
|
3183
|
-
function
|
|
5794
|
+
async function persistManagedOwnerTranscriptDeletion(
|
|
5795
|
+
scope: ManagedScope,
|
|
5796
|
+
tombstone: string,
|
|
5797
|
+
target: RetiredTarget,
|
|
5798
|
+
fallback: CleanupReceipt,
|
|
5799
|
+
lock: ManagedStorageLock,
|
|
5800
|
+
deletion: Extract<VerifiedSessionDeleteResult, { kind: "deleted" }>,
|
|
5801
|
+
): Promise<void> {
|
|
5802
|
+
return persistManagedGcOwnerTranscriptDeletion({
|
|
5803
|
+
target,
|
|
5804
|
+
fallback,
|
|
5805
|
+
deletion,
|
|
5806
|
+
readLatest: () => pendingCleanupReceipt(scope, tombstone, target),
|
|
5807
|
+
nextReceipt: latest => nextCleanupReceipt(target, latest),
|
|
5808
|
+
publishPending: receipt => publishCleanupPending(scope, tombstone, receipt, lock),
|
|
5809
|
+
});
|
|
5810
|
+
}
|
|
5811
|
+
|
|
5812
|
+
async function cleanupCompleted(scope: ManagedScope, tombstone: string, target: RetiredTarget): Promise<boolean> {
|
|
3184
5813
|
try {
|
|
5814
|
+
const ownerReceipt = target.taskArtifactOwnerDeletionEvidence
|
|
5815
|
+
? await readManagedGcSessionRetirementReceipt(scope, target.path)
|
|
5816
|
+
: undefined;
|
|
5817
|
+
if (
|
|
5818
|
+
target.taskArtifactOwnerDeletionEvidence &&
|
|
5819
|
+
(ownerReceipt?.state !== "owner_retired" ||
|
|
5820
|
+
!deepSame(ownerReceipt.taskArtifactOwnerDeletionEvidence, target.taskArtifactOwnerDeletionEvidence))
|
|
5821
|
+
)
|
|
5822
|
+
return false;
|
|
3185
5823
|
const value: unknown = JSON.parse(
|
|
3186
5824
|
captureManagedFileNoFollow(cleanupReceiptPath(tombstone, target, "completed", 1)).bytes.toString("utf8"),
|
|
3187
5825
|
);
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
} catch {
|
|
5826
|
+
return cleanupCompletedRecordMatches(scope, tombstone, target, ownerReceipt, value);
|
|
5827
|
+
} catch (error) {
|
|
5828
|
+
if (isOwnerBoundaryFailure(error) || isManagedGcJournalCapacityError(error)) throw error;
|
|
5829
|
+
return false;
|
|
5830
|
+
}
|
|
5831
|
+
}
|
|
5832
|
+
|
|
5833
|
+
function cleanupCompletedRecordMatches(
|
|
5834
|
+
scope: ManagedScope,
|
|
5835
|
+
tombstone: string,
|
|
5836
|
+
target: RetiredTarget,
|
|
5837
|
+
ownerReceipt: ManagedGcSessionRetirementReceipt | undefined,
|
|
5838
|
+
value: unknown,
|
|
5839
|
+
): boolean {
|
|
5840
|
+
if (
|
|
5841
|
+
target.taskArtifactOwnerDeletionEvidence &&
|
|
5842
|
+
(ownerReceipt?.state !== "owner_retired" ||
|
|
5843
|
+
!deepSame(ownerReceipt.taskArtifactOwnerDeletionEvidence, target.taskArtifactOwnerDeletionEvidence))
|
|
5844
|
+
)
|
|
3208
5845
|
return false;
|
|
5846
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
5847
|
+
const record = value as Record<string, unknown>;
|
|
5848
|
+
const recorded = record.target as Record<string, unknown> | undefined;
|
|
5849
|
+
const identity = recorded?.identity as Record<string, unknown> | undefined;
|
|
5850
|
+
const expectedKeys = ownerReceipt
|
|
5851
|
+
? [
|
|
5852
|
+
"schemaVersion",
|
|
5853
|
+
"state",
|
|
5854
|
+
"scope",
|
|
5855
|
+
"tombstone",
|
|
5856
|
+
"attempt",
|
|
5857
|
+
"target",
|
|
5858
|
+
"taskArtifactOwnerDeletionEvidence",
|
|
5859
|
+
"taskArtifactOwnerRetirementOutcome",
|
|
5860
|
+
"taskArtifactOwnerRetired",
|
|
5861
|
+
"taskArtifactOwnerTranscriptDeleted",
|
|
5862
|
+
]
|
|
5863
|
+
: ["schemaVersion", "state", "scope", "tombstone", "attempt", "target"];
|
|
5864
|
+
if (Object.keys(record).length !== expectedKeys.length || !expectedKeys.every(key => key in record)) return false;
|
|
5865
|
+
if (ownerReceipt) {
|
|
5866
|
+
const pending = pendingCleanupReceipt(scope, tombstone, target);
|
|
5867
|
+
if (record.taskArtifactOwnerTranscriptDeleted !== true || !pending?.taskArtifactOwnerTranscriptDeleted)
|
|
5868
|
+
return false;
|
|
5869
|
+
const evidence = parseTaskArtifactOwnerDeletionEvidence(record.taskArtifactOwnerDeletionEvidence);
|
|
5870
|
+
const outcome = parseTaskArtifactOwnerRetirementOutcome(
|
|
5871
|
+
taskArtifactOwnerStorageContextForScope(scope),
|
|
5872
|
+
evidence,
|
|
5873
|
+
record.taskArtifactOwnerRetirementOutcome,
|
|
5874
|
+
);
|
|
5875
|
+
if (
|
|
5876
|
+
record.taskArtifactOwnerRetired !== true ||
|
|
5877
|
+
outcome.kind !== "completed" ||
|
|
5878
|
+
!deepSame(evidence, ownerReceipt.taskArtifactOwnerDeletionEvidence) ||
|
|
5879
|
+
!deepSame(outcome, ownerReceipt.taskArtifactOwnerRetirementOutcome)
|
|
5880
|
+
)
|
|
5881
|
+
return false;
|
|
3209
5882
|
}
|
|
5883
|
+
return (
|
|
5884
|
+
record.schemaVersion === 1 &&
|
|
5885
|
+
record.state === "cleanup_completed" &&
|
|
5886
|
+
record.scope === scopeDigest(scope.platform, scope.canonicalCwd) &&
|
|
5887
|
+
record.tombstone === tombstone &&
|
|
5888
|
+
record.attempt === 1 &&
|
|
5889
|
+
recorded?.path === target.path &&
|
|
5890
|
+
recorded.sessionId === target.sessionId &&
|
|
5891
|
+
recorded.cwd === target.cwd &&
|
|
5892
|
+
identity?.dev === String(target.identity.dev) &&
|
|
5893
|
+
identity.ino === String(target.identity.ino) &&
|
|
5894
|
+
identity.size === target.identity.size &&
|
|
5895
|
+
identity.mtimeNs === String(target.identity.mtimeNs) &&
|
|
5896
|
+
identity.sha256 === target.identity.sha256
|
|
5897
|
+
);
|
|
3210
5898
|
}
|
|
3211
5899
|
|
|
3212
5900
|
async function publishCleanupCompleted(
|
|
@@ -3215,6 +5903,22 @@ async function publishCleanupCompleted(
|
|
|
3215
5903
|
target: RetiredTarget,
|
|
3216
5904
|
lock: ManagedStorageLock,
|
|
3217
5905
|
): Promise<void> {
|
|
5906
|
+
const ownerReceipt = target.taskArtifactOwnerDeletionEvidence
|
|
5907
|
+
? await readManagedGcSessionRetirementReceipt(scope, target.path)
|
|
5908
|
+
: undefined;
|
|
5909
|
+
if (
|
|
5910
|
+
target.taskArtifactOwnerDeletionEvidence &&
|
|
5911
|
+
(ownerReceipt?.state !== "owner_retired" ||
|
|
5912
|
+
!deepSame(ownerReceipt.taskArtifactOwnerDeletionEvidence, target.taskArtifactOwnerDeletionEvidence))
|
|
5913
|
+
)
|
|
5914
|
+
throw new Error("durability_failed");
|
|
5915
|
+
const ownerOutcome = ownerReceipt?.taskArtifactOwnerRetirementOutcome;
|
|
5916
|
+
if (
|
|
5917
|
+
ownerReceipt &&
|
|
5918
|
+
(ownerOutcome?.kind !== "completed" ||
|
|
5919
|
+
!pendingCleanupReceipt(scope, tombstone, target)?.taskArtifactOwnerTranscriptDeleted)
|
|
5920
|
+
)
|
|
5921
|
+
throw new Error("durability_failed");
|
|
3218
5922
|
try {
|
|
3219
5923
|
await publishManagedTombstone(
|
|
3220
5924
|
cleanupReceiptPath(tombstone, target, "completed", 1),
|
|
@@ -3225,12 +5929,21 @@ async function publishCleanupCompleted(
|
|
|
3225
5929
|
tombstone,
|
|
3226
5930
|
attempt: 1,
|
|
3227
5931
|
target: { path: target.path, sessionId: target.sessionId, cwd: target.cwd, identity: target.identity },
|
|
5932
|
+
...(ownerReceipt && ownerOutcome
|
|
5933
|
+
? {
|
|
5934
|
+
taskArtifactOwnerDeletionEvidence: ownerReceipt.taskArtifactOwnerDeletionEvidence,
|
|
5935
|
+
taskArtifactOwnerRetirementOutcome: ownerOutcome,
|
|
5936
|
+
taskArtifactOwnerRetired: true,
|
|
5937
|
+
taskArtifactOwnerTranscriptDeleted: true,
|
|
5938
|
+
}
|
|
5939
|
+
: {}),
|
|
3228
5940
|
},
|
|
3229
5941
|
lock.assertOwned,
|
|
3230
5942
|
);
|
|
3231
5943
|
} catch (error) {
|
|
3232
5944
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
3233
5945
|
}
|
|
5946
|
+
if (!(await cleanupCompleted(scope, tombstone, target))) throw new Error("durability_failed");
|
|
3234
5947
|
}
|
|
3235
5948
|
|
|
3236
5949
|
function tombstonePathContaining(scope: ManagedScope, candidate: ManagedCandidate): string | undefined {
|
|
@@ -3240,7 +5953,8 @@ function tombstonePathContaining(scope: ManagedScope, candidate: ManagedCandidat
|
|
|
3240
5953
|
const pathname = path.join(directory, name);
|
|
3241
5954
|
if (retiredTargets(scope, pathname)?.some(target => sameCandidate(target, candidate))) return pathname;
|
|
3242
5955
|
}
|
|
3243
|
-
} catch {
|
|
5956
|
+
} catch (error) {
|
|
5957
|
+
if (isOwnerBoundaryFailure(error) || (error as Error).message === "durability_failed") throw error;
|
|
3244
5958
|
return undefined;
|
|
3245
5959
|
}
|
|
3246
5960
|
return undefined;
|
|
@@ -3554,7 +6268,7 @@ export function cleanupAuthorityMatches(
|
|
|
3554
6268
|
parentStat.ino !== cleanup.identity.parentIno
|
|
3555
6269
|
)
|
|
3556
6270
|
return false;
|
|
3557
|
-
const snapshot = nativeScope().
|
|
6271
|
+
const snapshot = nativeScope().snapshotEmptyDirectory(cleanup.retainedPath);
|
|
3558
6272
|
const observedRoot = snapshot.snapshot?.entries.find(
|
|
3559
6273
|
entry => entry.relativePath === "" && entry.kind === "directory",
|
|
3560
6274
|
);
|
|
@@ -3618,7 +6332,7 @@ export function detachArtifactRootForMigration(
|
|
|
3618
6332
|
const stat = fs.lstatSync(placeholder, { bigint: true });
|
|
3619
6333
|
if (!stat.isDirectory() || stat.isSymbolicLink() || path.dirname(placeholder) !== path.dirname(plan.originalPath))
|
|
3620
6334
|
throw new Error("durability_failed");
|
|
3621
|
-
const snapshot = nativeScope().
|
|
6335
|
+
const snapshot = nativeScope().snapshotEmptyDirectory(placeholder);
|
|
3622
6336
|
if (!snapshot.ok || !snapshot.snapshot) throw new Error("durability_failed");
|
|
3623
6337
|
// Windows directory size/mtime authority is the native tree root, never Bun's
|
|
3624
6338
|
// zero-valued directory lstat. Capturing Bun values here would guarantee a
|
|
@@ -3983,11 +6697,23 @@ export async function reconcileManagedTombstones(
|
|
|
3983
6697
|
expectedCandidate?: ManagedCandidate,
|
|
3984
6698
|
): Promise<void> {
|
|
3985
6699
|
const directory = path.join(managedInternalDirectory(scope), MANAGED_TOMBSTONES_DIRECTORY);
|
|
6700
|
+
// Ensure scope authority is prepared before any operations that might access the GC protocol
|
|
6701
|
+
const prepared = await ensureManagedScope(
|
|
6702
|
+
scope,
|
|
6703
|
+
scope.platform === "win32" ? "windows-existing-verify-first" : "default",
|
|
6704
|
+
);
|
|
6705
|
+
if (prepared.kind === "error") throw new Error(prepared.message);
|
|
3986
6706
|
for (const name of fs.readdirSync(directory)) {
|
|
3987
6707
|
const tombstone = path.join(directory, name);
|
|
3988
|
-
const
|
|
3989
|
-
if (!
|
|
3990
|
-
|
|
6708
|
+
const discoveredTargets = retiredTargets(scope, tombstone);
|
|
6709
|
+
if (!discoveredTargets || discoveredTargets.some(target => hasUnsupportedLegacyOwnerTarget(scope, target)))
|
|
6710
|
+
continue;
|
|
6711
|
+
if (
|
|
6712
|
+
await Promise.all(discoveredTargets.map(target => cleanupCompleted(scope, tombstone, target))).then(values =>
|
|
6713
|
+
values.every(Boolean),
|
|
6714
|
+
)
|
|
6715
|
+
)
|
|
6716
|
+
continue;
|
|
3991
6717
|
let lock: ManagedStorageLock | undefined;
|
|
3992
6718
|
try {
|
|
3993
6719
|
lock = await acquireManagedLock(
|
|
@@ -3995,11 +6721,22 @@ export async function reconcileManagedTombstones(
|
|
|
3995
6721
|
path.basename(tombstone, ".json"),
|
|
3996
6722
|
scopeRoot(scope),
|
|
3997
6723
|
);
|
|
3998
|
-
const
|
|
3999
|
-
if (
|
|
6724
|
+
const discoveredLockedTargets = retiredTargets(scope, tombstone);
|
|
6725
|
+
if (
|
|
6726
|
+
!discoveredLockedTargets ||
|
|
6727
|
+
discoveredLockedTargets.some(target => hasUnsupportedLegacyOwnerTarget(scope, target))
|
|
6728
|
+
)
|
|
6729
|
+
continue;
|
|
6730
|
+
const lockedTargets = await prepareManagedGcOwnerTargets(scope, discoveredLockedTargets, lock);
|
|
6731
|
+
if (
|
|
6732
|
+
await Promise.all(lockedTargets.map(target => cleanupCompleted(scope, tombstone, target))).then(values =>
|
|
6733
|
+
values.every(Boolean),
|
|
6734
|
+
)
|
|
6735
|
+
)
|
|
6736
|
+
continue;
|
|
4000
6737
|
for (const target of lockedTargets) {
|
|
4001
6738
|
lock.assertOwned();
|
|
4002
|
-
if (cleanupCompleted(scope, tombstone, target)) continue;
|
|
6739
|
+
if (await cleanupCompleted(scope, tombstone, target)) continue;
|
|
4003
6740
|
try {
|
|
4004
6741
|
const pending = pendingCleanupReceipt(scope, tombstone, target);
|
|
4005
6742
|
const observedPending = pending ? probePlannedCleanupDetach(target, pending) : undefined;
|
|
@@ -4024,8 +6761,23 @@ export async function reconcileManagedTombstones(
|
|
|
4024
6761
|
target,
|
|
4025
6762
|
replayReceipt,
|
|
4026
6763
|
artifactsEvidence.retainedArtifactsRootPath,
|
|
4027
|
-
)
|
|
6764
|
+
) &&
|
|
6765
|
+
(!target.taskArtifactOwnerDeletionEvidence ||
|
|
6766
|
+
pending?.taskArtifactOwnerTranscriptDeleted === true)
|
|
4028
6767
|
) {
|
|
6768
|
+
// A conflicting legacy completion record is not fresh completion authority.
|
|
6769
|
+
if (
|
|
6770
|
+
!target.taskArtifactOwnerDeletionEvidence &&
|
|
6771
|
+
fs.existsSync(cleanupReceiptPath(tombstone, target, "completed", 1))
|
|
6772
|
+
)
|
|
6773
|
+
continue;
|
|
6774
|
+
const ownerProgress = await retireManagedGcOwnerAfterArtifacts(
|
|
6775
|
+
scope,
|
|
6776
|
+
target,
|
|
6777
|
+
lockedTargets,
|
|
6778
|
+
lock,
|
|
6779
|
+
);
|
|
6780
|
+
if (ownerProgress.state === "pending" || ownerProgress.state === "payload_retired") continue;
|
|
4029
6781
|
fsyncManagedParent(target.path);
|
|
4030
6782
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
4031
6783
|
continue;
|
|
@@ -4050,38 +6802,11 @@ export async function reconcileManagedTombstones(
|
|
|
4050
6802
|
? target
|
|
4051
6803
|
: validateCandidateForScope(scope, target);
|
|
4052
6804
|
if (!initialTarget) throw new Error("source_changed");
|
|
4053
|
-
let
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
transcriptIdentity: {
|
|
4059
|
-
...initialTarget.identity,
|
|
4060
|
-
nlink: fs.lstatSync(observedPending?.detachedTranscriptPath ?? target.path, { bigint: true })
|
|
4061
|
-
.nlink,
|
|
4062
|
-
},
|
|
4063
|
-
expectedArtifactsIdentity: active.expectedArtifactsIdentity,
|
|
4064
|
-
expectedArtifactsTree: active.expectedArtifactsTree,
|
|
4065
|
-
detachedArtifactsPath:
|
|
4066
|
-
active.detachedArtifactsPath ??
|
|
4067
|
-
observedPending?.detachedArtifactsPath ??
|
|
4068
|
-
(fs.existsSync(active.plannedArtifactsPath) ? active.plannedArtifactsPath : undefined),
|
|
4069
|
-
detachedTranscriptPath:
|
|
4070
|
-
active.detachedTranscriptPath ??
|
|
4071
|
-
observedPending?.detachedTranscriptPath ??
|
|
4072
|
-
(pending && fs.existsSync(pending.plannedTranscriptPath)
|
|
4073
|
-
? pending.plannedTranscriptPath
|
|
4074
|
-
: undefined) ??
|
|
4075
|
-
(fs.existsSync(active.plannedTranscriptPath) ? active.plannedTranscriptPath : undefined),
|
|
4076
|
-
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
4077
|
-
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
4078
|
-
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
4079
|
-
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
4080
|
-
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
4081
|
-
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
4082
|
-
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
4083
|
-
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
4084
|
-
...(cleanupArtifactsRemoved(
|
|
6805
|
+
let ownerProgress = await managedGcOwnerProgressBeforeDelete(
|
|
6806
|
+
scope,
|
|
6807
|
+
target,
|
|
6808
|
+
lockedTargets,
|
|
6809
|
+
cleanupArtifactsRemoved(
|
|
4085
6810
|
scope,
|
|
4086
6811
|
tombstone,
|
|
4087
6812
|
target,
|
|
@@ -4089,12 +6814,65 @@ export async function reconcileManagedTombstones(
|
|
|
4089
6814
|
active.artifactsRemovedAttempt ??
|
|
4090
6815
|
pending?.attempt ??
|
|
4091
6816
|
active.attempt,
|
|
4092
|
-
)
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
6817
|
+
),
|
|
6818
|
+
lock,
|
|
6819
|
+
);
|
|
6820
|
+
if (ownerProgress.state === "pending") continue;
|
|
6821
|
+
let deletion = await deleteSessionVerifiedWithFence(
|
|
6822
|
+
"reconcile",
|
|
6823
|
+
"initial",
|
|
6824
|
+
lock,
|
|
6825
|
+
{
|
|
6826
|
+
sessionsRoot: scope.sessionsRoot,
|
|
6827
|
+
transcriptPath: target.path,
|
|
6828
|
+
sessionId: target.sessionId,
|
|
6829
|
+
cwd: target.cwd,
|
|
6830
|
+
transcriptIdentity: {
|
|
6831
|
+
...initialTarget.identity,
|
|
6832
|
+
nlink: fs.lstatSync(observedPending?.detachedTranscriptPath ?? target.path, { bigint: true })
|
|
6833
|
+
.nlink,
|
|
6834
|
+
},
|
|
6835
|
+
expectedArtifactsIdentity: active.expectedArtifactsIdentity,
|
|
6836
|
+
expectedArtifactsTree: active.expectedArtifactsTree,
|
|
6837
|
+
detachedArtifactsPath:
|
|
6838
|
+
active.detachedArtifactsPath ??
|
|
6839
|
+
observedPending?.detachedArtifactsPath ??
|
|
6840
|
+
(fs.existsSync(active.plannedArtifactsPath) ? active.plannedArtifactsPath : undefined),
|
|
6841
|
+
detachedTranscriptPath:
|
|
6842
|
+
active.detachedTranscriptPath ??
|
|
6843
|
+
observedPending?.detachedTranscriptPath ??
|
|
6844
|
+
(pending && fs.existsSync(pending.plannedTranscriptPath)
|
|
6845
|
+
? pending.plannedTranscriptPath
|
|
6846
|
+
: undefined) ??
|
|
6847
|
+
(fs.existsSync(active.plannedTranscriptPath) ? active.plannedTranscriptPath : undefined),
|
|
6848
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
6849
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
6850
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
6851
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
6852
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
6853
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
6854
|
+
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
6855
|
+
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
6856
|
+
...(cleanupArtifactsRemoved(
|
|
6857
|
+
scope,
|
|
6858
|
+
tombstone,
|
|
6859
|
+
target,
|
|
6860
|
+
pending?.artifactsRemovedAttempt ??
|
|
6861
|
+
active.artifactsRemovedAttempt ??
|
|
6862
|
+
pending?.attempt ??
|
|
6863
|
+
active.attempt,
|
|
6864
|
+
)
|
|
6865
|
+
? { artifactsRemoved: true as const }
|
|
6866
|
+
: {}),
|
|
6867
|
+
},
|
|
6868
|
+
scope,
|
|
6869
|
+
target,
|
|
6870
|
+
);
|
|
4096
6871
|
if (deletion.kind === "artifacts_removed") {
|
|
6872
|
+
await publishManagedGcArtifactsRemoved(scope, target, lock);
|
|
4097
6873
|
await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
|
|
6874
|
+
ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, lockedTargets, lock);
|
|
6875
|
+
if (ownerProgress.state === "pending") continue;
|
|
4098
6876
|
active = { ...active, artifactsRemovedAttempt: active.attempt };
|
|
4099
6877
|
const refreshedTarget = validateCandidateForScope(scope, target);
|
|
4100
6878
|
if (!refreshedTarget) throw new Error("source_changed");
|
|
@@ -4121,9 +6899,15 @@ export async function reconcileManagedTombstones(
|
|
|
4121
6899
|
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
4122
6900
|
artifactsRemoved: true,
|
|
4123
6901
|
},
|
|
6902
|
+
scope,
|
|
6903
|
+
target,
|
|
4124
6904
|
);
|
|
4125
6905
|
}
|
|
4126
6906
|
if (deletion.kind === "cleanup_pending") {
|
|
6907
|
+
if (deletion.phase === "task_artifact_owner") {
|
|
6908
|
+
ownerProgress = await persistManagedGcStorageOwnerDisposition(scope, target, deletion, lock);
|
|
6909
|
+
continue;
|
|
6910
|
+
}
|
|
4127
6911
|
assertAuthorizedCleanupPending(target, active, deletion);
|
|
4128
6912
|
const retry = nextCleanupReceipt(target, active);
|
|
4129
6913
|
let pendingEvidence = cleanupPendingEvidence(retry, active, deletion);
|
|
@@ -4150,7 +6934,10 @@ export async function reconcileManagedTombstones(
|
|
|
4150
6934
|
!retainedArtifactPayloadAbsent(deletion.detachedArtifactsPath))
|
|
4151
6935
|
)
|
|
4152
6936
|
continue;
|
|
6937
|
+
await publishManagedGcArtifactsRemoved(scope, target, lock);
|
|
4153
6938
|
await publishCleanupArtifactsRemoved(scope, tombstone, pendingEvidence, lock);
|
|
6939
|
+
ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, lockedTargets, lock);
|
|
6940
|
+
if (ownerProgress.state === "pending") continue;
|
|
4154
6941
|
pendingEvidence = { ...pendingEvidence, artifactsRemovedAttempt: pendingEvidence.attempt };
|
|
4155
6942
|
const retainedProof = cleanupArtifactsRemovedReceipt(
|
|
4156
6943
|
tombstone,
|
|
@@ -4186,6 +6973,8 @@ export async function reconcileManagedTombstones(
|
|
|
4186
6973
|
retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
|
|
4187
6974
|
artifactsRemoved: true,
|
|
4188
6975
|
},
|
|
6976
|
+
scope,
|
|
6977
|
+
target,
|
|
4189
6978
|
() => {
|
|
4190
6979
|
if (
|
|
4191
6980
|
!cleanupArtifactsRemoved(
|
|
@@ -4198,6 +6987,10 @@ export async function reconcileManagedTombstones(
|
|
|
4198
6987
|
throw new Error("durability_failed");
|
|
4199
6988
|
},
|
|
4200
6989
|
);
|
|
6990
|
+
if (deletion.kind === "cleanup_pending" && deletion.phase === "task_artifact_owner") {
|
|
6991
|
+
ownerProgress = await persistManagedGcStorageOwnerDisposition(scope, target, deletion, lock);
|
|
6992
|
+
continue;
|
|
6993
|
+
}
|
|
4201
6994
|
if (deletion.kind === "cleanup_pending") {
|
|
4202
6995
|
if (
|
|
4203
6996
|
deletion.phase !== "transcript" ||
|
|
@@ -4231,9 +7024,19 @@ export async function reconcileManagedTombstones(
|
|
|
4231
7024
|
)
|
|
4232
7025
|
continue;
|
|
4233
7026
|
}
|
|
7027
|
+
if (deletion.kind === "deleted")
|
|
7028
|
+
await persistManagedOwnerTranscriptDeletion(scope, tombstone, target, active, lock, deletion);
|
|
7029
|
+
const ownerDisposition = await taskArtifactOwnerTranscriptResult(scope, target, lock);
|
|
7030
|
+
if (
|
|
7031
|
+
(ownerDisposition !== "none" && ownerDisposition !== "retired") ||
|
|
7032
|
+
(target.taskArtifactOwnerDeletionEvidence &&
|
|
7033
|
+
!pendingCleanupReceipt(scope, tombstone, target)?.taskArtifactOwnerTranscriptDeleted)
|
|
7034
|
+
)
|
|
7035
|
+
continue;
|
|
4234
7036
|
fsyncManagedParent(target.path);
|
|
4235
7037
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
4236
7038
|
} catch (error) {
|
|
7039
|
+
if (isManagedGcJournalCapacityError(error)) throw error;
|
|
4237
7040
|
if (!expectedCandidate || target.sessionId === expectedCandidate.sessionId) throw error;
|
|
4238
7041
|
logger.warn("Tombstone reconciliation failed for one target; will retry on a future scope open", {
|
|
4239
7042
|
tombstone,
|
|
@@ -4651,6 +7454,24 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4651
7454
|
scope: ManagedScope,
|
|
4652
7455
|
candidate: ManagedCandidate,
|
|
4653
7456
|
): Promise<ManagedDeleteCandidateResult> {
|
|
7457
|
+
try {
|
|
7458
|
+
if (hasUnsupportedLegacyOwnerTargetInternal(scope.directoryPath, candidate)) {
|
|
7459
|
+
const tombstonePath = path.join(
|
|
7460
|
+
managedInternalDirectory(scope),
|
|
7461
|
+
MANAGED_TOMBSTONES_DIRECTORY,
|
|
7462
|
+
`${stableOperationName(candidate)}.json`,
|
|
7463
|
+
);
|
|
7464
|
+
return {
|
|
7465
|
+
kind: "cleanup_pending",
|
|
7466
|
+
tombstonePath,
|
|
7467
|
+
phase: "artifacts",
|
|
7468
|
+
message: "task_artifact_owner_legacy_scope_unsupported",
|
|
7469
|
+
};
|
|
7470
|
+
}
|
|
7471
|
+
} catch (error) {
|
|
7472
|
+
const code = expectedFailure(error);
|
|
7473
|
+
return { kind: "error", code, message: error instanceof Error ? error.message : "Managed deletion failed." };
|
|
7474
|
+
}
|
|
4654
7475
|
const prepared = await prepareManagedSessionScopeForWrite(scope);
|
|
4655
7476
|
if (prepared.kind === "error")
|
|
4656
7477
|
return {
|
|
@@ -4660,6 +7481,12 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4660
7481
|
};
|
|
4661
7482
|
const current = validateCandidateForScope(scope, candidate);
|
|
4662
7483
|
const paired = current ? receiptPair(scope, current) : undefined;
|
|
7484
|
+
if (paired)
|
|
7485
|
+
assertOwnerTranscriptLocator({
|
|
7486
|
+
transcriptPath: paired.path,
|
|
7487
|
+
sessionId: paired.sessionId,
|
|
7488
|
+
transcriptIdentity: paired.identity,
|
|
7489
|
+
});
|
|
4663
7490
|
const logical = paired?.provenance === "legacy" ? paired : (current ?? candidate);
|
|
4664
7491
|
const existingTombstone = tombstonePathContaining(scope, candidate);
|
|
4665
7492
|
const tombstone =
|
|
@@ -4676,9 +7503,17 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4676
7503
|
scopeRoot(scope),
|
|
4677
7504
|
);
|
|
4678
7505
|
let targets = retiredTargets(scope, tombstone);
|
|
7506
|
+
const initialTargets = targets ?? (current ? [current, ...(paired ? [paired] : [])] : []);
|
|
7507
|
+
if (initialTargets.some(target => hasUnsupportedLegacyOwnerTarget(scope, target)))
|
|
7508
|
+
return {
|
|
7509
|
+
kind: "cleanup_pending",
|
|
7510
|
+
tombstonePath: tombstone,
|
|
7511
|
+
phase: "artifacts",
|
|
7512
|
+
message: "task_artifact_owner_legacy_scope_unsupported",
|
|
7513
|
+
};
|
|
4679
7514
|
if (!targets) {
|
|
4680
7515
|
if (!current) throw new Error("source_changed");
|
|
4681
|
-
targets = [current, ...(paired ? [paired] : [])];
|
|
7516
|
+
targets = await prepareManagedGcOwnerTargets(scope, [current, ...(paired ? [paired] : [])], lock);
|
|
4682
7517
|
lock.assertOwned();
|
|
4683
7518
|
try {
|
|
4684
7519
|
await publishManagedTombstone(
|
|
@@ -4693,6 +7528,9 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4693
7528
|
cwd: target.cwd,
|
|
4694
7529
|
provenance: target.provenance,
|
|
4695
7530
|
identity: target.identity,
|
|
7531
|
+
...(target.taskArtifactOwnerDeletionEvidence
|
|
7532
|
+
? { taskArtifactOwnerDeletionEvidence: target.taskArtifactOwnerDeletionEvidence }
|
|
7533
|
+
: {}),
|
|
4696
7534
|
})),
|
|
4697
7535
|
},
|
|
4698
7536
|
lock.assertOwned,
|
|
@@ -4703,6 +7541,7 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4703
7541
|
targets = retiredTargets(scope, tombstone);
|
|
4704
7542
|
if (!targets) throw new Error("durability_failed");
|
|
4705
7543
|
}
|
|
7544
|
+
targets = await prepareManagedGcOwnerTargets(scope, targets, lock);
|
|
4706
7545
|
let deletedAny = false;
|
|
4707
7546
|
for (const target of targets) {
|
|
4708
7547
|
lock.assertOwned();
|
|
@@ -4712,6 +7551,42 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4712
7551
|
fs.lstatSync(target.path);
|
|
4713
7552
|
} catch (error) {
|
|
4714
7553
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
7554
|
+
if (!observedPending && target.taskArtifactOwnerDeletionEvidence) {
|
|
7555
|
+
if (!pending?.taskArtifactOwnerTranscriptDeleted)
|
|
7556
|
+
return {
|
|
7557
|
+
kind: "cleanup_pending",
|
|
7558
|
+
tombstonePath: tombstone,
|
|
7559
|
+
phase: "artifacts",
|
|
7560
|
+
message: "task_artifact_owner_transcript_deletion_unverified",
|
|
7561
|
+
};
|
|
7562
|
+
const artifactsEvidence = cleanupArtifactsRemovedEvidence(
|
|
7563
|
+
scope,
|
|
7564
|
+
tombstone,
|
|
7565
|
+
target,
|
|
7566
|
+
pending.artifactsRemovedAttempt ?? pending.attempt,
|
|
7567
|
+
);
|
|
7568
|
+
if (
|
|
7569
|
+
!artifactsEvidence ||
|
|
7570
|
+
!cleanupRootsAbsent(tombstone, target, pending, artifactsEvidence.retainedArtifactsRootPath)
|
|
7571
|
+
)
|
|
7572
|
+
return {
|
|
7573
|
+
kind: "cleanup_pending",
|
|
7574
|
+
tombstonePath: tombstone,
|
|
7575
|
+
phase: "artifacts",
|
|
7576
|
+
message: "task_artifact_owner_retirement_pending",
|
|
7577
|
+
};
|
|
7578
|
+
const ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, targets, lock);
|
|
7579
|
+
if (ownerProgress.state === "pending" || ownerProgress.state === "payload_retired")
|
|
7580
|
+
return {
|
|
7581
|
+
kind: "cleanup_pending",
|
|
7582
|
+
tombstonePath: tombstone,
|
|
7583
|
+
phase: "artifacts",
|
|
7584
|
+
message: ownerProgress.message ?? "task_artifact_owner_retirement_pending",
|
|
7585
|
+
};
|
|
7586
|
+
fsyncManagedParent(target.path);
|
|
7587
|
+
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
7588
|
+
continue;
|
|
7589
|
+
}
|
|
4715
7590
|
if (!observedPending) continue;
|
|
4716
7591
|
const artifactsEvidence = cleanupArtifactsRemovedEvidence(
|
|
4717
7592
|
scope,
|
|
@@ -4721,15 +7596,31 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4721
7596
|
);
|
|
4722
7597
|
if (
|
|
4723
7598
|
artifactsEvidence &&
|
|
4724
|
-
cleanupRootsAbsent(tombstone, target, observedPending, artifactsEvidence.retainedArtifactsRootPath)
|
|
7599
|
+
cleanupRootsAbsent(tombstone, target, observedPending, artifactsEvidence.retainedArtifactsRootPath) &&
|
|
7600
|
+
(!target.taskArtifactOwnerDeletionEvidence || pending?.taskArtifactOwnerTranscriptDeleted === true)
|
|
4725
7601
|
) {
|
|
7602
|
+
const ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, targets, lock);
|
|
7603
|
+
if (ownerProgress.state === "pending" || ownerProgress.state === "payload_retired")
|
|
7604
|
+
return {
|
|
7605
|
+
kind: "cleanup_pending",
|
|
7606
|
+
tombstonePath: tombstone,
|
|
7607
|
+
phase: "artifacts",
|
|
7608
|
+
message: ownerProgress.message ?? "task_artifact_owner_retirement_pending",
|
|
7609
|
+
};
|
|
4726
7610
|
fsyncManagedParent(target.path);
|
|
4727
7611
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
4728
7612
|
continue;
|
|
4729
7613
|
}
|
|
7614
|
+
if (target.taskArtifactOwnerDeletionEvidence && !observedPending.detachedTranscriptPath)
|
|
7615
|
+
return {
|
|
7616
|
+
kind: "cleanup_pending",
|
|
7617
|
+
tombstonePath: tombstone,
|
|
7618
|
+
phase: "artifacts",
|
|
7619
|
+
message: "task_artifact_owner_transcript_deletion_unverified",
|
|
7620
|
+
};
|
|
4730
7621
|
} else throw error;
|
|
4731
7622
|
}
|
|
4732
|
-
if (cleanupCompleted(scope, tombstone, target)) continue;
|
|
7623
|
+
if (await cleanupCompleted(scope, tombstone, target)) continue;
|
|
4733
7624
|
deletedAny = true;
|
|
4734
7625
|
const verified = observedPending ? target : validateCandidateForScope(scope, target);
|
|
4735
7626
|
if (!verified || !sameCandidate(verified, target)) throw new Error("source_changed");
|
|
@@ -4745,64 +7636,118 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4745
7636
|
await publishCleanupPending(scope, tombstone, active, lock);
|
|
4746
7637
|
const initialTarget = validateCandidateForScope(scope, target);
|
|
4747
7638
|
if (!initialTarget) throw new Error("source_changed");
|
|
4748
|
-
let
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
transcriptIdentity: initialTarget.identity,
|
|
4754
|
-
transcriptParentIdentity: (() => {
|
|
4755
|
-
const parent = fs.lstatSync(path.dirname(target.path), { bigint: true });
|
|
4756
|
-
return { dev: parent.dev, ino: parent.ino };
|
|
4757
|
-
})(),
|
|
4758
|
-
expectedArtifactsIdentity: active.expectedArtifactsIdentity,
|
|
4759
|
-
expectedArtifactsTree: active.expectedArtifactsTree,
|
|
4760
|
-
detachedArtifactsPath: active.detachedArtifactsPath ?? observedPending?.detachedArtifactsPath,
|
|
4761
|
-
detachedTranscriptPath:
|
|
4762
|
-
active.detachedTranscriptPath ??
|
|
4763
|
-
observedPending?.detachedTranscriptPath ??
|
|
4764
|
-
(pending && fs.existsSync(pending.plannedTranscriptPath) ? pending.plannedTranscriptPath : undefined),
|
|
4765
|
-
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
4766
|
-
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
4767
|
-
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
4768
|
-
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
4769
|
-
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
4770
|
-
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
4771
|
-
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
4772
|
-
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
4773
|
-
...(cleanupArtifactsRemoved(
|
|
7639
|
+
let ownerProgress = await managedGcOwnerProgressBeforeDelete(
|
|
7640
|
+
scope,
|
|
7641
|
+
target,
|
|
7642
|
+
targets,
|
|
7643
|
+
cleanupArtifactsRemoved(
|
|
4774
7644
|
scope,
|
|
4775
7645
|
tombstone,
|
|
4776
7646
|
target,
|
|
4777
7647
|
pending?.artifactsRemovedAttempt ?? active.artifactsRemovedAttempt ?? pending?.attempt ?? active.attempt,
|
|
4778
|
-
)
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
7648
|
+
),
|
|
7649
|
+
lock,
|
|
7650
|
+
);
|
|
7651
|
+
if (ownerProgress.state === "pending")
|
|
7652
|
+
return {
|
|
7653
|
+
kind: "cleanup_pending",
|
|
7654
|
+
tombstonePath: tombstone,
|
|
7655
|
+
phase: "artifacts",
|
|
7656
|
+
message: ownerProgress.message ?? "task_artifact_owner_retirement_pending",
|
|
7657
|
+
};
|
|
7658
|
+
let deletion = await deleteSessionVerifiedWithFence(
|
|
7659
|
+
"direct",
|
|
7660
|
+
"initial",
|
|
7661
|
+
lock,
|
|
7662
|
+
{
|
|
4788
7663
|
sessionsRoot: scope.sessionsRoot,
|
|
4789
7664
|
transcriptPath: target.path,
|
|
4790
7665
|
sessionId: target.sessionId,
|
|
4791
7666
|
cwd: target.cwd,
|
|
4792
|
-
transcriptIdentity:
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
7667
|
+
transcriptIdentity: initialTarget.identity,
|
|
7668
|
+
transcriptParentIdentity: (() => {
|
|
7669
|
+
const parent = fs.lstatSync(path.dirname(target.path), { bigint: true });
|
|
7670
|
+
return { dev: parent.dev, ino: parent.ino };
|
|
7671
|
+
})(),
|
|
7672
|
+
expectedArtifactsIdentity: active.expectedArtifactsIdentity,
|
|
7673
|
+
expectedArtifactsTree: active.expectedArtifactsTree,
|
|
7674
|
+
detachedArtifactsPath: active.detachedArtifactsPath ?? observedPending?.detachedArtifactsPath,
|
|
7675
|
+
detachedTranscriptPath:
|
|
7676
|
+
active.detachedTranscriptPath ??
|
|
7677
|
+
observedPending?.detachedTranscriptPath ??
|
|
7678
|
+
(pending && fs.existsSync(pending.plannedTranscriptPath) ? pending.plannedTranscriptPath : undefined),
|
|
4796
7679
|
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
4797
7680
|
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
4798
7681
|
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
4799
7682
|
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
4800
7683
|
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
4801
7684
|
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
4802
|
-
|
|
4803
|
-
|
|
7685
|
+
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
7686
|
+
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
7687
|
+
...(cleanupArtifactsRemoved(
|
|
7688
|
+
scope,
|
|
7689
|
+
tombstone,
|
|
7690
|
+
target,
|
|
7691
|
+
pending?.artifactsRemovedAttempt ??
|
|
7692
|
+
active.artifactsRemovedAttempt ??
|
|
7693
|
+
pending?.attempt ??
|
|
7694
|
+
active.attempt,
|
|
7695
|
+
)
|
|
7696
|
+
? { artifactsRemoved: true as const }
|
|
7697
|
+
: {}),
|
|
7698
|
+
},
|
|
7699
|
+
scope,
|
|
7700
|
+
target,
|
|
7701
|
+
);
|
|
7702
|
+
if (deletion.kind === "artifacts_removed") {
|
|
7703
|
+
await publishManagedGcArtifactsRemoved(scope, target, lock);
|
|
7704
|
+
await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
|
|
7705
|
+
ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, targets, lock);
|
|
7706
|
+
if (ownerProgress.state === "pending")
|
|
7707
|
+
return {
|
|
7708
|
+
kind: "cleanup_pending",
|
|
7709
|
+
tombstonePath: tombstone,
|
|
7710
|
+
phase: "artifacts",
|
|
7711
|
+
message: ownerProgress.message ?? "task_artifact_owner_retirement_pending",
|
|
7712
|
+
};
|
|
7713
|
+
active = { ...active, artifactsRemovedAttempt: active.attempt };
|
|
7714
|
+
const refreshedTarget = validateCandidateForScope(scope, target);
|
|
7715
|
+
if (!refreshedTarget) throw new Error("source_changed");
|
|
7716
|
+
deletion = await deleteSessionVerifiedWithFence(
|
|
7717
|
+
"direct",
|
|
7718
|
+
"transcript-after-artifacts-removed",
|
|
7719
|
+
lock,
|
|
7720
|
+
{
|
|
7721
|
+
sessionsRoot: scope.sessionsRoot,
|
|
7722
|
+
transcriptPath: target.path,
|
|
7723
|
+
sessionId: target.sessionId,
|
|
7724
|
+
cwd: target.cwd,
|
|
7725
|
+
transcriptIdentity: refreshedTarget.identity,
|
|
7726
|
+
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
7727
|
+
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
7728
|
+
detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
|
|
7729
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
7730
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
7731
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
7732
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
7733
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
7734
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
7735
|
+
artifactsRemoved: true,
|
|
7736
|
+
},
|
|
7737
|
+
scope,
|
|
7738
|
+
target,
|
|
7739
|
+
);
|
|
4804
7740
|
}
|
|
4805
7741
|
if (deletion.kind === "cleanup_pending") {
|
|
7742
|
+
if (deletion.phase === "task_artifact_owner") {
|
|
7743
|
+
ownerProgress = await persistManagedGcStorageOwnerDisposition(scope, target, deletion, lock);
|
|
7744
|
+
return {
|
|
7745
|
+
kind: "cleanup_pending",
|
|
7746
|
+
tombstonePath: tombstone,
|
|
7747
|
+
phase: "artifacts",
|
|
7748
|
+
message: ownerProgress.message ?? deletion.error.message,
|
|
7749
|
+
};
|
|
7750
|
+
}
|
|
4806
7751
|
assertAuthorizedCleanupPending(target, active, deletion);
|
|
4807
7752
|
const retry = nextCleanupReceipt(target, active);
|
|
4808
7753
|
let pendingEvidence = cleanupPendingEvidence(retry, active, deletion);
|
|
@@ -4836,7 +7781,16 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4836
7781
|
phase: deletion.phase,
|
|
4837
7782
|
message: "Exact cleanup remains pending because descriptor-bound final deletion is unavailable.",
|
|
4838
7783
|
};
|
|
7784
|
+
await publishManagedGcArtifactsRemoved(scope, target, lock);
|
|
4839
7785
|
await publishCleanupArtifactsRemoved(scope, tombstone, pendingEvidence, lock);
|
|
7786
|
+
ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, targets, lock);
|
|
7787
|
+
if (ownerProgress.state === "pending")
|
|
7788
|
+
return {
|
|
7789
|
+
kind: "cleanup_pending",
|
|
7790
|
+
tombstonePath: tombstone,
|
|
7791
|
+
phase: "artifacts",
|
|
7792
|
+
message: ownerProgress.message ?? "task_artifact_owner_retirement_pending",
|
|
7793
|
+
};
|
|
4840
7794
|
pendingEvidence = { ...pendingEvidence, artifactsRemovedAttempt: pendingEvidence.attempt };
|
|
4841
7795
|
const retainedProof = cleanupArtifactsRemovedReceipt(
|
|
4842
7796
|
tombstone,
|
|
@@ -4872,6 +7826,8 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4872
7826
|
retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
|
|
4873
7827
|
artifactsRemoved: true,
|
|
4874
7828
|
},
|
|
7829
|
+
scope,
|
|
7830
|
+
target,
|
|
4875
7831
|
() => {
|
|
4876
7832
|
if (
|
|
4877
7833
|
!cleanupArtifactsRemoved(
|
|
@@ -4884,6 +7840,15 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4884
7840
|
throw new Error("durability_failed");
|
|
4885
7841
|
},
|
|
4886
7842
|
);
|
|
7843
|
+
if (deletion.kind === "cleanup_pending" && deletion.phase === "task_artifact_owner") {
|
|
7844
|
+
ownerProgress = await persistManagedGcStorageOwnerDisposition(scope, target, deletion, lock);
|
|
7845
|
+
return {
|
|
7846
|
+
kind: "cleanup_pending",
|
|
7847
|
+
tombstonePath: tombstone,
|
|
7848
|
+
phase: "artifacts",
|
|
7849
|
+
message: ownerProgress.message ?? deletion.error.message,
|
|
7850
|
+
};
|
|
7851
|
+
}
|
|
4887
7852
|
if (deletion.kind === "cleanup_pending") {
|
|
4888
7853
|
if (
|
|
4889
7854
|
deletion.phase !== "transcript" ||
|
|
@@ -4914,12 +7879,42 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4914
7879
|
return {
|
|
4915
7880
|
kind: "cleanup_pending",
|
|
4916
7881
|
tombstonePath: tombstone,
|
|
4917
|
-
phase: deletion.phase,
|
|
4918
|
-
message:
|
|
7882
|
+
phase: ownerProgress.state === "payload_retired" ? "artifacts" : deletion.phase,
|
|
7883
|
+
message:
|
|
7884
|
+
ownerProgress.state === "payload_retired"
|
|
7885
|
+
? "task_artifact_owner_namespace_cleanup_pending"
|
|
7886
|
+
: deletion.error.message,
|
|
4919
7887
|
};
|
|
4920
7888
|
// A retained transcript quarantine proves canonical absence and remains
|
|
4921
7889
|
// identity-bound in the durable pending receipt.
|
|
4922
7890
|
}
|
|
7891
|
+
if (deletion.kind === "deleted")
|
|
7892
|
+
await persistManagedOwnerTranscriptDeletion(scope, tombstone, target, active, lock, deletion);
|
|
7893
|
+
const ownerDisposition = await taskArtifactOwnerTranscriptResult(scope, target, lock);
|
|
7894
|
+
if (ownerDisposition === "payload_retired")
|
|
7895
|
+
return {
|
|
7896
|
+
kind: "cleanup_pending",
|
|
7897
|
+
tombstonePath: tombstone,
|
|
7898
|
+
phase: "artifacts",
|
|
7899
|
+
message: "task_artifact_owner_namespace_cleanup_pending",
|
|
7900
|
+
};
|
|
7901
|
+
if (target.taskArtifactOwnerDeletionEvidence && ownerDisposition !== "retired")
|
|
7902
|
+
return {
|
|
7903
|
+
kind: "cleanup_pending",
|
|
7904
|
+
tombstonePath: tombstone,
|
|
7905
|
+
phase: "artifacts",
|
|
7906
|
+
message: "task_artifact_owner_retirement_pending",
|
|
7907
|
+
};
|
|
7908
|
+
if (
|
|
7909
|
+
target.taskArtifactOwnerDeletionEvidence &&
|
|
7910
|
+
!pendingCleanupReceipt(scope, tombstone, target)?.taskArtifactOwnerTranscriptDeleted
|
|
7911
|
+
)
|
|
7912
|
+
return {
|
|
7913
|
+
kind: "cleanup_pending",
|
|
7914
|
+
tombstonePath: tombstone,
|
|
7915
|
+
phase: "transcript",
|
|
7916
|
+
message: "task_artifact_owner_transcript_deletion_unverified",
|
|
7917
|
+
};
|
|
4923
7918
|
fsyncManagedParent(target.path);
|
|
4924
7919
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
4925
7920
|
}
|